__pycache__/app.cpython-313.pyc ADDED
Binary file (864 Bytes). View file
 
__pycache__/client.cpython-313.pyc ADDED
Binary file (2.93 kB). View file
 
__pycache__/data_loader.cpython-313.pyc ADDED
Binary file (3.75 kB). View file
 
__pycache__/kb.cpython-313.pyc ADDED
Binary file (1.89 kB). View file
 
__pycache__/models.cpython-313.pyc ADDED
Binary file (2.5 kB). View file
 
__pycache__/tasks.cpython-313.pyc ADDED
Binary file (6.61 kB). View file
 
server/__init__.py ADDED
File without changes
server/__pycache__/app.cpython-313.pyc ADDED
Binary file (880 Bytes). View file
 
server/__pycache__/customer_support_environment.cpython-313.pyc ADDED
Binary file (10.4 kB). View file
 
server/app.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from openenv.core.env_server.http_server import create_app
4
+
5
+ from customer_support_environment import CustomerSupportEnvironment
6
+ from models import SupportAction, SupportObservation
7
+
8
+
9
+ app = create_app(
10
+ CustomerSupportEnvironment,
11
+ SupportAction,
12
+ SupportObservation,
13
+ env_name="customer_support_benchmark",
14
+ max_concurrent_envs=2,
15
+ )
16
+
17
+
18
+ def main(host: str = "0.0.0.0", port: int = 8000) -> None:
19
+ import uvicorn
20
+
21
+ uvicorn.run(app, host=host, port=port)
22
+
23
+
24
+ if __name__ == "__main__":
25
+ main()
server/customer_support_environment.py ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from difflib import SequenceMatcher
4
+ from typing import Any, Optional
5
+ from uuid import uuid4
6
+
7
+ from openenv.core.env_server.interfaces import Environment
8
+
9
+ from data_loader import build_expected, load_dataset, split_difficulty
10
+ from kb import build_knowledge_base
11
+ from models import SupportAction, SupportObservation, SupportState
12
+ from tasks import grade_task, task_for_difficulty
13
+
14
+
15
+ class CustomerSupportEnvironment(Environment[SupportAction, SupportObservation, SupportState]):
16
+ SUPPORTS_CONCURRENT_SESSIONS = True
17
+
18
+ def __init__(self, csv_path: str = "dataset.csv", max_steps: int = 6):
19
+ super().__init__()
20
+ self.df = load_dataset(csv_path)
21
+ self.splits = split_difficulty(self.df)
22
+ self.kb = build_knowledge_base(self.df)
23
+ self.max_steps = max_steps
24
+
25
+ self._episodes = self.df.reset_index(drop=True)
26
+ self._cursor = 0
27
+ self._expected: dict[str, Any] | None = None
28
+ self._history: list[dict[str, Any]] = []
29
+ self._difficulty_filter: Optional[str] = None
30
+ self._final_score = 0.0
31
+ self._done = False
32
+ self._task_id = ""
33
+ self._state = SupportState(episode_id=str(uuid4()), step_count=0)
34
+
35
+ def reset(
36
+ self,
37
+ seed: Optional[int] = None,
38
+ episode_id: Optional[str] = None,
39
+ difficulty: Optional[str] = None,
40
+ index: Optional[int] = None,
41
+ **kwargs: Any,
42
+ ) -> SupportObservation:
43
+ del seed, kwargs
44
+ if difficulty is not None:
45
+ normalized = difficulty.strip().lower()
46
+ if normalized not in self.splits:
47
+ raise ValueError(f"Unknown difficulty: {difficulty}")
48
+ self._difficulty_filter = normalized
49
+ self._episodes = self.splits[normalized].reset_index(drop=True)
50
+ elif self._difficulty_filter is None:
51
+ self._episodes = self.df.reset_index(drop=True)
52
+
53
+ if len(self._episodes) == 0:
54
+ raise ValueError("No episodes found for the requested filter")
55
+
56
+ if index is None:
57
+ self._cursor = self._cursor % len(self._episodes)
58
+ else:
59
+ self._cursor = int(index) % len(self._episodes)
60
+
61
+ row = self._episodes.iloc[self._cursor]
62
+ self._cursor = (self._cursor + 1) % len(self._episodes)
63
+ self._expected = build_expected(row)
64
+ self._task_id = task_for_difficulty(self._expected["difficulty"]).task_id
65
+ self._history = []
66
+ self._done = False
67
+ self._final_score = 0.0
68
+
69
+ self._state = SupportState(
70
+ episode_id=episode_id or str(uuid4()),
71
+ step_count=0,
72
+ difficulty_filter=self._difficulty_filter,
73
+ current_index=self._cursor,
74
+ task_id=self._task_id,
75
+ score_so_far=0.0,
76
+ final_score=0.0,
77
+ done=False,
78
+ )
79
+
80
+ return self._make_observation(reward=0.0, done=False, feedback="Environment reset")
81
+
82
+ def step(
83
+ self,
84
+ action: SupportAction,
85
+ timeout_s: Optional[float] = None,
86
+ **kwargs: Any,
87
+ ) -> SupportObservation:
88
+ del timeout_s, kwargs
89
+ if self._expected is None:
90
+ raise RuntimeError("Call reset before step")
91
+ if self._done:
92
+ return self._make_observation(reward=0.0, done=True, feedback="Episode already done")
93
+
94
+ self._state.step_count += 1
95
+ reward = -0.01 * self._state.step_count
96
+ feedback_parts: list[str] = ["time_penalty"]
97
+
98
+ if action.action_type == "classify":
99
+ if _norm(action.content) == _norm(self._expected["expected_category"]):
100
+ reward += 0.3
101
+ feedback_parts.append("classification_correct")
102
+ else:
103
+ reward -= 0.1
104
+ feedback_parts.append("classification_incorrect")
105
+
106
+ elif action.action_type == "search_kb":
107
+ if _norm(action.content) == _norm(self._expected["kb_id"]):
108
+ reward += 0.2
109
+ feedback_parts.append("kb_match")
110
+ else:
111
+ reward -= 0.05
112
+ feedback_parts.append("kb_mismatch")
113
+
114
+ elif action.action_type == "respond":
115
+ similarity = SequenceMatcher(
116
+ None,
117
+ _norm(action.content),
118
+ _norm(self._expected["expected_response"]),
119
+ ).ratio()
120
+ reward += 0.45 * similarity
121
+ if _is_polite(action.content):
122
+ reward += 0.1
123
+ feedback_parts.append("polite")
124
+ if similarity < 0.25:
125
+ reward -= 0.2
126
+ feedback_parts.append("hallucination_risk")
127
+ self._done = True
128
+ feedback_parts.append("terminal_respond")
129
+
130
+ elif action.action_type == "escalate":
131
+ if bool(self._expected["requires_escalation"]):
132
+ reward += 0.3
133
+ feedback_parts.append("escalation_correct")
134
+ else:
135
+ reward -= 0.2
136
+ feedback_parts.append("unnecessary_escalation")
137
+ self._done = True
138
+ feedback_parts.append("terminal_escalate")
139
+
140
+ if self._state.step_count >= self.max_steps:
141
+ self._done = True
142
+ feedback_parts.append("max_steps")
143
+
144
+ event = {
145
+ "step": self._state.step_count,
146
+ "action_type": action.action_type,
147
+ "content": action.content,
148
+ "reward": round(reward, 4),
149
+ }
150
+ self._history.append(event)
151
+
152
+ if self._done:
153
+ self._final_score = grade_task(self._task_id, self._expected, self._history)
154
+ self._state.final_score = self._final_score
155
+
156
+ self._state.score_so_far = max(0.0, min(1.0, self._state.score_so_far + max(reward, 0.0) / 2.0))
157
+ self._state.done = self._done
158
+
159
+ return self._make_observation(
160
+ reward=round(reward, 4),
161
+ done=self._done,
162
+ feedback=",".join(feedback_parts),
163
+ )
164
+
165
+ @property
166
+ def state(self) -> SupportState:
167
+ return self._state
168
+
169
+ def _make_observation(self, reward: float, done: bool, feedback: str) -> SupportObservation:
170
+ if self._expected is None:
171
+ raise RuntimeError("Environment is not initialized")
172
+ return SupportObservation(
173
+ ticket_id=self._expected["ticket_id"],
174
+ task_id=self._task_id,
175
+ difficulty=self._expected["difficulty"],
176
+ query=self._expected["query"],
177
+ kb_id=self._expected["kb_id"],
178
+ requires_escalation=self._expected["requires_escalation"],
179
+ history=list(self._history),
180
+ done=done,
181
+ reward=reward,
182
+ feedback=feedback,
183
+ metadata={
184
+ "expected_category": self._expected["expected_category"],
185
+ "expected_action": self._expected["expected_action"],
186
+ "expected_response": self._expected["expected_response"],
187
+ "kb_id": self._expected["kb_id"],
188
+ "requires_escalation": self._expected["requires_escalation"],
189
+ "final_score": self._final_score,
190
+ },
191
+ )
192
+
193
+
194
+ def _norm(text: str) -> str:
195
+ return " ".join(str(text).strip().lower().split())
196
+
197
+
198
+ def _is_polite(text: str) -> bool:
199
+ lowered = _norm(text)
200
+ return any(token in lowered for token in ("thank", "please", "assist", "apolog"))