Spaces:
Sleeping
Sleeping
File size: 15,393 Bytes
0e4f105 | 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 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 | """
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
|