elix3r commited on
Commit
0ddc036
·
verified ·
1 Parent(s): dd9493f

Upload folder using huggingface_hub

Browse files
DEBUG_DETERMINISM_PLAN.md ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Debug Plan: Deterministic Labels and Invariant Scores
2
+
3
+ ## Executive Summary
4
+ The observed behavior is not one single bug. It is a combination of:
5
+ 1. Label leakage paths in prediction logic.
6
+ 2. Difficulty-band score compression masking variance.
7
+ 3. Data-source mode differences (manifest vs task registry).
8
+ 4. Small/imbalanced episode pools causing repeated clip patterns.
9
+ 5. No cross-episode learning memory.
10
+
11
+ This plan is designed to debug in a strict order so we can separate each mechanism and avoid false conclusions.
12
+
13
+ ## Implementation Status (2026-04-06)
14
+ Completed in code:
15
+ 1. Removed expected-label leakage from fallback prediction path.
16
+ 2. Redacted `expected_label` from model-facing prompt metadata.
17
+ 3. Added structured per-step execution trace with score components and expected-label type/value.
18
+ 4. Added raw vs calibrated score telemetry in environment observations and dashboard reward rendering.
19
+ 5. Fixed expected-label integrity: derive label when value is missing, null, or invalid.
20
+ 6. Added `CLIP_CORPUS_SOURCE` mode toggle (`auto`, `manifest`, `task_registry`) for source-controlled debugging.
21
+
22
+ Validated:
23
+ 1. Focused regression suites pass for environment, inference, auto-execution, and grader.
24
+ 2. Runtime sanity checks show `none_expected=0` across current manifest pools.
25
+
26
+ Still pending from this plan:
27
+ 1. Full `tests/test_server_routes.py` alignment with newer one-click strategic flow and revised dashboard copy.
28
+
29
+ ## 20-Episode Matrix Snapshot (Fallback mode)
30
+
31
+ Source mode: `manifest`
32
+ - `task_easy`: raw_mean=0.9740 raw_var=0.000024 calibrated_mean=0.3117 calibrated_var=0.000002 match_rate=1.000 repeat_ratio=0.600 correction_rate=1.000
33
+ - `task_medium`: raw_mean=0.8385 raw_var=0.005583 calibrated_mean=0.6083 calibrated_var=0.000572 match_rate=0.680 repeat_ratio=0.000 correction_rate=0.700
34
+ - `task_hard`: raw_mean=0.8065 raw_var=0.001893 calibrated_mean=0.9381 calibrated_var=0.000194 match_rate=1.000 repeat_ratio=0.000 correction_rate=0.000
35
+
36
+ Source mode: `task_registry`
37
+ - `task_easy`: raw_mean=0.9400 raw_var=0.000000 calibrated_mean=0.3008 calibrated_var=0.000000 match_rate=1.000 repeat_ratio=0.000 correction_rate=1.000
38
+ - `task_medium`: raw_mean=0.8500 raw_var=0.000000 calibrated_mean=0.6120 calibrated_var=0.000000 match_rate=0.800 repeat_ratio=0.000 correction_rate=0.787
39
+ - `task_hard`: raw_mean=0.7500 raw_var=0.000000 calibrated_mean=0.9200 calibrated_var=0.000000 match_rate=0.600 repeat_ratio=0.000 correction_rate=0.000
40
+
41
+ Interpretation:
42
+ - Easy manifest behavior still shows repeat ratio because easy manifest pool has 2 clips across 5 episode steps.
43
+ - Score behavior is now fully explainable from raw components and calibration, with per-step trace available in `info.execution_trace`.
44
+
45
+ ## Verified Findings
46
+
47
+ ### 1) Prediction leakage path exists in code
48
+ - Fallback action can copy expected label directly:
49
+ - `inference.py:116`
50
+ - Prompt includes full clip metadata object (includes expected_label when present):
51
+ - `inference.py:148`
52
+ - Clip metadata schema includes expected_label field:
53
+ - `clip_quality_env/models.py:67`
54
+
55
+ Impact:
56
+ - In environments where expected labels are populated, predictions can trivially mirror labels.
57
+
58
+ ### 2) Difficulty calibration compresses scores into narrow task bands
59
+ - Bands:
60
+ - `clip_quality_env/difficulty.py:14`
61
+ - Calibration function:
62
+ - `clip_quality_env/difficulty.py:34`
63
+
64
+ Formula:
65
+ - `calibrated = band_min + raw * (band_max - band_min)`
66
+ - Band width is 0.32 for every task, which compresses raw score variance.
67
+
68
+ ### 3) Runtime data source is manifest, not task_registry (in this workspace)
69
+ Measured via reset observations:
70
+ - `task_easy`: source `manifest:easy`, corpus_size `2`
71
+ - `task_medium`: source `manifest:medium`, corpus_size `11`
72
+ - `task_hard`: source `manifest:hard`, corpus_size `7`
73
+
74
+ Why this matters:
75
+ - Easy episodes require 5 steps but only 2 unique easy clips exist, so clips repeat.
76
+ - Repetition can make aggregate rewards appear deterministic or nearly deterministic.
77
+
78
+ ### 4) expected_label is often None in manifest clips
79
+ Measured in current runtime:
80
+ - easy manifest clips show `expected_label=None` values.
81
+
82
+ Root cause in loading path:
83
+ - Loader keeps `expected_label` key with None value.
84
+ - Environment only derives expected label when key is missing, not when key is present but None:
85
+ - Derive check location: `clip_quality_env/env.py` (`if "expected_label" not in clip` logic in corpus load).
86
+
87
+ Impact:
88
+ - `EpisodeHistoryItem.expected_label` may become string `"None"`.
89
+ - Label match diagnostics can be misleading.
90
+ - Fallback leakage via expected_label is inactive for these rows (falls back to heuristic), but still dangerous in non-manifest mode.
91
+
92
+ ### 5) Episode memory resets each run
93
+ - `episode_history` is reset in `clip_quality_env/env.py:378`.
94
+
95
+ Impact:
96
+ - No cross-episode policy improvement.
97
+ - Any "learning" is at most within one 5-step episode prompt context.
98
+
99
+ ## Quick Reproduction Evidence (Fallback mode)
100
+ Command used:
101
+ - `unset HF_TOKEN OPENAI_API_KEY` then repeated `run_baseline(task=...)`
102
+
103
+ Observed behavior in this workspace:
104
+ - `task_easy`: ~0.310 to ~0.314 reward average, fallback mode always.
105
+ - `task_medium`: ~0.570 to ~0.628, fallback mode always.
106
+ - `task_hard`: ~0.930 to ~0.936, fallback mode always.
107
+ - Final-step rewards include values like `0.952` and occasionally `1.000` on hard clips.
108
+
109
+ Interpretation:
110
+ - Not perfectly constant per run in this workspace, but strongly constrained by:
111
+ - deterministic heuristics,
112
+ - repeated/limited clip sets,
113
+ - compressed calibration bands.
114
+
115
+ ## Prioritized Debug Sequence
116
+
117
+ ### Phase 1: Instrumentation Baseline (must do first)
118
+ Add temporary logs (or structured debug payload) for each step:
119
+ - mode: llm/fallback
120
+ - clip_id
121
+ - expected_label value and type
122
+ - predicted label
123
+ - format/label/reasoning score
124
+ - raw_total before calibration
125
+ - calibrated_total after calibration
126
+
127
+ Acceptance for Phase 1:
128
+ - Can explain each final score from recorded components and calibration math.
129
+
130
+ ### Phase 2: Isolate leakage mechanisms
131
+ Run four controlled scenarios:
132
+ 1. Current behavior, fallback mode.
133
+ 2. Fallback mode with expected_label removed from fallback path.
134
+ 3. LLM mode with expected_label redacted from prompt payload.
135
+ 4. LLM mode with both fallback and prompt redaction.
136
+
137
+ Acceptance:
138
+ - Prediction-match rate must drop from near-trivial baseline when leakage paths are removed.
139
+
140
+ ### Phase 3: Data-source validation
141
+ Run with both sources explicitly:
142
+ 1. Manifest enabled (current default)
143
+ 2. Manifest disabled / task_registry forced
144
+
145
+ Checks:
146
+ - corpus_size per difficulty
147
+ - expected_label completeness
148
+ - repeated clip ratio per episode
149
+
150
+ Acceptance:
151
+ - Source-dependent behavior is quantified and documented.
152
+
153
+ ### Phase 4: expected_label integrity fix validation
154
+ Validate expected-label derivation policy:
155
+ - If expected_label missing OR null, derive it before episode starts.
156
+
157
+ Acceptance:
158
+ - No history rows with expected_label equal to `"None"`.
159
+ - Label-match telemetry becomes meaningful.
160
+
161
+ ### Phase 5: Score compression interpretation
162
+ Report both:
163
+ - raw_total (unbanded)
164
+ - calibrated_total (banded)
165
+
166
+ Acceptance:
167
+ - Team can distinguish model/agent quality changes from banding artifacts.
168
+
169
+ ### Phase 6: Learning-signal evaluation
170
+ Measure in-episode adaptation quality:
171
+ - Compare step N vs step N+1 after low reward.
172
+ - Compute correction rate after negative feedback.
173
+
174
+ Measure cross-episode adaptation:
175
+ - Verify no carryover after reset.
176
+
177
+ Acceptance:
178
+ - Explicit statement of whether system is truly learning or only re-prompting.
179
+
180
+ ## Hypotheses Ranked by Confidence
181
+ 1. High: Leakage paths can force label correctness in non-manifest mode.
182
+ 2. High: Band calibration masks meaningful raw variance.
183
+ 3. High: Easy pool size=2 with 5-step episodes induces repeated pattern behavior.
184
+ 4. High: Null expected_label handling produces misleading diagnostics.
185
+ 5. Medium: User-observed exact constants likely depend on source mode and run conditions.
186
+
187
+ ## Patch Design (after debug confirms)
188
+ Do not apply until Phase 1-4 logs are captured.
189
+ 1. Remove expected_label from model-facing prompt payload.
190
+ 2. Change fallback label selection to heuristic-only.
191
+ 3. Derive expected label when value is missing OR null.
192
+ 4. Expose raw_total alongside calibrated_total in UI and logs.
193
+ 5. Add optional config to force task_registry source for reproducible testing.
194
+ 6. Expand easy manifest pool or reduce episode steps when pool is tiny.
195
+
196
+ ## Validation Matrix
197
+ For each task and source mode, run 20 episodes and report:
198
+ - mean/variance of raw_total
199
+ - mean/variance of calibrated_total
200
+ - label match rate
201
+ - fallback frequency
202
+ - repeated clip ratio
203
+ - correction rate after low reward steps
204
+
205
+ Success criteria:
206
+ - Scores are explainable and not suspiciously fixed.
207
+ - Predicted labels are no longer trivially leaked.
208
+ - Debug telemetry clearly separates calibration effects from model behavior.
clip_quality_env/env.py CHANGED
@@ -18,6 +18,14 @@ from server.tasks import TASK_REGISTRY
18
 
