From 4dfae0d323dc6dbc19b3f81eb78d147e82223e8c Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 2 Apr 2026 20:25:42 +0000 Subject: [PATCH 1/2] Fix url_citation annotations dropped in streaming (#5029) 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> --- .../agent_framework_openai/_chat_client.py | 22 +++++++ .../tests/openai/test_openai_chat_client.py | 60 ++++++++++++++++++- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/python/packages/openai/agent_framework_openai/_chat_client.py b/python/packages/openai/agent_framework_openai/_chat_client.py index 963888e45a..cad472425c 100644 --- a/python/packages/openai/agent_framework_openai/_chat_client.py +++ b/python/packages/openai/agent_framework_openai/_chat_client.py @@ -2415,6 +2415,28 @@ def _get_ann_value(key: str) -> Any: raw_representation=event, ) ) + elif ann_type == "url_citation": + ann_url = _get_ann_value("url") + if ann_url: + ann_start = _get_ann_value("start_index") + ann_end = _get_ann_value("end_index") + annotation_obj = Annotation( + type="citation", + title=_get_ann_value("title") or "", + url=str(ann_url), + raw_representation=event, + ) + if ann_start is not None and ann_end is not None: + annotation_obj["annotated_regions"] = [ + TextSpanRegion( + type="text_span", + start_index=ann_start, + end_index=ann_end, + ) + ] + contents.append( + Content.from_text(text="", annotations=[annotation_obj], raw_representation=event) + ) else: logger.debug("Unparsed annotation type in streaming: %s", ann_type) case "response.output_item.done": diff --git a/python/packages/openai/tests/openai/test_openai_chat_client.py b/python/packages/openai/tests/openai/test_openai_chat_client.py index 8c91048284..e7c56c4218 100644 --- a/python/packages/openai/tests/openai/test_openai_chat_client.py +++ b/python/packages/openai/tests/openai/test_openai_chat_client.py @@ -2492,6 +2492,61 @@ def test_streaming_annotation_added_with_container_file_citation() -> None: assert content.additional_properties.get("end_index") == 50 +def test_streaming_annotation_added_with_url_citation() -> None: + """Test streaming annotation added event with url_citation type produces citation annotation.""" + client = OpenAIChatClient(model="test-model", api_key="test-key") + chat_options = ChatOptions() + function_call_ids: dict[int, tuple[str, str]] = {} + + mock_event = MagicMock() + mock_event.type = "response.output_text.annotation.added" + mock_event.annotation_index = 0 + mock_event.annotation = { + "type": "url_citation", + "url": "https://example.sharepoint.com/sites/my-site/doc.pdf", + "title": "doc.pdf", + "start_index": 100, + "end_index": 112, + } + + response = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids) + + assert len(response.contents) == 1 + content = response.contents[0] + assert content.type == "text" + assert content.annotations is not None + assert len(content.annotations) == 1 + annotation = content.annotations[0] + assert annotation["type"] == "citation" + assert annotation["title"] == "doc.pdf" + assert annotation["url"] == "https://example.sharepoint.com/sites/my-site/doc.pdf" + assert annotation["annotated_regions"] is not None + assert len(annotation["annotated_regions"]) == 1 + region = annotation["annotated_regions"][0] + assert region["type"] == "text_span" + assert region["start_index"] == 100 + assert region["end_index"] == 112 + + +def test_streaming_annotation_added_with_url_citation_no_url() -> None: + """Test streaming annotation added event with url_citation but missing url is ignored.""" + client = OpenAIChatClient(model="test-model", api_key="test-key") + chat_options = ChatOptions() + function_call_ids: dict[int, tuple[str, str]] = {} + + mock_event = MagicMock() + mock_event.type = "response.output_text.annotation.added" + mock_event.annotation_index = 0 + mock_event.annotation = { + "type": "url_citation", + "title": "doc.pdf", + } + + response = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids) + + assert len(response.contents) == 0 + + def test_streaming_annotation_added_with_unknown_type() -> None: """Test streaming annotation added event with unknown type is ignored.""" client = OpenAIChatClient(model="test-model", api_key="test-key") @@ -2502,13 +2557,12 @@ def test_streaming_annotation_added_with_unknown_type() -> None: mock_event.type = "response.output_text.annotation.added" mock_event.annotation_index = 0 mock_event.annotation = { - "type": "url_citation", - "url": "https://example.com", + "type": "some_future_annotation_type", + "data": "test", } response = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids) - # url_citation should not produce HostedFileContent assert len(response.contents) == 0 From 5dff57d4dc44cc974282b4dae8caab3e042c3be8 Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 2 Apr 2026 20:38:20 +0000 Subject: [PATCH 2/2] Address review feedback for #5029: Python: [Bug]: url_citation annotations silently dropped in Foundry streaming (SharePoint grounding citations lost) --- .../agent_framework_openai/_chat_client.py | 3 +- .../tests/openai/test_openai_chat_client.py | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/python/packages/openai/agent_framework_openai/_chat_client.py b/python/packages/openai/agent_framework_openai/_chat_client.py index cad472425c..a453308cb4 100644 --- a/python/packages/openai/agent_framework_openai/_chat_client.py +++ b/python/packages/openai/agent_framework_openai/_chat_client.py @@ -2424,7 +2424,8 @@ def _get_ann_value(key: str) -> Any: type="citation", title=_get_ann_value("title") or "", url=str(ann_url), - raw_representation=event, + additional_properties={"annotation_index": event.annotation_index}, + raw_representation=annotation, ) if ann_start is not None and ann_end is not None: annotation_obj["annotated_regions"] = [ diff --git a/python/packages/openai/tests/openai/test_openai_chat_client.py b/python/packages/openai/tests/openai/test_openai_chat_client.py index e7c56c4218..b9281bd214 100644 --- a/python/packages/openai/tests/openai/test_openai_chat_client.py +++ b/python/packages/openai/tests/openai/test_openai_chat_client.py @@ -2520,6 +2520,8 @@ def test_streaming_annotation_added_with_url_citation() -> None: assert annotation["type"] == "citation" assert annotation["title"] == "doc.pdf" assert annotation["url"] == "https://example.sharepoint.com/sites/my-site/doc.pdf" + assert annotation["additional_properties"]["annotation_index"] == 0 + assert annotation["raw_representation"] == mock_event.annotation assert annotation["annotated_regions"] is not None assert len(annotation["annotated_regions"]) == 1 region = annotation["annotated_regions"][0] @@ -2547,6 +2549,32 @@ def test_streaming_annotation_added_with_url_citation_no_url() -> None: assert len(response.contents) == 0 +def test_streaming_annotation_added_with_url_citation_no_indices() -> None: + """Test streaming annotation with url_citation that has url but no start_index/end_index.""" + client = OpenAIChatClient(model="test-model", api_key="test-key") + chat_options = ChatOptions() + function_call_ids: dict[int, tuple[str, str]] = {} + + mock_event = MagicMock() + mock_event.type = "response.output_text.annotation.added" + mock_event.annotation_index = 0 + mock_event.annotation = { + "type": "url_citation", + "url": "https://example.com", + "title": "Example", + } + + response = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids) + + assert len(response.contents) == 1 + annotation = response.contents[0].annotations[0] + assert annotation["type"] == "citation" + assert annotation["title"] == "Example" + assert annotation["url"] == "https://example.com" + assert annotation["additional_properties"]["annotation_index"] == 0 + assert "annotated_regions" not in annotation + + def test_streaming_annotation_added_with_unknown_type() -> None: """Test streaming annotation added event with unknown type is ignored.""" client = OpenAIChatClient(model="test-model", api_key="test-key")