πŸ“§ PersonalMail-RL: Teaching LLMs to Handle Personal Emails Through Reinforcement Learning

Theme: #3.2 Personalized Tasks Β· OpenEnv Hackathon Apr 2026
Stack: OpenEnv Β· TRL GRPO Β· Unsloth Β· Qwen2.5-1.5B-Instruct
Demo: πŸš€ Live on HuggingFace Spaces
Model: πŸ€— ykshrestha/personalmail-rl-qwen
Code: πŸ’» GitHub Blog link: πŸ’» Blog Youtube Video: πŸ’»Youtube Link PPT link: Link Training Scripts link: Link Blog link: Link

⚠️ To view live inference results in the demo, please select Nvidia A100 Large GPU in HuggingFace Space Settings before running.


🎯 The Problem

Personal emails are not equal. A message from your manager about a missed deadline is completely different from a dinner invite from a friend. Yet most LLMs treat every email the same β€” they reply generically, miss urgency signals, use the wrong tone, and sometimes reply to emails that should never be replied to (like spam).

What we built: A 2-step RL environment where a model learns to first understand an email, then respond to it appropriately β€” rewarded by 6 independent verifiable functions.

Why RL? You cannot write ideal replies for every scenario in advance. But you can verify whether a reply is good: Does it have a proper greeting? Does it address the key points? Is the tone right? RL with verifiable rewards is the perfect fit.


πŸ—οΈ Architecture & Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     PersonalMail-RL                          β”‚
β”‚                                                              β”‚
β”‚   Email Scenario (25 scenarios Γ— 3 difficulties)            β”‚
β”‚          β”‚                                                   β”‚
β”‚          β–Ό                                                   β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   Step 1 Action    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚   β”‚   Agent     β”‚ ─────────────────► β”‚  Classification  β”‚  β”‚
β”‚   β”‚ (Qwen2.5    β”‚                    β”‚  Reward (2 fns)  β”‚  β”‚
β”‚   β”‚  1.5B-RL)   β”‚ ◄───────────────── β”‚  Score: 0-1      β”‚  β”‚
β”‚   β”‚             β”‚   Reward + Obs      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚   β”‚             β”‚                                           β”‚
β”‚   β”‚             β”‚   Step 2 Action    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚   β”‚             β”‚ ─────────────────► β”‚  Reply Quality   β”‚  β”‚
β”‚   β”‚             β”‚                    β”‚  Reward (5 fns)  β”‚  β”‚
β”‚   β”‚             β”‚ ◄───────────────── β”‚  Score: 0-1      β”‚  β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   Reward + Done    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                                              β”‚
β”‚   Episode Total = 0.30 Γ— Step1 + 0.70 Γ— Step2              β”‚
β”‚                                                              β”‚
β”‚   OpenEnv (FastAPI) β†’ TRL GRPO β†’ Unsloth QLoRA             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Step 1 β€” Email Classification

The agent receives the email and outputs:

{
  "urgency": "high | medium | low",
  "category": "work | personal | social | spam",
  "requires_reply": true | false,
  "reason": "2-3 sentence explanation"
}

Step 2 β€” Reply Drafting

Using its own classification as context, the agent drafts:

{
  "tone": "professional | friendly | assertive | apologetic | none",
  "reply_text": "Full email reply here"
}

🌍 Environment Design

Built with OpenEnv + FastAPI β€” fully OpenEnv spec compatible.

OpenEnv Interface

env.reset(scenario_id=None)   # Start new episode, receive email
env.step(action)               # Submit classification or reply β†’ get reward
env.state()                    # Current episode state for monitoring

Scenario Dataset

25 handcrafted real-world email scenarios Γ— 3 difficulty levels:

Difficulty Count Examples
🟒 Easy 10 Clear work emails, obvious spam, simple social invites
🟑 Medium 8 Ambiguous tone, mixed urgency, passive-aggressive messages
πŸ”΄ Hard 7 Conflict emails, emotional situations, multi-intent messages

Curriculum Learning

Training starts with easy scenarios only β†’ progressively adds medium β†’ then hard. This ensures the model sees successful trajectories early and avoids zero-reward stalls.


πŸ† 7 Independent Reward Functions

Using multiple independent reward functions is the core defense against reward hacking. Here is every function implemented:

Step 1 Rewards β€” Classification (30% of Episode)

Reward 1: classification_accuracy β€” 21% of Episode Total

βœ… Urgency correct (high/medium/low)            β†’ +0.34
βœ… Category correct (work/personal/social/spam) β†’ +0.33
βœ… Requires_reply correct (true/false)          β†’ +0.33

Reward 2: classification_format β€” 9% of Episode Total

βœ… All 4 required JSON fields present           β†’ base score
βœ… Reason field β‰₯ 5 words                       β†’ +0.10 bonus

Step 2 Rewards β€” Reply Quality (70% of Episode)

