"""Multi-component reward, coherence, optional LLM judge, and anti-gaming penalties.""" from __future__ import annotations import json import os import re from typing import Any, Callable, Dict, List, Optional from openai import OpenAI from server.llm_env import openai_client_kwargs_judge from server.swd import ( REQUIRED_TOP_LEVEL, VALID_PHASES, conflict_ids_from_swd, validate_milestone_shapes, ) from server.verifiers import is_verbatim_copy def compute_swd_coherence(swd: Dict[str, Any]) -> float: """Deterministic structural coherence score in [0, 1].""" checks: List[bool] = [] checks.append(all(k in swd for k in REQUIRED_TOP_LEVEL)) checks.append(swd.get("phase") in VALID_PHASES) checks.append(validate_milestone_shapes(swd)) conflict_ids = conflict_ids_from_swd(swd) res_ok = True for r in swd.get("conflict_resolutions", []) or []: if isinstance(r, dict) and r.get("conflict_id") is not None: if r["conflict_id"] not in conflict_ids: res_ok = False break checks.append(res_ok) v = swd.get("swd_version") checks.append(isinstance(v, int) and v >= 1) log = swd.get("reasoning_log", []) or [] checks.append( all(isinstance(e, dict) and "turn" in e for e in log) if log else True ) if not checks: return 0.0 return sum(1 for c in checks if c) / len(checks) def call_llm_judge(swd: Dict[str, Any], task_goal: str) -> float: """ Fast LLM judge (optional). Returns score in [0, 1] from YES count / 3. Uses CORP_JUDGE_* then global API keys (see server/llm_env.py). No call without a key. """ if os.getenv("CORP_DISABLE_LLM_JUDGE", "").lower() in ("1", "true", "yes"): return 0.0 kwargs = openai_client_kwargs_judge() if not kwargs.get("api_key"): return 0.0 model = os.getenv("CORP_JUDGE_MODEL", "Qwen/Qwen2.5-7B-Instruct") prompt = f""" You are evaluating a corporate decision document. Answer each question with YES or NO only. DOCUMENT: {json.dumps(swd, indent=2)[:3000]} TASK GOAL: {task_goal} QUESTIONS: 1. Does the final_recommendation address all three key stakeholder concerns present in the scenario? 2. Are the conflict_resolutions logically consistent with the agent_reports provided? 3. Does the reasoning_log show evidence of iterative thinking (not just a single dump)? Respond in this exact format: Q1: YES/NO Q2: YES/NO Q3: YES/NO """ client = OpenAI(**kwargs) resp = client.chat.completions.create( model=model, messages=[{"role": "user", "content": prompt}], max_tokens=64, temperature=0.0, ) text = (resp.choices[0].message.content or "").strip() yes_count = len(re.findall(r"Q\d:\s*YES", text, re.I)) return yes_count / 3.0 def reasoning_log_is_duplicated(swd: Dict[str, Any]) -> bool: log = swd.get("reasoning_log", []) or [] if len(log) < 2: return False texts = [str(e.get("text", e)) for e in log if isinstance(e, dict)] if len(texts) >= 6 and len(set(texts)) < len(texts) * 0.3: return True return False REWARD_HACKING_PENALTIES: List[Callable[[Dict[str, Any], Dict[str, Any]], float]] = [ lambda swd, ep: 0.3 if ep.get("finalized") and int(swd.get("swd_version", 0)) < 4 else 0.0, lambda swd, ep: 0.1 * float(ep.get("consecutive_same_agent_calls", 0)), lambda swd, ep: 0.25 if is_verbatim_copy(swd) else 0.0, lambda swd, ep: 0.5 if ep.get("version_decreased") else 0.0, lambda swd, ep: 0.15 if reasoning_log_is_duplicated(swd) else 0.0, ] def compute_reward( swd: Dict[str, Any], verify_result: Dict[str, bool], episode_metadata: Dict[str, Any], task_goal: str, ) -> float: """ Weighted reward in roughly [-1, 1] after penalties (guide Part 2). verify_result: dict of bool criterion name -> passed. """ if not verify_result: completion = 0.0 else: completion = sum(verify_result.values()) / len(verify_result) coherence = compute_swd_coherence(swd) milestones = swd.get("milestones", []) or [] turn_completed: Dict[str, int] = episode_metadata.get("turn_completed", {}) or {} completed_on_time = 0 for m in milestones: mid = m.get("id") if m.get("status") == "complete" and mid is not None: done_turn = turn_completed.get(mid, 999) if done_turn <= int(m.get("due_by_turn", 999)): completed_on_time += 1 milestone_score = completed_on_time / max(len(milestones), 1) log_entries = swd.get("reasoning_log", []) or [] unique_turns: set = set() for e in log_entries: if isinstance(e, dict) and e.get("turn") is not None: unique_turns.add(e["turn"]) reasoning_score = min(len(unique_turns) / 5.0, 1.0) llm_score = 0.0 if episode_metadata.get("finalized"): llm_score = call_llm_judge(swd, task_goal) raw = ( 0.35 * completion + 0.25 * coherence + 0.20 * milestone_score + 0.10 * reasoning_score + 0.10 * llm_score ) penalties = 0.0 penalties += float(episode_metadata.get("invalid_json_count", 0)) * 0.15 penalties += float(episode_metadata.get("wrong_agent_count", 0)) * 0.10 penalties += 0.20 if episode_metadata.get("token_budget_exceeded") else 0.0 penalties += sum(0.08 for m in milestones if m.get("status") == "missed") for fn in REWARD_HACKING_PENALTIES: penalties += fn(swd, episode_metadata) return max(0.0, min(1.0, raw - penalties))