Python: Centralize tool result parsing in FunctionTool.invoke()#3854
Python: Centralize tool result parsing in FunctionTool.invoke()#3854eavanvalkenburg merged 6 commits intomicrosoft:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR centralizes tool-result serialization in the Python Agent Framework by ensuring FunctionTool.invoke() always returns a pre-parsed str (plain text or JSON), and removes now-redundant consumer-side result parsing across provider integrations (OpenAI/Anthropic/Bedrock/Azure AI/AG-UI/observability) and MCP tooling.
Changes:
- Added
FunctionTool.parse_result()(+ optionalresult_parser) and madeFunctionTool.invoke()returnstrconsistently. - Removed public
prepare_function_call_resultsfrom_types.pyand updated all call sites to pass through pre-parsed strings. - Updated MCP tool/prompt result parsing to return
strdirectly and adjusted tests/type hints accordingly.
Reviewed changes
Copilot reviewed 30 out of 30 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| python/packages/core/agent_framework/_tools.py | Adds centralized result parsing, removes ReturnT, updates invoke() to return str. |
| python/packages/core/agent_framework/_types.py | Removes prepare_function_call_results from public API. |
| python/packages/core/agent_framework/_mcp.py | Changes MCP tool/prompt execution to return string results and adds new MCP parsers. |
| python/packages/core/agent_framework/openai/_chat_client.py | Stops consumer-side serialization; passes tool-result strings through directly. |
| python/packages/core/agent_framework/openai/_responses_client.py | Stops consumer-side serialization; passes tool-result strings through directly. |
| python/packages/core/agent_framework/openai/_assistants_client.py | Switches to direct string output for tool results. |
| python/packages/core/agent_framework/observability.py | Emits tool-call response content using content.result string directly. |
| python/packages/bedrock/agent_framework_bedrock/_chat_client.py | Updates Bedrock tool-result block conversion for string results. |
| python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py | Uses pre-parsed tool-result strings for Azure AI tool outputs. |
| python/packages/ag-ui/agent_framework_ag_ui/_message_adapters.py | Uses pre-parsed tool-result strings in AG-UI conversion. |
| python/packages/ag-ui/agent_framework_ag_ui/_run.py | Uses pre-parsed tool-result strings when emitting tool-result events. |
| python/packages/ag-ui/agent_framework_ag_ui/_utils.py | Updates AG-UI tool conversion typing for new FunctionTool generic. |
| python/packages/ag-ui/agent_framework_ag_ui/_client.py | Updates placeholder tool typing for new FunctionTool generic. |
| python/packages/ag-ui/agent_framework_ag_ui_examples/agents/ui_generator_agent.py | Updates example declaration-only tool typing for new FunctionTool generic. |
| python/packages/core/tests/core/test_tools.py | Updates expectations to string return values from invoke(). |
| python/packages/core/tests/core/test_types.py | Updates tests to use FunctionTool.parse_result() instead of removed helper. |
| python/packages/core/tests/core/test_mcp.py | Updates MCP tests for string results (but see comments). |
| python/packages/core/tests/openai/test_openai_chat_client.py | Updates tests to reflect string tool results and parse_result usage. |
| python/packages/azure-ai/tests/test_azure_ai_agent_client.py | Updates tests to treat tool results as pre-parsed strings. |
| python/packages/bedrock/tests/test_bedrock_settings.py | Updates Bedrock settings test to use stringified tool results. |
| python/packages/ag-ui/tests/ag_ui/test_message_adapters.py | Updates AG-UI adapter tests to use pre-parsed string tool results. |
| python/packages/github_copilot/agent_framework_github_copilot/_agent.py | Updates type hints for new FunctionTool generic. |
| python/packages/claude/agent_framework_claude/_agent.py | Updates type hints for new FunctionTool generic. |
| python/packages/orchestrations/agent_framework_orchestrations/_handoff.py | Updates type hints for new FunctionTool generic. |
| python/packages/lab/tau2/agent_framework_lab_tau2/_tau2_utils.py | Updates type hints for new FunctionTool generic. |
| python/packages/core/agent_framework/_middleware.py | Updates function middleware context typing for new FunctionTool generic. |
| python/packages/core/agent_framework/_agents.py | Updates agent-as-tool typing for new FunctionTool generic. |
Comments suppressed due to low confidence (1)
python/packages/core/tests/core/test_mcp.py:801
func.invoke()now returns astr, soresultis a string here. The subsequent assertions treat it like a list of Content (result[0].additional_properties), which will raise at runtime and makes the test invalid. Either remove the additional_properties checks, or change the code under test to return a structure that carries MCP_metametadata if that behavior is still required.
assert result == "Tool executed with metadata"
# Verify that _meta data is present in additional_properties
props = result[0].additional_properties
assert props is not None
assert props["executionTime"] == 1.5
assert props["cost"] == {"usd": 0.002}
assert props["isError"] is False
assert props["toolVersion"] == "1.2.3"
python/packages/bedrock/agent_framework_bedrock/_chat_client.py
Outdated
Show resolved
Hide resolved
TaoChenOSU
left a comment
There was a problem hiding this comment.
I approved but with a question: How is this different from forcing the tool to always return a string?
|
@TaoChenOSU there are a few cases relevant, here, 1) if you have a existing function you want to wrap, this ensures we get something back that can be sent to services/otel/etc and the same goes for MCP which returns certain types 2) in many cases the string will actually be structured but since no current provider accepts structured input, a string of that structure is the best we have. Finally I added a note to the @tool decorator to tell the user, that it doesn't make much sense to have a parser on the function if you could jsut write the function with strings in mind. |
- Add parse_result static method to FunctionTool that converts raw function return values to strings at invocation time - Add result_parser parameter to FunctionTool and @tool decorator for custom parsing - Remove prepare_function_call_results from all 9 consumer files and from the public API - Update MCPTool to parse MCP types directly to strings via _parse_tool_result_from_mcp and _parse_prompt_result_from_mcp - Change MCPTool parse_tool_results/parse_prompt_results type from Literal[True] | Callable | None to Callable | None - Remove ReturnT type parameter from FunctionTool (now single generic ArgsT since invoke() always returns str) - Update all subclass signatures and docstrings Fixes microsoft#1147
The test was still accessing result[0].additional_properties but invoke() now returns a string, not a list of Content objects.
str(result) turns None into literal 'None' and dicts into Python reprs with single quotes, breaking JSON parsing. Use the shared parse_result which handles None as '' and serializes via json.dumps.
686f683 to
794d29d
Compare
Summary
Centralizes tool result serialization by adding a
parse_resultstatic method toFunctionToolthat is called insideinvoke(), so all results are pre-parsed to strings before being returned. This aligns Python behavior with .NET'sAIFunction.InvokeAsyncwhich returnsJsonElementorAIContent.Fixes #1147
Changes
Core:
FunctionTool.parse_resultandinvoke()parse_resultstatic method that converts any return value to astr(plain text or serialized JSON)result_parser: Callable[[Any], str] | Noneparameter toFunctionTool.__init__and@tooldecorator for custom parsinginvoke()now calls the parser before returning, so all callers get pre-parsed string resultsstr()fallbackReturnTtype parameter fromFunctionTool— nowFunctionTool[ArgsT]sinceinvoke()always returnsstrRemoved:
prepare_function_call_results_prepare_function_call_results_as_dumpablefrom_types.pyand__all__MCPTool updates
_parse_tool_result_from_mcpand_parse_prompt_result_from_mcpthat convert MCP types directly to strings (skipping intermediate Content objects)parse_tool_resultstype fromLiteral[True] | Callable | NonetoCallable[[CallToolResult], str] | None(None = built-in parser)parse_prompt_resultsMCPStdioTool,MCPStreamableHTTPTool,MCPWebsocketTool)_parse_contents_from_mcp_tool_resultfunctionBreaking changes
FunctionTool.invoke()now returnsstrinstead of the raw function return typeFunctionToolis nowGeneric[ArgsT](wasGeneric[ArgsT, ReturnT])prepare_function_call_resultsremoved from public APIMCPTool.parse_tool_resultsandparse_prompt_resultsno longer acceptLiteral[True]