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)