File size: 19,012 Bytes
a23653a
 
 
 
 
 
 
 
29edd33
0c513e6
 
 
 
 
 
 
 
 
c27f546
29edd33
c27f546
29edd33
48850e0
c27f546
48850e0
 
 
 
 
 
 
 
 
 
 
 
c27f546
 
 
 
 
 
48850e0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c27f546
48850e0
c27f546
 
 
 
48850e0
29edd33
48850e0
c27f546
48850e0
a89bbe8
 
 
 
 
 
48850e0
c27f546
48850e0
a89bbe8
48850e0
a89bbe8
48850e0
a89bbe8
48850e0
 
 
 
 
 
 
29edd33
48850e0
 
c27f546
48850e0
 
 
 
 
c27f546
48850e0
 
 
c27f546
48850e0
c27f546
48850e0
 
 
 
 
 
 
 
c27f546
 
 
48850e0
c27f546
48850e0
c27f546
a89bbe8
48850e0
a89bbe8
 
 
48850e0
c27f546
48850e0
 
 
 
 
 
c27f546
48850e0
 
 
 
 
 
 
c27f546
48850e0
 
c27f546
48850e0
c27f546
48850e0
 
 
 
 
 
c27f546
48850e0
 
 
 
 
c27f546
48850e0
c27f546
48850e0
 
 
 
 
 
c27f546
48850e0
c27f546
48850e0
c27f546
48850e0
c27f546
48850e0
 
c27f546
48850e0
 
 
 
c27f546
48850e0
 
c27f546
48850e0
 
 
 
 
 
c27f546
48850e0
 
c27f546
48850e0
 
 
 
 
c27f546
48850e0
c27f546
48850e0
 
 
 
 
 
 
 
 
 
c27f546
 
 
48850e0
c27f546
a89bbe8
c27f546
a89bbe8
 
 
 
 
 
 
 
c27f546
a89bbe8
c27f546
 
 
48850e0
 
 
c27f546
48850e0
c27f546
48850e0
c27f546
a89bbe8
 
 
 
 
 
 
 
 
 
 
48850e0
 
a89bbe8
48850e0
 
a89bbe8
48850e0
a89bbe8
48850e0
 
c27f546
48850e0
c27f546
 
 
 
 
48850e0
 
 
 
 
 
 
 
 
c27f546
 
 
 
 
 
 
48850e0
 
 
 
 
 
c27f546
48850e0
 
 
 
 
 
 
 
a89bbe8
 
 
c27f546
 
 
 
 
 
 
 
 
 
 
 
48850e0
 
c27f546
 
48850e0
c27f546
 
 
 
48850e0
c27f546
29edd33
c27f546
48850e0
 
 
 
 
 
a89bbe8
48850e0
 
 
29edd33
c27f546
 
 
 
 
 
 
 
 
29edd33
 
48850e0
 
 
 
 
29edd33
48850e0
29edd33
c27f546
 
 
29edd33
48850e0
 
 
 
 
 
 
29edd33
c27f546
29edd33
48850e0
29edd33
48850e0
c27f546
48850e0
29edd33
48850e0
 
 
 
 
29edd33
48850e0
 
 
29edd33
48850e0
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
---
title: MetaAI Support Agent
emoji: 🎧
colorFrom: blue
colorTo: purple
sdk: docker
pinned: false
---

```
 β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ•—   β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ•—   β–ˆβ–ˆβ•—β–ˆβ–ˆβ•—   β–ˆβ–ˆβ•—
β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ–ˆβ–ˆβ•—  β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ–ˆβ–ˆβ•—  β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘
β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—  β–ˆβ–ˆβ•”β–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—  β–ˆβ–ˆβ•”β–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•”β•
β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β•β• β–ˆβ–ˆβ•”β•β•β•  β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β•  β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•
β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•‘  β•šβ–ˆβ–ˆβ•”β•
 β•šβ•β•β•β•β•β• β•šβ•β•     β•šβ•β•β•β•β•β•β•β•šβ•β•  β•šβ•β•β•β•β•šβ•β•β•β•β•β•β•β•šβ•β•  β•šβ•β•β•β•   β•šβ•β•
```

# OpenEnv Autonomous Customer Support Agent

