diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fc0d7ff..d7b1288 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.45.0" + ".": "0.45.1" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index e7d0669..64f8716 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/anthropic-7270ee0a79d885681ee507414608229f61c27f47c40f355dcd210b38aa7cddf1.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/anthropic-f5276eeef7512112e802c85530c51e0a971ee521eebe3a0db309621587b4973d.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dfc5d7..f0647d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 0.45.1 (2025-01-27) + +Full Changelog: [v0.45.0...v0.45.1](https://github.com/anthropics/anthropic-sdk-python/compare/v0.45.0...v0.45.1) + +### Bug Fixes + +* **streaming:** accumulate citations ([#844](https://github.com/anthropics/anthropic-sdk-python/issues/844)) ([e665f2f](https://github.com/anthropics/anthropic-sdk-python/commit/e665f2fefd4573fc45cd4c546a9480f15d18d1cd)) + + +### Chores + +* **docs:** updates ([#841](https://github.com/anthropics/anthropic-sdk-python/issues/841)) ([fb10a7d](https://github.com/anthropics/anthropic-sdk-python/commit/fb10a7d658044062e5023cd8495c80d3344af8df)) + ## 0.45.0 (2025-01-23) Full Changelog: [v0.44.0...v0.45.0](https://github.com/anthropics/anthropic-sdk-python/compare/v0.44.0...v0.45.0) diff --git a/pyproject.toml b/pyproject.toml index 2fdb785..1187b25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "anthropic" -version = "0.45.0" +version = "0.45.1" description = "The official Python library for the anthropic API" dynamic = ["readme"] license = "MIT" diff --git a/src/anthropic/_version.py b/src/anthropic/_version.py index 29afc8b..42d8e6a 100644 --- a/src/anthropic/_version.py +++ b/src/anthropic/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "anthropic" -__version__ = "0.45.0" # x-release-please-version +__version__ = "0.45.1" # x-release-please-version diff --git a/src/anthropic/lib/streaming/_beta_messages.py b/src/anthropic/lib/streaming/_beta_messages.py index 7e6a774..b6241d2 100644 --- a/src/anthropic/lib/streaming/_beta_messages.py +++ b/src/anthropic/lib/streaming/_beta_messages.py @@ -5,11 +5,13 @@ from typing_extensions import Self, Iterator, Awaitable, AsyncIterator, assert_never import httpx +from pydantic import BaseModel from ..._utils import consume_sync_iterator, consume_async_iterator from ..._models import build, construct_type from ._beta_types import ( BetaTextEvent, + BetaCitationEvent, BetaInputJsonEvent, BetaMessageStopEvent, BetaMessageStreamEvent, @@ -314,24 +316,40 @@ def build_events( events_to_fire.append(event) content_block = message_snapshot.content[event.index] - if event.delta.type == "text_delta" and content_block.type == "text": - events_to_fire.append( - build( - BetaTextEvent, - type="text", - text=event.delta.text, - snapshot=content_block.text, + if event.delta.type == "text_delta": + if content_block.type == "text": + events_to_fire.append( + build( + BetaTextEvent, + type="text", + text=event.delta.text, + snapshot=content_block.text, + ) ) - ) - elif event.delta.type == "input_json_delta" and content_block.type == "tool_use": - events_to_fire.append( - build( - BetaInputJsonEvent, - type="input_json", - partial_json=event.delta.partial_json, - snapshot=content_block.input, + elif event.delta.type == "input_json_delta": + if content_block.type == "tool_use": + events_to_fire.append( + build( + BetaInputJsonEvent, + type="input_json", + partial_json=event.delta.partial_json, + snapshot=content_block.input, + ) ) - ) + elif event.delta.type == "citations_delta": + if content_block.type == "text": + events_to_fire.append( + build( + BetaCitationEvent, + type="citation", + citation=event.delta.citation, + snapshot=content_block.citations or [], + ) + ) + else: + # we only want exhaustive checking for linters, not at runtime + if TYPE_CHECKING: # type: ignore[unreachable] + assert_never(event.delta) elif event.type == "content_block_stop": content_block = message_snapshot.content[event.index] @@ -354,6 +372,9 @@ def accumulate_event( event: BetaRawMessageStreamEvent, current_snapshot: BetaMessage | None, ) -> BetaMessage: + if not isinstance(event, BaseModel): # pyright: ignore[reportUnnecessaryIsInstance] + raise TypeError(f"Unexpected event runtime type - {event}") + if current_snapshot is None: if event.type == "message_start": return BetaMessage.construct(**cast(Any, event.message.to_dict())) @@ -370,21 +391,33 @@ def accumulate_event( ) elif event.type == "content_block_delta": content = current_snapshot.content[event.index] - if content.type == "text" and event.delta.type == "text_delta": - content.text += event.delta.text - elif content.type == "tool_use" and event.delta.type == "input_json_delta": - from jiter import from_json - - # we need to keep track of the raw JSON string as well so that we can - # re-parse it for each delta, for now we just store it as an untyped - # property on the snapshot - json_buf = cast(bytes, getattr(content, JSON_BUF_PROPERTY, b"")) - json_buf += bytes(event.delta.partial_json, "utf-8") - - if json_buf: - content.input = from_json(json_buf, partial_mode=True) - - setattr(content, JSON_BUF_PROPERTY, json_buf) + if event.delta.type == "text_delta": + if content.type == "text": + content.text += event.delta.text + elif event.delta.type == "input_json_delta": + if content.type == "tool_use": + from jiter import from_json + + # we need to keep track of the raw JSON string as well so that we can + # re-parse it for each delta, for now we just store it as an untyped + # property on the snapshot + json_buf = cast(bytes, getattr(content, JSON_BUF_PROPERTY, b"")) + json_buf += bytes(event.delta.partial_json, "utf-8") + + if json_buf: + content.input = from_json(json_buf, partial_mode=True) + + setattr(content, JSON_BUF_PROPERTY, json_buf) + elif event.delta.type == "citations_delta": + if content.type == "text": + if not content.citations: + content.citations = [event.delta.citation] + else: + content.citations.append(event.delta.citation) + else: + # we only want exhaustive checking for linters, not at runtime + if TYPE_CHECKING: # type: ignore[unreachable] + assert_never(event.delta) elif event.type == "message_delta": current_snapshot.stop_reason = event.delta.stop_reason current_snapshot.stop_sequence = event.delta.stop_sequence diff --git a/src/anthropic/lib/streaming/_beta_types.py b/src/anthropic/lib/streaming/_beta_types.py index c3ee61f..4ef7e13 100644 --- a/src/anthropic/lib/streaming/_beta_types.py +++ b/src/anthropic/lib/streaming/_beta_types.py @@ -1,5 +1,5 @@ from typing import Union -from typing_extensions import Literal, Annotated +from typing_extensions import List, Literal, Annotated from ..._models import BaseModel from ...types.beta import ( @@ -13,6 +13,7 @@ BetaRawContentBlockStartEvent, ) from ..._utils._transform import PropertyInfo +from ...types.beta.beta_citations_delta import Citation class BetaTextEvent(BaseModel): @@ -25,6 +26,16 @@ class BetaTextEvent(BaseModel): """The entire accumulated text""" +class BetaCitationEvent(BaseModel): + type: Literal["citation"] + + citation: Citation + """The new citation""" + + snapshot: List[Citation] + """All of the accumulated citations""" + + class BetaInputJsonEvent(BaseModel): type: Literal["input_json"] @@ -57,6 +68,7 @@ class BetaContentBlockStopEvent(BetaRawContentBlockStopEvent): BetaMessageStreamEvent = Annotated[ Union[ BetaTextEvent, + BetaCitationEvent, BetaInputJsonEvent, BetaRawMessageStartEvent, BetaRawMessageDeltaEvent, diff --git a/src/anthropic/lib/streaming/_messages.py b/src/anthropic/lib/streaming/_messages.py index ece0a16..146a1ba 100644 --- a/src/anthropic/lib/streaming/_messages.py +++ b/src/anthropic/lib/streaming/_messages.py @@ -9,6 +9,7 @@ from ._types import ( TextEvent, + CitationEvent, InputJsonEvent, MessageStopEvent, MessageStreamEvent, @@ -315,24 +316,40 @@ def build_events( events_to_fire.append(event) content_block = message_snapshot.content[event.index] - if event.delta.type == "text_delta" and content_block.type == "text": - events_to_fire.append( - build( - TextEvent, - type="text", - text=event.delta.text, - snapshot=content_block.text, + if event.delta.type == "text_delta": + if content_block.type == "text": + events_to_fire.append( + build( + TextEvent, + type="text", + text=event.delta.text, + snapshot=content_block.text, + ) ) - ) - elif event.delta.type == "input_json_delta" and content_block.type == "tool_use": - events_to_fire.append( - build( - InputJsonEvent, - type="input_json", - partial_json=event.delta.partial_json, - snapshot=content_block.input, + elif event.delta.type == "input_json_delta": + if content_block.type == "tool_use": + events_to_fire.append( + build( + InputJsonEvent, + type="input_json", + partial_json=event.delta.partial_json, + snapshot=content_block.input, + ) ) - ) + elif event.delta.type == "citations_delta": + if content_block.type == "text": + events_to_fire.append( + build( + CitationEvent, + type="citation", + citation=event.delta.citation, + snapshot=content_block.citations or [], + ) + ) + else: + # we only want exhaustive checking for linters, not at runtime + if TYPE_CHECKING: # type: ignore[unreachable] + assert_never(event.delta) elif event.type == "content_block_stop": content_block = message_snapshot.content[event.index] @@ -374,21 +391,33 @@ def accumulate_event( ) elif event.type == "content_block_delta": content = current_snapshot.content[event.index] - if content.type == "text" and event.delta.type == "text_delta": - content.text += event.delta.text - elif content.type == "tool_use" and event.delta.type == "input_json_delta": - from jiter import from_json - - # we need to keep track of the raw JSON string as well so that we can - # re-parse it for each delta, for now we just store it as an untyped - # property on the snapshot - json_buf = cast(bytes, getattr(content, JSON_BUF_PROPERTY, b"")) - json_buf += bytes(event.delta.partial_json, "utf-8") - - if json_buf: - content.input = from_json(json_buf, partial_mode=True) - - setattr(content, JSON_BUF_PROPERTY, json_buf) + if event.delta.type == "text_delta": + if content.type == "text": + content.text += event.delta.text + elif event.delta.type == "input_json_delta": + if content.type == "tool_use": + from jiter import from_json + + # we need to keep track of the raw JSON string as well so that we can + # re-parse it for each delta, for now we just store it as an untyped + # property on the snapshot + json_buf = cast(bytes, getattr(content, JSON_BUF_PROPERTY, b"")) + json_buf += bytes(event.delta.partial_json, "utf-8") + + if json_buf: + content.input = from_json(json_buf, partial_mode=True) + + setattr(content, JSON_BUF_PROPERTY, json_buf) + elif event.delta.type == "citations_delta": + if content.type == "text": + if not content.citations: + content.citations = [event.delta.citation] + else: + content.citations.append(event.delta.citation) + else: + # we only want exhaustive checking for linters, not at runtime + if TYPE_CHECKING: # type: ignore[unreachable] + assert_never(event.delta) elif event.type == "message_delta": current_snapshot.stop_reason = event.delta.stop_reason current_snapshot.stop_sequence = event.delta.stop_sequence diff --git a/src/anthropic/lib/streaming/_types.py b/src/anthropic/lib/streaming/_types.py index 59ee779..40af5ee 100644 --- a/src/anthropic/lib/streaming/_types.py +++ b/src/anthropic/lib/streaming/_types.py @@ -1,5 +1,5 @@ from typing import Union -from typing_extensions import Literal, Annotated +from typing_extensions import List, Literal, Annotated from ...types import ( Message, @@ -13,6 +13,7 @@ ) from ..._models import BaseModel from ..._utils._transform import PropertyInfo +from ...types.citations_delta import Citation class TextEvent(BaseModel): @@ -25,6 +26,16 @@ class TextEvent(BaseModel): """The entire accumulated text""" +class CitationEvent(BaseModel): + type: Literal["citation"] + + citation: Citation + """The new citation""" + + snapshot: List[Citation] + """All of the accumulated citations""" + + class InputJsonEvent(BaseModel): type: Literal["input_json"] @@ -57,6 +68,7 @@ class ContentBlockStopEvent(RawContentBlockStopEvent): MessageStreamEvent = Annotated[ Union[ TextEvent, + CitationEvent, InputJsonEvent, RawMessageStartEvent, RawMessageDeltaEvent,