vajeeda commited on
Commit
5c28dc0
Β·
1 Parent(s): e6b6793

Phase 6 implemented

Browse files
docs/progress.md CHANGED
@@ -67,8 +67,18 @@ Do not read entire codebase to understand progress β€” read this file.
67
  βœ… r5_defender_preservation.py β€” rewritten to TF-IDF cosine sim (pyarrow DLL workaround)
68
  βœ… Phase 5 gate β€” submission_check 10/10 PASS, demo runs end-to-end
69
 
70
- ## Phase 6 β€” [Pending]
71
- ⏳ [feature name] β€” [one line description]
 
 
 
 
 
 
 
 
 
 
72
 
73
  ## Phase 7 β€” [Pending]
74
  ⏳ [feature name] β€” [one line description]
 
67
  βœ… r5_defender_preservation.py β€” rewritten to TF-IDF cosine sim (pyarrow DLL workaround)
68
  βœ… Phase 5 gate β€” submission_check 10/10 PASS, demo runs end-to-end
69
 
70
+ ## Phase 6 β€” Moderation Agent + Originality Agent
71
+ βœ… ModerationAgent β€” zero-LLM rule-based shadowban detection, 6 categories, severity mapping
72
+ βœ… OriginalityAgent β€” zero-LLM fuzzy template matching, difflib SequenceMatcher at 0.75 threshold
73
+ βœ… SafetyReward (R6) β€” hard zero on high-severity, tiered scoring for medium/low/clean
74
+ βœ… OriginalityReward (R7) β€” cliff at 0.4, continuous scoring above
75
+ βœ… data/shadowban_triggers.json β€” 20+ entries per 6 categories
76
+ βœ… data/viral_templates.json β€” 20+ entries per 4 categories (hooks, structures, CTAs, transitions)
77
+ βœ… observations.py β€” R6/R7 fields in RewardComponents, moderation/originality outputs in DebateRound
78
+ βœ… env.py β€” ModerationAgent + OriginalityAgent wired into reset() and step()
79
+ βœ… reward_aggregator.py β€” new weights (R6: 0.10, R7: 0.10), R6 hard-zero fires before catastrophic drop check
80
+ βœ… test_phase6.py β€” 16 tests, all passing
81
+ βœ… Phase 6 gate β€” PHASE 6 GATE: PASS, R6+R7 active, 7 total reward components
82
 
83
  ## Phase 7 β€” [Pending]
84
  ⏳ [feature name] β€” [one line description]
session/phase-log.md CHANGED
@@ -24,6 +24,7 @@ ROLLED BACK β€” changes reverted, reason in line
24
  [2026-04-26] [Phase 3] COMPLETE β€” curriculum tiers, GRPO pipeline, rollout fn, dry-run gate PASS
25
  [2026-04-26] [Phase 4] COMPLETE β€” DifficultyTracker, CriticEscalationEngine, env wiring, 6 tests pass, gate PASS
26
  [2026-04-26] [Phase 5] COMPLETE β€” HF deploy infra, demo, README, submission_check 10/10 PASS, demo end-to-end ok
 
27
 
28
  ---
29
 
 
24
  [2026-04-26] [Phase 3] COMPLETE β€” curriculum tiers, GRPO pipeline, rollout fn, dry-run gate PASS
25
  [2026-04-26] [Phase 4] COMPLETE β€” DifficultyTracker, CriticEscalationEngine, env wiring, 6 tests pass, gate PASS
26
  [2026-04-26] [Phase 5] COMPLETE β€” HF deploy infra, demo, README, submission_check 10/10 PASS, demo end-to-end ok
27
+ [2026-04-26] [Phase 6] COMPLETE β€” ModerationAgent, OriginalityAgent, R6/R7 rewards, 16 tests PASS, gate PASS
28
 
29
  ---
30
 
