Compare commits

...
Author SHA1 Message Date
open-swe[bot] 89b465cc73 Empty commit to trigger CI 2025-08-11 20:32:18 +00:00
open-swe[bot] 01e7d027fc Apply patch [skip ci] 2025-08-11 20:28:47 +00:00
open-swe[bot] 03bf206805 Apply patch [skip ci] 2025-08-11 20:28:29 +00:00
open-swe[bot] 5aa0525965 Apply patch [skip ci] 2025-08-11 20:27:15 +00:00
open-swe[bot] db409f4e1a Apply patch [skip ci] 2025-08-11 20:26:55 +00:00
open-swe[bot] e168ea4505 Apply patch [skip ci] 2025-08-11 20:26:33 +00:00
open-swe[bot] 8bdd96b2fc Apply patch [skip ci] 2025-08-11 20:26:00 +00:00
open-swe[bot] 8f1cfbfd7c Apply patch [skip ci] 2025-08-11 20:25:25 +00:00
open-swe[bot] 3a5d791ce5 Apply patch [skip ci] 2025-08-11 20:24:22 +00:00
open-swe[bot] 5e28ac3000 Apply patch [skip ci] 2025-08-11 20:24:00 +00:00
open-swe[bot] 6cf20694ff Apply patch [skip ci] 2025-08-11 20:23:30 +00:00
open-swe[bot] 250d781af1 Apply patch [skip ci] 2025-08-11 20:22:22 +00:00
open-swe[bot] b1d480521c Apply patch [skip ci] 2025-08-11 20:21:42 +00:00
open-swe[bot] 44a64dacb3 Apply patch [skip ci] 2025-08-11 20:21:01 +00:00
open-swe[bot] 1115c53f9a Apply patch [skip ci] 2025-08-11 20:20:04 +00:00
open-swe[bot] 6c8269909d Apply patch [skip ci] 2025-08-11 20:19:16 +00:00
open-swe[bot] ad8946655f Apply patch [skip ci] 2025-08-11 20:18:59 +00:00
open-swe[bot] 2d4988dc59 Apply patch [skip ci] 2025-08-11 20:18:29 +00:00
open-swe[bot] f8c1e4477a Apply patch [skip ci] 2025-08-11 20:17:33 +00:00
open-swe[bot] a69de5596d Apply patch [skip ci] 2025-08-11 20:17:18 +00:00
open-swe[bot] ebd88829b8 Apply patch [skip ci] 2025-08-11 20:16:47 +00:00
open-swe[bot] 9f7aa90e05 Apply patch [skip ci] 2025-08-11 20:15:48 +00:00
open-swe[bot] aaf1265a17 Apply patch [skip ci] 2025-08-11 20:13:22 +00:00
open-swe[bot] 74c537b2d9 Apply patch [skip ci] 2025-08-11 20:12:29 +00:00
open-swe[bot] 71b8316140 Apply patch [skip ci] 2025-08-11 20:12:05 +00:00
open-swe[bot] 2d242baaa5 Apply patch [skip ci] 2025-08-11 20:11:21 +00:00
open-swe[bot] 669e941fce Apply patch [skip ci] 2025-08-11 20:11:09 +00:00
open-swe[bot] a94dd39edb Apply patch [skip ci] 2025-08-11 20:10:43 +00:00
open-swe[bot] d0d88ba871 Apply patch [skip ci] 2025-08-11 20:10:19 +00:00
open-swe[bot] e968421b75 Apply patch [skip ci] 2025-08-11 20:10:06 +00:00
open-swe[bot] 3711cca7a1 Apply patch [skip ci] 2025-08-11 20:09:17 +00:00
open-swe[bot] f5b007c196 Apply patch [skip ci] 2025-08-11 20:08:34 +00:00
open-swe[bot] 665792bbd2 Apply patch [skip ci] 2025-08-11 20:07:49 +00:00
open-swe[bot] b73d94a5f1 Apply patch [skip ci] 2025-08-11 20:07:35 +00:00
open-swe[bot] e5a560cb7d Apply patch [skip ci] 2025-08-11 20:06:05 +00:00
3 changed files with 295 additions and 69 deletions
@@ -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__;
'''
# ---
+6 -3
View File
@@ -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