19
  EPISODE_STEPS = 5
20
  DEFAULT_REAL_CLIPS_MANIFEST = "data/real_clips_manifest.jsonl"
 
 
 
 
 
 
 
 
21
 
22
 
23
  @dataclass
@@ -50,13 +58,23 @@ class ClipQualityEnvironment(Environment[Action, Observation, State]):
50
  "format_score": 0.0,
51
  "label_score": 0.0,
52
  "reasoning_score": 0.0,
 
 
53
  "total_reward": 0.0,
54
  }
55
  self._corpus_source = "task_registry"
 
56
  self._manifest_warning = ""
57
  self._manifest_path = os.environ.get("REAL_CLIPS_MANIFEST", DEFAULT_REAL_CLIPS_MANIFEST)
58
  self._real_clip_pools = self._load_real_clip_pools()
59
 
 
 
 
 
 
 
 
60
  def _load_real_clip_pools(self) -> dict[str, list[dict[str, Any]]]:
61
  if not os.path.exists(self._manifest_path):
62
  self._manifest_warning = (
@@ -85,8 +103,23 @@ class ClipQualityEnvironment(Environment[Action, Observation, State]):
85
  task = TASK_REGISTRY[task_id]
86
  difficulty = str(task.get("difficulty", "")).lower()
87
  corpus: list[dict[str, Any]]
 
88
 
89
- if difficulty and self._real_clip_pools.get(difficulty):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  corpus = copy.deepcopy(self._real_clip_pools[difficulty])
91
  self._corpus_source = f"manifest:{difficulty}"
92
  else:
@@ -98,8 +131,7 @@ class ClipQualityEnvironment(Environment[Action, Observation, State]):
98
 
99
  for clip in corpus:
100
  clip.setdefault("clip_id", clip.get("id", str(uuid.uuid4())))
101
- if "expected_label" not in clip:
102
- clip["expected_label"] = self._rubric.derive_label(clip)
103
  clip["review_status"] = str(clip.get("review_status", "pending")).lower()
104
 
105
  corpus.sort(key=lambda item: str(item.get("clip_id", item.get("id", ""))))
@@ -269,6 +301,8 @@ class ClipQualityEnvironment(Environment[Action, Observation, State]):
269
  format_score=h.format_score,
270
  label_score=h.label_score,
271
  reasoning_score=h.reasoning_score,
 
 
272
  feedback_message=h.feedback_message,
273
  missing_features=list(h.missing_features),
274
  hallucinated_features=list(h.hallucinated_features),
@@ -314,12 +348,15 @@ class ClipQualityEnvironment(Environment[Action, Observation, State]):
314
  "format_score": float(self._last_reward_breakdown["format_score"]),
315
  "label_score": float(self._last_reward_breakdown["label_score"]),
316
  "reasoning_score": float(self._last_reward_breakdown["reasoning_score"]),
 
 
317
  "reward_total": float(self._last_reward_breakdown["total_reward"]),
318
  "session_history": session_history,
319
  "processed_clips": processed_clips,
320
  "wrong_label_count": wrong_labels,
321
  "label_accuracy": float(label_accuracy),
322
  "corpus_source": self._corpus_source,
 
323
  },
