Imaginephoenix commited on
Commit
b409c37
·
verified ·
1 Parent(s): 990f01b

Delete server.py

Browse files
Files changed (1) hide show
  1. server.py +0 -621
server.py DELETED
@@ -1,621 +0,0 @@
1
- """Flask server wrapper for the OpenEnv email triage environment."""
2
-
3
- import os
4
-
5
- from flask import Flask, Response, jsonify, request
6
-
7
- from environment import EmailTriageEnv
8
- from tasks import get_task_scenario_count, list_task_ids
9
-
10
- FRONTEND_HTML = """<!doctype html>
11
- <html lang="en">
12
- <head>
13
- <meta charset="utf-8" />
14
- <meta name="viewport" content="width=device-width, initial-scale=1" />
15
- <title>Inbox Helper Practice</title>
16
- <style>
17
- @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap');
18
-
19
- :root {
20
- --bg: #f5f1e9;
21
- --paper: #fffaf2;
22
- --ink: #102433;
23
- --accent: #ea6a2a;
24
- --accent-soft: #ffd6bf;
25
- --line: #d7cabb;
26
- --ok: #0f7b6c;
27
- --warn: #9a3a12;
28
- --radius: 14px;
29
- }
30
-
31
- * { box-sizing: border-box; }
32
-
33
- body {
34
- margin: 0;
35
- font-family: 'Space Grotesk', sans-serif;
36
- color: var(--ink);
37
- background:
38
- radial-gradient(1100px 460px at -10% -20%, #f2bc9f 0%, transparent 60%),
39
- radial-gradient(1100px 520px at 120% 115%, #b8d7cf 0%, transparent 62%),
40
- var(--bg);
41
- min-height: 100vh;
42
- }
43
-
44
- .wrap {
45
- max-width: 1100px;
46
- margin: 28px auto;
47
- padding: 0 16px;
48
- animation: reveal .45s ease-out;
49
- }
50
-
51
- @keyframes reveal {
52
- from { opacity: 0; transform: translateY(10px); }
53
- to { opacity: 1; transform: translateY(0); }
54
- }
55
-
56
- .title {
57
- display: flex;
58
- justify-content: space-between;
59
- align-items: baseline;
60
- gap: 14px;
61
- margin-bottom: 14px;
62
- }
63
-
64
- h1 {
65
- margin: 0;
66
- font-size: clamp(1.5rem, 2vw, 2.2rem);
67
- letter-spacing: .4px;
68
- }
69
-
70
- .subtitle {
71
- margin: 6px 0 0;
72
- font-size: .95rem;
73
- opacity: .8;
74
- }
75
-
76
- .badge {
77
- background: var(--accent-soft);
78
- border: 1px solid #f2b693;
79
- color: #7f2e0b;
80
- padding: 6px 10px;
81
- border-radius: 999px;
82
- font-size: .85rem;
83
- font-weight: 600;
84
- }
85
-
86
- .grid {
87
- display: grid;
88
- grid-template-columns: 1fr;
89
- gap: 14px;
90
- }
91
-
92
- @media (min-width: 900px) {
93
- .grid { grid-template-columns: 1fr 1fr; }
94
- .wide { grid-column: span 2; }
95
- }
96
-
97
- .card {
98
- background: var(--paper);
99
- border: 1px solid var(--line);
100
- border-radius: var(--radius);
101
- padding: 14px;
102
- box-shadow: 0 8px 28px rgba(16, 36, 51, 0.08);
103
- }
104
-
105
- .card h2 {
106
- margin: 0 0 10px;
107
- font-size: 1rem;
108
- text-transform: uppercase;
109
- letter-spacing: .08em;
110
- opacity: .86;
111
- }
112
-
113
- .row {
114
- display: flex;
115
- flex-wrap: wrap;
116
- gap: 8px;
117
- align-items: center;
118
- margin-bottom: 10px;
119
- }
120
-
121
- select, input, textarea, button {
122
- font-family: inherit;
123
- font-size: .95rem;
124
- }
125
-
126
- select, input, textarea {
127
- width: 100%;
128
- border: 1px solid #cdbba6;
129
- border-radius: 10px;
130
- padding: 9px 10px;
131
- background: #fff;
132
- color: var(--ink);
133
- }
134
-
135
- textarea {
136
- min-height: 92px;
137
- resize: vertical;
138
- }
139
-
140
- button {
141
- border: 0;
142
- border-radius: 10px;
143
- padding: 9px 12px;
144
- font-weight: 700;
145
- background: var(--ink);
146
- color: #fff;
147
- cursor: pointer;
148
- transition: transform .12s ease, opacity .12s ease;
149
- }
150
-
151
- button.secondary {
152
- background: #285066;
153
- }
154
-
155
- button.accent {
156
- background: var(--accent);
157
- }
158
-
159
- button:hover { transform: translateY(-1px); }
160
- button:active { transform: translateY(0); opacity: .92; }
161
-
162
- .status {
163
- padding: 8px 10px;
164
- border-radius: 10px;
165
- background: #eef7f5;
166
- border: 1px solid #c7e4de;
167
- color: var(--ok);
168
- font-weight: 600;
169
- min-height: 40px;
170
- display: flex;
171
- align-items: center;
172
- }
173
-
174
- .status.error {
175
- background: #fff1ea;
176
- border-color: #ffc8ae;
177
- color: var(--warn);
178
- }
179
-
180
- pre {
181
- margin: 0;
182
- white-space: pre-wrap;
183
- background: #0f1b24;
184
- color: #d9efe9;
185
- border-radius: 10px;
186
- padding: 12px;
187
- max-height: 340px;
188
- overflow: auto;
189
- font-family: 'IBM Plex Mono', monospace;
190
- font-size: .85rem;
191
- border: 1px solid #21313f;
192
- }
193
-
194
- .email-block {
195
- background: #fff;
196
- border: 1px solid #d9ccbc;
197
- border-radius: 10px;
198
- padding: 12px;
199
- }
200
-
201
- .email-row {
202
- margin-bottom: 8px;
203
- font-size: .95rem;
204
- line-height: 1.35;
205
- }
206
-
207
- .email-row strong {
208
- display: inline-block;
209
- min-width: 66px;
210
- }
211
-
212
- .help {
213
- margin: 0 0 10px;
214
- font-size: .9rem;
215
- opacity: .8;
216
- }
217
- </style>
218
- </head>
219
- <body>
220
- <div class="wrap">
221
- <div class="title">
222
- <div>
223
- <h1>Inbox Helper Practice</h1>
224
- <p class="subtitle">Practice deciding priority, category, and who should handle each email.</p>
225
- </div>
226
- <span class="badge" id="badge">connecting...</span>
227
- </div>
228
-
229
- <div class="grid">
230
- <section class="card">
231
- <h2>Start a Scenario</h2>
232
- <p class="help">Pick a difficulty, then click Start.</p>
233
- <div class="row">
234
- <select id="taskId">
235
- <option value="task_easy">Easy: one clear email</option>
236
- <option value="task_medium">Medium: mixed inbox</option>
237
- <option value="task_hard">Hard: high-risk complaint</option>
238
- <option value="task_production">Production: full inbox simulator</option>
239
- </select>
240
- </div>
241
- <div id="productionControls" style="display:none;">
242
- <div class="row">
243
- <select id="productionProfile">
244
- <option value="light">Workload: Light</option>
245
- <option value="standard" selected>Workload: Standard</option>
246
- <option value="heavy">Workload: Heavy</option>
247
- </select>
248
- </div>
249
- <div class="row">
250
- <select id="businessHoursMode">
251
- <option value="false" selected>Time Profile: 24x7 inbox</option>
252
- <option value="true">Time Profile: business hours focus</option>
253
- </select>
254
- </div>
255
- <div class="row">
256
- <select id="escalationMode">
257
- <option value="low">Escalation: Low</option>
258
- <option value="normal" selected>Escalation: Normal</option>
259
- <option value="high">Escalation: High</option>
260
- </select>
261
- </div>
262
- </div>
263
- <div class="row">
264
- <button class="accent" id="btnReset">Start</button>
265
- <button class="secondary" id="btnState">Check Progress</button>
266
- </div>
267
- <div class="status" id="status">Ready. Start a scenario.</div>
268
- </section>
269
-
270
- <section class="card">
271
- <h2>Your Decision</h2>
272
- <p class="help">Choose priority, who should handle it, and a short reason.</p>
273
- <div class="row">
274
- <select id="label">
275
- <option value="urgent">Urgent</option>
276
- <option value="normal" selected>Normal</option>
277
- <option value="spam">Spam</option>
278
- <option value="archive">Archive</option>
279
- </select>
280
- </div>
281
- <div class="row">
282
- <input id="routeTo" placeholder="Who should handle this? (billing, safety, engineering, support)" value="general" />
283
- </div>
284
- <div class="row">
285
- <textarea id="summary" placeholder="Write one clear sentence with key clues from the email.">Needs review.</textarea>
286
- </div>
287
- <div class="row">
288
- <button id="btnStep">Send Decision</button>
289
- </div>
290
- </section>
291
-
292
- <section class="card wide">
293
- <h2>Current Email</h2>
294
- <div class="email-block">
295
- <div class="email-row"><strong>Subject:</strong> <span id="mailSubject">No email loaded yet.</span></div>
296
- <div class="email-row"><strong>From:</strong> <span id="mailSender">-</span></div>
297
- <div class="email-row"><strong>Message:</strong> <span id="mailBody">Start a scenario to load an email.</span></div>
298
- </div>
299
- </section>
300
-
301
- <section class="card wide">
302
- <h2>Details (Advanced)</h2>
303
- <pre id="output">Waiting for your first action...</pre>
304
- </section>
305
- </div>
306
- </div>
307
-
308
- <script>
309
- const statusEl = document.getElementById('status');
310
- const badgeEl = document.getElementById('badge');
311
- const outEl = document.getElementById('output');
312
- const mailSubjectEl = document.getElementById('mailSubject');
313
- const mailSenderEl = document.getElementById('mailSender');
314
- const mailBodyEl = document.getElementById('mailBody');
315
- const taskIdEl = document.getElementById('taskId');
316
- const productionControlsEl = document.getElementById('productionControls');
317
-
318
- function setStatus(msg, isError = false) {
319
- statusEl.textContent = msg;
320
- statusEl.classList.toggle('error', isError);
321
- }
322
-
323
- function writeOutput(value) {
324
- outEl.textContent = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
325
- }
326
-
327
- function updateEmailPanel(data) {
328
- if (!data || !data.observation) {
329
- return;
330
- }
331
- const obs = data.observation;
332
- mailSubjectEl.textContent = obs.subject || 'No subject';
333
- mailSenderEl.textContent = obs.sender || '-';
334
- mailBodyEl.textContent = obs.body || '';
335
- }
336
-
337
- function updateProductionControlsVisibility() {
338
- const isProduction = taskIdEl.value === 'task_production';
339
- productionControlsEl.style.display = isProduction ? 'block' : 'none';
340
- }
341
-
342
- async function postJson(path, payload) {
343
- const response = await fetch(path, {
344
- method: 'POST',
345
- headers: { 'Content-Type': 'application/json' },
346
- body: JSON.stringify(payload || {}),
347
- });
348
- const text = await response.text();
349
- let data = text;
350
- try { data = JSON.parse(text); } catch (e) {}
351
- if (!response.ok) {
352
- throw new Error('HTTP ' + response.status + ' - ' + text);
353
- }
354
- return data;
355
- }
356
-
357
- async function warmup() {
358
- try {
359
- const res = await fetch('/meta');
360
- const data = await res.json();
361
- badgeEl.textContent = data.status === 'ok' ? 'ready' : 'check service';
362
- } catch (e) {
363
- badgeEl.textContent = 'offline';
364
- }
365
- }
366
-
367
- document.getElementById('btnReset').addEventListener('click', async () => {
368
- const taskId = taskIdEl.value;
369
- setStatus('Starting a new scenario...');
370
- try {
371
- const payload = { task_id: taskId };
372
- if (taskId === 'task_production') {
373
- payload.production_profile = document.getElementById('productionProfile').value;
374
- payload.business_hours_mode = document.getElementById('businessHoursMode').value === 'true';
375
- payload.escalation_mode = document.getElementById('escalationMode').value;
376
- }
377
- const data = await postJson('/reset', payload);
378
- setStatus('Scenario started. Read the email below.');
379
- updateEmailPanel(data);
380
- writeOutput(data);
381
- } catch (e) {
382
- setStatus('Could not start scenario. See details below.', true);
383
- writeOutput(String(e));
384
- }
385
- });
386
-
387
- document.getElementById('btnState').addEventListener('click', async () => {
388
- setStatus('Checking progress...');
389
- try {
390
- const data = await postJson('/state', {});
391
- setStatus('Progress updated.');
392
- writeOutput(data);
393
- } catch (e) {
394
- setStatus('Could not fetch progress. See details below.', true);
395
- writeOutput(String(e));
396
- }
397
- });
398
-
399
- document.getElementById('btnStep').addEventListener('click', async () => {
400
- const payload = {
401
- label: document.getElementById('label').value,
402
- summary: document.getElementById('summary').value,
403
- route_to: document.getElementById('routeTo').value,
404
- };
405
- setStatus('Sending your decision...');
406
- try {
407
- const data = await postJson('/step', payload);
408
- setStatus('Decision saved.');
409
- updateEmailPanel(data);
410
- writeOutput(data);
411
- } catch (e) {
412
- setStatus('Could not submit decision. See details below.', true);
413
- writeOutput(String(e));
414
- }
415
- });
416
-
417
- taskIdEl.addEventListener('change', updateProductionControlsVisibility);
418
-
419
- updateProductionControlsVisibility();
420
- warmup();
421
- </script>
422
- </body>
423
- </html>
424
- """
425
-
426
- app = Flask(__name__)
427
- current_env = EmailTriageEnv(task_id="task_easy")
428
- SCENARIO_COUNTERS = {task_id: 0 for task_id in list_task_ids()}
429
- DEFAULT_EVAL_SPLIT = os.getenv("OPENENV_EVAL_SPLIT", "public")
430
- ALLOW_CLIENT_EVAL_OVERRIDE = (
431
- os.getenv("OPENENV_ALLOW_CLIENT_EVAL_OVERRIDE", "false").strip().lower() == "true"
432
- )
433
-
434
-
435
- @app.get("/")
436
- def root_page():
437
- """Render a lightweight frontend for interacting with the environment."""
438
- return Response(FRONTEND_HTML, mimetype="text/html")
439
-
440
-
441
- @app.get("/meta")
442
- def root_endpoint():
443
- """Return service metadata for health checks and machine clients."""
444
- return jsonify(
445
- {
446
- "name": "email-triage-env",
447
- "status": "ok",
448
- "endpoints": {
449
- "reset": {"method": "POST", "path": "/reset"},
450
- "step": {"method": "POST", "path": "/step"},
451
- "state": {"method": "POST", "path": "/state"},
452
- },
453
- "scenario_pools": {
454
- "public": {
455
- task_id: get_task_scenario_count(task_id, "public")
456
- for task_id in list_task_ids()
457
- },
458
- },
459
- "eval_split": DEFAULT_EVAL_SPLIT,
460
- "production_runtime_controls": {
461
- "production_profile": ["light", "standard", "heavy"],
462
- "business_hours_mode": [True, False],
463
- "escalation_mode": ["low", "normal", "high"],
464
- },
465
- }
466
- )
467
-
468
-
469
- @app.post("/reset")
470
- def reset_endpoint():
471
- """Reset the environment with a selected task and return ResetResult JSON.
472
-
473
- Returns:
474
- Flask response containing reset payload.
475
- """
476
- global current_env
477
- global SCENARIO_COUNTERS
478
-
479
- payload = request.get_json(silent=True)
480
- if payload is None:
481
- payload = {}
482
- elif not isinstance(payload, dict):
483
- return jsonify({"error": "Malformed JSON payload."}), 400
484
-
485
- task_id = payload.get("task_id", "task_easy")
486
- if not isinstance(task_id, str):
487
- return jsonify({"error": "Field 'task_id' must be a string."}), 400
488
-
489
- runtime_options: dict[str, object] = {}
490
- if task_id == "task_production":
491
- production_profile = payload.get("production_profile", "standard")
492
- if not isinstance(production_profile, str) or production_profile not in {
493
- "light",
494
- "standard",
495
- "heavy",
496
- }:
497
- return (
498
- jsonify(
499
- {
500
- "error": (
501
- "Field 'production_profile' must be one of "
502
- "light/standard/heavy."
503
- )
504
- }
505
- ),
506
- 400,
507
- )
508
-
509
- escalation_mode = payload.get("escalation_mode", "normal")
510
- if not isinstance(escalation_mode, str) or escalation_mode not in {
511
- "low",
512
- "normal",
513
- "high",
514
- }:
515
- return (
516
- jsonify(
517
- {
518
- "error": (
519
- "Field 'escalation_mode' must be one of "
520
- "low/normal/high."
521
- )
522
- }
523
- ),
524
- 400,
525
- )
526
-
527
- business_hours_mode = payload.get("business_hours_mode", False)
528
- if isinstance(business_hours_mode, str):
529
- business_hours_mode = business_hours_mode.strip().lower() in {
530
- "1",
531
- "true",
532
- "yes",
533
- "on",
534
- }
535
- elif not isinstance(business_hours_mode, bool):
536
- return jsonify({"error": "Field 'business_hours_mode' must be boolean."}), 400
537
-
538
- runtime_options = {
539
- "production_profile": production_profile,
540
- "business_hours_mode": business_hours_mode,
541
- "escalation_mode": escalation_mode,
542
- }
543
-
544
- if not ALLOW_CLIENT_EVAL_OVERRIDE and (
545
- "eval_split" in payload or "scenario_index" in payload
546
- ):
547
- return jsonify(
548
- {
549
- "error": (
550
- "Client overrides for eval_split/scenario_index are disabled "
551
- "by server policy."
552
- )
553
- }
554
- ), 400
555
-
556
- eval_split = DEFAULT_EVAL_SPLIT
557
- if ALLOW_CLIENT_EVAL_OVERRIDE:
558
- requested_split = payload.get("eval_split", DEFAULT_EVAL_SPLIT)
559
- if not isinstance(requested_split, str):
560
- return jsonify({"error": "Field 'eval_split' must be a string."}), 400
561
- eval_split = requested_split
562
-
563
- requested_index = payload.get("scenario_index") if ALLOW_CLIENT_EVAL_OVERRIDE else None
564
- if requested_index is not None and (not isinstance(requested_index, int) or requested_index < 0):
565
- return jsonify({"error": "Field 'scenario_index' must be a non-negative integer."}), 400
566
-
567
- try:
568
- scenario_count = get_task_scenario_count(task_id, eval_split)
569
- if requested_index is None:
570
- scenario_index = SCENARIO_COUNTERS.get(task_id, 0)
571
- if scenario_count > 0:
572
- SCENARIO_COUNTERS[task_id] = (scenario_index + 1) % scenario_count
573
- else:
574
- scenario_index = requested_index
575
-
576
- current_env = EmailTriageEnv(
577
- task_id=task_id,
578
- scenario_index=scenario_index,
579
- split=eval_split,
580
- runtime_options=runtime_options,
581
- )
582
- reset_result = current_env.reset()
583
- except KeyError as error:
584
- return jsonify({"error": str(error)}), 400
585
-
586
- return jsonify(reset_result.model_dump())
587
-
588
-
589
- @app.post("/step")
590
- def step_endpoint():
591
- """Advance environment by one action and return StepResult JSON.
592
-
593
- Returns:
594
- Flask response containing step payload.
595
- """
596
- payload = request.get_json(silent=True)
597
- if payload is None:
598
- return jsonify({"error": "Malformed JSON payload."}), 400
599
-
600
- step_result = current_env.step(payload)
601
- return jsonify(step_result.model_dump())
602
-
603
-
604
- @app.post("/state")
605
- def state_endpoint():
606
- """Return read-only EnvironmentState JSON snapshot.
607
-
608
- Returns:
609
- Flask response containing state payload.
610
- """
611
- state_result = current_env.state()
612
- return jsonify(state_result.model_dump())
613
-
614
-
615
- def main() -> None:
616
- """Run the Flask app for local and script-based launches."""
617
- app.run(host="0.0.0.0", port=7860)
618
-
619
-
620
- if __name__ == "__main__":
621
- main()