test(langchain): activate test_responses_spec tests (#34564)

description by @mdrxy

- Enable `test_responses_spec.py` integration tests that were previously
skipped at module level
- Widen `ToolStrategy.schema` type annotation from `type[SchemaT]` to
`type[SchemaT] | dict[str, Any]` to match actual supported usage (JSON
schema dicts were already handled at runtime)
- Fix type annotations and linting issues in test file (modernize to
`dict`/`list`, add return types, prefix unused `_request` param)
- Improve generic typing in `load_spec` utility with bounded `TypeVar`

Co-authored-by: Mason Daugherty <mason@langchain.dev>
This commit is contained in:
Christophe Bornet
2026-01-09 23:44:33 +01:00
committed by GitHub
parent b4cd67ac15
commit 9ce73a73f8
4 changed files with 35 additions and 14 deletions

View File

@@ -192,7 +192,7 @@ class _SchemaSpec(Generic[SchemaT]):
class ToolStrategy(Generic[SchemaT]): class ToolStrategy(Generic[SchemaT]):
"""Use a tool calling strategy for model responses.""" """Use a tool calling strategy for model responses."""
schema: type[SchemaT] schema: type[SchemaT] | dict[str, Any]
"""Schema for the tool calls.""" """Schema for the tool calls."""
schema_specs: list[_SchemaSpec[SchemaT]] schema_specs: list[_SchemaSpec[SchemaT]]
@@ -218,7 +218,7 @@ class ToolStrategy(Generic[SchemaT]):
def __init__( def __init__(
self, self,
schema: type[SchemaT], schema: type[SchemaT] | dict[str, Any],
*, *,
tool_message_content: str | None = None, tool_message_content: str | None = None,
handle_errors: bool handle_errors: bool

View File

@@ -139,7 +139,6 @@ ignore-var-parameters = true # ignore missing documentation for *args and **kwa
"ARG", # Arguments, needs to fix "ARG", # Arguments, needs to fix
] ]
"tests/unit_tests/agents/test_return_direct_spec.py" = ["F821"] "tests/unit_tests/agents/test_return_direct_spec.py" = ["F821"]
"tests/unit_tests/agents/test_responses_spec.py" = ["F821"]
"tests/unit_tests/agents/test_responses.py" = ["F821"] "tests/unit_tests/agents/test_responses.py" = ["F821"]
"tests/unit_tests/agents/test_react_agent.py" = ["ALL"] "tests/unit_tests/agents/test_react_agent.py" = ["ALL"]

View File

@@ -1,16 +1,34 @@
from __future__ import annotations from __future__ import annotations
import pytest import os
from typing import (
TYPE_CHECKING,
Any,
)
from unittest.mock import MagicMock
import httpx
import pytest
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from pydantic import BaseModel, create_model
from langchain.agents import create_agent
from langchain.agents.structured_output import (
ToolStrategy,
)
from tests.unit_tests.agents.utils import BaseSchema, load_spec
if TYPE_CHECKING:
from collections.abc import Callable
# Skip this test since langgraph.prebuilt.responses is not available
pytest.skip("langgraph.prebuilt.responses not available", allow_module_level=True)
try: try:
from langchain_openai import ChatOpenAI from langchain_openai import ChatOpenAI
except ImportError: except ImportError:
skip_openai_integration_tests = True skip_openai_integration_tests = True
else: else:
skip_openai_integration_tests = False skip_openai_integration_tests = "OPENAI_API_KEY" not in os.environ
AGENT_PROMPT = "You are an HR assistant." AGENT_PROMPT = "You are an HR assistant."
@@ -30,8 +48,8 @@ class AssertionByInvocation(BaseSchema):
class TestCase(BaseSchema): class TestCase(BaseSchema):
name: str name: str
response_format: Union[Dict[str, Any], List[Dict[str, Any]]] response_format: dict[str, Any] | list[dict[str, Any]]
assertions_by_invocation: List[AssertionByInvocation] assertions_by_invocation: list[AssertionByInvocation]
class Employee(BaseModel): class Employee(BaseModel):
@@ -49,12 +67,12 @@ EMPLOYEES: list[Employee] = [
TEST_CASES = load_spec("responses", as_model=TestCase) TEST_CASES = load_spec("responses", as_model=TestCase)
def _make_tool(fn, *, name: str, description: str): def _make_tool(fn: Callable[..., str | None], *, name: str, description: str) -> dict[str, Any]:
mock = MagicMock(side_effect=lambda *, name: fn(name=name)) mock = MagicMock(side_effect=lambda *, name: fn(name=name))
input_model = create_model(f"{name}_input", name=(str, ...)) input_model = create_model(f"{name}_input", name=(str, ...))
@tool(name, description=description, args_schema=input_model) @tool(name, description=description, args_schema=input_model)
def _wrapped(name: str): def _wrapped(name: str) -> Any:
return mock(name=name) return mock(name=name)
return {"tool": _wrapped, "mock": mock} return {"tool": _wrapped, "mock": mock}
@@ -106,7 +124,7 @@ def test_responses_integration_matrix(case: TestCase) -> None:
for assertion in case.assertions_by_invocation: for assertion in case.assertions_by_invocation:
def on_request(request: httpx.Request) -> None: def on_request(_request: httpx.Request) -> None:
nonlocal llm_request_count nonlocal llm_request_count
llm_request_count += 1 llm_request_count += 1
@@ -123,7 +141,7 @@ def test_responses_integration_matrix(case: TestCase) -> None:
agent = create_agent( agent = create_agent(
model, model,
tools=[role_tool["tool"], dept_tool["tool"]], tools=[role_tool["tool"], dept_tool["tool"]],
prompt=AGENT_PROMPT, system_prompt=AGENT_PROMPT,
response_format=tool_output, response_format=tool_output,
) )

View File

@@ -1,5 +1,6 @@
import json import json
from pathlib import Path from pathlib import Path
from typing import TypeVar
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel from pydantic.alias_generators import to_camel
@@ -13,7 +14,10 @@ class BaseSchema(BaseModel):
) )
def load_spec(spec_name: str, as_model: type[BaseModel]) -> list[BaseModel]: _T = TypeVar("_T", bound=BaseModel)
def load_spec(spec_name: str, as_model: type[_T]) -> list[_T]:
with (Path(__file__).parent / "specifications" / f"{spec_name}.json").open( with (Path(__file__).parent / "specifications" / f"{spec_name}.json").open(
"r", encoding="utf-8" "r", encoding="utf-8"
) as f: ) as f: