Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1366,6 +1366,16 @@ def _parse_contents_from_anthropic(
annotations=self._parse_citations_from_anthropic(content_block),
)
)
case "citations_delta":
# Streaming delivers citations as separate deltas on the text block;
# surface them as an empty text carrying the annotation so they merge into it.
contents.append(
Content.from_text(
text="",
raw_representation=content_block,
annotations=self._parse_citations_from_anthropic(content_block),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zakaria Khchiche (@Zakariakhchiche), streaming a citation from a plain-text document now aborts here. BetaCitationCharLocation exposes document_title, not title, but _parse_citations_from_anthropic() reads citation.title in its char_location branch. This new dispatch therefore turns a valid document citation delta into an AttributeError, terminating get_response(..., stream=True) as a ChatClientException instead of returning the response. Please map document_title and add a streaming regression using a real BetaCitationCharLocation; the new test covers only web-search citations, and the existing character-citation test supplies an invented title attribute through MagicMock.

)
)
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
Expand Down Expand Up @@ -1664,7 +1674,9 @@ def _parse_contents_from_anthropic(
def _parse_citations_from_anthropic(
self, content_block: BetaContentBlock | BetaRawContentBlockDelta | BetaTextBlock
) -> list[Annotation] | None:
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]
Comment on lines +1677 to +1679
if not content_blocks:
return None
annotations: list[Annotation] = []
Expand Down
54 changes: 54 additions & 0 deletions python/packages/anthropic/tests/test_anthropic_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,14 @@
)
from agent_framework.observability import ChatTelemetryLayer
from anthropic.types.beta import (
BetaCitationsDelta,
BetaCitationsWebSearchResultLocation,
BetaMessage,
BetaMessageDeltaUsage,
BetaRawContentBlockDeltaEvent,
BetaRawContentBlockStartEvent,
BetaTextBlock,
BetaTextDelta,
BetaToolUseBlock,
BetaUsage,
)
Expand Down Expand Up @@ -4194,6 +4199,55 @@ def test_parse_citations_search_result_location(
assert len(result) > 0


async def test_streaming_citations_delta_surfaces_annotations(mock_anthropic_client: MagicMock) -> None:
"""Citations streamed as citations_delta events must end up as annotations, like non-streaming."""
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)
)
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 len(annotations) == 1
assert annotations[0]["url"] == "https://example.com/paris"
assert annotations[0]["title"] == "Paris weather"
assert annotations[0]["snippet"] == "Paris is sunny today."


@pytest.mark.flaky
@pytest.mark.integration
@skip_if_anthropic_integration_tests_disabled
Expand Down
Loading