openai[minor]: release 0.3 (#29100)

## Goal

Solve the following problems with `langchain-openai`:

- Structured output with `o1` [breaks out of the
box](https://langchain.slack.com/archives/C050X0VTN56/p1735232400232099).
- `with_structured_output` by default does not use OpenAI’s [structured
output
feature](https://platform.openai.com/docs/guides/structured-outputs).
- We override API defaults for temperature and other parameters.

## Breaking changes:

- Default method for structured output is changing to OpenAI’s dedicated
[structured output
feature](https://platform.openai.com/docs/guides/structured-outputs).
For schemas specified via TypedDict or JSON schema, strict schema
validation is disabled by default but can be enabled by specifying
`strict=True`.
- To recover previous default, pass `method="function_calling"` into
`with_structured_output`.
- Models that don’t support `method="json_schema"` (e.g., `gpt-4` and
`gpt-3.5-turbo`, currently the default model for ChatOpenAI) will raise
an error unless `method` is explicitly specified.
- To recover previous default, pass `method="function_calling"` into
`with_structured_output`.
- Schemas specified via Pydantic `BaseModel` that have fields with
non-null defaults or metadata (like min/max constraints) will raise an
error.
- To recover previous default, pass `method="function_calling"` into
`with_structured_output`.
- `strict` now defaults to False for `method="json_schema"` when schemas
are specified via TypedDict or JSON schema.
- To recover previous behavior, use `with_structured_output(schema,
strict=True)`
- Schemas specified via Pydantic V1 will raise a warning (and use
`method="function_calling"`) unless `method` is explicitly specified.
- To remove the warning, pass `method="function_calling"` into
`with_structured_output`.
- Streaming with default structured output method / Pydantic schema no
longer generates intermediate streamed chunks.
- To recover previous behavior, pass `method="function_calling"` into
`with_structured_output`.
- We no longer override default temperature (was 0.7 in LangChain, now
will follow OpenAI, currently 1.0).
- To recover previous behavior, initialize `ChatOpenAI` or
`AzureChatOpenAI` with `temperature=0.7`.
- Note: conceptually there is a difference between forcing a tool call
and forcing a response format. Tool calls may have more concise
arguments vs. generating content adhering to a schema. Prompts may need
to be adjusted to recover desired behavior.

---------

Co-authored-by: Jacob Lee <jacoblee93@gmail.com>
Co-authored-by: Bagatur <baskaryan@gmail.com>
This commit is contained in:
ccurme
2025-01-10 10:50:32 -05:00
committed by GitHub
parent facfd42768
commit 6e63ccba84
14 changed files with 912 additions and 295 deletions

View File

@@ -21,6 +21,7 @@ from langchain_core.utils.function_calling import tool_example_to_messages
from pydantic import BaseModel, Field
from pydantic.v1 import BaseModel as BaseModelV1
from pydantic.v1 import Field as FieldV1
from typing_extensions import Annotated, TypedDict
from langchain_tests.unit_tests.chat_models import (
ChatModelTests,
@@ -191,6 +192,19 @@ class ChatModelIntegrationTests(ChatModelTests):
def has_structured_output(self) -> bool:
return True
.. dropdown:: structured_output_kwargs
Dict property that can be used to specify additional kwargs for
``with_structured_output``. Useful for testing different models.
Example:
.. code-block:: python
@property
def structured_output_kwargs(self) -> dict:
return {"method": "function_calling"}
.. dropdown:: supports_json_mode
Boolean property indicating whether the chat model supports JSON mode in
@@ -1128,10 +1142,7 @@ class ChatModelIntegrationTests(ChatModelTests):
Joke = _get_joke_class()
# Pydantic class
# Type ignoring since the interface only officially supports pydantic 1
# or pydantic.v1.BaseModel but not pydantic.BaseModel from pydantic 2.
# We'll need to do a pass updating the type signatures.
chat = model.with_structured_output(Joke) # type: ignore[arg-type]
chat = model.with_structured_output(Joke, **self.structured_output_kwargs)
result = chat.invoke("Tell me a joke about cats.")
assert isinstance(result, Joke)
@@ -1139,7 +1150,9 @@ class ChatModelIntegrationTests(ChatModelTests):
assert isinstance(chunk, Joke)
# Schema
chat = model.with_structured_output(Joke.model_json_schema())
chat = model.with_structured_output(
Joke.model_json_schema(), **self.structured_output_kwargs
)
result = chat.invoke("Tell me a joke about cats.")
assert isinstance(result, dict)
assert set(result.keys()) == {"setup", "punchline"}
@@ -1182,10 +1195,7 @@ class ChatModelIntegrationTests(ChatModelTests):
Joke = _get_joke_class()
# Pydantic class
# Type ignoring since the interface only officially supports pydantic 1
# or pydantic.v1.BaseModel but not pydantic.BaseModel from pydantic 2.
# We'll need to do a pass updating the type signatures.
chat = model.with_structured_output(Joke) # type: ignore[arg-type]
chat = model.with_structured_output(Joke, **self.structured_output_kwargs)
result = await chat.ainvoke("Tell me a joke about cats.")
assert isinstance(result, Joke)
@@ -1193,7 +1203,9 @@ class ChatModelIntegrationTests(ChatModelTests):
assert isinstance(chunk, Joke)
# Schema
chat = model.with_structured_output(Joke.model_json_schema())
chat = model.with_structured_output(
Joke.model_json_schema(), **self.structured_output_kwargs
)
result = await chat.ainvoke("Tell me a joke about cats.")
assert isinstance(result, dict)
assert set(result.keys()) == {"setup", "punchline"}
@@ -1244,7 +1256,7 @@ class ChatModelIntegrationTests(ChatModelTests):
punchline: str = FieldV1(description="answer to resolve the joke")
# Pydantic class
chat = model.with_structured_output(Joke)
chat = model.with_structured_output(Joke, **self.structured_output_kwargs)
result = chat.invoke("Tell me a joke about cats.")
assert isinstance(result, Joke)
@@ -1252,7 +1264,9 @@ class ChatModelIntegrationTests(ChatModelTests):
assert isinstance(chunk, Joke)
# Schema
chat = model.with_structured_output(Joke.schema())
chat = model.with_structured_output(
Joke.schema(), **self.structured_output_kwargs
)
result = chat.invoke("Tell me a joke about cats.")
assert isinstance(result, dict)
assert set(result.keys()) == {"setup", "punchline"}
@@ -1293,6 +1307,7 @@ class ChatModelIntegrationTests(ChatModelTests):
if not self.has_tool_calling:
pytest.skip("Test requires tool calling.")
# Pydantic
class Joke(BaseModel):
"""Joke to tell user."""
@@ -1301,7 +1316,7 @@ class ChatModelIntegrationTests(ChatModelTests):
default=None, description="answer to resolve the joke"
)
chat = model.with_structured_output(Joke) # type: ignore[arg-type]
chat = model.with_structured_output(Joke, **self.structured_output_kwargs)
setup_result = chat.invoke(
"Give me the setup to a joke about cats, no punchline."
)
@@ -1310,6 +1325,24 @@ class ChatModelIntegrationTests(ChatModelTests):
joke_result = chat.invoke("Give me a joke about cats, include the punchline.")
assert isinstance(joke_result, Joke)
# Schema
chat = model.with_structured_output(
Joke.model_json_schema(), **self.structured_output_kwargs
)
result = chat.invoke("Tell me a joke about cats.")
assert isinstance(result, dict)
# TypedDict
class JokeDict(TypedDict):
"""Joke to tell user."""
setup: Annotated[str, ..., "question to set up a joke"]
punchline: Annotated[Optional[str], None, "answer to resolve the joke"]
chat = model.with_structured_output(JokeDict, **self.structured_output_kwargs)
result = chat.invoke("Tell me a joke about cats.")
assert isinstance(result, dict)
def test_json_mode(self, model: BaseChatModel) -> None:
"""Test structured output via `JSON mode. <https://python.langchain.com/docs/concepts/structured_outputs/#json-mode>`_

View File

@@ -132,6 +132,11 @@ class ChatModelTests(BaseStandardTests):
is not BaseChatModel.with_structured_output
)
@property
def structured_output_kwargs(self) -> dict:
"""If specified, additional kwargs for with_structured_output."""
return {}
@property
def supports_json_mode(self) -> bool:
"""(bool) whether the chat model supports JSON mode."""
@@ -299,6 +304,19 @@ class ChatModelUnitTests(ChatModelTests):
def has_structured_output(self) -> bool:
return True
.. dropdown:: structured_output_kwargs
Dict property that can be used to specify additional kwargs for
``with_structured_output``. Useful for testing different models.
Example:
.. code-block:: python
@property
def structured_output_kwargs(self) -> dict:
return {"method": "function_calling"}
.. dropdown:: supports_json_mode
Boolean property indicating whether the chat model supports JSON mode in