viral_script_engine/agents/moderation_agent.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import re
3
+ from pathlib import Path
4
+ from typing import Dict, List
5
+
6
+ from pydantic import BaseModel
7
+
8
+
9
+ class ModerationFlag(BaseModel):
10
+ category: str
11
+ trigger_phrase: str
12
+ position: str
13
+ severity: str
14
+ suggestion: str
15
+
16
+
17
+ class ModerationOutput(BaseModel):
18
+ flags: List[ModerationFlag]
19
+ is_safe: bool
20
+ overall_risk: str
21
+ total_flags: int
22
+
23
+
24
+ _CATEGORY_LABEL_MAP = {
25
+ "hate_speech_patterns": "hate_speech",
26
+ "misleading_health_claims": "misleading_health",
27
+ "copyright_bait_phrases": "copyright_bait",
28
+ "engagement_bait": "engagement_bait",
29
+ "spam_signals": "spam",
30
+ "platform_policy_violations": "policy_violation",
31
+ }
32
+
33
+ _SEVERITY_MAP = {
34
+ "hate_speech_patterns": "high",
35
+ "misleading_health_claims": "high",
36
+ "copyright_bait_phrases": "medium",
37
+ "engagement_bait": "low",
38
+ "spam_signals": "medium",
39
+ "platform_policy_violations": "high",
40
+ }
41
+
42
+ _SUGGESTIONS = {
43
+ "hate_speech_patterns": "Remove or replace with respectful, inclusive language.",
44
+ "misleading_health_claims": "Replace with evidence-based language; avoid absolute health guarantees.",
45
+ "copyright_bait_phrases": "Remove references to free/leaked content to avoid DMCA flags.",
46
+ "engagement_bait": "Replace with a genuine question or value-based CTA.",
47
+ "spam_signals": "Remove external link bait; focus on in-app value delivery.",
48
+ "platform_policy_violations": "Remove policy-violating claims; keep messaging compliant.",
49
+ }
50
+
51
+
52
+ def _split_script(script: str) -> Dict[str, str]:
53
+ sentences = re.split(r'(?<=[.!?])\s+', script.strip())
54
+ if len(sentences) <= 5:
55
+ return {"hook": script, "body": "", "cta": ""}
56
+ hook = " ".join(sentences[:3])
57
+ cta = " ".join(sentences[-2:])
58
+ body = " ".join(sentences[3:-2])
59
+ return {"hook": hook, "body": body, "cta": cta}
60
+
61
+
62
+ class ModerationAgent:
63
+ """
64
+ Checks scripts for content that would get flagged or shadowbanned on Reels.
65
+ Zero LLM calls β€” purely rule-based against shadowban_triggers.json.
66
+ """
67
+
68
+ def __init__(self, kb_path: str = "data/shadowban_triggers.json"):
69
+ resolved = Path(kb_path)
70
+ if not resolved.is_absolute():
71
+ resolved = Path(__file__).parent.parent / kb_path
72
+ with open(resolved) as f:
73
+ self._kb: Dict[str, List[str]] = json.load(f)
74
+
75
+ def check(self, script: str) -> ModerationOutput:
76
+ sections = _split_script(script)
77
+ flags: List[ModerationFlag] = []
78
+
79
+ for category, triggers in self._kb.items():
80
+ severity = _SEVERITY_MAP.get(category, "low")
81
+ label = _CATEGORY_LABEL_MAP.get(category, category)
82
+ suggestion = _SUGGESTIONS.get(category, "Review and revise this content.")
83
+ for position, text in sections.items():
84
+ if not text:
85
+ continue
86
+ text_lower = text.lower()
87
+ for trigger in triggers:
88
+ if trigger in text_lower:
89
+ flags.append(ModerationFlag(
90
+ category=label,
91
+ trigger_phrase=trigger,
92
+ position=position,
93
+ severity=severity,
94
+ suggestion=suggestion,
95
+ ))
96
+
97
+ has_high = any(f.severity == "high" for f in flags)
98
+ has_medium = any(f.severity == "medium" for f in flags)
99
+
100
+ if has_high:
101
+ overall_risk = "high_risk"
102
+ elif has_medium:
103
+ overall_risk = "medium_risk"
104
+ elif flags:
105
+ overall_risk = "low_risk"
106
+ else:
107
+ overall_risk = "safe"
108
+
109
+ return ModerationOutput(
110
+ flags=flags,
111
+ is_safe=not has_high,
112
+ overall_risk=overall_risk,
113
+ total_flags=len(flags),
114
+ )
viral_script_engine/agents/originality_agent.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import re
3
+ from difflib import SequenceMatcher
4
+ from pathlib import Path
5
+ from typing import Dict, List
6
+
7
+ from pydantic import BaseModel
8
+
9
+
10
+ class OriginalityFlag(BaseModel):
11
+ template_type: str
12
+ matched_pattern: str
13
+ script_excerpt: str
14
+ suggestion: str
15
+
16
+
17
+ class OriginalityOutput(BaseModel):
18
+ flags: List[OriginalityFlag]
19
+ originality_score: float
20
+ is_generic: bool
21
+ unique_elements: List[str]
22
+
23
+
24
+ _TEMPLATE_TYPE_MAP = {
25
+ "overused_hooks": "overused_hook",
26
+ "overused_structures": "overused_structure",
27
+ "overused_cta_phrases": "overused_cta",
28
+ "overused_transitions": "overused_transition",
29
+ }
30
+
31
+ _SUGGESTIONS = {
32
+ "overused_hook": "Rewrite the hook with a specific data point, personal story, or unexpected angle.",
33
+ "overused_structure": "Try an unconventional narrative arc β€” start mid-story or end with the question.",
34
+ "overused_cta": "Replace with a specific, contextual call-to-action tied to the video's content.",
35
+ "overused_transition": "Cut the transition filler and jump directly to the next point.",
36
+ }
37
+
38
+ _FUZZY_THRESHOLD = 0.75
39
+
40
+
41
+ def _split_script(script: str) -> Dict[str, str]:
42
+ sentences = re.split(r'(?<=[.!?])\s+', script.strip())
43
+ if len(sentences) <= 5:
44
+ return {"hook": script, "body": "", "cta": ""}
45
+ hook = " ".join(sentences[:3])
46
+ cta = " ".join(sentences[-2:])
47
+ body = " ".join(sentences[3:-2])
48
+ return {"hook": hook, "body": body, "cta": cta}
49
+
50
+
51
+ def _fuzzy_match(text: str, pattern: str) -> bool:
52
+ text_lower = text.lower()
53
+ pattern_lower = pattern.lower()
54
+ if pattern_lower in text_lower:
55
+ return True
56
+ ratio = SequenceMatcher(None, text_lower, pattern_lower).ratio()
57
+ return ratio >= _FUZZY_THRESHOLD
58
+
59
+
60
+ class OriginalityAgent:
61
+ """
62
+ Measures how distinct the script sounds compared to overused Reels formats.
63
+ Zero LLM calls β€” fuzzy string matching against viral_templates.json.
64
+ Uses difflib.SequenceMatcher (threshold: 0.75 similarity).
65
+ """
66
+
67
+ def __init__(self, templates_path: str = "data/viral_templates.json"):
68
+ resolved = Path(templates_path)
69
+ if not resolved.is_absolute():
70
+ resolved = Path(__file__).parent.parent / templates_path
71
+ with open(resolved) as f:
72
+ self._templates: Dict[str, List[str]] = json.load(f)
73
+
74
+ def check(self, script: str) -> OriginalityOutput:
75
+ sections = _split_script(script)
76
+ flags: List[OriginalityFlag] = []
77
+ matched_sections = set()
78
+
79
+ for category, patterns in self._templates.items():
80
+ template_type = _TEMPLATE_TYPE_MAP.get(category, category)
81
+ suggestion = _SUGGESTIONS.get(template_type, "Make this section more original.")
82
+ for pos, text in sections.items():
83
+ if not text:
84
+ continue
85
+ sentences = re.split(r'(?<=[.!?])\s+', text.strip())
86
+ for sentence in sentences:
87
+ for pattern in patterns:
88
+ if _fuzzy_match(sentence, pattern):
89
+ section_key = f"{pos}:{sentence[:40]}"
90
+ matched_sections.add(section_key)
91
+ flags.append(OriginalityFlag(
92
+ template_type=template_type,
93
+ matched_pattern=pattern,
94
+ script_excerpt=sentence[:80],
95
+ suggestion=suggestion,
96
+ ))
97
+
98
+ all_sentences = re.split(r'(?<=[.!?])\s+', script.strip())
99
+ total = max(len(all_sentences), 1)
100
+ matched_count = len(matched_sections)
101
+ originality_score = max(0.0, min(1.0, 1.0 - (matched_count / total)))
102
+
103
+ unique_elements = [
104
+ s for s in all_sentences
105
+ if not any(
106
+ _fuzzy_match(s, pattern)
107
+ for patterns in self._templates.values()
108
+ for pattern in patterns
109
+ )
110
+ ]
111
+
112
+ return OriginalityOutput(
113
+ flags=flags,
114
+ originality_score=originality_score,
115
+ is_generic=originality_score < 0.4,
116
+ unique_elements=unique_elements,
117
+ )
viral_script_engine/data/shadowban_triggers.json ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "hate_speech_patterns": [
3
+ "go back to your country",
4
+ "sub-human",
5
+ "vermin",
6
+ "parasite",
7
+ "pest",
8
+ "replace the whites",
9
+ "white genocide",
10
+ "great replacement",
11
+ "race traitor",
12
+ "race mixing is wrong",
13
+ "demographic replacement",
14
+ "they control everything",
15
+ "globalist agenda",
16
+ "kalergi plan",
17
+ "blood and soil",
18
+ "14 words",
19
+ "heil",
20
+ "racial purity",
21
+ "ethnic cleansing",
22
+ "gas the"
23
+ ],
24
+ "misleading_health_claims": [
25
+ "cure",
26
+ "doctors don't want you to know",
27
+ "guaranteed weight loss",
28
+ "100% natural treatment",
29
+ "miracle remedy",
30
+ "big pharma hiding",
31
+ "instant cure",
32
+ "detox your body in 24 hours",
33
+ "reverses diabetes naturally",
34
+ "shrinks tumors",
35
+ "eliminate cancer",
36
+ "fda banned",
37
+ "secret ingredient doctors hate",
38
+ "heal yourself at home",
39
+ "no side effects guaranteed",
40
+ "clears arteries overnight",
41
+ "secret ancient remedy",
42
+ "boosts immunity by 1000%",
43
+ "prevents all diseases",
44
+ "one weird trick doctors"
45
+ ],
46
+ "copyright_bait_phrases": [
47
+ "full movie",
48
+ "free download",
49
+ "watch without ads",
50
+ "leaked footage",
51
+ "full episode free",
52
+ "no copyright",
53
+ "watch full film",
54
+ "download for free",
55
+ "stream free online",
56
+ "bypass paywall",
57
+ "full series free",
58
+ "watch hd without paying",
59
+ "crack version",
60
+ "pirated",
61
+ "torrent link",
62
+ "free streaming no sign up",
63
+ "leaked scene",
64
+ "free premium account",
65
+ "account generator free",
66
+ "watch full length movie"
67
+ ],
68
+ "engagement_bait": [
69
+ "comment if you agree",
70
+ "share to save",
71
+ "follow or bad luck",
72
+ "tag 3 friends",
73
+ "double tap if",
74
+ "like if you think",
75
+ "share if you care",
76
+ "type yes if",
77
+ "comment amen",
78
+ "repost to spread awareness",
79
+ "tag someone who needs to see this",
80
+ "like for part 2",
81
+ "comment your answer below",
82
+ "share to your story if you agree",
83
+ "send this to 10 people",
84
+ "type 1 if yes 2 if no",
85
+ "follow to find out",
86
+ "like to support",
87
+ "comment done when you finish",
88
+ "share with every parent you know"
89
+ ],
90
+ "spam_signals": [
91
+ "link in bio for free",
92
+ "dm me the word",
93
+ "click the link below for",
94
+ "comment to get access",
95
+ "free gift in my bio",
96
+ "limited time only dm",
97
+ "get yours now in bio",
98
+ "dm for price",
99
+ "free pdf in bio",
100
+ "drop your email below",
101
+ "click link in story",
102
+ "comment your phone number",
103
+ "dm for exclusive access",
104
+ "free course in bio link",
105
+ "type below to get the template",
106
+ "visit my website in bio",
107
+ "dm for the checklist",
108
+ "grab the free guide in link below",
109
+ "swipe up and get it free",
110
+ "first 100 comments get free"
111
+ ],
112
+ "platform_policy_violations": [
113
+ "buy followers",
114
+ "get rich quick",
115
+ "make $x in y days guaranteed",
116
+ "buy instagram likes",
117
+ "boost your followers instantly",
118
+ "earn money doing nothing",
119
+ "passive income no effort",
120
+ "100k in 30 days",
121
+ "work from home no experience $500/day",
122
+ "financial freedom in 30 days",
123
+ "easy money from your phone",
124
+ "turn $100 into $10,000",
125
+ "forex signals guaranteed profit",
126
+ "crypto pump group",
127
+ "ponzi scheme",
128
+ "pyramid scheme",
129
+ "make money fast guaranteed",
130
+ "binary options guaranteed",
131
+ "offshore account secret",
132
+ "tax evasion method"
133
+ ]
134
+ }
viral_script_engine/data/viral_templates.json ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "overused_hooks": [
3
+ "pov: you finally figured out",
4
+ "nobody talks about this but",
5
+ "things that are actually red flags",
6
+ "tell me you're x without telling me you're x",
7
+ "as someone who has done x for y years",
8
+ "the reason you're not seeing results is",
9
+ "stop doing x immediately",
10
+ "x things i wish i knew before",
11
+ "wait for it",
12
+ "this changed everything for me",
13
+ "i can't believe i didn't know this sooner",
14
+ "if you're struggling with x watch this",
15
+ "the truth about x nobody tells you",
16
+ "i tried x for 30 days and here's what happened",
17
+ "you've been doing x wrong your whole life",
18
+ "why i quit x after y years",
19
+ "the x that changed my life",
20
+ "how i went from x to y in z days",
21
+ "day x of doing y",
22
+ "let me show you something crazy",
23
+ "here's what they don't want you to know",
24
+ "this is your sign to",
25
+ "can we normalize",
26
+ "unpopular opinion but"
27
+ ],
28
+ "overused_structures": [
29
+ "hook β†’ 3 numbered tips β†’ cta to follow",
30
+ "controversial take β†’ explanation β†’ agree with me?",
31
+ "before and after β†’ what changed β†’ product mention",
32
+ "problem β†’ agitate β†’ solution",
33
+ "story time β†’ lesson learned β†’ apply it",
34
+ "myth busting β†’ real answer β†’ follow for more",
35
+ "countdown from 5 to 1 β†’ reveal at end",
36
+ "reaction to trending topic β†’ opinion β†’ like if you agree",
37
+ "day in my life β†’ relatable moments β†’ subscribe",
38
+ "this or that comparison β†’ winner announced β†’ share",
39
+ "product review β†’ pros cons β†’ buy link in bio",
40
+ "step 1 step 2 step 3 β†’ results revealed β†’ cta",
41
+ "question hook β†’ answer delayed β†’ follow to find out",
42
+ "challenge accepted β†’ process montage β†’ final result",
43
+ "morning routine β†’ productivity tips β†’ follow for inspo",
44
+ "hot take β†’ justification β†’ engage in comments",
45
+ "transformation reveal β†’ how i did it β†’ coaching plug",
46
+ "ask me anything β†’ batch answers β†’ comment more questions",
47
+ "mistakes i made β†’ lessons β†’ save this post",
48
+ "trend recreation β†’ personal twist β†’ duet with me",
49
+ "storytime gossip β†’ drama recap β†’ give your opinion",
50
+ "meet my x β†’ intro β†’ follow for more content"
51
+ ],
52
+ "overused_cta_phrases": [
53
+ "follow for more",
54
+ "save this for later",
55
+ "share with someone who needs this",
56
+ "comment your thoughts below",
57
+ "like if you agree",
58
+ "drop a comment if this helped",
59
+ "hit the follow button",
60
+ "bookmark this",
61
+ "share to your story",
62
+ "tag a friend who needs to hear this",
63
+ "let me know in the comments",
64
+ "smash that like button",
65
+ "turn on notifications",
66
+ "click the link in bio",
67
+ "follow for part 2",
68
+ "share this before it gets removed",
69
+ "comment yes if you want more",
70
+ "send this to your bestie",
71
+ "like and subscribe",
72
+ "save this post it will help you later",
73
+ "follow now don't regret it",
74
+ "repost if you found this useful"
75
+ ],
76
+ "overused_transitions": [
77
+ "but wait there's more",
78
+ "and here's the thing",
79
+ "plot twist",
80
+ "but actually",
81
+ "real talk though",
82
+ "here's where it gets interesting",
83
+ "stay with me",
84
+ "now here's the kicker",
85
+ "this is the part nobody talks about",
86
+ "i know what you're thinking",
87
+ "but before i get into that",
88
+ "and this is the important part",
89
+ "okay so",
90
+ "so basically",
91
+ "long story short",
92
+ "here's the truth",
93
+ "i have to be honest",
94
+ "not gonna lie",
95
+ "the crazy part is",
96
+ "fast forward to",
97
+ "spoiler alert",
98
+ "and that's when it hit me",
99
+ "here's what blew my mind",
100
+ "and guess what happened next"
101
+ ]
102
+ }
viral_script_engine/environment/env.py CHANGED
@@ -11,11 +11,15 @@ from viral_script_engine.environment.episode_state import EpisodeState
11
  from viral_script_engine.environment.observations import (
12
  DebateRound, Observation, RewardComponents,
13
  )
 
 
