claude-dev-env 8.90.0 → 8.91.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/install-constants.mjs +3 -0
- package/bin/install.cursor-rules.test.mjs +11 -1
- package/bin/install.mjs +5 -0
- package/bin/install.settings-defaults.test.mjs +0 -1
- package/hooks/features/session-context.md +5 -4
- package/hooks/hooks.json +0 -13
- package/hooks/hooks_constants/compaction_user_messages_constants.py +141 -47
- package/hooks/session/compaction_recall_prompt.py +29 -18
- package/hooks/session/compaction_trim.py +252 -0
- package/hooks/session/compaction_trim_eval.py +85 -279
- package/hooks/session/compaction_user_messages.py +42 -143
- package/hooks/session/fixtures/compaction_user_messages/codex_rollout.jsonl +22 -0
- package/hooks/session/fixtures/compaction_user_messages/random_9031_1.jsonl +42 -0
- package/hooks/session/fixtures/compaction_user_messages/random_9031_2.jsonl +42 -0
- package/hooks/session/fixtures/compaction_user_messages/trim_cases.json +46 -0
- package/hooks/session/test_compaction_recall_prompt.py +45 -82
- package/hooks/session/test_compaction_trim.py +169 -0
- package/hooks/session/test_compaction_trim_eval.py +32 -85
- package/hooks/session/test_compaction_user_messages.py +37 -95
- package/package.json +1 -1
- package/settings.json +1 -2
|
@@ -1,40 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Score how well the
|
|
2
|
+
"""Score how well the Python trim keeps the user's messages after a compaction.
|
|
3
3
|
|
|
4
4
|
::
|
|
5
5
|
|
|
6
|
-
python3 compaction_trim_eval.py
|
|
7
|
-
deploy_session.jsonl
|
|
6
|
+
python3 compaction_trim_eval.py
|
|
7
|
+
deploy_session.jsonl facts 13/13 overruled 1/1 ratio 1.6x
|
|
8
8
|
...
|
|
9
|
-
|
|
10
|
-
oracle (raw
|
|
9
|
+
trim: facts 1.00 overruled 1.00 ratio 1.7x (4 cases)
|
|
10
|
+
oracle (raw messages): facts 1.00 ... null (empty recall): facts 0.00 ...
|
|
11
11
|
|
|
12
12
|
Each case is a fixture transcript with planted facts: paths, numbers, a
|
|
13
|
-
link, and a decision the user later reversed.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
the
|
|
20
|
-
that each fact survives and that the reversed line says ``overruled``.
|
|
21
|
-
|
|
22
|
-
The oracle grades the raw extract and the null grades an empty reply, so a
|
|
23
|
-
broken grader shows before any model score is trusted. A failed call goes
|
|
24
|
-
to the errors list, never to a zero score.
|
|
13
|
+
link, and a decision the user later reversed. The eval trims the fixture's
|
|
14
|
+
typed messages exactly as the SessionStart hook does, then checks that each
|
|
15
|
+
fact survives and that each reversed message's line says ``overruled``.
|
|
16
|
+
|
|
17
|
+
The oracle grades the raw messages and the null grades an empty recall, so a
|
|
18
|
+
broken grader shows before the trim score is trusted. The eval exits 1 when
|
|
19
|
+
the trim loses a fact or leaves a reversal unmarked.
|
|
25
20
|
"""
|
|
26
21
|
|
|
27
22
|
from __future__ import annotations
|
|
28
23
|
|
|
29
|
-
import argparse
|
|
30
24
|
import json
|
|
31
25
|
import re
|
|
32
|
-
import shlex
|
|
33
|
-
import shutil
|
|
34
|
-
import subprocess
|
|
35
26
|
import sys
|
|
36
|
-
import
|
|
37
|
-
from dataclasses import dataclass, field
|
|
27
|
+
from dataclasses import dataclass
|
|
38
28
|
from pathlib import Path
|
|
39
29
|
|
|
40
30
|
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
@@ -44,46 +34,32 @@ _session_directory = str(Path(__file__).resolve().parent)
|
|
|
44
34
|
if _session_directory not in sys.path:
|
|
45
35
|
sys.path.insert(0, _session_directory)
|
|
46
36
|
|
|
47
|
-
from
|
|
37
|
+
from compaction_trim import recall_text
|
|
38
|
+
from compaction_user_messages import typed_user_messages
|
|
48
39
|
from hooks_constants.compaction_user_messages_constants import (
|
|
49
|
-
|
|
40
|
+
EVAL_CASE_EARLIER_KEY,
|
|
41
|
+
EVAL_CASE_FACTS_KEY,
|
|
42
|
+
EVAL_CASE_OVERRULED_KEY,
|
|
43
|
+
EVAL_CASE_TRANSCRIPT_KEY,
|
|
50
44
|
EVAL_CASES_FILE_NAME,
|
|
51
|
-
|
|
52
|
-
EVAL_DEBUG_FILE_NAME,
|
|
53
|
-
EVAL_DECISION_SEPARATOR,
|
|
54
|
-
EVAL_DEFAULT_MODEL,
|
|
55
|
-
EVAL_EMPTY_MCP_CONFIG,
|
|
45
|
+
EVAL_FILE_ENCODING,
|
|
56
46
|
EVAL_FIXTURE_DIRECTORY,
|
|
57
|
-
EVAL_HOOK_TYPE,
|
|
58
|
-
EVAL_HOOKS_JSON_PATH,
|
|
59
|
-
EVAL_JSON_OBJECT_START,
|
|
60
47
|
EVAL_MESSAGE_LINE_PATTERN_TEMPLATE,
|
|
61
48
|
EVAL_OVERRULED_MARKER,
|
|
62
|
-
|
|
63
|
-
EVAL_RESULTS_FILE_NAME,
|
|
64
|
-
EVAL_RESULTS_JSON_INDENT,
|
|
65
|
-
EVAL_RUN_PROMPT_TEMPLATE,
|
|
66
|
-
EVAL_RUN_SETTINGS_FILE_NAME,
|
|
67
|
-
EVAL_SERVED_MODEL_SEPARATOR,
|
|
68
|
-
EVAL_SESSION_DIRECTORY,
|
|
69
|
-
EVAL_STRUCTURED_OUTPUT_MARKER,
|
|
70
|
-
EVAL_TIMEOUT_SECONDS,
|
|
71
|
-
EXTRACTOR_SCRIPT_NAME,
|
|
72
|
-
MESSAGES_HEADING,
|
|
73
|
-
OUTPUT_ENCODING,
|
|
49
|
+
LINE_SEPARATOR,
|
|
74
50
|
)
|
|
75
51
|
|
|
76
52
|
|
|
77
53
|
@dataclass(frozen=True)
|
|
78
54
|
class CaseScore:
|
|
79
|
-
"""One graded
|
|
55
|
+
"""One graded recall.
|
|
80
56
|
|
|
81
57
|
Attributes:
|
|
82
|
-
facts_kept: Planted facts found in the
|
|
58
|
+
facts_kept: Planted facts found in the recall.
|
|
83
59
|
facts_total: Planted facts in the case.
|
|
84
60
|
overruled_marked: Reversed messages whose line says overruled.
|
|
85
61
|
overruled_total: Reversed messages in the case.
|
|
86
|
-
compression_ratio:
|
|
62
|
+
compression_ratio: Raw message characters per recall character.
|
|
87
63
|
"""
|
|
88
64
|
|
|
89
65
|
facts_kept: int
|
|
@@ -92,42 +68,39 @@ class CaseScore:
|
|
|
92
68
|
overruled_total: int
|
|
93
69
|
compression_ratio: float
|
|
94
70
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
all_null_scores: list[CaseScore] = field(default_factory=list)
|
|
103
|
-
all_errors: list[str] = field(default_factory=list)
|
|
104
|
-
all_records: list[dict[str, object]] = field(default_factory=list)
|
|
71
|
+
@property
|
|
72
|
+
def is_lossless(self) -> bool:
|
|
73
|
+
"""True when every fact survives and every reversal is marked."""
|
|
74
|
+
return (
|
|
75
|
+
self.facts_kept == self.facts_total
|
|
76
|
+
and self.overruled_marked == self.overruled_total
|
|
77
|
+
)
|
|
105
78
|
|
|
106
79
|
|
|
107
80
|
def _normalized(text: str) -> str:
|
|
108
81
|
return re.sub(r"\s+", " ", text).casefold()
|
|
109
82
|
|
|
110
83
|
|
|
111
|
-
def _line_for_message(
|
|
84
|
+
def _line_for_message(recall: str, position: int) -> str:
|
|
112
85
|
line_pattern = re.compile(
|
|
113
86
|
EVAL_MESSAGE_LINE_PATTERN_TEMPLATE.format(position=position), re.MULTILINE
|
|
114
87
|
)
|
|
115
|
-
found_line = line_pattern.search(
|
|
88
|
+
found_line = line_pattern.search(recall)
|
|
116
89
|
return found_line.group(0) if found_line else ""
|
|
117
90
|
|
|
118
91
|
|
|
119
|
-
def _overruled_marked(
|
|
92
|
+
def _overruled_marked(recall: str, all_reversals: list[dict[str, int]]) -> int:
|
|
120
93
|
return sum(
|
|
121
94
|
EVAL_OVERRULED_MARKER
|
|
122
|
-
in _line_for_message(
|
|
95
|
+
in _line_for_message(recall, int(each_reversal[EVAL_CASE_EARLIER_KEY])).casefold()
|
|
123
96
|
for each_reversal in all_reversals
|
|
124
97
|
)
|
|
125
98
|
|
|
126
99
|
|
|
127
100
|
def score_condensed(
|
|
128
|
-
|
|
101
|
+
recall: str, all_case_fields: dict[str, object], raw_messages: str
|
|
129
102
|
) -> CaseScore:
|
|
130
|
-
"""Grade one
|
|
103
|
+
"""Grade one recall against the case's planted facts.
|
|
131
104
|
|
|
132
105
|
::
|
|
133
106
|
|
|
@@ -135,257 +108,90 @@ def score_condensed(
|
|
|
135
108
|
-> each fact found case-insensitively, the #2 line marked overruled
|
|
136
109
|
|
|
137
110
|
Args:
|
|
138
|
-
|
|
111
|
+
recall: The trimmed lines the hook would return.
|
|
139
112
|
all_case_fields: One entry from trim_cases.json.
|
|
140
|
-
|
|
113
|
+
raw_messages: The typed messages the recall was made from, for the ratio.
|
|
141
114
|
|
|
142
115
|
Returns:
|
|
143
116
|
The counts and the compression ratio.
|
|
144
117
|
"""
|
|
145
|
-
all_facts = [str(each_fact) for each_fact in all_case_fields[
|
|
146
|
-
all_reversals = list(all_case_fields[
|
|
147
|
-
|
|
148
|
-
message_characters = len(extract_text.split(MESSAGES_HEADING, 1)[-1])
|
|
118
|
+
all_facts = [str(each_fact) for each_fact in all_case_fields[EVAL_CASE_FACTS_KEY]]
|
|
119
|
+
all_reversals = list(all_case_fields[EVAL_CASE_OVERRULED_KEY])
|
|
120
|
+
normalized_recall = _normalized(recall)
|
|
149
121
|
return CaseScore(
|
|
150
|
-
facts_kept=sum(_normalized(each_fact) in
|
|
122
|
+
facts_kept=sum(_normalized(each_fact) in normalized_recall for each_fact in all_facts),
|
|
151
123
|
facts_total=len(all_facts),
|
|
152
|
-
overruled_marked=_overruled_marked(
|
|
124
|
+
overruled_marked=_overruled_marked(recall, all_reversals),
|
|
153
125
|
overruled_total=len(all_reversals),
|
|
154
|
-
compression_ratio=
|
|
126
|
+
compression_ratio=len(raw_messages) / max(len(recall), 1),
|
|
155
127
|
)
|
|
156
128
|
|
|
157
129
|
|
|
158
|
-
def
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
130
|
+
def _scored_case(
|
|
131
|
+
fixture_directory: Path, all_case_fields: dict[str, object]
|
|
132
|
+
) -> tuple[str, CaseScore, CaseScore, CaseScore]:
|
|
133
|
+
transcript_name = str(all_case_fields[EVAL_CASE_TRANSCRIPT_KEY])
|
|
134
|
+
transcript_text = (fixture_directory / transcript_name).read_text(encoding=EVAL_FILE_ENCODING)
|
|
135
|
+
all_messages = typed_user_messages(transcript_text.splitlines())
|
|
136
|
+
raw_messages = LINE_SEPARATOR.join(each_message.text for each_message in all_messages)
|
|
137
|
+
recall = recall_text(all_messages) or ""
|
|
138
|
+
return (
|
|
139
|
+
transcript_name,
|
|
140
|
+
score_condensed(recall, all_case_fields, raw_messages),
|
|
141
|
+
score_condensed(raw_messages, all_case_fields, raw_messages),
|
|
142
|
+
score_condensed("", all_case_fields, raw_messages),
|
|
167
143
|
)
|
|
168
144
|
|
|
169
145
|
|
|
170
|
-
def
|
|
171
|
-
|
|
172
|
-
) -> dict[str, object]:
|
|
173
|
-
"""Return headless session settings that register the hook on the given model."""
|
|
174
|
-
all_hooks = [{**each_hook, "model": model_name} for each_hook in all_group_fields["hooks"]]
|
|
175
|
-
return {
|
|
176
|
-
"permissions": {"allow": all_allow_rules},
|
|
177
|
-
"hooks": {"PostToolUse": [{**all_group_fields, "hooks": all_hooks}]},
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def hook_reason(model_reply: str) -> str | None:
|
|
182
|
-
"""Return the ``reason`` of the last JSON decision in the reply, or None when there is none."""
|
|
183
|
-
decoder = json.JSONDecoder()
|
|
184
|
-
found_reason = None
|
|
185
|
-
for each_index, each_character in enumerate(model_reply):
|
|
186
|
-
if each_character != EVAL_JSON_OBJECT_START:
|
|
187
|
-
continue
|
|
188
|
-
try:
|
|
189
|
-
decision, _ = decoder.raw_decode(model_reply, each_index)
|
|
190
|
-
except json.JSONDecodeError:
|
|
191
|
-
continue
|
|
192
|
-
if isinstance(decision, dict) and isinstance(decision.get("reason"), str):
|
|
193
|
-
found_reason = decision["reason"]
|
|
194
|
-
return found_reason
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
def debug_log_reason(debug_text: str) -> str | None:
|
|
198
|
-
"""Return the reason of the last hook decision a debug log records, or None.
|
|
146
|
+
def scored_cases(fixture_directory: Path) -> list[tuple[str, CaseScore, CaseScore, CaseScore]]:
|
|
147
|
+
"""Trim every case's transcript and grade it beside the oracle and the null.
|
|
199
148
|
|
|
200
149
|
::
|
|
201
150
|
|
|
202
|
-
|
|
203
|
-
-> "
|
|
151
|
+
trim_cases.json with deploy_session.jsonl
|
|
152
|
+
-> [("deploy_session.jsonl", trim score, oracle score, null score)]
|
|
204
153
|
|
|
205
154
|
Args:
|
|
206
|
-
|
|
155
|
+
fixture_directory: The folder that holds trim_cases.json and its transcripts.
|
|
207
156
|
|
|
208
157
|
Returns:
|
|
209
|
-
|
|
158
|
+
One row per case: its transcript name, then the trim, oracle, and null scores.
|
|
210
159
|
"""
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
for each_line in debug_text.splitlines()
|
|
214
|
-
if EVAL_STRUCTURED_OUTPUT_MARKER in each_line
|
|
215
|
-
]
|
|
216
|
-
return hook_reason(EVAL_DECISION_SEPARATOR.join(all_decisions))
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def _session_command(
|
|
220
|
-
transcript_path: Path, settings_path: Path, model_name: str, debug_path: Path
|
|
221
|
-
) -> list[str]:
|
|
222
|
-
extractor_command = shlex.join(
|
|
223
|
-
["python3", str(EVAL_SESSION_DIRECTORY / EXTRACTOR_SCRIPT_NAME), str(transcript_path)]
|
|
224
|
-
)
|
|
225
|
-
return [
|
|
226
|
-
shutil.which(EVAL_CLAUDE_EXECUTABLE) or EVAL_CLAUDE_EXECUTABLE,
|
|
227
|
-
"-p",
|
|
228
|
-
"--model",
|
|
229
|
-
model_name,
|
|
230
|
-
"--setting-sources",
|
|
231
|
-
"",
|
|
232
|
-
"--settings",
|
|
233
|
-
str(settings_path),
|
|
234
|
-
"--strict-mcp-config",
|
|
235
|
-
"--mcp-config",
|
|
236
|
-
EVAL_EMPTY_MCP_CONFIG,
|
|
237
|
-
"--allowedTools",
|
|
238
|
-
EVAL_ALLOWED_TOOLS,
|
|
239
|
-
"--no-session-persistence",
|
|
240
|
-
"--debug-file",
|
|
241
|
-
str(debug_path),
|
|
242
|
-
"--output-format",
|
|
243
|
-
"json",
|
|
244
|
-
EVAL_RUN_PROMPT_TEMPLATE.format(command=extractor_command),
|
|
245
|
-
]
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
def _run_hook_live(
|
|
249
|
-
transcript_path: Path, settings_path: Path, model_name: str
|
|
250
|
-
) -> tuple[str | None, dict[str, object]]:
|
|
251
|
-
run_directory = Path(tempfile.mkdtemp(dir=settings_path.parent))
|
|
252
|
-
debug_path = run_directory / EVAL_DEBUG_FILE_NAME
|
|
253
|
-
completed = subprocess.run(
|
|
254
|
-
_session_command(transcript_path, settings_path, model_name, debug_path),
|
|
255
|
-
stdin=subprocess.DEVNULL,
|
|
256
|
-
capture_output=True,
|
|
257
|
-
text=True,
|
|
258
|
-
timeout=EVAL_TIMEOUT_SECONDS,
|
|
259
|
-
cwd=run_directory,
|
|
260
|
-
check=False,
|
|
261
|
-
)
|
|
262
|
-
session_output = json.loads(completed.stdout)
|
|
263
|
-
debug_text = debug_path.read_text(encoding=OUTPUT_ENCODING, errors="replace")
|
|
264
|
-
return debug_log_reason(debug_text), session_output
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
def _run_line(run_label: str, case_score: CaseScore, all_served_models: list[str]) -> str:
|
|
268
|
-
return (
|
|
269
|
-
f"{run_label} facts {case_score.facts_kept}/{case_score.facts_total} "
|
|
270
|
-
f"overruled {case_score.overruled_marked}/{case_score.overruled_total} "
|
|
271
|
-
f"ratio {case_score.compression_ratio:.1f}x "
|
|
272
|
-
f"served {EVAL_SERVED_MODEL_SEPARATOR.join(all_served_models)}\n"
|
|
273
|
-
)
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
def _run_once(
|
|
277
|
-
run_label: str,
|
|
278
|
-
transcript_path: Path,
|
|
279
|
-
settings_path: Path,
|
|
280
|
-
model_name: str,
|
|
281
|
-
all_case_fields: dict[str, object],
|
|
282
|
-
extract_text: str,
|
|
283
|
-
tally: EvalTally,
|
|
284
|
-
) -> None:
|
|
285
|
-
try:
|
|
286
|
-
condensed_text, model_output = _run_hook_live(transcript_path, settings_path, model_name)
|
|
287
|
-
except (OSError, subprocess.TimeoutExpired, json.JSONDecodeError) as call_error:
|
|
288
|
-
tally.all_errors.append(f"{run_label}: {type(call_error).__name__}")
|
|
289
|
-
return
|
|
290
|
-
if condensed_text is None:
|
|
291
|
-
tally.all_errors.append(f"{run_label}: no hook reason in the debug log")
|
|
292
|
-
return
|
|
293
|
-
case_score = score_condensed(condensed_text, all_case_fields, extract_text)
|
|
294
|
-
all_served_models = sorted(dict(model_output.get("modelUsage") or {}))
|
|
295
|
-
tally.all_model_scores.append(case_score)
|
|
296
|
-
tally.all_records.append(
|
|
297
|
-
{
|
|
298
|
-
"run": run_label,
|
|
299
|
-
"served_models": all_served_models,
|
|
300
|
-
"score": case_score.__dict__,
|
|
301
|
-
"condensed": condensed_text,
|
|
302
|
-
}
|
|
160
|
+
all_cases = json.loads(
|
|
161
|
+
(fixture_directory / EVAL_CASES_FILE_NAME).read_text(encoding=EVAL_FILE_ENCODING)
|
|
303
162
|
)
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
def _run_case(
|
|
308
|
-
all_case_fields: dict[str, object],
|
|
309
|
-
settings_path: Path,
|
|
310
|
-
parsed_arguments: argparse.Namespace,
|
|
311
|
-
tally: EvalTally,
|
|
312
|
-
) -> None:
|
|
313
|
-
transcript_path = EVAL_FIXTURE_DIRECTORY / str(all_case_fields["transcript"])
|
|
314
|
-
all_lines = transcript_path.read_text(encoding=OUTPUT_ENCODING).splitlines()
|
|
315
|
-
extract_text = rendered_extract(typed_user_messages(all_lines))
|
|
316
|
-
tally.all_oracle_scores.append(score_condensed(extract_text, all_case_fields, extract_text))
|
|
317
|
-
tally.all_null_scores.append(score_condensed("", all_case_fields, extract_text))
|
|
318
|
-
for each_rep in range(1, parsed_arguments.reps + 1):
|
|
319
|
-
_run_once(
|
|
320
|
-
f"{transcript_path.name} rep {each_rep}",
|
|
321
|
-
transcript_path,
|
|
322
|
-
settings_path,
|
|
323
|
-
parsed_arguments.model,
|
|
324
|
-
all_case_fields,
|
|
325
|
-
extract_text,
|
|
326
|
-
tally,
|
|
327
|
-
)
|
|
163
|
+
return [_scored_case(fixture_directory, each_case) for each_case in all_cases]
|
|
328
164
|
|
|
329
165
|
|
|
330
166
|
def _summary_line(label: str, all_scores: list[CaseScore]) -> str:
|
|
331
|
-
if not all_scores:
|
|
332
|
-
return f"{label}: no scored runs"
|
|
333
167
|
facts_total = sum(each_score.facts_total for each_score in all_scores)
|
|
334
|
-
facts_share = sum(each_score.facts_kept for each_score in all_scores) / facts_total
|
|
168
|
+
facts_share = sum(each_score.facts_kept for each_score in all_scores) / max(facts_total, 1)
|
|
335
169
|
overruled_total = sum(each_score.overruled_total for each_score in all_scores)
|
|
336
170
|
overruled_marked = sum(each_score.overruled_marked for each_score in all_scores)
|
|
337
171
|
overruled_share = overruled_marked / max(overruled_total, 1)
|
|
338
|
-
mean_ratio = sum(each_score.compression_ratio for each_score in all_scores) /
|
|
172
|
+
mean_ratio = sum(each_score.compression_ratio for each_score in all_scores) / max(
|
|
173
|
+
len(all_scores), 1
|
|
174
|
+
)
|
|
339
175
|
return (
|
|
340
176
|
f"{label}: facts {facts_share:.2f} overruled {overruled_share:.2f} "
|
|
341
|
-
f"ratio {mean_ratio:.1f}x ({len(all_scores)}
|
|
342
|
-
)
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
def _write_report(tally: EvalTally, output_directory: Path | None) -> None:
|
|
346
|
-
sys.stdout.write(
|
|
347
|
-
_summary_line("model", tally.all_model_scores) + f" errors {len(tally.all_errors)}\n"
|
|
348
|
-
)
|
|
349
|
-
sys.stdout.write(_summary_line("oracle (raw extract)", tally.all_oracle_scores) + "\n")
|
|
350
|
-
sys.stdout.write(_summary_line("null (empty reply)", tally.all_null_scores) + "\n")
|
|
351
|
-
for each_error in tally.all_errors:
|
|
352
|
-
sys.stdout.write(f"error: {each_error}\n")
|
|
353
|
-
if output_directory is None:
|
|
354
|
-
return
|
|
355
|
-
output_directory.mkdir(parents=True, exist_ok=True)
|
|
356
|
-
(output_directory / EVAL_RESULTS_FILE_NAME).write_text(
|
|
357
|
-
json.dumps(
|
|
358
|
-
{"records": tally.all_records, "errors": tally.all_errors},
|
|
359
|
-
indent=EVAL_RESULTS_JSON_INDENT,
|
|
360
|
-
),
|
|
361
|
-
encoding=OUTPUT_ENCODING,
|
|
177
|
+
f"ratio {mean_ratio:.1f}x ({len(all_scores)} cases)\n"
|
|
362
178
|
)
|
|
363
179
|
|
|
364
180
|
|
|
365
181
|
def main() -> int:
|
|
366
|
-
"""
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
settings_path = Path(tempfile.mkdtemp()) / EVAL_RUN_SETTINGS_FILE_NAME
|
|
380
|
-
settings_path.write_text(
|
|
381
|
-
json.dumps(run_settings(hook_group, all_allow_rules, parsed_arguments.model)),
|
|
382
|
-
encoding=OUTPUT_ENCODING,
|
|
383
|
-
)
|
|
384
|
-
tally = EvalTally()
|
|
385
|
-
for each_case in all_cases:
|
|
386
|
-
_run_case(each_case, settings_path, parsed_arguments, tally)
|
|
387
|
-
_write_report(tally, parsed_arguments.output_dir)
|
|
388
|
-
return 0 if tally.all_model_scores and not tally.all_errors else 1
|
|
182
|
+
"""Grade the trim on every fixture case, print the scores, and exit 1 on any loss."""
|
|
183
|
+
all_rows = scored_cases(EVAL_FIXTURE_DIRECTORY)
|
|
184
|
+
for each_name, each_score, _, _ in all_rows:
|
|
185
|
+
sys.stdout.write(
|
|
186
|
+
f"{each_name} facts {each_score.facts_kept}/{each_score.facts_total} "
|
|
187
|
+
f"overruled {each_score.overruled_marked}/{each_score.overruled_total} "
|
|
188
|
+
f"ratio {each_score.compression_ratio:.1f}x\n"
|
|
189
|
+
)
|
|
190
|
+
all_trim_scores = [each_row[1] for each_row in all_rows]
|
|
191
|
+
sys.stdout.write(_summary_line("trim", all_trim_scores))
|
|
192
|
+
sys.stdout.write(_summary_line("oracle (raw messages)", [each_row[2] for each_row in all_rows]))
|
|
193
|
+
sys.stdout.write(_summary_line("null (empty recall)", [each_row[3] for each_row in all_rows]))
|
|
194
|
+
return 0 if all_trim_scores and all(each.is_lossless for each in all_trim_scores) else 1
|
|
389
195
|
|
|
390
196
|
|
|
391
197
|
if __name__ == "__main__":
|