forked from ChelseaKR/fare-policy-assistant
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_answer.py
More file actions
313 lines (269 loc) · 12.8 KB
/
Copy pathtest_answer.py
File metadata and controls
313 lines (269 loc) · 12.8 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
302
303
304
305
306
307
308
309
310
311
312
313
from dataclasses import replace
from assistant import config
from assistant.answer import _safe_url, answer_question
from assistant.models import Completion, MockModel
def test_safe_url_drops_non_http_schemes():
# Defence in depth: a citation link href only ever carries http(s).
assert _safe_url("https://mst.org/fares/") == "https://mst.org/fares/"
assert _safe_url("http://example.org") == "http://example.org"
assert _safe_url("javascript:alert(1)") == ""
assert _safe_url("data:text/html,<script>") == ""
assert _safe_url(" https://x.test") == "" # no leading junk allowed
class ScriptedModel:
"""Returns a fixed completion; lets tests exercise the output guard."""
def __init__(self, text: str):
self.text = text
def complete(self, system, user, max_tokens, temperature):
return Completion(text=self.text, model="scripted")
def _cfg():
return config.Config(
models=config.ModelConfig(provider="mock", answer_model="mock", judge_model="mock")
)
class TestAnswerPipeline:
def test_grounded_answer_carries_citation(self, retriever):
result = answer_question(
"Do youth ride free on Yolobus?",
model=MockModel(),
retriever=retriever,
cfg=_cfg(),
)
assert result.kind == "answered"
assert result.citations
assert result.citations[0].url.startswith("https://")
assert result.as_of_date == "2026-06-12"
def test_answered_response_reports_confidence_band(self, retriever):
result = answer_question(
"Do youth ride free on Yolobus?",
model=MockModel(),
retriever=retriever,
cfg=_cfg(),
)
assert result.confidence in {"medium", "high"}
assert result.retrieval_score > 0
def test_unsupported_question_reports_low_confidence(self, chunks):
# A retriever that declines anything below a high bar: the band on a
# declined answer is "low".
from assistant.retrieve import Retriever
strict = Retriever(chunks, config.RetrievalConfig(top_k=3, decline_z_threshold=50.0))
result = answer_question(
"Do youth ride free on Yolobus?",
model=MockModel(),
retriever=strict,
cfg=_cfg(),
)
assert result.kind == "refused_no_support"
assert result.confidence == "low"
def test_pii_refused_before_retrieval(self, retriever):
result = answer_question(
"My SSN is 123-45-6789, what's my fare?",
model=MockModel(),
retriever=retriever,
cfg=_cfg(),
)
assert result.kind == "refused_input"
assert not result.passages
def test_operator_disabled_source_fails_closed_before_model(self, retriever, monkeypatch):
monkeypatch.setenv("FPA_DISABLED_DOC_IDS", "yolobus-fares")
class ModelMustNotRun:
def complete(self, system, user, max_tokens, temperature):
raise AssertionError("disabled source must be contained before the model runs")
result = answer_question(
"How much is the local fare on Yolobus?",
model=ModelMustNotRun(),
retriever=retriever,
cfg=_cfg(),
)
assert result.kind == "refused_no_support"
assert result.confidence == "low"
assert "source_disabled:yolobus-fares" in result.guard_flags
assert all(sc.chunk.doc_id != "yolobus-fares" for sc in result.passages)
def test_offtopic_refused_with_redirect(self, retriever):
result = answer_question(
"weather forecast astronomy parliament",
model=MockModel(),
retriever=retriever,
cfg=_cfg(),
)
assert result.kind == "refused_no_support"
assert "511" in result.answer or "agency" in result.answer
def test_spanish_offtopic_refused_in_spanish(self, retriever):
result = answer_question(
"¿Va a llover mañana en Salinas? Quiero saber el pronóstico del clima.",
model=MockModel(),
retriever=retriever,
cfg=_cfg(),
)
assert result.kind == "refused_no_support"
assert "agencia" in result.answer
def test_determination_sentence_redacted_content_kept(self, retriever):
result = answer_question(
"Do I qualify for the Yolobus senior fare discount?",
model=ScriptedModel(
"Yes, you qualify for the discount. "
"The published criteria are 62 and older [doc:yolobus-fares]."
),
retriever=retriever,
cfg=_cfg(),
)
assert result.kind == "answered"
assert "you qualify" not in result.answer
assert "62 and older" in result.answer
assert any(f.startswith("redacted_determination") for f in result.guard_flags)
assert "you qualify" in result.raw_model_answer
def test_fully_offending_answer_blocked_by_guard(self, retriever):
result = answer_question(
"Do I qualify for the Yolobus senior fare discount?",
model=ScriptedModel("Yes, you qualify for the discount, trust me."),
retriever=retriever,
cfg=_cfg(),
)
assert result.kind == "answered_guarded"
assert "you qualify" not in result.answer
def test_uncited_answer_blocked_by_guard(self, retriever):
result = answer_question(
"How much is the Yolobus local fare discount?",
model=ScriptedModel("The fare is $1.00 for seniors."),
retriever=retriever,
cfg=_cfg(),
)
assert result.kind == "answered_guarded"
assert "missing_citation" in result.guard_flags
def test_unknown_only_citation_fails_closed(self, retriever):
result = answer_question(
"How much is the Yolobus local fare discount?",
model=ScriptedModel(
"The senior fare is $1.00 [doc:not-in-the-corpus], as of 2026-06-12."
),
retriever=retriever,
cfg=_cfg(),
)
assert result.kind == "answered_guarded"
assert result.citations == []
assert "unretrieved_citation:not-in-the-corpus" in result.guard_flags
assert "not-in-the-corpus" not in result.answer
def test_mixed_valid_and_unknown_citations_fail_closed(self, retriever):
result = answer_question(
"How much is the Yolobus local fare discount?",
model=ScriptedModel(
"The senior fare is $1.00 [doc:yolobus-fares, doc:not-retrieved], as of 2026-06-12."
),
retriever=retriever,
cfg=_cfg(),
)
assert result.kind == "answered_guarded"
assert result.citations == []
assert "unretrieved_citation:not-retrieved" in result.guard_flags
def test_valid_but_nonretrieved_citation_fails_closed(self, retriever):
result = answer_question(
"How much is the Yolobus local fare discount?",
model=ScriptedModel("The fare is $2.00 [doc:mst-fares], as of 2026-06-12."),
retriever=retriever,
cfg=_cfg(),
)
retrieved_ids = {sc.chunk.doc_id for sc in result.passages}
assert "mst-fares" not in retrieved_ids
assert result.kind == "answered_guarded"
assert "unretrieved_citation:mst-fares" in result.guard_flags
class TestAsOfDate:
"""The rider-facing snapshot date must describe the cited evidence.
`as_of_date` is rendered as "Based on policies published as of <date>"
directly beneath the answer (web/index.html, web/embed.py) and is the one
claim this assistant makes about its own limits. It used to be
`max(fetch_date)` over the *retrieved* top-k, so a single recently
refetched document — HTA was refetched 2026-08-10 while every other agency
still sat at 2026-06-12 — dated an answer that stood on a two-month-older
citation to the fresh date. Documents are refetched one at a time, so a
mixed-age top-k is routine rather than exotic.
"""
@staticmethod
def _mixed_freshness_retriever(chunks):
"""The corpus fixture plus one Yolobus document refetched two months
later than the Yolobus fare page. Snapshots are taken per document, so
one agency's pages routinely carry different fetch dates."""
from assistant.retrieve import Retriever
fresh = replace(
chunks[1],
chunk_id="yolobus-fare-notice#0",
doc_id="yolobus-fare-notice",
doc_title="Fare Notices",
url="https://yolobus.com/notices/",
fetch_date="2026-08-10",
section="Fare Notices",
text=(
"Fare notice: the day pass price is unchanged at $6.00 and paper "
"tickets remain valid on every fixed route."
),
)
# top_k covers the whole fixture, so the fresh document is in the
# retrieved set for any Yolobus query and the test cannot go vacuous.
return Retriever([*chunks, fresh], config.RetrievalConfig(top_k=len(chunks) + 1))
def test_headline_date_is_the_cited_passage_not_the_freshest_retrieved(self, chunks):
retriever = self._mixed_freshness_retriever(chunks)
result = answer_question(
"Do youth ride free on Yolobus?",
model=ScriptedModel(
"Youth ages 0-18 ride free [doc:yolobus-fares], based on policies "
"published as of 2026-06-12."
),
retriever=retriever,
cfg=_cfg(),
)
assert result.kind == "answered"
# Preconditions: the fresh document really was retrieved, and really was
# not cited. Without both, this test would pass for the wrong reason.
retrieved_dates = {sc.chunk.fetch_date for sc in result.passages}
assert "2026-08-10" in retrieved_dates
assert [c.doc_id for c in result.citations] == ["yolobus-fares"]
# The rider is told the date of the page the answer stands on, not the
# date of the page that merely turned up beside it.
assert result.as_of_date == "2026-06-12"
assert result.as_of_date != max(sc.chunk.fetch_date for sc in result.passages)
def test_headline_date_is_the_oldest_of_several_cited_passages(self, chunks):
# Weakest link: an answer resting on a June page and an August page is
# only verified as of June, because the June page could have changed
# since without anyone looking.
retriever = self._mixed_freshness_retriever(chunks)
result = answer_question(
"Do youth ride free on Yolobus and is the Yolobus day pass price unchanged?",
model=ScriptedModel(
"Youth ages 0-18 ride free on Yolobus [doc:yolobus-fares] and the day pass "
"price is unchanged [doc:yolobus-fare-notice], based on policies published "
"as of 2026-06-12."
),
retriever=retriever,
cfg=_cfg(),
)
assert result.kind == "answered"
assert {c.fetch_date for c in result.citations} == {"2026-06-12", "2026-08-10"}
assert result.as_of_date == "2026-06-12"
def test_declined_answer_still_dates_the_corpus_it_consulted(self, chunks):
# No citations exist on a decline, so there is no cited evidence to
# date; the field keeps describing the corpus that was consulted.
from assistant.retrieve import Retriever
strict = Retriever(chunks, config.RetrievalConfig(top_k=3, decline_z_threshold=50.0))
result = answer_question(
"Do youth ride free on Yolobus?",
model=MockModel(),
retriever=strict,
cfg=_cfg(),
)
assert result.kind == "refused_no_support"
assert result.citations == []
assert result.as_of_date == "2026-06-12"
class TestMultiTurn:
def test_retrieval_query_inherits_prior_turn(self):
from assistant.answer import _retrieval_query
q = _retrieval_query("what about my spouse?", [("MST veteran discount", "...")])
assert q == "MST veteran discount what about my spouse?"
def test_retrieval_query_unchanged_without_history(self):
from assistant.answer import _retrieval_query
assert _retrieval_query("how much is the fare?", None) == "how much is the fare?"
def test_history_block_empty_without_history(self):
from assistant.answer import _history_block
assert _history_block(None) == ""
assert _history_block([]) == ""
def test_history_block_includes_prior_turns(self):
from assistant.answer import _history_block
block = _history_block([("how much on MST?", "Single ride is $2.00.")])
assert "how much on MST?" in block and "$2.00" in block
assert "context only" in block # framed as context, must re-ground