From 4bc12e4d6bd408e6256f2e6e5539fd8feb29fab2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 3 Apr 2025 18:40:58 +0000 Subject: [PATCH 01/15] chore(internal): version bump (#99) From 5675c9efbda688fa1799f1e48f64c20143fcf410 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 17:29:48 +0000 Subject: [PATCH 02/15] feat(api): api update (#102) --- .stats.yml | 2 +- src/codex/resources/projects/clusters.py | 6 +++--- src/codex/types/projects/cluster_list_params.py | 4 ++-- src/codex/types/projects/entry.py | 3 +++ 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.stats.yml b/.stats.yml index c22765c..be1ee82 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,3 +1,3 @@ configured_endpoints: 36 -openapi_spec_hash: 6a5cfa54c19b354978b1654c152b0431 +openapi_spec_hash: 1850a850b7e27992c6e47190db9add88 config_hash: adbedb6317fca6f566f54564cc341846 diff --git a/src/codex/resources/projects/clusters.py b/src/codex/resources/projects/clusters.py index 54b6c4c..2faed31 100644 --- a/src/codex/resources/projects/clusters.py +++ b/src/codex/resources/projects/clusters.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List +from typing import List, Optional from typing_extensions import Literal import httpx @@ -53,7 +53,7 @@ def list( limit: int | NotGiven = NOT_GIVEN, offset: int | NotGiven = NOT_GIVEN, order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, - sort: Literal["created_at", "answered_at", "cluster_frequency_count"] | NotGiven = NOT_GIVEN, + sort: Optional[Literal["created_at", "answered_at", "cluster_frequency_count"]] | NotGiven = NOT_GIVEN, states: List[Literal["unanswered", "draft", "published", "published_with_draft"]] | NotGiven = NOT_GIVEN, # 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. @@ -164,7 +164,7 @@ def list( limit: int | NotGiven = NOT_GIVEN, offset: int | NotGiven = NOT_GIVEN, order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, - sort: Literal["created_at", "answered_at", "cluster_frequency_count"] | NotGiven = NOT_GIVEN, + sort: Optional[Literal["created_at", "answered_at", "cluster_frequency_count"]] | NotGiven = NOT_GIVEN, states: List[Literal["unanswered", "draft", "published", "published_with_draft"]] | NotGiven = NOT_GIVEN, # 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. diff --git a/src/codex/types/projects/cluster_list_params.py b/src/codex/types/projects/cluster_list_params.py index 438b481..4889324 100644 --- a/src/codex/types/projects/cluster_list_params.py +++ b/src/codex/types/projects/cluster_list_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List +from typing import List, Optional from typing_extensions import Literal, TypedDict __all__ = ["ClusterListParams"] @@ -15,6 +15,6 @@ class ClusterListParams(TypedDict, total=False): order: Literal["asc", "desc"] - sort: Literal["created_at", "answered_at", "cluster_frequency_count"] + sort: Optional[Literal["created_at", "answered_at", "cluster_frequency_count"]] states: List[Literal["unanswered", "draft", "published", "published_with_draft"]] diff --git a/src/codex/types/projects/entry.py b/src/codex/types/projects/entry.py index 77e2ca3..b8eeb0e 100644 --- a/src/codex/types/projects/entry.py +++ b/src/codex/types/projects/entry.py @@ -29,3 +29,6 @@ class Entry(BaseModel): draft_answer: Optional[str] = None draft_answer_last_edited: Optional[datetime] = None + + frequency_count: Optional[int] = None + """number of times the entry matched for a /query request""" From 45f7fde22223d99bec5ab8142b59f5124b275c28 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 19:58:07 +0000 Subject: [PATCH 03/15] chore(internal): remove trailing character (#103) --- tests/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_client.py b/tests/test_client.py index 0b0b783..2d356fa 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1577,7 +1577,7 @@ def test_get_platform(self) -> None: import threading from codex._utils import asyncify - from codex._base_client import get_platform + from codex._base_client import get_platform async def test_main() -> None: result = await asyncify(get_platform)() From 404e013054df741a1d3c77597f35030cba080b82 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 15 Apr 2025 22:56:16 +0000 Subject: [PATCH 04/15] feat(api): api update (#104) --- .stats.yml | 2 +- README.md | 445 +++++++++++++++++- src/codex/resources/projects/entries.py | 16 +- src/codex/types/project_create_params.py | 12 + src/codex/types/project_list_response.py | 12 + src/codex/types/project_return_schema.py | 12 + src/codex/types/project_update_params.py | 12 + .../types/projects/entry_query_params.py | 2 + tests/api_resources/projects/test_entries.py | 2 + tests/api_resources/test_projects.py | 40 +- 10 files changed, 547 insertions(+), 8 deletions(-) diff --git a/.stats.yml b/.stats.yml index be1ee82..69bb6fc 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,3 +1,3 @@ configured_endpoints: 36 -openapi_spec_hash: 1850a850b7e27992c6e47190db9add88 +openapi_spec_hash: 8ef89533cd58e3b2ceb53a877832f48b config_hash: adbedb6317fca6f566f54564cc341846 diff --git a/README.md b/README.md index 434ebb1..1042b46 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,447 @@ [![PyPI version](https://img.shields.io/pypi/v/codex-sdk.svg)](https://pypi.org/project/codex-sdk/) -This library is not meant to be used directly. Refer to https://pypi.org/project/cleanlab-codex/ instead. +The Codex SDK library provides convenient access to the Codex REST API from any Python 3.8+ +application. The library includes type definitions for all request params and response fields, +and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). + +It is generated with [Stainless](https://www.stainless.com/). + +## Documentation + +The REST API documentation can be found on [help.cleanlab.ai](https://help.cleanlab.ai). The full API of this library can be found in [api.md](api.md). + +## Installation + +```sh +# install from PyPI +pip install --pre codex-sdk +``` + +## Usage + +The full API of this library can be found in [api.md](api.md). + +```python +from codex import Codex + +client = Codex( + # or 'production' | 'local'; defaults to "production". + environment="staging", +) + +project_return_schema = client.projects.create( + config={}, + name="name", + organization_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", +) +print(project_return_schema.id) +``` + +## Async usage + +Simply import `AsyncCodex` instead of `Codex` and use `await` with each API call: + +```python +import asyncio +from codex import AsyncCodex + +client = AsyncCodex( + # or 'production' | 'local'; defaults to "production". + environment="staging", +) + + +async def main() -> None: + project_return_schema = await client.projects.create( + config={}, + name="name", + organization_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + print(project_return_schema.id) + + +asyncio.run(main()) +``` + +Functionality between the synchronous and asynchronous clients is otherwise identical. + +## Using types + +Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like: + +- Serializing back into JSON, `model.to_json()` +- Converting to a dictionary, `model.to_dict()` + +Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. + +## Pagination + +List methods in the Codex API are paginated. + +This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually: + +```python +from codex import Codex + +client = Codex() + +all_clusters = [] +# Automatically fetches more pages as needed. +for cluster in client.projects.clusters.list( + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", +): + # Do something with cluster here + all_clusters.append(cluster) +print(all_clusters) +``` + +Or, asynchronously: + +```python +import asyncio +from codex import AsyncCodex + +client = AsyncCodex() + + +async def main() -> None: + all_clusters = [] + # Iterate through items across all pages, issuing requests as needed. + async for cluster in client.projects.clusters.list( + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ): + all_clusters.append(cluster) + print(all_clusters) + + +asyncio.run(main()) +``` + +Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages: + +```python +first_page = await client.projects.clusters.list( + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", +) +if first_page.has_next_page(): + print(f"will fetch next page using these details: {first_page.next_page_info()}") + next_page = await first_page.get_next_page() + print(f"number of items we just fetched: {len(next_page.clusters)}") + +# Remove `await` for non-async usage. +``` + +Or just work directly with the returned data: + +```python +first_page = await client.projects.clusters.list( + project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", +) +for cluster in first_page.clusters: + print(cluster.id) + +# Remove `await` for non-async usage. +``` + +## Nested params + +Nested parameters are dictionaries, typed using `TypedDict`, for example: + +```python +from codex import Codex + +client = Codex() + +project_return_schema = client.projects.create( + config={ + "clustering_use_llm_matching": True, + "llm_matching_model": "llm_matching_model", + "llm_matching_quality_preset": "llm_matching_quality_preset", + "lower_llm_match_distance_threshold": 0, + "max_distance": 0, + "query_use_llm_matching": True, + "upper_llm_match_distance_threshold": 0, + }, + name="name", + organization_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", +) +print(project_return_schema.config) +``` + +## Handling errors + +When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `codex.APIConnectionError` is raised. + +When the API returns a non-success status code (that is, 4xx or 5xx +response), a subclass of `codex.APIStatusError` is raised, containing `status_code` and `response` properties. + +All errors inherit from `codex.APIError`. + +```python +import codex +from codex import Codex + +client = Codex() + +try: + client.projects.create( + config={}, + name="name", + organization_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) +except codex.APIConnectionError as e: + print("The server could not be reached") + print(e.__cause__) # an underlying Exception, likely raised within httpx. +except codex.RateLimitError as e: + print("A 429 status code was received; we should back off a bit.") +except codex.APIStatusError as e: + print("Another non-200-range status code was received") + print(e.status_code) + print(e.response) +``` + +Error codes are as follows: + +| Status Code | Error Type | +| ----------- | -------------------------- | +| 400 | `BadRequestError` | +| 401 | `AuthenticationError` | +| 403 | `PermissionDeniedError` | +| 404 | `NotFoundError` | +| 422 | `UnprocessableEntityError` | +| 429 | `RateLimitError` | +| >=500 | `InternalServerError` | +| N/A | `APIConnectionError` | + +### Retries + +Certain errors are automatically retried 2 times by default, with a short exponential backoff. +Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, +429 Rate Limit, and >=500 Internal errors are all retried by default. + +You can use the `max_retries` option to configure or disable retry settings: + +```python +from codex import Codex + +# Configure the default for all requests: +client = Codex( + # default is 2 + max_retries=0, +) + +# Or, configure per-request: +client.with_options(max_retries=5).projects.create( + config={}, + name="name", + organization_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", +) +``` + +### Timeouts + +By default requests time out after 1 minute. You can configure this with a `timeout` option, +which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/#fine-tuning-the-configuration) object: + +```python +from codex import Codex + +# Configure the default for all requests: +client = Codex( + # 20 seconds (default is 1 minute) + timeout=20.0, +) + +# More granular control: +client = Codex( + timeout=httpx.Timeout(60.0, read=5.0, write=10.0, connect=2.0), +) + +# Override per-request: +client.with_options(timeout=5.0).projects.create( + config={}, + name="name", + organization_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", +) +``` + +On timeout, an `APITimeoutError` is thrown. + +Note that requests that time out are [retried twice by default](#retries). + +## Advanced + +### Logging + +We use the standard library [`logging`](https://docs.python.org/3/library/logging.html) module. + +You can enable logging by setting the environment variable `CODEX_LOG` to `info`. + +```shell +$ export CODEX_LOG=info +``` + +Or to `debug` for more verbose logging. + +### How to tell whether `None` means `null` or missing + +In an API response, a field may be explicitly `null`, or missing entirely; in either case, its value is `None` in this library. You can differentiate the two cases with `.model_fields_set`: + +```py +if response.my_field is None: + if 'my_field' not in response.model_fields_set: + print('Got json like {}, without a "my_field" key present at all.') + else: + print('Got json like {"my_field": null}.') +``` + +### Accessing raw response data (e.g. headers) + +The "raw" Response object can be accessed by prefixing `.with_raw_response.` to any HTTP method call, e.g., + +```py +from codex import Codex + +client = Codex() +response = client.projects.with_raw_response.create( + config={}, + name="name", + organization_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", +) +print(response.headers.get('X-My-Header')) + +project = response.parse() # get the object that `projects.create()` would have returned +print(project.id) +``` + +These methods return an [`APIResponse`](https://github.com/cleanlab/codex-python/tree/main/src/codex/_response.py) object. + +The async client returns an [`AsyncAPIResponse`](https://github.com/cleanlab/codex-python/tree/main/src/codex/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. + +#### `.with_streaming_response` + +The above interface eagerly reads the full response body when you make the request, which may not always be what you want. + +To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. + +```python +with client.projects.with_streaming_response.create( + config={}, + name="name", + organization_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", +) as response: + print(response.headers.get("X-My-Header")) + + for line in response.iter_lines(): + print(line) +``` + +The context manager is required so that the response will reliably be closed. + +### Making custom/undocumented requests + +This library is typed for convenient access to the documented API. + +If you need to access undocumented endpoints, params, or response properties, the library can still be used. + +#### Undocumented endpoints + +To make requests to undocumented endpoints, you can make requests using `client.get`, `client.post`, and other +http verbs. Options on the client will be respected (such as retries) when making this request. + +```py +import httpx + +response = client.post( + "/foo", + cast_to=httpx.Response, + body={"my_param": True}, +) + +print(response.headers.get("x-foo")) +``` + +#### Undocumented request params + +If you want to explicitly send an extra param, you can do so with the `extra_query`, `extra_body`, and `extra_headers` request +options. + +#### Undocumented response properties + +To access undocumented response properties, you can access the extra fields like `response.unknown_prop`. You +can also get all the extra fields on the Pydantic model as a dict with +[`response.model_extra`](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel.model_extra). + +### Configuring the HTTP client + +You can directly override the [httpx client](https://www.python-httpx.org/api/#client) to customize it for your use case, including: + +- Support for [proxies](https://www.python-httpx.org/advanced/proxies/) +- Custom [transports](https://www.python-httpx.org/advanced/transports/) +- Additional [advanced](https://www.python-httpx.org/advanced/clients/) functionality + +```python +import httpx +from codex import Codex, DefaultHttpxClient + +client = Codex( + # Or use the `CODEX_BASE_URL` env var + base_url="http://my.test.server.example.com:8083", + http_client=DefaultHttpxClient( + proxy="http://my.test.proxy.example.com", + transport=httpx.HTTPTransport(local_address="0.0.0.0"), + ), +) +``` + +You can also customize the client on a per-request basis by using `with_options()`: + +```python +client.with_options(http_client=DefaultHttpxClient(...)) +``` + +### Managing HTTP resources + +By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting. + +```py +from codex import Codex + +with Codex() as client: + # make requests here + ... + +# HTTP client is now closed +``` + +## Versioning + +This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: + +1. Changes that only affect static types, without breaking runtime behavior. +2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_ +3. Changes that we do not expect to impact the vast majority of users in practice. + +We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. + +We are keen for your feedback; please open an [issue](https://www.github.com/cleanlab/codex-python/issues) with questions, bugs, or suggestions. + +### Determining the installed version + +If you've upgraded to the latest version but aren't seeing any new features you were expecting then your python environment is likely still using an older version. + +You can determine the version that is being used at runtime with: + +```py +import codex +print(codex.__version__) +``` + +## Requirements + +Python 3.8 or higher. + +## Contributing + +See [the contributing documentation](./CONTRIBUTING.md). diff --git a/src/codex/resources/projects/entries.py b/src/codex/resources/projects/entries.py index 7df9f1f..f0fa39c 100644 --- a/src/codex/resources/projects/entries.py +++ b/src/codex/resources/projects/entries.py @@ -234,6 +234,7 @@ def query( project_id: str, *, question: str, + use_llm_matching: bool | NotGiven = NOT_GIVEN, client_metadata: Optional[object] | NotGiven = NOT_GIVEN, x_client_library_version: str | NotGiven = NOT_GIVEN, x_integration_type: str | NotGiven = NOT_GIVEN, @@ -281,7 +282,11 @@ def query( entry_query_params.EntryQueryParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"use_llm_matching": use_llm_matching}, entry_query_params.EntryQueryParams), ), cast_to=EntryQueryResponse, ) @@ -493,6 +498,7 @@ async def query( project_id: str, *, question: str, + use_llm_matching: bool | NotGiven = NOT_GIVEN, client_metadata: Optional[object] | NotGiven = NOT_GIVEN, x_client_library_version: str | NotGiven = NOT_GIVEN, x_integration_type: str | NotGiven = NOT_GIVEN, @@ -540,7 +546,13 @@ async def query( entry_query_params.EntryQueryParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + {"use_llm_matching": use_llm_matching}, entry_query_params.EntryQueryParams + ), ), cast_to=EntryQueryResponse, ) diff --git a/src/codex/types/project_create_params.py b/src/codex/types/project_create_params.py index 80882b2..ecdd194 100644 --- a/src/codex/types/project_create_params.py +++ b/src/codex/types/project_create_params.py @@ -19,4 +19,16 @@ class ProjectCreateParams(TypedDict, total=False): class Config(TypedDict, total=False): + clustering_use_llm_matching: bool + + llm_matching_model: str + + llm_matching_quality_preset: str + + lower_llm_match_distance_threshold: float + max_distance: float + + query_use_llm_matching: bool + + upper_llm_match_distance_threshold: float diff --git a/src/codex/types/project_list_response.py b/src/codex/types/project_list_response.py index 84aafa9..2b4fec4 100644 --- a/src/codex/types/project_list_response.py +++ b/src/codex/types/project_list_response.py @@ -9,8 +9,20 @@ class ProjectConfig(BaseModel): + clustering_use_llm_matching: Optional[bool] = None + + llm_matching_model: Optional[str] = None + + llm_matching_quality_preset: Optional[str] = None + + lower_llm_match_distance_threshold: Optional[float] = None + max_distance: Optional[float] = None + query_use_llm_matching: Optional[bool] = None + + upper_llm_match_distance_threshold: Optional[float] = None + class Project(BaseModel): id: str diff --git a/src/codex/types/project_return_schema.py b/src/codex/types/project_return_schema.py index 8531272..51a6c1a 100644 --- a/src/codex/types/project_return_schema.py +++ b/src/codex/types/project_return_schema.py @@ -9,8 +9,20 @@ class Config(BaseModel): + clustering_use_llm_matching: Optional[bool] = None + + llm_matching_model: Optional[str] = None + + llm_matching_quality_preset: Optional[str] = None + + lower_llm_match_distance_threshold: Optional[float] = None + max_distance: Optional[float] = None + query_use_llm_matching: Optional[bool] = None + + upper_llm_match_distance_threshold: Optional[float] = None + class ProjectReturnSchema(BaseModel): id: str diff --git a/src/codex/types/project_update_params.py b/src/codex/types/project_update_params.py index 46d747e..0a5aa54 100644 --- a/src/codex/types/project_update_params.py +++ b/src/codex/types/project_update_params.py @@ -17,4 +17,16 @@ class ProjectUpdateParams(TypedDict, total=False): class Config(TypedDict, total=False): + clustering_use_llm_matching: bool + + llm_matching_model: str + + llm_matching_quality_preset: str + + lower_llm_match_distance_threshold: float + max_distance: float + + query_use_llm_matching: bool + + upper_llm_match_distance_threshold: float diff --git a/src/codex/types/projects/entry_query_params.py b/src/codex/types/projects/entry_query_params.py index 50b5f26..d58b7bf 100644 --- a/src/codex/types/projects/entry_query_params.py +++ b/src/codex/types/projects/entry_query_params.py @@ -13,6 +13,8 @@ class EntryQueryParams(TypedDict, total=False): question: Required[str] + use_llm_matching: bool + client_metadata: Optional[object] x_client_library_version: Annotated[str, PropertyInfo(alias="x-client-library-version")] diff --git a/tests/api_resources/projects/test_entries.py b/tests/api_resources/projects/test_entries.py index 5fa5ed9..ca7eecb 100644 --- a/tests/api_resources/projects/test_entries.py +++ b/tests/api_resources/projects/test_entries.py @@ -262,6 +262,7 @@ def test_method_query_with_all_params(self, client: Codex) -> None: entry = client.projects.entries.query( project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", question="question", + use_llm_matching=True, client_metadata={}, x_client_library_version="x-client-library-version", x_integration_type="x-integration-type", @@ -556,6 +557,7 @@ async def test_method_query_with_all_params(self, async_client: AsyncCodex) -> N entry = await async_client.projects.entries.query( project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", question="question", + use_llm_matching=True, client_metadata={}, x_client_library_version="x-client-library-version", x_integration_type="x-integration-type", diff --git a/tests/api_resources/test_projects.py b/tests/api_resources/test_projects.py index 10d2346..210f4e1 100644 --- a/tests/api_resources/test_projects.py +++ b/tests/api_resources/test_projects.py @@ -34,7 +34,15 @@ def test_method_create(self, client: Codex) -> None: @parametrize def test_method_create_with_all_params(self, client: Codex) -> None: project = client.projects.create( - config={"max_distance": 0}, + config={ + "clustering_use_llm_matching": True, + "llm_matching_model": "llm_matching_model", + "llm_matching_quality_preset": "llm_matching_quality_preset", + "lower_llm_match_distance_threshold": 0, + "max_distance": 0, + "query_use_llm_matching": True, + "upper_llm_match_distance_threshold": 0, + }, name="name", organization_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", @@ -128,7 +136,15 @@ def test_method_update(self, client: Codex) -> None: def test_method_update_with_all_params(self, client: Codex) -> None: project = client.projects.update( project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - config={"max_distance": 0}, + config={ + "clustering_use_llm_matching": True, + "llm_matching_model": "llm_matching_model", + "llm_matching_quality_preset": "llm_matching_quality_preset", + "lower_llm_match_distance_threshold": 0, + "max_distance": 0, + "query_use_llm_matching": True, + "upper_llm_match_distance_threshold": 0, + }, name="name", description="description", ) @@ -324,7 +340,15 @@ async def test_method_create(self, async_client: AsyncCodex) -> None: @parametrize async def test_method_create_with_all_params(self, async_client: AsyncCodex) -> None: project = await async_client.projects.create( - config={"max_distance": 0}, + config={ + "clustering_use_llm_matching": True, + "llm_matching_model": "llm_matching_model", + "llm_matching_quality_preset": "llm_matching_quality_preset", + "lower_llm_match_distance_threshold": 0, + "max_distance": 0, + "query_use_llm_matching": True, + "upper_llm_match_distance_threshold": 0, + }, name="name", organization_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", @@ -418,7 +442,15 @@ async def test_method_update(self, async_client: AsyncCodex) -> None: async def test_method_update_with_all_params(self, async_client: AsyncCodex) -> None: project = await async_client.projects.update( project_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - config={"max_distance": 0}, + config={ + "clustering_use_llm_matching": True, + "llm_matching_model": "llm_matching_model", + "llm_matching_quality_preset": "llm_matching_quality_preset", + "lower_llm_match_distance_threshold": 0, + "max_distance": 0, + "query_use_llm_matching": True, + "upper_llm_match_distance_threshold": 0, + }, name="name", description="description", ) From 772695b60b05439999d9f8bd81ec772908c664d3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 04:40:49 +0000 Subject: [PATCH 05/15] fix(client): send all configured auth headers (#106) --- src/codex/_client.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/codex/_client.py b/src/codex/_client.py index 9bdd5f0..2641513 100644 --- a/src/codex/_client.py +++ b/src/codex/_client.py @@ -154,11 +154,7 @@ def qs(self) -> Querystring: @property @override def auth_headers(self) -> dict[str, str]: - if self._authenticated_api_key: - return self._authenticated_api_key - if self._public_access_key: - return self._public_access_key - return {} + return {**self._authenticated_api_key, **self._public_access_key} @property def _authenticated_api_key(self) -> dict[str, str]: @@ -386,11 +382,7 @@ def qs(self) -> Querystring: @property @override def auth_headers(self) -> dict[str, str]: - if self._authenticated_api_key: - return self._authenticated_api_key - if self._public_access_key: - return self._public_access_key - return {} + return {**self._authenticated_api_key, **self._public_access_key} @property def _authenticated_api_key(self) -> dict[str, str]: From 9a3564e4e18decc1a7238a986f6a5b23db845f35 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 19:28:30 +0000 Subject: [PATCH 06/15] feat(api): api update (#107) --- .stats.yml | 2 +- api.md | 9 +++- src/codex/resources/projects/clusters.py | 6 ++- src/codex/resources/projects/projects.py | 9 ++-- src/codex/types/__init__.py | 1 + src/codex/types/project_retrieve_response.py | 44 +++++++++++++++++++ .../types/projects/cluster_list_params.py | 2 +- tests/api_resources/test_projects.py | 13 +++--- 8 files changed, 70 insertions(+), 16 deletions(-) create mode 100644 src/codex/types/project_retrieve_response.py diff --git a/.stats.yml b/.stats.yml index 69bb6fc..4500dc4 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,3 +1,3 @@ configured_endpoints: 36 -openapi_spec_hash: 8ef89533cd58e3b2ceb53a877832f48b +openapi_spec_hash: ee7ad81c8308305b6a609a18615ae394 config_hash: adbedb6317fca6f566f54564cc341846 diff --git a/api.md b/api.md index b241c15..e9dd565 100644 --- a/api.md +++ b/api.md @@ -135,13 +135,18 @@ Methods: Types: ```python -from codex.types import ProjectReturnSchema, ProjectListResponse, ProjectExportResponse +from codex.types import ( + ProjectReturnSchema, + ProjectRetrieveResponse, + ProjectListResponse, + ProjectExportResponse, +) ``` Methods: - client.projects.create(\*\*params) -> ProjectReturnSchema -- client.projects.retrieve(project_id) -> ProjectReturnSchema +- client.projects.retrieve(project_id) -> ProjectRetrieveResponse - client.projects.update(project_id, \*\*params) -> ProjectReturnSchema - client.projects.list(\*\*params) -> ProjectListResponse - client.projects.delete(project_id) -> None diff --git a/src/codex/resources/projects/clusters.py b/src/codex/resources/projects/clusters.py index 2faed31..f376c0b 100644 --- a/src/codex/resources/projects/clusters.py +++ b/src/codex/resources/projects/clusters.py @@ -53,7 +53,8 @@ def list( limit: int | NotGiven = NOT_GIVEN, offset: int | NotGiven = NOT_GIVEN, order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, - sort: Optional[Literal["created_at", "answered_at", "cluster_frequency_count"]] | NotGiven = NOT_GIVEN, + sort: Optional[Literal["created_at", "answered_at", "cluster_frequency_count", "custom_rank"]] + | NotGiven = NOT_GIVEN, states: List[Literal["unanswered", "draft", "published", "published_with_draft"]] | NotGiven = NOT_GIVEN, # 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. @@ -164,7 +165,8 @@ def list( limit: int | NotGiven = NOT_GIVEN, offset: int | NotGiven = NOT_GIVEN, order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, - sort: Optional[Literal["created_at", "answered_at", "cluster_frequency_count"]] | NotGiven = NOT_GIVEN, + sort: Optional[Literal["created_at", "answered_at", "cluster_frequency_count", "custom_rank"]] + | NotGiven = NOT_GIVEN, states: List[Literal["unanswered", "draft", "published", "published_with_draft"]] | NotGiven = NOT_GIVEN, # 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. diff --git a/src/codex/resources/projects/projects.py b/src/codex/resources/projects/projects.py index 8a7ff1b..ccc7726 100644 --- a/src/codex/resources/projects/projects.py +++ b/src/codex/resources/projects/projects.py @@ -48,6 +48,7 @@ from ..._base_client import make_request_options from ...types.project_list_response import ProjectListResponse from ...types.project_return_schema import ProjectReturnSchema +from ...types.project_retrieve_response import ProjectRetrieveResponse __all__ = ["ProjectsResource", "AsyncProjectsResource"] @@ -137,7 +138,7 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ProjectReturnSchema: + ) -> ProjectRetrieveResponse: """ Get a single project. @@ -157,7 +158,7 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ProjectReturnSchema, + cast_to=ProjectRetrieveResponse, ) def update( @@ -409,7 +410,7 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ProjectReturnSchema: + ) -> ProjectRetrieveResponse: """ Get a single project. @@ -429,7 +430,7 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ProjectReturnSchema, + cast_to=ProjectRetrieveResponse, ) async def update( diff --git a/src/codex/types/__init__.py b/src/codex/types/__init__.py index 8f241bc..6c18437 100644 --- a/src/codex/types/__init__.py +++ b/src/codex/types/__init__.py @@ -13,5 +13,6 @@ from .project_list_response import ProjectListResponse as ProjectListResponse from .project_return_schema import ProjectReturnSchema as ProjectReturnSchema from .project_update_params import ProjectUpdateParams as ProjectUpdateParams +from .project_retrieve_response import ProjectRetrieveResponse as ProjectRetrieveResponse from .organization_schema_public import OrganizationSchemaPublic as OrganizationSchemaPublic from .user_activate_account_params import UserActivateAccountParams as UserActivateAccountParams diff --git a/src/codex/types/project_retrieve_response.py b/src/codex/types/project_retrieve_response.py new file mode 100644 index 0000000..62209d3 --- /dev/null +++ b/src/codex/types/project_retrieve_response.py @@ -0,0 +1,44 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["ProjectRetrieveResponse", "Config"] + + +class Config(BaseModel): + clustering_use_llm_matching: Optional[bool] = None + + llm_matching_model: Optional[str] = None + + llm_matching_quality_preset: Optional[str] = None + + lower_llm_match_distance_threshold: Optional[float] = None + + max_distance: Optional[float] = None + + query_use_llm_matching: Optional[bool] = None + + upper_llm_match_distance_threshold: Optional[float] = None + + +class ProjectRetrieveResponse(BaseModel): + id: str + + config: Config + + created_at: datetime + + created_by_user_id: str + + name: str + + organization_id: str + + updated_at: datetime + + custom_rank_enabled: Optional[bool] = None + + description: Optional[str] = None diff --git a/src/codex/types/projects/cluster_list_params.py b/src/codex/types/projects/cluster_list_params.py index 4889324..b272d80 100644 --- a/src/codex/types/projects/cluster_list_params.py +++ b/src/codex/types/projects/cluster_list_params.py @@ -15,6 +15,6 @@ class ClusterListParams(TypedDict, total=False): order: Literal["asc", "desc"] - sort: Optional[Literal["created_at", "answered_at", "cluster_frequency_count"]] + sort: Optional[Literal["created_at", "answered_at", "cluster_frequency_count", "custom_rank"]] states: List[Literal["unanswered", "draft", "published", "published_with_draft"]] diff --git a/tests/api_resources/test_projects.py b/tests/api_resources/test_projects.py index 210f4e1..1e2fccb 100644 --- a/tests/api_resources/test_projects.py +++ b/tests/api_resources/test_projects.py @@ -11,6 +11,7 @@ from codex.types import ( ProjectListResponse, ProjectReturnSchema, + ProjectRetrieveResponse, ) from tests.utils import assert_matches_type @@ -85,7 +86,7 @@ def test_method_retrieve(self, client: Codex) -> None: project = client.projects.retrieve( "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(ProjectReturnSchema, project, path=["response"]) + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) @pytest.mark.skip() @parametrize @@ -97,7 +98,7 @@ def test_raw_response_retrieve(self, client: Codex) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(ProjectReturnSchema, project, path=["response"]) + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) @pytest.mark.skip() @parametrize @@ -109,7 +110,7 @@ def test_streaming_response_retrieve(self, client: Codex) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(ProjectReturnSchema, project, path=["response"]) + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) assert cast(Any, response.is_closed) is True @@ -391,7 +392,7 @@ async def test_method_retrieve(self, async_client: AsyncCodex) -> None: project = await async_client.projects.retrieve( "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(ProjectReturnSchema, project, path=["response"]) + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) @pytest.mark.skip() @parametrize @@ -403,7 +404,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncCodex) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(ProjectReturnSchema, project, path=["response"]) + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) @pytest.mark.skip() @parametrize @@ -415,7 +416,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncCodex) -> No assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(ProjectReturnSchema, project, path=["response"]) + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) assert cast(Any, response.is_closed) is True From 552640f0d27993032b3b6b0a9861c3dbaf09d852 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 03:40:16 +0000 Subject: [PATCH 07/15] chore(internal): slight transform perf improvement (#108) --- src/codex/_utils/_transform.py | 22 ++++++++++++++++++++++ tests/test_transform.py | 12 ++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/codex/_utils/_transform.py b/src/codex/_utils/_transform.py index 7ac2e17..3ec6208 100644 --- a/src/codex/_utils/_transform.py +++ b/src/codex/_utils/_transform.py @@ -142,6 +142,10 @@ def _maybe_transform_key(key: str, type_: type) -> str: return key +def _no_transform_needed(annotation: type) -> bool: + return annotation == float or annotation == int + + def _transform_recursive( data: object, *, @@ -184,6 +188,15 @@ def _transform_recursive( return cast(object, data) inner_type = extract_type_arg(stripped_type, 0) + if _no_transform_needed(inner_type): + # for some types there is no need to transform anything, so we can get a small + # perf boost from skipping that work. + # + # but we still need to convert to a list to ensure the data is json-serializable + if is_list(data): + return data + return list(data) + return [_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] if is_union_type(stripped_type): @@ -332,6 +345,15 @@ async def _async_transform_recursive( return cast(object, data) inner_type = extract_type_arg(stripped_type, 0) + if _no_transform_needed(inner_type): + # for some types there is no need to transform anything, so we can get a small + # perf boost from skipping that work. + # + # but we still need to convert to a list to ensure the data is json-serializable + if is_list(data): + return data + return list(data) + return [await _async_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] if is_union_type(stripped_type): diff --git a/tests/test_transform.py b/tests/test_transform.py index 324f31a..b1ea479 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -432,3 +432,15 @@ async def test_base64_file_input(use_async: bool) -> None: assert await transform({"foo": io.BytesIO(b"Hello, world!")}, TypedDictBase64Input, use_async) == { "foo": "SGVsbG8sIHdvcmxkIQ==" } # type: ignore[comparison-overlap] + + +@parametrize +@pytest.mark.asyncio +async def test_transform_skipping(use_async: bool) -> None: + # lists of ints are left as-is + data = [1, 2, 3] + assert await transform(data, List[int], use_async) is data + + # iterables of ints are converted to a list + data = iter([1, 2, 3]) + assert await transform(data, Iterable[int], use_async) == [1, 2, 3] From a25d0033bafc06b6c6ad35597278067c02794071 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 10 Apr 2025 02:29:52 +0000 Subject: [PATCH 08/15] feat(api): api update --- .stats.yml | 2 +- .../types/projects/cluster_list_response.py | 110 ++++++++++++++++- src/codex/types/projects/entry.py | 110 ++++++++++++++++- .../types/projects/entry_query_response.py | 111 +++++++++++++++++- 4 files changed, 329 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4500dc4..ab9fbe2 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,3 +1,3 @@ configured_endpoints: 36 -openapi_spec_hash: ee7ad81c8308305b6a609a18615ae394 +openapi_spec_hash: 2e10455457751c9efb36adc4399c684d config_hash: adbedb6317fca6f566f54564cc341846 diff --git a/src/codex/types/projects/cluster_list_response.py b/src/codex/types/projects/cluster_list_response.py index d13a5f9..33bc18a 100644 --- a/src/codex/types/projects/cluster_list_response.py +++ b/src/codex/types/projects/cluster_list_response.py @@ -6,7 +6,112 @@ from ..._models import BaseModel -__all__ = ["ClusterListResponse"] +__all__ = [ + "ClusterListResponse", + "ManagedMetadata", + "ManagedMetadataContextSufficiency", + "ManagedMetadataQueryEaseCustomized", + "ManagedMetadataResponseHelpfulness", + "ManagedMetadataTrustworthiness", +] + + +class ManagedMetadataContextSufficiency(BaseModel): + average: Optional[float] = None + """The average of all scores.""" + + latest: Optional[float] = None + """The most recent score.""" + + max: Optional[float] = None + """The maximum score.""" + + min: Optional[float] = None + """The minimum score.""" + + scores: Optional[List[float]] = None + + +class ManagedMetadataQueryEaseCustomized(BaseModel): + average: Optional[float] = None + """The average of all scores.""" + + latest: Optional[float] = None + """The most recent score.""" + + max: Optional[float] = None + """The maximum score.""" + + min: Optional[float] = None + """The minimum score.""" + + scores: Optional[List[float]] = None + + +class ManagedMetadataResponseHelpfulness(BaseModel): + average: Optional[float] = None + """The average of all scores.""" + + latest: Optional[float] = None + """The most recent score.""" + + max: Optional[float] = None + """The maximum score.""" + + min: Optional[float] = None + """The minimum score.""" + + scores: Optional[List[float]] = None + + +class ManagedMetadataTrustworthiness(BaseModel): + average: Optional[float] = None + """The average of all scores.""" + + latest: Optional[float] = None + """The most recent score.""" + + max: Optional[float] = None + """The maximum score.""" + + min: Optional[float] = None + """The minimum score.""" + + scores: Optional[List[float]] = None + + +class ManagedMetadata(BaseModel): + latest_context: Optional[str] = None + """The most recent context string.""" + + latest_entry_point: Optional[str] = None + """The most recent entry point string.""" + + latest_llm_response: Optional[str] = None + """The most recent LLM response string.""" + + latest_location: Optional[str] = None + """The most recent location string.""" + + context_sufficiency: Optional[ManagedMetadataContextSufficiency] = None + """Holds a list of scores and computes aggregate statistics.""" + + contexts: Optional[List[str]] = None + + entry_points: Optional[List[str]] = None + + llm_responses: Optional[List[str]] = None + + locations: Optional[List[str]] = None + + query_ease_customized: Optional[ManagedMetadataQueryEaseCustomized] = None + """Holds a list of scores and computes aggregate statistics.""" + + response_helpfulness: Optional[ManagedMetadataResponseHelpfulness] = None + """Holds a list of scores and computes aggregate statistics.""" + + trustworthiness: Optional[ManagedMetadataTrustworthiness] = None + """Holds a list of scores and computes aggregate statistics.""" class ClusterListResponse(BaseModel): @@ -16,6 +121,9 @@ class ClusterListResponse(BaseModel): created_at: datetime + managed_metadata: ManagedMetadata + """Extract system-defined, managed metadata from client_query_metadata.""" + project_id: str question: str diff --git a/src/codex/types/projects/entry.py b/src/codex/types/projects/entry.py index b8eeb0e..24d57c8 100644 --- a/src/codex/types/projects/entry.py +++ b/src/codex/types/projects/entry.py @@ -6,7 +6,112 @@ from ..._models import BaseModel -__all__ = ["Entry"] +__all__ = [ + "Entry", + "ManagedMetadata", + "ManagedMetadataContextSufficiency", + "ManagedMetadataQueryEaseCustomized", + "ManagedMetadataResponseHelpfulness", + "ManagedMetadataTrustworthiness", +] + + +class ManagedMetadataContextSufficiency(BaseModel): + average: Optional[float] = None + """The average of all scores.""" + + latest: Optional[float] = None + """The most recent score.""" + + max: Optional[float] = None + """The maximum score.""" + + min: Optional[float] = None + """The minimum score.""" + + scores: Optional[List[float]] = None + + +class ManagedMetadataQueryEaseCustomized(BaseModel): + average: Optional[float] = None + """The average of all scores.""" + + latest: Optional[float] = None + """The most recent score.""" + + max: Optional[float] = None + """The maximum score.""" + + min: Optional[float] = None + """The minimum score.""" + + scores: Optional[List[float]] = None + + +class ManagedMetadataResponseHelpfulness(BaseModel): + average: Optional[float] = None + """The average of all scores.""" + + latest: Optional[float] = None + """The most recent score.""" + + max: Optional[float] = None + """The maximum score.""" + + min: Optional[float] = None + """The minimum score.""" + + scores: Optional[List[float]] = None + + +class ManagedMetadataTrustworthiness(BaseModel): + average: Optional[float] = None + """The average of all scores.""" + + latest: Optional[float] = None + """The most recent score.""" + + max: Optional[float] = None + """The maximum score.""" + + min: Optional[float] = None + """The minimum score.""" + + scores: Optional[List[float]] = None + + +class ManagedMetadata(BaseModel): + latest_context: Optional[str] = None + """The most recent context string.""" + + latest_entry_point: Optional[str] = None + """The most recent entry point string.""" + + latest_llm_response: Optional[str] = None + """The most recent LLM response string.""" + + latest_location: Optional[str] = None + """The most recent location string.""" + + context_sufficiency: Optional[ManagedMetadataContextSufficiency] = None + """Holds a list of scores and computes aggregate statistics.""" + + contexts: Optional[List[str]] = None + + entry_points: Optional[List[str]] = None + + llm_responses: Optional[List[str]] = None + + locations: Optional[List[str]] = None + + query_ease_customized: Optional[ManagedMetadataQueryEaseCustomized] = None + """Holds a list of scores and computes aggregate statistics.""" + + response_helpfulness: Optional[ManagedMetadataResponseHelpfulness] = None + """Holds a list of scores and computes aggregate statistics.""" + + trustworthiness: Optional[ManagedMetadataTrustworthiness] = None + """Holds a list of scores and computes aggregate statistics.""" class Entry(BaseModel): @@ -14,6 +119,9 @@ class Entry(BaseModel): created_at: datetime + managed_metadata: ManagedMetadata + """Extract system-defined, managed metadata from client_query_metadata.""" + project_id: str question: str diff --git a/src/codex/types/projects/entry_query_response.py b/src/codex/types/projects/entry_query_response.py index 766e1e2..26db1d8 100644 --- a/src/codex/types/projects/entry_query_response.py +++ b/src/codex/types/projects/entry_query_response.py @@ -4,12 +4,121 @@ from ..._models import BaseModel -__all__ = ["EntryQueryResponse", "Entry"] +__all__ = [ + "EntryQueryResponse", + "Entry", + "EntryManagedMetadata", + "EntryManagedMetadataContextSufficiency", + "EntryManagedMetadataQueryEaseCustomized", + "EntryManagedMetadataResponseHelpfulness", + "EntryManagedMetadataTrustworthiness", +] + + +class EntryManagedMetadataContextSufficiency(BaseModel): + average: Optional[float] = None + """The average of all scores.""" + + latest: Optional[float] = None + """The most recent score.""" + + max: Optional[float] = None + """The maximum score.""" + + min: Optional[float] = None + """The minimum score.""" + + scores: Optional[List[float]] = None + + +class EntryManagedMetadataQueryEaseCustomized(BaseModel): + average: Optional[float] = None + """The average of all scores.""" + + latest: Optional[float] = None + """The most recent score.""" + + max: Optional[float] = None + """The maximum score.""" + + min: Optional[float] = None + """The minimum score.""" + + scores: Optional[List[float]] = None + + +class EntryManagedMetadataResponseHelpfulness(BaseModel): + average: Optional[float] = None + """The average of all scores.""" + + latest: Optional[float] = None + """The most recent score.""" + + max: Optional[float] = None + """The maximum score.""" + + min: Optional[float] = None + """The minimum score.""" + + scores: Optional[List[float]] = None + + +class EntryManagedMetadataTrustworthiness(BaseModel): + average: Optional[float] = None + """The average of all scores.""" + + latest: Optional[float] = None + """The most recent score.""" + + max: Optional[float] = None + """The maximum score.""" + + min: Optional[float] = None + """The minimum score.""" + + scores: Optional[List[float]] = None + + +class EntryManagedMetadata(BaseModel): + latest_context: Optional[str] = None + """The most recent context string.""" + + latest_entry_point: Optional[str] = None + """The most recent entry point string.""" + + latest_llm_response: Optional[str] = None + """The most recent LLM response string.""" + + latest_location: Optional[str] = None + """The most recent location string.""" + + context_sufficiency: Optional[EntryManagedMetadataContextSufficiency] = None + """Holds a list of scores and computes aggregate statistics.""" + + contexts: Optional[List[str]] = None + + entry_points: Optional[List[str]] = None + + llm_responses: Optional[List[str]] = None + + locations: Optional[List[str]] = None + + query_ease_customized: Optional[EntryManagedMetadataQueryEaseCustomized] = None + """Holds a list of scores and computes aggregate statistics.""" + + response_helpfulness: Optional[EntryManagedMetadataResponseHelpfulness] = None + """Holds a list of scores and computes aggregate statistics.""" + + trustworthiness: Optional[EntryManagedMetadataTrustworthiness] = None + """Holds a list of scores and computes aggregate statistics.""" class Entry(BaseModel): id: str + managed_metadata: EntryManagedMetadata + """Extract system-defined, managed metadata from client_query_metadata.""" + question: str answer: Optional[str] = None From 8ce4af445131dae911282d7330cd31d3124212ca Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 10 Apr 2025 04:04:24 +0000 Subject: [PATCH 09/15] chore(internal): expand CI branch coverage --- .github/workflows/ci.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ac5f63..b892020 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,18 +1,18 @@ name: CI on: push: - branches: - - main - pull_request: - branches: - - main - - next + branches-ignore: + - 'generated' + - 'codegen/**' + - 'integrated/**' + - 'preview-head/**' + - 'preview-base/**' + - 'preview/**' jobs: lint: name: lint runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 From 0aee51f42c7400fea6538a505ab0368f9397681a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 10 Apr 2025 04:08:38 +0000 Subject: [PATCH 10/15] chore(internal): reduce CI branch coverage --- .github/workflows/ci.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b892020..e8b7236 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,13 +1,12 @@ name: CI on: push: - branches-ignore: - - 'generated' - - 'codegen/**' - - 'integrated/**' - - 'preview-head/**' - - 'preview-base/**' - - 'preview/**' + branches: + - main + pull_request: + branches: + - main + - next jobs: lint: From 38418ad25f1ce660c9ee1cfabf0fc48e965c02f9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 12 Apr 2025 02:09:29 +0000 Subject: [PATCH 11/15] fix(perf): skip traversing types for NotGiven values --- src/codex/_utils/_transform.py | 11 +++++++++++ tests/test_transform.py | 9 ++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/codex/_utils/_transform.py b/src/codex/_utils/_transform.py index 3ec6208..3b2b8e0 100644 --- a/src/codex/_utils/_transform.py +++ b/src/codex/_utils/_transform.py @@ -12,6 +12,7 @@ from ._utils import ( is_list, + is_given, is_mapping, is_iterable, ) @@ -258,6 +259,11 @@ def _transform_typeddict( result: dict[str, object] = {} annotations = get_type_hints(expected_type, include_extras=True) for key, value in data.items(): + if not is_given(value): + # we don't need to include `NotGiven` values here as they'll + # be stripped out before the request is sent anyway + continue + type_ = annotations.get(key) if type_ is None: # we do not have a type annotation for this field, leave it as is @@ -415,6 +421,11 @@ async def _async_transform_typeddict( result: dict[str, object] = {} annotations = get_type_hints(expected_type, include_extras=True) for key, value in data.items(): + if not is_given(value): + # we don't need to include `NotGiven` values here as they'll + # be stripped out before the request is sent anyway + continue + type_ = annotations.get(key) if type_ is None: # we do not have a type annotation for this field, leave it as is diff --git a/tests/test_transform.py b/tests/test_transform.py index b1ea479..527845a 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -8,7 +8,7 @@ import pytest -from codex._types import Base64FileInput +from codex._types import NOT_GIVEN, Base64FileInput from codex._utils import ( PropertyInfo, transform as _transform, @@ -444,3 +444,10 @@ async def test_transform_skipping(use_async: bool) -> None: # iterables of ints are converted to a list data = iter([1, 2, 3]) assert await transform(data, Iterable[int], use_async) == [1, 2, 3] + + +@parametrize +@pytest.mark.asyncio +async def test_strips_notgiven(use_async: bool) -> None: + assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"} + assert await transform({"foo_bar": NOT_GIVEN}, Foo1, use_async) == {} From fb1f7c1783af5dd17ef959733b670604ac6b877b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 12 Apr 2025 02:11:14 +0000 Subject: [PATCH 12/15] fix(perf): optimize some hot paths --- src/codex/_utils/_transform.py | 14 +++++++++++++- src/codex/_utils/_typing.py | 2 ++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/codex/_utils/_transform.py b/src/codex/_utils/_transform.py index 3b2b8e0..b0cc20a 100644 --- a/src/codex/_utils/_transform.py +++ b/src/codex/_utils/_transform.py @@ -5,7 +5,7 @@ import pathlib from typing import Any, Mapping, TypeVar, cast from datetime import date, datetime -from typing_extensions import Literal, get_args, override, get_type_hints +from typing_extensions import Literal, get_args, override, get_type_hints as _get_type_hints import anyio import pydantic @@ -13,6 +13,7 @@ from ._utils import ( is_list, is_given, + lru_cache, is_mapping, is_iterable, ) @@ -109,6 +110,7 @@ class Params(TypedDict, total=False): return cast(_T, transformed) +@lru_cache(maxsize=8096) def _get_annotated_type(type_: type) -> type | None: """If the given type is an `Annotated` type then it is returned, if not `None` is returned. @@ -433,3 +435,13 @@ async def _async_transform_typeddict( else: result[_maybe_transform_key(key, type_)] = await _async_transform_recursive(value, annotation=type_) return result + + +@lru_cache(maxsize=8096) +def get_type_hints( + obj: Any, + globalns: dict[str, Any] | None = None, + localns: Mapping[str, Any] | None = None, + include_extras: bool = False, +) -> dict[str, Any]: + return _get_type_hints(obj, globalns=globalns, localns=localns, include_extras=include_extras) diff --git a/src/codex/_utils/_typing.py b/src/codex/_utils/_typing.py index 278749b..1958820 100644 --- a/src/codex/_utils/_typing.py +++ b/src/codex/_utils/_typing.py @@ -13,6 +13,7 @@ get_origin, ) +from ._utils import lru_cache from .._types import InheritsGeneric from .._compat import is_union as _is_union @@ -66,6 +67,7 @@ def is_type_alias_type(tp: Any, /) -> TypeIs[typing_extensions.TypeAliasType]: # Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]] +@lru_cache(maxsize=8096) def strip_annotated_type(typ: type) -> type: if is_required_type(typ) or is_annotated_type(typ): return strip_annotated_type(cast(type, get_args(typ)[0])) From fc0f5bcf5e4f92ed59aa3498a4fb52713fe0c7c7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 15 Apr 2025 02:11:58 +0000 Subject: [PATCH 13/15] chore(internal): update pyright settings --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 6ca8da4..ff6cde5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -147,6 +147,7 @@ exclude = [ ] reportImplicitOverride = true +reportOverlappingOverload = false reportImportCycles = false reportPrivateUsage = false From 228700d4e54104c50f0c8eac09bb490a2e0b4fe8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 15 Apr 2025 02:12:59 +0000 Subject: [PATCH 14/15] chore(client): minor internal fixes --- src/codex/_base_client.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/codex/_base_client.py b/src/codex/_base_client.py index 273341b..564ab8a 100644 --- a/src/codex/_base_client.py +++ b/src/codex/_base_client.py @@ -409,7 +409,8 @@ def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0 idempotency_header = self._idempotency_header if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers: - headers[idempotency_header] = options.idempotency_key or self._idempotency_key() + options.idempotency_key = options.idempotency_key or self._idempotency_key() + headers[idempotency_header] = options.idempotency_key # Don't set these headers if they were already set or removed by the caller. We check # `custom_headers`, which can contain `Omit()`, instead of `headers` to account for the removal case. @@ -943,6 +944,10 @@ def _request( request = self._build_request(options, retries_taken=retries_taken) self._prepare_request(request) + if options.idempotency_key: + # ensure the idempotency key is reused between requests + input_options.idempotency_key = options.idempotency_key + kwargs: HttpxSendArgs = {} if self.custom_auth is not None: kwargs["auth"] = self.custom_auth @@ -1475,6 +1480,10 @@ async def _request( request = self._build_request(options, retries_taken=retries_taken) await self._prepare_request(request) + if options.idempotency_key: + # ensure the idempotency key is reused between requests + input_options.idempotency_key = options.idempotency_key + kwargs: HttpxSendArgs = {} if self.custom_auth is not None: kwargs["auth"] = self.custom_auth From 23e92dbce4294d777e0b56cbcd00ccf43114ca7d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 15 Apr 2025 22:59:09 +0000 Subject: [PATCH 15/15] release: 0.1.0-alpha.15 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ pyproject.toml | 2 +- src/codex/_version.py | 2 +- 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b069996..08e82c4 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.14" + ".": "0.1.0-alpha.15" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index b9b6747..cff410b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # Changelog +## 0.1.0-alpha.15 (2025-04-15) + +Full Changelog: [v0.1.0-alpha.14...v0.1.0-alpha.15](https://github.com/cleanlab/codex-python/compare/v0.1.0-alpha.14...v0.1.0-alpha.15) + +### Features + +* **api:** api update ([a25d003](https://github.com/cleanlab/codex-python/commit/a25d0033bafc06b6c6ad35597278067c02794071)) +* **api:** api update ([#102](https://github.com/cleanlab/codex-python/issues/102)) ([5675c9e](https://github.com/cleanlab/codex-python/commit/5675c9efbda688fa1799f1e48f64c20143fcf410)) +* **api:** api update ([#104](https://github.com/cleanlab/codex-python/issues/104)) ([404e013](https://github.com/cleanlab/codex-python/commit/404e013054df741a1d3c77597f35030cba080b82)) +* **api:** api update ([#107](https://github.com/cleanlab/codex-python/issues/107)) ([9a3564e](https://github.com/cleanlab/codex-python/commit/9a3564e4e18decc1a7238a986f6a5b23db845f35)) + + +### Bug Fixes + +* **client:** send all configured auth headers ([#106](https://github.com/cleanlab/codex-python/issues/106)) ([772695b](https://github.com/cleanlab/codex-python/commit/772695b60b05439999d9f8bd81ec772908c664d3)) +* **perf:** optimize some hot paths ([fb1f7c1](https://github.com/cleanlab/codex-python/commit/fb1f7c1783af5dd17ef959733b670604ac6b877b)) +* **perf:** skip traversing types for NotGiven values ([38418ad](https://github.com/cleanlab/codex-python/commit/38418ad25f1ce660c9ee1cfabf0fc48e965c02f9)) + + +### Chores + +* **client:** minor internal fixes ([228700d](https://github.com/cleanlab/codex-python/commit/228700d4e54104c50f0c8eac09bb490a2e0b4fe8)) +* **internal:** expand CI branch coverage ([8ce4af4](https://github.com/cleanlab/codex-python/commit/8ce4af445131dae911282d7330cd31d3124212ca)) +* **internal:** reduce CI branch coverage ([0aee51f](https://github.com/cleanlab/codex-python/commit/0aee51f42c7400fea6538a505ab0368f9397681a)) +* **internal:** remove trailing character ([#103](https://github.com/cleanlab/codex-python/issues/103)) ([45f7fde](https://github.com/cleanlab/codex-python/commit/45f7fde22223d99bec5ab8142b59f5124b275c28)) +* **internal:** slight transform perf improvement ([#108](https://github.com/cleanlab/codex-python/issues/108)) ([552640f](https://github.com/cleanlab/codex-python/commit/552640f0d27993032b3b6b0a9861c3dbaf09d852)) +* **internal:** update pyright settings ([fc0f5bc](https://github.com/cleanlab/codex-python/commit/fc0f5bcf5e4f92ed59aa3498a4fb52713fe0c7c7)) +* **internal:** version bump ([#99](https://github.com/cleanlab/codex-python/issues/99)) ([4bc12e4](https://github.com/cleanlab/codex-python/commit/4bc12e4d6bd408e6256f2e6e5539fd8feb29fab2)) + ## 0.1.0-alpha.14 (2025-04-03) Full Changelog: [v0.1.0-alpha.13...v0.1.0-alpha.14](https://github.com/cleanlab/codex-python/compare/v0.1.0-alpha.13...v0.1.0-alpha.14) diff --git a/pyproject.toml b/pyproject.toml index ff6cde5..686a2e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "codex-sdk" -version = "0.1.0-alpha.14" +version = "0.1.0-alpha.15" description = "Internal SDK used within cleanlab-codex package. Refer to https://pypi.org/project/cleanlab-codex/ instead." dynamic = ["readme"] license = "MIT" diff --git a/src/codex/_version.py b/src/codex/_version.py index 1509753..d4919cf 100644 --- a/src/codex/_version.py +++ b/src/codex/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "codex" -__version__ = "0.1.0-alpha.14" # x-release-please-version +__version__ = "0.1.0-alpha.15" # x-release-please-version