forked from ChelseaKR/sprout
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_identify.py
More file actions
301 lines (229 loc) · 10.9 KB
/
Copy pathtest_identify.py
File metadata and controls
301 lines (229 loc) · 10.9 KB
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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
"""Photo plant-ID -> grounded care lookup: resolution, fallback, and grounding preserved.
These tests prove the load-bearing property: a photo identification is only ever a
*selector*. The species it yields is routed back through the unchanged grounded pipeline,
so the rendered answer still carries citations and the never-certify-safe routing; the
visual match is never presented as a cited fact. Everything here is offline/deterministic.
"""
from __future__ import annotations
import pytest
from sprout.answer import Assistant
from sprout.config import Config
from sprout.identify import (
Identification,
OfflineIdentifier,
PhotoCareService,
PlantCandidate,
build_identifier,
format_candidates,
parse_plantnet,
photo_candidates_intro_for,
resolve_species,
)
class FakeIdentifier:
"""A deterministic stand-in for a vision API: returns a preset identification."""
def __init__(self, identification: Identification) -> None:
self._identification = identification
def identify(self, image: bytes) -> Identification:
return self._identification
def _ident(*candidates: PlantCandidate, provider: str = "fake") -> Identification:
return Identification(provider=provider, candidates=candidates)
def test_offline_identifier_always_falls_back() -> None:
ident = OfflineIdentifier().identify(b"jpeg-bytes")
assert ident.provider == "offline"
assert ident.candidates == ()
assert ident.best is None
def test_resolve_species_by_scientific_name(config: Config) -> None:
ident = _ident(PlantCandidate(scientific_name="Epipremnum aureum", score=0.9))
resolved = resolve_species(ident, {"monstera", "pothos", "spider-plant"}, config)
assert resolved is not None
assert resolved.slug == "pothos"
assert resolved.display_name == "Pothos"
def test_resolve_species_by_common_name(config: Config) -> None:
ident = _ident(
PlantCandidate(
scientific_name="Unknownus plantus",
common_names=("Golden pothos", "Devil's ivy"),
score=0.8,
)
)
resolved = resolve_species(ident, {"monstera", "pothos"}, config)
assert resolved is not None and resolved.slug == "pothos"
def test_resolve_species_below_confidence_does_not_resolve(config: Config) -> None:
ident = _ident(PlantCandidate(scientific_name="Epipremnum aureum", score=0.1))
assert resolve_species(ident, {"pothos"}, config) is None
def test_resolve_species_unknown_species_does_not_resolve(config: Config) -> None:
ident = _ident(PlantCandidate(scientific_name="Ficus benjamina", score=0.95))
assert resolve_species(ident, {"monstera", "pothos"}, config) is None
def test_resolve_species_via_common_name_alias() -> None:
# An unknown scientific name whose common name matches the configured alias glossary.
cfg = Config.model_validate(
{"retrieval": {"species_aliases": {"unrelated name": "monstera", "golden vine": "pothos"}}}
)
ident = _ident(
PlantCandidate(
scientific_name="Mysterius plantus", common_names=("Golden vine",), score=0.9
)
)
resolved = resolve_species(ident, {"pothos"}, cfg)
assert resolved is not None and resolved.slug == "pothos"
def test_resolve_species_prefers_first_confident_known_candidate(config: Config) -> None:
ident = _ident(
PlantCandidate(scientific_name="Ficus benjamina", score=0.9), # unknown
PlantCandidate(scientific_name="Monstera deliciosa", score=0.7), # known
)
resolved = resolve_species(ident, {"monstera", "pothos"}, config)
assert resolved is not None and resolved.slug == "monstera"
def test_parse_plantnet_sorts_and_skips_malformed() -> None:
payload = {
"results": [
{"score": 0.4, "species": {"scientificNameWithoutAuthor": "Monstera deliciosa"}},
{
"score": 0.9,
"species": {
"scientificNameWithoutAuthor": "Epipremnum aureum",
"commonNames": ["Golden pothos"],
},
},
{"score": "bad", "species": {"scientificNameWithoutAuthor": "Aloe vera"}},
"not-a-dict",
{"score": 0.3, "species": "not-a-dict"},
{"score": 0.2, "species": {"scientificNameWithoutAuthor": ""}},
]
}
ident = parse_plantnet(payload, top_k=10)
names = [c.scientific_name for c in ident.candidates]
# Highest score first; malformed entries dropped; the "bad" score coerces to 0.0.
assert names[0] == "Epipremnum aureum"
assert "Monstera deliciosa" in names
assert ident.best is not None and ident.best.common_names == ("Golden pothos",)
def test_parse_plantnet_empty_on_missing_results() -> None:
assert parse_plantnet({}, top_k=5).candidates == ()
def test_format_candidates_ranks_best_first_and_respects_limit() -> None:
ident = _ident(
PlantCandidate(scientific_name="Ficus lyrata", score=0.20),
PlantCandidate(scientific_name="Ficus elastica", score=0.42),
PlantCandidate(scientific_name="Ficus benjamina", score=0.31),
)
assert format_candidates(ident, limit=2) == [
"Ficus elastica (0.42)",
"Ficus benjamina (0.31)",
]
def test_format_candidates_empty_when_no_candidates() -> None:
assert format_candidates(_ident()) == []
def test_photo_candidates_intro_for_en_and_es_and_unknown_falls_back_to_en() -> None:
en = photo_candidates_intro_for("en")
es = photo_candidates_intro_for("es")
assert en and es and en != es
assert photo_candidates_intro_for("xx") == en
def test_photo_care_grounded_and_routes_safety(assistant: Assistant, config: Config) -> None:
ident = _ident(
PlantCandidate(
scientific_name="Epipremnum aureum", common_names=("Golden pothos",), score=0.92
)
)
service = PhotoCareService(assistant, FakeIdentifier(ident), config)
result = service.identify_and_answer(b"img", question="is this toxic to my cat?")
assert result.identified is True
assert result.species_slug == "pothos"
assert result.label is not None and "not a cited fact" in result.label
answer = result.answer
assert answer is not None and not answer.refused
# Grounding preserved: every rendered sentence is cited, and toxicity routes to a vet.
assert answer.citations
assert answer.citation_coverage == 1.0
assert answer.is_safety_query
assert answer.safety_notice is not None
def test_photo_care_default_question_when_none(assistant: Assistant, config: Config) -> None:
ident = _ident(PlantCandidate(scientific_name="Monstera deliciosa", score=0.8))
service = PhotoCareService(assistant, FakeIdentifier(ident), config)
result = service.identify_and_answer(b"img")
assert result.identified is True and result.species_slug == "monstera"
assert result.answer is not None and result.answer.citations
def test_photo_care_falls_back_when_unidentified(assistant: Assistant, config: Config) -> None:
service = PhotoCareService(assistant, OfflineIdentifier(), config)
result = service.identify_and_answer(b"img", question="how often do I water?")
assert result.identified is False
assert result.answer is None
assert result.message and "type the plant" in result.message.lower()
def test_photo_care_rejects_empty_and_oversized(assistant: Assistant) -> None:
cfg = Config.model_validate({"identification": {"max_image_bytes": 4}})
service = PhotoCareService(assistant, OfflineIdentifier(), cfg)
assert service.identify_and_answer(b"").identified is False
assert service.identify_and_answer(b"toolong").identified is False
def test_photo_care_spanish_fallback(assistant: Assistant, config: Config) -> None:
service = PhotoCareService(assistant, OfflineIdentifier(), config)
result = service.identify_and_answer(b"img", language="es")
assert result.identified is False
assert result.message and "Escribe el nombre" in result.message
def test_build_identifier_offline(config: Config) -> None:
assert isinstance(build_identifier(config), OfflineIdentifier)
# --- the network provider, exercised through an injected fake transport ----------
class _FakeResponse:
def __init__(self, payload: dict[str, object]) -> None:
self._payload = payload
def raise_for_status(self) -> None:
return None
def json(self) -> dict[str, object]:
return self._payload
class _FakeClient:
def __init__(self, payload: dict[str, object]) -> None:
self._payload = payload
self.calls: list[dict[str, object]] = []
def post(self, url: str, **kwargs: object) -> _FakeResponse:
self.calls.append({"url": url, **kwargs})
return _FakeResponse(self._payload)
def test_plantnet_identifier_parses_via_injected_client() -> None:
from sprout.providers.plantnet import PlantNetIdentifier
payload: dict[str, object] = {
"results": [
{
"score": 0.77,
"species": {
"scientificNameWithoutAuthor": "Monstera deliciosa",
"commonNames": ["Swiss cheese plant"],
},
}
]
}
client = _FakeClient(payload)
identifier = PlantNetIdentifier(client=client, api_key="test-key", top_k=3)
ident = identifier.identify(b"jpeg")
assert ident.provider == "plantnet"
assert ident.best is not None
assert ident.best.scientific_name == "Monstera deliciosa"
assert client.calls # the endpoint was actually called
def test_plantnet_operational_client_wrapper_is_cached(monkeypatch: pytest.MonkeyPatch) -> None:
import httpx
from sprout.provider_lifecycle import CachedHttpClient
from sprout.providers.plantnet import PlantNetIdentifier
payload: dict[str, object] = {
"results": [
{
"score": 0.77,
"species": {"scientificNameWithoutAuthor": "Monstera deliciosa"},
}
]
}
constructions = 0
client = _FakeClient(payload)
def build(*_args: object, **_kwargs: object) -> _FakeClient:
nonlocal constructions
constructions += 1
return client
monkeypatch.setattr(httpx, "Client", build)
identifier = PlantNetIdentifier(api_key="test-key", client=CachedHttpClient(timeout=30.0))
assert identifier.identify(b"jpeg").best is not None
assert identifier.identify(b"jpeg").best is not None
assert constructions == 1
assert len(client.calls) == 2
def test_plantnet_identifier_fails_closed_without_key() -> None:
from sprout.providers.plantnet import PlantNetIdentifier
identifier = PlantNetIdentifier(api_key="")
assert identifier.identify(b"jpeg").candidates == ()
def test_plantnet_identifier_fails_closed_on_error() -> None:
from sprout.providers.plantnet import PlantNetIdentifier
class _Boom:
def post(self, *a: object, **k: object) -> object:
raise RuntimeError("network down")
identifier = PlantNetIdentifier(client=_Boom(), api_key="k")
assert identifier.identify(b"jpeg").candidates == ()