Mirror of GitHub source: OpenEnv-compliant LeniencyBench environment + training scripts
6b4f87f verified | """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 | |