14
  from viral_script_engine.rewards.r1_hook_strength import HookStrengthReward
15
  from viral_script_engine.rewards.r2_coherence import CoherenceReward
16
  from viral_script_engine.rewards.r3_cultural_alignment import CulturalAlignmentReward
17
  from viral_script_engine.rewards.r4_debate_resolution import DebateResolutionReward
18
  from viral_script_engine.rewards.r5_defender_preservation import DefenderPreservationReward
 
 
19
  from viral_script_engine.rewards.reward_aggregator import RewardAggregator
20
 
21
  _TIERS = {
@@ -59,6 +63,10 @@ class ViralScriptEnv:
59
  self.r3 = CulturalAlignmentReward(knowledge_base_path=cultural_kb_path)
60
  self.r4 = DebateResolutionReward(critic_agent=self.critic)
61
  self.r5 = DefenderPreservationReward()
 
 
 
 
62
  self.aggregator = RewardAggregator()
63
  self._state: Optional[EpisodeState] = None
64
 
@@ -125,10 +133,16 @@ class ViralScriptEnv:
125
  r1_result = self.r1.score(script["script_text"])
126
  r2_result = self.r2.score(script["script_text"], script["script_text"])
127
  r3_result = self.r3.score(script["script_text"], script.get("region", "pan_india_english"))
 
 
 
 
128
  initial_rewards = RewardComponents(
129
  r1_hook_strength=r1_result.score,
130
  r2_coherence=r2_result.score,
131
  r3_cultural_alignment=r3_result.score,
 
 
132
  )
133
  initial_rewards.compute_total()
134
 
@@ -186,12 +200,19 @@ class ViralScriptEnv:
186
 
187
  r5_result = self.r5.score(defender_output, new_script)
188
 
 
 
 
 
 
189
  components = RewardComponents(
190
  r1_hook_strength=r1_result.score,
191
  r2_coherence=r2_result.score,
192
  r3_cultural_alignment=r3_result.score,
193
  r4_debate_resolution=r4_result.score if r4_result else None,
194
  r5_defender_preservation=r5_result.score,
 
 
195
  )
196
 
197
  self._state.action_history.append(arb_action.action_type)
@@ -222,6 +243,8 @@ class ViralScriptEnv:
222
  arbitrator_action=arb_action,
223
  rewrite_diff=rewrite_result.diff,
224
  reward_components=components,
 
 
225
  )
