File size: 4,777 Bytes
258783b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ebae6ab
258783b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ebae6ab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258783b
 
ebae6ab
258783b
 
 
 
 
ebae6ab
258783b
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import json
from typing import List

from pydantic import BaseModel

from viral_script_engine.agents.llm_backend import LLMBackend
from viral_script_engine.agents.critic import CritiqueClaim

SYSTEM_PROMPT = """You are a script defender for short-form video content. Your job is NOT to say the script is perfect.
Your job is to identify what is genuinely working — and protect it from being edited away.

Specifically:
1. Find the single most powerful element of the script. Quote it exactly.
2. Explain why a viewer would respond positively to this element.
3. Review the Critic's claims. Flag any that would destroy the script's core strength or strip its regional authenticity if acted on.
4. List any phrases, idioms, or references that are intentionally regional — these must not be "corrected" away.

OUTPUT (JSON only, no preamble):
{
  "core_strength": "one sentence describing the strongest element",
  "core_strength_quote": "exact verbatim quote from the script",
  "defense_argument": "why this element should be preserved",
  "flagged_critic_claims": ["C2", "C3"],
  "regional_voice_elements": ["specific phrase 1", "specific phrase 2"]
}"""

STRICT_RETRY_SUFFIX = (
    "\n\nIMPORTANT: Your previous response was not valid JSON. "
    "Respond ONLY with the raw JSON object. No markdown fences, no explanation, no preamble."
)


class DefenderParseError(Exception):
    pass


class DefenderOutput(BaseModel):
    core_strength: str
    core_strength_quote: str
    defense_argument: str
    flagged_critic_claims: List[str]
    regional_voice_elements: List[str]


class DefenderAgent:
    def __init__(self, backend: str = "anthropic", model_name: str = "claude-haiku-4-5-20251001"):
        self.llm = LLMBackend(backend=backend, model_name=model_name)

    def _build_user_prompt(
        self,
        script: str,
        critic_claims: List[CritiqueClaim],
        region: str,
        platform: str,
    ) -> str:
        claims_lines = []
        for i, claim in enumerate(critic_claims, start=1):
            claims_lines.append(
                f"{i}. [{claim.claim_id}] ({claim.critique_class}) {claim.claim_text} | Evidence: {claim.evidence}"
            )
        claims_block = "\n".join(claims_lines) if claims_lines else "No critic claims provided."

        return (
            f"SCRIPT:\n{script}\n\n"
            f"CRITIC CLAIMS:\n{claims_block}\n\n"
            f"REGION: {region}\n"
            f"PLATFORM: {platform}\n\n"
            "Defend the script now."
        )

    @staticmethod
    def _extract_json(text: str) -> dict:
        import re
        text = text.strip()
        text = re.sub(r"^```(?:json)?", "", text).strip()
        text = re.sub(r"```$", "", text).strip()
        try:
            return json.loads(text)
        except json.JSONDecodeError:
            pass
        # Walk character-by-character to extract the first balanced {...}
        start = text.find("{")
        if start != -1:
            depth, in_str, esc = 0, False, False
            for i, c in enumerate(text[start:], start):
                if esc:
                    esc = False
                    continue
                if c == "\\" and in_str:
                    esc = True
                    continue
                if c == '"':
                    in_str = not in_str
                elif not in_str:
                    if c == "{":
                        depth += 1
                    elif c == "}":
                        depth -= 1
                        if depth == 0:
                            try:
                                return json.loads(text[start : i + 1])
                            except json.JSONDecodeError:
                                break
        raise ValueError(f"No valid JSON found in response: {text[:200]}")

    def _parse_response(self, raw: str, user_prompt: str) -> DefenderOutput:
        try:
            data = self._extract_json(raw)
            return DefenderOutput(**data)
        except Exception:
            strict_prompt = user_prompt + STRICT_RETRY_SUFFIX
            raw2 = self.llm.generate(SYSTEM_PROMPT, strict_prompt, max_tokens=1024)
            try:
                data = self._extract_json(raw2)
                return DefenderOutput(**data)
            except Exception as e:
                raise DefenderParseError(f"Failed to parse defender output after 2 attempts: {e}")

    def defend(
        self,
        script: str,
        critic_claims: List[CritiqueClaim],
        region: str,
        platform: str,
    ) -> DefenderOutput:
        user_prompt = self._build_user_prompt(script, critic_claims, region, platform)
        raw = self.llm.generate(SYSTEM_PROMPT, user_prompt, max_tokens=1024)
        return self._parse_response(raw, user_prompt)