vajeeda commited on
Commit
fea7356
Β·
1 Parent(s): 4e6f033

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