226
  self._state.debate_history.append(round_)
227
  self._state.current_script = new_script
@@ -251,6 +274,8 @@ class ViralScriptEnv:
251
  "anti_gaming_triggered": anti_log.triggered,
252
  "penalty_reason": anti_log.rule_triggered,
253
  "anti_gaming_log": anti_log.model_dump(),
 
 
254
  }
255
  return self._build_observation().model_dump(), components.total, terminated, False, info
256
 
@@ -278,6 +303,13 @@ class ViralScriptEnv:
278
 
279
  def _build_observation(self) -> Observation:
280
  s = self._state
 
 
 
 
 
 
 
281
  return Observation(
282
  current_script=s.current_script,
283
  original_script=s.original_script,
@@ -290,4 +322,6 @@ class ViralScriptEnv:
290
  reward_components=s.last_reward_components,
291
  difficulty_level=s.difficulty_level,
292
  episode_id=s.episode_id,
 
 
293
  )
 
11
  from viral_script_engine.environment.observations import (
12
  DebateRound, Observation, RewardComponents,
13
  )
14
+ from viral_script_engine.agents.moderation_agent import ModerationAgent
15
+ from viral_script_engine.agents.originality_agent import OriginalityAgent
16
  from viral_script_engine.rewards.r1_hook_strength import HookStrengthReward
17
  from viral_script_engine.rewards.r2_coherence import CoherenceReward
18
  from viral_script_engine.rewards.r3_cultural_alignment import CulturalAlignmentReward
19
  from viral_script_engine.rewards.r4_debate_resolution import DebateResolutionReward
20
  from viral_script_engine.rewards.r5_defender_preservation import DefenderPreservationReward
21
+ from viral_script_engine.rewards.r6_safety import SafetyReward
22
+ from viral_script_engine.rewards.r7_originality import OriginalityReward
23
  from viral_script_engine.rewards.reward_aggregator import RewardAggregator
24
 
25
  _TIERS = {
 
63
  self.r3 = CulturalAlignmentReward(knowledge_base_path=cultural_kb_path)
64
  self.r4 = DebateResolutionReward(critic_agent=self.critic)
65
  self.r5 = DefenderPreservationReward()
66
+ self.r6 = SafetyReward()
67
+ self.r7 = OriginalityReward()
68
+ self.moderation_agent = ModerationAgent()
69
+ self.originality_agent = OriginalityAgent()
70
  self.aggregator = RewardAggregator()
71
  self._state: Optional[EpisodeState] = None
72
 
 
133
  r1_result = self.r1.score(script["script_text"])
134
  r2_result = self.r2.score(script["script_text"], script["script_text"])
135
  r3_result = self.r3.score(script["script_text"], script.get("region", "pan_india_english"))
136
+ mod_out = self.moderation_agent.check(script["script_text"])
137
+ orig_out = self.originality_agent.check(script["script_text"])
138
+ r6_result = self.r6.score(mod_out)
139
+ r7_result = self.r7.score(orig_out)
140
  initial_rewards = RewardComponents(
141
  r1_hook_strength=r1_result.score,
142
  r2_coherence=r2_result.score,
143
  r3_cultural_alignment=r3_result.score,
144
+ r6_safety=r6_result.score,
145
+ r7_originality=r7_result.score,
146
  )
147
  initial_rewards.compute_total()
148
 
 
200
 
201
  r5_result = self.r5.score(defender_output, new_script)
202
 
203
+ moderation_out = self.moderation_agent.check(new_script)
204
+ originality_out = self.originality_agent.check(new_script)
205
+ r6_result = self.r6.score(moderation_out)
206
+ r7_result = self.r7.score(originality_out)
207
+
208
  components = RewardComponents(
209
  r1_hook_strength=r1_result.score,
210
  r2_coherence=r2_result.score,
211
  r3_cultural_alignment=r3_result.score,
212
  r4_debate_resolution=r4_result.score if r4_result else None,
213
  r5_defender_preservation=r5_result.score,
214
+ r6_safety=r6_result.score,
215
+ r7_originality=r7_result.score,
216
  )
217
 
218
  self._state.action_history.append(arb_action.action_type)
 
243
  arbitrator_action=arb_action,
244
  rewrite_diff=rewrite_result.diff,
245
  reward_components=components,
246
+ moderation_output=moderation_out.model_dump(),
247
+ originality_output=originality_out.model_dump(),
248
  )
249
  self._state.debate_history.append(round_)
250
  self._state.current_script = new_script
 
274
  "anti_gaming_triggered": anti_log.triggered,
275
  "penalty_reason": anti_log.rule_triggered,
276
  "anti_gaming_log": anti_log.model_dump(),
277
+ "moderation_output": moderation_out.model_dump(),
278
+ "originality_output": originality_out.model_dump(),
279
  }
280
  return self._build_observation().model_dump(), components.total, terminated, False, info
281
 
 
303
 
304
  def _build_observation(self) -> Observation:
305
  s = self._state
306
+ last_round = s.debate_history[-1] if s.debate_history else None
307
+ mod_flags = []
308
+ orig_flags = []
309
+ if last_round and last_round.moderation_output:
310
+ mod_flags = last_round.moderation_output.get("flags", [])
311
+ if last_round and last_round.originality_output:
312
+ orig_flags = last_round.originality_output.get("flags", [])
313
  return Observation(
314
  current_script=s.current_script,
315
  original_script=s.original_script,
 
322
  reward_components=s.last_reward_components,
323
  difficulty_level=s.difficulty_level,
324
  episode_id=s.episode_id,
325
+ current_moderation_flags=mod_flags,
326
+ current_originality_flags=orig_flags,
327
  )
viral_script_engine/environment/observations.py CHANGED
@@ -6,7 +6,8 @@ from viral_script_engine.agents.critic import CritiqueClaim
6
  from viral_script_engine.environment.actions import ArbitratorAction
7
 
8
  _WEIGHTS: Dict[str, float] = {
9
- "r1": 0.25, "r2": 0.20, "r3": 0.20, "r4": 0.20, "r5": 0.15
 
10
  }
11
 
12
 
@@ -16,6 +17,8 @@ class RewardComponents(BaseModel):
16
  r3_cultural_alignment: Optional[float] = None
17
  r4_debate_resolution: Optional[float] = None
18
  r5_defender_preservation: Optional[float] = None
 
 
19
  anti_gaming_penalty: float = 0.0
20
  total: float = 0.0
21
 
@@ -26,6 +29,8 @@ class RewardComponents(BaseModel):
26
  "r3": self.r3_cultural_alignment,
27
  "r4": self.r4_debate_resolution,
28
  "r5": self.r5_defender_preservation,
 
 
29
  }
30
  active = {k: v for k, v in vals.items() if v is not None}
31
  if not active:
@@ -44,6 +49,8 @@ class DebateRound(BaseModel):
44
  arbitrator_action: Optional[ArbitratorAction] = None
45
  rewrite_diff: Optional[str] = None
46
  reward_components: Optional[RewardComponents] = None
 
 
47
 
48
 
49
  class Observation(BaseModel):
@@ -58,3 +65,5 @@ class Observation(BaseModel):
58
  reward_components: RewardComponents
59
  difficulty_level: str
60
  episode_id: str
 
 
 
6
  from viral_script_engine.environment.actions import ArbitratorAction
7
 
8
  _WEIGHTS: Dict[str, float] = {
9
+ "r1": 0.20, "r2": 0.15, "r3": 0.15, "r4": 0.15, "r5": 0.15,
10
+ "r6": 0.10, "r7": 0.10,
11
  }
12
 
13
 
 
17
  r3_cultural_alignment: Optional[float] = None
18
  r4_debate_resolution: Optional[float] = None
19
  r5_defender_preservation: Optional[float] = None
20
+ r6_safety: Optional[float] = None
21
+ r7_originality: Optional[float] = None
22
  anti_gaming_penalty: float = 0.0
23
  total: float = 0.0
24
 
 
29
  "r3": self.r3_cultural_alignment,
30
  "r4": self.r4_debate_resolution,
31
  "r5": self.r5_defender_preservation,
32
+ "r6": self.r6_safety,
33
+ "r7": self.r7_originality,
34
  }
35
  active = {k: v for k, v in vals.items() if v is not None}
36
  if not active:
 
49
  arbitrator_action: Optional[ArbitratorAction] = None
50
  rewrite_diff: Optional[str] = None
51
  reward_components: Optional[RewardComponents] = None
52
+ moderation_output: Optional[Any] = None
53
+ originality_output: Optional[Any] = None
54
 
55
 
56
  class Observation(BaseModel):
 
65
  reward_components: RewardComponents
66
  difficulty_level: str
67
  episode_id: str
68
+ current_moderation_flags: List[Any] = []
69
+ current_originality_flags: List[Any] = []
viral_script_engine/rewards/r6_safety.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+ from viral_script_engine.agents.moderation_agent import ModerationOutput
4
+
5
+
6
+ class SafetyRewardResult(BaseModel):
7
+ score: float
8
+ flag_count: int
9
+ highest_severity: str
10
+ breakdown: str
11
+
12
+
13
+ class SafetyReward:
14
+ """
15
+ Converts ModerationOutput into a reward signal.
16
+
17
+ Scoring:
18
+ - Zero flags: 1.0
19
+ - Only low-severity flags: 0.8
20
+ - Any medium-severity flag: 0.5
21
+ - Any high-severity flag: 0.0 (hard zero β€” non-negotiable per platform policy)
22
+ """
23
+
24
+ def score(self, moderation_output: ModerationOutput) -> SafetyRewardResult:
25
+ if moderation_output.total_flags == 0:
26
+ return SafetyRewardResult(
27
+ score=1.0,
28
+ flag_count=0,
29
+ highest_severity="none",
30
+ breakdown="No flags detected.",
31
+ )
32
+
33
+ severities = [f.severity for f in moderation_output.flags]
34
+ if "high" in severities:
35
+ highest = "high"
36
+ reward = 0.0
37
+ breakdown = "Hard zero: high-severity flag present (platform policy violation)."
38
+ elif "medium" in severities:
39
+ highest = "medium"
40
+ reward = 0.5
41
+ breakdown = f"Medium-severity flags detected ({moderation_output.total_flags} total)."
42
+ else:
43
+ highest = "low"
44
+ reward = 0.8
45
+ breakdown = f"Only low-severity flags detected ({moderation_output.total_flags} total)."
46
+
47
+ return SafetyRewardResult(
48
+ score=reward,
49
+ flag_count=moderation_output.total_flags,
50
+ highest_severity=highest,
51
+ breakdown=breakdown,
52
+ )
viral_script_engine/rewards/r7_originality.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+ from viral_script_engine.agents.originality_agent import OriginalityOutput
4
+
5
+
6
+ class OriginalityRewardResult(BaseModel):
7
+ score: float
8
+ originality_score: float
9
+ flag_count: int
10
+ breakdown: str
11
+
12
+
13
+ class OriginalityReward:
14
+ """
15
+ Converts OriginalityOutput into a reward signal.
16
+
17
+ Scoring maps directly from originality_score:
18
+ - originality_score >= 0.8: reward = 1.0 (genuinely distinctive)
19
+ - originality_score 0.6–0.8: reward = originality_score
20
+ - originality_score 0.4–0.6: reward = 0.3 (mediocre β€” generic but not terrible)
21
+ - originality_score < 0.4: reward = 0.0 (template clone β€” cliff penalty)
22
+ """
23
+
24
+ def score(self, originality_output: OriginalityOutput) -> OriginalityRewardResult:
25
+ os_ = originality_output.originality_score
26
+ flag_count = len(originality_output.flags)
27
+
28
+ if os_ >= 0.8:
29
+ reward = 1.0
30
+ breakdown = f"Highly original (score={os_:.2f}). No dominant template patterns."
31
+ elif os_ >= 0.6:
32
+ reward = os_
33
+ breakdown = f"Moderately original (score={os_:.2f}). Some template overlap detected."
34
+ elif os_ >= 0.4:
35
+ reward = 0.3
36
+ breakdown = f"Generic content (score={os_:.2f}). Multiple overused patterns present."
37
+ else:
38
+ reward = 0.0
39
+ breakdown = f"Template clone (score={os_:.2f}). Script heavily relies on overused formats."
40
+
41
+ return OriginalityRewardResult(
42
+ score=reward,
43
+ originality_score=os_,
44
+ flag_count=flag_count,
45
+ breakdown=breakdown,
46
+ )
viral_script_engine/rewards/reward_aggregator.py CHANGED
@@ -11,6 +11,7 @@ logger = logging.getLogger(__name__)
11
  _COMPONENT_FIELDS = [
12
  "r1_hook_strength", "r2_coherence", "r3_cultural_alignment",
13
  "r4_debate_resolution", "r5_defender_preservation",
 
14
  ]
15
 
16
  _DROP_THRESHOLD = 0.25
@@ -36,6 +37,26 @@ class RewardAggregator:
36
  episode_id: str = "",
37
  step_num: int = 0,
38
  ) -> Tuple[RewardComponents, AntiGamingLog]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  components.compute_total()
40
  pre_penalty_total = components.total
41
 
 
11
  _COMPONENT_FIELDS = [
12
  "r1_hook_strength", "r2_coherence", "r3_cultural_alignment",
13
  "r4_debate_resolution", "r5_defender_preservation",
14
+ "r6_safety", "r7_originality",
15
  ]
16
 
17
  _DROP_THRESHOLD = 0.25
 
37
  episode_id: str = "",
38
  step_num: int = 0,
39
  ) -> Tuple[RewardComponents, AntiGamingLog]:
40
+ # Hard zero: if R6 (safety) is 0.0, the entire step reward is zeroed out
41
+ # regardless of other component scores β€” any shadowban trigger is non-negotiable.
42
+ if components.r6_safety is not None and components.r6_safety == 0.0:
43
+ components.compute_total()
44
+ pre_penalty_total = components.total
45
+ components.total = 0.0
46
+ components.anti_gaming_penalty = 1.0
47
+ logger.warning("R6 safety hard zero triggered β€” shadowban content detected, zeroing step reward.")
48
+ log = AntiGamingLog(
49
+ episode_id=episode_id,
50
+ step_num=step_num,
51
+ triggered=True,
52
+ rule_triggered="r6_safety_hard_zero",
53
+ component_that_dropped="r6_safety",
54
+ penalty_applied=1.0,
55
+ pre_penalty_total=pre_penalty_total,
56
+ post_penalty_total=0.0,
57
+ )
58
+ return components, log
59
+
60
  components.compute_total()
61
  pre_penalty_total = components.total
62
 
viral_script_engine/scripts/run_dummy_episode.py CHANGED
@@ -21,6 +21,11 @@ console = Console()
21
  BASE_DIR = Path(__file__).parent.parent
22
 
23
 
 
 
 
 
 
24
  def build_random_action(action_type: ActionType) -> dict:
