From ebae33096c32f5b351da1ac54609f38bbd6b14b5 Mon Sep 17 00:00:00 2001 From: Zakaria Khchiche <45357915+Zakariakhchiche@users.noreply.github.com> Date: Fri, 30 Oct 2026 19:22:49 +0100 Subject: [PATCH 1/2] Python: surface Anthropic citations_delta annotations and read document_title for char_location --- .../agent_framework_anthropic/_chat_client.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py index 8b6f49e821..d722e1b993 100644 --- a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py +++ b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py @@ -1646,6 +1646,17 @@ def _parse_contents_from_anthropic( annotations=self._parse_citations_from_anthropic(content_block), ) ) + case "citations_delta": + # Streaming delivers each citation as its own delta on the open text block; + # surface it as an empty appended text so the annotation merges into that text. + contents.append( + Content.from_text( + text="", + merge_mode="append" if streaming else None, + raw_representation=content_block, + annotations=self._parse_citations_from_anthropic(content_block), + ) + ) case "tool_use" | "mcp_tool_use" | "server_tool_use": request_state.active_call_id = content_block.id request_state.active_call_content_type = content_block.type @@ -1977,7 +1988,9 @@ def _parse_citations_from_anthropic( Their ``annotated_regions_coordinate_space`` is ``"source"`` to distinguish these locations from generated-content character offsets. Protocol adapters do not emit these regions as output-text spans. """ - content_blocks = getattr(content_block, "citations", None) + content_blocks: Any = getattr(content_block, "citations", None) + if content_block.type == "citations_delta": + content_blocks = [content_block.citation] if not content_blocks: return None annotations: list[Annotation] = [] @@ -1985,7 +1998,7 @@ def _parse_citations_from_anthropic( cit = Annotation(type="citation", raw_representation=citation) match citation.type: case "char_location": - cit["title"] = citation.title + cit["title"] = citation.document_title cit["snippet"] = citation.cited_text if citation.file_id: cit["file_id"] = citation.file_id From 266fe5a42dbf44362fc42ad3a89d4147526c5789 Mon Sep 17 00:00:00 2001 From: Zakaria Khchiche <45357915+Zakariakhchiche@users.noreply.github.com> Date: Fri, 30 Oct 2026 19:23:54 +0100 Subject: [PATCH 2/2] Python: add tests for Anthropic citations_delta and char_location document_title --- .../anthropic/tests/test_anthropic_client.py | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/python/packages/anthropic/tests/test_anthropic_client.py b/python/packages/anthropic/tests/test_anthropic_client.py index 2a864e32ef..9436f3d5bb 100644 --- a/python/packages/anthropic/tests/test_anthropic_client.py +++ b/python/packages/anthropic/tests/test_anthropic_client.py @@ -39,11 +39,17 @@ ) from agent_framework.observability import ChatTelemetryLayer from anthropic.types.beta import ( + BetaCitationCharLocation, + BetaCitationsDelta, + BetaCitationsWebSearchResultLocation, BetaMessage, BetaMessageDeltaUsage, + BetaRawContentBlockDeltaEvent, + BetaRawContentBlockStartEvent, BetaRedactedThinkingBlock, BetaServerToolUseBlock, BetaTextBlock, + BetaTextDelta, BetaToolUseBlock, BetaUsage, BetaWebSearchResultBlock, @@ -5372,6 +5378,89 @@ def test_parse_citations_search_result_location( assert len(result) > 0 +def test_parse_citations_char_location_uses_document_title(mock_anthropic_client: MagicMock) -> None: + """A real char_location citation exposes ``document_title``; parsing must not read a missing ``title``.""" + client = create_test_anthropic_client(mock_anthropic_client) + citation = BetaCitationCharLocation( + type="char_location", + cited_text="The sky is blue.", + document_index=0, + document_title="Facts", + start_char_index=0, + end_char_index=16, + file_id=None, + ) + block = BetaTextBlock(type="text", text="The sky is blue.", citations=[citation]) + + result = client._parse_citations_from_anthropic(block) + + assert result is not None + assert len(result) == 1 + assert result[0]["title"] == "Facts" + assert result[0]["snippet"] == "The sky is blue." + assert result[0]["annotated_regions"] == [{"type": "text_span", "start_index": 0, "end_index": 16}] + + +async def test_streaming_citations_delta_surfaces_annotations(mock_anthropic_client: MagicMock) -> None: + """Citations streamed as citations_delta events end up as annotations on the text, like non-streaming.""" + char_citation = BetaCitationCharLocation( + type="char_location", + cited_text="Paris is sunny today.", + document_index=0, + document_title="Weather report", + start_char_index=0, + end_char_index=21, + file_id=None, + ) + web_citation = BetaCitationsWebSearchResultLocation( + type="web_search_result_location", + url="https://example.com/paris", + title="Paris weather", + cited_text="Paris is sunny today.", + encrypted_index="enc-1", + ) + + async def events() -> Any: + yield _message_start_event("msg_cited") + yield BetaRawContentBlockStartEvent( + type="content_block_start", index=0, content_block=BetaTextBlock(type="text", text="", citations=None) + ) + for citation in (char_citation, web_citation): + yield BetaRawContentBlockDeltaEvent( + type="content_block_delta", + index=0, + delta=BetaCitationsDelta(type="citations_delta", citation=citation), + ) + yield BetaRawContentBlockDeltaEvent( + type="content_block_delta", index=0, delta=BetaTextDelta(type="text_delta", text="It is sunny in Paris.") + ) + yield _content_block_stop_event() + yield _message_delta_event("end_turn") + yield _message_stop_event() + + async def create(**kwargs: Any) -> Any: + return events() + + client = create_test_anthropic_client(mock_anthropic_client) + mock_anthropic_client.beta.messages.create.side_effect = create + stream = client.get_response([Message(role="user", contents=["Weather in Paris?"])], stream=True) + async for _ in stream: + pass + response = await stream.get_final_response() + + assert response.text == "It is sunny in Paris." + annotations = [ + annotation + for message in response.messages + for content in message.contents + if content.type == "text" + for annotation in content.annotations or [] + ] + assert [annotation["title"] for annotation in annotations] == ["Weather report", "Paris weather"] + assert annotations[1]["url"] == "https://example.com/paris" + assert all(annotation["snippet"] == "Paris is sunny today." for annotation in annotations) + + @pytest.mark.parametrize( ("citation_type", "location_fields", "expected_span"), [