SupportOps / env /rewards.py
prathvi9607's picture
done phase 1
9c1645f
Raw History Blame Contribute Delete
6.18 kB
"""
Reward calculation system for SupportOps Arena.
Implements dense reward shaping with detailed breakdown tracking.
"""
from typing import Any
from env.state import EnvState, TaskLevel
from env.actions import ActionType
# ─── Reward Events ────────────────────────────────────────────
REWARD_EVENTS = {
# Positive rewards
"DIAGNOSTIC_ACTION": +0.10,
"CORRECT_TARGETED_DIAGNOSTIC": +0.20,
"EVIDENCE_BEFORE_RISKY": +0.05,
"CORRECT_REMEDIATION": +0.40,
"OPTIMAL_RESOLUTION": +0.20, # Only if steps_used <= min_required_steps
"JUSTIFIED_ESCALATION": +0.30,
"TERMINAL_CORRECT": +1.00,
# Negative rewards
"REDUNDANT_ACTION": -0.05,
"RISKY_WITHOUT_DIAGNOSTIC": -0.15,
"INCORRECT_REMEDIATION": -0.25,
"HARMFUL_ACTION": -0.35,
"PREMATURE_CLOSURE": -0.40,
"BUDGET_EXCEEDED_PER_STEP": -0.02,
"PREMATURE_ESCALATION": -0.20, # Escalate before unlock
"UNJUSTIFIED_ESCALATION": -0.20, # Escalate without enough evidence
}
# ─── Configuration ────────────────────────────────────────────
MIN_STEPS_FOR_OPTIMAL = {
TaskLevel.EASY: 4,
TaskLevel.MEDIUM: 7,
TaskLevel.HARD: 12,
}
MAX_POSSIBLE_REWARD = {
TaskLevel.EASY: 2.05,
TaskLevel.MEDIUM: 2.35,
TaskLevel.HARD: 2.75,
}
class RewardCalculator:
"""
Calculates rewards for agent actions with dense reward shaping.
Tracks individual reward events for interpretability.
"""
def calculate(
self,
events: list[str],
state: EnvState,
action: ActionType
) -> tuple[float, dict[str, Any]]:
"""
Calculate total reward from a list of reward event keys.
Args:
events: List of reward event keys (e.g., ["DIAGNOSTIC_ACTION", "CORRECT_TARGETED_DIAGNOSTIC"])
state: Current environment state
action: Action that was taken
Returns:
Tuple of (total_reward, breakdown_dict)
- total_reward: Sum of all event rewards
- breakdown_dict: Dictionary mapping event keys to their reward values
"""
breakdown = {}
total_reward = 0.0
for event_key in events:
if event_key not in REWARD_EVENTS:
raise ValueError(f"Unknown reward event: {event_key}")
reward_value = REWARD_EVENTS[event_key]
breakdown[event_key] = reward_value
total_reward += reward_value
return total_reward, breakdown
def calculate_episode_score(
self,
cumulative_reward: float,
task_level: TaskLevel
) -> float:
"""
Normalize cumulative reward to episode score in [0, 1].
Args:
cumulative_reward: Total reward accumulated during episode
task_level: Difficulty level of the task
Returns:
Normalized score clamped to [0, 1]
"""
max_reward = MAX_POSSIBLE_REWARD[task_level]
score = cumulative_reward / max_reward
return max(0.0, min(1.0, score))
def is_optimal_resolution(
self,
step_count: int,
task_level: TaskLevel
) -> bool:
"""
Check if resolution was achieved within optimal step count.
Args:
step_count: Number of steps taken
task_level: Difficulty level
Returns:
True if step_count <= minimum optimal steps for this level
"""
return step_count <= MIN_STEPS_FOR_OPTIMAL[task_level]
def get_max_possible_reward(self, task_level: TaskLevel) -> float:
"""Get maximum possible reward for a task level."""
return MAX_POSSIBLE_REWARD[task_level]
def validate_events(self, events: list[str]) -> bool:
"""
Validate that all event keys are recognized.
Args:
events: List of event keys to validate
Returns:
True if all events are valid
Raises:
ValueError: If any event key is not recognized
"""
for event in events:
if event not in REWARD_EVENTS:
raise ValueError(f"Unknown reward event: {event}")
return True
def get_reward_event_description(event_key: str) -> str:
"""Get human-readable description of a reward event."""
descriptions = {
"DIAGNOSTIC_ACTION": "Performed a diagnostic action to gather information",
"CORRECT_TARGETED_DIAGNOSTIC": "Performed diagnostic action that targets the actual root cause",
"EVIDENCE_BEFORE_RISKY": "Gathered evidence before taking risky action",
"CORRECT_REMEDIATION": "Applied the correct fix for the root cause",
"OPTIMAL_RESOLUTION": "Resolved issue within optimal step count",
"JUSTIFIED_ESCALATION": "Escalated with sufficient evidence",
"TERMINAL_CORRECT": "Episode terminated with correct resolution",
"REDUNDANT_ACTION": "Repeated an action already performed",
"RISKY_WITHOUT_DIAGNOSTIC": "Took risky action without prior diagnostics",
"INCORRECT_REMEDIATION": "Applied wrong fix for the root cause",
"HARMFUL_ACTION": "Action caused harm or disruption",
"PREMATURE_CLOSURE": "Closed ticket without proper resolution",
"BUDGET_EXCEEDED_PER_STEP": "Exceeded step budget (per step penalty)",
"PREMATURE_ESCALATION": "Escalated before gathering minimum evidence",
"UNJUSTIFIED_ESCALATION": "Escalated without sufficient diagnostic evidence",
}
return descriptions.get(event_key, f"Unknown event: {event_key}")
def get_all_positive_events() -> list[str]:
"""Get list of all positive reward event keys."""
return [k for k, v in REWARD_EVENTS.items() if v > 0]
def get_all_negative_events() -> list[str]:
"""Get list of all negative reward event keys."""
return [k for k, v in REWARD_EVENTS.items() if v < 0]