rushi07ai/email-triage-openenv
๐ง Email Triage OpenEnv
A real-world OpenEnv-compatible environment where AI agents learn to triage a business support inbox: classify urgency, route to departments, and draft professional replies.
Domain: Customer support email operations โ a task millions of humans do daily.
Why Email Triage?
Email triage is genuinely complex for AI agents:
- Requires natural language understanding across domains (billing, technical, returns)
- Tests priority judgment (what's truly urgent vs. just loud?)
- Demands professional communication (Task 3)
- Has clear ground-truth labels enabling deterministic, reproducible scoring
Environment Specification
Tasks
Task 1 โ task_classify (Easy)
Goal: Classify the urgency of each email.
Action fields required:
urgency:urgent|normal|low
Grader: Full credit (1.0) for correct urgency, 0.0 for wrong. Small penalty (-0.05) if unnecessary fields are included.
Expected score: Frontier LLMs ~0.85+, keyword baseline ~0.70
Task 2 โ task_route (Medium)
Goal: Classify urgency AND route to the correct department.
Action fields required:
urgency:urgent|normal|lowdepartment:billing|technical|returns|general
Grader: 0.5 for urgency + 0.5 for department. Penalty if response is included.
Expected score: Frontier LLMs ~0.70+, keyword baseline ~0.55
Task 3 โ task_respond (Hard)
Goal: Classify urgency, route to department, AND draft a professional customer reply.
Action fields required:
urgency:urgent|normal|lowdepartment:billing|technical|returns|generalresponse: string โ your draft reply to the customer
Grader breakdown (total = 1.0):
- Urgency correct โ +0.25
- Department correct โ +0.25
- Response provided โ +0.10
- Response โฅ50 words โ +0.05
- Response โฅ100 words โ +0.05
- โฅ1 required keyword present โ +0.10
- โฅ3 required keywords present โ +0.10
- All required keywords present โ +0.10
- Professional closing (Best regards, Sincerely, etc.) โ +0.05
- No unfilled placeholders โ +0.05
Expected score: Frontier LLMs ~0.65+, keyword baseline ~0.45
Action Space
class TriageAction(BaseModel):
urgency: Literal["urgent", "normal", "low"] # required always
department: Optional[Literal["billing", "technical",
"returns", "general"]] # required for task 2+3
response: Optional[str] # required for task 3
reasoning: Optional[str] # optional, not scoredObservation Space
class EmailObservation(BaseModel):
email_id: str # e.g. "E001"
subject: str # email subject line
body: str # full email body
sender: str # sender email address
timestamp: str # ISO 8601
task_id: str # active task
task_description: str # what the agent must do
step_feedback: str # human-readable feedback on last action
reward: float # reward for last step (0.0 on reset)
done: bool # True when episode ends
score: float # running cumulative average scoreReward Function Design
Rewards are dense โ the agent receives a signal after every email, not just at the end. This enables:
- Gradient flow across the entire trajectory
- Clear partial credit for multi-component tasks
- Interpretable per-step debugging
Penalties discourage providing unnecessary fields (verbosity) and missing the response entirely on Task 3.
API Endpoints
Quick Start
1. Local via Docker
# Build
docker build -t email-triage-openenv .
# Run
docker run -p 7860:7860 email-triage-openenv
# Health check
curl http://localhost:7860/health2. Local via Python (no Docker)
pip install -r requirements.txt
uvicorn server.app:app --host 0.0.0.0 --port 78603. Run an episode with curl
# Start episode
curl -X POST http://localhost:7860/reset \
-H "Content-Type: application/json" \
-d '{"task_id": "task_classify", "seed": 42}'
# Submit action
curl -X POST http://localhost:7860/step \
-H "Content-Type: application/json" \
-d '{"urgency": "urgent"}'
# Check state
curl http://localhost:7860/state4. Python client
from client import EmailTriageClient
with EmailTriageClient("http://localhost:7860") as c:
obs = c.reset("task_respond", seed=42)
while not obs.get("done"):
print(f"Email: {obs['subject']}")
result = c.step(
urgency="urgent",
department="billing",
response="Dear Customer, I apologize for the issue. "
"Our billing team will resolve this within 24 hours. "
"Best regards, Support Team"
)
obs = result["observation"]
print(f" Reward: {result['reward']:.3f}")5. Run the LLM inference baseline
export OPENAI_API_KEY=sk-...
export ENV_BASE_URL=http://localhost:7860
python baseline.py
# Or against your HF Space:
export ENV_BASE_URL=https://YOUR-USERNAME-email-triage-openenv.hf.space
python baseline.pyBaseline Scores
Episode length: 20 emails per episode (dataset of 20 realistic support emails).
Project Structure
email-triage-env/
โโโ server/
โ โโโ __init__.py
โ โโโ app.py # FastAPI server (all endpoints)
โ โโโ environment.py # Core env: reset() / step() / state()
โ โโโ models.py # Pydantic: TriageAction, EmailObservation, TriageState
โ โโโ email_dataset.py # 10 realistic emails with ground-truth labels
โ โโโ graders.py # Deterministic graders for all 3 tasks
โ โโโ baseline_agent.py # Keyword heuristic agent (for /baseline endpoint)
โโโ client.py # Python HTTP client
โโโ baseline.py # LLM baseline (OpenAI API)
โโโ openenv.yaml # OpenEnv metadata manifest
โโโ requirements.txt
โโโ Dockerfile
โโโ README.mdRunning Tests
pip install -r requirements.txt
pytest -qDeploying to Hugging Face Spaces
hf auth login
hf repos create email-triage-openenv --type space --space-sdk docker
git remote add hf https://huggingface.co/spaces/YOUR_USERNAME/email-triage-openenv
git push -u hf mainThe Space will automatically build and run the Dockerfile. Port 7860 is used by default.
License
Apache 2.0
