Python: Fix duplicate arguments in declaration-only tool streaming#7110
Python: Fix duplicate arguments in declaration-only tool streaming#7110kartikmadan11 wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes a Python streaming bug in the core function-invocation layer where declaration-only tool calls could end up with duplicated arguments in the final aggregated response due to the same function_call content being yielded twice during streaming.
Changes:
- Filters
function_callitems out of the post-processing streamedfunction_call_resultsupdate to prevent_process_updatefrom concatenating the same arguments twice. - Adds regression tests for both multi-chunk and single-chunk streaming declaration-only tool calls.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| python/packages/core/agent_framework/_tools.py | Adjusts streaming post-processing to avoid re-emitting function_call items that were already emitted by the inner stream. |
| python/packages/core/tests/core/test_function_invocation_logic.py | Adds tests ensuring declaration-only tool streaming does not duplicate function-call arguments. |
026ca32 to
7def61f
Compare
|
Great catch, Copilot! I've addressed the concern about empty updates. The fix: Implementation:
This way declaration-only tools in streaming mode won't emit spurious empty updates—only the already-streamed chunks and any results/approvals will be emitted. |
7def61f to
c06f65d
Compare
c06f65d to
be7054d
Compare
…lls (microsoft#6973) When streaming a declaration-only function call, the _stream() loop yields function_call chunks from the inner stream, then _process_function_requests re-emits the finalized call from function_call_results. _process_update merges same-call_id function_call contents via +=, so re-emitting the full arguments concatenated them twice: {"location":"Seattle"}{"location":"Seattle"}. The finalized call gains its control metadata (user_input_request=True, id) only during post-processing, so it must still reach the stream: AgentExecutor relies on that signal to emit request_info and pause for client-side tools. Fix: strip only the already-streamed arguments from function_call items in the streamed update while preserving their metadata, instead of dropping the items entirely. Also preserve id and user_input_request when merging function_call contents in Content.__add__ so the aggregated response retains the signal. Tests: split-chunk and single-chunk no-duplication, plus metadata preservation. Fixes microsoft#6973
be7054d to
10aa390
Compare
| ] | ||
|
|
||
| updates = [] | ||
| async for update in chat_client_base.get_response( # type: ignore[call-overload] # pyrefly: ignore[no-matching-overload] # ty: ignore[no-matching-overload] |
There was a problem hiding this comment.
we should use the get_final_response method on the stream object, to test this with
There was a problem hiding this comment.
Done, switched the test to use stream.get_final_response() and assert on the aggregated response. Thanks!
|
@kartikmadan11 there are both open comments and failed checks, please have a look! |
Rewrite the multi-chunk declaration-only streaming test to use stream.get_final_response() instead of manually re-summing arguments across streamed updates, per review feedback. Asserting against the aggregated response mirrors the adjacent single-chunk test and matches how consumers actually observe the result. Also fixes the Test Typing Checks failure: the removed `all_args += content.arguments or ""` concatenated a str with `str | Mapping`, which zuban rejected.
|
Both handled. Fixed the test per the review comment, which also clears the typing check failure. Rebased onto latest main so the merge gate should clear too. |
Motivation & Context
When streaming declaration-only function calls, the final response contains duplicated arguments.
Fixes #6973
Description & Review Guide
What are the major changes?
_process_function_requests()in_tools.py(line 2403) to filter outfunction_calltype items from the streamed result updateWhat is the impact?
Root Cause: The
_stream()loop yieldsfunction_callchunks as they arrive, then later re-yields those same items fromfunction_call_results. The_process_update()callback concatenates arguments with+=, so duplicate yields cause duplication.Solution: Filter out
function_calltype items from the result'sfunction_call_resultsbefore yielding, since they were already emitted by the inner stream.Tests Added
test_declaration_only_tool_streaming_no_argument_duplication()- Verifies multi-chunk streamingtest_declaration_only_tool_streaming_single_chunk_no_duplication()- Verifies single-chunk streamingBoth tests pass ✅