Repository navigation
Expand file tree
/
Copy pathtest_retry.py
More file actions
375 lines (302 loc) · 11 KB
/
Copy pathtest_retry.py
File metadata and controls
375 lines (302 loc) · 11 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
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
import os
import tempfile
from pathlib import Path
import pytest
from test_helpers.utils import (
failing_task,
failing_task_deterministic,
skip_if_no_docker,
)
from inspect_ai import Task, eval, eval_retry, task
from inspect_ai.dataset import Sample
from inspect_ai.log import (
ProvenanceData,
invalidate_samples,
list_eval_logs,
read_eval_log,
retryable_eval_logs,
write_eval_log,
)
from inspect_ai.model import GenerateConfig, get_model
from inspect_ai.model._providers.mockllm import MockLLM
from inspect_ai.scorer import exact
from inspect_ai.solver import TaskState, generate, solver
def test_eval_retry():
# run eval with a solver that fails 2/3 times
log = eval(failing_task, limit=1, model="mockllm/model")[0]
# note the task id so we can be certain it remains the same
task_id = log.eval.task_id
# retry until we succeed (confirming the task_id is stable)
while log.status != "success":
log = eval_retry(log)[0]
assert log.eval.task_id == task_id
def test_eval_retryable():
with tempfile.TemporaryDirectory() as log_dir:
# run eval with a solver that fails 2/3 of the time
log = eval(tasks=failing_task, limit=1, model="mockllm/model", log_dir=log_dir)[
0
]
# note the task id so we can be certain it remains the same
task_id = log.eval.task_id
# retry until we succeed (confirming the task_id is stable)
retryable = retryable_eval_logs(list_eval_logs(log_dir))
while len(retryable) > 0:
assert len(retryable) == 1
assert retryable[0].task_id == task_id
eval_retry(retryable, log_dir=log_dir)
retryable = retryable_eval_logs(list_eval_logs(log_dir))
@task
def mytask():
return Task(name="custom-task-name", solver=[])
def test_eval_retry_with_task_name():
log = eval(mytask())[0]
log = eval_retry(log)[0]
@task
def hello_world():
return Task(
dataset=[
Sample(
input="Just reply with Hello World",
target="Hello World",
)
],
solver=[generate()],
scorer=exact(),
)
def test_eval_retry_with_model_generate_config():
generate_config = GenerateConfig(
seed=42,
temperature=0.7,
top_p=0.95,
max_connections=1,
)
model = get_model(
model="mockllm/model",
config=generate_config,
)
log = eval(
model=model,
tasks=hello_world(),
)[0]
assert log.status == "success"
assert log.eval.model_generate_config == generate_config
log = eval_retry(log)[0]
assert log.status == "success"
assert log.eval.model_generate_config == generate_config
def test_eval_retry_preserves_token_limit_type():
log = eval(
model="mockllm/model",
tasks=hello_world(),
token_limit="output:1m",
)[0]
assert log.status == "success"
assert log.eval.config.token_limit == 1_000_000
assert log.eval.config.token_limit_type == "output"
log = eval_retry(log)[0]
assert log.status == "success"
assert log.eval.config.token_limit == 1_000_000
assert log.eval.config.token_limit_type == "output"
def test_eval_retry_honors_zero_max_retries(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
original = eval(
tasks=hello_world(),
model="mockllm/model",
max_retries=2,
log_dir=str(tmp_path),
display="none",
)[0]
assert original.plan.config.max_retries == 2
original = invalidate_samples(
original,
sample_uuids="all",
provenance=ProvenanceData(author="test"),
)
attempts = 0
async def retryable_failure(*args: object, **kwargs: object) -> None:
nonlocal attempts
attempts += 1
raise RuntimeError("retryable failure")
monkeypatch.setattr(MockLLM, "generate", retryable_failure)
monkeypatch.setattr(MockLLM, "should_retry", lambda _self, _ex: True)
retried = eval_retry(
original,
max_retries=0,
log_dir=str(tmp_path),
display="none",
)[0]
assert retried.status == "error"
assert retried.plan.config.max_retries == 0
assert attempts == 1
@solver
def failing_solver():
async def solve(state: TaskState, generate) -> TaskState:
raise ValueError("Eval failed!")
return solve
@skip_if_no_docker
@pytest.mark.slow
def test_eval_retry_resolves_relative_sandbox_paths():
"""Test that eval-retry correctly resolves relative sandbox config paths.
This test verifies that when a task uses a relative path for its sandbox
config (e.g., "compose.yaml"), eval-retry resolves it relative to the
task file's directory, not the current working directory.
"""
with tempfile.TemporaryDirectory() as tmpdir:
task_dir = Path(tmpdir) / "my_task"
task_dir.mkdir()
compose_file = task_dir / "compose.yaml"
compose_file.write_text(
"""services:
default:
image: "python:3.12-slim"
command: "tail -f /dev/null"
init: true
network_mode: none
"""
)
task_file = task_dir / "task.py"
task_file.write_text(
"""from inspect_ai import Task, task
from inspect_ai.dataset import Sample
@task
def my_task():
return Task(
dataset=[Sample(input="test")],
sandbox=("docker", "compose.yaml"),
)
"""
)
# Change to tmpdir (different from task_dir) to ensure CWD != task dir
eval_wd = os.getcwd()
try:
os.chdir(tmpdir)
# Run eval with a failing solver (limit to 1 sample)
log = eval(
f"{task_file}@my_task",
solver=failing_solver(),
model="mockllm/model",
limit=1,
)[0]
assert log.status == "error"
# Verify sandbox config was stored as relative path in log
assert log.eval.sandbox is not None
assert log.eval.sandbox.config == "compose.yaml"
# Now retry from tmpdir (not task_dir)
# This should work because eval-retry resolves relative to task file
# eval_retry uses model and solver from the log
log = eval_retry(log)[0]
# If we get here without Docker errors, the path was resolved correctly
# (The eval will still fail due to our failing_solver, but that's expected)
assert log.status == "error"
if log.error:
assert "Error reading docker config" not in log.error.message
finally:
os.chdir(eval_wd)
def test_invalidation(tmp_path: Path):
@task
def task_for_invalidation():
return Task(
dataset=[
Sample(input=f"Just reply with {idx_sample}", target="Hello World")
for idx_sample in range(10)
],
solver=[generate()],
scorer=exact(),
)
model = get_model(model="mockllm/model")
(log,) = eval(tasks=[task_for_invalidation()], model=model, log_dir=str(tmp_path))
assert log.status == "success"
log = read_eval_log(log.location)
assert log.samples is not None
invalid_sample_uuid = str(log.samples[0].uuid)
log = invalidate_samples(
log,
sample_uuids=[invalid_sample_uuid],
provenance=ProvenanceData(author="test_person", reason="test_reason"),
)
write_eval_log(log, location=log.location)
(log_retried,) = eval_retry(log.location)
assert log_retried.status == "success"
assert log_retried.eval.eval_id != log.eval.eval_id
assert log_retried.samples is not None
assert len(log_retried.samples) == 10
new_uuids = {sample.uuid for sample in log_retried.samples}
old_uuids = {sample.uuid for sample in log.samples or []}
assert len(new_uuids) == len(old_uuids)
assert new_uuids != old_uuids
reused_uuids = old_uuids.intersection(new_uuids)
assert reused_uuids == old_uuids - {invalid_sample_uuid}
def test_eval_retry_preserves_scorer_attribution(monkeypatch) -> None:
# Restored (cached) samples must carry the same SampleScore.scorer
# attribution as freshly executed samples. Regression: the cached path
# previously omitted `scorer=` so restored scores had scorer=None and
# were indistinguishable from solver-set scores in results aggregation.
import inspect_ai._eval.task.run as run_mod
captured: list[list[dict]] = []
orig = getattr(run_mod, "eval_results")
def spy(*args, **kwargs):
scores = kwargs.get("scores", args[1] if len(args) > 1 else None)
captured.append(list(scores))
return orig(*args, **kwargs)
monkeypatch.setattr(run_mod, "eval_results", spy)
# 2 samples: first passes, second fails -> retry restores first from
# cache and reruns second fresh
log1 = eval(
failing_task_deterministic([False, True]),
model="mockllm/model",
fail_on_error=False,
)[0]
assert log1.status == "success"
captured.clear()
log2 = eval_retry(log1)[0]
assert log2.status == "success"
by_id: dict[int, str | None] = {}
for call_scores in captured:
for d in call_scores:
for ss in d.values():
by_id[ss.sample_id] = ss.scorer
assert 1 in by_id and 2 in by_id
assert by_id[2] == "match", f"fresh sample scorer={by_id[2]!r}"
assert by_id[1] == "match", f"restored sample scorer={by_id[1]!r}"
def test_eval_retry_preserves_token_usage():
# 10 samples: first 8 pass, last 2 fail
# On retry: fresh iterator, 2 samples read [False, False] -> both pass
log1 = eval(
failing_task_deterministic([False] * 8 + [True] * 2),
model="mockllm/model",
)[0]
assert log1.status == "error"
assert log1.stats.model_usage
model_name = list(log1.stats.model_usage.keys())[0]
tokens1 = log1.stats.model_usage[model_name].total_tokens
assert tokens1 > 0
# Retry - fresh iterator reads [False, False] for the 2 failed samples
log2 = eval_retry(log1)[0]
assert log2.status == "success"
tokens2 = log2.stats.model_usage[model_name].total_tokens
assert tokens2 > tokens1
def test_eval_retry_token_usage_multi_retry():
# 4 samples with pattern: [False, True, False, True]
# First run: samples 0,2 pass; samples 1,3 fail
# Retry 1: 2 samples read [False, True] -> sample 1 passes, sample 3 fails
# Retry 2: 1 sample reads [False] -> sample 3 passes
log1 = eval(
failing_task_deterministic([False, True, False, True]),
model="mockllm/model",
)[0]
def get_tokens(log):
model_name = list(log.stats.model_usage.keys())[0]
return log.stats.model_usage[model_name].total_tokens
assert log1.status == "error"
tokens1 = get_tokens(log1)
assert tokens1 > 0
# First retry
log2 = eval_retry(log1)[0]
assert log2.status == "error"
tokens2 = get_tokens(log2)
assert tokens2 > tokens1
# Second retry
log3 = eval_retry(log2)[0]
assert log3.status == "success"
tokens3 = get_tokens(log3)
assert tokens3 > tokens2