mirror of
https://github.com/hwchase17/langchain.git
synced 2025-09-15 22:44:36 +00:00
openai[patch]: support structured output via Responses API (#30265)
Also runs all standard tests using Responses API.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"""Test Responses API usage."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Any, Optional, cast
|
||||
|
||||
@@ -10,9 +11,13 @@ from langchain_core.messages import (
|
||||
BaseMessage,
|
||||
BaseMessageChunk,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
MODEL_NAME = "gpt-4o-mini"
|
||||
|
||||
|
||||
def _check_response(response: Optional[BaseMessage]) -> None:
|
||||
assert isinstance(response, AIMessage)
|
||||
@@ -48,7 +53,7 @@ def _check_response(response: Optional[BaseMessage]) -> None:
|
||||
|
||||
|
||||
def test_web_search() -> None:
|
||||
llm = ChatOpenAI(model="gpt-4o-mini")
|
||||
llm = ChatOpenAI(model=MODEL_NAME)
|
||||
first_response = llm.invoke(
|
||||
"What was a positive news story from today?",
|
||||
tools=[{"type": "web_search_preview"}],
|
||||
@@ -94,7 +99,7 @@ def test_web_search() -> None:
|
||||
|
||||
|
||||
async def test_web_search_async() -> None:
|
||||
llm = ChatOpenAI(model="gpt-4o-mini")
|
||||
llm = ChatOpenAI(model=MODEL_NAME)
|
||||
response = await llm.ainvoke(
|
||||
"What was a positive news story from today?",
|
||||
tools=[{"type": "web_search_preview"}],
|
||||
@@ -119,7 +124,7 @@ def test_function_calling() -> None:
|
||||
"""return x * y"""
|
||||
return x * y
|
||||
|
||||
llm = ChatOpenAI(model="gpt-4o-mini")
|
||||
llm = ChatOpenAI(model=MODEL_NAME)
|
||||
bound_llm = llm.bind_tools([multiply, {"type": "web_search_preview"}])
|
||||
ai_msg = cast(AIMessage, bound_llm.invoke("whats 5 * 4"))
|
||||
assert len(ai_msg.tool_calls) == 1
|
||||
@@ -138,8 +143,110 @@ def test_function_calling() -> None:
|
||||
_check_response(response)
|
||||
|
||||
|
||||
class Foo(BaseModel):
|
||||
response: str
|
||||
|
||||
|
||||
class FooDict(TypedDict):
|
||||
response: str
|
||||
|
||||
|
||||
def test_parsed_pydantic_schema() -> None:
|
||||
llm = ChatOpenAI(model=MODEL_NAME, use_responses_api=True)
|
||||
response = llm.invoke("how are ya", response_format=Foo)
|
||||
parsed = Foo(**json.loads(response.text()))
|
||||
assert parsed == response.additional_kwargs["parsed"]
|
||||
assert parsed.response
|
||||
|
||||
# Test stream
|
||||
full: Optional[BaseMessageChunk] = None
|
||||
for chunk in llm.stream("how are ya", response_format=Foo):
|
||||
assert isinstance(chunk, AIMessageChunk)
|
||||
full = chunk if full is None else full + chunk
|
||||
assert isinstance(full, AIMessageChunk)
|
||||
parsed = Foo(**json.loads(full.text()))
|
||||
assert parsed == full.additional_kwargs["parsed"]
|
||||
assert parsed.response
|
||||
|
||||
|
||||
async def test_parsed_pydantic_schema_async() -> None:
|
||||
llm = ChatOpenAI(model=MODEL_NAME, use_responses_api=True)
|
||||
response = await llm.ainvoke("how are ya", response_format=Foo)
|
||||
parsed = Foo(**json.loads(response.text()))
|
||||
assert parsed == response.additional_kwargs["parsed"]
|
||||
assert parsed.response
|
||||
|
||||
# Test stream
|
||||
full: Optional[BaseMessageChunk] = None
|
||||
async for chunk in llm.astream("how are ya", response_format=Foo):
|
||||
assert isinstance(chunk, AIMessageChunk)
|
||||
full = chunk if full is None else full + chunk
|
||||
assert isinstance(full, AIMessageChunk)
|
||||
parsed = Foo(**json.loads(full.text()))
|
||||
assert parsed == full.additional_kwargs["parsed"]
|
||||
assert parsed.response
|
||||
|
||||
|
||||
@pytest.mark.parametrize("schema", [Foo.model_json_schema(), FooDict])
|
||||
def test_parsed_dict_schema(schema: Any) -> None:
|
||||
llm = ChatOpenAI(model=MODEL_NAME, use_responses_api=True)
|
||||
response = llm.invoke("how are ya", response_format=schema)
|
||||
parsed = json.loads(response.text())
|
||||
assert parsed == response.additional_kwargs["parsed"]
|
||||
assert parsed["response"] and isinstance(parsed["response"], str)
|
||||
|
||||
# Test stream
|
||||
full: Optional[BaseMessageChunk] = None
|
||||
for chunk in llm.stream("how are ya", response_format=schema):
|
||||
assert isinstance(chunk, AIMessageChunk)
|
||||
full = chunk if full is None else full + chunk
|
||||
assert isinstance(full, AIMessageChunk)
|
||||
parsed = json.loads(full.text())
|
||||
assert parsed == full.additional_kwargs["parsed"]
|
||||
assert parsed["response"] and isinstance(parsed["response"], str)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("schema", [Foo.model_json_schema(), FooDict])
|
||||
async def test_parsed_dict_schema_async(schema: Any) -> None:
|
||||
llm = ChatOpenAI(model=MODEL_NAME, use_responses_api=True)
|
||||
response = await llm.ainvoke("how are ya", response_format=schema)
|
||||
parsed = json.loads(response.text())
|
||||
assert parsed == response.additional_kwargs["parsed"]
|
||||
assert parsed["response"] and isinstance(parsed["response"], str)
|
||||
|
||||
# Test stream
|
||||
full: Optional[BaseMessageChunk] = None
|
||||
async for chunk in llm.astream("how are ya", response_format=schema):
|
||||
assert isinstance(chunk, AIMessageChunk)
|
||||
full = chunk if full is None else full + chunk
|
||||
assert isinstance(full, AIMessageChunk)
|
||||
parsed = json.loads(full.text())
|
||||
assert parsed == full.additional_kwargs["parsed"]
|
||||
assert parsed["response"] and isinstance(parsed["response"], str)
|
||||
|
||||
|
||||
def test_function_calling_and_structured_output() -> None:
|
||||
def multiply(x: int, y: int) -> int:
|
||||
"""return x * y"""
|
||||
return x * y
|
||||
|
||||
llm = ChatOpenAI(model=MODEL_NAME)
|
||||
bound_llm = llm.bind_tools([multiply], response_format=Foo, strict=True)
|
||||
# Test structured output
|
||||
response = llm.invoke("how are ya", response_format=Foo)
|
||||
parsed = Foo(**json.loads(response.text()))
|
||||
assert parsed == response.additional_kwargs["parsed"]
|
||||
assert parsed.response
|
||||
|
||||
# Test function calling
|
||||
ai_msg = cast(AIMessage, bound_llm.invoke("whats 5 * 4"))
|
||||
assert len(ai_msg.tool_calls) == 1
|
||||
assert ai_msg.tool_calls[0]["name"] == "multiply"
|
||||
assert set(ai_msg.tool_calls[0]["args"]) == {"x", "y"}
|
||||
|
||||
|
||||
def test_stateful_api() -> None:
|
||||
llm = ChatOpenAI(model="gpt-4o-mini", use_responses_api=True)
|
||||
llm = ChatOpenAI(model=MODEL_NAME, use_responses_api=True)
|
||||
response = llm.invoke("how are you, my name is Bobo")
|
||||
assert "id" in response.response_metadata
|
||||
|
||||
@@ -152,7 +259,7 @@ def test_stateful_api() -> None:
|
||||
|
||||
def test_file_search() -> None:
|
||||
pytest.skip() # TODO: set up infra
|
||||
llm = ChatOpenAI(model="gpt-4o-mini")
|
||||
llm = ChatOpenAI(model=MODEL_NAME)
|
||||
tool = {
|
||||
"type": "file_search",
|
||||
"vector_store_ids": [os.environ["OPENAI_VECTOR_STORE_ID"]],
|
||||
|
@@ -0,0 +1,23 @@
|
||||
"""Standard LangChain interface tests for Responses API"""
|
||||
|
||||
from typing import Type
|
||||
|
||||
import pytest
|
||||
from langchain_core.language_models import BaseChatModel
|
||||
|
||||
from langchain_openai import ChatOpenAI
|
||||
from tests.integration_tests.chat_models.test_base_standard import TestOpenAIStandard
|
||||
|
||||
|
||||
class TestOpenAIResponses(TestOpenAIStandard):
|
||||
@property
|
||||
def chat_model_class(self) -> Type[BaseChatModel]:
|
||||
return ChatOpenAI
|
||||
|
||||
@property
|
||||
def chat_model_params(self) -> dict:
|
||||
return {"model": "gpt-4o-mini", "use_responses_api": True}
|
||||
|
||||
@pytest.mark.xfail(reason="Unsupported.")
|
||||
def test_stop_sequence(self, model: BaseChatModel) -> None:
|
||||
super().test_stop_sequence(model)
|
Reference in New Issue
Block a user