25
  labels = {
26
  ActionType.HOOK_REWRITE: ("hook", "Rewrite the hook to open with a specific number or bold claim."),
@@ -48,7 +53,7 @@ def run_episode(difficulty: str, steps: int, verbose: bool) -> dict:
48
  f"Difficulty: {difficulty} | Max steps: {steps}\n"
49
  f"Region: {obs['region']} | Platform: {obs['platform']} | Niche: {obs['niche']}\n"
50
  f"Episode ID: {obs['episode_id']}",
51
- title="[bold blue]Phase 1 Demo Episode[/bold blue]",
52
  border_style="blue",
53
  ))
54
 
@@ -65,22 +70,61 @@ def run_episode(difficulty: str, steps: int, verbose: bool) -> dict:
65
 
66
  obs, reward, terminated, truncated, info = env.step(action)
67
  rc = info["reward_components"]
 
 
68
 
69
  if verbose:
70
  t = Table(title=f"Step {step_num + 1} β€” {action_type.value}", box=box.SIMPLE_HEAD)
71
  t.add_column("Metric", style="cyan", min_width=22)
72
- t.add_column("Value", min_width=12)
73
- r1_val = rc.get("r1_hook_strength")
74
- r2_val = rc.get("r2_coherence")
75
- t.add_row("R1 Hook Strength", f"{r1_val:.3f}" if r1_val is not None else "N/A")
76
- t.add_row("R2 Coherence", f"{r2_val:.3f}" if r2_val is not None else "N/A")
77
- t.add_row("Total Reward", f"[bold]{reward:.3f}[/bold]")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  if info.get("anti_gaming_triggered"):
79
- t.add_row("Anti-Gaming Penalty", f"[red]{rc.get('anti_gaming_penalty', 0):.3f}[/red]")
80
- t.add_row("Penalty Reason", f"[red]{info.get('penalty_reason', '')}[/red]")
81
- t.add_row("Terminated", str(terminated))
82
  console.print(t)
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  if obs.get("debate_history"):
85
  latest = obs["debate_history"][-1]
86
  if latest.get("rewrite_diff"):
@@ -95,6 +139,8 @@ def run_episode(difficulty: str, steps: int, verbose: bool) -> dict:
95
  "action": action,
96
  "reward": reward,
97
  "reward_components": rc,
 
 
98
  "anti_gaming": info.get("anti_gaming_triggered", False),
99
  "terminated": terminated,
100
  })
@@ -108,8 +154,10 @@ def run_episode(difficulty: str, steps: int, verbose: bool) -> dict:
108
 
109
  console.print(Panel(
110
  f"[bold green]Final Reward:[/bold green] {final_rc.get('total', 0):.3f}\n"
111
- f"R1 Hook Strength: {final_rc.get('r1_hook_strength', 'N/A')}\n"
112
- f"R2 Coherence: {final_rc.get('r2_coherence', 'N/A')}\n"
 
 
113
  f"Steps completed: {final_state['step_num']}",
114
  title="Episode Summary",
115
  border_style="green",
@@ -119,7 +167,7 @@ def run_episode(difficulty: str, steps: int, verbose: bool) -> dict:
119
 
120
 
121
  def main():
122
- parser = argparse.ArgumentParser(description="Run Phase 1 dummy episode")
123
  parser.add_argument("--difficulty", default="easy", choices=["easy", "medium", "hard"])
124
  parser.add_argument("--steps", type=int, default=3)
125
  parser.add_argument("--verbose", action="store_true")
@@ -136,12 +184,15 @@ def main():
136
 
137
  final_rc = episode_log["final_state"]["reward_components"]
138
  gate_pass = (
139
- final_rc.get("r1_hook_strength") is not None
140
- and final_rc.get("r2_coherence") is not None
141
  and log_path.exists()
142
  )
143
  style = "bold green" if gate_pass else "bold red"
144
- label = f"PHASE 1 GATE: {'PASS' if gate_pass else 'FAIL'}"
 
 
 
145
  console.print(Panel(f"[{style}]{label}[/{style}]", border_style="green" if gate_pass else "red"))
146
 
147
 
 
21
  BASE_DIR = Path(__file__).parent.parent
22
 
23
 
24
+ def _bar(score: float, width: int = 8) -> str:
25
+ filled = round(score * width)
26
+ return "#" * filled + "." * (width - filled)
27
+
28
+
29
  def build_random_action(action_type: ActionType) -> dict:
30
  labels = {
31
  ActionType.HOOK_REWRITE: ("hook", "Rewrite the hook to open with a specific number or bold claim."),
 
53
  f"Difficulty: {difficulty} | Max steps: {steps}\n"
54
  f"Region: {obs['region']} | Platform: {obs['platform']} | Niche: {obs['niche']}\n"
55
  f"Episode ID: {obs['episode_id']}",
56
+ title="[bold blue]Phase 6 Demo Episode[/bold blue]",
57
  border_style="blue",
58
  ))
59
 
 
70
 
71
  obs, reward, terminated, truncated, info = env.step(action)
72
  rc = info["reward_components"]
73
+ mod_out = info.get("moderation_output", {})
74
+ orig_out = info.get("originality_output", {})
75
 
76
  if verbose:
77
  t = Table(title=f"Step {step_num + 1} β€” {action_type.value}", box=box.SIMPLE_HEAD)
78
  t.add_column("Metric", style="cyan", min_width=22)
79
+ t.add_column("Score", min_width=12)
80
+ t.add_column("Bar", min_width=10)
81
+
82
+ def _row(label, key, suffix=""):
83
+ val = rc.get(key)
84
+ score_str = f"{val:.3f}" if val is not None else "N/A"
85
+ bar_str = _bar(val) if val is not None else ""
86
+ t.add_row(label, score_str + suffix, bar_str)
87
+
88
+ _row("R1 Hook Strength", "r1_hook_strength")
89
+ _row("R2 Coherence", "r2_coherence")
90
+ _row("R3 Cultural", "r3_cultural_alignment")
91
+ _row("R4 Resolution", "r4_debate_resolution")
92
+ _row("R5 Preservation", "r5_defender_preservation")
93
+
94
+ r6_val = rc.get("r6_safety")
95
+ r6_suffix = " [OK] No flags" if mod_out.get("total_flags", 0) == 0 else f" [!] {mod_out.get('total_flags', 0)} flag(s)"
96
+ r6_str = (f"{r6_val:.3f}{r6_suffix}" if r6_val is not None else "N/A")
97
+ t.add_row("R6 Safety", r6_str, _bar(r6_val) if r6_val is not None else "")
98
+
99
+ r7_val = rc.get("r7_originality")
100
+ orig_flags = len(orig_out.get("flags", []))
101
+ r7_suffix = f" [!] {orig_flags} template match(es)" if orig_flags > 0 else " [OK] Original"
102
+ r7_str = (f"{r7_val:.3f}{r7_suffix}" if r7_val is not None else "N/A")
103
+ t.add_row("R7 Originality", r7_str, _bar(r7_val) if r7_val is not None else "")
104
+
105
+ t.add_row("-" * 22, "-" * 12, "-" * 10)
106
+ t.add_row("[bold]Total[/bold]", f"[bold]{reward:.3f}[/bold]", _bar(reward))
107
+
108
  if info.get("anti_gaming_triggered"):
109
+ t.add_row("Anti-Gaming Penalty", f"[red]{rc.get('anti_gaming_penalty', 0):.3f}[/red]", "")
110
+ t.add_row("Penalty Reason", f"[red]{info.get('penalty_reason', '')}[/red]", "")
111
+ t.add_row("Terminated", str(terminated), "")
112
  console.print(t)
113
 
114
+ # Show moderation flags in red panel if any
115
+ mod_flags = mod_out.get("flags", [])
116
+ if mod_flags:
117
+ flag_lines = []
118
+ for fl in mod_flags:
119
+ flag_lines.append(
120
+ f" [{fl['severity']}] {fl['category']} in {fl['position']}: \"{fl['trigger_phrase']}\" β†’ {fl['suggestion']}"
121
+ )
122
+ console.print(Panel(
123
+ "\n".join(flag_lines),
124
+ title="[bold red]!! MODERATION FLAGS DETECTED[/bold red]",
125
+ border_style="red",
126
+ ))
127
+
128
  if obs.get("debate_history"):
129
  latest = obs["debate_history"][-1]
130
  if latest.get("rewrite_diff"):
 
139
  "action": action,
140
  "reward": reward,
141
  "reward_components": rc,
142
+ "moderation_output": mod_out,
143
+ "originality_output": orig_out,
144
  "anti_gaming": info.get("anti_gaming_triggered", False),
145
  "terminated": terminated,
146
  })
 
