Skip to content

Commit fdf8181

Browse files
authored
fix(langchain_v1): dynamic response format (#33273)
* Preserve Auto type for the response format. cc @sydney-runkle Creating an extra type was the nicest devx I could find for this (makes it easy to do isinstance(thingy, AutoStrategy) Remaining issue to address: * Going to sort out why we're including tools in the tool node
1 parent 8a95eb1 commit fdf8181

File tree

3 files changed

+49
-23
lines changed

3 files changed

+49
-23
lines changed

libs/langchain_v1/langchain/agents/middleware/types.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
from langgraph.channels.ephemeral_value import EphemeralValue
2727
from langgraph.channels.untracked_value import UntrackedValue
2828
from langgraph.graph.message import add_messages
29-
from langgraph.runtime import Runtime
3029
from langgraph.typing import ContextT
3130
from typing_extensions import NotRequired, Required, TypedDict, TypeVar
3231

libs/langchain_v1/langchain/agents/middleware_agent.py

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
PublicAgentState,
2525
)
2626
from langchain.agents.structured_output import (
27+
AutoStrategy,
2728
MultipleStructuredOutputsError,
2829
OutputToolBinding,
2930
ProviderStrategy,
@@ -194,22 +195,33 @@ def create_agent( # noqa: PLR0915
194195
tools = []
195196

196197
# Convert response format and setup structured output tools
197-
# Raw schemas are converted to ToolStrategy upfront to calculate tools during agent creation.
198-
# If auto-detection is needed, the strategy may be replaced with ProviderStrategy later.
199-
initial_response_format: ToolStrategy | ProviderStrategy | None
200-
is_auto_detect: bool
198+
# Raw schemas are wrapped in AutoStrategy to preserve auto-detection intent.
199+
# AutoStrategy is converted to ToolStrategy upfront to calculate tools during agent creation,
200+
# but may be replaced with ProviderStrategy later based on model capabilities.
201+
initial_response_format: ToolStrategy | ProviderStrategy | AutoStrategy | None
201202
if response_format is None:
202-
initial_response_format, is_auto_detect = None, False
203+
initial_response_format = None
203204
elif isinstance(response_format, (ToolStrategy, ProviderStrategy)):
204205
# Preserve explicitly requested strategies
205-
initial_response_format, is_auto_detect = response_format, False
206+
initial_response_format = response_format
207+
elif isinstance(response_format, AutoStrategy):
208+
# AutoStrategy provided - preserve it for later auto-detection
209+
initial_response_format = response_format
206210
else:
207-
# Raw schema - convert to ToolStrategy for now (may be replaced with ProviderStrategy)
208-
initial_response_format, is_auto_detect = ToolStrategy(schema=response_format), True
211+
# Raw schema - wrap in AutoStrategy to enable auto-detection
212+
initial_response_format = AutoStrategy(schema=response_format)
213+
214+
# For AutoStrategy, convert to ToolStrategy to setup tools upfront
215+
# (may be replaced with ProviderStrategy later based on model)
216+
tool_strategy_for_setup: ToolStrategy | None = None
217+
if isinstance(initial_response_format, AutoStrategy):
218+
tool_strategy_for_setup = ToolStrategy(schema=initial_response_format.schema)
219+
elif isinstance(initial_response_format, ToolStrategy):
220+
tool_strategy_for_setup = initial_response_format
209221

210222
structured_output_tools: dict[str, OutputToolBinding] = {}
211-
if isinstance(initial_response_format, ToolStrategy):
212-
for response_schema in initial_response_format.schema_specs:
223+
if tool_strategy_for_setup:
224+
for response_schema in tool_strategy_for_setup.schema_specs:
213225
structured_tool_info = OutputToolBinding.from_schema_spec(response_schema)
214226
structured_output_tools[structured_tool_info.tool.name] = structured_tool_info
215227
middleware_tools = [t for m in middleware for t in getattr(m, "tools", [])]
@@ -412,17 +424,18 @@ def _get_bound_model(request: ModelRequest) -> tuple[Runnable, ResponseFormat |
412424
raise ValueError(msg)
413425

414426
# Determine effective response format (auto-detect if needed)
415-
effective_response_format: ResponseFormat | None = request.response_format
416-
if (
417-
# User provided raw schema - auto-detect best strategy based on model
418-
is_auto_detect
419-
and isinstance(request.response_format, ToolStrategy)
420-
and
421-
# Model supports provider strategy - use it instead
422-
_supports_provider_strategy(request.model)
423-
):
424-
effective_response_format = ProviderStrategy(schema=response_format) # type: ignore[arg-type]
425-
# else: keep ToolStrategy from initial conversion
427+
effective_response_format: ResponseFormat | None
428+
if isinstance(request.response_format, AutoStrategy):
429+
# User provided raw schema via AutoStrategy - auto-detect best strategy based on model
430+
if _supports_provider_strategy(request.model):
431+
# Model supports provider strategy - use it
432+
effective_response_format = ProviderStrategy(schema=request.response_format.schema)
433+
else:
434+
# Model doesn't support provider strategy - use ToolStrategy
435+
effective_response_format = ToolStrategy(schema=request.response_format.schema)
436+
else:
437+
# User explicitly specified a strategy - preserve it
438+
effective_response_format = request.response_format
426439

427440
# Bind model based on effective response format
428441
if isinstance(effective_response_format, ProviderStrategy):

libs/langchain_v1/langchain/agents/structured_output.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,4 +405,18 @@ def _extract_text_content_from_message(self, message: AIMessage) -> str:
405405
return str(content)
406406

407407

408-
ResponseFormat = ToolStrategy[SchemaT] | ProviderStrategy[SchemaT]
408+
class AutoStrategy(Generic[SchemaT]):
409+
"""Automatically select the best strategy for structured output."""
410+
411+
schema: type[SchemaT]
412+
"""Schema for automatic mode."""
413+
414+
def __init__(
415+
self,
416+
schema: type[SchemaT],
417+
) -> None:
418+
"""Initialize AutoStrategy with schema."""
419+
self.schema = schema
420+
421+
422+
ResponseFormat = ToolStrategy[SchemaT] | ProviderStrategy[SchemaT] | AutoStrategy[SchemaT]

0 commit comments

Comments
 (0)