vajeeda's picture
Phase 7 implemented
0e4f105
Raw
History Blame Contribute Delete
15.4 kB
"""
Phase 7 tests — Process-Aware Reward Shaping
"""
import json
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from viral_script_engine.agents.critic import CritiqueClaim
from viral_script_engine.agents.defender import DefenderOutput
from viral_script_engine.agents.reasoning_parser import (
ArbitratorParseError,
ReasoningChain,
ReasoningParser,
)
from viral_script_engine.environment.actions import ArbitratorAction, ActionType
from viral_script_engine.environment.observations import RewardComponents
from viral_script_engine.rewards.process_reward import ProcessReward, ProcessRewardResult
from viral_script_engine.rewards.process_verifier import ProcessVerifier
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def parser():
return ReasoningParser()
@pytest.fixture
def verifier():
return ProcessVerifier()
@pytest.fixture
def process_reward():
return ProcessReward()
def _make_claim(claim_id: str, critique_class: str, severity: str) -> CritiqueClaim:
return CritiqueClaim(
claim_id=claim_id,
critique_class=critique_class,
claim_text=f"Test claim for {critique_class}",
timestamp_range="0:00-0:05",
evidence="test evidence",
is_falsifiable=True,
severity=severity,
)
def _make_action(
action_type: str = "hook_rewrite",
target_section: str = "hook",
) -> ArbitratorAction:
return ArbitratorAction(
action_type=action_type,
target_section=target_section,
instruction="Test instruction",
critique_claim_id="C1",
reasoning="Test reasoning",
)
def _make_defender(core_strength_quote: str = "The hook is strong and engaging") -> DefenderOutput:
return DefenderOutput(
core_strength="Great opening hook",
core_strength_quote=core_strength_quote,
defense_argument="This element should be preserved",
flagged_critic_claims=["C2"],
regional_voice_elements=["local phrase"],
)
def _make_components(**kwargs) -> RewardComponents:
rc = RewardComponents(**kwargs)
rc.compute_total()
return rc
# ---------------------------------------------------------------------------
# ReasoningParser tests
# ---------------------------------------------------------------------------
_FULL_JSON = json.dumps({
"priority_assessment": "hook_weakness is highest severity (high) — opens weakly",
"conflict_check": "yes — hook rewrite risks R3 cultural alignment",
"defender_consideration": "yes — core strength is in hook section",
"action_type": "hook_rewrite",
"target_section": "hook",
"instruction": "Replace generic opener with Mumbai local reference",
"critique_claim_id": "C1",
"reasoning": "Hook is highest severity, must be fixed first",
})
_MINIMAL_JSON = json.dumps({
"action_type": "hook_rewrite",
"target_section": "hook",
"instruction": "Fix the hook",
"critique_claim_id": "C1",
"reasoning": "default",
})
def test_reasoning_parser_full_json(parser):
chain = parser.parse(_FULL_JSON)
assert isinstance(chain, ReasoningChain)
assert "hook_weakness" in chain.priority_assessment
assert chain.conflict_check_answer == "yes"
assert chain.defender_consideration_answer == "yes"
assert chain.action.action_type == ActionType.HOOK_REWRITE
def test_reasoning_parser_fallback_missing_reasoning(parser):
"""Baseline model output without reasoning fields should parse without error."""
chain = parser.parse(_MINIMAL_JSON)
assert chain.priority_assessment == ""
assert chain.conflict_check_answer == ""
assert chain.defender_consideration_answer == ""
assert chain.action.action_type == ActionType.HOOK_REWRITE
def test_reasoning_parser_raises_on_invalid_action(parser):
bad_json = json.dumps({"action_type": "invalid_action", "target_section": "hook"})
with pytest.raises(ArbitratorParseError):
parser.parse(bad_json)
def test_reasoning_parser_raises_on_missing_action(parser):
bad_json = json.dumps({"priority_assessment": "something"})
with pytest.raises(ArbitratorParseError):
parser.parse(bad_json)
# ---------------------------------------------------------------------------
# ProcessVerifier.verify_priority_assessment tests
# ---------------------------------------------------------------------------
def test_verify_priority_high_severity_mention(verifier):
claims = [
_make_claim("C1", "hook_weakness", "high"),
_make_claim("C2", "pacing_issue", "medium"),
_make_claim("C3", "cta_buried", "low"),
]
rc = _make_components(r1_hook_strength=0.5)
score = verifier.verify_priority_assessment(
priority_assessment="hook_weakness is the most urgent issue",
critic_claims=claims,
current_reward_components=rc,
)
assert score == 1.0
def test_verify_priority_medium_severity_mention(verifier):
claims = [
_make_claim("C1", "hook_weakness", "high"),
_make_claim("C2", "pacing_issue", "medium"),
]
rc = _make_components(r1_hook_strength=0.5)
score = verifier.verify_priority_assessment(
priority_assessment="pacing_issue should be addressed",
critic_claims=claims,
current_reward_components=rc,
)
assert score == 0.5
def test_verify_priority_random_mention_scores_zero(verifier):
claims = [
_make_claim("C1", "hook_weakness", "high"),
_make_claim("C2", "pacing_issue", "medium"),
]
rc = _make_components(r1_hook_strength=0.5)
score = verifier.verify_priority_assessment(
priority_assessment="we should just make this better",
critic_claims=claims,
current_reward_components=rc,
)
assert score == 0.0
def test_verify_priority_empty_assessment(verifier):
claims = [_make_claim("C1", "hook_weakness", "high")]
rc = _make_components()
score = verifier.verify_priority_assessment("", claims, rc)
assert score == 0.0
# ---------------------------------------------------------------------------
# ProcessVerifier.verify_conflict_check tests — all 4 known patterns
# ---------------------------------------------------------------------------
def test_conflict_check_hook_rewrite_with_high_r3(verifier):
action = _make_action("hook_rewrite", "hook")
start = _make_components(r1_hook_strength=0.6, r3_cultural_alignment=0.75)
current = _make_components(r1_hook_strength=0.6, r3_cultural_alignment=0.80)
# r3 >= 0.7 → conflict exists → correct answer is "yes"
score = verifier.verify_conflict_check("yes — hook rewrite risks cultural refs", "", action, current, start)
assert score == 1.0
score_wrong = verifier.verify_conflict_check("no — no conflict", "", action, current, start)
assert score_wrong == 0.0
def test_conflict_check_section_reorder_with_low_r2(verifier):
action = _make_action("section_reorder", "body")
start = _make_components(r2_coherence=0.5)
current = _make_components(r2_coherence=0.5)
# r2 <= 0.6 → conflict exists
score = verifier.verify_conflict_check("yes", "", action, current, start)
assert score == 1.0
def test_conflict_check_cultural_ref_sub_with_low_r5(verifier):
action = _make_action("cultural_ref_sub", "full")
start = _make_components(r5_defender_preservation=0.4)
current = _make_components(r5_defender_preservation=0.4)
# r5 <= 0.5 → conflict exists
score = verifier.verify_conflict_check("yes", "", action, current, start)
assert score == 1.0
def test_conflict_check_cta_placement_with_low_r1(verifier):
action = _make_action("cta_placement", "cta")
start = _make_components(r1_hook_strength=0.3)
current = _make_components(r1_hook_strength=0.3)
# r1 <= 0.4 → conflict exists
score = verifier.verify_conflict_check("yes — CTA premature while hook is weak", "", action, current, start)
assert score == 1.0
score_wrong = verifier.verify_conflict_check("no conflict detected", "", action, current, start)
assert score_wrong == 0.0
def test_conflict_check_no_conflict_scenario(verifier):
# hook_rewrite when r3 < 0.7 → no conflict → correct answer is "no"
action = _make_action("hook_rewrite", "hook")
start = _make_components(r3_cultural_alignment=0.5)
current = _make_components(r3_cultural_alignment=0.5)
score = verifier.verify_conflict_check("no — r3 is low, no conflict", "", action, current, start)
assert score == 1.0
# ---------------------------------------------------------------------------
# ProcessVerifier.verify_defender_consideration tests
# ---------------------------------------------------------------------------
def test_defender_consideration_yes_when_core_in_target(verifier):
# Core strength is in hook, action targets hook → should say yes
action = _make_action("hook_rewrite", "hook")
defender = _make_defender(core_strength_quote="The opening hook draws viewers immediately")
score = verifier.verify_defender_consideration("yes", "", action, defender)
assert score == 1.0
def test_defender_consideration_no_when_core_not_in_target(verifier):
# Core strength is in CTA section, action targets hook → should say no
action = _make_action("hook_rewrite", "hook")
defender = _make_defender(core_strength_quote="The ending call to action is very strong")
score = verifier.verify_defender_consideration("no", "", action, defender)
assert score == 1.0
def test_defender_consideration_wrong_answer_scores_zero(verifier):
action = _make_action("hook_rewrite", "hook")
defender = _make_defender(core_strength_quote="The opening hook draws viewers immediately")
score = verifier.verify_defender_consideration("no — no overlap", "", action, defender)
assert score == 0.0
def test_defender_consideration_empty_answer(verifier):
action = _make_action("hook_rewrite", "hook")
defender = _make_defender()
score = verifier.verify_defender_consideration("", "", action, defender)
assert score == 0.0
# ---------------------------------------------------------------------------
# ProcessReward.score() weighted total
# ---------------------------------------------------------------------------
def test_process_reward_correct_weighted_total(process_reward):
claims = [
_make_claim("C1", "hook_weakness", "high"),
_make_claim("C2", "pacing_issue", "medium"),
]
defender = _make_defender(core_strength_quote="The opening hook draws viewers immediately")
rc = _make_components(r1_hook_strength=0.5, r2_coherence=0.5, r3_cultural_alignment=0.8)
start = _make_components(r1_hook_strength=0.5, r2_coherence=0.5, r3_cultural_alignment=0.8)
chain = ReasoningChain(
priority_assessment="hook_weakness is highest severity",
conflict_check_answer="yes",
conflict_check_reason="hook rewrite risks r3",
defender_consideration_answer="yes",
defender_consideration_reason="core strength is in hook",
action=_make_action("hook_rewrite", "hook"),
)
result = process_reward.score(chain, claims, defender, rc, start)
assert isinstance(result, ProcessRewardResult)
# All three checks should score 1.0 → process_score = 1.0, contribution = 0.15
assert result.priority_score == 1.0
assert result.conflict_score == 1.0 # hook_rewrite + r3 >= 0.7 → conflict, model says yes
assert result.defender_score == 1.0
assert abs(result.process_score - 1.0) < 1e-6
assert abs(result.weighted_contribution - 0.15) < 1e-6
def test_process_reward_zero_for_empty_reasoning(process_reward):
claims = [_make_claim("C1", "hook_weakness", "high")]
defender = _make_defender()
rc = _make_components(r1_hook_strength=0.5)
start = _make_components(r1_hook_strength=0.5)
chain = ReasoningChain(
priority_assessment="",
conflict_check_answer="",
conflict_check_reason="",
defender_consideration_answer="",
defender_consideration_reason="",
action=_make_action("hook_rewrite", "hook"),
)
result = process_reward.score(chain, claims, defender, rc, start)
assert result.process_score == 0.0
assert result.weighted_contribution == 0.0
# ---------------------------------------------------------------------------
# env.step() integration — process_reward in RewardComponents
# ---------------------------------------------------------------------------
_ACTION = {
"action_type": "hook_rewrite",
"target_section": "hook",
"instruction": "Make the hook more engaging.",
"critique_claim_id": "C1",
"reasoning": "Test",
}
_SCRIPTS_PATH = str(Path(__file__).parent.parent / "data" / "test_scripts" / "scripts.json")
_CULTURAL_KB = str(Path(__file__).parent.parent / "data" / "cultural_kb.json")
_MOCK_CRITIC = json.dumps({
"claims": [
{
"claim_id": "C1",
"critique_class": "hook_weakness",
"claim_text": "Weak hook.",
"timestamp_range": "0:00-0:03",
"evidence": "generic opener",
"is_falsifiable": True,
"severity": "high",
}
],
"overall_severity": "high",
})
_MOCK_DEFENDER = json.dumps({
"core_strength": "Strong regional authenticity",
"core_strength_quote": "The hook draws viewers immediately",
"defense_argument": "Regional voice is valuable",
"flagged_critic_claims": [],
"regional_voice_elements": ["local phrase"],
})
_MOCK_REWRITER = json.dumps({
"rewritten_script": "Better script content here.",
"changes_made": ["improved hook"],
})
def _multi_mock(sys_prompt, usr_prompt, **kw):
"""Return appropriate mock JSON based on which agent is calling."""
if "core_strength" in sys_prompt or "defender" in sys_prompt.lower():
return _MOCK_DEFENDER
if "rewriter" in sys_prompt.lower() or "rewrite" in sys_prompt.lower()[:50]:
return _MOCK_REWRITER
return _MOCK_CRITIC
@pytest.fixture
def env_mock_llm(monkeypatch):
monkeypatch.setattr(
"viral_script_engine.agents.llm_backend.LLMBackend.generate",
lambda self, sys_prompt, usr_prompt, **kw: _multi_mock(sys_prompt, usr_prompt, **kw),
)
def _make_env():
from viral_script_engine.environment.env import ViralScriptEnv
return ViralScriptEnv(
scripts_path=_SCRIPTS_PATH,
cultural_kb_path=_CULTURAL_KB,
max_steps=1,
difficulty="easy",
use_escalation=False,
)
def test_env_step_has_process_reward_key(env_mock_llm):
"""env.step() must include process_reward key in reward_components."""
env = _make_env()
env.reset()
_, _, _, _, info = env.step(_ACTION)
rc = info["reward_components"]
assert "process_reward" in rc
def test_env_step_process_reward_graceful_zero(env_mock_llm):
"""process_reward is None when no raw_output is supplied (graceful zero)."""
env = _make_env()
env.reset()
_, _, _, _, info = env.step(_ACTION) # no raw_output
rc = info["reward_components"]
assert rc.get("process_reward") is None
assert info.get("process_reward_result") is None