mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-12 20:57:52 +02:00
Merge pull request #1152 from langchain-ai/nc/26jul/pydantic-2-compat
lib: Improve compat with pydantic 2 models
This commit is contained in:
@@ -452,7 +452,11 @@ class CompiledStateGraph(CompiledGraph):
|
||||
def get_input_schema(
|
||||
self, config: Optional[RunnableConfig] = None
|
||||
) -> type[BaseModel]:
|
||||
if isclass(self.builder.input) and issubclass(self.builder.input, BaseModel):
|
||||
from pydantic import BaseModel as BaseModelP
|
||||
|
||||
if isclass(self.builder.input) and issubclass(
|
||||
self.builder.input, (BaseModel, BaseModelP)
|
||||
):
|
||||
return self.builder.input
|
||||
else:
|
||||
keys = list(self.builder.schemas[self.builder.input].keys())
|
||||
@@ -475,7 +479,11 @@ class CompiledStateGraph(CompiledGraph):
|
||||
def get_output_schema(
|
||||
self, config: Optional[RunnableConfig] = None
|
||||
) -> type[BaseModel]:
|
||||
if isclass(self.builder.input) and issubclass(self.builder.output, BaseModel):
|
||||
from pydantic import BaseModel as BaseModelP
|
||||
|
||||
if isclass(self.builder.input) and issubclass(
|
||||
self.builder.output, (BaseModel, BaseModelP)
|
||||
):
|
||||
return self.builder.output
|
||||
|
||||
return super().get_output_schema(config)
|
||||
@@ -497,7 +505,7 @@ class CompiledStateGraph(CompiledGraph):
|
||||
return SKIP_WRITE
|
||||
elif isinstance(input, dict):
|
||||
return input.get(key, SKIP_WRITE)
|
||||
elif get_type_hints(type(input)).get(key):
|
||||
elif get_type_hints(type(input)):
|
||||
value = getattr(input, key, SKIP_WRITE)
|
||||
return value if value is not None else SKIP_WRITE
|
||||
else:
|
||||
|
||||
@@ -615,6 +615,232 @@
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> rewrite_query;
|
||||
analyzer_one --> retriever_one;
|
||||
qa --> __end__;
|
||||
retriever_one --> qa;
|
||||
retriever_two --> qa;
|
||||
rewrite_query --> analyzer_one;
|
||||
rewrite_query -.-> retriever_two;
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1.1
|
||||
dict({
|
||||
'definitions': dict({
|
||||
'InnerObject': dict({
|
||||
'properties': dict({
|
||||
'yo': dict({
|
||||
'title': 'Yo',
|
||||
'type': 'integer',
|
||||
}),
|
||||
}),
|
||||
'required': list([
|
||||
'yo',
|
||||
]),
|
||||
'title': 'InnerObject',
|
||||
'type': 'object',
|
||||
}),
|
||||
}),
|
||||
'properties': dict({
|
||||
'answer': dict({
|
||||
'title': 'Answer',
|
||||
'type': 'string',
|
||||
}),
|
||||
'docs': dict({
|
||||
'items': dict({
|
||||
'type': 'string',
|
||||
}),
|
||||
'title': 'Docs',
|
||||
'type': 'array',
|
||||
}),
|
||||
'inner': dict({
|
||||
'$ref': '#/definitions/InnerObject',
|
||||
}),
|
||||
'query': dict({
|
||||
'title': 'Query',
|
||||
'type': 'string',
|
||||
}),
|
||||
}),
|
||||
'required': list([
|
||||
'query',
|
||||
'inner',
|
||||
'docs',
|
||||
]),
|
||||
'title': 'State',
|
||||
'type': 'object',
|
||||
})
|
||||
# ---
|
||||
# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1.2
|
||||
dict({
|
||||
'definitions': dict({
|
||||
'InnerObject': dict({
|
||||
'properties': dict({
|
||||
'yo': dict({
|
||||
'title': 'Yo',
|
||||
'type': 'integer',
|
||||
}),
|
||||
}),
|
||||
'required': list([
|
||||
'yo',
|
||||
]),
|
||||
'title': 'InnerObject',
|
||||
'type': 'object',
|
||||
}),
|
||||
}),
|
||||
'properties': dict({
|
||||
'answer': dict({
|
||||
'title': 'Answer',
|
||||
'type': 'string',
|
||||
}),
|
||||
'docs': dict({
|
||||
'items': dict({
|
||||
'type': 'string',
|
||||
}),
|
||||
'title': 'Docs',
|
||||
'type': 'array',
|
||||
}),
|
||||
'inner': dict({
|
||||
'$ref': '#/definitions/InnerObject',
|
||||
}),
|
||||
'query': dict({
|
||||
'title': 'Query',
|
||||
'type': 'string',
|
||||
}),
|
||||
}),
|
||||
'required': list([
|
||||
'query',
|
||||
'inner',
|
||||
'docs',
|
||||
]),
|
||||
'title': 'State',
|
||||
'type': 'object',
|
||||
})
|
||||
# ---
|
||||
# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> rewrite_query;
|
||||
analyzer_one --> retriever_one;
|
||||
qa --> __end__;
|
||||
retriever_one --> qa;
|
||||
retriever_two --> qa;
|
||||
rewrite_query --> analyzer_one;
|
||||
rewrite_query -.-> retriever_two;
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2.1
|
||||
dict({
|
||||
'$defs': dict({
|
||||
'InnerObject': dict({
|
||||
'properties': dict({
|
||||
'yo': dict({
|
||||
'title': 'Yo',
|
||||
'type': 'integer',
|
||||
}),
|
||||
}),
|
||||
'required': list([
|
||||
'yo',
|
||||
]),
|
||||
'title': 'InnerObject',
|
||||
'type': 'object',
|
||||
}),
|
||||
}),
|
||||
'properties': dict({
|
||||
'answer': dict({
|
||||
'anyOf': list([
|
||||
dict({
|
||||
'type': 'string',
|
||||
}),
|
||||
dict({
|
||||
'type': 'null',
|
||||
}),
|
||||
]),
|
||||
'default': None,
|
||||
'title': 'Answer',
|
||||
}),
|
||||
'docs': dict({
|
||||
'items': dict({
|
||||
'type': 'string',
|
||||
}),
|
||||
'title': 'Docs',
|
||||
'type': 'array',
|
||||
}),
|
||||
'inner': dict({
|
||||
'$ref': '#/$defs/InnerObject',
|
||||
}),
|
||||
'query': dict({
|
||||
'title': 'Query',
|
||||
'type': 'string',
|
||||
}),
|
||||
}),
|
||||
'required': list([
|
||||
'query',
|
||||
'inner',
|
||||
'docs',
|
||||
]),
|
||||
'title': 'State',
|
||||
'type': 'object',
|
||||
})
|
||||
# ---
|
||||
# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2.2
|
||||
dict({
|
||||
'$defs': dict({
|
||||
'InnerObject': dict({
|
||||
'properties': dict({
|
||||
'yo': dict({
|
||||
'title': 'Yo',
|
||||
'type': 'integer',
|
||||
}),
|
||||
}),
|
||||
'required': list([
|
||||
'yo',
|
||||
]),
|
||||
'title': 'InnerObject',
|
||||
'type': 'object',
|
||||
}),
|
||||
}),
|
||||
'properties': dict({
|
||||
'answer': dict({
|
||||
'anyOf': list([
|
||||
dict({
|
||||
'type': 'string',
|
||||
}),
|
||||
dict({
|
||||
'type': 'null',
|
||||
}),
|
||||
]),
|
||||
'default': None,
|
||||
'title': 'Answer',
|
||||
}),
|
||||
'docs': dict({
|
||||
'items': dict({
|
||||
'type': 'string',
|
||||
}),
|
||||
'title': 'Docs',
|
||||
'type': 'array',
|
||||
}),
|
||||
'inner': dict({
|
||||
'$ref': '#/$defs/InnerObject',
|
||||
}),
|
||||
'query': dict({
|
||||
'title': 'Query',
|
||||
'type': 'string',
|
||||
}),
|
||||
}),
|
||||
'required': list([
|
||||
'query',
|
||||
'inner',
|
||||
'docs',
|
||||
]),
|
||||
'title': 'State',
|
||||
'type': 'object',
|
||||
})
|
||||
# ---
|
||||
# name: test_in_one_fan_out_state_graph_waiting_edge_via_branch
|
||||
'''
|
||||
graph TD;
|
||||
|
||||
@@ -7048,7 +7048,7 @@ def test_in_one_fan_out_state_graph_waiting_edge_via_branch(
|
||||
]
|
||||
|
||||
|
||||
def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class(
|
||||
def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1(
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
from langchain_core.pydantic_v1 import BaseModel, ValidationError
|
||||
@@ -7062,8 +7062,12 @@ def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class(
|
||||
y = [t[1] for t in y]
|
||||
return sorted(operator.add(x, y))
|
||||
|
||||
class InnerObject(BaseModel):
|
||||
yo: int
|
||||
|
||||
class State(BaseModel):
|
||||
query: str
|
||||
inner: InnerObject
|
||||
answer: Optional[str] = None
|
||||
docs: Annotated[list[str], sorted_add]
|
||||
|
||||
@@ -7112,17 +7116,20 @@ def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class(
|
||||
app = workflow.compile()
|
||||
|
||||
assert app.get_graph().draw_mermaid(with_styles=False) == snapshot
|
||||
assert app.get_input_schema().schema() == snapshot
|
||||
assert app.get_output_schema().schema() == snapshot
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
app.invoke({"query": {}})
|
||||
|
||||
assert app.invoke({"query": "what is weather in sf"}) == {
|
||||
assert app.invoke({"query": "what is weather in sf", "inner": {"yo": 1}}) == {
|
||||
"query": "analyzed: query: what is weather in sf",
|
||||
"docs": ["doc1", "doc2", "doc3", "doc4"],
|
||||
"answer": "doc1,doc2,doc3,doc4",
|
||||
"inner": {"yo": 1},
|
||||
}
|
||||
|
||||
assert [*app.stream({"query": "what is weather in sf"})] == [
|
||||
assert [*app.stream({"query": "what is weather in sf", "inner": {"yo": 1}})] == [
|
||||
{"rewrite_query": {"query": "query: what is weather in sf"}},
|
||||
{"analyzer_one": {"query": "analyzed: query: what is weather in sf"}},
|
||||
{"retriever_two": {"docs": ["doc3", "doc4"]}},
|
||||
@@ -7137,7 +7144,122 @@ def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class(
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
assert [
|
||||
c for c in app_w_interrupt.stream({"query": "what is weather in sf"}, config)
|
||||
c
|
||||
for c in app_w_interrupt.stream(
|
||||
{"query": "what is weather in sf", "inner": {"yo": 1}}, config
|
||||
)
|
||||
] == [
|
||||
{"rewrite_query": {"query": "query: what is weather in sf"}},
|
||||
{"analyzer_one": {"query": "analyzed: query: what is weather in sf"}},
|
||||
{"retriever_two": {"docs": ["doc3", "doc4"]}},
|
||||
{"retriever_one": {"docs": ["doc1", "doc2"]}},
|
||||
]
|
||||
|
||||
assert [c for c in app_w_interrupt.stream(None, config)] == [
|
||||
{"qa": {"answer": "doc1,doc2,doc3,doc4"}},
|
||||
]
|
||||
|
||||
|
||||
def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2(
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
def sorted_add(
|
||||
x: list[str], y: Union[list[str], list[tuple[str, str]]]
|
||||
) -> list[str]:
|
||||
if isinstance(y[0], tuple):
|
||||
for rem, _ in y:
|
||||
x.remove(rem)
|
||||
y = [t[1] for t in y]
|
||||
return sorted(operator.add(x, y))
|
||||
|
||||
class InnerObject(BaseModel):
|
||||
yo: int
|
||||
|
||||
class State(BaseModel):
|
||||
query: str
|
||||
inner: InnerObject
|
||||
answer: Optional[str] = None
|
||||
docs: Annotated[list[str], sorted_add]
|
||||
|
||||
class StateUpdate(BaseModel):
|
||||
query: Optional[str] = None
|
||||
answer: Optional[str] = None
|
||||
docs: Optional[list[str]] = None
|
||||
|
||||
def rewrite_query(data: State) -> State:
|
||||
return {"query": f"query: {data.query}"}
|
||||
|
||||
def analyzer_one(data: State) -> State:
|
||||
return StateUpdate(query=f"analyzed: {data.query}")
|
||||
|
||||
def retriever_one(data: State) -> State:
|
||||
return {"docs": ["doc1", "doc2"]}
|
||||
|
||||
def retriever_two(data: State) -> State:
|
||||
time.sleep(0.1)
|
||||
return {"docs": ["doc3", "doc4"]}
|
||||
|
||||
def qa(data: State) -> State:
|
||||
return {"answer": ",".join(data.docs)}
|
||||
|
||||
def decider(data: State) -> str:
|
||||
assert isinstance(data, State)
|
||||
return "retriever_two"
|
||||
|
||||
workflow = StateGraph(State)
|
||||
|
||||
workflow.add_node("rewrite_query", rewrite_query)
|
||||
workflow.add_node("analyzer_one", analyzer_one)
|
||||
workflow.add_node("retriever_one", retriever_one)
|
||||
workflow.add_node("retriever_two", retriever_two)
|
||||
workflow.add_node("qa", qa)
|
||||
|
||||
workflow.set_entry_point("rewrite_query")
|
||||
workflow.add_edge("rewrite_query", "analyzer_one")
|
||||
workflow.add_edge("analyzer_one", "retriever_one")
|
||||
workflow.add_conditional_edges(
|
||||
"rewrite_query", decider, {"retriever_two": "retriever_two"}
|
||||
)
|
||||
workflow.add_edge(["retriever_one", "retriever_two"], "qa")
|
||||
workflow.set_finish_point("qa")
|
||||
|
||||
app = workflow.compile()
|
||||
|
||||
assert app.get_graph().draw_mermaid(with_styles=False) == snapshot
|
||||
assert app.get_input_schema().schema() == snapshot
|
||||
assert app.get_output_schema().schema() == snapshot
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
app.invoke({"query": {}})
|
||||
|
||||
assert app.invoke({"query": "what is weather in sf", "inner": {"yo": 1}}) == {
|
||||
"query": "analyzed: query: what is weather in sf",
|
||||
"docs": ["doc1", "doc2", "doc3", "doc4"],
|
||||
"answer": "doc1,doc2,doc3,doc4",
|
||||
"inner": {"yo": 1},
|
||||
}
|
||||
|
||||
assert [*app.stream({"query": "what is weather in sf", "inner": {"yo": 1}})] == [
|
||||
{"rewrite_query": {"query": "query: what is weather in sf"}},
|
||||
{"analyzer_one": {"query": "analyzed: query: what is weather in sf"}},
|
||||
{"retriever_two": {"docs": ["doc3", "doc4"]}},
|
||||
{"retriever_one": {"docs": ["doc1", "doc2"]}},
|
||||
{"qa": {"answer": "doc1,doc2,doc3,doc4"}},
|
||||
]
|
||||
|
||||
app_w_interrupt = workflow.compile(
|
||||
checkpointer=MemorySaverAssertImmutable(),
|
||||
interrupt_after=["retriever_one"],
|
||||
)
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
assert [
|
||||
c
|
||||
for c in app_w_interrupt.stream(
|
||||
{"query": "what is weather in sf", "inner": {"yo": 1}}, config
|
||||
)
|
||||
] == [
|
||||
{"rewrite_query": {"query": "query: what is weather in sf"}},
|
||||
{"analyzer_one": {"query": "analyzed: query: what is weather in sf"}},
|
||||
|
||||
Reference in New Issue
Block a user