openenv
leniencybench / drift_env /grader.py
shreyas-garg's picture
Mirror of GitHub source: OpenEnv-compliant LeniencyBench environment + training scripts
6b4f87f verified
Raw
History Blame Contribute Delete
5.38 kB
"""Deterministic 3-component grader.
Per-step reward in [0.0, 2.0] composed of:
1. compliance 0.0 – 1.0 β€” did the action respect current policy?
2. appropriateness 0.0 – 0.5 β€” was the action TYPE sensible for the email?
3. drift_bonus 0.0 – 0.5 β€” first correct drift-aware action after a
drift fires once per drift event.
No LLM-as-judge, no randomness. Ground truth is pre-computed in
`episodes.EpisodeStep.correct_action_hint`, so the grader is a pure function
of (action, correct_hint, email_meta, drift_sensitive_to, armed_drifts).
"""
from __future__ import annotations
from typing import Tuple
from drift_env.models import Action
# --------------------------------------------------------------------------
# Compliance β€” exact structural match on the fields that define "correct"
# --------------------------------------------------------------------------
def _compliance(action: Action, hint: dict) -> float:
want = hint.get("action_type")
got = action.action_type.value
if got != want:
return 0.0
# Field-level exact match depending on action_type
if want == "approve_refund":
return 1.0 if action.refund_amount == hint.get("refund_amount") else 0.5
if want == "escalate":
tier_ok = action.escalation_tier == hint.get("escalation_tier")
hours_want = hint.get("followup_hours")
if hours_want is None:
return 1.0 if tier_ok else 0.5
hours_ok = action.followup_hours == hours_want
if tier_ok and hours_ok:
return 1.0
if tier_ok or hours_ok:
return 0.7
return 0.4
if want == "request_info":
return 1.0 if action.info_field == hint.get("info_field") else 0.5
if want == "close":
return 1.0 if action.resolution_code == hint.get("resolution_code") else 0.7
# Plain reply / anything else: action_type match is enough.
return 1.0
# --------------------------------------------------------------------------
# Appropriateness β€” is the chosen action TYPE reasonable for this email kind?
# Gives partial credit when the agent picks a sensible action type even if
# specific fields are wrong. Caps at 0.5.
# --------------------------------------------------------------------------
_APPROPRIATE_BY_KIND = {
"refund": {"approve_refund", "escalate", "request_info"},
"critical_incident": {"escalate"},
"info_request": {"request_info"},
"billing_q": {"reply"},
"chitchat": {"close"},
}
def _appropriateness(action: Action, email_meta: dict) -> float:
kind = email_meta.get("kind")
if kind is None:
return 0.0
ok_set = _APPROPRIATE_BY_KIND.get(kind, set())
if action.action_type.value in ok_set:
return 0.5
# Admin emails are handled by the env; this path is customer-only.
return 0.0
# --------------------------------------------------------------------------
# Drift bonus β€” +0.5 the FIRST time the agent nails a drift-sensitive step
# after the corresponding drift has fired. Stateful across the episode,
# tracked by the environment in `armed_drifts`.
# --------------------------------------------------------------------------
def _drift_bonus(
drift_sensitive_to: str | None,
armed_drifts: set[str],
compliance_score: float,
) -> Tuple[float, str | None]:
"""Returns (bonus, drift_to_clear_if_awarded)."""
if drift_sensitive_to is None:
return 0.0, None
if drift_sensitive_to not in armed_drifts:
return 0.0, None
if compliance_score < 1.0:
return 0.0, None
return 0.5, drift_sensitive_to
# --------------------------------------------------------------------------
# Admin emails β€” handled as a separate scoring path (not drift-sensitive,
# grader doesn't compute appropriateness for them).
# --------------------------------------------------------------------------
def _grade_admin(action: Action) -> float:
if action.action_type.value == "close":
# Full credit regardless of resolution_code β€” we don't want to
# punish harmless reword-ing of the ack.
return 1.0
# Any non-close action on an admin email is a minor error.
return 0.2
# --------------------------------------------------------------------------
# Top-level: returns (total_reward, breakdown_dict, drift_to_clear)
# --------------------------------------------------------------------------
def grade_step(
action: Action,
hint: dict,
email_meta: dict,
drift_sensitive_to: str | None,
armed_drifts: set[str],
is_admin_email: bool,
) -> Tuple[float, dict, str | None]:
if is_admin_email:
reward = _grade_admin(action)
return reward, {"compliance": reward, "appropriateness": 0.0,
"drift_bonus": 0.0}, None
compliance = _compliance(action, hint)
appropriateness = _appropriateness(action, email_meta)
drift_bonus, drift_to_clear = _drift_bonus(
drift_sensitive_to, armed_drifts, compliance,
)
total = compliance + appropriateness + drift_bonus
breakdown = {
"compliance": round(compliance, 4),
"appropriateness": round(appropriateness, 4),
"drift_bonus": round(drift_bonus, 4),
}
return round(total, 4), breakdown, drift_to_clear