instructor
188 строк · 5.7 Кб
1from typing import TypeVar
2
3import pytest
4from anthropic.types import Message, Usage
5from openai.resources.chat.completions import ChatCompletion
6from pydantic import BaseModel, ValidationError
7
8import instructor
9from instructor import OpenAISchema, openai_schema
10from instructor.exceptions import IncompleteOutputException
11
12T = TypeVar("T")
13
14
15@pytest.fixture # type: ignore[misc]
16def test_model() -> type[OpenAISchema]:
17class TestModel(OpenAISchema): # type: ignore[misc]
18name: str = "TestModel"
19data: str
20
21return TestModel
22
23
24@pytest.fixture # type: ignore[misc]
25def mock_completion(request: T) -> ChatCompletion:
26finish_reason = "stop"
27data_content = '{\n"data": "complete data"\n}'
28
29if hasattr(request, "param"):
30finish_reason = request.param.get("finish_reason", finish_reason)
31data_content = request.param.get("data_content", data_content)
32
33mock_choices = [
34{
35"index": 0,
36"message": {
37"role": "assistant",
38"function_call": {"name": "TestModel", "arguments": data_content},
39"content": data_content,
40},
41"finish_reason": finish_reason,
42}
43]
44
45completion = ChatCompletion(
46id="test_id",
47choices=mock_choices,
48created=1234567890,
49model="gpt-3.5-turbo",
50object="chat.completion",
51)
52
53return completion
54
55
56@pytest.fixture # type: ignore[misc]
57def mock_anthropic_message(request: T) -> Message:
58data_content = '{\n"data": "Claude says hi"\n}'
59if hasattr(request, "param"):
60data_content = request.param.get("data_content", data_content)
61return Message(
62id="test_id",
63content=[{"type": "text", "text": data_content}],
64model="claude-3-haiku-20240307",
65role="assistant",
66stop_reason="end_turn",
67stop_sequence=None,
68type="message",
69usage=Usage(
70input_tokens=100,
71output_tokens=100,
72),
73)
74
75
76def test_openai_schema() -> None:
77@openai_schema
78class Dataframe(BaseModel): # type: ignore[misc]
79"""
80Class representing a dataframe. This class is used to convert
81data into a frame that can be used by pandas.
82"""
83
84data: str
85columns: str
86
87def to_pandas(self) -> None:
88pass
89
90assert hasattr(Dataframe, "openai_schema")
91assert hasattr(Dataframe, "from_response")
92assert hasattr(Dataframe, "to_pandas")
93assert Dataframe.openai_schema["name"] == "Dataframe"
94
95
96def test_openai_schema_raises_error() -> None:
97with pytest.raises(TypeError, match="must be a subclass of pydantic.BaseModel"):
98
99@openai_schema
100class Dummy:
101pass
102
103
104def test_no_docstring() -> None:
105class Dummy(OpenAISchema): # type: ignore[misc]
106attr: str
107
108assert (
109Dummy.openai_schema["description"]
110== "Correctly extracted `Dummy` with all the required parameters with correct types"
111)
112
113
114@pytest.mark.parametrize(
115"mock_completion",
116[{"finish_reason": "length", "data_content": '{\n"data": "incomplete dat"\n}'}],
117indirect=True,
118) # type: ignore[misc]
119def test_incomplete_output_exception(
120test_model: type[OpenAISchema], mock_completion: ChatCompletion
121) -> None:
122with pytest.raises(IncompleteOutputException):
123test_model.from_response(mock_completion, mode=instructor.Mode.FUNCTIONS)
124
125
126def test_complete_output_no_exception(
127test_model: type[OpenAISchema], mock_completion: ChatCompletion
128) -> None:
129test_model_instance = test_model.from_response(
130mock_completion, mode=instructor.Mode.FUNCTIONS
131)
132assert test_model_instance.data == "complete data"
133
134
135@pytest.mark.asyncio # type: ignore[misc]
136@pytest.mark.parametrize(
137"mock_completion",
138[{"finish_reason": "length", "data_content": '{\n"data": "incomplete dat"\n}'}],
139indirect=True,
140) # type: ignore[misc]
141def test_incomplete_output_exception_raise(
142test_model: type[OpenAISchema], mock_completion: ChatCompletion
143) -> None:
144with pytest.raises(IncompleteOutputException):
145test_model.from_response(mock_completion, mode=instructor.Mode.FUNCTIONS)
146
147
148def test_anthropic_no_exception(
149test_model: type[OpenAISchema], mock_anthropic_message: Message
150) -> None:
151test_model_instance = test_model.from_response(
152mock_anthropic_message, mode=instructor.Mode.ANTHROPIC_JSON
153)
154assert test_model_instance.data == "Claude says hi"
155
156
157@pytest.mark.parametrize(
158"mock_anthropic_message",
159[{"data_content": '{\n"data": "Claude likes\ncontrol\ncharacters"\n}'}],
160indirect=True,
161) # type: ignore[misc]
162def test_control_characters_not_allowed_in_anthropic_json_strict_mode(
163test_model: type[OpenAISchema], mock_anthropic_message: Message
164) -> None:
165with pytest.raises(ValidationError) as exc_info:
166test_model.from_response(
167mock_anthropic_message, mode=instructor.Mode.ANTHROPIC_JSON, strict=True
168)
169
170# https://docs.pydantic.dev/latest/errors/validation_errors/#json_invalid
171exc = exc_info.value
172assert len(exc.errors()) == 1
173assert exc.errors()[0]["type"] == "json_invalid"
174assert "control character" in exc.errors()[0]["msg"]
175
176
177@pytest.mark.parametrize(
178"mock_anthropic_message",
179[{"data_content": '{\n"data": "Claude likes\ncontrol\ncharacters"\n}'}],
180indirect=True,
181) # type: ignore[misc]
182def test_control_characters_allowed_in_anthropic_json_non_strict_mode(
183test_model: type[OpenAISchema], mock_anthropic_message: Message
184) -> None:
185test_model_instance = test_model.from_response(
186mock_anthropic_message, mode=instructor.Mode.ANTHROPIC_JSON, strict=False
187)
188assert test_model_instance.data == "Claude likes\ncontrol\ncharacters"
189