-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Make end_strategy also work for output tools, not just tools
#3523
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1d2a547
b3371e5
c5bfc1b
7856ce7
52d6a4c
9a27360
4a1a34e
3a0fd30
95d979e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -306,6 +306,54 @@ print(repr(result.output)) | |
|
|
||
| _(This example is complete, it can be run "as is")_ | ||
|
|
||
| ##### Handling Multiple Output Tool Calls | ||
|
|
||
| When a model calls multiple output tools in the same response (for example, when you have multiple output types in a union or list), the agent's `end_strategy` parameter controls whether all output tool functions are executed or only the first one: | ||
|
|
||
| - `'early'` (default): Only the first output tool is executed, and additional output tool calls are skipped once a final result is found. This is the default behavior. | ||
| - `'exhaustive'`: All output tool functions are executed, even after a final result is found. The first tool's result is still used as the final output. | ||
|
|
||
| This parameter also applies to [function tools](tools.md), not just output tools. | ||
|
|
||
| ```python {title="exhaustive_output.py"} | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lets remove this xample that doesn't really show usage, just "proves" that a feature works. I think we can do without, people know how to pass a kwarg to |
||
| from pydantic import BaseModel | ||
|
|
||
| from pydantic_ai import Agent, ToolOutput | ||
|
|
||
| calls_made: list[str] = [] | ||
|
|
||
| class ResultA(BaseModel): | ||
| value: str | ||
|
|
||
| class ResultB(BaseModel): | ||
| value: str | ||
|
|
||
| def process_a(result: ResultA) -> ResultA: | ||
| calls_made.append('A') | ||
| return result | ||
|
|
||
| def process_b(result: ResultB) -> ResultB: | ||
| calls_made.append('B') | ||
| return result | ||
|
|
||
| # With 'exhaustive' strategy, both functions will be called | ||
| agent = Agent( | ||
| 'openai:gpt-5', | ||
| output_type=[ | ||
| ToolOutput(process_a, name='return_a'), | ||
| ToolOutput(process_b, name='return_b'), | ||
| ], | ||
| end_strategy='exhaustive', # (1)! | ||
| ) | ||
|
|
||
| # If the model calls both output tools, both process_a and process_b will execute | ||
| # calls_made will be ['A', 'B'] | ||
| ``` | ||
|
|
||
| 1. Setting `end_strategy='exhaustive'` ensures all output tool functions are executed, which can be useful for side effects like logging, metrics, or validation. | ||
|
|
||
| _(This example is complete, it can be run "as is")_ | ||
|
|
||
| #### Native Output | ||
|
|
||
| Native Output mode uses a model's native "Structured Outputs" feature (aka "JSON Schema response format"), where the model is forced to only output text matching the provided JSON schema. Note that this is not supported by all models, and sometimes comes with restrictions. For example, Anthropic does not support this at all, and Gemini cannot use tools at the same time as structured output, and attempting to do so will result in an error. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -378,6 +378,14 @@ If a tool requires sequential/serial execution, you can pass the [`sequential`][ | |
|
|
||
| Async functions are run on the event loop, while sync functions are offloaded to threads. To get the best performance, _always_ use an async function _unless_ you're doing blocking I/O (and there's no way to use a non-blocking library instead) or CPU-bound work (like `numpy` or `scikit-learn` operations), so that simple functions are not offloaded to threads unnecessarily. | ||
|
|
||
| !!! note "Handling tool calls after a final result" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is worth a dedicated section so we get an item in the ToC sidebar and people ask about |
||
| When a model returns multiple tool calls including an [output tool](output.md) (which produces a final result), the agent's `end_strategy` parameter controls whether remaining function tools are executed: | ||
|
|
||
| - `'early'` (default): Function tools are skipped once a final result is found | ||
| - `'exhaustive'`: All function tools are executed even after a final result is found | ||
|
|
||
| This is useful when function tools have side effects (like logging or metrics) that should always execute. See [Handling Multiple Output Tool Calls](output.md#handling-multiple-output-tool-calls) for more details. | ||
|
|
||
| !!! note "Limiting tool executions" | ||
| You can cap tool executions within a run using [`UsageLimits(tool_calls_limit=...)`](agents.md#usage-limits). The counter increments only after a successful tool invocation. Output tools (used for [structured output](output.md)) are not counted in the `tool_calls` metric. | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -61,8 +61,10 @@ | |
| EndStrategy = Literal['early', 'exhaustive'] | ||
| """The strategy for handling multiple tool calls when a final result is found. | ||
|
|
||
| - `'early'`: Stop processing other tool calls once a final result is found | ||
| - `'exhaustive'`: Process all tool calls even after finding a final result | ||
| - `'early'`: Stop processing other tool calls (both function tools and output tools) once a final result is found | ||
| - `'exhaustive'`: Process all tool calls (both function tools and output tools) even after finding a final result | ||
|
|
||
| This applies to both function tools and output tools. | ||
| """ | ||
| DepsT = TypeVar('DepsT') | ||
| OutputT = TypeVar('OutputT') | ||
|
|
@@ -833,24 +835,27 @@ async def process_tool_calls( # noqa: C901 | |
|
|
||
| # First, we handle output tool calls | ||
| for call in tool_calls_by_kind['output']: | ||
| if final_result: | ||
| if final_result.tool_call_id == call.tool_call_id: | ||
| part = _messages.ToolReturnPart( | ||
| tool_name=call.tool_name, | ||
| content='Final result processed.', | ||
| tool_call_id=call.tool_call_id, | ||
| ) | ||
| else: | ||
| yield _messages.FunctionToolCallEvent(call) | ||
| part = _messages.ToolReturnPart( | ||
| tool_name=call.tool_name, | ||
| content='Output tool not used - a final result was already processed.', | ||
| tool_call_id=call.tool_call_id, | ||
| ) | ||
| yield _messages.FunctionToolResultEvent(part) | ||
|
|
||
| # In case we got two tool calls with the same ID | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is actually for the case where |
||
| if final_result and final_result.tool_call_id == call.tool_call_id: | ||
| part = _messages.ToolReturnPart( | ||
| tool_name=call.tool_name, | ||
| content='Final result processed.', | ||
| tool_call_id=call.tool_call_id, | ||
| ) | ||
| output_parts.append(part) | ||
| else: | ||
| # Early strategy is chosen and final result is already set | ||
| elif ctx.deps.end_strategy == 'early' and final_result: | ||
| yield _messages.FunctionToolCallEvent(call) | ||
| part = _messages.ToolReturnPart( | ||
| tool_name=call.tool_name, | ||
| content='Output tool not used - a final result was already processed.', | ||
| tool_call_id=call.tool_call_id, | ||
| ) | ||
| yield _messages.FunctionToolResultEvent(part) | ||
| output_parts.append(part) | ||
| # Early strategy is chosen and final result is not yet set | ||
| # Or exhaustive strategy is chosen | ||
| elif (ctx.deps.end_strategy == 'early' and not final_result) or ctx.deps.end_strategy == 'exhaustive': | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this be
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, but I prefer more straight-forward code, that's why I've chosen this approach |
||
| try: | ||
| result_data = await tool_manager.handle_call(call) | ||
| except exceptions.UnexpectedModelBehavior as e: | ||
|
|
@@ -872,7 +877,15 @@ async def process_tool_calls( # noqa: C901 | |
| tool_call_id=call.tool_call_id, | ||
| ) | ||
| output_parts.append(part) | ||
| final_result = result.FinalResult(result_data, call.tool_name, call.tool_call_id) | ||
|
|
||
| # Even in exhaustive mode, use the first output tool's result as the final result | ||
| if not final_result: | ||
| final_result = result.FinalResult(result_data, call.tool_name, call.tool_call_id) | ||
| # Unknown strategy or invalid state | ||
| else: | ||
| assert False, ( | ||
| f'Unexpected state: end_strategy={ctx.deps.end_strategy!r}, final_result={final_result!r};' | ||
| ) # pragma: no cover | ||
|
|
||
| # Then, we handle function tool calls | ||
| calls_to_run: list[_messages.ToolCallPart] = [] | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3132,7 +3132,7 @@ def deferred_tool(x: int) -> int: # pragma: no cover | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ToolReturnPart( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tool_name='final_result', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content='Output tool not used - a final result was already processed.', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content='Final result processed.', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tool_call_id=IsStr(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timestamp=IsNow(tz=timezone.utc), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -3163,6 +3163,174 @@ def deferred_tool(x: int) -> int: # pragma: no cover | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_exhaustive_strategy_calls_all_output_tools(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Test that 'exhaustive' strategy executes all output tool functions.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| output_tools_called: list[str] = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class FirstOutput(BaseModel): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: str | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class SecondOutput(BaseModel): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: str | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def process_first(output: FirstOutput) -> FirstOutput: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Process first output.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| output_tools_called.append('first') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return output | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def process_second(output: SecondOutput) -> SecondOutput: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Process second output.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| output_tools_called.append('second') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return output | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def return_model(_: list[ModelMessage], info: AgentInfo) -> ModelResponse: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert info.output_tools is not None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ModelResponse( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parts=[ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ToolCallPart('first_output', {'value': 'first'}), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ToolCallPart('second_output', {'value': 'second'}), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we also test here what happens if the second call is invalid? It should be consistent with exhaustive execution of non-output tool calls in terms of whether it causes us to go back to the model to make it try again, or whether we ignore the tool call failure because we already have valid final output
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. pydantic-ai/pydantic_ai_slim/pydantic_ai/_agent_graph.py Lines 1079 to 1106 in 5afc2d6
Judging by those lines, it seems retries are done to model in exhaustive strategy, if any tool (not output) is failed
Here's a small gift that confirms this behavior:
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok great. Can you add a test here please for that? We have one for streaming, but I think we should have one here as well
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's also worth testing what happens if the first is invalid, and the second is valid. I think the second should become the output value |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| agent = Agent( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FunctionModel(return_model), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| output_type=[ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ToolOutput(process_first, name='first_output'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ToolOutput(process_second, name='second_output'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end_strategy='exhaustive', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result = agent.run_sync('test exhaustive output tools') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Verify the result came from the first output tool | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert isinstance(result.output, FirstOutput) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert result.output.value == 'first' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Verify both output tools were called | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert output_tools_called == ['first', 'second'] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we verify that
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Verify we got tool returns in the correct order | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert result.all_messages() == snapshot( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ModelRequest( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parts=[UserPromptPart(content='test exhaustive output tools', timestamp=IsNow(tz=timezone.utc))], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| run_id=IsStr(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ModelResponse( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parts=[ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ToolCallPart(tool_name='first_output', args={'value': 'first'}, tool_call_id=IsStr()), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ToolCallPart(tool_name='second_output', args={'value': 'second'}, tool_call_id=IsStr()), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| usage=RequestUsage(input_tokens=54, output_tokens=10), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| model_name='function:return_model:', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timestamp=IsNow(tz=timezone.utc), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| run_id=IsStr(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ModelRequest( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parts=[ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ToolReturnPart( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tool_name='first_output', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content='Final result processed.', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tool_call_id=IsStr(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timestamp=IsNow(tz=timezone.utc), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ToolReturnPart( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tool_name='second_output', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content='Final result processed.', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tool_call_id=IsStr(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timestamp=IsNow(tz=timezone.utc), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| run_id=IsStr(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_early_strategy_does_not_call_additional_output_tools(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Test that 'early' strategy does not execute additional output tool functions.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| output_tools_called: list[str] = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class FirstOutput(BaseModel): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: str | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class SecondOutput(BaseModel): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: str | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def process_first(output: FirstOutput) -> FirstOutput: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Process first output.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| output_tools_called.append('first') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return output | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def process_second(output: SecondOutput) -> SecondOutput: # pragma: no cover | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Process second output.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| output_tools_called.append('second') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return output | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def return_model(_: list[ModelMessage], info: AgentInfo) -> ModelResponse: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert info.output_tools is not None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ModelResponse( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parts=[ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ToolCallPart('first_output', {'value': 'first'}), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ToolCallPart('second_output', {'value': 'second'}), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| agent = Agent( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FunctionModel(return_model), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| output_type=[ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ToolOutput(process_first, name='first_output'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ToolOutput(process_second, name='second_output'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end_strategy='early', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result = agent.run_sync('test early output tools') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Verify the result came from the first output tool | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert isinstance(result.output, FirstOutput) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert result.output.value == 'first' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Verify only the first output tool was called | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert output_tools_called == ['first'] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we verify that
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Verify we got tool returns in the correct order | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert result.all_messages() == snapshot( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ModelRequest( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parts=[UserPromptPart(content='test early output tools', timestamp=IsNow(tz=timezone.utc))], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| run_id=IsStr(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ModelResponse( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parts=[ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ToolCallPart(tool_name='first_output', args={'value': 'first'}, tool_call_id=IsStr()), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ToolCallPart(tool_name='second_output', args={'value': 'second'}, tool_call_id=IsStr()), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| usage=RequestUsage(input_tokens=54, output_tokens=10), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| model_name='function:return_model:', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timestamp=IsNow(tz=timezone.utc), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| run_id=IsStr(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ModelRequest( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parts=[ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ToolReturnPart( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tool_name='first_output', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content='Final result processed.', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tool_call_id=IsStr(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timestamp=IsNow(tz=timezone.utc), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ToolReturnPart( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tool_name='second_output', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content='Output tool not used - a final result was already processed.', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tool_call_id=IsStr(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timestamp=IsNow(tz=timezone.utc), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| run_id=IsStr(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_early_strategy_with_final_result_in_middle(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Test that 'early' strategy stops at first final result, regardless of position.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tool_called = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be a
!!! notesection, above the previous example? I don't want it to show up in the ToC here