parthpethia commited on
Commit
1e1ca31
·
1 Parent(s): 8e75de8

Complete restructuring for hackathon validator compliance with server package, uv.lock, and openenv-core

Browse files
Files changed (7) hide show
  1. Dockerfile +3 -3
  2. pyproject.toml +6 -1
  3. requirements.txt +6 -5
  4. server/__init__.py +8 -0
  5. server/app.py +144 -0
  6. server/templates/index.html +833 -0
  7. uv.lock +147 -0
Dockerfile CHANGED
@@ -8,10 +8,10 @@ RUN pip install --no-cache-dir -r requirements.txt
8
 
9
  # Copy application code
10
  COPY environment/ ./environment/
11
- COPY templates/ ./templates/
12
- COPY app.py .
13
  COPY openenv.yaml .
14
  COPY inference.py .
 
15
 
16
  # Health check
17
  HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
@@ -20,4 +20,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
20
  # Run Flask app on port 7860 (HF Space standard)
21
  EXPOSE 7860
22
  ENV PORT=7860
23
- CMD ["python", "app.py"]
 
8
 
9
  # Copy application code
10
  COPY environment/ ./environment/
11
+ COPY server/ ./server/
 
12
  COPY openenv.yaml .
13
  COPY inference.py .
14
+ COPY pyproject.toml .
15
 
16
  # Health check
17
  HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
 
20
  # Run Flask app on port 7860 (HF Space standard)
21
  EXPOSE 7860
22
  ENV PORT=7860
23
+ CMD ["python", "-m", "server.app"]
pyproject.toml CHANGED
@@ -35,6 +35,7 @@ classifiers = [
35
  ]
36
 
37
  dependencies = [
 
38
  "pydantic>=2.5.0",
39
  "flask>=3.0.0",
40
  "openai>=1.3.0",
@@ -49,6 +50,9 @@ dev = [
49
  "pylint>=2.17.0",
50
  ]
51
 
 
 
 
52
  [project.urls]
53
  Homepage = "https://huggingface.co/spaces/parthpethia/Meta-Hackathon"
54
  Documentation = "https://github.com/parthpethia/Meta-Hackathon"
@@ -56,10 +60,11 @@ Repository = "https://github.com/parthpethia/Meta-Hackathon.git"
56
  Issues = "https://github.com/parthpethia/Meta-Hackathon/issues"
57
 
58
  [tool.setuptools]
59
- packages = ["environment"]
60
 
61
  [tool.setuptools.package-data]
62
  environment = ["**/*.py"]
 
63
 
64
  [tool.black]
65
  line-length = 120
 
35
  ]
36
 
37
  dependencies = [
38
+ "openenv-core>=0.2.0",
39
  "pydantic>=2.5.0",
40
  "flask>=3.0.0",
41
  "openai>=1.3.0",
 
50
  "pylint>=2.17.0",
51
  ]
52
 
53
+ [project.scripts]
54
+ server = "server.app:main"
55
+
56
  [project.urls]
57
  Homepage = "https://huggingface.co/spaces/parthpethia/Meta-Hackathon"
58
  Documentation = "https://github.com/parthpethia/Meta-Hackathon"
 
60
  Issues = "https://github.com/parthpethia/Meta-Hackathon/issues"
61
 
62
  [tool.setuptools]
63
+ packages = ["environment", "server"]
64
 
65
  [tool.setuptools.package-data]
66
  environment = ["**/*.py"]
67
+ server = ["**/*.py", "templates/**"]
68
 
69
  [tool.black]
70
  line-length = 120
