Skip to content

Comments

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

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

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

Conversation

@eavanvalkenburg
Copy link
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 lab Agent Framework 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
Copy link
Contributor

Copilot AI left a comment

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"

@markwallace-microsoft
Copy link
Member

markwallace-microsoft commented Feb 11, 2026

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 ⏱️

Copy link
Contributor

@TaoChenOSU TaoChenOSU left a comment

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
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lab Agent Framework Lab python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

5 participants