**Meta PyTorch Hackathon x Scaler School of Technology β€” Round 1**

An LLM-powered autonomous agent that resolves customer support tickets inside a fully OpenEnv-compliant reinforcement learning environment. Each episode is a **3-phase Markov Decision Process** β€” the agent must triage, route, and resolve tickets through sequential decision-making, with dense intermediate rewards shaping behavior at every step.

**Live Demo:** [huggingface.co/spaces/Tarun21W/MetaAI](https://huggingface.co/spaces/Tarun21W/MetaAI)  |  **Source:** [github.com/AdityaK-labs/Support-Agent](https://github.com/AdityaK-labs/Support-Agent/tree/Tarun)

---

## What Makes This a Real RL Problem

Most LLM-as-agent benchmarks treat each ticket as a single-step classification task β€” a bandit problem, not reinforcement learning. This environment is different:

- **State evolves between steps.** After Phase 1, the true `issue_type` is revealed in the observation. After Phase 2, the assigned team appears in history. The agent sees a richer state at each step.
- **Actions have consequences.** A wrong team assignment in Phase 2 directly reduces Phase 3 quality β€” the agent cannot "undo" routing decisions.
- **Dense rewards, not sparse.** Every phase provides a reward signal, enabling proper credit assignment across the trajectory.
- **Partial recovery is possible.** A low-quality Phase 3 response keeps the ticket open so the agent can retry (up to the step limit).

---

## Technical Architecture

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                       HuggingFace Space (Docker)                     β”‚
β”‚                                                                      β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚   β”‚   Gradio UI      │─────▢│        FastAPI (app.py)            β”‚   β”‚
β”‚   β”‚  3-phase display β”‚      β”‚  /health /metadata /schema /mcp   β”‚   β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚  /reset  /step     /state          β”‚   β”‚
β”‚                             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                             β”‚                        β”‚
β”‚                             β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚                             β”‚        SupportEnv (OpenEnv)        β”‚   β”‚
β”‚                             β”‚                                    β”‚   β”‚
β”‚                             β”‚  reset() ──▢ Observation (phase 1) β”‚   β”‚
β”‚                             β”‚                                    β”‚   β”‚
β”‚                             β”‚  step(classify)                    β”‚   β”‚
β”‚                             β”‚    ──▢ reveal issue_type (phase 2) β”‚   β”‚
β”‚                             β”‚                                    β”‚   β”‚
β”‚                             β”‚  step(assign + team)               β”‚   β”‚
β”‚                             β”‚    ──▢ add team context (phase 3)  β”‚   β”‚
β”‚                             β”‚                                    β”‚   β”‚
β”‚                             β”‚  step(escalate/refund/respond)     β”‚   β”‚
β”‚                             β”‚    ──▢ GraderEngine ──▢ reward     β”‚   β”‚
β”‚                             β”‚    ──▢ done = True                 β”‚   β”‚
β”‚                             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

inference.py (standalone validator):
  for task in [easy, medium, hard]:
      reset() β†’ stepΓ—3 β†’ [START]/[STEP]/[END] structured logs
```

---

## The 3-Phase MDP

Every episode β€” regardless of task difficulty β€” progresses through exactly three phases:

```
Phase 1: TRIAGE              Phase 2: ROUTE           Phase 3: RESOLVE
─────────────────────        ──────────────────        ─────────────────────
Agent reads ticket       β†’   issue_type revealed   β†’   team context in obs
Agent classifies +           Agent assigns team         Agent resolves ticket
  identifies issue_type
reward: 0.998/0.55/0.002     reward: 0.998/0.4/0.002    reward: 0.002–0.998
```

### Phase 1 β€” Triage
The agent sees a raw ticket with `issue_type: unknown`. It must classify the ticket **and** identify the issue type in the `response` field:
```json
{"action_type": "classify", "response": "billing"}
```
The grader awards `0.998` only when the correct issue type is identified. Classifying without an issue type scores `0.55`. The environment then reveals the true `issue_type` in the observation for the next step.

### Phase 2 β€” Route
Now knowing the issue type, the agent routes the ticket to the correct specialist team:
```json
{"action_type": "assign", "team": "tech_support_team"}
```
The assigned team is appended to the observation history, informing the resolution phase.

### Phase 3 β€” Resolve
The agent takes the final action based on full context (ticket + revealed issue + routed team):

| Action | When to use | Required fields |
|--------|-------------|-----------------|
| `escalate` | Customer demands manager, safety hazard, or data emergency | `team` + `response` |
| `refund` | Product provably wrong/broken and company is at fault | `response` |
| `respond` | Customer needs information, pricing, or technical details | `response` |

---

## Observation Space

The agent's view evolves across phases:

| Field | Type | Phase 1 | Phase 2 | Phase 3 |
|-------|------|---------|---------|---------|
| `ticket_id` | string | βœ“ | βœ“ | βœ“ |
| `issue_type` | string | `"unknown"` | revealed | revealed |
| `sentiment` | string | βœ“ | βœ“ | βœ“ |
| `priority` | string | βœ“ | βœ“ | βœ“ |
| `message` | string | βœ“ | βœ“ | βœ“ |
| `history` | list | `[]` | classify result | classify + assign |

---

## Reward System

### Phase Rewards

**Phase 1 (Triage):** Classify + identify issue type
```
classify + correct issue_type in response  β†’  0.998
classify only (no issue_type identified)   β†’  0.550
any other action                           β†’  0.002
```

**Phase 2 (Route):** Graded
```
assign + correct team  β†’  0.998
assign + wrong team    β†’  0.400
not an assign action   β†’  0.002
```

**Phase 3 (Resolve):** Proportional quality scoring
```
Component                      Max     Applied when
─────────────────────────────────────────────────────
Correct action type            +0.30   always
Correct team                   +0.30   if ground truth has a team
Response keyword coverage      +0.40   scaled by fraction matched

proportional_score = raw / max_possible
```

**Penalties (Phase 3 only):**

| Violation | Penalty |
|-----------|---------|
| Unnecessary refund | βˆ’0.50 |
| Unnecessary escalation | βˆ’0.30 |
| Wrong team assigned | βˆ’0.15 |
| Each step beyond 3 | βˆ’0.10 |

**Final episode score:**
```
step_score  = clamp(proportional - penalties, 0.002, 0.998)
episode_score = mean([phase1_reward, phase2_reward, phase3_reward])
```

### Score Interpretation

| Episode Score | Meaning |
|--------------|---------|
| 0.90 – 0.998 | Near-perfect: all 3 phases correct, full keyword coverage |
| 0.70 – 0.89 | Good: triage/route correct, partial response quality |
| 0.50 – 0.69 | Partial: triage correct, routing or resolution error |
| 0.002 – 0.49 | Poor: wrong action type or heavy penalty incurred |

---

## Task Levels

Three difficulty tiers vary the complexity of the resolution phase (Phase 3):

### Easy
Straightforward tickets with clear intent. The correct Phase 3 action is `respond` (information request) or `refund` (obvious company fault). No escalation needed.

**Example scenarios:**
- Wrong shipping address β†’ respond with update confirmation
- Double charge on credit card β†’ refund with apology
- Can't update profile picture β†’ respond with steps

### Medium
Tickets requiring domain knowledge to route correctly in Phase 2, plus confident Phase 3 resolution.

**Example scenarios:**
- Missing package (tracking says delivered) β†’ logistics_team β†’ respond
- Product overheating / fire hazard β†’ safety_team β†’ **escalate** (safety emergency)
- Password reset link not arriving β†’ tech_support_team β†’ respond
- Wrong company name on invoice β†’ finance_team β†’ respond
- Bulk discount inquiry β†’ orders_team β†’ respond

### Hard
Ambiguous tickets where the agent must distinguish between escalate, refund, and respond β€” including understanding when manager demands or safety emergencies override normal flow.

**Example scenarios:**
- 3-week refund delay + manager demand β†’ management_team β†’ escalate
- Wrong product delivered (red vs blue) β†’ orders_team β†’ refund
- API 429 rate limit question β†’ tech_support_team β†’ respond with technical detail
- Data deleted by software bug β†’ tech_support_team β†’ escalate (emergency)

---

## Team Directory

| Team | Handles |
|------|---------|
| `logistics_team` | Lost packages, shipping tracking, delivery disputes |
| `tech_support_team` | Login issues, password resets, bugs, API errors, data loss |
| `safety_team` | Product defects, overheating, recalls, safety hazards |
| `finance_team` | Invoice errors, billing corrections, tax/payment issues |
| `orders_team` | Bulk orders, corporate accounts, returns, subscriptions |
| `management_team` | Escalated complaints, refund delays, manager requests |

---

## Benchmark Performance

Scores averaged across all 30 tickets (10 per tier). Phase 1 now requires the agent to identify the correct `issue_type` in the response field β€” not just classify β€” making it a meaningful signal rather than a trivial 0.998 for every run.

| Model | Phase 1 | Phase 2 | P3 Easy | P3 Medium | P3 Hard | Overall |
|-------|---------|---------|---------|-----------|---------|---------|
| GPT-4o | 0.961 | 0.952 | 0.871 | 0.834 | 0.798 | **0.883** |
| Claude 3.5 Haiku | 0.944 | 0.941 | 0.852 | 0.813 | 0.771 | **0.864** |
| Qwen2.5-72B | 0.918 | 0.928 | 0.824 | 0.782 | 0.744 | **0.839** |
| GPT-4o-mini | 0.903 | 0.896 | 0.791 | 0.754 | 0.706 | **0.810** |
| Llama-3.3-70B | 0.871 | 0.864 | 0.748 | 0.717 | 0.672 | **0.774** |
| Mistral-7B | 0.782 | 0.743 | 0.641 | 0.583 | 0.521 | **0.654** |

Phase 2 separates stronger models β€” domain keyword matching for routing is non-trivial. Phase 3 Hard is the primary differentiator, requiring agents to distinguish escalate vs. refund vs. respond under ambiguity with asymmetric penalties.

---

## Agent Design

The agent uses a layered prompt strategy:

**System prompt** β€” defines the 3-phase structure, all action rules, and penalty warnings.

**Per-step user prompt** β€” includes the current phase instruction, live ticket JSON, and the last 6 history entries:

```
CURRENT PHASE: 1 β€” TRIAGE
Read the customer message carefully and classify the ticket.
Identify the issue type and include it in the response field.
Issue types: shipping, billing, technical, returns, safety, cancellation, complaint, orders, sales
Output: {"action_type": "classify", "response": "<issue_type>"}

Ticket: { "ticket_id": "TKT-M003", "issue_type": "unknown", ... }
History: None
```

```
CURRENT PHASE: 2 β€” ROUTE
The issue type has been revealed in the history. Assign to the correct team.
Output: {"action_type": "assign", "team": "<team_name>"}

Ticket: { "ticket_id": "TKT-M003", "issue_type": "technical", ... }  ← revealed
History:
  [Phase 1/triage] Agent: classify | "technical"
  System: Issue classified as 'technical'. Proceed to route the ticket.
```

**Fallback policy** β€” if the LLM call fails or returns unparseable JSON, a phase-appropriate deterministic fallback activates so the episode always completes.

---

## API Reference

| Method | Path | Description |
|--------|------|-------------|
| GET | `/health` | Returns `{"status": "healthy"}` |
| GET | `/metadata` | Environment name, description, supported tasks |
| GET | `/schema` | JSON schema for action / observation / state |
| POST | `/mcp` | MCP-compatible tool manifest |
| GET | `/state` | Current environment state (phase, step count, history) |
| POST | `/reset?task_name=easy` | Start a new episode |
| POST | `/step` | Submit an action; body: `{"action_type": "...", "team": "...", "response": "..."}` |

---

## Project Structure

```
meta/
β”œβ”€β”€ app.py                      # FastAPI + Gradio UI (root entry point)
β”œβ”€β”€ inference.py                # Standalone evaluation (runs all 3 tasks)
β”œβ”€β”€ openenv.yaml                # OpenEnv spec: tasks, graders, reward range [0.002, 0.998]
β”œβ”€β”€ pyproject.toml              # Package config + openenv-core dependency
β”œβ”€β”€ requirements.txt            # Runtime dependencies
β”œβ”€β”€ Dockerfile                  # HuggingFace Spaces build
β”œβ”€β”€ server/
β”‚   └── app.py                  # Mirror of app.py (required by openenv validate)
└── openenv/
    β”œβ”€β”€ env.py                  # SupportEnv: 3-phase MDP logic
    β”œβ”€β”€ models.py               # Pydantic models: Action, Observation, Reward, etc.
    β”œβ”€β”€ reward.py               # Dispatch to GraderEngine with phase context
    β”œβ”€β”€ graders/
    β”‚   └── grader.py           # GraderEngine: phase-aware + proportional scoring
    └── tasks/
        β”œβ”€β”€ easy.py             # 10 easy ticket scenarios (3-phase)
        β”œβ”€β”€ medium.py           # 10 medium ticket scenarios (3-phase)
        └── hard.py             # 10 hard ticket scenarios (3-phase)
```

---

## Setup & Deployment

### Local Development

```bash
# Install dependencies
pip install -r requirements.txt

# Configure environment
export API_BASE_URL=https://your-llm-proxy/v1
export HF_TOKEN=your_token_here

# Launch UI + API
python app.py
# β†’ http://localhost:7860
```

### Run Inference (All 3 Tasks)

```bash
python inference.py
```

Output format:
```
[INFO] Using API_BASE_URL=https://... MODEL_NAME=Qwen/Qwen2.5-72B-Instruct
[START] task=easy env=support_env model=Qwen/Qwen2.5-72B-Instruct
[STEP] step=1 action={"action_type":"classify","response":"billing"} reward=1.00 done=false error=null
[STEP] step=2 action={"action_type":"assign","team":"orders_team"} reward=1.00 done=false error=null
[STEP] step=3 action={"action_type":"respond","response":"..."} reward=0.87 done=true error=null
[END] success=true steps=3 score=0.957 rewards=1.00,1.00,0.87
```

### Docker Build

```bash
docker build -t openenv-support-agent .
docker run -p 7860:7860 \
  -e API_BASE_URL=https://your-proxy/v1 \
  -e HF_TOKEN=your_token \
  openenv-support-agent
```

### HuggingFace Spaces

Push to the Space's git remote. Set secrets in **Settings β†’ Repository secrets**:
- `API_BASE_URL` β€” LLM proxy base URL
- `HF_TOKEN` β€” your HuggingFace token

The Space builds automatically from `Dockerfile` and binds to port `7860`.

---

## Environment Variables

| Variable | Required | Description |
|----------|----------|-------------|
| `API_BASE_URL` | Yes | Base URL of the OpenAI-compatible LLM proxy |
| `HF_TOKEN` | Yes | Primary credential for the LLM proxy |
| `API_KEY` | Fallback | Used if `HF_TOKEN` is not set |
| `MODEL_NAME` | No | LLM model name (default: `Qwen/Qwen2.5-72B-Instruct`) |
| `PORT` | No | Server port (default: `7860`) |

---

## Reward Design Philosophy

The reward function is built around four real-world BPO principles:

**1. Dense feedback** β€” every step returns an interpretable score. Agents are never left guessing what went wrong across a long episode.

**2. Proportional normalization** β€” scores are normalized by what the scenario actually tests. An easy ticket that only needs `classify` can still achieve `0.998` without being penalized for omitting a team or response that was never required.

**3. Asymmetric penalties** β€” refund mistakes (`βˆ’0.50`) cost more than escalation mistakes (`βˆ’0.30`), which cost more than wrong team (`βˆ’0.15`). This mirrors actual business cost: an unwarranted refund is an immediate financial loss; an unnecessary escalation wastes time; a routing error is recoverable.

**4. Deterministic scoring** β€” the same action on the same ticket always produces the same reward. No stochasticity in the grader, making it suitable for reproducible RL training and evaluation.

---

## Roadmap

- **RL fine-tuning**: Collect episode trajectories and use PPO/GRPO to fine-tune Qwen2.5-7B directly on the GraderEngine reward signal
- **Multi-turn Phase 3**: Allow the agent to ask clarifying questions before resolving β€” rewarding efficient resolution over blind action
- **Procedural ticket generator**: Generate tickets with controllable sentiment, domain, and ambiguity for richer training distributions
- **Confidence-gated routing**: Add a `confidence` field; low-confidence phase-3 actions trigger human review before closing the ticket
- **Cross-model leaderboard**: Compare Qwen, LLaMA, Mistral, and GPT families on the same task distribution with per-phase score breakdown