324
  )
325
 
@@ -360,6 +397,8 @@ class ClipQualityEnvironment(Environment[Action, Observation, State]):
360
  "format_score": 0.0,
361
  "label_score": 0.0,
362
  "reasoning_score": 0.0,
 
 
363
  "total_reward": 0.0,
364
  }
365
  self._state = State(
@@ -408,11 +447,18 @@ class ClipQualityEnvironment(Environment[Action, Observation, State]):
408
  reward_obj=reward_obj,
409
  difficulty=current.difficulty,
410
  )
 
 
 
 
 
411
  reward = float(reward_obj.total)
412
  self._last_reward_breakdown = {
413
  "format_score": float(reward_obj.format_score),
414
  "label_score": float(reward_obj.label_score),
415
  "reasoning_score": float(reward_obj.reasoning_score),
 
 
416
  "total_reward": float(reward),
417
  }
418
  self._state.total_reward += reward
@@ -438,17 +484,20 @@ class ClipQualityEnvironment(Environment[Action, Observation, State]):
438
  break
439
 
440
  self._state.actions_taken.append(action_label)
 
441
  self._state.episode_history.append(
442
  EpisodeHistoryItem(
443
  step=current_index + 1,
444
  difficulty=current.difficulty,
445
  clip_id=str(clip.get("clip_id", "")),
446
  label=action_label,
447
- expected_label=str(clip.get("expected_label", "")),
448
  reward=reward,
449
  format_score=float(reward_obj.format_score),
450
  label_score=float(reward_obj.label_score),
451
  reasoning_score=float(reward_obj.reasoning_score),
 
 
452
  feedback_message=str(feedback.get("feedback_message", "")),
453
  missing_features=list(feedback.get("missing_features", [])),
454
  hallucinated_features=list(feedback.get("hallucinated_features", [])),
@@ -466,7 +515,7 @@ class ClipQualityEnvironment(Environment[Action, Observation, State]):
466
  "clip": self._episode_plan[-1].clip,
467
  "action": action_obj.model_dump(),
468
  "reward": reward,
469
- "expected_label": self._episode_plan[-1].clip.get("expected_label"),
470
  }
471
  try:
472
  gt_promoted = self._gt_store.try_promote(step3_result, episode=self._state.episode_count)
 
18
 
19
  EPISODE_STEPS = 5
20
  DEFAULT_REAL_CLIPS_MANIFEST = "data/real_clips_manifest.jsonl"
21
+ VALID_LABELS = {"KEEP", "BORDERLINE", "REJECT"}
22
+
23
+
24
+ def _normalize_corpus_mode(value: str | None) -> str:
25
+ mode = str(value or "auto").strip().lower()
26
+ if mode in {"auto", "manifest", "task_registry"}:
27
+ return mode
28
+ return "auto"
29
 
30
 
31
  @dataclass
 
58
  "format_score": 0.0,
59
  "label_score": 0.0,
60
  "reasoning_score": 0.0,
61
+ "raw_total": 0.0,
62
+ "calibrated_total": 0.0,
63
  "total_reward": 0.0,
64
  }
65
  self._corpus_source = "task_registry"
66
+ self._corpus_mode = _normalize_corpus_mode(os.environ.get("CLIP_CORPUS_SOURCE"))
67
  self._manifest_warning = ""
68
  self._manifest_path = os.environ.get("REAL_CLIPS_MANIFEST", DEFAULT_REAL_CLIPS_MANIFEST)
