Skip to content

Python: Centralize tool result parsing in FunctionTool.invoke() - #3854

Merged
eavanvalkenburg merged 6 commits into
microsoft:mainfrom
eavanvalkenburg:tools_update
Feb 12, 2026
Merged

Python: Centralize tool result parsing in FunctionTool.invoke()#3854
eavanvalkenburg merged 6 commits into
microsoft:mainfrom
eavanvalkenburg:tools_update

Conversation

@eavanvalkenburg

Copy link
Copy Markdown
Member

Summary

Centralizes tool result serialization by adding a parse_result static method to FunctionTool that is called inside invoke(), so all results are pre-parsed to strings before being returned. This aligns Python behavior with .NET's AIFunction.InvokeAsync which returns JsonElement or AIContent.

Fixes #1147

Changes

Core: FunctionTool.parse_result and invoke()

  • Added parse_result static method that converts any return value to a str (plain text or serialized JSON)
  • Added result_parser: Callable[[Any], str] | None parameter to FunctionTool.__init__ and @tool decorator for custom parsing
  • invoke() now calls the parser before returning, so all callers get pre-parsed string results
  • Parser exceptions are caught gracefully with a str() fallback
  • Removed ReturnT type parameter from FunctionTool — now FunctionTool[ArgsT] since invoke() always returns str

Removed: prepare_function_call_results

  • Removed the function and its helper _prepare_function_call_results_as_dumpable from _types.py and __all__
  • Removed all 9 consumer-side calls (OpenAI, Anthropic, Bedrock, Azure AI, AG-UI, observability)

MCPTool updates

  • Added _parse_tool_result_from_mcp and _parse_prompt_result_from_mcp that convert MCP types directly to strings (skipping intermediate Content objects)
  • Changed parse_tool_results type from Literal[True] | Callable | None to Callable[[CallToolResult], str] | None (None = built-in parser)
  • Same for parse_prompt_results
  • Updated all 3 subclass signatures (MCPStdioTool, MCPStreamableHTTPTool, MCPWebsocketTool)
  • Removed unused _parse_contents_from_mcp_tool_result function

Breaking changes

  • FunctionTool.invoke() now returns str instead of the raw function return type
  • FunctionTool is now Generic[ArgsT] (was Generic[ArgsT, ReturnT])
  • prepare_function_call_results removed from public API
  • MCPTool.parse_tool_results and parse_prompt_results no longer accept Literal[True]

Copilot AI review requested due to automatic review settings February 11, 2026 16:28
@markwallace-microsoft markwallace-microsoft added python Usage: [Issues, PRs], Target: Python lab labels Feb 11, 2026
@github-actions github-actions Bot changed the title Centralize tool result parsing in FunctionTool.invoke() Python: Centralize tool result parsing in FunctionTool.invoke() Feb 11, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() (+ optional result_parser) and made FunctionTool.invoke() return str consistently.
  • Removed public prepare_function_call_results from _types.py and updated all call sites to pass through pre-parsed strings.
  • Updated MCP tool/prompt result parsing to return str directly 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 a str, so result is 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 _meta metadata 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"

Comment thread python/packages/core/agent_framework/_tools.py Outdated
Comment thread python/packages/core/agent_framework/openai/_assistants_client.py
Comment thread python/packages/bedrock/agent_framework_bedrock/_chat_client.py Outdated
@markwallace-microsoft

markwallace-microsoft commented Feb 11, 2026

Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/ag-ui/agent_framework_ag_ui
   _client.py1511788%85–86, 90–94, 98–102, 265, 295, 464–466
   _message_adapters.py4559579%90, 100–101, 110–113, 116–120, 122–127, 130, 139–145, 148, 152–154, 163–165, 185, 191–193, 223, 236–237, 247–248, 285, 288, 290, 293, 296, 312, 329, 351, 382, 387, 398–399, 450, 466–467, 533–536, 538, 544, 552–553, 555, 559–562, 575, 664–667, 669, 734, 769–771, 773–776, 779–780, 782, 788, 791, 793, 796, 798, 804–805, 807
   _run.py48212174%156–163, 306, 325–326, 341–342, 357, 385–387, 412, 415–418, 420–421, 424–430, 433–435, 438, 454–456, 463, 469–471, 475, 480–482, 484–485, 501–505, 516, 529, 531–532, 548, 569–570, 625–627, 639–641, 839, 850–851, 858, 876–878, 912–914, 931, 937, 945, 947, 983–989, 992–995, 997–1006, 1009, 1017–1020, 1027, 1030–1031, 1036, 1042–1044, 1048, 1053–1056, 1070–1072
   _utils.py100199%74
packages/anthropic/agent_framework_anthropic
   _chat_client.py36314859%422, 451, 483, 485, 500, 522–525, 534, 536, 573–577, 579, 581–582, 584, 589–590, 592, 625–626, 635, 637–638, 643, 660–661, 705, 728, 737, 739, 743–744, 787–789, 791, 804–805, 812–814, 818–820, 824–827, 838, 840, 862, 872, 894–900, 907–908, 916–917, 925–928, 935–936, 942–943, 949–950, 956, 964–966, 970, 977–978, 984–985, 991–992, 998, 1006–1009, 1016–1017, 1036, 1043–1044, 1063, 1085, 1087, 1096–1097, 1103, 1125–1126, 1132–1133, 1142–1152, 1159–1165, 1172–1178, 1185–1194, 1201–1204
