mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-12 04:37:51 +02:00
Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89b465cc73 | ||
|
|
01e7d027fc | ||
|
|
03bf206805 | ||
|
|
5aa0525965 | ||
|
|
db409f4e1a | ||
|
|
e168ea4505 | ||
|
|
8bdd96b2fc | ||
|
|
8f1cfbfd7c | ||
|
|
3a5d791ce5 | ||
|
|
5e28ac3000 | ||
|
|
6cf20694ff | ||
|
|
250d781af1 | ||
|
|
b1d480521c | ||
|
|
44a64dacb3 | ||
|
|
1115c53f9a | ||
|
|
6c8269909d | ||
|
|
ad8946655f | ||
|
|
2d4988dc59 | ||
|
|
f8c1e4477a | ||
|
|
a69de5596d | ||
|
|
ebd88829b8 | ||
|
|
9f7aa90e05 | ||
|
|
aaf1265a17 | ||
|
|
74c537b2d9 | ||
|
|
71b8316140 | ||
|
|
2d242baaa5 | ||
|
|
669e941fce | ||
|
|
a94dd39edb | ||
|
|
d0d88ba871 | ||
|
|
e968421b75 | ||
|
|
3711cca7a1 | ||
|
|
f5b007c196 | ||
|
|
665792bbd2 | ||
|
|
b73d94a5f1 | ||
|
|
e5a560cb7d |
@@ -508,6 +508,51 @@ def create_react_agent(
|
||||
tool_node = ToolNode([t for t in tools if not isinstance(t, dict)])
|
||||
tool_classes = list(tool_node.tools_by_name.values())
|
||||
|
||||
# Create a tool from response_format schema if provided
|
||||
response_tool_name = None
|
||||
if response_format is not None:
|
||||
from langchain_core.tools import StructuredTool
|
||||
|
||||
# Extract the actual schema from tuple if needed
|
||||
actual_schema = response_format
|
||||
if isinstance(response_format, tuple):
|
||||
_, actual_schema = response_format
|
||||
|
||||
# Get the schema name for the tool
|
||||
if hasattr(actual_schema, "__name__"):
|
||||
response_tool_name = actual_schema.__name__ # type: ignore[union-attr]
|
||||
elif isinstance(actual_schema, dict) and "title" in actual_schema:
|
||||
response_tool_name = actual_schema["title"]
|
||||
else:
|
||||
response_tool_name = "ResponseSchema"
|
||||
|
||||
# Create a proper tool from the schema
|
||||
def response_tool_func(**kwargs):
|
||||
"""Tool function for structured response generation."""
|
||||
return kwargs
|
||||
|
||||
# Create a StructuredTool from the schema
|
||||
if isinstance(actual_schema, type) and issubclass(actual_schema, BaseModel):
|
||||
response_tool = StructuredTool.from_function(
|
||||
func=response_tool_func,
|
||||
name=response_tool_name,
|
||||
description=f"Generate a structured response using {response_tool_name}",
|
||||
args_schema=actual_schema,
|
||||
)
|
||||
else:
|
||||
# For dict schemas, create a tool dict representation
|
||||
response_tool = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": response_tool_name,
|
||||
"description": f"Generate a structured response using {response_tool_name}",
|
||||
"parameters": actual_schema if isinstance(actual_schema, dict) else {}
|
||||
}
|
||||
}
|
||||
|
||||
# Add the tool to the tool classes
|
||||
tool_classes.append(response_tool)
|
||||
|
||||
is_dynamic_model = not isinstance(model, (str, Runnable)) and callable(model)
|
||||
is_async_dynamic_model = is_dynamic_model and inspect.iscoroutinefunction(model)
|
||||
|
||||
@@ -549,7 +594,17 @@ def create_react_agent(
|
||||
) -> LanguageModelLike:
|
||||
"""Resolve the model to use, handling both static and dynamic models."""
|
||||
if is_dynamic_model:
|
||||
return _get_prompt_runnable(prompt) | model(state, runtime) # type: ignore[operator]
|
||||
resolved_model = model(state, runtime) # type: ignore[operator]
|
||||
if (
|
||||
_should_bind_tools(
|
||||
resolved_model, tool_classes, num_builtin=len(llm_builtin_tools)
|
||||
) # type: ignore[arg-type]
|
||||
and len(tool_classes + llm_builtin_tools) > 0
|
||||
):
|
||||
resolved_model = cast(BaseChatModel, resolved_model).bind_tools(
|
||||
tool_classes + llm_builtin_tools # type: ignore[operator]
|
||||
)
|
||||
return _get_prompt_runnable(prompt) | resolved_model # type: ignore[operator]
|
||||
else:
|
||||
return static_model
|
||||
|
||||
@@ -559,9 +614,28 @@ def create_react_agent(
|
||||
"""Async resolve the model to use, handling both static and dynamic models."""
|
||||
if is_async_dynamic_model:
|
||||
resolved_model = await model(state, runtime) # type: ignore[misc,operator]
|
||||
return _get_prompt_runnable(prompt) | resolved_model
|
||||
if (
|
||||
_should_bind_tools(
|
||||
resolved_model, tool_classes, num_builtin=len(llm_builtin_tools)
|
||||
) # type: ignore[arg-type]
|
||||
and len(tool_classes + llm_builtin_tools) > 0
|
||||
):
|
||||
resolved_model = cast(BaseChatModel, resolved_model).bind_tools(
|
||||
tool_classes + llm_builtin_tools # type: ignore[operator]
|
||||
)
|
||||
return _get_prompt_runnable(prompt) | resolved_model # type: ignore[operator]
|
||||
elif is_dynamic_model:
|
||||
return _get_prompt_runnable(prompt) | model(state, runtime) # type: ignore[operator]
|
||||
resolved_model = model(state, runtime) # type: ignore[operator]
|
||||
if (
|
||||
_should_bind_tools(
|
||||
resolved_model, tool_classes, num_builtin=len(llm_builtin_tools)
|
||||
) # type: ignore[arg-type]
|
||||
and len(tool_classes + llm_builtin_tools) > 0
|
||||
):
|
||||
resolved_model = cast(BaseChatModel, resolved_model).bind_tools(
|
||||
tool_classes + llm_builtin_tools # type: ignore[operator]
|
||||
)
|
||||
return _get_prompt_runnable(prompt) | resolved_model # type: ignore[operator]
|
||||
else:
|
||||
return static_model
|
||||
|
||||
@@ -689,48 +763,167 @@ def create_react_agent(
|
||||
else:
|
||||
input_schema = state_schema
|
||||
|
||||
def generate_structured_response(
|
||||
def respond(
|
||||
state: StateSchema, runtime: Runtime[ContextT], config: RunnableConfig
|
||||
) -> StateSchema:
|
||||
if is_async_dynamic_model:
|
||||
msg = (
|
||||
"Async model callable provided but agent invoked synchronously. "
|
||||
"Use agent.ainvoke() or agent.astream(), or provide a sync model callable."
|
||||
"""Handle structured response creation when response schema tool is called or when no tool calls are present."""
|
||||
messages = _get_state_value(state, "messages")
|
||||
last_message = messages[-1]
|
||||
|
||||
# Check if this is a tool call scenario or no tool calls scenario
|
||||
if isinstance(last_message, AIMessage) and last_message.tool_calls:
|
||||
# Find the response schema tool call
|
||||
response_tool_call = None
|
||||
for tool_call in last_message.tool_calls:
|
||||
if tool_call["name"] == response_tool_name:
|
||||
response_tool_call = tool_call
|
||||
break
|
||||
|
||||
if response_tool_call is None:
|
||||
# No response schema tool call found, but there are other tool calls
|
||||
# This shouldn't happen in normal flow, fall back to old behavior
|
||||
if is_async_dynamic_model:
|
||||
msg = (
|
||||
"Async model callable provided but agent invoked synchronously. "
|
||||
"Use agent.ainvoke() or agent.astream(), or provide a sync model callable."
|
||||
)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
structured_response_schema = response_format
|
||||
if isinstance(response_format, tuple):
|
||||
system_prompt, structured_response_schema = response_format
|
||||
messages = [SystemMessage(content=system_prompt)] + list(messages)
|
||||
|
||||
resolved_model = _resolve_model(state, runtime)
|
||||
model_with_structured_output = _get_model(
|
||||
resolved_model
|
||||
).with_structured_output(
|
||||
cast(StructuredResponseSchema, structured_response_schema)
|
||||
)
|
||||
response = model_with_structured_output.invoke(messages, config)
|
||||
return {"structured_response": response}
|
||||
|
||||
# Extract the actual schema from tuple if needed
|
||||
actual_schema = response_format
|
||||
if isinstance(response_format, tuple):
|
||||
_, actual_schema = response_format
|
||||
|
||||
# Coerce tool call arguments into response schema
|
||||
try:
|
||||
if hasattr(actual_schema, "__call__"):
|
||||
# For BaseModel classes
|
||||
structured_response = actual_schema(**response_tool_call["args"]) # type: ignore[operator]
|
||||
else:
|
||||
# For dict schemas, just return the args
|
||||
structured_response = response_tool_call["args"]
|
||||
except Exception as e:
|
||||
raise ValueError(
|
||||
f"Failed to coerce tool call args into response schema: {e}"
|
||||
)
|
||||
|
||||
# Create artificial tool message
|
||||
tool_message = ToolMessage(
|
||||
content="Here is your structured response",
|
||||
tool_call_id=response_tool_call["id"],
|
||||
)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
messages = _get_state_value(state, "messages")
|
||||
structured_response_schema = response_format
|
||||
if isinstance(response_format, tuple):
|
||||
system_prompt, structured_response_schema = response_format
|
||||
messages = [SystemMessage(content=system_prompt)] + list(messages)
|
||||
return {"messages": [tool_message], "structured_response": structured_response}
|
||||
else:
|
||||
# No tool calls - generate structured response using the old approach
|
||||
if is_async_dynamic_model:
|
||||
msg = (
|
||||
"Async model callable provided but agent invoked synchronously. "
|
||||
"Use agent.ainvoke() or agent.astream(), or provide a sync model callable."
|
||||
)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
resolved_model = _resolve_model(state, runtime)
|
||||
model_with_structured_output = _get_model(
|
||||
resolved_model
|
||||
).with_structured_output(
|
||||
cast(StructuredResponseSchema, structured_response_schema)
|
||||
)
|
||||
response = model_with_structured_output.invoke(messages, config)
|
||||
return {"structured_response": response}
|
||||
structured_response_schema = response_format
|
||||
if isinstance(response_format, tuple):
|
||||
system_prompt, structured_response_schema = response_format
|
||||
messages = [SystemMessage(content=system_prompt)] + list(messages)
|
||||
|
||||
async def agenerate_structured_response(
|
||||
resolved_model = _resolve_model(state, runtime)
|
||||
model_with_structured_output = _get_model(
|
||||
resolved_model
|
||||
).with_structured_output(
|
||||
cast(StructuredResponseSchema, structured_response_schema)
|
||||
)
|
||||
response = model_with_structured_output.invoke(messages, config)
|
||||
return {"structured_response": response}
|
||||
|
||||
async def arespond(
|
||||
state: StateSchema, runtime: Runtime[ContextT], config: RunnableConfig
|
||||
) -> StateSchema:
|
||||
"""Async handle structured response creation when response schema tool is called or when no tool calls are present."""
|
||||
messages = _get_state_value(state, "messages")
|
||||
structured_response_schema = response_format
|
||||
if isinstance(response_format, tuple):
|
||||
system_prompt, structured_response_schema = response_format
|
||||
messages = [SystemMessage(content=system_prompt)] + list(messages)
|
||||
last_message = messages[-1]
|
||||
|
||||
resolved_model = await _aresolve_model(state, runtime)
|
||||
model_with_structured_output = _get_model(
|
||||
resolved_model
|
||||
).with_structured_output(
|
||||
cast(StructuredResponseSchema, structured_response_schema)
|
||||
)
|
||||
response = await model_with_structured_output.ainvoke(messages, config)
|
||||
return {"structured_response": response}
|
||||
# Check if this is a tool call scenario or no tool calls scenario
|
||||
if isinstance(last_message, AIMessage) and last_message.tool_calls:
|
||||
# Find the response schema tool call
|
||||
response_tool_call = None
|
||||
for tool_call in last_message.tool_calls:
|
||||
if tool_call["name"] == response_tool_name:
|
||||
response_tool_call = tool_call
|
||||
break
|
||||
|
||||
if response_tool_call is None:
|
||||
# No response schema tool call found, but there are other tool calls
|
||||
# This shouldn't happen in normal flow, fall back to old behavior
|
||||
structured_response_schema = response_format
|
||||
if isinstance(response_format, tuple):
|
||||
system_prompt, structured_response_schema = response_format
|
||||
messages = [SystemMessage(content=system_prompt)] + list(messages)
|
||||
|
||||
resolved_model = await _aresolve_model(state, runtime)
|
||||
model_with_structured_output = _get_model(
|
||||
resolved_model
|
||||
).with_structured_output(
|
||||
cast(StructuredResponseSchema, structured_response_schema)
|
||||
)
|
||||
response = await model_with_structured_output.ainvoke(messages, config)
|
||||
return {"structured_response": response}
|
||||
|
||||
# Extract the actual schema from tuple if needed
|
||||
actual_schema = response_format
|
||||
if isinstance(response_format, tuple):
|
||||
_, actual_schema = response_format
|
||||
|
||||
# Coerce tool call arguments into response schema
|
||||
try:
|
||||
if hasattr(actual_schema, "__call__"):
|
||||
# For BaseModel classes
|
||||
structured_response = actual_schema(**response_tool_call["args"]) # type: ignore[operator]
|
||||
else:
|
||||
# For dict schemas, just return the args
|
||||
structured_response = response_tool_call["args"]
|
||||
except Exception as e:
|
||||
raise ValueError(
|
||||
f"Failed to coerce tool call args into response schema: {e}"
|
||||
)
|
||||
|
||||
# Create artificial tool message
|
||||
tool_message = ToolMessage(
|
||||
content="Here is your structured response",
|
||||
tool_call_id=response_tool_call["id"],
|
||||
)
|
||||
|
||||
return {"messages": [tool_message], "structured_response": structured_response}
|
||||
else:
|
||||
# No tool calls - generate structured response using the old approach
|
||||
structured_response_schema = response_format
|
||||
if isinstance(response_format, tuple):
|
||||
system_prompt, structured_response_schema = response_format
|
||||
messages = [SystemMessage(content=system_prompt)] + list(messages)
|
||||
|
||||
resolved_model = await _aresolve_model(state, runtime)
|
||||
model_with_structured_output = _get_model(
|
||||
resolved_model
|
||||
).with_structured_output(
|
||||
cast(StructuredResponseSchema, structured_response_schema)
|
||||
)
|
||||
response = await model_with_structured_output.ainvoke(messages, config)
|
||||
return {"structured_response": response}
|
||||
|
||||
if not tool_calling_enabled:
|
||||
# Define a new graph
|
||||
@@ -755,16 +948,12 @@ def create_react_agent(
|
||||
|
||||
if response_format is not None:
|
||||
workflow.add_node(
|
||||
"generate_structured_response",
|
||||
"respond",
|
||||
RunnableCallable(
|
||||
generate_structured_response,
|
||||
agenerate_structured_response,
|
||||
respond,
|
||||
arespond,
|
||||
),
|
||||
)
|
||||
if post_model_hook is not None:
|
||||
workflow.add_edge("post_model_hook", "generate_structured_response")
|
||||
else:
|
||||
workflow.add_edge("agent", "generate_structured_response")
|
||||
|
||||
return workflow.compile(
|
||||
checkpointer=checkpointer,
|
||||
@@ -784,11 +973,17 @@ def create_react_agent(
|
||||
if post_model_hook is not None:
|
||||
return "post_model_hook"
|
||||
elif response_format is not None:
|
||||
return "generate_structured_response"
|
||||
return "respond"
|
||||
else:
|
||||
return END
|
||||
# Otherwise if there is, we continue
|
||||
else:
|
||||
# Check if the response schema tool is called
|
||||
if response_format is not None and response_tool_name is not None:
|
||||
for tool_call in last_message.tool_calls:
|
||||
if tool_call["name"] == response_tool_name:
|
||||
return "respond"
|
||||
|
||||
if version == "v1":
|
||||
return "tools"
|
||||
elif version == "v2":
|
||||
@@ -840,16 +1035,16 @@ def create_react_agent(
|
||||
# Add a structured output node if response_format is provided
|
||||
if response_format is not None:
|
||||
workflow.add_node(
|
||||
"generate_structured_response",
|
||||
"respond",
|
||||
RunnableCallable(
|
||||
generate_structured_response,
|
||||
agenerate_structured_response,
|
||||
respond,
|
||||
arespond,
|
||||
),
|
||||
)
|
||||
if post_model_hook is not None:
|
||||
post_model_hook_paths.append("generate_structured_response")
|
||||
post_model_hook_paths.extend(["respond", END])
|
||||
else:
|
||||
agent_paths.append("generate_structured_response")
|
||||
agent_paths.extend(["respond", END])
|
||||
else:
|
||||
if post_model_hook is not None:
|
||||
post_model_hook_paths.append(END)
|
||||
@@ -863,7 +1058,7 @@ def create_react_agent(
|
||||
|
||||
Routes to one of:
|
||||
* "tools": if there are pending tool calls without a corresponding message.
|
||||
* "generate_structured_response": if no pending tool calls exist and response_format is specified.
|
||||
* "respond": if no pending tool calls exist and response_format is specified.
|
||||
* END: if no pending tool calls exist and no response_format is specified.
|
||||
"""
|
||||
|
||||
@@ -887,7 +1082,7 @@ def create_react_agent(
|
||||
elif isinstance(messages[-1], ToolMessage):
|
||||
return entrypoint
|
||||
elif response_format is not None:
|
||||
return "generate_structured_response"
|
||||
return "respond"
|
||||
else:
|
||||
return END
|
||||
|
||||
@@ -949,3 +1144,13 @@ __all__ = [
|
||||
"AgentStateWithStructuredResponse",
|
||||
"AgentStateWithStructuredResponsePydantic",
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -85,8 +85,11 @@
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> agent;
|
||||
agent --> generate_structured_response;
|
||||
generate_structured_response --> __end__;
|
||||
agent -.-> __end__;
|
||||
agent -.-> respond;
|
||||
agent -.-> tools;
|
||||
tools --> agent;
|
||||
respond --> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
@@ -94,10 +97,11 @@
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> agent;
|
||||
agent -.-> generate_structured_response;
|
||||
agent -.-> __end__;
|
||||
agent -.-> respond;
|
||||
agent -.-> tools;
|
||||
tools --> agent;
|
||||
generate_structured_response --> __end__;
|
||||
respond --> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
@@ -105,9 +109,12 @@
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> pre_model_hook;
|
||||
agent --> generate_structured_response;
|
||||
agent -.-> __end__;
|
||||
agent -.-> respond;
|
||||
agent -.-> tools;
|
||||
pre_model_hook --> agent;
|
||||
generate_structured_response --> __end__;
|
||||
tools --> pre_model_hook;
|
||||
respond --> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
@@ -115,11 +122,12 @@
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> pre_model_hook;
|
||||
agent -.-> generate_structured_response;
|
||||
agent -.-> __end__;
|
||||
agent -.-> respond;
|
||||
agent -.-> tools;
|
||||
pre_model_hook --> agent;
|
||||
tools --> pre_model_hook;
|
||||
generate_structured_response --> __end__;
|
||||
respond --> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
@@ -128,8 +136,12 @@
|
||||
graph TD;
|
||||
__start__ --> agent;
|
||||
agent --> post_model_hook;
|
||||
post_model_hook --> generate_structured_response;
|
||||
generate_structured_response --> __end__;
|
||||
post_model_hook -.-> __end__;
|
||||
post_model_hook -.-> agent;
|
||||
post_model_hook -.-> respond;
|
||||
post_model_hook -.-> tools;
|
||||
tools --> agent;
|
||||
respond --> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
@@ -138,11 +150,12 @@
|
||||
graph TD;
|
||||
__start__ --> agent;
|
||||
agent --> post_model_hook;
|
||||
post_model_hook -.-> __end__;
|
||||
post_model_hook -.-> agent;
|
||||
post_model_hook -.-> generate_structured_response;
|
||||
post_model_hook -.-> respond;
|
||||
post_model_hook -.-> tools;
|
||||
tools --> agent;
|
||||
generate_structured_response --> __end__;
|
||||
respond --> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
@@ -151,9 +164,13 @@
|
||||
graph TD;
|
||||
__start__ --> pre_model_hook;
|
||||
agent --> post_model_hook;
|
||||
post_model_hook --> generate_structured_response;
|
||||
post_model_hook -.-> __end__;
|
||||
post_model_hook -.-> pre_model_hook;
|
||||
post_model_hook -.-> respond;
|
||||
post_model_hook -.-> tools;
|
||||
pre_model_hook --> agent;
|
||||
generate_structured_response --> __end__;
|
||||
tools --> pre_model_hook;
|
||||
respond --> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
@@ -162,12 +179,13 @@
|
||||
graph TD;
|
||||
__start__ --> pre_model_hook;
|
||||
agent --> post_model_hook;
|
||||
post_model_hook -.-> generate_structured_response;
|
||||
post_model_hook -.-> __end__;
|
||||
post_model_hook -.-> pre_model_hook;
|
||||
post_model_hook -.-> respond;
|
||||
post_model_hook -.-> tools;
|
||||
pre_model_hook --> agent;
|
||||
tools --> pre_model_hook;
|
||||
generate_structured_response --> __end__;
|
||||
respond --> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
|
||||
@@ -2024,7 +2024,7 @@ def test_post_model_hook_with_structured_output() -> None:
|
||||
)
|
||||
|
||||
assert "post_model_hook" in agent.nodes
|
||||
assert "generate_structured_response" in agent.nodes
|
||||
assert "respond" in agent.nodes
|
||||
|
||||
response = agent.invoke(
|
||||
{"messages": [HumanMessage("What's the weather?")], "flag": False}
|
||||
@@ -2035,7 +2035,7 @@ def test_post_model_hook_with_structured_output() -> None:
|
||||
events = list(
|
||||
agent.stream({"messages": [HumanMessage("What's the weather?")], "flag": False})
|
||||
)
|
||||
assert "generate_structured_response" in events[-1]
|
||||
assert "respond" in events[-1]
|
||||
assert events == [
|
||||
{
|
||||
"agent": {
|
||||
@@ -2091,7 +2091,7 @@ def test_post_model_hook_with_structured_output() -> None:
|
||||
},
|
||||
{"post_model_hook": {"flag": True}},
|
||||
{
|
||||
"generate_structured_response": {
|
||||
"respond": {
|
||||
"structured_response": WeatherResponse(temperature=75.0)
|
||||
}
|
||||
},
|
||||
@@ -2157,3 +2157,6 @@ def test_create_react_agent_inject_vars_with_post_model_hook(
|
||||
AIMessage("hi-hi-6", id="1"),
|
||||
]
|
||||
assert result["foo"] == 2
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user