requirements.txt CHANGED
@@ -1,5 +1,6 @@
1
- pydantic==2.5.0
2
- flask==3.0.0
3
- python-dotenv==1.0.0
4
- openai==1.3.0
5
- pyyaml==6.0
 
 
1
+ openenv-core>=0.2.0
2
+ pydantic>=2.5.0
3
+ flask>=3.0.0
4
+ openai>=1.3.0
5
+ python-dotenv>=1.0.0
6
+ pyyaml>=6.0
server/__init__.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ """Email Triage OpenEnv Server Package"""
2
+
3
+ __version__ = "1.0.0"
4
+ __author__ = "Meta Hackathon"
5
+
6
+ from server.app import app, main
7
+
8
+ __all__ = ["app", "main"]
server/app.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Flask REST API server for Email Triage OpenEnv."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ from flask import Flask, request, jsonify, render_template, send_file
7
+
8
+ from environment.env import EmailTriageEnv
9
+ from environment.types import Action
10
+
11
+ app = Flask(__name__, template_folder=os.path.join(os.path.dirname(__file__), 'templates'))
12
+
13
+ # Global environment instances (one per task)
14
+ environments = {}
15
+
16
+ def get_env(task_name: str = "spam_detection") -> EmailTriageEnv:
17
+ """Get or create environment for task"""
18
+ if task_name not in environments:
19
+ environments[task_name] = EmailTriageEnv(task_name=task_name)
20
+ return environments[task_name]
21
+
22
+ @app.route("/", methods=["GET"])
23
+ def index():
24
+ """Root endpoint - Interactive dashboard"""
25
+ try:
26
+ return render_template("index.html")
27
+ except Exception:
28
+ # Fallback: read HTML file directly
29
+ html_path = os.path.join(os.path.dirname(__file__), 'templates', 'index.html')
30
+ if os.path.exists(html_path):
31
+ with open(html_path, 'r', encoding='utf-8') as f:
32
+ return f.read()
33
+ return jsonify({
34
+ "status": "ok",
35
+ "message": "Email Triage OpenEnv API",
36
+ "version": "1.0.0",
37
+ "note": "Dashboard not available. Use API endpoints directly.",
38
+ "endpoints": {
39
+ "health": "GET /health",
40
+ "tasks": "GET /tasks",
41
+ "reset": "POST /reset?task=spam_detection",
42
+ "step": "POST /step?task=spam_detection",
43
+ "state": "GET /state?task=spam_detection",
44
+ "state-describe": "GET /state-describe?task=spam_detection"
45
+ }
46
+ }), 200
47
+
48
+ @app.route("/health", methods=["GET"])
49
+ def health():
50
+ """Health check endpoint"""
51
+ return jsonify({"status": "ok"}), 200
52
+
53
+ @app.route("/reset", methods=["POST"])
54
+ def reset():
55
+ """Reset environment - POST /reset?task=spam_detection"""
56
+ task_name = request.args.get("task", "spam_detection")
57
+ env = get_env(task_name)
58
+ obs = env.reset()
59
+ return jsonify({
60
+ "observation": obs.model_dump(mode="json"),
61
+ "task": task_name
62
+ }), 200
63
+
64
+ @app.route("/step", methods=["POST"])
65
+ def step():
66
+ """Step environment - POST /step with JSON action"""
67
+ task_name = request.args.get("task", "spam_detection")
68
+ env = get_env(task_name)
69
+
70
+ data = request.get_json()
71
+ if not data:
72
+ return jsonify({"error": "No action provided"}), 400
73
+
74
+ try:
75
+ action = Action(
76
+ classification=data.get("classification"),
77
+ team=data.get("team", "none"),
78
+ priority=int(data.get("priority", 1))
79
+ )
80
+ except Exception as e:
81
+ return jsonify({"error": f"Invalid action: {str(e)}"}), 400
82
+
83
+ obs, reward, done, info = env.step(action)
84
+
85
+ return jsonify({
86
+ "observation": obs.model_dump(mode="json"),
87
+ "reward": reward.model_dump(mode="json"),
88
+ "done": done,
89
+ "info": info
90
+ }), 200
91
+
92
+ @app.route("/state", methods=["GET"])
93
+ def state():
94
+ """Get current state - GET /state?task=spam_detection"""
95
+ task_name = request.args.get("task", "spam_detection")
96
+ env = get_env(task_name)
97
+ state = env.state()
98
+ return jsonify(state.model_dump(mode="json")), 200
99
+
100
+ @app.route("/state-describe", methods=["GET"])
101
+ def state_describe():
102
+ """Describe observation and action spaces"""
103
+ task_name = request.args.get("task", "spam_detection")
104
+ env = get_env(task_name)
105
+ return jsonify({
106
+ "observation_space": env.describe_observation_space(),
107
+ "action_space": env.describe_action_space()
108
+ }), 200
109
+
110
+ @app.route("/tasks", methods=["GET"])
111
+ def tasks():
112
+ """List available tasks"""
113
+ return jsonify({
114
+ "tasks": [
115
+ {
116
+ "name": "spam_detection",
117
+ "description": "Binary spam/non-spam classification",
118
+ "difficulty": "easy",
119
+ "num_emails": 10
120
+ },
121
+ {
122
+ "name": "multi_class_routing",
123
+ "description": "Multi-class classification with routing",
124
+ "difficulty": "medium",
125
+ "num_emails": 12
126
+ },
127
+ {
128
+ "name": "context_aware_triage",
129
+ "description": "Complex context-aware triage with escalation",
130
+ "difficulty": "hard",
131
+ "num_emails": 20
132
+ }
133
+ ]
134
+ }), 200
135
+
136
+
137
+ def main():
138
+ """Main entry point for the server"""
139
+ port = int(os.environ.get("PORT", 7860))
140
+ app.run(host="0.0.0.0", port=port, debug=False)
141
+
142
+
143
+ if __name__ == "__main__":
144
+ main()
server/templates/index.html ADDED
@@ -0,0 +1,833 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Email Triage OpenEnv - Interactive Dashboard</title>
8
+ <style>
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
17
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
18
+ min-height: 100vh;
19
+ padding: 20px;
20
+ }
21
+
22
+ .container {
23
+ max-width: 1400px;
24
+ margin: 0 auto;
25
+ }
26
+
27
+ header {
28
+ text-align: center;
29
+ color: white;
30
+ margin-bottom: 30px;
31
+ }
32
+
33
+ header h1 {
34
+ font-size: 2.5em;
35
+ margin-bottom: 10px;
36
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
37
+ }
38
+
39
+ header p {
40
+ font-size: 1.1em;
41
+ opacity: 0.9;
42
+ }
43
+
44
+ .main-layout {
45
+ display: grid;
46
+ grid-template-columns: 1fr 2fr 1fr;
47
+ gap: 20px;
48
+ margin-bottom: 20px;
49
+ }
50
+
51
+ .panel {
52
+ background: white;
53
+ border-radius: 12px;
54
+ padding: 20px;
55
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
56
+ display: flex;
57
+ flex-direction: column;
58
+ }
59
+
60
+ .panel h2 {
61
+ color: #333;
62
+ font-size: 1.3em;
63
+ margin-bottom: 15px;
64
+ padding-bottom: 10px;
65
+ border-bottom: 2px solid #667eea;
66
+ }
67
+
68
+ .panel h3 {
69
+ color: #555;
70
+ font-size: 1em;
71
+ margin-top: 15px;
72
+ margin-bottom: 10px;
73
+ }
74
+
75
+ .task-selector {
76
+ display: flex;
77
+ flex-direction: column;
78
+ gap: 10px;
79
+ }
80
+
81
+ .task-btn {
82
+ padding: 12px;
83
+ border: 2px solid #ddd;
84
+ border-radius: 8px;
85
+ background: white;
86
+ cursor: pointer;
87
+ font-size: 0.95em;
88
+ transition: all 0.3s;
89
+ text-align: left;
90
+ }
91
+
92
+ .task-btn:hover {
93
+ border-color: #667eea;
94
+ background: #f0f4ff;
95
+ }
96
+
97
+ .task-btn.active {
98
+ background: #667eea;
99
+ color: white;
100
+ border-color: #667eea;
101
+ }
102
+
103
+ .task-btn-title {
104
+ font-weight: bold;
105
+ display: block;
106
+ margin-bottom: 5px;
107
+ }
108
+
109
+ .task-btn-desc {
110
+ font-size: 0.85em;
111
+ opacity: 0.8;
112
+ }
113
+
114
+ .email-display {
115
+ background: #f9f9f9;
116
+ border-left: 4px solid #667eea;
117
+ padding: 15px;
118
+ border-radius: 8px;
119
+ margin-bottom: 15px;
120
+ min-height: 200px;
121
+ }
122
+
123
+ .email-header {
124
+ display: grid;
125
+ grid-template-columns: 1fr 1fr;
126
+ gap: 15px;
127
+ margin-bottom: 15px;
128
+ font-size: 0.9em;
129
+ }
130
+
131
+ .email-field {
132
+ display: flex;
133
+ flex-direction: column;
134
+ }
135
+
136
+ .email-label {
137
+ color: #666;
138
+ font-weight: bold;
139
+ font-size: 0.85em;
140
+ margin-bottom: 5px;
141
+ }
142
+
143
+ .email-value {
144
+ color: #333;
145
+ padding: 8px;
146
+ background: white;
147
+ border-radius: 4px;
148
+ }
149
+
150
+ .email-subject {
151
+ font-weight: bold;
152
+ margin-bottom: 10px;
153
+ color: #333;
154
+ }
155
+
156
+ .email-body {
157
+ background: white;
158
+ padding: 12px;
159
+ border-radius: 4px;
160
+ line-height: 1.5;
161
+ color: #555;
162
+ max-height: 300px;
163
+ overflow-y: auto;
164
+ }
165
+
166
+ .form-group {
167
+ margin-bottom: 15px;
168
+ }
169
+
170
+ .form-group label {
171
+ display: block;
172
+ margin-bottom: 8px;
173
+ color: #333;
174
+ font-weight: bold;
175
+ font-size: 0.95em;
176
+ }
177
+
178
+ select,
179
+ input {
180
+ width: 100%;
181
+ padding: 10px;
182
+ border: 1px solid #ddd;
183
+ border-radius: 6px;
184
+ font-size: 0.95em;
185
+ font-family: inherit;
186
+ }
187
+
188
+ select:focus,
189
+ input:focus {
190
+ outline: none;
191
+ border-color: #667eea;
192
+ box-shadow: 0 0 5px rgba(102, 126, 234, 0.3);
193
+ }
194
+
195
+ .button-group {
196
+ display: grid;
197
+ grid-template-columns: 1fr 1fr;
198
+ gap: 10px;
199
+ margin-top: 15px;
200
+ }
201
+
202
+ button {
203
+ padding: 12px;
204
+ border: none;
205
+ border-radius: 6px;
206
+ font-size: 0.95em;
207
+ font-weight: bold;
208
+ cursor: pointer;
209
+ transition: all 0.3s;
210
+ }
211
+
212
+ .btn-primary {
213
+ background: #667eea;
214
+ color: white;
215
+ }
216
+
217
+ .btn-primary:hover:not(:disabled) {
218
+ background: #5568d3;
219
+ transform: translateY(-2px);
220
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
221
+ }
222
+
223
+ .btn-primary:disabled {
224
+ background: #ccc;
225
+ cursor: not-allowed;
226
+ }
227
+
228
+ .btn-secondary {
229
+ background: #f0f0f0;
230
+ color: #333;
231
+ border: 1px solid #ddd;
232
+ }
233
+
234
+ .btn-secondary:hover:not(:disabled) {
235
+ background: #e0e0e0;
236
+ }
237
+
238
+ .btn-secondary:disabled {
239
+ opacity: 0.5;
240
+ cursor: not-allowed;
241
+ }
242
+
243
+ .stats-panel {
244
+ display: grid;
245
+ grid-template-columns: 1fr;
246
+ gap: 10px;
247
+ }
248
+
249
+ .stat-box {
250
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
251
+ color: white;
252
+ padding: 15px;
253
+ border-radius: 8px;
254
+ text-align: center;
255
+ }
256
+
257
+ .stat-label {
258
+ font-size: 0.9em;
259
+ opacity: 0.9;
260
+ margin-bottom: 5px;
261
+ }
262
+
263
+ .stat-value {
264
+ font-size: 1.8em;
265
+ font-weight: bold;
266
+ }
267
+
268
+ .results-area {
269
+ background: #f0f8ff;
270
+ border: 2px solid #667eea;
271
+ padding: 15px;
272
+ border-radius: 8px;
273
+ margin-top: 15px;
274
+ min-height: 100px;
275
+ }
276
+
277
+ .result-item {
278
+ padding: 10px;
279
+ margin-bottom: 8px;
280
+ background: white;
281
+ border-left: 4px solid #667eea;
282
+ border-radius: 4px;
283
+ }
284
+
285
+ .result-label {
286
+ font-weight: bold;
287
+ color: #667eea;
288
+ font-size: 0.9em;
289
+ }
290
+
291
+ .result-value {
292
+ color: #333;
293
+ margin-top: 5px;
294
+ }
295
+
296
+ .reward-high {
297
+ color: #28a745;
298
+ font-weight: bold;
299
+ }
300
+
301
+ .reward-low {
302
+ color: #dc3545;
303
+ font-weight: bold;
304
+ }
305
+
306
+ .progress-bar {
307
+ width: 100%;
308
+ height: 8px;
309
+ background: #e0e0e0;
310
+ border-radius: 4px;
311
+ overflow: hidden;
312
+ margin-top: 10px;
313
+ }
314
+
315
+ .progress-fill {
316
+ height: 100%;
317
+ background: linear-gradient(90deg, #667eea, #764ba2);
318
+ transition: width 0.3s;
319
+ }
320
+
321
+ .status-message {
322
+ padding: 12px;
323
+ border-radius: 6px;
324
+ margin-bottom: 15px;
325
+ font-size: 0.95em;
326
+ }
327
+
328
+ .status-info {
329
+ background: #e7f3ff;
330
+ color: #00396b;
331
+ border: 1px solid #667eea;
332
+ }
333
+
334
+ .status-success {
335
+ background: #d4edda;
336
+ color: #155724;
337
+ border: 1px solid #28a745;
338
+ }
339
+
340
+ .status-error {
341
+ background: #f8d7da;
342
+ color: #721c24;
343
+ border: 1px solid #dc3545;
344
+ }
345
+
346
+ .status-warning {
347
+ background: #fff3cd;
348
+ color: #856404;
349
+ border: 1px solid #ffc107;
350
+ }
351
+
352
+ .history-table {
353
+ width: 100%;
354
+ border-collapse: collapse;
355
+ margin-top: 10px;
356
+ font-size: 0.85em;
357
+ }
358
+
359
+ .history-table th {
360
+ background: #667eea;
361
+ color: white;
362
+ padding: 8px;
363
+ text-align: left;
364
+ }
365
+
366
+ .history-table td {
367
+ padding: 8px;
368
+ border-bottom: 1px solid #ddd;
369
+ }
370
+
371
+ .history-table tr:hover {
372
+ background: #f5f5f5;
373
+ }
374
+
375
+ .task-complete {
376
+ background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
377
+ color: white;
378
+ padding: 20px;
379
+ border-radius: 8px;
380
+ text-align: center;
381
+ }
382
+
383
+ .task-complete h3 {
384
+ font-size: 1.5em;
385
+ margin-bottom: 10px;
386
+ color: white;
387
+ }
388
+
389
+ .idle-message {
390
+ text-align: center;
391
+ padding: 30px;
392
+ color: #999;
393
+ }
394
+
395
+ @media (max-width: 1200px) {
396
+ .main-layout {
397
+ grid-template-columns: 1fr;
398
+ }
399
+
400
+ header h1 {
401
+ font-size: 1.8em;
402
+ }
403
+ }
404
+
405
+ .loader {
406
+ display: inline-block;
407
+ width: 20px;
408
+ height: 20px;
409
+ border: 3px solid #f3f3f3;
410
+ border-top: 3px solid #667eea;
411
+ border-radius: 50%;
412
+ animation: spin 1s linear infinite;
413
+ margin-right: 10px;
414
+ }
415
+
416
+ @keyframes spin {
417
+ 0% {
418
+ transform: rotate(0deg);
419
+ }
420
+
421
+ 100% {
422
+ transform: rotate(360deg);
423
+ }
424
+ }
425
+ </style>
426
+ </head>
427
+
428
+ <body>
429
+ <div class="container">
430
+ <header>
431
+ <h1>📧 Email Triage OpenEnv</h1>
432
+ <p>Interactive Dashboard - Test & Evaluate Email Classification Tasks</p>
433
+ </header>
434
+
435
+ <div id="statusMessage"></div>
436
+
437
+ <div class="main-layout">
438
+ <!-- Left Panel: Task Selection & Controls -->
439
+ <div class="panel">
440
+ <h2>📋 Tasks</h2>
441
+
442
+ <div class="task-selector">
443
+ <button class="task-btn active" onclick="selectTask('spam_detection')">
444
+ <span class="task-btn-title">Task 1: Spam Detection</span>
445
+ <span class="task-btn-desc">Easy - 10 emails</span>
446
+ </button>
447
+ <button class="task-btn" onclick="selectTask('multi_class_routing')">
448
+ <span class="task-btn-title">Task 2: Multi-Class Routing</span>
449
+ <span class="task-btn-desc">Medium - 12 emails</span>
450
+ </button>
451
+ <button class="task-btn" onclick="selectTask('context_aware_triage')">
452
+ <span class="task-btn-title">Task 3: Context-Aware Triage</span>
453
+ <span class="task-btn-desc">Hard - 20 emails</span>
454
+ </button>
455
+ </div>
456
+
457
+ <h3 style="margin-top: 25px;">⚙️ Controls</h3>
458
+ <button class="btn-primary" style="width: 100%; margin-bottom: 10px;" onclick="resetTask()">
459
+ 🔄 Reset Task
460
+ </button>
461
+ <button class="btn-secondary" style="width: 100%;" onclick="loadTaskInfo()">
462
+ ℹ️ Task Info
463
+ </button>
464
+
465
+ <h3 style="margin-top: 25px;">📊 Statistics</h3>
466
+ <div class="stats-panel">
467
+ <div class="stat-box">
468
+ <div class="stat-label">Current Step</div>
469
+ <div class="stat-value" id="statStep">0</div>
470
+ </div>
471
+ <div class="stat-box">
472
+ <div class="stat-label">Total Reward</div>
473
+ <div class="stat-value" id="statReward">0.00</div>
474
+ </div>
475
+ <div class="stat-box">
476
+ <div class="stat-label">Average Reward</div>
477
+ <div class="stat-value" id="statAvg">0.00</div>
478
+ </div>
479
+ <div class="stat-box">
480
+ <div class="stat-label">Final Score</div>
481
+ <div class="stat-value" id="statScore">-</div>
482
+ </div>
483
+ </div>
484
+ </div>
485
+
486
+ <!-- Middle Panel: Email Display & Classification Form -->
487
+ <div class="panel">
488
+ <h2>✉️ Email Classification</h2>
489
+
490
+ <div id="emailContainer">
491
+ <div class="idle-message">
492
+ <p>Click "Reset Task" to start</p>
493
+ </div>
494
+ </div>
495
+
496
+ <div id="formContainer" style="display: none;">
497
+ <div class="form-group">
498
+ <label for="classification">📌 Classification:</label>
499
+ <select id="classification" onchange="updatePriorities()">
500
+ <option value="">-- Select Classification --</option>
501
+ <option value="spam">🚫 Spam</option>
502
+ <option value="normal">📄 Normal</option>
503
+ <option value="urgent">⚡ Urgent</option>
504
+ <option value="billing">💳 Billing</option>
505
+ </select>
506
+ </div>
507
+
508
+ <div class="form-group">
509
+ <label for="team">🏢 Route to Team:</label>
510
+ <select id="team">
511
+ <option value="none">🚫 None</option>
512
+ <option value="support">🆘 Support</option>
513
+ <option value="sales">💼 Sales</option>
514
+ <option value="billing">💰 Billing</option>
515
+ </select>
516
+ </div>
517
+
518
+ <div class="form-group">
519
+ <label for="priority">⭐ Priority (0-3):</label>
520
+ <select id="priority">
521
+ <option value="0">0 - Low</option>
522
+ <option value="1" selected>1 - Medium</option>
523
+ <option value="2">2 - High</option>
524
+ <option value="3">3 - Critical</option>
525
+ </select>
526
+ </div>
527
+
528
+ <div class="button-group">
529
+ <button class="btn-primary" onclick="submitAction()">✓ Submit</button>
530
+ <button class="btn-secondary" onclick="resetTask()">⟲ Reset</button>
531
+ </div>
532
+
533
+ <div id="resultArea" class="results-area" style="display: none;">
534
+ <div id="resultContent"></div>
535
+ <div class="progress-bar">
536
+ <div class="progress-fill" id="progressFill"></div>
537
+ </div>
538
+ </div>
539
+ </div>
540
+ </div>
541
+
542
+ <!-- Right Panel: History & Results -->
543
+ <div class="panel">
544
+ <h2>📝 History & Results</h2>
545
+
546
+ <div id="historyContainer">
547
+ <div class="idle-message">
548
+ <p>History will appear here</p>
549
+ </div>
550
+ </div>
551
+
552
+ <div id="completeMessage" style="display: none;">
553
+ <div class="task-complete">
554
+ <h3>✓ Task Complete!</h3>
555
+ <p>Final Score: <span id="finalScore">0.00</span></p>
556
+ <p>Steps Taken: <span id="finalSteps">0</span></p>
557
+ <p style="margin-top: 15px; font-size: 0.9em;">Click "Reset Task" to try again or select another
558
+ task</p>
559
+ </div>
560
+ </div>
561
+ </div>
562
+ </div>
563
+ </div>
564
+
565
+ <script>
566
+ let currentTask = 'spam_detection';
567
+ let currentState = null;
568
+ let history = [];
569
+ let totalReward = 0;
570
+ let taskDone = false;
571
+
572
+ const taskDescriptions = {
573
+ 'spam_detection': {
574
+ title: 'Spam Detection (Easy)',
575
+ desc: 'Classify 10 emails as spam or legitimate. High accuracy expected.',
576
+ steps: 10,
577
+ categories: ['spam', 'normal']
578
+ },
579
+ 'multi_class_routing': {
580
+ title: 'Multi-Class Routing (Medium)',
581
+ desc: 'Classify 12 emails into 4 categories and route to appropriate teams.',
582
+ steps: 12,
583
+ categories: ['spam', 'normal', 'urgent', 'billing']
584
+ },
585
+ 'context_aware_triage': {
586
+ title: 'Context-Aware Triage (Hard)',
587
+ desc: 'Handle 20 emails with VIP flags, SLAs, and complex context.',
588
+ steps: 20,
589
+ categories: ['spam', 'normal', 'urgent', 'billing']
590
+ }
591
+ };
592
+
593
+ function showStatus(message, type = 'info') {
594
+ const statusEl = document.getElementById('statusMessage');
595
+ statusEl.className = `status-message status-${type}`;
596
+ statusEl.innerHTML = message;
597
+ statusEl.style.display = 'block';
598
+ setTimeout(() => {
599
+ statusEl.style.display = 'none';
600
+ }, 5000);
601
+ }
602
+
603
+ async function selectTask(taskName) {
604
+ currentTask = taskName;
605
+ document.querySelectorAll('.task-btn').forEach(btn => btn.classList.remove('active'));
606
+ event.target.closest('.task-btn').classList.add('active');
607
+ loadTaskInfo();
608
+ showStatus(`Selected: ${taskDescriptions[taskName].title}`, 'info');
609
+ }
610
+
611
+ function loadTaskInfo() {
612
+ const task = taskDescriptions[currentTask];
613
+ showStatus(`<strong>${task.title}</strong><br>${task.desc}`, 'info');
614
+ }
615
+
616
+ async function resetTask() {
617
+ try {
618
+ showStatus('🔄 Resetting task...', 'info');
619
+ const response = await fetch(`/reset?task=${currentTask}`, {
620
+ method: 'POST'
621
+ });
622
+ const data = await response.json();
623
+
624
+ currentState = data.observation;
625
+ history = [];
626
+ totalReward = 0;
627
+ taskDone = false;
628
+
629
+ document.getElementById('statStep').textContent = '0';
630
+ document.getElementById('statReward').textContent = '0.00';
631
+ document.getElementById('statAvg').textContent = '0.00';
632
+ document.getElementById('statScore').textContent = '-';
633
+ document.getElementById('completeMessage').style.display = 'none';
634
+ document.getElementById('formContainer').style.display = 'block';
635
+
636
+ displayEmail();
637
+ updateHistory();
638
+ showStatus(`✓ Task reset! Ready to classify emails.`, 'success');
639
+ } catch (error) {
640
+ showStatus(`Error resetting task: ${error.message}`, 'error');
641
+ }
642
+ }
643
+
644
+ function displayEmail() {
645
+ const email = currentState.current_email;
646
+ const emailHTML = `
647
+ <div class="email-display">
648
+ <div class="email-subject">Subject: ${email.subject}</div>
649
+ <div class="email-header">
650
+ <div class="email-field">
651
+ <span class="email-label">From:</span>
652
+ <span class="email-value">${email.sender_domain}</span>
653
+ </div>
654
+ <div class="email-field">
655
+ <span class="email-label">VIP Sender:</span>
656
+ <span class="email-value">${email.is_vip_sender ? '✓ Yes' : '✗ No'}</span>
657
+ </div>
658
+ </div>
659
+ <div class="email-header" style="margin-bottom: 15px;">
660
+ <div class="email-field">
661
+ <span class="email-label">SLA Hours:</span>
662
+ <span class="email-value">${email.sla_hours || 'N/A'}</span>
663
+ </div>
664
+ <div class="email-field">
665
+ <span class="email-label">Timestamp:</span>
666
+ <span class="email-value">${new Date(email.timestamp).toLocaleString()}</span>
667
+ </div>
668
+ </div>
669
+ <div class="email-body">${email.body.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div>
670
+ </div>
671
+ `;
672
+ document.getElementById('emailContainer').innerHTML = emailHTML;
673
+ }
674
+
675
+ function updatePriorities() {
676
+ const classification = document.getElementById('classification').value;
677
+ updateTeamOptions();
678
+ }
679
+
680
+ function updateTeamOptions() {
681
+ const classification = document.getElementById('classification').value;
682
+ const teamSelect = document.getElementById('team');
683
+
684
+ if (classification === 'spam') {
685
+ teamSelect.value = 'none';
686
+ }
687
+ }
688
+
689
+ async function submitAction() {
690
+ const classification = document.getElementById('classification').value;
691
+ const team = document.getElementById('team').value;
692
+ const priority = parseInt(document.getElementById('priority').value);
693
+
694
+ if (!classification) {
695
+ showStatus('Please select a classification', 'warning');
696
+ return;
697
+ }
698
+
699
+ try {
700
+ const response = await fetch(`/step?task=${currentTask}`, {
701
+ method: 'POST',
702
+ headers: {
703
+ 'Content-Type': 'application/json'
704
+ },
705
+ body: JSON.stringify({
706
+ classification: classification,
707
+ team: team,
708
+ priority: priority
709
+ })
710
+ });
711
+
712
+ const data = await response.json();
713
+ currentState = data.observation;
714
+ totalReward += data.reward.value;
715
+ taskDone = data.done;
716
+
717
+ const stepNum = history.length + 1;
718
+ history.push({
719
+ step: stepNum,
720
+ email_subject: currentState.current_email.subject.substring(0, 30),
721
+ classification: classification,
722
+ team: team,
723
+ priority: priority,
724
+ reward: data.reward.value,
725
+ done: taskDone
726
+ });
727
+
728
+ updateStats();
729
+ displayResults(data);
730
+ updateHistory();
731
+
732
+ if (taskDone) {
733
+ showTaskComplete();
734
+ } else {
735
+ setTimeout(() => {
736
+ displayEmail();
737
+ document.getElementById('classification').value = '';
738
+ document.getElementById('team').value = 'none';
739
+ document.getElementById('priority').value = '1';
740
+ document.getElementById('resultArea').style.display = 'none';
741
+ }, 1500);
742
+ }
743
+
744
+ showStatus(`✓ Step ${stepNum} submitted!`, 'success');
745
+ } catch (error) {
746
+ showStatus(`Error: ${error.message}`, 'error');
747
+ }
748
+ }
749
+
750
+ function updateStats() {
751
+ const stepNum = history.length;
752
+ const avgReward = stepNum > 0 ? (totalReward / stepNum) : 0;
753
+
754
+ document.getElementById('statStep').textContent = stepNum.toString();
755
+ document.getElementById('statReward').textContent = totalReward.toFixed(2);
756
+ document.getElementById('statAvg').textContent = avgReward.toFixed(2);
757
+
758
+ const progressPercent = (stepNum / taskDescriptions[currentTask].steps) * 100;
759
+ document.getElementById('progressFill').style.width = progressPercent + '%';
760
+ }
761
+
762
+ function displayResults(data) {
763
+ const resultHTML = `
764
+ <div class="result-item">
765
+ <span class="result-label">Reward:</span>
766
+ <span class="result-value ${data.reward.value >= 0.7 ? 'reward-high' : 'reward-low'}">
767
+ ${data.reward.value.toFixed(3)}
768
+ </span>
769
+ </div>
770
+ <div class="result-item">
771
+ <span class="result-label">Breakdown:</span>
772
+ <span class="result-value">${JSON.stringify(data.reward.breakdown).replace(/[{}]/g, '')}</span>
773
+ </div>
774
+ <div class="result-item">
775
+ <span class="result-label">Status:</span>
776
+ <span class="result-value">${data.done ? '✓ Task Complete' : '▶ In Progress'}</span>
777
+ </div>
778
+ `;
779
+ document.getElementById('resultContent').innerHTML = resultHTML;
780
+ document.getElementById('resultArea').style.display = 'block';
781
+ }
782
+
783
+ function updateHistory() {
784
+ if (history.length === 0) {
785
+ document.getElementById('historyContainer').innerHTML = '<div class="idle-message"><p>No actions yet</p></div>';
786
+ return;
787
+ }
788
+
789
+ const tableHTML = `
790
+ <table class="history-table">
791
+ <thead>
792
+ <tr>
793
+ <th>Step</th>
794
+ <th>Classification</th>
795
+ <th>Team</th>
796
+ <th>Priority</th>
797
+ <th>Reward</th>
798
+ </tr>
799
+ </thead>
800
+ <tbody>
801
+ ${history.map(h => `
802
+ <tr>
803
+ <td>${h.step}</td>
804
+ <td>${h.classification}</td>
805
+ <td>${h.team}</td>
806
+ <td>${h.priority}</td>
807
+ <td>${h.reward.toFixed(3)}</td>
808
+ </tr>
809
+ `).join('')}
810
+ </tbody>
811
+ </table>
812
+ `;
813
+ document.getElementById('historyContainer').innerHTML = tableHTML;
814
+ }
815
+
816
+ function showTaskComplete() {
817
+ const finalScore = (totalReward / history.length).toFixed(3);
818
+ document.getElementById('finalScore').textContent = finalScore;
819
+ document.getElementById('finalSteps').textContent = history.length;
820
+ document.getElementById('completeMessage').style.display = 'block';
821
+ document.getElementById('statScore').textContent = finalScore;
822
+ document.getElementById('formContainer').style.display = 'none';
823
+ showStatus(`🎉 Task complete! Final score: ${finalScore}`, 'success');
824
+ }
825
+
826
+ // Initialize on load
827
+ window.addEventListener('load', () => {
828
+ loadTaskInfo();
829
+ });
830
+ </script>
831
+ </body>
832
+
833
+ </html>
uv.lock ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version = 4
2
+ requires-python = ">=3.11"
3
+
4
+ [[package]]
5
+ name = "openenv-core"
6
+ version = "0.2.0"
7
+ source = { type = "registry", url = "https://pypi.org/simple" }
8
+ dependencies = [
9
+ { name = "pydantic", specifier = ">=2.0" },
10
+ { name = "pyyaml", specifier = ">=6.0" },
11
+ ]
12
+
13
+ [[package]]
14
+ name = "pydantic"
15
+ version = "2.5.0"
16
+ source = { type = "registry", url = "https://pypi.org/simple" }
17
+ dependencies = [
18
+ { name = "pydantic-core", specifier = "==2.14.1" },
19
+ { name = "typing-extensions", specifier = "!=4.7.0,>=4.6.1" },
20
+ ]
21
+
22
+ [[package]]
23
+ name = "pydantic-core"
24
+ version = "2.14.1"
25
+ source = { type = "registry", url = "https://pypi.org/simple" }
26
+
27
+ [[package]]
28
+ name = "typing-extensions"
29
+ version = "4.8.0"
30
+ source = { type = "registry", url = "https://pypi.org/simple" }
31
+
32
+ [[package]]
33
+ name = "flask"
34
+ version = "3.0.0"
35
+ source = { type = "registry", url = "https://pypi.org/simple" }
36
+ dependencies = [
37
+ { name = "Werkzeug", specifier = ">=2.3.0" },
38
+ { name = "Jinja2", specifier = ">=3.0" },
39
+ { name = "itsdangerous", specifier = ">=2.1.2" },
40
+ { name = "click", specifier = ">=8.1.3" },
41
+ ]
42
+
43
+ [[package]]
44
+ name = "Werkzeug"
45
+ version = "3.0.0"
46
+ source = { type = "registry", url = "https://pypi.org/simple" }
47
+
48
+ [[package]]
49
+ name = "Jinja2"
50
+ version = "3.1.2"
51
+ source = { type = "registry", url = "https://pypi.org/simple" }
52
+ dependencies = [
53
+ { name = "MarkupSafe", specifier = ">=2.0" },
54
+ ]
55
+
56
+ [[package]]
57
+ name = "MarkupSafe"
58
+ version = "2.1.3"
59
+ source = { type = "registry", url = "https://pypi.org/simple" }
60
+
61
+ [[package]]
62
+ name = "itsdangerous"
63
+ version = "2.1.2"
64
+ source = { type = "registry", url = "https://pypi.org/simple" }
65
+
66
+ [[package]]
67
+ name = "click"
68
+ version = "8.1.7"
69
+ source = { type = "registry", url = "https://pypi.org/simple" }
70
+
71
+ [[package]]
72
+ name = "openai"
73
+ version = "1.3.0"
74
+ source = { type = "registry", url = "https://pypi.org/simple" }
75
+ dependencies = [
76
+ { name = "requests", specifier = ">=2.20" },
77
+ { name = "httpx", specifier = ">=0.23.0" },
78
+ { name = "pydantic", specifier = ">=1.9.0" },
79
+ ]
80
+
81
+ [[package]]
82
+ name = "requests"
83
+ version = "2.31.0"
84
+ source = { type = "registry", url = "https://pypi.org/simple" }
85
+ dependencies = [
86
+ { name = "charset-normalizer", specifier = ">=2,<4" },
87
+ { name = "idna", specifier = ">=2.5,<4" },
88
+ { name = "urllib3", specifier = ">=1.21.1,<3" },
89
+ ]
90
+
91
+ [[package]]
92
+ name = "charset-normalizer"
93
+ version = "3.3.2"
94
+ source = { type = "registry", url = "https://pypi.org/simple" }
95
+
96
+ [[package]]
97
+ name = "idna"
98
+ version = "3.6"
99
+ source = { type = "registry", url = "https://pypi.org/simple" }
100
+
101
+ [[package]]
102
+ name = "urllib3"
103
+ version = "2.1.0"
104
+ source = { type = "registry", url = "https://pypi.org/simple" }
105
+
106
+ [[package]]
107
+ name = "httpx"
108
+ version = "0.25.2"
109
+ source = { type = "registry", url = "https://pypi.org/simple" }
110
+ dependencies = [
111
+ { name = "httpcore", specifier = ">=1.0.0,<2.0.0" },
112
+ { name = "sniffio", specifier = "" },
113
+ ]
114
+
115
+ [[package]]
116
+ name = "httpcore"
117
+ version = "1.0.2"
118
+ source = { type = "registry", url = "https://pypi.org/simple" }
119
+ dependencies = [
120
+ { name = "certifi", specifier = "" },
121
+ { name = "h11", specifier = ">=0.13,<0.15" },
122
+ ]
123
+
124
+ [[package]]
125
+ name = "certifi"
126
+ version = "2023.7.22"
127
+ source = { type = "registry", url = "https://pypi.org/simple" }
128
+
129
+ [[package]]
130
+ name = "h11"
131
+ version = "0.14.0"
132
+ source = { type = "registry", url = "https://pypi.org/simple" }
133
+
134
+ [[package]]
135
+ name = "sniffio"
136
+ version = "1.3.0"
137
+ source = { type = "registry", url = "https://pypi.org/simple" }
138
+
139
+ [[package]]
140
+ name = "python-dotenv"
141
+ version = "1.0.0"
142
+ source = { type = "registry", url = "https://pypi.org/simple" }
143
+
144
+ [[package]]
145
+ name = "pyyaml"
146
+ version = "6.0"
147
+ source = { type = "registry", url = "https://pypi.org/simple" }