Python: Handle url_citation annotations in FoundryChatClient streaming responses#5071
Python: Handle url_citation annotations in FoundryChatClient streaming responses#5071giles17 merged 5 commits intomicrosoft:mainfrom
FoundryChatClient streaming responses#5071Conversation
Add url_citation branch to the streaming annotation handler in _parse_chunk_from_openai, mirroring the existing non-streaming path. The handler creates an Annotation with type='citation', title, url, and annotated_regions (TextSpanRegion), wrapped in Content.from_text. Update test_streaming_annotation_added_with_unknown_type to use a truly unknown type, and add new tests for url_citation (with and without url). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
giles17
left a comment
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 92%
✓ Correctness
The fix correctly adds
url_citationhandling to the streaming annotation path, mirroring the non-streaming path at line 1615. The implementation properly usesContent.from_textwith anAnnotation(instead ofContent.from_hosted_file), defensively guards against missing URL, and conditionally buildsannotated_regions. Tests are well-structured covering happy path, missing-URL edge case, and a properly updated unknown-type test. One minor inconsistency: the other three streaming annotation branches (file_path,file_citation,container_file_citation) all storeannotation_indexinadditional_properties, but the newurl_citationbranch omits it. This is not a bug (the non-streaming path also omits it for url_citation), but it breaks the pattern established by the other streaming branches and could surprise consumers that rely onannotation_indexfor ordering.
✓ Security Reliability
The change adds url_citation handling in the streaming annotation path, mirroring the existing non-streaming path. The implementation is sound from a security/reliability perspective: the URL is treated as opaque data (stored, not dereferenced), the required
urlfield is guarded with a truthiness check before processing (consistent with how other annotation types guard onann_file_id), andstr(ann_url)ensures type safety. The_get_ann_valuehelper already handles both dict and object annotation formats safely. Test coverage is good, including the missing-URL edge case and the updated unknown-type test.
✗ Test Coverage
The test coverage for the new url_citation streaming branch is good overall: there's a happy-path test with full assertions on type, title, url, and annotated_regions, a test for the missing-url guard, and the unknown-type test was properly updated. However, there is one meaningful uncovered branch: the code conditionally omits
annotated_regionswhenstart_indexorend_indexisNone(line 2429), but no test exercises that path. A test for url_citation with a URL but no start/end indices would close this gap.
✗ Design Approach
The fix correctly adds
url_citationhandling to the streaming annotation path, mirroring the non-streaming path's logic. The approach is sound and the two new tests are well-structured. One design inconsistency stands out: every other streaming annotation type (file_path,file_citation,container_file_citation) storesannotation_indexfrom the event inadditional_properties, but the newurl_citationbranch omits it entirely. This breaks parity with the established streaming-path pattern and means consumers who rely onannotation_indexto correlate annotations with their position in the streamed text will not receive it forurl_citationannotations — even though the data is available on the sameeventobject.
Flagged Issues
- The
url_citationstreaming handler omits"annotation_index": event.annotation_indexinadditional_properties, unlike every other annotation type in the same streaming handler (file_path,file_citation,container_file_citation). This breaks parity with the established pattern and means consumers relying onannotation_indexto order or correlate streaming annotations will silently get no value forurl_citationannotations. - Missing test for the
url_citationbranch whereurlis present butstart_index/end_indexare absent. The conditional on line 2429 (if ann_start is not None and ann_end is not None) is never exercised by the current tests, leaving the no-region annotation path uncovered.
Suggestions
- The non-streaming path (lines 1615–1630) unconditionally accesses
annotation.urlandannotation.start_index/annotation.end_indexwithout None guards, while the streaming path defensively checks them. Consider adding similar guards to the non-streaming path for consistency, though that is outside this diff's scope. - Consider adding a test where
titleisNoneto verify theor "fallback on line 2425 produces an empty-string title. - Once
annotation_indexis added to the implementation, the testtest_streaming_annotation_added_with_url_citationshould assert thatannotation_indexis present inadditional_propertiesto lock in the behaviour.
Automated review by giles17's agents
Python Test Coverage Report •
Python Unit Test Overview
|
||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Pull request overview
Fixes streaming support for OpenAI Responses API response.output_text.annotation.added events so url_citation annotations (e.g., SharePoint grounding citations) are surfaced to consumers instead of being silently dropped.
Changes:
- Add a
url_citationbranch in the streaming annotation handler to emit atextContent containing acitationAnnotation (with URL + optionalTextSpanRegion). - Add unit tests covering the
url_citationhappy path and the missing-URL guard, and update the “unknown type” test to use a truly unknown annotation type.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
python/packages/openai/agent_framework_openai/_chat_client.py |
Parses streaming url_citation annotations into citation annotations on text content updates. |
python/packages/openai/tests/openai/test_openai_chat_client.py |
Adds/adjusts tests to validate streaming url_citation handling and unknown-type behavior. |
…on annotations silently dropped in Foundry streaming (SharePoint grounding citations lost)
giles17
left a comment
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 92%
✓ Correctness
The diff correctly adds
additional_propertiesand changesraw_representationfromeventtoannotation(i.e.,event.annotation) in the streamingurl_citationhandler. This aligns with the non-streaming path (line 1628) which also usesraw_representation=annotation. The new tests properly cover the happy path with indices, the no-URL edge case, the no-indices edge case, and the unknown-type fallback. No correctness issues found.
✓ Security Reliability
The diff correctly fixes the streaming
url_citationhandler to storeraw_representation=annotation(matching the non-streaming path at line 1628) instead ofraw_representation=event, and addsadditional_propertieswith theannotation_index. The change is consistent with the existing non-streaming code path. Tests cover the happy path (with indices), missing-url edge case, missing-indices edge case, and unknown-type fallback. No security or reliability concerns found.
✓ Test Coverage
The diff adds two new test assertions to the existing
test_streaming_annotation_added_with_url_citationtest (checkingadditional_propertiesandraw_representation) and introduces a newtest_streaming_annotation_added_with_url_citation_no_indicestest covering the edge case wherestart_index/end_indexare absent. The test coverage is thorough: the happy path with indices, missing URL (ignored), missing indices (noannotated_regions), and unknown type (ignored) are all covered. The new assertions are meaningful and verify the behavioral change in the production code (raw_representationnow stores the annotation dict instead of the event, andadditional_propertiesincludesannotation_index). Theno_indicestest correctly verifies thatannotated_regionsis absent rather than empty, matching the conditional logic in production code. No issues found.
✓ Design Approach
The diff is a small, focused fix: it corrects
raw_representationon theAnnotationobject fromevent(the streaming event) toannotation(i.e.,event.annotation, the annotation payload), making the streaming path consistent with the non-streaming path at line 1628. It also addsadditional_properties={'annotation_index': event.annotation_index}for streaming-specific metadata, and adds a well-targeted test for the no-indices edge case. The design approach is correct and consistent with the existing codebase conventions. No blocking issues were found.
Suggestions
- Consider adding a test assertion for
raw_representationon theContentobject itself (i.e.,response.contents[0].raw_representation == event) intest_streaming_annotation_added_with_url_citation, sinceContent.from_textpassesraw_representation=eventwhile theAnnotationnow getsraw_representation=annotation. This would verify both levels are set correctly. - For consistency with the non-streaming path (line 1628), consider whether the
annotated_regionsconditional guard (if ann_start is not None and ann_end is not None) is truly needed — the non-streaming path accesses those fields unconditionally, suggesting they are always present forurl_citation. Keeping the guard is harmless but may silently produce an incompleteAnnotationif the API omits them unexpectedly.
Automated review by giles17's agents
FoundryChatClient streaming responses
Motivation and Context
When using
FoundryChatClientwith SharePoint grounding,url_citationannotations present in the streamed response were silently dropped — inline citation markers appeared in the text but no URL metadata was surfaced to consumers, making citations unusable.Fixes #5029
Description
The streaming handler for
response.output_text.annotation.addedevents handledfile_path,file_citation, andcontainer_file_citationbut had no branch forurl_citation, causing it to fall through to a debug log with no content emitted. The fix adds aurl_citationcase that constructs anAnnotationwithtype="citation",title,url, andTextSpanRegion-basedannotated_regions, mirroring the logic already present in the non-streaming completed-response path. The existing test for unknown annotation types was updated to use a genuinely unknown type instead ofurl_citation, and new tests verify both the happy path and the missing-URL guard.Contribution Checklist