Skip to content

Commit 8aea6dd

Browse files
feat: support structured output retry middleware (#33663)
* attach the latest `AIMessage` to all `StructuredOutputError`s so that relevant middleware can use as desired * raise `StructuredOutputError` from `ProviderStrategy` logic in case of failed parsing (so that we can retry from middleware) * added a test suite w/ example custom middleware that retries for tool + provider strategy Long term, we could add our own opinionated structured output retry middleware, but this at least unblocks folks who want to use custom retry logic in the short term :) ```py class StructuredOutputRetryMiddleware(AgentMiddleware): """Retries model calls when structured output parsing fails.""" def __init__(self, max_retries: int) -> None: self.max_retries = max_retries def wrap_model_call( self, request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse] ) -> ModelResponse: for attempt in range(self.max_retries + 1): try: return handler(request) except StructuredOutputError as exc: if attempt == self.max_retries: raise ai_content = exc.ai_message.content error_message = ( f"Your previous response was:\n{ai_content}\n\n" f"Error: {exc}. Please try again with a valid response." ) request.messages.append(HumanMessage(content=error_message)) ```
1 parent 78a2f86 commit 8aea6dd

File tree

4 files changed

+442
-7
lines changed

4 files changed

+442
-7
lines changed

libs/langchain_v1/langchain/agents/factory.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
ProviderStrategy,
3434
ProviderStrategyBinding,
3535
ResponseFormat,
36+
StructuredOutputError,
3637
StructuredOutputValidationError,
3738
ToolStrategy,
3839
)
@@ -797,8 +798,16 @@ def _handle_model_output(
797798
provider_strategy_binding = ProviderStrategyBinding.from_schema_spec(
798799
effective_response_format.schema_spec
799800
)
800-
structured_response = provider_strategy_binding.parse(output)
801-
return {"messages": [output], "structured_response": structured_response}
801+
try:
802+
structured_response = provider_strategy_binding.parse(output)
803+
except Exception as exc: # noqa: BLE001
804+
schema_name = getattr(
805+
effective_response_format.schema_spec.schema, "__name__", "response_format"
806+
)
807+
validation_error = StructuredOutputValidationError(schema_name, exc, output)
808+
raise validation_error
809+
else:
810+
return {"messages": [output], "structured_response": structured_response}
802811
return {"messages": [output]}
803812

804813
# Handle structured output with tool strategy
@@ -812,11 +821,11 @@ def _handle_model_output(
812821
]
813822

814823
if structured_tool_calls:
815-
exception: Exception | None = None
824+
exception: StructuredOutputError | None = None
816825
if len(structured_tool_calls) > 1:
817826
# Handle multiple structured outputs error
818827
tool_names = [tc["name"] for tc in structured_tool_calls]
819-
exception = MultipleStructuredOutputsError(tool_names)
828+
exception = MultipleStructuredOutputsError(tool_names, output)
820829
should_retry, error_message = _handle_structured_output_error(
821830
exception, effective_response_format
822831
)
@@ -858,7 +867,7 @@ def _handle_model_output(
858867
"structured_response": structured_response,
859868
}
860869
except Exception as exc: # noqa: BLE001
861-
exception = StructuredOutputValidationError(tool_call["name"], exc)
870+
exception = StructuredOutputValidationError(tool_call["name"], exc, output)
862871
should_retry, error_message = _handle_structured_output_error(
863872
exception, effective_response_format
864873
)

libs/langchain_v1/langchain/agents/structured_output.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,21 @@
3434
class StructuredOutputError(Exception):
3535
"""Base class for structured output errors."""
3636

37+
ai_message: AIMessage
38+
3739

3840
class MultipleStructuredOutputsError(StructuredOutputError):
3941
"""Raised when model returns multiple structured output tool calls when only one is expected."""
4042

41-
def __init__(self, tool_names: list[str]) -> None:
43+
def __init__(self, tool_names: list[str], ai_message: AIMessage) -> None:
4244
"""Initialize `MultipleStructuredOutputsError`.
4345
4446
Args:
4547
tool_names: The names of the tools called for structured output.
48+
ai_message: The AI message that contained the invalid multiple tool calls.
4649
"""
4750
self.tool_names = tool_names
51+
self.ai_message = ai_message
4852

4953
super().__init__(
5054
"Model incorrectly returned multiple structured responses "
@@ -55,15 +59,17 @@ def __init__(self, tool_names: list[str]) -> None:
5559
class StructuredOutputValidationError(StructuredOutputError):
5660
"""Raised when structured output tool call arguments fail to parse according to the schema."""
5761

58-
def __init__(self, tool_name: str, source: Exception) -> None:
62+
def __init__(self, tool_name: str, source: Exception, ai_message: AIMessage) -> None:
5963
"""Initialize `StructuredOutputValidationError`.
6064
6165
Args:
6266
tool_name: The name of the tool that failed.
6367
source: The exception that occurred.
68+
ai_message: The AI message that contained the invalid structured output.
6469
"""
6570
self.tool_name = tool_name
6671
self.source = source
72+
self.ai_message = ai_message
6773
super().__init__(f"Failed to parse structured output for tool '{tool_name}': {source}.")
6874

6975

0 commit comments

Comments
 (0)