diff --git a/python/packages/devui/agent_framework_devui/_mapper.py b/python/packages/devui/agent_framework_devui/_mapper.py index 5c0dc19cdd..929f47e349 100644 --- a/python/packages/devui/agent_framework_devui/_mapper.py +++ b/python/packages/devui/agent_framework_devui/_mapper.py @@ -1340,9 +1340,9 @@ async def _map_function_call_content( """ events: list[ResponseFunctionCallArgumentsDeltaEvent | ResponseOutputItemAddedEvent] = [] - # CASE 1: New function call (has call_id and name) - # This is the first event that establishes the function call - if content.call_id and content.name: + # CASE 1: New function call (has a call_id and name not seen in an earlier chunk) + # Streaming providers may repeat call metadata with every argument chunk. + if content.call_id and content.name and content.call_id not in context["active_function_calls"]: # Use call_id as item_id (simpler, and call_id uniquely identifies the call) item_id = content.call_id diff --git a/python/packages/devui/tests/devui/test_mapper.py b/python/packages/devui/tests/devui/test_mapper.py index c6ec065253..b37647b05d 100644 --- a/python/packages/devui/tests/devui/test_mapper.py +++ b/python/packages/devui/tests/devui/test_mapper.py @@ -139,6 +139,36 @@ async def test_function_call_mapping(mapper: MessageMapper, test_request: AgentF assert "TestCity" in full_json +async def test_streaming_function_call_mapping_adds_item_once( + mapper: MessageMapper, test_request: AgentFrameworkRequest +) -> None: + """Test repeated function metadata does not add duplicate output items.""" + first_update = create_test_agent_update([ + Content.from_function_call( + call_id="call_123", + name="get_weather", + arguments='{"location":', + ) + ]) + second_update = create_test_agent_update([ + Content.from_function_call( + call_id="call_123", + name="get_weather", + arguments='"Seattle"}', + ) + ]) + + events = [ + *await mapper.convert_event(first_update, test_request), + *await mapper.convert_event(second_update, test_request), + ] + + added_events = [event for event in events if event.type == "response.output_item.added"] + assert len(added_events) == 1 + delta_events = [event for event in events if event.type == "response.function_call_arguments.delta"] + assert "".join(event.delta for event in delta_events) == '{"location":"Seattle"}' + + async def test_function_result_content_with_string_result( mapper: MessageMapper, test_request: AgentFrameworkRequest ) -> None: