3v324v23 commited on
Commit
a77725d
Β·
0 Parent(s):

FastAPI OpenEnv server

Browse files
Dockerfile ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+ WORKDIR /app
3
+ COPY requirements.txt .
4
+ RUN pip install --no-cache-dir -r requirements.txt
5
+ COPY . .
6
+ EXPOSE 7860
7
+ CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "7860"]
api.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from pydantic import BaseModel
4
+ from environment import CustomerSupportEnv
5
+
6
+ app = FastAPI(title="Customer Support AI OpenEnv")
7
+
8
+ app.add_middleware(
9
+ CORSMiddleware,
10
+ allow_origins=["*"],
11
+ allow_methods=["*"],
12
+ allow_headers=["*"],
13
+ )
14
+
15
+ _env = None
16
+
17
+
18
+ class ResetRequest(BaseModel):
19
+ task: str = "easy"
20
+
21
+
22
+ class StepRequest(BaseModel):
23
+ action: str
24
+
25
+
26
+ def obs_to_dict(obs) -> dict:
27
+ step = obs.current_step
28
+ return {
29
+ "task_id": obs.task_id,
30
+ "task_description": obs.task_description,
31
+ "current_customer_message": obs.current_customer_message,
32
+ "current_step": {
33
+ "step_id": str(step.step_id),
34
+ "label": str(step.label),
35
+ "description": str(step.description),
36
+ },
37
+ "progress": obs.progress,
38
+ "step_number": int(obs.step_number),
39
+ "total_steps": int(obs.total_steps),
40
+ "max_steps": int(obs.max_steps),
41
+ "metadata": obs.metadata,
42
+ "conversation_history": obs.conversation_history,
43
+ "context_memory": obs.context_memory,
44
+ "steps_completed": list(obs.steps_completed),
45
+ "consecutive_failures": int(obs.consecutive_failures),
46
+ "episode_status": str(obs.episode_status),
47
+ "fail_conditions": list(obs.fail_conditions),
48
+ }
49
+
50
+
51
+ @app.get("/")
52
+ def root():
53
+ return {"status": "ok", "message": "Customer Support AI OpenEnv API"}
54
+
55
+
56
+ @app.post("/reset")
57
+ def reset_env(body: ResetRequest = None):
58
+ global _env
59
+ task = (body.task if body else "easy").lower()
60
+ if task not in ["easy", "medium", "hard"]:
61
+ task = "easy"
62
+ _env = CustomerSupportEnv(task=task)
63
+ obs = _env.reset()
64
+ return {"observation": obs_to_dict(obs)}
65
+
66
+
67
+ @app.post("/step")
68
+ def step_env(body: StepRequest):
69
+ global _env
70
+ if _env is None:
71
+ return {"error": "Call /reset first"}
72
+ result = _env.step(body.action)
73
+ info = result.info.copy()
74
+ return {
75
+ "observation": obs_to_dict(result.observation),
76
+ "reward": float(result.reward),
77
+ "done": bool(result.done),
78
+ "info": info,
79
+ }
80
+
81
+
82
+ @app.get("/state")
83
+ def get_state():
84
+ global _env
85
+ if _env is None:
86
+ return {"error": "Call /reset first"}
87
+ return _env.state()
88
+
89
+
90
+ @app.get("/observation_space")
91
+ def observation_space():
92
+ return {
93
+ "type": "Dict",
94
+ "fields": {
95
+ "task_id": "str",
96
+ "current_customer_message": "str",
97
+ "current_step": "StepInfo",
98
+ "progress": "str",
99
+ "episode_status": "str",
100
+ }
101
+ }
102
+
103
+
104
+ @app.get("/action_space")
105
+ def action_space():
106
+ return {
107
+ "type": "Text",
108
+ "description": "Agent free-text reply",
109
+ "min_words": 6,
110
+ }
environment.py ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ environment.py β€” Strict RL-style Customer Support Environment
3
+ Handles: step enforcement, repeat detection, fail conditions, reward calculation
4
+ """
5
+
6
+ from __future__ import annotations
7
+ import re
8
+ from typing import List, Tuple, Optional
9
+
10
+ from models import (
11
+ Episode, EpisodeStatus, StepName, StepResult,
12
+ DifficultyLevel, Task
13
+ )
14
+ from graders.base_grader import BaseGrader
15
+
16
+
17
+ # ── Step order ────────────────────────────────────────────────────────────────
18
+ STEP_ORDER = [
19
+ StepName.EMPATHY,
20
+ StepName.COLLECT_INFO,
21
+ StepName.INVESTIGATE,
22
+ StepName.RESOLUTION,
23
+ ]
24
+
25
+ # ── Reward constants ───────────────────────────────────────────────────────────
26
+ BASE_SCORE_CORRECT = 1.0
27
+ BASE_SCORE_INCORRECT = 0.2
28
+ STEP_BONUS = 0.2 # bonus when step is correct
29
+ WRONG_STEP_PENALTY = 0.3 # wrong action in correct step position
30
+ REPEAT_PENALTY = 0.2 # repeated question / response
31
+ SKIP_STEP_PENALTY = 0.3 # jumped ahead
32
+ EARLY_SOLUTION_PENALTY = 0.25 # gave resolution before investigation
33
+ EMOTION_IGNORE_PENALTY = 0.25 # angry customer β†’ neutral / cold reply
34
+ GENERIC_RESPONSE_PENALTY = 0.15 # "ok", "done", vague one-liners
35
+ WRONG_ASSUMPTION_PENALTY = 0.2 # stated wrong facts
36
+ LOOP_PENALTY = 0.35 # agent stuck in loop (same step repeated 2+)
37
+
38
+ # ── Overlap threshold for repeat detection ─────────────────────────────────────
39
+ REPEAT_WORD_OVERLAP_MIN = 10 # words in common β†’ flagged as repeat
40
+
41
+
42
+ class CustomerSupportEnv:
43
+ """
44
+ Strict step-based RL environment for customer support training.
45
+ """
46
+
47
+ def __init__(self, task: Task, grader: BaseGrader):
48
+ self.task = task
49
+ self.grader = grader
50
+ self.episode = Episode(
51
+ task_id = task.task_id,
52
+ difficulty = task.difficulty,
53
+ )
54
+ self._response_history: List[str] = []
55
+ self._step_index = 0 # which step we expect next
56
+ self._consecutive_wrong = 0 # wrong attempts at current step
57
+
58
+ # ── Public API ─────────────────────────────────────────────────────────────
59
+
60
+ def step(self, agent_response: str) -> Tuple[StepResult, bool]:
61
+ """
62
+ Process one agent response.
63
+ Returns (StepResult, done:bool).
64
+ """
65
+ if self.episode.status != EpisodeStatus.RUNNING:
66
+ raise RuntimeError("Episode is already finished.")
67
+
68
+ expected_step = STEP_ORDER[self._step_index]
69
+ result = self._evaluate(agent_response, expected_step)
70
+ self.episode.add_step(result)
71
+ self._response_history.append(agent_response.lower().strip())
72
+
73
+ done = False
74
+ if result.correct:
75
+ self._step_index += 1
76
+ self._consecutive_wrong = 0
77
+ else:
78
+ self._consecutive_wrong += 1
79
+
80
+ # Loop fail: 3 consecutive wrong attempts at the same step
81
+ if self._consecutive_wrong >= 3 and self.episode.status == EpisodeStatus.RUNNING:
82
+ self.episode.status = EpisodeStatus.FAIL
83
+ self.episode.fail_reason = (
84
+ f"Agent stuck in loop at step '{expected_step.value}' "
85
+ f"({self._consecutive_wrong} consecutive failures)"
86
+ )
87
+
88
+ if self.episode.status != EpisodeStatus.RUNNING:
89
+ done = True
90
+ elif self._step_index >= len(STEP_ORDER):
91
+ done = True # episode.add_step() already set SUCCESS/FAIL
92
+
93
+ return result, done
94
+
95
+ def reset(self) -> None:
96
+ self.episode = Episode(
97
+ task_id = self.task.task_id,
98
+ difficulty = self.task.difficulty,
99
+ )
100
+ self._response_history = []
101
+ self._step_index = 0
102
+ self._consecutive_wrong = 0
103
+
104
+ def summary(self):
105
+ return self.episode.summary()
106
+
107
+ # ── Internal evaluation ────────────────────────────────────────────────────
108
+
109
+ def _evaluate(self, response: str, expected_step: StepName) -> StepResult:
110
+ penalties: List[str] = []
111
+ total_penalty = 0.0
112
+
113
+ # 1. Grade the response against expected step
114
+ grader_result = self.grader.grade(
115
+ response = response,
116
+ expected_step = expected_step,
117
+ task = self.task,
118
+ )
119
+ correct = grader_result["correct"]
120
+ base_score = BASE_SCORE_CORRECT if correct else BASE_SCORE_INCORRECT
121
+ detected_action = grader_result.get("detected_action", "unknown")
122
+
123
+ # 2. Step bonus
124
+ step_bonus = STEP_BONUS if correct else 0.0
125
+
126
+ # 3. Wrong-step penalty
127
+ if not correct:
128
+ total_penalty += WRONG_STEP_PENALTY
129
+ penalties.append(
130
+ f"Wrong action detected ('{detected_action}' "
131
+ f"β‰  '{expected_step.value}'): -{WRONG_STEP_PENALTY}"
132
+ )
133
+
134
+ # 4. Repeat detection
135
+ if self._is_repeated_response(response):
136
+ total_penalty += REPEAT_PENALTY
137
+ penalties.append(f"Repeated/duplicate response: -{REPEAT_PENALTY}")
138
+
139
+ # 5. Early solution penalty
140
+ if self._is_early_solution(response, expected_step):
141
+ total_penalty += EARLY_SOLUTION_PENALTY
142
+ penalties.append(f"Solution given too early: -{EARLY_SOLUTION_PENALTY}")
143
+
144
+ # 6. Emotion mismatch penalty
145
+ if self._is_emotion_mismatch(response):
146
+ total_penalty += EMOTION_IGNORE_PENALTY
147
+ penalties.append(
148
+ f"Angry customer ignored (no empathy/de-escalation): "
149
+ f"-{EMOTION_IGNORE_PENALTY}"
150
+ )
151
+
152
+ # 7. Generic / too-short response
153
+ if self._is_generic_response(response):
154
+ total_penalty += GENERIC_RESPONSE_PENALTY
155
+ penalties.append(f"Generic/too-short response: -{GENERIC_RESPONSE_PENALTY}")
156
+
157
+ # 8. Wrong assumption detection
158
+ wrong_assumption = grader_result.get("wrong_assumption", False)
159
+ if wrong_assumption:
160
+ total_penalty += WRONG_ASSUMPTION_PENALTY
161
+ penalties.append(f"Incorrect assumption stated: -{WRONG_ASSUMPTION_PENALTY}")
162
+
163
+ # 9. Skip-step penalty (grader signals this)
164
+ if grader_result.get("skipped_step", False):
165
+ total_penalty += SKIP_STEP_PENALTY
166
+ penalties.append(f"Step skipped: -{SKIP_STEP_PENALTY}")
167
+
168
+ # ── Reward formula ─────────────────────────────────────────────────────
169
+ reward = max(0.0, base_score + step_bonus - total_penalty)
170
+
171
+ # ── Fail trigger (individual step) ────────────────────────────────────
172
+ fail_triggered = False
173
+ fail_reason = ""
174
+ if total_penalty >= 0.8:
175
+ fail_triggered = True
176
+ fail_reason = f"Single step penalty exceeded threshold ({total_penalty:.2f})"
177
+
178
+ return StepResult(
179
+ step = expected_step,
180
+ agent_response = response,
181
+ detected_action = detected_action,
182
+ expected_action = expected_step.value,
183
+ correct = correct,
184
+ base_score = base_score,
185
+ step_bonus = step_bonus,
186
+ penalty = total_penalty,
187
+ penalty_reasons = penalties,
188
+ reward = reward,
189
+ fail_triggered = fail_triggered,
190
+ fail_reason = fail_reason,
191
+ )
192
+
193
+ # ── Helper detectors ───────────────────────────────────────────────────────
194
+
195
+ def _is_repeated_response(self, response: str) -> bool:
196
+ if not self._response_history:
197
+ return False
198
+ words_new = set(response.lower().split())
199
+ for prev in self._response_history:
200
+ words_prev = set(prev.split())
201
+ overlap = len(words_new & words_prev)
202
+ if overlap >= REPEAT_WORD_OVERLAP_MIN:
203
+ return True
204
+ return False
205
+
206
+ def _is_early_solution(self, response: str, step: StepName) -> bool:
207
+ """Penalise giving resolution keywords before the resolution step."""
208
+ if step in (StepName.EMPATHY, StepName.COLLECT_INFO):
209
+ resolution_signals = [
210
+ "refund", "replacement", "we will fix", "we will credit",
211
+ "here is the solution", "the fix is", "escalate your",
212
+ ]
213
+ r = response.lower()
214
+ return any(sig in r for sig in resolution_signals)
215
+ return False
216
+
217
+ def _is_emotion_mismatch(self, response: str) -> bool:
218
+ """Flag cold/neutral replies when customer is angry/frustrated."""
219
+ if self.task.customer_emotion not in ("angry", "frustrated"):
220
+ return False
221
+ empathy_signals = [
222
+ "sorry", "apologize", "apology", "understand your frustration",
223
+ "i hear you", "i completely understand", "that must be",
224
+ "deeply sorry", "sincerely apologize",
225
+ ]
226
+ r = response.lower()
227
+ return not any(sig in r for sig in empathy_signals)
228
+
229
+ def _is_generic_response(self, response: str) -> bool:
230
+ stripped = response.strip().lower()
231
+ # Very short replies
232
+ if len(stripped.split()) <= 4:
233
+ return True
234
+ # Generic filler phrases
235
+ generic_phrases = [
236
+ "ok", "okay", "done", "sure", "got it",
237
+ "no problem", "understood", "alright",
238
+ ]
239
+ return stripped in generic_phrases
graders/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from graders.base_grader import BaseGrader, HardTaskGrader
2
+
3
+ __all__ = ["BaseGrader", "HardTaskGrader"]
graders/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (291 Bytes). View file
 
graders/__pycache__/base_grader.cpython-312.pyc ADDED
Binary file (9.88 kB). View file
 
graders/__pycache__/easy_grader.cpython-312.pyc ADDED
Binary file (3.36 kB). View file
 
graders/__pycache__/hard_grader.cpython-312.pyc ADDED
Binary file (6.23 kB). View file
 
graders/__pycache__/medium_grader.cpython-312.pyc ADDED
Binary file (4.89 kB). View file
 
graders/base_grader.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ graders/base_grader.py β€” Deterministic, rule-based grader
3
+ Handles: step detection, edge cases, emotion mismatch, wrong assumptions
4
+ """
5
+
6
+ from __future__ import annotations
7
+ import re
8
+ from typing import Dict, Any, List
9
+
10
+ from models import StepName, Task
11
+
12
+
13
+ # ── Keyword banks ──────────────────────────────────────────────────────────────
14
+
15
+ EMPATHY_KEYWORDS = [
16
+ "sorry", "apologize", "apology", "understand", "frustrat",
17
+ "hear you", "feel", "concern", "inconvenien", "deeply sorry",
18
+ "sincerely", "personally sorry", "personally and deeply sorry",
19
+ "i take full", "full responsibility",
20
+ ]
21
+
22
+ COLLECT_INFO_KEYWORDS = [
23
+ "order number", "order id", "account number", "could you provide",
24
+ "can you share", "what is your", "please confirm", "transaction id",
25
+ "booking reference", "invoice", "date of purchase", "when did",
26
+ "which product", "what happened", "could you tell me", "may i have",
27
+ "can i have your", "full name", "email address", "phone number",
28
+ # Additional strong collect-info signals
29
+ "could you please provide", "please provide", "provide me with",
30
+ "associated with your account", "your account", "look into this",
31
+ "look into further", "assist you further", "help me locate",
32
+ "need a few details", "need some information", "need your",
33
+ "retrieve your", "verify your", "pull up your", "access your",
34
+ "your email", "your order", "your reference", "your details",
35
+ "please share your", "share your", "to look into", "to assist you",
36
+ "to help you", "can you provide", "can you confirm", "can you give",
37
+ "i would need", "i will need", "so i can", "in order to",
38
+ ]
39
+
40
+ INVESTIGATE_KEYWORDS = [
41
+ "checking", "looking into", "pulling up", "reviewing",
42
+ "investigating", "let me check", "i can see", "our records show",
43
+ "according to our system", "i have found", "i found", "found that",
44
+ "it appears", "it seems", "the issue", "root cause",
45
+ ]
46
+
47
+ RESOLUTION_KEYWORDS = [
48
+ "refund", "replacement", "credit", "we will", "i will",
49
+ "here is what", "the solution", "fix this", "resolved",
50
+ "compensation", "escalate", "expedite", "waive", "reimburse",
51
+ "send a new", "process a", "arrange for", "free of charge",
52
+ ]
53
+
54
+ # De-escalation signals required for hard tasks
55
+ DEESCALATION_KEYWORDS = [
56
+ "i completely understand", "that is unacceptable", "i take full responsibility",
57
+ "this should not have happened", "you have every right",
58
+ "personally ensure", "highest priority", "immediate attention",
59
+ "personally and deeply sorry", "sincerely apologize",
60
+ "i hear your frustration", "deeply sorry for the experience",
61
+ ]
62
+
63
+ # Wrong-assumption triggers (generic bad guesses)
64
+ WRONG_ASSUMPTION_PHRASES = [
65
+ "you probably forgot", "you must have", "it is your fault",
66
+ "you should have", "clearly you", "obviously you",
67
+ ]
68
+
69
+ # Generic / non-answer phrases
70
+ GENERIC_FILLERS = [
71
+ "ok", "okay", "done", "sure", "got it", "understood",
72
+ "no problem", "alright", "i see", "noted",
73
+ ]
74
+
75
+
76
+ class BaseGrader:
77
+ """
78
+ Deterministic grader. Returns a dict with:
79
+ correct, detected_action, wrong_assumption, skipped_step, notes
80
+ """
81
+
82
+ def grade(
83
+ self,
84
+ response: str,
85
+ expected_step: StepName,
86
+ task: Task,
87
+ ) -> Dict[str, Any]:
88
+
89
+ r = response.lower().strip()
90
+ result: Dict[str, Any] = {
91
+ "correct": False,
92
+ "detected_action": "unknown",
93
+ "wrong_assumption": False,
94
+ "skipped_step": False,
95
+ "notes": [],
96
+ }
97
+
98
+ # ── Edge-case guard ────────────────────────────────────────────────────
99
+ if self._is_irrelevant(r):
100
+ result["detected_action"] = "irrelevant"
101
+ result["notes"].append("Response is irrelevant / generic filler.")
102
+ return result
103
+
104
+ # ── Detect what the agent actually did ─────────────────────────────────
105
+ detected = self._detect_action(r, task)
106
+ result["detected_action"] = detected
107
+
108
+ # ── Correctness check ──────────────────────────────────────────────────
109
+ result["correct"] = (detected == expected_step.value)
110
+
111
+ # ── Skip-step check ────────────────────────────────────────────────────
112
+ # e.g. agent jumps straight to RESOLUTION while expected EMPATHY
113
+ step_rank = {
114
+ StepName.EMPATHY: 0,
115
+ StepName.COLLECT_INFO: 1,
116
+ StepName.INVESTIGATE: 2,
117
+ StepName.RESOLUTION: 3,
118
+ }
119
+ detected_rank = self._action_to_rank(detected)
120
+ expected_rank = step_rank[expected_step]
121
+ if detected_rank > expected_rank + 0:
122
+ result["skipped_step"] = True
123
+ result["notes"].append(
124
+ f"Skipped step: expected '{expected_step.value}', "
125
+ f"agent jumped to '{detected}'."
126
+ )
127
+
128
+ # ── Wrong-assumption check ─────────────────────────────────────────────
129
+ if any(phrase in r for phrase in WRONG_ASSUMPTION_PHRASES):
130
+ result["wrong_assumption"] = True
131
+ result["notes"].append("Agent made a wrong assumption about the customer.")
132
+
133
+ # ── Hard-task: de-escalation required ─────────────────────────────────
134
+ if task.escalation_risk and expected_step == StepName.EMPATHY:
135
+ if not any(kw in r for kw in DEESCALATION_KEYWORDS):
136
+ result["correct"] = False
137
+ result["notes"].append(
138
+ "Hard task requires de-escalation language; none detected."
139
+ )
140
+
141
+ return result
142
+
143
+ # ── Helpers ────────────────────────────────────────────────────────────────
144
+
145
+ def _detect_action(self, r: str, task: Task) -> str:
146
+ """Return the name of the step the response most resembles."""
147
+ scores = {
148
+ "empathy": self._score_keywords(r, EMPATHY_KEYWORDS),
149
+ "collect_info": self._score_keywords(r, COLLECT_INFO_KEYWORDS),
150
+ "investigate": self._score_keywords(r, INVESTIGATE_KEYWORDS),
151
+ "resolution": self._score_keywords(r, RESOLUTION_KEYWORDS),
152
+ }
153
+ best = max(scores, key=scores.get)
154
+ if scores[best] == 0:
155
+ return "unknown"
156
+ return best
157
+
158
+ def _score_keywords(self, text: str, keywords: List[str]) -> int:
159
+ return sum(1 for kw in keywords if kw in text)
160
+
161
+ def _is_irrelevant(self, r: str) -> bool:
162
+ # Very short
163
+ if len(r.split()) <= 3:
164
+ return True
165
+ # Pure filler
166
+ if r in GENERIC_FILLERS:
167
+ return True
168
+ # No customer-support vocabulary at all
169
+ all_keywords = (
170
+ EMPATHY_KEYWORDS + COLLECT_INFO_KEYWORDS
171
+ + INVESTIGATE_KEYWORDS + RESOLUTION_KEYWORDS
172
+ )
173
+ return not any(kw in r for kw in all_keywords)
174
+
175
+ def _action_to_rank(self, action: str) -> int:
176
+ mapping = {
177
+ "empathy": 0,
178
+ "collect_info": 1,
179
+ "investigate": 2,
180
+ "resolution": 3,
181
+ "unknown": -1,
182
+ "irrelevant": -1,
183
+ }
184
+ return mapping.get(action, -1)
185
+
186
+
187
+ # ── Hard-task specific grader ──────────────────────────────────────────────────
188
+
189
+ class HardTaskGrader(BaseGrader):
190
+ """
191
+ Extended grader for the hard escalation scenario.
192
+ Adds: emotional control score, conflict handling, professional tone.
193
+ """
194
+
195
+ PROFESSIONAL_PHRASES = [
196
+ "i assure you", "rest assured", "our team will",
197
+ "i will personally", "i will escalate", "within 24 hours",
198
+ "we take this very seriously", "top priority",
199
+ ]
200
+
201
+ UNPROFESSIONAL_PHRASES = [
202
+ "calm down", "you need to", "stop complaining",
203
+ "it is not our fault", "nothing we can do", "policy says",
204
+ ]
205
+
206
+ def grade(
207
+ self,
208
+ response: str,
209
+ expected_step: StepName,
210
+ task: Task,
211
+ ) -> Dict[str, Any]:
212
+ result = super().grade(response, expected_step, task)
213
+ r = response.lower()
214
+
215
+ notes = result["notes"]
216
+
217
+ # Emotional-control bonus / penalty
218
+ if any(kw in r for kw in self.PROFESSIONAL_PHRASES):
219
+ notes.append("βœ… Professional / reassuring language detected.")
220
+ if any(kw in r for kw in self.UNPROFESSIONAL_PHRASES):
221
+ result["correct"] = False
222
+ result["wrong_assumption"] = True
223
+ notes.append("❌ Unprofessional language detected (e.g. 'calm down').")
224
+
225
+ # Resolution accuracy: must offer concrete action on resolution step
226
+ if expected_step == StepName.RESOLUTION:
227
+ concrete = [
228
+ "refund", "replacement", "credit", "waive", "free",
229
+ "expedite", "send", "process", "arrange",
230
+ ]
231
+ if not any(c in r for c in concrete):
232
+ result["correct"] = False
233
+ notes.append(
234
+ "❌ Resolution step requires a concrete action (refund/replacement/credit)."
235
+ )
236
+
237
+ result["notes"] = notes
238
+ return result
graders/easy_grader.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Easy Grader β€” Scores FAQ responses based on keyword coverage and tone.
3
+ """
4
+
5
+ from typing import Tuple
6
+
7
+
8
+ class EasyGrader:
9
+ """
10
+ Scoring logic for Easy (FAQ) Task.
11
+
12
+ Formula:
13
+ score = (keyword_matches / total_keywords) * tone_multiplier
14
+
15
+ Breakdown:
16
+ keyword_matches: How many expected keywords appear in the agent's reply.
17
+ total_keywords: Total number of expected keywords in the scenario.
18
+ tone_multiplier: 0.8 if tone is bad (rude/dismissive), 1.0 if neutral, 1.1 if polite.
19
+ """
20
+
21
+ # Polite/professional phrases boost score slightly
22
+ POLITE_PHRASES = [
23
+ "happy to help", "great question", "certainly", "of course",
24
+ "please", "thank you", "let me", "i'd be happy", "absolutely",
25
+ "sure", "glad", "assist", "help you",
26
+ ]
27
+
28
+ # Dismissive/rude phrases reduce score
29
+ BAD_PHRASES = [
30
+ "i don't know", "not sure", "no idea", "can't help",
31
+ "figure it out", "read the manual", "obvious",
32
+ ]
33
+
34
+ def grade(self, action: str, scenario: dict, history: list) -> Tuple[float, dict]:
35
+ """
36
+ Grade the agent's FAQ response.
37
+
38
+ Returns:
39
+ (score: float [0.0–1.0], grader_info: dict)
40
+ """
41
+ action_lower = action.lower()
42
+ keywords = scenario.get("correct_answer_keywords", [])
43
+
44
+ # 1. Keyword Coverage
45
+ matched = [kw for kw in keywords if kw.lower() in action_lower]
46
+ keyword_score = len(matched) / len(keywords) if keywords else 0.5
47
+
48
+ # 2. Tone Multiplier
49
+ has_polite = any(p in action_lower for p in self.POLITE_PHRASES)
50
+ has_bad = any(p in action_lower for p in self.BAD_PHRASES)
51
+
52
+ if has_bad:
53
+ tone_multiplier = 0.7
54
+ tone_label = "unprofessional"
55
+ elif has_polite:
56
+ tone_multiplier = 1.1
57
+ tone_label = "polite"
58
+ else:
59
+ tone_multiplier = 1.0
60
+ tone_label = "neutral"
61
+
62
+ # 3. Length check β€” too short = incomplete
63
+ word_count = len(action.split())
64
+ if word_count < 5:
65
+ length_penalty = 0.4 # very short answers likely incomplete
66
+ elif word_count < 10:
67
+ length_penalty = 0.85
68
+ else:
69
+ length_penalty = 1.0
70
+
71
+ # Final score (capped at 1.0)
72
+ score = min(1.0, keyword_score * tone_multiplier * length_penalty)
73
+
74
+ grader_info = {
75
+ "matched_keywords": matched,
76
+ "missing_keywords": [kw for kw in keywords if kw.lower() not in action_lower],
77
+ "keyword_score": round(keyword_score, 3),
78
+ "tone": tone_label,
79
+ "tone_multiplier": tone_multiplier,
80
+ "word_count": word_count,
81
+ "final_score": round(score, 3),
82
+ }
83
+
84
+ return round(score, 3), grader_info
graders/hard_grader.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hard Grader - Scores emotional complaint handling.
3
+ """
4
+
5
+ from typing import Tuple, Optional
6
+
7
+
8
+ class HardGrader:
9
+
10
+ BEHAVIOR_KEYWORDS = {
11
+ "genuine_empathy": [
12
+ "i understand how", "i can only imagine", "that must be incredibly",
13
+ "i truly understand", "how frustrating", "how upsetting", "i hear you",
14
+ "i completely understand", "your feelings are valid", "that's terrible",
15
+ "i'm so sorry you're going through", "that sounds incredibly",
16
+ ],
17
+ "personal_apology": [
18
+ "i personally apologize", "i am deeply sorry", "i sincerely apologize",
19
+ "i'm truly sorry", "you deserve better", "this is unacceptable",
20
+ "we have failed you", "i take full responsibility",
21
+ ],
22
+ "take_ownership": [
23
+ "i will personally", "i'm taking ownership", "i'll make sure",
24
+ "i'll see this through", "let me personally", "i'll handle this myself",
25
+ "i'm going to fix", "i'll escalate this myself",
26
+ ],
27
+ "offer_compensation": [
28
+ "credit", "discount", "refund", "compensat", "free month",
29
+ "extend your subscription", "make this right", "as a gesture",
30
+ "offer you", "we'd like to offer",
31
+ ],
32
+ "provide_resolution": [
33
+ "fixed", "resolved", "escalated to", "engineering team",
34
+ "senior team", "within 24 hours", "immediately", "priority",
35
+ "restore", "recovery", "our team is working",
36
+ ],
37
+ "express_concern_for_health": [
38
+ "please see a doctor", "seek medical attention", "your health",
39
+ "medical professional", "health comes first", "please get checked",
40
+ "are you okay", "how are you feeling", "go to a doctor",
41
+ ],
42
+ "collect_product_info": [
43
+ "batch number", "product name", "order number", "lot number",
44
+ "when did you purchase", "which product", "can you provide",
45
+ ],
46
+ "escalate_to_safety_team": [
47
+ "safety team", "quality team", "investigating", "immediate investigation",
48
+ "food safety", "health and safety", "escalate to our", "report this",
49
+ "regulatory", "quality control",
50
+ ],
51
+ "investigate_data_loss": [
52
+ "engineering team", "data recovery", "backup logs", "investigate",
53
+ "technical team", "escalate immediately", "attempt to recover",
54
+ "our engineers", "priority investigation",
55
+ ],
56
+ }
57
+
58
+ VIOLATION_KEYWORDS = {
59
+ "robotic_response": [
60
+ "we apologize for the inconvenience",
61
+ "your feedback is important to us",
62
+ "we strive to provide",
63
+ "as per our policy",
64
+ "please note that",
65
+ "kindly be advised",
66
+ ],
67
+ "dismiss_emotion": [
68
+ "calm down", "theres no need to", "youre overreacting",
69
+ "this is normal", "happens to everyone",
70
+ ],
71
+ "blame_customer": [
72
+ "you should have", "if you had", "user error",
73
+ "thats your fault", "you didnt follow",
74
+ ],
75
+ "deny_fault": [
76
+ "not our fault", "we cant be held responsible",
77
+ "no evidence", "impossible that our product",
78
+ ],
79
+ "minimize_health_concern": [
80
+ "just a mild reaction", "probably not related",
81
+ "unlikely to be our product", "youre probably fine",
82
+ ],
83
+ "offer_no_compensation": [],
84
+ "downplay_data_loss": [
85
+ "just data", "its only files", "you should have backed up",
86
+ "not our responsibility for backups",
87
+ ],
88
+ }
89
+
90
+ GENERIC_INDICATORS = [
91
+ "dear valued customer",
92
+ "we take all complaints seriously",
93
+ "please contact us at",
94
+ "for further assistance",
95
+ "thank you for contacting",
96
+ ]
97
+
98
+ def grade(
99
+ self,
100
+ action: str,
101
+ scenario: dict,
102
+ history: list,
103
+ behaviors_demonstrated: list,
104
+ avoid_violations: list,
105
+ ) -> Tuple[float, dict, Optional[str], Optional[str]]:
106
+
107
+ action_lower = action.lower()
108
+ required_behaviors = scenario.get("required_behaviors", [])
109
+ avoid_behaviors = scenario.get("avoid_behaviors", [])
110
+
111
+ behavior_found = None
112
+ best_behavior_score = 0.0
113
+
114
+ for behavior in required_behaviors:
115
+ if behavior in behaviors_demonstrated:
116
+ continue
117
+ kw_list = self.BEHAVIOR_KEYWORDS.get(behavior, [])
118
+ matches = sum(1 for kw in kw_list if kw in action_lower)
119
+ if matches > 0:
120
+ ratio = matches / len(kw_list) if kw_list else 0
121
+ if ratio > best_behavior_score:
122
+ best_behavior_score = ratio
123
+ behavior_found = behavior
124
+
125
+ all_empathy_keywords = self.BEHAVIOR_KEYWORDS.get("genuine_empathy", [])
126
+ empathy_matches = sum(1 for kw in all_empathy_keywords if kw in action_lower)
127
+ empathy_score = min(1.3, 1.0 + (empathy_matches * 0.1))
128
+
129
+ is_generic = any(gi in action_lower for gi in self.GENERIC_INDICATORS)
130
+ personalization_mult = 0.7 if is_generic else 1.0
131
+
132
+ violation_found = None
133
+ for avoidance in avoid_behaviors:
134
+ if avoidance in avoid_violations:
135
+ continue
136
+ kw_list = self.VIOLATION_KEYWORDS.get(avoidance, [])
137
+ matches = sum(1 for kw in kw_list if kw in action_lower)
138
+ if matches > 0:
139
+ violation_found = avoidance
140
+ break
141
+
142
+ if behavior_found:
143
+ base = min(1.0, best_behavior_score * 1.5)
144
+ score = min(1.0, base * empathy_score * personalization_mult)
145
+ else:
146
+ score = 0.15 * empathy_score if empathy_matches > 0 else 0.05
147
+
148
+ grader_info = {
149
+ "behavior_found": behavior_found,
150
+ "behavior_match_score": round(best_behavior_score, 3),
151
+ "empathy_score": round(empathy_score, 3),
152
+ "personalization": "generic" if is_generic else "personalized",
153
+ "violation_found": violation_found,
154
+ }
155
+
156
+ return round(score, 3), grader_info, behavior_found, violation_found
graders/hard_grader.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hard Grader β€” Scores emotional complaint handling.
3
+ """
4
+
5
+ from typing import Tuple, Optional
6
+
7
+
8
+ class HardGrader:
9
+
10
+ BEHAVIOR_KEYWORDS = {
11
+ "genuine_empathy": [
12
+ "i understand how", "i can only imagine", "that must be incredibly",
13
+ "i truly understand", "how frustrating", "how upsetting", "i hear you",
14
+ "i completely understand", "your feelings are valid", "that's terrible",
15
+ "i'm so sorry you're going through", "that sounds incredibly",
16
+ ],
17
+ "personal_apology": [
18
+ "i personally apologize", "i am deeply sorry", "i sincerely apologize",
19
+ "i'm truly sorry", "you deserve better", "this is unacceptable",
20
+ "we have failed you", "i take full responsibility",
21
+ ],
22
+ "take_ownership": [
23
+ "i will personally", "i'm taking ownership", "i'll make sure",
24
+ "i'll see this through", "let me personally", "i'll handle this myself",
25
+ "i'm going to fix", "i'll escalate this myself",
26
+ ],
27
+ "offer_compensation": [
28
+ "credit", "discount", "refund", "compensat", "free month",
29
+ "extend your subscription", "make this right", "as a gesture",
30
+ "offer you", "we'd like to offer",
31
+ ],
32
+ "provide_resolution": [
33
+ "fixed", "resolved", "escalated to", "engineering team",
34
+ "senior team", "within 24 hours", "immediately", "priority",
35
+ "restore", "recovery", "our team is working",
36
+ ],
37
+ "express_concern_for_health": [
38
+ "please see a doctor", "seek medical attention", "your health",
39
+ "medical professional", "health comes first", "please get checked",
40
+ "are you okay", "how are you feeling", "go to a doctor",
41
+ ],
42
+ "collect_product_info": [
43
+ "batch number", "product name", "order number", "lot number",
44
+ "when did you purchase", "which product", "can you provide",
45
+ ],
46
+ "escalate_to_safety_team": [
47
+ "safety team", "quality team", "investigating", "immediate investigation",
48
+ "food safety", "health and safety", "escalate to our", "report this",
49
+ "regulatory", "quality control",
50
+ ],
51
+ "investigate_data_loss": [
52
+ "engineering team", "data recovery", "backup logs", "investigate",
53
+ "technical team", "escalate immediately", "attempt to recover",
54
+ "our engineers", "priority investigation",
55
+ ],
56
+ }
57
+
58
+ VIOLATION_KEYWORDS = {
59
+ "robotic_response": [
60
+ "we apologize for the inconvenience",
61
+ "your feedback is important to us",
62
+ "we strive to provide",
63
+ "as per our policy",
64
+ "please note that",
65
+ "kindly be advised",
66
+ ],
67
+ "dismiss_emotion": [
68
+ "calm down", "there's no need to", "you're overreacting",
69
+ "this is normal", "happens to everyone",
70
+ ],
71
+ "blame_customer": [
72
+ "you should have", "if you had", "user error",
73
+ "that's your fault", "you didn't follow",
74
+ ],
75
+ "deny_fault": [
76
+ "not our fault", "we can't be held responsible",
77
+ "no evidence", "impossible that our product",
78
+ ],
79
+ "minimize_health_concern": [
80
+ "just a mild reaction", "probably not related",
81
+ "unlikely to be our product", "you're probably fine",
82
+ ],
83
+ "offer_no_compensation": [],
84
+ "downplay_data_loss": [
85
+ "just data", "it's only files", "you should have backed up",
86
+ "not our responsibility for backups",
87
+ ],
88
+ }
89
+
90
+ GENERIC_INDICATORS = [
91
+ "dear valued customer",
92
+ "we take all complaints seriously",
93
+ "please contact us at",
94
+ "for further assistance",
95
+ "thank you for contacting",
96
+ ]
97
+
98
+ def grade(
99
+ self,
100
+ action: str,
101
+ scenario: dict,
102
+ history: list,
103
+ behaviors_demonstrated: list,
104
+ avoid_violations: list,
105
+ ) -> Tuple[float, dict, Optional[str], Optional[str]]:
106
+
107
+ action_lower = action.lower()
108
+ required_behaviors = scenario.get("required_behaviors", [])
109
+ avoid_behaviors = scenario.get("avoid_behaviors", [])
110
+
111
+ # 1. Behavior Detection
112
+ behavior_found = None
113
+ best_behavior_score = 0.0
114
+
115
+ for behavior in required_behaviors:
116
+ if behavior in behaviors_demonstrated:
117
+ continue
118
+ kw_list = self.BEHAVIOR_KEYWORDS.get(behavior, [])
119
+ matches = sum(1 for kw in kw_list if kw in action_lower)
120
+ if matches > 0:
121
+ ratio = matches / len(kw_list) if kw_list else 0
122
+ if ratio > best_behavior_score:
123
+ best_behavior_score = ratio
124
+ behavior_found = behavior
125
+
126
+ # 2. Empathy Multiplier
127
+ all_empathy_keywords = self.BEHAVIOR_KEYWORDS.get("genuine_empathy", [])
128
+ empathy_matches = sum(1 for kw in all_empathy_keywords if kw in action_lower)
129
+ empathy_score = min(1.3, 1.0 + (empathy_matches * 0.1))
130
+
131
+ # 3. Personalization
132
+ is_generic = any(gi in action_lower for gi in self.GENERIC_INDICATORS)
133
+ personalization_mult = 0.7 if is_generic else 1.0
134
+
135
+ # 4. Violation Detection
136
+ violation_found = None
137
+ for avoidance in avoid_behaviors:
138
+ if avoidance in avoid_violations:
139
+ continue
140
+ kw_list = self.VIOLATION_KEYWORDS.get(avoidance, [])
141
+ matches = sum(1 for kw in kw_list if kw in action_lower)
142
+ if matches > 0:
143
+ violation_found = avoidance
144
+ break
145
+
146
+ # 5. Final Score
147
+ if behavior_found:
148
+ base = min(1.0, best_behavior_score * 1.5)
149
+ score = min(1.0, base * empathy_score * personalization_mult)
150
+ else:
151
+ score = 0.15 * empathy_score if empathy_matches > 0 else 0.05
152
+
153
+ grader_info = {
154
+ "behavior_found": behavior_found,
155
+ "behavior_match_score": round(best_behavior_score, 3),
156
+ "empathy_score": round(empathy_score, 3),
157
+ "personalization": "generic" if is_generic else "personalized",
158
+ "violation_found": violation_found,
159
+ }
160
+
161
+ return round(score, 3), grader_info, behavior_found, violation_found
graders/medium_grader.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Medium Grader β€” Scores multi-step issue resolution responses.
3
+ """
4
+
5
+ from typing import Tuple, Optional
6
+
7
+
8
+ class MediumGrader:
9
+ """
10
+ Scoring logic for Medium (Multi-Step) Task.
11
+
12
+ Formula:
13
+ score = step_detection_score * completeness_multiplier * tone_multiplier
14
+
15
+ Step Detection:
16
+ Each required step has associated keywords.
17
+ If the agent's reply matches those keywords β†’ step is identified.
18
+
19
+ Completeness:
20
+ Partial answers (missing some required info for the step) get partial credit.
21
+
22
+ Tone:
23
+ Professional, empathetic tone adds a small bonus.
24
+ """
25
+
26
+ # Keywords that signal each step type
27
+ STEP_KEYWORDS = {
28
+ # Medium scenario step types β†’ what words trigger them
29
+ "acknowledge_frustration": ["understand", "frustrat", "sorry to hear", "apologize", "that must", "i can see"],
30
+ "acknowledge_issue": ["understand", "sorry", "i can see", "must be", "apologize", "frustrat"],
31
+ "collect_account_info": ["email", "account", "order number", "name", "id", "could you provide", "can i get", "please share"],
32
+ "collect_order_info": ["order number", "order id", "reference", "can i get", "could you share"],
33
+ "investigate": ["look into", "check", "investigate", "pull up", "look at", "review your account"],
34
+ "check_basic_steps": ["browser", "cache", "clear", "try", "incognito", "different browser", "cookies"],
35
+ "offer_password_reset": ["reset", "password", "link", "send you", "email you", "forgot password"],
36
+ "confirm_resolution": ["resolved", "able to log", "working now", "fixed", "sorted", "everything ok", "is that working"],
37
+ "resolve_or_escalate": ["refund", "credit", "resolve", "fix", "escalate", "team will", "processed"],
38
+ "apologize_sincerely": ["sorry", "apologize", "sincerely apologize", "deeply sorry", "truly sorry"],
39
+ "confirm_item_details": ["ordered", "confirm", "blue jacket", "correct item", "you ordered", "size"],
40
+ "arrange_replacement": ["replacement", "send the correct", "return", "new", "reship", "express", "free return"],
41
+ }
42
+
43
+ POLITE_PHRASES = [
44
+ "happy to help", "certainly", "of course", "absolutely",
45
+ "please", "thank you", "glad", "i understand", "i appreciate",
46
+ "let me help", "my pleasure",
47
+ ]
48
+
49
+ BAD_PHRASES = [
50
+ "not my problem", "policy says", "can't do anything", "nothing i can do",
51
+ "you should have", "your fault", "read the faq",
52
+ ]
53
+
54
+ def grade(
55
+ self,
56
+ action: str,
57
+ scenario: dict,
58
+ history: list,
59
+ steps_completed: list,
60
+ ) -> Tuple[float, dict, Optional[str]]:
61
+ """
62
+ Grade the agent's response for a medium-task step.
63
+
64
+ Returns:
65
+ (score: float, grader_info: dict, step_identified: str or None)
66
+ """
67
+ action_lower = action.lower()
68
+ required_steps = scenario.get("required_steps", [])
69
+
70
+ # 1. Detect which step the agent is performing
71
+ step_identified = None
72
+ step_match_score = 0.0
73
+
74
+ for step in required_steps:
75
+ if step in steps_completed:
76
+ continue # Already done, skip
77
+ step_keywords = self.STEP_KEYWORDS.get(step, [])
78
+ if not step_keywords:
79
+ continue
80
+ matches = sum(1 for kw in step_keywords if kw in action_lower)
81
+ match_ratio = matches / len(step_keywords)
82
+ if match_ratio > step_match_score and matches >= 1:
83
+ step_match_score = match_ratio
84
+ step_identified = step
85
+
86
+ # 2. Completeness score (how thoroughly the step is addressed)
87
+ completeness = min(1.0, step_match_score * 1.5) if step_identified else 0.3
88
+
89
+ # 3. Tone multiplier
90
+ has_polite = any(p in action_lower for p in self.POLITE_PHRASES)
91
+ has_bad = any(p in action_lower for p in self.BAD_PHRASES)
92
+
93
+ if has_bad:
94
+ tone_mult = 0.6
95
+ tone = "bad"
96
+ elif has_polite:
97
+ tone_mult = 1.1
98
+ tone = "polite"
99
+ else:
100
+ tone_mult = 1.0
101
+ tone = "neutral"
102
+
103
+ # Final score
104
+ if step_identified:
105
+ score = min(1.0, completeness * tone_mult)
106
+ else:
107
+ # No useful step found β€” partial score for being polite
108
+ score = 0.2 * tone_mult if has_polite else 0.1
109
+
110
+ grader_info = {
111
+ "step_identified": step_identified,
112
+ "step_match_score": round(step_match_score, 3),
113
+ "completeness": round(completeness, 3),
114
+ "tone": tone,
115
+ "already_done": steps_completed,
116
+ }
117
+
118
+ return round(score, 3), grader_info, step_identified
inference.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ inference.py β€” Rule-based agent + CLI test runner
3
+ Usage:
4
+ python inference.py --task easy --agent rule
5
+ python inference.py --task medium --agent rule
6
+ python inference.py --task hard --agent rule
7
+ python inference.py --task all --agent rule
8
+ """
9
+
10
+ import argparse
11
+ import json
12
+ from typing import List
13
+
14
+ from models import StepName, DifficultyLevel
15
+ from environment import CustomerSupportEnv
16
+ from graders.base_grader import BaseGrader, HardTaskGrader
17
+ from tasks import TASK_REGISTRY
18
+
19
+
20
+ # ── Rule-based agent responses ─────────────────────────────────────────────────
21
+
22
+ RULE_BASED_RESPONSES = {
23
+ "easy": [
24
+ # Step 1: Empathy
25
+ (
26
+ "I am truly sorry to hear that your laptop has not arrived yet. "
27
+ "I completely understand how frustrating and urgent this must be for you, "
28
+ "especially when you need it for work. Please allow me to help resolve this immediately."
29
+ ),
30
+ # Step 2: Collect Info
31
+ (
32
+ "To assist you as quickly as possible, could you please provide me with "
33
+ "your order number or order ID? Additionally, may I have your full name "
34
+ "and the email address associated with the order?"
35
+ ),
36
+ # Step 3: Investigate
37
+ (
38
+ "Thank you for that information. I am checking our system right now. "
39
+ "I found that order #ORD-8821 is currently tracked and it appears the package "
40
+ "is stuck at a sorting facility. Our records show the delay is due to a "
41
+ "logistical hold-up that we are actively investigating."
42
+ ),
43
+ # Step 4: Resolution
44
+ (
45
+ "I sincerely apologize for this unacceptable delay. I will personally "
46
+ "expedite your delivery and escalate this to our logistics partner immediately. "
47
+ "You will receive your laptop within 2 business days. As compensation, "
48
+ "I will also credit β‚Ή500 to your account for the inconvenience caused."
49
+ ),
50
+ ],
51
+
52
+ "medium": [
53
+ # Step 1: Empathy
54
+ (
55
+ "I sincerely apologize for the double charge on your account. "
56
+ "I completely understand how upsetting and inconvenient this is. "
57
+ "This should never have happened and I take full responsibility. "
58
+ "I am going to resolve this for you right away."
59
+ ),
60
+ # Step 2: Collect Info
61
+ (
62
+ "To look into this immediately, may I please have your account number "
63
+ "or the email address linked to your subscription? "
64
+ "Could you also confirm the transaction dates and the card used for billing?"
65
+ ),
66
+ # Step 3: Investigate
67
+ (
68
+ "Thank you. I am reviewing your billing history right now. "
69
+ "I can see in our billing system that there was indeed a duplicate charge "
70
+ "on both the 1st and the 15th. It appears this was caused by a payment "
71
+ "gateway retry issue on our end. I found the duplicate transaction clearly."
72
+ ),
73
+ # Step 4: Resolution
74
+ (
75
+ "I sincerely apologize for this error. I will process a full refund of "
76
+ "$49.99 immediately. You should see this credited back to your card "
77
+ "within 3 to 5 business days. I am also escalating this to our billing "
78
+ "team to ensure this does not happen again. Thank you for your patience."
79
+ ),
80
+ ],
81
+
82
+ "hard": [
83
+ # Step 1: Empathy (de-escalation required)
84
+ (
85
+ "Mr. Mehta, I am personally and deeply sorry for what you have experienced. "
86
+ "This is completely unacceptable and you have every right to be angry. "
87
+ "I take full responsibility for the failure of our team to respond properly "
88
+ "over the past three weeks. This should not have happened, especially for a "
89
+ "valued VIP customer like yourself. I hear your frustration and I assure you "
90
+ "that I will personally ensure this is resolved today."
91
+ ),
92
+ # Step 2: Collect Info
93
+ (
94
+ "To immediately escalate this as our top priority, could you please confirm "
95
+ "your order number and VIP account details? I also need your email and "
96
+ "contact number so I can personally follow up with you today."
97
+ ),
98
+ # Step 3: Investigate
99
+ (
100
+ "Thank you, Mr. Mehta. I am reviewing your tickets right now. "
101
+ "I can see five tickets β€” all marked pending β€” with no technician dispatched. "
102
+ "I found that this is unacceptable internally. I have already escalated "
103
+ "this to our senior operations manager and our VIP support head. "
104
+ "Our records show the replacement unit is available and ready to ship."
105
+ ),
106
+ # Step 4: Resolution
107
+ (
108
+ "Mr. Mehta, here is what I am doing right now: "
109
+ "1) A replacement Premium Standing Desk Pro will be dispatched today with "
110
+ "priority delivery β€” you will receive it within 24 hours. "
111
+ "2) As per our VIP policy, I am authorizing a compensation of β‚Ή9,000 "
112
+ "(20% of your order value) credited to your account immediately. "
113
+ "3) I will personally ensure a senior technician contacts you within 2 hours. "
114
+ "4) I am waiving your next month's subscription fee as an additional apology. "
115
+ "I personally guarantee this will be resolved to your satisfaction today."
116
+ ),
117
+ ],
118
+ }
119
+
120
+
121
+ # ── Runner ─────────────────────────────────────────────────────────────────────
122
+
123
+ def run_task(task_name: str) -> dict:
124
+ task = TASK_REGISTRY[task_name]
125
+ grader = HardTaskGrader() if task_name == "hard" else BaseGrader()
126
+ env = CustomerSupportEnv(task=task, grader=grader)
127
+ responses = RULE_BASED_RESPONSES[task_name]
128
+
129
+ print(f"\n{'='*60}")
130
+ print(f" TASK: {task_name.upper()} | {task.task_id}")
131
+ print(f" Customer emotion: {task.customer_emotion}")
132
+ print(f"{'='*60}")
133
+ print(f" Customer: {task.customer_message[:120]}...")
134
+ print(f"{'='*60}\n")
135
+
136
+ for i, response in enumerate(responses):
137
+ step_name = list(StepName)[i]
138
+ result, done = env.step(response)
139
+
140
+ status = "βœ… CORRECT" if result.correct else "❌ WRONG"
141
+ print(f"[Step {i+1}/4] {step_name.value.upper()} β€” {status}")
142
+ print(f" Detected : {result.detected_action}")
143
+ print(f" Reward : {result.reward:.3f} "
144
+ f"(base={result.base_score:.2f}, bonus={result.step_bonus:.2f}, "
145
+ f"penalty={result.penalty:.2f})")
146
+ if result.penalty_reasons:
147
+ for pr in result.penalty_reasons:
148
+ print(f" ⚠ {pr}")
149
+ print()
150
+
151
+ if done:
152
+ break
153
+
154
+ summary = env.summary()
155
+ print(f"\n{'='*60}")
156
+ print(f" EPISODE STATUS : {summary['status'].upper()}")
157
+ print(f" TOTAL REWARD : {summary['total_reward']:.3f} / 4.8 max")
158
+ print(f" WRONG STEPS : {summary['wrong_steps']}")
159
+ if summary["fail_reason"]:
160
+ print(f" FAIL REASON : {summary['fail_reason']}")
161
+ print(f"{'='*60}\n")
162
+
163
+ return summary
164
+
165
+
166
+ def main():
167
+ parser = argparse.ArgumentParser()
168
+ parser.add_argument("--task", choices=["easy", "medium", "hard", "all"],
169
+ default="all")
170
+ parser.add_argument("--agent", choices=["rule"], default="rule")
171
+ args = parser.parse_args()
172
+
173
+ tasks = ["easy", "medium", "hard"] if args.task == "all" else [args.task]
174
+ results = {}
175
+ for t in tasks:
176
+ results[t] = run_task(t)
177
+
178
+ print("\nπŸ“Š FINAL SUMMARY")
179
+ print(json.dumps(results, indent=2))
180
+
181
+
182
+ if __name__ == "__main__":
183
+ main()
models.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ models.py β€” Core data models for AI Customer Support Simulator
3
+ """
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import List, Optional, Dict, Any
7
+ from enum import Enum
8
+
9
+
10
+ class StepName(str, Enum):
11
+ EMPATHY = "empathy"
12
+ COLLECT_INFO = "collect_info"
13
+ INVESTIGATE = "investigate"
14
+ RESOLUTION = "resolution"
15
+
16
+
17
+ class DifficultyLevel(str, Enum):
18
+ EASY = "easy"
19
+ MEDIUM = "medium"
20
+ HARD = "hard"
21
+
22
+
23
+ class EpisodeStatus(str, Enum):
24
+ RUNNING = "running"
25
+ SUCCESS = "success"
26
+ FAIL = "fail"
27
+
28
+
29
+ @dataclass
30
+ class StepResult:
31
+ step: StepName
32
+ agent_response: str
33
+ detected_action: str # what grader detected
34
+ expected_action: str # what was required
35
+ correct: bool
36
+ base_score: float # 0.0 – 1.0
37
+ step_bonus: float # 0.0 – 0.2
38
+ penalty: float # 0.0+ (subtracted)
39
+ penalty_reasons: List[str]
40
+ reward: float # base_score + step_bonus - penalty
41
+ fail_triggered: bool = False
42
+ fail_reason: str = ""
43
+
44
+
45
+ @dataclass
46
+ class Episode:
47
+ task_id: str
48
+ difficulty: DifficultyLevel
49
+ steps: List[StepResult] = field(default_factory=list)
50
+ total_reward: float = 0.0
51
+ wrong_step_count: int = 0
52
+ status: EpisodeStatus = EpisodeStatus.RUNNING
53
+ fail_reason: str = ""
54
+
55
+ # Thresholds
56
+ MAX_WRONG_STEPS: int = 3
57
+ MIN_TOTAL_REWARD: float = 1.5 # out of 4.0 max
58
+
59
+ def add_step(self, result: StepResult) -> None:
60
+ self.steps.append(result)
61
+ self.total_reward += result.reward
62
+ if not result.correct:
63
+ self.wrong_step_count += 1
64
+ self._check_fail_conditions(result)
65
+
66
+ def _check_fail_conditions(self, result: StepResult) -> None:
67
+ if self.status != EpisodeStatus.RUNNING:
68
+ return
69
+
70
+ # Too many wrong steps
71
+ if self.wrong_step_count >= self.MAX_WRONG_STEPS:
72
+ self.status = EpisodeStatus.FAIL
73
+ self.fail_reason = f"Too many wrong steps ({self.wrong_step_count}/{self.MAX_WRONG_STEPS})"
74
+ return
75
+
76
+ # Step-level fail
77
+ if result.fail_triggered:
78
+ self.status = EpisodeStatus.FAIL
79
+ self.fail_reason = result.fail_reason
80
+ return
81
+
82
+ # All 4 steps done β€” check total score
83
+ if len(self.steps) == 4:
84
+ if self.total_reward < self.MIN_TOTAL_REWARD:
85
+ self.status = EpisodeStatus.FAIL
86
+ self.fail_reason = (
87
+ f"Total reward {self.total_reward:.2f} below threshold "
88
+ f"{self.MIN_TOTAL_REWARD}"
89
+ )
90
+ else:
91
+ self.status = EpisodeStatus.SUCCESS
92
+
93
+ def summary(self) -> Dict[str, Any]:
94
+ return {
95
+ "task_id": self.task_id,
96
+ "difficulty": self.difficulty.value,
97
+ "status": self.status.value,
98
+ "total_reward": round(self.total_reward, 3),
99
+ "wrong_steps": self.wrong_step_count,
100
+ "fail_reason": self.fail_reason,
101
+ "steps": [
102
+ {
103
+ "step": s.step.value,
104
+ "correct": s.correct,
105
+ "base_score": round(s.base_score, 3),
106
+ "step_bonus": round(s.step_bonus, 3),
107
+ "penalty": round(s.penalty, 3),
108
+ "reward": round(s.reward, 3),
109
+ "penalty_reasons": s.penalty_reasons,
110
+ }
111
+ for s in self.steps
112
+ ],
113
+ }
114
+
115
+
116
+ @dataclass
117
+ class Task:
118
+ task_id: str
119
+ difficulty: DifficultyLevel
120
+ customer_message: str
121
+ scenario_context: str
122
+ required_steps: List[StepName]
123
+ step_keywords: Dict[StepName, List[str]] # keywords that prove step done
124
+ escalation_risk: bool = False # hard tasks only
125
+ customer_emotion: str = "neutral" # neutral | frustrated | angry
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ fastapi>=0.100.0
2
+ uvicorn>=0.23.0
3
+ groq>=0.4.0
4
+ pyyaml>=6.0
5
+ pydantic>=2.0.0
tasks/ medium_task.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MEDIUM TASK β€” Multi-Step Issue Resolution
3
+ Goal: Agent guides a customer through a multi-turn troubleshooting process.
4
+ """
5
+
6
+ import random
7
+ from graders.medium_grader import MediumGrader
8
+
9
+
10
+ class MediumTask:
11
+ """
12
+ Task: Resolve a customer's technical/account issue over multiple turns.
13
+
14
+ - 3–5 turn conversations.
15
+ - Agent must ask clarifying questions, diagnose the problem, and resolve it.
16
+ - Resolution requires following a correct sequence of steps.
17
+ - Rewarded for each correct step, penalized for skipping or wrong order.
18
+ """
19
+
20
+ description = (
21
+ "Help the customer resolve their issue step-by-step. "
22
+ "Ask clarifying questions, diagnose the problem, and guide them to a resolution. "
23
+ "Complete all required steps in a logical order."
24
+ )
25
+ max_steps = 6
26
+
27
+ SCENARIOS = [
28
+ {
29
+ "id": "med_001",
30
+ "topic": "billing_issue",
31
+ "opening_message": "I was charged twice for my subscription this month. This is really frustrating!",
32
+ "required_steps": [
33
+ "acknowledge_frustration", # "I understand, I'm sorry to hear that..."
34
+ "collect_account_info", # "Could I get your account email/ID?"
35
+ "investigate", # "Let me look into this for you..."
36
+ "resolve_or_escalate", # "I've initiated a refund..." or "escalating..."
37
+ ],
38
+ "resolution": "Issue a full refund for the duplicate charge and confirm via email.",
39
+ "metadata": {
40
+ "product": "Subscription Service",
41
+ "account_id": "ACC-88321",
42
+ "charge_amount": "$29.99",
43
+ "charge_date": "March 28, 2026",
44
+ },
45
+ # Simulated customer replies for each agent step
46
+ "customer_replies": {
47
+ "acknowledge_frustration": "Thank you for understanding. Yes, I need this fixed.",
48
+ "collect_account_info": "Sure, my email is user@example.com.",
49
+ "investigate": "Okay, please check it. I can see two charges on my bank statement.",
50
+ "resolve_or_escalate": "Great, thank you! When will the refund appear?",
51
+ },
52
+ },
53
+ {
54
+ "id": "med_002",
55
+ "topic": "login_problem",
56
+ "opening_message": "I can't log into my account. It keeps saying my password is wrong but I haven't changed it.",
57
+ "required_steps": [
58
+ "acknowledge_issue",
59
+ "check_basic_steps", # "Have you tried clearing your browser cache?"
60
+ "offer_password_reset", # "Let me send you a password reset link."
61
+ "confirm_resolution", # "Were you able to log in successfully?"
62
+ ],
63
+ "resolution": "Send password reset link and confirm the customer successfully logged in.",
64
+ "metadata": {
65
+ "product": "SaaS App",
66
+ "account_status": "Active",
67
+ "last_login": "3 days ago",
68
+ },
69
+ "customer_replies": {
70
+ "acknowledge_issue": "Yes, I've been trying for an hour!",
71
+ "check_basic_steps": "I tried that, it didn't work.",
72
+ "offer_password_reset": "Okay, I got the email. Let me try.",
73
+ "confirm_resolution": "Yes! I'm in now. Thank you so much!",
74
+ },
75
+ },
76
+ {
77
+ "id": "med_003",
78
+ "topic": "wrong_item_shipped",
79
+ "opening_message": "I ordered a blue jacket (size M) but received a red one in size L. What do I do?",
80
+ "required_steps": [
81
+ "apologize_sincerely",
82
+ "collect_order_info", # "Could I get your order number?"
83
+ "confirm_item_details", # "Let me confirm what you ordered..."
84
+ "arrange_replacement", # "I'll arrange a free return and send the correct item."
85
+ ],
86
+ "resolution": "Arrange free return and ship correct item with express delivery.",
87
+ "metadata": {
88
+ "product": "Online Clothing Store",
89
+ "order_id": "ORD-44512",
90
+ "ordered_item": "Blue Jacket Size M",
91
+ "received_item": "Red Jacket Size L",
92
+ },
93
+ "customer_replies": {
94
+ "apologize_sincerely": "Thank you, I hope you can fix this.",
95
+ "collect_order_info": "My order number is ORD-44512.",
96
+ "confirm_item_details": "Yes, that's correct, I ordered the blue one.",
97
+ "arrange_replacement": "That's great, when will the correct one arrive?",
98
+ },
99
+ },
100
+ ]
101
+
102
+ def __init__(self):
103
+ self._grader = MediumGrader()
104
+ self._current_scenario = None
105
+ self._steps_completed = []
106
+
107
+ def reset(self) -> dict:
108
+ self._steps_completed = []
109
+ self._current_scenario = random.choice(self.SCENARIOS).copy()
110
+ return self._current_scenario
111
+
112
+ def step(self, action: str, scenario: dict, history: list, step_number: int) -> dict:
113
+ score, grader_info, step_identified = self._grader.grade(
114
+ action=action,
115
+ scenario=scenario,
116
+ history=history,
117
+ steps_completed=self._steps_completed,
118
+ )
119
+
120
+ # Track completed steps
121
+ if step_identified and step_identified not in self._steps_completed:
122
+ self._steps_completed.append(step_identified)
123
+
124
+ required = scenario["required_steps"]
125
+ completed_count = len(self._steps_completed)
126
+ total_required = len(required)
127
+
128
+ # Reward formula:
129
+ # Each correctly completed step earns proportional reward.
130
+ # Final step earns a bonus. Missing steps or wrong answers penalize.
131
+ if step_identified in required and step_identified not in grader_info.get("already_done", []):
132
+ step_reward = (1.0 / total_required) * score # partial step reward
133
+ else:
134
+ step_reward = -0.1 * (1 - score) # small penalty for non-progress
135
+
136
+ # Check if all required steps are done
137
+ all_done = all(s in self._steps_completed for s in required)
138
+ done = all_done or step_number >= self.max_steps
139
+
140
+ if all_done:
141
+ step_reward += 0.2 # bonus for completing all steps
142
+
143
+ # Generate next customer message based on what step was just done
144
+ next_msg = ""
145
+ if not done:
146
+ replies = scenario.get("customer_replies", {})
147
+ next_msg = replies.get(step_identified, "Okay, what should I do next?")
148
+
149
+ return {
150
+ "reward": max(-1.0, min(1.0, step_reward)),
151
+ "done": done,
152
+ "next_customer_message": next_msg,
153
+ "grader_info": {
154
+ **grader_info,
155
+ "score": score,
156
+ "step_identified": step_identified,
157
+ "steps_completed": list(self._steps_completed),
158
+ "progress": f"{completed_count}/{total_required}",
159
+ },
160
+ }
tasks/__init__.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from tasks.easy_task import EASY_TASK
2
+ from tasks.medium_task import MEDIUM_TASK
3
+ from tasks.hard_task import HARD_TASK, evaluate_hard_response
4
+
5
+ TASK_REGISTRY = {
6
+ "easy": EASY_TASK,
7
+ "medium": MEDIUM_TASK,
8
+ "hard": HARD_TASK,
9
+ }
10
+
11
+ __all__ = ["EASY_TASK", "MEDIUM_TASK", "HARD_TASK",
12
+ "TASK_REGISTRY", "evaluate_hard_response"]
tasks/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (476 Bytes). View file
 
tasks/__pycache__/easy_task.cpython-312.pyc ADDED
Binary file (1.45 kB). View file
 
tasks/__pycache__/hard_task.cpython-312.pyc ADDED
Binary file (6.03 kB). View file
 
tasks/__pycache__/medium_task.cpython-312.pyc ADDED
Binary file (1.72 kB). View file
 
tasks/easy_task.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ tasks/easy_task.py β€” Easy scenario: delayed delivery complaint
3
+ """
4
+
5
+ from models import Task, StepName, DifficultyLevel
6
+
7
+ EASY_TASK = Task(
8
+ task_id = "EASY_001",
9
+ difficulty = DifficultyLevel.EASY,
10
+ customer_emotion = "frustrated",
11
+ escalation_risk = False,
12
+ customer_message = (
13
+ "Hello, I ordered a laptop 10 days ago and it still hasn't arrived. "
14
+ "I need it urgently for work. Can you help me?"
15
+ ),
16
+ scenario_context = (
17
+ "Customer placed order #ORD-8821 on 25 March. "
18
+ "Standard delivery is 5–7 business days. "
19
+ "Tracking shows package is stuck at a sorting facility."
20
+ ),
21
+ required_steps = [
22
+ StepName.EMPATHY,
23
+ StepName.COLLECT_INFO,
24
+ StepName.INVESTIGATE,
25
+ StepName.RESOLUTION,
26
+ ],
27
+ step_keywords = {
28
+ StepName.EMPATHY: ["sorry", "apologize", "understand", "frustrat"],
29
+ StepName.COLLECT_INFO: ["order number", "order id", "could you provide",
30
+ "may i have", "can i have"],
31
+ StepName.INVESTIGATE: ["checking", "looking into", "tracking", "found",
32
+ "our records", "it appears"],
33
+ StepName.RESOLUTION: ["refund", "replacement", "expedite", "credit",
34
+ "we will", "i will"],
35
+ },
36
+ )
tasks/hard_task.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ tasks/hard_task.py β€” Hard scenario: angry VIP customer threatening legal action
3
+ after a 3-week unresolved product failure
4
+
5
+ Grader used: HardTaskGrader (requires de-escalation + concrete resolution)
6
+ """
7
+
8
+ from models import Task, StepName, DifficultyLevel
9
+
10
+ HARD_TASK = Task(
11
+ task_id = "HARD_001",
12
+ difficulty = DifficultyLevel.HARD,
13
+ customer_emotion = "angry",
14
+ escalation_risk = True,
15
+ customer_message = (
16
+ "I am ABSOLUTELY FURIOUS. I purchased your premium standing desk "
17
+ "(β‚Ή45,000) three weeks ago and it arrived broken. I've called five times, "
18
+ "sent four emails, and NOBODY has fixed this. I'm a VIP member and this "
19
+ "is how you treat me?! I am going to post this on every review site, "
20
+ "contact consumer court, and make sure everyone knows how pathetic your "
21
+ "company's service is. I want this resolved TODAY or I'm cancelling my "
22
+ "entire β‚Ή2 lakh annual contract!"
23
+ ),
24
+ scenario_context = (
25
+ "Customer: Rajesh Mehta | VIP Tier | Annual contract: β‚Ή2,00,000. "
26
+ "Order #ORD-VIP-2241 β€” Premium Standing Desk Pro (β‚Ή45,000) delivered "
27
+ "on 12 March with a broken motorised lift mechanism. "
28
+ "5 prior support tickets (TKT-001 to TKT-005) β€” all marked 'pending'. "
29
+ "No technician dispatched yet. Replacement stock available (3–5 days). "
30
+ "Policy: VIP customers get same-day escalation + compensation up to 20% "
31
+ "of order value for delays > 7 days."
32
+ ),
33
+ required_steps = [
34
+ StepName.EMPATHY,
35
+ StepName.COLLECT_INFO,
36
+ StepName.INVESTIGATE,
37
+ StepName.RESOLUTION,
38
+ ],
39
+ step_keywords = {
40
+ StepName.EMPATHY: [
41
+ "i am personally and deeply sorry",
42
+ "deeply sorry",
43
+ "sincerely apologize",
44
+ "i completely understand",
45
+ "that is completely unacceptable",
46
+ "you have every right",
47
+ "i take full responsibility",
48
+ "this should not have happened",
49
+ "i hear your frustration",
50
+ ],
51
+ StepName.COLLECT_INFO: [
52
+ "order number", "ticket number", "vip", "account",
53
+ "could you confirm", "full name", "email", "contact",
54
+ ],
55
+ StepName.INVESTIGATE: [
56
+ "reviewing your tickets", "i can see", "five tickets",
57
+ "all marked pending", "this is unacceptable internally",
58
+ "i have escalated", "our records show", "found that",
59
+ "i found", "looking into",
60
+ ],
61
+ StepName.RESOLUTION: [
62
+ "replacement", "technician today", "within 24 hours",
63
+ "β‚Ή9,000", "9000", "20%", "compensation",
64
+ "personally ensure", "i will personally",
65
+ "waive", "free of charge", "priority",
66
+ ],
67
+ },
68
+ )
69
+
70
+
71
+ # ── Hard-task specific scoring weights ────────────────────────────────────────
72
+ HARD_TASK_SCORING = {
73
+ "emotional_control": 0.30, # calm + empathetic tone
74
+ "conflict_handling": 0.25, # de-escalation phrases
75
+ "professional_tone": 0.20, # no blame, no dismissal
76
+ "resolution_accuracy": 0.25, # correct refund % + timeline
77
+ }
78
+
79
+
80
+ def evaluate_hard_response(response: str, step: StepName) -> dict:
81
+ """
82
+ Returns a breakdown dict for hard-task evaluation display.
83
+ Used by the UI to show per-dimension scores.
84
+ """
85
+ r = response.lower()
86
+ scores = {}
87
+
88
+ if step == StepName.EMPATHY:
89
+ emotional_hits = sum(1 for kw in HARD_TASK.step_keywords[StepName.EMPATHY]
90
+ if kw in r)
91
+ scores["emotional_control"] = min(1.0, emotional_hits / 3)
92
+
93
+ deesc = ["completely understand", "every right", "full responsibility",
94
+ "should not have happened", "hear your frustration"]
95
+ deesc_hits = sum(1 for kw in deesc if kw in r)
96
+ scores["conflict_handling"] = min(1.0, deesc_hits / 2)
97
+
98
+ bad = ["calm down", "your fault", "nothing we can do", "policy says"]
99
+ scores["professional_tone"] = 0.0 if any(b in r for b in bad) else 1.0
100
+
101
+ scores["resolution_accuracy"] = 0.0 # N/A at empathy step
102
+
103
+ elif step == StepName.RESOLUTION:
104
+ scores["emotional_control"] = 1.0 if "personally ensure" in r else 0.5
105
+ scores["conflict_handling"] = 1.0 if "priority" in r else 0.5
106
+ scores["professional_tone"] = 1.0 if "i will personally" in r else 0.5
107
+
108
+ # Check resolution accuracy: compensation + timeline
109
+ has_comp = any(c in r for c in ["9,000", "9000", "20%", "compensation"])
110
+ has_timeline = any(t in r for t in ["24 hours", "today", "tomorrow", "3 days"])
111
+ scores["resolution_accuracy"] = (0.5 * int(has_comp)) + (0.5 * int(has_timeline))
112
+
113
+ else:
114
+ # Default neutral for mid steps
115
+ scores = {k: 0.5 for k in HARD_TASK_SCORING}
116
+
117
+ weighted = sum(HARD_TASK_SCORING[k] * scores.get(k, 0.0)
118
+ for k in HARD_TASK_SCORING)
119
+ scores["weighted_total"] = round(weighted, 3)
120
+ return scores
tasks/medium_task.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ tasks/medium_task.py β€” Medium scenario: billing error + overcharge
3
+ """
4
+
5
+ from models import Task, StepName, DifficultyLevel
6
+
7
+ MEDIUM_TASK = Task(
8
+ task_id = "MEDIUM_001",
9
+ difficulty = DifficultyLevel.MEDIUM,
10
+ customer_emotion = "frustrated",
11
+ escalation_risk = False,
12
+ customer_message = (
13
+ "I've been charged twice for my subscription this month β€” "
14
+ "$49.99 appeared on my card on both the 1st and the 15th. "
15
+ "This is completely unacceptable. I want my money back!"
16
+ ),
17
+ scenario_context = (
18
+ "Customer account: ACC-4492. "
19
+ "Subscription plan: Pro Monthly ($49.99/month). "
20
+ "Billing system shows a duplicate charge due to a payment gateway retry bug on the 15th. "
21
+ "Policy: full refund issued within 3–5 business days for duplicate charges."
22
+ ),
23
+ required_steps = [
24
+ StepName.EMPATHY,
25
+ StepName.COLLECT_INFO,
26
+ StepName.INVESTIGATE,
27
+ StepName.RESOLUTION,
28
+ ],
29
+ step_keywords = {
30
+ StepName.EMPATHY: ["sorry", "apologize", "understand", "frustrat",
31
+ "unacceptable", "inconvenien"],
32
+ StepName.COLLECT_INFO: ["account number", "email", "could you confirm",
33
+ "transaction", "billing date", "card", "may i"],
34
+ StepName.INVESTIGATE: ["checking", "i can see", "our records", "duplicate",
35
+ "found", "it appears", "billing system"],
36
+ StepName.RESOLUTION: ["refund", "credit", "3 to 5", "3-5", "business days",
37
+ "we will process", "i will process", "reimburse"],
38
+ },
39
+ )