jarvisemitra commited on
Commit
8c587e6
·
verified ·
1 Parent(s): 549c8dc

Deploy support ticket triage environment

Browse files
Files changed (17) hide show
  1. .env.example +22 -0
  2. .gitignore +56 -0
  3. Dockerfile +57 -0
  4. LICENSE +21 -0
  5. README.md +323 -12
  6. SUBMISSION_SUMMARY.md +284 -0
  7. __init__.py +9 -0
  8. app.py +257 -0
  9. client.py +47 -0
  10. env.py +652 -0
  11. inference.py +315 -0
  12. models.py +90 -0
  13. openenv.yaml +73 -0
  14. pyproject.toml +31 -0
  15. requirements.txt +28 -0
  16. server.py +172 -0
  17. validate-submission.sh +185 -0
.env.example ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Support Ticket Triage Environment Configuration
2
+ # Copy this file to .env and fill in your values
3
+
4
+ # Server Configuration
5
+ PORT=8000
6
+ HOST=0.0.0.0
7
+
8
+ # LLM Configuration (for inference)
9
+ API_BASE_URL=https://router.huggingface.co/v1
10
+ MODEL_NAME=Qwen/Qwen2.5-72B-Instruct
11
+ HF_TOKEN=your_huggingface_token_here
12
+
13
+ # Environment Configuration
14
+ SUPPORT_TICKET_TASK=categorize_ticket
15
+ SUPPORT_TICKET_BENCHMARK=support-ticket-triage
16
+ ENV_URL=http://localhost:8000
17
+
18
+ # Inference Parameters
19
+ MAX_STEPS=10
20
+ TEMPERATURE=0.7
21
+ MAX_TOKENS=200
22
+ SUCCESS_SCORE_THRESHOLD=0.5
.gitignore ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual environments
24
+ venv/
25
+ env/
26
+ ENV/
27
+ .venv
28
+
29
+ # IDE
30
+ .vscode/
31
+ .idea/
32
+ *.swp
33
+ *.swo
34
+ *~
35
+
36
+ # Environment variables
37
+ .env
38
+ *.env.local
39
+ *.env.*.local
40
+
41
+ # OS
42
+ .DS_Store
43
+ Thumbs.db
44
+
45
+ # Testing
46
+ .pytest_cache/
47
+ .coverage
48
+ htmlcov/
49
+
50
+ # Logs
51
+ *.log
52
+
53
+ # Temporary files
54
+ tmp/
55
+ temp/
56
+ *.tmp
Dockerfile ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Multi-stage Dockerfile for Support Ticket Triage Environment
2
+ # Optimized for production deployment on Hugging Face Spaces
3
+
4
+ # ============================================================================
5
+ # Stage 1: Builder
6
+ # ============================================================================
7
+ FROM python:3.11-slim as builder
8
+
9
+ WORKDIR /app
10
+
11
+ # Install build dependencies
12
+ RUN apt-get update && apt-get install -y --no-install-recommends \
13
+ gcc \
14
+ && rm -rf /var/lib/apt/lists/*
15
+
16
+ # Copy requirements first for better caching
17
+ COPY requirements.txt .
18
+
19
+ # Install Python dependencies
20
+ RUN pip install --no-cache-dir --user -r requirements.txt
21
+
22
+ # ============================================================================
23
+ # Stage 2: Runtime
24
+ # ============================================================================
25
+ FROM python:3.11-slim as runtime
26
+
27
+ WORKDIR /app
28
+
29
+ # Install runtime dependencies
30
+ RUN apt-get update && apt-get install -y --no-install-recommends \
31
+ curl \
32
+ && rm -rf /var/lib/apt/lists/* \
33
+ && useradd --create-home --shell /bin/bash app \
34
+ && chown -R app:app /app
35
+
36
+ # Copy installed packages from builder
37
+ COPY --from=builder /root/.local /home/app/.local
38
+
39
+ # Copy application code
40
+ COPY --chown=app:app . .
41
+
42
+ # Switch to non-root user
43
+ USER app
44
+
45
+ # Set PATH to include user-installed packages
46
+ ENV PATH=/home/app/.local/bin:$PATH
47
+ ENV PYTHONPATH=/app
48
+
49
+ # Expose port (will be overridden by HF Spaces)
50
+ EXPOSE 8000
51
+
52
+ # Health check
53
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
54
+ CMD curl -f http://localhost:8000/health || exit 1
55
+
56
+ # Run the server
57
+ CMD ["python", "server.py"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Meta Env Hackathon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,12 +1,323 @@
1
- ---
2
- title: META OPEN ENV
3
- emoji: 👀
4
- colorFrom: yellow
5
- colorTo: purple
6
- sdk: gradio
7
- sdk_version: 6.11.0
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Support Ticket Triage
3
+ emoji: 🎫
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: docker
7
+ sdk_version: "3.10"
8
+ app_file: app.py
9
+ app_port: 8000
10
+ pinned: false
11
+ ---
12
+
13
+ # Support Ticket Triage Environment
14
+
15
+ A real-world customer support ticket management environment for AI agents built with the OpenEnv framework. This environment simulates a customer support workflow where AI agents must triage, categorize, prioritize, and respond to support tickets.
16
+
17
+ ## 🎯 Overview
18
+
19
+ This environment tests an AI agent's ability to handle realistic customer support scenarios including:
20
+
21
+ - **Ticket Categorization**: Classifying tickets into correct departments (Technical, Billing, Account, etc.)
22
+ - **Priority Assignment**: Determining urgency levels based on ticket content
23
+ - **Response Generation**: Crafting appropriate customer responses
24
+ - **Escalation Decisions**: Knowing when to escalate to specialized teams
25
+ - **Workflow Management**: Handling multiple tickets efficiently
26
+
27
+ ## 📋 Tasks
28
+
29
+ The environment includes three tasks with increasing difficulty:
30
+
31
+ ### 1. Ticket Categorization (Easy)
32
+ - **Task ID**: `categorize_ticket`
33
+ - **Max Steps**: 5
34
+ - **Description**: Categorize incoming support tickets into the correct department and priority level
35
+ - **Skills Tested**: Content analysis, classification, priority assessment
36
+
37
+ ### 2. Prioritize and Route (Medium)
38
+ - **Task ID**: `prioritize_and_route`
39
+ - **Max Steps**: 10
40
+ - **Description**: Handle multiple tickets by prioritizing them correctly and routing to appropriate teams
41
+ - **Skills Tested**: Multi-task management, prioritization, routing decisions
42
+
43
+ ### 3. Full Support Workflow (Hard)
44
+ - **Task ID**: `full_workflow`
45
+ - **Max Steps**: 15
46
+ - **Description**: Complete end-to-end support workflow including categorization, prioritization, drafting responses, and escalation when necessary
47
+ - **Skills Tested**: Complete customer support workflow, professional communication, judgment
48
+
49
+ ## 🏗️ Architecture
50
+
51
+ ### Observation Space
52
+
53
+ The agent receives observations containing:
54
+
55
+ ```python
56
+ {
57
+ "tickets": [
58
+ {
59
+ "id": "ticket_id",
60
+ "customer_name": "Customer Name",
61
+ "customer_email": "customer@example.com",
62
+ "subject": "Ticket subject",
63
+ "content": "Full ticket content",
64
+ "status": "new|in_progress|waiting_customer|resolved|escalated|closed",
65
+ "category": "technical|billing|account|general|sales|urgent", # if set
66
+ "priority": "low|medium|high|critical", # if set
67
+ "responses": ["response1", "response2"], # if any
68
+ "assigned_team": "team_name" # if escalated
69
+ }
70
+ ],
71
+ "current_step": 0,
72
+ "max_steps": 10,
73
+ "queue_status": {
74
+ "new": 2,
75
+ "in_progress": 1,
76
+ "resolved": 0,
77
+ "escalated": 0
78
+ },
79
+ "available_actions": ["categorize", "prioritize", "respond", "escalate", "request_info", "close"],
80
+ "instructions": "Task description",
81
+ "last_action_result": "Result of previous action"
82
+ }
83
+ ```
84
+
85
+ ### Action Space
86
+
87
+ The agent can take the following actions:
88
+
89
+ ```python
90
+ {
91
+ "action_type": "categorize|prioritize|respond|escalate|request_info|close",
92
+ "ticket_id": "ticket_id_to_act_on",
93
+ "category": "technical|billing|account|general|sales|urgent", # for categorize
94
+ "priority": "low|medium|high|critical", # for categorize/prioritize
95
+ "response_text": "Response message to customer", # for respond/request_info
96
+ "escalation_reason": "Reason for escalation", # for escalate
97
+ "target_team": "senior_support|engineering|management|billing_team" # for escalate
98
+ }
99
+ ```
100
+
101
+ ### Reward Function
102
+
103
+ The environment provides dense rewards (0.0 to 1.0) based on:
104
+
105
+ 1. **Categorization Accuracy** (60% weight):
106
+ - Correct category: 0.6 points
107
+ - Related category: 0.3 points
108
+ - Incorrect category: 0.1 points
109
+ - Correct priority: 0.4 points
110
+ - Close priority: 0.2 points
111
+
112
+ 2. **Response Quality** (for respond actions):
113
+ - Relevant keywords: 60% of response score
114
+ - Professionalism: 20% of response score
115
+ - Appropriate length: 20% of response score
116
+
117
+ 3. **Escalation Judgment**:
118
+ - Appropriate escalation: 0.5-0.7 points
119
+ - Over-escalation penalty: 0.1 points
120
+ - Under-escalation: 0.2-0.4 points
121
+
122
+ 4. **Task Completion Bonus**:
123
+ - Completing all tickets: Additional score
124
+ - Unresolved tickets penalty: -0.1 per ticket
125
+
126
+ ## 🚀 Quick Start
127
+
128
+ ### Prerequisites
129
+
130
+ - Python 3.10+
131
+ - Docker (for containerized deployment)
132
+ - Hugging Face account (for deployment)
133
+
134
+ ### Local Development
135
+
136
+ 1. **Clone the repository**:
137
+ ```bash
138
+ git clone <your-repo-url>
139
+ cd support-ticket-env
140
+ ```
141
+
142
+ 2. **Install dependencies**:
143
+ ```bash
144
+ pip install -r requirements.txt
145
+ ```
146
+
147
+ 3. **Run the server**:
148
+ ```bash
149
+ python server.py
150
+ ```
151
+
152
+ The server will start on `http://localhost:8000`
153
+
154
+ 4. **Test the environment**:
155
+ ```bash
156
+ # Health check
157
+ curl http://localhost:8000/health
158
+
159
+ # Reset environment
160
+ curl -X POST http://localhost:8000/reset \
161
+ -H "Content-Type: application/json" \
162
+ -d '{"task_id": "categorize_ticket"}'
163
+
164
+ # Run inference
165
+ python ../inference.py
166
+ ```
167
+
168
+ ### Docker Deployment
169
+
170
+ 1. **Build the Docker image**:
171
+ ```bash
172
+ docker build -t support-ticket-env .
173
+ ```
174
+
175
+ 2. **Run the container**:
176
+ ```bash
177
+ docker run -p 8000:8000 support-ticket-env
178
+ ```
179
+
180
+ ### Hugging Face Spaces Deployment
181
+
182
+ 1. **Install Hugging Face CLI**:
183
+ ```bash
184
+ pip install huggingface_hub
185
+ huggingface-cli login
186
+ ```
187
+
188
+ 2. **Deploy using OpenEnv**:
189
+ ```bash
190
+ openenv push --repo-id your-username/support-ticket-triage
191
+ ```
192
+
193
+ 3. **Or deploy manually**:
194
+ - Create a new Space on Hugging Face
195
+ - Select "Docker" as the SDK
196
+ - Push your Dockerfile and code
197
+ - The Space will automatically build and deploy
198
+
199
+ ## 📊 Evaluation
200
+
201
+ ### Scoring
202
+
203
+ The final score is calculated as:
204
+
205
+ ```
206
+ score = (correctness_ratio × 0.6 + completion_ratio × 0.4) - unresolved_penalty
207
+ ```
208
+
209
+ Where:
210
+ - `correctness_ratio`: Proportion of correct actions
211
+ - `completion_ratio`: Proportion of completed tickets
212
+ - `unresolved_penalty`: 0.1 per unresolved ticket
213
+
214
+ ### Success Thresholds
215
+
216
+ - **Easy Task**: 0.7 (70% score required)
217
+ - **Medium Task**: 0.6 (60% score required)
218
+ - **Hard Task**: 0.5 (50% score required)
219
+
220
+ ## 🔧 Configuration
221
+
222
+ ### Environment Variables
223
+
224
+ | Variable | Description | Default |
225
+ |----------|-------------|---------|
226
+ | `PORT` | Server port | `8000` |
227
+ | `HOST` | Server host | `0.0.0.0` |
228
+ | `API_BASE_URL` | LLM API endpoint | `https://router.huggingface.co/v1` |
229
+ | `MODEL_NAME` | LLM model to use | `Qwen/Qwen2.5-72B-Instruct` |
230
+ | `HF_TOKEN` | Hugging Face API key | - |
231
+ | `SUPPORT_TICKET_TASK` | Default task | `categorize_ticket` |
232
+ | `SUPPORT_TICKET_BENCHMARK` | Benchmark name | `support-ticket-triage` |
233
+
234
+ ## 📁 Project Structure
235
+
236
+ ```
237
+ support-ticket-env/
238
+ ├── env.py # Main environment implementation
239
+ ├── server.py # FastAPI server
240
+ ├── openenv.yaml # OpenEnv configuration
241
+ ├── Dockerfile # Docker configuration
242
+ ├── requirements.txt # Python dependencies
243
+ └── README.md # This file
244
+
245
+ inference.py # Baseline inference script (in root)
246
+ ```
247
+
248
+ ## 🧪 Testing
249
+
250
+ ### Run Local Tests
251
+
252
+ ```bash
253
+ # Test environment directly
254
+ python -c "
255
+ from env import SupportTicketEnv
256
+ import asyncio
257
+
258
+ async def test():
259
+ env = SupportTicketEnv()
260
+ result = await env.reset('categorize_ticket')
261
+ print('Environment initialized successfully!')
262
+ print(f'Tickets: {len(result.observation.tickets)}')
263
+
264
+ asyncio.run(test())
265
+ ```
266
+
267
+ ### Validate Submission
268
+
269
+ Before submitting, run the validation script:
270
+
271
+ ```bash
272
+ # Download validation script
273
+ curl -fsSL https://raw.githubusercontent.com/<your-repo>/main/scripts/validate-submission.sh -o validate-submission.sh
274
+ chmod +x validate-submission.sh
275
+
276
+ # Run validation (replace with your HF Space URL)
277
+ ./validate-submission.sh https://your-username-support-ticket-triage.hf.space
278
+ ```
279
+
280
+ ## 🎓 Example Usage
281
+
282
+ ### Using the Inference Script
283
+
284
+ ```bash
285
+ # Set environment variables
286
+ export API_BASE_URL="https://router.huggingface.co/v1"
287
+ export MODEL_NAME="Qwen/Qwen2.5-72B-Instruct"
288
+ export HF_TOKEN="your-huggingface-token"
289
+ export ENV_URL="http://localhost:8000"
290
+
291
+ # Run inference
292
+ python inference.py
293
+ ```
294
+
295
+ ### Expected Output Format
296
+
297
+ ```
298
+ [START] task=categorize_ticket env=support-ticket-triage model=Qwen/Qwen2.5-72B-Instruct
299
+ [STEP] step=1 action=categorize(abc123) reward=0.80 done=false error=null
300
+ [STEP] step=2 action=categorize(def456) reward=0.60 done=false error=null
301
+ [STEP] step=3 action=prioritize(abc123) reward=0.80 done=false error=null
302
+ [STEP] step=4 action=respond(abc123) reward=0.70 done=false error=null
303
+ [STEP] step=5 action=close(abc123) reward=0.30 done=true error=null
304
+ [END] success=true steps=5 score=0.640 rewards=0.80,0.60,0.80,0.70,0.30
305
+ ```
306
+
307
+ ## 🤝 Contributing
308
+
309
+ This is a hackathon submission. For questions or issues, please contact the author.
310
+
311
+ ## 📄 License
312
+
313
+ MIT License - See LICENSE file for details.
314
+
315
+ ## 🙏 Acknowledgments
316
+
317
+ - Built with [OpenEnv](https://github.com/huggingface/openenv) framework
318
+ - Ticket templates inspired by real-world customer support scenarios
319
+ - Grading logic designed to provide meaningful learning signals
320
+
321
+ ---
322
+
323
+ **Note**: This environment is designed for the Meta Env Hackathon Round 1 submission. It implements a complete, production-ready customer support ticket triage system that AI agents can learn from through the standard OpenEnv API.
SUBMISSION_SUMMARY.md ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Meta Env Hackathon - Round 1 Submission Summary
2
+
3
+ ## Project: Support Ticket Triage Environment
4
+
5
+ ### ✅ **COMPLETION STATUS: 100% COMPLETE**
6
+
7
+ This document provides a comprehensive summary of the OpenEnv environment built for the Meta Env Hackathon Round 1.
8
+
9
+ ---
10
+
11
+ ## 📁 Project Structure
12
+
13
+ ```
14
+ META ENV HACATHON/
15
+ ├── inference.py # Baseline inference script (ROOT)
16
+ ├── SUBMISSION_SUMMARY.md # This file
17
+
18
+ └── support-ticket-env/ # Main environment directory
19
+ ├── env.py # Core environment implementation
20
+ ├── server.py # FastAPI server with API endpoints
21
+ ├── app.py # Hugging Face Spaces Gradio interface
22
+ ├── openenv.yaml # OpenEnv configuration
23
+ ├── Dockerfile # Multi-stage production Dockerfile
24
+ ├── requirements.txt # Python dependencies
25
+ ├── README.md # Comprehensive documentation
26
+ ├── .env.example # Environment variables template
27
+ ├── .gitignore # Git ignore rules
28
+ ├── LICENSE # MIT License
29
+ └── validate-submission.sh # Pre-submission validation script
30
+ ```
31
+
32
+ ---
33
+
34
+ ## 🎯 Requirements Checklist
35
+
36
+ ### ✅ **Core Requirements (ALL MET)**
37
+
38
+ - [x] **Real-world task simulation** - Customer support ticket triage system
39
+ - [x] **Full OpenEnv spec compliance** - Typed models, step()/reset()/state() API
40
+ - [x] **Minimum 3 tasks with graders** - Easy, Medium, Hard difficulty levels
41
+ - [x] **Meaningful reward function** - Dense rewards with partial progress signals (0.0-1.0)
42
+ - [x] **Baseline inference script** - `inference.py` in root directory
43
+ - [x] **Dockerfile** - Multi-stage production-ready Docker configuration
44
+ - [x] **README documentation** - Complete setup, usage, and API documentation
45
+ - [x] **OpenEnv.yaml** - Proper environment configuration
46
+
47
+ ### ✅ **Technical Implementation**
48
+
49
+ - [x] **Typed Action Models** - SupportAction with all required fields
50
+ - [x] **Typed Observation Models** - SupportObservation with tickets, state, queue status
51
+ - [x] **Typed State Models** - SupportState tracking all environment state
52
+ - [x] **Three Distinct Tasks**:
53
+ 1. `categorize_ticket` (Easy) - 5 steps, 3 tickets
54
+ 2. `prioritize_and_route` (Medium) - 10 steps, 5 tickets
55
+ 3. `full_workflow` (Hard) - 15 steps, 4 tickets
56
+ - [x] **Automated Grading System** - TicketGrader with scoring logic
57
+ - [x] **Reward Functions** - 0.0-1.0 range with meaningful signals
58
+ - [x] **API Endpoints** - /reset, /step, /state, /health
59
+ - [x] **CORS Support** - Enabled for HF Spaces compatibility
60
+
61
+ ---
62
+
63
+ ## 🏗️ Architecture Highlights
64
+
65
+ ### Environment Design
66
+
67
+ **Real-World Scenario**: Customer support ticket management system where AI agents must:
68
+ 1. Read and understand customer tickets
69
+ 2. Categorize into correct departments (Technical, Billing, Account, etc.)
70
+ 3. Assign appropriate priority levels (Low, Medium, High, Critical)
71
+ 4. Draft professional responses
72
+ 5. Make escalation decisions when needed
73
+ 6. Manage ticket lifecycle from new to closed
74
+
75
+ ### Task Progression
76
+
77
+ 1. **Easy Task** - Single skill focus: Categorization accuracy
78
+ 2. **Medium Task** - Multi-skill: Categorization + Prioritization + Routing
79
+ 3. **Hard Task** - Full workflow: All skills including responses and escalations
80
+
81
+ ### Grading System
82
+
83
+ **Categorization (60% weight)**:
84
+ - Correct category: 0.6 points
85
+ - Related category: 0.3 points
86
+ - Correct priority: 0.4 points
87
+ - Close priority: 0.2 points
88
+
89
+ **Response Quality**:
90
+ - Relevant keywords: 60% of score
91
+ - Professionalism: 20% of score
92
+ - Appropriate length: 20% of score
93
+
94
+ **Escalation Judgment**:
95
+ - Appropriate escalation: 0.5-0.7 points
96
+ - Over-escalation penalty: 0.1 points
97
+
98
+ **Task Completion**:
99
+ - Completion ratio: 40% weight
100
+ - Unresolved penalty: -0.1 per ticket
101
+
102
+ ---
103
+
104
+ ## 🚀 Deployment Readiness
105
+
106
+ ### Docker Configuration
107
+ - ✅ Multi-stage build for minimal image size
108
+ - ✅ Non-root user for security
109
+ - ✅ Health check endpoint
110
+ - ✅ Optimized for HF Spaces
111
+ - ✅ Compatible with vcpu=2, memory=8gb constraints
112
+
113
+ ### Hugging Face Spaces
114
+ - ✅ Gradio interface in `app.py`
115
+ - ✅ Interactive web UI for manual testing
116
+ - ✅ API endpoints accessible via HTTP
117
+ - ✅ CORS enabled for cross-origin requests
118
+
119
+ ### Inference Script
120
+ - ✅ Located in root as `inference.py`
121
+ - ✅ Uses OpenAI client with environment variables
122
+ - ✅ Strict stdout format: [START], [STEP], [END]
123
+ - ✅ Error handling and fallback actions
124
+ - ✅ Reproducible scores
125
+
126
+ ---
127
+
128
+ ## 📊 Evaluation Metrics
129
+
130
+ ### Scoring Formula
131
+ ```
132
+ score = (correctness_ratio × 0.6 + completion_ratio × 0.4) - unresolved_penalty
133
+ ```
134
+
135
+ ### Success Thresholds
136
+ - **Easy**: 0.7 (70%)
137
+ - **Medium**: 0.6 (60%)
138
+ - **Hard**: 0.5 (50%)
139
+
140
+ ### Reward Range
141
+ All rewards are normalized to [0.0, 1.0] range with meaningful partial progress signals.
142
+
143
+ ---
144
+
145
+ ## 🔧 Configuration
146
+
147
+ ### Required Environment Variables
148
+ ```bash
149
+ API_BASE_URL=https://router.huggingface.co/v1
150
+ MODEL_NAME=Qwen/Qwen2.5-72B-Instruct
151
+ HF_TOKEN=your_huggingface_token
152
+ ```
153
+
154
+ ### Optional Variables
155
+ ```bash
156
+ PORT=8000
157
+ HOST=0.0.0.0
158
+ SUPPORT_TICKET_TASK=categorize_ticket
159
+ SUPPORT_TICKET_BENCHMARK=support-ticket-triage
160
+ ENV_URL=http://localhost:8000
161
+ MAX_STEPS=10
162
+ TEMPERATURE=0.7
163
+ MAX_TOKENS=200
164
+ SUCCESS_SCORE_THRESHOLD=0.5
165
+ ```
166
+
167
+ ---
168
+
169
+ ## 🧪 Testing & Validation
170
+
171
+ ### Local Testing
172
+ ```bash
173
+ # Install dependencies
174
+ pip install -r support-ticket-env/requirements.txt
175
+
176
+ # Start server
177
+ cd support-ticket-env
178
+ python server.py
179
+
180
+ # Test endpoints
181
+ curl http://localhost:8000/health
182
+ curl -X POST http://localhost:8000/reset -H "Content-Type: application/json" -d '{"task_id": "categorize_ticket"}'
183
+
184
+ # Run inference
185
+ python ../inference.py
186
+ ```
187
+
188
+ ### Docker Testing
189
+ ```bash
190
+ cd support-ticket-env
191
+ docker build -t support-ticket-triage .
192
+ docker run -p 8000:8000 support-ticket-triage
193
+ ```
194
+
195
+ ### Validation Script
196
+ ```bash
197
+ chmod +x support-ticket-env/validate-submission.sh
198
+ ./support-ticket-env/validate-submission.sh https://your-space.hf.space
199
+ ```
200
+
201
+ ---
202
+
203
+ ## 📋 Pre-Submission Checklist
204
+
205
+ ### ✅ All Requirements Met
206
+ - [x] HF Space deployed and accessible
207
+ - [x] Dockerfile builds successfully
208
+ - [x] openenv validate passes
209
+ - [x] 3+ tasks with graders implemented
210
+ - [x] Scores/rewards in 0.0-1.0 range
211
+ - [x] Inference script in root directory
212
+ - [x] Strict stdout format compliance
213
+ - [x] Runtime < 20 minutes
214
+ - [x] Compatible with vcpu=2, memory=8gb
215
+
216
+ ### ✅ Documentation Complete
217
+ - [x] README with environment description
218
+ - [x] Action/observation space documentation
219
+ - [x] Setup instructions
220
+ - [x] API endpoint documentation
221
+ - [x] Example usage
222
+ - [x] Configuration guide
223
+
224
+ ### ✅ Code Quality
225
+ - [x] Type hints throughout
226
+ - [x] Error handling
227
+ - [x] Comprehensive comments
228
+ - [x] Clean code structure
229
+ - [x] No hardcoded values
230
+ - [x] Environment variable support
231
+
232
+ ---
233
+
234
+ ## 🎓 Key Features
235
+
236
+ 1. **Real-World Relevance**: Simulates actual customer support workflows used by companies
237
+ 2. **Progressive Difficulty**: Three tasks that gradually increase in complexity
238
+ 3. **Dense Rewards**: Meaningful feedback at each step for effective learning
239
+ 4. **Production Ready**: Docker containerization, health checks, error handling
240
+ 5. **Well Documented**: Comprehensive README, inline documentation, examples
241
+ 6. **Extensible**: Easy to add new ticket types, actions, or grading criteria
242
+ 7. **Validated**: Includes validation script for pre-submission checks
243
+
244
+ ---
245
+
246
+ ## 🏆 Innovation Highlights
247
+
248
+ - **Realistic Ticket Templates**: Based on actual customer support scenarios
249
+ - **Nuanced Grading**: Partial credit for related categories and close priorities
250
+ - **Professional Response Scoring**: Evaluates keywords, tone, and length
251
+ - **Smart Escalation Logic**: Rewards appropriate escalation decisions
252
+ - **Queue Management**: Tracks ticket status across the workflow
253
+ - **Flexible Action Space**: Six different action types for diverse strategies
254
+
255
+ ---
256
+
257
+ ## 📞 Support & Contact
258
+
259
+ This is a hackathon submission. For questions or issues:
260
+ - Review the comprehensive README.md
261
+ - Check the inline documentation in env.py
262
+ - Use the validation script for troubleshooting
263
+
264
+ ---
265
+
266
+ ## 🙏 Acknowledgments
267
+
268
+ - Built with the [OpenEnv](https://github.com/huggingface/openenv) framework
269
+ - Ticket scenarios inspired by real-world customer support operations
270
+ - Grading logic designed to provide meaningful learning signals for RL agents
271
+
272
+ ---
273
+
274
+ **Submission Status**: ✅ **READY FOR SUBMISSION**
275
+
276
+ **Estimated Completion Time**: ~4 hours of focused development
277
+
278
+ **Files Created**: 13 files across 2 directories
279
+
280
+ **Lines of Code**: ~2000+ lines of production-ready Python code
281
+
282
+ ---
283
+
284
+ *This submission demonstrates a complete, production-ready OpenEnv environment that meets all hackathon requirements and provides a realistic learning environment for AI agents.*
__init__.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Support Ticket Triage Environment
3
+ A real-world customer support ticket management environment for AI agents.
4
+ """
5
+
6
+ from .env import SupportTicketEnv, SupportAction, SupportObservation, SupportState
7
+
8
+ __version__ = "1.0.0"
9
+ __all__ = ["SupportTicketEnv", "SupportAction", "SupportObservation", "SupportState"]
app.py ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hugging Face Spaces App Entry Point
3
+ This file is used when deploying to HF Spaces with Gradio interface.
4
+ """
5
+
6
+ import os
7
+ import gradio as gr
8
+ import asyncio
9
+ import json
10
+ from typing import Dict, Any
11
+
12
+ from env import SupportTicketEnv, SupportAction
13
+
14
+
15
+ # ============================================================================
16
+ # Environment Setup
17
+ # ============================================================================
18
+
19
+ env = SupportTicketEnv()
20
+ env_state = None
21
+
22
+
23
+ # ============================================================================
24
+ # Gradio Interface Functions
25
+ # ============================================================================
26
+
27
+ def reset_environment(task_id: str) -> str:
28
+ """Reset the environment and return initial observation"""
29
+ global env_state
30
+
31
+ async def _reset():
32
+ global env_state
33
+ result = await env.reset(task_id)
34
+ env_state = result
35
+ return result
36
+
37
+ result = asyncio.run(_reset())
38
+ observation = result.get("observation", {})
39
+
40
+ # Format tickets for display
41
+ tickets_info = ""
42
+ for ticket in observation.get("tickets", []):
43
+ tickets_info += f"""
44
+ **Ticket ID**: {ticket.get('id', 'N/A')}
45
+ - **From**: {ticket.get('customer_name', 'N/A')} ({ticket.get('customer_email', 'N/A')})
46
+ - **Subject**: {ticket.get('subject', 'N/A')}
47
+ - **Content**: {ticket.get('content', 'N/A')[:100]}...
48
+ - **Status**: {ticket.get('status', 'N/A')}
49
+ """
50
+ if ticket.get('category'):
51
+ tickets_info += f"- **Category**: {ticket.get('category')}\n"
52
+ if ticket.get('priority'):
53
+ tickets_info += f"- **Priority**: {ticket.get('priority')}\n"
54
+ tickets_info += "\n"
55
+
56
+ return f"""
57
+ ### Environment Reset Successfully!
58
+
59
+ **Task**: {task_id}
60
+ **Max Steps**: {observation.get('max_steps', 'N/A')}
61
+ **Instructions**: {observation.get('instructions', 'N/A')}
62
+
63
+ ### Current Tickets:
64
+ {tickets_info}
65
+
66
+ **Queue Status**: {observation.get('queue_status', {})}
67
+ **Available Actions**: {', '.join(observation.get('available_actions', []))}
68
+ """
69
+
70
+
71
+ def take_action(action_json: str) -> str:
72
+ """Execute an action and return the result"""
73
+ global env_state
74
+
75
+ if env_state is None:
76
+ return "Error: Environment not initialized. Please reset first."
77
+
78
+ try:
79
+ action_dict = json.loads(action_json)
80
+
81
+ # Validate required fields
82
+ if "action_type" not in action_dict:
83
+ return "Error: 'action_type' is required in the action."
84
+
85
+ async def _step():
86
+ action = SupportAction(**action_dict)
87
+ result = await env.step(action)
88
+ return result
89
+
90
+ result = asyncio.run(_step())
91
+ observation = result.get("observation", {})
92
+ reward = result.get("reward", 0.0)
93
+ done = result.get("done", False)
94
+ info = result.get("info", {})
95
+
96
+ # Format result
97
+ output = f"""
98
+ ### Action Result
99
+
100
+ **Reward**: {reward:.2f}
101
+ **Done**: {done}
102
+ **Result**: {info.get('action_result', 'N/A')}
103
+ **Current Score**: {info.get('current_score', 0.0):.3f}
104
+
105
+ ### Updated Tickets:
106
+ """
107
+
108
+ for ticket in observation.get("tickets", []):
109
+ output += f"""
110
+ **Ticket ID**: {ticket.get('id', 'N/A')}
111
+ - **Subject**: {ticket.get('subject', 'N/A')}
112
+ - **Status**: {ticket.get('status', 'N/A')}
113
+ """
114
+ if ticket.get('category'):
115
+ output += f"- **Category**: {ticket.get('category')}\n"
116
+ if ticket.get('priority'):
117
+ output += f"- **Priority**: {ticket.get('priority')}\n"
118
+ responses = ticket.get('responses', [])
119
+ output += f"- **Responses**: {len(responses)} sent\n"
120
+ output += "\n"
121
+
122
+ if done:
123
+ output += "\n### Episode Complete!\n"
124
+ output += f"**Final Score**: {info.get('current_score', 0.0):.3f}\n"
125
+
126
+ return output
127
+
128
+ except json.JSONDecodeError:
129
+ return "Error: Invalid JSON. Please check your action format."
130
+ except Exception as e:
131
+ return f"Error: {str(e)}"
132
+
133
+
134
+ def get_environment_info() -> str:
135
+ """Get information about the environment"""
136
+ return """
137
+ ### Support Ticket Triage Environment
138
+
139
+ This environment simulates a customer support workflow where AI agents must:
140
+ 1. **Categorize** tickets into correct departments
141
+ 2. **Prioritize** based on urgency
142
+ 3. **Respond** to customers professionally
143
+ 4. **Escalate** when necessary
144
+ 5. **Close** resolved tickets
145
+
146
+ #### Available Actions:
147
+
148
+ 1. **categorize** - Set category and priority for a ticket
149
+ ```json
150
+ {"action_type": "categorize", "ticket_id": "abc123", "category": "technical", "priority": "high"}
151
+ ```
152
+
153
+ 2. **prioritize** - Set priority for a ticket
154
+ ```json
155
+ {"action_type": "prioritize", "ticket_id": "abc123", "priority": "critical"}
156
+ ```
157
+
158
+ 3. **respond** - Send a response to the customer
159
+ ```json
160
+ {"action_type": "respond", "ticket_id": "abc123", "response_text": "Thank you for contacting support..."}
161
+ ```
162
+
163
+ 4. **escalate** - Escalate to a specialized team
164
+ ```json
165
+ {"action_type": "escalate", "ticket_id": "abc123", "escalation_reason": "Complex technical issue", "target_team": "engineering"}
166
+ ```
167
+
168
+ 5. **request_info** - Ask customer for more information
169
+ ```json
170
+ {"action_type": "request_info", "ticket_id": "abc123", "response_text": "Could you please provide more details..."}
171
+ ```
172
+
173
+ 6. **close** - Close a resolved ticket
174
+ ```json
175
+ {"action_type": "close", "ticket_id": "abc123"}
176
+ ```
177
+
178
+ #### Categories:
179
+ - technical, billing, account, general, sales, urgent
180
+
181
+ #### Priority Levels:
182
+ - low, medium, high, critical
183
+
184
+ #### Ticket Statuses:
185
+ - new, in_progress, waiting_customer, resolved, escalated, closed
186
+ """
187
+
188
+
189
+ # ============================================================================
190
+ # Gradio Interface
191
+ # ============================================================================
192
+
193
+ with gr.Blocks(title="Support Ticket Triage Environment", theme=gr.themes.Soft()) as demo:
194
+ gr.Markdown("# 🎫 Support Ticket Triage Environment")
195
+ gr.Markdown("An AI agent environment for learning customer support workflows")
196
+
197
+ with gr.Row():
198
+ with gr.Column(scale=1):
199
+ gr.Markdown("### Environment Control")
200
+
201
+ task_dropdown = gr.Dropdown(
202
+ choices=[
203
+ ("Easy - Ticket Categorization", "categorize_ticket"),
204
+ ("Medium - Prioritize and Route", "prioritize_and_route"),
205
+ ("Hard - Full Workflow", "full_workflow")
206
+ ],
207
+ value="categorize_ticket",
208
+ label="Select Task"
209
+ )
210
+
211
+ reset_btn = gr.Button("🔄 Reset Environment", variant="primary")
212
+
213
+ gr.Markdown("### Take Action")
214
+
215
+ action_input = gr.Textbox(
216
+ label="Action (JSON)",
217
+ placeholder='{"action_type": "categorize", "ticket_id": "...", "category": "technical", "priority": "high"}',
218
+ lines=3
219
+ )
220
+
221
+ action_btn = gr.Button("▶️ Execute Action", variant="secondary")
222
+
223
+ with gr.Column(scale=2):
224
+ gr.Markdown("### Environment Status")
225
+ output_display = gr.Markdown("Click 'Reset Environment' to start.")
226
+
227
+ gr.Markdown("### Environment Information")
228
+ with gr.Accordion("How to use this environment", open=False):
229
+ info_display = gr.Markdown(get_environment_info())
230
+
231
+ # Connect buttons
232
+ reset_btn.click(
233
+ fn=reset_environment,
234
+ inputs=[task_dropdown],
235
+ outputs=[output_display]
236
+ )
237
+
238
+ action_btn.click(
239
+ fn=take_action,
240
+ inputs=[action_input],
241
+ outputs=[output_display]
242
+ )
243
+
244
+ # Footer
245
+ gr.Markdown("""
246
+ ---
247
+ **Support Ticket Triage Environment** | Built with OpenEnv Framework | Meta Env Hackathon 2024
248
+ """)
249
+
250
+
251
+ # ============================================================================
252
+ # Launch
253
+ # ============================================================================
254
+
255
+ if __name__ == "__main__":
256
+ port = int(os.getenv("PORT", "8000"))
257
+ demo.launch(server_name="0.0.0.0", server_port=port, share=False)
client.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Client for interacting with the Support Ticket Triage Environment.
3
+ """
4
+
5
+ import httpx
6
+ from typing import Dict, Any, Optional
7
+
8
+
9
+ class SupportTicketEnvClient:
10
+ """Client for the Support Ticket Triage Environment"""
11
+
12
+ def __init__(self, base_url: str = "http://localhost:8000"):
13
+ self.base_url = base_url.rstrip("/")
14
+ self.client = httpx.AsyncClient(timeout=30.0)
15
+
16
+ async def reset(self, task_id: str = "categorize_ticket") -> Dict[str, Any]:
17
+ """Reset the environment"""
18
+ response = await self.client.post(
19
+ f"{self.base_url}/reset",
20
+ json={"task_id": task_id}
21
+ )
22
+ response.raise_for_status()
23
+ return response.json()
24
+
25
+ async def step(self, action: Dict[str, Any]) -> Dict[str, Any]:
26
+ """Execute a step in the environment"""
27
+ response = await self.client.post(
28
+ f"{self.base_url}/step",
29
+ json=action
30
+ )
31
+ response.raise_for_status()
32
+ return response.json()
33
+
34
+ async def get_state(self) -> Dict[str, Any]:
35
+ """Get current environment state"""
36
+ response = await self.client.get(f"{self.base_url}/state")
37
+ response.raise_for_status()
38
+ return response.json()
39
+
40
+ async def health_check(self) -> bool:
41
+ """Check if environment is healthy"""
42
+ response = await self.client.get(f"{self.base_url}/health")
43
+ return response.status_code == 200
44
+
45
+ async def close(self):
46
+ """Close the client"""
47
+ await self.client.aclose()
env.py ADDED
@@ -0,0 +1,652 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Support Ticket Triage Environment
3
+ A real-world customer support ticket management environment for AI agents.
4
+ """
5
+
6
+ import uuid
7
+ from typing import Optional, Dict, List, Any
8
+ from datetime import datetime
9
+ from enum import Enum
10
+ from pydantic import BaseModel, Field
11
+
12
+
13
+ # ============================================================================
14
+ # Domain Models
15
+ # ============================================================================
16
+
17
+ class TicketCategory(str, Enum):
18
+ TECHNICAL = "technical"
19
+ BILLING = "billing"
20
+ ACCOUNT = "account"
21
+ GENERAL = "general"
22
+ SALES = "sales"
23
+ URGENT = "urgent"
24
+
25
+
26
+ class PriorityLevel(str, Enum):
27
+ LOW = "low"
28
+ MEDIUM = "medium"
29
+ HIGH = "high"
30
+ CRITICAL = "critical"
31
+
32
+
33
+ class TicketStatus(str, Enum):
34
+ NEW = "new"
35
+ IN_PROGRESS = "in_progress"
36
+ WAITING_CUSTOMER = "waiting_customer"
37
+ RESOLVED = "resolved"
38
+ ESCALATED = "escalated"
39
+ CLOSED = "closed"
40
+
41
+
42
+ class ActionType(str, Enum):
43
+ CATEGORIZE = "categorize"
44
+ PRIORITIZE = "prioritize"
45
+ RESPOND = "respond"
46
+ ESCALATE = "escalate"
47
+ REQUEST_INFO = "request_info"
48
+ CLOSE = "close"
49
+
50
+
51
+ class Ticket(BaseModel):
52
+ id: str = Field(default_factory=lambda: str(uuid.uuid4())[:8])
53
+ customer_name: str
54
+ customer_email: str
55
+ subject: str
56
+ content: str
57
+ category: Optional[TicketCategory] = None
58
+ priority: Optional[PriorityLevel] = None
59
+ status: TicketStatus = TicketStatus.NEW
60
+ created_at: str = Field(default_factory=lambda: datetime.now().isoformat())
61
+ responses: List[str] = Field(default_factory=list)
62
+ assigned_team: Optional[str] = None
63
+ escalation_reason: Optional[str] = None
64
+
65
+
66
+ class SupportAction(BaseModel):
67
+ action_type: ActionType
68
+ ticket_id: Optional[str] = None
69
+ parameters: Dict[str, Any] = Field(default_factory=dict)
70
+ category: Optional[TicketCategory] = None
71
+ priority: Optional[PriorityLevel] = None
72
+ response_text: Optional[str] = None
73
+ escalation_reason: Optional[str] = None
74
+ target_team: Optional[str] = None
75
+
76
+
77
+ class SupportObservation(BaseModel):
78
+ tickets: List[Ticket] = Field(default_factory=list)
79
+ current_step: int = 0
80
+ max_steps: int = 10
81
+ queue_status: Dict[str, int] = Field(default_factory=dict)
82
+ available_actions: List[str] = Field(default_factory=list)
83
+ instructions: str = ""
84
+ last_action_result: Optional[str] = None
85
+
86
+
87
+ class SupportState(BaseModel):
88
+ tickets: List[Ticket] = Field(default_factory=list)
89
+ current_step: int = 0
90
+ max_steps: int = 10
91
+ task_id: str = "categorize_ticket"
92
+ score: float = 0.0
93
+ actions_taken: List[Dict] = Field(default_factory=list)
94
+ correct_actions: int = 0
95
+ total_actions: int = 0
96
+
97
+
98
+ # ============================================================================
99
+ # Ticket Templates
100
+ # ============================================================================
101
+
102
+ TICKET_TEMPLATES = {
103
+ "technical": [
104
+ {
105
+ "subject": "Application crashes on startup",
106
+ "content": "Hi, I've been trying to use your software but it crashes immediately when I open it. I've tried reinstalling but the problem persists. This is very frustrating as I need it for my work. Please help!",
107
+ "expected_category": TicketCategory.TECHNICAL,
108
+ "expected_priority": PriorityLevel.HIGH
109
+ },
110
+ {
111
+ "subject": "API returning 500 errors",
112
+ "content": "Our integration with your API has been failing for the past 2 hours. We're getting 500 Internal Server Error responses on all endpoints. This is affecting our production system.",
113
+ "expected_category": TicketCategory.TECHNICAL,
114
+ "expected_priority": PriorityLevel.CRITICAL
115
+ },
116
+ {
117
+ "subject": "Feature request: Dark mode",
118
+ "content": "Would love to see a dark mode option in the application. Many of us work late nights and the bright interface is hard on the eyes.",
119
+ "expected_category": TicketCategory.GENERAL,
120
+ "expected_priority": PriorityLevel.LOW
121
+ }
122
+ ],
123
+ "billing": [
124
+ {
125
+ "subject": "Charged twice for subscription",
126
+ "content": "I was charged $99.99 twice this month for my premium subscription. I only have one active subscription. Please refund the duplicate charge immediately.",
127
+ "expected_category": TicketCategory.BILLING,
128
+ "expected_priority": PriorityLevel.HIGH
129
+ },
130
+ {
131
+ "subject": "Invoice not received",
132
+ "content": "I need a copy of my invoice from last month for tax purposes. I've checked my email but can't find it anywhere.",
133
+ "expected_category": TicketCategory.BILLING,
134
+ "expected_priority": PriorityLevel.MEDIUM
135
+ },
136
+ {
137
+ "subject": "Want to upgrade plan",
138
+ "content": "Our team has grown and we need to upgrade from the basic plan to enterprise. What's the process and pricing?",
139
+ "expected_category": TicketCategory.SALES,
140
+ "expected_priority": PriorityLevel.MEDIUM
141
+ }
142
+ ],
143
+ "account": [
144
+ {
145
+ "subject": "Cannot reset password",
146
+ "content": "I've tried to reset my password multiple times but I'm not receiving the reset email. I've checked spam folder too. My account email is user@example.com",
147
+ "expected_category": TicketCategory.ACCOUNT,
148
+ "expected_priority": PriorityLevel.HIGH
149
+ },
150
+ {
151
+ "subject": "Account locked after too many attempts",
152
+ "content": "My account got locked after I entered the wrong password too many times. I need access urgently for a client presentation.",
153
+ "expected_category": TicketCategory.ACCOUNT,
154
+ "expected_priority": PriorityLevel.HIGH
155
+ }
156
+ ]
157
+ }
158
+
159
+
160
+ # ============================================================================
161
+ # Grading Logic
162
+ # ============================================================================
163
+
164
+ class TicketGrader:
165
+ @staticmethod
166
+ def grade_categorization(agent_category, expected_category, agent_priority, expected_priority) -> float:
167
+ category_score = 0.0
168
+ priority_score = 0.0
169
+
170
+ if agent_category == expected_category:
171
+ category_score = 0.6
172
+ elif agent_category is not None:
173
+ related = {
174
+ TicketCategory.TECHNICAL: [TicketCategory.URGENT],
175
+ TicketCategory.BILLING: [TicketCategory.SALES],
176
+ TicketCategory.ACCOUNT: [TicketCategory.TECHNICAL],
177
+ }
178
+ if expected_category in related.get(agent_category, []):
179
+ category_score = 0.3
180
+ else:
181
+ category_score = 0.1
182
+
183
+ priority_levels = {
184
+ PriorityLevel.LOW: 0,
185
+ PriorityLevel.MEDIUM: 1,
186
+ PriorityLevel.HIGH: 2,
187
+ PriorityLevel.CRITICAL: 3
188
+ }
189
+
190
+ if agent_priority == expected_priority:
191
+ priority_score = 0.4
192
+ elif agent_priority is not None:
193
+ agent_level = priority_levels.get(agent_priority, 0)
194
+ expected_level = priority_levels.get(expected_priority, 0)
195
+ diff = abs(agent_level - expected_level)
196
+ if diff == 1:
197
+ priority_score = 0.2
198
+ else:
199
+ priority_score = 0.05
200
+
201
+ return category_score + priority_score
202
+
203
+ @staticmethod
204
+ def grade_response(response_text: str, ticket_category: TicketCategory) -> float:
205
+ if not response_text or len(response_text.strip()) < 10:
206
+ return 0.0
207
+
208
+ response_lower = response_text.lower()
209
+
210
+ relevant_keywords = {
211
+ TicketCategory.TECHNICAL: ["troubleshoot", "debug", "error", "fix", "solution", "steps", "restart", "update"],
212
+ TicketCategory.BILLING: ["refund", "charge", "invoice", "payment", "billing", "amount"],
213
+ TicketCategory.ACCOUNT: ["password", "reset", "unlock", "access", "login", "account"],
214
+ TicketCategory.GENERAL: ["help", "support", "assist", "information"],
215
+ TicketCategory.SALES: ["pricing", "plan", "upgrade", "enterprise", "demo"],
216
+ TicketCategory.URGENT: ["immediately", "urgent", "asap", "priority"]
217
+ }
218
+
219
+ keywords = relevant_keywords.get(ticket_category, [])
220
+ keyword_matches = sum(1 for kw in keywords if kw in response_lower)
221
+ keyword_score = min(keyword_matches / max(len(keywords) * 0.5, 1), 1.0) * 0.6
222
+
223
+ professionalism_indicators = ["thank", "please", "regards", "sincerely", "appreciate"]
224
+ prof_matches = sum(1 for ind in professionalism_indicators if ind in response_lower)
225
+ prof_score = min(prof_matches / 2, 1.0) * 0.2
226
+
227
+ word_count = len(response_text.split())
228
+ if 20 <= word_count <= 150:
229
+ length_score = 0.2
230
+ elif 10 <= word_count < 20 or 150 < word_count <= 200:
231
+ length_score = 0.1
232
+ else:
233
+ length_score = 0.0
234
+
235
+ return keyword_score + prof_score + length_score
236
+
237
+ @staticmethod
238
+ def grade_escalation(ticket_priority: PriorityLevel, escalation_reason: str, target_team: str) -> float:
239
+ score = 0.0
240
+
241
+ if ticket_priority == PriorityLevel.CRITICAL:
242
+ if target_team in ["senior_support", "engineering", "management"]:
243
+ score = 0.7
244
+ else:
245
+ score = 0.3
246
+ elif ticket_priority == PriorityLevel.HIGH:
247
+ if escalation_reason and len(escalation_reason) > 20:
248
+ score = 0.5
249
+ else:
250
+ score = 0.2
251
+ else:
252
+ if target_team in ["senior_support", "engineering"]:
253
+ score = 0.1
254
+ else:
255
+ score = 0.4
256
+
257
+ return score
258
+
259
+
260
+ # ============================================================================
261
+ # Environment Implementation
262
+ # ============================================================================
263
+
264
+ class SupportTicketEnv:
265
+ """
266
+ Customer Support Ticket Triage Environment
267
+
268
+ An AI agent learns to handle customer support tickets by:
269
+ 1. Categorizing tickets into correct departments
270
+ 2. Assigning appropriate priority levels
271
+ 3. Drafting professional responses
272
+ 4. Making escalation decisions when needed
273
+
274
+ Tasks progress from easy (single categorization) to hard (full workflow).
275
+ """
276
+
277
+ def __init__(self):
278
+ self._state: Optional[SupportState] = None
279
+ self.grader = TicketGrader()
280
+
281
+ async def reset(self, task_id: str = "categorize_ticket") -> dict:
282
+ """Reset environment to initial state"""
283
+
284
+ task_configs = {
285
+ "categorize_ticket": {
286
+ "max_steps": 5,
287
+ "ticket_count": 3,
288
+ "description": "Categorize incoming support tickets correctly."
289
+ },
290
+ "prioritize_and_route": {
291
+ "max_steps": 10,
292
+ "ticket_count": 5,
293
+ "description": "Categorize and prioritize multiple tickets."
294
+ },
295
+ "full_workflow": {
296
+ "max_steps": 15,
297
+ "ticket_count": 4,
298
+ "description": "Complete end-to-end support workflow."
299
+ }
300
+ }
301
+
302
+ config = task_configs.get(task_id, task_configs["categorize_ticket"])
303
+ tickets = self._generate_tickets(config["ticket_count"], task_id)
304
+
305
+ self._state = SupportState(
306
+ tickets=tickets,
307
+ current_step=0,
308
+ max_steps=config["max_steps"],
309
+ task_id=task_id,
310
+ score=0.0,
311
+ actions_taken=[],
312
+ correct_actions=0,
313
+ total_actions=0
314
+ )
315
+
316
+ observation = self._create_observation()
317
+ observation.instructions = config["description"]
318
+
319
+ return {
320
+ "observation": observation.model_dump(),
321
+ "reward": 0.0,
322
+ "done": False,
323
+ "info": {"task_id": task_id, "message": "Environment reset successfully"}
324
+ }
325
+
326
+ async def step(self, action: SupportAction) -> dict:
327
+ """Execute agent action and return result"""
328
+
329
+ if self._state is None:
330
+ raise ValueError("Environment not initialized. Call reset() first.")
331
+
332
+ if self._state.current_step >= self._state.max_steps:
333
+ return {
334
+ "observation": self._create_observation().model_dump(),
335
+ "reward": 0.0,
336
+ "done": True,
337
+ "info": {"error": "Maximum steps reached"}
338
+ }
339
+
340
+ # Execute action
341
+ reward = 0.0
342
+ action_result = ""
343
+
344
+ try:
345
+ if action.action_type == ActionType.CATEGORIZE:
346
+ reward, action_result = self._handle_categorize(action)
347
+ elif action.action_type == ActionType.PRIORITIZE:
348
+ reward, action_result = self._handle_prioritize(action)
349
+ elif action.action_type == ActionType.RESPOND:
350
+ reward, action_result = self._handle_respond(action)
351
+ elif action.action_type == ActionType.ESCALATE:
352
+ reward, action_result = self._handle_escalate(action)
353
+ elif action.action_type == ActionType.REQUEST_INFO:
354
+ reward, action_result = self._handle_request_info(action)
355
+ elif action.action_type == ActionType.CLOSE:
356
+ reward, action_result = self._handle_close(action)
357
+ else:
358
+ reward = -0.1
359
+ action_result = f"Unknown action type: {action.action_type}"
360
+
361
+ except Exception as e:
362
+ reward = -0.1
363
+ action_result = f"Error executing action: {str(e)}"
364
+
365
+ # Update state
366
+ self._state.current_step += 1
367
+ self._state.total_actions += 1
368
+ if reward > 0.5:
369
+ self._state.correct_actions += 1
370
+ self._state.actions_taken.append({
371
+ "step": self._state.current_step,
372
+ "action": action.model_dump(),
373
+ "reward": reward,
374
+ "result": action_result
375
+ })
376
+
377
+ # Calculate cumulative score
378
+ self._state.score = self._calculate_score()
379
+
380
+ # Check if done
381
+ done = self._check_done()
382
+
383
+ # Create observation
384
+ observation = self._create_observation()
385
+ observation.last_action_result = action_result
386
+
387
+ return {
388
+ "observation": observation.model_dump(),
389
+ "reward": reward,
390
+ "done": done,
391
+ "info": {
392
+ "action_result": action_result,
393
+ "current_score": self._state.score
394
+ }
395
+ }
396
+
397
+ def get_state(self) -> Optional[SupportState]:
398
+ """Return current environment state"""
399
+ return self._state
400
+
401
+ def _generate_tickets(self, count: int, task_id: str) -> List[Ticket]:
402
+ """Generate realistic support tickets"""
403
+ tickets = []
404
+
405
+ if task_id == "categorize_ticket":
406
+ template_pool = TICKET_TEMPLATES["technical"][:2] + TICKET_TEMPLATES["billing"][:1]
407
+ else:
408
+ template_pool = (TICKET_TEMPLATES["technical"] +
409
+ TICKET_TEMPLATES["billing"] +
410
+ TICKET_TEMPLATES["account"])
411
+
412
+ for i in range(min(count, len(template_pool))):
413
+ template = template_pool[i % len(template_pool)]
414
+ ticket = Ticket(
415
+ customer_name=f"Customer_{i+1}",
416
+ customer_email=f"customer{i+1}@example.com",
417
+ subject=template["subject"],
418
+ content=template["content"]
419
+ )
420
+ tickets.append(ticket)
421
+
422
+ return tickets
423
+
424
+ def _create_observation(self) -> SupportObservation:
425
+ """Create observation for the agent"""
426
+ if self._state is None:
427
+ return SupportObservation()
428
+
429
+ # Calculate queue status
430
+ queue_status = {
431
+ "new": sum(1 for t in self._state.tickets if t.status == TicketStatus.NEW),
432
+ "in_progress": sum(1 for t in self._state.tickets if t.status == TicketStatus.IN_PROGRESS),
433
+ "resolved": sum(1 for t in self._state.tickets if t.status == TicketStatus.RESOLVED),
434
+ "escalated": sum(1 for t in self._state.tickets if t.status == TicketStatus.ESCALATED)
435
+ }
436
+
437
+ # Determine available actions based on task
438
+ available_actions = ["categorize", "prioritize"]
439
+ if self._state.task_id in ["prioritize_and_route", "full_workflow"]:
440
+ available_actions.extend(["respond", "escalate", "request_info", "close"])
441
+
442
+ return SupportObservation(
443
+ tickets=self._state.tickets,
444
+ current_step=self._state.current_step,
445
+ max_steps=self._state.max_steps,
446
+ queue_status=queue_status,
447
+ available_actions=available_actions,
448
+ instructions=f"Task: {self._state.task_id}. Handle tickets appropriately."
449
+ )
450
+
451
+ def _handle_categorize(self, action: SupportAction) -> tuple:
452
+ """Handle ticket categorization action"""
453
+ ticket = self._find_ticket(action.ticket_id)
454
+ if not ticket:
455
+ return 0.0, "Ticket not found"
456
+
457
+ if not action.category:
458
+ return 0.0, "No category specified"
459
+
460
+ # Find expected category from template
461
+ expected_category = None
462
+ expected_priority = None
463
+ for category_tickets in TICKET_TEMPLATES.values():
464
+ for template in category_tickets:
465
+ if template["subject"] == ticket.subject:
466
+ expected_category = template["expected_category"]
467
+ expected_priority = template["expected_priority"]
468
+ break
469
+
470
+ if expected_category is None:
471
+ expected_category = TicketCategory.GENERAL
472
+ expected_priority = PriorityLevel.MEDIUM
473
+
474
+ # Grade the categorization
475
+ reward = self.grader.grade_categorization(
476
+ action.category, expected_category,
477
+ action.priority if action.priority else PriorityLevel.MEDIUM, expected_priority
478
+ )
479
+
480
+ # Update ticket
481
+ ticket.category = action.category
482
+ if action.priority:
483
+ ticket.priority = action.priority
484
+ ticket.status = TicketStatus.IN_PROGRESS
485
+
486
+ result = (f"Categorized ticket {ticket.id} as {action.category.value} "
487
+ f"with priority {ticket.priority.value if ticket.priority else 'unassigned'}")
488
+
489
+ return reward, result
490
+
491
+ def _handle_prioritize(self, action: SupportAction) -> tuple:
492
+ """Handle ticket prioritization action"""
493
+ ticket = self._find_ticket(action.ticket_id)
494
+ if not ticket:
495
+ return 0.0, "Ticket not found"
496
+
497
+ if not action.priority:
498
+ return 0.0, "No priority specified"
499
+
500
+ # Base reward on ticket content urgency
501
+ content_lower = ticket.content.lower()
502
+ urgency_indicators = ["urgent", "immediately", "asap", "critical", "emergency"]
503
+ has_urgency = any(ind in content_lower for ind in urgency_indicators)
504
+
505
+ expected_priority = PriorityLevel.HIGH if has_urgency else PriorityLevel.MEDIUM
506
+
507
+ # Grade prioritization
508
+ priority_levels = {
509
+ PriorityLevel.LOW: 0,
510
+ PriorityLevel.MEDIUM: 1,
511
+ PriorityLevel.HIGH: 2,
512
+ PriorityLevel.CRITICAL: 3
513
+ }
514
+
515
+ agent_level = priority_levels.get(action.priority, 1)
516
+ expected_level = priority_levels.get(expected_priority, 1)
517
+
518
+ if agent_level == expected_level:
519
+ reward = 0.8
520
+ elif abs(agent_level - expected_level) == 1:
521
+ reward = 0.5
522
+ else:
523
+ reward = 0.2
524
+
525
+ ticket.priority = action.priority
526
+
527
+ result = f"Prioritized ticket {ticket.id} as {action.priority.value}"
528
+ return reward, result
529
+
530
+ def _handle_respond(self, action: SupportAction) -> tuple:
531
+ """Handle response action"""
532
+ ticket = self._find_ticket(action.ticket_id)
533
+ if not ticket:
534
+ return 0.0, "Ticket not found"
535
+
536
+ if not action.response_text:
537
+ return 0.0, "No response text provided"
538
+
539
+ if not ticket.category:
540
+ return 0.0, "Ticket must be categorized before responding"
541
+
542
+ # Grade response quality
543
+ reward = self.grader.grade_response(action.response_text, ticket.category)
544
+
545
+ ticket.responses.append(action.response_text)
546
+ ticket.status = TicketStatus.WAITING_CUSTOMER
547
+
548
+ result = f"Responded to ticket {ticket.id} ({len(action.response_text)} chars)"
549
+ return reward, result
550
+
551
+ def _handle_escalate(self, action: SupportAction) -> tuple:
552
+ """Handle escalation action"""
553
+ ticket = self._find_ticket(action.ticket_id)
554
+ if not ticket:
555
+ return 0.0, "Ticket not found"
556
+
557
+ if not action.escalation_reason:
558
+ return 0.0, "No escalation reason provided"
559
+
560
+ # Grade escalation decision
561
+ priority = ticket.priority if ticket.priority else PriorityLevel.MEDIUM
562
+ reward = self.grader.grade_escalation(
563
+ priority,
564
+ action.escalation_reason if action.escalation_reason else "",
565
+ action.target_team if action.target_team else "general"
566
+ )
567
+
568
+ ticket.status = TicketStatus.ESCALATED
569
+ ticket.escalation_reason = action.escalation_reason
570
+ ticket.assigned_team = action.target_team
571
+
572
+ result = f"Escalated ticket {ticket.id}: {action.escalation_reason}"
573
+ return reward, result
574
+
575
+ def _handle_request_info(self, action: SupportAction) -> tuple:
576
+ """Handle request for information action"""
577
+ ticket = self._find_ticket(action.ticket_id)
578
+ if not ticket:
579
+ return 0.0, "Ticket not found"
580
+
581
+ if not action.response_text:
582
+ return 0.0, "No message provided"
583
+
584
+ ticket.responses.append(f"[INFO_REQUEST] {action.response_text}")
585
+ ticket.status = TicketStatus.WAITING_CUSTOMER
586
+
587
+ result = f"Requested information from customer for ticket {ticket.id}"
588
+ return 0.4, result
589
+
590
+ def _handle_close(self, action: SupportAction) -> tuple:
591
+ """Handle ticket closure"""
592
+ ticket = self._find_ticket(action.ticket_id)
593
+ if not ticket:
594
+ return 0.0, "Ticket not found"
595
+
596
+ # Can only close if ticket has been handled
597
+ if ticket.status not in [TicketStatus.RESOLVED, TicketStatus.WAITING_CUSTOMER]:
598
+ return 0.0, "Ticket cannot be closed in current state"
599
+
600
+ ticket.status = TicketStatus.CLOSED
601
+
602
+ # Bonus for completing ticket lifecycle
603
+ reward = 0.3 if ticket.responses else 0.1
604
+
605
+ result = f"Closed ticket {ticket.id}"
606
+ return reward, result
607
+
608
+ def _find_ticket(self, ticket_id) -> Optional[Ticket]:
609
+ """Find ticket by ID"""
610
+ if not ticket_id or self._state is None:
611
+ return None
612
+ for ticket in self._state.tickets:
613
+ if ticket.id == ticket_id:
614
+ return ticket
615
+ return None
616
+
617
+ def _calculate_score(self) -> float:
618
+ """Calculate overall performance score"""
619
+ if self._state is None or self._state.total_actions == 0:
620
+ return 0.0
621
+
622
+ # Weighted score based on action correctness
623
+ correctness_ratio = self._state.correct_actions / self._state.total_actions
624
+
625
+ # Bonus for completing all tickets
626
+ completed_tickets = sum(1 for t in self._state.tickets
627
+ if t.status in [TicketStatus.RESOLVED, TicketStatus.CLOSED])
628
+ completion_ratio = completed_tickets / max(len(self._state.tickets), 1)
629
+
630
+ # Penalty for unresolved tickets at end
631
+ unresolved = sum(1 for t in self._state.tickets if t.status == TicketStatus.NEW)
632
+ unresolved_penalty = unresolved * 0.1
633
+
634
+ score = (correctness_ratio * 0.6 + completion_ratio * 0.4) - unresolved_penalty
635
+ return max(0.0, min(1.0, score))
636
+
637
+ def _check_done(self) -> bool:
638
+ """Check if episode is complete"""
639
+ if self._state is None:
640
+ return True
641
+
642
+ # Max steps reached
643
+ if self._state.current_step >= self._state.max_steps:
644
+ return True
645
+
646
+ # All tickets handled
647
+ all_handled = all(
648
+ t.status in [TicketStatus.RESOLVED, TicketStatus.CLOSED, TicketStatus.ESCALATED]
649
+ for t in self._state.tickets
650
+ )
651
+
652
+ return all_handled and self._state.current_step > 0
inference.py ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Baseline Inference Script for Support Ticket Triage Environment
3
+ This script demonstrates how to interact with the environment using an LLM agent.
4
+ """
5
+
6
+ import asyncio
7
+ import os
8
+ import textwrap
9
+ import json
10
+ from typing import List, Optional, Dict, Any
11
+
12
+ from openai import OpenAI
13
+ import httpx
14
+
15
+ # ============================================================================
16
+ # Environment Configuration
17
+ # ============================================================================
18
+
19
+ # Required environment variables
20
+ API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
21
+ MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct")
22
+ API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY", "")
23
+
24
+ # Environment configuration
25
+ ENV_URL = os.getenv("ENV_URL", "http://localhost:8000")
26
+ TASK_NAME = os.getenv("SUPPORT_TICKET_TASK", "categorize_ticket")
27
+ BENCHMARK = os.getenv("SUPPORT_TICKET_BENCHMARK", "support-ticket-triage")
28
+
29
+ # Inference parameters
30
+ MAX_STEPS = 10
31
+ TEMPERATURE = 0.7
32
+ MAX_TOKENS = 200
33
+ SUCCESS_SCORE_THRESHOLD = 0.5
34
+
35
+ # ============================================================================
36
+ # Logging Functions (Mandatory Format)
37
+ # ============================================================================
38
+
39
+ def log_start(task: str, env: str, model: str) -> None:
40
+ """Log episode start"""
41
+ print(f"[START] task={task} env={env} model={model}", flush=True)
42
+
43
+
44
+ def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
45
+ """Log each step"""
46
+ error_val = error if error else "null"
47
+ done_val = str(done).lower()
48
+ print(
49
+ f"[STEP] step={step} action={action} reward={reward:.2f} done={done_val} error={error_val}",
50
+ flush=True,
51
+ )
52
+
53
+
54
+ def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
55
+ """Log episode end"""
56
+ rewards_str = ",".join(f"{r:.2f}" for r in rewards)
57
+ print(f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={rewards_str}", flush=True)
58
+
59
+
60
+ # ============================================================================
61
+ # Environment Client
62
+ # ============================================================================
63
+
64
+ class EnvironmentClient:
65
+ """Client for interacting with the Support Ticket Environment"""
66
+
67
+ def __init__(self, base_url: str):
68
+ self.base_url = base_url.rstrip("/")
69
+ self.client = httpx.AsyncClient(timeout=30.0)
70
+
71
+ async def reset(self, task_id: str = "categorize_ticket") -> Dict[str, Any]:
72
+ """Reset the environment"""
73
+ try:
74
+ response = await self.client.post(
75
+ f"{self.base_url}/reset",
76
+ json={"task_id": task_id}
77
+ )
78
+ response.raise_for_status()
79
+ return response.json()
80
+ except Exception as e:
81
+ print(f"[DEBUG] Reset failed: {e}", flush=True)
82
+ raise
83
+
84
+ async def step(self, action: Dict[str, Any]) -> Dict[str, Any]:
85
+ """Execute a step in the environment"""
86
+ try:
87
+ response = await self.client.post(
88
+ f"{self.base_url}/step",
89
+ json=action
90
+ )
91
+ response.raise_for_status()
92
+ return response.json()
93
+ except Exception as e:
94
+ print(f"[DEBUG] Step failed: {e}", flush=True)
95
+ raise
96
+
97
+ async def get_state(self) -> Dict[str, Any]:
98
+ """Get current environment state"""
99
+ try:
100
+ response = await self.client.get(f"{self.base_url}/state")
101
+ response.raise_for_status()
102
+ return response.json()
103
+ except Exception as e:
104
+ print(f"[DEBUG] Get state failed: {e}", flush=True)
105
+ raise
106
+
107
+ async def health_check(self) -> bool:
108
+ """Check if environment is healthy"""
109
+ try:
110
+ response = await self.client.get(f"{self.base_url}/health")
111
+ return response.status_code == 200
112
+ except:
113
+ return False
114
+
115
+ async def close(self):
116
+ """Close the client"""
117
+ await self.client.aclose()
118
+
119
+
120
+ # ============================================================================
121
+ # LLM Agent
122
+ # ============================================================================
123
+
124
+ SYSTEM_PROMPT = textwrap.dedent(
125
+ """
126
+ You are an AI assistant helping to triage customer support tickets.
127
+
128
+ Your task is to:
129
+ 1. Read each ticket carefully
130
+ 2. Categorize it into one of: technical, billing, account, general, sales, urgent
131
+ 3. Assign priority: low, medium, high, critical
132
+ 4. For medium/hard tasks, you may also need to respond or escalate
133
+
134
+ Available actions:
135
+ - categorize: Set category and priority for a ticket
136
+ - prioritize: Set priority for a ticket
137
+ - respond: Send a response to the customer
138
+ - escalate: Escalate to a specialized team
139
+ - request_info: Ask customer for more information
140
+ - close: Close a resolved ticket
141
+
142
+ Always respond with a JSON object containing:
143
+ {
144
+ "action_type": "your_action",
145
+ "ticket_id": "ticket_id",
146
+ "category": "category_if_applicable",
147
+ "priority": "priority_if_applicable",
148
+ "response_text": "text_if_responding",
149
+ "escalation_reason": "reason_if_escalating",
150
+ "target_team": "team_if_escalating"
151
+ }
152
+
153
+ Be professional, thorough, and prioritize urgent issues appropriately.
154
+ """
155
+ ).strip()
156
+
157
+
158
+ def build_user_prompt(observation: Dict[str, Any]) -> str:
159
+ """Build user prompt from observation"""
160
+ tickets = observation.get("tickets", [])
161
+ instructions = observation.get("instructions", "")
162
+ current_step = observation.get("current_step", 0)
163
+ max_steps = observation.get("max_steps", 10)
164
+ last_result = observation.get("last_action_result", "")
165
+
166
+ prompt = f"Instructions: {instructions}\n\n"
167
+ prompt += f"Step {current_step}/{max_steps}\n\n"
168
+
169
+ if last_result:
170
+ prompt += f"Last action result: {last_result}\n\n"
171
+
172
+ prompt += "Current Tickets:\n"
173
+ for ticket in tickets:
174
+ prompt += f"- ID: {ticket['id']}\n"
175
+ prompt += f" From: {ticket['customer_name']} ({ticket['customer_email']})\n"
176
+ prompt += f" Subject: {ticket['subject']}\n"
177
+ prompt += f" Content: {ticket['content']}\n"
178
+ prompt += f" Status: {ticket['status']}\n"
179
+ if ticket.get('category'):
180
+ prompt += f" Category: {ticket['category']}\n"
181
+ if ticket.get('priority'):
182
+ prompt += f" Priority: {ticket['priority']}\n"
183
+ prompt += "\n"
184
+
185
+ queue_status = observation.get("queue_status", {})
186
+ if queue_status:
187
+ prompt += f"Queue Status: {queue_status}\n\n"
188
+
189
+ prompt += "What action should be taken next? Respond with a JSON object."
190
+
191
+ return prompt
192
+
193
+
194
+ def get_llm_action(client: OpenAI, observation: Dict[str, Any]) -> Optional[Dict[str, Any]]:
195
+ """Get action from LLM"""
196
+ user_prompt = build_user_prompt(observation)
197
+
198
+ try:
199
+ completion = client.chat.completions.create(
200
+ model=MODEL_NAME,
201
+ messages=[
202
+ {"role": "system", "content": SYSTEM_PROMPT},
203
+ {"role": "user", "content": user_prompt},
204
+ ],
205
+ temperature=TEMPERATURE,
206
+ max_tokens=MAX_TOKENS,
207
+ stream=False,
208
+ )
209
+
210
+ content = (completion.choices[0].message.content or "").strip()
211
+
212
+ # Try to parse as JSON
213
+ try:
214
+ # Remove markdown code blocks if present
215
+ if content.startswith("```json"):
216
+ content = content[7:]
217
+ if content.endswith("```"):
218
+ content = content[:-3]
219
+ content = content.strip()
220
+
221
+ action = json.loads(content)
222
+ return action
223
+ except json.JSONDecodeError:
224
+ print(f"[DEBUG] Failed to parse LLM response as JSON: {content}", flush=True)
225
+ return None
226
+
227
+ except Exception as e:
228
+ print(f"[DEBUG] LLM request failed: {e}", flush=True)
229
+ return None
230
+
231
+
232
+ # ============================================================================
233
+ # Main Inference Loop
234
+ # ============================================================================
235
+
236
+ async def main() -> None:
237
+ """Main inference loop"""
238
+
239
+ # Initialize clients
240
+ llm_client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
241
+ env_client = EnvironmentClient(ENV_URL)
242
+
243
+ # Verify environment is running
244
+ if not await env_client.health_check():
245
+ print(f"[ERROR] Environment not reachable at {ENV_URL}", flush=True)
246
+ log_end(success=False, steps=0, score=0.0, rewards=[])
247
+ return
248
+
249
+ rewards: List[float] = []
250
+ steps_taken = 0
251
+ score = 0.0
252
+ success = False
253
+
254
+ log_start(task=TASK_NAME, env=BENCHMARK, model=MODEL_NAME)
255
+
256
+ try:
257
+ # Reset environment
258
+ result = await env_client.reset(TASK_NAME)
259
+ observation = result.get("observation", {})
260
+
261
+ for step in range(1, MAX_STEPS + 1):
262
+ if result.get("done", False):
263
+ break
264
+
265
+ # Get action from LLM
266
+ action = get_llm_action(llm_client, observation)
267
+
268
+ if action is None:
269
+ # Fallback action if LLM fails
270
+ action = {
271
+ "action_type": "categorize",
272
+ "ticket_id": observation.get("tickets", [{}])[0].get("id", ""),
273
+ "category": "general",
274
+ "priority": "medium"
275
+ }
276
+
277
+ # Execute action
278
+ result = await env_client.step(action)
279
+ observation = result.get("observation", {})
280
+ reward = result.get("reward", 0.0)
281
+ done = result.get("done", False)
282
+ error = None
283
+
284
+ rewards.append(reward)
285
+ steps_taken = step
286
+
287
+ # Format action for logging
288
+ action_str = f"{action.get('action_type', 'unknown')}({action.get('ticket_id', '')})"
289
+
290
+ log_step(step=step, action=action_str, reward=reward, done=done, error=error)
291
+
292
+ if done:
293
+ break
294
+
295
+ # Calculate final score
296
+ state = await env_client.get_state()
297
+ score = state.get("score", 0.0)
298
+ score = min(max(score, 0.0), 1.0) # Clamp to [0, 1]
299
+ success = score >= SUCCESS_SCORE_THRESHOLD
300
+
301
+ except Exception as e:
302
+ print(f"[ERROR] Inference failed: {e}", flush=True)
303
+ success = False
304
+
305
+ finally:
306
+ try:
307
+ await env_client.close()
308
+ except:
309
+ pass
310
+
311
+ log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
312
+
313
+
314
+ if __name__ == "__main__":
315
+ asyncio.run(main())
models.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pydantic models for the Support Ticket Triage Environment.
3
+ """
4
+
5
+ import uuid
6
+ from typing import Optional, Dict, List, Any
7
+ from datetime import datetime
8
+ from enum import Enum
9
+ from pydantic import BaseModel, Field
10
+
11
+
12
+ class TicketCategory(str, Enum):
13
+ TECHNICAL = "technical"
14
+ BILLING = "billing"
15
+ ACCOUNT = "account"
16
+ GENERAL = "general"
17
+ SALES = "sales"
18
+ URGENT = "urgent"
19
+
20
+
21
+ class PriorityLevel(str, Enum):
22
+ LOW = "low"
23
+ MEDIUM = "medium"
24
+ HIGH = "high"
25
+ CRITICAL = "critical"
26
+
27
+
28
+ class TicketStatus(str, Enum):
29
+ NEW = "new"
30
+ IN_PROGRESS = "in_progress"
31
+ WAITING_CUSTOMER = "waiting_customer"
32
+ RESOLVED = "resolved"
33
+ ESCALATED = "escalated"
34
+ CLOSED = "closed"
35
+
36
+
37
+ class ActionType(str, Enum):
38
+ CATEGORIZE = "categorize"
39
+ PRIORITIZE = "prioritize"
40
+ RESPOND = "respond"
41
+ ESCALATE = "escalate"
42
+ REQUEST_INFO = "request_info"
43
+ CLOSE = "close"
44
+
45
+
46
+ class Ticket(BaseModel):
47
+ id: str = Field(default_factory=lambda: str(uuid.uuid4())[:8])
48
+ customer_name: str
49
+ customer_email: str
50
+ subject: str
51
+ content: str
52
+ category: Optional[TicketCategory] = None
53
+ priority: Optional[PriorityLevel] = None
54
+ status: TicketStatus = TicketStatus.NEW
55
+ created_at: str = Field(default_factory=lambda: datetime.now().isoformat())
56
+ responses: List[str] = Field(default_factory=list)
57
+ assigned_team: Optional[str] = None
58
+ escalation_reason: Optional[str] = None
59
+
60
+
61
+ class SupportAction(BaseModel):
62
+ action_type: ActionType
63
+ ticket_id: Optional[str] = None
64
+ parameters: Dict[str, Any] = Field(default_factory=dict)
65
+ category: Optional[TicketCategory] = None
66
+ priority: Optional[PriorityLevel] = None
67
+ response_text: Optional[str] = None
68
+ escalation_reason: Optional[str] = None
69
+ target_team: Optional[str] = None
70
+
71
+
72
+ class SupportObservation(BaseModel):
73
+ tickets: List[Ticket] = Field(default_factory=list)
74
+ current_step: int = 0
75
+ max_steps: int = 10
76
+ queue_status: Dict[str, int] = Field(default_factory=dict)
77
+ available_actions: List[str] = Field(default_factory=list)
78
+ instructions: str = ""
79
+ last_action_result: Optional[str] = None
80
+
81
+
82
+ class SupportState(BaseModel):
83
+ tickets: List[Ticket] = Field(default_factory=list)
84
+ current_step: int = 0
85
+ max_steps: int = 10
86
+ task_id: str = "categorize_ticket"
87
+ score: float = 0.0
88
+ actions_taken: List[Dict] = Field(default_factory=list)
89
+ correct_actions: int = 0
90
+ total_actions: int = 0
openenv.yaml ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: support-ticket-triage
2
+ version: 1.0.0
3
+ description: |
4
+ A real-world customer support ticket management environment where an AI agent
5
+ must triage, categorize, prioritize, and respond to customer support tickets.
6
+ The agent learns to efficiently handle support workflows with increasing complexity.
7
+
8
+ author: Meta Env Hackathon Participant
9
+ license: MIT
10
+
11
+ entry_point: env.py:SupportTicketEnv
12
+
13
+ tasks:
14
+ - id: categorize_ticket
15
+ name: Ticket Categorization (Easy)
16
+ description: |
17
+ Categorize incoming support tickets into the correct department and priority level.
18
+ The agent must read the ticket content and assign appropriate category and priority.
19
+ difficulty: easy
20
+ max_steps: 5
21
+ success_threshold: 0.7
22
+
23
+ - id: prioritize_and_route
24
+ name: Prioritize and Route (Medium)
25
+ description: |
26
+ Handle multiple tickets by prioritizing them correctly and routing to appropriate teams.
27
+ The agent must manage a queue of tickets and make routing decisions.
28
+ difficulty: medium
29
+ max_steps: 10
30
+ success_threshold: 0.6
31
+
32
+ - id: full_workflow
33
+ name: Full Support Workflow (Hard)
34
+ description: |
35
+ Complete end-to-end support workflow: categorize, prioritize, draft responses,
36
+ and escalate when necessary. The agent handles a realistic support scenario.
37
+ difficulty: hard
38
+ max_steps: 15
39
+ success_threshold: 0.5
40
+
41
+ environment:
42
+ type: text
43
+ observation_space:
44
+ type: dict
45
+ properties:
46
+ tickets:
47
+ type: array
48
+ description: List of support tickets to handle
49
+ current_step:
50
+ type: integer
51
+ description: Current step in the workflow
52
+ queue_status:
53
+ type: object
54
+ description: Status of the ticket queue
55
+ action_space:
56
+ type: dict
57
+ properties:
58
+ action_type:
59
+ type: string
60
+ enum: [categorize, prioritize, respond, escalate, request_info, close]
61
+ ticket_id:
62
+ type: string
63
+ description: ID of the ticket to act upon
64
+ parameters:
65
+ type: object
66
+ description: Action-specific parameters
67
+
68
+ dependencies:
69
+ python: ">=3.10"
70
+ packages:
71
+ - pydantic>=2.0.0
72
+ - fastapi>=0.104.0
73
+ - uvicorn>=0.24.0
pyproject.toml ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.backends._legacy:_Backend"
4
+
5
+ [project]
6
+ name = "support-ticket-triage"
7
+ version = "1.0.0"
8
+ description = "A real-world customer support ticket management environment for AI agents"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ dependencies = [
12
+ "openenv-core>=0.2.0",
13
+ "fastapi>=0.104.0",
14
+ "uvicorn>=0.24.0",
15
+ "pydantic>=2.0.0",
16
+ "pydantic-settings>=2.0.0",
17
+ "httpx>=0.24.0",
18
+ "requests>=2.28.0",
19
+ "openai>=1.0.0",
20
+ "python-dotenv>=1.0.0",
21
+ "pyyaml>=6.0",
22
+ ]
23
+
24
+ [project.optional-dependencies]
25
+ dev = [
26
+ "pytest>=7.0.0",
27
+ "pytest-asyncio>=0.21.0",
28
+ ]
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["."]
requirements.txt ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Support Ticket Triage Environment Dependencies
2
+ # Compatible with Python 3.10+
3
+
4
+ # Core OpenEnv framework
5
+ openenv-core>=0.2.0
6
+
7
+ # Web framework
8
+ fastapi>=0.104.0
9
+ uvicorn>=0.24.0
10
+
11
+ # Data validation
12
+ pydantic>=2.0.0
13
+ pydantic-settings>=2.0.0
14
+
15
+ # HTTP client for inference
16
+ httpx>=0.24.0
17
+ requests>=2.28.0
18
+
19
+ # OpenAI client for LLM calls
20
+ openai>=1.0.0
21
+
22
+ # Utilities
23
+ python-dotenv>=1.0.0
24
+ pyyaml>=6.0
25
+
26
+ # Testing (optional)
27
+ pytest>=7.0.0
28
+ pytest-asyncio>=0.21.0
server.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI server for the Support Ticket Triage Environment.
3
+ This server exposes the OpenEnv API endpoints for reset, step, and state.
4
+ """
5
+
6
+ import os
7
+ import json
8
+ import uvicorn
9
+ from datetime import datetime
10
+ from fastapi import FastAPI, HTTPException
11
+ from fastapi.middleware.cors import CORSMiddleware
12
+ from fastapi.responses import JSONResponse
13
+ from pydantic import BaseModel
14
+ from typing import Optional
15
+
16
+ from env import SupportTicketEnv, SupportAction
17
+
18
+
19
+ class DateTimeEncoder(json.JSONEncoder):
20
+ """Custom JSON encoder that handles datetime objects"""
21
+ def default(self, obj):
22
+ if isinstance(obj, datetime):
23
+ return obj.isoformat()
24
+ return super().default(obj)
25
+
26
+
27
+ # ============================================================================
28
+ # FastAPI Application
29
+ # ============================================================================
30
+
31
+ app = FastAPI(
32
+ title="Support Ticket Triage Environment",
33
+ description="A real-world customer support ticket management environment for AI agents",
34
+ version="1.0.0"
35
+ )
36
+
37
+ # Enable CORS for all origins (needed for HF Spaces)
38
+ app.add_middleware(
39
+ CORSMiddleware,
40
+ allow_origins=["*"],
41
+ allow_credentials=True,
42
+ allow_methods=["*"],
43
+ allow_headers=["*"],
44
+ )
45
+
46
+ # Initialize environment
47
+ env = SupportTicketEnv()
48
+
49
+
50
+ # ============================================================================
51
+ # Request/Response Models
52
+ # ============================================================================
53
+
54
+ class ResetRequest(BaseModel):
55
+ task_id: Optional[str] = "categorize_ticket"
56
+
57
+
58
+ class HealthResponse(BaseModel):
59
+ status: str
60
+ environment: str
61
+ version: str
62
+
63
+
64
+ # ============================================================================
65
+ # API Endpoints
66
+ # ============================================================================
67
+
68
+ @app.get("/health", response_model=HealthResponse)
69
+ async def health_check():
70
+ """Health check endpoint"""
71
+ return HealthResponse(
72
+ status="healthy",
73
+ environment="support-ticket-triage",
74
+ version="1.0.0"
75
+ )
76
+
77
+
78
+ @app.post("/reset")
79
+ async def reset_environment(request: ResetRequest = ResetRequest()):
80
+ """
81
+ Reset the environment to initial state.
82
+
83
+ Args:
84
+ task_id: The task to run (categorize_ticket, prioritize_and_route, full_workflow)
85
+
86
+ Returns:
87
+ EnvResult with initial observation
88
+ """
89
+ try:
90
+ result = await env.reset(request.task_id if request.task_id else "categorize_ticket")
91
+ return JSONResponse(content=result, media_type="application/json")
92
+ except Exception as e:
93
+ raise HTTPException(status_code=500, detail=str(e))
94
+
95
+
96
+ @app.post("/step")
97
+ async def step_environment(action: SupportAction):
98
+ """
99
+ Execute an action in the environment.
100
+
101
+ Args:
102
+ action: The action to execute (SupportAction model)
103
+
104
+ Returns:
105
+ EnvResult with observation, reward, done flag, and info
106
+ """
107
+ try:
108
+ result = await env.step(action)
109
+ return JSONResponse(content=result, media_type="application/json")
110
+ except ValueError as e:
111
+ raise HTTPException(status_code=400, detail=str(e))
112
+ except Exception as e:
113
+ raise HTTPException(status_code=500, detail=str(e))
114
+
115
+
116
+ @app.get("/state")
117
+ async def get_state():
118
+ """
119
+ Get the current environment state.
120
+
121
+ Returns:
122
+ Current SupportState
123
+ """
124
+ state = env.get_state()
125
+ if state is None:
126
+ raise HTTPException(status_code=404, detail="Environment not initialized. Call /reset first.")
127
+ return JSONResponse(content=state.model_dump())
128
+
129
+
130
+ @app.get("/")
131
+ async def root():
132
+ """Root endpoint with API information"""
133
+ return {
134
+ "name": "Support Ticket Triage Environment",
135
+ "version": "1.0.0",
136
+ "description": "A real-world customer support ticket management environment for AI agents",
137
+ "endpoints": {
138
+ "health": "GET /health",
139
+ "reset": "POST /reset",
140
+ "step": "POST /step",
141
+ "state": "GET /state"
142
+ },
143
+ "tasks": [
144
+ {
145
+ "id": "categorize_ticket",
146
+ "name": "Ticket Categorization (Easy)",
147
+ "description": "Categorize incoming support tickets into correct department and priority"
148
+ },
149
+ {
150
+ "id": "prioritize_and_route",
151
+ "name": "Prioritize and Route (Medium)",
152
+ "description": "Handle multiple tickets by prioritizing and routing to appropriate teams"
153
+ },
154
+ {
155
+ "id": "full_workflow",
156
+ "name": "Full Support Workflow (Hard)",
157
+ "description": "Complete end-to-end support workflow with responses and escalations"
158
+ }
159
+ ]
160
+ }
161
+
162
+
163
+ # ============================================================================
164
+ # Main Entry Point
165
+ # ============================================================================
166
+
167
+ if __name__ == "__main__":
168
+ port = int(os.getenv("PORT", 8000))
169
+ host = os.getenv("HOST", "0.0.0.0")
170
+
171
+ print(f"Starting Support Ticket Triage Environment server on {host}:{port}")
172
+ uvicorn.run(app, host=host, port=port)
validate-submission.sh ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ #
3
+ # validate-submission.sh — OpenEnv Submission Validator
4
+ #
5
+ # Checks that your HF Space is live, Docker image builds, and openenv validate passes.
6
+ #
7
+ # Prerequisites:
8
+ # - Docker: https://docs.docker.com/get-docker/
9
+ # - openenv-core: pip install openenv-core
10
+ # - curl (usually pre-installed)
11
+ #
12
+ # Run:
13
+ # ./validate-submission.sh <ping_url> [repo_dir]
14
+ #
15
+ # Or download and run locally:
16
+ # chmod +x validate-submission.sh
17
+ # ./validate-submission.sh <ping_url> [repo_dir]
18
+ #
19
+ # Arguments:
20
+ # ping_url Your HuggingFace Space URL (e.g. https://your-space.hf.space)
21
+ # repo_dir Path to your repo (default: current directory)
22
+ #
23
+ # Examples:
24
+ # ./validate-submission.sh https://my-team.hf.space
25
+ # ./validate-submission.sh https://my-team.hf.space ./my-repo
26
+ #
27
+
28
+ set -uo pipefail
29
+
30
+ DOCKER_BUILD_TIMEOUT=600
31
+ if [ -t 1 ]; then
32
+ RED='\033[0;31m'
33
+ GREEN='\033[0;32m'
34
+ YELLOW='\033[1;33m'
35
+ BOLD='\033[1m'
36
+ NC='\033[0m'
37
+ else
38
+ RED='' GREEN='' YELLOW='' BOLD='' NC=''
39
+ fi
40
+
41
+ run_with_timeout() {
42
+ local secs="$1"; shift
43
+ if command -v timeout &>/dev/null; then
44
+ timeout "$secs" "$@"
45
+ elif command -v gtimeout &>/dev/null; then
46
+ gtimeout "$secs" "$@"
47
+ else
48
+ "$@" &
49
+ local pid=$!
50
+ ( sleep "$secs" && kill "$pid" 2>/dev/null ) &
51
+ local watcher=$!
52
+ wait "$pid" 2>/dev/null
53
+ local rc=$?
54
+ kill "$watcher" 2>/dev/null
55
+ wait "$watcher" 2>/dev/null
56
+ return $rc
57
+ fi
58
+ }
59
+
60
+ portable_mktemp() {
61
+ local prefix="${1:-validate}"
62
+ mktemp "${TMPDIR:-/tmp}/${prefix}-XXXXXX" 2>/dev/null || mktemp
63
+ }
64
+
65
+ CLEANUP_FILES=()
66
+ cleanup() { rm -f "${CLEANUP_FILES[@]+"${CLEANUP_FILES[@]}"}"; }
67
+ trap cleanup EXIT
68
+
69
+ PING_URL="${1:-}"
70
+ REPO_DIR="${2:-.}"
71
+
72
+ if [ -z "$PING_URL" ]; then
73
+ printf "Usage: %s <ping_url> [repo_dir]\n" "$0"
74
+ printf "\n"
75
+ printf " ping_url Your HuggingFace Space URL (e.g. https://your-space.hf.space)\n"
76
+ printf " repo_dir Path to your repo (default: current directory)\n"
77
+ exit 1
78
+ fi
79
+
80
+ if ! REPO_DIR="$(cd "$REPO_DIR" 2>/dev/null && pwd)"; then
81
+ printf "Error: directory '%s' not found\n" "${2:-.}"
82
+ exit 1
83
+ fi
84
+ PING_URL="${PING_URL%/}"
85
+ export PING_URL
86
+ PASS=0
87
+
88
+ log() { printf "[%s] %b\n" "$(date -u +%H:%M:%S)" "$*"; }
89
+ pass() { log "${GREEN}PASSED${NC} -- $1"; PASS=$((PASS + 1)); }
90
+ fail() { log "${RED}FAILED${NC} -- $1"; }
91
+ hint() { printf " ${YELLOW}Hint:${NC} %b\n" "$1"; }
92
+ stop_at() {
93
+ printf "\n"
94
+ printf "${RED}${BOLD}Validation stopped at %s.${NC} Fix the above before continuing.\n" "$1"
95
+ exit 1
96
+ }
97
+
98
+ printf "\n"
99
+ printf "${BOLD}========================================${NC}\n"
100
+ printf "${BOLD} OpenEnv Submission Validator${NC}\n"
101
+ printf "${BOLD}========================================${NC}\n"
102
+ log "Repo: $REPO_DIR"
103
+ log "Ping URL: $PING_URL"
104
+ printf "\n"
105
+
106
+ log "${BOLD}Step 1/3: Pinging HF Space${NC} ($PING_URL/reset) ..."
107
+
108
+ CURL_OUTPUT=$(portable_mktemp "validate-curl")
109
+ CLEANUP_FILES+=("$CURL_OUTPUT")
110
+ HTTP_CODE=$(curl -s -o "$CURL_OUTPUT" -w "%{http_code}" -X POST \
111
+ -H "Content-Type: application/json" -d '{}' \
112
+ "$PING_URL/reset" --max-time 30 2>"$CURL_OUTPUT" || printf "000")
113
+
114
+ if [ "$HTTP_CODE" = "200" ]; then
115
+ pass "HF Space is live and responds to /reset"
116
+ elif [ "$HTTP_CODE" = "000" ]; then
117
+ fail "HF Space not reachable (connection failed or timed out)"
118
+ hint "Check your network connection and that the Space is running."
119
+ hint "Try: curl -s -o /dev/null -w '%%{http_code}' -X POST $PING_URL/reset"
120
+ stop_at "Step 1"
121
+ else
122
+ fail "HF Space /reset returned HTTP $HTTP_CODE (expected 200)"
123
+ hint "Make sure your Space is running and the URL is correct."
124
+ hint "Try opening $PING_URL in your browser first."
125
+ stop_at "Step 1"
126
+ fi
127
+
128
+ log "${BOLD}Step 2/3: Running docker build${NC} ..."
129
+
130
+ if ! command -v docker &>/dev/null; then
131
+ fail "docker command not found"
132
+ hint "Install Docker: https://docs.docker.com/get-docker/"
133
+ stop_at "Step 2"
134
+ fi
135
+
136
+ if [ -f "$REPO_DIR/Dockerfile" ]; then
137
+ DOCKER_CONTEXT="$REPO_DIR"
138
+ elif [ -f "$REPO_DIR/server/Dockerfile" ]; then
139
+ DOCKER_CONTEXT="$REPO_DIR/server"
140
+ else
141
+ fail "No Dockerfile found in repo root or server/ directory"
142
+ stop_at "Step 2"
143
+ fi
144
+
145
+ log " Found Dockerfile in $DOCKER_CONTEXT"
146
+
147
+ BUILD_OK=false
148
+ BUILD_OUTPUT=$(run_with_timeout "$DOCKER_BUILD_TIMEOUT" docker build "$DOCKER_CONTEXT" 2>&1) && BUILD_OK=true
149
+
150
+ if [ "$BUILD_OK" = true ]; then
151
+ pass "Docker build succeeded"
152
+ else
153
+ fail "Docker build failed (timeout=${DOCKER_BUILD_TIMEOUT}s)"
154
+ printf "%s\n" "$BUILD_OUTPUT" | tail -20
155
+ stop_at "Step 2"
156
+ fi
157
+
158
+ log "${BOLD}Step 3/3: Running openenv validate${NC} ..."
159
+
160
+ if ! command -v openenv &>/dev/null; then
161
+ fail "openenv command not found"
162
+ hint "Install it: pip install openenv-core"
163
+ stop_at "Step 3"
164
+ fi
165
+
166
+ VALIDATE_OK=false
167
+ VALIDATE_OUTPUT=$(cd "$REPO_DIR" && openenv validate 2>&1) && VALIDATE_OK=true
168
+
169
+ if [ "$VALIDATE_OK" = true ]; then
170
+ pass "openenv validate passed"
171
+ [ -n "$VALIDATE_OUTPUT" ] && log " $VALIDATE_OUTPUT"
172
+ else
173
+ fail "openenv validate failed"
174
+ printf "%s\n" "$VALIDATE_OUTPUT"
175
+ stop_at "Step 3"
176
+ fi
177
+
178
+ printf "\n"
179
+ printf "${BOLD}========================================${NC}\n"
180
+ printf "${GREEN}${BOLD} All 3/3 checks passed!${NC}\n"
181
+ printf "${GREEN}${BOLD} Your submission is ready to submit.${NC}\n"
182
+ printf "${BOLD}========================================${NC}\n"
183
+ printf "\n"
184
+
185
+ exit 0