diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3cec802..cd3bfb0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/alchemyst-ai-sdk-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | @@ -44,7 +44,7 @@ jobs: id-token: write runs-on: ${{ github.repository == 'stainless-sdks/alchemyst-ai-sdk-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | @@ -81,7 +81,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/alchemyst-ai-sdk-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 2d8d220..2eabb25 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 78d40bc..44d3d6a 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'Alchemyst-ai/alchemyst-sdk-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Check release environment run: | diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 6d78745..091cfb1 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.9.0" + ".": "0.10.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 6571c4d..7a1ec0c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 11 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/alchemyst-ai%2Falchemyst-ai-sdk-e8e44e0e972cd2e130110834826053d128e088f8c94fa824b4e8f0498cc3ff7a.yml -openapi_spec_hash: 23b2524dd290e587b543601546731842 -config_hash: 5aa39dd4797ce0dacbe19c69fcf11be6 +configured_endpoints: 15 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/alchemyst-ai%2Falchemyst-ai-sdk-af43a0360c784fd31239bbacd7343fefe06847df4706d18ddf0d1778ef597e80.yml +openapi_spec_hash: 3e6daacc7aa9b60ca68fdb7b6f76fbf9 +config_hash: 32e700129a8eaba38674e583e3d39e2b diff --git a/CHANGELOG.md b/CHANGELOG.md index c09ff89..533fc42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## 0.10.0 (2026-01-17) + +Full Changelog: [v0.9.0...v0.10.0](https://github.com/Alchemyst-ai/alchemyst-sdk-python/compare/v0.9.0...v0.10.0) + +### Features + +* **api:** api update ([938b01f](https://github.com/Alchemyst-ai/alchemyst-sdk-python/commit/938b01ff0092d7ac349bf0c2978ac5feccd8bd68)) +* **api:** api update ([04c852d](https://github.com/Alchemyst-ai/alchemyst-sdk-python/commit/04c852da6d40f9b8ca4b157d978134cf4db180b5)) +* **api:** api update ([6b3b044](https://github.com/Alchemyst-ai/alchemyst-sdk-python/commit/6b3b0445a1d1920cc732b29155718f4c80a44b46)) +* **api:** api update ([7d5b6e4](https://github.com/Alchemyst-ai/alchemyst-sdk-python/commit/7d5b6e4828f2e010412e1a6947303d0282500fd6)) +* **api:** manual updates ([a502f74](https://github.com/Alchemyst-ai/alchemyst-sdk-python/commit/a502f74122c3183ff20d0146547349e9ecd062fc)) +* **api:** manual updates ([628e068](https://github.com/Alchemyst-ai/alchemyst-sdk-python/commit/628e068d6f4ced825a7d90a9a7730fecdfc8984a)) +* **client:** add support for binary request streaming ([020ff9a](https://github.com/Alchemyst-ai/alchemyst-sdk-python/commit/020ff9a9078ffa02c7407fe73e07d508ab1f7b5c)) + + +### Bug Fixes + +* **client:** loosen auth header validation ([3d175cb](https://github.com/Alchemyst-ai/alchemyst-sdk-python/commit/3d175cb24d4ec289e8654eb2dcf4685ddb2a17af)) + + +### Chores + +* **internal:** update `actions/checkout` version ([11aa112](https://github.com/Alchemyst-ai/alchemyst-sdk-python/commit/11aa11229f32a79c7e061f805c64918b3cc409f5)) + ## 0.9.0 (2026-01-06) Full Changelog: [v0.8.1...v0.9.0](https://github.com/Alchemyst-ai/alchemyst-sdk-python/compare/v0.8.1...v0.9.0) diff --git a/README.md b/README.md index 0f70e7a..125221a 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ It is generated with [Stainless](https://www.stainless.com/). ## Documentation -The REST API documentation can be found on [docs.getalchemystai.com](https://docs.getalchemystai.com). The full API of this library can be found in [api.md](api.md). +The REST API documentation can be found on [getalchemystai.com](https://getalchemystai.com/docs). The full API of this library can be found in [api.md](api.md). ## Installation diff --git a/api.md b/api.md index a5e2d22..9605b3f 100644 --- a/api.md +++ b/api.md @@ -54,6 +54,32 @@ Methods: - client.v1.context.memory.delete(\*\*params) -> None - client.v1.context.memory.add(\*\*params) -> MemoryAddResponse +### AddAsync + +Types: + +```python +from alchemyst_ai.types.v1.context import AddAsyncCreateResponse, AddAsyncCancelResponse +``` + +Methods: + +- client.v1.context.add_async.create(\*\*params) -> AddAsyncCreateResponse +- client.v1.context.add_async.cancel(id) -> AddAsyncCancelResponse + +#### Status + +Types: + +```python +from alchemyst_ai.types.v1.context.add_async import StatusRetrieveResponse, StatusListResponse +``` + +Methods: + +- client.v1.context.add_async.status.retrieve(id) -> StatusRetrieveResponse +- client.v1.context.add_async.status.list(\*\*params) -> StatusListResponse + ## Org ### Context diff --git a/pyproject.toml b/pyproject.toml index fc3fd2d..418b5ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "alchemystai" -version = "0.9.0" +version = "0.10.0" description = "The official Python library for the alchemyst_ai API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/alchemyst_ai/_base_client.py b/src/alchemyst_ai/_base_client.py index 424dbd9..93ef387 100644 --- a/src/alchemyst_ai/_base_client.py +++ b/src/alchemyst_ai/_base_client.py @@ -9,6 +9,7 @@ import inspect import logging import platform +import warnings import email.utils from types import TracebackType from random import random @@ -51,9 +52,11 @@ ResponseT, AnyMapping, PostParser, + BinaryTypes, RequestFiles, HttpxSendArgs, RequestOptions, + AsyncBinaryTypes, HttpxRequestFiles, ModelBuilderProtocol, not_given, @@ -477,8 +480,19 @@ def _build_request( retries_taken: int = 0, ) -> httpx.Request: if log.isEnabledFor(logging.DEBUG): - log.debug("Request options: %s", model_dump(options, exclude_unset=True)) - + log.debug( + "Request options: %s", + model_dump( + options, + exclude_unset=True, + # Pydantic v1 can't dump every type we support in content, so we exclude it for now. + exclude={ + "content", + } + if PYDANTIC_V1 + else {}, + ), + ) kwargs: dict[str, Any] = {} json_data = options.json_data @@ -532,7 +546,13 @@ def _build_request( is_body_allowed = options.method.lower() != "get" if is_body_allowed: - if isinstance(json_data, bytes): + if options.content is not None and json_data is not None: + raise TypeError("Passing both `content` and `json_data` is not supported") + if options.content is not None and files is not None: + raise TypeError("Passing both `content` and `files` is not supported") + if options.content is not None: + kwargs["content"] = options.content + elif isinstance(json_data, bytes): kwargs["content"] = json_data else: kwargs["json"] = json_data if is_given(json_data) else None @@ -1194,6 +1214,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[False] = False, @@ -1206,6 +1227,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[True], @@ -1219,6 +1241,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool, @@ -1231,13 +1254,25 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool = False, stream_cls: type[_StreamT] | None = None, ) -> ResponseT | _StreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) @@ -1247,11 +1282,23 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + method="patch", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1261,11 +1308,23 @@ def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1275,9 +1334,19 @@ def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return self.request(cast_to, opts) def get_api_list( @@ -1717,6 +1786,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[False] = False, @@ -1729,6 +1799,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[True], @@ -1742,6 +1813,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool, @@ -1754,13 +1826,25 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool = False, stream_cls: type[_AsyncStreamT] | None = None, ) -> ResponseT | _AsyncStreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) @@ -1770,11 +1854,28 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="patch", + url=path, + json_data=body, + content=content, + files=await async_to_httpx_files(files), + **options, ) return await self.request(cast_to, opts) @@ -1784,11 +1885,23 @@ async def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts) @@ -1798,9 +1911,19 @@ async def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return await self.request(cast_to, opts) def get_api_list( diff --git a/src/alchemyst_ai/_client.py b/src/alchemyst_ai/_client.py index 37b2393..67f63b1 100644 --- a/src/alchemyst_ai/_client.py +++ b/src/alchemyst_ai/_client.py @@ -136,9 +136,7 @@ def default_headers(self) -> dict[str, str | Omit]: @override def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None: - if self.api_key and headers.get("Authorization"): - return - if isinstance(custom_headers.get("Authorization"), Omit): + if headers.get("Authorization") or isinstance(custom_headers.get("Authorization"), Omit): return raise TypeError( @@ -319,9 +317,7 @@ def default_headers(self) -> dict[str, str | Omit]: @override def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None: - if self.api_key and headers.get("Authorization"): - return - if isinstance(custom_headers.get("Authorization"), Omit): + if headers.get("Authorization") or isinstance(custom_headers.get("Authorization"), Omit): return raise TypeError( diff --git a/src/alchemyst_ai/_models.py b/src/alchemyst_ai/_models.py index ca9500b..29070e0 100644 --- a/src/alchemyst_ai/_models.py +++ b/src/alchemyst_ai/_models.py @@ -3,7 +3,20 @@ import os import inspect import weakref -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast +from typing import ( + IO, + TYPE_CHECKING, + Any, + Type, + Union, + Generic, + TypeVar, + Callable, + Iterable, + Optional, + AsyncIterable, + cast, +) from datetime import date, datetime from typing_extensions import ( List, @@ -787,6 +800,7 @@ class FinalRequestOptionsInput(TypedDict, total=False): timeout: float | Timeout | None files: HttpxRequestFiles | None idempotency_key: str + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] json_data: Body extra_json: AnyMapping follow_redirects: bool @@ -805,6 +819,7 @@ class FinalRequestOptions(pydantic.BaseModel): post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() follow_redirects: Union[bool, None] = None + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None # It should be noted that we cannot use `json` here as that would override # a BaseModel method in an incompatible fashion. json_data: Union[Body, None] = None diff --git a/src/alchemyst_ai/_types.py b/src/alchemyst_ai/_types.py index b10f73b..74039ed 100644 --- a/src/alchemyst_ai/_types.py +++ b/src/alchemyst_ai/_types.py @@ -13,9 +13,11 @@ Mapping, TypeVar, Callable, + Iterable, Iterator, Optional, Sequence, + AsyncIterable, ) from typing_extensions import ( Set, @@ -56,6 +58,13 @@ else: Base64FileInput = Union[IO[bytes], PathLike] FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. + + +# Used for sending raw binary data / streaming data in request bodies +# e.g. for file uploads without multipart encoding +BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]] +AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]] + FileTypes = Union[ # file (or bytes) FileContent, diff --git a/src/alchemyst_ai/_version.py b/src/alchemyst_ai/_version.py index 6263ed6..c11ca7b 100644 --- a/src/alchemyst_ai/_version.py +++ b/src/alchemyst_ai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "alchemyst_ai" -__version__ = "0.9.0" # x-release-please-version +__version__ = "0.10.0" # x-release-please-version diff --git a/src/alchemyst_ai/resources/v1/context/__init__.py b/src/alchemyst_ai/resources/v1/context/__init__.py index 236a4b4..cb39ab2 100644 --- a/src/alchemyst_ai/resources/v1/context/__init__.py +++ b/src/alchemyst_ai/resources/v1/context/__init__.py @@ -32,6 +32,14 @@ ContextResourceWithStreamingResponse, AsyncContextResourceWithStreamingResponse, ) +from .add_async import ( + AddAsyncResource, + AsyncAddAsyncResource, + AddAsyncResourceWithRawResponse, + AsyncAddAsyncResourceWithRawResponse, + AddAsyncResourceWithStreamingResponse, + AsyncAddAsyncResourceWithStreamingResponse, +) __all__ = [ "TracesResource", @@ -52,6 +60,12 @@ "AsyncMemoryResourceWithRawResponse", "MemoryResourceWithStreamingResponse", "AsyncMemoryResourceWithStreamingResponse", + "AddAsyncResource", + "AsyncAddAsyncResource", + "AddAsyncResourceWithRawResponse", + "AsyncAddAsyncResourceWithRawResponse", + "AddAsyncResourceWithStreamingResponse", + "AsyncAddAsyncResourceWithStreamingResponse", "ContextResource", "AsyncContextResource", "ContextResourceWithRawResponse", diff --git a/src/alchemyst_ai/resources/v1/context/add_async/__init__.py b/src/alchemyst_ai/resources/v1/context/add_async/__init__.py new file mode 100644 index 0000000..f425106 --- /dev/null +++ b/src/alchemyst_ai/resources/v1/context/add_async/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .status import ( + StatusResource, + AsyncStatusResource, + StatusResourceWithRawResponse, + AsyncStatusResourceWithRawResponse, + StatusResourceWithStreamingResponse, + AsyncStatusResourceWithStreamingResponse, +) +from .add_async import ( + AddAsyncResource, + AsyncAddAsyncResource, + AddAsyncResourceWithRawResponse, + AsyncAddAsyncResourceWithRawResponse, + AddAsyncResourceWithStreamingResponse, + AsyncAddAsyncResourceWithStreamingResponse, +) + +__all__ = [ + "StatusResource", + "AsyncStatusResource", + "StatusResourceWithRawResponse", + "AsyncStatusResourceWithRawResponse", + "StatusResourceWithStreamingResponse", + "AsyncStatusResourceWithStreamingResponse", + "AddAsyncResource", + "AsyncAddAsyncResource", + "AddAsyncResourceWithRawResponse", + "AsyncAddAsyncResourceWithRawResponse", + "AddAsyncResourceWithStreamingResponse", + "AsyncAddAsyncResourceWithStreamingResponse", +] diff --git a/src/alchemyst_ai/resources/v1/context/add_async/add_async.py b/src/alchemyst_ai/resources/v1/context/add_async/add_async.py new file mode 100644 index 0000000..6810950 --- /dev/null +++ b/src/alchemyst_ai/resources/v1/context/add_async/add_async.py @@ -0,0 +1,337 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable +from typing_extensions import Literal + +import httpx + +from .status import ( + StatusResource, + AsyncStatusResource, + StatusResourceWithRawResponse, + AsyncStatusResourceWithRawResponse, + StatusResourceWithStreamingResponse, + AsyncStatusResourceWithStreamingResponse, +) +from ....._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ....._utils import maybe_transform, async_maybe_transform +from ....._compat import cached_property +from ....._resource import SyncAPIResource, AsyncAPIResource +from ....._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ....._base_client import make_request_options +from .....types.v1.context import add_async_create_params +from .....types.v1.context.add_async_cancel_response import AddAsyncCancelResponse +from .....types.v1.context.add_async_create_response import AddAsyncCreateResponse + +__all__ = ["AddAsyncResource", "AsyncAddAsyncResource"] + + +class AddAsyncResource(SyncAPIResource): + @cached_property + def status(self) -> StatusResource: + return StatusResource(self._client) + + @cached_property + def with_raw_response(self) -> AddAsyncResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Alchemyst-ai/alchemyst-sdk-python#accessing-raw-response-data-eg-headers + """ + return AddAsyncResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AddAsyncResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Alchemyst-ai/alchemyst-sdk-python#with_streaming_response + """ + return AddAsyncResourceWithStreamingResponse(self) + + def create( + self, + *, + context_type: Literal["resource", "conversation", "instruction"], + documents: Iterable[add_async_create_params.Document], + scope: Literal["internal", "external"], + source: str, + metadata: add_async_create_params.Metadata | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AddAsyncCreateResponse: + """ + This endpoint accepts context data and queues it for asynchronous processing by + the context processor. It returns a success or error response depending on the + queuing result. + + Args: + context_type: Type of context being added + + documents: Array of documents with content and additional metadata + + scope: Scope of the context + + source: The source of the context data + + metadata: Additional metadata for the context + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/api/v1/context/add-async", + body=maybe_transform( + { + "context_type": context_type, + "documents": documents, + "scope": scope, + "source": source, + "metadata": metadata, + }, + add_async_create_params.AddAsyncCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AddAsyncCreateResponse, + ) + + def cancel( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AddAsyncCancelResponse: + """ + Attempts to cancel a context add job by job id. + + - If the job is already completed or failed, returns 404. + - If the job is currently running ("active"), returns 409 and cannot be + cancelled. + - Only jobs in "waiting" or "delayed" state can be cancelled. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._delete( + f"/api/v1/context/add-async/{id}/cancel", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AddAsyncCancelResponse, + ) + + +class AsyncAddAsyncResource(AsyncAPIResource): + @cached_property + def status(self) -> AsyncStatusResource: + return AsyncStatusResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncAddAsyncResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Alchemyst-ai/alchemyst-sdk-python#accessing-raw-response-data-eg-headers + """ + return AsyncAddAsyncResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAddAsyncResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Alchemyst-ai/alchemyst-sdk-python#with_streaming_response + """ + return AsyncAddAsyncResourceWithStreamingResponse(self) + + async def create( + self, + *, + context_type: Literal["resource", "conversation", "instruction"], + documents: Iterable[add_async_create_params.Document], + scope: Literal["internal", "external"], + source: str, + metadata: add_async_create_params.Metadata | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AddAsyncCreateResponse: + """ + This endpoint accepts context data and queues it for asynchronous processing by + the context processor. It returns a success or error response depending on the + queuing result. + + Args: + context_type: Type of context being added + + documents: Array of documents with content and additional metadata + + scope: Scope of the context + + source: The source of the context data + + metadata: Additional metadata for the context + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/api/v1/context/add-async", + body=await async_maybe_transform( + { + "context_type": context_type, + "documents": documents, + "scope": scope, + "source": source, + "metadata": metadata, + }, + add_async_create_params.AddAsyncCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AddAsyncCreateResponse, + ) + + async def cancel( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AddAsyncCancelResponse: + """ + Attempts to cancel a context add job by job id. + + - If the job is already completed or failed, returns 404. + - If the job is currently running ("active"), returns 409 and cannot be + cancelled. + - Only jobs in "waiting" or "delayed" state can be cancelled. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._delete( + f"/api/v1/context/add-async/{id}/cancel", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AddAsyncCancelResponse, + ) + + +class AddAsyncResourceWithRawResponse: + def __init__(self, add_async: AddAsyncResource) -> None: + self._add_async = add_async + + self.create = to_raw_response_wrapper( + add_async.create, + ) + self.cancel = to_raw_response_wrapper( + add_async.cancel, + ) + + @cached_property + def status(self) -> StatusResourceWithRawResponse: + return StatusResourceWithRawResponse(self._add_async.status) + + +class AsyncAddAsyncResourceWithRawResponse: + def __init__(self, add_async: AsyncAddAsyncResource) -> None: + self._add_async = add_async + + self.create = async_to_raw_response_wrapper( + add_async.create, + ) + self.cancel = async_to_raw_response_wrapper( + add_async.cancel, + ) + + @cached_property + def status(self) -> AsyncStatusResourceWithRawResponse: + return AsyncStatusResourceWithRawResponse(self._add_async.status) + + +class AddAsyncResourceWithStreamingResponse: + def __init__(self, add_async: AddAsyncResource) -> None: + self._add_async = add_async + + self.create = to_streamed_response_wrapper( + add_async.create, + ) + self.cancel = to_streamed_response_wrapper( + add_async.cancel, + ) + + @cached_property + def status(self) -> StatusResourceWithStreamingResponse: + return StatusResourceWithStreamingResponse(self._add_async.status) + + +class AsyncAddAsyncResourceWithStreamingResponse: + def __init__(self, add_async: AsyncAddAsyncResource) -> None: + self._add_async = add_async + + self.create = async_to_streamed_response_wrapper( + add_async.create, + ) + self.cancel = async_to_streamed_response_wrapper( + add_async.cancel, + ) + + @cached_property + def status(self) -> AsyncStatusResourceWithStreamingResponse: + return AsyncStatusResourceWithStreamingResponse(self._add_async.status) diff --git a/src/alchemyst_ai/resources/v1/context/add_async/status.py b/src/alchemyst_ai/resources/v1/context/add_async/status.py new file mode 100644 index 0000000..8979ac1 --- /dev/null +++ b/src/alchemyst_ai/resources/v1/context/add_async/status.py @@ -0,0 +1,282 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +from ....._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ....._utils import maybe_transform, async_maybe_transform +from ....._compat import cached_property +from ....._resource import SyncAPIResource, AsyncAPIResource +from ....._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ....._base_client import make_request_options +from .....types.v1.context.add_async import status_list_params +from .....types.v1.context.add_async.status_list_response import StatusListResponse +from .....types.v1.context.add_async.status_retrieve_response import StatusRetrieveResponse + +__all__ = ["StatusResource", "AsyncStatusResource"] + + +class StatusResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> StatusResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Alchemyst-ai/alchemyst-sdk-python#accessing-raw-response-data-eg-headers + """ + return StatusResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> StatusResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Alchemyst-ai/alchemyst-sdk-python#with_streaming_response + """ + return StatusResourceWithStreamingResponse(self) + + def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> StatusRetrieveResponse: + """ + Returns the status and result of a context add job by job id. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/api/v1/context/add-async/{id}/status", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=StatusRetrieveResponse, + ) + + def list( + self, + *, + limit: str | Omit = omit, + offset: str | Omit = omit, + type: Literal["all", "active", "failed", "completed"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> StatusListResponse: + """ + Returns all jobs (active, waiting, delayed, failed, completed) belonging to the + authenticated user. + + Args: + limit: Maximum number of jobs to return + + offset: Number of jobs to skip before starting to collect the result set + + type: Type of jobs to list + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get( + "/api/v1/context/add-async/status", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + "type": type, + }, + status_list_params.StatusListParams, + ), + ), + cast_to=StatusListResponse, + ) + + +class AsyncStatusResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncStatusResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Alchemyst-ai/alchemyst-sdk-python#accessing-raw-response-data-eg-headers + """ + return AsyncStatusResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncStatusResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Alchemyst-ai/alchemyst-sdk-python#with_streaming_response + """ + return AsyncStatusResourceWithStreamingResponse(self) + + async def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> StatusRetrieveResponse: + """ + Returns the status and result of a context add job by job id. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/api/v1/context/add-async/{id}/status", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=StatusRetrieveResponse, + ) + + async def list( + self, + *, + limit: str | Omit = omit, + offset: str | Omit = omit, + type: Literal["all", "active", "failed", "completed"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> StatusListResponse: + """ + Returns all jobs (active, waiting, delayed, failed, completed) belonging to the + authenticated user. + + Args: + limit: Maximum number of jobs to return + + offset: Number of jobs to skip before starting to collect the result set + + type: Type of jobs to list + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._get( + "/api/v1/context/add-async/status", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "limit": limit, + "offset": offset, + "type": type, + }, + status_list_params.StatusListParams, + ), + ), + cast_to=StatusListResponse, + ) + + +class StatusResourceWithRawResponse: + def __init__(self, status: StatusResource) -> None: + self._status = status + + self.retrieve = to_raw_response_wrapper( + status.retrieve, + ) + self.list = to_raw_response_wrapper( + status.list, + ) + + +class AsyncStatusResourceWithRawResponse: + def __init__(self, status: AsyncStatusResource) -> None: + self._status = status + + self.retrieve = async_to_raw_response_wrapper( + status.retrieve, + ) + self.list = async_to_raw_response_wrapper( + status.list, + ) + + +class StatusResourceWithStreamingResponse: + def __init__(self, status: StatusResource) -> None: + self._status = status + + self.retrieve = to_streamed_response_wrapper( + status.retrieve, + ) + self.list = to_streamed_response_wrapper( + status.list, + ) + + +class AsyncStatusResourceWithStreamingResponse: + def __init__(self, status: AsyncStatusResource) -> None: + self._status = status + + self.retrieve = async_to_streamed_response_wrapper( + status.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + status.list, + ) diff --git a/src/alchemyst_ai/resources/v1/context/context.py b/src/alchemyst_ai/resources/v1/context/context.py index 359c195..ccdc934 100644 --- a/src/alchemyst_ai/resources/v1/context/context.py +++ b/src/alchemyst_ai/resources/v1/context/context.py @@ -43,6 +43,14 @@ async_to_streamed_response_wrapper, ) from ...._base_client import make_request_options +from .add_async.add_async import ( + AddAsyncResource, + AsyncAddAsyncResource, + AddAsyncResourceWithRawResponse, + AsyncAddAsyncResourceWithRawResponse, + AddAsyncResourceWithStreamingResponse, + AsyncAddAsyncResourceWithStreamingResponse, +) from ....types.v1.context_add_response import ContextAddResponse from ....types.v1.context_search_response import ContextSearchResponse @@ -62,6 +70,10 @@ def view(self) -> ViewResource: def memory(self) -> MemoryResource: return MemoryResource(self._client) + @cached_property + def add_async(self) -> AddAsyncResource: + return AddAsyncResource(self._client) + @cached_property def with_raw_response(self) -> ContextResourceWithRawResponse: """ @@ -201,7 +213,7 @@ def search( minimum_similarity_threshold: float, query: str, similarity_threshold: float, - metadata: Literal | Omit = omit, + metadata: Literal["true", "false"] | Omit = omit, mode: Literal["fast", "standard"] | Omit = omit, body_metadata: object | Omit = omit, scope: Literal["internal", "external"] | Omit = omit, @@ -295,6 +307,10 @@ def view(self) -> AsyncViewResource: def memory(self) -> AsyncMemoryResource: return AsyncMemoryResource(self._client) + @cached_property + def add_async(self) -> AsyncAddAsyncResource: + return AsyncAddAsyncResource(self._client) + @cached_property def with_raw_response(self) -> AsyncContextResourceWithRawResponse: """ @@ -434,7 +450,7 @@ async def search( minimum_similarity_threshold: float, query: str, similarity_threshold: float, - metadata: Literal | Omit = omit, + metadata: Literal["true", "false"] | Omit = omit, mode: Literal["fast", "standard"] | Omit = omit, body_metadata: object | Omit = omit, scope: Literal["internal", "external"] | Omit = omit, @@ -541,6 +557,10 @@ def view(self) -> ViewResourceWithRawResponse: def memory(self) -> MemoryResourceWithRawResponse: return MemoryResourceWithRawResponse(self._context.memory) + @cached_property + def add_async(self) -> AddAsyncResourceWithRawResponse: + return AddAsyncResourceWithRawResponse(self._context.add_async) + class AsyncContextResourceWithRawResponse: def __init__(self, context: AsyncContextResource) -> None: @@ -568,6 +588,10 @@ def view(self) -> AsyncViewResourceWithRawResponse: def memory(self) -> AsyncMemoryResourceWithRawResponse: return AsyncMemoryResourceWithRawResponse(self._context.memory) + @cached_property + def add_async(self) -> AsyncAddAsyncResourceWithRawResponse: + return AsyncAddAsyncResourceWithRawResponse(self._context.add_async) + class ContextResourceWithStreamingResponse: def __init__(self, context: ContextResource) -> None: @@ -595,6 +619,10 @@ def view(self) -> ViewResourceWithStreamingResponse: def memory(self) -> MemoryResourceWithStreamingResponse: return MemoryResourceWithStreamingResponse(self._context.memory) + @cached_property + def add_async(self) -> AddAsyncResourceWithStreamingResponse: + return AddAsyncResourceWithStreamingResponse(self._context.add_async) + class AsyncContextResourceWithStreamingResponse: def __init__(self, context: AsyncContextResource) -> None: @@ -621,3 +649,7 @@ def view(self) -> AsyncViewResourceWithStreamingResponse: @cached_property def memory(self) -> AsyncMemoryResourceWithStreamingResponse: return AsyncMemoryResourceWithStreamingResponse(self._context.memory) + + @cached_property + def add_async(self) -> AsyncAddAsyncResourceWithStreamingResponse: + return AsyncAddAsyncResourceWithStreamingResponse(self._context.add_async) diff --git a/src/alchemyst_ai/resources/v1/context/memory.py b/src/alchemyst_ai/resources/v1/context/memory.py index 450c03d..d549d70 100644 --- a/src/alchemyst_ai/resources/v1/context/memory.py +++ b/src/alchemyst_ai/resources/v1/context/memory.py @@ -48,7 +48,7 @@ def update( self, *, contents: Iterable[memory_update_params.Content], - memory_id: str, + session_id: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -62,7 +62,7 @@ def update( Args: contents: Array of updated content objects - memory_id: The ID of the memory to update + session_id: The ID of the memory to update extra_headers: Send extra headers @@ -77,7 +77,7 @@ def update( body=maybe_transform( { "contents": contents, - "memory_id": memory_id, + "session_id": session_id, }, memory_update_params.MemoryUpdateParams, ), @@ -147,7 +147,7 @@ def add( self, *, contents: Iterable[memory_add_params.Content], - memory_id: str, + session_id: str, metadata: memory_add_params.Metadata | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -157,14 +157,16 @@ def add( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> MemoryAddResponse: """ - This endpoint adds memory context data, fetching chat history if needed. + This endpoint adds memory (chat history) as context. Args: - contents: Array of content objects with metadata + contents: Array of content objects. Each object must contain at least the 'content' field. + Additional properties are allowed. - memory_id: The ID of the memory + session_id: The ID of the session - metadata: Optional metadata with groupName defaulting to ["default"] + metadata: Optional metadata for the memory context. Defaults to ["default"] if not + provided. extra_headers: Send extra headers @@ -179,7 +181,7 @@ def add( body=maybe_transform( { "contents": contents, - "memory_id": memory_id, + "session_id": session_id, "metadata": metadata, }, memory_add_params.MemoryAddParams, @@ -215,7 +217,7 @@ async def update( self, *, contents: Iterable[memory_update_params.Content], - memory_id: str, + session_id: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -229,7 +231,7 @@ async def update( Args: contents: Array of updated content objects - memory_id: The ID of the memory to update + session_id: The ID of the memory to update extra_headers: Send extra headers @@ -244,7 +246,7 @@ async def update( body=await async_maybe_transform( { "contents": contents, - "memory_id": memory_id, + "session_id": session_id, }, memory_update_params.MemoryUpdateParams, ), @@ -314,7 +316,7 @@ async def add( self, *, contents: Iterable[memory_add_params.Content], - memory_id: str, + session_id: str, metadata: memory_add_params.Metadata | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -324,14 +326,16 @@ async def add( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> MemoryAddResponse: """ - This endpoint adds memory context data, fetching chat history if needed. + This endpoint adds memory (chat history) as context. Args: - contents: Array of content objects with metadata + contents: Array of content objects. Each object must contain at least the 'content' field. + Additional properties are allowed. - memory_id: The ID of the memory + session_id: The ID of the session - metadata: Optional metadata with groupName defaulting to ["default"] + metadata: Optional metadata for the memory context. Defaults to ["default"] if not + provided. extra_headers: Send extra headers @@ -346,7 +350,7 @@ async def add( body=await async_maybe_transform( { "contents": contents, - "memory_id": memory_id, + "session_id": session_id, "metadata": metadata, }, memory_add_params.MemoryAddParams, diff --git a/src/alchemyst_ai/types/v1/context/__init__.py b/src/alchemyst_ai/types/v1/context/__init__.py index 66caec5..971caba 100644 --- a/src/alchemyst_ai/types/v1/context/__init__.py +++ b/src/alchemyst_ai/types/v1/context/__init__.py @@ -14,3 +14,6 @@ from .trace_delete_response import TraceDeleteResponse as TraceDeleteResponse from .memory_update_response import MemoryUpdateResponse as MemoryUpdateResponse from .view_retrieve_response import ViewRetrieveResponse as ViewRetrieveResponse +from .add_async_create_params import AddAsyncCreateParams as AddAsyncCreateParams +from .add_async_cancel_response import AddAsyncCancelResponse as AddAsyncCancelResponse +from .add_async_create_response import AddAsyncCreateResponse as AddAsyncCreateResponse diff --git a/src/alchemyst_ai/types/v1/context/add_async/__init__.py b/src/alchemyst_ai/types/v1/context/add_async/__init__.py new file mode 100644 index 0000000..8fa4546 --- /dev/null +++ b/src/alchemyst_ai/types/v1/context/add_async/__init__.py @@ -0,0 +1,7 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .status_list_params import StatusListParams as StatusListParams +from .status_list_response import StatusListResponse as StatusListResponse +from .status_retrieve_response import StatusRetrieveResponse as StatusRetrieveResponse diff --git a/src/alchemyst_ai/types/v1/context/add_async/status_list_params.py b/src/alchemyst_ai/types/v1/context/add_async/status_list_params.py new file mode 100644 index 0000000..93fd3e9 --- /dev/null +++ b/src/alchemyst_ai/types/v1/context/add_async/status_list_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["StatusListParams"] + + +class StatusListParams(TypedDict, total=False): + limit: str + """Maximum number of jobs to return""" + + offset: str + """Number of jobs to skip before starting to collect the result set""" + + type: Literal["all", "active", "failed", "completed"] + """Type of jobs to list""" diff --git a/src/alchemyst_ai/types/v1/context/add_async/status_list_response.py b/src/alchemyst_ai/types/v1/context/add_async/status_list_response.py new file mode 100644 index 0000000..1b3c4e3 --- /dev/null +++ b/src/alchemyst_ai/types/v1/context/add_async/status_list_response.py @@ -0,0 +1,31 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from pydantic import Field as FieldInfo + +from ....._models import BaseModel + +__all__ = ["StatusListResponse", "Job"] + + +class Job(BaseModel): + attempts_made: float = FieldInfo(alias="attemptsMade") + + data: object + + job_id: str = FieldInfo(alias="jobId") + + status: str + + failed_reason: Optional[str] = FieldInfo(alias="failedReason", default=None) + + finished_on: Optional[float] = FieldInfo(alias="finishedOn", default=None) + + processed_on: Optional[float] = FieldInfo(alias="processedOn", default=None) + + +class StatusListResponse(BaseModel): + jobs: List[Job] + + success: bool diff --git a/src/alchemyst_ai/types/v1/context/add_async/status_retrieve_response.py b/src/alchemyst_ai/types/v1/context/add_async/status_retrieve_response.py new file mode 100644 index 0000000..065ab0d --- /dev/null +++ b/src/alchemyst_ai/types/v1/context/add_async/status_retrieve_response.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from pydantic import Field as FieldInfo + +from ....._models import BaseModel + +__all__ = ["StatusRetrieveResponse"] + + +class StatusRetrieveResponse(BaseModel): + job_id: str = FieldInfo(alias="jobId") + + status: str + + success: bool + + attempts_made: Optional[float] = FieldInfo(alias="attemptsMade", default=None) + + failed_reason: Optional[str] = FieldInfo(alias="failedReason", default=None) + + finished_on: Optional[float] = FieldInfo(alias="finishedOn", default=None) + + processed_on: Optional[float] = FieldInfo(alias="processedOn", default=None) + + result: Optional[object] = None + """Result of the job (if available)""" diff --git a/src/alchemyst_ai/types/v1/context/add_async_cancel_response.py b/src/alchemyst_ai/types/v1/context/add_async_cancel_response.py new file mode 100644 index 0000000..33b816a --- /dev/null +++ b/src/alchemyst_ai/types/v1/context/add_async_cancel_response.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from pydantic import Field as FieldInfo + +from ...._models import BaseModel + +__all__ = ["AddAsyncCancelResponse"] + + +class AddAsyncCancelResponse(BaseModel): + job_id: str = FieldInfo(alias="jobId") + + message: str + + status: str + + success: bool diff --git a/src/alchemyst_ai/types/v1/context/add_async_create_params.py b/src/alchemyst_ai/types/v1/context/add_async_create_params.py new file mode 100644 index 0000000..12512b9 --- /dev/null +++ b/src/alchemyst_ai/types/v1/context/add_async_create_params.py @@ -0,0 +1,55 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, Union, Iterable +from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict + +from ...._types import SequenceNotStr +from ...._utils import PropertyInfo + +__all__ = ["AddAsyncCreateParams", "Document", "Metadata"] + + +class AddAsyncCreateParams(TypedDict, total=False): + context_type: Required[Literal["resource", "conversation", "instruction"]] + """Type of context being added""" + + documents: Required[Iterable[Document]] + """Array of documents with content and additional metadata""" + + scope: Required[Literal["internal", "external"]] + """Scope of the context""" + + source: Required[str] + """The source of the context data""" + + metadata: Metadata + """Additional metadata for the context""" + + +class DocumentTyped(TypedDict, total=False): + content: str + """The content of the document""" + + +Document: TypeAlias = Union[DocumentTyped, Dict[str, str]] + + +class Metadata(TypedDict, total=False): + """Additional metadata for the context""" + + file_name: Annotated[str, PropertyInfo(alias="fileName")] + """Name of the file""" + + file_size: Annotated[float, PropertyInfo(alias="fileSize")] + """Size of the file in bytes""" + + file_type: Annotated[str, PropertyInfo(alias="fileType")] + """Type/MIME of the file""" + + group_name: Annotated[SequenceNotStr[str], PropertyInfo(alias="groupName")] + """Array of Group Name to which the file belongs to""" + + last_modified: Annotated[str, PropertyInfo(alias="lastModified")] + """Last modified timestamp""" diff --git a/src/alchemyst_ai/types/v1/context/add_async_create_response.py b/src/alchemyst_ai/types/v1/context/add_async_create_response.py new file mode 100644 index 0000000..484f567 --- /dev/null +++ b/src/alchemyst_ai/types/v1/context/add_async_create_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from pydantic import Field as FieldInfo + +from ...._models import BaseModel + +__all__ = ["AddAsyncCreateResponse"] + + +class AddAsyncCreateResponse(BaseModel): + job_id: str = FieldInfo(alias="jobId") + + queued: bool diff --git a/src/alchemyst_ai/types/v1/context/memory_add_params.py b/src/alchemyst_ai/types/v1/context/memory_add_params.py index 6688f40..bdfe560 100644 --- a/src/alchemyst_ai/types/v1/context/memory_add_params.py +++ b/src/alchemyst_ai/types/v1/context/memory_add_params.py @@ -13,38 +13,51 @@ class MemoryAddParams(TypedDict, total=False): contents: Required[Iterable[Content]] - """Array of content objects with metadata""" + """Array of content objects. - memory_id: Required[Annotated[str, PropertyInfo(alias="memoryId")]] - """The ID of the memory""" + Each object must contain at least the 'content' field. Additional properties are + allowed. + """ + + session_id: Required[Annotated[str, PropertyInfo(alias="sessionId")]] + """The ID of the session""" metadata: Metadata - """Optional metadata with groupName defaulting to ["default"]""" + """Optional metadata for the memory context. + Defaults to ["default"] if not provided. + """ -class ContentMetadataTyped(TypedDict, total=False): - message_id: Required[Annotated[str, PropertyInfo(alias="messageId")]] - """Unique message ID""" +class ContentMetadata(TypedDict, total=False): + """Additional metadata for the message (optional)""" -ContentMetadata: TypeAlias = Union[ContentMetadataTyped, Dict[str, object]] + message_id: Annotated[str, PropertyInfo(alias="messageId")] + """Unique message ID""" class ContentTyped(TypedDict, total=False): content: Required[str] """The content of the memory message""" - metadata: Required[ContentMetadata] + metadata: ContentMetadata + """Additional metadata for the message (optional)""" Content: TypeAlias = Union[ContentTyped, Dict[str, object]] class MetadataTyped(TypedDict, total=False): - """Optional metadata with groupName defaulting to ["default"]""" + """Optional metadata for the memory context. + + Defaults to ["default"] if not provided. + """ group_name: Annotated[SequenceNotStr[str], PropertyInfo(alias="groupName")] - """Group names for the memory context""" + """Optional group names for the memory context. + + Defaults to ["default"] if not provided. + """ Metadata: TypeAlias = Union[MetadataTyped, Dict[str, object]] diff --git a/src/alchemyst_ai/types/v1/context/memory_update_params.py b/src/alchemyst_ai/types/v1/context/memory_update_params.py index a890494..d786b79 100644 --- a/src/alchemyst_ai/types/v1/context/memory_update_params.py +++ b/src/alchemyst_ai/types/v1/context/memory_update_params.py @@ -14,7 +14,7 @@ class MemoryUpdateParams(TypedDict, total=False): contents: Required[Iterable[Content]] """Array of updated content objects""" - memory_id: Required[Annotated[str, PropertyInfo(alias="memoryId")]] + session_id: Required[Annotated[str, PropertyInfo(alias="sessionId")]] """The ID of the memory to update""" diff --git a/src/alchemyst_ai/types/v1/context_search_params.py b/src/alchemyst_ai/types/v1/context_search_params.py index 789711b..a516136 100644 --- a/src/alchemyst_ai/types/v1/context_search_params.py +++ b/src/alchemyst_ai/types/v1/context_search_params.py @@ -17,7 +17,7 @@ class ContextSearchParams(TypedDict, total=False): similarity_threshold: Required[float] """Maximum similarity threshold (must be >= minimum_similarity_threshold)""" - metadata: Literal + metadata: Literal["true", "false"] """Controls whether metadata is included in the response: - metadata=true → metadata will be included in each context item in the diff --git a/tests/api_resources/v1/context/add_async/__init__.py b/tests/api_resources/v1/context/add_async/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/api_resources/v1/context/add_async/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/v1/context/add_async/test_status.py b/tests/api_resources/v1/context/add_async/test_status.py new file mode 100644 index 0000000..189c8e3 --- /dev/null +++ b/tests/api_resources/v1/context/add_async/test_status.py @@ -0,0 +1,184 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from alchemyst_ai import AlchemystAI, AsyncAlchemystAI +from alchemyst_ai.types.v1.context.add_async import StatusListResponse, StatusRetrieveResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestStatus: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_retrieve(self, client: AlchemystAI) -> None: + status = client.v1.context.add_async.status.retrieve( + "id", + ) + assert_matches_type(StatusRetrieveResponse, status, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: AlchemystAI) -> None: + response = client.v1.context.add_async.status.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + status = response.parse() + assert_matches_type(StatusRetrieveResponse, status, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: AlchemystAI) -> None: + with client.v1.context.add_async.status.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + status = response.parse() + assert_matches_type(StatusRetrieveResponse, status, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: AlchemystAI) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.v1.context.add_async.status.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_list(self, client: AlchemystAI) -> None: + status = client.v1.context.add_async.status.list() + assert_matches_type(StatusListResponse, status, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: AlchemystAI) -> None: + status = client.v1.context.add_async.status.list( + limit="limit", + offset="offset", + type="all", + ) + assert_matches_type(StatusListResponse, status, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_list(self, client: AlchemystAI) -> None: + response = client.v1.context.add_async.status.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + status = response.parse() + assert_matches_type(StatusListResponse, status, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_list(self, client: AlchemystAI) -> None: + with client.v1.context.add_async.status.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + status = response.parse() + assert_matches_type(StatusListResponse, status, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncStatus: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncAlchemystAI) -> None: + status = await async_client.v1.context.add_async.status.retrieve( + "id", + ) + assert_matches_type(StatusRetrieveResponse, status, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncAlchemystAI) -> None: + response = await async_client.v1.context.add_async.status.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + status = await response.parse() + assert_matches_type(StatusRetrieveResponse, status, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncAlchemystAI) -> None: + async with async_client.v1.context.add_async.status.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + status = await response.parse() + assert_matches_type(StatusRetrieveResponse, status, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncAlchemystAI) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.v1.context.add_async.status.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncAlchemystAI) -> None: + status = await async_client.v1.context.add_async.status.list() + assert_matches_type(StatusListResponse, status, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncAlchemystAI) -> None: + status = await async_client.v1.context.add_async.status.list( + limit="limit", + offset="offset", + type="all", + ) + assert_matches_type(StatusListResponse, status, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncAlchemystAI) -> None: + response = await async_client.v1.context.add_async.status.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + status = await response.parse() + assert_matches_type(StatusListResponse, status, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncAlchemystAI) -> None: + async with async_client.v1.context.add_async.status.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + status = await response.parse() + assert_matches_type(StatusListResponse, status, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/v1/context/test_add_async.py b/tests/api_resources/v1/context/test_add_async.py new file mode 100644 index 0000000..ddf8684 --- /dev/null +++ b/tests/api_resources/v1/context/test_add_async.py @@ -0,0 +1,230 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from alchemyst_ai import AlchemystAI, AsyncAlchemystAI +from alchemyst_ai.types.v1.context import AddAsyncCancelResponse, AddAsyncCreateResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestAddAsync: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_create(self, client: AlchemystAI) -> None: + add_async = client.v1.context.add_async.create( + context_type="resource", + documents=[{}], + scope="internal", + source="support-inbox", + ) + assert_matches_type(AddAsyncCreateResponse, add_async, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: AlchemystAI) -> None: + add_async = client.v1.context.add_async.create( + context_type="resource", + documents=[{"content": "Customer asked about pricing for the Scale plan."}], + scope="internal", + source="support-inbox", + metadata={ + "file_name": "support_thread_TCK-1234.txt", + "file_size": 2048, + "file_type": "text/plain", + "group_name": ["support", "pricing"], + "last_modified": "2025-01-10T12:34:56.000Z", + }, + ) + assert_matches_type(AddAsyncCreateResponse, add_async, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_create(self, client: AlchemystAI) -> None: + response = client.v1.context.add_async.with_raw_response.create( + context_type="resource", + documents=[{}], + scope="internal", + source="support-inbox", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + add_async = response.parse() + assert_matches_type(AddAsyncCreateResponse, add_async, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_create(self, client: AlchemystAI) -> None: + with client.v1.context.add_async.with_streaming_response.create( + context_type="resource", + documents=[{}], + scope="internal", + source="support-inbox", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + add_async = response.parse() + assert_matches_type(AddAsyncCreateResponse, add_async, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_cancel(self, client: AlchemystAI) -> None: + add_async = client.v1.context.add_async.cancel( + "id", + ) + assert_matches_type(AddAsyncCancelResponse, add_async, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_cancel(self, client: AlchemystAI) -> None: + response = client.v1.context.add_async.with_raw_response.cancel( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + add_async = response.parse() + assert_matches_type(AddAsyncCancelResponse, add_async, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_cancel(self, client: AlchemystAI) -> None: + with client.v1.context.add_async.with_streaming_response.cancel( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + add_async = response.parse() + assert_matches_type(AddAsyncCancelResponse, add_async, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_cancel(self, client: AlchemystAI) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.v1.context.add_async.with_raw_response.cancel( + "", + ) + + +class TestAsyncAddAsync: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_create(self, async_client: AsyncAlchemystAI) -> None: + add_async = await async_client.v1.context.add_async.create( + context_type="resource", + documents=[{}], + scope="internal", + source="support-inbox", + ) + assert_matches_type(AddAsyncCreateResponse, add_async, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncAlchemystAI) -> None: + add_async = await async_client.v1.context.add_async.create( + context_type="resource", + documents=[{"content": "Customer asked about pricing for the Scale plan."}], + scope="internal", + source="support-inbox", + metadata={ + "file_name": "support_thread_TCK-1234.txt", + "file_size": 2048, + "file_type": "text/plain", + "group_name": ["support", "pricing"], + "last_modified": "2025-01-10T12:34:56.000Z", + }, + ) + assert_matches_type(AddAsyncCreateResponse, add_async, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncAlchemystAI) -> None: + response = await async_client.v1.context.add_async.with_raw_response.create( + context_type="resource", + documents=[{}], + scope="internal", + source="support-inbox", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + add_async = await response.parse() + assert_matches_type(AddAsyncCreateResponse, add_async, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncAlchemystAI) -> None: + async with async_client.v1.context.add_async.with_streaming_response.create( + context_type="resource", + documents=[{}], + scope="internal", + source="support-inbox", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + add_async = await response.parse() + assert_matches_type(AddAsyncCreateResponse, add_async, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_cancel(self, async_client: AsyncAlchemystAI) -> None: + add_async = await async_client.v1.context.add_async.cancel( + "id", + ) + assert_matches_type(AddAsyncCancelResponse, add_async, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_cancel(self, async_client: AsyncAlchemystAI) -> None: + response = await async_client.v1.context.add_async.with_raw_response.cancel( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + add_async = await response.parse() + assert_matches_type(AddAsyncCancelResponse, add_async, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_cancel(self, async_client: AsyncAlchemystAI) -> None: + async with async_client.v1.context.add_async.with_streaming_response.cancel( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + add_async = await response.parse() + assert_matches_type(AddAsyncCancelResponse, add_async, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_cancel(self, async_client: AsyncAlchemystAI) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.v1.context.add_async.with_raw_response.cancel( + "", + ) diff --git a/tests/api_resources/v1/context/test_memory.py b/tests/api_resources/v1/context/test_memory.py index 68d7261..d1fbafc 100644 --- a/tests/api_resources/v1/context/test_memory.py +++ b/tests/api_resources/v1/context/test_memory.py @@ -25,7 +25,7 @@ class TestMemory: def test_method_update(self, client: AlchemystAI) -> None: memory = client.v1.context.memory.update( contents=[{}, {}], - memory_id="support-thread-TCK-1234", + session_id="support-thread-TCK-1234", ) assert_matches_type(MemoryUpdateResponse, memory, path=["response"]) @@ -34,7 +34,7 @@ def test_method_update(self, client: AlchemystAI) -> None: def test_raw_response_update(self, client: AlchemystAI) -> None: response = client.v1.context.memory.with_raw_response.update( contents=[{}, {}], - memory_id="support-thread-TCK-1234", + session_id="support-thread-TCK-1234", ) assert response.is_closed is True @@ -47,7 +47,7 @@ def test_raw_response_update(self, client: AlchemystAI) -> None: def test_streaming_response_update(self, client: AlchemystAI) -> None: with client.v1.context.memory.with_streaming_response.update( contents=[{}, {}], - memory_id="support-thread-TCK-1234", + session_id="support-thread-TCK-1234", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -110,17 +110,8 @@ def test_streaming_response_delete(self, client: AlchemystAI) -> None: @parametrize def test_method_add(self, client: AlchemystAI) -> None: memory = client.v1.context.memory.add( - contents=[ - { - "content": "Customer asked about pricing for the Scale plan.", - "metadata": {"message_id": "msg-1"}, - }, - { - "content": "Explained the Scale plan pricing and shared the pricing page link.", - "metadata": {"message_id": "msg-2"}, - }, - ], - memory_id="support-thread-TCK-1234", + contents=[{"content": "Customer asked about pricing for the Scale plan."}], + session_id="support-thread-TCK-1234", ) assert_matches_type(MemoryAddResponse, memory, path=["response"]) @@ -131,15 +122,11 @@ def test_method_add_with_all_params(self, client: AlchemystAI) -> None: contents=[ { "content": "Customer asked about pricing for the Scale plan.", - "metadata": {"message_id": "msg-1"}, - }, - { - "content": "Explained the Scale plan pricing and shared the pricing page link.", - "metadata": {"message_id": "msg-2"}, - }, + "metadata": {"message_id": "messageId"}, + } ], - memory_id="support-thread-TCK-1234", - metadata={"group_name": ["support", "pricing"]}, + session_id="support-thread-TCK-1234", + metadata={"group_name": ["string"]}, ) assert_matches_type(MemoryAddResponse, memory, path=["response"]) @@ -147,17 +134,8 @@ def test_method_add_with_all_params(self, client: AlchemystAI) -> None: @parametrize def test_raw_response_add(self, client: AlchemystAI) -> None: response = client.v1.context.memory.with_raw_response.add( - contents=[ - { - "content": "Customer asked about pricing for the Scale plan.", - "metadata": {"message_id": "msg-1"}, - }, - { - "content": "Explained the Scale plan pricing and shared the pricing page link.", - "metadata": {"message_id": "msg-2"}, - }, - ], - memory_id="support-thread-TCK-1234", + contents=[{"content": "Customer asked about pricing for the Scale plan."}], + session_id="support-thread-TCK-1234", ) assert response.is_closed is True @@ -169,17 +147,8 @@ def test_raw_response_add(self, client: AlchemystAI) -> None: @parametrize def test_streaming_response_add(self, client: AlchemystAI) -> None: with client.v1.context.memory.with_streaming_response.add( - contents=[ - { - "content": "Customer asked about pricing for the Scale plan.", - "metadata": {"message_id": "msg-1"}, - }, - { - "content": "Explained the Scale plan pricing and shared the pricing page link.", - "metadata": {"message_id": "msg-2"}, - }, - ], - memory_id="support-thread-TCK-1234", + contents=[{"content": "Customer asked about pricing for the Scale plan."}], + session_id="support-thread-TCK-1234", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -200,7 +169,7 @@ class TestAsyncMemory: async def test_method_update(self, async_client: AsyncAlchemystAI) -> None: memory = await async_client.v1.context.memory.update( contents=[{}, {}], - memory_id="support-thread-TCK-1234", + session_id="support-thread-TCK-1234", ) assert_matches_type(MemoryUpdateResponse, memory, path=["response"]) @@ -209,7 +178,7 @@ async def test_method_update(self, async_client: AsyncAlchemystAI) -> None: async def test_raw_response_update(self, async_client: AsyncAlchemystAI) -> None: response = await async_client.v1.context.memory.with_raw_response.update( contents=[{}, {}], - memory_id="support-thread-TCK-1234", + session_id="support-thread-TCK-1234", ) assert response.is_closed is True @@ -222,7 +191,7 @@ async def test_raw_response_update(self, async_client: AsyncAlchemystAI) -> None async def test_streaming_response_update(self, async_client: AsyncAlchemystAI) -> None: async with async_client.v1.context.memory.with_streaming_response.update( contents=[{}, {}], - memory_id="support-thread-TCK-1234", + session_id="support-thread-TCK-1234", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -285,17 +254,8 @@ async def test_streaming_response_delete(self, async_client: AsyncAlchemystAI) - @parametrize async def test_method_add(self, async_client: AsyncAlchemystAI) -> None: memory = await async_client.v1.context.memory.add( - contents=[ - { - "content": "Customer asked about pricing for the Scale plan.", - "metadata": {"message_id": "msg-1"}, - }, - { - "content": "Explained the Scale plan pricing and shared the pricing page link.", - "metadata": {"message_id": "msg-2"}, - }, - ], - memory_id="support-thread-TCK-1234", + contents=[{"content": "Customer asked about pricing for the Scale plan."}], + session_id="support-thread-TCK-1234", ) assert_matches_type(MemoryAddResponse, memory, path=["response"]) @@ -306,15 +266,11 @@ async def test_method_add_with_all_params(self, async_client: AsyncAlchemystAI) contents=[ { "content": "Customer asked about pricing for the Scale plan.", - "metadata": {"message_id": "msg-1"}, - }, - { - "content": "Explained the Scale plan pricing and shared the pricing page link.", - "metadata": {"message_id": "msg-2"}, - }, + "metadata": {"message_id": "messageId"}, + } ], - memory_id="support-thread-TCK-1234", - metadata={"group_name": ["support", "pricing"]}, + session_id="support-thread-TCK-1234", + metadata={"group_name": ["string"]}, ) assert_matches_type(MemoryAddResponse, memory, path=["response"]) @@ -322,17 +278,8 @@ async def test_method_add_with_all_params(self, async_client: AsyncAlchemystAI) @parametrize async def test_raw_response_add(self, async_client: AsyncAlchemystAI) -> None: response = await async_client.v1.context.memory.with_raw_response.add( - contents=[ - { - "content": "Customer asked about pricing for the Scale plan.", - "metadata": {"message_id": "msg-1"}, - }, - { - "content": "Explained the Scale plan pricing and shared the pricing page link.", - "metadata": {"message_id": "msg-2"}, - }, - ], - memory_id="support-thread-TCK-1234", + contents=[{"content": "Customer asked about pricing for the Scale plan."}], + session_id="support-thread-TCK-1234", ) assert response.is_closed is True @@ -344,17 +291,8 @@ async def test_raw_response_add(self, async_client: AsyncAlchemystAI) -> None: @parametrize async def test_streaming_response_add(self, async_client: AsyncAlchemystAI) -> None: async with async_client.v1.context.memory.with_streaming_response.add( - contents=[ - { - "content": "Customer asked about pricing for the Scale plan.", - "metadata": {"message_id": "msg-1"}, - }, - { - "content": "Explained the Scale plan pricing and shared the pricing page link.", - "metadata": {"message_id": "msg-2"}, - }, - ], - memory_id="support-thread-TCK-1234", + contents=[{"content": "Customer asked about pricing for the Scale plan."}], + session_id="support-thread-TCK-1234", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/api_resources/v1/test_context.py b/tests/api_resources/v1/test_context.py index 3584510..98aad82 100644 --- a/tests/api_resources/v1/test_context.py +++ b/tests/api_resources/v1/test_context.py @@ -147,7 +147,7 @@ def test_method_search_with_all_params(self, client: AlchemystAI) -> None: minimum_similarity_threshold=0.5, query="What did the customer ask about pricing for the Scale plan?", similarity_threshold=0.8, - metadata=None, + metadata="true", mode="fast", body_metadata={}, scope="internal", @@ -318,7 +318,7 @@ async def test_method_search_with_all_params(self, async_client: AsyncAlchemystA minimum_similarity_threshold=0.5, query="What did the customer ask about pricing for the Scale plan?", similarity_threshold=0.8, - metadata=None, + metadata="true", mode="fast", body_metadata={}, scope="internal", diff --git a/tests/test_client.py b/tests/test_client.py index 9601eca..2df3827 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -8,10 +8,11 @@ import json import asyncio import inspect +import dataclasses import tracemalloc -from typing import Any, Union, cast +from typing import Any, Union, TypeVar, Callable, Iterable, Iterator, Optional, Coroutine, cast from unittest import mock -from typing_extensions import Literal +from typing_extensions import Literal, AsyncIterator, override import httpx import pytest @@ -36,6 +37,7 @@ from .utils import update_env +T = TypeVar("T") base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") api_key = "My API Key" @@ -50,6 +52,57 @@ def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float: return 0.1 +def mirror_request_content(request: httpx.Request) -> httpx.Response: + return httpx.Response(200, content=request.content) + + +# note: we can't use the httpx.MockTransport class as it consumes the request +# body itself, which means we can't test that the body is read lazily +class MockTransport(httpx.BaseTransport, httpx.AsyncBaseTransport): + def __init__( + self, + handler: Callable[[httpx.Request], httpx.Response] + | Callable[[httpx.Request], Coroutine[Any, Any, httpx.Response]], + ) -> None: + self.handler = handler + + @override + def handle_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert not inspect.iscoroutinefunction(self.handler), "handler must not be a coroutine function" + assert inspect.isfunction(self.handler), "handler must be a function" + return self.handler(request) + + @override + async def handle_async_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert inspect.iscoroutinefunction(self.handler), "handler must be a coroutine function" + return await self.handler(request) + + +@dataclasses.dataclass +class Counter: + value: int = 0 + + +def _make_sync_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> Iterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + +async def _make_async_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> AsyncIterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + def _get_open_connections(client: AlchemystAI | AsyncAlchemystAI) -> int: transport = client._client._transport assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) @@ -511,6 +564,70 @@ def test_multipart_repeating_array(self, client: AlchemystAI) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload(self, respx_mock: MockRouter, client: AlchemystAI) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + def test_binary_content_upload_with_iterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_sync_iterator([file_content], counter=counter) + + def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=request.read()) + + with AlchemystAI( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.Client(transport=MockTransport(handler=mock_handler)), + ) as client: + response = client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload_with_body_is_deprecated(self, respx_mock: MockRouter, client: AlchemystAI) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) def test_basic_union_response(self, respx_mock: MockRouter, client: AlchemystAI) -> None: class Model1(BaseModel): @@ -1367,6 +1484,72 @@ def test_multipart_repeating_array(self, async_client: AsyncAlchemystAI) -> None b"", ] + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncAlchemystAI) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = await async_client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + async def test_binary_content_upload_with_asynciterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_async_iterator([file_content], counter=counter) + + async def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=await request.aread()) + + async with AsyncAlchemystAI( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)), + ) as client: + response = await client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload_with_body_is_deprecated( + self, respx_mock: MockRouter, async_client: AsyncAlchemystAI + ) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = await async_client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncAlchemystAI) -> None: class Model1(BaseModel):