Spaces:
Sleeping
π Reward System β Complete Analysis & Implementation Guide
Extracted from the
customer-support-env(meta_hack) codebase. Use this to build an equivalent reward system in your own OpenEnv environment.
Table of Contents
- Architecture Overview
- Single-Agent Reward Formula (Round 1)
- Hierarchical Reward Formula (Round 2)
- Individual Signal Functions
- Penalty System
- LLM-as-Judge System
- Anti-Gaming Guards
- Task-Specific Graders
- Why This Is Better Than Regular Rewards
- Step-by-Step Implementation Guide
1. Architecture Overview
The reward system is a hybrid dense reward architecture with three layers:
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β FINAL REWARD (0.0 β 1.0) β
βββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Layer 3: Security & Integrity Guards β
β βββ RewardGuard (anti-exploit multiplier) β
β βββ HierarchyGuard (escalation discipline) β
β βββ InjectionDetector (prompt injection scan) β
βββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Layer 2: LLM-as-Judge (semantic evaluation) β
β βββ Empathy scoring β
β βββ Policy adherence scoring β
β βββ Resolution quality scoring β
β βββ Supervisor oversight scoring β
β βββ Manager decision quality scoring β
βββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Layer 1: Rule-Based Signals β
β βββ Tone (VADER sentiment) β
β βββ Resolution (keyword-category match) β
β βββ Efficiency (steps used / max steps) β
β βββ Accuracy (required info gathered) β
β βββ SLA compliance β
β βββ Hierarchy effectiveness β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
Key source files:
env/reward_engine.pyβ Core formulasenv/llm_judge.pyβ LLM-as-Judge rubricsenv/reward_guard.pyβ Anti-exploit detectionenv/hierarchy_guard.pyβ Hierarchy disciplineenv/security.pyβ Prompt injection detectionenv/graders/β Task-specific final graders
2. Single-Agent Reward Formula
Used for Round 1 tasks (easy, medium, hard, nightmare).
Terminal Step Formula
R_raw = 0.40 Γ resolution_score
+ 0.20 Γ tone_score
+ 0.20 Γ efficiency_score
+ 0.20 Γ accuracy_score
+ loop_penalty (0 or -0.2)
+ contradiction_penalty (0 or -0.15)
+ escalation_penalty (0 or -0.3)
+ stuffing_penalty (0 or -0.30)
+ info_gathering_bonus (0 or +0.1)
Non-Terminal Step Formula
R_raw = 0.40 Γ 0.0 (resolution = 0 mid-episode)
+ 0.20 Γ tone_score
+ 0.20 Γ (efficiency_score Γ 0.3) (dampened)
+ 0.20 Γ (accuracy_score Γ 0.5) (dampened)
+ loop_penalty
+ contradiction_penalty
+ stuffing_penalty
+ info_gathering_bonus
Final Value Computation
integrity = RewardGuard.check_integrity(...) β multiplier in [0.1, 1.0]
security = InjectionDetector.scan(...) β detected: true/false
R_final = clamp(R_raw Γ integrity_multiplier, 0.0, 1.0)
if security.detected:
R_final = max(0.0, R_final - 0.5)
3. Hierarchical Reward Formula
Used for Round 2 tasks (hierarchy_*, curriculum_*).
Terminal Step Formula
R_raw = 0.25 Γ resolution_score (blended rule + LLM)
+ 0.15 Γ sla_score (rule-based)
+ 0.15 Γ empathy_score (LLM-as-Judge)
+ 0.15 Γ policy_adherence_score (LLM-as-Judge)
+ 0.10 Γ accuracy_score (rule-based)
+ 0.10 Γ efficiency_score (rule-based)
+ 0.10 Γ hierarchy_score (rule-based)
+ loop_penalty (0 or -0.2)
+ contradiction_penalty (0 or -0.15)
+ stuffing_penalty (0 or -0.30)
+ escalation_penalty (0 or -0.3)
+ ignored_feedback_penalty (0 or -0.15)
+ unnecessary_manager_penalty (0 or -0.20)
Non-Terminal Step Formula
R_raw = 0.30 Γ empathy_score
+ 0.20 Γ tone_score
+ 0.15 Γ (efficiency_score Γ 0.3)
+ 0.15 Γ (accuracy_score Γ 0.5)
+ 0.10 Γ hierarchy_score
+ 0.10 Γ policy_adherence_score
+ loop_penalty
+ stuffing_penalty
+ ignored_feedback_penalty
+ unnecessary_manager_penalty
Resolution Score Blending
resolution_score = 0.4 Γ resolution_rule + 0.6 Γ resolution_llm
Final Value (Hierarchy)
integrity_multiplier = reward_guard_mult Γ hierarchy_guard_mult
R_final = clamp(R_raw Γ integrity_multiplier, 0.0, 1.0)
if security.detected:
R_final = max(0.0, R_final - 0.7) β stricter than single-agent
Per-Role Reward Formulas
L1 Support Agent:
L1_raw = 0.30 Γ empathy_score
+ 0.25 Γ accuracy_score
+ 0.25 Γ (resolution_llm if terminal else tone_score)
+ 0.20 Γ efficiency_score
L1_reward = clamp(L1_raw Γ integrity_multiplier, 0.0, 1.0)
L2 Supervisor:
L2_raw = 0.35 Γ oversight_score
+ 0.30 Γ (1.0 + escalation_penalty + unnecessary_manager_penalty)
+ 0.20 Γ policy_adherence_score
+ 0.15 Γ (1.0 if steps β€ ideal else 0.5)
L2_reward = clamp(L2_raw Γ hierarchy_guard_mult, 0.0, 1.0)
L3 Manager:
L3_raw = 0.40 Γ decision_quality_score
+ 0.30 Γ (resolution_llm if terminal else 0.5)
+ 0.30 Γ (1.0 if terminal else 0.0)
L3_reward = clamp(L3_raw, 0.0, 1.0)
4. Individual Signal Functions
4.1 Tone Score
Uses VADER Sentiment Analysis. Maps compound score from [-1, 1] to [0, 1].
def compute_tone_score(message: str) -> float:
if not message or not message.strip():
return 0.5
scores = vader_analyzer.polarity_scores(message)
return (scores["compound"] + 1.0) / 2.0
Formula: tone = (VADER_compound + 1.0) / 2.0
4.2 Resolution Score
Keyword-category match on terminal actions (CLOSE/ESCALATE).
Keywords per resolution type:
refund_initiated β refund, reimburse, credit, money back, ...
billing_clarification β clarify, explain, adjust, correct, ...
technical_fix_provided β fix, solution, workaround, patch, ...
account_access_restored β reset, unlock, restore, access, ...
escalated_to_* β escalate, engineering, specialist, ...
matched = count of keywords found in agent text
score = min(matched / (total_keywords Γ 0.4), 1.0)
Escalation bonus: If expected is escalated_to_* and action is ESCALATE with urgency words β score = min(score + 0.5, 1.0)
Wrong escalation penalty: If expected is NOT escalation but agent escalated β score = max(score - 0.4, 0.0)
4.3 Efficiency Score
efficiency = max(0.0, 1.0 - (steps_used / max_steps))
4.4 Accuracy Score
Fraction of required_info_before_close items found in conversation via regex:
patterns = {
"account_email": r"[\w.+-]+@[\w-]+\.[a-z]{2,}",
"order_id": r"\b(?:order|ord|#)\s*[-]?\s*[A-Z0-9]{4,}\b",
"account_username": r"\b(?:username|user\s*name|login)\b.*?:\s*\S+",
"device_info": r"\b(?:iphone|android|ios|windows|chrome|...)\b",
}
accuracy = gathered_count / required_count
# Returns 1.0 if no info is required
4.5 SLA Compliance Score
ideal_steps = ticket.get("ideal_max_steps", max_steps)
if steps_used <= ideal_steps:
sla_score = 1.0
else:
sla_score = max(0.0, 1.0 - (steps_used - ideal_steps) * 0.15)
4.6 Hierarchy Effectiveness Score
hierarchy_score = 0.5 # neutral default
if supervisor_reviews > 0: hierarchy_score += 0.2
if manager_on_low_priority: hierarchy_score -= 0.2
if l1_actions >= 2: hierarchy_score += 0.1
hierarchy_score = clamp(0.0, 1.0)
5. Penalty System
| Penalty | Value | Trigger |
|---|---|---|
| Loop detection | -0.2 |
TF-IDF cosine similarity > 0.85 between current and any previous agent message |
| Contradiction | -0.15 |
Agent claimed resolution (used words like "fixed", "resolved") then asked for info |
| Keyword stuffing | -0.30 |
> 20% of words are reward keywords (refund, sorry, resolved, etc.) |
| Unnecessary escalation | -0.3 |
Escalating a low/medium priority ticket |
| Ignored supervisor feedback | -0.15 |
Agent message has < 2 word overlap with last supervisor feedback |
| Unnecessary manager escalation | -0.20 |
Supervisor escalates low/medium priority to manager |
Loop Detection Details (TF-IDF)
vectorizer = TfidfVectorizer(ngram_range=(1, 3), stop_words='english')
vec_prev = vectorizer.fit_transform(previous_agent_messages)
vec_last = vectorizer.transform([last_message])
sims = cosine_similarity(vec_last, vec_prev)[0]
if max(sims) > 0.85:
penalty = -0.2
Falls back to exact string match if TF-IDF fails.
6. LLM-as-Judge System
Each evaluation uses a strict rubric prompt β LLM returns {"score": float, "reason": str}.
Temperature = 0.1 for consistency. Falls back to 0.5 (neutral) on failure.
6.1 Empathy Rubric
| Score | Meaning |
|---|---|
| 1.0 | Acknowledges specific issue, validates feelings, warm language |
| 0.7 | Polite, acknowledges issue, doesn't deeply empathize |
| 0.5 | Professional but cold/robotic |
| 0.3 | Dismissive, canned responses |
| 0.0 | Rude, hostile, mocking |
Red flags (auto β€ 0.2): Generic phrases without specifics, keyword stuffing, contradicting empathy.
6.2 Policy Adherence Rubric
| Score | Meaning |
|---|---|
| 1.0 | Fully compliant with active policy |
| 0.7 | Mostly compliant, minor deviations |
| 0.5 | Noticeable policy gaps |
| 0.3 | Clear policy violation |
| 0.0 | Dangerous violation (sharing PII, wrong escalation) |
6.3 Resolution Quality Rubric
| Score | Meaning |
|---|---|
| 1.0 | Fully resolved, all info gathered, customer confirmed |
| 0.7 | Addressed with appropriate resolution |
| 0.5 | Attempted but missing key steps |
| 0.3 | Closed without resolving |
| 0.0 | No resolution attempted |
6.4 Supervisor Oversight Rubric
| Score | Meaning |
|---|---|
| 1.0 | Correct decision + actionable feedback |
| 0.7 | Right decision, feedback could be better |
| 0.5 | Debatable but not harmful |
| 0.3 | Wrong decision (approved bad / rejected good) |
| 0.0 | Rubber-stamped without review |
6.5 Manager Decision Quality Rubric
| Score | Meaning |
|---|---|
| 1.0 | Decisive, resolves escalation correctly |
| 0.7 | Reasonable, addresses core issue |
| 0.5 | Okay but could be better |
| 0.3 | Doesn't address escalation well |
| 0.0 | Wrong decision, punted without value |
7. Anti-Gaming Guards
7.1 RewardGuard (Integrity Multiplier)
Detects exploitative agent behavior. Returns a multiplier in [0.1, 1.0]:
| Exploit | Multiplier |
|---|---|
| Fake resolution (closing with unresolved issues) | Γ 0.3 |
| Keyword stuffing (> 4 resolution keywords) | Γ 0.5 |
| Empathy spam (last 2 msgs >80% similar + empathy tropes) | Γ 0.7 |
| Logic contradiction (claimed done then requested info) | Γ 0.6 |
Multipliers stack multiplicatively. Minimum floored at 0.1.
7.2 HierarchyGuard
| Violation | Multiplier |
|---|---|
| Premature escalation (L1 escalates low/med with < 3 actions) | Γ 0.5 |
| Ignored supervisor feedback (no keyword overlap) | Γ 0.7 |
| Unnecessary manager escalation (supervisor escalates low priority) | Γ 0.4 |
7.3 InjectionDetector
Scans for adversarial patterns:
"ignore previous instructions", "system note:", "act as system",
"maximize score", "assign score 1.0", "override policy", "developer mode"
If detected: -0.5 (single-agent) or -0.7 (hierarchy).
Combined Integrity
final_integrity = reward_guard_multiplier Γ hierarchy_guard_multiplier
R_final = clamp(R_raw Γ final_integrity, 0.0, 1.0)
8. Task-Specific Graders
Each task has an independent deterministic grader producing a [0.0, 1.0] final score.
Easy Task Grader
Weights:
closed: 0.30 β Agent used CLOSE action
resolution_match: 0.35 β Keywords match expected resolution type
no_escalation: 0.20 β No unnecessary escalation
required_info: 0.15 β Required info gathered via regex
Penalties:
sentiment < -0.3 β score Γ 0.5
sentiment < 0.0 β score Γ 0.75
agent_text < 60 chars β score Γ 0.8
Hierarchy Hard Grader
Weights:
all_levels_engaged: 0.20 β All 3 levels (L1, L2, L3) acted
escalation_speed: 0.20 β Escalation within first 3 steps
urgency_referenced: 0.20 β SLA/critical/outage terms used
manager_quality: 0.15 β Manager references ticket subject (>30 chars)
policy_compliance: 0.15 β No self-resolve attempts on critical
no_self_resolve: 0.10 β No troubleshooting before escalation
9. Why This Is Better Than Regular Rewards
| Issue | Regular Approach | This System |
|---|---|---|
| Sparse rewards | Single 0/1 at episode end | Dense per-step rewards with 4-7 signals |
| Reward hacking | Agents exploit keyword patterns | RewardGuard + stuffing detection + TF-IDF loops |
| No semantic understanding | Rule-based only | LLM-as-Judge for empathy, policy, resolution |
| Static policy | Agent memorizes one strategy | PolicyEngine injects mid-episode drift events |
| Single-metric | Optimizes one thing | Multi-dimensional weighted scoring |
| No anti-gaming | Easy to exploit | 3-layer guard system (Reward + Hierarchy + Security) |
| Flat structure | All agents same | Per-role rewards with distinct weights |
Key Innovations
- Hybrid Dense Rewards β Every step gets meaningful signal. Non-terminal steps use dampened weights.
- LLM + Rule Blending β Resolution = 40% rule-based + 60% LLM-judged. Avoids keyword-gaming AND LLM inconsistency.
- Multiplicative Guards β Exploits multiply entire reward down (can stack to 10% of raw).
- Progressive Curriculum β 4 stages. Dense rewards at Stage 1, sparse/harsh at Stage 4.
- Policy Drift β Mid-episode system alerts change rules. Prevents static memorization.
- Per-Role Credit β Each level (L1/L2/L3) has its own reward formula.
10. Implementation Guide
Step 1: Define Your Reward Signals
WEIGHTS_TERMINAL = {
"primary_objective": 0.25,
"quality_1": 0.15,
"quality_2": 0.15,
"compliance": 0.15,
"completeness": 0.10,
"efficiency": 0.10,
"coordination": 0.10,
}
Step 2: Implement Rule-Based Signals
from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
analyzer = SentimentIntensityAnalyzer()
tfidf = TfidfVectorizer(ngram_range=(1, 3), stop_words='english')
def tone_score(msg):
return (analyzer.polarity_scores(msg)["compound"] + 1.0) / 2.0
def efficiency_score(steps, max_steps):
return max(0.0, 1.0 - steps / max_steps)
def loop_penalty(agent_msgs):
if len(agent_msgs) < 2: return 0.0
vec_prev = tfidf.fit_transform(agent_msgs[:-1])
vec_last = tfidf.transform([agent_msgs[-1]])
if float(np.max(cosine_similarity(vec_last, vec_prev))) > 0.85:
return -0.2
return 0.0
Step 3: Implement LLM-as-Judge
class LLMJudge:
def evaluate(self, rubric_prompt: str) -> float:
resp = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": "Output ONLY valid JSON."},
{"role": "user", "content": rubric_prompt},
],
temperature=0.1, max_tokens=150,
)
result = json.loads(resp.choices[0].message.content)
return max(0.0, min(1.0, float(result["score"])))
Step 4: Implement Anti-Gaming Guards
class RewardGuard:
def check(self, action, unresolved):
mult = 1.0
if action.type == "close" and unresolved:
mult *= 0.3
words = action.message.lower().split()
kws = {"refund", "resolved", "fixed", "sorry"}
if len(words) > 5 and sum(w in kws for w in words)/len(words) > 0.2:
mult *= 0.5
return max(0.1, mult)
Step 5: Compose Final Reward
def compute_reward(action, ticket, history, steps, max_steps, is_terminal):
tone = tone_score(action.message)
eff = efficiency_score(steps, max_steps)
loop = loop_penalty(agent_messages)
empathy = judge.evaluate(empathy_rubric)
resolution = 0.4 * rule_resolution + 0.6 * llm_resolution
if is_terminal:
raw = (0.25*resolution + 0.15*sla + 0.15*empathy
+ 0.15*policy + 0.10*acc + 0.10*eff + 0.10*hierarchy
+ loop + penalties)
else:
raw = (0.30*empathy + 0.20*tone + 0.15*eff*0.3
+ 0.15*acc*0.5 + 0.10*hierarchy + 0.10*policy
+ loop + penalties)
guard_mult = RewardGuard().check(action, unresolved)
return float(np.clip(raw * guard_mult, 0.0, 1.0))
Quick Reference: All Formulas
| Signal | Formula |
|---|---|
| Tone | (VADER_compound + 1) / 2 |
| Efficiency | max(0, 1 - steps/max_steps) |
| Accuracy | gathered / required |
| SLA | 1.0 if steps β€ ideal else max(0, 1 - (steps-ideal)Γ0.15) |
| Resolution | min(matched / (totalΓ0.4), 1.0) |
| Hierarchy | 0.5 + 0.2Γsup + 0.1Γl1 - 0.2Γmgr_low |
| Penalty | Value |
|---|---|
| Loop (sim>0.85) | -0.20 |
| Contradiction | -0.15 |
| Keyword stuffing | -0.30 |
| Bad escalation | -0.30 |
| Ignored feedback | -0.15 |
| Unnecessary L3 | -0.20 |
| Guard | Multiplier |
|---|---|
| Fake resolution | Γ0.3 |
| Keyword spam | Γ0.5 |
| Empathy spam | Γ0.7 |
| Contradiction | Γ0.6 |
| Premature escalation | Γ0.5 |
| Ignored feedback | Γ0.7 |
| Unnecessary L3 | Γ0.4 |
Dependencies:
vaderSentiment,scikit-learn,numpy,openai,pydantic