Files
langchain/libs/langchain_v1/tests/unit_tests/agents/test_responses.py
Christophe Bornet 36e590ca5f test(langchain): complete and activate test_responses tests (#34560)
Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2026-01-09 22:17:03 -05:00

283 lines
11 KiB
Python

"""Unit tests for langchain.agents.structured_output module."""
import pytest
from langchain_core.messages import AIMessage
from pydantic import BaseModel
from langchain.agents.structured_output import (
OutputToolBinding,
ProviderStrategy,
ProviderStrategyBinding,
ToolStrategy,
_SchemaSpec,
)
class _TestModel(BaseModel):
"""A test model for structured output."""
name: str
age: int
email: str = "default@example.com"
class CustomModel(BaseModel):
"""Custom model with a custom docstring."""
value: float
description: str
class EmptyDocModel(BaseModel):
# No custom docstring, should have no description in tool
data: str
class TestToolStrategy:
"""Test ToolStrategy dataclass."""
def test_basic_creation(self) -> None:
"""Test basic ToolStrategy creation."""
strategy = ToolStrategy(schema=_TestModel)
assert strategy.schema == _TestModel
assert strategy.tool_message_content is None
assert len(strategy.schema_specs) == 1
assert strategy.schema_specs[0].schema == _TestModel
def test_multiple_schemas(self) -> None:
"""Test ToolStrategy with multiple schemas."""
strategy = ToolStrategy(schema=_TestModel | CustomModel)
assert len(strategy.schema_specs) == 2
assert strategy.schema_specs[0].schema == _TestModel
assert strategy.schema_specs[1].schema == CustomModel
def test_schema_with_tool_message_content(self) -> None:
"""Test ToolStrategy with tool message content."""
strategy = ToolStrategy(schema=_TestModel, tool_message_content="custom message")
assert strategy.schema == _TestModel
assert strategy.tool_message_content == "custom message"
assert len(strategy.schema_specs) == 1
assert strategy.schema_specs[0].schema == _TestModel
class TestProviderStrategy:
"""Test ProviderStrategy dataclass."""
def test_basic_creation(self) -> None:
"""Test basic ProviderStrategy creation."""
strategy = ProviderStrategy(schema=_TestModel)
assert strategy.schema == _TestModel
assert strategy.schema_spec.schema == _TestModel
assert strategy.schema_spec.strict is None
def test_strict(self) -> None:
"""Test ProviderStrategy creation with strict=True."""
strategy = ProviderStrategy(schema=_TestModel, strict=True)
assert strategy.schema == _TestModel
assert strategy.schema_spec.schema == _TestModel
assert strategy.schema_spec.strict is True
def test_to_model_kwargs(self) -> None:
strategy_default = ProviderStrategy(schema=_TestModel)
assert strategy_default.to_model_kwargs() == {
"response_format": {
"json_schema": {
"name": "_TestModel",
"schema": {
"description": "A test model for structured output.",
"properties": {
"age": {"title": "Age", "type": "integer"},
"email": {
"default": "default@example.com",
"title": "Email",
"type": "string",
},
"name": {"title": "Name", "type": "string"},
},
"required": ["name", "age"],
"title": "_TestModel",
"type": "object",
},
},
"type": "json_schema",
}
}
def test_to_model_kwargs_strict(self) -> None:
strategy_default = ProviderStrategy(schema=_TestModel, strict=True)
assert strategy_default.to_model_kwargs() == {
"response_format": {
"json_schema": {
"name": "_TestModel",
"schema": {
"description": "A test model for structured output.",
"properties": {
"age": {"title": "Age", "type": "integer"},
"email": {
"default": "default@example.com",
"title": "Email",
"type": "string",
},
"name": {"title": "Name", "type": "string"},
},
"required": ["name", "age"],
"title": "_TestModel",
"type": "object",
},
"strict": True,
},
"type": "json_schema",
}
}
class TestOutputToolBinding:
"""Test OutputToolBinding dataclass and its methods."""
def test_from_schema_spec_basic(self) -> None:
"""Test basic OutputToolBinding creation from SchemaSpec."""
schema_spec = _SchemaSpec(schema=_TestModel)
tool_binding = OutputToolBinding.from_schema_spec(schema_spec)
assert tool_binding.schema == _TestModel
assert tool_binding.schema_kind == "pydantic"
assert tool_binding.tool is not None
assert tool_binding.tool.name == "_TestModel"
def test_from_schema_spec_with_custom_name(self) -> None:
"""Test OutputToolBinding creation with custom name."""
schema_spec = _SchemaSpec(schema=_TestModel, name="custom_tool_name")
tool_binding = OutputToolBinding.from_schema_spec(schema_spec)
assert tool_binding.tool.name == "custom_tool_name"
def test_from_schema_spec_with_custom_description(self) -> None:
"""Test OutputToolBinding creation with custom description."""
schema_spec = _SchemaSpec(schema=_TestModel, description="Custom tool description")
tool_binding = OutputToolBinding.from_schema_spec(schema_spec)
assert tool_binding.tool.description == "Custom tool description"
def test_from_schema_spec_with_model_docstring(self) -> None:
"""Test OutputToolBinding creation using model docstring as description."""
schema_spec = _SchemaSpec(schema=CustomModel)
tool_binding = OutputToolBinding.from_schema_spec(schema_spec)
assert tool_binding.tool.description == "Custom model with a custom docstring."
def test_from_schema_spec_empty_docstring(self) -> None:
"""Test OutputToolBinding creation with model that has default docstring."""
# Create a model with the same docstring as BaseModel
class DefaultDocModel(BaseModel):
# This should have the same docstring as BaseModel
pass
schema_spec = _SchemaSpec(schema=DefaultDocModel)
tool_binding = OutputToolBinding.from_schema_spec(schema_spec)
# Should use empty description when model has default BaseModel docstring
assert not tool_binding.tool.description
def test_parse_payload_pydantic_success(self) -> None:
"""Test successful parsing for Pydantic model."""
schema_spec = _SchemaSpec(schema=_TestModel)
tool_binding = OutputToolBinding.from_schema_spec(schema_spec)
tool_args = {"name": "John", "age": 30}
result = tool_binding.parse(tool_args)
assert isinstance(result, _TestModel)
assert result.name == "John"
assert result.age == 30
assert result.email == "default@example.com" # default value
def test_parse_payload_pydantic_validation_error(self) -> None:
"""Test parsing failure for invalid Pydantic data."""
schema_spec = _SchemaSpec(schema=_TestModel)
tool_binding = OutputToolBinding.from_schema_spec(schema_spec)
# Missing required field 'name'
tool_args = {"age": 30}
with pytest.raises(ValueError, match="Failed to parse data to _TestModel"):
tool_binding.parse(tool_args)
class TestProviderStrategyBinding:
"""Test ProviderStrategyBinding dataclass and its methods."""
def test_from_schema_spec_basic(self) -> None:
"""Test basic ProviderStrategyBinding creation from SchemaSpec."""
schema_spec = _SchemaSpec(schema=_TestModel)
tool_binding = ProviderStrategyBinding.from_schema_spec(schema_spec)
assert tool_binding.schema == _TestModel
assert tool_binding.schema_kind == "pydantic"
def test_parse_payload_pydantic_success(self) -> None:
"""Test successful parsing for Pydantic model."""
schema_spec = _SchemaSpec(schema=_TestModel)
tool_binding = ProviderStrategyBinding.from_schema_spec(schema_spec)
message = AIMessage(content='{"name": "John", "age": 30}')
result = tool_binding.parse(message)
assert isinstance(result, _TestModel)
assert result.name == "John"
assert result.age == 30
assert result.email == "default@example.com" # default value
def test_parse_payload_pydantic_validation_error(self) -> None:
"""Test parsing failure for invalid Pydantic data."""
schema_spec = _SchemaSpec(schema=_TestModel)
tool_binding = ProviderStrategyBinding.from_schema_spec(schema_spec)
# Missing required field 'name'
message = AIMessage(content='{"age": 30}')
with pytest.raises(ValueError, match="Failed to parse data to _TestModel"):
tool_binding.parse(message)
def test_parse_payload_pydantic_json_error(self) -> None:
"""Test parsing failure for invalid JSON data."""
schema_spec = _SchemaSpec(schema=_TestModel)
tool_binding = ProviderStrategyBinding.from_schema_spec(schema_spec)
message = AIMessage(content="invalid json")
with pytest.raises(
ValueError,
match="Native structured output expected valid JSON for _TestModel, but parsing failed",
):
tool_binding.parse(message)
def test_parse_content_list(self) -> None:
"""Test successful parsing for Pydantic model with content as list."""
schema_spec = _SchemaSpec(schema=_TestModel)
tool_binding = ProviderStrategyBinding.from_schema_spec(schema_spec)
message = AIMessage(
content=['{"name":', {"content": ' "John",'}, {"type": "text", "text": ' "age": 30}'}]
)
result = tool_binding.parse(message)
assert isinstance(result, _TestModel)
assert result.name == "John"
assert result.age == 30
assert result.email == "default@example.com" # default value
class TestEdgeCases:
"""Test edge cases and error conditions."""
def test_single_schema(self) -> None:
"""Test ToolStrategy with a single schema creates one schema spec."""
strategy = ToolStrategy(EmptyDocModel)
assert len(strategy.schema_specs) == 1
def test_empty_docstring_model(self) -> None:
"""Test that models without explicit docstrings have empty tool descriptions."""
binding = OutputToolBinding.from_schema_spec(_SchemaSpec(EmptyDocModel))
assert binding.tool.name == "EmptyDocModel"
assert not binding.tool.description