Mirror of GitHub source: OpenEnv-compliant LeniencyBench environment + training scripts
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitignore +29 -0
- .vscode/settings.json +3 -0
- Dockerfile +20 -0
- README.md +405 -0
- baseline_output.txt +17 -0
- demo_before_after.py +231 -0
- drift_env/__init__.py +0 -0
- drift_env/dataset.py +147 -0
- drift_env/emails.py +190 -0
- drift_env/environment.py +134 -0
- drift_env/episodes.py +181 -0
- drift_env/grader.py +138 -0
- drift_env/llm_agent.py +90 -0
- drift_env/models.py +70 -0
- drift_env/policy.py +189 -0
- drift_env/prompts.py +69 -0
- drift_env/server/__init__.py +0 -0
- drift_env/server/app.py +52 -0
- drift_env/tests/__init__.py +0 -0
- drift_env/tests/test_adversarial.py +127 -0
- drift_env/tests/test_environment.py +127 -0
- drift_env/tests/test_grader.py +179 -0
- drift_env/training/__init__.py +0 -0
- drift_env/training/rewards.py +186 -0
- email_env/__init__.py +0 -0
- email_env/baseline.py +89 -0
- email_env/client.py +42 -0
- email_env/grader.py +117 -0
- email_env/models.py +27 -0
- email_env/openenv.yaml +6 -0
- email_env/server/Dockerfile +14 -0
- email_env/server/__init__.py +0 -0
- email_env/server/app.py +78 -0
- email_env/server/environment.py +59 -0
- email_env/tasks.py +108 -0
- eval_baseline.py +243 -0
- eval_results.json +2816 -0
- inference.py +147 -0
- openenv.yaml +6 -0
- outputs/baseline_direction_split.png +0 -0
- outputs/cross_model_baseline.png +0 -0
- outputs/direction_split.png +0 -0
- outputs/direction_split_v6.png +0 -0
- outputs/evals_v7.json +79 -0
- outputs/sft_log_v7.json +711 -0
- outputs/sft_loss.png +0 -0
- outputs/sft_loss_v6.png +0 -0
- outputs/v6_full_logs.txt +315 -0
- outputs/v7_full_logs.txt +449 -0
- plot_training.py +216 -0
.gitignore
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Environment variables
|
| 2 |
+
.env
|
| 3 |
+
*.env
|
| 4 |
+
|
| 5 |
+
# Python
|
| 6 |
+
__pycache__/
|
| 7 |
+
*.pyc
|
| 8 |
+
*.pyo
|
| 9 |
+
*.egg-info/
|
| 10 |
+
dist/
|
| 11 |
+
build/
|
| 12 |
+
.venv/
|
| 13 |
+
venv/
|
| 14 |
+
|
| 15 |
+
# IDE
|
| 16 |
+
.vscode/
|
| 17 |
+
.idea/
|
| 18 |
+
|
| 19 |
+
# OS
|
| 20 |
+
.DS_Store
|
| 21 |
+
Thumbs.db
|
| 22 |
+
|
| 23 |
+
# Private hackathon prep (not for public repo)
|
| 24 |
+
pitch/
|
| 25 |
+
ONSITE_RUNBOOK.md
|
| 26 |
+
|
| 27 |
+
# Local downloads of HF Hub artifacts (the trained adapter is already
|
| 28 |
+
# hosted at huggingface.co/shreyas-garg/leniencybench-qwen3b-outputs).
|
| 29 |
+
outputs/v7/
|
.vscode/settings.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"postman.settings.dotenv-detection-notification-visibility": false
|
| 3 |
+
}
|
Dockerfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
RUN useradd -m -u 1000 user
|
| 4 |
+
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
COPY requirements.txt .
|
| 8 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 9 |
+
|
| 10 |
+
COPY . .
|
| 11 |
+
|
| 12 |
+
RUN chown -R user:user /app
|
| 13 |
+
USER user
|
| 14 |
+
|
| 15 |
+
ENV PYTHONPATH=/app
|
| 16 |
+
ENV HOME=/home/user
|
| 17 |
+
|
| 18 |
+
EXPOSE 7860
|
| 19 |
+
|
| 20 |
+
CMD ["uvicorn", "drift_env.server.app:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: LeniencyBench
|
| 3 |
+
emoji: 📉
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
license: mit
|
| 10 |
+
tags:
|
| 11 |
+
- openenv
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
# LeniencyBench
|
| 15 |
+
|
| 16 |
+
**We found that frontier LLMs systematically obey policy *loosening* and silently ignore policy *tightening*. Llama 3.1 8B scores 0 % on rules that tighten vs 37.5 % on rules that loosen — a 37.5-point asymmetry from a single admin message in the context. One epoch of SFT on LeniencyBench's auto-generated supervision closes the tightening gap from 0 % to 91.3 % on Qwen 2.5 3B.**
|
| 17 |
+
|
| 18 |
+
**This isn't a reasoning failure.** It's a pretraining prior overriding an explicit mid-context instruction — exactly the failure mode that makes deployed LLM agents silently wrong the moment a company changes a rule.
|
| 19 |
+
|
| 20 |
+
**LeniencyBench** is the OpenEnv-compliant environment we built to measure this gap and train against it.
|
| 21 |
+
|
| 22 |
+

|
| 23 |
+
|
| 24 |
+
*Llama 3.1 8B untrained on LeniencyBench: **0 / 17** tightening decisions right, **3 / 8** loosening. Across 8 episodes × 20 emails, the model fails every rule that gets stricter.*
|
| 25 |
+
|
| 26 |
+

|
| 27 |
+
|
| 28 |
+
*One epoch of SFT on LeniencyBench's auto-generated labels: **tightening accuracy 0 % → 91.3 %**, loosening accuracy 21.4 % → 71.4 % on Qwen 2.5 3B (200 held-out samples).*
|
| 29 |
+
|
| 30 |
+
| |
|
| 31 |
+
|---|
|
| 32 |
+
| 🤗 **Live env:** [huggingface.co/spaces/shreyas-garg/drift-env](https://huggingface.co/spaces/shreyas-garg/drift-env) |
|
| 33 |
+
| 💻 **Code:** [github.com/shreyas-garg/OpenEnv](https://github.com/shreyas-garg/OpenEnv) |
|
| 34 |
+
| 📓 **Training notebook:** [`train_colab.ipynb`](./train_colab.ipynb) |
|
| 35 |
+
|
| 36 |
+
---
|
| 37 |
+
|
| 38 |
+
## The finding in one failure
|
| 39 |
+
|
| 40 |
+
Here is a verbatim rollout from the baseline run. Episode seed 100, an admin email at step 3 tightened the refund auto-approval cap from $100 to **$25**. At step 5, a customer requests a $90 refund:
|
| 41 |
+
|
| 42 |
+
> **Customer (step 5):** "Hi, I was quoted a $90 setup fee that I was told would be waived, and it was still charged. Please refund $90."
|
| 43 |
+
>
|
| 44 |
+
> **Correct action** (per current policy after admin email): `escalate(tier="manager")` — $90 is above the new $25 cap.
|
| 45 |
+
>
|
| 46 |
+
> **Llama 3.1 8B:** `approve_refund(amount=90.0)`. Rationale inferred from pattern: the model's pretraining prior is that $90 is a reasonable refund. It ignored the admin email from two turns ago.
|
| 47 |
+
|
| 48 |
+
This is **not** an outlier — it is the dominant failure pattern across our baseline run. The base model fails **every** tightening in the episode.
|
| 49 |
+
|
| 50 |
+
---
|
| 51 |
+
|
| 52 |
+
## Why this belongs at a training-environments hackathon
|
| 53 |
+
|
| 54 |
+
Most RL environments for LLM training have **static** rules. Chess rules don't change. Grid-world mazes don't re-wire themselves mid-episode. But every deployed-agent failure story you read in production has the same shape: *"we changed a policy, and the model silently kept applying the old one."*
|
| 55 |
+
|
| 56 |
+
We call the target capability **prior-override instruction following**: reading an admin-level instruction mid-context and applying it correctly, even when it contradicts what pretraining made the model expect. It's distinct from reasoning depth, tool use, or final-answer correctness — and it's what deployed agents silently fail at. Most existing post-training work optimises for the other three skills; this one is underexplored and directly verifiable.
|
| 57 |
+
|
| 58 |
+
LeniencyBench makes the **policy itself** the thing that changes, and scores the agent's response programmatically. A trained model on this env learns to track admin-level updates across long contexts instead of autopiloting its internet prior.
|
| 59 |
+
|
| 60 |
+
**"Isn't this just email triage?"** No. The substrate is support emails — they are the cleanest surface we found to controllably inject policy drifts with verifiable ground truth. The *mechanic* is domain-agnostic: any delegated-authority setting where instructions arrive mid-context (HR, IT, legal review, compliance) has the same leniency-bias structure.
|
| 61 |
+
|
| 62 |
+
**Themes addressed.** LeniencyBench fits **Theme 3.2 (World Modeling — Personalized Tasks)** by simulating realistic operator-controlled task handling under policy drift, and **Theme 2 (Long-Horizon Planning)** through 20-step episodes with mid-context policy events that require cross-step memory.
|
| 63 |
+
|
| 64 |
+
---
|
| 65 |
+
|
| 66 |
+
## Environment
|
| 67 |
+
|
| 68 |
+
### Episode structure
|
| 69 |
+
- **20 emails per episode**, deterministic from a seed.
|
| 70 |
+
- **2 admin emails at fixed positions (3 and 11)**, each announcing a policy change.
|
| 71 |
+
- The remaining 18 are regular customer tickets — refund requests, outage reports, billing questions, chit-chat.
|
| 72 |
+
- Agent processes one email at a time; inbox history (with its own prior actions) is exposed in each observation.
|
| 73 |
+
|
| 74 |
+
### Observation space
|
| 75 |
+
|
| 76 |
+
| Field | Type | Description |
|
| 77 |
+
|---|---|---|
|
| 78 |
+
| `current_email` | `Email` | Subject, body, sender, kind (customer or admin) |
|
| 79 |
+
| `email_index` | int | 0-based position in the 20-email episode |
|
| 80 |
+
| `total_emails` | int | Always 20 |
|
| 81 |
+
| `inbox_history` | list[dict] | Prior emails + the action the agent already took on each |
|
| 82 |
+
|
| 83 |
+
Grader-relevant metadata (`refund_amount`, `severity`, etc.) is stripped before the observation is exposed — the agent has to infer these from the email body.
|
| 84 |
+
|
| 85 |
+
### Action space (6 discrete actions)
|
| 86 |
+
|
| 87 |
+
| Action | Parameters |
|
| 88 |
+
|---|---|
|
| 89 |
+
| `reply` | — |
|
| 90 |
+
| `approve_refund` | `refund_amount: float` |
|
| 91 |
+
| `escalate` | `escalation_tier: tier_1/tier_2/manager`, `followup_hours: int` |
|
| 92 |
+
| `schedule_followup` | `followup_hours: int` |
|
| 93 |
+
| `close` | `resolution_code: str` |
|
| 94 |
+
| `request_info` | `info_field: str` |
|
| 95 |
+
|
| 96 |
+
### Drift scenarios — 9 total, 2 stacked per episode
|
| 97 |
+
|
| 98 |
+
| Type | Variant | Direction | New value |
|
| 99 |
+
|---|---|---|---|
|
| 100 |
+
| **Refund cap** | `refund_cap_25` | tightening | $100 → $25 |
|
| 101 |
+
| | `refund_cap_50` | tightening | $100 → $50 |
|
| 102 |
+
| | `refund_cap_200` | loosening | $100 → $200 |
|
| 103 |
+
| **Escalation routing** | `escalate_manager` | tightening | tier_2 → manager |
|
| 104 |
+
| | `escalate_tier_1` | loosening | tier_2 → tier_1 |
|
| 105 |
+
| | `escalate_keep_tier_2` | neutral | no change (distractor) |
|
| 106 |
+
| **SLA window** | `sla_2hr` | tightening | 24h → 2h |
|
| 107 |
+
| | `sla_4hr` | tightening | 24h → 4h |
|
| 108 |
+
| | `sla_48hr` | loosening | 24h → 48h |
|
| 109 |
+
|
| 110 |
+
Each episode samples two drifts from different types, so they stack. **"Neutral" drifts (like `escalate_keep_tier_2`) are distractors** — they announce a rule change that actually equals the default. They are not counted as drift-sensitive for accuracy, but they do test whether the agent over-reacts to any admin-looking message.
|
| 111 |
+
|
| 112 |
+
---
|
| 113 |
+
|
| 114 |
+
## Architecture at a glance
|
| 115 |
+
|
| 116 |
+
```
|
| 117 |
+
┌──────────────────────────────────────────────────────────────────┐
|
| 118 |
+
│ Episode generator (deterministic from seed) │
|
| 119 |
+
│ → 20 emails per episode: 18 customer + 2 admin (drift events) │
|
| 120 |
+
└────────────────────────────────┬─────────────────────────────────┘
|
| 121 |
+
│
|
| 122 |
+
▼
|
| 123 |
+
┌──────────────────────────────────────────────────────────────────┐
|
| 124 |
+
│ DriftEnv — OpenEnv interface (reset / step / state) │
|
| 125 |
+
│ Observation: current email + inbox history (no leaked metadata) │
|
| 126 |
+
│ Action: 1 of 6 discrete types + typed parameters │
|
| 127 |
+
└──────────────┬────────────────────────────────────┬──────────────┘
|
| 128 |
+
│ │
|
| 129 |
+
▼ ▼
|
| 130 |
+
┌──────────────────────────┐ ┌──────────────────────────────┐
|
| 131 |
+
│ LLM Agent (Qwen 2.5 3B) │ │ Grader (deterministic) │
|
| 132 |
+
│ + LoRA adapter (rank 16)│ ──► │ • compliance [0, 1.0] │
|
| 133 |
+
│ emits JSON action │ │ • appropriateness [0, 0.5] │
|
| 134 |
+
│ │ │ • drift_bonus [0, 0.5] │
|
| 135 |
+
└──────────┬───────────────┘ └──────────────┬───────────────┘
|
| 136 |
+
│ │
|
| 137 |
+
└──────────────┬──────────────────────┘
|
| 138 |
+
│ per-step reward ∈ [0, 2]
|
| 139 |
+
▼
|
| 140 |
+
┌─────────────────────────────────┐
|
| 141 |
+
│ Training pipeline │
|
| 142 |
+
│ SFT (1 epoch, 16K samples) │
|
| 143 |
+
│ Unsloth + HF TRL, LoRA-only │
|
| 144 |
+
└────────────────┬────────────────┘
|
| 145 |
+
│
|
| 146 |
+
▼
|
| 147 |
+
┌─────────────────────────────────┐
|
| 148 |
+
│ Held-out eval (seeds 10000+) │
|
| 149 |
+
│ Direction-split accuracy: │
|
| 150 |
+
│ tightening vs loosening │
|
| 151 |
+
└─────────────────────────────────┘
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
---
|
| 155 |
+
|
| 156 |
+
## Reward design
|
| 157 |
+
|
| 158 |
+
The reward is a **deterministic, 3-component score** computed by Python — no LLM-as-judge anywhere in the reward path. This matters for reproducibility and to prevent reward hacking.
|
| 159 |
+
|
| 160 |
+
| Component | Range | What it measures |
|
| 161 |
+
|---|---|---|
|
| 162 |
+
| **Compliance** | 0 – 1.0 | Exact structural match on policy-dependent fields (refund amount, escalation tier, SLA hours). |
|
| 163 |
+
| **Appropriateness** | 0 – 0.5 | Action *type* sensible for the email kind (refund email → refund-ish action). |
|
| 164 |
+
| **Drift-attention bonus** | 0 – 0.5 | +0.5 the *first* time the agent correctly handles a drift-sensitive step after each drift fires. Rewards memory of the admin email. |
|
| 165 |
+
|
| 166 |
+
Per-step reward ∈ [0, 2]. Episode max = 30. Ground truth is pre-computed via a deterministic table lookup per (email, policy) pair.
|
| 167 |
+
|
| 168 |
+
### Why this grader isn't gameable
|
| 169 |
+
|
| 170 |
+
We ship a `pytest`-style **adversarial agent suite** (`drift_env/tests/test_adversarial.py`) that runs 7 dumb policies against the environment:
|
| 171 |
+
|
| 172 |
+
| Dumb policy | Mean score (% of max, 20 seeds) |
|
| 173 |
+
|---|---|
|
| 174 |
+
| always `close` | 14.3 % |
|
| 175 |
+
| always `approve_refund $40` | 25.2 % |
|
| 176 |
+
| always `escalate manager` | 40.9 % |
|
| 177 |
+
| always `reply` | 11.3 % |
|
| 178 |
+
| always `request_info` | 21.8 % |
|
| 179 |
+
| action-type sweep | max 40.9 % |
|
| 180 |
+
| stale-policy (ignore drifts) | 50–95 % (bounded; this is essentially what Llama 8B does) |
|
| 181 |
+
| perfect (ground-truth oracle) | ≥ 95 % |
|
| 182 |
+
|
| 183 |
+
No constant policy beats 60 % of max. A perfect policy hits ~100 %. The ~60-point gap is the training signal.
|
| 184 |
+
|
| 185 |
+
### Why our post-training numbers are not reward hacking
|
| 186 |
+
|
| 187 |
+
The dramatic post-training tightening accuracy (**91.3 %** on the held-out 200-sample eval) is not a constant-policy exploit — three independent reasons:
|
| 188 |
+
|
| 189 |
+
1. **Always-escalate-manager ceilings at 40.9 %** in our committed adversarial test suite. A trained model scoring 73 % of total reward sits 32 points above that ceiling — that gap is what learning looks like.
|
| 190 |
+
2. **Post-SFT appropriateness = 0.45 / 0.5 (90 % of max).** Appropriateness scores zero when the action TYPE doesn't fit the email kind. If the model rotated to "always escalate," all chitchat (62 of 200), billing-question (88 of 200), and info-request (55 of 200) emails would score 0 here — pulling the average far below 0.45. The 0.45 means the model picks REPLY for billing questions, CLOSE for thank-yous, REQUEST_INFO for ambiguous tickets, and only ESCALATE on things that genuinely need escalation.
|
| 191 |
+
3. **Post-SFT compliance = 0.968 / 1.0.** Compliance requires correct action *parameters* — escalation tier, follow-up hours, refund amounts. Always-escalating without the right tier and SLA hours scores partial compliance at best (~0.5–0.7). The 0.968 number means the model is reading the admin email's specific SLA and routing rules, not picking a single safe action.
|
| 192 |
+
|
| 193 |
+
---
|
| 194 |
+
|
| 195 |
+
## Baseline: the leniency bias, in numbers
|
| 196 |
+
|
| 197 |
+
We ran the env against **Llama 3.1 8B via Groq's OpenAI-compatible endpoint**. No training. 8 episodes, 160 total steps, 25 drift-sensitive decisions.
|
| 198 |
+
|
| 199 |
+
| Metric | Value |
|
| 200 |
+
|---|---|
|
| 201 |
+
| Mean reward per episode | **23.1 / 30** (77 %) |
|
| 202 |
+
| Drift-sensitive accuracy (overall) | **12 %** (3 / 25) |
|
| 203 |
+
| **Tightening drifts** | **0 %** (0 / 17) |
|
| 204 |
+
| **Loosening drifts** | **37.5 %** (3 / 8) |
|
| 205 |
+
| Neutral drifts | n/a (0 / 0) |
|
| 206 |
+
|
| 207 |
+
The tightening/loosening split is the finding.
|
| 208 |
+
- On **loosening** drifts (the new rule is *looser* than the internet prior), the model gets things partly right — its prior coincidentally agrees with the new rule.
|
| 209 |
+
- On **tightening** drifts (the new rule is *stricter*), it fails uniformly.
|
| 210 |
+
- This is not measurement noise. It is a systematic, direction-asymmetric failure that only an environment like this can surface.
|
| 211 |
+
|
| 212 |
+
Per-drift, the loosening accuracy is concentrated in `refund_cap_200` (**2 / 2 = 100 %**); the SLA loosening case `sla_48hr` is harder (**1 / 6 ≈ 17 %**). The loosening number is the average. Full per-drift breakdown is in [`eval_results.json`](./eval_results.json).
|
| 213 |
+
|
| 214 |
+

|
| 215 |
+
|
| 216 |
+
*Cross-model baseline. The leniency bias is not a Llama-specific quirk — both Llama 3.1 8B and Qwen 2.5 3B score **exactly 0 % on tightening** while still getting partial credit on loosening drifts. Two different model families, same direction-asymmetric failure.*
|
| 217 |
+
|
| 218 |
+
---
|
| 219 |
+
|
| 220 |
+
## Training: pipeline + results
|
| 221 |
+
|
| 222 |
+
### Pipeline
|
| 223 |
+
- **Base model:** Qwen 2.5 3B-Instruct (Colab validation on 0.5B first)
|
| 224 |
+
- **Stack:** Unsloth (4-bit, LoRA rank 16) + HF TRL — supervised fine-tuning
|
| 225 |
+
- **SFT:** 1 epoch, lr = 2e-4, 800 episodes × ~20 steps = **16,000 auto-labelled per-step samples**
|
| 226 |
+
- **Hardware:** A100-SXM4-80GB via HF Jobs, bf16
|
| 227 |
+
- **What gets saved:** LoRA adapters only (no naive 4-bit merge — the Unsloth footgun)
|
| 228 |
+
- **GRPO end-to-end is wired in [`train.py`](./train.py)** as a follow-up pass, but the headline reportable result here is from SFT alone.
|
| 229 |
+
|
| 230 |
+
### Train / eval split (no leakage)
|
| 231 |
+
|
| 232 |
+
Training and evaluation use **disjoint seed ranges** over the env's deterministic episode generator: training draws from seeds **0–799** (16,000 per-step samples), eval draws from seeds **10000–10039** (200 held-out per-step samples capped from 800 generated). The 10,000-seed gap guarantees zero episode-level overlap. The eval rollouts share *component vocabulary* (28 customer email templates, 9 drift event types) with training but contain **no specific (email, drift, ordering) combination the model has seen** — the standard generalization claim for synthetic-environment RL benchmarks.
|
| 233 |
+
|
| 234 |
+
### Colab pipeline validation (Qwen 2.5 0.5B)
|
| 235 |
+
|
| 236 |
+
Before committing compute credits, we ran the full SFT → GRPO pipeline on a Colab T4 with Qwen 2.5 0.5B-Instruct as a sanity check. On 100 held-out eval rows, drift-sensitive accuracy moved **0 % → 50 %** after one epoch of SFT, and GRPO held the SFT result without regression (also 50 %). The Colab run is what proved the pipeline correctness end-to-end. Headline numbers come from the 3B onsite run.
|
| 237 |
+
|
| 238 |
+
### Onsite 3B run — confirmed result
|
| 239 |
+
|
| 240 |
+
Final 3B training ran on HF Jobs A100-80GB during the onsite compute window (2026-04-25 / 26). The pipeline executed end-to-end: SFT (16,000 samples, 1 epoch, ~100 min) → adapter saved + pushed to Hub → eval. The numbers below are drawn from the held-out 200-sample eval at seeds 10000–10039.
|
| 241 |
+
|
| 242 |
+
| Stage | Drift-sens (overall) | Tightening | Loosening |
|
| 243 |
+
|---|---|---|---|
|
| 244 |
+
| Pre-training (Qwen 2.5 3B) | **11.8 %** (2/17) | **0.0 %** (0/23) | **21.4 %** (3/14) |
|
| 245 |
+
| Post-SFT (1 epoch) | **88.2 %** (15/17) | **91.3 %** (21/23) | **71.4 %** (10/14) |
|
| 246 |
+
|
| 247 |
+
Component-wise: compliance avg moved **0.343 → 0.968** (out of 1.0), appropriateness avg moved **0.280 → 0.450** (out of 0.5). Total per-step reward moved from 0.62 → 1.46 (out of 2.0). The numbers are reproducible — they replicate exactly across two independent runs (v6 a10g + v7 a100), giving us confidence the result is the env's signal, not run-to-run variance.
|
| 248 |
+
|
| 249 |
+

|
| 250 |
+
|
| 251 |
+
*SFT loss collapses from ~1.3 to ~0.01 within the first 10 % of the epoch and stays flat after — the model fits the env's auto-generated labels hard, which is exactly what closes the leniency bias on the held-out eval.*
|
| 252 |
+
|
| 253 |
+

|
| 254 |
+
|
| 255 |
+
*Held-out direction-split accuracy on Qwen 2.5 3B before vs after SFT.*
|
| 256 |
+
|
| 257 |
+
**A note on GRPO.** Our pipeline wires SFT → GRPO end-to-end ([`train.py`](./train.py)), and the v7 run attempted both. GRPO's first training step crashed with a torch dtype mismatch arising from the Unsloth + TRL precision interaction at this configuration — a known integration friction we did not resolve inside our compute window. Our `try/except` around GRPO caught this gracefully, kept the post-SFT adapter as the final artifact, and pushed it to Hub. We report the SFT-only number because it's what the data supports. The 0.5B Colab pipeline run executed full SFT → GRPO cleanly and showed GRPO holding the SFT result without further uplift, which is consistent with our framing (the env's auto-generated labels carry the signal; SFT is enough to express it).
|
| 258 |
+
|
| 259 |
+
Raw outputs (adapter, log, evals): [`shreyas-garg/leniencybench-qwen3b-outputs`](https://huggingface.co/shreyas-garg/leniencybench-qwen3b-outputs). Full eval print: [`outputs/v7_full_logs.txt`](./outputs/v7_full_logs.txt).
|
| 260 |
+
|
| 261 |
+
---
|
| 262 |
+
|
| 263 |
+
## How to run
|
| 264 |
+
|
| 265 |
+
### Interact with the live env
|
| 266 |
+
|
| 267 |
+
```bash
|
| 268 |
+
curl -X POST https://shreyas-garg-drift-env.hf.space/reset \
|
| 269 |
+
-H "Content-Type: application/json" -d '{"seed": 42}'
|
| 270 |
+
|
| 271 |
+
curl -X POST https://shreyas-garg-drift-env.hf.space/step \
|
| 272 |
+
-H "Content-Type: application/json" \
|
| 273 |
+
-d '{"action_type": "approve_refund", "refund_amount": 40.0}'
|
| 274 |
+
```
|
| 275 |
+
|
| 276 |
+
### Run locally
|
| 277 |
+
|
| 278 |
+
```bash
|
| 279 |
+
git clone https://github.com/shreyas-garg/OpenEnv.git && cd OpenEnv
|
| 280 |
+
pip install -r requirements.txt
|
| 281 |
+
PYTHONPATH=. uvicorn drift_env.server.app:app --host 0.0.0.0 --port 7860
|
| 282 |
+
```
|
| 283 |
+
|
| 284 |
+
Or via Docker:
|
| 285 |
+
```bash
|
| 286 |
+
docker build -t drift-env . && docker run -p 7860:7860 drift-env
|
| 287 |
+
```
|
| 288 |
+
|
| 289 |
+
### Reproduce the baseline
|
| 290 |
+
```bash
|
| 291 |
+
API_BASE_URL=https://api.groq.com/openai/v1 HF_TOKEN=<groq_key> \
|
| 292 |
+
MODEL_NAME=llama-3.1-8b-instant \
|
| 293 |
+
PYTHONPATH=. python3 eval_baseline.py --episodes 8
|
| 294 |
+
```
|
| 295 |
+
|
| 296 |
+
### Train your own adapter
|
| 297 |
+
Open [`train_colab.ipynb`](./train_colab.ipynb) in Colab, enable a GPU runtime, run top-to-bottom. Takes ~10 min on T4 in `QUICK_MODE=true`.
|
| 298 |
+
|
| 299 |
+
For the full onsite setup, see [`train.py`](./train.py) — set `QUICK_MODE=false` for Qwen 2.5 3B + 600 GRPO steps.
|
| 300 |
+
|
| 301 |
+
### Generate plots from a training run
|
| 302 |
+
```bash
|
| 303 |
+
python plot_training.py ./outputs
|
| 304 |
+
```
|
| 305 |
+
|
| 306 |
+
### Side-by-side before/after demo on a fixed episode
|
| 307 |
+
```bash
|
| 308 |
+
python demo_before_after.py --seed 42 \
|
| 309 |
+
--base-model unsloth/Qwen2.5-3B-Instruct \
|
| 310 |
+
--trained-adapter ./outputs/lora_adapters
|
| 311 |
+
```
|
| 312 |
+
|
| 313 |
+
### Reproducibility
|
| 314 |
+
|
| 315 |
+
Tested on:
|
| 316 |
+
|
| 317 |
+
- **Python** 3.10 / 3.12 (local dev 3.13 also works for non-training code)
|
| 318 |
+
- **CUDA** 12.1–12.8 (A100 / H100 / T4 tested)
|
| 319 |
+
- **torch** ≥ 2.3, **transformers** ≥ 4.51, **trl** 0.24, **unsloth** from GitHub `main` (late Apr 2026)
|
| 320 |
+
- **bitsandbytes** ≥ 0.45.5, **accelerate** ≥ 1.0, **peft** ≥ 0.18
|
| 321 |
+
|
| 322 |
+
For the env server (no GPU required): `pip install -r requirements.txt` — `fastapi`, `uvicorn`, `pydantic`, `openai`, `python-dotenv` are enough.
|
| 323 |
+
|
| 324 |
+
For training: the `train_colab.ipynb` cell 1 installs an exact working stack on a fresh Colab. Pin everything from there if you need byte-reproducible training.
|
| 325 |
+
|
| 326 |
+
---
|
| 327 |
+
|
| 328 |
+
## Repository layout
|
| 329 |
+
|
| 330 |
+
```
|
| 331 |
+
.
|
| 332 |
+
├── README.md # this file
|
| 333 |
+
├── Dockerfile # HF Space entrypoint (uvicorn on 7860)
|
| 334 |
+
├── openenv.yaml # OpenEnv spec_version 1 manifest
|
| 335 |
+
├── pyproject.toml # package metadata + `server` entry point
|
| 336 |
+
├── train.py # SFT + GRPO end-to-end
|
| 337 |
+
├── train_colab.ipynb # runnable notebook
|
| 338 |
+
├── plot_training.py # reward curves + bar charts from logs
|
| 339 |
+
├── demo_before_after.py # render pre/post rollouts side-by-side
|
| 340 |
+
├── eval_baseline.py # evaluate any OpenAI-compatible model against the env
|
| 341 |
+
├── eval_results.json # baseline run output (Llama 3.1 8B)
|
| 342 |
+
├── server/
|
| 343 |
+
│ └── app.py # re-exports drift_env.server.app for validator convention
|
| 344 |
+
└── drift_env/
|
| 345 |
+
├── models.py # Pydantic typed interfaces
|
| 346 |
+
├── policy.py # PolicyState + 9 DriftEvents with direction labels
|
| 347 |
+
├── emails.py # 28 customer email templates
|
| 348 |
+
├── episodes.py # seed-deterministic 20-email episode generator
|
| 349 |
+
├── grader.py # 3-component deterministic reward
|
| 350 |
+
├── environment.py # DriftEnv: reset / step / state
|
| 351 |
+
├── dataset.py # episodes → per-step training rows
|
| 352 |
+
├── llm_agent.py # OpenAI-client agent wrapper
|
| 353 |
+
├── prompts.py # shared prompt rendering (agent + training)
|
| 354 |
+
├── training/rewards.py # 3 independent TRL reward functions
|
| 355 |
+
├── server/app.py # FastAPI server
|
| 356 |
+
└── tests/ # 35+ unit + adversarial tests
|
| 357 |
+
```
|
| 358 |
+
|
| 359 |
+
---
|
| 360 |
+
|
| 361 |
+
## Honest limitations
|
| 362 |
+
|
| 363 |
+
A healthy submission names its own weaknesses.
|
| 364 |
+
|
| 365 |
+
- **Baseline sample size is small.** 8 episodes × 25 drift-sensitive decisions = 25 data points for the headline 0 %/37.5 % split. A 50-episode extension is planned; the directional asymmetry is robust, but confidence intervals on the exact percentages are wide.
|
| 366 |
+
- **Component-level vs composition-level generalization.** Our train/eval split holds episode *compositions* out (different seeds, different orderings of drifts and emails), but the underlying customer email templates and drift event types are shared between train and eval. This is the standard generalization claim for synthetic-environment RL benchmarks (cf. Reasoning Gym, BrowserGym), but a stronger test would hold out templates or drift types entirely. Future work: measure transfer to held-out drift types (e.g. train only on refund-cap drifts, eval on SLA drifts).
|
| 367 |
+
- **One domain.** Support inboxes. The leniency-bias hypothesis plausibly generalises to other delegated-authority settings (HR policy, IT helpdesk, legal review), but we haven't tested it there.
|
| 368 |
+
- **GRPO did not produce additional uplift in our compute window.** The 0.5B Colab pipeline ran full SFT → GRPO cleanly and GRPO held the SFT result at 50 % drift-sensitive accuracy without further movement. The 3B onsite GRPO step hit a torch dtype mismatch at the Unsloth + TRL boundary that we did not resolve in time; the SFT-only adapter is the reported result. We interpret the broader pattern as: the env's auto-generated supervision is rich enough that SFT extracts most of the available signal on this task. A clean GRPO run is straightforward future work — see "How we'd extend this" below.
|
| 369 |
+
- **English-only email text.** No multilingual robustness claim.
|
| 370 |
+
- **Ground-truth table is the ceiling.** The grader compares to a pre-computed correct action. Agents cannot be rewarded for *better-than-the-hint* behaviour (e.g. a more empathetic message). This is a deliberate trade-off for reproducibility over subjective polish.
|
| 371 |
+
- **No online training loop.** Each episode is single-rollout; we don't explore iterative refinement within an episode.
|
| 372 |
+
|
| 373 |
+
---
|
| 374 |
+
|
| 375 |
+
## How we'd extend this
|
| 376 |
+
|
| 377 |
+
If the env finds traction beyond the hackathon, the natural follow-ups are:
|
| 378 |
+
|
| 379 |
+
1. **Cross-model baseline.** Measure the leniency-bias asymmetry across Mistral, Claude, GPT-4-class, and base-vs-instruct pairs of the same model family. The hypothesis is that the bias magnitude scales inversely with instruction-tuning quality; we'd want to test it.
|
| 380 |
+
2. **Port the mechanic to other substrates.** CRM tickets, IT helpdesks, legal-review workflows, compliance queues. Same "policy drift mid-context" mechanic, different domain text — a generalisation test for whether the trained capability transfers.
|
| 381 |
+
3. **Longer horizons + more drifts.** 50–100 emails per episode with 4+ stacked drifts, some of them contradicting each other, to test ordered-most-recent-wins semantics under pressure.
|
| 382 |
+
4. **Process-level rewards.** Right now the reward is outcome-only (did you pick the correct action). A future version could reward *explicitly citing the admin email in a rationale* — training interpretable instruction-following.
|
| 383 |
+
5. **RL from verifiable environment + human preference pairs.** The deterministic reward is great for reproducibility; combining it with a small DPO head for reply-text quality would give us both reliability and polish.
|
| 384 |
+
|
| 385 |
+
---
|
| 386 |
+
|
| 387 |
+
## Related work / context
|
| 388 |
+
|
| 389 |
+
**Knowledge conflict / parametric-vs-context.** A growing literature studies what happens when an LLM's pretrained knowledge contradicts evidence presented in its context. Longpre et al. (2021, *"Entity-Based Knowledge Conflicts in Question Answering"*) and follow-ups document that models default to parametric memory even when context provides a clearly authoritative correction. The leniency-bias asymmetry we report is a directional special case of this: models concede when the contextual rule is *looser* than their prior, but resist when it is *stricter*.
|
| 390 |
+
|
| 391 |
+
**Lost in the middle.** Liu et al. (2023, [*"Lost in the Middle: How Language Models Use Long Contexts"*](https://arxiv.org/abs/2307.03172)) showed that LLMs systematically under-attend to information placed in the middle of long contexts. Our admin emails are placed at fixed positions (3 and 11 of 20), and the corresponding drift-sensitive customer emails fall later in the sequence — putting our task squarely in the middle-of-context regime that paper warns about. Training on LeniencyBench is, in part, training the attention pattern out.
|
| 392 |
+
|
| 393 |
+
**RLHF-induced bias toward leniency.** Perez et al. (2022, [*"Discovering Language Model Behaviors with Model-Written Evaluations"*](https://arxiv.org/abs/2212.09251)) document a family of RLHF-induced biases including sycophancy and refusal-aversion. The pattern that "approve the refund / accommodate the user" is rewarded during instruction-tuning is a direct descendant of those findings. LeniencyBench provides one concrete, programmatically-verifiable target for measuring and removing one such bias.
|
| 394 |
+
|
| 395 |
+
**Instruction following benchmarks.** Zhou et al. (2023, [IFEval](https://arxiv.org/abs/2311.07911)) and follow-ups measure verifiable instruction adherence on single-turn prompts. LeniencyBench extends that idea to *cross-turn* instruction following — whether a mid-context instruction propagates into action-level decisions on later turns.
|
| 396 |
+
|
| 397 |
+
**RLVR + OpenEnv.** [OpenEnv](https://github.com/meta-pytorch/OpenEnv) (Meta × Hugging Face) provides the standardised `reset/step/state` interface this benchmark targets. The training stack is **Unsloth + HF TRL**, in the RLVR (reinforcement learning with verifiable rewards) pattern: reward computed by deterministic Python rather than a learned reward model.
|
| 398 |
+
|
| 399 |
+
**Industry context.** [Patronus AI](https://www.patronus.ai/) (consumer-workflow schema drift) and [Scale AI](https://scale.com/) (long-horizon business-workflow benchmarks) study problems whose stateful-inbox shape parallels LeniencyBench's substrate.
|
| 400 |
+
|
| 401 |
+
---
|
| 402 |
+
|
| 403 |
+
## License
|
| 404 |
+
|
| 405 |
+
MIT.
|
baseline_output.txt
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[START] task=task_1 env=email-triage model=llama-3.1-8b-instant
|
| 2 |
+
[STEP] step=1 action=triage(category='general',priority='low') reward=0.92 done=true error=null
|
| 3 |
+
[END] success=true steps=1 score=0.92 rewards=0.92
|
| 4 |
+
[START] task=task_2 env=email-triage model=llama-3.1-8b-instant
|
| 5 |
+
[STEP] step=1 action=triage(category='billing',priority='high') reward=0.95 done=true error=null
|
| 6 |
+
[END] success=true steps=1 score=0.95 rewards=0.95
|
| 7 |
+
[START] task=task_3 env=email-triage model=llama-3.1-8b-instant
|
| 8 |
+
[STEP] step=1 action=triage(category='technical',priority='high') reward=0.91 done=true error=null
|
| 9 |
+
[END] success=true steps=1 score=0.91 rewards=0.91
|
| 10 |
+
[START] task=task_4 env=email-triage model=llama-3.1-8b-instant
|
| 11 |
+
[STEP] step=1 action=triage(category='technical',priority='high') reward=0.94 done=true error=null
|
| 12 |
+
[END] success=true steps=1 score=0.94 rewards=0.94
|
| 13 |
+
[START] task=task_5 env=email-triage model=llama-3.1-8b-instant
|
| 14 |
+
[STEP] step=1 action=triage(category='technical',priority='high') reward=0.65 done=true error=null
|
| 15 |
+
[END] success=true steps=1 score=0.65 rewards=0.65
|
| 16 |
+
|
| 17 |
+
=== Average Score: 0.87 ===
|
demo_before_after.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Side-by-side before/after demo on a single fixed episode.
|
| 2 |
+
|
| 3 |
+
Runs TWO models against the same episode seed and prints their actions at each
|
| 4 |
+
step, marking drift-sensitive turns. Produces the pitch's money-shot clip.
|
| 5 |
+
|
| 6 |
+
Usage (run inside the Colab / HF compute env after training finishes):
|
| 7 |
+
|
| 8 |
+
python demo_before_after.py \\
|
| 9 |
+
--base-model unsloth/Qwen2.5-0.5B-Instruct \\
|
| 10 |
+
--trained-adapter ./outputs/lora_adapters \\
|
| 11 |
+
--seed 42
|
| 12 |
+
|
| 13 |
+
The script can also render a Markdown table (--markdown) suitable for pasting
|
| 14 |
+
into a slide, or just print a terminal-colored side-by-side view.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import argparse
|
| 20 |
+
import os
|
| 21 |
+
import sys
|
| 22 |
+
from dataclasses import dataclass
|
| 23 |
+
from typing import Optional
|
| 24 |
+
|
| 25 |
+
import torch
|
| 26 |
+
|
| 27 |
+
from drift_env.environment import DriftEnv
|
| 28 |
+
from drift_env.episodes import generate_episode
|
| 29 |
+
from drift_env.models import ActionType
|
| 30 |
+
from drift_env.prompts import SYSTEM_PROMPT
|
| 31 |
+
from drift_env.training.rewards import parse_generated_action
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
# ---------------------------------------------------------------------------
|
| 35 |
+
# Pretty-printing
|
| 36 |
+
# ---------------------------------------------------------------------------
|
| 37 |
+
RESET = "\033[0m"; BOLD = "\033[1m"; DIM = "\033[2m"
|
| 38 |
+
RED = "\033[31m"; GREEN = "\033[32m"; YELLOW = "\033[33m"; BLUE = "\033[34m"
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _fmt_action(action) -> str:
|
| 42 |
+
"""Compact printable representation of an Action."""
|
| 43 |
+
at = action.action_type.value
|
| 44 |
+
parts = [at]
|
| 45 |
+
for key in ("refund_amount", "escalation_tier", "followup_hours",
|
| 46 |
+
"resolution_code", "info_field"):
|
| 47 |
+
v = getattr(action, key, None)
|
| 48 |
+
if v is not None:
|
| 49 |
+
parts.append(f"{key}={v}")
|
| 50 |
+
return "(" + ", ".join(parts) + ")"
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _fmt_email(email) -> str:
|
| 54 |
+
tag = "ADMIN" if email.kind.value == "admin" else "cust"
|
| 55 |
+
return f"[{tag}] {email.subject}"
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@dataclass
|
| 59 |
+
class StepDecision:
|
| 60 |
+
email_subject: str
|
| 61 |
+
email_kind: str
|
| 62 |
+
drift_sensitive_to: Optional[str]
|
| 63 |
+
correct_action: dict
|
| 64 |
+
before_action: object
|
| 65 |
+
after_action: object
|
| 66 |
+
before_correct: bool
|
| 67 |
+
after_correct: bool
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
# ---------------------------------------------------------------------------
|
| 71 |
+
# Agent wrapper (loads a model + adapters and can generate one action per obs)
|
| 72 |
+
# ---------------------------------------------------------------------------
|
| 73 |
+
class LocalAgent:
|
| 74 |
+
def __init__(self, base_model: str, adapter_path: Optional[str] = None,
|
| 75 |
+
max_new_tokens: int = 128):
|
| 76 |
+
from unsloth import FastLanguageModel
|
| 77 |
+
self.model, self.tokenizer = FastLanguageModel.from_pretrained(
|
| 78 |
+
model_name=base_model,
|
| 79 |
+
max_seq_length=4096,
|
| 80 |
+
load_in_4bit=True,
|
| 81 |
+
)
|
| 82 |
+
if adapter_path and os.path.isdir(adapter_path):
|
| 83 |
+
from peft import PeftModel
|
| 84 |
+
self.model = PeftModel.from_pretrained(self.model, adapter_path)
|
| 85 |
+
print(f"[agent] loaded adapter from {adapter_path}")
|
| 86 |
+
FastLanguageModel.for_inference(self.model)
|
| 87 |
+
self.max_new_tokens = max_new_tokens
|
| 88 |
+
|
| 89 |
+
def act(self, obs) -> object:
|
| 90 |
+
from drift_env.prompts import render_user_prompt
|
| 91 |
+
chat = [
|
| 92 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 93 |
+
{"role": "user", "content": render_user_prompt(obs)},
|
| 94 |
+
]
|
| 95 |
+
inputs = self.tokenizer.apply_chat_template(
|
| 96 |
+
chat, add_generation_prompt=True, return_tensors="pt",
|
| 97 |
+
).to(self.model.device)
|
| 98 |
+
with torch.no_grad():
|
| 99 |
+
out = self.model.generate(
|
| 100 |
+
inputs, max_new_tokens=self.max_new_tokens,
|
| 101 |
+
do_sample=False, pad_token_id=self.tokenizer.eos_token_id,
|
| 102 |
+
use_cache=True,
|
| 103 |
+
)
|
| 104 |
+
text = self.tokenizer.decode(out[0][inputs.shape[1]:], skip_special_tokens=True)
|
| 105 |
+
return parse_generated_action(text)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
# ---------------------------------------------------------------------------
|
| 109 |
+
def run_one_episode(agent: LocalAgent, seed: int) -> list[object]:
|
| 110 |
+
"""Return the list of actions the agent took across an episode."""
|
| 111 |
+
env = DriftEnv()
|
| 112 |
+
obs = env.reset(seed=seed, episode_id=f"demo_{seed}")
|
| 113 |
+
actions = []
|
| 114 |
+
ep = generate_episode(seed=seed, episode_id=f"demo_{seed}")
|
| 115 |
+
for _ in ep.steps:
|
| 116 |
+
a = agent.act(obs)
|
| 117 |
+
actions.append(a)
|
| 118 |
+
res = env.step(a)
|
| 119 |
+
if res.done:
|
| 120 |
+
break
|
| 121 |
+
if res.observation is not None:
|
| 122 |
+
obs = res.observation
|
| 123 |
+
return actions
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def _check_compliance(action, hint: dict) -> bool:
|
| 127 |
+
"""Same as grader._compliance >= 1.0."""
|
| 128 |
+
from drift_env.grader import _compliance
|
| 129 |
+
return _compliance(action, hint) >= 1.0
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def collect(before_actions, after_actions, seed: int) -> list[StepDecision]:
|
| 133 |
+
ep = generate_episode(seed=seed, episode_id=f"demo_{seed}")
|
| 134 |
+
rows = []
|
| 135 |
+
for step, ba, aa in zip(ep.steps, before_actions, after_actions):
|
| 136 |
+
rows.append(StepDecision(
|
| 137 |
+
email_subject=step.email.subject,
|
| 138 |
+
email_kind=step.email.kind.value,
|
| 139 |
+
drift_sensitive_to=step.drift_sensitive_to,
|
| 140 |
+
correct_action=step.correct_action_hint,
|
| 141 |
+
before_action=ba,
|
| 142 |
+
after_action=aa,
|
| 143 |
+
before_correct=_check_compliance(ba, step.correct_action_hint),
|
| 144 |
+
after_correct=_check_compliance(aa, step.correct_action_hint),
|
| 145 |
+
))
|
| 146 |
+
return rows
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
# ---------------------------------------------------------------------------
|
| 150 |
+
# Renderers
|
| 151 |
+
# ---------------------------------------------------------------------------
|
| 152 |
+
def render_terminal(rows: list[StepDecision]) -> None:
|
| 153 |
+
print(f"\n{BOLD}Step Email Before After{RESET}")
|
| 154 |
+
print("-" * 120)
|
| 155 |
+
for i, r in enumerate(rows):
|
| 156 |
+
tag = f"{YELLOW}DRIFT-SENSITIVE{RESET}" if r.drift_sensitive_to else ""
|
| 157 |
+
if r.email_kind == "admin":
|
| 158 |
+
tag = f"{BLUE}ADMIN EMAIL{RESET}"
|
| 159 |
+
subj = r.email_subject[:40].ljust(40)
|
| 160 |
+
b_sym = f"{GREEN}✓{RESET}" if r.before_correct else f"{RED}✗{RESET}"
|
| 161 |
+
a_sym = f"{GREEN}✓{RESET}" if r.after_correct else f"{RED}✗{RESET}"
|
| 162 |
+
b_txt = _fmt_action(r.before_action)[:45]
|
| 163 |
+
a_txt = _fmt_action(r.after_action)[:45]
|
| 164 |
+
print(f"{i:>3} {subj} {b_sym} {b_txt:<46} {a_sym} {a_txt} {tag}")
|
| 165 |
+
|
| 166 |
+
b_count = sum(r.before_correct for r in rows)
|
| 167 |
+
a_count = sum(r.after_correct for r in rows)
|
| 168 |
+
print()
|
| 169 |
+
print(f"{BOLD}Before: {b_count}/{len(rows)} correct After: {a_count}/{len(rows)} correct{RESET}")
|
| 170 |
+
drift_rows = [r for r in rows if r.drift_sensitive_to]
|
| 171 |
+
if drift_rows:
|
| 172 |
+
db = sum(r.before_correct for r in drift_rows)
|
| 173 |
+
da = sum(r.after_correct for r in drift_rows)
|
| 174 |
+
print(f"{BOLD}Drift-sensitive: before {db}/{len(drift_rows)} after {da}/{len(drift_rows)}{RESET}")
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def render_markdown(rows: list[StepDecision], out_path: str) -> None:
|
| 178 |
+
b_count = sum(r.before_correct for r in rows)
|
| 179 |
+
a_count = sum(r.after_correct for r in rows)
|
| 180 |
+
drift_rows = [r for r in rows if r.drift_sensitive_to]
|
| 181 |
+
db = sum(r.before_correct for r in drift_rows) if drift_rows else 0
|
| 182 |
+
da = sum(r.after_correct for r in drift_rows) if drift_rows else 0
|
| 183 |
+
|
| 184 |
+
lines = ["# Before vs After — single episode\n",
|
| 185 |
+
f"- Overall: **{b_count}/{len(rows)}** → **{a_count}/{len(rows)}**",
|
| 186 |
+
f"- Drift-sensitive: **{db}/{len(drift_rows)}** → **{da}/{len(drift_rows)}**\n",
|
| 187 |
+
"| # | Email | Drift? | Before | After |",
|
| 188 |
+
"|---|-------|--------|--------|-------|"]
|
| 189 |
+
for i, r in enumerate(rows):
|
| 190 |
+
drift = f"**{r.drift_sensitive_to}**" if r.drift_sensitive_to else ("_admin_" if r.email_kind == "admin" else "-")
|
| 191 |
+
b = ("✅ " if r.before_correct else "❌ ") + _fmt_action(r.before_action)
|
| 192 |
+
a = ("✅ " if r.after_correct else "❌ ") + _fmt_action(r.after_action)
|
| 193 |
+
subj = r.email_subject[:40]
|
| 194 |
+
lines.append(f"| {i} | {subj} | {drift} | {b} | {a} |")
|
| 195 |
+
|
| 196 |
+
with open(out_path, "w") as f:
|
| 197 |
+
f.write("\n".join(lines) + "\n")
|
| 198 |
+
print(f"[ok] wrote {out_path}")
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
# ---------------------------------------------------------------------------
|
| 202 |
+
def main() -> int:
|
| 203 |
+
ap = argparse.ArgumentParser()
|
| 204 |
+
ap.add_argument("--base-model", default="unsloth/Qwen2.5-0.5B-Instruct")
|
| 205 |
+
ap.add_argument("--trained-adapter", default="./outputs/lora_adapters")
|
| 206 |
+
ap.add_argument("--seed", type=int, default=42)
|
| 207 |
+
ap.add_argument("--markdown", type=str, default=None,
|
| 208 |
+
help="If given, also write a markdown table to this path")
|
| 209 |
+
args = ap.parse_args()
|
| 210 |
+
|
| 211 |
+
print(f"=== Episode seed={args.seed} ===")
|
| 212 |
+
|
| 213 |
+
print("\n[1/2] Rolling out BEFORE (base model, no adapter)...")
|
| 214 |
+
before_agent = LocalAgent(args.base_model, adapter_path=None)
|
| 215 |
+
before_actions = run_one_episode(before_agent, args.seed)
|
| 216 |
+
del before_agent
|
| 217 |
+
torch.cuda.empty_cache() if torch.cuda.is_available() else None
|
| 218 |
+
|
| 219 |
+
print("\n[2/2] Rolling out AFTER (base + trained adapter)...")
|
| 220 |
+
after_agent = LocalAgent(args.base_model, adapter_path=args.trained_adapter)
|
| 221 |
+
after_actions = run_one_episode(after_agent, args.seed)
|
| 222 |
+
|
| 223 |
+
rows = collect(before_actions, after_actions, args.seed)
|
| 224 |
+
render_terminal(rows)
|
| 225 |
+
if args.markdown:
|
| 226 |
+
render_markdown(rows, args.markdown)
|
| 227 |
+
return 0
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
if __name__ == "__main__":
|
| 231 |
+
sys.exit(main())
|
drift_env/__init__.py
ADDED
|
File without changes
|
drift_env/dataset.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Dataset generator: episodes -> per-step training rows.
|
| 2 |
+
|
| 3 |
+
Each row contains:
|
| 4 |
+
- `prompt` : the user prompt the model would see at that step
|
| 5 |
+
- `correct_action` : JSON string of the ground-truth correct action (SFT target)
|
| 6 |
+
- `email_kind` : for appropriateness scoring in the reward function
|
| 7 |
+
- `drift_sensitive_to` : drift_event_name this step tests, or None
|
| 8 |
+
- `can_earn_drift_bonus`: True only for the FIRST drift-sensitive step after
|
| 9 |
+
each drift event (matches env's armed-drift state)
|
| 10 |
+
- `episode_id` / `step_index` : traceability
|
| 11 |
+
|
| 12 |
+
History is built using the GROUND-TRUTH actions (teacher-forced). This matches
|
| 13 |
+
the inference distribution after the agent has been trained — we want the model
|
| 14 |
+
to see clean histories, not noise.
|
| 15 |
+
|
| 16 |
+
Usage:
|
| 17 |
+
from drift_env.dataset import build_dataset
|
| 18 |
+
rows = build_dataset(n_episodes=500, start_seed=0)
|
| 19 |
+
# -> list[dict] OR datasets.Dataset if `as_hf=True`
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
import json
|
| 25 |
+
from typing import List
|
| 26 |
+
|
| 27 |
+
from drift_env.emails import CUSTOMER_TEMPLATES
|
| 28 |
+
from drift_env.episodes import Episode, EpisodeStep, generate_episode
|
| 29 |
+
from drift_env.models import Email, EmailKind, Observation
|
| 30 |
+
from drift_env.prompts import render_user_prompt
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _ground_truth_action_json(hint: dict) -> str:
|
| 34 |
+
"""Serialize the correct action hint as the canonical JSON the model should emit."""
|
| 35 |
+
out = {"action_type": hint["action_type"]}
|
| 36 |
+
for key in ("refund_amount", "escalation_tier", "followup_hours",
|
| 37 |
+
"resolution_code", "info_field"):
|
| 38 |
+
v = hint.get(key)
|
| 39 |
+
if v is not None:
|
| 40 |
+
out[key] = v
|
| 41 |
+
return json.dumps(out, separators=(",", ": "))
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _summary_for_history(step: EpisodeStep) -> dict:
|
| 45 |
+
"""Build the inbox-history entry as if the correct action had been taken."""
|
| 46 |
+
email = step.email
|
| 47 |
+
return {
|
| 48 |
+
"email_id": email.id,
|
| 49 |
+
"kind": email.kind.value,
|
| 50 |
+
"subject": email.subject,
|
| 51 |
+
"body": email.body,
|
| 52 |
+
"sender": email.sender,
|
| 53 |
+
"action_taken": step.correct_action_hint["action_type"],
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _mark_first_bonus_steps(steps: List[EpisodeStep]) -> List[bool]:
|
| 58 |
+
"""For each step, return True iff it is the FIRST drift-sensitive step
|
| 59 |
+
(post-drift) that can earn the drift-attention bonus for its drift event.
|
| 60 |
+
"""
|
| 61 |
+
seen_drifts: set[str] = set()
|
| 62 |
+
flags = []
|
| 63 |
+
for s in steps:
|
| 64 |
+
earn = False
|
| 65 |
+
if s.drift_sensitive_to is not None and s.drift_sensitive_to not in seen_drifts:
|
| 66 |
+
earn = True
|
| 67 |
+
seen_drifts.add(s.drift_sensitive_to)
|
| 68 |
+
flags.append(earn)
|
| 69 |
+
return flags
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def _observation_from_step(
|
| 73 |
+
step: EpisodeStep, history: list[dict], index: int, total: int,
|
| 74 |
+
) -> Observation:
|
| 75 |
+
"""Build an Observation as the agent would see it (no grader metadata)."""
|
| 76 |
+
clean_email = Email(
|
| 77 |
+
id=step.email.id, kind=step.email.kind, subject=step.email.subject,
|
| 78 |
+
body=step.email.body, sender=step.email.sender, meta={},
|
| 79 |
+
)
|
| 80 |
+
return Observation(
|
| 81 |
+
current_email=clean_email,
|
| 82 |
+
email_index=index,
|
| 83 |
+
total_emails=total,
|
| 84 |
+
inbox_history=list(history),
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def episode_to_rows(ep: Episode) -> List[dict]:
|
| 89 |
+
"""Convert one episode to a list of per-step training rows."""
|
| 90 |
+
bonus_flags = _mark_first_bonus_steps(ep.steps)
|
| 91 |
+
rows: List[dict] = []
|
| 92 |
+
history: list[dict] = []
|
| 93 |
+
total = len(ep.steps)
|
| 94 |
+
|
| 95 |
+
for i, step in enumerate(ep.steps):
|
| 96 |
+
obs = _observation_from_step(step, history, i, total)
|
| 97 |
+
prompt = render_user_prompt(obs)
|
| 98 |
+
row = {
|
| 99 |
+
"episode_id": ep.id,
|
| 100 |
+
"step_index": i,
|
| 101 |
+
"prompt": prompt,
|
| 102 |
+
"correct_action_json": _ground_truth_action_json(step.correct_action_hint),
|
| 103 |
+
"correct_action_hint": step.correct_action_hint,
|
| 104 |
+
"email_kind": step.email.meta.get("kind"),
|
| 105 |
+
"is_admin_email": step.email.kind == EmailKind.ADMIN,
|
| 106 |
+
"drift_sensitive_to": step.drift_sensitive_to,
|
| 107 |
+
"can_earn_drift_bonus": bonus_flags[i],
|
| 108 |
+
"policy_refund_cap": step.policy_at_step.refund_cap,
|
| 109 |
+
"policy_escalation_tier": step.policy_at_step.critical_escalation_tier,
|
| 110 |
+
"policy_sla_hours": step.policy_at_step.sla_hours_critical,
|
| 111 |
+
}
|
| 112 |
+
rows.append(row)
|
| 113 |
+
|
| 114 |
+
# Teacher-force: append what the CORRECT action would have been to history
|
| 115 |
+
history.append(_summary_for_history(step))
|
| 116 |
+
|
| 117 |
+
return rows
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def build_dataset(n_episodes: int, start_seed: int = 0) -> List[dict]:
|
| 121 |
+
"""Generate a list of training rows from `n_episodes` episodes."""
|
| 122 |
+
all_rows: List[dict] = []
|
| 123 |
+
for i in range(n_episodes):
|
| 124 |
+
seed = start_seed + i
|
| 125 |
+
ep = generate_episode(seed=seed, episode_id=f"train_{seed}")
|
| 126 |
+
all_rows.extend(episode_to_rows(ep))
|
| 127 |
+
return all_rows
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def dataset_stats(rows: List[dict]) -> dict:
|
| 131 |
+
"""Quick sanity-check numbers."""
|
| 132 |
+
n = len(rows)
|
| 133 |
+
admin = sum(1 for r in rows if r["is_admin_email"])
|
| 134 |
+
drift_sens = sum(1 for r in rows if r["drift_sensitive_to"])
|
| 135 |
+
bonus_eligible = sum(1 for r in rows if r["can_earn_drift_bonus"])
|
| 136 |
+
kinds: dict[str, int] = {}
|
| 137 |
+
for r in rows:
|
| 138 |
+
k = r["email_kind"] or "admin"
|
| 139 |
+
kinds[k] = kinds.get(k, 0) + 1
|
| 140 |
+
return {
|
| 141 |
+
"n_rows": n,
|
| 142 |
+
"admin_rows": admin,
|
| 143 |
+
"customer_rows": n - admin,
|
| 144 |
+
"drift_sensitive_rows": drift_sens,
|
| 145 |
+
"bonus_eligible_rows": bonus_eligible,
|
| 146 |
+
"kinds_distribution": kinds,
|
| 147 |
+
}
|
drift_env/emails.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Customer email templates. Each template carries structured metadata
|
| 2 |
+
(`meta`) that the grader uses to compute the ground-truth correct action
|
| 3 |
+
under the current policy. The agent does NOT see `meta`.
|
| 4 |
+
|
| 5 |
+
Refund amounts are chosen to span all policy caps ($25/$50/$100/$200) so
|
| 6 |
+
every drift scenario produces boundary-flipping cases.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
from dataclasses import dataclass
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass(frozen=True)
|
| 15 |
+
class CustomerTemplate:
|
| 16 |
+
id: str
|
| 17 |
+
subject: str
|
| 18 |
+
body: str
|
| 19 |
+
kind: str
|
| 20 |
+
refund_amount: float | None = None
|
| 21 |
+
severity: str | None = None
|
| 22 |
+
needs_info: str | None = None
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
CUSTOMER_TEMPLATES: list[CustomerTemplate] = [
|
| 26 |
+
# ---------- refunds at varied amounts, crossing all caps ----------
|
| 27 |
+
CustomerTemplate(
|
| 28 |
+
"refund_15", "Small refund for cancelled add-on",
|
| 29 |
+
"Hi, please refund the $15 add-on I cancelled yesterday. Account 18244.",
|
| 30 |
+
"refund", refund_amount=15.0,
|
| 31 |
+
),
|
| 32 |
+
CustomerTemplate(
|
| 33 |
+
"refund_30", "Partial month refund",
|
| 34 |
+
"I downgraded mid-cycle on April 8 and was overcharged $30. Please refund.",
|
| 35 |
+
"refund", refund_amount=30.0,
|
| 36 |
+
),
|
| 37 |
+
CustomerTemplate(
|
| 38 |
+
"refund_40", "Charged twice for my subscription",
|
| 39 |
+
"Hi, my card was charged $40 twice this month. Order IDs 82811 and 82812. "
|
| 40 |
+
"Please refund the duplicate.",
|
| 41 |
+
"refund", refund_amount=40.0,
|
| 42 |
+
),
|
| 43 |
+
CustomerTemplate(
|
| 44 |
+
"refund_50", "Refund for unused trial upgrade",
|
| 45 |
+
"I upgraded but never used the premium features — please refund the $50.",
|
| 46 |
+
"refund", refund_amount=50.0,
|
| 47 |
+
),
|
| 48 |
+
CustomerTemplate(
|
| 49 |
+
"refund_60", "Service interruption refund",
|
| 50 |
+
"We lost ~3 hours of service last night. The pro-rated refund should be $60.",
|
| 51 |
+
"refund", refund_amount=60.0,
|
| 52 |
+
),
|
| 53 |
+
CustomerTemplate(
|
| 54 |
+
"refund_75", "Refund for bad experience",
|
| 55 |
+
"Your service was down during our event. We paid $75 for the premium "
|
| 56 |
+
"slot and couldn't use it. Please refund the $75.",
|
| 57 |
+
"refund", refund_amount=75.0,
|
| 58 |
+
),
|
| 59 |
+
CustomerTemplate(
|
| 60 |
+
"refund_90", "Misquoted setup fee",
|
| 61 |
+
"I was quoted a $90 setup fee that I was told would be waived, and it "
|
| 62 |
+
"was still charged. Please refund $90.",
|
| 63 |
+
"refund", refund_amount=90.0,
|
| 64 |
+
),
|
| 65 |
+
CustomerTemplate(
|
| 66 |
+
"refund_100", "Failed project kickoff refund",
|
| 67 |
+
"The onboarding session never happened. Please refund the $100 fee.",
|
| 68 |
+
"refund", refund_amount=100.0,
|
| 69 |
+
),
|
| 70 |
+
CustomerTemplate(
|
| 71 |
+
"refund_120", "Duplicate annual renewal",
|
| 72 |
+
"I was charged $120 for a plan I already renewed through my partner "
|
| 73 |
+
"account. Please reverse one of the two charges.",
|
| 74 |
+
"refund", refund_amount=120.0,
|
| 75 |
+
),
|
| 76 |
+
CustomerTemplate(
|
| 77 |
+
"refund_150", "Refund for cancelled annual plan",
|
| 78 |
+
"I cancelled the annual plan within the 30-day window but still got "
|
| 79 |
+
"charged $150. Please reverse it.",
|
| 80 |
+
"refund", refund_amount=150.0,
|
| 81 |
+
),
|
| 82 |
+
CustomerTemplate(
|
| 83 |
+
"refund_180", "Misapplied promo code",
|
| 84 |
+
"The BLACKFRIDAY code I used never applied. I overpaid by $180.",
|
| 85 |
+
"refund", refund_amount=180.0,
|
| 86 |
+
),
|
| 87 |
+
CustomerTemplate(
|
| 88 |
+
"refund_250", "Wrong tier charged",
|
| 89 |
+
"I'm on the Pro plan but was billed for Enterprise ($250 delta). "
|
| 90 |
+
"Please refund the difference.",
|
| 91 |
+
"refund", refund_amount=250.0,
|
| 92 |
+
),
|
| 93 |
+
|
| 94 |
+
# ---------- critical incidents ----------
|
| 95 |
+
CustomerTemplate(
|
| 96 |
+
"critical_outage", "URGENT: Production is down",
|
| 97 |
+
"Our entire production environment is offline since 8 AM. This is "
|
| 98 |
+
"critical — customers can't check out. Please escalate immediately.",
|
| 99 |
+
"critical_incident", severity="critical",
|
| 100 |
+
),
|
| 101 |
+
CustomerTemplate(
|
| 102 |
+
"critical_data_loss", "Data loss — need urgent help",
|
| 103 |
+
"I just noticed 3 months of customer records are missing from our "
|
| 104 |
+
"account. This is critical for our business.",
|
| 105 |
+
"critical_incident", severity="critical",
|
| 106 |
+
),
|
| 107 |
+
CustomerTemplate(
|
| 108 |
+
"critical_security", "Possible unauthorised access",
|
| 109 |
+
"I see logins from IPs in two countries I've never been in. This "
|
| 110 |
+
"looks like a security breach — need urgent help.",
|
| 111 |
+
"critical_incident", severity="critical",
|
| 112 |
+
),
|
| 113 |
+
CustomerTemplate(
|
| 114 |
+
"critical_api_down", "API returning 500 across the board",
|
| 115 |
+
"Every endpoint is returning 500 since ~10 minutes ago. We have a "
|
| 116 |
+
"customer demo in 30 minutes. This is critical.",
|
| 117 |
+
"critical_incident", severity="critical",
|
| 118 |
+
),
|
| 119 |
+
CustomerTemplate(
|
| 120 |
+
"critical_payment_fail", "Payment processor is rejecting all charges",
|
| 121 |
+
"Stripe integration just started rejecting every transaction. We're "
|
| 122 |
+
"losing live sales. Critical priority.",
|
| 123 |
+
"critical_incident", severity="critical",
|
| 124 |
+
),
|
| 125 |
+
CustomerTemplate(
|
| 126 |
+
"critical_auth_locked", "All admins locked out",
|
| 127 |
+
"Nobody on our team can sign into the admin panel since the update. "
|
| 128 |
+
"We can't run the business — this is critical.",
|
| 129 |
+
"critical_incident", severity="critical",
|
| 130 |
+
),
|
| 131 |
+
|
| 132 |
+
# ---------- info requests (missing fields) ----------
|
| 133 |
+
CustomerTemplate(
|
| 134 |
+
"info_missing_order_id", "Question about my invoice",
|
| 135 |
+
"Hi, I got an invoice but I don't recognise the line item. Could you "
|
| 136 |
+
"check what it's for?",
|
| 137 |
+
"info_request", needs_info="order_id",
|
| 138 |
+
),
|
| 139 |
+
CustomerTemplate(
|
| 140 |
+
"info_missing_account", "Help with login",
|
| 141 |
+
"I can't log in anymore. Could you help me get back into my account?",
|
| 142 |
+
"info_request", needs_info="account_email",
|
| 143 |
+
),
|
| 144 |
+
CustomerTemplate(
|
| 145 |
+
"info_missing_date", "Billing concern",
|
| 146 |
+
"I think there was an extra charge recently but I'm not sure when. "
|
| 147 |
+
"Can you check?",
|
| 148 |
+
"info_request", needs_info="charge_date",
|
| 149 |
+
),
|
| 150 |
+
|
| 151 |
+
# ---------- billing / product questions ----------
|
| 152 |
+
CustomerTemplate(
|
| 153 |
+
"billing_tiers", "How do subscription tiers work?",
|
| 154 |
+
"Quick question — what's the difference between Pro and Team tiers? "
|
| 155 |
+
"Are there usage limits on Team?",
|
| 156 |
+
"billing_q",
|
| 157 |
+
),
|
| 158 |
+
CustomerTemplate(
|
| 159 |
+
"billing_prorated", "Does annual billing prorate?",
|
| 160 |
+
"If I upgrade mid-cycle on the annual plan, is the delta prorated or "
|
| 161 |
+
"charged in full?",
|
| 162 |
+
"billing_q",
|
| 163 |
+
),
|
| 164 |
+
CustomerTemplate(
|
| 165 |
+
"billing_invoice_download", "Where do I get past invoices?",
|
| 166 |
+
"Where can I download invoices from prior months? The billing page "
|
| 167 |
+
"only shows the current one.",
|
| 168 |
+
"billing_q",
|
| 169 |
+
),
|
| 170 |
+
|
| 171 |
+
# ---------- chitchat / no action needed ----------
|
| 172 |
+
CustomerTemplate(
|
| 173 |
+
"chitchat_thanks", "Thanks for the quick help",
|
| 174 |
+
"Just wanted to say thanks — the team resolved my issue from last "
|
| 175 |
+
"week really fast. Appreciate it!",
|
| 176 |
+
"chitchat",
|
| 177 |
+
),
|
| 178 |
+
CustomerTemplate(
|
| 179 |
+
"chitchat_feedback", "Love the new dashboard",
|
| 180 |
+
"Wanted to say the new dashboard looks great. Much cleaner than the "
|
| 181 |
+
"old one. Keep it up!",
|
| 182 |
+
"chitchat",
|
| 183 |
+
),
|
| 184 |
+
CustomerTemplate(
|
| 185 |
+
"chitchat_ooo", "Out of office until Monday",
|
| 186 |
+
"Hey team, I'll be out until Monday. No action needed — just flagging "
|
| 187 |
+
"so you're not waiting on me for the ticket from last week.",
|
| 188 |
+
"chitchat",
|
| 189 |
+
),
|
| 190 |
+
]
|
drift_env/environment.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""DriftEnv — OpenEnv-compliant multi-step environment for the
|
| 2 |
+
Policy-Drift support-triage task.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
from typing import Optional, List, Set
|
| 8 |
+
|
| 9 |
+
from drift_env.models import Action, Email, EmailKind, Observation, State, StepResult
|
| 10 |
+
from drift_env.episodes import Episode, generate_episode
|
| 11 |
+
from drift_env.grader import grade_step
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _email_to_summary(email: Email, action_taken: Optional[Action] = None) -> dict:
|
| 15 |
+
entry = {
|
| 16 |
+
"email_id": email.id,
|
| 17 |
+
"kind": email.kind.value,
|
| 18 |
+
"subject": email.subject,
|
| 19 |
+
"body": email.body,
|
| 20 |
+
"sender": email.sender,
|
| 21 |
+
}
|
| 22 |
+
if action_taken is not None:
|
| 23 |
+
entry["action_taken"] = action_taken.action_type.value
|
| 24 |
+
return entry
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _email_for_observation(email: Email) -> Email:
|
| 28 |
+
"""Strip grader-only metadata before exposing to agent."""
|
| 29 |
+
return Email(
|
| 30 |
+
id=email.id,
|
| 31 |
+
kind=email.kind,
|
| 32 |
+
subject=email.subject,
|
| 33 |
+
body=email.body,
|
| 34 |
+
sender=email.sender,
|
| 35 |
+
meta={},
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class DriftEnv:
|
| 40 |
+
def __init__(self) -> None:
|
| 41 |
+
self._episode: Optional[Episode] = None
|
| 42 |
+
self._index: int = 0
|
| 43 |
+
self._cumulative: float = 0.0
|
| 44 |
+
self._done: bool = True
|
| 45 |
+
self._history: List[dict] = []
|
| 46 |
+
self._armed_drifts: Set[str] = set()
|
| 47 |
+
|
| 48 |
+
# ------------------------------------------------------------------
|
| 49 |
+
# OpenEnv API
|
| 50 |
+
# ------------------------------------------------------------------
|
| 51 |
+
def reset(self, seed: int = 0, episode_id: str = "ep_0") -> Observation:
|
| 52 |
+
self._episode = generate_episode(seed=seed, episode_id=episode_id)
|
| 53 |
+
self._index = 0
|
| 54 |
+
self._cumulative = 0.0
|
| 55 |
+
self._done = False
|
| 56 |
+
self._history = []
|
| 57 |
+
self._armed_drifts = set()
|
| 58 |
+
return self._current_observation()
|
| 59 |
+
|
| 60 |
+
def step(self, action: Action) -> StepResult:
|
| 61 |
+
if self._episode is None:
|
| 62 |
+
raise RuntimeError("Call reset() before step().")
|
| 63 |
+
if self._done:
|
| 64 |
+
raise RuntimeError("Episode is done. Call reset() to start a new one.")
|
| 65 |
+
|
| 66 |
+
step_record = self._episode.steps[self._index]
|
| 67 |
+
is_admin = step_record.email.kind == EmailKind.ADMIN
|
| 68 |
+
|
| 69 |
+
reward, breakdown, drift_to_clear = grade_step(
|
| 70 |
+
action=action,
|
| 71 |
+
hint=step_record.correct_action_hint,
|
| 72 |
+
email_meta=step_record.email.meta,
|
| 73 |
+
drift_sensitive_to=step_record.drift_sensitive_to,
|
| 74 |
+
armed_drifts=self._armed_drifts,
|
| 75 |
+
is_admin_email=is_admin,
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
# If agent proved awareness of a drift, clear the arm.
|
| 79 |
+
if drift_to_clear:
|
| 80 |
+
self._armed_drifts.discard(drift_to_clear)
|
| 81 |
+
|
| 82 |
+
# If THIS step is an admin email, arm its drift for future steps.
|
| 83 |
+
if is_admin:
|
| 84 |
+
drift_name = step_record.email.meta.get("drift_event")
|
| 85 |
+
if drift_name:
|
| 86 |
+
self._armed_drifts.add(drift_name)
|
| 87 |
+
|
| 88 |
+
self._cumulative += reward
|
| 89 |
+
|
| 90 |
+
# Record this email + the action for history.
|
| 91 |
+
self._history.append(_email_to_summary(step_record.email, action_taken=action))
|
| 92 |
+
|
| 93 |
+
self._index += 1
|
| 94 |
+
self._done = self._index >= len(self._episode.steps)
|
| 95 |
+
|
| 96 |
+
next_obs = None if self._done else self._current_observation()
|
| 97 |
+
|
| 98 |
+
info = {
|
| 99 |
+
"episode_id": self._episode.id,
|
| 100 |
+
"step": self._index,
|
| 101 |
+
"total_steps": len(self._episode.steps),
|
| 102 |
+
"correct_action_hint": step_record.correct_action_hint,
|
| 103 |
+
"drift_sensitive_to": step_record.drift_sensitive_to,
|
| 104 |
+
"armed_drifts_after": sorted(self._armed_drifts),
|
| 105 |
+
"breakdown": breakdown,
|
| 106 |
+
"cumulative_reward": round(self._cumulative, 4),
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
return StepResult(
|
| 110 |
+
observation=next_obs,
|
| 111 |
+
reward=round(reward, 4),
|
| 112 |
+
done=self._done,
|
| 113 |
+
info=info,
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
def state(self) -> State:
|
| 117 |
+
return State(
|
| 118 |
+
episode_id=self._episode.id if self._episode else None,
|
| 119 |
+
email_index=self._index,
|
| 120 |
+
total_emails=len(self._episode.steps) if self._episode else 0,
|
| 121 |
+
done=self._done,
|
| 122 |
+
cumulative_reward=round(self._cumulative, 4),
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
# ------------------------------------------------------------------
|
| 126 |
+
def _current_observation(self) -> Observation:
|
| 127 |
+
assert self._episode is not None
|
| 128 |
+
step = self._episode.steps[self._index]
|
| 129 |
+
return Observation(
|
| 130 |
+
current_email=_email_for_observation(step.email),
|
| 131 |
+
email_index=self._index,
|
| 132 |
+
total_emails=len(self._episode.steps),
|
| 133 |
+
inbox_history=list(self._history),
|
| 134 |
+
)
|
drift_env/episodes.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Episode generator — deterministic given a seed.
|
| 2 |
+
|
| 3 |
+
Produces a 20-email sequence with 2 admin (drift) emails at positions (3, 11).
|
| 4 |
+
The policy timeline and per-step correct_action_hint are pre-computed so the
|
| 5 |
+
grader can look up ground truth in O(1).
|
| 6 |
+
|
| 7 |
+
Each customer step also records whether it is "drift-sensitive" — i.e. the
|
| 8 |
+
correct action under the current policy is different from what it would have
|
| 9 |
+
been BEFORE the most recent drift event. The grader uses this to award the
|
| 10 |
+
drift-attention bonus.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import random
|
| 16 |
+
from dataclasses import dataclass, field
|
| 17 |
+
from typing import List, Optional
|
| 18 |
+
|
| 19 |
+
from drift_env.emails import CUSTOMER_TEMPLATES, CustomerTemplate
|
| 20 |
+
from drift_env.models import Email, EmailKind
|
| 21 |
+
from drift_env.policy import DEFAULT_POLICY, DRIFT_EVENTS, DriftEvent, Policy
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
EPISODE_LENGTH = 20
|
| 25 |
+
DRIFT_POSITIONS = (3, 11)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@dataclass(frozen=True)
|
| 29 |
+
class EpisodeStep:
|
| 30 |
+
email: Email
|
| 31 |
+
policy_at_step: Policy
|
| 32 |
+
correct_action_hint: dict
|
| 33 |
+
# Drift sensitivity: which drift_event_name (if any) this step proves
|
| 34 |
+
# awareness of. None = not drift-sensitive.
|
| 35 |
+
drift_sensitive_to: Optional[str] = None
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@dataclass(frozen=True)
|
| 39 |
+
class Episode:
|
| 40 |
+
id: str
|
| 41 |
+
steps: List[EpisodeStep]
|
| 42 |
+
drift_timeline: List[tuple[int, str]] = field(default_factory=list)
|
| 43 |
+
# e.g. [(3, "sla_2hr"), (11, "refund_cap_200")]
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _make_admin_email(event: DriftEvent, idx: int) -> Email:
|
| 47 |
+
return Email(
|
| 48 |
+
id=f"admin_{idx}_{event.name}",
|
| 49 |
+
kind=EmailKind.ADMIN,
|
| 50 |
+
subject=event.admin_subject,
|
| 51 |
+
body=event.admin_body,
|
| 52 |
+
sender="ops@company.com",
|
| 53 |
+
meta={"drift_event": event.name},
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _make_customer_email(t: CustomerTemplate, idx: int) -> Email:
|
| 58 |
+
return Email(
|
| 59 |
+
id=f"c_{idx}_{t.id}",
|
| 60 |
+
kind=EmailKind.CUSTOMER,
|
| 61 |
+
subject=t.subject,
|
| 62 |
+
body=t.body,
|
| 63 |
+
sender="customer@example.com",
|
| 64 |
+
meta={
|
| 65 |
+
"template_id": t.id,
|
| 66 |
+
"kind": t.kind,
|
| 67 |
+
"refund_amount": t.refund_amount,
|
| 68 |
+
"severity": t.severity,
|
| 69 |
+
"needs_info": t.needs_info,
|
| 70 |
+
},
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _correct_action_hint(template: CustomerTemplate, p: Policy) -> dict:
|
| 75 |
+
"""Pre-computed ground-truth correct action for (template, policy)."""
|
| 76 |
+
if template.kind == "refund":
|
| 77 |
+
if template.refund_amount is None:
|
| 78 |
+
return {"action_type": "request_info", "info_field": "refund_amount"}
|
| 79 |
+
if template.refund_amount <= p.refund_cap:
|
| 80 |
+
return {"action_type": "approve_refund", "refund_amount": template.refund_amount}
|
| 81 |
+
return {"action_type": "escalate", "escalation_tier": "manager"}
|
| 82 |
+
|
| 83 |
+
if template.kind == "critical_incident":
|
| 84 |
+
return {
|
| 85 |
+
"action_type": "escalate",
|
| 86 |
+
"escalation_tier": p.critical_escalation_tier,
|
| 87 |
+
"followup_hours": p.sla_hours_critical,
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
if template.kind == "info_request":
|
| 91 |
+
return {"action_type": "request_info", "info_field": template.needs_info}
|
| 92 |
+
|
| 93 |
+
if template.kind == "billing_q":
|
| 94 |
+
return {"action_type": "reply"}
|
| 95 |
+
|
| 96 |
+
if template.kind == "chitchat":
|
| 97 |
+
return {"action_type": "close", "resolution_code": "no_action_needed"}
|
| 98 |
+
|
| 99 |
+
return {"action_type": "reply"}
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def _hints_differ(a: dict, b: dict) -> bool:
|
| 103 |
+
"""Do two action hints prescribe a meaningfully different action?
|
| 104 |
+
We ignore free-text fields (reply_text) and compare the structural fields.
|
| 105 |
+
"""
|
| 106 |
+
keys = ("action_type", "refund_amount", "escalation_tier",
|
| 107 |
+
"followup_hours", "resolution_code", "info_field")
|
| 108 |
+
return any(a.get(k) != b.get(k) for k in keys)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def generate_episode(seed: int = 0, episode_id: str = "ep_0") -> Episode:
|
| 112 |
+
rng = random.Random(seed)
|
| 113 |
+
|
| 114 |
+
# Sample 2 drift events from different TYPES so they stack.
|
| 115 |
+
drift_by_type = {
|
| 116 |
+
"refund": ["refund_cap_50", "refund_cap_25", "refund_cap_200"],
|
| 117 |
+
"escalate": ["escalate_manager", "escalate_tier_1", "escalate_keep_tier_2"],
|
| 118 |
+
"sla": ["sla_2hr", "sla_4hr", "sla_48hr"],
|
| 119 |
+
}
|
| 120 |
+
types = rng.sample(list(drift_by_type.keys()), k=2)
|
| 121 |
+
drift_names = [rng.choice(drift_by_type[t]) for t in types]
|
| 122 |
+
drifts = [DRIFT_EVENTS[n] for n in drift_names]
|
| 123 |
+
|
| 124 |
+
# Sample customer templates with a bias toward drift-sensitive kinds.
|
| 125 |
+
drift_sensitive_pool = [t for t in CUSTOMER_TEMPLATES
|
| 126 |
+
if t.kind in ("refund", "critical_incident")]
|
| 127 |
+
other_pool = [t for t in CUSTOMER_TEMPLATES
|
| 128 |
+
if t.kind not in ("refund", "critical_incident")]
|
| 129 |
+
|
| 130 |
+
customer_count = EPISODE_LENGTH - len(drifts)
|
| 131 |
+
picks: List[CustomerTemplate] = []
|
| 132 |
+
for _ in range(customer_count):
|
| 133 |
+
pool = drift_sensitive_pool if rng.random() < 0.7 else other_pool
|
| 134 |
+
picks.append(rng.choice(pool))
|
| 135 |
+
|
| 136 |
+
# Walk through episode positions, interleaving admin emails.
|
| 137 |
+
steps: List[EpisodeStep] = []
|
| 138 |
+
current_policy = DEFAULT_POLICY
|
| 139 |
+
policy_before_last_drift: Optional[Policy] = None
|
| 140 |
+
last_drift_name: Optional[str] = None
|
| 141 |
+
|
| 142 |
+
drift_queue = list(zip(DRIFT_POSITIONS, drifts))
|
| 143 |
+
cust_iter = iter(picks)
|
| 144 |
+
timeline: List[tuple[int, str]] = []
|
| 145 |
+
|
| 146 |
+
for idx in range(EPISODE_LENGTH):
|
| 147 |
+
if drift_queue and drift_queue[0][0] == idx:
|
| 148 |
+
_, event = drift_queue.pop(0)
|
| 149 |
+
admin_email = _make_admin_email(event, idx)
|
| 150 |
+
steps.append(EpisodeStep(
|
| 151 |
+
email=admin_email,
|
| 152 |
+
policy_at_step=current_policy,
|
| 153 |
+
correct_action_hint={"action_type": "close",
|
| 154 |
+
"resolution_code": "policy_acknowledged"},
|
| 155 |
+
drift_sensitive_to=None,
|
| 156 |
+
))
|
| 157 |
+
# AFTER processing this admin email, record prior policy + update.
|
| 158 |
+
policy_before_last_drift = current_policy
|
| 159 |
+
current_policy = event.apply(current_policy)
|
| 160 |
+
last_drift_name = event.name
|
| 161 |
+
timeline.append((idx, event.name))
|
| 162 |
+
else:
|
| 163 |
+
template = next(cust_iter)
|
| 164 |
+
cust_email = _make_customer_email(template, idx)
|
| 165 |
+
correct_now = _correct_action_hint(template, current_policy)
|
| 166 |
+
|
| 167 |
+
# Drift sensitivity: does the answer change vs pre-drift policy?
|
| 168 |
+
sensitive_to: Optional[str] = None
|
| 169 |
+
if last_drift_name and policy_before_last_drift is not None:
|
| 170 |
+
correct_pre = _correct_action_hint(template, policy_before_last_drift)
|
| 171 |
+
if _hints_differ(correct_now, correct_pre):
|
| 172 |
+
sensitive_to = last_drift_name
|
| 173 |
+
|
| 174 |
+
steps.append(EpisodeStep(
|
| 175 |
+
email=cust_email,
|
| 176 |
+
policy_at_step=current_policy,
|
| 177 |
+
correct_action_hint=correct_now,
|
| 178 |
+
drift_sensitive_to=sensitive_to,
|
| 179 |
+
))
|
| 180 |
+
|
| 181 |
+
return Episode(id=episode_id, steps=steps, drift_timeline=timeline)
|
drift_env/grader.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Deterministic 3-component grader.
|
| 2 |
+
|
| 3 |
+
Per-step reward in [0.0, 2.0] composed of:
|
| 4 |
+
1. compliance 0.0 – 1.0 — did the action respect current policy?
|
| 5 |
+
2. appropriateness 0.0 – 0.5 — was the action TYPE sensible for the email?
|
| 6 |
+
3. drift_bonus 0.0 – 0.5 — first correct drift-aware action after a
|
| 7 |
+
drift fires once per drift event.
|
| 8 |
+
|
| 9 |
+
No LLM-as-judge, no randomness. Ground truth is pre-computed in
|
| 10 |
+
`episodes.EpisodeStep.correct_action_hint`, so the grader is a pure function
|
| 11 |
+
of (action, correct_hint, email_meta, drift_sensitive_to, armed_drifts).
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
from typing import Tuple
|
| 17 |
+
|
| 18 |
+
from drift_env.models import Action
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
# --------------------------------------------------------------------------
|
| 22 |
+
# Compliance — exact structural match on the fields that define "correct"
|
| 23 |
+
# --------------------------------------------------------------------------
|
| 24 |
+
def _compliance(action: Action, hint: dict) -> float:
|
| 25 |
+
want = hint.get("action_type")
|
| 26 |
+
got = action.action_type.value
|
| 27 |
+
if got != want:
|
| 28 |
+
return 0.0
|
| 29 |
+
|
| 30 |
+
# Field-level exact match depending on action_type
|
| 31 |
+
if want == "approve_refund":
|
| 32 |
+
return 1.0 if action.refund_amount == hint.get("refund_amount") else 0.5
|
| 33 |
+
if want == "escalate":
|
| 34 |
+
tier_ok = action.escalation_tier == hint.get("escalation_tier")
|
| 35 |
+
hours_want = hint.get("followup_hours")
|
| 36 |
+
if hours_want is None:
|
| 37 |
+
return 1.0 if tier_ok else 0.5
|
| 38 |
+
hours_ok = action.followup_hours == hours_want
|
| 39 |
+
if tier_ok and hours_ok:
|
| 40 |
+
return 1.0
|
| 41 |
+
if tier_ok or hours_ok:
|
| 42 |
+
return 0.7
|
| 43 |
+
return 0.4
|
| 44 |
+
if want == "request_info":
|
| 45 |
+
return 1.0 if action.info_field == hint.get("info_field") else 0.5
|
| 46 |
+
if want == "close":
|
| 47 |
+
return 1.0 if action.resolution_code == hint.get("resolution_code") else 0.7
|
| 48 |
+
|
| 49 |
+
# Plain reply / anything else: action_type match is enough.
|
| 50 |
+
return 1.0
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# --------------------------------------------------------------------------
|
| 54 |
+
# Appropriateness — is the chosen action TYPE reasonable for this email kind?
|
| 55 |
+
# Gives partial credit when the agent picks a sensible action type even if
|
| 56 |
+
# specific fields are wrong. Caps at 0.5.
|
| 57 |
+
# --------------------------------------------------------------------------
|
| 58 |
+
_APPROPRIATE_BY_KIND = {
|
| 59 |
+
"refund": {"approve_refund", "escalate", "request_info"},
|
| 60 |
+
"critical_incident": {"escalate"},
|
| 61 |
+
"info_request": {"request_info"},
|
| 62 |
+
"billing_q": {"reply"},
|
| 63 |
+
"chitchat": {"close"},
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _appropriateness(action: Action, email_meta: dict) -> float:
|
| 68 |
+
kind = email_meta.get("kind")
|
| 69 |
+
if kind is None:
|
| 70 |
+
return 0.0
|
| 71 |
+
ok_set = _APPROPRIATE_BY_KIND.get(kind, set())
|
| 72 |
+
if action.action_type.value in ok_set:
|
| 73 |
+
return 0.5
|
| 74 |
+
# Admin emails are handled by the env; this path is customer-only.
|
| 75 |
+
return 0.0
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
# --------------------------------------------------------------------------
|
| 79 |
+
# Drift bonus — +0.5 the FIRST time the agent nails a drift-sensitive step
|
| 80 |
+
# after the corresponding drift has fired. Stateful across the episode,
|
| 81 |
+
# tracked by the environment in `armed_drifts`.
|
| 82 |
+
# --------------------------------------------------------------------------
|
| 83 |
+
def _drift_bonus(
|
| 84 |
+
drift_sensitive_to: str | None,
|
| 85 |
+
armed_drifts: set[str],
|
| 86 |
+
compliance_score: float,
|
| 87 |
+
) -> Tuple[float, str | None]:
|
| 88 |
+
"""Returns (bonus, drift_to_clear_if_awarded)."""
|
| 89 |
+
if drift_sensitive_to is None:
|
| 90 |
+
return 0.0, None
|
| 91 |
+
if drift_sensitive_to not in armed_drifts:
|
| 92 |
+
return 0.0, None
|
| 93 |
+
if compliance_score < 1.0:
|
| 94 |
+
return 0.0, None
|
| 95 |
+
return 0.5, drift_sensitive_to
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
# --------------------------------------------------------------------------
|
| 99 |
+
# Admin emails — handled as a separate scoring path (not drift-sensitive,
|
| 100 |
+
# grader doesn't compute appropriateness for them).
|
| 101 |
+
# --------------------------------------------------------------------------
|
| 102 |
+
def _grade_admin(action: Action) -> float:
|
| 103 |
+
if action.action_type.value == "close":
|
| 104 |
+
# Full credit regardless of resolution_code — we don't want to
|
| 105 |
+
# punish harmless reword-ing of the ack.
|
| 106 |
+
return 1.0
|
| 107 |
+
# Any non-close action on an admin email is a minor error.
|
| 108 |
+
return 0.2
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
# --------------------------------------------------------------------------
|
| 112 |
+
# Top-level: returns (total_reward, breakdown_dict, drift_to_clear)
|
| 113 |
+
# --------------------------------------------------------------------------
|
| 114 |
+
def grade_step(
|
| 115 |
+
action: Action,
|
| 116 |
+
hint: dict,
|
| 117 |
+
email_meta: dict,
|
| 118 |
+
drift_sensitive_to: str | None,
|
| 119 |
+
armed_drifts: set[str],
|
| 120 |
+
is_admin_email: bool,
|
| 121 |
+
) -> Tuple[float, dict, str | None]:
|
| 122 |
+
if is_admin_email:
|
| 123 |
+
reward = _grade_admin(action)
|
| 124 |
+
return reward, {"compliance": reward, "appropriateness": 0.0,
|
| 125 |
+
"drift_bonus": 0.0}, None
|
| 126 |
+
|
| 127 |
+
compliance = _compliance(action, hint)
|
| 128 |
+
appropriateness = _appropriateness(action, email_meta)
|
| 129 |
+
drift_bonus, drift_to_clear = _drift_bonus(
|
| 130 |
+
drift_sensitive_to, armed_drifts, compliance,
|
| 131 |
+
)
|
| 132 |
+
total = compliance + appropriateness + drift_bonus
|
| 133 |
+
breakdown = {
|
| 134 |
+
"compliance": round(compliance, 4),
|
| 135 |
+
"appropriateness": round(appropriateness, 4),
|
| 136 |
+
"drift_bonus": round(drift_bonus, 4),
|
| 137 |
+
}
|
| 138 |
+
return round(total, 4), breakdown, drift_to_clear
|
drift_env/llm_agent.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""A thin LLM agent that reads the current email + inbox history and emits
|
| 2 |
+
an Action. Used by both the eval harness and the onsite inference pipeline.
|
| 3 |
+
|
| 4 |
+
Uses the OpenAI Python client against any OpenAI-compatible endpoint
|
| 5 |
+
(HF router, Groq, etc). No Anthropic / Google SDKs — hackathon requires
|
| 6 |
+
OpenAI client only.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import json
|
| 12 |
+
from typing import Optional
|
| 13 |
+
|
| 14 |
+
from openai import OpenAI
|
| 15 |
+
|
| 16 |
+
from drift_env.models import Action, ActionType, Observation
|
| 17 |
+
from drift_env.prompts import SYSTEM_PROMPT, render_user_prompt
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
FALLBACK = Action(action_type=ActionType.CLOSE, resolution_code="error_fallback")
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _parse_action(raw: str) -> Action:
|
| 24 |
+
"""Parse the LLM output into an Action. Returns FALLBACK on failure."""
|
| 25 |
+
text = raw.strip()
|
| 26 |
+
# strip markdown fences if present
|
| 27 |
+
if text.startswith("```"):
|
| 28 |
+
lines = [l for l in text.split("\n") if not l.strip().startswith("```")]
|
| 29 |
+
text = "\n".join(lines).strip()
|
| 30 |
+
# Try to locate a JSON object if surrounded by prose
|
| 31 |
+
if not text.startswith("{"):
|
| 32 |
+
start = text.find("{")
|
| 33 |
+
end = text.rfind("}")
|
| 34 |
+
if start == -1 or end == -1 or end < start:
|
| 35 |
+
return FALLBACK
|
| 36 |
+
text = text[start:end + 1]
|
| 37 |
+
try:
|
| 38 |
+
obj = json.loads(text)
|
| 39 |
+
except json.JSONDecodeError:
|
| 40 |
+
return FALLBACK
|
| 41 |
+
a_type = obj.get("action_type")
|
| 42 |
+
if a_type not in {e.value for e in ActionType}:
|
| 43 |
+
return FALLBACK
|
| 44 |
+
try:
|
| 45 |
+
return Action(
|
| 46 |
+
action_type=ActionType(a_type),
|
| 47 |
+
refund_amount=obj.get("refund_amount"),
|
| 48 |
+
escalation_tier=obj.get("escalation_tier"),
|
| 49 |
+
followup_hours=obj.get("followup_hours"),
|
| 50 |
+
resolution_code=obj.get("resolution_code"),
|
| 51 |
+
info_field=obj.get("info_field"),
|
| 52 |
+
reply_text=obj.get("reply_text"),
|
| 53 |
+
)
|
| 54 |
+
except Exception:
|
| 55 |
+
return FALLBACK
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class LLMAgent:
|
| 59 |
+
def __init__(
|
| 60 |
+
self,
|
| 61 |
+
api_key: str,
|
| 62 |
+
base_url: str,
|
| 63 |
+
model: str,
|
| 64 |
+
temperature: float = 0.0,
|
| 65 |
+
max_tokens: int = 200,
|
| 66 |
+
system_prompt: str = SYSTEM_PROMPT,
|
| 67 |
+
) -> None:
|
| 68 |
+
self.client = OpenAI(api_key=api_key, base_url=base_url)
|
| 69 |
+
self.model = model
|
| 70 |
+
self.temperature = temperature
|
| 71 |
+
self.max_tokens = max_tokens
|
| 72 |
+
self.system_prompt = system_prompt
|
| 73 |
+
|
| 74 |
+
def act(self, obs: Observation) -> tuple[Action, str]:
|
| 75 |
+
"""Returns (parsed_action, raw_text) for inspection."""
|
| 76 |
+
user_msg = render_user_prompt(obs)
|
| 77 |
+
try:
|
| 78 |
+
completion = self.client.chat.completions.create(
|
| 79 |
+
model=self.model,
|
| 80 |
+
messages=[
|
| 81 |
+
{"role": "system", "content": self.system_prompt},
|
| 82 |
+
{"role": "user", "content": user_msg},
|
| 83 |
+
],
|
| 84 |
+
temperature=self.temperature,
|
| 85 |
+
max_tokens=self.max_tokens,
|
| 86 |
+
)
|
| 87 |
+
raw = completion.choices[0].message.content or ""
|
| 88 |
+
except Exception as e:
|
| 89 |
+
return FALLBACK, f"ERROR: {e}"
|
| 90 |
+
return _parse_action(raw), raw
|
drift_env/models.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Typed Pydantic models for the Policy Drift environment."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from enum import Enum
|
| 6 |
+
from typing import Optional, List
|
| 7 |
+
from pydantic import BaseModel, Field
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class EmailKind(str, Enum):
|
| 11 |
+
CUSTOMER = "customer"
|
| 12 |
+
ADMIN = "admin"
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class Email(BaseModel):
|
| 16 |
+
"""A single email visible to the agent."""
|
| 17 |
+
id: str
|
| 18 |
+
kind: EmailKind
|
| 19 |
+
subject: str
|
| 20 |
+
body: str
|
| 21 |
+
sender: str
|
| 22 |
+
# For customer emails only — drift-relevant metadata used by the grader
|
| 23 |
+
# (agent does NOT see these fields; they're stripped before observation).
|
| 24 |
+
meta: dict = Field(default_factory=dict)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class ActionType(str, Enum):
|
| 28 |
+
REPLY = "reply"
|
| 29 |
+
APPROVE_REFUND = "approve_refund"
|
| 30 |
+
ESCALATE = "escalate"
|
| 31 |
+
SCHEDULE_FOLLOWUP = "schedule_followup"
|
| 32 |
+
CLOSE = "close"
|
| 33 |
+
REQUEST_INFO = "request_info"
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class Action(BaseModel):
|
| 37 |
+
"""A single discrete action with optional scalar/string args."""
|
| 38 |
+
action_type: ActionType
|
| 39 |
+
refund_amount: Optional[float] = None # approve_refund
|
| 40 |
+
escalation_tier: Optional[str] = None # escalate: tier_1 | tier_2 | manager
|
| 41 |
+
followup_hours: Optional[int] = None # schedule_followup
|
| 42 |
+
resolution_code: Optional[str] = None # close
|
| 43 |
+
info_field: Optional[str] = None # request_info
|
| 44 |
+
reply_text: Optional[str] = None # reply
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class Observation(BaseModel):
|
| 48 |
+
"""What the agent sees. Current policy is NOT exposed — agent must infer
|
| 49 |
+
it from admin emails in the inbox history."""
|
| 50 |
+
current_email: Email
|
| 51 |
+
email_index: int # 0-based position in episode
|
| 52 |
+
total_emails: int
|
| 53 |
+
inbox_history: List[dict] = Field(default_factory=list)
|
| 54 |
+
# Each history entry: {email_id, kind, subject, body_summary, action_taken?}
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class StepResult(BaseModel):
|
| 58 |
+
observation: Optional[Observation]
|
| 59 |
+
reward: float
|
| 60 |
+
done: bool
|
| 61 |
+
info: dict
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class State(BaseModel):
|
| 65 |
+
"""Env state snapshot for debugging / validator."""
|
| 66 |
+
episode_id: Optional[str]
|
| 67 |
+
email_index: int
|
| 68 |
+
total_emails: int
|
| 69 |
+
done: bool
|
| 70 |
+
cumulative_reward: float
|
drift_env/policy.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Policy state + drift scenarios.
|
| 2 |
+
|
| 3 |
+
The policy is the hidden ground-truth rule set the agent must infer from
|
| 4 |
+
admin emails. The environment updates this when an admin email is "read."
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
from dataclasses import dataclass, field, replace
|
| 10 |
+
from typing import Literal
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
Tier = Literal["tier_1", "tier_2", "manager"]
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@dataclass(frozen=True)
|
| 17 |
+
class Policy:
|
| 18 |
+
"""Current policy rules. Immutable — drift events produce new instances."""
|
| 19 |
+
refund_cap: float # max auto-approvable refund USD
|
| 20 |
+
critical_escalation_tier: Tier # where "critical" issues must go
|
| 21 |
+
sla_hours_critical: int # response SLA for critical issues
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# ---------------------------------------------------------------------------
|
| 25 |
+
# Drift scenarios — each is a (trigger, delta) pair describing one change.
|
| 26 |
+
# ---------------------------------------------------------------------------
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
Direction = Literal["tightening", "loosening", "neutral"]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@dataclass(frozen=True)
|
| 33 |
+
class DriftEvent:
|
| 34 |
+
"""A single drift event delivered via an admin email."""
|
| 35 |
+
name: str
|
| 36 |
+
# Direction captures whether the new rule is STRICTER than the default
|
| 37 |
+
# ("tightening" — agents with internet priors will get this wrong),
|
| 38 |
+
# LOOSER than the default ("loosening" — base models already handle this
|
| 39 |
+
# by accident because it matches their lenient prior), or unchanged
|
| 40 |
+
# ("neutral" — acts as a distractor). This is the single most important
|
| 41 |
+
# piece of instrumentation for the pitch story: measure drift-sensitive
|
| 42 |
+
# accuracy separately on tightening vs loosening drifts.
|
| 43 |
+
direction: Direction
|
| 44 |
+
admin_subject: str
|
| 45 |
+
admin_body: str
|
| 46 |
+
|
| 47 |
+
# What fields does this drift change? Only the mentioned ones are applied.
|
| 48 |
+
new_refund_cap: float | None = None
|
| 49 |
+
new_critical_escalation_tier: Tier | None = None
|
| 50 |
+
new_sla_hours_critical: int | None = None
|
| 51 |
+
|
| 52 |
+
def apply(self, p: Policy) -> Policy:
|
| 53 |
+
return replace(
|
| 54 |
+
p,
|
| 55 |
+
refund_cap=self.new_refund_cap if self.new_refund_cap is not None else p.refund_cap,
|
| 56 |
+
critical_escalation_tier=self.new_critical_escalation_tier or p.critical_escalation_tier,
|
| 57 |
+
sla_hours_critical=self.new_sla_hours_critical if self.new_sla_hours_critical is not None else p.sla_hours_critical,
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
# Starting policy — the "internet prior" that base models will default to.
|
| 62 |
+
DEFAULT_POLICY = Policy(
|
| 63 |
+
refund_cap=100.0,
|
| 64 |
+
critical_escalation_tier="tier_2",
|
| 65 |
+
sla_hours_critical=24,
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
# 3 drift TYPES × 3 variations = 9 configs. Each variation stays realistic
|
| 70 |
+
# but moves the correct answer far from the default prior.
|
| 71 |
+
DRIFT_EVENTS: dict[str, DriftEvent] = {
|
| 72 |
+
# --- refund cap changes -------------------------------------------------
|
| 73 |
+
"refund_cap_50": DriftEvent(
|
| 74 |
+
name="refund_cap_50",
|
| 75 |
+
direction="tightening",
|
| 76 |
+
admin_subject="Policy update: refund approval threshold",
|
| 77 |
+
admin_body=(
|
| 78 |
+
"Team — effective immediately, the auto-approval cap for customer "
|
| 79 |
+
"refunds is lowered from $100 to $50. Any refund above $50 must be "
|
| 80 |
+
"escalated to a manager. Please update your workflow accordingly."
|
| 81 |
+
),
|
| 82 |
+
new_refund_cap=50.0,
|
| 83 |
+
),
|
| 84 |
+
"refund_cap_25": DriftEvent(
|
| 85 |
+
name="refund_cap_25",
|
| 86 |
+
direction="tightening",
|
| 87 |
+
admin_subject="Q2 budget controls — refund changes",
|
| 88 |
+
admin_body=(
|
| 89 |
+
"Hi all, due to Q2 budget tightening the refund auto-approval cap "
|
| 90 |
+
"is now $25 (previously $100). Anything over $25 goes to manager "
|
| 91 |
+
"review. This is effective for all tickets from today onward."
|
| 92 |
+
),
|
| 93 |
+
new_refund_cap=25.0,
|
| 94 |
+
),
|
| 95 |
+
"refund_cap_200": DriftEvent(
|
| 96 |
+
name="refund_cap_200",
|
| 97 |
+
direction="loosening",
|
| 98 |
+
admin_subject="Refund policy loosened for retention push",
|
| 99 |
+
admin_body=(
|
| 100 |
+
"Everyone — for the next retention campaign, refund auto-approval "
|
| 101 |
+
"cap is raised from $100 to $200. You can approve refunds up to "
|
| 102 |
+
"$200 without escalation. Use judgement on obvious abuse cases."
|
| 103 |
+
),
|
| 104 |
+
new_refund_cap=200.0,
|
| 105 |
+
),
|
| 106 |
+
|
| 107 |
+
# --- escalation routing changes ----------------------------------------
|
| 108 |
+
"escalate_manager": DriftEvent(
|
| 109 |
+
name="escalate_manager",
|
| 110 |
+
direction="tightening",
|
| 111 |
+
admin_subject="Critical-issue routing change",
|
| 112 |
+
admin_body=(
|
| 113 |
+
"FYI — all critical-severity customer issues must now route "
|
| 114 |
+
"directly to a manager (not Tier 2 as before). This is to comply "
|
| 115 |
+
"with the new incident-response policy. Effective immediately."
|
| 116 |
+
),
|
| 117 |
+
new_critical_escalation_tier="manager",
|
| 118 |
+
),
|
| 119 |
+
"escalate_tier_1": DriftEvent(
|
| 120 |
+
name="escalate_tier_1",
|
| 121 |
+
direction="loosening",
|
| 122 |
+
admin_subject="Tier 1 expanded — critical handling",
|
| 123 |
+
admin_body=(
|
| 124 |
+
"Team — we've trained Tier 1 on critical incident handling. Route "
|
| 125 |
+
"critical issues to Tier 1 now (down from Tier 2). This cuts "
|
| 126 |
+
"response time and lets Tier 2 focus on deep investigations."
|
| 127 |
+
),
|
| 128 |
+
new_critical_escalation_tier="tier_1",
|
| 129 |
+
),
|
| 130 |
+
"escalate_keep_tier_2": DriftEvent(
|
| 131 |
+
name="escalate_keep_tier_2",
|
| 132 |
+
direction="neutral",
|
| 133 |
+
admin_subject="Routing confirmation — no change",
|
| 134 |
+
admin_body=(
|
| 135 |
+
"Quick confirmation: critical issues continue to route to Tier 2. "
|
| 136 |
+
"No change from existing workflow. Ignore any conflicting updates "
|
| 137 |
+
"you may have seen on Slack earlier this week."
|
| 138 |
+
),
|
| 139 |
+
new_critical_escalation_tier="tier_2",
|
| 140 |
+
),
|
| 141 |
+
|
| 142 |
+
# --- SLA changes --------------------------------------------------------
|
| 143 |
+
"sla_2hr": DriftEvent(
|
| 144 |
+
name="sla_2hr",
|
| 145 |
+
direction="tightening",
|
| 146 |
+
admin_subject="Critical SLA tightened to 2 hours",
|
| 147 |
+
admin_body=(
|
| 148 |
+
"Per the updated enterprise contract, critical-severity issues "
|
| 149 |
+
"now have a 2-hour response SLA (was 24 hours). Schedule any "
|
| 150 |
+
"follow-ups on critical tickets within 2 hours of receipt."
|
| 151 |
+
),
|
| 152 |
+
new_sla_hours_critical=2,
|
| 153 |
+
),
|
| 154 |
+
"sla_4hr": DriftEvent(
|
| 155 |
+
name="sla_4hr",
|
| 156 |
+
direction="tightening",
|
| 157 |
+
admin_subject="SLA adjustment — critical issues",
|
| 158 |
+
admin_body=(
|
| 159 |
+
"Small change — critical-issue response SLA is now 4 hours "
|
| 160 |
+
"(previously 24 hours). Please adjust your follow-up scheduling. "
|
| 161 |
+
"Non-critical SLAs are unchanged."
|
| 162 |
+
),
|
| 163 |
+
new_sla_hours_critical=4,
|
| 164 |
+
),
|
| 165 |
+
"sla_48hr": DriftEvent(
|
| 166 |
+
name="sla_48hr",
|
| 167 |
+
direction="loosening",
|
| 168 |
+
admin_subject="SLA relaxed during platform migration",
|
| 169 |
+
admin_body=(
|
| 170 |
+
"During the platform migration this week, critical-issue SLA is "
|
| 171 |
+
"temporarily extended to 48 hours to give the infra team room. "
|
| 172 |
+
"Please batch follow-ups accordingly."
|
| 173 |
+
),
|
| 174 |
+
new_sla_hours_critical=48,
|
| 175 |
+
),
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def list_drift_events() -> list[str]:
|
| 180 |
+
return list(DRIFT_EVENTS.keys())
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def drift_direction(name: str | None) -> Direction | None:
|
| 184 |
+
"""Look up the direction label of a drift event by name. Returns None if
|
| 185 |
+
the name is unknown or None."""
|
| 186 |
+
if name is None:
|
| 187 |
+
return None
|
| 188 |
+
ev = DRIFT_EVENTS.get(name)
|
| 189 |
+
return ev.direction if ev else None
|
drift_env/prompts.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared prompt rendering used by both the inference agent and the
|
| 2 |
+
training-dataset generator. Keeping it in one place guarantees the training
|
| 3 |
+
distribution matches the inference distribution exactly.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
from drift_env.models import Observation
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
SYSTEM_PROMPT = """You are a customer-support triage agent.
|
| 12 |
+
|
| 13 |
+
Your inbox contains a mix of:
|
| 14 |
+
- CUSTOMER emails: regular tickets (refunds, complaints, questions, chit-chat).
|
| 15 |
+
- ADMIN emails from ops@company.com: internal policy announcements that
|
| 16 |
+
change the rules you must follow from that point onwards.
|
| 17 |
+
|
| 18 |
+
You must read the CURRENT email and reply with exactly one action, formatted
|
| 19 |
+
as a single JSON object. Allowed actions and fields:
|
| 20 |
+
|
| 21 |
+
{"action_type": "reply"}
|
| 22 |
+
{"action_type": "approve_refund", "refund_amount": <number>}
|
| 23 |
+
{"action_type": "escalate", "escalation_tier": "tier_1" | "tier_2" | "manager", "followup_hours": <int>}
|
| 24 |
+
{"action_type": "schedule_followup", "followup_hours": <int>}
|
| 25 |
+
{"action_type": "close", "resolution_code": <short string>}
|
| 26 |
+
{"action_type": "request_info", "info_field": <short string>}
|
| 27 |
+
|
| 28 |
+
Rules of thumb (apply the MOST RECENT admin policy you have seen):
|
| 29 |
+
- Refunds at or below the current auto-approval cap -> approve_refund with
|
| 30 |
+
the requested amount. Above the cap -> escalate to manager.
|
| 31 |
+
- Critical incidents -> escalate to the currently-mandated tier with a
|
| 32 |
+
followup_hours matching the current critical-SLA.
|
| 33 |
+
- Admin email -> close with resolution_code "policy_acknowledged".
|
| 34 |
+
- Questions you cannot answer without a detail the customer omitted
|
| 35 |
+
-> request_info with the missing field name (e.g. order_id, account_email).
|
| 36 |
+
- Pure thank-you / FYI / chit-chat -> close with resolution_code
|
| 37 |
+
"no_action_needed".
|
| 38 |
+
- Product / how-does-this-work questions -> reply.
|
| 39 |
+
|
| 40 |
+
Reply with ONLY the JSON object. No prose, no markdown."""
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def render_history(history: list[dict], last_n: int = 8, max_body_chars: int = 200) -> str:
|
| 44 |
+
"""Compact text rendering of prior inbox entries."""
|
| 45 |
+
if not history:
|
| 46 |
+
return "(no prior emails in this session)"
|
| 47 |
+
shown = history[-last_n:]
|
| 48 |
+
lines = []
|
| 49 |
+
for h in shown:
|
| 50 |
+
prefix = "[ADMIN]" if h["kind"] == "admin" else "[CUSTOMER]"
|
| 51 |
+
action = h.get("action_taken", "?")
|
| 52 |
+
body = h["body"]
|
| 53 |
+
if len(body) > max_body_chars:
|
| 54 |
+
body = body[:max_body_chars] + "..."
|
| 55 |
+
lines.append(
|
| 56 |
+
f"{prefix} subject={h['subject']!r}\n body: {body}\n action_taken: {action}"
|
| 57 |
+
)
|
| 58 |
+
return "\n".join(lines)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def render_user_prompt(obs: Observation) -> str:
|
| 62 |
+
hist = render_history(obs.inbox_history)
|
| 63 |
+
cur = obs.current_email
|
| 64 |
+
return (
|
| 65 |
+
f"INBOX SO FAR (most recent last):\n{hist}\n\n"
|
| 66 |
+
f"------\nCURRENT EMAIL (#{obs.email_index + 1} of {obs.total_emails}):\n"
|
| 67 |
+
f" from: {cur.sender}\n subject: {cur.subject}\n body: {cur.body}\n\n"
|
| 68 |
+
f"Reply with exactly one JSON action."
|
| 69 |
+
)
|
drift_env/server/__init__.py
ADDED
|
File without changes
|
drift_env/server/app.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FastAPI server for the DriftEnv. Same OpenEnv contract as round 1."""
|
| 2 |
+
|
| 3 |
+
from typing import Optional
|
| 4 |
+
|
| 5 |
+
from fastapi import FastAPI, HTTPException
|
| 6 |
+
from pydantic import BaseModel
|
| 7 |
+
|
| 8 |
+
from drift_env.environment import DriftEnv
|
| 9 |
+
from drift_env.models import Action, Observation, State, StepResult
|
| 10 |
+
|
| 11 |
+
app = FastAPI(title="LeniencyBench", version="0.1.0")
|
| 12 |
+
env = DriftEnv()
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@app.get("/")
|
| 16 |
+
def root():
|
| 17 |
+
return {
|
| 18 |
+
"name": "LeniencyBench",
|
| 19 |
+
"code_name": "policy-drift",
|
| 20 |
+
"version": "0.1.0",
|
| 21 |
+
"description": (
|
| 22 |
+
"An OpenEnv benchmark that measures and trains out LLM leniency "
|
| 23 |
+
"bias: the tendency to apply old/loose policies even after an "
|
| 24 |
+
"admin message tightens the rule. 20-email customer-support "
|
| 25 |
+
"episodes with 2 policy drifts per episode at fixed positions."
|
| 26 |
+
),
|
| 27 |
+
"endpoints": ["/reset", "/step", "/state"],
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class ResetRequest(BaseModel):
|
| 32 |
+
seed: int = 0
|
| 33 |
+
episode_id: str = "ep_0"
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@app.post("/reset", response_model=Observation)
|
| 37 |
+
def reset(req: Optional[ResetRequest] = None):
|
| 38 |
+
req = req or ResetRequest()
|
| 39 |
+
return env.reset(seed=req.seed, episode_id=req.episode_id)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@app.post("/step", response_model=StepResult)
|
| 43 |
+
def step(action: Action):
|
| 44 |
+
try:
|
| 45 |
+
return env.step(action)
|
| 46 |
+
except RuntimeError as e:
|
| 47 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
@app.get("/state", response_model=State)
|
| 51 |
+
def state():
|
| 52 |
+
return env.state()
|
drift_env/tests/__init__.py
ADDED
|
File without changes
|
drift_env/tests/test_adversarial.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Adversarial sanity test.
|
| 2 |
+
|
| 3 |
+
Runs a handful of dumb policies (constant-action agents, mode-based agents,
|
| 4 |
+
stale-policy agents) against the environment. None of them should score well —
|
| 5 |
+
if any of them beats ~60% of max, our reward is gameable and we need to fix it.
|
| 6 |
+
|
| 7 |
+
Per organizer guidance: "Do not optimize a reward you have not tried to break
|
| 8 |
+
yourself first."
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from drift_env.environment import DriftEnv
|
| 12 |
+
from drift_env.episodes import generate_episode
|
| 13 |
+
from drift_env.models import Action, ActionType
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
EP_MAX = 30.0 # per-episode maximum reward (20 compliance + 9 appropriateness + 1 drift bonus)
|
| 17 |
+
N_SEEDS = 20 # episodes to average over for stability
|
| 18 |
+
GAMEABILITY_CEILING = 0.60 # no dumb policy should exceed this fraction of max
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _run_policy(policy_fn, n_seeds: int = N_SEEDS) -> float:
|
| 22 |
+
"""Run a callable(step_plan) -> Action across n episodes and return mean
|
| 23 |
+
fraction of max reward."""
|
| 24 |
+
total = 0.0
|
| 25 |
+
for i in range(n_seeds):
|
| 26 |
+
env = DriftEnv()
|
| 27 |
+
env.reset(seed=i, episode_id=f"adv_{i}")
|
| 28 |
+
ep = generate_episode(seed=i, episode_id=f"adv_{i}")
|
| 29 |
+
ep_reward = 0.0
|
| 30 |
+
for step in ep.steps:
|
| 31 |
+
result = env.step(policy_fn(step))
|
| 32 |
+
ep_reward += result.reward
|
| 33 |
+
total += ep_reward / EP_MAX
|
| 34 |
+
return total / n_seeds
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def test_always_close_fails():
|
| 38 |
+
mean = _run_policy(lambda s: Action(action_type=ActionType.CLOSE, resolution_code="x"))
|
| 39 |
+
assert mean < GAMEABILITY_CEILING, f"always-close scored {mean:.2%}"
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def test_always_approve_40_fails():
|
| 43 |
+
mean = _run_policy(lambda s: Action(
|
| 44 |
+
action_type=ActionType.APPROVE_REFUND, refund_amount=40.0,
|
| 45 |
+
))
|
| 46 |
+
assert mean < GAMEABILITY_CEILING, f"always-approve-40 scored {mean:.2%}"
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def test_always_escalate_manager_fails():
|
| 50 |
+
mean = _run_policy(lambda s: Action(
|
| 51 |
+
action_type=ActionType.ESCALATE, escalation_tier="manager", followup_hours=24,
|
| 52 |
+
))
|
| 53 |
+
assert mean < GAMEABILITY_CEILING, f"always-escalate-manager scored {mean:.2%}"
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def test_always_reply_fails():
|
| 57 |
+
mean = _run_policy(lambda s: Action(action_type=ActionType.REPLY))
|
| 58 |
+
assert mean < GAMEABILITY_CEILING, f"always-reply scored {mean:.2%}"
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def test_random_action_mode_fails():
|
| 62 |
+
"""Constant mode: pick whichever ACTION_TYPE dominates correct answers."""
|
| 63 |
+
# empirically close, approve_refund, and escalate will dominate
|
| 64 |
+
for at in ActionType:
|
| 65 |
+
mean = _run_policy(lambda s, at=at: Action(
|
| 66 |
+
action_type=at,
|
| 67 |
+
refund_amount=50.0, escalation_tier="manager", followup_hours=24,
|
| 68 |
+
resolution_code="x", info_field="x",
|
| 69 |
+
))
|
| 70 |
+
assert mean < GAMEABILITY_CEILING, f"always-{at.value} scored {mean:.2%}"
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def test_stale_policy_agent_approximates_baseline():
|
| 74 |
+
"""An agent that behaves correctly under the STARTING policy (refund_cap=$100,
|
| 75 |
+
tier_2 escalations, 24h SLA) but ignores all drift should score ~baseline:
|
| 76 |
+
good on non-drift-sensitive steps, 0 on drift-sensitive ones. This is the
|
| 77 |
+
canonical base-model behaviour we saw in the eval (~77% of max). The point
|
| 78 |
+
of this test: it should NOT accidentally earn the drift bonus."""
|
| 79 |
+
from drift_env.policy import DEFAULT_POLICY
|
| 80 |
+
from drift_env.episodes import _correct_action_hint
|
| 81 |
+
|
| 82 |
+
def stale(step):
|
| 83 |
+
if step.email.kind.value == "admin":
|
| 84 |
+
return Action(action_type=ActionType.CLOSE, resolution_code="policy_acknowledged")
|
| 85 |
+
# Pick the action correct under the DEFAULT policy (ignoring drifts)
|
| 86 |
+
template_kind = step.email.meta.get("kind", "")
|
| 87 |
+
refund_amt = step.email.meta.get("refund_amount")
|
| 88 |
+
# Rebuild a minimal template-like object:
|
| 89 |
+
class _T:
|
| 90 |
+
kind = template_kind
|
| 91 |
+
refund_amount = refund_amt
|
| 92 |
+
severity = step.email.meta.get("severity")
|
| 93 |
+
needs_info = step.email.meta.get("needs_info")
|
| 94 |
+
hint = _correct_action_hint(_T(), DEFAULT_POLICY)
|
| 95 |
+
return Action(
|
| 96 |
+
action_type=ActionType(hint["action_type"]),
|
| 97 |
+
refund_amount=hint.get("refund_amount"),
|
| 98 |
+
escalation_tier=hint.get("escalation_tier"),
|
| 99 |
+
followup_hours=hint.get("followup_hours"),
|
| 100 |
+
resolution_code=hint.get("resolution_code"),
|
| 101 |
+
info_field=hint.get("info_field"),
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
mean = _run_policy(stale)
|
| 105 |
+
# Stale agent should beat dumb constants but shouldn't reach perfect
|
| 106 |
+
assert 0.5 < mean < 0.95, f"stale-policy agent scored {mean:.2%} (expected 0.5-0.95)"
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def test_perfect_agent_still_wins_by_margin():
|
| 110 |
+
"""Reference: a perfect agent (uses policy-aware ground truth) must clearly
|
| 111 |
+
beat all the above. This protects against the worst failure mode: a dumb
|
| 112 |
+
agent scoring as high as a perfect one."""
|
| 113 |
+
from drift_env.episodes import _correct_action_hint
|
| 114 |
+
|
| 115 |
+
def perfect(step):
|
| 116 |
+
h = step.correct_action_hint
|
| 117 |
+
return Action(
|
| 118 |
+
action_type=ActionType(h["action_type"]),
|
| 119 |
+
refund_amount=h.get("refund_amount"),
|
| 120 |
+
escalation_tier=h.get("escalation_tier"),
|
| 121 |
+
followup_hours=h.get("followup_hours"),
|
| 122 |
+
resolution_code=h.get("resolution_code"),
|
| 123 |
+
info_field=h.get("info_field"),
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
mean = _run_policy(perfect)
|
| 127 |
+
assert mean > 0.95, f"perfect agent only scored {mean:.2%}"
|
drift_env/tests/test_environment.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Environment + episode-generator tests."""
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
from drift_env.environment import DriftEnv
|
| 6 |
+
from drift_env.episodes import generate_episode, EPISODE_LENGTH, DRIFT_POSITIONS
|
| 7 |
+
from drift_env.models import Action, ActionType, EmailKind
|
| 8 |
+
from drift_env.policy import DEFAULT_POLICY
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def test_episode_has_expected_length():
|
| 12 |
+
ep = generate_episode(seed=1)
|
| 13 |
+
assert len(ep.steps) == EPISODE_LENGTH
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def test_episode_has_two_admin_emails_at_expected_positions():
|
| 17 |
+
ep = generate_episode(seed=1)
|
| 18 |
+
admin_positions = [i for i, s in enumerate(ep.steps)
|
| 19 |
+
if s.email.kind == EmailKind.ADMIN]
|
| 20 |
+
assert admin_positions == list(DRIFT_POSITIONS)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def test_policy_evolves_after_each_admin_email():
|
| 24 |
+
ep = generate_episode(seed=42)
|
| 25 |
+
# steps 0..3 (inclusive of admin) operate under starting policy
|
| 26 |
+
for i in range(DRIFT_POSITIONS[0] + 1):
|
| 27 |
+
assert ep.steps[i].policy_at_step == DEFAULT_POLICY
|
| 28 |
+
# step after first drift must differ
|
| 29 |
+
assert ep.steps[DRIFT_POSITIONS[0] + 1].policy_at_step != DEFAULT_POLICY
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def test_episode_is_deterministic_with_seed():
|
| 33 |
+
a = generate_episode(seed=7)
|
| 34 |
+
b = generate_episode(seed=7)
|
| 35 |
+
assert [s.email.id for s in a.steps] == [s.email.id for s in b.steps]
|
| 36 |
+
assert [s.correct_action_hint for s in a.steps] == [s.correct_action_hint for s in b.steps]
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def test_different_seeds_produce_different_episodes():
|
| 40 |
+
a = generate_episode(seed=1)
|
| 41 |
+
b = generate_episode(seed=2)
|
| 42 |
+
assert [s.email.id for s in a.steps] != [s.email.id for s in b.steps]
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def test_drift_sensitive_steps_exist_after_each_drift():
|
| 46 |
+
ep = generate_episode(seed=42)
|
| 47 |
+
# At least one drift-sensitive step must appear after each drift
|
| 48 |
+
sens_after_first = any(
|
| 49 |
+
s.drift_sensitive_to for s in ep.steps[DRIFT_POSITIONS[0] + 1:DRIFT_POSITIONS[1]]
|
| 50 |
+
)
|
| 51 |
+
sens_after_second = any(
|
| 52 |
+
s.drift_sensitive_to for s in ep.steps[DRIFT_POSITIONS[1] + 1:]
|
| 53 |
+
)
|
| 54 |
+
assert sens_after_first or sens_after_second
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def test_reset_returns_observation_with_first_email():
|
| 58 |
+
env = DriftEnv()
|
| 59 |
+
obs = env.reset(seed=1)
|
| 60 |
+
assert obs.email_index == 0
|
| 61 |
+
assert obs.total_emails == EPISODE_LENGTH
|
| 62 |
+
assert obs.current_email.sender # non-empty
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def test_step_before_reset_raises():
|
| 66 |
+
env = DriftEnv()
|
| 67 |
+
with pytest.raises(RuntimeError):
|
| 68 |
+
env.step(Action(action_type=ActionType.CLOSE, resolution_code="x"))
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def test_step_after_done_raises():
|
| 72 |
+
env = DriftEnv()
|
| 73 |
+
env.reset(seed=1)
|
| 74 |
+
for _ in range(EPISODE_LENGTH):
|
| 75 |
+
env.step(Action(action_type=ActionType.CLOSE, resolution_code="x"))
|
| 76 |
+
with pytest.raises(RuntimeError):
|
| 77 |
+
env.step(Action(action_type=ActionType.CLOSE, resolution_code="x"))
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def test_agent_cannot_see_grader_metadata():
|
| 81 |
+
"""The hidden `meta` dict (refund_amount, severity etc) must not leak."""
|
| 82 |
+
env = DriftEnv()
|
| 83 |
+
obs = env.reset(seed=1)
|
| 84 |
+
assert obs.current_email.meta == {}
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def test_perfect_agent_hits_max_reward():
|
| 88 |
+
"""Running with ground-truth hints should produce a high, reproducible score."""
|
| 89 |
+
ep = generate_episode(seed=42)
|
| 90 |
+
env = DriftEnv()
|
| 91 |
+
env.reset(seed=42)
|
| 92 |
+
total = 0.0
|
| 93 |
+
for step in ep.steps:
|
| 94 |
+
h = step.correct_action_hint
|
| 95 |
+
action = Action(
|
| 96 |
+
action_type=ActionType(h["action_type"]),
|
| 97 |
+
refund_amount=h.get("refund_amount"),
|
| 98 |
+
escalation_tier=h.get("escalation_tier"),
|
| 99 |
+
followup_hours=h.get("followup_hours"),
|
| 100 |
+
resolution_code=h.get("resolution_code"),
|
| 101 |
+
info_field=h.get("info_field"),
|
| 102 |
+
)
|
| 103 |
+
r = env.step(action)
|
| 104 |
+
total += r.reward
|
| 105 |
+
# Perfect agent: 20 × 1.0 compliance + 18 × 0.5 appropriateness
|
| 106 |
+
# (admin emails don't get appropriateness) + 2 × 0.5 drift bonus = 30
|
| 107 |
+
assert total == 30.0
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def test_drift_bonus_arms_and_clears_correctly():
|
| 111 |
+
env = DriftEnv()
|
| 112 |
+
env.reset(seed=42)
|
| 113 |
+
# Run perfect agent until admin email fires (step 3)
|
| 114 |
+
ep = generate_episode(seed=42)
|
| 115 |
+
for i in range(DRIFT_POSITIONS[0]):
|
| 116 |
+
h = ep.steps[i].correct_action_hint
|
| 117 |
+
env.step(Action(action_type=ActionType(h["action_type"]),
|
| 118 |
+
resolution_code=h.get("resolution_code"),
|
| 119 |
+
refund_amount=h.get("refund_amount"),
|
| 120 |
+
escalation_tier=h.get("escalation_tier"),
|
| 121 |
+
followup_hours=h.get("followup_hours"),
|
| 122 |
+
info_field=h.get("info_field")))
|
| 123 |
+
# Process admin email
|
| 124 |
+
h = ep.steps[DRIFT_POSITIONS[0]].correct_action_hint
|
| 125 |
+
r = env.step(Action(action_type=ActionType.CLOSE, resolution_code="policy_acknowledged"))
|
| 126 |
+
# After admin, the drift should be armed
|
| 127 |
+
assert len(r.info["armed_drifts_after"]) >= 1
|
drift_env/tests/test_grader.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unit tests for the 3-component drift grader."""
|
| 2 |
+
|
| 3 |
+
from drift_env.grader import grade_step
|
| 4 |
+
from drift_env.models import Action, ActionType
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
# ---------- compliance ----------
|
| 8 |
+
|
| 9 |
+
def test_perfect_refund_approval_under_cap():
|
| 10 |
+
action = Action(action_type=ActionType.APPROVE_REFUND, refund_amount=40.0)
|
| 11 |
+
hint = {"action_type": "approve_refund", "refund_amount": 40.0}
|
| 12 |
+
r, b, _ = grade_step(action, hint, {"kind": "refund"}, None, set(), False)
|
| 13 |
+
assert b["compliance"] == 1.0
|
| 14 |
+
assert b["appropriateness"] == 0.5
|
| 15 |
+
assert r == 1.5
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def test_refund_wrong_amount_partial_compliance():
|
| 19 |
+
action = Action(action_type=ActionType.APPROVE_REFUND, refund_amount=50.0)
|
| 20 |
+
hint = {"action_type": "approve_refund", "refund_amount": 40.0}
|
| 21 |
+
r, b, _ = grade_step(action, hint, {"kind": "refund"}, None, set(), False)
|
| 22 |
+
assert b["compliance"] == 0.5 # right action_type, wrong amount
|
| 23 |
+
assert b["appropriateness"] == 0.5
|
| 24 |
+
assert r == 1.0
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def test_refund_above_cap_should_escalate():
|
| 28 |
+
# Under cap=$50, $75 refund must escalate to manager
|
| 29 |
+
action = Action(action_type=ActionType.ESCALATE, escalation_tier="manager")
|
| 30 |
+
hint = {"action_type": "escalate", "escalation_tier": "manager"}
|
| 31 |
+
r, b, _ = grade_step(action, hint, {"kind": "refund"}, None, set(), False)
|
| 32 |
+
assert b["compliance"] == 1.0
|
| 33 |
+
assert b["appropriateness"] == 0.5
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def test_approve_when_should_escalate():
|
| 37 |
+
# Agent approves $75 when cap is $50 — compliance 0
|
| 38 |
+
action = Action(action_type=ActionType.APPROVE_REFUND, refund_amount=75.0)
|
| 39 |
+
hint = {"action_type": "escalate", "escalation_tier": "manager"}
|
| 40 |
+
r, b, _ = grade_step(action, hint, {"kind": "refund"}, None, set(), False)
|
| 41 |
+
assert b["compliance"] == 0.0
|
| 42 |
+
# Appropriate TYPE for refund email (approve_refund is in the valid set)
|
| 43 |
+
assert b["appropriateness"] == 0.5
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def test_critical_escalation_tier_match():
|
| 47 |
+
action = Action(action_type=ActionType.ESCALATE, escalation_tier="manager", followup_hours=2)
|
| 48 |
+
hint = {"action_type": "escalate", "escalation_tier": "manager", "followup_hours": 2}
|
| 49 |
+
r, b, _ = grade_step(action, hint, {"kind": "critical_incident"}, None, set(), False)
|
| 50 |
+
assert b["compliance"] == 1.0
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def test_critical_wrong_tier_partial():
|
| 54 |
+
action = Action(action_type=ActionType.ESCALATE, escalation_tier="tier_2", followup_hours=2)
|
| 55 |
+
hint = {"action_type": "escalate", "escalation_tier": "manager", "followup_hours": 2}
|
| 56 |
+
r, b, _ = grade_step(action, hint, {"kind": "critical_incident"}, None, set(), False)
|
| 57 |
+
assert 0.5 < b["compliance"] < 1.0 # hours ok, tier wrong
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# ---------- appropriateness ----------
|
| 61 |
+
|
| 62 |
+
def test_refund_close_is_inappropriate():
|
| 63 |
+
# Agent closes a refund email -> wrong compliance AND wrong appropriateness
|
| 64 |
+
action = Action(action_type=ActionType.CLOSE, resolution_code="ack")
|
| 65 |
+
hint = {"action_type": "approve_refund", "refund_amount": 40.0}
|
| 66 |
+
r, b, _ = grade_step(action, hint, {"kind": "refund"}, None, set(), False)
|
| 67 |
+
assert b["compliance"] == 0.0
|
| 68 |
+
assert b["appropriateness"] == 0.0
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def test_chitchat_close_is_perfect():
|
| 72 |
+
action = Action(action_type=ActionType.CLOSE, resolution_code="no_action_needed")
|
| 73 |
+
hint = {"action_type": "close", "resolution_code": "no_action_needed"}
|
| 74 |
+
r, b, _ = grade_step(action, hint, {"kind": "chitchat"}, None, set(), False)
|
| 75 |
+
assert b["compliance"] == 1.0
|
| 76 |
+
assert b["appropriateness"] == 0.5
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
# ---------- drift bonus ----------
|
| 80 |
+
|
| 81 |
+
def test_drift_bonus_awarded_on_first_correct_drift_sensitive():
|
| 82 |
+
action = Action(action_type=ActionType.APPROVE_REFUND, refund_amount=75.0)
|
| 83 |
+
hint = {"action_type": "approve_refund", "refund_amount": 75.0}
|
| 84 |
+
armed = {"refund_cap_200"}
|
| 85 |
+
r, b, clear = grade_step(
|
| 86 |
+
action, hint, {"kind": "refund"},
|
| 87 |
+
drift_sensitive_to="refund_cap_200", armed_drifts=armed, is_admin_email=False,
|
| 88 |
+
)
|
| 89 |
+
assert b["drift_bonus"] == 0.5
|
| 90 |
+
assert clear == "refund_cap_200"
|
| 91 |
+
assert r == 1.0 + 0.5 + 0.5 # compliance + appropriateness + bonus
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def test_drift_bonus_not_awarded_when_already_cleared():
|
| 95 |
+
action = Action(action_type=ActionType.APPROVE_REFUND, refund_amount=75.0)
|
| 96 |
+
hint = {"action_type": "approve_refund", "refund_amount": 75.0}
|
| 97 |
+
armed: set[str] = set() # already awarded earlier
|
| 98 |
+
r, b, clear = grade_step(
|
| 99 |
+
action, hint, {"kind": "refund"},
|
| 100 |
+
drift_sensitive_to="refund_cap_200", armed_drifts=armed, is_admin_email=False,
|
| 101 |
+
)
|
| 102 |
+
assert b["drift_bonus"] == 0.0
|
| 103 |
+
assert clear is None
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def test_drift_bonus_not_awarded_when_compliance_fails():
|
| 107 |
+
# Agent got drift-sensitive step WRONG — no bonus even if armed
|
| 108 |
+
action = Action(action_type=ActionType.ESCALATE, escalation_tier="manager")
|
| 109 |
+
hint = {"action_type": "approve_refund", "refund_amount": 75.0}
|
| 110 |
+
armed = {"refund_cap_200"}
|
| 111 |
+
r, b, clear = grade_step(
|
| 112 |
+
action, hint, {"kind": "refund"},
|
| 113 |
+
drift_sensitive_to="refund_cap_200", armed_drifts=armed, is_admin_email=False,
|
| 114 |
+
)
|
| 115 |
+
assert b["drift_bonus"] == 0.0
|
| 116 |
+
assert clear is None
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def test_drift_bonus_not_awarded_when_step_not_drift_sensitive():
|
| 120 |
+
action = Action(action_type=ActionType.APPROVE_REFUND, refund_amount=40.0)
|
| 121 |
+
hint = {"action_type": "approve_refund", "refund_amount": 40.0}
|
| 122 |
+
armed = {"refund_cap_200"}
|
| 123 |
+
r, b, clear = grade_step(
|
| 124 |
+
action, hint, {"kind": "refund"},
|
| 125 |
+
drift_sensitive_to=None, armed_drifts=armed, is_admin_email=False,
|
| 126 |
+
)
|
| 127 |
+
assert b["drift_bonus"] == 0.0
|
| 128 |
+
assert clear is None
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
# ---------- admin emails ----------
|
| 132 |
+
|
| 133 |
+
def test_admin_email_close_scores_full():
|
| 134 |
+
action = Action(action_type=ActionType.CLOSE, resolution_code="policy_acknowledged")
|
| 135 |
+
r, b, clear = grade_step(
|
| 136 |
+
action, {}, {}, drift_sensitive_to=None, armed_drifts=set(), is_admin_email=True,
|
| 137 |
+
)
|
| 138 |
+
assert r == 1.0
|
| 139 |
+
assert b["drift_bonus"] == 0.0
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def test_admin_email_wrong_action_scores_low():
|
| 143 |
+
action = Action(action_type=ActionType.APPROVE_REFUND, refund_amount=100.0)
|
| 144 |
+
r, b, _ = grade_step(
|
| 145 |
+
action, {}, {}, drift_sensitive_to=None, armed_drifts=set(), is_admin_email=True,
|
| 146 |
+
)
|
| 147 |
+
assert r <= 0.3
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
# ---------- determinism ----------
|
| 151 |
+
|
| 152 |
+
def test_grader_is_deterministic():
|
| 153 |
+
action = Action(action_type=ActionType.APPROVE_REFUND, refund_amount=40.0)
|
| 154 |
+
hint = {"action_type": "approve_refund", "refund_amount": 40.0}
|
| 155 |
+
r1, _, _ = grade_step(action, hint, {"kind": "refund"}, None, set(), False)
|
| 156 |
+
r2, _, _ = grade_step(action, hint, {"kind": "refund"}, None, set(), False)
|
| 157 |
+
assert r1 == r2
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def test_reward_bounds_per_step():
|
| 161 |
+
"""Sweep many (action, hint) combos — reward always in [0, 2.0]."""
|
| 162 |
+
hints = [
|
| 163 |
+
{"action_type": "approve_refund", "refund_amount": 40.0},
|
| 164 |
+
{"action_type": "escalate", "escalation_tier": "manager"},
|
| 165 |
+
{"action_type": "reply"},
|
| 166 |
+
{"action_type": "close", "resolution_code": "no_action_needed"},
|
| 167 |
+
]
|
| 168 |
+
for h in hints:
|
| 169 |
+
for at in ActionType:
|
| 170 |
+
action = Action(action_type=at, refund_amount=50.0,
|
| 171 |
+
escalation_tier="tier_2", followup_hours=24,
|
| 172 |
+
resolution_code="x", info_field="y")
|
| 173 |
+
r, _, _ = grade_step(
|
| 174 |
+
action, h, {"kind": "refund"},
|
| 175 |
+
drift_sensitive_to="refund_cap_200",
|
| 176 |
+
armed_drifts={"refund_cap_200"},
|
| 177 |
+
is_admin_email=False,
|
| 178 |
+
)
|
| 179 |
+
assert 0.0 <= r <= 2.0, f"out of bounds: {r}"
|
drift_env/training/__init__.py
ADDED
|
File without changes
|
drift_env/training/rewards.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""TRL-compatible reward functions.
|
| 2 |
+
|
| 3 |
+
Exposes each component of the 3-component grader as an independent
|
| 4 |
+
reward_func so TRL/wandb can plot them separately during training.
|
| 5 |
+
Per the organizers' guidance: "use multiple independent reward functions"
|
| 6 |
+
and "monitor individual reward components, not just the total."
|
| 7 |
+
|
| 8 |
+
TRL calls each reward_func with signature:
|
| 9 |
+
func(completions: list[str], **kwargs) -> list[float]
|
| 10 |
+
|
| 11 |
+
where kwargs contains the dataset columns (prompt, correct_action_hint,
|
| 12 |
+
email_kind, can_earn_drift_bonus, is_admin_email).
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import json
|
| 18 |
+
from typing import Any
|
| 19 |
+
|
| 20 |
+
from drift_env.grader import _compliance, _appropriateness, _drift_bonus, _grade_admin
|
| 21 |
+
from drift_env.models import Action, ActionType
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
FALLBACK = Action(action_type=ActionType.CLOSE, resolution_code="error_fallback")
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def parse_generated_action(raw: str) -> Action:
|
| 28 |
+
text = raw.strip()
|
| 29 |
+
if text.startswith("```"):
|
| 30 |
+
lines = [l for l in text.split("\n") if not l.strip().startswith("```")]
|
| 31 |
+
text = "\n".join(lines).strip()
|
| 32 |
+
if not text.startswith("{"):
|
| 33 |
+
start, end = text.find("{"), text.rfind("}")
|
| 34 |
+
if start == -1 or end == -1 or end < start:
|
| 35 |
+
return FALLBACK
|
| 36 |
+
text = text[start:end + 1]
|
| 37 |
+
try:
|
| 38 |
+
obj = json.loads(text)
|
| 39 |
+
except json.JSONDecodeError:
|
| 40 |
+
return FALLBACK
|
| 41 |
+
a_type = obj.get("action_type")
|
| 42 |
+
if a_type not in {e.value for e in ActionType}:
|
| 43 |
+
return FALLBACK
|
| 44 |
+
try:
|
| 45 |
+
return Action(
|
| 46 |
+
action_type=ActionType(a_type),
|
| 47 |
+
refund_amount=obj.get("refund_amount"),
|
| 48 |
+
escalation_tier=obj.get("escalation_tier"),
|
| 49 |
+
followup_hours=obj.get("followup_hours"),
|
| 50 |
+
resolution_code=obj.get("resolution_code"),
|
| 51 |
+
info_field=obj.get("info_field"),
|
| 52 |
+
reply_text=obj.get("reply_text"),
|
| 53 |
+
)
|
| 54 |
+
except Exception:
|
| 55 |
+
return FALLBACK
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _as_list(v, n):
|
| 59 |
+
"""Utility to lift scalar dataset columns into per-completion lists."""
|
| 60 |
+
if isinstance(v, list):
|
| 61 |
+
return v
|
| 62 |
+
return [v] * n
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _per_sample(
|
| 66 |
+
completions: list[str],
|
| 67 |
+
correct_action_hint: Any,
|
| 68 |
+
email_kind: Any,
|
| 69 |
+
can_earn_drift_bonus: Any,
|
| 70 |
+
drift_sensitive_to: Any,
|
| 71 |
+
is_admin_email: Any,
|
| 72 |
+
):
|
| 73 |
+
"""Generator yielding (action, hint, email_kind, can_earn, sensitive_to, is_admin)
|
| 74 |
+
for each completion. Handles TRL's list-or-scalar column conventions."""
|
| 75 |
+
n = len(completions)
|
| 76 |
+
hints = _as_list(correct_action_hint, n)
|
| 77 |
+
kinds = _as_list(email_kind, n)
|
| 78 |
+
earns = _as_list(can_earn_drift_bonus, n)
|
| 79 |
+
sens = _as_list(drift_sensitive_to, n)
|
| 80 |
+
admins = _as_list(is_admin_email, n)
|
| 81 |
+
for comp, hint, kind, earn, s, admin in zip(completions, hints, kinds, earns, sens, admins):
|
| 82 |
+
action = parse_generated_action(
|
| 83 |
+
comp[0]["content"] if isinstance(comp, list) else comp
|
| 84 |
+
)
|
| 85 |
+
yield action, hint, kind, earn, s, admin
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
# ---------------------------------------------------------------------------
|
| 89 |
+
# Reward components — each exposed to TRL as a separate reward_func
|
| 90 |
+
# ---------------------------------------------------------------------------
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def reward_compliance(
|
| 94 |
+
completions: list[str],
|
| 95 |
+
correct_action_hint=None,
|
| 96 |
+
email_kind=None,
|
| 97 |
+
can_earn_drift_bonus=None,
|
| 98 |
+
drift_sensitive_to=None,
|
| 99 |
+
is_admin_email=None,
|
| 100 |
+
**_,
|
| 101 |
+
) -> list[float]:
|
| 102 |
+
out = []
|
| 103 |
+
for action, hint, kind, earn, s, admin in _per_sample(
|
| 104 |
+
completions, correct_action_hint, email_kind,
|
| 105 |
+
can_earn_drift_bonus, drift_sensitive_to, is_admin_email,
|
| 106 |
+
):
|
| 107 |
+
if admin:
|
| 108 |
+
out.append(_grade_admin(action))
|
| 109 |
+
else:
|
| 110 |
+
out.append(_compliance(action, hint))
|
| 111 |
+
return out
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def reward_appropriateness(
|
| 115 |
+
completions: list[str],
|
| 116 |
+
correct_action_hint=None,
|
| 117 |
+
email_kind=None,
|
| 118 |
+
can_earn_drift_bonus=None,
|
| 119 |
+
drift_sensitive_to=None,
|
| 120 |
+
is_admin_email=None,
|
| 121 |
+
**_,
|
| 122 |
+
) -> list[float]:
|
| 123 |
+
out = []
|
| 124 |
+
for action, hint, kind, earn, s, admin in _per_sample(
|
| 125 |
+
completions, correct_action_hint, email_kind,
|
| 126 |
+
can_earn_drift_bonus, drift_sensitive_to, is_admin_email,
|
| 127 |
+
):
|
| 128 |
+
if admin:
|
| 129 |
+
out.append(0.0)
|
| 130 |
+
else:
|
| 131 |
+
out.append(_appropriateness(action, {"kind": kind}))
|
| 132 |
+
return out
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def reward_drift_bonus(
|
| 136 |
+
completions: list[str],
|
| 137 |
+
correct_action_hint=None,
|
| 138 |
+
email_kind=None,
|
| 139 |
+
can_earn_drift_bonus=None,
|
| 140 |
+
drift_sensitive_to=None,
|
| 141 |
+
is_admin_email=None,
|
| 142 |
+
**_,
|
| 143 |
+
) -> list[float]:
|
| 144 |
+
"""Only fires on rows flagged `can_earn_drift_bonus=True` AND where the
|
| 145 |
+
model's action is policy-compliant (compliance >= 1.0). This matches the
|
| 146 |
+
env's armed-drift semantics: first correct drift-aware action earns +0.5."""
|
| 147 |
+
out = []
|
| 148 |
+
for action, hint, kind, earn, s, admin in _per_sample(
|
| 149 |
+
completions, correct_action_hint, email_kind,
|
| 150 |
+
can_earn_drift_bonus, drift_sensitive_to, is_admin_email,
|
| 151 |
+
):
|
| 152 |
+
if admin or not earn:
|
| 153 |
+
out.append(0.0)
|
| 154 |
+
continue
|
| 155 |
+
comp = _compliance(action, hint)
|
| 156 |
+
bonus, _ = _drift_bonus(s, {s} if s else set(), comp)
|
| 157 |
+
out.append(bonus)
|
| 158 |
+
return out
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
# Handy wrapper for quick total-reward sanity checks outside TRL
|
| 162 |
+
def total_reward(
|
| 163 |
+
completion: str,
|
| 164 |
+
correct_action_hint: dict,
|
| 165 |
+
email_kind: str | None,
|
| 166 |
+
can_earn_drift_bonus: bool,
|
| 167 |
+
drift_sensitive_to: str | None,
|
| 168 |
+
is_admin_email: bool,
|
| 169 |
+
) -> dict:
|
| 170 |
+
action = parse_generated_action(completion)
|
| 171 |
+
if is_admin_email:
|
| 172 |
+
comp = _grade_admin(action)
|
| 173 |
+
appr = 0.0
|
| 174 |
+
else:
|
| 175 |
+
comp = _compliance(action, correct_action_hint)
|
| 176 |
+
appr = _appropriateness(action, {"kind": email_kind})
|
| 177 |
+
bonus = 0.0
|
| 178 |
+
if not is_admin_email and can_earn_drift_bonus:
|
| 179 |
+
b, _ = _drift_bonus(drift_sensitive_to, {drift_sensitive_to} if drift_sensitive_to else set(), comp)
|
| 180 |
+
bonus = b
|
| 181 |
+
return {
|
| 182 |
+
"compliance": round(comp, 4),
|
| 183 |
+
"appropriateness": round(appr, 4),
|
| 184 |
+
"drift_bonus": round(bonus, 4),
|
| 185 |
+
"total": round(comp + appr + bonus, 4),
|
| 186 |
+
}
|
email_env/__init__.py
ADDED
|
File without changes
|
email_env/baseline.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Baseline agent using OpenAI gpt-4o-mini.
|
| 3 |
+
Runs on all 3 tasks and prints average score.
|
| 4 |
+
|
| 5 |
+
Usage:
|
| 6 |
+
OPENAI_API_KEY=sk-... python -m email_env.baseline
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import json
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from dotenv import load_dotenv
|
| 13 |
+
from openai import OpenAI # groq uses the same openai SDK
|
| 14 |
+
|
| 15 |
+
load_dotenv(Path(__file__).parent / ".env")
|
| 16 |
+
|
| 17 |
+
from email_env.server.environment import EmailTriageEnv
|
| 18 |
+
from email_env.models import Action
|
| 19 |
+
from email_env.tasks import TASKS
|
| 20 |
+
|
| 21 |
+
SYSTEM_PROMPT = """You are an email triage assistant. Given an email, you must:
|
| 22 |
+
1. Classify the email into exactly one category: billing, technical, or general
|
| 23 |
+
2. Assign a priority: low, medium, or high
|
| 24 |
+
3. Write a professional response
|
| 25 |
+
|
| 26 |
+
Reply ONLY with valid JSON in this exact format:
|
| 27 |
+
{
|
| 28 |
+
"category": "<billing|technical|general>",
|
| 29 |
+
"priority": "<low|medium|high>",
|
| 30 |
+
"response": "<your response text>"
|
| 31 |
+
}"""
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def run_baseline():
|
| 35 |
+
api_key = os.environ.get("GROQ_API_KEY") or os.environ.get("OPENAI_API_KEY")
|
| 36 |
+
if not api_key:
|
| 37 |
+
raise EnvironmentError("Set GROQ_API_KEY (or OPENAI_API_KEY) environment variable.")
|
| 38 |
+
|
| 39 |
+
base_url = "https://api.groq.com/openai/v1" if os.environ.get("GROQ_API_KEY") else None
|
| 40 |
+
client = OpenAI(api_key=api_key, base_url=base_url)
|
| 41 |
+
env = EmailTriageEnv()
|
| 42 |
+
scores = []
|
| 43 |
+
|
| 44 |
+
for task_id, task in TASKS.items():
|
| 45 |
+
print(f"\n--- {task_id} ({task['difficulty']}) ---")
|
| 46 |
+
obs = env.reset(task_id=task_id)
|
| 47 |
+
print(f"Email: {obs.email_text[:80]}...")
|
| 48 |
+
|
| 49 |
+
user_msg = (
|
| 50 |
+
f"Sender type: {obs.sender_type}\n\n"
|
| 51 |
+
f"Email:\n{obs.email_text}"
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
completion = client.chat.completions.create(
|
| 55 |
+
model="llama-3.1-8b-instant" if os.environ.get("GROQ_API_KEY") else "gpt-4o-mini",
|
| 56 |
+
messages=[
|
| 57 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 58 |
+
{"role": "user", "content": user_msg},
|
| 59 |
+
],
|
| 60 |
+
temperature=0,
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
raw = completion.choices[0].message.content.strip()
|
| 64 |
+
try:
|
| 65 |
+
parsed = json.loads(raw)
|
| 66 |
+
except json.JSONDecodeError:
|
| 67 |
+
print(f"Failed to parse response: {raw}")
|
| 68 |
+
scores.append(0.0)
|
| 69 |
+
continue
|
| 70 |
+
|
| 71 |
+
action = Action(
|
| 72 |
+
category=parsed.get("category", "general"),
|
| 73 |
+
priority=parsed.get("priority", "low"),
|
| 74 |
+
response=parsed.get("response", ""),
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
result = env.step(action)
|
| 78 |
+
print(f"Category: {action.category} (expected: {task['expected_category']})")
|
| 79 |
+
print(f"Priority: {action.priority} (expected: {task['expected_priority']})")
|
| 80 |
+
print(f"Score: {result.reward}")
|
| 81 |
+
scores.append(result.reward)
|
| 82 |
+
|
| 83 |
+
avg = round(sum(scores) / len(scores), 4)
|
| 84 |
+
print(f"\n=== Average Score: {avg} ===")
|
| 85 |
+
return avg
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
if __name__ == "__main__":
|
| 89 |
+
run_baseline()
|
email_env/client.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Python client for the Email Triage OpenEnv server.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import requests
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class EmailTriageClient:
|
| 9 |
+
def __init__(self, base_url: str = "http://localhost:8000"):
|
| 10 |
+
self.base_url = base_url.rstrip("/")
|
| 11 |
+
|
| 12 |
+
def reset(self, task_id: str = "task_1") -> dict:
|
| 13 |
+
r = requests.post(f"{self.base_url}/reset", json={"task_id": task_id})
|
| 14 |
+
r.raise_for_status()
|
| 15 |
+
return r.json()
|
| 16 |
+
|
| 17 |
+
def step(self, category: str, priority: str, response: str) -> dict:
|
| 18 |
+
payload = {"category": category, "priority": priority, "response": response}
|
| 19 |
+
r = requests.post(f"{self.base_url}/step", json=payload)
|
| 20 |
+
r.raise_for_status()
|
| 21 |
+
return r.json()
|
| 22 |
+
|
| 23 |
+
def state(self) -> dict:
|
| 24 |
+
r = requests.get(f"{self.base_url}/state")
|
| 25 |
+
r.raise_for_status()
|
| 26 |
+
return r.json()
|
| 27 |
+
|
| 28 |
+
def tasks(self) -> list:
|
| 29 |
+
r = requests.get(f"{self.base_url}/tasks")
|
| 30 |
+
r.raise_for_status()
|
| 31 |
+
return r.json()
|
| 32 |
+
|
| 33 |
+
def grade(self, task_id: str, category: str, priority: str, response: str) -> float:
|
| 34 |
+
payload = {
|
| 35 |
+
"task_id": task_id,
|
| 36 |
+
"category": category,
|
| 37 |
+
"priority": priority,
|
| 38 |
+
"response": response,
|
| 39 |
+
}
|
| 40 |
+
r = requests.post(f"{self.base_url}/grader", json=payload)
|
| 41 |
+
r.raise_for_status()
|
| 42 |
+
return r.json()["score"]
|
email_env/grader.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Deterministic hybrid grader: grade(action, expected) -> float in [0.0, 1.0]
|
| 3 |
+
|
| 4 |
+
Response quality combines three signals to defeat keyword-stuffing exploits:
|
| 5 |
+
1. Keyword coverage (40%) — distinct keywords present
|
| 6 |
+
2. Length & coherence sanity (20%) — penalises too-short / rambling / no-punctuation
|
| 7 |
+
3. Structural requirement (40%) — must contain BOTH an acknowledgement
|
| 8 |
+
(apology / acknowledge) AND an action/timeline phrase
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import re
|
| 12 |
+
|
| 13 |
+
CATEGORY_WEIGHT = 0.4
|
| 14 |
+
PRIORITY_WEIGHT = 0.3
|
| 15 |
+
RESPONSE_WEIGHT = 0.3
|
| 16 |
+
|
| 17 |
+
EMPTY_RESPONSE_PENALTY = 0.2
|
| 18 |
+
|
| 19 |
+
ACKNOWLEDGEMENT_TERMS = (
|
| 20 |
+
"sorry", "apolog", "regret", "understand", "acknowledge", "thank"
|
| 21 |
+
)
|
| 22 |
+
ACTION_TERMS = (
|
| 23 |
+
"will", "shall", "investigat", "refund", "resolv", "fix", "escalat",
|
| 24 |
+
"team", "contact", "process", "issue", "follow up", "update", "check",
|
| 25 |
+
"review", "look into", "assist", "help"
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
MIN_RESPONSE_LEN = 20
|
| 29 |
+
MAX_RESPONSE_LEN = 600
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _keyword_coverage(response_lower: str, keywords: list[str]) -> float:
|
| 33 |
+
if not keywords:
|
| 34 |
+
return 0.5
|
| 35 |
+
matched = sum(1 for kw in keywords if kw.lower() in response_lower)
|
| 36 |
+
return matched / len(keywords)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _length_coherence(response: str) -> float:
|
| 40 |
+
"""Sanity score based on length, punctuation and lexical diversity."""
|
| 41 |
+
text = response.strip()
|
| 42 |
+
n = len(text)
|
| 43 |
+
if n == 0:
|
| 44 |
+
return 0.0
|
| 45 |
+
|
| 46 |
+
score = 1.0
|
| 47 |
+
# length bounds
|
| 48 |
+
if n < MIN_RESPONSE_LEN:
|
| 49 |
+
score *= 0.4
|
| 50 |
+
elif n > MAX_RESPONSE_LEN:
|
| 51 |
+
score *= 0.6
|
| 52 |
+
|
| 53 |
+
# must contain at least one sentence terminator
|
| 54 |
+
if not re.search(r"[.!?]", text):
|
| 55 |
+
score *= 0.5
|
| 56 |
+
|
| 57 |
+
# detect keyword stuffing: repeated identical tokens dominate text
|
| 58 |
+
tokens = re.findall(r"[A-Za-z']+", text.lower())
|
| 59 |
+
if tokens:
|
| 60 |
+
unique_ratio = len(set(tokens)) / len(tokens)
|
| 61 |
+
if unique_ratio < 0.5:
|
| 62 |
+
score *= 0.4
|
| 63 |
+
|
| 64 |
+
# penalise SHOUTING / no spaces
|
| 65 |
+
letters = [c for c in text if c.isalpha()]
|
| 66 |
+
if letters and sum(1 for c in letters if c.isupper()) / len(letters) > 0.7:
|
| 67 |
+
score *= 0.5
|
| 68 |
+
|
| 69 |
+
return max(0.0, min(1.0, score))
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def _structural_requirement(response_lower: str) -> float:
|
| 73 |
+
has_ack = any(term in response_lower for term in ACKNOWLEDGEMENT_TERMS)
|
| 74 |
+
has_action = any(term in response_lower for term in ACTION_TERMS)
|
| 75 |
+
if has_ack and has_action:
|
| 76 |
+
return 1.0
|
| 77 |
+
if has_ack or has_action:
|
| 78 |
+
return 0.5
|
| 79 |
+
return 0.0
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def grade_response_quality(response: str, keywords: list[str]) -> float:
|
| 83 |
+
"""Hybrid response score 0.0–1.0 resistant to keyword stuffing."""
|
| 84 |
+
if not response or not response.strip():
|
| 85 |
+
return 0.0
|
| 86 |
+
response_lower = response.lower()
|
| 87 |
+
|
| 88 |
+
coverage = _keyword_coverage(response_lower, keywords)
|
| 89 |
+
coherence = _length_coherence(response)
|
| 90 |
+
structure = _structural_requirement(response_lower)
|
| 91 |
+
|
| 92 |
+
quality = 0.40 * coverage + 0.20 * coherence + 0.40 * structure
|
| 93 |
+
return round(max(0.0, min(1.0, quality)), 4)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def grade(action: dict, expected: dict) -> float:
|
| 97 |
+
"""
|
| 98 |
+
action: dict with keys category, priority, response
|
| 99 |
+
expected: dict with keys expected_category, expected_priority, response_keywords
|
| 100 |
+
Returns a score between 0.0 and 1.0.
|
| 101 |
+
"""
|
| 102 |
+
score = 0.0
|
| 103 |
+
|
| 104 |
+
if action.get("category", "").lower() == expected["expected_category"].lower():
|
| 105 |
+
score += CATEGORY_WEIGHT
|
| 106 |
+
|
| 107 |
+
if action.get("priority", "").lower() == expected["expected_priority"].lower():
|
| 108 |
+
score += PRIORITY_WEIGHT
|
| 109 |
+
|
| 110 |
+
response = action.get("response", "")
|
| 111 |
+
if not response or not response.strip():
|
| 112 |
+
score -= EMPTY_RESPONSE_PENALTY
|
| 113 |
+
else:
|
| 114 |
+
quality = grade_response_quality(response, expected.get("response_keywords", []))
|
| 115 |
+
score += RESPONSE_WEIGHT * quality
|
| 116 |
+
|
| 117 |
+
return round(max(0.0, min(1.0, score)), 4)
|
email_env/models.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import Optional
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class Observation(BaseModel):
|
| 6 |
+
email_text: str
|
| 7 |
+
sender_type: str # customer / internal / system
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class Action(BaseModel):
|
| 11 |
+
category: str # billing / technical / general
|
| 12 |
+
priority: str # low / medium / high
|
| 13 |
+
response: str
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class StepResult(BaseModel):
|
| 17 |
+
observation: Optional[Observation]
|
| 18 |
+
reward: float
|
| 19 |
+
done: bool
|
| 20 |
+
info: dict
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class State(BaseModel):
|
| 24 |
+
current_email: Optional[str]
|
| 25 |
+
step_count: int
|
| 26 |
+
done: bool
|
| 27 |
+
task_id: Optional[str]
|
email_env/openenv.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
spec_version: 1
|
| 2 |
+
name: email-triage
|
| 3 |
+
type: space
|
| 4 |
+
runtime: fastapi
|
| 5 |
+
app: server.app:app
|
| 6 |
+
port: 7860
|
email_env/server/Dockerfile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
|
| 8 |
+
COPY . .
|
| 9 |
+
|
| 10 |
+
ENV PYTHONPATH=/app
|
| 11 |
+
|
| 12 |
+
EXPOSE 8000
|
| 13 |
+
|
| 14 |
+
CMD ["uvicorn", "email_env.server.app:app", "--host", "0.0.0.0", "--port", "8000"]
|
email_env/server/__init__.py
ADDED
|
File without changes
|
email_env/server/app.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional
|
| 2 |
+
from fastapi import FastAPI, HTTPException
|
| 3 |
+
from pydantic import BaseModel
|
| 4 |
+
|
| 5 |
+
from email_env.models import Action, StepResult, State, Observation
|
| 6 |
+
from email_env.server.environment import EmailTriageEnv
|
| 7 |
+
from email_env.tasks import list_tasks, TASKS
|
| 8 |
+
from email_env.grader import grade
|
| 9 |
+
|
| 10 |
+
app = FastAPI(title="Email Triage OpenEnv", version="1.0.0")
|
| 11 |
+
env = EmailTriageEnv()
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@app.get("/")
|
| 15 |
+
def root():
|
| 16 |
+
return {
|
| 17 |
+
"name": "email-triage",
|
| 18 |
+
"version": "1.0.0",
|
| 19 |
+
"description": "Email Triage and Response OpenEnv Environment",
|
| 20 |
+
"endpoints": ["/reset", "/step", "/state", "/tasks", "/grader", "/baseline"],
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class ResetRequest(BaseModel):
|
| 25 |
+
task_id: str = "task_1"
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class GradeRequest(BaseModel):
|
| 29 |
+
task_id: str
|
| 30 |
+
category: str
|
| 31 |
+
priority: str
|
| 32 |
+
response: str
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@app.post("/reset", response_model=Observation)
|
| 36 |
+
def reset(req: Optional[ResetRequest] = None):
|
| 37 |
+
task_id = req.task_id if req else "task_1"
|
| 38 |
+
try:
|
| 39 |
+
obs = env.reset(task_id=task_id)
|
| 40 |
+
except ValueError as e:
|
| 41 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 42 |
+
return obs
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@app.post("/step", response_model=StepResult)
|
| 46 |
+
def step(action: Action):
|
| 47 |
+
try:
|
| 48 |
+
result = env.step(action)
|
| 49 |
+
except RuntimeError as e:
|
| 50 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 51 |
+
return result
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
@app.get("/state", response_model=State)
|
| 55 |
+
def state():
|
| 56 |
+
return env.state()
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@app.get("/tasks")
|
| 60 |
+
def tasks():
|
| 61 |
+
return list_tasks()
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@app.post("/grader")
|
| 65 |
+
def grader(req: GradeRequest):
|
| 66 |
+
if req.task_id not in TASKS:
|
| 67 |
+
raise HTTPException(status_code=404, detail=f"Unknown task_id: {req.task_id}")
|
| 68 |
+
action = {"category": req.category, "priority": req.priority, "response": req.response}
|
| 69 |
+
score = grade(action, TASKS[req.task_id])
|
| 70 |
+
return {"task_id": req.task_id, "score": score}
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@app.get("/baseline")
|
| 74 |
+
def baseline_info():
|
| 75 |
+
return {
|
| 76 |
+
"description": "Run baseline.py locally with OPENAI_API_KEY set to evaluate the agent.",
|
| 77 |
+
"command": "python -m email_env.baseline",
|
| 78 |
+
}
|
email_env/server/environment.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Core OpenEnv environment: reset(), step(action), state()
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from email_env.models import Observation, Action, StepResult, State
|
| 6 |
+
from email_env.tasks import get_task
|
| 7 |
+
from email_env.grader import grade
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class EmailTriageEnv:
|
| 11 |
+
def __init__(self):
|
| 12 |
+
self._task: dict | None = None
|
| 13 |
+
self._step_count: int = 0
|
| 14 |
+
self._done: bool = False
|
| 15 |
+
|
| 16 |
+
def reset(self, task_id: str = "task_1") -> Observation:
|
| 17 |
+
task = get_task(task_id)
|
| 18 |
+
self._task = task
|
| 19 |
+
self._step_count = 0
|
| 20 |
+
self._done = False
|
| 21 |
+
return Observation(
|
| 22 |
+
email_text=task["email_text"],
|
| 23 |
+
sender_type=task["sender_type"],
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
def step(self, action: Action) -> StepResult:
|
| 27 |
+
if self._task is None:
|
| 28 |
+
raise RuntimeError("Call reset() before step().")
|
| 29 |
+
if self._done:
|
| 30 |
+
raise RuntimeError("Episode is done. Call reset() to start a new one.")
|
| 31 |
+
|
| 32 |
+
self._step_count += 1
|
| 33 |
+
self._done = True # single-step episode
|
| 34 |
+
|
| 35 |
+
reward = grade(action.model_dump(), self._task)
|
| 36 |
+
|
| 37 |
+
info = {
|
| 38 |
+
"task_id": self._task["id"],
|
| 39 |
+
"step_count": self._step_count,
|
| 40 |
+
"expected_category": self._task["expected_category"],
|
| 41 |
+
"expected_priority": self._task["expected_priority"],
|
| 42 |
+
"got_category": action.category,
|
| 43 |
+
"got_priority": action.priority,
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
return StepResult(
|
| 47 |
+
observation=None,
|
| 48 |
+
reward=reward,
|
| 49 |
+
done=self._done,
|
| 50 |
+
info=info,
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
def state(self) -> State:
|
| 54 |
+
return State(
|
| 55 |
+
current_email=self._task["email_text"] if self._task else None,
|
| 56 |
+
step_count=self._step_count,
|
| 57 |
+
done=self._done,
|
| 58 |
+
task_id=self._task["id"] if self._task else None,
|
| 59 |
+
)
|
email_env/tasks.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Task definitions for the Email Triage environment.
|
| 3 |
+
|
| 4 |
+
Difficulty labels reflect how reliably a small/medium LLM baseline solves them.
|
| 5 |
+
Tasks are ordered easy → hard along the difficulty axis.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
TASKS = {
|
| 9 |
+
"task_1": {
|
| 10 |
+
"id": "task_1",
|
| 11 |
+
"difficulty": "easy",
|
| 12 |
+
"description": "Simple general inquiry about business hours and live chat",
|
| 13 |
+
"email_text": (
|
| 14 |
+
"Hi there, I was wondering what your customer support hours are. "
|
| 15 |
+
"I tried calling yesterday evening but nobody picked up. "
|
| 16 |
+
"Could you also let me know if you have a live chat option? Thanks!"
|
| 17 |
+
),
|
| 18 |
+
"sender_type": "customer",
|
| 19 |
+
"expected_category": "general",
|
| 20 |
+
"expected_priority": "low",
|
| 21 |
+
"response_keywords": ["hours", "available", "chat", "support", "help"],
|
| 22 |
+
},
|
| 23 |
+
"task_2": {
|
| 24 |
+
"id": "task_2",
|
| 25 |
+
"difficulty": "easy",
|
| 26 |
+
"description": "Clear billing issue — duplicate charge with obvious classification",
|
| 27 |
+
"email_text": (
|
| 28 |
+
"Hi, I was charged twice for my subscription this month. "
|
| 29 |
+
"My account number is 84729 and both charges appeared on March 15th. "
|
| 30 |
+
"Please issue a refund for the duplicate charge as soon as possible. "
|
| 31 |
+
"Thank you."
|
| 32 |
+
),
|
| 33 |
+
"sender_type": "customer",
|
| 34 |
+
"expected_category": "billing",
|
| 35 |
+
"expected_priority": "high",
|
| 36 |
+
"response_keywords": ["refund", "apologize", "billing", "resolve", "account"],
|
| 37 |
+
},
|
| 38 |
+
"task_3": {
|
| 39 |
+
"id": "task_3",
|
| 40 |
+
"difficulty": "medium",
|
| 41 |
+
"description": "Multi-issue complaint requiring correct classification and tone",
|
| 42 |
+
"email_text": (
|
| 43 |
+
"I am absolutely furious! For the third time this week your app keeps "
|
| 44 |
+
"crashing every time I try to export my data. On top of that, you charged "
|
| 45 |
+
"me a premium fee for a feature that is completely broken! I have deadlines "
|
| 46 |
+
"to meet and your incompetent support team has not responded in 48 hours. "
|
| 47 |
+
"If this is not fixed TODAY I will be disputing the charge with my bank "
|
| 48 |
+
"and leaving a public review. This is completely unacceptable."
|
| 49 |
+
),
|
| 50 |
+
"sender_type": "customer",
|
| 51 |
+
"expected_category": "technical",
|
| 52 |
+
"expected_priority": "high",
|
| 53 |
+
"response_keywords": ["sorry", "apologize", "urgent", "escalate", "refund", "fix", "priority"],
|
| 54 |
+
},
|
| 55 |
+
"task_4": {
|
| 56 |
+
"id": "task_4",
|
| 57 |
+
"difficulty": "medium",
|
| 58 |
+
"description": "Internal system alert requiring urgent technical escalation",
|
| 59 |
+
"email_text": (
|
| 60 |
+
"ALERT: Production database replica lag has exceeded 120 seconds "
|
| 61 |
+
"on db-replica-03. Read queries are returning stale data. Multiple "
|
| 62 |
+
"customers have reported seeing outdated order statuses. The primary "
|
| 63 |
+
"node CPU is at 98% and autoscaling has not triggered. Oncall has "
|
| 64 |
+
"been paged but has not acknowledged. This is impacting checkout flow."
|
| 65 |
+
),
|
| 66 |
+
"sender_type": "system",
|
| 67 |
+
"expected_category": "technical",
|
| 68 |
+
"expected_priority": "high",
|
| 69 |
+
"response_keywords": ["immediately", "escalate", "database", "investigate", "oncall", "production"],
|
| 70 |
+
},
|
| 71 |
+
"task_5": {
|
| 72 |
+
"id": "task_5",
|
| 73 |
+
"difficulty": "hard",
|
| 74 |
+
"description": "Genuinely ambiguous email — looks like billing but root cause is technical",
|
| 75 |
+
"email_text": (
|
| 76 |
+
"Hi support, something weird is going on with my account. Last week I "
|
| 77 |
+
"upgraded from the basic plan to pro using the in-app upgrade button, "
|
| 78 |
+
"and the app showed a confirmation. My credit card was charged the pro "
|
| 79 |
+
"amount the same day. But when I log in, the app still shows me as a "
|
| 80 |
+
"basic user, my pro features are greyed out, and the billing page lists "
|
| 81 |
+
"my plan as 'basic'. I tried logging out and back in, and even reinstalling "
|
| 82 |
+
"the app on a different device — same thing. I am not sure if I should be "
|
| 83 |
+
"asking for a refund or if there is a bug in your upgrade flow that did "
|
| 84 |
+
"not actually flip my account over. Please advise."
|
| 85 |
+
),
|
| 86 |
+
"sender_type": "customer",
|
| 87 |
+
"expected_category": "technical",
|
| 88 |
+
"expected_priority": "medium",
|
| 89 |
+
"response_keywords": ["investigate", "account", "upgrade", "team", "check"],
|
| 90 |
+
},
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def get_task(task_id: str) -> dict:
|
| 95 |
+
if task_id not in TASKS:
|
| 96 |
+
raise ValueError(f"Unknown task_id: {task_id}. Valid ids: {list(TASKS.keys())}")
|
| 97 |
+
return TASKS[task_id]
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def list_tasks() -> list:
|
| 101 |
+
return [
|
| 102 |
+
{
|
| 103 |
+
"id": t["id"],
|
| 104 |
+
"difficulty": t["difficulty"],
|
| 105 |
+
"description": t["description"],
|
| 106 |
+
}
|
| 107 |
+
for t in TASKS.values()
|
| 108 |
+
]
|
eval_baseline.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pre-training evaluation harness.
|
| 2 |
+
|
| 3 |
+
Runs a base LLM agent against N episodes of DriftEnv and reports:
|
| 4 |
+
- mean episode reward
|
| 5 |
+
- mean fraction of max (max = 30.0 per episode)
|
| 6 |
+
- drift-sensitive-step accuracy <-- the KEY metric
|
| 7 |
+
- breakdown by drift event
|
| 8 |
+
- sample trajectories
|
| 9 |
+
|
| 10 |
+
The whole point is to confirm the base model's drift-sensitive-step accuracy
|
| 11 |
+
sits in the 20%-40% training-headroom zone. Above 60% means our task is too
|
| 12 |
+
easy and training won't show improvement. Below 10% means our task is too
|
| 13 |
+
hard for any RL within the time budget.
|
| 14 |
+
|
| 15 |
+
Usage:
|
| 16 |
+
API_BASE_URL=https://api.groq.com/openai/v1 \\
|
| 17 |
+
HF_TOKEN=gsk_... \\
|
| 18 |
+
MODEL_NAME=llama-3.1-8b-instant \\
|
| 19 |
+
PYTHONPATH=. python3 eval_baseline.py --episodes 10
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
import argparse
|
| 25 |
+
import json
|
| 26 |
+
import os
|
| 27 |
+
import sys
|
| 28 |
+
import time
|
| 29 |
+
from collections import defaultdict
|
| 30 |
+
from typing import Any
|
| 31 |
+
|
| 32 |
+
from dotenv import load_dotenv
|
| 33 |
+
|
| 34 |
+
from drift_env.environment import DriftEnv
|
| 35 |
+
from drift_env.llm_agent import LLMAgent
|
| 36 |
+
from drift_env.episodes import generate_episode
|
| 37 |
+
from drift_env.policy import drift_direction
|
| 38 |
+
|
| 39 |
+
load_dotenv()
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def run_one_episode(
|
| 43 |
+
agent: LLMAgent, seed: int, verbose: bool = False,
|
| 44 |
+
) -> dict[str, Any]:
|
| 45 |
+
env = DriftEnv()
|
| 46 |
+
obs = env.reset(seed=seed, episode_id=f"eval_{seed}")
|
| 47 |
+
ep_plan = generate_episode(seed=seed, episode_id=f"eval_{seed}")
|
| 48 |
+
|
| 49 |
+
total_reward = 0.0
|
| 50 |
+
breakdown_totals = defaultdict(float)
|
| 51 |
+
drift_sensitive_total = 0
|
| 52 |
+
drift_sensitive_correct = 0
|
| 53 |
+
per_drift: dict[str, dict[str, int]] = defaultdict(
|
| 54 |
+
lambda: {"total": 0, "correct": 0}
|
| 55 |
+
)
|
| 56 |
+
per_direction: dict[str, dict[str, int]] = defaultdict(
|
| 57 |
+
lambda: {"total": 0, "correct": 0}
|
| 58 |
+
)
|
| 59 |
+
trajectory = []
|
| 60 |
+
|
| 61 |
+
for i, step_plan in enumerate(ep_plan.steps):
|
| 62 |
+
action, raw = agent.act(obs)
|
| 63 |
+
result = env.step(action)
|
| 64 |
+
|
| 65 |
+
total_reward += result.reward
|
| 66 |
+
for k, v in result.info["breakdown"].items():
|
| 67 |
+
breakdown_totals[k] += v
|
| 68 |
+
|
| 69 |
+
sensitive_to = step_plan.drift_sensitive_to
|
| 70 |
+
is_correct = result.info["breakdown"]["compliance"] >= 1.0
|
| 71 |
+
if sensitive_to is not None:
|
| 72 |
+
drift_sensitive_total += 1
|
| 73 |
+
per_drift[sensitive_to]["total"] += 1
|
| 74 |
+
direction = drift_direction(sensitive_to)
|
| 75 |
+
if direction is not None:
|
| 76 |
+
per_direction[direction]["total"] += 1
|
| 77 |
+
if is_correct:
|
| 78 |
+
drift_sensitive_correct += 1
|
| 79 |
+
per_drift[sensitive_to]["correct"] += 1
|
| 80 |
+
if direction is not None:
|
| 81 |
+
per_direction[direction]["correct"] += 1
|
| 82 |
+
|
| 83 |
+
trajectory.append({
|
| 84 |
+
"step": i,
|
| 85 |
+
"email_kind": step_plan.email.kind.value,
|
| 86 |
+
"email_id": step_plan.email.id,
|
| 87 |
+
"action": action.model_dump(exclude_none=True),
|
| 88 |
+
"correct_hint": step_plan.correct_action_hint,
|
| 89 |
+
"drift_sensitive_to": sensitive_to,
|
| 90 |
+
"reward": result.reward,
|
| 91 |
+
"compliance": result.info["breakdown"]["compliance"],
|
| 92 |
+
})
|
| 93 |
+
|
| 94 |
+
if verbose:
|
| 95 |
+
sens = f" [SENS→{sensitive_to}]" if sensitive_to else ""
|
| 96 |
+
print(f" step {i:>2} [{step_plan.email.kind.value:<8}] "
|
| 97 |
+
f"reward={result.reward:.2f} comp={result.info['breakdown']['compliance']:.2f}{sens}")
|
| 98 |
+
|
| 99 |
+
if result.done:
|
| 100 |
+
break
|
| 101 |
+
if result.observation is not None:
|
| 102 |
+
obs = result.observation
|
| 103 |
+
|
| 104 |
+
return {
|
| 105 |
+
"seed": seed,
|
| 106 |
+
"total_reward": round(total_reward, 4),
|
| 107 |
+
"max_reward": 30.0,
|
| 108 |
+
"frac_of_max": round(total_reward / 30.0, 4),
|
| 109 |
+
"breakdown_totals": {k: round(v, 4) for k, v in breakdown_totals.items()},
|
| 110 |
+
"drift_sensitive_total": drift_sensitive_total,
|
| 111 |
+
"drift_sensitive_correct": drift_sensitive_correct,
|
| 112 |
+
"drift_sensitive_acc": (
|
| 113 |
+
round(drift_sensitive_correct / drift_sensitive_total, 4)
|
| 114 |
+
if drift_sensitive_total else None
|
| 115 |
+
),
|
| 116 |
+
"per_drift": {k: dict(v) for k, v in per_drift.items()},
|
| 117 |
+
"per_direction": {k: dict(v) for k, v in per_direction.items()},
|
| 118 |
+
"trajectory": trajectory,
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def summarise(results: list[dict[str, Any]]) -> dict[str, Any]:
|
| 123 |
+
n = len(results)
|
| 124 |
+
mean_reward = sum(r["total_reward"] for r in results) / n
|
| 125 |
+
mean_frac = sum(r["frac_of_max"] for r in results) / n
|
| 126 |
+
dst = sum(r["drift_sensitive_total"] for r in results)
|
| 127 |
+
dsc = sum(r["drift_sensitive_correct"] for r in results)
|
| 128 |
+
|
| 129 |
+
per_drift_agg: dict[str, dict[str, int]] = defaultdict(
|
| 130 |
+
lambda: {"total": 0, "correct": 0}
|
| 131 |
+
)
|
| 132 |
+
per_direction_agg: dict[str, dict[str, int]] = defaultdict(
|
| 133 |
+
lambda: {"total": 0, "correct": 0}
|
| 134 |
+
)
|
| 135 |
+
for r in results:
|
| 136 |
+
for name, stats in r["per_drift"].items():
|
| 137 |
+
per_drift_agg[name]["total"] += stats["total"]
|
| 138 |
+
per_drift_agg[name]["correct"] += stats["correct"]
|
| 139 |
+
for direction, stats in r.get("per_direction", {}).items():
|
| 140 |
+
per_direction_agg[direction]["total"] += stats["total"]
|
| 141 |
+
per_direction_agg[direction]["correct"] += stats["correct"]
|
| 142 |
+
|
| 143 |
+
return {
|
| 144 |
+
"episodes": n,
|
| 145 |
+
"mean_reward": round(mean_reward, 4),
|
| 146 |
+
"mean_frac_of_max": round(mean_frac, 4),
|
| 147 |
+
"drift_sensitive_total": dst,
|
| 148 |
+
"drift_sensitive_correct": dsc,
|
| 149 |
+
"drift_sensitive_acc": round(dsc / dst, 4) if dst else None,
|
| 150 |
+
"per_direction": {
|
| 151 |
+
k: {
|
| 152 |
+
**v,
|
| 153 |
+
"acc": round(v["correct"] / v["total"], 4) if v["total"] else None,
|
| 154 |
+
}
|
| 155 |
+
for k, v in per_direction_agg.items()
|
| 156 |
+
},
|
| 157 |
+
"per_drift": {
|
| 158 |
+
k: {
|
| 159 |
+
**v,
|
| 160 |
+
"acc": round(v["correct"] / v["total"], 4) if v["total"] else None,
|
| 161 |
+
}
|
| 162 |
+
for k, v in per_drift_agg.items()
|
| 163 |
+
},
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def main() -> int:
|
| 168 |
+
ap = argparse.ArgumentParser()
|
| 169 |
+
ap.add_argument("--episodes", type=int, default=10)
|
| 170 |
+
ap.add_argument("--start-seed", type=int, default=100)
|
| 171 |
+
ap.add_argument("--verbose", action="store_true")
|
| 172 |
+
ap.add_argument("--save", type=str, default="eval_results.json")
|
| 173 |
+
args = ap.parse_args()
|
| 174 |
+
|
| 175 |
+
api_base = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
|
| 176 |
+
api_key = os.getenv("HF_TOKEN") or os.getenv("API_KEY")
|
| 177 |
+
model = os.getenv("MODEL_NAME", "meta-llama/Llama-3.1-8B-Instruct")
|
| 178 |
+
|
| 179 |
+
if not api_key:
|
| 180 |
+
print("ERROR: set HF_TOKEN (or API_KEY) env var.", file=sys.stderr)
|
| 181 |
+
return 1
|
| 182 |
+
|
| 183 |
+
print(f"Model: {model}")
|
| 184 |
+
print(f"Base URL: {api_base}")
|
| 185 |
+
print(f"Episodes: {args.episodes}")
|
| 186 |
+
print("-" * 60)
|
| 187 |
+
|
| 188 |
+
agent = LLMAgent(api_key=api_key, base_url=api_base, model=model)
|
| 189 |
+
|
| 190 |
+
results = []
|
| 191 |
+
t0 = time.time()
|
| 192 |
+
for i in range(args.episodes):
|
| 193 |
+
seed = args.start_seed + i
|
| 194 |
+
if args.verbose:
|
| 195 |
+
print(f"\n=== Episode seed={seed} ===")
|
| 196 |
+
try:
|
| 197 |
+
r = run_one_episode(agent, seed=seed, verbose=args.verbose)
|
| 198 |
+
except Exception as e:
|
| 199 |
+
print(f" FAILED: {e}")
|
| 200 |
+
continue
|
| 201 |
+
results.append(r)
|
| 202 |
+
dsa = r["drift_sensitive_acc"]
|
| 203 |
+
dsa_str = f"{dsa:.2%}" if dsa is not None else "n/a"
|
| 204 |
+
pd = r.get("per_direction", {})
|
| 205 |
+
def _fmt(dir_):
|
| 206 |
+
s = pd.get(dir_, {})
|
| 207 |
+
t = s.get("total", 0); c = s.get("correct", 0)
|
| 208 |
+
return f"{dir_[:4]}={c}/{t}"
|
| 209 |
+
per_dir_str = " ".join(_fmt(d) for d in ("tightening", "loosening", "neutral"))
|
| 210 |
+
print(f" seed={seed} reward={r['total_reward']:.2f}/30 "
|
| 211 |
+
f"drift_acc={dsa_str} "
|
| 212 |
+
f"({r['drift_sensitive_correct']}/{r['drift_sensitive_total']}) "
|
| 213 |
+
f"[{per_dir_str}]")
|
| 214 |
+
|
| 215 |
+
dt = time.time() - t0
|
| 216 |
+
summary = summarise(results)
|
| 217 |
+
print("\n" + "=" * 60)
|
| 218 |
+
print("SUMMARY")
|
| 219 |
+
print("=" * 60)
|
| 220 |
+
print(json.dumps(summary, indent=2))
|
| 221 |
+
print(f"\nTook {dt:.1f}s for {args.episodes} episodes.")
|
| 222 |
+
|
| 223 |
+
# Interpretation
|
| 224 |
+
dsa = summary["drift_sensitive_acc"]
|
| 225 |
+
if dsa is None:
|
| 226 |
+
print("\n[warn] no drift-sensitive steps encountered.")
|
| 227 |
+
elif dsa < 0.10:
|
| 228 |
+
print(f"\n[warn] drift-sensitive acc = {dsa:.0%}. Too hard for RL — redesign.")
|
| 229 |
+
elif dsa > 0.60:
|
| 230 |
+
print(f"\n[warn] drift-sensitive acc = {dsa:.0%}. Too easy — base model already solves it.")
|
| 231 |
+
elif dsa > 0.40:
|
| 232 |
+
print(f"\n[ok-ish] drift-sensitive acc = {dsa:.0%}. In the training zone but on the easy side.")
|
| 233 |
+
else:
|
| 234 |
+
print(f"\n[OK] drift-sensitive acc = {dsa:.0%}. In the sweet spot for training.")
|
| 235 |
+
|
| 236 |
+
with open(args.save, "w") as f:
|
| 237 |
+
json.dump({"summary": summary, "results": results}, f, indent=2)
|
| 238 |
+
print(f"\nFull results written to {args.save}")
|
| 239 |
+
return 0
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
if __name__ == "__main__":
|
| 243 |
+
sys.exit(main())
|
eval_results.json
ADDED
|
@@ -0,0 +1,2816 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"summary": {
|
| 3 |
+
"episodes": 8,
|
| 4 |
+
"mean_reward": 23.125,
|
| 5 |
+
"mean_frac_of_max": 0.7708,
|
| 6 |
+
"drift_sensitive_total": 25,
|
| 7 |
+
"drift_sensitive_correct": 3,
|
| 8 |
+
"drift_sensitive_acc": 0.12,
|
| 9 |
+
"per_drift": {
|
| 10 |
+
"refund_cap_25": {
|
| 11 |
+
"total": 6,
|
| 12 |
+
"correct": 0,
|
| 13 |
+
"acc": 0.0
|
| 14 |
+
},
|
| 15 |
+
"escalate_manager": {
|
| 16 |
+
"total": 5,
|
| 17 |
+
"correct": 0,
|
| 18 |
+
"acc": 0.0
|
| 19 |
+
},
|
| 20 |
+
"sla_48hr": {
|
| 21 |
+
"total": 6,
|
| 22 |
+
"correct": 1,
|
| 23 |
+
"acc": 0.1667
|
| 24 |
+
},
|
| 25 |
+
"refund_cap_200": {
|
| 26 |
+
"total": 2,
|
| 27 |
+
"correct": 2,
|
| 28 |
+
"acc": 1.0
|
| 29 |
+
},
|
| 30 |
+
"sla_2hr": {
|
| 31 |
+
"total": 6,
|
| 32 |
+
"correct": 0,
|
| 33 |
+
"acc": 0.0
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
},
|
| 37 |
+
"results": [
|
| 38 |
+
{
|
| 39 |
+
"seed": 100,
|
| 40 |
+
"total_reward": 22.2,
|
| 41 |
+
"max_reward": 30.0,
|
| 42 |
+
"frac_of_max": 0.74,
|
| 43 |
+
"breakdown_totals": {
|
| 44 |
+
"compliance": 13.2,
|
| 45 |
+
"appropriateness": 9.0,
|
| 46 |
+
"drift_bonus": 0.0
|
| 47 |
+
},
|
| 48 |
+
"drift_sensitive_total": 4,
|
| 49 |
+
"drift_sensitive_correct": 0,
|
| 50 |
+
"drift_sensitive_acc": 0.0,
|
| 51 |
+
"per_drift": {
|
| 52 |
+
"refund_cap_25": {
|
| 53 |
+
"total": 1,
|
| 54 |
+
"correct": 0
|
| 55 |
+
},
|
| 56 |
+
"escalate_manager": {
|
| 57 |
+
"total": 3,
|
| 58 |
+
"correct": 0
|
| 59 |
+
}
|
| 60 |
+
},
|
| 61 |
+
"trajectory": [
|
| 62 |
+
{
|
| 63 |
+
"step": 0,
|
| 64 |
+
"email_kind": "customer",
|
| 65 |
+
"email_id": "c_0_billing_invoice_download",
|
| 66 |
+
"action": {
|
| 67 |
+
"action_type": "reply"
|
| 68 |
+
},
|
| 69 |
+
"correct_hint": {
|
| 70 |
+
"action_type": "reply"
|
| 71 |
+
},
|
| 72 |
+
"drift_sensitive_to": null,
|
| 73 |
+
"reward": 1.5,
|
| 74 |
+
"compliance": 1.0
|
| 75 |
+
},
|
| 76 |
+
{
|
| 77 |
+
"step": 1,
|
| 78 |
+
"email_kind": "customer",
|
| 79 |
+
"email_id": "c_1_refund_50",
|
| 80 |
+
"action": {
|
| 81 |
+
"action_type": "approve_refund",
|
| 82 |
+
"refund_amount": 50.0
|
| 83 |
+
},
|
| 84 |
+
"correct_hint": {
|
| 85 |
+
"action_type": "approve_refund",
|
| 86 |
+
"refund_amount": 50.0
|
| 87 |
+
},
|
| 88 |
+
"drift_sensitive_to": null,
|
| 89 |
+
"reward": 1.5,
|
| 90 |
+
"compliance": 1.0
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
"step": 2,
|
| 94 |
+
"email_kind": "customer",
|
| 95 |
+
"email_id": "c_2_refund_40",
|
| 96 |
+
"action": {
|
| 97 |
+
"action_type": "approve_refund",
|
| 98 |
+
"refund_amount": 40.0
|
| 99 |
+
},
|
| 100 |
+
"correct_hint": {
|
| 101 |
+
"action_type": "approve_refund",
|
| 102 |
+
"refund_amount": 40.0
|
| 103 |
+
},
|
| 104 |
+
"drift_sensitive_to": null,
|
| 105 |
+
"reward": 1.5,
|
| 106 |
+
"compliance": 1.0
|
| 107 |
+
},
|
| 108 |
+
{
|
| 109 |
+
"step": 3,
|
| 110 |
+
"email_kind": "admin",
|
| 111 |
+
"email_id": "admin_3_refund_cap_25",
|
| 112 |
+
"action": {
|
| 113 |
+
"action_type": "close",
|
| 114 |
+
"resolution_code": "policy_acknowledged"
|
| 115 |
+
},
|
| 116 |
+
"correct_hint": {
|
| 117 |
+
"action_type": "close",
|
| 118 |
+
"resolution_code": "policy_acknowledged"
|
| 119 |
+
},
|
| 120 |
+
"drift_sensitive_to": null,
|
| 121 |
+
"reward": 1.0,
|
| 122 |
+
"compliance": 1.0
|
| 123 |
+
},
|
| 124 |
+
{
|
| 125 |
+
"step": 4,
|
| 126 |
+
"email_kind": "customer",
|
| 127 |
+
"email_id": "c_4_billing_prorated",
|
| 128 |
+
"action": {
|
| 129 |
+
"action_type": "reply"
|
| 130 |
+
},
|
| 131 |
+
"correct_hint": {
|
| 132 |
+
"action_type": "reply"
|
| 133 |
+
},
|
| 134 |
+
"drift_sensitive_to": null,
|
| 135 |
+
"reward": 1.5,
|
| 136 |
+
"compliance": 1.0
|
| 137 |
+
},
|
| 138 |
+
{
|
| 139 |
+
"step": 5,
|
| 140 |
+
"email_kind": "customer",
|
| 141 |
+
"email_id": "c_5_refund_90",
|
| 142 |
+
"action": {
|
| 143 |
+
"action_type": "approve_refund",
|
| 144 |
+
"refund_amount": 90.0
|
| 145 |
+
},
|
| 146 |
+
"correct_hint": {
|
| 147 |
+
"action_type": "escalate",
|
| 148 |
+
"escalation_tier": "manager"
|
| 149 |
+
},
|
| 150 |
+
"drift_sensitive_to": "refund_cap_25",
|
| 151 |
+
"reward": 0.5,
|
| 152 |
+
"compliance": 0.0
|
| 153 |
+
},
|
| 154 |
+
{
|
| 155 |
+
"step": 6,
|
| 156 |
+
"email_kind": "customer",
|
| 157 |
+
"email_id": "c_6_refund_150",
|
| 158 |
+
"action": {
|
| 159 |
+
"action_type": "approve_refund",
|
| 160 |
+
"refund_amount": 150.0
|
| 161 |
+
},
|
| 162 |
+
"correct_hint": {
|
| 163 |
+
"action_type": "escalate",
|
| 164 |
+
"escalation_tier": "manager"
|
| 165 |
+
},
|
| 166 |
+
"drift_sensitive_to": null,
|
| 167 |
+
"reward": 0.5,
|
| 168 |
+
"compliance": 0.0
|
| 169 |
+
},
|
| 170 |
+
{
|
| 171 |
+
"step": 7,
|
| 172 |
+
"email_kind": "customer",
|
| 173 |
+
"email_id": "c_7_billing_tiers",
|
| 174 |
+
"action": {
|
| 175 |
+
"action_type": "reply"
|
| 176 |
+
},
|
| 177 |
+
"correct_hint": {
|
| 178 |
+
"action_type": "reply"
|
| 179 |
+
},
|
| 180 |
+
"drift_sensitive_to": null,
|
| 181 |
+
"reward": 1.5,
|
| 182 |
+
"compliance": 1.0
|
| 183 |
+
},
|
| 184 |
+
{
|
| 185 |
+
"step": 8,
|
| 186 |
+
"email_kind": "customer",
|
| 187 |
+
"email_id": "c_8_info_missing_date",
|
| 188 |
+
"action": {
|
| 189 |
+
"action_type": "request_info",
|
| 190 |
+
"info_field": "order_id"
|
| 191 |
+
},
|
| 192 |
+
"correct_hint": {
|
| 193 |
+
"action_type": "request_info",
|
| 194 |
+
"info_field": "charge_date"
|
| 195 |
+
},
|
| 196 |
+
"drift_sensitive_to": null,
|
| 197 |
+
"reward": 1.0,
|
| 198 |
+
"compliance": 0.5
|
| 199 |
+
},
|
| 200 |
+
{
|
| 201 |
+
"step": 9,
|
| 202 |
+
"email_kind": "customer",
|
| 203 |
+
"email_id": "c_9_refund_250",
|
| 204 |
+
"action": {
|
| 205 |
+
"action_type": "escalate",
|
| 206 |
+
"escalation_tier": "manager",
|
| 207 |
+
"followup_hours": 48
|
| 208 |
+
},
|
| 209 |
+
"correct_hint": {
|
| 210 |
+
"action_type": "escalate",
|
| 211 |
+
"escalation_tier": "manager"
|
| 212 |
+
},
|
| 213 |
+
"drift_sensitive_to": null,
|
| 214 |
+
"reward": 1.5,
|
| 215 |
+
"compliance": 1.0
|
| 216 |
+
},
|
| 217 |
+
{
|
| 218 |
+
"step": 10,
|
| 219 |
+
"email_kind": "customer",
|
| 220 |
+
"email_id": "c_10_critical_data_loss",
|
| 221 |
+
"action": {
|
| 222 |
+
"action_type": "escalate",
|
| 223 |
+
"escalation_tier": "tier_1",
|
| 224 |
+
"followup_hours": 2
|
| 225 |
+
},
|
| 226 |
+
"correct_hint": {
|
| 227 |
+
"action_type": "escalate",
|
| 228 |
+
"escalation_tier": "tier_2",
|
| 229 |
+
"followup_hours": 24
|
| 230 |
+
},
|
| 231 |
+
"drift_sensitive_to": null,
|
| 232 |
+
"reward": 0.9,
|
| 233 |
+
"compliance": 0.4
|
| 234 |
+
},
|
| 235 |
+
{
|
| 236 |
+
"step": 11,
|
| 237 |
+
"email_kind": "admin",
|
| 238 |
+
"email_id": "admin_11_escalate_manager",
|
| 239 |
+
"action": {
|
| 240 |
+
"action_type": "escalate",
|
| 241 |
+
"escalation_tier": "manager",
|
| 242 |
+
"followup_hours": 2
|
| 243 |
+
},
|
| 244 |
+
"correct_hint": {
|
| 245 |
+
"action_type": "close",
|
| 246 |
+
"resolution_code": "policy_acknowledged"
|
| 247 |
+
},
|
| 248 |
+
"drift_sensitive_to": null,
|
| 249 |
+
"reward": 0.2,
|
| 250 |
+
"compliance": 0.2
|
| 251 |
+
},
|
| 252 |
+
{
|
| 253 |
+
"step": 12,
|
| 254 |
+
"email_kind": "customer",
|
| 255 |
+
"email_id": "c_12_billing_tiers",
|
| 256 |
+
"action": {
|
| 257 |
+
"action_type": "reply"
|
| 258 |
+
},
|
| 259 |
+
"correct_hint": {
|
| 260 |
+
"action_type": "reply"
|
| 261 |
+
},
|
| 262 |
+
"drift_sensitive_to": null,
|
| 263 |
+
"reward": 1.5,
|
| 264 |
+
"compliance": 1.0
|
| 265 |
+
},
|
| 266 |
+
{
|
| 267 |
+
"step": 13,
|
| 268 |
+
"email_kind": "customer",
|
| 269 |
+
"email_id": "c_13_critical_security",
|
| 270 |
+
"action": {
|
| 271 |
+
"action_type": "escalate",
|
| 272 |
+
"escalation_tier": "manager",
|
| 273 |
+
"followup_hours": 2
|
| 274 |
+
},
|
| 275 |
+
"correct_hint": {
|
| 276 |
+
"action_type": "escalate",
|
| 277 |
+
"escalation_tier": "manager",
|
| 278 |
+
"followup_hours": 24
|
| 279 |
+
},
|
| 280 |
+
"drift_sensitive_to": "escalate_manager",
|
| 281 |
+
"reward": 1.2,
|
| 282 |
+
"compliance": 0.7
|
| 283 |
+
},
|
| 284 |
+
{
|
| 285 |
+
"step": 14,
|
| 286 |
+
"email_kind": "customer",
|
| 287 |
+
"email_id": "c_14_critical_outage",
|
| 288 |
+
"action": {
|
| 289 |
+
"action_type": "escalate",
|
| 290 |
+
"escalation_tier": "manager",
|
| 291 |
+
"followup_hours": 2
|
| 292 |
+
},
|
| 293 |
+
"correct_hint": {
|
| 294 |
+
"action_type": "escalate",
|
| 295 |
+
"escalation_tier": "manager",
|
| 296 |
+
"followup_hours": 24
|
| 297 |
+
},
|
| 298 |
+
"drift_sensitive_to": "escalate_manager",
|
| 299 |
+
"reward": 1.2,
|
| 300 |
+
"compliance": 0.7
|
| 301 |
+
},
|
| 302 |
+
{
|
| 303 |
+
"step": 15,
|
| 304 |
+
"email_kind": "customer",
|
| 305 |
+
"email_id": "c_15_refund_50",
|
| 306 |
+
"action": {
|
| 307 |
+
"action_type": "approve_refund",
|
| 308 |
+
"refund_amount": 50.0
|
| 309 |
+
},
|
| 310 |
+
"correct_hint": {
|
| 311 |
+
"action_type": "escalate",
|
| 312 |
+
"escalation_tier": "manager"
|
| 313 |
+
},
|
| 314 |
+
"drift_sensitive_to": null,
|
| 315 |
+
"reward": 0.5,
|
| 316 |
+
"compliance": 0.0
|
| 317 |
+
},
|
| 318 |
+
{
|
| 319 |
+
"step": 16,
|
| 320 |
+
"email_kind": "customer",
|
| 321 |
+
"email_id": "c_16_critical_outage",
|
| 322 |
+
"action": {
|
| 323 |
+
"action_type": "escalate",
|
| 324 |
+
"escalation_tier": "manager",
|
| 325 |
+
"followup_hours": 2
|
| 326 |
+
},
|
| 327 |
+
"correct_hint": {
|
| 328 |
+
"action_type": "escalate",
|
| 329 |
+
"escalation_tier": "manager",
|
| 330 |
+
"followup_hours": 24
|
| 331 |
+
},
|
| 332 |
+
"drift_sensitive_to": "escalate_manager",
|
| 333 |
+
"reward": 1.2,
|
| 334 |
+
"compliance": 0.7
|
| 335 |
+
},
|
| 336 |
+
{
|
| 337 |
+
"step": 17,
|
| 338 |
+
"email_kind": "customer",
|
| 339 |
+
"email_id": "c_17_refund_75",
|
| 340 |
+
"action": {
|
| 341 |
+
"action_type": "approve_refund",
|
| 342 |
+
"refund_amount": 75.0
|
| 343 |
+
},
|
| 344 |
+
"correct_hint": {
|
| 345 |
+
"action_type": "escalate",
|
| 346 |
+
"escalation_tier": "manager"
|
| 347 |
+
},
|
| 348 |
+
"drift_sensitive_to": null,
|
| 349 |
+
"reward": 0.5,
|
| 350 |
+
"compliance": 0.0
|
| 351 |
+
},
|
| 352 |
+
{
|
| 353 |
+
"step": 18,
|
| 354 |
+
"email_kind": "customer",
|
| 355 |
+
"email_id": "c_18_refund_15",
|
| 356 |
+
"action": {
|
| 357 |
+
"action_type": "approve_refund",
|
| 358 |
+
"refund_amount": 15.0
|
| 359 |
+
},
|
| 360 |
+
"correct_hint": {
|
| 361 |
+
"action_type": "approve_refund",
|
| 362 |
+
"refund_amount": 15.0
|
| 363 |
+
},
|
| 364 |
+
"drift_sensitive_to": null,
|
| 365 |
+
"reward": 1.5,
|
| 366 |
+
"compliance": 1.0
|
| 367 |
+
},
|
| 368 |
+
{
|
| 369 |
+
"step": 19,
|
| 370 |
+
"email_kind": "customer",
|
| 371 |
+
"email_id": "c_19_billing_tiers",
|
| 372 |
+
"action": {
|
| 373 |
+
"action_type": "reply"
|
| 374 |
+
},
|
| 375 |
+
"correct_hint": {
|
| 376 |
+
"action_type": "reply"
|
| 377 |
+
},
|
| 378 |
+
"drift_sensitive_to": null,
|
| 379 |
+
"reward": 1.5,
|
| 380 |
+
"compliance": 1.0
|
| 381 |
+
}
|
| 382 |
+
]
|
| 383 |
+
},
|
| 384 |
+
{
|
| 385 |
+
"seed": 101,
|
| 386 |
+
"total_reward": 20.5,
|
| 387 |
+
"max_reward": 30.0,
|
| 388 |
+
"frac_of_max": 0.6833,
|
| 389 |
+
"breakdown_totals": {
|
| 390 |
+
"compliance": 11.0,
|
| 391 |
+
"appropriateness": 9.0,
|
| 392 |
+
"drift_bonus": 0.5
|
| 393 |
+
},
|
| 394 |
+
"drift_sensitive_total": 5,
|
| 395 |
+
"drift_sensitive_correct": 1,
|
| 396 |
+
"drift_sensitive_acc": 0.2,
|
| 397 |
+
"per_drift": {
|
| 398 |
+
"sla_48hr": {
|
| 399 |
+
"total": 3,
|
| 400 |
+
"correct": 1
|
| 401 |
+
},
|
| 402 |
+
"refund_cap_25": {
|
| 403 |
+
"total": 2,
|
| 404 |
+
"correct": 0
|
| 405 |
+
}
|
| 406 |
+
},
|
| 407 |
+
"trajectory": [
|
| 408 |
+
{
|
| 409 |
+
"step": 0,
|
| 410 |
+
"email_kind": "customer",
|
| 411 |
+
"email_id": "c_0_critical_payment_fail",
|
| 412 |
+
"action": {
|
| 413 |
+
"action_type": "escalate",
|
| 414 |
+
"escalation_tier": "tier_1",
|
| 415 |
+
"followup_hours": 2
|
| 416 |
+
},
|
| 417 |
+
"correct_hint": {
|
| 418 |
+
"action_type": "escalate",
|
| 419 |
+
"escalation_tier": "tier_2",
|
| 420 |
+
"followup_hours": 24
|
| 421 |
+
},
|
| 422 |
+
"drift_sensitive_to": null,
|
| 423 |
+
"reward": 0.9,
|
| 424 |
+
"compliance": 0.4
|
| 425 |
+
},
|
| 426 |
+
{
|
| 427 |
+
"step": 1,
|
| 428 |
+
"email_kind": "customer",
|
| 429 |
+
"email_id": "c_1_refund_100",
|
| 430 |
+
"action": {
|
| 431 |
+
"action_type": "approve_refund",
|
| 432 |
+
"refund_amount": 100.0
|
| 433 |
+
},
|
| 434 |
+
"correct_hint": {
|
| 435 |
+
"action_type": "approve_refund",
|
| 436 |
+
"refund_amount": 100.0
|
| 437 |
+
},
|
| 438 |
+
"drift_sensitive_to": null,
|
| 439 |
+
"reward": 1.5,
|
| 440 |
+
"compliance": 1.0
|
| 441 |
+
},
|
| 442 |
+
{
|
| 443 |
+
"step": 2,
|
| 444 |
+
"email_kind": "customer",
|
| 445 |
+
"email_id": "c_2_chitchat_feedback",
|
| 446 |
+
"action": {
|
| 447 |
+
"action_type": "close",
|
| 448 |
+
"resolution_code": "no_action_needed"
|
| 449 |
+
},
|
| 450 |
+
"correct_hint": {
|
| 451 |
+
"action_type": "close",
|
| 452 |
+
"resolution_code": "no_action_needed"
|
| 453 |
+
},
|
| 454 |
+
"drift_sensitive_to": null,
|
| 455 |
+
"reward": 1.5,
|
| 456 |
+
"compliance": 1.0
|
| 457 |
+
},
|
| 458 |
+
{
|
| 459 |
+
"step": 3,
|
| 460 |
+
"email_kind": "admin",
|
| 461 |
+
"email_id": "admin_3_sla_48hr",
|
| 462 |
+
"action": {
|
| 463 |
+
"action_type": "close",
|
| 464 |
+
"resolution_code": "error_fallback"
|
| 465 |
+
},
|
| 466 |
+
"correct_hint": {
|
| 467 |
+
"action_type": "close",
|
| 468 |
+
"resolution_code": "policy_acknowledged"
|
| 469 |
+
},
|
| 470 |
+
"drift_sensitive_to": null,
|
| 471 |
+
"reward": 1.0,
|
| 472 |
+
"compliance": 1.0
|
| 473 |
+
},
|
| 474 |
+
{
|
| 475 |
+
"step": 4,
|
| 476 |
+
"email_kind": "customer",
|
| 477 |
+
"email_id": "c_4_refund_90",
|
| 478 |
+
"action": {
|
| 479 |
+
"action_type": "approve_refund",
|
| 480 |
+
"refund_amount": 90.0
|
| 481 |
+
},
|
| 482 |
+
"correct_hint": {
|
| 483 |
+
"action_type": "approve_refund",
|
| 484 |
+
"refund_amount": 90.0
|
| 485 |
+
},
|
| 486 |
+
"drift_sensitive_to": null,
|
| 487 |
+
"reward": 1.5,
|
| 488 |
+
"compliance": 1.0
|
| 489 |
+
},
|
| 490 |
+
{
|
| 491 |
+
"step": 5,
|
| 492 |
+
"email_kind": "customer",
|
| 493 |
+
"email_id": "c_5_critical_security",
|
| 494 |
+
"action": {
|
| 495 |
+
"action_type": "escalate",
|
| 496 |
+
"escalation_tier": "tier_1",
|
| 497 |
+
"followup_hours": 48
|
| 498 |
+
},
|
| 499 |
+
"correct_hint": {
|
| 500 |
+
"action_type": "escalate",
|
| 501 |
+
"escalation_tier": "tier_2",
|
| 502 |
+
"followup_hours": 48
|
| 503 |
+
},
|
| 504 |
+
"drift_sensitive_to": "sla_48hr",
|
| 505 |
+
"reward": 1.2,
|
| 506 |
+
"compliance": 0.7
|
| 507 |
+
},
|
| 508 |
+
{
|
| 509 |
+
"step": 6,
|
| 510 |
+
"email_kind": "customer",
|
| 511 |
+
"email_id": "c_6_refund_90",
|
| 512 |
+
"action": {
|
| 513 |
+
"action_type": "approve_refund",
|
| 514 |
+
"refund_amount": 90.0
|
| 515 |
+
},
|
| 516 |
+
"correct_hint": {
|
| 517 |
+
"action_type": "approve_refund",
|
| 518 |
+
"refund_amount": 90.0
|
| 519 |
+
},
|
| 520 |
+
"drift_sensitive_to": null,
|
| 521 |
+
"reward": 1.5,
|
| 522 |
+
"compliance": 1.0
|
| 523 |
+
},
|
| 524 |
+
{
|
| 525 |
+
"step": 7,
|
| 526 |
+
"email_kind": "customer",
|
| 527 |
+
"email_id": "c_7_critical_security",
|
| 528 |
+
"action": {
|
| 529 |
+
"action_type": "escalate",
|
| 530 |
+
"escalation_tier": "tier_2",
|
| 531 |
+
"followup_hours": 48
|
| 532 |
+
},
|
| 533 |
+
"correct_hint": {
|
| 534 |
+
"action_type": "escalate",
|
| 535 |
+
"escalation_tier": "tier_2",
|
| 536 |
+
"followup_hours": 48
|
| 537 |
+
},
|
| 538 |
+
"drift_sensitive_to": "sla_48hr",
|
| 539 |
+
"reward": 2.0,
|
| 540 |
+
"compliance": 1.0
|
| 541 |
+
},
|
| 542 |
+
{
|
| 543 |
+
"step": 8,
|
| 544 |
+
"email_kind": "customer",
|
| 545 |
+
"email_id": "c_8_billing_invoice_download",
|
| 546 |
+
"action": {
|
| 547 |
+
"action_type": "reply"
|
| 548 |
+
},
|
| 549 |
+
"correct_hint": {
|
| 550 |
+
"action_type": "reply"
|
| 551 |
+
},
|
| 552 |
+
"drift_sensitive_to": null,
|
| 553 |
+
"reward": 1.5,
|
| 554 |
+
"compliance": 1.0
|
| 555 |
+
},
|
| 556 |
+
{
|
| 557 |
+
"step": 9,
|
| 558 |
+
"email_kind": "customer",
|
| 559 |
+
"email_id": "c_9_refund_180",
|
| 560 |
+
"action": {
|
| 561 |
+
"action_type": "approve_refund",
|
| 562 |
+
"refund_amount": 180.0
|
| 563 |
+
},
|
| 564 |
+
"correct_hint": {
|
| 565 |
+
"action_type": "escalate",
|
| 566 |
+
"escalation_tier": "manager"
|
| 567 |
+
},
|
| 568 |
+
"drift_sensitive_to": null,
|
| 569 |
+
"reward": 0.5,
|
| 570 |
+
"compliance": 0.0
|
| 571 |
+
},
|
| 572 |
+
{
|
| 573 |
+
"step": 10,
|
| 574 |
+
"email_kind": "customer",
|
| 575 |
+
"email_id": "c_10_critical_outage",
|
| 576 |
+
"action": {
|
| 577 |
+
"action_type": "escalate",
|
| 578 |
+
"escalation_tier": "tier_1",
|
| 579 |
+
"followup_hours": 48
|
| 580 |
+
},
|
| 581 |
+
"correct_hint": {
|
| 582 |
+
"action_type": "escalate",
|
| 583 |
+
"escalation_tier": "tier_2",
|
| 584 |
+
"followup_hours": 48
|
| 585 |
+
},
|
| 586 |
+
"drift_sensitive_to": "sla_48hr",
|
| 587 |
+
"reward": 1.2,
|
| 588 |
+
"compliance": 0.7
|
| 589 |
+
},
|
| 590 |
+
{
|
| 591 |
+
"step": 11,
|
| 592 |
+
"email_kind": "admin",
|
| 593 |
+
"email_id": "admin_11_refund_cap_25",
|
| 594 |
+
"action": {
|
| 595 |
+
"action_type": "close",
|
| 596 |
+
"resolution_code": "policy_acknowledged"
|
| 597 |
+
},
|
| 598 |
+
"correct_hint": {
|
| 599 |
+
"action_type": "close",
|
| 600 |
+
"resolution_code": "policy_acknowledged"
|
| 601 |
+
},
|
| 602 |
+
"drift_sensitive_to": null,
|
| 603 |
+
"reward": 1.0,
|
| 604 |
+
"compliance": 1.0
|
| 605 |
+
},
|
| 606 |
+
{
|
| 607 |
+
"step": 12,
|
| 608 |
+
"email_kind": "customer",
|
| 609 |
+
"email_id": "c_12_refund_40",
|
| 610 |
+
"action": {
|
| 611 |
+
"action_type": "approve_refund",
|
| 612 |
+
"refund_amount": 40.0
|
| 613 |
+
},
|
| 614 |
+
"correct_hint": {
|
| 615 |
+
"action_type": "escalate",
|
| 616 |
+
"escalation_tier": "manager"
|
| 617 |
+
},
|
| 618 |
+
"drift_sensitive_to": "refund_cap_25",
|
| 619 |
+
"reward": 0.5,
|
| 620 |
+
"compliance": 0.0
|
| 621 |
+
},
|
| 622 |
+
{
|
| 623 |
+
"step": 13,
|
| 624 |
+
"email_kind": "customer",
|
| 625 |
+
"email_id": "c_13_refund_150",
|
| 626 |
+
"action": {
|
| 627 |
+
"action_type": "approve_refund",
|
| 628 |
+
"refund_amount": 150.0
|
| 629 |
+
},
|
| 630 |
+
"correct_hint": {
|
| 631 |
+
"action_type": "escalate",
|
| 632 |
+
"escalation_tier": "manager"
|
| 633 |
+
},
|
| 634 |
+
"drift_sensitive_to": null,
|
| 635 |
+
"reward": 0.5,
|
| 636 |
+
"compliance": 0.0
|
| 637 |
+
},
|
| 638 |
+
{
|
| 639 |
+
"step": 14,
|
| 640 |
+
"email_kind": "customer",
|
| 641 |
+
"email_id": "c_14_refund_180",
|
| 642 |
+
"action": {
|
| 643 |
+
"action_type": "approve_refund",
|
| 644 |
+
"refund_amount": 180.0
|
| 645 |
+
},
|
| 646 |
+
"correct_hint": {
|
| 647 |
+
"action_type": "escalate",
|
| 648 |
+
"escalation_tier": "manager"
|
| 649 |
+
},
|
| 650 |
+
"drift_sensitive_to": null,
|
| 651 |
+
"reward": 0.5,
|
| 652 |
+
"compliance": 0.0
|
| 653 |
+
},
|
| 654 |
+
{
|
| 655 |
+
"step": 15,
|
| 656 |
+
"email_kind": "customer",
|
| 657 |
+
"email_id": "c_15_refund_120",
|
| 658 |
+
"action": {
|
| 659 |
+
"action_type": "approve_refund",
|
| 660 |
+
"refund_amount": 120.0
|
| 661 |
+
},
|
| 662 |
+
"correct_hint": {
|
| 663 |
+
"action_type": "escalate",
|
| 664 |
+
"escalation_tier": "manager"
|
| 665 |
+
},
|
| 666 |
+
"drift_sensitive_to": null,
|
| 667 |
+
"reward": 0.5,
|
| 668 |
+
"compliance": 0.0
|
| 669 |
+
},
|
| 670 |
+
{
|
| 671 |
+
"step": 16,
|
| 672 |
+
"email_kind": "customer",
|
| 673 |
+
"email_id": "c_16_critical_outage",
|
| 674 |
+
"action": {
|
| 675 |
+
"action_type": "escalate",
|
| 676 |
+
"escalation_tier": "tier_1",
|
| 677 |
+
"followup_hours": 2
|
| 678 |
+
},
|
| 679 |
+
"correct_hint": {
|
| 680 |
+
"action_type": "escalate",
|
| 681 |
+
"escalation_tier": "tier_2",
|
| 682 |
+
"followup_hours": 48
|
| 683 |
+
},
|
| 684 |
+
"drift_sensitive_to": null,
|
| 685 |
+
"reward": 0.9,
|
| 686 |
+
"compliance": 0.4
|
| 687 |
+
},
|
| 688 |
+
{
|
| 689 |
+
"step": 17,
|
| 690 |
+
"email_kind": "customer",
|
| 691 |
+
"email_id": "c_17_refund_40",
|
| 692 |
+
"action": {
|
| 693 |
+
"action_type": "approve_refund",
|
| 694 |
+
"refund_amount": 40.0
|
| 695 |
+
},
|
| 696 |
+
"correct_hint": {
|
| 697 |
+
"action_type": "escalate",
|
| 698 |
+
"escalation_tier": "manager"
|
| 699 |
+
},
|
| 700 |
+
"drift_sensitive_to": "refund_cap_25",
|
| 701 |
+
"reward": 0.5,
|
| 702 |
+
"compliance": 0.0
|
| 703 |
+
},
|
| 704 |
+
{
|
| 705 |
+
"step": 18,
|
| 706 |
+
"email_kind": "customer",
|
| 707 |
+
"email_id": "c_18_critical_data_loss",
|
| 708 |
+
"action": {
|
| 709 |
+
"action_type": "escalate",
|
| 710 |
+
"escalation_tier": "tier_1",
|
| 711 |
+
"followup_hours": 2
|
| 712 |
+
},
|
| 713 |
+
"correct_hint": {
|
| 714 |
+
"action_type": "escalate",
|
| 715 |
+
"escalation_tier": "tier_2",
|
| 716 |
+
"followup_hours": 48
|
| 717 |
+
},
|
| 718 |
+
"drift_sensitive_to": null,
|
| 719 |
+
"reward": 0.9,
|
| 720 |
+
"compliance": 0.4
|
| 721 |
+
},
|
| 722 |
+
{
|
| 723 |
+
"step": 19,
|
| 724 |
+
"email_kind": "customer",
|
| 725 |
+
"email_id": "c_19_critical_security",
|
| 726 |
+
"action": {
|
| 727 |
+
"action_type": "escalate",
|
| 728 |
+
"escalation_tier": "tier_1",
|
| 729 |
+
"followup_hours": 2
|
| 730 |
+
},
|
| 731 |
+
"correct_hint": {
|
| 732 |
+
"action_type": "escalate",
|
| 733 |
+
"escalation_tier": "tier_2",
|
| 734 |
+
"followup_hours": 48
|
| 735 |
+
},
|
| 736 |
+
"drift_sensitive_to": null,
|
| 737 |
+
"reward": 0.9,
|
| 738 |
+
"compliance": 0.4
|
| 739 |
+
}
|
| 740 |
+
]
|
| 741 |
+
},
|
| 742 |
+
{
|
| 743 |
+
"seed": 102,
|
| 744 |
+
"total_reward": 24.7,
|
| 745 |
+
"max_reward": 30.0,
|
| 746 |
+
"frac_of_max": 0.8233,
|
| 747 |
+
"breakdown_totals": {
|
| 748 |
+
"compliance": 15.7,
|
| 749 |
+
"appropriateness": 9.0,
|
| 750 |
+
"drift_bonus": 0.0
|
| 751 |
+
},
|
| 752 |
+
"drift_sensitive_total": 0,
|
| 753 |
+
"drift_sensitive_correct": 0,
|
| 754 |
+
"drift_sensitive_acc": null,
|
| 755 |
+
"per_drift": {},
|
| 756 |
+
"trajectory": [
|
| 757 |
+
{
|
| 758 |
+
"step": 0,
|
| 759 |
+
"email_kind": "customer",
|
| 760 |
+
"email_id": "c_0_info_missing_date",
|
| 761 |
+
"action": {
|
| 762 |
+
"action_type": "request_info",
|
| 763 |
+
"info_field": "order_id"
|
| 764 |
+
},
|
| 765 |
+
"correct_hint": {
|
| 766 |
+
"action_type": "request_info",
|
| 767 |
+
"info_field": "charge_date"
|
| 768 |
+
},
|
| 769 |
+
"drift_sensitive_to": null,
|
| 770 |
+
"reward": 1.0,
|
| 771 |
+
"compliance": 0.5
|
| 772 |
+
},
|
| 773 |
+
{
|
| 774 |
+
"step": 1,
|
| 775 |
+
"email_kind": "customer",
|
| 776 |
+
"email_id": "c_1_critical_data_loss",
|
| 777 |
+
"action": {
|
| 778 |
+
"action_type": "escalate",
|
| 779 |
+
"escalation_tier": "tier_1",
|
| 780 |
+
"followup_hours": 2
|
| 781 |
+
},
|
| 782 |
+
"correct_hint": {
|
| 783 |
+
"action_type": "escalate",
|
| 784 |
+
"escalation_tier": "tier_2",
|
| 785 |
+
"followup_hours": 24
|
| 786 |
+
},
|
| 787 |
+
"drift_sensitive_to": null,
|
| 788 |
+
"reward": 0.9,
|
| 789 |
+
"compliance": 0.4
|
| 790 |
+
},
|
| 791 |
+
{
|
| 792 |
+
"step": 2,
|
| 793 |
+
"email_kind": "customer",
|
| 794 |
+
"email_id": "c_2_refund_30",
|
| 795 |
+
"action": {
|
| 796 |
+
"action_type": "approve_refund",
|
| 797 |
+
"refund_amount": 30.0
|
| 798 |
+
},
|
| 799 |
+
"correct_hint": {
|
| 800 |
+
"action_type": "approve_refund",
|
| 801 |
+
"refund_amount": 30.0
|
| 802 |
+
},
|
| 803 |
+
"drift_sensitive_to": null,
|
| 804 |
+
"reward": 1.5,
|
| 805 |
+
"compliance": 1.0
|
| 806 |
+
},
|
| 807 |
+
{
|
| 808 |
+
"step": 3,
|
| 809 |
+
"email_kind": "admin",
|
| 810 |
+
"email_id": "admin_3_refund_cap_50",
|
| 811 |
+
"action": {
|
| 812 |
+
"action_type": "close",
|
| 813 |
+
"resolution_code": "policy_acknowledged"
|
| 814 |
+
},
|
| 815 |
+
"correct_hint": {
|
| 816 |
+
"action_type": "close",
|
| 817 |
+
"resolution_code": "policy_acknowledged"
|
| 818 |
+
},
|
| 819 |
+
"drift_sensitive_to": null,
|
| 820 |
+
"reward": 1.0,
|
| 821 |
+
"compliance": 1.0
|
| 822 |
+
},
|
| 823 |
+
{
|
| 824 |
+
"step": 4,
|
| 825 |
+
"email_kind": "customer",
|
| 826 |
+
"email_id": "c_4_chitchat_ooo",
|
| 827 |
+
"action": {
|
| 828 |
+
"action_type": "close",
|
| 829 |
+
"resolution_code": "no_action_needed"
|
| 830 |
+
},
|
| 831 |
+
"correct_hint": {
|
| 832 |
+
"action_type": "close",
|
| 833 |
+
"resolution_code": "no_action_needed"
|
| 834 |
+
},
|
| 835 |
+
"drift_sensitive_to": null,
|
| 836 |
+
"reward": 1.5,
|
| 837 |
+
"compliance": 1.0
|
| 838 |
+
},
|
| 839 |
+
{
|
| 840 |
+
"step": 5,
|
| 841 |
+
"email_kind": "customer",
|
| 842 |
+
"email_id": "c_5_critical_payment_fail",
|
| 843 |
+
"action": {
|
| 844 |
+
"action_type": "escalate",
|
| 845 |
+
"escalation_tier": "tier_1",
|
| 846 |
+
"followup_hours": 2
|
| 847 |
+
},
|
| 848 |
+
"correct_hint": {
|
| 849 |
+
"action_type": "escalate",
|
| 850 |
+
"escalation_tier": "tier_2",
|
| 851 |
+
"followup_hours": 24
|
| 852 |
+
},
|
| 853 |
+
"drift_sensitive_to": null,
|
| 854 |
+
"reward": 0.9,
|
| 855 |
+
"compliance": 0.4
|
| 856 |
+
},
|
| 857 |
+
{
|
| 858 |
+
"step": 6,
|
| 859 |
+
"email_kind": "customer",
|
| 860 |
+
"email_id": "c_6_info_missing_date",
|
| 861 |
+
"action": {
|
| 862 |
+
"action_type": "request_info",
|
| 863 |
+
"info_field": "time_period"
|
| 864 |
+
},
|
| 865 |
+
"correct_hint": {
|
| 866 |
+
"action_type": "request_info",
|
| 867 |
+
"info_field": "charge_date"
|
| 868 |
+
},
|
| 869 |
+
"drift_sensitive_to": null,
|
| 870 |
+
"reward": 1.0,
|
| 871 |
+
"compliance": 0.5
|
| 872 |
+
},
|
| 873 |
+
{
|
| 874 |
+
"step": 7,
|
| 875 |
+
"email_kind": "customer",
|
| 876 |
+
"email_id": "c_7_chitchat_thanks",
|
| 877 |
+
"action": {
|
| 878 |
+
"action_type": "close",
|
| 879 |
+
"resolution_code": "no_action_needed"
|
| 880 |
+
},
|
| 881 |
+
"correct_hint": {
|
| 882 |
+
"action_type": "close",
|
| 883 |
+
"resolution_code": "no_action_needed"
|
| 884 |
+
},
|
| 885 |
+
"drift_sensitive_to": null,
|
| 886 |
+
"reward": 1.5,
|
| 887 |
+
"compliance": 1.0
|
| 888 |
+
},
|
| 889 |
+
{
|
| 890 |
+
"step": 8,
|
| 891 |
+
"email_kind": "customer",
|
| 892 |
+
"email_id": "c_8_critical_payment_fail",
|
| 893 |
+
"action": {
|
| 894 |
+
"action_type": "escalate",
|
| 895 |
+
"escalation_tier": "tier_1",
|
| 896 |
+
"followup_hours": 2
|
| 897 |
+
},
|
| 898 |
+
"correct_hint": {
|
| 899 |
+
"action_type": "escalate",
|
| 900 |
+
"escalation_tier": "tier_2",
|
| 901 |
+
"followup_hours": 24
|
| 902 |
+
},
|
| 903 |
+
"drift_sensitive_to": null,
|
| 904 |
+
"reward": 0.9,
|
| 905 |
+
"compliance": 0.4
|
| 906 |
+
},
|
| 907 |
+
{
|
| 908 |
+
"step": 9,
|
| 909 |
+
"email_kind": "customer",
|
| 910 |
+
"email_id": "c_9_refund_30",
|
| 911 |
+
"action": {
|
| 912 |
+
"action_type": "approve_refund",
|
| 913 |
+
"refund_amount": 30.0
|
| 914 |
+
},
|
| 915 |
+
"correct_hint": {
|
| 916 |
+
"action_type": "approve_refund",
|
| 917 |
+
"refund_amount": 30.0
|
| 918 |
+
},
|
| 919 |
+
"drift_sensitive_to": null,
|
| 920 |
+
"reward": 1.5,
|
| 921 |
+
"compliance": 1.0
|
| 922 |
+
},
|
| 923 |
+
{
|
| 924 |
+
"step": 10,
|
| 925 |
+
"email_kind": "customer",
|
| 926 |
+
"email_id": "c_10_critical_security",
|
| 927 |
+
"action": {
|
| 928 |
+
"action_type": "escalate",
|
| 929 |
+
"escalation_tier": "tier_1",
|
| 930 |
+
"followup_hours": 2
|
| 931 |
+
},
|
| 932 |
+
"correct_hint": {
|
| 933 |
+
"action_type": "escalate",
|
| 934 |
+
"escalation_tier": "tier_2",
|
| 935 |
+
"followup_hours": 24
|
| 936 |
+
},
|
| 937 |
+
"drift_sensitive_to": null,
|
| 938 |
+
"reward": 0.9,
|
| 939 |
+
"compliance": 0.4
|
| 940 |
+
},
|
| 941 |
+
{
|
| 942 |
+
"step": 11,
|
| 943 |
+
"email_kind": "admin",
|
| 944 |
+
"email_id": "admin_11_escalate_keep_tier_2",
|
| 945 |
+
"action": {
|
| 946 |
+
"action_type": "close",
|
| 947 |
+
"resolution_code": "policy_acknowledged"
|
| 948 |
+
},
|
| 949 |
+
"correct_hint": {
|
| 950 |
+
"action_type": "close",
|
| 951 |
+
"resolution_code": "policy_acknowledged"
|
| 952 |
+
},
|
| 953 |
+
"drift_sensitive_to": null,
|
| 954 |
+
"reward": 1.0,
|
| 955 |
+
"compliance": 1.0
|
| 956 |
+
},
|
| 957 |
+
{
|
| 958 |
+
"step": 12,
|
| 959 |
+
"email_kind": "customer",
|
| 960 |
+
"email_id": "c_12_billing_tiers",
|
| 961 |
+
"action": {
|
| 962 |
+
"action_type": "reply"
|
| 963 |
+
},
|
| 964 |
+
"correct_hint": {
|
| 965 |
+
"action_type": "reply"
|
| 966 |
+
},
|
| 967 |
+
"drift_sensitive_to": null,
|
| 968 |
+
"reward": 1.5,
|
| 969 |
+
"compliance": 1.0
|
| 970 |
+
},
|
| 971 |
+
{
|
| 972 |
+
"step": 13,
|
| 973 |
+
"email_kind": "customer",
|
| 974 |
+
"email_id": "c_13_chitchat_ooo",
|
| 975 |
+
"action": {
|
| 976 |
+
"action_type": "close",
|
| 977 |
+
"resolution_code": "no_action_needed"
|
| 978 |
+
},
|
| 979 |
+
"correct_hint": {
|
| 980 |
+
"action_type": "close",
|
| 981 |
+
"resolution_code": "no_action_needed"
|
| 982 |
+
},
|
| 983 |
+
"drift_sensitive_to": null,
|
| 984 |
+
"reward": 1.5,
|
| 985 |
+
"compliance": 1.0
|
| 986 |
+
},
|
| 987 |
+
{
|
| 988 |
+
"step": 14,
|
| 989 |
+
"email_kind": "customer",
|
| 990 |
+
"email_id": "c_14_critical_data_loss",
|
| 991 |
+
"action": {
|
| 992 |
+
"action_type": "escalate",
|
| 993 |
+
"escalation_tier": "tier_2",
|
| 994 |
+
"followup_hours": 2
|
| 995 |
+
},
|
| 996 |
+
"correct_hint": {
|
| 997 |
+
"action_type": "escalate",
|
| 998 |
+
"escalation_tier": "tier_2",
|
| 999 |
+
"followup_hours": 24
|
| 1000 |
+
},
|
| 1001 |
+
"drift_sensitive_to": null,
|
| 1002 |
+
"reward": 1.2,
|
| 1003 |
+
"compliance": 0.7
|
| 1004 |
+
},
|
| 1005 |
+
{
|
| 1006 |
+
"step": 15,
|
| 1007 |
+
"email_kind": "customer",
|
| 1008 |
+
"email_id": "c_15_refund_40",
|
| 1009 |
+
"action": {
|
| 1010 |
+
"action_type": "approve_refund",
|
| 1011 |
+
"refund_amount": 40.0
|
| 1012 |
+
},
|
| 1013 |
+
"correct_hint": {
|
| 1014 |
+
"action_type": "approve_refund",
|
| 1015 |
+
"refund_amount": 40.0
|
| 1016 |
+
},
|
| 1017 |
+
"drift_sensitive_to": null,
|
| 1018 |
+
"reward": 1.5,
|
| 1019 |
+
"compliance": 1.0
|
| 1020 |
+
},
|
| 1021 |
+
{
|
| 1022 |
+
"step": 16,
|
| 1023 |
+
"email_kind": "customer",
|
| 1024 |
+
"email_id": "c_16_refund_40",
|
| 1025 |
+
"action": {
|
| 1026 |
+
"action_type": "approve_refund",
|
| 1027 |
+
"refund_amount": 40.0
|
| 1028 |
+
},
|
| 1029 |
+
"correct_hint": {
|
| 1030 |
+
"action_type": "approve_refund",
|
| 1031 |
+
"refund_amount": 40.0
|
| 1032 |
+
},
|
| 1033 |
+
"drift_sensitive_to": null,
|
| 1034 |
+
"reward": 1.5,
|
| 1035 |
+
"compliance": 1.0
|
| 1036 |
+
},
|
| 1037 |
+
{
|
| 1038 |
+
"step": 17,
|
| 1039 |
+
"email_kind": "customer",
|
| 1040 |
+
"email_id": "c_17_chitchat_thanks",
|
| 1041 |
+
"action": {
|
| 1042 |
+
"action_type": "close",
|
| 1043 |
+
"resolution_code": "no_action_needed"
|
| 1044 |
+
},
|
| 1045 |
+
"correct_hint": {
|
| 1046 |
+
"action_type": "close",
|
| 1047 |
+
"resolution_code": "no_action_needed"
|
| 1048 |
+
},
|
| 1049 |
+
"drift_sensitive_to": null,
|
| 1050 |
+
"reward": 1.5,
|
| 1051 |
+
"compliance": 1.0
|
| 1052 |
+
},
|
| 1053 |
+
{
|
| 1054 |
+
"step": 18,
|
| 1055 |
+
"email_kind": "customer",
|
| 1056 |
+
"email_id": "c_18_critical_payment_fail",
|
| 1057 |
+
"action": {
|
| 1058 |
+
"action_type": "escalate",
|
| 1059 |
+
"escalation_tier": "tier_2",
|
| 1060 |
+
"followup_hours": 2
|
| 1061 |
+
},
|
| 1062 |
+
"correct_hint": {
|
| 1063 |
+
"action_type": "escalate",
|
| 1064 |
+
"escalation_tier": "tier_2",
|
| 1065 |
+
"followup_hours": 24
|
| 1066 |
+
},
|
| 1067 |
+
"drift_sensitive_to": null,
|
| 1068 |
+
"reward": 1.2,
|
| 1069 |
+
"compliance": 0.7
|
| 1070 |
+
},
|
| 1071 |
+
{
|
| 1072 |
+
"step": 19,
|
| 1073 |
+
"email_kind": "customer",
|
| 1074 |
+
"email_id": "c_19_critical_data_loss",
|
| 1075 |
+
"action": {
|
| 1076 |
+
"action_type": "escalate",
|
| 1077 |
+
"escalation_tier": "tier_2",
|
| 1078 |
+
"followup_hours": 2
|
| 1079 |
+
},
|
| 1080 |
+
"correct_hint": {
|
| 1081 |
+
"action_type": "escalate",
|
| 1082 |
+
"escalation_tier": "tier_2",
|
| 1083 |
+
"followup_hours": 24
|
| 1084 |
+
},
|
| 1085 |
+
"drift_sensitive_to": null,
|
| 1086 |
+
"reward": 1.2,
|
| 1087 |
+
"compliance": 0.7
|
| 1088 |
+
}
|
| 1089 |
+
]
|
| 1090 |
+
},
|
| 1091 |
+
{
|
| 1092 |
+
"seed": 103,
|
| 1093 |
+
"total_reward": 26.2,
|
| 1094 |
+
"max_reward": 30.0,
|
| 1095 |
+
"frac_of_max": 0.8733,
|
| 1096 |
+
"breakdown_totals": {
|
| 1097 |
+
"compliance": 17.2,
|
| 1098 |
+
"appropriateness": 9.0,
|
| 1099 |
+
"drift_bonus": 0.0
|
| 1100 |
+
},
|
| 1101 |
+
"drift_sensitive_total": 2,
|
| 1102 |
+
"drift_sensitive_correct": 0,
|
| 1103 |
+
"drift_sensitive_acc": 0.0,
|
| 1104 |
+
"per_drift": {
|
| 1105 |
+
"sla_48hr": {
|
| 1106 |
+
"total": 2,
|
| 1107 |
+
"correct": 0
|
| 1108 |
+
}
|
| 1109 |
+
},
|
| 1110 |
+
"trajectory": [
|
| 1111 |
+
{
|
| 1112 |
+
"step": 0,
|
| 1113 |
+
"email_kind": "customer",
|
| 1114 |
+
"email_id": "c_0_billing_tiers",
|
| 1115 |
+
"action": {
|
| 1116 |
+
"action_type": "reply"
|
| 1117 |
+
},
|
| 1118 |
+
"correct_hint": {
|
| 1119 |
+
"action_type": "reply"
|
| 1120 |
+
},
|
| 1121 |
+
"drift_sensitive_to": null,
|
| 1122 |
+
"reward": 1.5,
|
| 1123 |
+
"compliance": 1.0
|
| 1124 |
+
},
|
| 1125 |
+
{
|
| 1126 |
+
"step": 1,
|
| 1127 |
+
"email_kind": "customer",
|
| 1128 |
+
"email_id": "c_1_refund_50",
|
| 1129 |
+
"action": {
|
| 1130 |
+
"action_type": "approve_refund",
|
| 1131 |
+
"refund_amount": 50.0
|
| 1132 |
+
},
|
| 1133 |
+
"correct_hint": {
|
| 1134 |
+
"action_type": "approve_refund",
|
| 1135 |
+
"refund_amount": 50.0
|
| 1136 |
+
},
|
| 1137 |
+
"drift_sensitive_to": null,
|
| 1138 |
+
"reward": 1.5,
|
| 1139 |
+
"compliance": 1.0
|
| 1140 |
+
},
|
| 1141 |
+
{
|
| 1142 |
+
"step": 2,
|
| 1143 |
+
"email_kind": "customer",
|
| 1144 |
+
"email_id": "c_2_critical_security",
|
| 1145 |
+
"action": {
|
| 1146 |
+
"action_type": "escalate",
|
| 1147 |
+
"escalation_tier": "tier_1",
|
| 1148 |
+
"followup_hours": 2
|
| 1149 |
+
},
|
| 1150 |
+
"correct_hint": {
|
| 1151 |
+
"action_type": "escalate",
|
| 1152 |
+
"escalation_tier": "tier_2",
|
| 1153 |
+
"followup_hours": 24
|
| 1154 |
+
},
|
| 1155 |
+
"drift_sensitive_to": null,
|
| 1156 |
+
"reward": 0.9,
|
| 1157 |
+
"compliance": 0.4
|
| 1158 |
+
},
|
| 1159 |
+
{
|
| 1160 |
+
"step": 3,
|
| 1161 |
+
"email_kind": "admin",
|
| 1162 |
+
"email_id": "admin_3_sla_48hr",
|
| 1163 |
+
"action": {
|
| 1164 |
+
"action_type": "close",
|
| 1165 |
+
"resolution_code": "error_fallback"
|
| 1166 |
+
},
|
| 1167 |
+
"correct_hint": {
|
| 1168 |
+
"action_type": "close",
|
| 1169 |
+
"resolution_code": "policy_acknowledged"
|
| 1170 |
+
},
|
| 1171 |
+
"drift_sensitive_to": null,
|
| 1172 |
+
"reward": 1.0,
|
| 1173 |
+
"compliance": 1.0
|
| 1174 |
+
},
|
| 1175 |
+
{
|
| 1176 |
+
"step": 4,
|
| 1177 |
+
"email_kind": "customer",
|
| 1178 |
+
"email_id": "c_4_info_missing_account",
|
| 1179 |
+
"action": {
|
| 1180 |
+
"action_type": "request_info",
|
| 1181 |
+
"info_field": "account_email"
|
| 1182 |
+
},
|
| 1183 |
+
"correct_hint": {
|
| 1184 |
+
"action_type": "request_info",
|
| 1185 |
+
"info_field": "account_email"
|
| 1186 |
+
},
|
| 1187 |
+
"drift_sensitive_to": null,
|
| 1188 |
+
"reward": 1.5,
|
| 1189 |
+
"compliance": 1.0
|
| 1190 |
+
},
|
| 1191 |
+
{
|
| 1192 |
+
"step": 5,
|
| 1193 |
+
"email_kind": "customer",
|
| 1194 |
+
"email_id": "c_5_refund_60",
|
| 1195 |
+
"action": {
|
| 1196 |
+
"action_type": "approve_refund",
|
| 1197 |
+
"refund_amount": 60.0
|
| 1198 |
+
},
|
| 1199 |
+
"correct_hint": {
|
| 1200 |
+
"action_type": "approve_refund",
|
| 1201 |
+
"refund_amount": 60.0
|
| 1202 |
+
},
|
| 1203 |
+
"drift_sensitive_to": null,
|
| 1204 |
+
"reward": 1.5,
|
| 1205 |
+
"compliance": 1.0
|
| 1206 |
+
},
|
| 1207 |
+
{
|
| 1208 |
+
"step": 6,
|
| 1209 |
+
"email_kind": "customer",
|
| 1210 |
+
"email_id": "c_6_refund_15",
|
| 1211 |
+
"action": {
|
| 1212 |
+
"action_type": "approve_refund",
|
| 1213 |
+
"refund_amount": 15.0
|
| 1214 |
+
},
|
| 1215 |
+
"correct_hint": {
|
| 1216 |
+
"action_type": "approve_refund",
|
| 1217 |
+
"refund_amount": 15.0
|
| 1218 |
+
},
|
| 1219 |
+
"drift_sensitive_to": null,
|
| 1220 |
+
"reward": 1.5,
|
| 1221 |
+
"compliance": 1.0
|
| 1222 |
+
},
|
| 1223 |
+
{
|
| 1224 |
+
"step": 7,
|
| 1225 |
+
"email_kind": "customer",
|
| 1226 |
+
"email_id": "c_7_refund_50",
|
| 1227 |
+
"action": {
|
| 1228 |
+
"action_type": "approve_refund",
|
| 1229 |
+
"refund_amount": 50.0
|
| 1230 |
+
},
|
| 1231 |
+
"correct_hint": {
|
| 1232 |
+
"action_type": "approve_refund",
|
| 1233 |
+
"refund_amount": 50.0
|
| 1234 |
+
},
|
| 1235 |
+
"drift_sensitive_to": null,
|
| 1236 |
+
"reward": 1.5,
|
| 1237 |
+
"compliance": 1.0
|
| 1238 |
+
},
|
| 1239 |
+
{
|
| 1240 |
+
"step": 8,
|
| 1241 |
+
"email_kind": "customer",
|
| 1242 |
+
"email_id": "c_8_critical_payment_fail",
|
| 1243 |
+
"action": {
|
| 1244 |
+
"action_type": "escalate",
|
| 1245 |
+
"escalation_tier": "tier_1",
|
| 1246 |
+
"followup_hours": 48
|
| 1247 |
+
},
|
| 1248 |
+
"correct_hint": {
|
| 1249 |
+
"action_type": "escalate",
|
| 1250 |
+
"escalation_tier": "tier_2",
|
| 1251 |
+
"followup_hours": 48
|
| 1252 |
+
},
|
| 1253 |
+
"drift_sensitive_to": "sla_48hr",
|
| 1254 |
+
"reward": 1.2,
|
| 1255 |
+
"compliance": 0.7
|
| 1256 |
+
},
|
| 1257 |
+
{
|
| 1258 |
+
"step": 9,
|
| 1259 |
+
"email_kind": "customer",
|
| 1260 |
+
"email_id": "c_9_critical_data_loss",
|
| 1261 |
+
"action": {
|
| 1262 |
+
"action_type": "escalate",
|
| 1263 |
+
"escalation_tier": "tier_1",
|
| 1264 |
+
"followup_hours": 48
|
| 1265 |
+
},
|
| 1266 |
+
"correct_hint": {
|
| 1267 |
+
"action_type": "escalate",
|
| 1268 |
+
"escalation_tier": "tier_2",
|
| 1269 |
+
"followup_hours": 48
|
| 1270 |
+
},
|
| 1271 |
+
"drift_sensitive_to": "sla_48hr",
|
| 1272 |
+
"reward": 1.2,
|
| 1273 |
+
"compliance": 0.7
|
| 1274 |
+
},
|
| 1275 |
+
{
|
| 1276 |
+
"step": 10,
|
| 1277 |
+
"email_kind": "customer",
|
| 1278 |
+
"email_id": "c_10_chitchat_thanks",
|
| 1279 |
+
"action": {
|
| 1280 |
+
"action_type": "close",
|
| 1281 |
+
"resolution_code": "no_action_needed"
|
| 1282 |
+
},
|
| 1283 |
+
"correct_hint": {
|
| 1284 |
+
"action_type": "close",
|
| 1285 |
+
"resolution_code": "no_action_needed"
|
| 1286 |
+
},
|
| 1287 |
+
"drift_sensitive_to": null,
|
| 1288 |
+
"reward": 1.5,
|
| 1289 |
+
"compliance": 1.0
|
| 1290 |
+
},
|
| 1291 |
+
{
|
| 1292 |
+
"step": 11,
|
| 1293 |
+
"email_kind": "admin",
|
| 1294 |
+
"email_id": "admin_11_escalate_keep_tier_2",
|
| 1295 |
+
"action": {
|
| 1296 |
+
"action_type": "close",
|
| 1297 |
+
"resolution_code": "policy_acknowledged"
|
| 1298 |
+
},
|
| 1299 |
+
"correct_hint": {
|
| 1300 |
+
"action_type": "close",
|
| 1301 |
+
"resolution_code": "policy_acknowledged"
|
| 1302 |
+
},
|
| 1303 |
+
"drift_sensitive_to": null,
|
| 1304 |
+
"reward": 1.0,
|
| 1305 |
+
"compliance": 1.0
|
| 1306 |
+
},
|
| 1307 |
+
{
|
| 1308 |
+
"step": 12,
|
| 1309 |
+
"email_kind": "customer",
|
| 1310 |
+
"email_id": "c_12_refund_100",
|
| 1311 |
+
"action": {
|
| 1312 |
+
"action_type": "approve_refund",
|
| 1313 |
+
"refund_amount": 100.0
|
| 1314 |
+
},
|
| 1315 |
+
"correct_hint": {
|
| 1316 |
+
"action_type": "approve_refund",
|
| 1317 |
+
"refund_amount": 100.0
|
| 1318 |
+
},
|
| 1319 |
+
"drift_sensitive_to": null,
|
| 1320 |
+
"reward": 1.5,
|
| 1321 |
+
"compliance": 1.0
|
| 1322 |
+
},
|
| 1323 |
+
{
|
| 1324 |
+
"step": 13,
|
| 1325 |
+
"email_kind": "customer",
|
| 1326 |
+
"email_id": "c_13_refund_90",
|
| 1327 |
+
"action": {
|
| 1328 |
+
"action_type": "approve_refund",
|
| 1329 |
+
"refund_amount": 90.0
|
| 1330 |
+
},
|
| 1331 |
+
"correct_hint": {
|
| 1332 |
+
"action_type": "approve_refund",
|
| 1333 |
+
"refund_amount": 90.0
|
| 1334 |
+
},
|
| 1335 |
+
"drift_sensitive_to": null,
|
| 1336 |
+
"reward": 1.5,
|
| 1337 |
+
"compliance": 1.0
|
| 1338 |
+
},
|
| 1339 |
+
{
|
| 1340 |
+
"step": 14,
|
| 1341 |
+
"email_kind": "customer",
|
| 1342 |
+
"email_id": "c_14_critical_payment_fail",
|
| 1343 |
+
"action": {
|
| 1344 |
+
"action_type": "escalate",
|
| 1345 |
+
"escalation_tier": "tier_2",
|
| 1346 |
+
"followup_hours": 2
|
| 1347 |
+
},
|
| 1348 |
+
"correct_hint": {
|
| 1349 |
+
"action_type": "escalate",
|
| 1350 |
+
"escalation_tier": "tier_2",
|
| 1351 |
+
"followup_hours": 48
|
| 1352 |
+
},
|
| 1353 |
+
"drift_sensitive_to": null,
|
| 1354 |
+
"reward": 1.2,
|
| 1355 |
+
"compliance": 0.7
|
| 1356 |
+
},
|
| 1357 |
+
{
|
| 1358 |
+
"step": 15,
|
| 1359 |
+
"email_kind": "customer",
|
| 1360 |
+
"email_id": "c_15_refund_120",
|
| 1361 |
+
"action": {
|
| 1362 |
+
"action_type": "approve_refund",
|
| 1363 |
+
"refund_amount": 120.0
|
| 1364 |
+
},
|
| 1365 |
+
"correct_hint": {
|
| 1366 |
+
"action_type": "escalate",
|
| 1367 |
+
"escalation_tier": "manager"
|
| 1368 |
+
},
|
| 1369 |
+
"drift_sensitive_to": null,
|
| 1370 |
+
"reward": 0.5,
|
| 1371 |
+
"compliance": 0.0
|
| 1372 |
+
},
|
| 1373 |
+
{
|
| 1374 |
+
"step": 16,
|
| 1375 |
+
"email_kind": "customer",
|
| 1376 |
+
"email_id": "c_16_billing_tiers",
|
| 1377 |
+
"action": {
|
| 1378 |
+
"action_type": "reply"
|
| 1379 |
+
},
|
| 1380 |
+
"correct_hint": {
|
| 1381 |
+
"action_type": "reply"
|
| 1382 |
+
},
|
| 1383 |
+
"drift_sensitive_to": null,
|
| 1384 |
+
"reward": 1.5,
|
| 1385 |
+
"compliance": 1.0
|
| 1386 |
+
},
|
| 1387 |
+
{
|
| 1388 |
+
"step": 17,
|
| 1389 |
+
"email_kind": "customer",
|
| 1390 |
+
"email_id": "c_17_chitchat_ooo",
|
| 1391 |
+
"action": {
|
| 1392 |
+
"action_type": "close",
|
| 1393 |
+
"resolution_code": "no_action_needed"
|
| 1394 |
+
},
|
| 1395 |
+
"correct_hint": {
|
| 1396 |
+
"action_type": "close",
|
| 1397 |
+
"resolution_code": "no_action_needed"
|
| 1398 |
+
},
|
| 1399 |
+
"drift_sensitive_to": null,
|
| 1400 |
+
"reward": 1.5,
|
| 1401 |
+
"compliance": 1.0
|
| 1402 |
+
},
|
| 1403 |
+
{
|
| 1404 |
+
"step": 18,
|
| 1405 |
+
"email_kind": "customer",
|
| 1406 |
+
"email_id": "c_18_chitchat_feedback",
|
| 1407 |
+
"action": {
|
| 1408 |
+
"action_type": "close",
|
| 1409 |
+
"resolution_code": "no_action_needed"
|
| 1410 |
+
},
|
| 1411 |
+
"correct_hint": {
|
| 1412 |
+
"action_type": "close",
|
| 1413 |
+
"resolution_code": "no_action_needed"
|
| 1414 |
+
},
|
| 1415 |
+
"drift_sensitive_to": null,
|
| 1416 |
+
"reward": 1.5,
|
| 1417 |
+
"compliance": 1.0
|
| 1418 |
+
},
|
| 1419 |
+
{
|
| 1420 |
+
"step": 19,
|
| 1421 |
+
"email_kind": "customer",
|
| 1422 |
+
"email_id": "c_19_critical_payment_fail",
|
| 1423 |
+
"action": {
|
| 1424 |
+
"action_type": "escalate",
|
| 1425 |
+
"escalation_tier": "tier_2",
|
| 1426 |
+
"followup_hours": 2
|
| 1427 |
+
},
|
| 1428 |
+
"correct_hint": {
|
| 1429 |
+
"action_type": "escalate",
|
| 1430 |
+
"escalation_tier": "tier_2",
|
| 1431 |
+
"followup_hours": 48
|
| 1432 |
+
},
|
| 1433 |
+
"drift_sensitive_to": null,
|
| 1434 |
+
"reward": 1.2,
|
| 1435 |
+
"compliance": 0.7
|
| 1436 |
+
}
|
| 1437 |
+
]
|
| 1438 |
+
},
|
| 1439 |
+
{
|
| 1440 |
+
"seed": 104,
|
| 1441 |
+
"total_reward": 27.0,
|
| 1442 |
+
"max_reward": 30.0,
|
| 1443 |
+
"frac_of_max": 0.9,
|
| 1444 |
+
"breakdown_totals": {
|
| 1445 |
+
"compliance": 17.5,
|
| 1446 |
+
"appropriateness": 9.0,
|
| 1447 |
+
"drift_bonus": 0.5
|
| 1448 |
+
},
|
| 1449 |
+
"drift_sensitive_total": 4,
|
| 1450 |
+
"drift_sensitive_correct": 2,
|
| 1451 |
+
"drift_sensitive_acc": 0.5,
|
| 1452 |
+
"per_drift": {
|
| 1453 |
+
"refund_cap_200": {
|
| 1454 |
+
"total": 2,
|
| 1455 |
+
"correct": 2
|
| 1456 |
+
},
|
| 1457 |
+
"sla_2hr": {
|
| 1458 |
+
"total": 2,
|
| 1459 |
+
"correct": 0
|
| 1460 |
+
}
|
| 1461 |
+
},
|
| 1462 |
+
"trajectory": [
|
| 1463 |
+
{
|
| 1464 |
+
"step": 0,
|
| 1465 |
+
"email_kind": "customer",
|
| 1466 |
+
"email_id": "c_0_refund_15",
|
| 1467 |
+
"action": {
|
| 1468 |
+
"action_type": "approve_refund",
|
| 1469 |
+
"refund_amount": 15.0
|
| 1470 |
+
},
|
| 1471 |
+
"correct_hint": {
|
| 1472 |
+
"action_type": "approve_refund",
|
| 1473 |
+
"refund_amount": 15.0
|
| 1474 |
+
},
|
| 1475 |
+
"drift_sensitive_to": null,
|
| 1476 |
+
"reward": 1.5,
|
| 1477 |
+
"compliance": 1.0
|
| 1478 |
+
},
|
| 1479 |
+
{
|
| 1480 |
+
"step": 1,
|
| 1481 |
+
"email_kind": "customer",
|
| 1482 |
+
"email_id": "c_1_info_missing_order_id",
|
| 1483 |
+
"action": {
|
| 1484 |
+
"action_type": "request_info",
|
| 1485 |
+
"info_field": "order_id"
|
| 1486 |
+
},
|
| 1487 |
+
"correct_hint": {
|
| 1488 |
+
"action_type": "request_info",
|
| 1489 |
+
"info_field": "order_id"
|
| 1490 |
+
},
|
| 1491 |
+
"drift_sensitive_to": null,
|
| 1492 |
+
"reward": 1.5,
|
| 1493 |
+
"compliance": 1.0
|
| 1494 |
+
},
|
| 1495 |
+
{
|
| 1496 |
+
"step": 2,
|
| 1497 |
+
"email_kind": "customer",
|
| 1498 |
+
"email_id": "c_2_info_missing_account",
|
| 1499 |
+
"action": {
|
| 1500 |
+
"action_type": "request_info",
|
| 1501 |
+
"info_field": "account_email"
|
| 1502 |
+
},
|
| 1503 |
+
"correct_hint": {
|
| 1504 |
+
"action_type": "request_info",
|
| 1505 |
+
"info_field": "account_email"
|
| 1506 |
+
},
|
| 1507 |
+
"drift_sensitive_to": null,
|
| 1508 |
+
"reward": 1.5,
|
| 1509 |
+
"compliance": 1.0
|
| 1510 |
+
},
|
| 1511 |
+
{
|
| 1512 |
+
"step": 3,
|
| 1513 |
+
"email_kind": "admin",
|
| 1514 |
+
"email_id": "admin_3_refund_cap_200",
|
| 1515 |
+
"action": {
|
| 1516 |
+
"action_type": "close",
|
| 1517 |
+
"resolution_code": "error_fallback"
|
| 1518 |
+
},
|
| 1519 |
+
"correct_hint": {
|
| 1520 |
+
"action_type": "close",
|
| 1521 |
+
"resolution_code": "policy_acknowledged"
|
| 1522 |
+
},
|
| 1523 |
+
"drift_sensitive_to": null,
|
| 1524 |
+
"reward": 1.0,
|
| 1525 |
+
"compliance": 1.0
|
| 1526 |
+
},
|
| 1527 |
+
{
|
| 1528 |
+
"step": 4,
|
| 1529 |
+
"email_kind": "customer",
|
| 1530 |
+
"email_id": "c_4_info_missing_account",
|
| 1531 |
+
"action": {
|
| 1532 |
+
"action_type": "request_info",
|
| 1533 |
+
"info_field": "account_email"
|
| 1534 |
+
},
|
| 1535 |
+
"correct_hint": {
|
| 1536 |
+
"action_type": "request_info",
|
| 1537 |
+
"info_field": "account_email"
|
| 1538 |
+
},
|
| 1539 |
+
"drift_sensitive_to": null,
|
| 1540 |
+
"reward": 1.5,
|
| 1541 |
+
"compliance": 1.0
|
| 1542 |
+
},
|
| 1543 |
+
{
|
| 1544 |
+
"step": 5,
|
| 1545 |
+
"email_kind": "customer",
|
| 1546 |
+
"email_id": "c_5_refund_180",
|
| 1547 |
+
"action": {
|
| 1548 |
+
"action_type": "approve_refund",
|
| 1549 |
+
"refund_amount": 180.0
|
| 1550 |
+
},
|
| 1551 |
+
"correct_hint": {
|
| 1552 |
+
"action_type": "approve_refund",
|
| 1553 |
+
"refund_amount": 180.0
|
| 1554 |
+
},
|
| 1555 |
+
"drift_sensitive_to": "refund_cap_200",
|
| 1556 |
+
"reward": 2.0,
|
| 1557 |
+
"compliance": 1.0
|
| 1558 |
+
},
|
| 1559 |
+
{
|
| 1560 |
+
"step": 6,
|
| 1561 |
+
"email_kind": "customer",
|
| 1562 |
+
"email_id": "c_6_refund_30",
|
| 1563 |
+
"action": {
|
| 1564 |
+
"action_type": "approve_refund",
|
| 1565 |
+
"refund_amount": 30.0
|
| 1566 |
+
},
|
| 1567 |
+
"correct_hint": {
|
| 1568 |
+
"action_type": "approve_refund",
|
| 1569 |
+
"refund_amount": 30.0
|
| 1570 |
+
},
|
| 1571 |
+
"drift_sensitive_to": null,
|
| 1572 |
+
"reward": 1.5,
|
| 1573 |
+
"compliance": 1.0
|
| 1574 |
+
},
|
| 1575 |
+
{
|
| 1576 |
+
"step": 7,
|
| 1577 |
+
"email_kind": "customer",
|
| 1578 |
+
"email_id": "c_7_billing_prorated",
|
| 1579 |
+
"action": {
|
| 1580 |
+
"action_type": "reply"
|
| 1581 |
+
},
|
| 1582 |
+
"correct_hint": {
|
| 1583 |
+
"action_type": "reply"
|
| 1584 |
+
},
|
| 1585 |
+
"drift_sensitive_to": null,
|
| 1586 |
+
"reward": 1.5,
|
| 1587 |
+
"compliance": 1.0
|
| 1588 |
+
},
|
| 1589 |
+
{
|
| 1590 |
+
"step": 8,
|
| 1591 |
+
"email_kind": "customer",
|
| 1592 |
+
"email_id": "c_8_refund_180",
|
| 1593 |
+
"action": {
|
| 1594 |
+
"action_type": "approve_refund",
|
| 1595 |
+
"refund_amount": 180.0
|
| 1596 |
+
},
|
| 1597 |
+
"correct_hint": {
|
| 1598 |
+
"action_type": "approve_refund",
|
| 1599 |
+
"refund_amount": 180.0
|
| 1600 |
+
},
|
| 1601 |
+
"drift_sensitive_to": "refund_cap_200",
|
| 1602 |
+
"reward": 1.5,
|
| 1603 |
+
"compliance": 1.0
|
| 1604 |
+
},
|
| 1605 |
+
{
|
| 1606 |
+
"step": 9,
|
| 1607 |
+
"email_kind": "customer",
|
| 1608 |
+
"email_id": "c_9_info_missing_date",
|
| 1609 |
+
"action": {
|
| 1610 |
+
"action_type": "request_info",
|
| 1611 |
+
"info_field": "order_id"
|
| 1612 |
+
},
|
| 1613 |
+
"correct_hint": {
|
| 1614 |
+
"action_type": "request_info",
|
| 1615 |
+
"info_field": "charge_date"
|
| 1616 |
+
},
|
| 1617 |
+
"drift_sensitive_to": null,
|
| 1618 |
+
"reward": 1.0,
|
| 1619 |
+
"compliance": 0.5
|
| 1620 |
+
},
|
| 1621 |
+
{
|
| 1622 |
+
"step": 10,
|
| 1623 |
+
"email_kind": "customer",
|
| 1624 |
+
"email_id": "c_10_critical_data_loss",
|
| 1625 |
+
"action": {
|
| 1626 |
+
"action_type": "escalate",
|
| 1627 |
+
"escalation_tier": "tier_1",
|
| 1628 |
+
"followup_hours": 2
|
| 1629 |
+
},
|
| 1630 |
+
"correct_hint": {
|
| 1631 |
+
"action_type": "escalate",
|
| 1632 |
+
"escalation_tier": "tier_2",
|
| 1633 |
+
"followup_hours": 24
|
| 1634 |
+
},
|
| 1635 |
+
"drift_sensitive_to": null,
|
| 1636 |
+
"reward": 0.9,
|
| 1637 |
+
"compliance": 0.4
|
| 1638 |
+
},
|
| 1639 |
+
{
|
| 1640 |
+
"step": 11,
|
| 1641 |
+
"email_kind": "admin",
|
| 1642 |
+
"email_id": "admin_11_sla_2hr",
|
| 1643 |
+
"action": {
|
| 1644 |
+
"action_type": "schedule_followup",
|
| 1645 |
+
"followup_hours": 2
|
| 1646 |
+
},
|
| 1647 |
+
"correct_hint": {
|
| 1648 |
+
"action_type": "close",
|
| 1649 |
+
"resolution_code": "policy_acknowledged"
|
| 1650 |
+
},
|
| 1651 |
+
"drift_sensitive_to": null,
|
| 1652 |
+
"reward": 0.2,
|
| 1653 |
+
"compliance": 0.2
|
| 1654 |
+
},
|
| 1655 |
+
{
|
| 1656 |
+
"step": 12,
|
| 1657 |
+
"email_kind": "customer",
|
| 1658 |
+
"email_id": "c_12_critical_security",
|
| 1659 |
+
"action": {
|
| 1660 |
+
"action_type": "escalate",
|
| 1661 |
+
"escalation_tier": "tier_1",
|
| 1662 |
+
"followup_hours": 2
|
| 1663 |
+
},
|
| 1664 |
+
"correct_hint": {
|
| 1665 |
+
"action_type": "escalate",
|
| 1666 |
+
"escalation_tier": "tier_2",
|
| 1667 |
+
"followup_hours": 2
|
| 1668 |
+
},
|
| 1669 |
+
"drift_sensitive_to": "sla_2hr",
|
| 1670 |
+
"reward": 1.2,
|
| 1671 |
+
"compliance": 0.7
|
| 1672 |
+
},
|
| 1673 |
+
{
|
| 1674 |
+
"step": 13,
|
| 1675 |
+
"email_kind": "customer",
|
| 1676 |
+
"email_id": "c_13_refund_120",
|
| 1677 |
+
"action": {
|
| 1678 |
+
"action_type": "approve_refund",
|
| 1679 |
+
"refund_amount": 120.0
|
| 1680 |
+
},
|
| 1681 |
+
"correct_hint": {
|
| 1682 |
+
"action_type": "approve_refund",
|
| 1683 |
+
"refund_amount": 120.0
|
| 1684 |
+
},
|
| 1685 |
+
"drift_sensitive_to": null,
|
| 1686 |
+
"reward": 1.5,
|
| 1687 |
+
"compliance": 1.0
|
| 1688 |
+
},
|
| 1689 |
+
{
|
| 1690 |
+
"step": 14,
|
| 1691 |
+
"email_kind": "customer",
|
| 1692 |
+
"email_id": "c_14_info_missing_order_id",
|
| 1693 |
+
"action": {
|
| 1694 |
+
"action_type": "request_info",
|
| 1695 |
+
"info_field": "order_id"
|
| 1696 |
+
},
|
| 1697 |
+
"correct_hint": {
|
| 1698 |
+
"action_type": "request_info",
|
| 1699 |
+
"info_field": "order_id"
|
| 1700 |
+
},
|
| 1701 |
+
"drift_sensitive_to": null,
|
| 1702 |
+
"reward": 1.5,
|
| 1703 |
+
"compliance": 1.0
|
| 1704 |
+
},
|
| 1705 |
+
{
|
| 1706 |
+
"step": 15,
|
| 1707 |
+
"email_kind": "customer",
|
| 1708 |
+
"email_id": "c_15_refund_50",
|
| 1709 |
+
"action": {
|
| 1710 |
+
"action_type": "approve_refund",
|
| 1711 |
+
"refund_amount": 50.0
|
| 1712 |
+
},
|
| 1713 |
+
"correct_hint": {
|
| 1714 |
+
"action_type": "approve_refund",
|
| 1715 |
+
"refund_amount": 50.0
|
| 1716 |
+
},
|
| 1717 |
+
"drift_sensitive_to": null,
|
| 1718 |
+
"reward": 1.5,
|
| 1719 |
+
"compliance": 1.0
|
| 1720 |
+
},
|
| 1721 |
+
{
|
| 1722 |
+
"step": 16,
|
| 1723 |
+
"email_kind": "customer",
|
| 1724 |
+
"email_id": "c_16_billing_prorated",
|
| 1725 |
+
"action": {
|
| 1726 |
+
"action_type": "reply"
|
| 1727 |
+
},
|
| 1728 |
+
"correct_hint": {
|
| 1729 |
+
"action_type": "reply"
|
| 1730 |
+
},
|
| 1731 |
+
"drift_sensitive_to": null,
|
| 1732 |
+
"reward": 1.5,
|
| 1733 |
+
"compliance": 1.0
|
| 1734 |
+
},
|
| 1735 |
+
{
|
| 1736 |
+
"step": 17,
|
| 1737 |
+
"email_kind": "customer",
|
| 1738 |
+
"email_id": "c_17_refund_180",
|
| 1739 |
+
"action": {
|
| 1740 |
+
"action_type": "approve_refund",
|
| 1741 |
+
"refund_amount": 180.0
|
| 1742 |
+
},
|
| 1743 |
+
"correct_hint": {
|
| 1744 |
+
"action_type": "approve_refund",
|
| 1745 |
+
"refund_amount": 180.0
|
| 1746 |
+
},
|
| 1747 |
+
"drift_sensitive_to": null,
|
| 1748 |
+
"reward": 1.5,
|
| 1749 |
+
"compliance": 1.0
|
| 1750 |
+
},
|
| 1751 |
+
{
|
| 1752 |
+
"step": 18,
|
| 1753 |
+
"email_kind": "customer",
|
| 1754 |
+
"email_id": "c_18_billing_invoice_download",
|
| 1755 |
+
"action": {
|
| 1756 |
+
"action_type": "reply"
|
| 1757 |
+
},
|
| 1758 |
+
"correct_hint": {
|
| 1759 |
+
"action_type": "reply"
|
| 1760 |
+
},
|
| 1761 |
+
"drift_sensitive_to": null,
|
| 1762 |
+
"reward": 1.5,
|
| 1763 |
+
"compliance": 1.0
|
| 1764 |
+
},
|
| 1765 |
+
{
|
| 1766 |
+
"step": 19,
|
| 1767 |
+
"email_kind": "customer",
|
| 1768 |
+
"email_id": "c_19_critical_auth_locked",
|
| 1769 |
+
"action": {
|
| 1770 |
+
"action_type": "escalate",
|
| 1771 |
+
"escalation_tier": "tier_1",
|
| 1772 |
+
"followup_hours": 2
|
| 1773 |
+
},
|
| 1774 |
+
"correct_hint": {
|
| 1775 |
+
"action_type": "escalate",
|
| 1776 |
+
"escalation_tier": "tier_2",
|
| 1777 |
+
"followup_hours": 2
|
| 1778 |
+
},
|
| 1779 |
+
"drift_sensitive_to": "sla_2hr",
|
| 1780 |
+
"reward": 1.2,
|
| 1781 |
+
"compliance": 0.7
|
| 1782 |
+
}
|
| 1783 |
+
]
|
| 1784 |
+
},
|
| 1785 |
+
{
|
| 1786 |
+
"seed": 105,
|
| 1787 |
+
"total_reward": 22.6,
|
| 1788 |
+
"max_reward": 30.0,
|
| 1789 |
+
"frac_of_max": 0.7533,
|
| 1790 |
+
"breakdown_totals": {
|
| 1791 |
+
"compliance": 14.1,
|
| 1792 |
+
"appropriateness": 8.5,
|
| 1793 |
+
"drift_bonus": 0.0
|
| 1794 |
+
},
|
| 1795 |
+
"drift_sensitive_total": 3,
|
| 1796 |
+
"drift_sensitive_correct": 0,
|
| 1797 |
+
"drift_sensitive_acc": 0.0,
|
| 1798 |
+
"per_drift": {
|
| 1799 |
+
"sla_48hr": {
|
| 1800 |
+
"total": 1,
|
| 1801 |
+
"correct": 0
|
| 1802 |
+
},
|
| 1803 |
+
"escalate_manager": {
|
| 1804 |
+
"total": 2,
|
| 1805 |
+
"correct": 0
|
| 1806 |
+
}
|
| 1807 |
+
},
|
| 1808 |
+
"trajectory": [
|
| 1809 |
+
{
|
| 1810 |
+
"step": 0,
|
| 1811 |
+
"email_kind": "customer",
|
| 1812 |
+
"email_id": "c_0_billing_prorated",
|
| 1813 |
+
"action": {
|
| 1814 |
+
"action_type": "reply"
|
| 1815 |
+
},
|
| 1816 |
+
"correct_hint": {
|
| 1817 |
+
"action_type": "reply"
|
| 1818 |
+
},
|
| 1819 |
+
"drift_sensitive_to": null,
|
| 1820 |
+
"reward": 1.5,
|
| 1821 |
+
"compliance": 1.0
|
| 1822 |
+
},
|
| 1823 |
+
{
|
| 1824 |
+
"step": 1,
|
| 1825 |
+
"email_kind": "customer",
|
| 1826 |
+
"email_id": "c_1_refund_180",
|
| 1827 |
+
"action": {
|
| 1828 |
+
"action_type": "approve_refund",
|
| 1829 |
+
"refund_amount": 180.0
|
| 1830 |
+
},
|
| 1831 |
+
"correct_hint": {
|
| 1832 |
+
"action_type": "escalate",
|
| 1833 |
+
"escalation_tier": "manager"
|
| 1834 |
+
},
|
| 1835 |
+
"drift_sensitive_to": null,
|
| 1836 |
+
"reward": 0.5,
|
| 1837 |
+
"compliance": 0.0
|
| 1838 |
+
},
|
| 1839 |
+
{
|
| 1840 |
+
"step": 2,
|
| 1841 |
+
"email_kind": "customer",
|
| 1842 |
+
"email_id": "c_2_refund_250",
|
| 1843 |
+
"action": {
|
| 1844 |
+
"action_type": "approve_refund",
|
| 1845 |
+
"refund_amount": 250.0
|
| 1846 |
+
},
|
| 1847 |
+
"correct_hint": {
|
| 1848 |
+
"action_type": "escalate",
|
| 1849 |
+
"escalation_tier": "manager"
|
| 1850 |
+
},
|
| 1851 |
+
"drift_sensitive_to": null,
|
| 1852 |
+
"reward": 0.5,
|
| 1853 |
+
"compliance": 0.0
|
| 1854 |
+
},
|
| 1855 |
+
{
|
| 1856 |
+
"step": 3,
|
| 1857 |
+
"email_kind": "admin",
|
| 1858 |
+
"email_id": "admin_3_sla_48hr",
|
| 1859 |
+
"action": {
|
| 1860 |
+
"action_type": "close",
|
| 1861 |
+
"resolution_code": "error_fallback"
|
| 1862 |
+
},
|
| 1863 |
+
"correct_hint": {
|
| 1864 |
+
"action_type": "close",
|
| 1865 |
+
"resolution_code": "policy_acknowledged"
|
| 1866 |
+
},
|
| 1867 |
+
"drift_sensitive_to": null,
|
| 1868 |
+
"reward": 1.0,
|
| 1869 |
+
"compliance": 1.0
|
| 1870 |
+
},
|
| 1871 |
+
{
|
| 1872 |
+
"step": 4,
|
| 1873 |
+
"email_kind": "customer",
|
| 1874 |
+
"email_id": "c_4_refund_60",
|
| 1875 |
+
"action": {
|
| 1876 |
+
"action_type": "approve_refund",
|
| 1877 |
+
"refund_amount": 60.0
|
| 1878 |
+
},
|
| 1879 |
+
"correct_hint": {
|
| 1880 |
+
"action_type": "approve_refund",
|
| 1881 |
+
"refund_amount": 60.0
|
| 1882 |
+
},
|
| 1883 |
+
"drift_sensitive_to": null,
|
| 1884 |
+
"reward": 1.5,
|
| 1885 |
+
"compliance": 1.0
|
| 1886 |
+
},
|
| 1887 |
+
{
|
| 1888 |
+
"step": 5,
|
| 1889 |
+
"email_kind": "customer",
|
| 1890 |
+
"email_id": "c_5_critical_outage",
|
| 1891 |
+
"action": {
|
| 1892 |
+
"action_type": "escalate",
|
| 1893 |
+
"escalation_tier": "tier_1",
|
| 1894 |
+
"followup_hours": 48
|
| 1895 |
+
},
|
| 1896 |
+
"correct_hint": {
|
| 1897 |
+
"action_type": "escalate",
|
| 1898 |
+
"escalation_tier": "tier_2",
|
| 1899 |
+
"followup_hours": 48
|
| 1900 |
+
},
|
| 1901 |
+
"drift_sensitive_to": "sla_48hr",
|
| 1902 |
+
"reward": 1.2,
|
| 1903 |
+
"compliance": 0.7
|
| 1904 |
+
},
|
| 1905 |
+
{
|
| 1906 |
+
"step": 6,
|
| 1907 |
+
"email_kind": "customer",
|
| 1908 |
+
"email_id": "c_6_refund_75",
|
| 1909 |
+
"action": {
|
| 1910 |
+
"action_type": "escalate",
|
| 1911 |
+
"escalation_tier": "tier_1",
|
| 1912 |
+
"followup_hours": 48
|
| 1913 |
+
},
|
| 1914 |
+
"correct_hint": {
|
| 1915 |
+
"action_type": "approve_refund",
|
| 1916 |
+
"refund_amount": 75.0
|
| 1917 |
+
},
|
| 1918 |
+
"drift_sensitive_to": null,
|
| 1919 |
+
"reward": 0.5,
|
| 1920 |
+
"compliance": 0.0
|
| 1921 |
+
},
|
| 1922 |
+
{
|
| 1923 |
+
"step": 7,
|
| 1924 |
+
"email_kind": "customer",
|
| 1925 |
+
"email_id": "c_7_chitchat_thanks",
|
| 1926 |
+
"action": {
|
| 1927 |
+
"action_type": "close",
|
| 1928 |
+
"resolution_code": "no_action_needed"
|
| 1929 |
+
},
|
| 1930 |
+
"correct_hint": {
|
| 1931 |
+
"action_type": "close",
|
| 1932 |
+
"resolution_code": "no_action_needed"
|
| 1933 |
+
},
|
| 1934 |
+
"drift_sensitive_to": null,
|
| 1935 |
+
"reward": 1.5,
|
| 1936 |
+
"compliance": 1.0
|
| 1937 |
+
},
|
| 1938 |
+
{
|
| 1939 |
+
"step": 8,
|
| 1940 |
+
"email_kind": "customer",
|
| 1941 |
+
"email_id": "c_8_chitchat_feedback",
|
| 1942 |
+
"action": {
|
| 1943 |
+
"action_type": "close",
|
| 1944 |
+
"resolution_code": "no_action_needed"
|
| 1945 |
+
},
|
| 1946 |
+
"correct_hint": {
|
| 1947 |
+
"action_type": "close",
|
| 1948 |
+
"resolution_code": "no_action_needed"
|
| 1949 |
+
},
|
| 1950 |
+
"drift_sensitive_to": null,
|
| 1951 |
+
"reward": 1.5,
|
| 1952 |
+
"compliance": 1.0
|
| 1953 |
+
},
|
| 1954 |
+
{
|
| 1955 |
+
"step": 9,
|
| 1956 |
+
"email_kind": "customer",
|
| 1957 |
+
"email_id": "c_9_billing_tiers",
|
| 1958 |
+
"action": {
|
| 1959 |
+
"action_type": "reply"
|
| 1960 |
+
},
|
| 1961 |
+
"correct_hint": {
|
| 1962 |
+
"action_type": "reply"
|
| 1963 |
+
},
|
| 1964 |
+
"drift_sensitive_to": null,
|
| 1965 |
+
"reward": 1.5,
|
| 1966 |
+
"compliance": 1.0
|
| 1967 |
+
},
|
| 1968 |
+
{
|
| 1969 |
+
"step": 10,
|
| 1970 |
+
"email_kind": "customer",
|
| 1971 |
+
"email_id": "c_10_info_missing_date",
|
| 1972 |
+
"action": {
|
| 1973 |
+
"action_type": "request_info",
|
| 1974 |
+
"info_field": "order_id"
|
| 1975 |
+
},
|
| 1976 |
+
"correct_hint": {
|
| 1977 |
+
"action_type": "request_info",
|
| 1978 |
+
"info_field": "charge_date"
|
| 1979 |
+
},
|
| 1980 |
+
"drift_sensitive_to": null,
|
| 1981 |
+
"reward": 1.0,
|
| 1982 |
+
"compliance": 0.5
|
| 1983 |
+
},
|
| 1984 |
+
{
|
| 1985 |
+
"step": 11,
|
| 1986 |
+
"email_kind": "admin",
|
| 1987 |
+
"email_id": "admin_11_escalate_manager",
|
| 1988 |
+
"action": {
|
| 1989 |
+
"action_type": "close",
|
| 1990 |
+
"resolution_code": "policy_acknowledged"
|
| 1991 |
+
},
|
| 1992 |
+
"correct_hint": {
|
| 1993 |
+
"action_type": "close",
|
| 1994 |
+
"resolution_code": "policy_acknowledged"
|
| 1995 |
+
},
|
| 1996 |
+
"drift_sensitive_to": null,
|
| 1997 |
+
"reward": 1.0,
|
| 1998 |
+
"compliance": 1.0
|
| 1999 |
+
},
|
| 2000 |
+
{
|
| 2001 |
+
"step": 12,
|
| 2002 |
+
"email_kind": "customer",
|
| 2003 |
+
"email_id": "c_12_critical_payment_fail",
|
| 2004 |
+
"action": {
|
| 2005 |
+
"action_type": "escalate",
|
| 2006 |
+
"escalation_tier": "manager",
|
| 2007 |
+
"followup_hours": 2
|
| 2008 |
+
},
|
| 2009 |
+
"correct_hint": {
|
| 2010 |
+
"action_type": "escalate",
|
| 2011 |
+
"escalation_tier": "manager",
|
| 2012 |
+
"followup_hours": 48
|
| 2013 |
+
},
|
| 2014 |
+
"drift_sensitive_to": "escalate_manager",
|
| 2015 |
+
"reward": 1.2,
|
| 2016 |
+
"compliance": 0.7
|
| 2017 |
+
},
|
| 2018 |
+
{
|
| 2019 |
+
"step": 13,
|
| 2020 |
+
"email_kind": "customer",
|
| 2021 |
+
"email_id": "c_13_info_missing_order_id",
|
| 2022 |
+
"action": {
|
| 2023 |
+
"action_type": "request_info",
|
| 2024 |
+
"info_field": "invoice_details"
|
| 2025 |
+
},
|
| 2026 |
+
"correct_hint": {
|
| 2027 |
+
"action_type": "request_info",
|
| 2028 |
+
"info_field": "order_id"
|
| 2029 |
+
},
|
| 2030 |
+
"drift_sensitive_to": null,
|
| 2031 |
+
"reward": 1.0,
|
| 2032 |
+
"compliance": 0.5
|
| 2033 |
+
},
|
| 2034 |
+
{
|
| 2035 |
+
"step": 14,
|
| 2036 |
+
"email_kind": "customer",
|
| 2037 |
+
"email_id": "c_14_info_missing_account",
|
| 2038 |
+
"action": {
|
| 2039 |
+
"action_type": "reply"
|
| 2040 |
+
},
|
| 2041 |
+
"correct_hint": {
|
| 2042 |
+
"action_type": "request_info",
|
| 2043 |
+
"info_field": "account_email"
|
| 2044 |
+
},
|
| 2045 |
+
"drift_sensitive_to": null,
|
| 2046 |
+
"reward": 0.0,
|
| 2047 |
+
"compliance": 0.0
|
| 2048 |
+
},
|
| 2049 |
+
{
|
| 2050 |
+
"step": 15,
|
| 2051 |
+
"email_kind": "customer",
|
| 2052 |
+
"email_id": "c_15_refund_75",
|
| 2053 |
+
"action": {
|
| 2054 |
+
"action_type": "approve_refund",
|
| 2055 |
+
"refund_amount": 75.0
|
| 2056 |
+
},
|
| 2057 |
+
"correct_hint": {
|
| 2058 |
+
"action_type": "approve_refund",
|
| 2059 |
+
"refund_amount": 75.0
|
| 2060 |
+
},
|
| 2061 |
+
"drift_sensitive_to": null,
|
| 2062 |
+
"reward": 1.5,
|
| 2063 |
+
"compliance": 1.0
|
| 2064 |
+
},
|
| 2065 |
+
{
|
| 2066 |
+
"step": 16,
|
| 2067 |
+
"email_kind": "customer",
|
| 2068 |
+
"email_id": "c_16_chitchat_thanks",
|
| 2069 |
+
"action": {
|
| 2070 |
+
"action_type": "close",
|
| 2071 |
+
"resolution_code": "no_action_needed"
|
| 2072 |
+
},
|
| 2073 |
+
"correct_hint": {
|
| 2074 |
+
"action_type": "close",
|
| 2075 |
+
"resolution_code": "no_action_needed"
|
| 2076 |
+
},
|
| 2077 |
+
"drift_sensitive_to": null,
|
| 2078 |
+
"reward": 1.5,
|
| 2079 |
+
"compliance": 1.0
|
| 2080 |
+
},
|
| 2081 |
+
{
|
| 2082 |
+
"step": 17,
|
| 2083 |
+
"email_kind": "customer",
|
| 2084 |
+
"email_id": "c_17_chitchat_ooo",
|
| 2085 |
+
"action": {
|
| 2086 |
+
"action_type": "close",
|
| 2087 |
+
"resolution_code": "no_action_needed"
|
| 2088 |
+
},
|
| 2089 |
+
"correct_hint": {
|
| 2090 |
+
"action_type": "close",
|
| 2091 |
+
"resolution_code": "no_action_needed"
|
| 2092 |
+
},
|
| 2093 |
+
"drift_sensitive_to": null,
|
| 2094 |
+
"reward": 1.5,
|
| 2095 |
+
"compliance": 1.0
|
| 2096 |
+
},
|
| 2097 |
+
{
|
| 2098 |
+
"step": 18,
|
| 2099 |
+
"email_kind": "customer",
|
| 2100 |
+
"email_id": "c_18_critical_api_down",
|
| 2101 |
+
"action": {
|
| 2102 |
+
"action_type": "escalate",
|
| 2103 |
+
"escalation_tier": "manager",
|
| 2104 |
+
"followup_hours": 2
|
| 2105 |
+
},
|
| 2106 |
+
"correct_hint": {
|
| 2107 |
+
"action_type": "escalate",
|
| 2108 |
+
"escalation_tier": "manager",
|
| 2109 |
+
"followup_hours": 48
|
| 2110 |
+
},
|
| 2111 |
+
"drift_sensitive_to": "escalate_manager",
|
| 2112 |
+
"reward": 1.2,
|
| 2113 |
+
"compliance": 0.7
|
| 2114 |
+
},
|
| 2115 |
+
{
|
| 2116 |
+
"step": 19,
|
| 2117 |
+
"email_kind": "customer",
|
| 2118 |
+
"email_id": "c_19_chitchat_ooo",
|
| 2119 |
+
"action": {
|
| 2120 |
+
"action_type": "close",
|
| 2121 |
+
"resolution_code": "no_action_needed"
|
| 2122 |
+
},
|
| 2123 |
+
"correct_hint": {
|
| 2124 |
+
"action_type": "close",
|
| 2125 |
+
"resolution_code": "no_action_needed"
|
| 2126 |
+
},
|
| 2127 |
+
"drift_sensitive_to": null,
|
| 2128 |
+
"reward": 1.5,
|
| 2129 |
+
"compliance": 1.0
|
| 2130 |
+
}
|
| 2131 |
+
]
|
| 2132 |
+
},
|
| 2133 |
+
{
|
| 2134 |
+
"seed": 106,
|
| 2135 |
+
"total_reward": 23.8,
|
| 2136 |
+
"max_reward": 30.0,
|
| 2137 |
+
"frac_of_max": 0.7933,
|
| 2138 |
+
"breakdown_totals": {
|
| 2139 |
+
"compliance": 14.8,
|
| 2140 |
+
"appropriateness": 9.0,
|
| 2141 |
+
"drift_bonus": 0.0
|
| 2142 |
+
},
|
| 2143 |
+
"drift_sensitive_total": 4,
|
| 2144 |
+
"drift_sensitive_correct": 0,
|
| 2145 |
+
"drift_sensitive_acc": 0.0,
|
| 2146 |
+
"per_drift": {
|
| 2147 |
+
"sla_2hr": {
|
| 2148 |
+
"total": 4,
|
| 2149 |
+
"correct": 0
|
| 2150 |
+
}
|
| 2151 |
+
},
|
| 2152 |
+
"trajectory": [
|
| 2153 |
+
{
|
| 2154 |
+
"step": 0,
|
| 2155 |
+
"email_kind": "customer",
|
| 2156 |
+
"email_id": "c_0_chitchat_feedback",
|
| 2157 |
+
"action": {
|
| 2158 |
+
"action_type": "close",
|
| 2159 |
+
"resolution_code": "no_action_needed"
|
| 2160 |
+
},
|
| 2161 |
+
"correct_hint": {
|
| 2162 |
+
"action_type": "close",
|
| 2163 |
+
"resolution_code": "no_action_needed"
|
| 2164 |
+
},
|
| 2165 |
+
"drift_sensitive_to": null,
|
| 2166 |
+
"reward": 1.5,
|
| 2167 |
+
"compliance": 1.0
|
| 2168 |
+
},
|
| 2169 |
+
{
|
| 2170 |
+
"step": 1,
|
| 2171 |
+
"email_kind": "customer",
|
| 2172 |
+
"email_id": "c_1_refund_120",
|
| 2173 |
+
"action": {
|
| 2174 |
+
"action_type": "request_info",
|
| 2175 |
+
"info_field": "partner_account_id"
|
| 2176 |
+
},
|
| 2177 |
+
"correct_hint": {
|
| 2178 |
+
"action_type": "escalate",
|
| 2179 |
+
"escalation_tier": "manager"
|
| 2180 |
+
},
|
| 2181 |
+
"drift_sensitive_to": null,
|
| 2182 |
+
"reward": 0.5,
|
| 2183 |
+
"compliance": 0.0
|
| 2184 |
+
},
|
| 2185 |
+
{
|
| 2186 |
+
"step": 2,
|
| 2187 |
+
"email_kind": "customer",
|
| 2188 |
+
"email_id": "c_2_billing_tiers",
|
| 2189 |
+
"action": {
|
| 2190 |
+
"action_type": "reply"
|
| 2191 |
+
},
|
| 2192 |
+
"correct_hint": {
|
| 2193 |
+
"action_type": "reply"
|
| 2194 |
+
},
|
| 2195 |
+
"drift_sensitive_to": null,
|
| 2196 |
+
"reward": 1.5,
|
| 2197 |
+
"compliance": 1.0
|
| 2198 |
+
},
|
| 2199 |
+
{
|
| 2200 |
+
"step": 3,
|
| 2201 |
+
"email_kind": "admin",
|
| 2202 |
+
"email_id": "admin_3_sla_2hr",
|
| 2203 |
+
"action": {
|
| 2204 |
+
"action_type": "close",
|
| 2205 |
+
"resolution_code": "error_fallback"
|
| 2206 |
+
},
|
| 2207 |
+
"correct_hint": {
|
| 2208 |
+
"action_type": "close",
|
| 2209 |
+
"resolution_code": "policy_acknowledged"
|
| 2210 |
+
},
|
| 2211 |
+
"drift_sensitive_to": null,
|
| 2212 |
+
"reward": 1.0,
|
| 2213 |
+
"compliance": 1.0
|
| 2214 |
+
},
|
| 2215 |
+
{
|
| 2216 |
+
"step": 4,
|
| 2217 |
+
"email_kind": "customer",
|
| 2218 |
+
"email_id": "c_4_chitchat_feedback",
|
| 2219 |
+
"action": {
|
| 2220 |
+
"action_type": "close",
|
| 2221 |
+
"resolution_code": "no_action_needed"
|
| 2222 |
+
},
|
| 2223 |
+
"correct_hint": {
|
| 2224 |
+
"action_type": "close",
|
| 2225 |
+
"resolution_code": "no_action_needed"
|
| 2226 |
+
},
|
| 2227 |
+
"drift_sensitive_to": null,
|
| 2228 |
+
"reward": 1.5,
|
| 2229 |
+
"compliance": 1.0
|
| 2230 |
+
},
|
| 2231 |
+
{
|
| 2232 |
+
"step": 5,
|
| 2233 |
+
"email_kind": "customer",
|
| 2234 |
+
"email_id": "c_5_critical_security",
|
| 2235 |
+
"action": {
|
| 2236 |
+
"action_type": "escalate",
|
| 2237 |
+
"escalation_tier": "tier_1",
|
| 2238 |
+
"followup_hours": 2
|
| 2239 |
+
},
|
| 2240 |
+
"correct_hint": {
|
| 2241 |
+
"action_type": "escalate",
|
| 2242 |
+
"escalation_tier": "tier_2",
|
| 2243 |
+
"followup_hours": 2
|
| 2244 |
+
},
|
| 2245 |
+
"drift_sensitive_to": "sla_2hr",
|
| 2246 |
+
"reward": 1.2,
|
| 2247 |
+
"compliance": 0.7
|
| 2248 |
+
},
|
| 2249 |
+
{
|
| 2250 |
+
"step": 6,
|
| 2251 |
+
"email_kind": "customer",
|
| 2252 |
+
"email_id": "c_6_billing_prorated",
|
| 2253 |
+
"action": {
|
| 2254 |
+
"action_type": "reply"
|
| 2255 |
+
},
|
| 2256 |
+
"correct_hint": {
|
| 2257 |
+
"action_type": "reply"
|
| 2258 |
+
},
|
| 2259 |
+
"drift_sensitive_to": null,
|
| 2260 |
+
"reward": 1.5,
|
| 2261 |
+
"compliance": 1.0
|
| 2262 |
+
},
|
| 2263 |
+
{
|
| 2264 |
+
"step": 7,
|
| 2265 |
+
"email_kind": "customer",
|
| 2266 |
+
"email_id": "c_7_critical_security",
|
| 2267 |
+
"action": {
|
| 2268 |
+
"action_type": "escalate",
|
| 2269 |
+
"escalation_tier": "tier_1",
|
| 2270 |
+
"followup_hours": 2
|
| 2271 |
+
},
|
| 2272 |
+
"correct_hint": {
|
| 2273 |
+
"action_type": "escalate",
|
| 2274 |
+
"escalation_tier": "tier_2",
|
| 2275 |
+
"followup_hours": 2
|
| 2276 |
+
},
|
| 2277 |
+
"drift_sensitive_to": "sla_2hr",
|
| 2278 |
+
"reward": 1.2,
|
| 2279 |
+
"compliance": 0.7
|
| 2280 |
+
},
|
| 2281 |
+
{
|
| 2282 |
+
"step": 8,
|
| 2283 |
+
"email_kind": "customer",
|
| 2284 |
+
"email_id": "c_8_critical_api_down",
|
| 2285 |
+
"action": {
|
| 2286 |
+
"action_type": "escalate",
|
| 2287 |
+
"escalation_tier": "tier_1",
|
| 2288 |
+
"followup_hours": 2
|
| 2289 |
+
},
|
| 2290 |
+
"correct_hint": {
|
| 2291 |
+
"action_type": "escalate",
|
| 2292 |
+
"escalation_tier": "tier_2",
|
| 2293 |
+
"followup_hours": 2
|
| 2294 |
+
},
|
| 2295 |
+
"drift_sensitive_to": "sla_2hr",
|
| 2296 |
+
"reward": 1.2,
|
| 2297 |
+
"compliance": 0.7
|
| 2298 |
+
},
|
| 2299 |
+
{
|
| 2300 |
+
"step": 9,
|
| 2301 |
+
"email_kind": "customer",
|
| 2302 |
+
"email_id": "c_9_refund_75",
|
| 2303 |
+
"action": {
|
| 2304 |
+
"action_type": "approve_refund",
|
| 2305 |
+
"refund_amount": 75.0
|
| 2306 |
+
},
|
| 2307 |
+
"correct_hint": {
|
| 2308 |
+
"action_type": "approve_refund",
|
| 2309 |
+
"refund_amount": 75.0
|
| 2310 |
+
},
|
| 2311 |
+
"drift_sensitive_to": null,
|
| 2312 |
+
"reward": 1.5,
|
| 2313 |
+
"compliance": 1.0
|
| 2314 |
+
},
|
| 2315 |
+
{
|
| 2316 |
+
"step": 10,
|
| 2317 |
+
"email_kind": "customer",
|
| 2318 |
+
"email_id": "c_10_critical_api_down",
|
| 2319 |
+
"action": {
|
| 2320 |
+
"action_type": "escalate",
|
| 2321 |
+
"escalation_tier": "tier_1",
|
| 2322 |
+
"followup_hours": 2
|
| 2323 |
+
},
|
| 2324 |
+
"correct_hint": {
|
| 2325 |
+
"action_type": "escalate",
|
| 2326 |
+
"escalation_tier": "tier_2",
|
| 2327 |
+
"followup_hours": 2
|
| 2328 |
+
},
|
| 2329 |
+
"drift_sensitive_to": "sla_2hr",
|
| 2330 |
+
"reward": 1.2,
|
| 2331 |
+
"compliance": 0.7
|
| 2332 |
+
},
|
| 2333 |
+
{
|
| 2334 |
+
"step": 11,
|
| 2335 |
+
"email_kind": "admin",
|
| 2336 |
+
"email_id": "admin_11_escalate_keep_tier_2",
|
| 2337 |
+
"action": {
|
| 2338 |
+
"action_type": "close",
|
| 2339 |
+
"resolution_code": "policy_acknowledged"
|
| 2340 |
+
},
|
| 2341 |
+
"correct_hint": {
|
| 2342 |
+
"action_type": "close",
|
| 2343 |
+
"resolution_code": "policy_acknowledged"
|
| 2344 |
+
},
|
| 2345 |
+
"drift_sensitive_to": null,
|
| 2346 |
+
"reward": 1.0,
|
| 2347 |
+
"compliance": 1.0
|
| 2348 |
+
},
|
| 2349 |
+
{
|
| 2350 |
+
"step": 12,
|
| 2351 |
+
"email_kind": "customer",
|
| 2352 |
+
"email_id": "c_12_refund_150",
|
| 2353 |
+
"action": {
|
| 2354 |
+
"action_type": "approve_refund",
|
| 2355 |
+
"refund_amount": 150.0
|
| 2356 |
+
},
|
| 2357 |
+
"correct_hint": {
|
| 2358 |
+
"action_type": "escalate",
|
| 2359 |
+
"escalation_tier": "manager"
|
| 2360 |
+
},
|
| 2361 |
+
"drift_sensitive_to": null,
|
| 2362 |
+
"reward": 0.5,
|
| 2363 |
+
"compliance": 0.0
|
| 2364 |
+
},
|
| 2365 |
+
{
|
| 2366 |
+
"step": 13,
|
| 2367 |
+
"email_kind": "customer",
|
| 2368 |
+
"email_id": "c_13_refund_75",
|
| 2369 |
+
"action": {
|
| 2370 |
+
"action_type": "approve_refund",
|
| 2371 |
+
"refund_amount": 75.0
|
| 2372 |
+
},
|
| 2373 |
+
"correct_hint": {
|
| 2374 |
+
"action_type": "approve_refund",
|
| 2375 |
+
"refund_amount": 75.0
|
| 2376 |
+
},
|
| 2377 |
+
"drift_sensitive_to": null,
|
| 2378 |
+
"reward": 1.5,
|
| 2379 |
+
"compliance": 1.0
|
| 2380 |
+
},
|
| 2381 |
+
{
|
| 2382 |
+
"step": 14,
|
| 2383 |
+
"email_kind": "customer",
|
| 2384 |
+
"email_id": "c_14_refund_40",
|
| 2385 |
+
"action": {
|
| 2386 |
+
"action_type": "approve_refund",
|
| 2387 |
+
"refund_amount": 40.0
|
| 2388 |
+
},
|
| 2389 |
+
"correct_hint": {
|
| 2390 |
+
"action_type": "approve_refund",
|
| 2391 |
+
"refund_amount": 40.0
|
| 2392 |
+
},
|
| 2393 |
+
"drift_sensitive_to": null,
|
| 2394 |
+
"reward": 1.5,
|
| 2395 |
+
"compliance": 1.0
|
| 2396 |
+
},
|
| 2397 |
+
{
|
| 2398 |
+
"step": 15,
|
| 2399 |
+
"email_kind": "customer",
|
| 2400 |
+
"email_id": "c_15_refund_180",
|
| 2401 |
+
"action": {
|
| 2402 |
+
"action_type": "request_info",
|
| 2403 |
+
"info_field": "order_id"
|
| 2404 |
+
},
|
| 2405 |
+
"correct_hint": {
|
| 2406 |
+
"action_type": "escalate",
|
| 2407 |
+
"escalation_tier": "manager"
|
| 2408 |
+
},
|
| 2409 |
+
"drift_sensitive_to": null,
|
| 2410 |
+
"reward": 0.5,
|
| 2411 |
+
"compliance": 0.0
|
| 2412 |
+
},
|
| 2413 |
+
{
|
| 2414 |
+
"step": 16,
|
| 2415 |
+
"email_kind": "customer",
|
| 2416 |
+
"email_id": "c_16_refund_75",
|
| 2417 |
+
"action": {
|
| 2418 |
+
"action_type": "approve_refund",
|
| 2419 |
+
"refund_amount": 75.0
|
| 2420 |
+
},
|
| 2421 |
+
"correct_hint": {
|
| 2422 |
+
"action_type": "approve_refund",
|
| 2423 |
+
"refund_amount": 75.0
|
| 2424 |
+
},
|
| 2425 |
+
"drift_sensitive_to": null,
|
| 2426 |
+
"reward": 1.5,
|
| 2427 |
+
"compliance": 1.0
|
| 2428 |
+
},
|
| 2429 |
+
{
|
| 2430 |
+
"step": 17,
|
| 2431 |
+
"email_kind": "customer",
|
| 2432 |
+
"email_id": "c_17_billing_invoice_download",
|
| 2433 |
+
"action": {
|
| 2434 |
+
"action_type": "reply"
|
| 2435 |
+
},
|
| 2436 |
+
"correct_hint": {
|
| 2437 |
+
"action_type": "reply"
|
| 2438 |
+
},
|
| 2439 |
+
"drift_sensitive_to": null,
|
| 2440 |
+
"reward": 1.5,
|
| 2441 |
+
"compliance": 1.0
|
| 2442 |
+
},
|
| 2443 |
+
{
|
| 2444 |
+
"step": 18,
|
| 2445 |
+
"email_kind": "customer",
|
| 2446 |
+
"email_id": "c_18_critical_auth_locked",
|
| 2447 |
+
"action": {
|
| 2448 |
+
"action_type": "escalate",
|
| 2449 |
+
"escalation_tier": "tier_2",
|
| 2450 |
+
"followup_hours": 2
|
| 2451 |
+
},
|
| 2452 |
+
"correct_hint": {
|
| 2453 |
+
"action_type": "escalate",
|
| 2454 |
+
"escalation_tier": "tier_2",
|
| 2455 |
+
"followup_hours": 2
|
| 2456 |
+
},
|
| 2457 |
+
"drift_sensitive_to": null,
|
| 2458 |
+
"reward": 1.5,
|
| 2459 |
+
"compliance": 1.0
|
| 2460 |
+
},
|
| 2461 |
+
{
|
| 2462 |
+
"step": 19,
|
| 2463 |
+
"email_kind": "customer",
|
| 2464 |
+
"email_id": "c_19_refund_120",
|
| 2465 |
+
"action": {
|
| 2466 |
+
"action_type": "approve_refund",
|
| 2467 |
+
"refund_amount": 120.0
|
| 2468 |
+
},
|
| 2469 |
+
"correct_hint": {
|
| 2470 |
+
"action_type": "escalate",
|
| 2471 |
+
"escalation_tier": "manager"
|
| 2472 |
+
},
|
| 2473 |
+
"drift_sensitive_to": null,
|
| 2474 |
+
"reward": 0.5,
|
| 2475 |
+
"compliance": 0.0
|
| 2476 |
+
}
|
| 2477 |
+
]
|
| 2478 |
+
},
|
| 2479 |
+
{
|
| 2480 |
+
"seed": 107,
|
| 2481 |
+
"total_reward": 18.0,
|
| 2482 |
+
"max_reward": 30.0,
|
| 2483 |
+
"frac_of_max": 0.6,
|
| 2484 |
+
"breakdown_totals": {
|
| 2485 |
+
"compliance": 9.5,
|
| 2486 |
+
"appropriateness": 8.5,
|
| 2487 |
+
"drift_bonus": 0.0
|
| 2488 |
+
},
|
| 2489 |
+
"drift_sensitive_total": 3,
|
| 2490 |
+
"drift_sensitive_correct": 0,
|
| 2491 |
+
"drift_sensitive_acc": 0.0,
|
| 2492 |
+
"per_drift": {
|
| 2493 |
+
"refund_cap_25": {
|
| 2494 |
+
"total": 3,
|
| 2495 |
+
"correct": 0
|
| 2496 |
+
}
|
| 2497 |
+
},
|
| 2498 |
+
"trajectory": [
|
| 2499 |
+
{
|
| 2500 |
+
"step": 0,
|
| 2501 |
+
"email_kind": "customer",
|
| 2502 |
+
"email_id": "c_0_chitchat_thanks",
|
| 2503 |
+
"action": {
|
| 2504 |
+
"action_type": "close",
|
| 2505 |
+
"resolution_code": "no_action_needed"
|
| 2506 |
+
},
|
| 2507 |
+
"correct_hint": {
|
| 2508 |
+
"action_type": "close",
|
| 2509 |
+
"resolution_code": "no_action_needed"
|
| 2510 |
+
},
|
| 2511 |
+
"drift_sensitive_to": null,
|
| 2512 |
+
"reward": 1.5,
|
| 2513 |
+
"compliance": 1.0
|
| 2514 |
+
},
|
| 2515 |
+
{
|
| 2516 |
+
"step": 1,
|
| 2517 |
+
"email_kind": "customer",
|
| 2518 |
+
"email_id": "c_1_info_missing_account",
|
| 2519 |
+
"action": {
|
| 2520 |
+
"action_type": "reply"
|
| 2521 |
+
},
|
| 2522 |
+
"correct_hint": {
|
| 2523 |
+
"action_type": "request_info",
|
| 2524 |
+
"info_field": "account_email"
|
| 2525 |
+
},
|
| 2526 |
+
"drift_sensitive_to": null,
|
| 2527 |
+
"reward": 0.0,
|
| 2528 |
+
"compliance": 0.0
|
| 2529 |
+
},
|
| 2530 |
+
{
|
| 2531 |
+
"step": 2,
|
| 2532 |
+
"email_kind": "customer",
|
| 2533 |
+
"email_id": "c_2_refund_30",
|
| 2534 |
+
"action": {
|
| 2535 |
+
"action_type": "approve_refund",
|
| 2536 |
+
"refund_amount": 30.0
|
| 2537 |
+
},
|
| 2538 |
+
"correct_hint": {
|
| 2539 |
+
"action_type": "approve_refund",
|
| 2540 |
+
"refund_amount": 30.0
|
| 2541 |
+
},
|
| 2542 |
+
"drift_sensitive_to": null,
|
| 2543 |
+
"reward": 1.5,
|
| 2544 |
+
"compliance": 1.0
|
| 2545 |
+
},
|
| 2546 |
+
{
|
| 2547 |
+
"step": 3,
|
| 2548 |
+
"email_kind": "admin",
|
| 2549 |
+
"email_id": "admin_3_refund_cap_25",
|
| 2550 |
+
"action": {
|
| 2551 |
+
"action_type": "close",
|
| 2552 |
+
"resolution_code": "policy_acknowledged"
|
| 2553 |
+
},
|
| 2554 |
+
"correct_hint": {
|
| 2555 |
+
"action_type": "close",
|
| 2556 |
+
"resolution_code": "policy_acknowledged"
|
| 2557 |
+
},
|
| 2558 |
+
"drift_sensitive_to": null,
|
| 2559 |
+
"reward": 1.0,
|
| 2560 |
+
"compliance": 1.0
|
| 2561 |
+
},
|
| 2562 |
+
{
|
| 2563 |
+
"step": 4,
|
| 2564 |
+
"email_kind": "customer",
|
| 2565 |
+
"email_id": "c_4_billing_prorated",
|
| 2566 |
+
"action": {
|
| 2567 |
+
"action_type": "reply"
|
| 2568 |
+
},
|
| 2569 |
+
"correct_hint": {
|
| 2570 |
+
"action_type": "reply"
|
| 2571 |
+
},
|
| 2572 |
+
"drift_sensitive_to": null,
|
| 2573 |
+
"reward": 1.5,
|
| 2574 |
+
"compliance": 1.0
|
| 2575 |
+
},
|
| 2576 |
+
{
|
| 2577 |
+
"step": 5,
|
| 2578 |
+
"email_kind": "customer",
|
| 2579 |
+
"email_id": "c_5_billing_tiers",
|
| 2580 |
+
"action": {
|
| 2581 |
+
"action_type": "reply"
|
| 2582 |
+
},
|
| 2583 |
+
"correct_hint": {
|
| 2584 |
+
"action_type": "reply"
|
| 2585 |
+
},
|
| 2586 |
+
"drift_sensitive_to": null,
|
| 2587 |
+
"reward": 1.5,
|
| 2588 |
+
"compliance": 1.0
|
| 2589 |
+
},
|
| 2590 |
+
{
|
| 2591 |
+
"step": 6,
|
| 2592 |
+
"email_kind": "customer",
|
| 2593 |
+
"email_id": "c_6_refund_90",
|
| 2594 |
+
"action": {
|
| 2595 |
+
"action_type": "approve_refund",
|
| 2596 |
+
"refund_amount": 90.0
|
| 2597 |
+
},
|
| 2598 |
+
"correct_hint": {
|
| 2599 |
+
"action_type": "escalate",
|
| 2600 |
+
"escalation_tier": "manager"
|
| 2601 |
+
},
|
| 2602 |
+
"drift_sensitive_to": "refund_cap_25",
|
| 2603 |
+
"reward": 0.5,
|
| 2604 |
+
"compliance": 0.0
|
| 2605 |
+
},
|
| 2606 |
+
{
|
| 2607 |
+
"step": 7,
|
| 2608 |
+
"email_kind": "customer",
|
| 2609 |
+
"email_id": "c_7_chitchat_ooo",
|
| 2610 |
+
"action": {
|
| 2611 |
+
"action_type": "close",
|
| 2612 |
+
"resolution_code": "no_action_needed"
|
| 2613 |
+
},
|
| 2614 |
+
"correct_hint": {
|
| 2615 |
+
"action_type": "close",
|
| 2616 |
+
"resolution_code": "no_action_needed"
|
| 2617 |
+
},
|
| 2618 |
+
"drift_sensitive_to": null,
|
| 2619 |
+
"reward": 1.5,
|
| 2620 |
+
"compliance": 1.0
|
| 2621 |
+
},
|
| 2622 |
+
{
|
| 2623 |
+
"step": 8,
|
| 2624 |
+
"email_kind": "customer",
|
| 2625 |
+
"email_id": "c_8_refund_250",
|
| 2626 |
+
"action": {
|
| 2627 |
+
"action_type": "escalate",
|
| 2628 |
+
"escalation_tier": "manager",
|
| 2629 |
+
"followup_hours": 48
|
| 2630 |
+
},
|
| 2631 |
+
"correct_hint": {
|
| 2632 |
+
"action_type": "escalate",
|
| 2633 |
+
"escalation_tier": "manager"
|
| 2634 |
+
},
|
| 2635 |
+
"drift_sensitive_to": null,
|
| 2636 |
+
"reward": 1.5,
|
| 2637 |
+
"compliance": 1.0
|
| 2638 |
+
},
|
| 2639 |
+
{
|
| 2640 |
+
"step": 9,
|
| 2641 |
+
"email_kind": "customer",
|
| 2642 |
+
"email_id": "c_9_refund_75",
|
| 2643 |
+
"action": {
|
| 2644 |
+
"action_type": "approve_refund",
|
| 2645 |
+
"refund_amount": 75.0
|
| 2646 |
+
},
|
| 2647 |
+
"correct_hint": {
|
| 2648 |
+
"action_type": "escalate",
|
| 2649 |
+
"escalation_tier": "manager"
|
| 2650 |
+
},
|
| 2651 |
+
"drift_sensitive_to": "refund_cap_25",
|
| 2652 |
+
"reward": 0.5,
|
| 2653 |
+
"compliance": 0.0
|
| 2654 |
+
},
|
| 2655 |
+
{
|
| 2656 |
+
"step": 10,
|
| 2657 |
+
"email_kind": "customer",
|
| 2658 |
+
"email_id": "c_10_refund_90",
|
| 2659 |
+
"action": {
|
| 2660 |
+
"action_type": "approve_refund",
|
| 2661 |
+
"refund_amount": 90.0
|
| 2662 |
+
},
|
| 2663 |
+
"correct_hint": {
|
| 2664 |
+
"action_type": "escalate",
|
| 2665 |
+
"escalation_tier": "manager"
|
| 2666 |
+
},
|
| 2667 |
+
"drift_sensitive_to": "refund_cap_25",
|
| 2668 |
+
"reward": 0.5,
|
| 2669 |
+
"compliance": 0.0
|
| 2670 |
+
},
|
| 2671 |
+
{
|
| 2672 |
+
"step": 11,
|
| 2673 |
+
"email_kind": "admin",
|
| 2674 |
+
"email_id": "admin_11_escalate_tier_1",
|
| 2675 |
+
"action": {
|
| 2676 |
+
"action_type": "close",
|
| 2677 |
+
"resolution_code": "policy_acknowledged"
|
| 2678 |
+
},
|
| 2679 |
+
"correct_hint": {
|
| 2680 |
+
"action_type": "close",
|
| 2681 |
+
"resolution_code": "policy_acknowledged"
|
| 2682 |
+
},
|
| 2683 |
+
"drift_sensitive_to": null,
|
| 2684 |
+
"reward": 1.0,
|
| 2685 |
+
"compliance": 1.0
|
| 2686 |
+
},
|
| 2687 |
+
{
|
| 2688 |
+
"step": 12,
|
| 2689 |
+
"email_kind": "customer",
|
| 2690 |
+
"email_id": "c_12_refund_120",
|
| 2691 |
+
"action": {
|
| 2692 |
+
"action_type": "request_info",
|
| 2693 |
+
"info_field": "partner_account_email"
|
| 2694 |
+
},
|
| 2695 |
+
"correct_hint": {
|
| 2696 |
+
"action_type": "escalate",
|
| 2697 |
+
"escalation_tier": "manager"
|
| 2698 |
+
},
|
| 2699 |
+
"drift_sensitive_to": null,
|
| 2700 |
+
"reward": 0.5,
|
| 2701 |
+
"compliance": 0.0
|
| 2702 |
+
},
|
| 2703 |
+
{
|
| 2704 |
+
"step": 13,
|
| 2705 |
+
"email_kind": "customer",
|
| 2706 |
+
"email_id": "c_13_refund_120",
|
| 2707 |
+
"action": {
|
| 2708 |
+
"action_type": "request_info",
|
| 2709 |
+
"info_field": "partner_account_id"
|
| 2710 |
+
},
|
| 2711 |
+
"correct_hint": {
|
| 2712 |
+
"action_type": "escalate",
|
| 2713 |
+
"escalation_tier": "manager"
|
| 2714 |
+
},
|
| 2715 |
+
"drift_sensitive_to": null,
|
| 2716 |
+
"reward": 0.5,
|
| 2717 |
+
"compliance": 0.0
|
| 2718 |
+
},
|
| 2719 |
+
{
|
| 2720 |
+
"step": 14,
|
| 2721 |
+
"email_kind": "customer",
|
| 2722 |
+
"email_id": "c_14_refund_100",
|
| 2723 |
+
"action": {
|
| 2724 |
+
"action_type": "approve_refund",
|
| 2725 |
+
"refund_amount": 100.0
|
| 2726 |
+
},
|
| 2727 |
+
"correct_hint": {
|
| 2728 |
+
"action_type": "escalate",
|
| 2729 |
+
"escalation_tier": "manager"
|
| 2730 |
+
},
|
| 2731 |
+
"drift_sensitive_to": null,
|
| 2732 |
+
"reward": 0.5,
|
| 2733 |
+
"compliance": 0.0
|
| 2734 |
+
},
|
| 2735 |
+
{
|
| 2736 |
+
"step": 15,
|
| 2737 |
+
"email_kind": "customer",
|
| 2738 |
+
"email_id": "c_15_refund_150",
|
| 2739 |
+
"action": {
|
| 2740 |
+
"action_type": "approve_refund",
|
| 2741 |
+
"refund_amount": 150.0
|
| 2742 |
+
},
|
| 2743 |
+
"correct_hint": {
|
| 2744 |
+
"action_type": "escalate",
|
| 2745 |
+
"escalation_tier": "manager"
|
| 2746 |
+
},
|
| 2747 |
+
"drift_sensitive_to": null,
|
| 2748 |
+
"reward": 0.5,
|
| 2749 |
+
"compliance": 0.0
|
| 2750 |
+
},
|
| 2751 |
+
{
|
| 2752 |
+
"step": 16,
|
| 2753 |
+
"email_kind": "customer",
|
| 2754 |
+
"email_id": "c_16_refund_150",
|
| 2755 |
+
"action": {
|
| 2756 |
+
"action_type": "approve_refund",
|
| 2757 |
+
"refund_amount": 150.0
|
| 2758 |
+
},
|
| 2759 |
+
"correct_hint": {
|
| 2760 |
+
"action_type": "escalate",
|
| 2761 |
+
"escalation_tier": "manager"
|
| 2762 |
+
},
|
| 2763 |
+
"drift_sensitive_to": null,
|
| 2764 |
+
"reward": 0.5,
|
| 2765 |
+
"compliance": 0.0
|
| 2766 |
+
},
|
| 2767 |
+
{
|
| 2768 |
+
"step": 17,
|
| 2769 |
+
"email_kind": "customer",
|
| 2770 |
+
"email_id": "c_17_info_missing_order_id",
|
| 2771 |
+
"action": {
|
| 2772 |
+
"action_type": "request_info",
|
| 2773 |
+
"info_field": "invoice_details"
|
| 2774 |
+
},
|
| 2775 |
+
"correct_hint": {
|
| 2776 |
+
"action_type": "request_info",
|
| 2777 |
+
"info_field": "order_id"
|
| 2778 |
+
},
|
| 2779 |
+
"drift_sensitive_to": null,
|
| 2780 |
+
"reward": 1.0,
|
| 2781 |
+
"compliance": 0.5
|
| 2782 |
+
},
|
| 2783 |
+
{
|
| 2784 |
+
"step": 18,
|
| 2785 |
+
"email_kind": "customer",
|
| 2786 |
+
"email_id": "c_18_billing_prorated",
|
| 2787 |
+
"action": {
|
| 2788 |
+
"action_type": "reply"
|
| 2789 |
+
},
|
| 2790 |
+
"correct_hint": {
|
| 2791 |
+
"action_type": "reply"
|
| 2792 |
+
},
|
| 2793 |
+
"drift_sensitive_to": null,
|
| 2794 |
+
"reward": 1.5,
|
| 2795 |
+
"compliance": 1.0
|
| 2796 |
+
},
|
| 2797 |
+
{
|
| 2798 |
+
"step": 19,
|
| 2799 |
+
"email_kind": "customer",
|
| 2800 |
+
"email_id": "c_19_refund_100",
|
| 2801 |
+
"action": {
|
| 2802 |
+
"action_type": "approve_refund",
|
| 2803 |
+
"refund_amount": 100.0
|
| 2804 |
+
},
|
| 2805 |
+
"correct_hint": {
|
| 2806 |
+
"action_type": "escalate",
|
| 2807 |
+
"escalation_tier": "manager"
|
| 2808 |
+
},
|
| 2809 |
+
"drift_sensitive_to": null,
|
| 2810 |
+
"reward": 0.5,
|
| 2811 |
+
"compliance": 0.0
|
| 2812 |
+
}
|
| 2813 |
+
]
|
| 2814 |
+
}
|
| 2815 |
+
]
|
| 2816 |
+
}
|
inference.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Inference Script — Email Triage OpenEnv
|
| 3 |
+
===================================
|
| 4 |
+
MANDATORY
|
| 5 |
+
- Before submitting, ensure the following variables are defined in your environment configuration:
|
| 6 |
+
API_BASE_URL The API endpoint for the LLM.
|
| 7 |
+
MODEL_NAME The model identifier to use for inference.
|
| 8 |
+
HF_TOKEN Your Hugging Face / API key.
|
| 9 |
+
|
| 10 |
+
- The inference script must be named `inference.py` and placed in the root directory of the project
|
| 11 |
+
- Participants must use OpenAI Client for all LLM calls using above variables
|
| 12 |
+
|
| 13 |
+
STDOUT FORMAT
|
| 14 |
+
- The script emits exactly three line types to stdout:
|
| 15 |
+
[START] task=<task_name> env=<benchmark> model=<model_name>
|
| 16 |
+
[STEP] step=<n> action=<action_str> reward=<0.00> done=<true|false> error=<msg|null>
|
| 17 |
+
[END] success=<true|false> steps=<n> score=<score> rewards=<r1,r2,...,rn>
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import os
|
| 21 |
+
import json
|
| 22 |
+
import sys
|
| 23 |
+
|
| 24 |
+
from openai import OpenAI
|
| 25 |
+
|
| 26 |
+
from email_env.server.environment import EmailTriageEnv
|
| 27 |
+
from email_env.models import Action
|
| 28 |
+
from email_env.tasks import TASKS
|
| 29 |
+
|
| 30 |
+
API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
|
| 31 |
+
MODEL_NAME = os.getenv("MODEL_NAME", "meta-llama/Llama-3.1-8B-Instruct")
|
| 32 |
+
HF_TOKEN = os.getenv("HF_TOKEN") or os.getenv("API_KEY")
|
| 33 |
+
BENCHMARK = "email-triage"
|
| 34 |
+
SUCCESS_THRESHOLD = 0.5
|
| 35 |
+
|
| 36 |
+
if not HF_TOKEN:
|
| 37 |
+
raise EnvironmentError("HF_TOKEN environment variable is required.")
|
| 38 |
+
|
| 39 |
+
SYSTEM_PROMPT = """You are an email triage assistant. Given an email, you must:
|
| 40 |
+
1. Classify the email into exactly one category: billing, technical, or general
|
| 41 |
+
2. Assign a priority: low, medium, or high
|
| 42 |
+
3. Write a professional response to the sender
|
| 43 |
+
|
| 44 |
+
Reply ONLY with valid JSON in this exact format (no markdown, no extra text):
|
| 45 |
+
{
|
| 46 |
+
"category": "<billing|technical|general>",
|
| 47 |
+
"priority": "<low|medium|high>",
|
| 48 |
+
"response": "<your response text>"
|
| 49 |
+
}"""
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def run_inference():
|
| 53 |
+
client = OpenAI(api_key=HF_TOKEN, base_url=API_BASE_URL)
|
| 54 |
+
env = EmailTriageEnv()
|
| 55 |
+
all_scores = []
|
| 56 |
+
|
| 57 |
+
for task_id in TASKS.keys():
|
| 58 |
+
rewards = []
|
| 59 |
+
success = False
|
| 60 |
+
score = 0.0
|
| 61 |
+
steps = 0
|
| 62 |
+
error_msg = "null"
|
| 63 |
+
action_str = "noop"
|
| 64 |
+
done = False
|
| 65 |
+
|
| 66 |
+
print(
|
| 67 |
+
f"[START] task={task_id} env={BENCHMARK} model={MODEL_NAME}",
|
| 68 |
+
flush=True,
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
try:
|
| 72 |
+
obs = env.reset(task_id=task_id)
|
| 73 |
+
|
| 74 |
+
user_msg = (
|
| 75 |
+
f"Sender type: {obs.sender_type}\n\n"
|
| 76 |
+
f"Email:\n{obs.email_text}"
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
completion = client.chat.completions.create(
|
| 80 |
+
model=MODEL_NAME,
|
| 81 |
+
messages=[
|
| 82 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 83 |
+
{"role": "user", "content": user_msg},
|
| 84 |
+
],
|
| 85 |
+
temperature=0.2,
|
| 86 |
+
max_tokens=300,
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
raw = completion.choices[0].message.content.strip()
|
| 90 |
+
if raw.startswith("```"):
|
| 91 |
+
lines = [l for l in raw.split("\n") if not l.startswith("```")]
|
| 92 |
+
raw = "\n".join(lines).strip()
|
| 93 |
+
|
| 94 |
+
try:
|
| 95 |
+
parsed = json.loads(raw)
|
| 96 |
+
except json.JSONDecodeError:
|
| 97 |
+
parsed = {"category": "general", "priority": "low", "response": ""}
|
| 98 |
+
error_msg = "json_parse_error"
|
| 99 |
+
|
| 100 |
+
action = Action(
|
| 101 |
+
category=parsed.get("category", "general"),
|
| 102 |
+
priority=parsed.get("priority", "low"),
|
| 103 |
+
response=parsed.get("response", ""),
|
| 104 |
+
)
|
| 105 |
+
action_str = (
|
| 106 |
+
f"triage(category='{action.category}',"
|
| 107 |
+
f"priority='{action.priority}')"
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
result = env.step(action)
|
| 111 |
+
reward = float(result.reward)
|
| 112 |
+
done = bool(result.done)
|
| 113 |
+
steps = 1
|
| 114 |
+
rewards.append(reward)
|
| 115 |
+
score = reward
|
| 116 |
+
success = score >= SUCCESS_THRESHOLD
|
| 117 |
+
|
| 118 |
+
print(
|
| 119 |
+
f"[STEP] step=1 action={action_str} reward={reward:.2f} "
|
| 120 |
+
f"done={'true' if done else 'false'} error={error_msg}",
|
| 121 |
+
flush=True,
|
| 122 |
+
)
|
| 123 |
+
all_scores.append(score)
|
| 124 |
+
|
| 125 |
+
except Exception as exc:
|
| 126 |
+
error_msg = str(exc).replace("\n", " ")
|
| 127 |
+
print(
|
| 128 |
+
f"[STEP] step=1 action={action_str} reward=0.00 done=true "
|
| 129 |
+
f"error={error_msg}",
|
| 130 |
+
file=sys.stderr,
|
| 131 |
+
flush=True,
|
| 132 |
+
)
|
| 133 |
+
finally:
|
| 134 |
+
rewards_str = ",".join(f"{r:.2f}" for r in rewards) if rewards else "0.00"
|
| 135 |
+
print(
|
| 136 |
+
f"[END] success={'true' if success else 'false'} steps={steps} "
|
| 137 |
+
f"score={score:.2f} rewards={rewards_str}",
|
| 138 |
+
flush=True,
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
avg = round(sum(all_scores) / len(all_scores), 2) if all_scores else 0.0
|
| 142 |
+
print(f"\n=== Average Score: {avg:.2f} ===", flush=True)
|
| 143 |
+
return avg
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
if __name__ == "__main__":
|
| 147 |
+
run_inference()
|
openenv.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
spec_version: 1
|
| 2 |
+
name: policy-drift
|
| 3 |
+
type: space
|
| 4 |
+
runtime: fastapi
|
| 5 |
+
app: server.app:app
|
| 6 |
+
port: 7860
|
outputs/baseline_direction_split.png
ADDED
|
outputs/cross_model_baseline.png
ADDED
|
outputs/direction_split.png
ADDED
|
outputs/direction_split_v6.png
ADDED
|
outputs/evals_v7.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"pre": {
|
| 3 |
+
"compliance": 0.5275000000000006,
|
| 4 |
+
"appropriateness": 0.41,
|
| 5 |
+
"drift_bonus": 0.005,
|
| 6 |
+
"drift_acc": 0.11764705882352941,
|
| 7 |
+
"drift_acc_by_direction": {
|
| 8 |
+
"tightening": 0.0,
|
| 9 |
+
"loosening": 0.21428571428571427,
|
| 10 |
+
"neutral": null
|
| 11 |
+
},
|
| 12 |
+
"drift_counts_by_direction": {
|
| 13 |
+
"tightening": {
|
| 14 |
+
"correct": 0,
|
| 15 |
+
"total": 23
|
| 16 |
+
},
|
| 17 |
+
"loosening": {
|
| 18 |
+
"correct": 3,
|
| 19 |
+
"total": 14
|
| 20 |
+
},
|
| 21 |
+
"neutral": {
|
| 22 |
+
"correct": 0,
|
| 23 |
+
"total": 0
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
},
|
| 27 |
+
"post_sft": {
|
| 28 |
+
"compliance": 0.9679999999999999,
|
| 29 |
+
"appropriateness": 0.45,
|
| 30 |
+
"drift_bonus": 0.0375,
|
| 31 |
+
"drift_acc": 0.8823529411764706,
|
| 32 |
+
"drift_acc_by_direction": {
|
| 33 |
+
"tightening": 0.9130434782608695,
|
| 34 |
+
"loosening": 0.7142857142857143,
|
| 35 |
+
"neutral": null
|
| 36 |
+
},
|
| 37 |
+
"drift_counts_by_direction": {
|
| 38 |
+
"tightening": {
|
| 39 |
+
"correct": 21,
|
| 40 |
+
"total": 23
|
| 41 |
+
},
|
| 42 |
+
"loosening": {
|
| 43 |
+
"correct": 10,
|
| 44 |
+
"total": 14
|
| 45 |
+
},
|
| 46 |
+
"neutral": {
|
| 47 |
+
"correct": 0,
|
| 48 |
+
"total": 0
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
},
|
| 52 |
+
"post_grpo": {
|
| 53 |
+
"compliance": 0.9679999999999999,
|
| 54 |
+
"appropriateness": 0.45,
|
| 55 |
+
"drift_bonus": 0.0375,
|
| 56 |
+
"drift_acc": 0.8823529411764706,
|
| 57 |
+
"drift_acc_by_direction": {
|
| 58 |
+
"tightening": 0.9130434782608695,
|
| 59 |
+
"loosening": 0.7142857142857143,
|
| 60 |
+
"neutral": null
|
| 61 |
+
},
|
| 62 |
+
"drift_counts_by_direction": {
|
| 63 |
+
"tightening": {
|
| 64 |
+
"correct": 21,
|
| 65 |
+
"total": 23
|
| 66 |
+
},
|
| 67 |
+
"loosening": {
|
| 68 |
+
"correct": 10,
|
| 69 |
+
"total": 14
|
| 70 |
+
},
|
| 71 |
+
"neutral": {
|
| 72 |
+
"correct": 0,
|
| 73 |
+
"total": 0
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
},
|
| 77 |
+
"model_name": "unsloth/Qwen2.5-3B-Instruct",
|
| 78 |
+
"quick_mode": false
|
| 79 |
+
}
|
outputs/sft_log_v7.json
ADDED
|
@@ -0,0 +1,711 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"loss": 1.293243408203125,
|
| 4 |
+
"grad_norm": 2.695000171661377,
|
| 5 |
+
"learning_rate": 1.8e-05,
|
| 6 |
+
"epoch": 0.01,
|
| 7 |
+
"step": 10
|
| 8 |
+
},
|
| 9 |
+
{
|
| 10 |
+
"loss": 0.9491410255432129,
|
| 11 |
+
"grad_norm": 4.51662540435791,
|
| 12 |
+
"learning_rate": 3.8e-05,
|
| 13 |
+
"epoch": 0.02,
|
| 14 |
+
"step": 20
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
"loss": 0.13204352855682372,
|
| 18 |
+
"grad_norm": 0.3303098678588867,
|
| 19 |
+
"learning_rate": 5.8e-05,
|
| 20 |
+
"epoch": 0.03,
|
| 21 |
+
"step": 30
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"loss": 0.07011271715164184,
|
| 25 |
+
"grad_norm": 0.28593316674232483,
|
| 26 |
+
"learning_rate": 7.800000000000001e-05,
|
| 27 |
+
"epoch": 0.04,
|
| 28 |
+
"step": 40
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"loss": 0.02982504665851593,
|
| 32 |
+
"grad_norm": 0.1960303634405136,
|
| 33 |
+
"learning_rate": 9.8e-05,
|
| 34 |
+
"epoch": 0.05,
|
| 35 |
+
"step": 50
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"loss": 0.030540597438812257,
|
| 39 |
+
"grad_norm": 0.361972838640213,
|
| 40 |
+
"learning_rate": 0.000118,
|
| 41 |
+
"epoch": 0.06,
|
| 42 |
+
"step": 60
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
"loss": 0.020638951659202577,
|
| 46 |
+
"grad_norm": 0.14442680776119232,
|
| 47 |
+
"learning_rate": 0.000138,
|
| 48 |
+
"epoch": 0.07,
|
| 49 |
+
"step": 70
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
"loss": 0.019276422262191773,
|
| 53 |
+
"grad_norm": 0.1991211175918579,
|
| 54 |
+
"learning_rate": 0.00015800000000000002,
|
| 55 |
+
"epoch": 0.08,
|
| 56 |
+
"step": 80
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"loss": 0.019253556430339814,
|
| 60 |
+
"grad_norm": 0.2892937660217285,
|
| 61 |
+
"learning_rate": 0.00017800000000000002,
|
| 62 |
+
"epoch": 0.09,
|
| 63 |
+
"step": 90
|
| 64 |
+
},
|
| 65 |
+
{
|
| 66 |
+
"loss": 0.020958416163921356,
|
| 67 |
+
"grad_norm": 0.14148078858852386,
|
| 68 |
+
"learning_rate": 0.00019800000000000002,
|
| 69 |
+
"epoch": 0.1,
|
| 70 |
+
"step": 100
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"loss": 0.02394942194223404,
|
| 74 |
+
"grad_norm": 0.23137691617012024,
|
| 75 |
+
"learning_rate": 0.00019800000000000002,
|
| 76 |
+
"epoch": 0.11,
|
| 77 |
+
"step": 110
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"loss": 0.02662351429462433,
|
| 81 |
+
"grad_norm": 0.12059047073125839,
|
| 82 |
+
"learning_rate": 0.0001957777777777778,
|
| 83 |
+
"epoch": 0.12,
|
| 84 |
+
"step": 120
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
"loss": 0.020230546593666077,
|
| 88 |
+
"grad_norm": 0.13224035501480103,
|
| 89 |
+
"learning_rate": 0.00019355555555555557,
|
| 90 |
+
"epoch": 0.13,
|
| 91 |
+
"step": 130
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
"loss": 0.014973150193691253,
|
| 95 |
+
"grad_norm": 0.14604584872722626,
|
| 96 |
+
"learning_rate": 0.00019133333333333334,
|
| 97 |
+
"epoch": 0.14,
|
| 98 |
+
"step": 140
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
"loss": 0.013179084658622742,
|
| 102 |
+
"grad_norm": 0.03541504964232445,
|
| 103 |
+
"learning_rate": 0.00018911111111111112,
|
| 104 |
+
"epoch": 0.15,
|
| 105 |
+
"step": 150
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
"loss": 0.017047987878322603,
|
| 109 |
+
"grad_norm": 0.14085718989372253,
|
| 110 |
+
"learning_rate": 0.0001868888888888889,
|
| 111 |
+
"epoch": 0.16,
|
| 112 |
+
"step": 160
|
| 113 |
+
},
|
| 114 |
+
{
|
| 115 |
+
"loss": 0.013664944469928742,
|
| 116 |
+
"grad_norm": 0.10603413730859756,
|
| 117 |
+
"learning_rate": 0.00018466666666666666,
|
| 118 |
+
"epoch": 0.17,
|
| 119 |
+
"step": 170
|
| 120 |
+
},
|
| 121 |
+
{
|
| 122 |
+
"loss": 0.014989382028579712,
|
| 123 |
+
"grad_norm": 0.0628352016210556,
|
| 124 |
+
"learning_rate": 0.00018244444444444447,
|
| 125 |
+
"epoch": 0.18,
|
| 126 |
+
"step": 180
|
| 127 |
+
},
|
| 128 |
+
{
|
| 129 |
+
"loss": 0.01449367105960846,
|
| 130 |
+
"grad_norm": 0.06855176389217377,
|
| 131 |
+
"learning_rate": 0.00018022222222222221,
|
| 132 |
+
"epoch": 0.19,
|
| 133 |
+
"step": 190
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
"loss": 0.015271393954753876,
|
| 137 |
+
"grad_norm": 0.0931730642914772,
|
| 138 |
+
"learning_rate": 0.00017800000000000002,
|
| 139 |
+
"epoch": 0.2,
|
| 140 |
+
"step": 200
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
"loss": 0.021130312979221345,
|
| 144 |
+
"grad_norm": 0.10431171208620071,
|
| 145 |
+
"learning_rate": 0.0001757777777777778,
|
| 146 |
+
"epoch": 0.21,
|
| 147 |
+
"step": 210
|
| 148 |
+
},
|
| 149 |
+
{
|
| 150 |
+
"loss": 0.013795286417007446,
|
| 151 |
+
"grad_norm": 0.0743161141872406,
|
| 152 |
+
"learning_rate": 0.00017355555555555557,
|
| 153 |
+
"epoch": 0.22,
|
| 154 |
+
"step": 220
|
| 155 |
+
},
|
| 156 |
+
{
|
| 157 |
+
"loss": 0.013209818303585053,
|
| 158 |
+
"grad_norm": 0.03357032686471939,
|
| 159 |
+
"learning_rate": 0.00017133333333333334,
|
| 160 |
+
"epoch": 0.23,
|
| 161 |
+
"step": 230
|
| 162 |
+
},
|
| 163 |
+
{
|
| 164 |
+
"loss": 0.011038484424352646,
|
| 165 |
+
"grad_norm": 0.021826738491654396,
|
| 166 |
+
"learning_rate": 0.00016911111111111112,
|
| 167 |
+
"epoch": 0.24,
|
| 168 |
+
"step": 240
|
| 169 |
+
},
|
| 170 |
+
{
|
| 171 |
+
"loss": 0.013412389159202575,
|
| 172 |
+
"grad_norm": 0.08149590343236923,
|
| 173 |
+
"learning_rate": 0.0001668888888888889,
|
| 174 |
+
"epoch": 0.25,
|
| 175 |
+
"step": 250
|
| 176 |
+
},
|
| 177 |
+
{
|
| 178 |
+
"loss": 0.016977037489414214,
|
| 179 |
+
"grad_norm": 0.0708199143409729,
|
| 180 |
+
"learning_rate": 0.00016466666666666667,
|
| 181 |
+
"epoch": 0.26,
|
| 182 |
+
"step": 260
|
| 183 |
+
},
|
| 184 |
+
{
|
| 185 |
+
"loss": 0.014706389605998993,
|
| 186 |
+
"grad_norm": 0.07086025923490524,
|
| 187 |
+
"learning_rate": 0.00016244444444444444,
|
| 188 |
+
"epoch": 0.27,
|
| 189 |
+
"step": 270
|
| 190 |
+
},
|
| 191 |
+
{
|
| 192 |
+
"loss": 0.009542294591665269,
|
| 193 |
+
"grad_norm": 0.10200642049312592,
|
| 194 |
+
"learning_rate": 0.00016022222222222222,
|
| 195 |
+
"epoch": 0.28,
|
| 196 |
+
"step": 280
|
| 197 |
+
},
|
| 198 |
+
{
|
| 199 |
+
"loss": 0.011282695829868317,
|
| 200 |
+
"grad_norm": 0.12065727263689041,
|
| 201 |
+
"learning_rate": 0.00015800000000000002,
|
| 202 |
+
"epoch": 0.29,
|
| 203 |
+
"step": 290
|
| 204 |
+
},
|
| 205 |
+
{
|
| 206 |
+
"loss": 0.0091792993247509,
|
| 207 |
+
"grad_norm": 0.056022465229034424,
|
| 208 |
+
"learning_rate": 0.00015577777777777777,
|
| 209 |
+
"epoch": 0.3,
|
| 210 |
+
"step": 300
|
| 211 |
+
},
|
| 212 |
+
{
|
| 213 |
+
"loss": 0.010022152960300446,
|
| 214 |
+
"grad_norm": 0.027627091854810715,
|
| 215 |
+
"learning_rate": 0.00015355555555555557,
|
| 216 |
+
"epoch": 0.31,
|
| 217 |
+
"step": 310
|
| 218 |
+
},
|
| 219 |
+
{
|
| 220 |
+
"loss": 0.010018721222877502,
|
| 221 |
+
"grad_norm": 0.04348656162619591,
|
| 222 |
+
"learning_rate": 0.00015133333333333334,
|
| 223 |
+
"epoch": 0.32,
|
| 224 |
+
"step": 320
|
| 225 |
+
},
|
| 226 |
+
{
|
| 227 |
+
"loss": 0.015866005420684816,
|
| 228 |
+
"grad_norm": 0.06167244166135788,
|
| 229 |
+
"learning_rate": 0.00014911111111111112,
|
| 230 |
+
"epoch": 0.33,
|
| 231 |
+
"step": 330
|
| 232 |
+
},
|
| 233 |
+
{
|
| 234 |
+
"loss": 0.009907713532447815,
|
| 235 |
+
"grad_norm": 0.043530818074941635,
|
| 236 |
+
"learning_rate": 0.0001468888888888889,
|
| 237 |
+
"epoch": 0.34,
|
| 238 |
+
"step": 340
|
| 239 |
+
},
|
| 240 |
+
{
|
| 241 |
+
"loss": 0.011248402297496796,
|
| 242 |
+
"grad_norm": 0.03280569612979889,
|
| 243 |
+
"learning_rate": 0.0001446666666666667,
|
| 244 |
+
"epoch": 0.35,
|
| 245 |
+
"step": 350
|
| 246 |
+
},
|
| 247 |
+
{
|
| 248 |
+
"loss": 0.01353382021188736,
|
| 249 |
+
"grad_norm": 0.04305155202746391,
|
| 250 |
+
"learning_rate": 0.00014244444444444444,
|
| 251 |
+
"epoch": 0.36,
|
| 252 |
+
"step": 360
|
| 253 |
+
},
|
| 254 |
+
{
|
| 255 |
+
"loss": 0.012516531348228454,
|
| 256 |
+
"grad_norm": 0.04698939993977547,
|
| 257 |
+
"learning_rate": 0.00014022222222222225,
|
| 258 |
+
"epoch": 0.37,
|
| 259 |
+
"step": 370
|
| 260 |
+
},
|
| 261 |
+
{
|
| 262 |
+
"loss": 0.010274696350097656,
|
| 263 |
+
"grad_norm": 0.1363413780927658,
|
| 264 |
+
"learning_rate": 0.000138,
|
| 265 |
+
"epoch": 0.38,
|
| 266 |
+
"step": 380
|
| 267 |
+
},
|
| 268 |
+
{
|
| 269 |
+
"loss": 0.010345887392759323,
|
| 270 |
+
"grad_norm": 0.06110126152634621,
|
| 271 |
+
"learning_rate": 0.00013577777777777777,
|
| 272 |
+
"epoch": 0.39,
|
| 273 |
+
"step": 390
|
| 274 |
+
},
|
| 275 |
+
{
|
| 276 |
+
"loss": 0.01669677793979645,
|
| 277 |
+
"grad_norm": 0.0443761982023716,
|
| 278 |
+
"learning_rate": 0.00013355555555555557,
|
| 279 |
+
"epoch": 0.4,
|
| 280 |
+
"step": 400
|
| 281 |
+
},
|
| 282 |
+
{
|
| 283 |
+
"loss": 0.010670651495456696,
|
| 284 |
+
"grad_norm": 0.046872515231370926,
|
| 285 |
+
"learning_rate": 0.00013133333333333332,
|
| 286 |
+
"epoch": 0.41,
|
| 287 |
+
"step": 410
|
| 288 |
+
},
|
| 289 |
+
{
|
| 290 |
+
"loss": 0.011344272643327713,
|
| 291 |
+
"grad_norm": 0.037704139947891235,
|
| 292 |
+
"learning_rate": 0.00012911111111111112,
|
| 293 |
+
"epoch": 0.42,
|
| 294 |
+
"step": 420
|
| 295 |
+
},
|
| 296 |
+
{
|
| 297 |
+
"loss": 0.010733240097761155,
|
| 298 |
+
"grad_norm": 0.03925054147839546,
|
| 299 |
+
"learning_rate": 0.0001268888888888889,
|
| 300 |
+
"epoch": 0.43,
|
| 301 |
+
"step": 430
|
| 302 |
+
},
|
| 303 |
+
{
|
| 304 |
+
"loss": 0.012961900234222412,
|
| 305 |
+
"grad_norm": 0.07470105588436127,
|
| 306 |
+
"learning_rate": 0.00012466666666666667,
|
| 307 |
+
"epoch": 0.44,
|
| 308 |
+
"step": 440
|
| 309 |
+
},
|
| 310 |
+
{
|
| 311 |
+
"loss": 0.012575212121009826,
|
| 312 |
+
"grad_norm": 0.01742836833000183,
|
| 313 |
+
"learning_rate": 0.00012244444444444445,
|
| 314 |
+
"epoch": 0.45,
|
| 315 |
+
"step": 450
|
| 316 |
+
},
|
| 317 |
+
{
|
| 318 |
+
"loss": 0.01014963984489441,
|
| 319 |
+
"grad_norm": 0.04182368889451027,
|
| 320 |
+
"learning_rate": 0.00012022222222222223,
|
| 321 |
+
"epoch": 0.46,
|
| 322 |
+
"step": 460
|
| 323 |
+
},
|
| 324 |
+
{
|
| 325 |
+
"loss": 0.012940752506256103,
|
| 326 |
+
"grad_norm": 0.14171363413333893,
|
| 327 |
+
"learning_rate": 0.000118,
|
| 328 |
+
"epoch": 0.47,
|
| 329 |
+
"step": 470
|
| 330 |
+
},
|
| 331 |
+
{
|
| 332 |
+
"loss": 0.011411245167255401,
|
| 333 |
+
"grad_norm": 0.0419924296438694,
|
| 334 |
+
"learning_rate": 0.00011577777777777778,
|
| 335 |
+
"epoch": 0.48,
|
| 336 |
+
"step": 480
|
| 337 |
+
},
|
| 338 |
+
{
|
| 339 |
+
"loss": 0.011432983726263047,
|
| 340 |
+
"grad_norm": 0.01165507361292839,
|
| 341 |
+
"learning_rate": 0.00011355555555555557,
|
| 342 |
+
"epoch": 0.49,
|
| 343 |
+
"step": 490
|
| 344 |
+
},
|
| 345 |
+
{
|
| 346 |
+
"loss": 0.014088569581508637,
|
| 347 |
+
"grad_norm": 0.03271564468741417,
|
| 348 |
+
"learning_rate": 0.00011133333333333333,
|
| 349 |
+
"epoch": 0.5,
|
| 350 |
+
"step": 500
|
| 351 |
+
},
|
| 352 |
+
{
|
| 353 |
+
"loss": 0.011598656326532364,
|
| 354 |
+
"grad_norm": 0.042704030871391296,
|
| 355 |
+
"learning_rate": 0.00010911111111111112,
|
| 356 |
+
"epoch": 0.51,
|
| 357 |
+
"step": 510
|
| 358 |
+
},
|
| 359 |
+
{
|
| 360 |
+
"loss": 0.007552731037139893,
|
| 361 |
+
"grad_norm": 0.05289052799344063,
|
| 362 |
+
"learning_rate": 0.00010688888888888891,
|
| 363 |
+
"epoch": 0.52,
|
| 364 |
+
"step": 520
|
| 365 |
+
},
|
| 366 |
+
{
|
| 367 |
+
"loss": 0.007692032307386398,
|
| 368 |
+
"grad_norm": 0.06659393757581711,
|
| 369 |
+
"learning_rate": 0.00010466666666666667,
|
| 370 |
+
"epoch": 0.53,
|
| 371 |
+
"step": 530
|
| 372 |
+
},
|
| 373 |
+
{
|
| 374 |
+
"loss": 0.014589102566242218,
|
| 375 |
+
"grad_norm": 0.10728471726179123,
|
| 376 |
+
"learning_rate": 0.00010244444444444446,
|
| 377 |
+
"epoch": 0.54,
|
| 378 |
+
"step": 540
|
| 379 |
+
},
|
| 380 |
+
{
|
| 381 |
+
"loss": 0.013890518248081208,
|
| 382 |
+
"grad_norm": 0.12965723872184753,
|
| 383 |
+
"learning_rate": 0.00010022222222222222,
|
| 384 |
+
"epoch": 0.55,
|
| 385 |
+
"step": 550
|
| 386 |
+
},
|
| 387 |
+
{
|
| 388 |
+
"loss": 0.010909823328256607,
|
| 389 |
+
"grad_norm": 0.04868817701935768,
|
| 390 |
+
"learning_rate": 9.8e-05,
|
| 391 |
+
"epoch": 0.56,
|
| 392 |
+
"step": 560
|
| 393 |
+
},
|
| 394 |
+
{
|
| 395 |
+
"loss": 0.010589510947465897,
|
| 396 |
+
"grad_norm": 0.06954590231180191,
|
| 397 |
+
"learning_rate": 9.577777777777777e-05,
|
| 398 |
+
"epoch": 0.57,
|
| 399 |
+
"step": 570
|
| 400 |
+
},
|
| 401 |
+
{
|
| 402 |
+
"loss": 0.006430118530988693,
|
| 403 |
+
"grad_norm": 0.03395122289657593,
|
| 404 |
+
"learning_rate": 9.355555555555556e-05,
|
| 405 |
+
"epoch": 0.58,
|
| 406 |
+
"step": 580
|
| 407 |
+
},
|
| 408 |
+
{
|
| 409 |
+
"loss": 0.009756166487932205,
|
| 410 |
+
"grad_norm": 0.10624136030673981,
|
| 411 |
+
"learning_rate": 9.133333333333334e-05,
|
| 412 |
+
"epoch": 0.59,
|
| 413 |
+
"step": 590
|
| 414 |
+
},
|
| 415 |
+
{
|
| 416 |
+
"loss": 0.009228541702032089,
|
| 417 |
+
"grad_norm": 0.03023010492324829,
|
| 418 |
+
"learning_rate": 8.911111111111111e-05,
|
| 419 |
+
"epoch": 0.6,
|
| 420 |
+
"step": 600
|
| 421 |
+
},
|
| 422 |
+
{
|
| 423 |
+
"loss": 0.011728598177433014,
|
| 424 |
+
"grad_norm": 0.03197360411286354,
|
| 425 |
+
"learning_rate": 8.68888888888889e-05,
|
| 426 |
+
"epoch": 0.61,
|
| 427 |
+
"step": 610
|
| 428 |
+
},
|
| 429 |
+
{
|
| 430 |
+
"loss": 0.008541644364595414,
|
| 431 |
+
"grad_norm": 0.046316880732774734,
|
| 432 |
+
"learning_rate": 8.466666666666667e-05,
|
| 433 |
+
"epoch": 0.62,
|
| 434 |
+
"step": 620
|
| 435 |
+
},
|
| 436 |
+
{
|
| 437 |
+
"loss": 0.0068620510399341585,
|
| 438 |
+
"grad_norm": 0.013749970123171806,
|
| 439 |
+
"learning_rate": 8.244444444444445e-05,
|
| 440 |
+
"epoch": 0.63,
|
| 441 |
+
"step": 630
|
| 442 |
+
},
|
| 443 |
+
{
|
| 444 |
+
"loss": 0.011553961038589477,
|
| 445 |
+
"grad_norm": 0.015892956405878067,
|
| 446 |
+
"learning_rate": 8.022222222222222e-05,
|
| 447 |
+
"epoch": 0.64,
|
| 448 |
+
"step": 640
|
| 449 |
+
},
|
| 450 |
+
{
|
| 451 |
+
"loss": 0.012946407496929168,
|
| 452 |
+
"grad_norm": 0.022329581901431084,
|
| 453 |
+
"learning_rate": 7.800000000000001e-05,
|
| 454 |
+
"epoch": 0.65,
|
| 455 |
+
"step": 650
|
| 456 |
+
},
|
| 457 |
+
{
|
| 458 |
+
"loss": 0.0086174339056015,
|
| 459 |
+
"grad_norm": 0.011772534810006618,
|
| 460 |
+
"learning_rate": 7.577777777777779e-05,
|
| 461 |
+
"epoch": 0.66,
|
| 462 |
+
"step": 660
|
| 463 |
+
},
|
| 464 |
+
{
|
| 465 |
+
"loss": 0.009604217112064361,
|
| 466 |
+
"grad_norm": 0.009181632660329342,
|
| 467 |
+
"learning_rate": 7.355555555555556e-05,
|
| 468 |
+
"epoch": 0.67,
|
| 469 |
+
"step": 670
|
| 470 |
+
},
|
| 471 |
+
{
|
| 472 |
+
"loss": 0.00848425179719925,
|
| 473 |
+
"grad_norm": 0.0981854572892189,
|
| 474 |
+
"learning_rate": 7.133333333333334e-05,
|
| 475 |
+
"epoch": 0.68,
|
| 476 |
+
"step": 680
|
| 477 |
+
},
|
| 478 |
+
{
|
| 479 |
+
"loss": 0.009772248566150665,
|
| 480 |
+
"grad_norm": 0.08253007382154465,
|
| 481 |
+
"learning_rate": 6.911111111111111e-05,
|
| 482 |
+
"epoch": 0.69,
|
| 483 |
+
"step": 690
|
| 484 |
+
},
|
| 485 |
+
{
|
| 486 |
+
"loss": 0.013774000108242035,
|
| 487 |
+
"grad_norm": 0.033140555024147034,
|
| 488 |
+
"learning_rate": 6.688888888888889e-05,
|
| 489 |
+
"epoch": 0.7,
|
| 490 |
+
"step": 700
|
| 491 |
+
},
|
| 492 |
+
{
|
| 493 |
+
"loss": 0.009199360758066178,
|
| 494 |
+
"grad_norm": 0.05315607041120529,
|
| 495 |
+
"learning_rate": 6.466666666666666e-05,
|
| 496 |
+
"epoch": 0.71,
|
| 497 |
+
"step": 710
|
| 498 |
+
},
|
| 499 |
+
{
|
| 500 |
+
"loss": 0.008602166920900345,
|
| 501 |
+
"grad_norm": 0.04834599792957306,
|
| 502 |
+
"learning_rate": 6.244444444444445e-05,
|
| 503 |
+
"epoch": 0.72,
|
| 504 |
+
"step": 720
|
| 505 |
+
},
|
| 506 |
+
{
|
| 507 |
+
"loss": 0.010712940990924836,
|
| 508 |
+
"grad_norm": 0.029719484969973564,
|
| 509 |
+
"learning_rate": 6.0222222222222225e-05,
|
| 510 |
+
"epoch": 0.73,
|
| 511 |
+
"step": 730
|
| 512 |
+
},
|
| 513 |
+
{
|
| 514 |
+
"loss": 0.012493643164634704,
|
| 515 |
+
"grad_norm": 0.11286692321300507,
|
| 516 |
+
"learning_rate": 5.8e-05,
|
| 517 |
+
"epoch": 0.74,
|
| 518 |
+
"step": 740
|
| 519 |
+
},
|
| 520 |
+
{
|
| 521 |
+
"loss": 0.013842716813087463,
|
| 522 |
+
"grad_norm": 0.08935993164777756,
|
| 523 |
+
"learning_rate": 5.577777777777778e-05,
|
| 524 |
+
"epoch": 0.75,
|
| 525 |
+
"step": 750
|
| 526 |
+
},
|
| 527 |
+
{
|
| 528 |
+
"loss": 0.017634442448616026,
|
| 529 |
+
"grad_norm": 0.08994075655937195,
|
| 530 |
+
"learning_rate": 5.355555555555556e-05,
|
| 531 |
+
"epoch": 0.76,
|
| 532 |
+
"step": 760
|
| 533 |
+
},
|
| 534 |
+
{
|
| 535 |
+
"loss": 0.009391002357006073,
|
| 536 |
+
"grad_norm": 0.044728901237249374,
|
| 537 |
+
"learning_rate": 5.133333333333333e-05,
|
| 538 |
+
"epoch": 0.77,
|
| 539 |
+
"step": 770
|
| 540 |
+
},
|
| 541 |
+
{
|
| 542 |
+
"loss": 0.011917909979820252,
|
| 543 |
+
"grad_norm": 0.04021375998854637,
|
| 544 |
+
"learning_rate": 4.9111111111111114e-05,
|
| 545 |
+
"epoch": 0.78,
|
| 546 |
+
"step": 780
|
| 547 |
+
},
|
| 548 |
+
{
|
| 549 |
+
"loss": 0.011780706793069839,
|
| 550 |
+
"grad_norm": 0.09859102964401245,
|
| 551 |
+
"learning_rate": 4.6888888888888895e-05,
|
| 552 |
+
"epoch": 0.79,
|
| 553 |
+
"step": 790
|
| 554 |
+
},
|
| 555 |
+
{
|
| 556 |
+
"loss": 0.0067037887871265415,
|
| 557 |
+
"grad_norm": 0.02168435789644718,
|
| 558 |
+
"learning_rate": 4.466666666666667e-05,
|
| 559 |
+
"epoch": 0.8,
|
| 560 |
+
"step": 800
|
| 561 |
+
},
|
| 562 |
+
{
|
| 563 |
+
"loss": 0.010100596398115159,
|
| 564 |
+
"grad_norm": 0.03414692357182503,
|
| 565 |
+
"learning_rate": 4.2444444444444445e-05,
|
| 566 |
+
"epoch": 0.81,
|
| 567 |
+
"step": 810
|
| 568 |
+
},
|
| 569 |
+
{
|
| 570 |
+
"loss": 0.011301630735397339,
|
| 571 |
+
"grad_norm": 0.11121732741594315,
|
| 572 |
+
"learning_rate": 4.022222222222222e-05,
|
| 573 |
+
"epoch": 0.82,
|
| 574 |
+
"step": 820
|
| 575 |
+
},
|
| 576 |
+
{
|
| 577 |
+
"loss": 0.014357231557369232,
|
| 578 |
+
"grad_norm": 0.05178866162896156,
|
| 579 |
+
"learning_rate": 3.8e-05,
|
| 580 |
+
"epoch": 0.83,
|
| 581 |
+
"step": 830
|
| 582 |
+
},
|
| 583 |
+
{
|
| 584 |
+
"loss": 0.005482625961303711,
|
| 585 |
+
"grad_norm": 0.025843730196356773,
|
| 586 |
+
"learning_rate": 3.577777777777778e-05,
|
| 587 |
+
"epoch": 0.84,
|
| 588 |
+
"step": 840
|
| 589 |
+
},
|
| 590 |
+
{
|
| 591 |
+
"loss": 0.008758357167243958,
|
| 592 |
+
"grad_norm": 0.01811465062201023,
|
| 593 |
+
"learning_rate": 3.355555555555556e-05,
|
| 594 |
+
"epoch": 0.85,
|
| 595 |
+
"step": 850
|
| 596 |
+
},
|
| 597 |
+
{
|
| 598 |
+
"loss": 0.013496673107147217,
|
| 599 |
+
"grad_norm": 0.07277406752109528,
|
| 600 |
+
"learning_rate": 3.1333333333333334e-05,
|
| 601 |
+
"epoch": 0.86,
|
| 602 |
+
"step": 860
|
| 603 |
+
},
|
| 604 |
+
{
|
| 605 |
+
"loss": 0.008429653942584991,
|
| 606 |
+
"grad_norm": 0.00885010790079832,
|
| 607 |
+
"learning_rate": 2.9111111111111112e-05,
|
| 608 |
+
"epoch": 0.87,
|
| 609 |
+
"step": 870
|
| 610 |
+
},
|
| 611 |
+
{
|
| 612 |
+
"loss": 0.007160598784685135,
|
| 613 |
+
"grad_norm": 0.06621793657541275,
|
| 614 |
+
"learning_rate": 2.688888888888889e-05,
|
| 615 |
+
"epoch": 0.88,
|
| 616 |
+
"step": 880
|
| 617 |
+
},
|
| 618 |
+
{
|
| 619 |
+
"loss": 0.009554611891508103,
|
| 620 |
+
"grad_norm": 0.03552632033824921,
|
| 621 |
+
"learning_rate": 2.466666666666667e-05,
|
| 622 |
+
"epoch": 0.89,
|
| 623 |
+
"step": 890
|
| 624 |
+
},
|
| 625 |
+
{
|
| 626 |
+
"loss": 0.012089543044567108,
|
| 627 |
+
"grad_norm": 0.1806514412164688,
|
| 628 |
+
"learning_rate": 2.2444444444444447e-05,
|
| 629 |
+
"epoch": 0.9,
|
| 630 |
+
"step": 900
|
| 631 |
+
},
|
| 632 |
+
{
|
| 633 |
+
"loss": 0.011534038186073303,
|
| 634 |
+
"grad_norm": 0.03898712247610092,
|
| 635 |
+
"learning_rate": 2.0222222222222222e-05,
|
| 636 |
+
"epoch": 0.91,
|
| 637 |
+
"step": 910
|
| 638 |
+
},
|
| 639 |
+
{
|
| 640 |
+
"loss": 0.011874783039093017,
|
| 641 |
+
"grad_norm": 0.158588245511055,
|
| 642 |
+
"learning_rate": 1.8e-05,
|
| 643 |
+
"epoch": 0.92,
|
| 644 |
+
"step": 920
|
| 645 |
+
},
|
| 646 |
+
{
|
| 647 |
+
"loss": 0.007286681234836579,
|
| 648 |
+
"grad_norm": 0.0023962175473570824,
|
| 649 |
+
"learning_rate": 1.577777777777778e-05,
|
| 650 |
+
"epoch": 0.93,
|
| 651 |
+
"step": 930
|
| 652 |
+
},
|
| 653 |
+
{
|
| 654 |
+
"loss": 0.011251521110534669,
|
| 655 |
+
"grad_norm": 0.11141131073236465,
|
| 656 |
+
"learning_rate": 1.3555555555555557e-05,
|
| 657 |
+
"epoch": 0.94,
|
| 658 |
+
"step": 940
|
| 659 |
+
},
|
| 660 |
+
{
|
| 661 |
+
"loss": 0.013448776304721832,
|
| 662 |
+
"grad_norm": 0.15390437841415405,
|
| 663 |
+
"learning_rate": 1.1333333333333334e-05,
|
| 664 |
+
"epoch": 0.95,
|
| 665 |
+
"step": 950
|
| 666 |
+
},
|
| 667 |
+
{
|
| 668 |
+
"loss": 0.016387876868247987,
|
| 669 |
+
"grad_norm": 0.09003526717424393,
|
| 670 |
+
"learning_rate": 9.111111111111112e-06,
|
| 671 |
+
"epoch": 0.96,
|
| 672 |
+
"step": 960
|
| 673 |
+
},
|
| 674 |
+
{
|
| 675 |
+
"loss": 0.008244573324918746,
|
| 676 |
+
"grad_norm": 0.019054576754570007,
|
| 677 |
+
"learning_rate": 6.888888888888889e-06,
|
| 678 |
+
"epoch": 0.97,
|
| 679 |
+
"step": 970
|
| 680 |
+
},
|
| 681 |
+
{
|
| 682 |
+
"loss": 0.006973282247781753,
|
| 683 |
+
"grad_norm": 0.1270657479763031,
|
| 684 |
+
"learning_rate": 4.666666666666667e-06,
|
| 685 |
+
"epoch": 0.98,
|
| 686 |
+
"step": 980
|
| 687 |
+
},
|
| 688 |
+
{
|
| 689 |
+
"loss": 0.007818463444709777,
|
| 690 |
+
"grad_norm": 0.1933678835630417,
|
| 691 |
+
"learning_rate": 2.4444444444444447e-06,
|
| 692 |
+
"epoch": 0.99,
|
| 693 |
+
"step": 990
|
| 694 |
+
},
|
| 695 |
+
{
|
| 696 |
+
"loss": 0.011809046566486358,
|
| 697 |
+
"grad_norm": 0.025083091109991074,
|
| 698 |
+
"learning_rate": 2.2222222222222224e-07,
|
| 699 |
+
"epoch": 1.0,
|
| 700 |
+
"step": 1000
|
| 701 |
+
},
|
| 702 |
+
{
|
| 703 |
+
"train_runtime": 5989.7828,
|
| 704 |
+
"train_samples_per_second": 2.671,
|
| 705 |
+
"train_steps_per_second": 0.167,
|
| 706 |
+
"total_flos": 2.065728538444923e+17,
|
| 707 |
+
"train_loss": 0.03656231210380793,
|
| 708 |
+
"epoch": 1.0,
|
| 709 |
+
"step": 1000
|
| 710 |
+
}
|
| 711 |
+
]
|
outputs/sft_loss.png
ADDED
|
outputs/sft_loss_v6.png
ADDED
|
outputs/v6_full_logs.txt
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
debconf: delaying package configuration, since apt-utils is not installed
|
| 2 |
+
Selecting previously unselected package less.
|
| 3 |
+
(Reading database ...
|
| 4 |
+
Preparing to unpack .../00-less_590-1ubuntu0.22.04.3_amd64.deb ...
|
| 5 |
+
Unpacking less (590-1ubuntu0.22.04.3) ...
|
| 6 |
+
Selecting previously unselected package libmd0:amd64.
|
| 7 |
+
Preparing to unpack .../01-libmd0_1.0.4-1build1_amd64.deb ...
|
| 8 |
+
Unpacking libmd0:amd64 (1.0.4-1build1) ...
|
| 9 |
+
Selecting previously unselected package libbsd0:amd64.
|
| 10 |
+
Preparing to unpack .../02-libbsd0_0.11.5-1_amd64.deb ...
|
| 11 |
+
Unpacking libbsd0:amd64 (0.11.5-1) ...
|
| 12 |
+
Selecting previously unselected package libexpat1:amd64.
|
| 13 |
+
Preparing to unpack .../03-libexpat1_2.4.7-1ubuntu0.7_amd64.deb ...
|
| 14 |
+
Unpacking libexpat1:amd64 (2.4.7-1ubuntu0.7) ...
|
| 15 |
+
Selecting previously unselected package libcbor0.8:amd64.
|
| 16 |
+
Preparing to unpack .../04-libcbor0.8_0.8.0-2ubuntu1_amd64.deb ...
|
| 17 |
+
Unpacking libcbor0.8:amd64 (0.8.0-2ubuntu1) ...
|
| 18 |
+
Selecting previously unselected package libedit2:amd64.
|
| 19 |
+
Preparing to unpack .../05-libedit2_3.1-20210910-1build1_amd64.deb ...
|
| 20 |
+
Unpacking libedit2:amd64 (3.1-20210910-1build1) ...
|
| 21 |
+
Selecting previously unselected package libfido2-1:amd64.
|
| 22 |
+
Preparing to unpack .../06-libfido2-1_1.10.0-1_amd64.deb ...
|
| 23 |
+
Unpacking libfido2-1:amd64 (1.10.0-1) ...
|
| 24 |
+
Selecting previously unselected package libnghttp2-14:amd64.
|
| 25 |
+
Preparing to unpack .../07-libnghttp2-14_1.43.0-1ubuntu0.2_amd64.deb ...
|
| 26 |
+
Unpacking libnghttp2-14:amd64 (1.43.0-1ubuntu0.2) ...
|
| 27 |
+
Selecting previously unselected package libpsl5:amd64.
|
| 28 |
+
Preparing to unpack .../08-libpsl5_0.21.0-1.2build2_amd64.deb ...
|
| 29 |
+
Unpacking libpsl5:amd64 (0.21.0-1.2build2) ...
|
| 30 |
+
Selecting previously unselected package libxau6:amd64.
|
| 31 |
+
Preparing to unpack .../09-libxau6_1%3a1.0.9-1build5_amd64.deb ...
|
| 32 |
+
Unpacking libxau6:amd64 (1:1.0.9-1build5) ...
|
| 33 |
+
Selecting previously unselected package libxdmcp6:amd64.
|
| 34 |
+
Preparing to unpack .../10-libxdmcp6_1%3a1.1.3-0ubuntu5_amd64.deb ...
|
| 35 |
+
Unpacking libxdmcp6:amd64 (1:1.1.3-0ubuntu5) ...
|
| 36 |
+
Selecting previously unselected package libxcb1:amd64.
|
| 37 |
+
Preparing to unpack .../11-libxcb1_1.14-3ubuntu3_amd64.deb ...
|
| 38 |
+
Unpacking libxcb1:amd64 (1.14-3ubuntu3) ...
|
| 39 |
+
Selecting previously unselected package libx11-data.
|
| 40 |
+
Preparing to unpack .../12-libx11-data_2%3a1.7.5-1ubuntu0.3_all.deb ...
|
| 41 |
+
Unpacking libx11-data (2:1.7.5-1ubuntu0.3) ...
|
| 42 |
+
Selecting previously unselected package libx11-6:amd64.
|
| 43 |
+
Preparing to unpack .../13-libx11-6_2%3a1.7.5-1ubuntu0.3_amd64.deb ...
|
| 44 |
+
Unpacking libx11-6:amd64 (2:1.7.5-1ubuntu0.3) ...
|
| 45 |
+
Selecting previously unselected package libxext6:amd64.
|
| 46 |
+
Preparing to unpack .../14-libxext6_2%3a1.3.4-1build1_amd64.deb ...
|
| 47 |
+
Unpacking libxext6:amd64 (2:1.3.4-1build1) ...
|
| 48 |
+
Selecting previously unselected package libxmuu1:amd64.
|
| 49 |
+
Preparing to unpack .../15-libxmuu1_2%3a1.1.3-3_amd64.deb ...
|
| 50 |
+
Unpacking libxmuu1:amd64 (2:1.1.3-3) ...
|
| 51 |
+
Selecting previously unselected package openssh-client.
|
| 52 |
+
Preparing to unpack .../16-openssh-client_1%3a8.9p1-3ubuntu0.14_amd64.deb ...
|
| 53 |
+
Unpacking openssh-client (1:8.9p1-3ubuntu0.14) ...
|
| 54 |
+
Selecting previously unselected package publicsuffix.
|
| 55 |
+
Preparing to unpack .../17-publicsuffix_20211207.1025-1_all.deb ...
|
| 56 |
+
Unpacking publicsuffix (20211207.1025-1) ...
|
| 57 |
+
Selecting previously unselected package xauth.
|
| 58 |
+
Preparing to unpack .../18-xauth_1%3a1.1-1build2_amd64.deb ...
|
| 59 |
+
Unpacking xauth (1:1.1-1build2) ...
|
| 60 |
+
Selecting previously unselected package libbrotli1:amd64.
|
| 61 |
+
Preparing to unpack .../19-libbrotli1_1.0.9-2build6_amd64.deb ...
|
| 62 |
+
Unpacking libbrotli1:amd64 (1.0.9-2build6) ...
|
| 63 |
+
Selecting previously unselected package librtmp1:amd64.
|
| 64 |
+
Preparing to unpack .../20-librtmp1_2.4+20151223.gitfa8646d.1-2build4_amd64.deb ...
|
| 65 |
+
Unpacking librtmp1:amd64 (2.4+20151223.gitfa8646d.1-2build4) ...
|
| 66 |
+
Selecting previously unselected package libssh-4:amd64.
|
| 67 |
+
Preparing to unpack .../21-libssh-4_0.9.6-2ubuntu0.22.04.7_amd64.deb ...
|
| 68 |
+
Unpacking libssh-4:amd64 (0.9.6-2ubuntu0.22.04.7) ...
|
| 69 |
+
Selecting previously unselected package libcurl3-gnutls:amd64.
|
| 70 |
+
Preparing to unpack .../22-libcurl3-gnutls_7.81.0-1ubuntu1.23_amd64.deb ...
|
| 71 |
+
Unpacking libcurl3-gnutls:amd64 (7.81.0-1ubuntu1.23) ...
|
| 72 |
+
Selecting previously unselected package liberror-perl.
|
| 73 |
+
Preparing to unpack .../23-liberror-perl_0.17029-1_all.deb ...
|
| 74 |
+
Unpacking liberror-perl (0.17029-1) ...
|
| 75 |
+
Selecting previously unselected package git-man.
|
| 76 |
+
Preparing to unpack .../24-git-man_1%3a2.34.1-1ubuntu1.17_all.deb ...
|
| 77 |
+
Unpacking git-man (1:2.34.1-1ubuntu1.17) ...
|
| 78 |
+
Selecting previously unselected package git.
|
| 79 |
+
Preparing to unpack .../25-git_1%3a2.34.1-1ubuntu1.17_amd64.deb ...
|
| 80 |
+
Unpacking git (1:2.34.1-1ubuntu1.17) ...
|
| 81 |
+
Setting up libexpat1:amd64 (2.4.7-1ubuntu0.7) ...
|
| 82 |
+
Setting up libxau6:amd64 (1:1.0.9-1build5) ...
|
| 83 |
+
Setting up libpsl5:amd64 (0.21.0-1.2build2) ...
|
| 84 |
+
Setting up libcbor0.8:amd64 (0.8.0-2ubuntu1) ...
|
| 85 |
+
Setting up libbrotli1:amd64 (1.0.9-2build6) ...
|
| 86 |
+
Setting up libnghttp2-14:amd64 (1.43.0-1ubuntu0.2) ...
|
| 87 |
+
Setting up less (590-1ubuntu0.22.04.3) ...
|
| 88 |
+
Setting up liberror-perl (0.17029-1) ...
|
| 89 |
+
Setting up libx11-data (2:1.7.5-1ubuntu0.3) ...
|
| 90 |
+
Setting up librtmp1:amd64 (2.4+20151223.gitfa8646d.1-2build4) ...
|
| 91 |
+
Setting up libssh-4:amd64 (0.9.6-2ubuntu0.22.04.7) ...
|
| 92 |
+
Setting up libmd0:amd64 (1.0.4-1build1) ...
|
| 93 |
+
Setting up git-man (1:2.34.1-1ubuntu1.17) ...
|
| 94 |
+
Setting up libfido2-1:amd64 (1.10.0-1) ...
|
| 95 |
+
Setting up libbsd0:amd64 (0.11.5-1) ...
|
| 96 |
+
Setting up publicsuffix (20211207.1025-1) ...
|
| 97 |
+
Setting up libxdmcp6:amd64 (1:1.1.3-0ubuntu5) ...
|
| 98 |
+
Setting up libxcb1:amd64 (1.14-3ubuntu3) ...
|
| 99 |
+
Setting up libedit2:amd64 (3.1-20210910-1build1) ...
|
| 100 |
+
Setting up libcurl3-gnutls:amd64 (7.81.0-1ubuntu1.23) ...
|
| 101 |
+
Setting up git (1:2.34.1-1ubuntu1.17) ...
|
| 102 |
+
Setting up libx11-6:amd64 (2:1.7.5-1ubuntu0.3) ...
|
| 103 |
+
Setting up libxmuu1:amd64 (2:1.1.3-3) ...
|
| 104 |
+
Setting up openssh-client (1:8.9p1-3ubuntu0.14) ...
|
| 105 |
+
update-alternatives: using /usr/bin/ssh to provide /usr/bin/rsh (rsh) in auto mode
|
| 106 |
+
update-alternatives: warning: skip creation of /usr/share/man/man1/rsh.1.gz because associated file /usr/share/man/man1/ssh.1.gz (of link group rsh) doesn't exist
|
| 107 |
+
update-alternatives: using /usr/bin/slogin to provide /usr/bin/rlogin (rlogin) in auto mode
|
| 108 |
+
update-alternatives: warning: skip creation of /usr/share/man/man1/rlogin.1.gz because associated file /usr/share/man/man1/slogin.1.gz (of link group rlogin) doesn't exist
|
| 109 |
+
update-alternatives: using /usr/bin/scp to provide /usr/bin/rcp (rcp) in auto mode
|
| 110 |
+
update-alternatives: warning: skip creation of /usr/share/man/man1/rcp.1.gz because associated file /usr/share/man/man1/scp.1.gz (of link group rcp) doesn't exist
|
| 111 |
+
Setting up libxext6:amd64 (2:1.3.4-1build1) ...
|
| 112 |
+
Setting up xauth (1:1.1-1build2) ...
|
| 113 |
+
Processing triggers for libc-bin (2.35-0ubuntu3.8) ...
|
| 114 |
+
Cloning into '/workspace/OpenEnv'...
|
| 115 |
+
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning.
|
| 116 |
+
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning.
|
| 117 |
+
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning.
|
| 118 |
+
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning.
|
| 119 |
+
QUICK_MODE=False MODEL=unsloth/Qwen2.5-3B-Instruct
|
| 120 |
+
GPU: NVIDIA A100-SXM4-80GB capability=(8, 0) bf16=True fp16=False
|
| 121 |
+
Train: {'n_rows': 16000, 'admin_rows': 1600, 'customer_rows': 14400, 'drift_sensitive_rows': 2240, 'bonus_eligible_rows': 1153, 'kinds_distribution': {'chitchat': 1467, 'refund': 6676, 'billing_q': 1439, 'admin': 1600, 'info_request': 1482, 'critical_incident': 3336}}
|
| 122 |
+
Eval: {'n_rows': 800, 'admin_rows': 80, 'customer_rows': 720, 'drift_sensitive_rows': 124, 'bonus_eligible_rows': 62, 'kinds_distribution': {'refund': 347, 'critical_incident': 168, 'admin': 80, 'billing_q': 88, 'chitchat': 62, 'info_request': 55}}
|
| 123 |
+
🦥 Unsloth: Will patch your computer to enable 2x faster free finetuning.
|
| 124 |
+
Unsloth: Your Flash Attention 2 installation seems to be broken. Using Xformers instead. No performance changes will be seen.
|
| 125 |
+
🦥 Unsloth Zoo will now patch everything to make training faster!
|
| 126 |
+
==((====))== Unsloth 2026.4.8: Fast Qwen2 patching. Transformers: 5.5.0.
|
| 127 |
+
\\ /| NVIDIA A100-SXM4-80GB. Num GPUs = 1. Max memory: 79.25 GB. Platform: Linux.
|
| 128 |
+
O^O/ \_/ \ Torch: 2.8.0+cu128. CUDA: 8.0. CUDA Toolkit: 12.8. Triton: 3.4.0
|
| 129 |
+
\ / Bfloat16 = TRUE. FA [Xformers = None. FA2 = False]
|
| 130 |
+
"-____-" Free license: http://github.com/unslothai/unsloth
|
| 131 |
+
Unsloth: Fast downloading is enabled - ignore downloading bars which are red colored!
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
unsloth/qwen2.5-3b-instruct-unsloth-bnb-4bit does not have a padding token! Will use pad_token = <|PAD_TOKEN|>.
|
| 154 |
+
|
| 155 |
+
=== Offline eval [pre-training] over 200 samples ===
|
| 156 |
+
compliance avg : 0.528 / 1.0
|
| 157 |
+
appropriateness avg: 0.410 / 0.5
|
| 158 |
+
drift_bonus avg : 0.005 / 0.5
|
| 159 |
+
total avg : 0.943 / 2.0
|
| 160 |
+
drift-sens acc : 11.8% (2/17)
|
| 161 |
+
tightening : 0.0% (0/23)
|
| 162 |
+
loosening : 21.4% (3/14)
|
| 163 |
+
neutral : n/a (0/0)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
🦥 Unsloth: Padding-free auto-enabled, enabling faster training.
|
| 188 |
+
|
| 189 |
+
=== SFT warm-up: 16000 samples, 1 epoch(s) ===
|
| 190 |
+
Unsloth: Will smartly offload gradients to save VRAM!
|
| 191 |
+
{'loss': '1.293', 'grad_norm': '2.695', 'learning_rate': '1.8e-05', 'epoch': '0.01'}
|
| 192 |
+
{'loss': '0.9491', 'grad_norm': '4.517', 'learning_rate': '3.8e-05', 'epoch': '0.02'}
|
| 193 |
+
{'loss': '0.132', 'grad_norm': '0.3303', 'learning_rate': '5.8e-05', 'epoch': '0.03'}
|
| 194 |
+
{'loss': '0.07011', 'grad_norm': '0.2859', 'learning_rate': '7.8e-05', 'epoch': '0.04'}
|
| 195 |
+
{'loss': '0.02983', 'grad_norm': '0.196', 'learning_rate': '9.8e-05', 'epoch': '0.05'}
|
| 196 |
+
{'loss': '0.03054', 'grad_norm': '0.362', 'learning_rate': '0.000118', 'epoch': '0.06'}
|
| 197 |
+
{'loss': '0.02064', 'grad_norm': '0.1444', 'learning_rate': '0.000138', 'epoch': '0.07'}
|
| 198 |
+
{'loss': '0.01928', 'grad_norm': '0.1991', 'learning_rate': '0.000158', 'epoch': '0.08'}
|
| 199 |
+
{'loss': '0.01925', 'grad_norm': '0.2893', 'learning_rate': '0.000178', 'epoch': '0.09'}
|
| 200 |
+
{'loss': '0.02096', 'grad_norm': '0.1415', 'learning_rate': '0.000198', 'epoch': '0.1'}
|
| 201 |
+
{'loss': '0.02395', 'grad_norm': '0.2314', 'learning_rate': '0.000198', 'epoch': '0.11'}
|
| 202 |
+
{'loss': '0.02662', 'grad_norm': '0.1206', 'learning_rate': '0.0001958', 'epoch': '0.12'}
|
| 203 |
+
{'loss': '0.02023', 'grad_norm': '0.1322', 'learning_rate': '0.0001936', 'epoch': '0.13'}
|
| 204 |
+
{'loss': '0.01497', 'grad_norm': '0.146', 'learning_rate': '0.0001913', 'epoch': '0.14'}
|
| 205 |
+
{'loss': '0.01318', 'grad_norm': '0.03542', 'learning_rate': '0.0001891', 'epoch': '0.15'}
|
| 206 |
+
{'loss': '0.01705', 'grad_norm': '0.1409', 'learning_rate': '0.0001869', 'epoch': '0.16'}
|
| 207 |
+
{'loss': '0.01366', 'grad_norm': '0.106', 'learning_rate': '0.0001847', 'epoch': '0.17'}
|
| 208 |
+
{'loss': '0.01499', 'grad_norm': '0.06284', 'learning_rate': '0.0001824', 'epoch': '0.18'}
|
| 209 |
+
{'loss': '0.01449', 'grad_norm': '0.06855', 'learning_rate': '0.0001802', 'epoch': '0.19'}
|
| 210 |
+
{'loss': '0.01527', 'grad_norm': '0.09317', 'learning_rate': '0.000178', 'epoch': '0.2'}
|
| 211 |
+
{'loss': '0.02113', 'grad_norm': '0.1043', 'learning_rate': '0.0001758', 'epoch': '0.21'}
|
| 212 |
+
{'loss': '0.0138', 'grad_norm': '0.07432', 'learning_rate': '0.0001736', 'epoch': '0.22'}
|
| 213 |
+
{'loss': '0.01321', 'grad_norm': '0.03357', 'learning_rate': '0.0001713', 'epoch': '0.23'}
|
| 214 |
+
{'loss': '0.01104', 'grad_norm': '0.02183', 'learning_rate': '0.0001691', 'epoch': '0.24'}
|
| 215 |
+
{'loss': '0.01341', 'grad_norm': '0.0815', 'learning_rate': '0.0001669', 'epoch': '0.25'}
|
| 216 |
+
{'loss': '0.01698', 'grad_norm': '0.07082', 'learning_rate': '0.0001647', 'epoch': '0.26'}
|
| 217 |
+
{'loss': '0.01471', 'grad_norm': '0.07086', 'learning_rate': '0.0001624', 'epoch': '0.27'}
|
| 218 |
+
{'loss': '0.009542', 'grad_norm': '0.102', 'learning_rate': '0.0001602', 'epoch': '0.28'}
|
| 219 |
+
{'loss': '0.01128', 'grad_norm': '0.1207', 'learning_rate': '0.000158', 'epoch': '0.29'}
|
| 220 |
+
{'loss': '0.009179', 'grad_norm': '0.05602', 'learning_rate': '0.0001558', 'epoch': '0.3'}
|
| 221 |
+
{'loss': '0.01002', 'grad_norm': '0.02763', 'learning_rate': '0.0001536', 'epoch': '0.31'}
|
| 222 |
+
{'loss': '0.01002', 'grad_norm': '0.04349', 'learning_rate': '0.0001513', 'epoch': '0.32'}
|
| 223 |
+
{'loss': '0.01587', 'grad_norm': '0.06167', 'learning_rate': '0.0001491', 'epoch': '0.33'}
|
| 224 |
+
{'loss': '0.009908', 'grad_norm': '0.04353', 'learning_rate': '0.0001469', 'epoch': '0.34'}
|
| 225 |
+
{'loss': '0.01125', 'grad_norm': '0.03281', 'learning_rate': '0.0001447', 'epoch': '0.35'}
|
| 226 |
+
{'loss': '0.01353', 'grad_norm': '0.04305', 'learning_rate': '0.0001424', 'epoch': '0.36'}
|
| 227 |
+
{'loss': '0.01252', 'grad_norm': '0.04699', 'learning_rate': '0.0001402', 'epoch': '0.37'}
|
| 228 |
+
{'loss': '0.01027', 'grad_norm': '0.1363', 'learning_rate': '0.000138', 'epoch': '0.38'}
|
| 229 |
+
{'loss': '0.01035', 'grad_norm': '0.0611', 'learning_rate': '0.0001358', 'epoch': '0.39'}
|
| 230 |
+
{'loss': '0.0167', 'grad_norm': '0.04438', 'learning_rate': '0.0001336', 'epoch': '0.4'}
|
| 231 |
+
{'loss': '0.01067', 'grad_norm': '0.04687', 'learning_rate': '0.0001313', 'epoch': '0.41'}
|
| 232 |
+
{'loss': '0.01134', 'grad_norm': '0.0377', 'learning_rate': '0.0001291', 'epoch': '0.42'}
|
| 233 |
+
{'loss': '0.01073', 'grad_norm': '0.03925', 'learning_rate': '0.0001269', 'epoch': '0.43'}
|
| 234 |
+
{'loss': '0.01296', 'grad_norm': '0.0747', 'learning_rate': '0.0001247', 'epoch': '0.44'}
|
| 235 |
+
{'loss': '0.01258', 'grad_norm': '0.01743', 'learning_rate': '0.0001224', 'epoch': '0.45'}
|
| 236 |
+
{'loss': '0.01015', 'grad_norm': '0.04182', 'learning_rate': '0.0001202', 'epoch': '0.46'}
|
| 237 |
+
{'loss': '0.01294', 'grad_norm': '0.1417', 'learning_rate': '0.000118', 'epoch': '0.47'}
|
| 238 |
+
{'loss': '0.01141', 'grad_norm': '0.04199', 'learning_rate': '0.0001158', 'epoch': '0.48'}
|
| 239 |
+
{'loss': '0.01143', 'grad_norm': '0.01166', 'learning_rate': '0.0001136', 'epoch': '0.49'}
|
| 240 |
+
{'loss': '0.01409', 'grad_norm': '0.03272', 'learning_rate': '0.0001113', 'epoch': '0.5'}
|
| 241 |
+
{'loss': '0.0116', 'grad_norm': '0.0427', 'learning_rate': '0.0001091', 'epoch': '0.51'}
|
| 242 |
+
{'loss': '0.007553', 'grad_norm': '0.05289', 'learning_rate': '0.0001069', 'epoch': '0.52'}
|
| 243 |
+
{'loss': '0.007692', 'grad_norm': '0.06659', 'learning_rate': '0.0001047', 'epoch': '0.53'}
|
| 244 |
+
{'loss': '0.01459', 'grad_norm': '0.1073', 'learning_rate': '0.0001024', 'epoch': '0.54'}
|
| 245 |
+
{'loss': '0.01389', 'grad_norm': '0.1297', 'learning_rate': '0.0001002', 'epoch': '0.55'}
|
| 246 |
+
{'loss': '0.01091', 'grad_norm': '0.04869', 'learning_rate': '9.8e-05', 'epoch': '0.56'}
|
| 247 |
+
{'loss': '0.01059', 'grad_norm': '0.06955', 'learning_rate': '9.578e-05', 'epoch': '0.57'}
|
| 248 |
+
{'loss': '0.00643', 'grad_norm': '0.03395', 'learning_rate': '9.356e-05', 'epoch': '0.58'}
|
| 249 |
+
{'loss': '0.009756', 'grad_norm': '0.1062', 'learning_rate': '9.133e-05', 'epoch': '0.59'}
|
| 250 |
+
{'loss': '0.009229', 'grad_norm': '0.03023', 'learning_rate': '8.911e-05', 'epoch': '0.6'}
|
| 251 |
+
{'loss': '0.01173', 'grad_norm': '0.03197', 'learning_rate': '8.689e-05', 'epoch': '0.61'}
|
| 252 |
+
{'loss': '0.008542', 'grad_norm': '0.04632', 'learning_rate': '8.467e-05', 'epoch': '0.62'}
|
| 253 |
+
{'loss': '0.006862', 'grad_norm': '0.01375', 'learning_rate': '8.244e-05', 'epoch': '0.63'}
|
| 254 |
+
{'loss': '0.01155', 'grad_norm': '0.01589', 'learning_rate': '8.022e-05', 'epoch': '0.64'}
|
| 255 |
+
{'loss': '0.01295', 'grad_norm': '0.02233', 'learning_rate': '7.8e-05', 'epoch': '0.65'}
|
| 256 |
+
{'loss': '0.008617', 'grad_norm': '0.01177', 'learning_rate': '7.578e-05', 'epoch': '0.66'}
|
| 257 |
+
{'loss': '0.009604', 'grad_norm': '0.009182', 'learning_rate': '7.356e-05', 'epoch': '0.67'}
|
| 258 |
+
{'loss': '0.008484', 'grad_norm': '0.09819', 'learning_rate': '7.133e-05', 'epoch': '0.68'}
|
| 259 |
+
{'loss': '0.009772', 'grad_norm': '0.08253', 'learning_rate': '6.911e-05', 'epoch': '0.69'}
|
| 260 |
+
{'loss': '0.01377', 'grad_norm': '0.03314', 'learning_rate': '6.689e-05', 'epoch': '0.7'}
|
| 261 |
+
{'loss': '0.009199', 'grad_norm': '0.05316', 'learning_rate': '6.467e-05', 'epoch': '0.71'}
|
| 262 |
+
{'loss': '0.008602', 'grad_norm': '0.04835', 'learning_rate': '6.244e-05', 'epoch': '0.72'}
|
| 263 |
+
{'loss': '0.01071', 'grad_norm': '0.02972', 'learning_rate': '6.022e-05', 'epoch': '0.73'}
|
| 264 |
+
{'loss': '0.01249', 'grad_norm': '0.1129', 'learning_rate': '5.8e-05', 'epoch': '0.74'}
|
| 265 |
+
{'loss': '0.01384', 'grad_norm': '0.08936', 'learning_rate': '5.578e-05', 'epoch': '0.75'}
|
| 266 |
+
{'loss': '0.01763', 'grad_norm': '0.08994', 'learning_rate': '5.356e-05', 'epoch': '0.76'}
|
| 267 |
+
{'loss': '0.009391', 'grad_norm': '0.04473', 'learning_rate': '5.133e-05', 'epoch': '0.77'}
|
| 268 |
+
{'loss': '0.01192', 'grad_norm': '0.04021', 'learning_rate': '4.911e-05', 'epoch': '0.78'}
|
| 269 |
+
{'loss': '0.01178', 'grad_norm': '0.09859', 'learning_rate': '4.689e-05', 'epoch': '0.79'}
|
| 270 |
+
{'loss': '0.006704', 'grad_norm': '0.02168', 'learning_rate': '4.467e-05', 'epoch': '0.8'}
|
| 271 |
+
{'loss': '0.0101', 'grad_norm': '0.03415', 'learning_rate': '4.244e-05', 'epoch': '0.81'}
|
| 272 |
+
{'loss': '0.0113', 'grad_norm': '0.1112', 'learning_rate': '4.022e-05', 'epoch': '0.82'}
|
| 273 |
+
{'loss': '0.01436', 'grad_norm': '0.05179', 'learning_rate': '3.8e-05', 'epoch': '0.83'}
|
| 274 |
+
{'loss': '0.005483', 'grad_norm': '0.02584', 'learning_rate': '3.578e-05', 'epoch': '0.84'}
|
| 275 |
+
{'loss': '0.008758', 'grad_norm': '0.01811', 'learning_rate': '3.356e-05', 'epoch': '0.85'}
|
| 276 |
+
{'loss': '0.0135', 'grad_norm': '0.07277', 'learning_rate': '3.133e-05', 'epoch': '0.86'}
|
| 277 |
+
{'loss': '0.00843', 'grad_norm': '0.00885', 'learning_rate': '2.911e-05', 'epoch': '0.87'}
|
| 278 |
+
{'loss': '0.007161', 'grad_norm': '0.06622', 'learning_rate': '2.689e-05', 'epoch': '0.88'}
|
| 279 |
+
{'loss': '0.009555', 'grad_norm': '0.03553', 'learning_rate': '2.467e-05', 'epoch': '0.89'}
|
| 280 |
+
{'loss': '0.01209', 'grad_norm': '0.1807', 'learning_rate': '2.244e-05', 'epoch': '0.9'}
|
| 281 |
+
{'loss': '0.01153', 'grad_norm': '0.03899', 'learning_rate': '2.022e-05', 'epoch': '0.91'}
|
| 282 |
+
{'loss': '0.01187', 'grad_norm': '0.1586', 'learning_rate': '1.8e-05', 'epoch': '0.92'}
|
| 283 |
+
{'loss': '0.007287', 'grad_norm': '0.002396', 'learning_rate': '1.578e-05', 'epoch': '0.93'}
|
| 284 |
+
{'loss': '0.01125', 'grad_norm': '0.1114', 'learning_rate': '1.356e-05', 'epoch': '0.94'}
|
| 285 |
+
{'loss': '0.01345', 'grad_norm': '0.1539', 'learning_rate': '1.133e-05', 'epoch': '0.95'}
|
| 286 |
+
{'loss': '0.01639', 'grad_norm': '0.09004', 'learning_rate': '9.111e-06', 'epoch': '0.96'}
|
| 287 |
+
{'loss': '0.008245', 'grad_norm': '0.01905', 'learning_rate': '6.889e-06', 'epoch': '0.97'}
|
| 288 |
+
{'loss': '0.006973', 'grad_norm': '0.1271', 'learning_rate': '4.667e-06', 'epoch': '0.98'}
|
| 289 |
+
{'loss': '0.007818', 'grad_norm': '0.1934', 'learning_rate': '2.444e-06', 'epoch': '0.99'}
|
| 290 |
+
{'loss': '0.01181', 'grad_norm': '0.02508', 'learning_rate': '2.222e-07', 'epoch': '1'}
|
| 291 |
+
{'train_runtime': '5911', 'train_samples_per_second': '2.707', 'train_steps_per_second': '0.169', 'train_loss': '0.03656', 'epoch': '1'}
|
| 292 |
+
SFT log saved -> ./outputs/sft_log.json
|
| 293 |
+
|
| 294 |
+
=== Offline eval [post-SFT] over 200 samples ===
|
| 295 |
+
compliance avg : 0.968 / 1.0
|
| 296 |
+
appropriateness avg: 0.450 / 0.5
|
| 297 |
+
drift_bonus avg : 0.037 / 0.5
|
| 298 |
+
total avg : 1.455 / 2.0
|
| 299 |
+
drift-sens acc : 88.2% (15/17)
|
| 300 |
+
tightening : 91.3% (21/23)
|
| 301 |
+
loosening : 71.4% (10/14)
|
| 302 |
+
neutral : n/a (0/0)
|
| 303 |
+
Traceback (most recent call last):
|
| 304 |
+
File "/workspace/OpenEnv/train.py", line 424, in <module>
|
| 305 |
+
sys.exit(main())
|
| 306 |
+
^^^^^^
|
| 307 |
+
File "/workspace/OpenEnv/train.py", line 366, in main
|
| 308 |
+
model = run_grpo(model, tokenizer, grpo_train, grpo_eval)
|
| 309 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 310 |
+
File "/workspace/OpenEnv/train.py", line 206, in run_grpo
|
| 311 |
+
args = GRPOConfig(
|
| 312 |
+
^^^^^^^^^^^
|
| 313 |
+
File "/workspace/OpenEnv/unsloth_compiled_cache/UnslothGRPOTrainer.py", line 1736, in __init__
|
| 314 |
+
super().__init__(
|
| 315 |
+
TypeError: GRPOConfig.__init__() got an unexpected keyword argument 'dataset_num_proc'
|
outputs/v7_full_logs.txt
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 0 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
|
|
|
|
|
|
| 1 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
|
|
|
|
|
|
|
|
|
| 2 |
...adapter_model.safetensors: 31%|███ | 37.4MB / 120MB [A[A[A
|
|
|
|
|
|
|
| 3 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
|
|
|
|
|
|
|
|
|
| 4 |
...adapter_model.safetensors: 31%|███ | 37.4MB / 120MB [A[A[A
|
|
|
|
|
|
|
| 5 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
|
|
|
|
|
|
|
|
|
| 6 |
...adapter_model.safetensors: 94%|█████████▍| 112MB / 120MB [A[A[A
|
|
|
|
|
|
|
| 7 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
|
|
|
|
|
|
|
|
|
| 8 |
...adapter_model.safetensors: 99%|█████████▉| 119MB / 120MB [A[A[A
|
|
|
|
|
|
|
| 9 |
...apters_sft/tokenizer.json: 100%|███████���██| 11.4MB / 11.4MB [A[A
|
|
|
|
|
|
|
|
|
|
| 10 |
...adapter_model.safetensors: 99%|█████████▉| 119MB / 120MB [A[A[A
|
|
|
|
|
|
|
|
|
|
| 11 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
|
|
|
|
|
|
|
|
|
| 12 |
...adapter_model.safetensors: 100%|█████████▉| 119MB / 120MB [A[A[A
|
|
|
|
|
|
|
| 13 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
|
|
|
|
|
|
|
|
|
| 14 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A
|
|
|
|
|
|
|
| 15 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
|
|
|
|
|
|
|
|
|
| 16 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A
|
|
|
|
|
|
|
| 17 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
|
|
|
|
|
|
|
|
|
| 18 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A
|
|
|
|
|
|
|
| 19 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB
|
|
|
|
| 20 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
|
|
|
|
|
|
|
|
|
| 22 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A[A
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
...a_adapters/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A[A[A[A
|
|
|
|
|
|
|
| 25 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
|
|
|
|
|
|
|
|
|
| 26 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A[A
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
...a_adapters/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A[A[A[A
|
|
|
|
|
|
|
| 29 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
|
|
|
|
|
|
|
|
|
| 30 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A[A
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
...a_adapters/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A[A[A[A
|
|
|
|
|
|
|
| 33 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
|
|
|
|
|
|
|
|
|
| 34 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A[A
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
...a_adapters/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A[A[A[A
|
|
|
|
|
|
|
| 37 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
|
|
|
|
|
|
|
|
|
| 38 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A[A
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
...a_adapters/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A[A[A[A
|
|
|
|
|
|
|
| 41 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB
|
|
|
|
| 42 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB
|
|
|
|
| 43 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB
|
|
|
|
| 44 |
...a_adapters/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB
|
|
|
|
|
|
| 1 |
+
debconf: delaying package configuration, since apt-utils is not installed
|
| 2 |
+
Selecting previously unselected package less.
|
| 3 |
+
(Reading database ...
|
| 4 |
+
Preparing to unpack .../00-less_590-1ubuntu0.22.04.3_amd64.deb ...
|
| 5 |
+
Unpacking less (590-1ubuntu0.22.04.3) ...
|
| 6 |
+
Selecting previously unselected package libmd0:amd64.
|
| 7 |
+
Preparing to unpack .../01-libmd0_1.0.4-1build1_amd64.deb ...
|
| 8 |
+
Unpacking libmd0:amd64 (1.0.4-1build1) ...
|
| 9 |
+
Selecting previously unselected package libbsd0:amd64.
|
| 10 |
+
Preparing to unpack .../02-libbsd0_0.11.5-1_amd64.deb ...
|
| 11 |
+
Unpacking libbsd0:amd64 (0.11.5-1) ...
|
| 12 |
+
Selecting previously unselected package libexpat1:amd64.
|
| 13 |
+
Preparing to unpack .../03-libexpat1_2.4.7-1ubuntu0.7_amd64.deb ...
|
| 14 |
+
Unpacking libexpat1:amd64 (2.4.7-1ubuntu0.7) ...
|
| 15 |
+
Selecting previously unselected package libcbor0.8:amd64.
|
| 16 |
+
Preparing to unpack .../04-libcbor0.8_0.8.0-2ubuntu1_amd64.deb ...
|
| 17 |
+
Unpacking libcbor0.8:amd64 (0.8.0-2ubuntu1) ...
|
| 18 |
+
Selecting previously unselected package libedit2:amd64.
|
| 19 |
+
Preparing to unpack .../05-libedit2_3.1-20210910-1build1_amd64.deb ...
|
| 20 |
+
Unpacking libedit2:amd64 (3.1-20210910-1build1) ...
|
| 21 |
+
Selecting previously unselected package libfido2-1:amd64.
|
| 22 |
+
Preparing to unpack .../06-libfido2-1_1.10.0-1_amd64.deb ...
|
| 23 |
+
Unpacking libfido2-1:amd64 (1.10.0-1) ...
|
| 24 |
+
Selecting previously unselected package libnghttp2-14:amd64.
|
| 25 |
+
Preparing to unpack .../07-libnghttp2-14_1.43.0-1ubuntu0.2_amd64.deb ...
|
| 26 |
+
Unpacking libnghttp2-14:amd64 (1.43.0-1ubuntu0.2) ...
|
| 27 |
+
Selecting previously unselected package libpsl5:amd64.
|
| 28 |
+
Preparing to unpack .../08-libpsl5_0.21.0-1.2build2_amd64.deb ...
|
| 29 |
+
Unpacking libpsl5:amd64 (0.21.0-1.2build2) ...
|
| 30 |
+
Selecting previously unselected package libxau6:amd64.
|
| 31 |
+
Preparing to unpack .../09-libxau6_1%3a1.0.9-1build5_amd64.deb ...
|
| 32 |
+
Unpacking libxau6:amd64 (1:1.0.9-1build5) ...
|
| 33 |
+
Selecting previously unselected package libxdmcp6:amd64.
|
| 34 |
+
Preparing to unpack .../10-libxdmcp6_1%3a1.1.3-0ubuntu5_amd64.deb ...
|
| 35 |
+
Unpacking libxdmcp6:amd64 (1:1.1.3-0ubuntu5) ...
|
| 36 |
+
Selecting previously unselected package libxcb1:amd64.
|
| 37 |
+
Preparing to unpack .../11-libxcb1_1.14-3ubuntu3_amd64.deb ...
|
| 38 |
+
Unpacking libxcb1:amd64 (1.14-3ubuntu3) ...
|
| 39 |
+
Selecting previously unselected package libx11-data.
|
| 40 |
+
Preparing to unpack .../12-libx11-data_2%3a1.7.5-1ubuntu0.3_all.deb ...
|
| 41 |
+
Unpacking libx11-data (2:1.7.5-1ubuntu0.3) ...
|
| 42 |
+
Selecting previously unselected package libx11-6:amd64.
|
| 43 |
+
Preparing to unpack .../13-libx11-6_2%3a1.7.5-1ubuntu0.3_amd64.deb ...
|
| 44 |
+
Unpacking libx11-6:amd64 (2:1.7.5-1ubuntu0.3) ...
|
| 45 |
+
Selecting previously unselected package libxext6:amd64.
|
| 46 |
+
Preparing to unpack .../14-libxext6_2%3a1.3.4-1build1_amd64.deb ...
|
| 47 |
+
Unpacking libxext6:amd64 (2:1.3.4-1build1) ...
|
| 48 |
+
Selecting previously unselected package libxmuu1:amd64.
|
| 49 |
+
Preparing to unpack .../15-libxmuu1_2%3a1.1.3-3_amd64.deb ...
|
| 50 |
+
Unpacking libxmuu1:amd64 (2:1.1.3-3) ...
|
| 51 |
+
Selecting previously unselected package openssh-client.
|
| 52 |
+
Preparing to unpack .../16-openssh-client_1%3a8.9p1-3ubuntu0.14_amd64.deb ...
|
| 53 |
+
Unpacking openssh-client (1:8.9p1-3ubuntu0.14) ...
|
| 54 |
+
Selecting previously unselected package publicsuffix.
|
| 55 |
+
Preparing to unpack .../17-publicsuffix_20211207.1025-1_all.deb ...
|
| 56 |
+
Unpacking publicsuffix (20211207.1025-1) ...
|
| 57 |
+
Selecting previously unselected package xauth.
|
| 58 |
+
Preparing to unpack .../18-xauth_1%3a1.1-1build2_amd64.deb ...
|
| 59 |
+
Unpacking xauth (1:1.1-1build2) ...
|
| 60 |
+
Selecting previously unselected package libbrotli1:amd64.
|
| 61 |
+
Preparing to unpack .../19-libbrotli1_1.0.9-2build6_amd64.deb ...
|
| 62 |
+
Unpacking libbrotli1:amd64 (1.0.9-2build6) ...
|
| 63 |
+
Selecting previously unselected package librtmp1:amd64.
|
| 64 |
+
Preparing to unpack .../20-librtmp1_2.4+20151223.gitfa8646d.1-2build4_amd64.deb ...
|
| 65 |
+
Unpacking librtmp1:amd64 (2.4+20151223.gitfa8646d.1-2build4) ...
|
| 66 |
+
Selecting previously unselected package libssh-4:amd64.
|
| 67 |
+
Preparing to unpack .../21-libssh-4_0.9.6-2ubuntu0.22.04.7_amd64.deb ...
|
| 68 |
+
Unpacking libssh-4:amd64 (0.9.6-2ubuntu0.22.04.7) ...
|
| 69 |
+
Selecting previously unselected package libcurl3-gnutls:amd64.
|
| 70 |
+
Preparing to unpack .../22-libcurl3-gnutls_7.81.0-1ubuntu1.23_amd64.deb ...
|
| 71 |
+
Unpacking libcurl3-gnutls:amd64 (7.81.0-1ubuntu1.23) ...
|
| 72 |
+
Selecting previously unselected package liberror-perl.
|
| 73 |
+
Preparing to unpack .../23-liberror-perl_0.17029-1_all.deb ...
|
| 74 |
+
Unpacking liberror-perl (0.17029-1) ...
|
| 75 |
+
Selecting previously unselected package git-man.
|
| 76 |
+
Preparing to unpack .../24-git-man_1%3a2.34.1-1ubuntu1.17_all.deb ...
|
| 77 |
+
Unpacking git-man (1:2.34.1-1ubuntu1.17) ...
|
| 78 |
+
Selecting previously unselected package git.
|
| 79 |
+
Preparing to unpack .../25-git_1%3a2.34.1-1ubuntu1.17_amd64.deb ...
|
| 80 |
+
Unpacking git (1:2.34.1-1ubuntu1.17) ...
|
| 81 |
+
Setting up libexpat1:amd64 (2.4.7-1ubuntu0.7) ...
|
| 82 |
+
Setting up libxau6:amd64 (1:1.0.9-1build5) ...
|
| 83 |
+
Setting up libpsl5:amd64 (0.21.0-1.2build2) ...
|
| 84 |
+
Setting up libcbor0.8:amd64 (0.8.0-2ubuntu1) ...
|
| 85 |
+
Setting up libbrotli1:amd64 (1.0.9-2build6) ...
|
| 86 |
+
Setting up libnghttp2-14:amd64 (1.43.0-1ubuntu0.2) ...
|
| 87 |
+
Setting up less (590-1ubuntu0.22.04.3) ...
|
| 88 |
+
Setting up liberror-perl (0.17029-1) ...
|
| 89 |
+
Setting up libx11-data (2:1.7.5-1ubuntu0.3) ...
|
| 90 |
+
Setting up librtmp1:amd64 (2.4+20151223.gitfa8646d.1-2build4) ...
|
| 91 |
+
Setting up libssh-4:amd64 (0.9.6-2ubuntu0.22.04.7) ...
|
| 92 |
+
Setting up libmd0:amd64 (1.0.4-1build1) ...
|
| 93 |
+
Setting up git-man (1:2.34.1-1ubuntu1.17) ...
|
| 94 |
+
Setting up libfido2-1:amd64 (1.10.0-1) ...
|
| 95 |
+
Setting up libbsd0:amd64 (0.11.5-1) ...
|
| 96 |
+
Setting up publicsuffix (20211207.1025-1) ...
|
| 97 |
+
Setting up libxdmcp6:amd64 (1:1.1.3-0ubuntu5) ...
|
| 98 |
+
Setting up libxcb1:amd64 (1.14-3ubuntu3) ...
|
| 99 |
+
Setting up libedit2:amd64 (3.1-20210910-1build1) ...
|
| 100 |
+
Setting up libcurl3-gnutls:amd64 (7.81.0-1ubuntu1.23) ...
|
| 101 |
+
Setting up git (1:2.34.1-1ubuntu1.17) ...
|
| 102 |
+
Setting up libx11-6:amd64 (2:1.7.5-1ubuntu0.3) ...
|
| 103 |
+
Setting up libxmuu1:amd64 (2:1.1.3-3) ...
|
| 104 |
+
Setting up openssh-client (1:8.9p1-3ubuntu0.14) ...
|
| 105 |
+
update-alternatives: using /usr/bin/ssh to provide /usr/bin/rsh (rsh) in auto mode
|
| 106 |
+
update-alternatives: warning: skip creation of /usr/share/man/man1/rsh.1.gz because associated file /usr/share/man/man1/ssh.1.gz (of link group rsh) doesn't exist
|
| 107 |
+
update-alternatives: using /usr/bin/slogin to provide /usr/bin/rlogin (rlogin) in auto mode
|
| 108 |
+
update-alternatives: warning: skip creation of /usr/share/man/man1/rlogin.1.gz because associated file /usr/share/man/man1/slogin.1.gz (of link group rlogin) doesn't exist
|
| 109 |
+
update-alternatives: using /usr/bin/scp to provide /usr/bin/rcp (rcp) in auto mode
|
| 110 |
+
update-alternatives: warning: skip creation of /usr/share/man/man1/rcp.1.gz because associated file /usr/share/man/man1/scp.1.gz (of link group rcp) doesn't exist
|
| 111 |
+
Setting up libxext6:amd64 (2:1.3.4-1build1) ...
|
| 112 |
+
Setting up xauth (1:1.1-1build2) ...
|
| 113 |
+
Processing triggers for libc-bin (2.35-0ubuntu3.8) ...
|
| 114 |
+
Cloning into '/workspace/OpenEnv'...
|
| 115 |
+
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning.
|
| 116 |
+
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning.
|
| 117 |
+
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning.
|
| 118 |
+
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning.
|
| 119 |
+
QUICK_MODE=False MODEL=unsloth/Qwen2.5-3B-Instruct
|
| 120 |
+
GPU: NVIDIA A100-SXM4-80GB capability=(8, 0) bf16=True fp16=False
|
| 121 |
+
Train: {'n_rows': 16000, 'admin_rows': 1600, 'customer_rows': 14400, 'drift_sensitive_rows': 2240, 'bonus_eligible_rows': 1153, 'kinds_distribution': {'chitchat': 1467, 'refund': 6676, 'billing_q': 1439, 'admin': 1600, 'info_request': 1482, 'critical_incident': 3336}}
|
| 122 |
+
Eval: {'n_rows': 800, 'admin_rows': 80, 'customer_rows': 720, 'drift_sensitive_rows': 124, 'bonus_eligible_rows': 62, 'kinds_distribution': {'refund': 347, 'critical_incident': 168, 'admin': 80, 'billing_q': 88, 'chitchat': 62, 'info_request': 55}}
|
| 123 |
+
🦥 Unsloth: Will patch your computer to enable 2x faster free finetuning.
|
| 124 |
+
Unsloth: Your Flash Attention 2 installation seems to be broken. Using Xformers instead. No performance changes will be seen.
|
| 125 |
+
🦥 Unsloth Zoo will now patch everything to make training faster!
|
| 126 |
+
==((====))== Unsloth 2026.4.8: Fast Qwen2 patching. Transformers: 5.5.0.
|
| 127 |
+
\\ /| NVIDIA A100-SXM4-80GB. Num GPUs = 1. Max memory: 79.25 GB. Platform: Linux.
|
| 128 |
+
O^O/ \_/ \ Torch: 2.8.0+cu128. CUDA: 8.0. CUDA Toolkit: 12.8. Triton: 3.4.0
|
| 129 |
+
\ / Bfloat16 = TRUE. FA [Xformers = None. FA2 = False]
|
| 130 |
+
"-____-" Free license: http://github.com/unslothai/unsloth
|
| 131 |
+
Unsloth: Fast downloading is enabled - ignore downloading bars which are red colored!
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
unsloth/qwen2.5-3b-instruct-unsloth-bnb-4bit does not have a padding token! Will use pad_token = <|PAD_TOKEN|>.
|
| 153 |
+
|
| 154 |
+
=== Offline eval [pre-training] over 200 samples ===
|
| 155 |
+
compliance avg : 0.528 / 1.0
|
| 156 |
+
appropriateness avg: 0.410 / 0.5
|
| 157 |
+
drift_bonus avg : 0.005 / 0.5
|
| 158 |
+
total avg : 0.943 / 2.0
|
| 159 |
+
drift-sens acc : 11.8% (2/17)
|
| 160 |
+
tightening : 0.0% (0/23)
|
| 161 |
+
loosening : 21.4% (3/14)
|
| 162 |
+
neutral : n/a (0/0)
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
🦥 Unsloth: Padding-free auto-enabled, enabling faster training.
|
| 184 |
+
|
| 185 |
+
=== SFT warm-up: 16000 samples, 1 epoch(s) ===
|
| 186 |
+
Unsloth: Will smartly offload gradients to save VRAM!
|
| 187 |
+
{'loss': '1.293', 'grad_norm': '2.695', 'learning_rate': '1.8e-05', 'epoch': '0.01'}
|
| 188 |
+
{'loss': '0.9491', 'grad_norm': '4.517', 'learning_rate': '3.8e-05', 'epoch': '0.02'}
|
| 189 |
+
{'loss': '0.132', 'grad_norm': '0.3303', 'learning_rate': '5.8e-05', 'epoch': '0.03'}
|
| 190 |
+
{'loss': '0.07011', 'grad_norm': '0.2859', 'learning_rate': '7.8e-05', 'epoch': '0.04'}
|
| 191 |
+
{'loss': '0.02983', 'grad_norm': '0.196', 'learning_rate': '9.8e-05', 'epoch': '0.05'}
|
| 192 |
+
{'loss': '0.03054', 'grad_norm': '0.362', 'learning_rate': '0.000118', 'epoch': '0.06'}
|
| 193 |
+
{'loss': '0.02064', 'grad_norm': '0.1444', 'learning_rate': '0.000138', 'epoch': '0.07'}
|
| 194 |
+
{'loss': '0.01928', 'grad_norm': '0.1991', 'learning_rate': '0.000158', 'epoch': '0.08'}
|
| 195 |
+
{'loss': '0.01925', 'grad_norm': '0.2893', 'learning_rate': '0.000178', 'epoch': '0.09'}
|
| 196 |
+
{'loss': '0.02096', 'grad_norm': '0.1415', 'learning_rate': '0.000198', 'epoch': '0.1'}
|
| 197 |
+
{'loss': '0.02395', 'grad_norm': '0.2314', 'learning_rate': '0.000198', 'epoch': '0.11'}
|
| 198 |
+
{'loss': '0.02662', 'grad_norm': '0.1206', 'learning_rate': '0.0001958', 'epoch': '0.12'}
|
| 199 |
+
{'loss': '0.02023', 'grad_norm': '0.1322', 'learning_rate': '0.0001936', 'epoch': '0.13'}
|
| 200 |
+
{'loss': '0.01497', 'grad_norm': '0.146', 'learning_rate': '0.0001913', 'epoch': '0.14'}
|
| 201 |
+
{'loss': '0.01318', 'grad_norm': '0.03542', 'learning_rate': '0.0001891', 'epoch': '0.15'}
|
| 202 |
+
{'loss': '0.01705', 'grad_norm': '0.1409', 'learning_rate': '0.0001869', 'epoch': '0.16'}
|
| 203 |
+
{'loss': '0.01366', 'grad_norm': '0.106', 'learning_rate': '0.0001847', 'epoch': '0.17'}
|
| 204 |
+
{'loss': '0.01499', 'grad_norm': '0.06284', 'learning_rate': '0.0001824', 'epoch': '0.18'}
|
| 205 |
+
{'loss': '0.01449', 'grad_norm': '0.06855', 'learning_rate': '0.0001802', 'epoch': '0.19'}
|
| 206 |
+
{'loss': '0.01527', 'grad_norm': '0.09317', 'learning_rate': '0.000178', 'epoch': '0.2'}
|
| 207 |
+
{'loss': '0.02113', 'grad_norm': '0.1043', 'learning_rate': '0.0001758', 'epoch': '0.21'}
|
| 208 |
+
{'loss': '0.0138', 'grad_norm': '0.07432', 'learning_rate': '0.0001736', 'epoch': '0.22'}
|
| 209 |
+
{'loss': '0.01321', 'grad_norm': '0.03357', 'learning_rate': '0.0001713', 'epoch': '0.23'}
|
| 210 |
+
{'loss': '0.01104', 'grad_norm': '0.02183', 'learning_rate': '0.0001691', 'epoch': '0.24'}
|
| 211 |
+
{'loss': '0.01341', 'grad_norm': '0.0815', 'learning_rate': '0.0001669', 'epoch': '0.25'}
|
| 212 |
+
{'loss': '0.01698', 'grad_norm': '0.07082', 'learning_rate': '0.0001647', 'epoch': '0.26'}
|
| 213 |
+
{'loss': '0.01471', 'grad_norm': '0.07086', 'learning_rate': '0.0001624', 'epoch': '0.27'}
|
| 214 |
+
{'loss': '0.009542', 'grad_norm': '0.102', 'learning_rate': '0.0001602', 'epoch': '0.28'}
|
| 215 |
+
{'loss': '0.01128', 'grad_norm': '0.1207', 'learning_rate': '0.000158', 'epoch': '0.29'}
|
| 216 |
+
{'loss': '0.009179', 'grad_norm': '0.05602', 'learning_rate': '0.0001558', 'epoch': '0.3'}
|
| 217 |
+
{'loss': '0.01002', 'grad_norm': '0.02763', 'learning_rate': '0.0001536', 'epoch': '0.31'}
|
| 218 |
+
{'loss': '0.01002', 'grad_norm': '0.04349', 'learning_rate': '0.0001513', 'epoch': '0.32'}
|
| 219 |
+
{'loss': '0.01587', 'grad_norm': '0.06167', 'learning_rate': '0.0001491', 'epoch': '0.33'}
|
| 220 |
+
{'loss': '0.009908', 'grad_norm': '0.04353', 'learning_rate': '0.0001469', 'epoch': '0.34'}
|
| 221 |
+
{'loss': '0.01125', 'grad_norm': '0.03281', 'learning_rate': '0.0001447', 'epoch': '0.35'}
|
| 222 |
+
{'loss': '0.01353', 'grad_norm': '0.04305', 'learning_rate': '0.0001424', 'epoch': '0.36'}
|
| 223 |
+
{'loss': '0.01252', 'grad_norm': '0.04699', 'learning_rate': '0.0001402', 'epoch': '0.37'}
|
| 224 |
+
{'loss': '0.01027', 'grad_norm': '0.1363', 'learning_rate': '0.000138', 'epoch': '0.38'}
|
| 225 |
+
{'loss': '0.01035', 'grad_norm': '0.0611', 'learning_rate': '0.0001358', 'epoch': '0.39'}
|
| 226 |
+
{'loss': '0.0167', 'grad_norm': '0.04438', 'learning_rate': '0.0001336', 'epoch': '0.4'}
|
| 227 |
+
{'loss': '0.01067', 'grad_norm': '0.04687', 'learning_rate': '0.0001313', 'epoch': '0.41'}
|
| 228 |
+
{'loss': '0.01134', 'grad_norm': '0.0377', 'learning_rate': '0.0001291', 'epoch': '0.42'}
|
| 229 |
+
{'loss': '0.01073', 'grad_norm': '0.03925', 'learning_rate': '0.0001269', 'epoch': '0.43'}
|
| 230 |
+
{'loss': '0.01296', 'grad_norm': '0.0747', 'learning_rate': '0.0001247', 'epoch': '0.44'}
|
| 231 |
+
{'loss': '0.01258', 'grad_norm': '0.01743', 'learning_rate': '0.0001224', 'epoch': '0.45'}
|
| 232 |
+
{'loss': '0.01015', 'grad_norm': '0.04182', 'learning_rate': '0.0001202', 'epoch': '0.46'}
|
| 233 |
+
{'loss': '0.01294', 'grad_norm': '0.1417', 'learning_rate': '0.000118', 'epoch': '0.47'}
|
| 234 |
+
{'loss': '0.01141', 'grad_norm': '0.04199', 'learning_rate': '0.0001158', 'epoch': '0.48'}
|
| 235 |
+
{'loss': '0.01143', 'grad_norm': '0.01166', 'learning_rate': '0.0001136', 'epoch': '0.49'}
|
| 236 |
+
{'loss': '0.01409', 'grad_norm': '0.03272', 'learning_rate': '0.0001113', 'epoch': '0.5'}
|
| 237 |
+
{'loss': '0.0116', 'grad_norm': '0.0427', 'learning_rate': '0.0001091', 'epoch': '0.51'}
|
| 238 |
+
{'loss': '0.007553', 'grad_norm': '0.05289', 'learning_rate': '0.0001069', 'epoch': '0.52'}
|
| 239 |
+
{'loss': '0.007692', 'grad_norm': '0.06659', 'learning_rate': '0.0001047', 'epoch': '0.53'}
|
| 240 |
+
{'loss': '0.01459', 'grad_norm': '0.1073', 'learning_rate': '0.0001024', 'epoch': '0.54'}
|
| 241 |
+
{'loss': '0.01389', 'grad_norm': '0.1297', 'learning_rate': '0.0001002', 'epoch': '0.55'}
|
| 242 |
+
{'loss': '0.01091', 'grad_norm': '0.04869', 'learning_rate': '9.8e-05', 'epoch': '0.56'}
|
| 243 |
+
{'loss': '0.01059', 'grad_norm': '0.06955', 'learning_rate': '9.578e-05', 'epoch': '0.57'}
|
| 244 |
+
{'loss': '0.00643', 'grad_norm': '0.03395', 'learning_rate': '9.356e-05', 'epoch': '0.58'}
|
| 245 |
+
{'loss': '0.009756', 'grad_norm': '0.1062', 'learning_rate': '9.133e-05', 'epoch': '0.59'}
|
| 246 |
+
{'loss': '0.009229', 'grad_norm': '0.03023', 'learning_rate': '8.911e-05', 'epoch': '0.6'}
|
| 247 |
+
{'loss': '0.01173', 'grad_norm': '0.03197', 'learning_rate': '8.689e-05', 'epoch': '0.61'}
|
| 248 |
+
{'loss': '0.008542', 'grad_norm': '0.04632', 'learning_rate': '8.467e-05', 'epoch': '0.62'}
|
| 249 |
+
{'loss': '0.006862', 'grad_norm': '0.01375', 'learning_rate': '8.244e-05', 'epoch': '0.63'}
|
| 250 |
+
{'loss': '0.01155', 'grad_norm': '0.01589', 'learning_rate': '8.022e-05', 'epoch': '0.64'}
|
| 251 |
+
{'loss': '0.01295', 'grad_norm': '0.02233', 'learning_rate': '7.8e-05', 'epoch': '0.65'}
|
| 252 |
+
{'loss': '0.008617', 'grad_norm': '0.01177', 'learning_rate': '7.578e-05', 'epoch': '0.66'}
|
| 253 |
+
{'loss': '0.009604', 'grad_norm': '0.009182', 'learning_rate': '7.356e-05', 'epoch': '0.67'}
|
| 254 |
+
{'loss': '0.008484', 'grad_norm': '0.09819', 'learning_rate': '7.133e-05', 'epoch': '0.68'}
|
| 255 |
+
{'loss': '0.009772', 'grad_norm': '0.08253', 'learning_rate': '6.911e-05', 'epoch': '0.69'}
|
| 256 |
+
{'loss': '0.01377', 'grad_norm': '0.03314', 'learning_rate': '6.689e-05', 'epoch': '0.7'}
|
| 257 |
+
{'loss': '0.009199', 'grad_norm': '0.05316', 'learning_rate': '6.467e-05', 'epoch': '0.71'}
|
| 258 |
+
{'loss': '0.008602', 'grad_norm': '0.04835', 'learning_rate': '6.244e-05', 'epoch': '0.72'}
|
| 259 |
+
{'loss': '0.01071', 'grad_norm': '0.02972', 'learning_rate': '6.022e-05', 'epoch': '0.73'}
|
| 260 |
+
{'loss': '0.01249', 'grad_norm': '0.1129', 'learning_rate': '5.8e-05', 'epoch': '0.74'}
|
| 261 |
+
{'loss': '0.01384', 'grad_norm': '0.08936', 'learning_rate': '5.578e-05', 'epoch': '0.75'}
|
| 262 |
+
{'loss': '0.01763', 'grad_norm': '0.08994', 'learning_rate': '5.356e-05', 'epoch': '0.76'}
|
| 263 |
+
{'loss': '0.009391', 'grad_norm': '0.04473', 'learning_rate': '5.133e-05', 'epoch': '0.77'}
|
| 264 |
+
{'loss': '0.01192', 'grad_norm': '0.04021', 'learning_rate': '4.911e-05', 'epoch': '0.78'}
|
| 265 |
+
{'loss': '0.01178', 'grad_norm': '0.09859', 'learning_rate': '4.689e-05', 'epoch': '0.79'}
|
| 266 |
+
{'loss': '0.006704', 'grad_norm': '0.02168', 'learning_rate': '4.467e-05', 'epoch': '0.8'}
|
| 267 |
+
{'loss': '0.0101', 'grad_norm': '0.03415', 'learning_rate': '4.244e-05', 'epoch': '0.81'}
|
| 268 |
+
{'loss': '0.0113', 'grad_norm': '0.1112', 'learning_rate': '4.022e-05', 'epoch': '0.82'}
|
| 269 |
+
{'loss': '0.01436', 'grad_norm': '0.05179', 'learning_rate': '3.8e-05', 'epoch': '0.83'}
|
| 270 |
+
{'loss': '0.005483', 'grad_norm': '0.02584', 'learning_rate': '3.578e-05', 'epoch': '0.84'}
|
| 271 |
+
{'loss': '0.008758', 'grad_norm': '0.01811', 'learning_rate': '3.356e-05', 'epoch': '0.85'}
|
| 272 |
+
{'loss': '0.0135', 'grad_norm': '0.07277', 'learning_rate': '3.133e-05', 'epoch': '0.86'}
|
| 273 |
+
{'loss': '0.00843', 'grad_norm': '0.00885', 'learning_rate': '2.911e-05', 'epoch': '0.87'}
|
| 274 |
+
{'loss': '0.007161', 'grad_norm': '0.06622', 'learning_rate': '2.689e-05', 'epoch': '0.88'}
|
| 275 |
+
{'loss': '0.009555', 'grad_norm': '0.03553', 'learning_rate': '2.467e-05', 'epoch': '0.89'}
|
| 276 |
+
{'loss': '0.01209', 'grad_norm': '0.1807', 'learning_rate': '2.244e-05', 'epoch': '0.9'}
|
| 277 |
+
{'loss': '0.01153', 'grad_norm': '0.03899', 'learning_rate': '2.022e-05', 'epoch': '0.91'}
|
| 278 |
+
{'loss': '0.01187', 'grad_norm': '0.1586', 'learning_rate': '1.8e-05', 'epoch': '0.92'}
|
| 279 |
+
{'loss': '0.007287', 'grad_norm': '0.002396', 'learning_rate': '1.578e-05', 'epoch': '0.93'}
|
| 280 |
+
{'loss': '0.01125', 'grad_norm': '0.1114', 'learning_rate': '1.356e-05', 'epoch': '0.94'}
|
| 281 |
+
{'loss': '0.01345', 'grad_norm': '0.1539', 'learning_rate': '1.133e-05', 'epoch': '0.95'}
|
| 282 |
+
{'loss': '0.01639', 'grad_norm': '0.09004', 'learning_rate': '9.111e-06', 'epoch': '0.96'}
|
| 283 |
+
{'loss': '0.008245', 'grad_norm': '0.01905', 'learning_rate': '6.889e-06', 'epoch': '0.97'}
|
| 284 |
+
{'loss': '0.006973', 'grad_norm': '0.1271', 'learning_rate': '4.667e-06', 'epoch': '0.98'}
|
| 285 |
+
{'loss': '0.007818', 'grad_norm': '0.1934', 'learning_rate': '2.444e-06', 'epoch': '0.99'}
|
| 286 |
+
{'loss': '0.01181', 'grad_norm': '0.02508', 'learning_rate': '2.222e-07', 'epoch': '1'}
|
| 287 |
+
{'train_runtime': '5990', 'train_samples_per_second': '2.671', 'train_steps_per_second': '0.167', 'train_loss': '0.03656', 'epoch': '1'}
|
| 288 |
+
SFT log saved -> ./outputs/sft_log.json
|
| 289 |
+
SFT-only adapter checkpoint saved -> ./outputs/lora_adapters_sft
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
|
| 294 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
| 295 |
+
|
| 296 |
+
|
| 297 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
|
| 301 |
...adapter_model.safetensors: 31%|███ | 37.4MB / 120MB [A[A[A
|
| 302 |
+
|
| 303 |
+
|
| 304 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
|
| 308 |
...adapter_model.safetensors: 31%|███ | 37.4MB / 120MB [A[A[A
|
| 309 |
+
|
| 310 |
+
|
| 311 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
|
| 315 |
...adapter_model.safetensors: 94%|█████████▍| 112MB / 120MB [A[A[A
|
| 316 |
+
|
| 317 |
+
|
| 318 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
|
| 322 |
...adapter_model.safetensors: 99%|█████████▉| 119MB / 120MB [A[A[A
|
| 323 |
+
|
| 324 |
+
|
| 325 |
...apters_sft/tokenizer.json: 100%|███████���██| 11.4MB / 11.4MB [A[A
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
|
| 329 |
...adapter_model.safetensors: 99%|█████████▉| 119MB / 120MB [A[A[A
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
|
| 333 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
|
| 337 |
...adapter_model.safetensors: 100%|█████████▉| 119MB / 120MB [A[A[A
|
| 338 |
+
|
| 339 |
+
|
| 340 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
|
| 344 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A
|
| 345 |
+
|
| 346 |
+
|
| 347 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
|
| 351 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A
|
| 352 |
+
|
| 353 |
+
|
| 354 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
|
| 358 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A
|
| 359 |
+
|
| 360 |
+
|
| 361 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB
|
| 362 |
+
|
| 363 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB
|
| 364 |
+
[hub] post-SFT checkpoint pushed -> https://huggingface.co/shreyas-garg/leniencybench-qwen3b-outputs
|
| 365 |
+
|
| 366 |
+
=== Offline eval [post-SFT] over 200 samples ===
|
| 367 |
+
compliance avg : 0.968 / 1.0
|
| 368 |
+
appropriateness avg: 0.450 / 0.5
|
| 369 |
+
drift_bonus avg : 0.037 / 0.5
|
| 370 |
+
total avg : 1.455 / 2.0
|
| 371 |
+
drift-sens acc : 88.2% (15/17)
|
| 372 |
+
tightening : 91.3% (21/23)
|
| 373 |
+
loosening : 71.4% (10/14)
|
| 374 |
+
neutral : n/a (0/0)
|
| 375 |
+
|
| 376 |
+
=== GRPO: 16000 prompts, max_steps=600, K=8 ===
|
| 377 |
+
Unsloth: Will smartly offload gradients to save VRAM!
|
| 378 |
+
|
| 379 |
+
[grpo] FAILED: RuntimeError: self and mat2 must have the same dtype, but got Half and Float
|
| 380 |
+
[grpo] Continuing with post-SFT model as the final artifact.
|
| 381 |
+
|
| 382 |
+
=== Improvement summary ===
|
| 383 |
+
compliance 0.528 -> 0.968 -> 0.968
|
| 384 |
+
appropriateness 0.410 -> 0.450 -> 0.450
|
| 385 |
+
drift_bonus 0.005 -> 0.037 -> 0.037
|
| 386 |
+
drift-sens acc (all) 11.8% -> 88.2% -> 88.2%
|
| 387 |
+
tightening 0.0% -> 91.3% -> 91.3%
|
| 388 |
+
loosening 21.4% -> 71.4% -> 71.4%
|
| 389 |
+
neutral n/a -> n/a -> n/a
|
| 390 |
+
Eval snapshots saved -> ./outputs/evals.json
|
| 391 |
+
|
| 392 |
+
LoRA adapters saved to ./outputs/lora_adapters
|
| 393 |
+
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
|
| 397 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
| 398 |
+
|
| 399 |
+
|
| 400 |
+
|
| 401 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
|
| 405 |
+
|
| 406 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A[A
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
|
| 412 |
...a_adapters/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A[A[A[A
|
| 413 |
+
|
| 414 |
+
|
| 415 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
|
| 419 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
|
| 424 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A[A
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
|
| 430 |
...a_adapters/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A[A[A[A
|
| 431 |
+
|
| 432 |
+
|
| 433 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
|
| 437 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
|
| 442 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A[A
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
|
| 448 |
...a_adapters/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A[A[A[A
|
| 449 |
+
|
| 450 |
+
|
| 451 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
| 452 |
+
|
| 453 |
+
|
| 454 |
+
|
| 455 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
|
| 460 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A[A
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
|
| 466 |
...a_adapters/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A[A[A[A
|
| 467 |
+
|
| 468 |
+
|
| 469 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A
|
| 470 |
+
|
| 471 |
+
|
| 472 |
+
|
| 473 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A
|
| 474 |
+
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
|
| 478 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB [A[A[A[A
|
| 479 |
+
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
|
| 483 |
+
|
| 484 |
...a_adapters/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB [A[A[A[A[A
|
| 485 |
+
|
| 486 |
+
|
| 487 |
...apters_sft/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB
|
| 488 |
+
|
| 489 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB
|
| 490 |
+
|
| 491 |
...adapter_model.safetensors: 100%|██████████| 120MB / 120MB
|
| 492 |
+
|
| 493 |
...a_adapters/tokenizer.json: 100%|██████████| 11.4MB / 11.4MB
|
| 494 |
+
[hub] post-GRPO final pushed -> https://huggingface.co/shreyas-garg/leniencybench-qwen3b-outputs
|
plot_training.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Plot training curves from the logs `train.py` saved.
|
| 2 |
+
|
| 3 |
+
Usage:
|
| 4 |
+
python plot_training.py ./outputs
|
| 5 |
+
|
| 6 |
+
Expects these files (any subset — plotter skips what's missing):
|
| 7 |
+
outputs/sft_log.json <- trainer.state.log_history from SFT
|
| 8 |
+
outputs/grpo_log.json <- trainer.state.log_history from GRPO
|
| 9 |
+
outputs/evals.json <- {pre, post_sft, post_grpo} snapshots
|
| 10 |
+
|
| 11 |
+
Produces:
|
| 12 |
+
outputs/reward_curve.png <- GRPO reward + components over steps
|
| 13 |
+
outputs/sft_loss.png <- SFT loss curve
|
| 14 |
+
outputs/drift_acc_bars.png <- pre / post-SFT / post-GRPO drift-sensitive accuracy
|
| 15 |
+
outputs/summary.png <- combined 1x3 figure suitable for a pitch slide
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import argparse
|
| 21 |
+
import json
|
| 22 |
+
import os
|
| 23 |
+
import sys
|
| 24 |
+
from typing import Optional
|
| 25 |
+
|
| 26 |
+
import matplotlib
|
| 27 |
+
matplotlib.use("Agg") # headless
|
| 28 |
+
import matplotlib.pyplot as plt
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# ---------------------------------------------------------------------------
|
| 32 |
+
# IO
|
| 33 |
+
# ---------------------------------------------------------------------------
|
| 34 |
+
def _load(path: str) -> Optional[object]:
|
| 35 |
+
if not os.path.isfile(path):
|
| 36 |
+
return None
|
| 37 |
+
with open(path) as f:
|
| 38 |
+
return json.load(f)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _extract_series(log: list[dict], key: str) -> tuple[list[int], list[float]]:
|
| 42 |
+
"""Pull a (step, value) time series from trainer.state.log_history."""
|
| 43 |
+
xs, ys = [], []
|
| 44 |
+
for entry in log:
|
| 45 |
+
if key not in entry or "step" not in entry:
|
| 46 |
+
continue
|
| 47 |
+
try:
|
| 48 |
+
ys.append(float(entry[key]))
|
| 49 |
+
xs.append(int(entry["step"]))
|
| 50 |
+
except (TypeError, ValueError):
|
| 51 |
+
continue
|
| 52 |
+
return xs, ys
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
# ---------------------------------------------------------------------------
|
| 56 |
+
# Plots
|
| 57 |
+
# ---------------------------------------------------------------------------
|
| 58 |
+
def plot_sft_loss(log: list[dict], out_path: str) -> None:
|
| 59 |
+
steps, losses = _extract_series(log, "loss")
|
| 60 |
+
if not steps:
|
| 61 |
+
print(f"[skip] no loss series in sft_log")
|
| 62 |
+
return
|
| 63 |
+
fig, ax = plt.subplots(figsize=(7, 4))
|
| 64 |
+
ax.plot(steps, losses, marker="o", markersize=3, linewidth=1.5, color="#2a6df4")
|
| 65 |
+
ax.set_xlabel("SFT step")
|
| 66 |
+
ax.set_ylabel("Loss")
|
| 67 |
+
ax.set_title("SFT warm-up — loss over training steps")
|
| 68 |
+
ax.grid(alpha=0.3)
|
| 69 |
+
fig.tight_layout()
|
| 70 |
+
fig.savefig(out_path, dpi=150)
|
| 71 |
+
plt.close(fig)
|
| 72 |
+
print(f"[ok] wrote {out_path}")
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def plot_grpo_reward_curve(log: list[dict], out_path: str) -> None:
|
| 76 |
+
steps_r, total = _extract_series(log, "reward")
|
| 77 |
+
_, comp = _extract_series(log, "rewards/reward_compliance/mean")
|
| 78 |
+
_, appr = _extract_series(log, "rewards/reward_appropriateness/mean")
|
| 79 |
+
_, bonus = _extract_series(log, "rewards/reward_drift_bonus/mean")
|
| 80 |
+
|
| 81 |
+
if not steps_r:
|
| 82 |
+
print(f"[skip] no reward series in grpo_log")
|
| 83 |
+
return
|
| 84 |
+
|
| 85 |
+
fig, ax = plt.subplots(figsize=(7, 4))
|
| 86 |
+
# Total as a bold line; components as thinner stacked lines.
|
| 87 |
+
if total:
|
| 88 |
+
ax.plot(steps_r, total, label="total", linewidth=2.2, color="#111")
|
| 89 |
+
if comp:
|
| 90 |
+
ax.plot(steps_r[:len(comp)], comp, label="compliance",
|
| 91 |
+
linewidth=1.5, color="#2a6df4")
|
| 92 |
+
if appr:
|
| 93 |
+
ax.plot(steps_r[:len(appr)], appr, label="appropriateness",
|
| 94 |
+
linewidth=1.5, color="#f29e2e")
|
| 95 |
+
if bonus:
|
| 96 |
+
ax.plot(steps_r[:len(bonus)], bonus, label="drift_bonus",
|
| 97 |
+
linewidth=1.5, color="#d5342a")
|
| 98 |
+
|
| 99 |
+
ax.set_xlabel("GRPO step")
|
| 100 |
+
ax.set_ylabel("Mean reward (per completion)")
|
| 101 |
+
ax.set_title("GRPO — reward and components over training")
|
| 102 |
+
ax.set_ylim(bottom=0)
|
| 103 |
+
ax.legend(loc="best")
|
| 104 |
+
ax.grid(alpha=0.3)
|
| 105 |
+
fig.tight_layout()
|
| 106 |
+
fig.savefig(out_path, dpi=150)
|
| 107 |
+
plt.close(fig)
|
| 108 |
+
print(f"[ok] wrote {out_path}")
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def plot_drift_acc_bars(evals: dict, out_path: str) -> None:
|
| 112 |
+
labels = ["pre", "post-SFT", "post-GRPO"]
|
| 113 |
+
keys = ["pre", "post_sft", "post_grpo"]
|
| 114 |
+
accs = []
|
| 115 |
+
for k in keys:
|
| 116 |
+
a = evals.get(k, {}).get("drift_acc")
|
| 117 |
+
accs.append(a if isinstance(a, (int, float)) else 0.0)
|
| 118 |
+
colors = ["#d5342a", "#f29e2e", "#2a6df4"]
|
| 119 |
+
|
| 120 |
+
fig, ax = plt.subplots(figsize=(7, 4))
|
| 121 |
+
bars = ax.bar(labels, [a * 100 for a in accs], color=colors, width=0.5)
|
| 122 |
+
for b, a in zip(bars, accs):
|
| 123 |
+
ax.text(b.get_x() + b.get_width() / 2, b.get_height() + 1.5,
|
| 124 |
+
f"{a:.0%}", ha="center", va="bottom", fontsize=11, fontweight="bold")
|
| 125 |
+
ax.set_ylabel("Drift-sensitive accuracy")
|
| 126 |
+
ax.set_title(f"Drift-sensitive accuracy — {evals.get('model_name', 'model')}")
|
| 127 |
+
ax.set_ylim(0, 105)
|
| 128 |
+
ax.set_yticks([0, 20, 40, 60, 80, 100])
|
| 129 |
+
ax.set_yticklabels([f"{v}%" for v in [0, 20, 40, 60, 80, 100]])
|
| 130 |
+
ax.grid(alpha=0.2, axis="y")
|
| 131 |
+
fig.tight_layout()
|
| 132 |
+
fig.savefig(out_path, dpi=150)
|
| 133 |
+
plt.close(fig)
|
| 134 |
+
print(f"[ok] wrote {out_path}")
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def plot_summary(sft_log: list[dict] | None, grpo_log: list[dict] | None,
|
| 138 |
+
evals: dict | None, out_path: str) -> None:
|
| 139 |
+
"""Combined 1x3 figure for the pitch slide."""
|
| 140 |
+
fig, axes = plt.subplots(1, 3, figsize=(16, 4.2))
|
| 141 |
+
|
| 142 |
+
# Panel 1: SFT loss
|
| 143 |
+
if sft_log:
|
| 144 |
+
steps, losses = _extract_series(sft_log, "loss")
|
| 145 |
+
if steps:
|
| 146 |
+
axes[0].plot(steps, losses, marker="o", markersize=3, color="#2a6df4")
|
| 147 |
+
axes[0].set_title("SFT loss")
|
| 148 |
+
axes[0].set_xlabel("step"); axes[0].set_ylabel("loss")
|
| 149 |
+
axes[0].grid(alpha=0.3)
|
| 150 |
+
|
| 151 |
+
# Panel 2: GRPO reward curve
|
| 152 |
+
if grpo_log:
|
| 153 |
+
steps_r, total = _extract_series(grpo_log, "reward")
|
| 154 |
+
_, comp = _extract_series(grpo_log, "rewards/reward_compliance/mean")
|
| 155 |
+
_, appr = _extract_series(grpo_log, "rewards/reward_appropriateness/mean")
|
| 156 |
+
_, bonus = _extract_series(grpo_log, "rewards/reward_drift_bonus/mean")
|
| 157 |
+
if steps_r:
|
| 158 |
+
axes[1].plot(steps_r, total, label="total", linewidth=2.2, color="#111")
|
| 159 |
+
if comp: axes[1].plot(steps_r[:len(comp)], comp, label="comp", color="#2a6df4")
|
| 160 |
+
if appr: axes[1].plot(steps_r[:len(appr)], appr, label="appr", color="#f29e2e")
|
| 161 |
+
if bonus: axes[1].plot(steps_r[:len(bonus)], bonus, label="drift", color="#d5342a")
|
| 162 |
+
axes[1].set_title("GRPO reward")
|
| 163 |
+
axes[1].set_xlabel("step"); axes[1].set_ylabel("reward")
|
| 164 |
+
axes[1].legend(fontsize=8); axes[1].grid(alpha=0.3)
|
| 165 |
+
|
| 166 |
+
# Panel 3: drift acc bars
|
| 167 |
+
if evals:
|
| 168 |
+
labels = ["pre", "post-SFT", "post-GRPO"]
|
| 169 |
+
keys = ["pre", "post_sft", "post_grpo"]
|
| 170 |
+
accs = [evals.get(k, {}).get("drift_acc") or 0.0 for k in keys]
|
| 171 |
+
colors = ["#d5342a", "#f29e2e", "#2a6df4"]
|
| 172 |
+
bars = axes[2].bar(labels, [a * 100 for a in accs], color=colors, width=0.55)
|
| 173 |
+
for b, a in zip(bars, accs):
|
| 174 |
+
axes[2].text(b.get_x() + b.get_width() / 2, b.get_height() + 1.5,
|
| 175 |
+
f"{a:.0%}", ha="center", va="bottom",
|
| 176 |
+
fontsize=10, fontweight="bold")
|
| 177 |
+
axes[2].set_ylim(0, 105)
|
| 178 |
+
axes[2].set_title("Drift-sensitive accuracy")
|
| 179 |
+
axes[2].grid(alpha=0.2, axis="y")
|
| 180 |
+
|
| 181 |
+
fig.suptitle("Policy-Drift env — training run summary", fontsize=14, y=1.02)
|
| 182 |
+
fig.tight_layout()
|
| 183 |
+
fig.savefig(out_path, dpi=150, bbox_inches="tight")
|
| 184 |
+
plt.close(fig)
|
| 185 |
+
print(f"[ok] wrote {out_path}")
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
# ---------------------------------------------------------------------------
|
| 189 |
+
def main() -> int:
|
| 190 |
+
ap = argparse.ArgumentParser()
|
| 191 |
+
ap.add_argument("outputs_dir", nargs="?", default="./outputs",
|
| 192 |
+
help="Directory containing sft_log.json, grpo_log.json, evals.json")
|
| 193 |
+
args = ap.parse_args()
|
| 194 |
+
|
| 195 |
+
d = args.outputs_dir
|
| 196 |
+
sft_log = _load(os.path.join(d, "sft_log.json"))
|
| 197 |
+
grpo_log = _load(os.path.join(d, "grpo_log.json"))
|
| 198 |
+
evals = _load(os.path.join(d, "evals.json"))
|
| 199 |
+
|
| 200 |
+
missing = [n for n, v in [("sft_log", sft_log), ("grpo_log", grpo_log), ("evals", evals)] if v is None]
|
| 201 |
+
if missing:
|
| 202 |
+
print(f"[warn] missing files (will skip corresponding plots): {missing}")
|
| 203 |
+
|
| 204 |
+
if sft_log:
|
| 205 |
+
plot_sft_loss(sft_log, os.path.join(d, "sft_loss.png"))
|
| 206 |
+
if grpo_log:
|
| 207 |
+
plot_grpo_reward_curve(grpo_log, os.path.join(d, "reward_curve.png"))
|
| 208 |
+
if evals:
|
| 209 |
+
plot_drift_acc_bars(evals, os.path.join(d, "drift_acc_bars.png"))
|
| 210 |
+
|
| 211 |
+
plot_summary(sft_log, grpo_log, evals, os.path.join(d, "summary.png"))
|
| 212 |
+
return 0
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
if __name__ == "__main__":
|
| 216 |
+
sys.exit(main())
|