diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_assistant_agent.py b/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_assistant_agent.py index 8b8316fb0a7c..39ab3c311966 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_assistant_agent.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_assistant_agent.py @@ -1551,6 +1551,7 @@ async def _execute_tool_call( FunctionExecutionResult( content=f"Error: {e}", call_id=tool_call.id, + uuid=tool_call.uuid, is_error=True, name=tool_call.name, ), @@ -1568,6 +1569,7 @@ async def _execute_tool_call( FunctionExecutionResult( content=result_as_str, call_id=tool_call.id, + uuid=tool_call.uuid, is_error=False, name=tool_call.name, ), @@ -1608,6 +1610,7 @@ async def _execute_tool_call( FunctionExecutionResult( content=tool_result.to_text(), call_id=tool_call.id, + uuid=tool_call.uuid, is_error=tool_result.is_error, name=tool_call.name, ), @@ -1618,6 +1621,7 @@ async def _execute_tool_call( FunctionExecutionResult( content=f"Error: tool '{tool_call.name}' not found in any workbench", call_id=tool_call.id, + uuid=tool_call.uuid, is_error=True, name=tool_call.name, ), diff --git a/python/packages/autogen-agentchat/tests/test_assistant_agent.py b/python/packages/autogen-agentchat/tests/test_assistant_agent.py index 935f2471045c..59d649ed7ef0 100644 --- a/python/packages/autogen-agentchat/tests/test_assistant_agent.py +++ b/python/packages/autogen-agentchat/tests/test_assistant_agent.py @@ -237,15 +237,14 @@ async def test_model_client_stream_with_tool_calls() -> None: assert isinstance(message.messages[-1], TextMessage) assert isinstance(message.messages[1], ToolCallRequestEvent) assert message.messages[-1].content == "Example response 2 to task" - assert message.messages[1].content == [ - FunctionCall(id="1", name="_pass_function", arguments=r'{"input": "task"}'), - FunctionCall(id="3", name="_echo_function", arguments=r'{"input": "task"}'), - ] + tool_calls = message.messages[1].content + assert [call.id for call in tool_calls] == ["1", "3"] + assert [call.name for call in tool_calls] == ["_pass_function", "_echo_function"] assert isinstance(message.messages[2], ToolCallExecutionEvent) - assert message.messages[2].content == [ - FunctionExecutionResult(call_id="1", content="pass", is_error=False, name="_pass_function"), - FunctionExecutionResult(call_id="3", content="task", is_error=False, name="_echo_function"), - ] + tool_results = message.messages[2].content + assert [result.call_id for result in tool_results] == ["1", "3"] + assert [result.content for result in tool_results] == ["pass", "task"] + assert [result.uuid for result in tool_results] == [call.uuid for call in tool_calls] elif isinstance(message, ModelClientStreamingChunkEvent): chunks.append(message.content) assert "".join(chunks) == "Example response 2 to task" diff --git a/python/packages/autogen-core/src/autogen_core/_types.py b/python/packages/autogen-core/src/autogen_core/_types.py index 5e3850ffae8b..903fa7e65ab8 100644 --- a/python/packages/autogen-core/src/autogen_core/_types.py +++ b/python/packages/autogen-core/src/autogen_core/_types.py @@ -1,6 +1,7 @@ from __future__ import annotations -from dataclasses import dataclass +import uuid +from dataclasses import dataclass, field @dataclass @@ -10,3 +11,5 @@ class FunctionCall: arguments: str # Function to call name: str + # Framework-generated identifier for correlating the call with its result. + uuid: str = field(default_factory=lambda: str(uuid.uuid4()), compare=False) diff --git a/python/packages/autogen-core/src/autogen_core/models/_types.py b/python/packages/autogen-core/src/autogen_core/models/_types.py index 6fd2e5c3534d..6286a60124f6 100644 --- a/python/packages/autogen-core/src/autogen_core/models/_types.py +++ b/python/packages/autogen-core/src/autogen_core/models/_types.py @@ -65,6 +65,9 @@ class FunctionExecutionResult(BaseModel): call_id: str """The ID of the function call. Note this ID may be empty for some models.""" + uuid: str | None = None + """Framework-generated identifier for correlating this result with its function call.""" + is_error: bool | None = None """Whether the function call resulted in an error.""" diff --git a/python/packages/autogen-core/src/autogen_core/tool_agent/_caller_loop.py b/python/packages/autogen-core/src/autogen_core/tool_agent/_caller_loop.py index e5d64c3a4d9a..e32b12cc4b76 100644 --- a/python/packages/autogen-core/src/autogen_core/tool_agent/_caller_loop.py +++ b/python/packages/autogen-core/src/autogen_core/tool_agent/_caller_loop.py @@ -64,7 +64,11 @@ async def tool_agent_caller_loop( elif isinstance(result, ToolException): function_results.append( FunctionExecutionResult( - content=f"Error: {result}", call_id=result.call_id, is_error=True, name=result.name + content=f"Error: {result}", + call_id=result.call_id, + is_error=True, + name=result.name, + uuid=result.uuid, ) ) elif isinstance(result, BaseException): diff --git a/python/packages/autogen-core/src/autogen_core/tool_agent/_tool_agent.py b/python/packages/autogen-core/src/autogen_core/tool_agent/_tool_agent.py index 2ddb8dc2da4c..a36cffd7bcb1 100644 --- a/python/packages/autogen-core/src/autogen_core/tool_agent/_tool_agent.py +++ b/python/packages/autogen-core/src/autogen_core/tool_agent/_tool_agent.py @@ -1,6 +1,7 @@ import json -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import List +from uuid import uuid4 from .. import FunctionCall, MessageContext, RoutedAgent, message_handler from ..models import FunctionExecutionResult @@ -20,6 +21,7 @@ class ToolException(BaseException): call_id: str content: str name: str + uuid: str = field(default_factory=lambda: str(uuid4())) @dataclass @@ -78,7 +80,10 @@ async def handle_function_call(self, message: FunctionCall, ctx: MessageContext) tool = next((tool for tool in self._tools if tool.name == message.name), None) if tool is None: raise ToolNotFoundException( - call_id=message.id, content=f"Error: Tool not found: {message.name}", name=message.name + call_id=message.id, + content=f"Error: Tool not found: {message.name}", + name=message.name, + uuid=message.uuid, ) else: try: @@ -89,8 +94,15 @@ async def handle_function_call(self, message: FunctionCall, ctx: MessageContext) result_as_str = tool.return_value_as_string(result) except json.JSONDecodeError as e: raise InvalidToolArgumentsException( - call_id=message.id, content=f"Error: Invalid arguments: {message.arguments}", name=message.name + call_id=message.id, + content=f"Error: Invalid arguments: {message.arguments}", + name=message.name, + uuid=message.uuid, ) from e except Exception as e: - raise ToolExecutionException(call_id=message.id, content=f"Error: {e}", name=message.name) from e - return FunctionExecutionResult(content=result_as_str, call_id=message.id, is_error=False, name=message.name) + raise ToolExecutionException( + call_id=message.id, content=f"Error: {e}", name=message.name, uuid=message.uuid + ) from e + return FunctionExecutionResult( + content=result_as_str, call_id=message.id, is_error=False, name=message.name, uuid=message.uuid + ) diff --git a/python/packages/autogen-core/tests/test_tool_agent.py b/python/packages/autogen-core/tests/test_tool_agent.py index edb2acfbf375..213d54f38bff 100644 --- a/python/packages/autogen-core/tests/test_tool_agent.py +++ b/python/packages/autogen-core/tests/test_tool_agent.py @@ -1,6 +1,7 @@ import asyncio import json import logging +import uuid from typing import Any, AsyncGenerator, List, Literal, Mapping, Optional, Sequence, Union import pytest @@ -62,10 +63,9 @@ async def test_tool_agent(caplog: pytest.LogCaptureFixture) -> None: runtime.start() # Test pass function - result = await runtime.send_message( - FunctionCall(id="1", arguments=json.dumps({"input": "pass"}), name="pass"), agent - ) - assert result == FunctionExecutionResult(call_id="1", content="pass", is_error=False, name="pass") + call = FunctionCall(id="1", arguments=json.dumps({"input": "pass"}), name="pass") + result = await runtime.send_message(call, agent) + assert result == FunctionExecutionResult(call_id="1", content="pass", is_error=False, name="pass", uuid=call.uuid) # Check log. assert any(("ToolCall" in record.message and str(agent) in record.message) for record in caplog.records) @@ -94,6 +94,15 @@ async def test_tool_agent(caplog: pytest.LogCaptureFixture) -> None: await runtime.stop() +def test_function_call_has_unique_uuid() -> None: + first = FunctionCall(id="1", arguments="{}", name="test") + second = FunctionCall(id="1", arguments="{}", name="test") + + assert first.uuid != second.uuid + uuid.UUID(first.uuid) + uuid.UUID(second.uuid) + + @pytest.mark.asyncio async def test_caller_loop() -> None: class MockChatCompletionClient(ChatCompletionClient): diff --git a/python/packages/autogen-ext/src/autogen_ext/agents/azure/_azure_ai_agent.py b/python/packages/autogen-ext/src/autogen_ext/agents/azure/_azure_ai_agent.py index 42b98f81625d..898ba95e2812 100644 --- a/python/packages/autogen-ext/src/autogen_ext/agents/azure/_azure_ai_agent.py +++ b/python/packages/autogen-ext/src/autogen_ext/agents/azure/_azure_ai_agent.py @@ -753,7 +753,11 @@ async def on_messages_stream( is_error = True tool_outputs.append( FunctionExecutionResult( - content=result, call_id=tool_call.id, is_error=is_error, name=tool_call.name + content=result, + call_id=tool_call.id, + uuid=tool_call.uuid, + is_error=is_error, + name=tool_call.name, ) ) diff --git a/python/packages/autogen-ext/src/autogen_ext/agents/openai/_openai_assistant_agent.py b/python/packages/autogen-ext/src/autogen_ext/agents/openai/_openai_assistant_agent.py index 38b27f248d32..d0db7164cd78 100644 --- a/python/packages/autogen-ext/src/autogen_ext/agents/openai/_openai_assistant_agent.py +++ b/python/packages/autogen-ext/src/autogen_ext/agents/openai/_openai_assistant_agent.py @@ -467,7 +467,11 @@ async def on_messages_stream( is_error = True tool_outputs.append( FunctionExecutionResult( - content=result, call_id=tool_call.id, is_error=is_error, name=tool_call.name + content=result, + call_id=tool_call.id, + uuid=tool_call.uuid, + is_error=is_error, + name=tool_call.name, ) )