diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fb1bd8f489..6a197bef5a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.16.2" + ".": "1.17.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b22f06aae..0da030b337 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 1.17.0 (2024-04-10) + +Full Changelog: [v1.16.2...v1.17.0](https://github.com/openai/openai-python/compare/v1.16.2...v1.17.0) + +### Features + +* **api:** add additional messages when creating thread run ([#1298](https://github.com/openai/openai-python/issues/1298)) ([70eb081](https://github.com/openai/openai-python/commit/70eb081804b14cc8c151ebd85458545a50a074fd)) +* **client:** add DefaultHttpxClient and DefaultAsyncHttpxClient ([#1302](https://github.com/openai/openai-python/issues/1302)) ([69cdfc3](https://github.com/openai/openai-python/commit/69cdfc319fff7ebf28cdd13cc6c1761b7d97811d)) +* **models:** add to_dict & to_json helper methods ([#1305](https://github.com/openai/openai-python/issues/1305)) ([40a881d](https://github.com/openai/openai-python/commit/40a881d10442af8b445ce030f8ab338710e1c4c8)) + ## 1.16.2 (2024-04-04) Full Changelog: [v1.16.1...v1.16.2](https://github.com/openai/openai-python/compare/v1.16.1...v1.16.2) diff --git a/README.md b/README.md index 5264026dc9..3bdd6c4a43 100644 --- a/README.md +++ b/README.md @@ -200,10 +200,10 @@ We recommend that you always instantiate a client (e.g., with `client = OpenAI() ## 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 provide helper methods for things like: +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.model_dump_json(indent=2, exclude_unset=True)` -- Converting to a dictionary, `model.model_dump(exclude_unset=True)` +- 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`. @@ -549,13 +549,12 @@ You can directly override the [httpx client](https://www.python-httpx.org/api/#c - Additional [advanced](https://www.python-httpx.org/advanced/#client-instances) functionality ```python -import httpx -from openai import OpenAI +from openai import OpenAI, DefaultHttpxClient client = OpenAI( # Or use the `OPENAI_BASE_URL` env var base_url="http://my.test.server.example.com:8083", - http_client=httpx.Client( + http_client=DefaultHttpxClient( proxies="http://my.test.proxy.example.com", transport=httpx.HTTPTransport(local_address="0.0.0.0"), ), @@ -595,7 +594,7 @@ completion = client.chat.completions.create( }, ], ) -print(completion.model_dump_json(indent=2)) +print(completion.to_json()) ``` In addition to the options provided in the base `OpenAI` client, the following options are provided: diff --git a/examples/azure.py b/examples/azure.py index a28b8cc433..6936c4cb0e 100755 --- a/examples/azure.py +++ b/examples/azure.py @@ -20,7 +20,7 @@ }, ], ) -print(completion.model_dump_json(indent=2)) +print(completion.to_json()) deployment_client = AzureOpenAI( @@ -40,4 +40,4 @@ }, ], ) -print(completion.model_dump_json(indent=2)) +print(completion.to_json()) diff --git a/examples/azure_ad.py b/examples/azure_ad.py index f13079dd04..1b0d81863d 100755 --- a/examples/azure_ad.py +++ b/examples/azure_ad.py @@ -27,4 +27,4 @@ }, ], ) -print(completion.model_dump_json(indent=2)) +print(completion.to_json()) diff --git a/examples/streaming.py b/examples/streaming.py index 368fa5f911..9a84891a83 100755 --- a/examples/streaming.py +++ b/examples/streaming.py @@ -22,12 +22,12 @@ def sync_main() -> None: # You can manually control iteration over the response first = next(response) - print(f"got response data: {first.model_dump_json(indent=2)}") + print(f"got response data: {first.to_json()}") # Or you could automatically iterate through all of data. # Note that the for loop will not exit until *all* of the data has been processed. for data in response: - print(data.model_dump_json()) + print(data.to_json()) async def async_main() -> None: @@ -43,12 +43,12 @@ async def async_main() -> None: # You can manually control iteration over the response. # In Python 3.10+ you can also use the `await anext(response)` builtin instead first = await response.__anext__() - print(f"got response data: {first.model_dump_json(indent=2)}") + print(f"got response data: {first.to_json()}") # Or you could automatically iterate through all of data. # Note that the for loop will not exit until *all* of the data has been processed. async for data in response: - print(data.model_dump_json()) + print(data.to_json()) sync_main() diff --git a/pyproject.toml b/pyproject.toml index 67006726fb..b3043bc0cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openai" -version = "1.16.2" +version = "1.17.0" description = "The official Python library for the openai API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/openai/__init__.py b/src/openai/__init__.py index cd05a749da..1daa26f7b7 100644 --- a/src/openai/__init__.py +++ b/src/openai/__init__.py @@ -29,6 +29,7 @@ UnprocessableEntityError, APIResponseValidationError, ) +from ._base_client import DefaultHttpxClient, DefaultAsyncHttpxClient from ._utils._logs import setup_logging as _setup_logging __all__ = [ @@ -67,6 +68,8 @@ "DEFAULT_TIMEOUT", "DEFAULT_MAX_RETRIES", "DEFAULT_CONNECTION_LIMITS", + "DefaultHttpxClient", + "DefaultAsyncHttpxClient", ] from .lib import azure as _azure diff --git a/src/openai/_base_client.py b/src/openai/_base_client.py index 502ed7c7ae..0bb284a211 100644 --- a/src/openai/_base_client.py +++ b/src/openai/_base_client.py @@ -716,7 +716,27 @@ def _idempotency_key(self) -> str: return f"stainless-python-retry-{uuid.uuid4()}" -class SyncHttpxClientWrapper(httpx.Client): +class _DefaultHttpxClient(httpx.Client): + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("timeout", DEFAULT_TIMEOUT) + kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) + kwargs.setdefault("follow_redirects", True) + super().__init__(**kwargs) + + +if TYPE_CHECKING: + DefaultHttpxClient = httpx.Client + """An alias to `httpx.Client` that provides the same defaults that this SDK + uses internally. + + This is useful because overriding the `http_client` with your own instance of + `httpx.Client` will result in httpx's defaults being used, not ours. + """ +else: + DefaultHttpxClient = _DefaultHttpxClient + + +class SyncHttpxClientWrapper(DefaultHttpxClient): def __del__(self) -> None: try: self.close() @@ -1262,7 +1282,27 @@ def get_api_list( return self._request_api_list(model, page, opts) -class AsyncHttpxClientWrapper(httpx.AsyncClient): +class _DefaultAsyncHttpxClient(httpx.AsyncClient): + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("timeout", DEFAULT_TIMEOUT) + kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) + kwargs.setdefault("follow_redirects", True) + super().__init__(**kwargs) + + +if TYPE_CHECKING: + DefaultAsyncHttpxClient = httpx.AsyncClient + """An alias to `httpx.AsyncClient` that provides the same defaults that this SDK + uses internally. + + This is useful because overriding the `http_client` with your own instance of + `httpx.AsyncClient` will result in httpx's defaults being used, not ours. + """ +else: + DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient + + +class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient): def __del__(self) -> None: try: # TODO(someday): support non asyncio runtimes here diff --git a/src/openai/_client.py b/src/openai/_client.py index 7fe2c9af79..e9169df72a 100644 --- a/src/openai/_client.py +++ b/src/openai/_client.py @@ -74,7 +74,9 @@ def __init__( max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, default_query: Mapping[str, object] | None = None, - # Configure a custom httpx client. See the [httpx documentation](https://www.python-httpx.org/api/#client) for more details. + # Configure a custom httpx client. + # We provide a `DefaultHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`. + # See the [httpx documentation](https://www.python-httpx.org/api/#client) for more details. http_client: httpx.Client | None = None, # Enable or disable schema validation for data returned by the API. # When enabled an error APIResponseValidationError is raised @@ -272,7 +274,9 @@ def __init__( max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, default_query: Mapping[str, object] | None = None, - # Configure a custom httpx client. See the [httpx documentation](https://www.python-httpx.org/api/#asyncclient) for more details. + # Configure a custom httpx client. + # We provide a `DefaultAsyncHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`. + # See the [httpx documentation](https://www.python-httpx.org/api/#asyncclient) for more details. http_client: httpx.AsyncClient | None = None, # Enable or disable schema validation for data returned by the API. # When enabled an error APIResponseValidationError is raised diff --git a/src/openai/_models.py b/src/openai/_models.py index 0f001150f5..80ab51256f 100644 --- a/src/openai/_models.py +++ b/src/openai/_models.py @@ -90,6 +90,79 @@ def model_fields_set(self) -> set[str]: class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] extra: Any = pydantic.Extra.allow # type: ignore + def to_dict( + self, + *, + mode: Literal["json", "python"] = "python", + use_api_names: bool = True, + exclude_unset: bool = True, + exclude_defaults: bool = False, + exclude_none: bool = False, + warnings: bool = True, + ) -> dict[str, object]: + """Recursively generate a dictionary representation of the model, optionally specifying which fields to include or exclude. + + By default, fields that were not set by the API will not be included, + and keys will match the API response, *not* the property names from the model. + + For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property, + the output will use the `"fooBar"` key (unless `use_api_names=False` is passed). + + Args: + mode: + If mode is 'json', the dictionary will only contain JSON serializable types. e.g. `datetime` will be turned into a string, `"2024-3-22T18:11:19.117000Z"`. + If mode is 'python', the dictionary may contain any Python objects. e.g. `datetime(2024, 3, 22)` + + use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that are set to their default value from the output. + exclude_none: Whether to exclude fields that have a value of `None` from the output. + warnings: Whether to log warnings when invalid fields are encountered. This is only supported in Pydantic v2. + """ + return self.model_dump( + mode=mode, + by_alias=use_api_names, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + warnings=warnings, + ) + + def to_json( + self, + *, + indent: int | None = 2, + use_api_names: bool = True, + exclude_unset: bool = True, + exclude_defaults: bool = False, + exclude_none: bool = False, + warnings: bool = True, + ) -> str: + """Generates a JSON string representing this model as it would be received from or sent to the API (but with indentation). + + By default, fields that were not set by the API will not be included, + and keys will match the API response, *not* the property names from the model. + + For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property, + the output will use the `"fooBar"` key (unless `use_api_names=False` is passed). + + Args: + indent: Indentation to use in the JSON output. If `None` is passed, the output will be compact. Defaults to `2` + use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that have the default value. + exclude_none: Whether to exclude fields that have a value of `None`. + warnings: Whether to show any warnings that occurred during serialization. This is only supported in Pydantic v2. + """ + return self.model_dump_json( + indent=indent, + by_alias=use_api_names, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + warnings=warnings, + ) + @override def __str__(self) -> str: # mypy complains about an invalid self arg diff --git a/src/openai/_version.py b/src/openai/_version.py index 85803a60a6..0c55423216 100644 --- a/src/openai/_version.py +++ b/src/openai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "openai" -__version__ = "1.16.2" # x-release-please-version +__version__ = "1.17.0" # x-release-please-version diff --git a/src/openai/lib/_validators.py b/src/openai/lib/_validators.py index e36f0e95fb..cf24cd2294 100644 --- a/src/openai/lib/_validators.py +++ b/src/openai/lib/_validators.py @@ -678,9 +678,11 @@ def write_out_file(df: pd.DataFrame, fname: str, any_remediations: bool, auto_ac df_train = df.sample(n=n_train, random_state=42) df_valid = df.drop(df_train.index) df_train[["prompt", "completion"]].to_json( # type: ignore - fnames[0], lines=True, orient="records", force_ascii=False + fnames[0], lines=True, orient="records", force_ascii=False, indent=None + ) + df_valid[["prompt", "completion"]].to_json( + fnames[1], lines=True, orient="records", force_ascii=False, indent=None ) - df_valid[["prompt", "completion"]].to_json(fnames[1], lines=True, orient="records", force_ascii=False) n_classes, pos_class = get_classification_hyperparams(df) additional_params += " --compute_classification_metrics" @@ -690,7 +692,9 @@ def write_out_file(df: pd.DataFrame, fname: str, any_remediations: bool, auto_ac additional_params += f" --classification_n_classes {n_classes}" else: assert len(fnames) == 1 - df[["prompt", "completion"]].to_json(fnames[0], lines=True, orient="records", force_ascii=False) + df[["prompt", "completion"]].to_json( + fnames[0], lines=True, orient="records", force_ascii=False, indent=None + ) # Add -v VALID_FILE if we split the file into train / valid files_string = ("s" if split else "") + " to `" + ("` and `".join(fnames)) diff --git a/src/openai/resources/beta/threads/runs/runs.py b/src/openai/resources/beta/threads/runs/runs.py index 4529c65025..8576a5c09a 100644 --- a/src/openai/resources/beta/threads/runs/runs.py +++ b/src/openai/resources/beta/threads/runs/runs.py @@ -75,6 +75,7 @@ def create( *, assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, metadata: Optional[object] | NotGiven = NOT_GIVEN, model: Optional[str] | NotGiven = NOT_GIVEN, @@ -100,6 +101,8 @@ def create( is useful for modifying the behavior on a per-run basis without overriding other instructions. + additional_messages: Adds additional messages to the thread before creating the run. + instructions: Overrides the [instructions](https://platform.openai.com/docs/api-reference/assistants/createAssistant) of the assistant. This is useful for modifying the behavior on a per-run basis. @@ -143,6 +146,7 @@ def create( assistant_id: str, stream: Literal[True], additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, metadata: Optional[object] | NotGiven = NOT_GIVEN, model: Optional[str] | NotGiven = NOT_GIVEN, @@ -171,6 +175,8 @@ def create( is useful for modifying the behavior on a per-run basis without overriding other instructions. + additional_messages: Adds additional messages to the thread before creating the run. + instructions: Overrides the [instructions](https://platform.openai.com/docs/api-reference/assistants/createAssistant) of the assistant. This is useful for modifying the behavior on a per-run basis. @@ -210,6 +216,7 @@ def create( assistant_id: str, stream: bool, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, metadata: Optional[object] | NotGiven = NOT_GIVEN, model: Optional[str] | NotGiven = NOT_GIVEN, @@ -238,6 +245,8 @@ def create( is useful for modifying the behavior on a per-run basis without overriding other instructions. + additional_messages: Adds additional messages to the thread before creating the run. + instructions: Overrides the [instructions](https://platform.openai.com/docs/api-reference/assistants/createAssistant) of the assistant. This is useful for modifying the behavior on a per-run basis. @@ -276,6 +285,7 @@ def create( *, assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, metadata: Optional[object] | NotGiven = NOT_GIVEN, model: Optional[str] | NotGiven = NOT_GIVEN, @@ -298,6 +308,7 @@ def create( { "assistant_id": assistant_id, "additional_instructions": additional_instructions, + "additional_messages": additional_messages, "instructions": instructions, "metadata": metadata, "model": model, @@ -505,6 +516,7 @@ def create_and_poll( *, assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, metadata: Optional[object] | NotGiven = NOT_GIVEN, model: Optional[str] | NotGiven = NOT_GIVEN, @@ -528,6 +540,7 @@ def create_and_poll( thread_id=thread_id, assistant_id=assistant_id, additional_instructions=additional_instructions, + additional_messages=additional_messages, instructions=instructions, metadata=metadata, model=model, @@ -557,6 +570,7 @@ def create_and_stream( *, assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, metadata: Optional[object] | NotGiven = NOT_GIVEN, model: Optional[str] | NotGiven = NOT_GIVEN, @@ -580,6 +594,7 @@ def create_and_stream( *, assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, metadata: Optional[object] | NotGiven = NOT_GIVEN, model: Optional[str] | NotGiven = NOT_GIVEN, @@ -603,6 +618,7 @@ def create_and_stream( *, assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, metadata: Optional[object] | NotGiven = NOT_GIVEN, model: Optional[str] | NotGiven = NOT_GIVEN, @@ -634,6 +650,7 @@ def create_and_stream( { "assistant_id": assistant_id, "additional_instructions": additional_instructions, + "additional_messages": additional_messages, "instructions": instructions, "metadata": metadata, "model": model, @@ -703,6 +720,7 @@ def stream( *, assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, metadata: Optional[object] | NotGiven = NOT_GIVEN, model: Optional[str] | NotGiven = NOT_GIVEN, @@ -725,6 +743,7 @@ def stream( *, assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, metadata: Optional[object] | NotGiven = NOT_GIVEN, model: Optional[str] | NotGiven = NOT_GIVEN, @@ -747,6 +766,7 @@ def stream( *, assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, metadata: Optional[object] | NotGiven = NOT_GIVEN, model: Optional[str] | NotGiven = NOT_GIVEN, @@ -778,6 +798,7 @@ def stream( { "assistant_id": assistant_id, "additional_instructions": additional_instructions, + "additional_messages": additional_messages, "instructions": instructions, "metadata": metadata, "model": model, @@ -1100,6 +1121,7 @@ async def create( *, assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, metadata: Optional[object] | NotGiven = NOT_GIVEN, model: Optional[str] | NotGiven = NOT_GIVEN, @@ -1125,6 +1147,8 @@ async def create( is useful for modifying the behavior on a per-run basis without overriding other instructions. + additional_messages: Adds additional messages to the thread before creating the run. + instructions: Overrides the [instructions](https://platform.openai.com/docs/api-reference/assistants/createAssistant) of the assistant. This is useful for modifying the behavior on a per-run basis. @@ -1168,6 +1192,7 @@ async def create( assistant_id: str, stream: Literal[True], additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, metadata: Optional[object] | NotGiven = NOT_GIVEN, model: Optional[str] | NotGiven = NOT_GIVEN, @@ -1196,6 +1221,8 @@ async def create( is useful for modifying the behavior on a per-run basis without overriding other instructions. + additional_messages: Adds additional messages to the thread before creating the run. + instructions: Overrides the [instructions](https://platform.openai.com/docs/api-reference/assistants/createAssistant) of the assistant. This is useful for modifying the behavior on a per-run basis. @@ -1235,6 +1262,7 @@ async def create( assistant_id: str, stream: bool, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, metadata: Optional[object] | NotGiven = NOT_GIVEN, model: Optional[str] | NotGiven = NOT_GIVEN, @@ -1263,6 +1291,8 @@ async def create( is useful for modifying the behavior on a per-run basis without overriding other instructions. + additional_messages: Adds additional messages to the thread before creating the run. + instructions: Overrides the [instructions](https://platform.openai.com/docs/api-reference/assistants/createAssistant) of the assistant. This is useful for modifying the behavior on a per-run basis. @@ -1301,6 +1331,7 @@ async def create( *, assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, metadata: Optional[object] | NotGiven = NOT_GIVEN, model: Optional[str] | NotGiven = NOT_GIVEN, @@ -1323,6 +1354,7 @@ async def create( { "assistant_id": assistant_id, "additional_instructions": additional_instructions, + "additional_messages": additional_messages, "instructions": instructions, "metadata": metadata, "model": model, @@ -1530,6 +1562,7 @@ async def create_and_poll( *, assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, metadata: Optional[object] | NotGiven = NOT_GIVEN, model: Optional[str] | NotGiven = NOT_GIVEN, @@ -1553,6 +1586,7 @@ async def create_and_poll( thread_id=thread_id, assistant_id=assistant_id, additional_instructions=additional_instructions, + additional_messages=additional_messages, instructions=instructions, metadata=metadata, model=model, @@ -1582,6 +1616,7 @@ def create_and_stream( *, assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, metadata: Optional[object] | NotGiven = NOT_GIVEN, model: Optional[str] | NotGiven = NOT_GIVEN, @@ -1605,6 +1640,7 @@ def create_and_stream( *, assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, metadata: Optional[object] | NotGiven = NOT_GIVEN, model: Optional[str] | NotGiven = NOT_GIVEN, @@ -1628,6 +1664,7 @@ def create_and_stream( *, assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, metadata: Optional[object] | NotGiven = NOT_GIVEN, model: Optional[str] | NotGiven = NOT_GIVEN, @@ -1661,6 +1698,7 @@ def create_and_stream( { "assistant_id": assistant_id, "additional_instructions": additional_instructions, + "additional_messages": additional_messages, "instructions": instructions, "metadata": metadata, "model": model, @@ -1730,6 +1768,7 @@ def stream( *, assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, metadata: Optional[object] | NotGiven = NOT_GIVEN, model: Optional[str] | NotGiven = NOT_GIVEN, @@ -1752,6 +1791,7 @@ def stream( *, assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, metadata: Optional[object] | NotGiven = NOT_GIVEN, model: Optional[str] | NotGiven = NOT_GIVEN, @@ -1774,6 +1814,7 @@ def stream( *, assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, metadata: Optional[object] | NotGiven = NOT_GIVEN, model: Optional[str] | NotGiven = NOT_GIVEN, @@ -1807,6 +1848,7 @@ def stream( { "assistant_id": assistant_id, "additional_instructions": additional_instructions, + "additional_messages": additional_messages, "instructions": instructions, "metadata": metadata, "model": model, diff --git a/src/openai/types/beta/threads/run_create_params.py b/src/openai/types/beta/threads/run_create_params.py index ac185973a5..e9bc19d980 100644 --- a/src/openai/types/beta/threads/run_create_params.py +++ b/src/openai/types/beta/threads/run_create_params.py @@ -2,12 +2,12 @@ from __future__ import annotations -from typing import Union, Iterable, Optional +from typing import List, Union, Iterable, Optional from typing_extensions import Literal, Required, TypedDict from ..assistant_tool_param import AssistantToolParam -__all__ = ["RunCreateParamsBase", "RunCreateParamsNonStreaming", "RunCreateParamsStreaming"] +__all__ = ["RunCreateParamsBase", "AdditionalMessage", "RunCreateParamsNonStreaming", "RunCreateParamsStreaming"] class RunCreateParamsBase(TypedDict, total=False): @@ -25,6 +25,9 @@ class RunCreateParamsBase(TypedDict, total=False): other instructions. """ + additional_messages: Optional[Iterable[AdditionalMessage]] + """Adds additional messages to the thread before creating the run.""" + instructions: Optional[str] """ Overrides the @@ -62,6 +65,36 @@ class RunCreateParamsBase(TypedDict, total=False): """ +class AdditionalMessage(TypedDict, total=False): + content: Required[str] + """The content of the message.""" + + role: Required[Literal["user", "assistant"]] + """The role of the entity that is creating the message. Allowed values include: + + - `user`: Indicates the message is sent by an actual user and should be used in + most cases to represent user-generated messages. + - `assistant`: Indicates the message is generated by the assistant. Use this + value to insert messages from the assistant into the conversation. + """ + + file_ids: List[str] + """ + A list of [File](https://platform.openai.com/docs/api-reference/files) IDs that + the message should use. There can be a maximum of 10 files attached to a + message. Useful for tools like `retrieval` and `code_interpreter` that can + access and use files. + """ + + metadata: Optional[object] + """Set of 16 key-value pairs that can be attached to an object. + + This can be useful for storing additional information about the object in a + structured format. Keys can be a maximum of 64 characters long and values can be + a maxium of 512 characters long. + """ + + class RunCreateParamsNonStreaming(RunCreateParamsBase): stream: Optional[Literal[False]] """ diff --git a/tests/api_resources/beta/threads/test_runs.py b/tests/api_resources/beta/threads/test_runs.py index b9f392dc87..271bcccdd3 100644 --- a/tests/api_resources/beta/threads/test_runs.py +++ b/tests/api_resources/beta/threads/test_runs.py @@ -36,6 +36,26 @@ def test_method_create_with_all_params_overload_1(self, client: OpenAI) -> None: "string", assistant_id="string", additional_instructions="string", + additional_messages=[ + { + "role": "user", + "content": "x", + "file_ids": ["string"], + "metadata": {}, + }, + { + "role": "user", + "content": "x", + "file_ids": ["string"], + "metadata": {}, + }, + { + "role": "user", + "content": "x", + "file_ids": ["string"], + "metadata": {}, + }, + ], instructions="string", metadata={}, model="string", @@ -95,6 +115,26 @@ def test_method_create_with_all_params_overload_2(self, client: OpenAI) -> None: assistant_id="string", stream=True, additional_instructions="string", + additional_messages=[ + { + "role": "user", + "content": "x", + "file_ids": ["string"], + "metadata": {}, + }, + { + "role": "user", + "content": "x", + "file_ids": ["string"], + "metadata": {}, + }, + { + "role": "user", + "content": "x", + "file_ids": ["string"], + "metadata": {}, + }, + ], instructions="string", metadata={}, model="string", @@ -492,6 +532,26 @@ async def test_method_create_with_all_params_overload_1(self, async_client: Asyn "string", assistant_id="string", additional_instructions="string", + additional_messages=[ + { + "role": "user", + "content": "x", + "file_ids": ["string"], + "metadata": {}, + }, + { + "role": "user", + "content": "x", + "file_ids": ["string"], + "metadata": {}, + }, + { + "role": "user", + "content": "x", + "file_ids": ["string"], + "metadata": {}, + }, + ], instructions="string", metadata={}, model="string", @@ -551,6 +611,26 @@ async def test_method_create_with_all_params_overload_2(self, async_client: Asyn assistant_id="string", stream=True, additional_instructions="string", + additional_messages=[ + { + "role": "user", + "content": "x", + "file_ids": ["string"], + "metadata": {}, + }, + { + "role": "user", + "content": "x", + "file_ids": ["string"], + "metadata": {}, + }, + { + "role": "user", + "content": "x", + "file_ids": ["string"], + "metadata": {}, + }, + ], instructions="string", metadata={}, model="string", diff --git a/tests/test_models.py b/tests/test_models.py index d003d32181..969e4eb315 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -501,6 +501,42 @@ class Model(BaseModel): assert "resource_id" in m.model_fields_set +def test_to_dict() -> None: + class Model(BaseModel): + foo: Optional[str] = Field(alias="FOO", default=None) + + m = Model(FOO="hello") + assert m.to_dict() == {"FOO": "hello"} + assert m.to_dict(use_api_names=False) == {"foo": "hello"} + + m2 = Model() + assert m2.to_dict() == {} + assert m2.to_dict(exclude_unset=False) == {"FOO": None} + assert m2.to_dict(exclude_unset=False, exclude_none=True) == {} + assert m2.to_dict(exclude_unset=False, exclude_defaults=True) == {} + + m3 = Model(FOO=None) + assert m3.to_dict() == {"FOO": None} + assert m3.to_dict(exclude_none=True) == {} + assert m3.to_dict(exclude_defaults=True) == {} + + if PYDANTIC_V2: + + class Model2(BaseModel): + created_at: datetime + + time_str = "2024-03-21T11:39:01.275859" + m4 = Model2.construct(created_at=time_str) + assert m4.to_dict(mode="python") == {"created_at": datetime.fromisoformat(time_str)} + assert m4.to_dict(mode="json") == {"created_at": time_str} + else: + with pytest.raises(ValueError, match="mode is only supported in Pydantic v2"): + m.to_dict(mode="json") + + with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): + m.to_dict(warnings=False) + + def test_forwards_compat_model_dump_method() -> None: class Model(BaseModel): foo: Optional[str] = Field(alias="FOO", default=None) @@ -532,6 +568,34 @@ class Model(BaseModel): m.model_dump(warnings=False) +def test_to_json() -> None: + class Model(BaseModel): + foo: Optional[str] = Field(alias="FOO", default=None) + + m = Model(FOO="hello") + assert json.loads(m.to_json()) == {"FOO": "hello"} + assert json.loads(m.to_json(use_api_names=False)) == {"foo": "hello"} + + if PYDANTIC_V2: + assert m.to_json(indent=None) == '{"FOO":"hello"}' + else: + assert m.to_json(indent=None) == '{"FOO": "hello"}' + + m2 = Model() + assert json.loads(m2.to_json()) == {} + assert json.loads(m2.to_json(exclude_unset=False)) == {"FOO": None} + assert json.loads(m2.to_json(exclude_unset=False, exclude_none=True)) == {} + assert json.loads(m2.to_json(exclude_unset=False, exclude_defaults=True)) == {} + + m3 = Model(FOO=None) + assert json.loads(m3.to_json()) == {"FOO": None} + assert json.loads(m3.to_json(exclude_none=True)) == {} + + if not PYDANTIC_V2: + with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): + m.to_json(warnings=False) + + def test_forwards_compat_model_dump_json_method() -> None: class Model(BaseModel): foo: Optional[str] = Field(alias="FOO", default=None)