154
 
155
  console.print(Panel(
156
  f"[bold green]Final Reward:[/bold green] {final_rc.get('total', 0):.3f}\n"
157
+ f"R1 Hook Strength: {final_rc.get('r1_hook_strength', 'N/A')}\n"
158
+ f"R2 Coherence: {final_rc.get('r2_coherence', 'N/A')}\n"
159
+ f"R6 Safety: {final_rc.get('r6_safety', 'N/A')}\n"
160
+ f"R7 Originality: {final_rc.get('r7_originality', 'N/A')}\n"
161
  f"Steps completed: {final_state['step_num']}",
162
  title="Episode Summary",
163
  border_style="green",
 
167
 
168
 
169
  def main():
170
+ parser = argparse.ArgumentParser(description="Run Phase 6 dummy episode")
171
  parser.add_argument("--difficulty", default="easy", choices=["easy", "medium", "hard"])
172
  parser.add_argument("--steps", type=int, default=3)
173
  parser.add_argument("--verbose", action="store_true")
 
184
 
185
  final_rc = episode_log["final_state"]["reward_components"]
186
  gate_pass = (
187
+ final_rc.get("r6_safety") is not None
188
+ and final_rc.get("r7_originality") is not None
189
  and log_path.exists()
190
  )
191
  style = "bold green" if gate_pass else "bold red"
192
+ if gate_pass:
193
+ label = "PHASE 6 GATE: PASS β€” R6 (safety) and R7 (originality) active. Total reward components: 7."
194
+ else:
195
+ label = "PHASE 6 GATE: FAIL β€” R6 or R7 missing from reward output."
196
  console.print(Panel(f"[{style}]{label}[/{style}]", border_style="green" if gate_pass else "red"))
197
 
198
 
viral_script_engine/tests/test_phase6.py ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Phase 6 tests: ModerationAgent, OriginalityAgent, R6/R7 rewards, aggregator, env.step()."""
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+
7
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
8
+
9
+ from viral_script_engine.agents.moderation_agent import ModerationAgent, ModerationOutput
10
+ from viral_script_engine.agents.originality_agent import OriginalityAgent, OriginalityOutput
11
+ from viral_script_engine.rewards.r6_safety import SafetyReward
12
+ from viral_script_engine.rewards.r7_originality import OriginalityReward
13
+ from viral_script_engine.rewards.reward_aggregator import RewardAggregator
14
+ from viral_script_engine.environment.observations import RewardComponents
15
+
16
+ BASE_DIR = Path(__file__).parent.parent
17
+
18
+
19
+ @pytest.fixture
20
+ def moderation_agent():
21
+ return ModerationAgent(kb_path=str(BASE_DIR / "data" / "shadowban_triggers.json"))
22
+
23
+
24
+ @pytest.fixture
25
+ def originality_agent():
26
+ return OriginalityAgent(templates_path=str(BASE_DIR / "data" / "viral_templates.json"))
27
+
28
+
29
+ @pytest.fixture
30
+ def r6():
31
+ return SafetyReward()
32
+
33
+
34
+ @pytest.fixture
35
+ def r7():
36
+ return OriginalityReward()
37
+
38
+
39
+ # ── ModerationAgent ────────────────────────────────────────────────────────────
40
+
41
+ class TestModerationAgent:
42
+ def test_flags_high_severity_health_claim(self, moderation_agent):
43
+ script = (
44
+ "Doctors don't want you to know this miracle remedy. "
45
+ "It is a guaranteed weight loss solution that cures everything. "
46
+ "Get started today."
47
+ )
48
+ result = moderation_agent.check(script)
49
+ assert result.total_flags > 0
50
+ severities = [f.severity for f in result.flags]
51
+ assert "high" in severities
52
+
53
+ def test_flags_policy_violation(self, moderation_agent):
54
+ script = (
55
+ "Want to grow your audience fast? Buy followers today. "
56
+ "Make $5000 in 30 days guaranteed β€” it's the easiest passive income. "
57
+ "Get rich quick with our proven method."
58
+ )
59
+ result = moderation_agent.check(script)
60
+ assert result.total_flags > 0
61
+ categories = [f.category for f in result.flags]
62
+ assert "policy_violation" in categories
63
+
64
+ def test_flags_engagement_bait(self, moderation_agent):
65
+ script = (
66
+ "Here's something nobody talks about in fitness. "
67
+ "These 3 exercises changed my life completely. "
68
+ "Comment if you agree and tag 3 friends who need this."
69
+ )
70
+ result = moderation_agent.check(script)
71
+ assert result.total_flags > 0
72
+
73
+ def test_clean_script_is_safe(self, moderation_agent):
74
+ script = (
75
+ "I tested three different morning routines for 30 days each. "
76
+ "The data surprised me β€” the simplest one produced the best results. "
77
+ "Here's what I found and what I now do every day."
78
+ )
79
+ result = moderation_agent.check(script)
80
+ assert result.is_safe is True
81
+ assert result.overall_risk == "safe"
82
+
83
+ def test_high_severity_flag_sets_is_safe_false(self, moderation_agent):
84
+ script = (
85
+ "This miracle remedy cures cancer permanently. "
86
+ "Big pharma is hiding this from you. "
87
+ "Share before it gets taken down."
88
+ )
89
+ result = moderation_agent.check(script)
90
+ assert result.is_safe is False
91
+
92
+
93
+ # ── SafetyReward ───────────────────────────────────────────────────────────────
94
+
95
+ class TestSafetyReward:
96
+ def test_zero_score_on_high_severity(self, moderation_agent, r6):
97
+ script = (
98
+ "This miracle remedy cures all diseases overnight. "
99
+ "Buy followers now to grow faster. "
100
+ "Make $10,000 in 30 days guaranteed."
101
+ )
102
+ mod_out = moderation_agent.check(script)
103
+ result = r6.score(mod_out)
104
+ assert result.score == 0.0
105
+
106
+ def test_full_score_on_clean_script(self, moderation_agent, r6):
107
+ script = (
108
+ "I spent 6 months studying sleep science. "
109
+ "These 3 evidence-based habits improved my sleep quality measurably. "
110
+ "Start with the simplest one first."
111
+ )
112
+ mod_out = moderation_agent.check(script)
113
+ result = r6.score(mod_out)
114
+ assert result.score == 1.0
115
+
116
+ def test_medium_score_on_medium_severity(self, moderation_agent, r6):
117
+ script = (
118
+ "This app is a must-have. "
119
+ "Watch the full movie version for free on our site. "
120
+ "Click the link below for the download."
121
+ )
122
+ mod_out = moderation_agent.check(script)
123
+ result = r6.score(mod_out)
124
+ # medium or high severity β€” score should be <= 0.5
125
+ assert result.score <= 0.5
126
+
127
+
128
+ # ── OriginalityAgent ────────────��──────────────────────────────────────────────
129
+
130
+ class TestOriginalityAgent:
131
+ def test_detects_overused_hook(self, originality_agent):
132
+ script = (
133
+ "Nobody talks about this but your morning routine is wrong. "
134
+ "Here are three things I wish I knew before starting. "
135
+ "Follow for more tips."
136
+ )
137
+ result = originality_agent.check(script)
138
+ assert len(result.flags) > 0
139
+ template_types = [f.template_type for f in result.flags]
140
+ assert any(t in ("overused_hook", "overused_cta") for t in template_types)
141
+
142
+ def test_unique_script_scores_high(self, originality_agent):
143
+ script = (
144
+ "In 2019, the average Indian millennial checked their phone 94 times a day. "
145
+ "I tracked my own usage for a month and found a pattern nobody warned me about. "
146
+ "The solution had nothing to do with willpower."
147
+ )
148
+ result = originality_agent.check(script)
149
+ assert result.originality_score >= 0.8
150
+
151
+ def test_template_clone_is_generic(self, originality_agent):
152
+ script = (
153
+ "Nobody talks about this but you have been doing it wrong your whole life. "
154
+ "Stop doing this immediately and save this for later. "
155
+ "Follow for more and share with someone who needs this."
156
+ )
157
+ result = originality_agent.check(script)
158
+ assert result.is_generic is True or result.originality_score < 0.6
159
+
160
+
161
+ # ── OriginalityReward ──────────────────────────────────────────────────────────
162
+
163
+ class TestOriginalityReward:
164
+ def test_zero_score_on_template_clone(self, originality_agent, r7):
165
+ script = (
166
+ "Nobody talks about this but you have been doing it wrong your whole life. "
167
+ "Stop doing this immediately and save this for later. "
168
+ "Follow for more and share with someone who needs this."
169
+ )
170
+ orig_out = originality_agent.check(script)
171
+ # Force a low originality_score scenario
172
+ from viral_script_engine.agents.originality_agent import OriginalityOutput
173
+ low_out = OriginalityOutput(
174
+ flags=orig_out.flags,
175
+ originality_score=0.2,
176
+ is_generic=True,
177
+ unique_elements=[],
178
+ )
179
+ result = r7.score(low_out)
180
+ assert result.score == 0.0
181
+
182
+ def test_full_score_on_high_originality(self, originality_agent, r7):
183
+ from viral_script_engine.agents.originality_agent import OriginalityOutput
184
+ high_out = OriginalityOutput(
185
+ flags=[],
186
+ originality_score=0.95,
187
+ is_generic=False,
188
+ unique_elements=["unique sentence 1", "unique sentence 2"],
189
+ )
190
+ result = r7.score(high_out)
191
+ assert result.score == 1.0
192
+
193
+
194
+ # ── RewardAggregator with R6/R7 ────────────────────────────────────────────────
195
+
196
+ class TestRewardAggregatorPhase6:
197
+ def test_r6_r7_included_in_total(self):
198
+ agg = RewardAggregator()
199
+ components = RewardComponents(
200
+ r1_hook_strength=0.8,
201
+ r2_coherence=0.7,
202
+ r3_cultural_alignment=0.75,
203
+ r4_debate_resolution=0.6,
204
+ r5_defender_preservation=0.7,
205
+ r6_safety=1.0,
206
+ r7_originality=0.9,
207
+ )
208
+ start = RewardComponents(
209
+ r1_hook_strength=0.5,
210
+ r2_coherence=0.5,
211
+ r3_cultural_alignment=0.5,
212
+ r4_debate_resolution=0.5,
213
+ r5_defender_preservation=0.5,
214
+ r6_safety=1.0,
215
+ r7_originality=0.9,
216
+ )
217
+ result, log = agg.compute(components, start, [], episode_id="test", step_num=1)
218
+ assert result.total > 0.0
219
+ assert not log.triggered
220
+
221
+ def test_catastrophic_drop_fires_on_r6_zero(self):
222
+ agg = RewardAggregator()
223
+ components = RewardComponents(
224
+ r1_hook_strength=0.8,
225
+ r2_coherence=0.7,
226
+ r3_cultural_alignment=0.75,
227
+ r4_debate_resolution=0.6,
228
+ r5_defender_preservation=0.7,
229
+ r6_safety=0.0,
230
+ r7_originality=0.9,
231
+ )
232
+ start = RewardComponents(
233
+ r1_hook_strength=0.8,
234
+ r2_coherence=0.7,
235
+ r3_cultural_alignment=0.75,
236
+ r4_debate_resolution=0.6,
237
+ r5_defender_preservation=0.7,
238
+ r6_safety=1.0,
239
+ r7_originality=0.9,
240
+ )
241
+ result, log = agg.compute(components, start, [], episode_id="test", step_num=1)
242
+ assert result.total == 0.0
243
+ assert log.triggered
244
+ assert log.rule_triggered == "r6_safety_hard_zero"
245
+
246
+
247
+ # ── env.step() integration ──────────────────────────���──────────────────────────
248
+
249
+ class TestEnvStepPhase6:
250
+ def test_step_includes_moderation_and_originality(self, monkeypatch):
251
+ from unittest.mock import MagicMock
252
+ from viral_script_engine.environment.env import ViralScriptEnv
253
+ from viral_script_engine.agents.critic import CritiqueOutput, CritiqueClaim
254
+ from viral_script_engine.agents.defender import DefenderOutput
255
+ from viral_script_engine.agents.rewriter import RewriteResult
256
+
257
+ env = ViralScriptEnv(
258
+ scripts_path=str(BASE_DIR / "data" / "test_scripts" / "scripts.json"),
259
+ max_steps=1,
260
+ difficulty="easy",
261
+ use_escalation=False,
262
+ )
263
+
264
+ dummy_claim = CritiqueClaim(
265
+ claim_id="C1",
266
+ critique_class="hook_weakness",
267
+ claim_text="Hook is weak",
268
+ timestamp_range="0-3s",
269
+ evidence="Opening is vague",
270
+ is_falsifiable=True,
271
+ severity="medium",
272
+ )
273
+ dummy_critique = CritiqueOutput(
274
+ claims=[dummy_claim],
275
+ overall_severity="medium",
276
+ raw_response="Hook is weak",
277
+ )
278
+ dummy_defender = DefenderOutput(
279
+ core_strength="The hook has genuine curiosity value.",
280
+ core_strength_quote="First sentence",
281
+ defense_argument="The structure is sound; only specificity needs improvement.",
282
+ flagged_critic_claims=["C1"],
283
+ regional_voice_elements=["regional phrase"],
284
+ )
285
+ dummy_rewrite = RewriteResult(
286
+ rewritten_script="3 things nobody tells you about morning routines that actually work.",
287
+ diff="- Old hook\n+ 3 things nobody tells you about morning routines that actually work.",
288
+ word_count_delta=2,
289
+ )
290
+
291
+ from viral_script_engine.rewards.r4_debate_resolution import DebateResolutionResult
292
+ dummy_r4 = DebateResolutionResult(
293
+ score=0.7,
294
+ resolution_status="resolved",
295
+ original_claim_id="C1",
296
+ original_claim_class="hook_weakness",
297
+ new_claims_count=0,
298
+ )
299
+
300
+ monkeypatch.setattr(env.critic, "critique", lambda *a, **kw: dummy_critique)
301
+ monkeypatch.setattr(env.defender, "defend", lambda **kw: dummy_defender)
302
+ monkeypatch.setattr(env.rewriter, "rewrite", lambda script, action: dummy_rewrite)
303
+ monkeypatch.setattr(env.r4, "score", lambda **kw: dummy_r4)
304
+
305
+ env.reset()
306
+ action = {
307
+ "action_type": "hook_rewrite",
308
+ "target_section": "hook",
309
+ "instruction": "Make the hook more engaging.",
310
+ "critique_claim_id": "C1",
311
+ "reasoning": "test",
312
+ }
313
+ _, _, _, _, info = env.step(action)
314
+ assert "moderation_output" in info
315
+ assert "originality_output" in info
316
+ rc = info["reward_components"]
317
+ assert rc.get("r6_safety") is not None
318
+ assert rc.get("r7_originality") is not None