97e91a83f3
Ruff / Ruff (push) Waiting to run
Test / Core Tests (push) Waiting to run
Test / Offline Coverage Tests (Python 3.10) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.11) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.12) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.13) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.9) (push) Waiting to run
Test / Full Coverage (Python 3.11) (push) Waiting to run
Test / Core Provider Tests (OpenAI) (push) Blocked by required conditions
Test / Core Provider Tests (Anthropic) (push) Blocked by required conditions
Test / Core Provider Tests (Google) (push) Blocked by required conditions
Test / Core Provider Tests (Other) (push) Blocked by required conditions
Test / Anthropic Tests (push) Blocked by required conditions
Test / Gemini Tests (push) Blocked by required conditions
Test / Google GenAI Tests (push) Blocked by required conditions
Test / Vertex AI Tests (push) Blocked by required conditions
Test / OpenAI Tests (push) Blocked by required conditions
Test / Writer Tests (push) Blocked by required conditions
Test / Auto Client Tests (push) Blocked by required conditions
ty / type-check (push) Waiting to run
167 lines
4.8 KiB
Python
167 lines
4.8 KiB
Python
from unittest.mock import MagicMock
|
|
|
|
from pydantic import BaseModel
|
|
|
|
from openai import pydantic_function_tool
|
|
|
|
from instructor.v2.providers.openai.handlers import (
|
|
OpenAIResponsesToolsHandler,
|
|
reask_responses_tools,
|
|
)
|
|
|
|
|
|
def _tool_parameters_schema(model: type[BaseModel]) -> dict:
|
|
return pydantic_function_tool(model)["function"]["parameters"]
|
|
|
|
|
|
class ResponseToolModel(BaseModel):
|
|
"""Extract a structured response for the user."""
|
|
|
|
name: str
|
|
|
|
|
|
class AlternateModel(BaseModel):
|
|
title: str
|
|
count: int
|
|
|
|
|
|
def test_responses_tools_preserves_function_description() -> None:
|
|
expected_description = pydantic_function_tool(ResponseToolModel)["function"][
|
|
"description"
|
|
]
|
|
|
|
_, kwargs = OpenAIResponsesToolsHandler().prepare_request(ResponseToolModel, {})
|
|
|
|
assert kwargs["tools"][0]["description"] == expected_description
|
|
|
|
|
|
def test_responses_tools_sets_text_format() -> None:
|
|
_, kwargs = OpenAIResponsesToolsHandler().prepare_request(ResponseToolModel, {})
|
|
|
|
fmt = kwargs["text"]["format"]
|
|
assert fmt["type"] == "json_schema"
|
|
assert fmt["name"] == "ResponseToolModel"
|
|
assert fmt["strict"] is True
|
|
assert fmt["schema"] == _tool_parameters_schema(ResponseToolModel)
|
|
assert fmt["schema"].get("additionalProperties") is False
|
|
|
|
|
|
def test_responses_tools_overrides_conflicting_text_format() -> None:
|
|
conflicting_text = {
|
|
"format": {
|
|
"type": "json_schema",
|
|
"name": "AlternateModel",
|
|
"strict": True,
|
|
"schema": AlternateModel.model_json_schema(),
|
|
},
|
|
"verbosity": "low",
|
|
}
|
|
|
|
_, kwargs = OpenAIResponsesToolsHandler().prepare_request(
|
|
ResponseToolModel,
|
|
{"text": conflicting_text},
|
|
)
|
|
|
|
fmt = kwargs["text"]["format"]
|
|
assert fmt["name"] == "ResponseToolModel"
|
|
assert fmt["schema"] == _tool_parameters_schema(ResponseToolModel)
|
|
assert kwargs["text"]["verbosity"] == "low"
|
|
assert kwargs["text"] is not conflicting_text
|
|
|
|
|
|
def test_responses_tools_preserves_matching_text_format() -> None:
|
|
matching_text = {
|
|
"format": {
|
|
"type": "json_schema",
|
|
"name": "ResponseToolModel",
|
|
"strict": True,
|
|
"schema": _tool_parameters_schema(ResponseToolModel),
|
|
}
|
|
}
|
|
|
|
_, kwargs = OpenAIResponsesToolsHandler().prepare_request(
|
|
ResponseToolModel,
|
|
{"text": matching_text},
|
|
)
|
|
|
|
assert kwargs["text"] is matching_text
|
|
|
|
|
|
def test_responses_tools_none_model_no_text() -> None:
|
|
_, kwargs = OpenAIResponsesToolsHandler().prepare_request(None, {})
|
|
|
|
assert "text" not in kwargs
|
|
|
|
|
|
def _make_mock_response(arguments: str | None) -> MagicMock:
|
|
tool_call = MagicMock()
|
|
tool_call.type = "function_call"
|
|
tool_call.arguments = arguments
|
|
tool_call.name = "ResponseToolModel"
|
|
tool_call.id = "call_123"
|
|
|
|
response = MagicMock()
|
|
response.output = [tool_call]
|
|
return response
|
|
|
|
|
|
def test_reask_responses_tools_empty_args_message() -> None:
|
|
response = _make_mock_response("{}")
|
|
error = ValueError(
|
|
"1 validation error for ResponseToolModel\nname\n Field required"
|
|
)
|
|
|
|
result = reask_responses_tools({"messages": []}, response, error)
|
|
|
|
msg = result["messages"][0]["content"]
|
|
assert "empty arguments" in msg
|
|
assert "MUST populate ALL required fields" in msg
|
|
assert "fix the errors with" not in msg
|
|
|
|
|
|
def test_reask_responses_tools_nonempty_args_message() -> None:
|
|
response = _make_mock_response('{"name": 123}')
|
|
error = ValueError(
|
|
"1 validation error for ResponseToolModel\nname\n Input should be a valid string"
|
|
)
|
|
|
|
result = reask_responses_tools({"messages": []}, response, error)
|
|
|
|
msg = result["messages"][0]["content"]
|
|
assert "fix the errors with" in msg
|
|
assert '{"name": 123}' in msg
|
|
|
|
|
|
def test_reask_responses_tools_none_arguments() -> None:
|
|
response = _make_mock_response(None)
|
|
|
|
result = reask_responses_tools({"messages": []}, response, ValueError("required"))
|
|
|
|
msg = result["messages"][0]["content"]
|
|
assert "MUST populate ALL required fields" in msg
|
|
|
|
|
|
def test_responses_tools_overrides_text_type_format() -> None:
|
|
_, kwargs = OpenAIResponsesToolsHandler().prepare_request(
|
|
ResponseToolModel,
|
|
{"text": {"format": {"type": "text"}}},
|
|
)
|
|
|
|
assert kwargs["text"]["format"]["type"] == "json_schema"
|
|
assert kwargs["text"]["format"]["name"] == "ResponseToolModel"
|
|
|
|
|
|
def test_parse_response_warns_on_empty_args(caplog) -> None:
|
|
import logging
|
|
|
|
response = _make_mock_response("{}")
|
|
response.choices = []
|
|
|
|
with caplog.at_level(logging.WARNING, logger="instructor"):
|
|
try:
|
|
OpenAIResponsesToolsHandler().parse_response(response, ResponseToolModel)
|
|
except Exception:
|
|
pass
|
|
|
|
assert any("empty arguments" in record.message for record in caplog.records)
|