Spaces:
Sleeping
Sleeping
feat(phase0): add CriticEvaluator with rule-based gate logic
Browse files
viral_script_engine/evaluation/critic_evaluator.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Tuple
|
| 2 |
+
|
| 3 |
+
from pydantic import BaseModel
|
| 4 |
+
|
| 5 |
+
from viral_script_engine.agents.critic import CritiqueOutput
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class EvaluationResult(BaseModel):
|
| 9 |
+
script_id: str = ""
|
| 10 |
+
claim_count: int
|
| 11 |
+
specificity_score: float
|
| 12 |
+
falsifiability_score: float
|
| 13 |
+
timestamp_coverage: float
|
| 14 |
+
critique_class_diversity: float
|
| 15 |
+
passes_gate: bool
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class BatchEvaluationResult(BaseModel):
|
| 19 |
+
pass_count: int
|
| 20 |
+
pass_rate: float
|
| 21 |
+
passes_overall_gate: bool
|
| 22 |
+
per_script_results: List[EvaluationResult]
|
| 23 |
+
failing_scripts: List[str]
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class CriticEvaluator:
|
| 27 |
+
def evaluate(self, output: CritiqueOutput, script_text: str, script_id: str = "") -> EvaluationResult:
|
| 28 |
+
claims = output.claims
|
| 29 |
+
claim_count = len(claims)
|
| 30 |
+
|
| 31 |
+
if claim_count == 0:
|
| 32 |
+
return EvaluationResult(
|
| 33 |
+
script_id=script_id,
|
| 34 |
+
claim_count=0,
|
| 35 |
+
specificity_score=0.0,
|
| 36 |
+
falsifiability_score=0.0,
|
| 37 |
+
timestamp_coverage=0.0,
|
| 38 |
+
critique_class_diversity=0.0,
|
| 39 |
+
passes_gate=False,
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
specificity_score = sum(
|
| 43 |
+
1 for c in claims if c.evidence and c.evidence.strip() in script_text
|
| 44 |
+
) / claim_count
|
| 45 |
+
|
| 46 |
+
falsifiability_score = sum(1 for c in claims if c.is_falsifiable) / claim_count
|
| 47 |
+
|
| 48 |
+
timestamp_coverage = sum(
|
| 49 |
+
1 for c in claims if c.timestamp_range and c.timestamp_range.strip() != "N/A"
|
| 50 |
+
) / claim_count
|
| 51 |
+
|
| 52 |
+
unique_classes = {c.critique_class for c in claims}
|
| 53 |
+
critique_class_diversity = len(unique_classes) / 6
|
| 54 |
+
|
| 55 |
+
passes_gate = (
|
| 56 |
+
claim_count >= 3
|
| 57 |
+
and specificity_score >= 0.6
|
| 58 |
+
and falsifiability_score >= 0.7
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
return EvaluationResult(
|
| 62 |
+
script_id=script_id,
|
| 63 |
+
claim_count=claim_count,
|
| 64 |
+
specificity_score=round(specificity_score, 3),
|
| 65 |
+
falsifiability_score=round(falsifiability_score, 3),
|
| 66 |
+
timestamp_coverage=round(timestamp_coverage, 3),
|
| 67 |
+
critique_class_diversity=round(critique_class_diversity, 3),
|
| 68 |
+
passes_gate=passes_gate,
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
def batch_evaluate(
|
| 72 |
+
self,
|
| 73 |
+
results: List[Tuple[CritiqueOutput, str]],
|
| 74 |
+
script_ids: List[str] = None,
|
| 75 |
+
) -> BatchEvaluationResult:
|
| 76 |
+
if script_ids is None:
|
| 77 |
+
script_ids = ["" for _ in results]
|
| 78 |
+
|
| 79 |
+
per_script = [
|
| 80 |
+
self.evaluate(output, script_text, sid)
|
| 81 |
+
for (output, script_text), sid in zip(results, script_ids)
|
| 82 |
+
]
|
| 83 |
+
|
| 84 |
+
pass_count = sum(1 for r in per_script if r.passes_gate)
|
| 85 |
+
pass_rate = pass_count / len(per_script) if per_script else 0.0
|
| 86 |
+
failing_scripts = [r.script_id for r in per_script if not r.passes_gate]
|
| 87 |
+
|
| 88 |
+
return BatchEvaluationResult(
|
| 89 |
+
pass_count=pass_count,
|
| 90 |
+
pass_rate=round(pass_rate, 3),
|
| 91 |
+
passes_overall_gate=pass_rate >= 0.8,
|
| 92 |
+
per_script_results=per_script,
|
| 93 |
+
failing_scripts=failing_scripts,
|
| 94 |
+
)
|
viral_script_engine/tests/test_critic.py
CHANGED
|
@@ -3,6 +3,7 @@ import pytest
|
|
| 3 |
from unittest.mock import MagicMock, patch
|
| 4 |
|
| 5 |
from viral_script_engine.agents.critic import CritiqueClaim, CritiqueOutput
|
|
|
|
| 6 |
|
| 7 |
|
| 8 |
# ββ Task 2: Model parsing tests βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -40,3 +41,49 @@ def test_critique_output_parses_valid_json():
|
|
| 40 |
}
|
| 41 |
output = CritiqueOutput(**data)
|
| 42 |
assert len(output.claims) == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
from unittest.mock import MagicMock, patch
|
| 4 |
|
| 5 |
from viral_script_engine.agents.critic import CritiqueClaim, CritiqueOutput
|
| 6 |
+
from viral_script_engine.evaluation.critic_evaluator import CriticEvaluator, EvaluationResult
|
| 7 |
|
| 8 |
|
| 9 |
# ββ Task 2: Model parsing tests βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 41 |
}
|
| 42 |
output = CritiqueOutput(**data)
|
| 43 |
assert len(output.claims) == 1
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
# ββ Task 4: CriticEvaluator tests ββββββββββββββββββββββββββββββββββββββββββββ
|
| 47 |
+
|
| 48 |
+
SCRIPT_TEXT = "Let me tell you a secret about money. First, save twenty percent. Second, invest in index funds. Finally, avoid lifestyle inflation. That is all."
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _make_output(claims_data, overall="medium"):
|
| 52 |
+
claims = [CritiqueClaim(**c) for c in claims_data]
|
| 53 |
+
return CritiqueOutput(claims=claims, overall_severity=overall, raw_response="{}")
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def test_evaluator_passes_good_critique():
|
| 57 |
+
output = _make_output([
|
| 58 |
+
{"claim_id": "C1", "critique_class": "hook_weakness", "claim_text": "x", "timestamp_range": "0:00-0:03", "evidence": "Let me tell you a secret", "is_falsifiable": True, "severity": "high"},
|
| 59 |
+
{"claim_id": "C2", "critique_class": "cta_buried", "claim_text": "x", "timestamp_range": "0:10-0:15", "evidence": "That is all", "is_falsifiable": True, "severity": "medium"},
|
| 60 |
+
{"claim_id": "C3", "critique_class": "pacing_issue", "claim_text": "x", "timestamp_range": "N/A", "evidence": "save twenty percent", "is_falsifiable": True, "severity": "low"},
|
| 61 |
+
])
|
| 62 |
+
evaluator = CriticEvaluator()
|
| 63 |
+
result = evaluator.evaluate(output, SCRIPT_TEXT)
|
| 64 |
+
assert result.passes_gate is True
|
| 65 |
+
assert result.claim_count == 3
|
| 66 |
+
assert result.specificity_score >= 0.6
|
| 67 |
+
assert result.falsifiability_score >= 0.7
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def test_evaluator_fails_too_few_claims():
|
| 71 |
+
output = _make_output([
|
| 72 |
+
{"claim_id": "C1", "critique_class": "hook_weakness", "claim_text": "x", "timestamp_range": "0:00-0:03", "evidence": "Let me tell you", "is_falsifiable": True, "severity": "high"},
|
| 73 |
+
{"claim_id": "C2", "critique_class": "cta_buried", "claim_text": "x", "timestamp_range": "0:10", "evidence": "invest", "is_falsifiable": True, "severity": "medium"},
|
| 74 |
+
])
|
| 75 |
+
evaluator = CriticEvaluator()
|
| 76 |
+
result = evaluator.evaluate(output, SCRIPT_TEXT)
|
| 77 |
+
assert result.passes_gate is False
|
| 78 |
+
assert result.claim_count == 2
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def test_evaluator_fails_low_specificity():
|
| 82 |
+
output = _make_output([
|
| 83 |
+
{"claim_id": "C1", "critique_class": "hook_weakness", "claim_text": "x", "timestamp_range": "0:00-0:03", "evidence": "this quote is not in the script", "is_falsifiable": True, "severity": "high"},
|
| 84 |
+
{"claim_id": "C2", "critique_class": "cta_buried", "claim_text": "x", "timestamp_range": "0:10", "evidence": "also not in script", "is_falsifiable": False, "severity": "medium"},
|
| 85 |
+
{"claim_id": "C3", "critique_class": "pacing_issue", "claim_text": "x", "timestamp_range": "N/A", "evidence": "still not in script", "is_falsifiable": False, "severity": "low"},
|
| 86 |
+
])
|
| 87 |
+
evaluator = CriticEvaluator()
|
| 88 |
+
result = evaluator.evaluate(output, SCRIPT_TEXT)
|
| 89 |
+
assert result.passes_gate is False
|