69
  self._real_clip_pools = self._load_real_clip_pools()
70
 
71
+ def _resolve_expected_label(self, clip: dict[str, Any]) -> str:
72
+ raw_expected = clip.get("expected_label")
73
+ normalized = str(raw_expected).strip().upper() if raw_expected is not None else ""
74
+ if normalized in VALID_LABELS:
75
+ return normalized
76
+ return self._rubric.derive_label(clip)
77
+
78
  def _load_real_clip_pools(self) -> dict[str, list[dict[str, Any]]]:
79
  if not os.path.exists(self._manifest_path):
80
  self._manifest_warning = (
 
103
  task = TASK_REGISTRY[task_id]
104
  difficulty = str(task.get("difficulty", "")).lower()
105
  corpus: list[dict[str, Any]]
106
+ manifest_available = bool(difficulty and self._real_clip_pools.get(difficulty))
107
 
108
+ if self._corpus_mode == "task_registry":
109
+ corpus = copy.deepcopy(task.get("data_corpus", []))
110
+ self._corpus_source = f"task_registry:{task_id}"
111
+ elif self._corpus_mode == "manifest" and manifest_available:
112
+ corpus = copy.deepcopy(self._real_clip_pools[difficulty])
113
+ self._corpus_source = f"manifest:{difficulty}"
114
+ elif self._corpus_mode == "manifest":
115
+ corpus = copy.deepcopy(task.get("data_corpus", []))
116
+ self._corpus_source = f"task_registry:{task_id}"
117
+ if not self._manifest_warning:
118
+ self._manifest_warning = (
119
+ f"Manifest source requested but unavailable for difficulty '{difficulty}'; "
120
+ "using static task corpora."
121
+ )
122
+ elif manifest_available:
123
  corpus = copy.deepcopy(self._real_clip_pools[difficulty])
124
  self._corpus_source = f"manifest:{difficulty}"
125
  else:
 
131
 
132
  for clip in corpus:
133
  clip.setdefault("clip_id", clip.get("id", str(uuid.uuid4())))
134
+ clip["expected_label"] = self._resolve_expected_label(clip)
 
135
  clip["review_status"] = str(clip.get("review_status", "pending")).lower()
136
 
137
  corpus.sort(key=lambda item: str(item.get("clip_id", item.get("id", ""))))
 
301
  format_score=h.format_score,
302
  label_score=h.label_score,
303
  reasoning_score=h.reasoning_score,
304
+ raw_total=h.raw_total,
305
+ calibrated_total=h.calibrated_total,
306
  feedback_message=h.feedback_message,
307
  missing_features=list(h.missing_features),
308
  hallucinated_features=list(h.hallucinated_features),
 
348
  "format_score": float(self._last_reward_breakdown["format_score"]),
349
  "label_score": float(self._last_reward_breakdown["label_score"]),
350
  "reasoning_score": float(self._last_reward_breakdown["reasoning_score"]),
351
+ "raw_total": float(self._last_reward_breakdown["raw_total"]),
352
+ "calibrated_total": float(self._last_reward_breakdown["calibrated_total"]),
353
  "reward_total": float(self._last_reward_breakdown["total_reward"]),
354
  "session_history": session_history,
355
  "processed_clips": processed_clips,
356
  "wrong_label_count": wrong_labels,
357
  "label_accuracy": float(label_accuracy),
358
  "corpus_source": self._corpus_source,
359
+ "corpus_mode": self._corpus_mode,
360
  },
361
  )
362
 
 
397
  "format_score": 0.0,
398
  "label_score": 0.0,
399
  "reasoning_score": 0.0,
400
+ "raw_total": 0.0,
401
+ "calibrated_total": 0.0,
402
  "total_reward": 0.0,
403
  }
404
  self._state = State(
 
447
  reward_obj=reward_obj,
448
  difficulty=current.difficulty,
449
  )
450
+ raw_total = (
451
+ float(reward_obj.format_score)
452
+ + float(reward_obj.label_score)
453
+ + float(reward_obj.reasoning_score)
454
+ )
455
  reward = float(reward_obj.total)
456
  self._last_reward_breakdown = {
457
  "format_score": float(reward_obj.format_score),
458
  "label_score": float(reward_obj.label_score),
459
  "reasoning_score": float(reward_obj.reasoning_score),
460
+ "raw_total": float(raw_total),
461
+ "calibrated_total": float(reward),
462
  "total_reward": float(reward),
463
  }
464
  self._state.total_reward += reward
 
484
  break
485
 
486
  self._state.actions_taken.append(action_label)
