Skip to content

Commit 5e33def

Browse files
giles17Copilot
andauthored
Python: Unify tool results as Content items with rich content support (#4331)
* feat(python): allow @tool functions to return rich content (images, audio) Add support for tool functions to return Content objects that the model can perceive natively. Closes #4272 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Anthropic logging + mypy fix * Address PR review: fix MCP ordering, fold helper into from_function_result, fix Chat client - Preserve original content order in MCP tool results instead of text-first - Move _build_function_result logic into Content.from_function_result() - Chat Completions: inject user message for rich items (API only supports string tool content) - Update tests for ordering and new from_function_result behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use native Responses API multi-part output, warn+omit for Chat client - Responses client: put rich items directly in function_call_output's output field as list (native API support) instead of user message injection - Chat client: warn and omit rich items (API doesn't support multi-part tool results), matching Ollama/Bedrock pattern - Unify test image: use sample_image.jpg across all integration tests - Add Azure OpenAI Responses integration test - Assert model describes house image to verify perception Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix lint: remove print statement, wrap long line Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review feedback: bug fixes, single-pass MCP, unit tests - Add isinstance guard in from_function_result for non-Content lists - Fix Anthropic empty tool_content fallback to string result - Fix Content(type='text', text=None) edge case in parse_result - Rewrite MCP _parse_tool_result_from_mcp as single-pass (no index counters) - Add Anthropic unit tests: data image, uri image, unsupported media, all-unsupported - Add OpenAI Chat unit test: rich items warning and omission - Add OpenAI Responses unit tests: function_result with/without items - Add test_types tests: only-rich-items list, non-Content list fallback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix pyright errors: add type ignore comments for Any list iteration Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix mypy/pyright: ensure ToolExecutionException receives str Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix lint: remove duplicate test_prepare_options_excludes_conversation_id Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: unify all tool results into Content items * addressed copilot comments * pyright fix * small fix * comments * fix: address Copilot review - warnings, blob safety, dedup - Add warning logs when rich content is dropped in Claude agent and MCP server handlers (matching Chat/Bedrock/Ollama pattern) - Defensive blob URI construction: wrap plain base64 in data: prefix - Simplify Chat client _prepare_content_for_openai to use content.result - Simplify Responses client text-only path, remove redundant nesting - Add test for plain base64 blob without data: prefix Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix token double-counting in compaction and address review comments - Exclude items from _serialize_content() to prevent double-counting tokens when items mirrors result in function_result content - Add rich content warning in GitHub Copilot agent tool handler - Replace raw Content debug log with concise item count/type summary - Update stale test comments about FunctionTool.invoke return type Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b6a1315 commit 5e33def

27 files changed

Lines changed: 1337 additions & 332 deletions

File tree

python/packages/ag-ui/tests/ag_ui/test_event_converters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ def test_tool_call_result_event(self) -> None:
185185
assert update.role == "tool"
186186
assert len(update.contents) == 1
187187
assert update.contents[0].call_id == "call_123"
188-
assert update.contents[0].result == {"temperature": 22, "condition": "sunny"}
188+
assert update.contents[0].result == '{"temperature": 22, "condition": "sunny"}'
189189

190190
def test_run_finished_event(self) -> None:
191191
"""Test conversion of RUN_FINISHED event."""

python/packages/anthropic/agent_framework_anthropic/_chat_client.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -716,12 +716,46 @@ def _prepare_message_for_anthropic(self, message: Message) -> dict[str, Any]:
716716
"input": content.parse_arguments(),
717717
})
718718
case "function_result":
719-
a_content.append({
720-
"type": "tool_result",
721-
"tool_use_id": content.call_id,
722-
"content": content.result if content.result is not None else "",
723-
"is_error": content.exception is not None,
724-
})
719+
if content.items:
720+
tool_content: list[dict[str, Any]] = []
721+
for item in content.items:
722+
if item.type == "text":
723+
tool_content.append({"type": "text", "text": item.text or ""})
724+
elif item.type == "data" and item.has_top_level_media_type("image"):
725+
tool_content.append({
726+
"type": "image",
727+
"source": {
728+
"data": _get_data_bytes_as_str(item), # type: ignore[attr-defined]
729+
"media_type": item.media_type,
730+
"type": "base64",
731+
},
732+
})
733+
elif item.type == "uri" and item.has_top_level_media_type("image"):
734+
tool_content.append({
735+
"type": "image",
736+
"source": {"type": "url", "url": item.uri},
737+
})
738+
else:
739+
logger.debug(
740+
"Ignoring unsupported rich content media type in tool result: %s",
741+
item.media_type,
742+
)
743+
tool_result_content = (
744+
tool_content if tool_content else (content.result if content.result is not None else "")
745+
)
746+
a_content.append({
747+
"type": "tool_result",
748+
"tool_use_id": content.call_id,
749+
"content": tool_result_content,
750+
"is_error": content.exception is not None,
751+
})
752+
else:
753+
a_content.append({
754+
"type": "tool_result",
755+
"tool_use_id": content.call_id,
756+
"content": content.result if content.result is not None else "",
757+
"is_error": content.exception is not None,
758+
})
725759
case "mcp_server_tool_call":
726760
mcp_call: dict[str, Any] = {
727761
"type": "mcp_tool_use",

0 commit comments

Comments
 (0)