diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2aca35a..d04f223 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.5.0" + ".": "0.5.1" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 21fa5a3..5624148 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## 0.5.1 (2025-12-20) + +Full Changelog: [v0.5.0...v0.5.1](https://github.com/AgentbaseHQ/agentbase-python/compare/v0.5.0...v0.5.1) + +### Bug Fixes + +* compat with Python 3.14 ([506a48f](https://github.com/AgentbaseHQ/agentbase-python/commit/506a48fb364425c839e712fda2c6232a547c04a7)) +* **compat:** update signatures of `model_dump` and `model_dump_json` for Pydantic v1 ([26fcf8c](https://github.com/AgentbaseHQ/agentbase-python/commit/26fcf8cfaa707947604b7f21770e375bdd8f2505)) +* ensure streams are always closed ([a5fbe1f](https://github.com/AgentbaseHQ/agentbase-python/commit/a5fbe1ff1cdb9577f78959979e26cbf051349a41)) +* **types:** allow pyright to infer TypedDict types within SequenceNotStr ([c8e85dc](https://github.com/AgentbaseHQ/agentbase-python/commit/c8e85dcadbd1c87e7eb467da4d5907d4be5ba067)) +* use async_to_httpx_files in patch method ([15b8a78](https://github.com/AgentbaseHQ/agentbase-python/commit/15b8a78b1702ab6b3b5f81e5ffd7620f35b4666a)) + + +### Chores + +* add missing docstrings ([7f81914](https://github.com/AgentbaseHQ/agentbase-python/commit/7f81914d3ad703d6e516c4207af8be9b29639e62)) +* add Python 3.14 classifier and testing ([e277845](https://github.com/AgentbaseHQ/agentbase-python/commit/e277845b95c046a36a261d5e541c3b834b9e6060)) +* **deps:** mypy 1.18.1 has a regression, pin to 1.17 ([62a2800](https://github.com/AgentbaseHQ/agentbase-python/commit/62a2800278f735e3a60478f2f5bf073247f26df4)) +* **docs:** use environment variables for authentication in code snippets ([6153b3a](https://github.com/AgentbaseHQ/agentbase-python/commit/6153b3ade4267a47d7e5c9f04f7df087736f14a9)) +* **internal:** add `--fix` argument to lint script ([40a5771](https://github.com/AgentbaseHQ/agentbase-python/commit/40a57716ebac55d0f9474861c4a2fc6e4c30531e)) +* **internal:** add missing files argument to base client ([fa1bf74](https://github.com/AgentbaseHQ/agentbase-python/commit/fa1bf74ee8af58a559cded1f9b3bdeab2bc9353b)) +* **package:** drop Python 3.8 support ([27398f4](https://github.com/AgentbaseHQ/agentbase-python/commit/27398f431d61623cf51afd80eb96fa29a8d0bb17)) +* speedup initial import ([ae80104](https://github.com/AgentbaseHQ/agentbase-python/commit/ae801043676a3027715757600d99416925b8e32f)) +* update lockfile ([c45d94f](https://github.com/AgentbaseHQ/agentbase-python/commit/c45d94f06694f09c807bff2c994a9207b2e5568e)) + + +### Documentation + +* add more examples ([07cd970](https://github.com/AgentbaseHQ/agentbase-python/commit/07cd9702f78069d1480235504de363aa26008bd8)) + ## 0.5.0 (2025-11-06) Full Changelog: [v0.4.0...v0.5.0](https://github.com/AgentbaseHQ/agentbase-python/compare/v0.4.0...v0.5.0) diff --git a/README.md b/README.md index 3722b75..6401b7f 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Visit the Agentbase API documentation [here](https://docs.agentbase.sh/). [![PyPI version](https://img.shields.io/pypi/v/agentbase-sdk.svg?label=pypi%20(stable))](https://pypi.org/project/agentbase-sdk/) -The Agentbase Python library provides convenient access to the Agentbase REST API from any Python 3.8+ +The Agentbase Python library provides convenient access to the Agentbase REST API from any Python 3.9+ application. The library includes type definitions for all request params and response fields, and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). @@ -85,6 +85,7 @@ pip install agentbase-sdk[aiohttp] Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: ```python +import os import asyncio from agentbase import DefaultAioHttpClient from agentbase import AsyncAgentbase @@ -92,7 +93,7 @@ from agentbase import AsyncAgentbase async def main() -> None: async with AsyncAgentbase( - api_key="My API Key", + api_key=os.environ.get("AGENTBASE_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: response = await client.run_agent( @@ -103,36 +104,6 @@ async def main() -> None: asyncio.run(main()) ``` -## Streaming responses - -We provide support for streaming responses using Server Side Events (SSE). - -```python -from agentbase import Agentbase - -client = Agentbase() - -stream = client.run_agent( - message="REPLACE_ME", -) -for response in stream: - print(response) -``` - -The async client uses the exact same interface. - -```python -from agentbase import AsyncAgentbase - -client = AsyncAgentbase() - -stream = await client.run_agent( - message="REPLACE_ME", -) -async for response in stream: - print(response) -``` - ## Using types Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like: @@ -419,7 +390,7 @@ print(agentbase.__version__) ## Requirements -Python 3.8 or higher. +Python 3.9 or higher. ## Contributing diff --git a/pyproject.toml b/pyproject.toml index 860aa6c..88843d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,30 +1,32 @@ [project] name = "agentbase-sdk" -version = "0.5.0" +version = "0.5.1" description = "The official Python library for the agentbase API" dynamic = ["readme"] license = "Apache-2.0" authors = [ { name = "Agentbase", email = "team@agentbase.sh" }, ] + dependencies = [ - "httpx>=0.23.0, <1", - "pydantic>=1.9.0, <3", - "typing-extensions>=4.10, <5", - "anyio>=3.5.0, <5", - "distro>=1.7.0, <2", - "sniffio", + "httpx>=0.23.0, <1", + "pydantic>=1.9.0, <3", + "typing-extensions>=4.10, <5", + "anyio>=3.5.0, <5", + "distro>=1.7.0, <2", + "sniffio", ] -requires-python = ">= 3.8" + +requires-python = ">= 3.9" classifiers = [ "Typing :: Typed", "Intended Audience :: Developers", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: MacOS", @@ -46,7 +48,7 @@ managed = true # version pins are in requirements-dev.lock dev-dependencies = [ "pyright==1.1.399", - "mypy", + "mypy==1.17", "respx", "pytest", "pytest-asyncio", @@ -141,7 +143,7 @@ filterwarnings = [ # there are a couple of flags that are still disabled by # default in strict mode as they are experimental and niche. typeCheckingMode = "strict" -pythonVersion = "3.8" +pythonVersion = "3.9" exclude = [ "_dev", diff --git a/requirements-dev.lock b/requirements-dev.lock index c6dcebb..aea729c 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -12,40 +12,45 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.12.8 +aiohttp==3.13.2 # via agentbase-sdk # via httpx-aiohttp -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.12.0 # via agentbase-sdk # via httpx -argcomplete==3.1.2 +argcomplete==3.6.3 # via nox async-timeout==5.0.1 # via aiohttp -attrs==25.3.0 +attrs==25.4.0 # via aiohttp -certifi==2023.7.22 + # via nox +backports-asyncio-runner==1.2.0 + # via pytest-asyncio +certifi==2025.11.12 # via httpcore # via httpx -colorlog==6.7.0 +colorlog==6.10.1 + # via nox +dependency-groups==1.3.1 # via nox -dirty-equals==0.6.0 -distlib==0.3.7 +dirty-equals==0.11 +distlib==0.4.0 # via virtualenv -distro==1.8.0 +distro==1.9.0 # via agentbase-sdk -exceptiongroup==1.2.2 +exceptiongroup==1.3.1 # via anyio # via pytest -execnet==2.1.1 +execnet==2.1.2 # via pytest-xdist -filelock==3.12.4 +filelock==3.19.1 # via virtualenv -frozenlist==1.6.2 +frozenlist==1.8.0 # via aiohttp # via aiosignal h11==0.16.0 @@ -58,80 +63,87 @@ httpx==0.28.1 # via respx httpx-aiohttp==0.1.9 # via agentbase-sdk -idna==3.4 +humanize==4.13.0 + # via nox +idna==3.11 # via anyio # via httpx # via yarl -importlib-metadata==7.0.0 -iniconfig==2.0.0 +importlib-metadata==8.7.0 +iniconfig==2.1.0 # via pytest markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -multidict==6.4.4 +multidict==6.7.0 # via aiohttp # via yarl -mypy==1.14.1 -mypy-extensions==1.0.0 +mypy==1.17.0 +mypy-extensions==1.1.0 # via mypy -nodeenv==1.8.0 +nodeenv==1.9.1 # via pyright -nox==2023.4.22 -packaging==23.2 +nox==2025.11.12 +packaging==25.0 + # via dependency-groups # via nox # via pytest -platformdirs==3.11.0 +pathspec==0.12.1 + # via mypy +platformdirs==4.4.0 # via virtualenv -pluggy==1.5.0 +pluggy==1.6.0 # via pytest -propcache==0.3.1 +propcache==0.4.1 # via aiohttp # via yarl -pydantic==2.11.9 +pydantic==2.12.5 # via agentbase-sdk -pydantic-core==2.33.2 +pydantic-core==2.41.5 # via pydantic -pygments==2.18.0 +pygments==2.19.2 + # via pytest # via rich pyright==1.1.399 -pytest==8.3.3 +pytest==8.4.2 # via pytest-asyncio # via pytest-xdist -pytest-asyncio==0.24.0 -pytest-xdist==3.7.0 -python-dateutil==2.8.2 +pytest-asyncio==1.2.0 +pytest-xdist==3.8.0 +python-dateutil==2.9.0.post0 # via time-machine -pytz==2023.3.post1 - # via dirty-equals respx==0.22.0 -rich==13.7.1 -ruff==0.9.4 -setuptools==68.2.2 - # via nodeenv -six==1.16.0 +rich==14.2.0 +ruff==0.14.7 +six==1.17.0 # via python-dateutil -sniffio==1.3.0 +sniffio==1.3.1 # via agentbase-sdk - # via anyio -time-machine==2.9.0 -tomli==2.0.2 +time-machine==2.19.0 +tomli==2.3.0 + # via dependency-groups # via mypy + # via nox # via pytest -typing-extensions==4.12.2 +typing-extensions==4.15.0 # via agentbase-sdk + # via aiosignal # via anyio + # via exceptiongroup # via multidict # via mypy # via pydantic # via pydantic-core # via pyright + # via pytest-asyncio # via typing-inspection -typing-inspection==0.4.1 + # via virtualenv +typing-inspection==0.4.2 # via pydantic -virtualenv==20.24.5 +virtualenv==20.35.4 # via nox -yarl==1.20.0 +yarl==1.22.0 # via aiohttp -zipp==3.17.0 +zipp==3.23.0 # via importlib-metadata diff --git a/requirements.lock b/requirements.lock index 5eb344f..a238188 100644 --- a/requirements.lock +++ b/requirements.lock @@ -12,28 +12,28 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.12.8 +aiohttp==3.13.2 # via agentbase-sdk # via httpx-aiohttp -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.12.0 # via agentbase-sdk # via httpx async-timeout==5.0.1 # via aiohttp -attrs==25.3.0 +attrs==25.4.0 # via aiohttp -certifi==2023.7.22 +certifi==2025.11.12 # via httpcore # via httpx -distro==1.8.0 +distro==1.9.0 # via agentbase-sdk -exceptiongroup==1.2.2 +exceptiongroup==1.3.1 # via anyio -frozenlist==1.6.2 +frozenlist==1.8.0 # via aiohttp # via aiosignal h11==0.16.0 @@ -45,31 +45,32 @@ httpx==0.28.1 # via httpx-aiohttp httpx-aiohttp==0.1.9 # via agentbase-sdk -idna==3.4 +idna==3.11 # via anyio # via httpx # via yarl -multidict==6.4.4 +multidict==6.7.0 # via aiohttp # via yarl -propcache==0.3.1 +propcache==0.4.1 # via aiohttp # via yarl -pydantic==2.11.9 +pydantic==2.12.5 # via agentbase-sdk -pydantic-core==2.33.2 +pydantic-core==2.41.5 # via pydantic -sniffio==1.3.0 +sniffio==1.3.1 # via agentbase-sdk - # via anyio -typing-extensions==4.12.2 +typing-extensions==4.15.0 # via agentbase-sdk + # via aiosignal # via anyio + # via exceptiongroup # via multidict # via pydantic # via pydantic-core # via typing-inspection -typing-inspection==0.4.1 +typing-inspection==0.4.2 # via pydantic -yarl==1.20.0 +yarl==1.22.0 # via aiohttp diff --git a/scripts/lint b/scripts/lint index 0d0eca1..7fbd3ef 100755 --- a/scripts/lint +++ b/scripts/lint @@ -4,8 +4,13 @@ set -e cd "$(dirname "$0")/.." -echo "==> Running lints" -rye run lint +if [ "$1" = "--fix" ]; then + echo "==> Running lints with --fix" + rye run fix:ruff +else + echo "==> Running lints" + rye run lint +fi echo "==> Making sure it imports" rye run python -c 'import agentbase' diff --git a/src/agentbase/_base_client.py b/src/agentbase/_base_client.py index 375d3a0..36cc846 100644 --- a/src/agentbase/_base_client.py +++ b/src/agentbase/_base_client.py @@ -1247,9 +1247,12 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + ) return self.request(cast_to, opts) def put( @@ -1767,9 +1770,12 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options + ) return await self.request(cast_to, opts) async def put( diff --git a/src/agentbase/_client.py b/src/agentbase/_client.py index b85f4d7..e6290d6 100644 --- a/src/agentbase/_client.py +++ b/src/agentbase/_client.py @@ -3,7 +3,7 @@ from __future__ import annotations import os -from typing import Any, Mapping, Iterable +from typing import TYPE_CHECKING, Any, Mapping, Iterable from typing_extensions import Self, Literal, override import httpx @@ -31,6 +31,7 @@ get_async_library, async_maybe_transform, ) +from ._compat import cached_property from ._version import __version__ from ._response import ( to_raw_response_wrapper, @@ -38,7 +39,6 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from .resources import agent, messages from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import AgentbaseError, APIStatusError from ._base_client import ( @@ -49,6 +49,11 @@ ) from .types.run_agent_response import RunAgentResponse +if TYPE_CHECKING: + from .resources import agent, messages + from .resources.agent import AgentResource, AsyncAgentResource + from .resources.messages import MessagesResource, AsyncMessagesResource + __all__ = [ "Timeout", "Transport", @@ -62,11 +67,6 @@ class Agentbase(SyncAPIClient): - agent: agent.AgentResource - messages: messages.MessagesResource - with_raw_response: AgentbaseWithRawResponse - with_streaming_response: AgentbaseWithStreamedResponse - # client options api_key: str @@ -121,10 +121,25 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.agent = agent.AgentResource(self) - self.messages = messages.MessagesResource(self) - self.with_raw_response = AgentbaseWithRawResponse(self) - self.with_streaming_response = AgentbaseWithStreamedResponse(self) + @cached_property + def agent(self) -> AgentResource: + from .resources.agent import AgentResource + + return AgentResource(self) + + @cached_property + def messages(self) -> MessagesResource: + from .resources.messages import MessagesResource + + return MessagesResource(self) + + @cached_property + def with_raw_response(self) -> AgentbaseWithRawResponse: + return AgentbaseWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AgentbaseWithStreamedResponse: + return AgentbaseWithStreamedResponse(self) @property @override @@ -349,11 +364,6 @@ def _make_status_error( class AsyncAgentbase(AsyncAPIClient): - agent: agent.AsyncAgentResource - messages: messages.AsyncMessagesResource - with_raw_response: AsyncAgentbaseWithRawResponse - with_streaming_response: AsyncAgentbaseWithStreamedResponse - # client options api_key: str @@ -408,10 +418,25 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.agent = agent.AsyncAgentResource(self) - self.messages = messages.AsyncMessagesResource(self) - self.with_raw_response = AsyncAgentbaseWithRawResponse(self) - self.with_streaming_response = AsyncAgentbaseWithStreamedResponse(self) + @cached_property + def agent(self) -> AsyncAgentResource: + from .resources.agent import AsyncAgentResource + + return AsyncAgentResource(self) + + @cached_property + def messages(self) -> AsyncMessagesResource: + from .resources.messages import AsyncMessagesResource + + return AsyncMessagesResource(self) + + @cached_property + def with_raw_response(self) -> AsyncAgentbaseWithRawResponse: + return AsyncAgentbaseWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAgentbaseWithStreamedResponse: + return AsyncAgentbaseWithStreamedResponse(self) @property @override @@ -636,44 +661,96 @@ def _make_status_error( class AgentbaseWithRawResponse: + _client: Agentbase + def __init__(self, client: Agentbase) -> None: - self.agent = agent.AgentResourceWithRawResponse(client.agent) - self.messages = messages.MessagesResourceWithRawResponse(client.messages) + self._client = client self.run_agent = to_raw_response_wrapper( client.run_agent, ) + @cached_property + def agent(self) -> agent.AgentResourceWithRawResponse: + from .resources.agent import AgentResourceWithRawResponse + + return AgentResourceWithRawResponse(self._client.agent) + + @cached_property + def messages(self) -> messages.MessagesResourceWithRawResponse: + from .resources.messages import MessagesResourceWithRawResponse + + return MessagesResourceWithRawResponse(self._client.messages) + class AsyncAgentbaseWithRawResponse: + _client: AsyncAgentbase + def __init__(self, client: AsyncAgentbase) -> None: - self.agent = agent.AsyncAgentResourceWithRawResponse(client.agent) - self.messages = messages.AsyncMessagesResourceWithRawResponse(client.messages) + self._client = client self.run_agent = async_to_raw_response_wrapper( client.run_agent, ) + @cached_property + def agent(self) -> agent.AsyncAgentResourceWithRawResponse: + from .resources.agent import AsyncAgentResourceWithRawResponse + + return AsyncAgentResourceWithRawResponse(self._client.agent) + + @cached_property + def messages(self) -> messages.AsyncMessagesResourceWithRawResponse: + from .resources.messages import AsyncMessagesResourceWithRawResponse + + return AsyncMessagesResourceWithRawResponse(self._client.messages) + class AgentbaseWithStreamedResponse: + _client: Agentbase + def __init__(self, client: Agentbase) -> None: - self.agent = agent.AgentResourceWithStreamingResponse(client.agent) - self.messages = messages.MessagesResourceWithStreamingResponse(client.messages) + self._client = client self.run_agent = to_streamed_response_wrapper( client.run_agent, ) + @cached_property + def agent(self) -> agent.AgentResourceWithStreamingResponse: + from .resources.agent import AgentResourceWithStreamingResponse + + return AgentResourceWithStreamingResponse(self._client.agent) + + @cached_property + def messages(self) -> messages.MessagesResourceWithStreamingResponse: + from .resources.messages import MessagesResourceWithStreamingResponse + + return MessagesResourceWithStreamingResponse(self._client.messages) + class AsyncAgentbaseWithStreamedResponse: + _client: AsyncAgentbase + def __init__(self, client: AsyncAgentbase) -> None: - self.agent = agent.AsyncAgentResourceWithStreamingResponse(client.agent) - self.messages = messages.AsyncMessagesResourceWithStreamingResponse(client.messages) + self._client = client self.run_agent = async_to_streamed_response_wrapper( client.run_agent, ) + @cached_property + def agent(self) -> agent.AsyncAgentResourceWithStreamingResponse: + from .resources.agent import AsyncAgentResourceWithStreamingResponse + + return AsyncAgentResourceWithStreamingResponse(self._client.agent) + + @cached_property + def messages(self) -> messages.AsyncMessagesResourceWithStreamingResponse: + from .resources.messages import AsyncMessagesResourceWithStreamingResponse + + return AsyncMessagesResourceWithStreamingResponse(self._client.messages) + Client = Agentbase diff --git a/src/agentbase/_models.py b/src/agentbase/_models.py index 6a3cd1d..ca9500b 100644 --- a/src/agentbase/_models.py +++ b/src/agentbase/_models.py @@ -2,6 +2,7 @@ import os import inspect +import weakref from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast from datetime import date, datetime from typing_extensions import ( @@ -256,15 +257,16 @@ def model_dump( mode: Literal["json", "python"] | str = "python", include: IncEx | None = None, exclude: IncEx | None = None, + context: Any | None = None, by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, + exclude_computed_fields: bool = False, round_trip: bool = False, warnings: bool | Literal["none", "warn", "error"] = True, - context: dict[str, Any] | None = None, - serialize_as_any: bool = False, fallback: Callable[[Any], Any] | None = None, + serialize_as_any: bool = False, ) -> dict[str, Any]: """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump @@ -272,16 +274,24 @@ def model_dump( Args: mode: The mode in which `to_python` should run. - If mode is 'json', the dictionary will only contain JSON serializable types. - If mode is 'python', the dictionary may contain any Python objects. - include: A list of fields to include in the output. - exclude: A list of fields to exclude from the output. + If mode is 'json', the output will only contain JSON serializable types. + If mode is 'python', the output may contain non-JSON-serializable Python objects. + include: A set of fields to include in the output. + exclude: A set of fields to exclude from the output. + context: Additional context to pass to the serializer. by_alias: Whether to use the field's alias in the dictionary key if defined. - exclude_unset: Whether to exclude fields that are unset or None from the output. - exclude_defaults: Whether to exclude fields that are set to their default value from the output. - exclude_none: Whether to exclude fields that have a value of `None` from the output. - round_trip: Whether to enable serialization and deserialization round-trip support. - warnings: Whether to log warnings when invalid fields are encountered. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that are set to their default value. + exclude_none: Whether to exclude fields that have a value of `None`. + exclude_computed_fields: Whether to exclude computed fields. + While this can be useful for round-tripping, it is usually recommended to use the dedicated + `round_trip` parameter instead. + round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T]. + warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors, + "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError]. + fallback: A function to call when an unknown value is encountered. If not provided, + a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised. + serialize_as_any: Whether to serialize fields with duck-typing serialization behavior. Returns: A dictionary representation of the model. @@ -298,6 +308,8 @@ def model_dump( raise ValueError("serialize_as_any is only supported in Pydantic v2") if fallback is not None: raise ValueError("fallback is only supported in Pydantic v2") + if exclude_computed_fields != False: + raise ValueError("exclude_computed_fields is only supported in Pydantic v2") dumped = super().dict( # pyright: ignore[reportDeprecated] include=include, exclude=exclude, @@ -314,15 +326,17 @@ def model_dump_json( self, *, indent: int | None = None, + ensure_ascii: bool = False, include: IncEx | None = None, exclude: IncEx | None = None, + context: Any | None = None, by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, + exclude_computed_fields: bool = False, round_trip: bool = False, warnings: bool | Literal["none", "warn", "error"] = True, - context: dict[str, Any] | None = None, fallback: Callable[[Any], Any] | None = None, serialize_as_any: bool = False, ) -> str: @@ -354,6 +368,10 @@ def model_dump_json( raise ValueError("serialize_as_any is only supported in Pydantic v2") if fallback is not None: raise ValueError("fallback is only supported in Pydantic v2") + if ensure_ascii != False: + raise ValueError("ensure_ascii is only supported in Pydantic v2") + if exclude_computed_fields != False: + raise ValueError("exclude_computed_fields is only supported in Pydantic v2") return super().json( # type: ignore[reportDeprecated] indent=indent, include=include, @@ -573,6 +591,9 @@ class CachedDiscriminatorType(Protocol): __discriminator__: DiscriminatorDetails +DISCRIMINATOR_CACHE: weakref.WeakKeyDictionary[type, DiscriminatorDetails] = weakref.WeakKeyDictionary() + + class DiscriminatorDetails: field_name: str """The name of the discriminator field in the variant class, e.g. @@ -615,8 +636,9 @@ def __init__( def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None: - if isinstance(union, CachedDiscriminatorType): - return union.__discriminator__ + cached = DISCRIMINATOR_CACHE.get(union) + if cached is not None: + return cached discriminator_field_name: str | None = None @@ -669,7 +691,7 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, discriminator_field=discriminator_field_name, discriminator_alias=discriminator_alias, ) - cast(CachedDiscriminatorType, union).__discriminator__ = details + DISCRIMINATOR_CACHE.setdefault(union, details) return details diff --git a/src/agentbase/_streaming.py b/src/agentbase/_streaming.py index 1de0920..5966ac9 100644 --- a/src/agentbase/_streaming.py +++ b/src/agentbase/_streaming.py @@ -54,11 +54,12 @@ def __stream__(self) -> Iterator[_T]: process_data = self._client._process_response_data iterator = self._iter_events() - for sse in iterator: - yield process_data(data=sse.json(), cast_to=cast_to, response=response) - - # As we might not fully consume the response stream, we need to close it explicitly - response.close() + try: + for sse in iterator: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + finally: + # Ensure the response is closed even if the consumer doesn't read all data + response.close() def __enter__(self) -> Self: return self @@ -117,11 +118,12 @@ async def __stream__(self) -> AsyncIterator[_T]: process_data = self._client._process_response_data iterator = self._iter_events() - async for sse in iterator: - yield process_data(data=sse.json(), cast_to=cast_to, response=response) - - # As we might not fully consume the response stream, we need to close it explicitly - await response.aclose() + try: + async for sse in iterator: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + finally: + # Ensure the response is closed even if the consumer doesn't read all data + await response.aclose() async def __aenter__(self) -> Self: return self diff --git a/src/agentbase/_types.py b/src/agentbase/_types.py index a65897d..5e6584d 100644 --- a/src/agentbase/_types.py +++ b/src/agentbase/_types.py @@ -243,6 +243,9 @@ class HttpxSendArgs(TypedDict, total=False): if TYPE_CHECKING: # This works because str.__contains__ does not accept object (either in typeshed or at runtime) # https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285 + # + # Note: index() and count() methods are intentionally omitted to allow pyright to properly + # infer TypedDict types when dict literals are used in lists assigned to SequenceNotStr. class SequenceNotStr(Protocol[_T_co]): @overload def __getitem__(self, index: SupportsIndex, /) -> _T_co: ... @@ -251,8 +254,6 @@ def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ... def __contains__(self, value: object, /) -> bool: ... def __len__(self) -> int: ... def __iter__(self) -> Iterator[_T_co]: ... - def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ... - def count(self, value: Any, /) -> int: ... def __reversed__(self) -> Iterator[_T_co]: ... else: # just point this to a normal `Sequence` at runtime to avoid having to special case diff --git a/src/agentbase/_utils/_sync.py b/src/agentbase/_utils/_sync.py index ad7ec71..f6027c1 100644 --- a/src/agentbase/_utils/_sync.py +++ b/src/agentbase/_utils/_sync.py @@ -1,10 +1,8 @@ from __future__ import annotations -import sys import asyncio import functools -import contextvars -from typing import Any, TypeVar, Callable, Awaitable +from typing import TypeVar, Callable, Awaitable from typing_extensions import ParamSpec import anyio @@ -15,34 +13,11 @@ T_ParamSpec = ParamSpec("T_ParamSpec") -if sys.version_info >= (3, 9): - _asyncio_to_thread = asyncio.to_thread -else: - # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread - # for Python 3.8 support - async def _asyncio_to_thread( - func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs - ) -> Any: - """Asynchronously run function *func* in a separate thread. - - Any *args and **kwargs supplied for this function are directly passed - to *func*. Also, the current :class:`contextvars.Context` is propagated, - allowing context variables from the main thread to be accessed in the - separate thread. - - Returns a coroutine that can be awaited to get the eventual result of *func*. - """ - loop = asyncio.events.get_running_loop() - ctx = contextvars.copy_context() - func_call = functools.partial(ctx.run, func, *args, **kwargs) - return await loop.run_in_executor(None, func_call) - - async def to_thread( func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs ) -> T_Retval: if sniffio.current_async_library() == "asyncio": - return await _asyncio_to_thread(func, *args, **kwargs) + return await asyncio.to_thread(func, *args, **kwargs) return await anyio.to_thread.run_sync( functools.partial(func, *args, **kwargs), @@ -53,10 +28,7 @@ async def to_thread( def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: """ Take a blocking function and create an async one that receives the same - positional and keyword arguments. For python version 3.9 and above, it uses - asyncio.to_thread to run the function in a separate thread. For python version - 3.8, it uses locally defined copy of the asyncio.to_thread function which was - introduced in python 3.9. + positional and keyword arguments. Usage: diff --git a/src/agentbase/_version.py b/src/agentbase/_version.py index cdf43ad..2830573 100644 --- a/src/agentbase/_version.py +++ b/src/agentbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "agentbase" -__version__ = "0.5.0" # x-release-please-version +__version__ = "0.5.1" # x-release-please-version diff --git a/src/agentbase/types/agent_run_params.py b/src/agentbase/types/agent_run_params.py index e1676ab..2240f0a 100644 --- a/src/agentbase/types/agent_run_params.py +++ b/src/agentbase/types/agent_run_params.py @@ -109,6 +109,11 @@ class Agent(TypedDict, total=False): class Callback(TypedDict, total=False): + """A callback endpoint configuration to send agent message events back to. + + Use with background true. + """ + url: Required[str] """The webhook URL to send events to.""" @@ -125,6 +130,11 @@ class Datastore(TypedDict, total=False): class FinalOutput(TypedDict, total=False): + """ + Configuration for an extra final output event that processes the entire agent + message thread and produces a structured output based on the provided JSON schema. + """ + name: Required[str] """Name for the final output.""" @@ -155,6 +165,8 @@ class Query(TypedDict, total=False): class WorkflowStepRetryPolicy(TypedDict, total=False): + """Retry configuration for the step.""" + backoff: str max_attempts: int diff --git a/src/agentbase/types/client_run_agent_params.py b/src/agentbase/types/client_run_agent_params.py index d4fb9f6..f639310 100644 --- a/src/agentbase/types/client_run_agent_params.py +++ b/src/agentbase/types/client_run_agent_params.py @@ -109,6 +109,11 @@ class Agent(TypedDict, total=False): class Callback(TypedDict, total=False): + """A callback endpoint configuration to send agent message events back to. + + Use with background true. + """ + url: Required[str] """The webhook URL to send events to.""" @@ -125,6 +130,11 @@ class Datastore(TypedDict, total=False): class FinalOutput(TypedDict, total=False): + """ + Configuration for an extra final output event that processes the entire agent + message thread and produces a structured output based on the provided JSON schema. + """ + name: Required[str] """Name for the final output.""" @@ -155,6 +165,8 @@ class Query(TypedDict, total=False): class WorkflowStepRetryPolicy(TypedDict, total=False): + """Retry configuration for the step.""" + backoff: str max_attempts: int diff --git a/tests/test_models.py b/tests/test_models.py index 84a7455..c1acbcd 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -9,7 +9,7 @@ from agentbase._utils import PropertyInfo from agentbase._compat import PYDANTIC_V1, parse_obj, model_dump, model_json -from agentbase._models import BaseModel, construct_type +from agentbase._models import DISCRIMINATOR_CACHE, BaseModel, construct_type class BasicModel(BaseModel): @@ -809,7 +809,7 @@ class B(BaseModel): UnionType = cast(Any, Union[A, B]) - assert not hasattr(UnionType, "__discriminator__") + assert not DISCRIMINATOR_CACHE.get(UnionType) m = construct_type( value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")]) @@ -818,7 +818,7 @@ class B(BaseModel): assert m.type == "b" assert m.data == "foo" # type: ignore[comparison-overlap] - discriminator = UnionType.__discriminator__ + discriminator = DISCRIMINATOR_CACHE.get(UnionType) assert discriminator is not None m = construct_type( @@ -830,7 +830,7 @@ class B(BaseModel): # if the discriminator details object stays the same between invocations then # we hit the cache - assert UnionType.__discriminator__ is discriminator + assert DISCRIMINATOR_CACHE.get(UnionType) is discriminator @pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1")