File size: 5,496 Bytes
d840c10 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | from __future__ import annotations
import pytest
from gcmd_classifier.config import ModelSettings
from gcmd_classifier.errors import (
NonRetryableModelError,
RetryableModelError,
StructuredModelResponseError,
)
from gcmd_classifier.llm import FakeModelClient, ModelRequest, ModelStage
from gcmd_classifier.llm.base import RetryPolicy, generate_with_retries
from gcmd_classifier.llm.schemas import TermResponse, TopicResponse, VariableResponse
def _request(stage: ModelStage, schema: type) -> ModelRequest:
settings = ModelSettings()
return ModelRequest.from_settings(
stage=stage,
prompt="prompt",
response_schema=schema,
settings=settings,
)
def _decision(candidate_id: str = "candidate-1") -> dict:
return {
"candidate_id": candidate_id,
"confidence": 0.8,
"evidence": "The article supports this candidate.",
"support_type": "explicit",
}
def test_fake_model_returns_typed_topic_response() -> None:
client = FakeModelClient([{"selected": [_decision()], "ambiguous_alternatives": []}])
response = client.generate_structured(_request(ModelStage.TOPIC, TopicResponse))
assert isinstance(response.parsed, TopicResponse)
assert response.parsed.selected[0].candidate_id == "candidate-1"
assert response.provider == "fake"
def test_fake_model_returns_typed_term_response() -> None:
client = FakeModelClient(
[
{
"selected": [_decision("term-1")],
"stop_at_parent": False,
"ambiguous_alternatives": [],
}
]
)
response = client.generate_structured(_request(ModelStage.TERM, TermResponse))
assert isinstance(response.parsed, TermResponse)
assert response.parsed.selected[0].candidate_id == "term-1"
def test_fake_model_returns_typed_variable_response() -> None:
client = FakeModelClient(
[
{
"selected": [_decision("variable-1")],
"stop_at_parent": False,
"ambiguous_alternatives": [],
}
]
)
response = client.generate_structured(_request(ModelStage.VARIABLE, VariableResponse))
assert isinstance(response.parsed, VariableResponse)
assert response.parsed.selected[0].candidate_id == "variable-1"
def test_fake_model_can_return_no_selection() -> None:
client = FakeModelClient(
[
{
"selected": [],
"ambiguous_alternatives": [],
"no_selection_reason": "No supplied Topic is supported.",
}
]
)
response = client.generate_structured(_request(ModelStage.TOPIC, TopicResponse))
assert response.parsed.selected == []
assert response.parsed.no_selection_reason == "No supplied Topic is supported."
def test_fake_model_records_requests() -> None:
client = FakeModelClient([{"selected": [], "ambiguous_alternatives": []}])
request = _request(ModelStage.TOPIC, TopicResponse)
client.generate_structured(request)
assert client.requests == [request]
def test_fake_model_can_return_invalid_candidate_ids_for_later_validation() -> None:
client = FakeModelClient([{"selected": [_decision("not-a-supplied-id")]}])
response = client.generate_structured(_request(ModelStage.TOPIC, TopicResponse))
assert response.parsed.selected[0].candidate_id == "not-a-supplied-id"
def test_fake_model_rejects_contradictory_structured_response() -> None:
client = FakeModelClient(
[
{
"selected": [_decision()],
"stop_at_parent": True,
"stop_reason": "Contradictory response.",
"ambiguous_alternatives": [],
}
]
)
with pytest.raises(StructuredModelResponseError):
client.generate_structured(_request(ModelStage.TERM, TermResponse))
def test_fake_model_can_raise_retryable_errors() -> None:
client = FakeModelClient([RetryableModelError("temporary failure")])
with pytest.raises(RetryableModelError):
client.generate_structured(_request(ModelStage.TOPIC, TopicResponse))
def test_fake_model_can_raise_non_retryable_errors() -> None:
client = FakeModelClient([NonRetryableModelError("permanent failure")])
with pytest.raises(NonRetryableModelError):
client.generate_structured(_request(ModelStage.TOPIC, TopicResponse))
def test_fake_model_can_fail_once_then_succeed_with_retry_wrapper() -> None:
client = FakeModelClient(
[
RetryableModelError("temporary failure"),
{"selected": [_decision()], "ambiguous_alternatives": []},
]
)
response = generate_with_retries(
client,
_request(ModelStage.TOPIC, TopicResponse),
RetryPolicy(max_retries=1),
)
assert response.retry_count == 1
assert len(client.requests) == 2
assert response.parsed.selected[0].candidate_id == "candidate-1"
def test_fake_model_can_exhaust_retries() -> None:
client = FakeModelClient(
[RetryableModelError("one"), RetryableModelError("two"), RetryableModelError("three")]
)
with pytest.raises(Exception) as exc_info:
generate_with_retries(
client,
_request(ModelStage.TOPIC, TopicResponse),
RetryPolicy(max_retries=2),
)
assert exc_info.value.__class__.__name__ == "ModelRetriesExhaustedError"
assert len(client.requests) == 3
|