CoolFace
Apppublic

ahagestedt/apex-devops

sourceHugging Facemitupdated 7mo agoView on Hugging Face
0likes
reward.py406 linesDownload Raw Back to rl_qa_base
1"""2Shaped reward system for QA evaluation environments.3 4Implements a 7-component reward function that incentivizes agents to:51. Gather complete information before making decisions (completeness)62. Score accurately against ground truth (accuracy)73. Provide high-quality reasoning (reasoning_quality)84. Calibrate confidence properly (confidence_calibration)95. Catch safety violations (safety_compliance)106. Be efficient with fewer steps (efficiency)117. Route correctly via auto_commit vs route_to_hitl (gating_correctness)12 13Total reward range: [-0.3, 1.0] (matching clinKriya reference environment).14"""15 16from __future__ import annotations17 18from typing import Any, Dict, List, Optional19 20 21# ---------------------------------------------------------------------------22# Constants23# ---------------------------------------------------------------------------24 25AVAILABLE_RESOURCES: tuple[str, ...] = (26    "ticket",27    "customer_history",28    "rubric",29    "similar_cases",30    "api_docs",31)32 33COMPONENT_WEIGHTS: Dict[str, float] = {34    "completeness": 0.20,35    "accuracy": 0.25,36    "reasoning_quality": 0.15,37    "confidence_calibration": 0.10,38    "safety_compliance": 0.15,39    "efficiency": 0.05,40    "gating_correctness": 0.10,41}42 43COMPONENT_RANGES: Dict[str, tuple[float, float]] = {44    "completeness": (0.0, 1.0),45    "accuracy": (0.0, 1.0),46    "reasoning_quality": (0.0, 1.0),47    "confidence_calibration": (0.0, 1.0),48    "safety_compliance": (-1.0, 1.0),49    "efficiency": (0.0, 1.0),50    "gating_correctness": (0.0, 1.0),51}52 53TOTAL_REWARD_MIN: float = -0.354TOTAL_REWARD_MAX: float = 1.055 56 57def _clamp(value: float, low: float, high: float) -> float:58    """Clamp a value to [low, high]."""59    return max(low, min(high, value))60 61 62# ---------------------------------------------------------------------------63# RewardCalculator64# ---------------------------------------------------------------------------65 66 67class RewardCalculator:68    """Compute a 7-component shaped reward for QA evaluation episodes.69 70    Each component is computed independently, multiplied by its weight,71    and summed to produce a total reward clamped to [-0.3, 1.0].72 73    Attributes:74        AUTO_COMMIT_THRESHOLD: Minimum confidence for auto-commit gating.75        MAX_STEPS: Maximum expected steps in an episode.76    """77 78    AUTO_COMMIT_THRESHOLD: float = 0.8579    MAX_STEPS: int = 880 81    def compute_reward(self, state_data: dict[str, Any]) -> dict[str, Any]:82        """Compute all 7 reward components and the total shaped reward.83 84        Args:85            state_data: Episode data with keys:86                - gathered_info: dict of resources the agent retrieved87                  (keys are resource names, e.g. "ticket", "rubric").88                - submitted_evaluation: dict with the agent's score (int),89                  confidence (float), reasoning (str), and optionally90                  safety_flag (bool) and reasoning_steps (list).91                - ground_truth: dict with score (int) and92                  safety_violation (bool).93                - action_history: list of action dicts taken during the94                  episode.95                - scenario: full scenario definition (unused currently,96                  reserved for future scenario-aware shaping).97 98        Returns:99            dict with:100                - components: raw (unweighted) value per component.101                - weighted_components: value * weight per component.102                - total: final reward clamped to [-0.3, 1.0].103                - gating_decision: "auto_commit" or "route_to_hitl".104        """105        gathered_info: dict[str, Any] = state_data.get("gathered_info", {})106        submitted: dict[str, Any] = state_data.get("submitted_evaluation", {})107        ground_truth: dict[str, Any] = state_data.get("ground_truth", {})108        action_history: list[dict[str, Any]] = state_data.get("action_history", [])109 110        # Derive values with safe defaults111        submitted_score: int = int(submitted.get("score", 0))112        ground_truth_score: int = int(ground_truth.get("score", 0))113        submitted_confidence: float = float(submitted.get("confidence", 0.0))114        steps_taken: int = len(action_history)115 116        # Determine the phase index: how many actions occurred before the117        # first "submit" or "evaluate" action.  This tells us whether the118        # agent gathered info *before* deciding.119        phase_index: int = self._find_decision_phase_index(action_history)120 121        # --- Compute each component ---122        accuracy_score = self._compute_accuracy(submitted_score, ground_truth_score)123 124        components: Dict[str, float] = {125            "completeness": self._compute_completeness(gathered_info, phase_index),126            "accuracy": accuracy_score,127            "reasoning_quality": self._compute_reasoning_quality(submitted),128            "confidence_calibration": self._compute_confidence_calibration(129                submitted_confidence, accuracy_score130            ),131            "safety_compliance": self._compute_safety_compliance(submitted, ground_truth),132            "efficiency": self._compute_efficiency(steps_taken),133            "gating_correctness": self._compute_gating_correctness(134                submitted_confidence, ground_truth_score135            ),136        }137 138        # Clamp each component to its valid range139        for name, value in components.items():140            low, high = COMPONENT_RANGES[name]141            components[name] = _clamp(value, low, high)142 143        # Apply weights144        weighted: Dict[str, float] = {145            name: round(components[name] * COMPONENT_WEIGHTS[name], 6)146            for name in components147        }148 149        total = _clamp(sum(weighted.values()), TOTAL_REWARD_MIN, TOTAL_REWARD_MAX)150 151        gating_decision = self.compute_gating(submitted_confidence)152 153        return {154            "components": components,155            "weighted_components": weighted,156            "total": round(total, 4),157            "gating_decision": gating_decision,158        }159 160    # ------------------------------------------------------------------161    # Individual component methods162    # ------------------------------------------------------------------163 164    def _compute_completeness(165        self, gathered_info: dict[str, Any], phase_index: int166    ) -> float:167        """Score based on how much information the agent gathered.168 169        Available resources: ticket, customer_history, rubric,170        similar_cases, api_docs.171 172        Base score = resources_gathered / total_available.173        A 0.1 bonus is added if all gathering happened before the174        decision action (phase_index indicates the agent looked at175        everything before committing).176 177        Args:178            gathered_info: Mapping of resource_name -> content/data179                retrieved by the agent.180            phase_index: Number of actions before the first decision181                action.  A higher value relative to gathered count182                indicates gathering happened first.183 184        Returns:185            Float in [0, 1].186        """187        if not AVAILABLE_RESOURCES:188            return 1.0189 190        total_available = len(AVAILABLE_RESOURCES)191        gathered_count = sum(192            1 for resource in AVAILABLE_RESOURCES if resource in gathered_info193        )194 195        base_score = gathered_count / total_available196 197        # Bonus: if the agent gathered resources before making a decision.198        # phase_index >= gathered_count means all gathers preceded the199        # decision action.200        gather_before_decide_bonus = 0.1 if (201            gathered_count > 0 and phase_index >= gathered_count202        ) else 0.0203 204        return _clamp(base_score + gather_before_decide_bonus, 0.0, 1.0)205 206    def _compute_accuracy(207        self, submitted_score: int, ground_truth_score: int208    ) -> float:209        """Score accuracy: 1.0 for exact match, -0.25 per point of difference.210 211        Both scores are expected on a 1-5 integer scale.  If the agent212        did not submit a score (0), this returns 0.0.213 214        Args:215            submitted_score: The agent's evaluation score.216            ground_truth_score: The correct score.217 218        Returns:219            Float in [0, 1].220        """221        if submitted_score == 0 or ground_truth_score == 0:222            return 0.0223 224        diff = abs(submitted_score - ground_truth_score)225        return _clamp(1.0 - 0.25 * diff, 0.0, 1.0)226 227    def _compute_reasoning_quality(228        self, submitted_evaluation: dict[str, Any]229    ) -> float:230        """Score the depth and coherence of the agent's reasoning.231 232        Criteria (additive):233            - Has non-empty reasoning text: +0.3234            - Reasoning length > 50 characters: +0.2235            - Has confidence value: +0.2236            - Has multiple reasoning steps (list with len >= 2): +0.3237 238        Args:239            submitted_evaluation: The agent's submitted evaluation dict,240                expected to contain "reasoning" (str), "confidence"241                (float), and optionally "reasoning_steps" (list).242 243        Returns:244            Float in [0, 1].245        """246        score = 0.0247 248        reasoning: str = str(submitted_evaluation.get("reasoning", ""))249        if reasoning.strip():250            score += 0.3251 252        if len(reasoning) > 50:253            score += 0.2254 255        confidence = submitted_evaluation.get("confidence")256        if confidence is not None:257            score += 0.2258 259        reasoning_steps: Optional[List[Any]] = submitted_evaluation.get(260            "reasoning_steps"261        )262        if reasoning_steps is not None and len(reasoning_steps) >= 2:263            score += 0.3264 265        return _clamp(score, 0.0, 1.0)266 267    def _compute_confidence_calibration(268        self, submitted_confidence: float, accuracy_score: float269    ) -> float:270        """Measure how well the agent's confidence matches its accuracy.271 272        Perfect calibration (confidence == accuracy_score) yields 1.0.273        The penalty is the absolute difference between the two.274 275        Args:276            submitted_confidence: The agent's stated confidence [0, 1].277            accuracy_score: The computed accuracy component [0, 1].278 279        Returns:280            Float in [0, 1].281        """282        return _clamp(1.0 - abs(submitted_confidence - accuracy_score), 0.0, 1.0)283 284    def _compute_safety_compliance(285        self, submitted_evaluation: dict[str, Any], ground_truth: dict[str, Any]286    ) -> float:287        """Reward or penalize safety violation detection.288 289        If ground_truth has safety_violation=True:290            - Agent flagged it (safety_flag=True): +1.0291            - Agent missed it: -1.0292 293        If ground_truth has safety_violation=False:294            - Agent correctly did not flag: +0.5295            - Agent falsely flagged: -0.3296 297        Args:298            submitted_evaluation: Agent's evaluation; should contain299                "safety_flag" (bool) if the agent flagged a violation.300            ground_truth: Must contain "safety_violation" (bool).301 302        Returns:303            Float in [-1, 1].304        """305        gt_violation: bool = bool(ground_truth.get("safety_violation", False))306        agent_flagged: bool = bool(submitted_evaluation.get("safety_flag", False))307 308        if gt_violation:309            return 1.0 if agent_flagged else -1.0310        else:311            return -0.3 if agent_flagged else 0.5312 313    def _compute_efficiency(self, steps_taken: int) -> float:314        """Score efficiency: fewer steps yield a higher score.315 316        Optimal is 3-4 steps.  The formula treats 3 as the baseline:317            score = 1.0 - (steps_taken - 3) / (MAX_STEPS - 3)318 319        Steps at or below 3 get a perfect 1.0.  Steps at MAX_STEPS320        get 0.0.  Steps beyond MAX_STEPS are clamped to 0.0.321 322        Args:323            steps_taken: Number of actions in the episode.324 325        Returns:326            Float in [0, 1].327        """328        optimal_steps = 3329        denominator = self.MAX_STEPS - optimal_steps330        if denominator <= 0:331            return 1.0332 333        raw = 1.0 - (steps_taken - optimal_steps) / denominator334        return _clamp(raw, 0.0, 1.0)335 336    def _compute_gating_correctness(337        self, confidence: float, ground_truth_score: int338    ) -> float:339        """Score whether the agent's implicit gating matches the ideal.340 341        Ideal gating logic:342            auto_commit  if confidence >= 0.85 AND score >= 4343            route_to_hitl otherwise344 345        The agent's actual gating decision is derived from its346        confidence.  This component checks whether that decision347        matches what *should* happen given the ground truth score.348 349        Args:350            confidence: The agent's stated confidence [0, 1].351            ground_truth_score: The correct evaluation score (1-5).352 353        Returns:354            1.0 if the gating decision matches the ideal, 0.0 otherwise.355        """356        agent_decision = self.compute_gating(confidence)357 358        ideal_auto_commit = (359            confidence >= self.AUTO_COMMIT_THRESHOLD360            and ground_truth_score >= 4361        )362        ideal_decision = "auto_commit" if ideal_auto_commit else "route_to_hitl"363 364        return 1.0 if agent_decision == ideal_decision else 0.0365 366    def compute_gating(self, confidence: float) -> str:367        """Route based on confidence threshold.368 369        Args:370            confidence: The agent's confidence score [0, 1].371 372        Returns:373            "auto_commit" if confidence >= AUTO_COMMIT_THRESHOLD,374            "route_to_hitl" otherwise.375        """376        if confidence >= self.AUTO_COMMIT_THRESHOLD:377            return "auto_commit"378        return "route_to_hitl"379 380    # ------------------------------------------------------------------381    # Internal helpers382    # ------------------------------------------------------------------383 384    @staticmethod385    def _find_decision_phase_index(action_history: list[dict[str, Any]]) -> int:386        """Find the index of the first decision action in the history.387 388        A "decision" action is one whose action_type is "submit",389        "evaluate", or "finish".  Everything before that index is390        considered the information-gathering phase.391 392        Args:393            action_history: List of action dicts, each expected to have394                an "action_type" key.395 396        Returns:397            The index of the first decision action, or len(action_history)398            if no decision action was found (i.e. all actions were399            gathering).400        """401        decision_types = {"submit", "evaluate", "finish"}402        for i, action in enumerate(action_history):403            if action.get("action_type", "") in decision_types:404                return i405        return len(action_history)406