487
+ expected_label = self._resolve_expected_label(clip)
488
  self._state.episode_history.append(
489
  EpisodeHistoryItem(
490
  step=current_index + 1,
491
  difficulty=current.difficulty,
492
  clip_id=str(clip.get("clip_id", "")),
493
  label=action_label,
494
+ expected_label=expected_label,
495
  reward=reward,
496
  format_score=float(reward_obj.format_score),
497
  label_score=float(reward_obj.label_score),
498
  reasoning_score=float(reward_obj.reasoning_score),
499
+ raw_total=float(raw_total),
500
+ calibrated_total=float(reward),
501
  feedback_message=str(feedback.get("feedback_message", "")),
502
  missing_features=list(feedback.get("missing_features", [])),
503
  hallucinated_features=list(feedback.get("hallucinated_features", [])),
 
515
  "clip": self._episode_plan[-1].clip,
516
  "action": action_obj.model_dump(),
517
  "reward": reward,
518
+ "expected_label": self._resolve_expected_label(self._episode_plan[-1].clip),
519
  }
520
  try:
521
  gt_promoted = self._gt_store.try_promote(step3_result, episode=self._state.episode_count)
clip_quality_env/models.py CHANGED
@@ -77,6 +77,8 @@ class HistoryItem(BaseModel):
77
  format_score: float = 0.0
78
  label_score: float = 0.0
79
  reasoning_score: float = 0.0
 
 
80
  feedback_message: str = ""
81
  missing_features: List[str] = Field(default_factory=list)
82
  hallucinated_features: List[str] = Field(default_factory=list)
@@ -96,6 +98,8 @@ class EpisodeHistoryItem(BaseModel):
96
  format_score: float = 0.0
97
  label_score: float = 0.0
98
  reasoning_score: float = 0.0
 
 
99
  feedback_message: str = ""
100
  missing_features: List[str] = Field(default_factory=list)
101
  hallucinated_features: List[str] = Field(default_factory=list)
 
77
  format_score: float = 0.0
78
  label_score: float = 0.0
79
  reasoning_score: float = 0.0
80
+ raw_total: float = 0.0
81
+ calibrated_total: float = 0.0
82
  feedback_message: str = ""
83
  missing_features: List[str] = Field(default_factory=list)
84
  hallucinated_features: List[str] = Field(default_factory=list)
 
98
  format_score: float = 0.0
99
  label_score: float = 0.0
100
  reasoning_score: float = 0.0
101
+ raw_total: float = 0.0
102
+ calibrated_total: float = 0.0
103
  feedback_message: str = ""
104
  missing_features: List[str] = Field(default_factory=list)
105
  hallucinated_features: List[str] = Field(default_factory=list)
inference.py CHANGED
@@ -113,7 +113,7 @@ class ClipQualityAgent:
113
  return "KEEP" if keep_signals >= 4 else "BORDERLINE"
114
 
115
  def _fallback_action(self, clip: Dict[str, Any]) -> Dict[str, Any]:
116
- label = _normalize_label(clip.get("expected_label"), fallback=self._heuristic_label(clip))
117
  confidence = 0.82 if label != "BORDERLINE" else 0.68
118
  reasoning = (
119
  f"{label} based on face_confidence={clip.get('face_confidence')}, "
@@ -138,6 +138,8 @@ class ClipQualityAgent:
138
 
139
  def act(self, task_id: str, obs: Dict, strategy_context: str | None = None) -> Dict:
140
  clip = obs.get("clip_metadata", {})
 
 
141
  rubric = obs.get("rubric_summary", "")
142
  history = self._get_history(obs)
143
  strategy = str(strategy_context or "").strip()
@@ -145,7 +147,7 @@ class ClipQualityAgent:
145
  prompt = (
146
  f"Task: {task_id}\n"
147
  f"Rubric:\n{rubric}\n"
148
- f"Clip metadata:\n{json.dumps(clip, indent=2)}\n"
149
  f"{history}\n"
150
  f"{strategy_block}\n"
151
  "Return JSON with keys: "
@@ -182,17 +184,45 @@ def execute_auto_episode(
182
 
183
  agent, mode, warning = load_agent_with_fallback()
184
  executed_steps = 0
 
185
  while not bool(obs.done) and executed_steps < int(obs.max_steps):
186
- action_dict = agent.act(task_id, obs.model_dump(), strategy_context=strategy_context)
 
 
 
 
187
  action_dict.setdefault("clip_id", obs.clip_metadata.clip_id)
 
188
  action = Action.model_validate(action_dict)
189
  obs = env.step(action)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  executed_steps += 1
191
 
192
  payload = obs.model_dump()
193
  payload.setdefault("info", {})
194
  payload["info"]["execution_mode"] = mode
195
  payload["info"]["executed_steps"] = executed_steps
 
196
  if warning:
197
  payload["info"]["warning"] = warning
198
  return payload
@@ -216,8 +246,15 @@ def run_episode(task_id: str, client: OpenAI | None, model_name: str) -> Dict:
216
  reward = float(obs.reward)
217
  done = bool(obs.done)
218
  rewards.append(reward)
 
 
219
  action_name = str(action.label)
220
- print(f"[STEP] step={step_num} label={action_name} reward={reward:.2f} done={str(done).lower()} error=null", flush=True)
 
 
 
 
 
221
  if done:
222
  break
223
 
 
113
  return "KEEP" if keep_signals >= 4 else "BORDERLINE"
114
 
115
  def _fallback_action(self, clip: Dict[str, Any]) -> Dict[str, Any]:
116
+ label = self._heuristic_label(clip)
117
  confidence = 0.82 if label != "BORDERLINE" else 0.68
118
  reasoning = (
119
  f"{label} based on face_confidence={clip.get('face_confidence')}, "
 
138
 
139
  def act(self, task_id: str, obs: Dict, strategy_context: str | None = None) -> Dict:
140
  clip = obs.get("clip_metadata", {})
141
+ clip_for_prompt = dict(clip)
142
+ clip_for_prompt.pop("expected_label", None)
143
  rubric = obs.get("rubric_summary", "")
144
  history = self._get_history(obs)
145
  strategy = str(strategy_context or "").strip()
 
147
  prompt = (
148
  f"Task: {task_id}\n"
149
  f"Rubric:\n{rubric}\n"
150
+ f"Clip metadata:\n{json.dumps(clip_for_prompt, indent=2)}\n"
151
  f"{history}\n"
152
  f"{strategy_block}\n"
153
  "Return JSON with keys: "
 
184
 
185
  agent, mode, warning = load_agent_with_fallback()
186
  executed_steps = 0
187
+ execution_trace: list[dict[str, Any]] = []
188
  while not bool(obs.done) and executed_steps < int(obs.max_steps):
189
+ obs_payload = obs.model_dump()
190
+ clip_metadata = dict(obs_payload.get("clip_metadata", {}))
191
+ expected_label_value = clip_metadata.get("expected_label")
192
+ expected_label_type = type(expected_label_value).__name__
193
+ action_dict = agent.act(task_id, obs_payload, strategy_context=strategy_context)
194
  action_dict.setdefault("clip_id", obs.clip_metadata.clip_id)
195
+ predicted_label = _normalize_label(action_dict.get("label"), fallback="BORDERLINE")
196
  action = Action.model_validate(action_dict)
197
  obs = env.step(action)
198
+ info = obs.info if isinstance(obs.info, dict) else {}
199
+ format_score = float(info.get("format_score", 0.0))
200
+ label_score = float(info.get("label_score", 0.0))
201
+ reasoning_score = float(info.get("reasoning_score", 0.0))
202
+ raw_total = float(info.get("raw_total", format_score + label_score + reasoning_score))
203
+ calibrated_total = float(info.get("calibrated_total", info.get("reward_total", obs.reward)))
204
+ execution_trace.append(
205
+ {
206
+ "step": int(executed_steps + 1),
207
+ "mode": mode,
208
+ "clip_id": str(clip_metadata.get("clip_id", "")),
209
+ "expected_label_value": expected_label_value,
210
+ "expected_label_type": expected_label_type,
211
+ "predicted_label": predicted_label,
212
+ "format_score": format_score,
213
+ "label_score": label_score,
214
+ "reasoning_score": reasoning_score,
215
+ "raw_total": raw_total,
216
+ "calibrated_total": calibrated_total,
217
+ }
218
+ )
219
  executed_steps += 1
220
 
221
  payload = obs.model_dump()
222
  payload.setdefault("info", {})
223
  payload["info"]["execution_mode"] = mode
224
  payload["info"]["executed_steps"] = executed_steps
225
+ payload["info"]["execution_trace"] = execution_trace
226
  if warning:
227
  payload["info"]["warning"] = warning
228
  return payload
 
246
  reward = float(obs.reward)
247
  done = bool(obs.done)
248
  rewards.append(reward)
249
+ raw_total = float(obs.info.get("raw_total", 0.0))
250
+ calibrated_total = float(obs.info.get("calibrated_total", reward))
251
  action_name = str(action.label)
252
+ print(
253
+ f"[STEP] step={step_num} label={action_name} reward={reward:.2f} "
254
+ f"raw_total={raw_total:.2f} calibrated_total={calibrated_total:.2f} "
255
+ f"done={str(done).lower()} error=null",
256
+ flush=True,
257
+ )
258
  if done:
259
  break
260
 
server/app.py CHANGED
@@ -337,8 +337,9 @@ def _reward_breakdown_markdown(obs: dict[str, Any], initialized: bool = False) -
337
  format_score = float(info.get("format_score", 0.0))
338
  label_score = float(info.get("label_score", 0.0))
339
  reasoning_score = float(info.get("reasoning_score", 0.0))
 
 
340
  total_reward = float(obs.get("reward", info.get("reward_total", 0.0)))
341
- running_total = float(info.get("total_reward", 0.0))
342
  best_score = float(info.get("best_score", 0.0))
343
  has_submission = bool(obs.get("history"))
344
 
@@ -361,6 +362,8 @@ def _reward_breakdown_markdown(obs: dict[str, Any], initialized: bool = False) -
361
  title,
362
  subtitle,
363
  cards,
 
 
364
  f"### **Total Reward:** <span style='color:{total_color};font-size:1.35rem;'>{total_reward:.3f}</span>",
365
  f"Current Best Score: **{best_score:.3f}**",
366
  ]
 
337
  format_score = float(info.get("format_score", 0.0))
338
  label_score = float(info.get("label_score", 0.0))
339
  reasoning_score = float(info.get("reasoning_score", 0.0))
340
+ raw_total = float(info.get("raw_total", format_score + label_score + reasoning_score))
341
+ calibrated_total = float(info.get("calibrated_total", info.get("reward_total", obs.get("reward", 0.0))))
342
  total_reward = float(obs.get("reward", info.get("reward_total", 0.0)))
 
343
  best_score = float(info.get("best_score", 0.0))
344
  has_submission = bool(obs.get("history"))
345
 
 
362
  title,
363
  subtitle,
364
  cards,
365
+ f"Raw Total (pre-calibration): **{raw_total:.3f}**",
366
+ f"Calibrated Total (difficulty-banded): **{calibrated_total:.3f}**",
367
  f"### **Total Reward:** <span style='color:{total_color};font-size:1.35rem;'>{total_reward:.3f}</span>",
368
  f"Current Best Score: **{best_score:.3f}**",
369
  ]
server/grader.py CHANGED
@@ -63,7 +63,10 @@ def _normalize_reasoning(action_dict: dict[str, Any]) -> str:
63
 
64
 
65
  def _normalize_action(action_dict: dict[str, Any], clip: dict[str, Any]) -> Action:
66
- fallback_label = _normalize_label(clip.get("expected_label"), fallback="BORDERLINE")
 
 
 
67
  payload = {
68
  "label": _normalize_label(
69
  action_dict.get("label")
@@ -79,14 +82,6 @@ def _normalize_action(action_dict: dict[str, Any], clip: dict[str, Any]) -> Acti
79
  return Action.model_validate(payload)
80
 
81
 
82
- def _expected_label_score(label: str, expected_label: str) -> float:
83
- if label == expected_label:
84
- return 0.60
85
- if expected_label == "BORDERLINE" and label in {"KEEP", "REJECT"}:
86
- return 0.25
87
- return 0.0
88
-
89
-
90
  def _mentions_cue(reasoning: str, cue: str) -> bool:
91
  text_tokens = set(_TEXT_TOKEN_RE.findall(reasoning.lower()))
92
  cue_tokens = [token for token in _TEXT_TOKEN_RE.findall(cue.lower()) if len(token) >= 4]
@@ -128,10 +123,6 @@ def grade(action_dict: dict[str, Any], task_id: str, temperature: float = 0.0, s
128
  label_score = float(reward.label_score)
129
  reasoning_score = float(reward.reasoning_score)
130
 
131
- expected_label = _normalize_label(clip.get("expected_label"), fallback="")
132
- if expected_label and _GT.lookup(str(clip.get("clip_id", ""))) is None:
133
- label_score = _expected_label_score(str(action.label), expected_label)
134
-
135
  reasoning_score = min(0.30, reasoning_score + _cue_bonus(str(action.reasoning), clip))
136
  task = TASK_REGISTRY.get(task_id, {})
137
  difficulty = normalize_difficulty(str(task.get("difficulty", "")))
 
63
 
64
 
65
  def _normalize_action(action_dict: dict[str, Any], clip: dict[str, Any]) -> Action:
66
+ clip_id = str(clip.get("clip_id", ""))
67
+ fallback_label = _normalize_label(_GT.lookup(clip_id), fallback="")
68
+ if fallback_label not in _VALID_LABELS:
69
+ fallback_label = _normalize_label(_RUBRIC.derive_label(clip), fallback="BORDERLINE")
70
  payload = {
71
  "label": _normalize_label(
72
  action_dict.get("label")
 
82
  return Action.model_validate(payload)
83
 
84
 
 
 
 
 
 
 
 
 
85
  def _mentions_cue(reasoning: str, cue: str) -> bool:
86
  text_tokens = set(_TEXT_TOKEN_RE.findall(reasoning.lower()))
87
  cue_tokens = [token for token in _TEXT_TOKEN_RE.findall(cue.lower()) if len(token) >= 4]
 
123
  label_score = float(reward.label_score)
124
  reasoning_score = float(reward.reasoning_score)
125
 
 
 
 
 
126
  reasoning_score = min(0.30, reasoning_score + _cue_bonus(str(action.reasoning), clip))
127
  task = TASK_REGISTRY.get(task_id, {})
128
  difficulty = normalize_difficulty(str(task.get("difficulty", "")))
tests/test_auto_execution.py CHANGED
@@ -1,6 +1,7 @@
1
  from __future__ import annotations
2
 
3
  import json
 
4
 
5
  import inference
6
  from clip_quality_env.grader import grade
@@ -53,6 +54,78 @@ def test_execute_auto_episode_includes_feedback_fields(monkeypatch):
53
  assert isinstance(first["feedback_message"], str)
54
 
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  def test_hard_tradeoff_language_improves_reasoning_score(tmp_path):
57
  rubric = RubricState(path=str(tmp_path / "rubric.json"))
58
  gt = GTStore(seed_path="data/seed_gt.json", state_path=str(tmp_path / "ground_truth.json"))
 
1
  from __future__ import annotations
2
 
3
  import json
4
+ from typing import Any
5
 
6
  import inference
7
  from clip_quality_env.grader import grade
 
54
  assert isinstance(first["feedback_message"], str)
55
 
56
 
57
+ def test_execute_auto_episode_includes_execution_trace(monkeypatch):
58
+ monkeypatch.delenv("HF_TOKEN", raising=False)
59
+ monkeypatch.delenv("OPENAI_API_KEY", raising=False)
60
+
61
+ env = ClipQualityEnvironment()
62
+ env.reset(task_id="task_hard", seed=2026)
63
+
64
+ payload = inference.execute_auto_episode(env=env, task_id="task_hard", strategy_context="")
65
+ trace = payload["info"]["execution_trace"]
66
+
67
+ assert len(trace) == 5
68
+ first_step = trace[0]
69
+ assert first_step["mode"] in {"llm", "fallback"}
70
+ assert first_step["predicted_label"] in {"KEEP", "BORDERLINE", "REJECT"}
71
+ assert "expected_label_value" in first_step
72
+ assert "expected_label_type" in first_step
73
+ assert "format_score" in first_step
74
+ assert "label_score" in first_step
75
+ assert "reasoning_score" in first_step
76
+ assert "raw_total" in first_step
77
+ assert "calibrated_total" in first_step
78
+
79
+
80
+ def test_fallback_action_does_not_copy_expected_label():
81
+ agent = inference.ClipQualityAgent(client=None, model="test")
82
+ clip = {
83
+ "clip_id": "clip_leak_check",
84
+ "expected_label": "KEEP",
85
+ "occlusion_present": True,
86
+ "face_confidence": 0.95,
87
+ "motion_score": 0.05,
88
+ "audio_snr_db": 30.0,
89
+ "lighting_uniformity": 0.9,
90
+ }
91
+
92
+ action = agent._fallback_action(clip)
93
+
94
+ assert action["label"] == "REJECT"
95
+
96
+
97
+ def test_agent_prompt_redacts_expected_label():
98
+ class _CaptureAgent(inference.ClipQualityAgent):
99
+ def __init__(self):
100
+ super().__init__(client=None, model="dummy")
101
+ self.prompt = ""
102
+
103
+ def _call(self, prompt: str) -> dict[str, Any] | None:
104
+ self.prompt = prompt
105
+ return {
106
+ "label": "KEEP",
107
+ "reasoning": "face_confidence is high and motion_score is low.",
108
+ "confidence": 0.9,
109
+ "clip_id": "clip_123",
110
+ }
111
+
112
+ agent = _CaptureAgent()
113
+ obs = {
114
+ "clip_metadata": {
115
+ "clip_id": "clip_123",
116
+ "face_confidence": 0.91,
117
+ "motion_score": 0.11,
118
+ "expected_label": "REJECT",
119
+ },
120
+ "rubric_summary": "Rubric text",
121
+ "history": [],
122
+ }
123
+
124
+ _ = agent.act("task_easy", obs)
125
+
126
+ assert "expected_label" not in agent.prompt
127
+
128
+
129
  def test_hard_tradeoff_language_improves_reasoning_score(tmp_path):
130
  rubric = RubricState(path=str(tmp_path / "rubric.json"))
131
  gt = GTStore(seed_path="data/seed_gt.json", state_path=str(tmp_path / "ground_truth.json"))
tests/test_environment.py CHANGED
@@ -1,5 +1,6 @@
1
  from __future__ import annotations
2
 
 
3
  from statistics import mean
4
 
5
  from clip_quality_env.env import ClipQualityEnvironment
@@ -57,12 +58,16 @@ def test_environment_observation_exposes_reward_decomposition():
57
  assert reset_obs.info["format_score"] == 0.0
58
  assert reset_obs.info["label_score"] == 0.0
59
  assert reset_obs.info["reasoning_score"] == 0.0
 
 
60
  assert reset_obs.info["reward_total"] == 0.0
61
  assert reset_obs.info["total_reward"] == 0.0
62
  assert reset_obs.info["reward_breakdown"] == {
63
  "format_score": 0.0,
64
  "label_score": 0.0,
65
  "reasoning_score": 0.0,
 
 
66
  "total_reward": 0.0,
67
  }
68
 
@@ -78,16 +83,62 @@ def test_environment_observation_exposes_reward_decomposition():
78
  assert step_obs.info["format_score"] in {0.0, 0.1}
79
  assert step_obs.info["label_score"] in {0.0, 0.25, 0.6}
80
  assert 0.0 <= step_obs.info["reasoning_score"] <= 0.3
 
 
 
 
 
 
 
 
 
81
  assert abs(float(step_obs.info["reward_total"]) - float(step_obs.reward)) < 1e-9
82
  assert abs(float(step_obs.info["total_reward"]) - float(env.state.total_reward)) < 1e-9
83
  assert step_obs.info["reward_breakdown"] == {
84
  "format_score": float(step_obs.info["format_score"]),
85
  "label_score": float(step_obs.info["label_score"]),
86
  "reasoning_score": float(step_obs.info["reasoning_score"]),
 
 
87
  "total_reward": float(step_obs.info["reward_total"]),
88
  }
89
 
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  def test_environment_observation_includes_full_unsliced_corpus():
92
  env = ClipQualityEnvironment()
93
  obs = env.reset(task_id="task_medium")
 
1
  from __future__ import annotations
2
 
3
+ import json
4
  from statistics import mean
5
 
6
  from clip_quality_env.env import ClipQualityEnvironment
 
58
  assert reset_obs.info["format_score"] == 0.0
59
  assert reset_obs.info["label_score"] == 0.0
60
  assert reset_obs.info["reasoning_score"] == 0.0
61
+ assert reset_obs.info["raw_total"] == 0.0
62
+ assert reset_obs.info["calibrated_total"] == 0.0
63
  assert reset_obs.info["reward_total"] == 0.0
64
  assert reset_obs.info["total_reward"] == 0.0
65
  assert reset_obs.info["reward_breakdown"] == {
66
  "format_score": 0.0,
67
  "label_score": 0.0,
68
  "reasoning_score": 0.0,
69
+ "raw_total": 0.0,
70
+ "calibrated_total": 0.0,
71
  "total_reward": 0.0,
72
  }
73
 
 
83
  assert step_obs.info["format_score"] in {0.0, 0.1}
84
  assert step_obs.info["label_score"] in {0.0, 0.25, 0.6}
85
  assert 0.0 <= step_obs.info["reasoning_score"] <= 0.3
86
+ assert abs(
87
+ float(step_obs.info["raw_total"])
88
+ - (
89
+ float(step_obs.info["format_score"])
90
+ + float(step_obs.info["label_score"])
91
+ + float(step_obs.info["reasoning_score"])
92
+ )
93
+ ) < 1e-9
94
+ assert abs(float(step_obs.info["calibrated_total"]) - float(step_obs.reward)) < 1e-9
95
  assert abs(float(step_obs.info["reward_total"]) - float(step_obs.reward)) < 1e-9
96
  assert abs(float(step_obs.info["total_reward"]) - float(env.state.total_reward)) < 1e-9
97
  assert step_obs.info["reward_breakdown"] == {
98
  "format_score": float(step_obs.info["format_score"]),
99
  "label_score": float(step_obs.info["label_score"]),
100
  "reasoning_score": float(step_obs.info["reasoning_score"]),
101
+ "raw_total": float(step_obs.info["raw_total"]),
102
+ "calibrated_total": float(step_obs.info["calibrated_total"]),
103
  "total_reward": float(step_obs.info["reward_total"]),
104
  }
105
 
106
 
107
+ def test_environment_derives_expected_label_when_manifest_value_is_null(monkeypatch, tmp_path):
108
+ manifest_path = tmp_path / "manifest_null_expected.jsonl"
109
+ manifest_row = {
110
+ "difficulty": "easy",
111
+ "clip_id": "manifest_null_expected",
112
+ "expected_label": None,
113
+ "face_confidence": 0.88,
114
+ "motion_score": 0.12,
115
+ "audio_snr_db": 24.0,
116
+ "lighting_uniformity": 0.8,
117
+ "duration_s": 8.0,
118
+ }
119
+ manifest_path.write_text(json.dumps(manifest_row) + "\n", encoding="utf-8")
120
+
121
+ monkeypatch.setenv("REAL_CLIPS_MANIFEST", str(manifest_path))
122
+ monkeypatch.setenv("CLIP_CORPUS_SOURCE", "manifest")
123
+
124
+ env = ClipQualityEnvironment()
125
+ obs = env.reset(task_id="task_easy", seed=2026)
126
+
127
+ assert obs.info["corpus_source"] == "manifest:easy"
128
+ assert str(obs.clip_metadata.expected_label).upper() in {"KEEP", "BORDERLINE", "REJECT"}
129
+ assert str(obs.clip_metadata.expected_label).upper() != "NONE"
130
+
131
+
132
+ def test_environment_can_force_task_registry_source(monkeypatch):
133
+ monkeypatch.setenv("CLIP_CORPUS_SOURCE", "task_registry")
134
+
135
+ env = ClipQualityEnvironment()
136
+ obs = env.reset(task_id="task_easy", seed=2026)
137
+
138
+ assert obs.info["corpus_mode"] == "task_registry"
139
+ assert obs.info["corpus_source"] == "task_registry:task_easy"
140
+
141
+
142
  def test_environment_observation_includes_full_unsliced_corpus():
143
  env = ClipQualityEnvironment()
144
  obs = env.reset(task_id="task_medium")