Reward 3: reply_relevance β€” 28% of Episode Total

βœ… Must-include keywords matched in reply
   Score = keywords_matched / total_keywords
βœ… Spam emails auto-score 1.0 (correct to not reply)

Reward 4: reply_format β€” 24.5% of Episode Total

βœ… Greeting present (Dear/Hi/Hello...)          β†’ +0.30
βœ… Body has β‰₯ 2 substantive sentences           β†’ +0.35
βœ… Closing present (Best/Regards/Thanks...)     β†’ +0.35

Reward 5: reply_length β€” 17.5% of Episode Total

Target: 40–150 words
βœ… 40-150 words  β†’ 1.0  (perfect)
⚠️ 25-40 words  β†’ 0.7  (short)
⚠️ 150-250 words β†’ 0.7  (long)
❌ <25 words    β†’ 0.3  (too short)
❌ >250 words   β†’ 0.4  (too long)

Reward 6: tone_matching β€” 15% of Episode Total

Tone detected via lexical signals:
professional β†’ "please", "kindly", "regards", "sincerely"
friendly     β†’ "hey", "cheers", "sounds great", "would love"
assertive    β†’ "I expect", "immediately", "by end of"
apologetic   β†’ "I apologize", "deeply sorry", "I take full responsibility"

Reward 7: reply_clarity β€” 15% of Episode Total

βœ… Actionable signals (timelines, action verbs, coordination phrases)
❌ Penalizes vague filler ("noted", "will revert", "as soon as possible")
βœ… Unique word ratio β‰₯ 0.55 (not repetitive)

Episode Total Formula

Episode = 0.30 Γ— Step1_total + 0.70 Γ— Step2_total

Reply quality is weighted 70% because drafting a contextually appropriate reply is the harder, more valuable task.


πŸ›‘οΈ Anti-Reward-Hacking Measures

1. 7 Independent Signals β€” Cannot game any one without the others catching it.

2. Timeout Penalty β€” Episodes exceeding 120 seconds receive βˆ’0.5 penalty.

3. Duplicate Action Detection β€” Repeated identical actions are penalized.

4. Spam Safeguard β€” Spam emails reward not replying. Replying to spam scores 0.

5. Reply Clarity Guard β€” Penalizes vague "polite-only" hacks like "noted, will revert."

6. Real Discovery During Training:

During GRPO training, we discovered the model learned to output "reason": ["some text"] (a list) instead of "reason": "some text" (a string) to exploit our format reward. The field was "present" but not actually a valid string. We identified this as reward hacking and fixed reward functions to be type-safe β€” a real RL failure mode caught and resolved.


πŸ”¬ Training Pipeline

Stack

Qwen2.5-1.5B-Instruct (base)
        ↓
  Unsloth 4-bit QLoRA (memory efficiency)
        ↓
  TRL GRPOTrainer
        ↓
  PersonalMail-RL Environment (live reward scoring)
        ↓
  LoRA Adapter β†’ ykshrestha/personalmail-rl-qwen

Training Configuration

model: Qwen2.5-1.5B-Instruct
lora_rank: 16
quantization: 4-bit QLoRA
rollouts: 6 per step
steps: 200
lr: 5e-5
save: adapter (no naive merge)

Training Script

# Run in Google Colab
PYTHONPATH=./personalmail-rl-opt \
  python training/train_grpo.py

πŸ“Š Evaluation Results β€” Before vs After Training

Evaluated on 12 scenarios comparing Baseline Qwen2.5-1.5B vs our GRPO-trained adapter. Model trained

Colab Evaluation Output

{
  "num_scenarios": 12,
  "baseline": {
    "classification": 0.4738,
    "reply_total": 0.3874,
    "reply_format": 0.4542,
    "reply_relevance": 0.3611,
    "reply_length": 0.4500,
    "tone_matching": 0.1949,
    "episode_total": 0.4133
  },
  "trained": {
    "classification": 0.2905,
    "reply_total": 0.7664,
    "reply_format": 0.8917,
    "reply_relevance": 0.7223,
    "reply_length": 0.8333,
    "tone_matching": 0.6207,
    "episode_total": 0.6237
  },
  "improvement": {
    "reply_format": "+0.4375 βœ…",
    "tone_matching": "+0.4258 βœ…",
    "reply_relevance": "+0.3612 βœ…",
    "reply_length": "+0.3833 βœ…",
    "reply_total": "+0.3790 βœ…",
    "episode_total": "+0.2104 βœ…"
  }
}

Rewards

Results Summary Table

Metric Baseline Trained Improvement
Reply Format 0.454 0.892 +0.438 βœ…
Tone Matching 0.195 0.621 +0.426 βœ…
Reply Length 0.450 0.833 +0.383 βœ…
Reply Relevance 0.361 0.722 +0.361 βœ…
Reply Total 0.387 0.766 +0.379 βœ…
Episode Total 0.413 0.624 +0.210 βœ…
Classification 0.474 0.291 -0.183 ⚠️