packages/azure-ai/agent_framework_azure_ai
   _chat_client.py4757584%387–388, 390, 573, 578–579, 581–582, 585, 588, 590, 595, 856–857, 859, 862, 865, 868–873, 876, 878, 886, 898–900, 904, 907–908, 916–919, 929, 937–940, 942–943, 945–946, 953, 961–962, 970–971, 976–977, 981–988, 993, 996, 1004, 1010, 1018–1020, 1023, 1045–1046, 1179, 1207, 1222, 1338, 1466
packages/core/agent_framework
   _agents.py3203589%474, 887, 924, 1024–1026, 1139, 1180, 1182, 1191–1196, 1202, 1204, 1214–1215, 1222, 1224–1225, 1233–1237, 1245–1246, 1248, 1253, 1255, 1289, 1329, 1349
   _mcp.py4066484%104–105, 110–115, 121, 126, 167–168, 173–178, 183–184, 236, 245, 308, 316, 337, 451, 518, 553, 555, 559–560, 562–563, 617, 632, 650, 691, 797, 810–815, 837, 874–875, 881–883, 902, 927–928, 932–936, 953–957, 1101
   _middleware.py3291695%80, 83, 88, 795, 797, 799, 920, 947, 949, 974, 1055, 1059, 1183, 1187, 1248, 1322
   _tools.py7558688%175–176, 306, 308, 326–328, 335, 353, 367, 379, 384, 386, 393, 430, 453–455, 504–506, 569, 591, 613–641, 676, 684, 925, 1187, 1244, 1248, 1327–1331, 1349, 1351–1352, 1464, 1468, 1518, 1520, 1536, 1538, 1602, 1629, 1686, 1754, 1933–1934, 1961, 1969, 1982, 1992–1993, 2028, 2084, 2116
   _types.py10269690%82, 91–92, 146, 151, 170, 172, 176, 180, 182, 184, 186, 204, 208, 234, 256, 261, 266, 270, 296, 300, 646–647, 1018, 1080, 1097, 1115, 1120, 1138, 1148, 1165–1166, 1168, 1186–1187, 1189, 1196–1197, 1199, 1234, 1245–1246, 1248, 1286, 1513, 1566, 1573, 1595, 1601, 1649, 1692–1697, 1719, 1724, 1890, 1902, 2145, 2154, 2175, 2270, 2495, 2702, 2772, 2784, 2791, 2802, 3006–3008, 3011–3013, 3017, 3022, 3026, 3138–3140, 3168, 3222, 3226–3228, 3230, 3241–3242, 3245–3249, 3255
   observability.py6188486%335, 337–339, 342–344, 349–350, 356–357, 363–364, 371, 373–375, 378–380, 385–386, 392–393, 399–400, 407, 676, 679, 687–688, 691–694, 696, 699–701, 704–705, 733, 735, 746–748, 750–753, 757, 765, 866, 868, 1017, 1019, 1023–1028, 1030, 1033–1037, 1039, 1151–1152, 1154, 1211–1212, 1347, 1401–1402, 1518–1520, 1579, 1747, 1901, 1903
packages/core/agent_framework/openai
   _assistants_client.py2743487%413, 415, 417, 420, 424–425, 428, 431, 436–437, 439, 442–444, 449, 460, 485, 487, 489, 491, 493, 498, 501, 504, 508, 519, 604, 689, 718, 755–758, 828
   _chat_client.py2692291%202, 232–233, 237, 351, 358, 439–446, 448–451, 461, 546, 581, 597
   _responses_client.py6098086%292–295, 299–300, 303–304, 310–311, 316, 329–335, 356, 364, 387, 550, 553, 608, 612, 614, 616, 618, 694, 704, 709, 752, 811, 825, 842, 855, 911, 996, 1001, 1005–1007, 1011–1012, 1035, 1104, 1126–1127, 1142–1143, 1161–1162, 1293–1294, 1310, 1312, 1391–1399, 1494, 1549, 1564, 1600–1601, 1603–1605, 1619–1621, 1631–1632, 1638, 1653
packages/orchestrations/agent_framework_orchestrations
   _handoff.py3265682%104–105, 107, 136–137, 159–169, 171, 173, 175, 180, 278, 332, 357, 385, 393–394, 408, 459–460, 492, 532–534, 539–541, 657, 660, 667, 672, 734, 739, 746, 756, 758, 777, 779, 861–862, 894–895, 977, 984, 1056–1057, 1059
TOTAL21191326784% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
4094 225 💤 0 ❌ 0 🔥 1m 11s ⏱️

@TaoChenOSU TaoChenOSU left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I approved but with a question: How is this different from forcing the tool to always return a string?

@eavanvalkenburg

Copy link
Copy Markdown
Member Author

@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.
@eavanvalkenburg
eavanvalkenburg added this pull request to the merge queue Feb 12, 2026
Merged via the queue into microsoft:main with commit 8ed5000 Feb 12, 2026
36 checks passed
@moonbox3 moonbox3 removed the lab label Jun 17, 2026
@eavanvalkenburg
eavanvalkenburg deleted the tools_update branch June 30, 2026 09:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: .NET: FunctionResultContent behaves differently between Python and C#

7 participants