vajeeda commited on
Commit
5ccdeee
Β·
1 Parent(s): 806216c

feat(phase0): add CriticAgent, CritiqueClaim, CritiqueOutput models

Browse files
viral_script_engine/agents/critic.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ from typing import List
4
+
5
+ import anthropic
6
+ from dotenv import load_dotenv
7
+ from pydantic import BaseModel
8
+
9
+ load_dotenv()
10
+
11
+ SYSTEM_PROMPT = """You are an expert social media content critic specialising in short-form video scripts for Reels and YouTube Shorts. Your job is to find specific, real problems in creator scripts β€” not vague feedback.
12
+
13
+ RULES:
14
+ 1. Every claim must cite a specific part of the script (quote it or reference the timestamp range)
15
+ 2. Every claim must be falsifiable β€” a human editor must be able to verify it by re-reading the script
16
+ 3. Never say "the hook is weak" β€” say "the hook at 0:00-0:03 promises [X] but the script delivers [Y] at 0:22, by which time most viewers have already dropped off"
17
+ 4. Focus on the 6 critique classes: hook_weakness, pacing_issue, cultural_mismatch, cta_buried, coherence_break, retention_risk
18
+ 5. Produce between 3 and 6 claims per script. No more, no less.
19
+ 6. For each claim, assign a timestamp range if the issue is locatable in the script. Use "N/A" only if it's a structural issue spanning the whole script.
20
+
21
+ OUTPUT FORMAT (respond ONLY with valid JSON, no markdown, no preamble):
22
+ {
23
+ "claims": [
24
+ {
25
+ "claim_id": "C1",
26
+ "critique_class": "hook_weakness",
27
+ "claim_text": "...",
28
+ "timestamp_range": "0:00-0:03",
29
+ "evidence": "exact quote from script",
30
+ "is_falsifiable": true,
31
+ "severity": "high"
32
+ }
33
+ ],
34
+ "overall_severity": "high"
35
+ }"""
36
+
37
+ USER_PROMPT_TEMPLATE = """SCRIPT TO CRITIQUE:
38
+ {script}
39
+
40
+ TARGET REGION: {region}
41
+ PLATFORM: {platform}
42
+ NICHE: {niche}
43
+
44
+ Produce your critique now."""
45
+
46
+ STRICT_RETRY_SUFFIX = (
47
+ "\n\nIMPORTANT: Your previous response was not valid JSON. "
48
+ "Respond ONLY with the raw JSON object. No markdown fences, no explanation, no preamble."
49
+ )
50
+
51
+
52
+ class CriticParseError(Exception):
53
+ pass
54
+
55
+
56
+ class CritiqueClaim(BaseModel):
57
+ claim_id: str
58
+ critique_class: str
59
+ claim_text: str
60
+ timestamp_range: str
61
+ evidence: str
62
+ is_falsifiable: bool
63
+ severity: str
64
+
65
+
66
+ class CritiqueOutput(BaseModel):
67
+ claims: List[CritiqueClaim]
68
+ overall_severity: str
69
+ raw_response: str
70
+
71
+
72
+ class CriticAgent:
73
+ def __init__(self, model_name: str = "claude-sonnet-4-20250514"):
74
+ self.model_name = model_name
75
+ self.client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
76
+
77
+ def _call_api(self, user_content: str) -> str:
78
+ message = self.client.messages.create(
79
+ model=self.model_name,
80
+ max_tokens=2048,
81
+ system=SYSTEM_PROMPT,
82
+ messages=[{"role": "user", "content": user_content}],
83
+ )
84
+ return message.content[0].text
85
+
86
+ def _parse_response(self, raw: str, user_content: str) -> CritiqueOutput:
87
+ try:
88
+ data = json.loads(raw)
89
+ data["raw_response"] = raw
90
+ return CritiqueOutput(**data)
91
+ except Exception:
92
+ strict_content = user_content + STRICT_RETRY_SUFFIX
93
+ raw2 = self._call_api(strict_content)
94
+ try:
95
+ data = json.loads(raw2)
96
+ data["raw_response"] = raw2
97
+ return CritiqueOutput(**data)
98
+ except Exception as e:
99
+ raise CriticParseError(f"Failed to parse critique after 2 attempts: {e}")
100
+
101
+ def critique(self, script: str, region: str, platform: str, niche: str) -> CritiqueOutput:
102
+ user_content = USER_PROMPT_TEMPLATE.format(
103
+ script=script, region=region, platform=platform, niche=niche
104
+ )
105
+ raw = self._call_api(user_content)
106
+ return self._parse_response(raw, user_content)
viral_script_engine/tests/test_critic.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ 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 ───────────────────────────────────────────────
9
+
10
+ def test_critique_claim_parses_valid_json():
11
+ data = {
12
+ "claim_id": "C1",
13
+ "critique_class": "hook_weakness",
14
+ "claim_text": "The hook is too slow.",
15
+ "timestamp_range": "0:00-0:03",
16
+ "evidence": "Let me tell you a secret",
17
+ "is_falsifiable": True,
18
+ "severity": "high",
19
+ }
20
+ claim = CritiqueClaim(**data)
21
+ assert claim.claim_id == "C1"
22
+ assert claim.severity == "high"
23
+
24
+
25
+ def test_critique_output_parses_valid_json():
26
+ data = {
27
+ "claims": [
28
+ {
29
+ "claim_id": "C1",
30
+ "critique_class": "hook_weakness",
31
+ "claim_text": "Weak hook.",
32
+ "timestamp_range": "0:00-0:03",
33
+ "evidence": "Let me tell you",
34
+ "is_falsifiable": True,
35
+ "severity": "high",
36
+ }
37
+ ],
38
+ "overall_severity": "high",
39
+ "raw_response": '{"claims": []}',
40
+ }
41
+ output = CritiqueOutput(**data)
42
+ assert len(output.claims) == 1