Spaces:
Runtime error
Runtime error
Upload folder using huggingface_hub
Browse files- DEBUG_DETERMINISM_PLAN.md +208 -0
- clip_quality_env/env.py +54 -5
- clip_quality_env/models.py +4 -0
- inference.py +41 -4
- server/app.py +4 -1
- server/grader.py +4 -13
- tests/test_auto_execution.py +73 -0
- tests/test_environment.py +51 -0
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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=
|
| 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
|
| 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 =
|
| 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(
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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")
|