Upload 10 files
Browse files- .gitattributes +35 -35
- Dockerfile +18 -0
- README.md +10 -10
- app.py +114 -0
- env.py +405 -0
- environment.py +501 -0
- inference.py +189 -0
- openenv.yaml +176 -0
- requirements.txt +8 -0
- test_cases_advanced.json +377 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,35 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#Base image
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
#Set work directory
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
#Install dependencies
|
| 8 |
+
COPY requirements.txt .
|
| 9 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 10 |
+
|
| 11 |
+
#Copy project files
|
| 12 |
+
COPY . .
|
| 13 |
+
|
| 14 |
+
#Expose the port Hugging Face expects
|
| 15 |
+
EXPOSE 7860
|
| 16 |
+
|
| 17 |
+
#Command to run FastAPI with uvicorn
|
| 18 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Code
|
| 3 |
-
emoji: π
|
| 4 |
-
colorFrom: red
|
| 5 |
-
colorTo: green
|
| 6 |
-
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
-
---
|
| 9 |
-
|
| 10 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Code
|
| 3 |
+
emoji: π
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
app.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
app.py β Gradio Web Interface for Hugging Face Spaces
|
| 3 |
+
=====================================================
|
| 4 |
+
Provides an interactive demo of the Email Gatekeeper RL environment.
|
| 5 |
+
Hugging Face Spaces serves this on port 7860 automatically.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import gradio as gr
|
| 9 |
+
import numpy as np
|
| 10 |
+
from env import (
|
| 11 |
+
EmailTriageEnv, TASK_SPLITS,
|
| 12 |
+
URGENCY_LABELS, ROUTING_LABELS, RESOLUTION_LABELS,
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
_LEGAL_SECURITY_KW = {"lawsuit", "attorney", "sue", "ransomware", "extortion"}
|
| 16 |
+
_BILLING_ESCALATE_KW = {"refund"}
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _classify(email: dict) -> np.ndarray:
|
| 20 |
+
kw = set(email.get("keywords", []))
|
| 21 |
+
context = email.get("context", "").lower()
|
| 22 |
+
if context == "legal" or kw & {"lawsuit", "attorney", "sue"}:
|
| 23 |
+
return np.array([2, 2, 2], dtype=np.int64)
|
| 24 |
+
if context == "security":
|
| 25 |
+
if kw & _LEGAL_SECURITY_KW or ("hacked" in kw and "breach" in kw):
|
| 26 |
+
return np.array([2, 2, 2], dtype=np.int64)
|
| 27 |
+
return np.array([2, 1, 2], dtype=np.int64)
|
| 28 |
+
if context == "billing":
|
| 29 |
+
return np.array([1, 2, 2] if kw & _BILLING_ESCALATE_KW
|
| 30 |
+
else [1, 0, 1], dtype=np.int64)
|
| 31 |
+
if context == "tech" or kw & {"crash", "error", "bug", "slow"}:
|
| 32 |
+
return np.array([0, 1, 1], dtype=np.int64)
|
| 33 |
+
return np.array([0, 0, 0], dtype=np.int64)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def run_task_demo(task: str) -> str:
|
| 37 |
+
env = EmailTriageEnv(task=task, shuffle=False)
|
| 38 |
+
env.reset(seed=42)
|
| 39 |
+
email_queue = list(env._queue)
|
| 40 |
+
|
| 41 |
+
lines = []
|
| 42 |
+
cumulative = 0.0
|
| 43 |
+
terminated = False
|
| 44 |
+
step = 0
|
| 45 |
+
|
| 46 |
+
while not terminated:
|
| 47 |
+
email = email_queue[step]
|
| 48 |
+
action = _classify(email)
|
| 49 |
+
_, norm_reward, terminated, _, info = env.step(action)
|
| 50 |
+
cumulative += norm_reward
|
| 51 |
+
|
| 52 |
+
raw = info["raw_reward"]
|
| 53 |
+
ca = info["correct_actions"]
|
| 54 |
+
|
| 55 |
+
verdict = ("β
EXACT" if raw >= 1.0 else
|
| 56 |
+
"πΆ PARTIAL" if raw > 0 else
|
| 57 |
+
"π¨ SECURITY MISS" if raw < 0 else "β WRONG")
|
| 58 |
+
|
| 59 |
+
lines.append(
|
| 60 |
+
f"#{step+1:02d} [{email['difficulty'].upper()}] "
|
| 61 |
+
f"{email['description'][:40]}\n"
|
| 62 |
+
f" Predicted : {URGENCY_LABELS[action[0]]} | "
|
| 63 |
+
f"{ROUTING_LABELS[action[1]]} | {RESOLUTION_LABELS[action[2]]}\n"
|
| 64 |
+
f" Correct : {URGENCY_LABELS[ca[0]]} | "
|
| 65 |
+
f"{ROUTING_LABELS[ca[1]]} | {RESOLUTION_LABELS[ca[2]]}\n"
|
| 66 |
+
f" Reward : {raw:+.1f} {verdict}\n"
|
| 67 |
+
)
|
| 68 |
+
step += 1
|
| 69 |
+
|
| 70 |
+
final = max(0.0, min(1.0, cumulative))
|
| 71 |
+
lines.append(f"\n{'β'*50}")
|
| 72 |
+
lines.append(f"Final Score : {final:.3f} / 1.0")
|
| 73 |
+
return "\n".join(lines)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
with gr.Blocks(title="Email Gatekeeper RL") as demo:
|
| 77 |
+
gr.Markdown("""
|
| 78 |
+
# π§ Email Gatekeeper β RL Environment Demo
|
| 79 |
+
**Meta x PyTorch Hackathon** | Gymnasium-based email triage agent
|
| 80 |
+
|
| 81 |
+
The agent classifies each email across **3 simultaneous dimensions**:
|
| 82 |
+
`Urgency` Γ `Department` Γ `Resolution Action`
|
| 83 |
+
""")
|
| 84 |
+
|
| 85 |
+
with gr.Row():
|
| 86 |
+
task_dropdown = gr.Dropdown(
|
| 87 |
+
choices=["easy", "medium", "hard"],
|
| 88 |
+
value="easy",
|
| 89 |
+
label="Select Task",
|
| 90 |
+
)
|
| 91 |
+
run_btn = gr.Button("βΆ Run Episode", variant="primary")
|
| 92 |
+
|
| 93 |
+
output_box = gr.Textbox(
|
| 94 |
+
label="Episode Results",
|
| 95 |
+
lines=30,
|
| 96 |
+
max_lines=50,
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
run_btn.click(fn=run_task_demo, inputs=task_dropdown, outputs=output_box)
|
| 100 |
+
|
| 101 |
+
gr.Markdown("""
|
| 102 |
+
### Reward Function
|
| 103 |
+
| Result | Reward |
|
| 104 |
+
|---|---|
|
| 105 |
+
| β
Exact Match (all 3 correct) | +1.0 |
|
| 106 |
+
| πΆ Partial (urgency correct, 1 wrong) | +0.2 |
|
| 107 |
+
| πΆ Partial (urgency correct, 2 wrong) | +0.1 |
|
| 108 |
+
| π¨ Security Miss | **-2.0** |
|
| 109 |
+
| β Wrong urgency | 0.0 |
|
| 110 |
+
""")
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
if __name__ == "__main__":
|
| 114 |
+
demo.launch(server_name="0.0.0.0", server_port=7860)
|
env.py
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
env.py β Email Gatekeeper RL Environment (OpenEnv Specification)
|
| 3 |
+
================================================================
|
| 4 |
+
Gymnasium environment for intelligent email triage.
|
| 5 |
+
Wraps the core EmailTriageEnv logic with:
|
| 6 |
+
- Pydantic typed Action and Observation models
|
| 7 |
+
- state() method returning current environment state
|
| 8 |
+
- Three task splits: easy / medium / hard
|
| 9 |
+
- Full OpenEnv-compatible interface
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import numpy as np
|
| 15 |
+
import gymnasium as gym
|
| 16 |
+
from gymnasium import spaces
|
| 17 |
+
from pydantic import BaseModel, Field
|
| 18 |
+
from typing import Optional
|
| 19 |
+
|
| 20 |
+
# ββ Vocabulary & encoding (canonical β must not change between versions) ββββββ
|
| 21 |
+
|
| 22 |
+
KEYWORD_VOCAB = [
|
| 23 |
+
"invoice", "payment", "overdue", "refund",
|
| 24 |
+
"hacked", "breach", "unauthorized", "password",
|
| 25 |
+
"crash", "error", "bug", "slow",
|
| 26 |
+
"lawsuit", "legal", "attorney", "sue",
|
| 27 |
+
"spam", "offer", "win", "free",
|
| 28 |
+
"urgent", "critical","angry", "threat",
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
SENTIMENT_MAP = {"positive": 0, "neutral": 1, "negative": 2}
|
| 32 |
+
CONTEXT_MAP = {"spam": 0, "billing": 1, "tech": 2, "security": 3, "legal": 4}
|
| 33 |
+
OBS_DIM = len(KEYWORD_VOCAB) + len(SENTIMENT_MAP) + len(CONTEXT_MAP) # 32
|
| 34 |
+
|
| 35 |
+
# ββ Label maps ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 36 |
+
URGENCY_LABELS = {0: "General", 1: "Billing", 2: "Security Breach"}
|
| 37 |
+
ROUTING_LABELS = {0: "AI Auto-Reply", 1: "Tech Support", 2: "Legal"}
|
| 38 |
+
RESOLUTION_LABELS = {0: "Archive", 1: "Draft Reply", 2: "Escalate"}
|
| 39 |
+
|
| 40 |
+
# ββ Reward weights ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 41 |
+
REWARD_EXACT = 1.0
|
| 42 |
+
REWARD_PARTIAL_1_WRONG = 0.2
|
| 43 |
+
REWARD_PARTIAL_2_WRONG = 0.1
|
| 44 |
+
PENALTY_SECURITY_MISS = -2.0
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 48 |
+
# Pydantic Typed Models
|
| 49 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 50 |
+
|
| 51 |
+
class EmailAction(BaseModel):
|
| 52 |
+
"""
|
| 53 |
+
The agent's triage decision for one email.
|
| 54 |
+
All three dimensions must be predicted simultaneously.
|
| 55 |
+
"""
|
| 56 |
+
urgency: int = Field(
|
| 57 |
+
..., ge=0, le=2,
|
| 58 |
+
description="0=General | 1=Billing | 2=Security Breach"
|
| 59 |
+
)
|
| 60 |
+
routing: int = Field(
|
| 61 |
+
..., ge=0, le=2,
|
| 62 |
+
description="0=AI Auto-Reply | 1=Tech Support | 2=Legal"
|
| 63 |
+
)
|
| 64 |
+
resolution: int = Field(
|
| 65 |
+
..., ge=0, le=2,
|
| 66 |
+
description="0=Archive | 1=Draft Reply | 2=Escalate"
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
def to_array(self) -> np.ndarray:
|
| 70 |
+
return np.array([self.urgency, self.routing, self.resolution],
|
| 71 |
+
dtype=np.int64)
|
| 72 |
+
|
| 73 |
+
@classmethod
|
| 74 |
+
def from_array(cls, arr: np.ndarray) -> "EmailAction":
|
| 75 |
+
return cls(urgency=int(arr[0]), routing=int(arr[1]),
|
| 76 |
+
resolution=int(arr[2]))
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class EmailObservation(BaseModel):
|
| 80 |
+
"""
|
| 81 |
+
The agent's view of the current email.
|
| 82 |
+
Encoded as a flat float32 vector of length 32.
|
| 83 |
+
"""
|
| 84 |
+
keyword_flags: list[float] = Field(
|
| 85 |
+
..., description=f"Binary flags for {len(KEYWORD_VOCAB)} vocab keywords"
|
| 86 |
+
)
|
| 87 |
+
sentiment_onehot: list[float] = Field(
|
| 88 |
+
..., description="One-hot: [positive, neutral, negative]"
|
| 89 |
+
)
|
| 90 |
+
context_onehot: list[float] = Field(
|
| 91 |
+
..., description="One-hot: [spam, billing, tech, security, legal]"
|
| 92 |
+
)
|
| 93 |
+
# Human-readable metadata (not used by the agent, useful for logging)
|
| 94 |
+
description: str = ""
|
| 95 |
+
difficulty: str = ""
|
| 96 |
+
context_str: str = ""
|
| 97 |
+
sentiment_str: str = ""
|
| 98 |
+
keywords: list[str] = Field(default_factory=list)
|
| 99 |
+
|
| 100 |
+
def to_array(self) -> np.ndarray:
|
| 101 |
+
return np.array(
|
| 102 |
+
self.keyword_flags + self.sentiment_onehot + self.context_onehot,
|
| 103 |
+
dtype=np.float32,
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
class EnvironmentState(BaseModel):
|
| 108 |
+
"""Current snapshot of the environment β returned by state()."""
|
| 109 |
+
step_index: int
|
| 110 |
+
total_emails: int
|
| 111 |
+
emails_remaining: int
|
| 112 |
+
current_email: dict
|
| 113 |
+
cumulative_reward: float
|
| 114 |
+
task: str # "easy" | "medium" | "hard" | "all"
|
| 115 |
+
terminated: bool
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
class StepResult(BaseModel):
|
| 119 |
+
"""Typed return value from step()."""
|
| 120 |
+
observation: EmailObservation
|
| 121 |
+
reward: float
|
| 122 |
+
normalised_reward: float
|
| 123 |
+
terminated: bool
|
| 124 |
+
truncated: bool
|
| 125 |
+
info: dict
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββοΏ½οΏ½ββββββββββββββββββββββ
|
| 129 |
+
# Dataset
|
| 130 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 131 |
+
|
| 132 |
+
EMAIL_DATASET: list[dict] = [
|
| 133 |
+
# ββ Easy: Spam detection βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 134 |
+
{"description": "Spam promo", "keywords": ["spam","offer","win","free"],
|
| 135 |
+
"sentiment": "positive", "context": "spam", "difficulty": "easy",
|
| 136 |
+
"correct_actions": (0, 0, 0)},
|
| 137 |
+
{"description": "Spam lottery", "keywords": ["free","win","offer"],
|
| 138 |
+
"sentiment": "positive", "context": "spam", "difficulty": "easy",
|
| 139 |
+
"correct_actions": (0, 0, 0)},
|
| 140 |
+
{"description": "Routine support", "keywords": ["slow","error"],
|
| 141 |
+
"sentiment": "neutral", "context": "tech", "difficulty": "easy",
|
| 142 |
+
"correct_actions": (0, 1, 1)},
|
| 143 |
+
{"description": "General billing", "keywords": ["invoice","payment"],
|
| 144 |
+
"sentiment": "neutral", "context": "billing", "difficulty": "easy",
|
| 145 |
+
"correct_actions": (1, 0, 1)},
|
| 146 |
+
# ββ Medium: Support routing βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 147 |
+
{"description": "Overdue invoice", "keywords": ["invoice","overdue","payment","angry"],
|
| 148 |
+
"sentiment": "negative", "context": "billing", "difficulty": "medium",
|
| 149 |
+
"correct_actions": (1, 0, 1)},
|
| 150 |
+
{"description": "Refund dispute", "keywords": ["refund","payment","angry"],
|
| 151 |
+
"sentiment": "negative", "context": "billing", "difficulty": "medium",
|
| 152 |
+
"correct_actions": (1, 2, 2)},
|
| 153 |
+
{"description": "App crash report", "keywords": ["crash","bug","error"],
|
| 154 |
+
"sentiment": "negative", "context": "tech", "difficulty": "medium",
|
| 155 |
+
"correct_actions": (0, 1, 1)},
|
| 156 |
+
{"description": "Persistent login bug","keywords": ["bug","password","error"],
|
| 157 |
+
"sentiment": "negative", "context": "tech", "difficulty": "medium",
|
| 158 |
+
"correct_actions": (0, 1, 1)},
|
| 159 |
+
{"description": "Polite legal ultimatum","keywords": ["refund","legal","angry","threat"],
|
| 160 |
+
"sentiment": "negative", "context": "legal", "difficulty": "medium",
|
| 161 |
+
"correct_actions": (2, 2, 2)},
|
| 162 |
+
{"description": "Attorney CC warning", "keywords": ["invoice","overdue","attorney","legal","payment","threat"],
|
| 163 |
+
"sentiment": "negative", "context": "legal", "difficulty": "medium",
|
| 164 |
+
"correct_actions": (2, 2, 2)},
|
| 165 |
+
{"description": "Regulatory complaint","keywords": ["angry","threat","legal"],
|
| 166 |
+
"sentiment": "negative", "context": "legal", "difficulty": "medium",
|
| 167 |
+
"correct_actions": (2, 2, 2)},
|
| 168 |
+
{"description": "SLA breach legal", "keywords": ["breach","legal","threat","angry"],
|
| 169 |
+
"sentiment": "negative", "context": "legal", "difficulty": "medium",
|
| 170 |
+
"correct_actions": (2, 2, 2)},
|
| 171 |
+
# ββ Hard: Phishing & security threats ββββββββββββββββββββββββββββββββββββ
|
| 172 |
+
{"description": "IT audit phish", "keywords": ["password","unauthorized","critical","urgent","threat"],
|
| 173 |
+
"sentiment": "negative", "context": "security","difficulty": "hard",
|
| 174 |
+
"correct_actions": (2, 1, 2)},
|
| 175 |
+
{"description": "Fake invoice portal", "keywords": ["invoice","payment","password","unauthorized","urgent"],
|
| 176 |
+
"sentiment": "neutral", "context": "security","difficulty": "hard",
|
| 177 |
+
"correct_actions": (2, 1, 2)},
|
| 178 |
+
{"description": "HR credential phish", "keywords": ["password","urgent","critical"],
|
| 179 |
+
"sentiment": "neutral", "context": "security","difficulty": "hard",
|
| 180 |
+
"correct_actions": (2, 1, 2)},
|
| 181 |
+
{"description": "Fake suspension", "keywords": ["unauthorized","password","breach","urgent","threat"],
|
| 182 |
+
"sentiment": "negative", "context": "security","difficulty": "hard",
|
| 183 |
+
"correct_actions": (2, 1, 2)},
|
| 184 |
+
{"description": "BEC vendor reply", "keywords": ["password","unauthorized","urgent"],
|
| 185 |
+
"sentiment": "neutral", "context": "security","difficulty": "hard",
|
| 186 |
+
"correct_actions": (2, 1, 2)},
|
| 187 |
+
{"description": "Sign-in alert phish", "keywords": ["unauthorized","password","hacked","breach","urgent"],
|
| 188 |
+
"sentiment": "negative", "context": "security","difficulty": "hard",
|
| 189 |
+
"correct_actions": (2, 1, 2)},
|
| 190 |
+
{"description": "Payroll phish", "keywords": ["payment","password","urgent","threat"],
|
| 191 |
+
"sentiment": "negative", "context": "security","difficulty": "hard",
|
| 192 |
+
"correct_actions": (2, 1, 2)},
|
| 193 |
+
{"description": "License renewal BEC", "keywords": ["password","critical","urgent","error"],
|
| 194 |
+
"sentiment": "neutral", "context": "security","difficulty": "hard",
|
| 195 |
+
"correct_actions": (2, 1, 2)},
|
| 196 |
+
{"description": "GDPR phish", "keywords": ["breach","hacked","password","legal","threat","urgent","unauthorized"],
|
| 197 |
+
"sentiment": "negative", "context": "security","difficulty": "hard",
|
| 198 |
+
"correct_actions": (2, 1, 2)},
|
| 199 |
+
{"description": "Ransomware audit", "keywords": ["hacked","breach","unauthorized","lawsuit","legal","threat","critical","urgent"],
|
| 200 |
+
"sentiment": "negative", "context": "security","difficulty": "hard",
|
| 201 |
+
"correct_actions": (2, 2, 2)},
|
| 202 |
+
{"description": "Data extortion", "keywords": ["hacked","breach","unauthorized","attorney","threat","critical","urgent"],
|
| 203 |
+
"sentiment": "negative", "context": "security","difficulty": "hard",
|
| 204 |
+
"correct_actions": (2, 2, 2)},
|
| 205 |
+
{"description": "Fake law firm", "keywords": ["unauthorized","breach","attorney","lawsuit","legal","threat"],
|
| 206 |
+
"sentiment": "negative", "context": "legal", "difficulty": "hard",
|
| 207 |
+
"correct_actions": (2, 2, 2)},
|
| 208 |
+
{"description": "Account hacked", "keywords": ["hacked","unauthorized","password","urgent","angry"],
|
| 209 |
+
"sentiment": "negative", "context": "security","difficulty": "hard",
|
| 210 |
+
"correct_actions": (2, 1, 2)},
|
| 211 |
+
{"description": "Data breach notice", "keywords": ["breach","unauthorized","critical","threat"],
|
| 212 |
+
"sentiment": "negative", "context": "security","difficulty": "hard",
|
| 213 |
+
"correct_actions": (2, 1, 2)},
|
| 214 |
+
{"description": "Legal lawsuit threat","keywords": ["lawsuit","legal","attorney","threat","angry"],
|
| 215 |
+
"sentiment": "negative", "context": "legal", "difficulty": "hard",
|
| 216 |
+
"correct_actions": (2, 2, 2)},
|
| 217 |
+
{"description": "Ransomware threat", "keywords": ["hacked","threat","critical","urgent","breach"],
|
| 218 |
+
"sentiment": "negative", "context": "security","difficulty": "hard",
|
| 219 |
+
"correct_actions": (2, 2, 2)},
|
| 220 |
+
]
|
| 221 |
+
|
| 222 |
+
# Task splits β used by inference.py for per-task scoring
|
| 223 |
+
TASK_SPLITS: dict[str, list[dict]] = {
|
| 224 |
+
"easy": [e for e in EMAIL_DATASET if e["difficulty"] == "easy"],
|
| 225 |
+
"medium": [e for e in EMAIL_DATASET if e["difficulty"] == "medium"],
|
| 226 |
+
"hard": [e for e in EMAIL_DATASET if e["difficulty"] == "hard"],
|
| 227 |
+
"all": EMAIL_DATASET,
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 232 |
+
# Core Environment
|
| 233 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 234 |
+
|
| 235 |
+
class EmailTriageEnv(gym.Env):
|
| 236 |
+
"""
|
| 237 |
+
OpenEnv-compliant Gymnasium environment for email triage.
|
| 238 |
+
|
| 239 |
+
The agent receives one email per step as a 32-dim observation vector
|
| 240 |
+
and must output three simultaneous discrete decisions.
|
| 241 |
+
|
| 242 |
+
Parameters
|
| 243 |
+
----------
|
| 244 |
+
task : str
|
| 245 |
+
"easy" | "medium" | "hard" | "all" β which email subset to use.
|
| 246 |
+
shuffle : bool
|
| 247 |
+
Shuffle emails on each reset (default True).
|
| 248 |
+
"""
|
| 249 |
+
|
| 250 |
+
metadata = {"render_modes": ["human"]}
|
| 251 |
+
|
| 252 |
+
def __init__(self, task: str = "all", shuffle: bool = True):
|
| 253 |
+
super().__init__()
|
| 254 |
+
|
| 255 |
+
if task not in TASK_SPLITS:
|
| 256 |
+
raise ValueError(f"task must be one of {list(TASK_SPLITS)}. Got '{task}'.")
|
| 257 |
+
|
| 258 |
+
self.task = task
|
| 259 |
+
self.shuffle = shuffle
|
| 260 |
+
self.email_batch = TASK_SPLITS[task]
|
| 261 |
+
|
| 262 |
+
# Gymnasium spaces
|
| 263 |
+
self.action_space = spaces.MultiDiscrete([3, 3, 3])
|
| 264 |
+
self.observation_space = spaces.Box(
|
| 265 |
+
low=0.0, high=1.0, shape=(OBS_DIM,), dtype=np.float32
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
# Internal state
|
| 269 |
+
self._queue: list[dict] = []
|
| 270 |
+
self._current_email: dict = {}
|
| 271 |
+
self._step_idx: int = 0
|
| 272 |
+
self._cumulative_reward: float = 0.0
|
| 273 |
+
self._max_episode_reward: float = len(self.email_batch) * REWARD_EXACT
|
| 274 |
+
|
| 275 |
+
# ββ Encoding helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 276 |
+
|
| 277 |
+
def _encode_to_obs(self, email: dict) -> EmailObservation:
|
| 278 |
+
"""Convert an email dict into a typed EmailObservation."""
|
| 279 |
+
kw_flags = [1.0 if kw in email["keywords"] else 0.0
|
| 280 |
+
for kw in KEYWORD_VOCAB]
|
| 281 |
+
|
| 282 |
+
sentiment_vec = [0.0] * len(SENTIMENT_MAP)
|
| 283 |
+
sentiment_vec[SENTIMENT_MAP[email["sentiment"]]] = 1.0
|
| 284 |
+
|
| 285 |
+
context_vec = [0.0] * len(CONTEXT_MAP)
|
| 286 |
+
context_vec[CONTEXT_MAP[email["context"]]] = 1.0
|
| 287 |
+
|
| 288 |
+
return EmailObservation(
|
| 289 |
+
keyword_flags=kw_flags,
|
| 290 |
+
sentiment_onehot=sentiment_vec,
|
| 291 |
+
context_onehot=context_vec,
|
| 292 |
+
description=email.get("description", ""),
|
| 293 |
+
difficulty=email.get("difficulty", ""),
|
| 294 |
+
context_str=email["context"],
|
| 295 |
+
sentiment_str=email["sentiment"],
|
| 296 |
+
keywords=email["keywords"],
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
def _compute_reward(self, action: np.ndarray, email: dict) -> float:
|
| 300 |
+
"""
|
| 301 |
+
Reward function β same logic as environment.py, priority order:
|
| 302 |
+
1. Security miss β -2.0 (correct urgency=2, predicted otherwise)
|
| 303 |
+
2. Exact match β +1.0
|
| 304 |
+
3. Partial-1 β +0.2 (urgency correct, 1 other wrong)
|
| 305 |
+
4. Partial-2 β +0.1 (urgency correct, both others wrong)
|
| 306 |
+
5. Wrong β 0.0
|
| 307 |
+
"""
|
| 308 |
+
u, r, res = int(action[0]), int(action[1]), int(action[2])
|
| 309 |
+
c = email["correct_actions"]
|
| 310 |
+
|
| 311 |
+
if c[0] == 2 and u != 2:
|
| 312 |
+
return PENALTY_SECURITY_MISS
|
| 313 |
+
if (u, r, res) == c:
|
| 314 |
+
return REWARD_EXACT
|
| 315 |
+
if u == c[0]:
|
| 316 |
+
wrong = sum([r != c[1], res != c[2]])
|
| 317 |
+
return REWARD_PARTIAL_1_WRONG if wrong == 1 else REWARD_PARTIAL_2_WRONG
|
| 318 |
+
return 0.0
|
| 319 |
+
|
| 320 |
+
# ββ OpenEnv API βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 321 |
+
|
| 322 |
+
def reset(
|
| 323 |
+
self,
|
| 324 |
+
*,
|
| 325 |
+
seed: Optional[int] = None,
|
| 326 |
+
options: Optional[dict] = None,
|
| 327 |
+
) -> tuple[np.ndarray, dict]:
|
| 328 |
+
super().reset(seed=seed)
|
| 329 |
+
|
| 330 |
+
self._queue = list(self.email_batch)
|
| 331 |
+
if self.shuffle:
|
| 332 |
+
self.np_random.shuffle(self._queue)
|
| 333 |
+
|
| 334 |
+
self._step_idx = 0
|
| 335 |
+
self._cumulative_reward = 0.0
|
| 336 |
+
self._current_email = self._queue[0]
|
| 337 |
+
|
| 338 |
+
obs = self._encode_to_obs(self._current_email)
|
| 339 |
+
info = {
|
| 340 |
+
"description": self._current_email["description"],
|
| 341 |
+
"difficulty": self._current_email["difficulty"],
|
| 342 |
+
"task": self.task,
|
| 343 |
+
"total_steps": len(self._queue),
|
| 344 |
+
}
|
| 345 |
+
return obs.to_array(), info
|
| 346 |
+
|
| 347 |
+
def step(
|
| 348 |
+
self, action: np.ndarray
|
| 349 |
+
) -> tuple[np.ndarray, float, bool, bool, dict]:
|
| 350 |
+
# Capture current email BEFORE advancing pointer
|
| 351 |
+
scored_email = self._current_email
|
| 352 |
+
raw_reward = self._compute_reward(action, scored_email)
|
| 353 |
+
norm_reward = raw_reward / self._max_episode_reward
|
| 354 |
+
|
| 355 |
+
self._cumulative_reward += norm_reward
|
| 356 |
+
self._step_idx += 1
|
| 357 |
+
terminated = self._step_idx >= len(self._queue)
|
| 358 |
+
|
| 359 |
+
if not terminated:
|
| 360 |
+
self._current_email = self._queue[self._step_idx]
|
| 361 |
+
obs = self._encode_to_obs(self._current_email)
|
| 362 |
+
else:
|
| 363 |
+
obs = self._encode_to_obs(scored_email)
|
| 364 |
+
|
| 365 |
+
# Decode action for info dict
|
| 366 |
+
typed_action = EmailAction.from_array(action)
|
| 367 |
+
correct = scored_email["correct_actions"]
|
| 368 |
+
|
| 369 |
+
info = {
|
| 370 |
+
"raw_reward": raw_reward,
|
| 371 |
+
"correct_actions": correct,
|
| 372 |
+
"predicted": (typed_action.urgency,
|
| 373 |
+
typed_action.routing,
|
| 374 |
+
typed_action.resolution),
|
| 375 |
+
"difficulty": scored_email["difficulty"],
|
| 376 |
+
"description": scored_email.get("description", ""),
|
| 377 |
+
"urgency_label": URGENCY_LABELS[typed_action.urgency],
|
| 378 |
+
"routing_label": ROUTING_LABELS[typed_action.routing],
|
| 379 |
+
"resolution_label": RESOLUTION_LABELS[typed_action.resolution],
|
| 380 |
+
"cumulative_score": self._cumulative_reward,
|
| 381 |
+
}
|
| 382 |
+
return obs.to_array(), norm_reward, terminated, False, info
|
| 383 |
+
|
| 384 |
+
def state(self) -> EnvironmentState:
|
| 385 |
+
"""
|
| 386 |
+
Return a typed snapshot of the current environment state.
|
| 387 |
+
Required by the OpenEnv specification.
|
| 388 |
+
"""
|
| 389 |
+
return EnvironmentState(
|
| 390 |
+
step_index=self._step_idx,
|
| 391 |
+
total_emails=len(self._queue),
|
| 392 |
+
emails_remaining=max(0, len(self._queue) - self._step_idx),
|
| 393 |
+
current_email=self._current_email,
|
| 394 |
+
cumulative_reward=self._cumulative_reward,
|
| 395 |
+
task=self.task,
|
| 396 |
+
terminated=self._step_idx >= len(self._queue),
|
| 397 |
+
)
|
| 398 |
+
|
| 399 |
+
def render(self, mode: str = "human") -> None:
|
| 400 |
+
e = self._current_email
|
| 401 |
+
print(
|
| 402 |
+
f"[{self.task.upper()} | Step {self._step_idx}/{len(self._queue)}] "
|
| 403 |
+
f"{e['description']} | {e['difficulty']} | "
|
| 404 |
+
f"sentiment={e['sentiment']} context={e['context']}"
|
| 405 |
+
)
|
environment.py
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
EmailTriageEnv β Intelligent Email Gatekeeper RL Environment
|
| 3 |
+
============================================================
|
| 4 |
+
Observation : flat Box vector encoding keywords, sentiment, and context.
|
| 5 |
+
Action : MultiDiscrete([3, 3, 3])
|
| 6 |
+
[0] Urgency β 0=General, 1=Billing, 2=Security Breach
|
| 7 |
+
[1] Routing β 0=AI Auto-Reply, 1=Tech Support, 2=Legal
|
| 8 |
+
[2] Resolutionβ 0=Archive, 1=Draft Reply, 2=Escalate to Human
|
| 9 |
+
Reward : +0.5 fully correct | -0.4 wrong priority on crisis email
|
| 10 |
+
Cumulative ideal score over one episode β 1.0 (normalised).
|
| 11 |
+
Episode : one email per step; done after all emails in the batch.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import numpy as np
|
| 15 |
+
import gymnasium as gym
|
| 16 |
+
from gymnasium import spaces
|
| 17 |
+
|
| 18 |
+
# ---------------------------------------------------------------------------
|
| 19 |
+
# Vocabulary & encoding helpers
|
| 20 |
+
# ---------------------------------------------------------------------------
|
| 21 |
+
|
| 22 |
+
# Fixed keyword vocabulary β extend freely.
|
| 23 |
+
KEYWORD_VOCAB = [
|
| 24 |
+
"invoice", "payment", "overdue", "refund", # billing
|
| 25 |
+
"hacked", "breach", "unauthorized", "password", # security
|
| 26 |
+
"crash", "error", "bug", "slow", # tech
|
| 27 |
+
"lawsuit", "legal", "attorney", "sue", # legal
|
| 28 |
+
"spam", "offer", "win", "free", # spam
|
| 29 |
+
"urgent", "critical", "angry", "threat", # sentiment signals
|
| 30 |
+
]
|
| 31 |
+
|
| 32 |
+
# Sentiment classes β index
|
| 33 |
+
SENTIMENT_MAP = {"positive": 0, "neutral": 1, "negative": 2}
|
| 34 |
+
|
| 35 |
+
# Context classes β index
|
| 36 |
+
CONTEXT_MAP = {"spam": 0, "billing": 1, "tech": 2, "security": 3, "legal": 4}
|
| 37 |
+
|
| 38 |
+
# Observation vector length: keyword flags + one-hot sentiment + one-hot context
|
| 39 |
+
OBS_DIM = len(KEYWORD_VOCAB) + len(SENTIMENT_MAP) + len(CONTEXT_MAP)
|
| 40 |
+
|
| 41 |
+
# ---------------------------------------------------------------------------
|
| 42 |
+
# Mock email dataset
|
| 43 |
+
# ---------------------------------------------------------------------------
|
| 44 |
+
# Each entry:
|
| 45 |
+
# keywords : list[str] β subset of KEYWORD_VOCAB
|
| 46 |
+
# sentiment : str β key in SENTIMENT_MAP
|
| 47 |
+
# context : str β key in CONTEXT_MAP
|
| 48 |
+
# difficulty : str β "easy" | "medium" | "hard"
|
| 49 |
+
# correct_actions: tuple β (urgency, routing, resolution)
|
| 50 |
+
# description : str β human-readable label (for debugging)
|
| 51 |
+
|
| 52 |
+
EMAIL_DATASET = [
|
| 53 |
+
# ββ Easy: Spam vs Real ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 54 |
+
{
|
| 55 |
+
"description": "Spam promo",
|
| 56 |
+
"keywords": ["spam", "offer", "win", "free"],
|
| 57 |
+
"sentiment": "positive",
|
| 58 |
+
"context": "spam",
|
| 59 |
+
"difficulty": "easy",
|
| 60 |
+
"correct_actions": (0, 0, 0), # General | AI Auto-Reply | Archive
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"description": "Spam lottery",
|
| 64 |
+
"keywords": ["free", "win", "offer"],
|
| 65 |
+
"sentiment": "positive",
|
| 66 |
+
"context": "spam",
|
| 67 |
+
"difficulty": "easy",
|
| 68 |
+
"correct_actions": (0, 0, 0),
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
"description": "Routine support request",
|
| 72 |
+
"keywords": ["slow", "error"],
|
| 73 |
+
"sentiment": "neutral",
|
| 74 |
+
"context": "tech",
|
| 75 |
+
"difficulty": "easy",
|
| 76 |
+
"correct_actions": (0, 1, 1), # General | Tech Support | Draft Reply
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
"description": "General billing inquiry",
|
| 80 |
+
"keywords": ["invoice", "payment"],
|
| 81 |
+
"sentiment": "neutral",
|
| 82 |
+
"context": "billing",
|
| 83 |
+
"difficulty": "easy",
|
| 84 |
+
"correct_actions": (1, 0, 1), # Billing | AI Auto-Reply | Draft Reply
|
| 85 |
+
},
|
| 86 |
+
# ββ Medium: Billing / Tech context ββββββββββββββββββββββββββββββββββββββ
|
| 87 |
+
{
|
| 88 |
+
"description": "Overdue invoice complaint",
|
| 89 |
+
"keywords": ["invoice", "overdue", "payment", "angry"],
|
| 90 |
+
"sentiment": "negative",
|
| 91 |
+
"context": "billing",
|
| 92 |
+
"difficulty": "medium",
|
| 93 |
+
"correct_actions": (1, 0, 1), # Billing | AI Auto-Reply | Draft Reply
|
| 94 |
+
},
|
| 95 |
+
{
|
| 96 |
+
"description": "Refund dispute",
|
| 97 |
+
"keywords": ["refund", "payment", "angry"],
|
| 98 |
+
"sentiment": "negative",
|
| 99 |
+
"context": "billing",
|
| 100 |
+
"difficulty": "medium",
|
| 101 |
+
"correct_actions": (1, 2, 2), # Billing | Legal | Escalate
|
| 102 |
+
},
|
| 103 |
+
{
|
| 104 |
+
"description": "App crash report",
|
| 105 |
+
"keywords": ["crash", "bug", "error"],
|
| 106 |
+
"sentiment": "negative",
|
| 107 |
+
"context": "tech",
|
| 108 |
+
"difficulty": "medium",
|
| 109 |
+
"correct_actions": (0, 1, 1), # General | Tech Support | Draft Reply
|
| 110 |
+
},
|
| 111 |
+
{
|
| 112 |
+
"description": "Persistent login bug",
|
| 113 |
+
"keywords": ["bug", "password", "error"],
|
| 114 |
+
"sentiment": "negative",
|
| 115 |
+
"context": "tech",
|
| 116 |
+
"difficulty": "medium",
|
| 117 |
+
"correct_actions": (0, 1, 1),
|
| 118 |
+
},
|
| 119 |
+
# ββ Medium: Passive-Aggressive Legal Threats ββββββββββββββββββββββββββββ
|
| 120 |
+
{
|
| 121 |
+
"id": "TC-M-01",
|
| 122 |
+
"description": "Polite refund ultimatum with implied legal action",
|
| 123 |
+
"keywords": ["refund", "legal", "angry", "threat"],
|
| 124 |
+
"sentiment": "negative",
|
| 125 |
+
"context": "legal",
|
| 126 |
+
"difficulty": "medium",
|
| 127 |
+
"correct_actions": (2, 2, 2), # Security | Legal | Escalate
|
| 128 |
+
},
|
| 129 |
+
{
|
| 130 |
+
"id": "TC-M-02",
|
| 131 |
+
"description": "Overdue invoice with attorney CC warning",
|
| 132 |
+
"keywords": ["invoice", "overdue", "attorney", "legal", "payment", "threat"],
|
| 133 |
+
"sentiment": "negative",
|
| 134 |
+
"context": "legal",
|
| 135 |
+
"difficulty": "medium",
|
| 136 |
+
"correct_actions": (2, 2, 2), # Security | Legal | Escalate
|
| 137 |
+
},
|
| 138 |
+
{
|
| 139 |
+
"id": "TC-M-03",
|
| 140 |
+
"description": "Disappointed customer implying regulatory complaint",
|
| 141 |
+
"keywords": ["angry", "threat", "legal"],
|
| 142 |
+
"sentiment": "negative",
|
| 143 |
+
"context": "legal",
|
| 144 |
+
"difficulty": "medium",
|
| 145 |
+
"correct_actions": (2, 2, 2), # Security | Legal | Escalate
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
"id": "TC-M-04",
|
| 149 |
+
"description": "SLA breach complaint with legal remedy language",
|
| 150 |
+
"keywords": ["breach", "legal", "threat", "angry"],
|
| 151 |
+
"sentiment": "negative",
|
| 152 |
+
"context": "legal",
|
| 153 |
+
"difficulty": "medium",
|
| 154 |
+
"correct_actions": (2, 2, 2), # Security | Legal | Escalate
|
| 155 |
+
},
|
| 156 |
+
{
|
| 157 |
+
"id": "TC-M-05",
|
| 158 |
+
"description": "Billing dispute with small claims court mention",
|
| 159 |
+
"keywords": ["refund", "payment", "angry", "lawsuit", "urgent"],
|
| 160 |
+
"sentiment": "negative",
|
| 161 |
+
"context": "legal",
|
| 162 |
+
"difficulty": "medium",
|
| 163 |
+
"correct_actions": (2, 2, 2), # Security | Legal | Escalate
|
| 164 |
+
},
|
| 165 |
+
{
|
| 166 |
+
"id": "TC-M-06",
|
| 167 |
+
"description": "GDPR deletion request with legal consequence warning",
|
| 168 |
+
"keywords": ["legal", "breach", "threat", "angry"],
|
| 169 |
+
"sentiment": "negative",
|
| 170 |
+
"context": "legal",
|
| 171 |
+
"difficulty": "medium",
|
| 172 |
+
"correct_actions": (2, 2, 2), # Security | Legal | Escalate
|
| 173 |
+
},
|
| 174 |
+
{
|
| 175 |
+
"id": "TC-M-07",
|
| 176 |
+
"description": "Subscription cancellation with chargeback and CFPB threat",
|
| 177 |
+
"keywords": ["payment", "refund", "angry", "threat", "legal"],
|
| 178 |
+
"sentiment": "negative",
|
| 179 |
+
"context": "legal",
|
| 180 |
+
"difficulty": "medium",
|
| 181 |
+
"correct_actions": (2, 2, 2), # Security | Legal | Escalate
|
| 182 |
+
},
|
| 183 |
+
{
|
| 184 |
+
"id": "TC-M-08",
|
| 185 |
+
"description": "Vendor threatening IP infringement claim",
|
| 186 |
+
"keywords": ["unauthorized", "legal", "attorney", "threat"],
|
| 187 |
+
"sentiment": "negative",
|
| 188 |
+
"context": "legal",
|
| 189 |
+
"difficulty": "medium",
|
| 190 |
+
"correct_actions": (2, 2, 2), # Security | Legal | Escalate
|
| 191 |
+
},
|
| 192 |
+
# ββ Hard: Subtle Phishing Attempts ββββββββββββββββββββββββββββββββββββββ
|
| 193 |
+
{
|
| 194 |
+
"id": "TC-H-01",
|
| 195 |
+
"description": "IT password reset disguised as routine security audit",
|
| 196 |
+
"keywords": ["password", "unauthorized", "critical", "urgent", "threat"],
|
| 197 |
+
"sentiment": "negative",
|
| 198 |
+
"context": "security",
|
| 199 |
+
"difficulty": "hard",
|
| 200 |
+
"correct_actions": (2, 1, 2), # Security | Tech Support | Escalate
|
| 201 |
+
},
|
| 202 |
+
{
|
| 203 |
+
"id": "TC-H-02",
|
| 204 |
+
"description": "Fake invoice payment portal redirect β credential harvest",
|
| 205 |
+
"keywords": ["invoice", "payment", "password", "unauthorized", "urgent"],
|
| 206 |
+
"sentiment": "neutral",
|
| 207 |
+
"context": "security",
|
| 208 |
+
"difficulty": "hard",
|
| 209 |
+
"correct_actions": (2, 1, 2), # Security | Tech Support | Escalate
|
| 210 |
+
},
|
| 211 |
+
{
|
| 212 |
+
"id": "TC-H-03",
|
| 213 |
+
"description": "HR benefits enrollment with credential capture",
|
| 214 |
+
"keywords": ["password", "urgent", "critical"],
|
| 215 |
+
"sentiment": "neutral",
|
| 216 |
+
"context": "security",
|
| 217 |
+
"difficulty": "hard",
|
| 218 |
+
"correct_actions": (2, 1, 2), # Security | Tech Support | Escalate
|
| 219 |
+
},
|
| 220 |
+
{
|
| 221 |
+
"id": "TC-H-04",
|
| 222 |
+
"description": "Fake account suspension notice with login link",
|
| 223 |
+
"keywords": ["unauthorized", "password", "breach", "urgent", "threat"],
|
| 224 |
+
"sentiment": "negative",
|
| 225 |
+
"context": "security",
|
| 226 |
+
"difficulty": "hard",
|
| 227 |
+
"correct_actions": (2, 1, 2), # Security | Tech Support | Escalate
|
| 228 |
+
},
|
| 229 |
+
{
|
| 230 |
+
"id": "TC-H-05",
|
| 231 |
+
"description": "Vendor onboarding BEC β admin credentials via reply",
|
| 232 |
+
"keywords": ["password", "unauthorized", "urgent"],
|
| 233 |
+
"sentiment": "neutral",
|
| 234 |
+
"context": "security",
|
| 235 |
+
"difficulty": "hard",
|
| 236 |
+
"correct_actions": (2, 1, 2), # Security | Tech Support | Escalate
|
| 237 |
+
},
|
| 238 |
+
{
|
| 239 |
+
"id": "TC-H-06",
|
| 240 |
+
"description": "Fake new sign-in alert β was this you? phish",
|
| 241 |
+
"keywords": ["unauthorized", "password", "hacked", "breach", "urgent"],
|
| 242 |
+
"sentiment": "negative",
|
| 243 |
+
"context": "security",
|
| 244 |
+
"difficulty": "hard",
|
| 245 |
+
"correct_actions": (2, 1, 2), # Security | Tech Support | Escalate
|
| 246 |
+
},
|
| 247 |
+
{
|
| 248 |
+
"id": "TC-H-07",
|
| 249 |
+
"description": "Payroll migration phish β salary interruption fear",
|
| 250 |
+
"keywords": ["payment", "password", "urgent", "threat"],
|
| 251 |
+
"sentiment": "negative",
|
| 252 |
+
"context": "security",
|
| 253 |
+
"difficulty": "hard",
|
| 254 |
+
"correct_actions": (2, 1, 2), # Security | Tech Support | Escalate
|
| 255 |
+
},
|
| 256 |
+
{
|
| 257 |
+
"id": "TC-H-08",
|
| 258 |
+
"description": "Software license renewal β admin credential request",
|
| 259 |
+
"keywords": ["password", "critical", "urgent", "error"],
|
| 260 |
+
"sentiment": "neutral",
|
| 261 |
+
"context": "security",
|
| 262 |
+
"difficulty": "hard",
|
| 263 |
+
"correct_actions": (2, 1, 2), # Security | Tech Support | Escalate
|
| 264 |
+
},
|
| 265 |
+
# ββ Hard: Phishing + Legal Threat Overlay βββββββββββββββββββββββββββββββ
|
| 266 |
+
{
|
| 267 |
+
"id": "TC-H-09",
|
| 268 |
+
"description": "Fake GDPR breach notice β credential harvest via legal fear",
|
| 269 |
+
"keywords": ["breach", "hacked", "password", "legal", "threat", "urgent", "unauthorized"],
|
| 270 |
+
"sentiment": "negative",
|
| 271 |
+
"context": "security",
|
| 272 |
+
"difficulty": "hard",
|
| 273 |
+
"correct_actions": (2, 1, 2), # Security | Tech Support | Escalate
|
| 274 |
+
# NOTE: legal language is the deception layer; credential harvest = Tech Support
|
| 275 |
+
},
|
| 276 |
+
{
|
| 277 |
+
"id": "TC-H-10",
|
| 278 |
+
"description": "Ransomware disguised as software compliance audit",
|
| 279 |
+
"keywords": ["hacked", "breach", "unauthorized", "lawsuit", "legal", "threat", "critical", "urgent"],
|
| 280 |
+
"sentiment": "negative",
|
| 281 |
+
"context": "security",
|
| 282 |
+
"difficulty": "hard",
|
| 283 |
+
"correct_actions": (2, 2, 2), # Security | Legal | Escalate
|
| 284 |
+
},
|
| 285 |
+
{
|
| 286 |
+
"id": "TC-H-11",
|
| 287 |
+
"description": "Extortion β threatening to publish stolen data",
|
| 288 |
+
"keywords": ["hacked", "breach", "unauthorized", "attorney", "threat", "critical", "urgent"],
|
| 289 |
+
"sentiment": "negative",
|
| 290 |
+
"context": "security",
|
| 291 |
+
"difficulty": "hard",
|
| 292 |
+
"correct_actions": (2, 2, 2), # Security | Legal | Escalate
|
| 293 |
+
},
|
| 294 |
+
{
|
| 295 |
+
"id": "TC-H-12",
|
| 296 |
+
"description": "Fake law firm letter claiming evidence of data misuse",
|
| 297 |
+
"keywords": ["unauthorized", "breach", "attorney", "lawsuit", "legal", "threat"],
|
| 298 |
+
"sentiment": "negative",
|
| 299 |
+
"context": "legal",
|
| 300 |
+
"difficulty": "hard",
|
| 301 |
+
"correct_actions": (2, 2, 2), # Security | Legal | Escalate
|
| 302 |
+
},
|
| 303 |
+
# ββ Hard: Crisis / Security threats βββββββββββββββββββββββββββββββββββββ
|
| 304 |
+
{
|
| 305 |
+
"description": "Account hacked β urgent",
|
| 306 |
+
"keywords": ["hacked", "unauthorized", "password", "urgent", "angry"],
|
| 307 |
+
"sentiment": "negative",
|
| 308 |
+
"context": "security",
|
| 309 |
+
"difficulty": "hard",
|
| 310 |
+
"correct_actions": (2, 1, 2), # Security | Tech Support | Escalate
|
| 311 |
+
},
|
| 312 |
+
{
|
| 313 |
+
"description": "Data breach notification",
|
| 314 |
+
"keywords": ["breach", "unauthorized", "critical", "threat"],
|
| 315 |
+
"sentiment": "negative",
|
| 316 |
+
"context": "security",
|
| 317 |
+
"difficulty": "hard",
|
| 318 |
+
"correct_actions": (2, 1, 2),
|
| 319 |
+
},
|
| 320 |
+
{
|
| 321 |
+
"description": "Legal threat β lawsuit",
|
| 322 |
+
"keywords": ["lawsuit", "legal", "attorney", "threat", "angry"],
|
| 323 |
+
"sentiment": "negative",
|
| 324 |
+
"context": "legal",
|
| 325 |
+
"difficulty": "hard",
|
| 326 |
+
"correct_actions": (2, 2, 2), # Security | Legal | Escalate
|
| 327 |
+
},
|
| 328 |
+
{
|
| 329 |
+
"description": "Ransomware / extortion threat",
|
| 330 |
+
"keywords": ["hacked", "threat", "critical", "urgent", "breach"],
|
| 331 |
+
"sentiment": "negative",
|
| 332 |
+
"context": "security",
|
| 333 |
+
"difficulty": "hard",
|
| 334 |
+
"correct_actions": (2, 2, 2), # Security | Legal | Escalate
|
| 335 |
+
},
|
| 336 |
+
]
|
| 337 |
+
|
| 338 |
+
# ---------------------------------------------------------------------------
|
| 339 |
+
# Reward weights β adjust freely
|
| 340 |
+
# ---------------------------------------------------------------------------
|
| 341 |
+
REWARD_CORRECT_FULL = 1.0 # teeno actions bilkul sahi
|
| 342 |
+
REWARD_PARTIAL_ONE_WRONG = 0.2 # urgency sahi + sirf ek dimension galat
|
| 343 |
+
REWARD_PARTIAL_BOTH_WRONG= 0.1 # urgency sahi lekin routing aur resolution dono galat
|
| 344 |
+
PENALTY_MISSED_SECURITY = -2.0 # security breach email miss kiya
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
# ---------------------------------------------------------------------------
|
| 348 |
+
# Environment
|
| 349 |
+
# ---------------------------------------------------------------------------
|
| 350 |
+
|
| 351 |
+
class EmailTriageEnv(gym.Env):
|
| 352 |
+
"""
|
| 353 |
+
Single-email-per-step triage environment.
|
| 354 |
+
|
| 355 |
+
Parameters
|
| 356 |
+
----------
|
| 357 |
+
batch : list[dict] | None
|
| 358 |
+
Custom email batch. Defaults to the full EMAIL_DATASET.
|
| 359 |
+
shuffle : bool
|
| 360 |
+
Shuffle the batch on each reset (default True).
|
| 361 |
+
"""
|
| 362 |
+
|
| 363 |
+
metadata = {"render_modes": ["human"]}
|
| 364 |
+
|
| 365 |
+
def __init__(self, batch: list | None = None, shuffle: bool = True):
|
| 366 |
+
super().__init__()
|
| 367 |
+
|
| 368 |
+
self.email_batch = batch if batch is not None else EMAIL_DATASET
|
| 369 |
+
self.shuffle = shuffle
|
| 370 |
+
|
| 371 |
+
# Action space: [urgency(3), routing(3), resolution(3)]
|
| 372 |
+
self.action_space = spaces.MultiDiscrete([3, 3, 3])
|
| 373 |
+
|
| 374 |
+
# Observation space: binary keyword flags + one-hot sentiment + one-hot context
|
| 375 |
+
self.observation_space = spaces.Box(
|
| 376 |
+
low=0.0, high=1.0, shape=(OBS_DIM,), dtype=np.float32
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
# Internal state (populated by reset)
|
| 380 |
+
self._queue: list[dict] = []
|
| 381 |
+
self._current_email: dict = {}
|
| 382 |
+
self._step_idx: int = 0
|
| 383 |
+
|
| 384 |
+
# Normalisation constant: max possible reward per episode
|
| 385 |
+
self._max_episode_reward = len(self.email_batch) * REWARD_CORRECT_FULL
|
| 386 |
+
|
| 387 |
+
# ------------------------------------------------------------------
|
| 388 |
+
# Helpers
|
| 389 |
+
# ------------------------------------------------------------------
|
| 390 |
+
|
| 391 |
+
def _encode(self, email: dict) -> np.ndarray:
|
| 392 |
+
"""Convert an email dict into a flat float32 observation vector."""
|
| 393 |
+
# Keyword flags (binary)
|
| 394 |
+
kw_flags = np.array(
|
| 395 |
+
[1.0 if kw in email["keywords"] else 0.0 for kw in KEYWORD_VOCAB],
|
| 396 |
+
dtype=np.float32,
|
| 397 |
+
)
|
| 398 |
+
# One-hot sentiment
|
| 399 |
+
sentiment_vec = np.zeros(len(SENTIMENT_MAP), dtype=np.float32)
|
| 400 |
+
sentiment_vec[SENTIMENT_MAP[email["sentiment"]]] = 1.0
|
| 401 |
+
|
| 402 |
+
# One-hot context
|
| 403 |
+
context_vec = np.zeros(len(CONTEXT_MAP), dtype=np.float32)
|
| 404 |
+
context_vec[CONTEXT_MAP[email["context"]]] = 1.0
|
| 405 |
+
|
| 406 |
+
return np.concatenate([kw_flags, sentiment_vec, context_vec])
|
| 407 |
+
|
| 408 |
+
def _compute_reward(self, action: np.ndarray, email: dict) -> float:
|
| 409 |
+
"""
|
| 410 |
+
Strict reward rules (priority order mein check hote hain):
|
| 411 |
+
-2.0 β security breach email ko urgency=2 nahi diya (sabse bada penalty)
|
| 412 |
+
+1.0 β teeno actions bilkul sahi (exact match)
|
| 413 |
+
+0.2 β sirf urgency sahi, routing AUR resolution dono sahi (2 out of 3)
|
| 414 |
+
+0.1 β sirf urgency sahi, baaki ek galat (1 dimension wrong)
|
| 415 |
+
0.0 β urgency hi galat hai (non-security email)
|
| 416 |
+
"""
|
| 417 |
+
urgency = int(action[0])
|
| 418 |
+
routing = int(action[1])
|
| 419 |
+
resolution = int(action[2])
|
| 420 |
+
correct = email["correct_actions"]
|
| 421 |
+
|
| 422 |
+
# Priority 1: Security breach miss β sabse bada crime
|
| 423 |
+
if correct[0] == 2 and urgency != 2:
|
| 424 |
+
return PENALTY_MISSED_SECURITY
|
| 425 |
+
|
| 426 |
+
# Priority 2: Perfect match
|
| 427 |
+
if (urgency, routing, resolution) == correct:
|
| 428 |
+
return REWARD_CORRECT_FULL
|
| 429 |
+
|
| 430 |
+
# Priority 3: Urgency sahi hai β partial credit
|
| 431 |
+
if urgency == correct[0]:
|
| 432 |
+
# Dono routing aur resolution galat hain
|
| 433 |
+
routing_ok = (routing == correct[1])
|
| 434 |
+
resolution_ok = (resolution == correct[2])
|
| 435 |
+
if routing_ok and not resolution_ok:
|
| 436 |
+
return REWARD_PARTIAL_ONE_WRONG # sirf resolution galat
|
| 437 |
+
if resolution_ok and not routing_ok:
|
| 438 |
+
return REWARD_PARTIAL_ONE_WRONG # sirf routing galat
|
| 439 |
+
return REWARD_PARTIAL_BOTH_WRONG # dono galat
|
| 440 |
+
|
| 441 |
+
return 0.0
|
| 442 |
+
|
| 443 |
+
# ------------------------------------------------------------------
|
| 444 |
+
# gymnasium API
|
| 445 |
+
# ------------------------------------------------------------------
|
| 446 |
+
|
| 447 |
+
def reset(self, *, seed: int | None = None, options: dict | None = None):
|
| 448 |
+
super().reset(seed=seed)
|
| 449 |
+
|
| 450 |
+
self._queue = list(self.email_batch)
|
| 451 |
+
if self.shuffle:
|
| 452 |
+
self.np_random.shuffle(self._queue) # uses gymnasium's seeded RNG
|
| 453 |
+
|
| 454 |
+
self._step_idx = 0
|
| 455 |
+
self._current_email = self._queue[self._step_idx]
|
| 456 |
+
|
| 457 |
+
obs = self._encode(self._current_email)
|
| 458 |
+
info = {"description": self._current_email["description"],
|
| 459 |
+
"difficulty": self._current_email["difficulty"]}
|
| 460 |
+
return obs, info
|
| 461 |
+
|
| 462 |
+
def step(self, action: np.ndarray):
|
| 463 |
+
"""
|
| 464 |
+
Process one email triage decision.
|
| 465 |
+
|
| 466 |
+
Returns
|
| 467 |
+
-------
|
| 468 |
+
obs, reward, terminated, truncated, info
|
| 469 |
+
"""
|
| 470 |
+
# ββ Bug Fix: correct_actions PEHLE save karo, PHIR pointer badlao ββ
|
| 471 |
+
# Pehle current email ka ground truth capture karo reward ke saath
|
| 472 |
+
scored_email = self._current_email
|
| 473 |
+
reward = self._compute_reward(action, scored_email)
|
| 474 |
+
normalised_reward = reward / self._max_episode_reward
|
| 475 |
+
|
| 476 |
+
# Ab pointer aage badhao
|
| 477 |
+
self._step_idx += 1
|
| 478 |
+
terminated = self._step_idx >= len(self._queue)
|
| 479 |
+
|
| 480 |
+
if not terminated:
|
| 481 |
+
self._current_email = self._queue[self._step_idx]
|
| 482 |
+
obs = self._encode(self._current_email)
|
| 483 |
+
else:
|
| 484 |
+
obs = self._encode(scored_email) # terminal step pe last obs return karo
|
| 485 |
+
|
| 486 |
+
# info mein SCORED email ka data β agli email ka nahi
|
| 487 |
+
info = {
|
| 488 |
+
"raw_reward": reward,
|
| 489 |
+
"correct_actions": scored_email["correct_actions"],
|
| 490 |
+
"difficulty": scored_email["difficulty"],
|
| 491 |
+
"description": scored_email.get("description", ""),
|
| 492 |
+
}
|
| 493 |
+
return obs, normalised_reward, terminated, False, info
|
| 494 |
+
|
| 495 |
+
def render(self, mode: str = "human"):
|
| 496 |
+
"""Print current email details to stdout."""
|
| 497 |
+
e = self._current_email
|
| 498 |
+
print(f"[Step {self._step_idx}] {e['description']} "
|
| 499 |
+
f"| difficulty={e['difficulty']} "
|
| 500 |
+
f"| sentiment={e['sentiment']} "
|
| 501 |
+
f"| context={e['context']}")
|
inference.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
inference.py β OpenEnv Baseline Inference Script
|
| 3 |
+
=================================================
|
| 4 |
+
Runs the rule-based classifier against all three tasks defined in
|
| 5 |
+
openenv.yaml and reports per-task scores in the 0.0 β 1.0 range.
|
| 6 |
+
|
| 7 |
+
This script proves reproducibility for the hackathon submission.
|
| 8 |
+
Run it with:
|
| 9 |
+
python inference.py
|
| 10 |
+
|
| 11 |
+
Expected output:
|
| 12 |
+
Task 1 [EASY] β Spam Detection : 1.000 β
|
| 13 |
+
Task 2 [MEDIUM] β Support Routing : 0.950 β
|
| 14 |
+
Task 3 [HARD] β Phishing / Security : 0.900 β
|
| 15 |
+
Overall Score : 0.950
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import numpy as np
|
| 19 |
+
from env import (
|
| 20 |
+
EmailTriageEnv,
|
| 21 |
+
EmailAction,
|
| 22 |
+
TASK_SPLITS,
|
| 23 |
+
URGENCY_LABELS,
|
| 24 |
+
ROUTING_LABELS,
|
| 25 |
+
RESOLUTION_LABELS,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# ββ Rule-based classifier (your 95%-accuracy agent) βββββββββββββββββββββββββββ
|
| 29 |
+
|
| 30 |
+
_LEGAL_SECURITY_KW = {"lawsuit", "attorney", "sue", "ransomware", "extortion"}
|
| 31 |
+
_BILLING_ESCALATE_KW = {"refund"}
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _classify(email: dict) -> np.ndarray:
|
| 35 |
+
"""
|
| 36 |
+
Deterministic rule-based classifier.
|
| 37 |
+
Returns np.ndarray([urgency, routing, resolution]).
|
| 38 |
+
"""
|
| 39 |
+
kw = set(email.get("keywords", []))
|
| 40 |
+
context = email.get("context", "").lower()
|
| 41 |
+
|
| 42 |
+
if context == "legal" or kw & {"lawsuit", "attorney", "sue"}:
|
| 43 |
+
return np.array([2, 2, 2], dtype=np.int64)
|
| 44 |
+
|
| 45 |
+
if context == "security":
|
| 46 |
+
if kw & _LEGAL_SECURITY_KW or ("hacked" in kw and "breach" in kw):
|
| 47 |
+
return np.array([2, 2, 2], dtype=np.int64)
|
| 48 |
+
return np.array([2, 1, 2], dtype=np.int64)
|
| 49 |
+
|
| 50 |
+
if context == "billing":
|
| 51 |
+
if kw & _BILLING_ESCALATE_KW:
|
| 52 |
+
return np.array([1, 2, 2], dtype=np.int64)
|
| 53 |
+
return np.array([1, 0, 1], dtype=np.int64)
|
| 54 |
+
|
| 55 |
+
if context == "tech" or kw & {"crash", "error", "bug", "slow"}:
|
| 56 |
+
return np.array([0, 1, 1], dtype=np.int64)
|
| 57 |
+
|
| 58 |
+
return np.array([0, 0, 0], dtype=np.int64)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
# ββ Per-task runner βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 62 |
+
|
| 63 |
+
def run_task(task: str, verbose: bool = False) -> float:
|
| 64 |
+
"""
|
| 65 |
+
Run one full episode on the given task using the rule-based classifier.
|
| 66 |
+
Returns the normalised cumulative score in [0.0, 1.0].
|
| 67 |
+
"""
|
| 68 |
+
env = EmailTriageEnv(task=task, shuffle=False)
|
| 69 |
+
obs, info = env.reset(seed=42)
|
| 70 |
+
|
| 71 |
+
email_queue = list(env._queue) # snapshot before any steps
|
| 72 |
+
cumulative_score = 0.0
|
| 73 |
+
step = 0
|
| 74 |
+
terminated = False
|
| 75 |
+
|
| 76 |
+
task_labels = {
|
| 77 |
+
"easy": "Task 1 [EASY] β Spam Detection ",
|
| 78 |
+
"medium": "Task 2 [MEDIUM] β Support Routing ",
|
| 79 |
+
"hard": "Task 3 [HARD] β Phishing / Security ",
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
if verbose:
|
| 83 |
+
print(f"\n {'β' * 58}")
|
| 84 |
+
print(f" {task_labels.get(task, task.upper())}")
|
| 85 |
+
print(f" {'β' * 58}")
|
| 86 |
+
|
| 87 |
+
while not terminated:
|
| 88 |
+
current_email = email_queue[step]
|
| 89 |
+
action = _classify(current_email)
|
| 90 |
+
|
| 91 |
+
obs, norm_reward, terminated, _, info = env.step(action)
|
| 92 |
+
cumulative_score += norm_reward
|
| 93 |
+
|
| 94 |
+
if verbose:
|
| 95 |
+
ca = info["correct_actions"]
|
| 96 |
+
raw = info["raw_reward"]
|
| 97 |
+
|
| 98 |
+
pred_str = (f"{URGENCY_LABELS[action[0]]} | "
|
| 99 |
+
f"{ROUTING_LABELS[action[1]]} | "
|
| 100 |
+
f"{RESOLUTION_LABELS[action[2]]}")
|
| 101 |
+
corr_str = (f"{URGENCY_LABELS[ca[0]]} | "
|
| 102 |
+
f"{ROUTING_LABELS[ca[1]]} | "
|
| 103 |
+
f"{RESOLUTION_LABELS[ca[2]]}")
|
| 104 |
+
|
| 105 |
+
if raw >= 1.0:
|
| 106 |
+
verdict = "β
EXACT"
|
| 107 |
+
elif raw > 0:
|
| 108 |
+
verdict = "πΆ PARTIAL"
|
| 109 |
+
elif raw < 0:
|
| 110 |
+
verdict = "π¨ SECURITY MISS"
|
| 111 |
+
else:
|
| 112 |
+
verdict = "β WRONG"
|
| 113 |
+
|
| 114 |
+
print(f" #{step+1:02d} [{current_email['difficulty'].upper():<6}] "
|
| 115 |
+
f"{current_email['description'][:35]:<35} "
|
| 116 |
+
f"reward={raw:+.1f} {verdict}")
|
| 117 |
+
if raw < 1.0:
|
| 118 |
+
print(f" Predicted : {pred_str}")
|
| 119 |
+
print(f" Correct : {corr_str}")
|
| 120 |
+
|
| 121 |
+
step += 1
|
| 122 |
+
|
| 123 |
+
# Clamp to [0.0, 1.0] β penalties can push below 0
|
| 124 |
+
final_score = max(0.0, min(1.0, cumulative_score))
|
| 125 |
+
|
| 126 |
+
env_state = env.state()
|
| 127 |
+
assert env_state.terminated, "Episode should be terminated after all steps"
|
| 128 |
+
|
| 129 |
+
return final_score
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
# ββ Main ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 133 |
+
|
| 134 |
+
def main():
|
| 135 |
+
print(f"\n{'β' * 62}")
|
| 136 |
+
print(" EMAIL GATEKEEPER β OpenEnv Baseline Inference")
|
| 137 |
+
print(" Meta x PyTorch Hackathon | Reproducibility Report")
|
| 138 |
+
print(f"{'β' * 62}")
|
| 139 |
+
|
| 140 |
+
tasks = [
|
| 141 |
+
("easy", "Task 1 [EASY] β Spam Detection "),
|
| 142 |
+
("medium", "Task 2 [MEDIUM] β Support Routing "),
|
| 143 |
+
("hard", "Task 3 [HARD] β Phishing / Security "),
|
| 144 |
+
]
|
| 145 |
+
|
| 146 |
+
scores = {}
|
| 147 |
+
all_correct = 0
|
| 148 |
+
all_total = 0
|
| 149 |
+
|
| 150 |
+
for task_id, label in tasks:
|
| 151 |
+
score = run_task(task_id, verbose=True)
|
| 152 |
+
scores[task_id] = score
|
| 153 |
+
|
| 154 |
+
n = len(TASK_SPLITS[task_id])
|
| 155 |
+
all_total += n
|
| 156 |
+
|
| 157 |
+
icon = "β
" if score >= 0.8 else ("β οΈ " if score >= 0.5 else "β")
|
| 158 |
+
print(f"\n {label}: {score:.3f} {icon}")
|
| 159 |
+
|
| 160 |
+
# Overall score = weighted average by number of emails per task
|
| 161 |
+
weights = {t: len(TASK_SPLITS[t]) for t in scores}
|
| 162 |
+
total_weight = sum(weights.values())
|
| 163 |
+
overall = sum(scores[t] * weights[t] / total_weight for t in scores)
|
| 164 |
+
|
| 165 |
+
print(f"\n{'β' * 62}")
|
| 166 |
+
print(f" {'Overall Score (weighted avg)':<42}: {overall:.3f}")
|
| 167 |
+
print(f" {'Total Emails Evaluated':<42}: {total_weight}")
|
| 168 |
+
|
| 169 |
+
# Per-task summary table
|
| 170 |
+
print(f"\n {'Task':<10} {'Emails':>7} {'Score':>8} {'Status':>10}")
|
| 171 |
+
print(f" {'β'*10} {'β'*7} {'β'*8} {'β'*10}")
|
| 172 |
+
for task_id, label in tasks:
|
| 173 |
+
n = len(TASK_SPLITS[task_id])
|
| 174 |
+
s = scores[task_id]
|
| 175 |
+
status = "PASS β
" if s >= 0.8 else ("WARN β οΈ " if s >= 0.5 else "FAIL β")
|
| 176 |
+
print(f" {task_id:<10} {n:>7} {s:>8.3f} {status:>10}")
|
| 177 |
+
|
| 178 |
+
print(f"\n{'β' * 62}\n")
|
| 179 |
+
|
| 180 |
+
# Return scores dict for programmatic use (e.g. CI pipelines)
|
| 181 |
+
return {
|
| 182 |
+
"task_scores": scores,
|
| 183 |
+
"overall": round(overall, 4),
|
| 184 |
+
"total_emails": total_weight,
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
if __name__ == "__main__":
|
| 189 |
+
results = main()
|
openenv.yaml
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# openenv.yaml β Email Gatekeeper RL Environment
|
| 2 |
+
# OpenEnv Specification v1.0
|
| 3 |
+
# Meta x PyTorch Hackathon Submission
|
| 4 |
+
# ============================================================
|
| 5 |
+
|
| 6 |
+
name: email-gatekeeper
|
| 7 |
+
version: "1.0.0"
|
| 8 |
+
description: >
|
| 9 |
+
Intelligent Email Gatekeeper β a Gymnasium-based Reinforcement Learning
|
| 10 |
+
environment where an agent learns to triage emails by simultaneously
|
| 11 |
+
predicting three dimensions: Urgency Category, Department Routing,
|
| 12 |
+
and Resolution Action. Covers 32 scenarios across spam detection,
|
| 13 |
+
support routing, and phishing/security threat identification.
|
| 14 |
+
|
| 15 |
+
author: zerogravity
|
| 16 |
+
license: MIT
|
| 17 |
+
framework: gymnasium
|
| 18 |
+
python_requires: ">=3.10"
|
| 19 |
+
|
| 20 |
+
# ββ Entry point βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 21 |
+
entry_point: "env:EmailTriageEnv"
|
| 22 |
+
|
| 23 |
+
# ββ Observation space βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 24 |
+
observation_space:
|
| 25 |
+
type: Box
|
| 26 |
+
shape: [32]
|
| 27 |
+
dtype: float32
|
| 28 |
+
low: 0.0
|
| 29 |
+
high: 1.0
|
| 30 |
+
description: >
|
| 31 |
+
Flat vector of 32 floats encoding:
|
| 32 |
+
[0:24] Binary keyword flags (24 vocab words)
|
| 33 |
+
[24:27] One-hot sentiment (positive / neutral / negative)
|
| 34 |
+
[27:32] One-hot context (spam / billing / tech / security / legal)
|
| 35 |
+
|
| 36 |
+
# ββ Action space ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 37 |
+
action_space:
|
| 38 |
+
type: MultiDiscrete
|
| 39 |
+
nvec: [3, 3, 3]
|
| 40 |
+
dimensions:
|
| 41 |
+
- name: urgency
|
| 42 |
+
index: 0
|
| 43 |
+
values:
|
| 44 |
+
0: General
|
| 45 |
+
1: Billing
|
| 46 |
+
2: Security Breach
|
| 47 |
+
- name: routing
|
| 48 |
+
index: 1
|
| 49 |
+
values:
|
| 50 |
+
0: AI Auto-Reply
|
| 51 |
+
1: Tech Support
|
| 52 |
+
2: Legal
|
| 53 |
+
- name: resolution
|
| 54 |
+
index: 2
|
| 55 |
+
values:
|
| 56 |
+
0: Archive
|
| 57 |
+
1: Draft Reply
|
| 58 |
+
2: Escalate
|
| 59 |
+
|
| 60 |
+
# ββ Reward function βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 61 |
+
reward:
|
| 62 |
+
description: >
|
| 63 |
+
Strict penalty-based reward. Security breach misses are penalised
|
| 64 |
+
at 4x the magnitude of a correct answer to reflect real-world risk.
|
| 65 |
+
rules:
|
| 66 |
+
- condition: "correct urgency=2 but predicted urgency != 2"
|
| 67 |
+
reward: -2.0
|
| 68 |
+
label: SECURITY_MISS
|
| 69 |
+
- condition: "all three dimensions exactly correct"
|
| 70 |
+
reward: +1.0
|
| 71 |
+
label: EXACT
|
| 72 |
+
- condition: "urgency correct, exactly one other dimension wrong"
|
| 73 |
+
reward: +0.2
|
| 74 |
+
label: PARTIAL_1
|
| 75 |
+
- condition: "urgency correct, both other dimensions wrong"
|
| 76 |
+
reward: +0.1
|
| 77 |
+
label: PARTIAL_2
|
| 78 |
+
- condition: "urgency wrong on non-security email"
|
| 79 |
+
reward: 0.0
|
| 80 |
+
label: WRONG
|
| 81 |
+
normalisation: >
|
| 82 |
+
Each raw reward is divided by (num_emails * 1.0) so the ideal
|
| 83 |
+
cumulative episode score = 1.0
|
| 84 |
+
|
| 85 |
+
# ββ Tasks βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 86 |
+
tasks:
|
| 87 |
+
|
| 88 |
+
- id: easy
|
| 89 |
+
name: "Task 1 β Spam vs Real Email Detection"
|
| 90 |
+
difficulty: easy
|
| 91 |
+
description: >
|
| 92 |
+
Agent must distinguish promotional spam from legitimate emails
|
| 93 |
+
and assign correct General/Billing urgency with appropriate routing.
|
| 94 |
+
num_emails: 4
|
| 95 |
+
email_types:
|
| 96 |
+
- Spam promotional
|
| 97 |
+
- Spam lottery
|
| 98 |
+
- Routine tech support
|
| 99 |
+
- General billing inquiry
|
| 100 |
+
target_score: 1.0
|
| 101 |
+
baseline_score: 1.0
|
| 102 |
+
success_threshold: 0.8
|
| 103 |
+
|
| 104 |
+
- id: medium
|
| 105 |
+
name: "Task 2 β Support Routing & Passive-Aggressive Legal Threats"
|
| 106 |
+
difficulty: medium
|
| 107 |
+
description: >
|
| 108 |
+
Agent must correctly route billing disputes, tech issues, and
|
| 109 |
+
passive-aggressive legal threats that use polite language to
|
| 110 |
+
disguise escalation intent.
|
| 111 |
+
num_emails: 8
|
| 112 |
+
email_types:
|
| 113 |
+
- Overdue invoice complaint
|
| 114 |
+
- Refund dispute
|
| 115 |
+
- App crash report
|
| 116 |
+
- Persistent login bug
|
| 117 |
+
- Polite legal ultimatum
|
| 118 |
+
- Attorney CC warning
|
| 119 |
+
- Regulatory complaint
|
| 120 |
+
- SLA breach legal notice
|
| 121 |
+
target_score: 1.0
|
| 122 |
+
baseline_score: 0.95
|
| 123 |
+
success_threshold: 0.75
|
| 124 |
+
|
| 125 |
+
- id: hard
|
| 126 |
+
name: "Task 3 β Phishing Detection & Security Threat Classification"
|
| 127 |
+
difficulty: hard
|
| 128 |
+
description: >
|
| 129 |
+
Agent must identify subtle phishing attempts disguised as IT notices,
|
| 130 |
+
HR emails, and vendor requests, plus classify ransomware and extortion
|
| 131 |
+
threats that combine security and legal signals.
|
| 132 |
+
num_emails: 16
|
| 133 |
+
email_types:
|
| 134 |
+
- IT audit phishing
|
| 135 |
+
- Fake invoice portal redirect
|
| 136 |
+
- HR credential capture
|
| 137 |
+
- Fake account suspension
|
| 138 |
+
- Business Email Compromise (BEC)
|
| 139 |
+
- Sign-in alert phishing
|
| 140 |
+
- Payroll migration phish
|
| 141 |
+
- License renewal BEC
|
| 142 |
+
- GDPR phishing with legal overlay
|
| 143 |
+
- Ransomware disguised as audit
|
| 144 |
+
- Data extortion threat
|
| 145 |
+
- Fake law firm letter
|
| 146 |
+
- Account hacked urgent
|
| 147 |
+
- Data breach notification
|
| 148 |
+
- Legal lawsuit threat
|
| 149 |
+
- Ransomware extortion
|
| 150 |
+
target_score: 1.0
|
| 151 |
+
baseline_score: 0.90
|
| 152 |
+
success_threshold: 0.70
|
| 153 |
+
|
| 154 |
+
# ββ Environment parameters ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 155 |
+
parameters:
|
| 156 |
+
shuffle:
|
| 157 |
+
type: bool
|
| 158 |
+
default: true
|
| 159 |
+
description: Shuffle email order on each reset for training variety
|
| 160 |
+
task:
|
| 161 |
+
type: str
|
| 162 |
+
default: all
|
| 163 |
+
choices: [easy, medium, hard, all]
|
| 164 |
+
description: Which difficulty subset to load
|
| 165 |
+
|
| 166 |
+
# ββ Dependencies ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 167 |
+
dependencies:
|
| 168 |
+
- gymnasium>=0.29.0
|
| 169 |
+
- numpy>=1.24.0
|
| 170 |
+
- pydantic>=2.0.0
|
| 171 |
+
|
| 172 |
+
# ββ Reproducibility βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 173 |
+
reproducibility:
|
| 174 |
+
seed: 42
|
| 175 |
+
deterministic: true
|
| 176 |
+
baseline_script: inference.py
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
gymnasium
|
| 4 |
+
numpy>
|
| 5 |
+
pydantic
|
| 6 |
+
gradio
|
| 7 |
+
pyyaml
|
| 8 |
+
google-generativeai
|
test_cases_advanced.json
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"_comment": "ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ",
|
| 4 |
+
"_section": "MEDIUM β Passive-Aggressive Legal Threats (polite tone, legal intent)",
|
| 5 |
+
"_comment_end": "ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
|
| 6 |
+
},
|
| 7 |
+
{
|
| 8 |
+
"id": "TC-M-01",
|
| 9 |
+
"difficulty": "medium",
|
| 10 |
+
"category": "Passive-Aggressive Legal Threat",
|
| 11 |
+
"description": "Polite refund ultimatum with implied legal action",
|
| 12 |
+
"raw_email": {
|
| 13 |
+
"subject": "Following Up on My Refund Request β Third Attempt",
|
| 14 |
+
"body": "Dear Support Team, I have now contacted you three times regarding my refund of $340. I have been patient, but I want to make it clear that I am aware of my consumer rights and have already spoken with a legal advisor about my options. I would strongly prefer to resolve this amicably before I am forced to take further steps. Please process my refund within 48 hours. Regards, A. Chen"
|
| 15 |
+
},
|
| 16 |
+
"keywords": ["refund", "legal", "angry", "threat"],
|
| 17 |
+
"sentiment": "negative",
|
| 18 |
+
"context": "legal",
|
| 19 |
+
"deception_tactic": "Polite tone masks explicit legal advisor mention and ultimatum deadline",
|
| 20 |
+
"classifier_trap": "Reads like a billing complaint β 'refund' + 'legal advisor' pushes it to legal escalation",
|
| 21 |
+
"correct_actions": [2, 2, 2],
|
| 22 |
+
"correct_label": "Security Breach | Legal | Escalate",
|
| 23 |
+
"reward_note": "urgency=2 because legal advisor + threat combo; routing=Legal; resolution=Escalate"
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"id": "TC-M-02",
|
| 27 |
+
"difficulty": "medium",
|
| 28 |
+
"category": "Passive-Aggressive Legal Threat",
|
| 29 |
+
"description": "Overdue invoice with attorney CC warning",
|
| 30 |
+
"raw_email": {
|
| 31 |
+
"subject": "Invoice #4821 β Final Notice Before Referral",
|
| 32 |
+
"body": "Hi, This is a courtesy reminder that Invoice #4821 for $1,200 remains overdue by 45 days. We have CC'd our attorney on this correspondence as a precautionary measure. We remain open to resolving this without formal proceedings, but require payment or a written payment plan by Friday. Thank you."
|
| 33 |
+
},
|
| 34 |
+
"keywords": ["invoice", "overdue", "attorney", "legal", "payment", "threat"],
|
| 35 |
+
"sentiment": "negative",
|
| 36 |
+
"context": "legal",
|
| 37 |
+
"deception_tactic": "Framed as a 'courtesy reminder' β attorney CC buried mid-sentence",
|
| 38 |
+
"classifier_trap": "Surface keywords look like billing; 'attorney' CC elevates to legal",
|
| 39 |
+
"correct_actions": [2, 2, 2],
|
| 40 |
+
"correct_label": "Security Breach | Legal | Escalate",
|
| 41 |
+
"reward_note": "attorney keyword present β legal context wins over billing"
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
"id": "TC-M-03",
|
| 45 |
+
"difficulty": "medium",
|
| 46 |
+
"category": "Passive-Aggressive Legal Threat",
|
| 47 |
+
"description": "Disappointed customer implying regulatory complaint",
|
| 48 |
+
"raw_email": {
|
| 49 |
+
"subject": "Extremely Disappointed β Will Be Escalating This Matter",
|
| 50 |
+
"body": "To Whom It May Concern, I am writing to express my deep dissatisfaction with how my case has been handled. I have documented every interaction and intend to escalate this to the relevant regulatory authority if I do not receive a satisfactory response within 5 business days. I trust you understand the implications. Sincerely, M. Okafor"
|
| 51 |
+
},
|
| 52 |
+
"keywords": ["angry", "threat", "legal"],
|
| 53 |
+
"sentiment": "negative",
|
| 54 |
+
"context": "legal",
|
| 55 |
+
"deception_tactic": "No explicit 'lawsuit' word β uses 'regulatory authority' and 'implications' as proxies",
|
| 56 |
+
"classifier_trap": "Vague enough to be misclassified as general complaint; 'escalate' + 'legal' intent is clear",
|
| 57 |
+
"correct_actions": [2, 2, 2],
|
| 58 |
+
"correct_label": "Security Breach | Legal | Escalate",
|
| 59 |
+
"reward_note": "threat + legal context β (2,2,2)"
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
"id": "TC-M-04",
|
| 63 |
+
"difficulty": "medium",
|
| 64 |
+
"category": "Passive-Aggressive Legal Threat",
|
| 65 |
+
"description": "SLA breach complaint with legal remedy language",
|
| 66 |
+
"raw_email": {
|
| 67 |
+
"subject": "SLA Violation β Formal Notice of Breach",
|
| 68 |
+
"body": "Dear Account Manager, As per our signed service agreement, your platform guarantees 99.9% uptime. Your service has been unavailable for 14 hours this month, constituting a material breach of contract. We are formally notifying you of this breach and reserving all legal remedies available to us under the agreement. Please respond within 2 business days. β Legal Dept, Nexus Corp"
|
| 69 |
+
},
|
| 70 |
+
"keywords": ["breach", "legal", "threat", "angry"],
|
| 71 |
+
"sentiment": "negative",
|
| 72 |
+
"context": "legal",
|
| 73 |
+
"deception_tactic": "Uses contract/SLA language β sounds like a tech complaint but is a formal legal notice",
|
| 74 |
+
"classifier_trap": "Contains 'breach' which could trigger security; context is contractual/legal",
|
| 75 |
+
"correct_actions": [2, 2, 2],
|
| 76 |
+
"correct_label": "Security Breach | Legal | Escalate",
|
| 77 |
+
"reward_note": "breach + legal dept sender + formal notice β (2,2,2)"
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"id": "TC-M-05",
|
| 81 |
+
"difficulty": "medium",
|
| 82 |
+
"category": "Passive-Aggressive Legal Threat",
|
| 83 |
+
"description": "Billing dispute with small claims court mention",
|
| 84 |
+
"raw_email": {
|
| 85 |
+
"subject": "Re: Disputed Charge β My Next Step",
|
| 86 |
+
"body": "Hello, I have disputed the $89 charge on my account twice now with no resolution. I want to be transparent: I have already filed the paperwork for small claims court as a precaution. I am still willing to resolve this directly if you can process my refund this week. Please treat this as urgent. β T. Vasquez"
|
| 87 |
+
},
|
| 88 |
+
"keywords": ["refund", "payment", "angry", "lawsuit", "urgent"],
|
| 89 |
+
"sentiment": "negative",
|
| 90 |
+
"context": "legal",
|
| 91 |
+
"deception_tactic": "Framed as cooperative ('still willing to resolve') while small claims filing already done",
|
| 92 |
+
"classifier_trap": "Refund + payment looks like billing dispute; 'lawsuit' (small claims) escalates to legal",
|
| 93 |
+
"correct_actions": [2, 2, 2],
|
| 94 |
+
"correct_label": "Security Breach | Legal | Escalate",
|
| 95 |
+
"reward_note": "lawsuit keyword present β legal routing regardless of billing surface"
|
| 96 |
+
},
|
| 97 |
+
{
|
| 98 |
+
"id": "TC-M-06",
|
| 99 |
+
"difficulty": "medium",
|
| 100 |
+
"category": "Passive-Aggressive Legal Threat",
|
| 101 |
+
"description": "GDPR deletion request with legal consequence warning",
|
| 102 |
+
"raw_email": {
|
| 103 |
+
"subject": "Data Deletion Request β GDPR Article 17",
|
| 104 |
+
"body": "Dear Data Controller, I am formally requesting erasure of all my personal data under GDPR Article 17 (Right to Erasure). Failure to comply within 30 days constitutes a violation subject to regulatory fines of up to 4% of annual turnover. I have retained legal counsel and will file a complaint with the supervisory authority if this is not actioned. Please confirm receipt."
|
| 105 |
+
},
|
| 106 |
+
"keywords": ["legal", "breach", "threat", "angry"],
|
| 107 |
+
"sentiment": "negative",
|
| 108 |
+
"context": "legal",
|
| 109 |
+
"deception_tactic": "Sounds like a routine data request β legal counsel + regulatory fine threat is the real signal",
|
| 110 |
+
"classifier_trap": "Could be misrouted to tech support as a 'data/account request'; it is a legal compliance matter",
|
| 111 |
+
"correct_actions": [2, 2, 2],
|
| 112 |
+
"correct_label": "Security Breach | Legal | Escalate",
|
| 113 |
+
"reward_note": "legal counsel + regulatory threat β (2,2,2)"
|
| 114 |
+
},
|
| 115 |
+
{
|
| 116 |
+
"id": "TC-M-07",
|
| 117 |
+
"difficulty": "medium",
|
| 118 |
+
"category": "Passive-Aggressive Legal Threat",
|
| 119 |
+
"description": "Subscription cancellation with chargeback threat",
|
| 120 |
+
"raw_email": {
|
| 121 |
+
"subject": "Cancellation and Chargeback Notice",
|
| 122 |
+
"body": "Hi, I cancelled my subscription 3 weeks ago but was charged again this month. I have already contacted my bank to initiate a chargeback and have documented all correspondence. If this is not refunded within 24 hours, I will also be filing a complaint with the Consumer Financial Protection Bureau. I expect a prompt response."
|
| 123 |
+
},
|
| 124 |
+
"keywords": ["payment", "refund", "angry", "threat", "legal"],
|
| 125 |
+
"sentiment": "negative",
|
| 126 |
+
"context": "legal",
|
| 127 |
+
"deception_tactic": "Chargeback + CFPB complaint = regulatory/legal action disguised as billing complaint",
|
| 128 |
+
"classifier_trap": "Payment + refund looks like billing; CFPB filing is a legal/regulatory escalation",
|
| 129 |
+
"correct_actions": [2, 2, 2],
|
| 130 |
+
"correct_label": "Security Breach | Legal | Escalate",
|
| 131 |
+
"reward_note": "regulatory body complaint + threat β legal routing"
|
| 132 |
+
},
|
| 133 |
+
{
|
| 134 |
+
"id": "TC-M-08",
|
| 135 |
+
"difficulty": "medium",
|
| 136 |
+
"category": "Passive-Aggressive Legal Threat",
|
| 137 |
+
"description": "Vendor threatening IP infringement claim",
|
| 138 |
+
"raw_email": {
|
| 139 |
+
"subject": "Unauthorized Use of Proprietary Content",
|
| 140 |
+
"body": "Dear Sir/Madam, It has come to our attention that content owned by our company is being used on your platform without a valid license. We have documented evidence of this unauthorized use. Our legal team has been instructed to pursue all available remedies, including injunctive relief and damages, unless we receive written confirmation of removal within 72 hours. β IP Counsel, Meridian Media"
|
| 141 |
+
},
|
| 142 |
+
"keywords": ["unauthorized", "legal", "attorney", "threat"],
|
| 143 |
+
"sentiment": "negative",
|
| 144 |
+
"context": "legal",
|
| 145 |
+
"deception_tactic": "Formal legal letter tone β 'injunctive relief' and 'damages' are legal action signals",
|
| 146 |
+
"classifier_trap": "'Unauthorized' could trigger security breach path; this is IP legal, not account security",
|
| 147 |
+
"correct_actions": [2, 2, 2],
|
| 148 |
+
"correct_label": "Security Breach | Legal | Escalate",
|
| 149 |
+
"reward_note": "attorney + legal team + unauthorized use β (2,2,2)"
|
| 150 |
+
},
|
| 151 |
+
{
|
| 152 |
+
"_comment": "ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ",
|
| 153 |
+
"_section": "HARD β Subtle Phishing Attempts (disguised as legitimate IT/billing/HR emails)",
|
| 154 |
+
"_comment_end": "ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
|
| 155 |
+
},
|
| 156 |
+
{
|
| 157 |
+
"id": "TC-H-01",
|
| 158 |
+
"difficulty": "hard",
|
| 159 |
+
"category": "Subtle Phishing",
|
| 160 |
+
"description": "IT password reset disguised as routine security audit",
|
| 161 |
+
"raw_email": {
|
| 162 |
+
"subject": "Mandatory Annual Security Audit β Action Required",
|
| 163 |
+
"body": "Dear Employee, As part of our annual security compliance audit, all staff are required to verify their account credentials by clicking the link below and re-entering their password. This process is mandatory and must be completed by EOD Friday. Failure to comply may result in temporary account suspension. IT Security Team."
|
| 164 |
+
},
|
| 165 |
+
"keywords": ["password", "unauthorized", "critical", "urgent", "threat"],
|
| 166 |
+
"sentiment": "negative",
|
| 167 |
+
"context": "security",
|
| 168 |
+
"deception_tactic": "Impersonates internal IT; 'mandatory audit' creates urgency; credential harvesting via fake link",
|
| 169 |
+
"classifier_trap": "Reads like a legitimate IT notice β urgency + password request is the phishing signal",
|
| 170 |
+
"correct_actions": [2, 1, 2],
|
| 171 |
+
"correct_label": "Security Breach | Tech Support | Escalate",
|
| 172 |
+
"reward_note": "password + unauthorized access attempt β (2,1,2); no legal keywords so Tech Support not Legal"
|
| 173 |
+
},
|
| 174 |
+
{
|
| 175 |
+
"id": "TC-H-02",
|
| 176 |
+
"difficulty": "hard",
|
| 177 |
+
"category": "Subtle Phishing",
|
| 178 |
+
"description": "Fake invoice payment portal redirect",
|
| 179 |
+
"raw_email": {
|
| 180 |
+
"subject": "Your Invoice #9923 is Ready β Secure Payment Required",
|
| 181 |
+
"body": "Hello, Your invoice for services rendered is now available. To avoid a late payment fee, please log in to our secure payment portal using your existing credentials to review and pay. Note: your current session has expired and you will need to re-enter your password to access the portal. Click here: http://pay-invoices-secure.net/login"
|
| 182 |
+
},
|
| 183 |
+
"keywords": ["invoice", "payment", "password", "unauthorized", "urgent"],
|
| 184 |
+
"sentiment": "neutral",
|
| 185 |
+
"context": "security",
|
| 186 |
+
"deception_tactic": "Billing-themed phish β 'session expired' forces credential re-entry on a fake domain",
|
| 187 |
+
"classifier_trap": "Invoice + payment looks like billing; password re-entry request on external URL is the phishing signal",
|
| 188 |
+
"correct_actions": [2, 1, 2],
|
| 189 |
+
"correct_label": "Security Breach | Tech Support | Escalate",
|
| 190 |
+
"reward_note": "password harvesting attempt embedded in billing context β security wins"
|
| 191 |
+
},
|
| 192 |
+
{
|
| 193 |
+
"id": "TC-H-03",
|
| 194 |
+
"difficulty": "hard",
|
| 195 |
+
"category": "Subtle Phishing",
|
| 196 |
+
"description": "HR benefits enrollment with credential capture",
|
| 197 |
+
"raw_email": {
|
| 198 |
+
"subject": "Open Enrollment Closes Friday β Update Your Benefits Now",
|
| 199 |
+
"body": "Hi Team, The annual benefits open enrollment window closes this Friday. To update your selections, please log in to the HR portal at the link below. You will be asked to verify your identity by entering your employee ID and current password. This is a one-time verification step. Don't miss the deadline β benefits cannot be changed until next year."
|
| 200 |
+
},
|
| 201 |
+
"keywords": ["password", "urgent", "critical"],
|
| 202 |
+
"sentiment": "neutral",
|
| 203 |
+
"context": "security",
|
| 204 |
+
"deception_tactic": "HR impersonation with artificial deadline; password verification step is credential theft",
|
| 205 |
+
"classifier_trap": "Completely routine-sounding HR email; password verification request is the only red flag",
|
| 206 |
+
"correct_actions": [2, 1, 2],
|
| 207 |
+
"correct_label": "Security Breach | Tech Support | Escalate",
|
| 208 |
+
"reward_note": "password request + impersonation pattern β security breach classification"
|
| 209 |
+
},
|
| 210 |
+
{
|
| 211 |
+
"id": "TC-H-04",
|
| 212 |
+
"difficulty": "hard",
|
| 213 |
+
"category": "Subtle Phishing",
|
| 214 |
+
"description": "Fake account suspension notice with login link",
|
| 215 |
+
"raw_email": {
|
| 216 |
+
"subject": "Your Account Has Been Temporarily Suspended",
|
| 217 |
+
"body": "We detected unusual activity on your account and have temporarily suspended access as a precaution. To restore your account, please verify your identity by logging in and confirming your password within 24 hours. If you do not take action, your account will be permanently closed. β Account Security Team"
|
| 218 |
+
},
|
| 219 |
+
"keywords": ["unauthorized", "password", "breach", "urgent", "threat"],
|
| 220 |
+
"sentiment": "negative",
|
| 221 |
+
"context": "security",
|
| 222 |
+
"deception_tactic": "Fear-based urgency β 'permanent closure' threat forces hasty credential submission",
|
| 223 |
+
"classifier_trap": "Looks like a legitimate security alert from the platform itself; it is an inbound phishing attempt",
|
| 224 |
+
"correct_actions": [2, 1, 2],
|
| 225 |
+
"correct_label": "Security Breach | Tech Support | Escalate",
|
| 226 |
+
"reward_note": "unauthorized + password + breach β (2,1,2); account-level attack pattern"
|
| 227 |
+
},
|
| 228 |
+
{
|
| 229 |
+
"id": "TC-H-05",
|
| 230 |
+
"difficulty": "hard",
|
| 231 |
+
"category": "Subtle Phishing",
|
| 232 |
+
"description": "Vendor onboarding form requesting system credentials",
|
| 233 |
+
"raw_email": {
|
| 234 |
+
"subject": "New Vendor Onboarding β Integration Credentials Needed",
|
| 235 |
+
"body": "Dear IT Team, We are completing the integration setup for your new vendor account. To finalize the API connection, please reply with your system admin username and current password so we can configure access on our end. This is a standard step in our onboarding process. Please respond by tomorrow to avoid delays."
|
| 236 |
+
},
|
| 237 |
+
"keywords": ["password", "unauthorized", "urgent"],
|
| 238 |
+
"sentiment": "neutral",
|
| 239 |
+
"context": "security",
|
| 240 |
+
"deception_tactic": "Business email compromise (BEC) β requests credentials via reply, not a link, to bypass URL filters",
|
| 241 |
+
"classifier_trap": "No suspicious link β credential request via email reply is the attack vector",
|
| 242 |
+
"correct_actions": [2, 1, 2],
|
| 243 |
+
"correct_label": "Security Breach | Tech Support | Escalate",
|
| 244 |
+
"reward_note": "password request via email reply = credential phishing β (2,1,2)"
|
| 245 |
+
},
|
| 246 |
+
{
|
| 247 |
+
"id": "TC-H-06",
|
| 248 |
+
"difficulty": "hard",
|
| 249 |
+
"category": "Subtle Phishing",
|
| 250 |
+
"description": "Fake security alert asking to 'confirm' recent login",
|
| 251 |
+
"raw_email": {
|
| 252 |
+
"subject": "New Sign-In Detected on Your Account",
|
| 253 |
+
"body": "We noticed a new sign-in to your account from an unrecognized device in Frankfurt, Germany. If this was you, no action is needed. If this was NOT you, please secure your account immediately by clicking below and resetting your password. Act within 1 hour to prevent unauthorized access."
|
| 254 |
+
},
|
| 255 |
+
"keywords": ["unauthorized", "password", "hacked", "breach", "urgent"],
|
| 256 |
+
"sentiment": "negative",
|
| 257 |
+
"context": "security",
|
| 258 |
+
"deception_tactic": "Classic 'was this you?' phish β creates panic about unauthorized access to harvest credentials",
|
| 259 |
+
"classifier_trap": "Indistinguishable from a real security alert; all security keywords present",
|
| 260 |
+
"correct_actions": [2, 1, 2],
|
| 261 |
+
"correct_label": "Security Breach | Tech Support | Escalate",
|
| 262 |
+
"reward_note": "unauthorized + hacked + password reset request β (2,1,2)"
|
| 263 |
+
},
|
| 264 |
+
{
|
| 265 |
+
"id": "TC-H-07",
|
| 266 |
+
"difficulty": "hard",
|
| 267 |
+
"category": "Subtle Phishing",
|
| 268 |
+
"description": "Payroll system update requiring credential re-verification",
|
| 269 |
+
"raw_email": {
|
| 270 |
+
"subject": "Payroll System Migration β Re-verify Your Account",
|
| 271 |
+
"body": "Dear Staff, We are migrating to a new payroll platform this weekend. To ensure your salary payment is not interrupted, you must re-verify your account credentials before Thursday. Please use the link below to log in with your current username and password. Accounts not verified by the deadline will be temporarily suspended pending manual review. β Payroll Department"
|
| 272 |
+
},
|
| 273 |
+
"keywords": ["payment", "password", "urgent", "threat"],
|
| 274 |
+
"sentiment": "negative",
|
| 275 |
+
"context": "security",
|
| 276 |
+
"deception_tactic": "Salary interruption fear + deadline + credential request β high-pressure BEC pattern",
|
| 277 |
+
"classifier_trap": "'Payment' keyword could route to billing; payroll phishing is a security/credential threat",
|
| 278 |
+
"correct_actions": [2, 1, 2],
|
| 279 |
+
"correct_label": "Security Breach | Tech Support | Escalate",
|
| 280 |
+
"reward_note": "password + payment context is phishing not billing β security classification"
|
| 281 |
+
},
|
| 282 |
+
{
|
| 283 |
+
"id": "TC-H-08",
|
| 284 |
+
"difficulty": "hard",
|
| 285 |
+
"category": "Subtle Phishing",
|
| 286 |
+
"description": "Software license renewal with admin credential request",
|
| 287 |
+
"raw_email": {
|
| 288 |
+
"subject": "License Renewal β Admin Verification Required",
|
| 289 |
+
"body": "Your enterprise software license expires in 3 days. To process the renewal without service interruption, our licensing team requires your system administrator credentials to apply the new license key directly. Please reply with your admin username and password at your earliest convenience. β Enterprise Licensing Support"
|
| 290 |
+
},
|
| 291 |
+
"keywords": ["password", "critical", "urgent", "error"],
|
| 292 |
+
"sentiment": "neutral",
|
| 293 |
+
"context": "security",
|
| 294 |
+
"deception_tactic": "Legitimate-sounding vendor request; 'apply license key' justification for admin credential theft",
|
| 295 |
+
"classifier_trap": "Routine renewal framing hides the admin credential request in the body",
|
| 296 |
+
"correct_actions": [2, 1, 2],
|
| 297 |
+
"correct_label": "Security Breach | Tech Support | Escalate",
|
| 298 |
+
"reward_note": "admin password request = credential phishing β (2,1,2)"
|
| 299 |
+
},
|
| 300 |
+
{
|
| 301 |
+
"_comment": "ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ",
|
| 302 |
+
"_section": "HARD β Combined: Phishing with Legal Threat Overlay (most dangerous category)",
|
| 303 |
+
"_comment_end": "ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββοΏ½οΏ½βββββββββββββ"
|
| 304 |
+
},
|
| 305 |
+
{
|
| 306 |
+
"id": "TC-H-09",
|
| 307 |
+
"difficulty": "hard",
|
| 308 |
+
"category": "Phishing + Legal Threat",
|
| 309 |
+
"description": "Fake GDPR breach notification demanding immediate credential verification",
|
| 310 |
+
"raw_email": {
|
| 311 |
+
"subject": "URGENT: Data Breach Affecting Your Account β Legal Obligation to Act",
|
| 312 |
+
"body": "Dear Account Holder, We are legally obligated to inform you that your account data may have been exposed in a recent breach. Under GDPR Article 33, you must verify your identity within 72 hours to confirm your data has not been compromised. Please log in immediately using the secure link and re-enter your password to initiate the verification process. Failure to act may result in your account being flagged for regulatory review. β Data Protection Officer"
|
| 313 |
+
},
|
| 314 |
+
"keywords": ["breach", "hacked", "password", "legal", "threat", "urgent", "unauthorized"],
|
| 315 |
+
"sentiment": "negative",
|
| 316 |
+
"context": "security",
|
| 317 |
+
"deception_tactic": "Weaponizes GDPR compliance language to force credential submission; DPO impersonation",
|
| 318 |
+
"classifier_trap": "Legal + breach + password all present β legal keywords could misdirect to (2,2,2) but credential harvest = Tech Support response",
|
| 319 |
+
"correct_actions": [2, 1, 2],
|
| 320 |
+
"correct_label": "Security Breach | Tech Support | Escalate",
|
| 321 |
+
"reward_note": "breach+password+hacked = account-level security attack; legal language is the deception layer not the context"
|
| 322 |
+
},
|
| 323 |
+
{
|
| 324 |
+
"id": "TC-H-10",
|
| 325 |
+
"difficulty": "hard",
|
| 326 |
+
"category": "Phishing + Legal Threat",
|
| 327 |
+
"description": "Ransomware demand disguised as a software audit notice",
|
| 328 |
+
"raw_email": {
|
| 329 |
+
"subject": "Software Compliance Audit β Unlicensed Software Detected",
|
| 330 |
+
"body": "Dear IT Administrator, Our automated audit system has detected unlicensed software installations on your network. Under the Software Protection Act, continued use constitutes copyright infringement. To avoid legal proceedings and a fine of $50,000, you must click the link below to download our compliance verification tool and run it with administrator privileges within 24 hours. β BSA Compliance Division"
|
| 331 |
+
},
|
| 332 |
+
"keywords": ["hacked", "breach", "unauthorized", "lawsuit", "legal", "threat", "critical", "urgent"],
|
| 333 |
+
"sentiment": "negative",
|
| 334 |
+
"context": "security",
|
| 335 |
+
"deception_tactic": "BSA impersonation + legal fine threat to trick admin into running malware with elevated privileges",
|
| 336 |
+
"classifier_trap": "Legal fine language could route to (2,2,2); but the attack vector is malware execution = security + legal",
|
| 337 |
+
"correct_actions": [2, 2, 2],
|
| 338 |
+
"correct_label": "Security Breach | Legal | Escalate",
|
| 339 |
+
"reward_note": "hacked+breach+lawsuit+legal = ransomware with legal overlay β (2,2,2)"
|
| 340 |
+
},
|
| 341 |
+
{
|
| 342 |
+
"id": "TC-H-11",
|
| 343 |
+
"difficulty": "hard",
|
| 344 |
+
"category": "Phishing + Legal Threat",
|
| 345 |
+
"description": "Extortion email threatening to publish stolen data",
|
| 346 |
+
"raw_email": {
|
| 347 |
+
"subject": "We Have Your Data β Read Carefully",
|
| 348 |
+
"body": "We have obtained a copy of your customer database through a vulnerability in your system. We are prepared to sell this data to your competitors or publish it publicly unless you transfer $15,000 in cryptocurrency within 48 hours. We have also retained an attorney to ensure our anonymity is protected. Do not contact law enforcement. This is your only warning."
|
| 349 |
+
},
|
| 350 |
+
"keywords": ["hacked", "breach", "unauthorized", "attorney", "threat", "critical", "urgent"],
|
| 351 |
+
"sentiment": "negative",
|
| 352 |
+
"context": "security",
|
| 353 |
+
"deception_tactic": "Classic extortion with attorney mention to add credibility and deter police contact",
|
| 354 |
+
"classifier_trap": "Attorney keyword present β but this is a security extortion event requiring both Tech + Legal response",
|
| 355 |
+
"correct_actions": [2, 2, 2],
|
| 356 |
+
"correct_label": "Security Breach | Legal | Escalate",
|
| 357 |
+
"reward_note": "hacked+breach+attorney+threat = ransomware/extortion β (2,2,2)"
|
| 358 |
+
},
|
| 359 |
+
{
|
| 360 |
+
"id": "TC-H-12",
|
| 361 |
+
"difficulty": "hard",
|
| 362 |
+
"category": "Phishing + Legal Threat",
|
| 363 |
+
"description": "Fake law firm letter claiming evidence of data misuse",
|
| 364 |
+
"raw_email": {
|
| 365 |
+
"subject": "Notice of Pending Legal Action β Hargrove & Associates",
|
| 366 |
+
"body": "Dear Sir/Madam, This firm represents a class of individuals whose personal data was allegedly misused by your organization. We have obtained forensic evidence of unauthorized data processing and intend to file suit unless we receive a response from your legal counsel within 10 business days. Please ensure this notice is forwarded to your Data Protection Officer and General Counsel immediately. β J. Hargrove, Esq."
|
| 367 |
+
},
|
| 368 |
+
"keywords": ["unauthorized", "breach", "attorney", "lawsuit", "legal", "threat"],
|
| 369 |
+
"sentiment": "negative",
|
| 370 |
+
"context": "legal",
|
| 371 |
+
"deception_tactic": "Convincing law firm letterhead format; 'forensic evidence' claim creates panic; may be fake to extract settlement",
|
| 372 |
+
"classifier_trap": "Looks identical to a real legal notice β all legal keywords present; requires Legal team verification",
|
| 373 |
+
"correct_actions": [2, 2, 2],
|
| 374 |
+
"correct_label": "Security Breach | Legal | Escalate",
|
| 375 |
+
"reward_note": "attorney+lawsuit+legal+breach = legal context confirmed β (2,2,2)"
|
| 376 |
+
}
|
| 377 |
+
]
|