Spaces:
Sleeping
Sleeping
File size: 7,671 Bytes
73608eb 2060fb3 73608eb 2060fb3 73608eb 2060fb3 73608eb 2060fb3 73608eb 2060fb3 73608eb 2060fb3 73608eb 2060fb3 73608eb 2060fb3 73608eb 2060fb3 73608eb 2060fb3 73608eb 2060fb3 73608eb 2060fb3 73608eb 2060fb3 73608eb 2060fb3 73608eb 2060fb3 73608eb 2060fb3 73608eb 2060fb3 73608eb 2060fb3 73608eb 2060fb3 73608eb 2060fb3 73608eb 2060fb3 73608eb 2060fb3 73608eb 2060fb3 73608eb | 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 | """
Task graders for the Email Triage environment.
Each grader takes an episode history (list of StepRecords) and returns a
deterministic score strictly within (0.0, 1.0) β never exactly 0 or 1.
Three graders implement progressive difficulty:
- grade_task_basic: 5 emails, 2 folders, accuracy-only scoring
- grade_task_medium: 15 emails, 4 folders, weighted per-folder accuracy
- grade_task_hard: 30 emails, 6 folders, multi-component with VIP/urgency
"""
from __future__ import annotations
import logging
from typing import Dict, List
from src.models import StepRecord
logger = logging.getLogger(__name__)
# Minimum accuracy thresholds
MEDIUM_THRESHOLD: float = 0.70
HARD_ACCURACY_THRESHOLD: float = 0.60
# Epsilon bounds to keep scores strictly in (0, 1)
_SCORE_MIN: float = 0.01
_SCORE_MAX: float = 0.99
def _clamp_score(score: float) -> float:
"""Clamp a score to the open interval (0, 1).
The OpenEnv evaluation requires scores strictly between 0 and 1 β
never exactly 0.0 or 1.0.
"""
return round(max(_SCORE_MIN, min(_SCORE_MAX, score)), 4)
def grade_task_basic(episode_history: List[StepRecord]) -> float:
"""Grade the basic email sorting task.
Task: Sort 5 emails into work vs. spam.
Scoring: Pure accuracy (fraction of emails placed in correct folder).
Args:
episode_history: List of StepRecords from the episode.
Returns:
Score strictly in (0.0, 1.0).
"""
if not episode_history:
return _SCORE_MIN
move_steps = _get_classification_steps(episode_history)
if not move_steps:
return _SCORE_MIN
correct = 0.0
for step in move_steps:
c = step.reward.components.get("correctness", 0.0)
if c >= 0.9:
correct += 1.0
elif c >= 0.4:
correct += 0.5 # partial credit
total_emails = max(len(move_steps), 5)
score = correct / total_emails
return _clamp_score(score)
def grade_task_medium(episode_history: List[StepRecord]) -> float:
"""Grade the multi-folder triage task.
Task: Sort 15 emails into 4 folders (work, finance, meetings, spam).
Scoring: Weighted per-folder accuracy, with a 70% threshold gate.
Folder weights reflect business importance:
- work: 0.35
- finance: 0.30
- meetings: 0.20
- spam: 0.15
Args:
episode_history: List of StepRecords from the episode.
Returns:
Score strictly in (0.0, 1.0).
"""
if not episode_history:
return _SCORE_MIN
move_steps = _get_classification_steps(episode_history)
if not move_steps:
return _SCORE_MIN
folder_weights: Dict[str, float] = {
"work": 0.35,
"finance": 0.30,
"meetings": 0.20,
"spam": 0.15,
}
folder_correct: Dict[str, float] = {}
folder_total: Dict[str, int] = {}
for step in move_steps:
gt_folder = step.email.ground_truth_folder
if gt_folder not in folder_weights:
gt_folder = "work"
folder_total[gt_folder] = folder_total.get(gt_folder, 0) + 1
c = step.reward.components.get("correctness", 0.0)
if c >= 0.9:
folder_correct[gt_folder] = folder_correct.get(gt_folder, 0.0) + 1.0
elif c >= 0.4:
folder_correct[gt_folder] = folder_correct.get(gt_folder, 0.0) + 0.5
weighted_score = 0.0
for folder, weight in folder_weights.items():
total = folder_total.get(folder, 0)
if total > 0:
accuracy = folder_correct.get(folder, 0.0) / total
weighted_score += weight * accuracy
active_weight = sum(
w for f, w in folder_weights.items() if folder_total.get(f, 0) > 0
)
if active_weight > 0:
weighted_score = weighted_score / active_weight
# Threshold gate: low accuracy gets a low (but non-zero) score
overall_accuracy = _overall_accuracy(move_steps)
if overall_accuracy < MEDIUM_THRESHOLD:
return _clamp_score(weighted_score * 0.3)
return _clamp_score(weighted_score)
def grade_task_hard(episode_history: List[StepRecord]) -> float:
"""Grade the advanced triage task with urgency and VIP handling.
Task: Sort 30 emails with deadline awareness and VIP prioritization.
Scoring: Multi-component.
- 50% Overall accuracy (urgent emails weighted 2x)
- 25% Efficiency (fewer steps = better)
- 25% VIP handling (correctly classified VIP emails)
Args:
episode_history: List of StepRecords from the episode.
Returns:
Score strictly in (0.0, 1.0).
"""
if not episode_history:
return _SCORE_MIN
move_steps = _get_classification_steps(episode_history)
if not move_steps:
return _SCORE_MIN
# ββ 1. Accuracy (50%) β urgent emails weighted 2x ββββββββββββββββββββ
weighted_correct = 0.0
weighted_total = 0.0
for step in move_steps:
weight = 2.0 if step.email.priority_flag else 1.0
weighted_total += weight
c = step.reward.components.get("correctness", 0.0)
if c >= 0.9:
weighted_correct += weight
elif c >= 0.4:
weighted_correct += weight * 0.5
accuracy_score = weighted_correct / max(weighted_total, 1.0)
# ββ 2. Efficiency (25%) ββββββββββββββββββββββββββββββββββββββββββββββ
total_steps = len(episode_history)
ideal_steps = 30
efficiency_score = max(0.0, 1.0 - max(0, total_steps - ideal_steps) / ideal_steps)
# ββ 3. VIP handling (25%) ββββββββββββββββββββββββββββββββββββββββββββ
vip_steps = [s for s in move_steps if s.email.is_vip_sender]
if vip_steps:
vip_correct = sum(
1.0 for s in vip_steps
if s.reward.components.get("correctness", 0.0) >= 0.9
)
vip_partial = sum(
0.5 for s in vip_steps
if 0.4 <= s.reward.components.get("correctness", 0.0) < 0.9
)
vip_score = (vip_correct + vip_partial) / len(vip_steps)
else:
vip_score = 0.0
# ββ Combine ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
final_score = (
0.50 * accuracy_score
+ 0.25 * efficiency_score
+ 0.25 * vip_score
)
# Gate: low accuracy gets a penalized (but non-zero) score
raw_accuracy = _overall_accuracy(move_steps)
if raw_accuracy < HARD_ACCURACY_THRESHOLD:
return _clamp_score(final_score * 0.3)
return _clamp_score(final_score)
# ββ Helper Functions βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _get_classification_steps(history: List[StepRecord]) -> List[StepRecord]:
"""Filter to steps that classify emails (move, mark_spam, delete)."""
classification_actions = {"move", "mark_spam", "delete"}
return [
s for s in history
if s.action.action_type in classification_actions
]
def _overall_accuracy(move_steps: List[StepRecord]) -> float:
"""Simple accuracy: fraction of correctly classified emails."""
if not move_steps:
return 0.0
correct = sum(
1.0 for s in move_steps
if s.reward.components.get("correctness", 0.0) >= 0.9
)
return correct / len(move_steps)
|