What Improved and Why

Reply quality improved dramatically across all metrics:

  • +44% reply format β€” Model learned to always include greeting + body + closing
  • +43% tone matching β€” Model learned to match professional/friendly/apologetic tone correctly
  • +38% reply length β€” Model learned to write replies of appropriate length
  • +36% reply relevance β€” Model learned to address actual content of the email
  • +21% overall episode β€” Significant improvement in end-to-end email handling

Classification slightly degraded (expected tradeoff): The base Qwen model already classifies emails reasonably. GRPO focused reward signal on reply quality (70% weight) so the model optimized for the harder, more valuable task. This is an expected behavior in weighted RL.

Evaluation Command

PYTHONPATH=./personalmail-rl-opt \
  python training/evaluate_before_after.py \
  --baseline_model "Qwen/Qwen2.5-1.5B-Instruct" \
  --trained_model "ykshrestha/personalmail-rl-qwen" \
  --num_scenarios 12 \
  --out_json eval_results.json

πŸ–₯️ Live Demo Screenshots

Live Demo β€” Step 1: Email Classification

The agent classifies the email and receives immediate reward feedback. Step 1 Reward: 0.762 on a conflict resolution email.

Live Demo Live Demo

Live Demo β€” Step 2: Reply Drafting

The agent drafts a contextually appropriate reply. Episode Complete with Total Reward: 0.681 Live Demo

Before vs After Comparison

Side-by-side comparison of Baseline vs GRPO-Trained model on the same email. Before After

Megha: Conflict email β€” Meeting overlaps with client call

Baseline Trained (GRPO)
Reply "Hi Megha, I understand there's an overlap... Could we possibly rearrange things..." "Hello Megha, I understand your concern about overlapping commitments. Let's try to find a mutually convenient time... If it's possible, we could adjust the design sync to another day or hour that doesn't clash with the client call. Please let me know if there's anything else I can assist with regarding scheduling adjustments. Best regards, [Your Name]"
Episode Reward 0.614 0.694
Improvement +0.080 βœ…

The trained model's reply is more structured, uses proper closing ("Best regards"), is more specific about solutions, and scores higher on clarity and format.

Environment Info Tab

Full checklist of all hackathon requirements β€” all green βœ… Environment Info


βœ… Hackathon Requirements Checklist

Requirement Status
OpenEnv-compatible FastAPI server βœ…
TRL GRPO training script βœ…
Unsloth QLoRA efficiency βœ…
Google Colab notebook βœ…
7 independent reward functions βœ…
Clarity guardrail against vague "polite-only" hacks βœ…
Anti-reward-hacking measures βœ…
Process supervision (2-step) βœ…
Interactive demo UI βœ…
Before/After comparison with real rewards βœ…
Docker deployment ready βœ…
HuggingFace Spaces deployed βœ…
Mini blog post βœ…

πŸš€ Deployment

HuggingFace Space

⚠️ Important: Select Nvidia A100 Large in Space Settings to enable live model inference. CPU mode will not support float16 inference.

Run Locally

git clone https://github.com/YASHASWINIKSHRESTHA/personalmail-rl-opt
cd personalmail-rl-opt/personalmail-rl-opt
pip install -r requirements.txt
uvicorn server:app --host 0.0.0.0 --port 7863

Docker

docker build -t personalmail-rl .
docker run -p 7863:7863 personalmail-rl

πŸ“ Repository Structure

personalmail-rl-opt/
β”œβ”€β”€ env/
β”‚   β”œβ”€β”€ environment.py           # OpenEnv reset/step/state
β”‚   β”œβ”€β”€ rewards.py               # 7 independent reward functions
β”‚   β”œβ”€β”€ scenarios.py             # 25 email scenarios Γ— 3 difficulties
β”‚   └── models.py                # Pydantic schemas
β”œβ”€β”€ training/
β”‚   β”œβ”€β”€ train_grpo.py            # GRPO training script
β”‚   β”œβ”€β”€ evaluate_before_after.py # Before/after evaluation
β”‚   └── PersonalMailRL_Colab.ipynb  # Full training notebook
β”œβ”€β”€ server.py                    # FastAPI OpenEnv server
β”œβ”€β”€ ui/index.html                # Interactive demo UI
β”œβ”€β”€ Dockerfile                   # HuggingFace Space deployment
└── openenv.yaml                 # OpenEnv configuration

πŸ”— Links

Resource Link
πŸ€— Trained Model ykshrestha/personalmail-rl-qwen
πŸš€ Live Demo Space ykshrestha/personalmail-rl-demo
πŸ’» GitHub Code YASHASWINIKSHRESTHA/personalmail-rl-opt
πŸ“Š Eval Results eval_v5.json β€” +0.21 episode total improvement
Downloads last month
-
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support