5ivatej commited on
Commit
d7a2f52
·
1 Parent(s): 8aca5b2

Serve interactive UI at the Space homepage

Browse files
Files changed (1) hide show
  1. server/app.py +352 -3
server/app.py CHANGED
@@ -23,6 +23,7 @@ import zlib
23
 
24
  import uvicorn
25
  from fastapi import FastAPI, HTTPException, Request, Response
 
26
 
27
  from src.env import ESCEnv
28
  from src.models import ResetRequest, StepRequest
@@ -39,6 +40,342 @@ app = FastAPI(
39
 
40
  SESSION_COOKIE = "esc_session_id"
41
  SESSION_SECRET = os.getenv("ESC_SESSION_SECRET", "esc-openenv-dev-secret").encode("utf-8")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
 
44
  def _urlsafe_b64encode(data: bytes) -> str:
@@ -85,16 +422,28 @@ def _get_env_for_request(request: Request) -> ESCEnv:
85
  return _decode_env(token)
86
 
87
 
88
- @app.get("/")
89
- def root() -> dict:
90
  return {
91
  "name": "emotional-support-conversations",
92
  "version": "0.1.0",
93
- "endpoints": ["/reset", "/step", "/state", "/tasks"],
94
  "tasks": [t["id"] for t in ESCEnv.list_tasks()],
95
  }
96
 
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  @app.get("/tasks")
99
  def list_tasks() -> dict:
100
  return {"tasks": ESCEnv.list_tasks()}
 
23
 
24
  import uvicorn
25
  from fastapi import FastAPI, HTTPException, Request, Response
26
+ from fastapi.responses import HTMLResponse
27
 
28
  from src.env import ESCEnv
29
  from src.models import ResetRequest, StepRequest
 
40
 
41
  SESSION_COOKIE = "esc_session_id"
42
  SESSION_SECRET = os.getenv("ESC_SESSION_SECRET", "esc-openenv-dev-secret").encode("utf-8")
43
+ UI_HTML = """<!doctype html>
44
+ <html lang="en">
45
+ <head>
46
+ <meta charset="utf-8">
47
+ <meta name="viewport" content="width=device-width, initial-scale=1">
48
+ <title>Emotional Support Conversations</title>
49
+ <style>
50
+ :root {
51
+ --bg: #f3efe7;
52
+ --panel: #fffaf4;
53
+ --ink: #1c1c1c;
54
+ --muted: #665f57;
55
+ --line: #ddd3c5;
56
+ --accent: #1f6f5f;
57
+ --accent-2: #d98d3c;
58
+ --seeker: #efe2d1;
59
+ --agent: #dceddf;
60
+ --error: #9e2b25;
61
+ }
62
+ * { box-sizing: border-box; }
63
+ body {
64
+ margin: 0;
65
+ font-family: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
66
+ color: var(--ink);
67
+ background:
68
+ radial-gradient(circle at top left, rgba(217,141,60,0.14), transparent 28%),
69
+ radial-gradient(circle at top right, rgba(31,111,95,0.16), transparent 30%),
70
+ linear-gradient(180deg, #f7f2ea 0%, var(--bg) 100%);
71
+ }
72
+ .shell {
73
+ max-width: 1100px;
74
+ margin: 0 auto;
75
+ padding: 24px 16px 40px;
76
+ }
77
+ .hero {
78
+ margin-bottom: 18px;
79
+ }
80
+ h1 {
81
+ margin: 0 0 8px;
82
+ font-size: clamp(2rem, 4vw, 3rem);
83
+ line-height: 1;
84
+ }
85
+ .sub {
86
+ color: var(--muted);
87
+ max-width: 70ch;
88
+ margin: 0;
89
+ }
90
+ .layout {
91
+ display: grid;
92
+ grid-template-columns: 320px 1fr;
93
+ gap: 18px;
94
+ }
95
+ .panel {
96
+ background: rgba(255, 250, 244, 0.92);
97
+ border: 1px solid var(--line);
98
+ border-radius: 18px;
99
+ padding: 16px;
100
+ box-shadow: 0 12px 40px rgba(0,0,0,0.06);
101
+ backdrop-filter: blur(8px);
102
+ }
103
+ .stack { display: grid; gap: 12px; }
104
+ label {
105
+ display: block;
106
+ font-size: 0.92rem;
107
+ margin-bottom: 6px;
108
+ color: var(--muted);
109
+ }
110
+ select, textarea, button {
111
+ width: 100%;
112
+ border-radius: 12px;
113
+ border: 1px solid var(--line);
114
+ padding: 10px 12px;
115
+ font: inherit;
116
+ }
117
+ select, textarea {
118
+ background: #fffdf9;
119
+ color: var(--ink);
120
+ }
121
+ textarea {
122
+ min-height: 110px;
123
+ resize: vertical;
124
+ }
125
+ button {
126
+ background: var(--accent);
127
+ color: #fff;
128
+ border: 0;
129
+ cursor: pointer;
130
+ font-weight: 600;
131
+ }
132
+ button.secondary {
133
+ background: #efe7dc;
134
+ color: var(--ink);
135
+ border: 1px solid var(--line);
136
+ }
137
+ button:disabled {
138
+ opacity: 0.55;
139
+ cursor: not-allowed;
140
+ }
141
+ .meta {
142
+ display: flex;
143
+ flex-wrap: wrap;
144
+ gap: 8px;
145
+ }
146
+ .badge {
147
+ background: #f4eadf;
148
+ border: 1px solid var(--line);
149
+ color: var(--muted);
150
+ border-radius: 999px;
151
+ padding: 6px 10px;
152
+ font-size: 0.84rem;
153
+ }
154
+ .chat {
155
+ display: grid;
156
+ gap: 10px;
157
+ min-height: 420px;
158
+ max-height: 68vh;
159
+ overflow: auto;
160
+ padding-right: 4px;
161
+ }
162
+ .msg {
163
+ border-radius: 16px;
164
+ padding: 12px 14px;
165
+ border: 1px solid var(--line);
166
+ line-height: 1.45;
167
+ }
168
+ .msg small {
169
+ display: block;
170
+ margin-bottom: 6px;
171
+ color: var(--muted);
172
+ }
173
+ .msg.seeker { background: var(--seeker); }
174
+ .msg.agent { background: var(--agent); }
175
+ .status {
176
+ color: var(--muted);
177
+ min-height: 1.4em;
178
+ }
179
+ .status.error { color: var(--error); }
180
+ .reward {
181
+ font-variant-numeric: tabular-nums;
182
+ color: var(--accent);
183
+ font-weight: 700;
184
+ }
185
+ .footer {
186
+ margin-top: 8px;
187
+ color: var(--muted);
188
+ font-size: 0.9rem;
189
+ }
190
+ @media (max-width: 860px) {
191
+ .layout { grid-template-columns: 1fr; }
192
+ .chat { min-height: 320px; max-height: none; }
193
+ }
194
+ </style>
195
+ </head>
196
+ <body>
197
+ <div class="shell">
198
+ <div class="hero">
199
+ <h1>Emotional Support Conversations</h1>
200
+ <p class="sub">
201
+ Interactive browser playground for the deterministic OpenEnv benchmark.
202
+ The API stays unchanged; this page just calls <code>/tasks</code>,
203
+ <code>/reset</code>, <code>/step</code>, and <code>/state</code>.
204
+ </p>
205
+ </div>
206
+ <div class="layout">
207
+ <div class="panel stack">
208
+ <div>
209
+ <label for="taskSelect">Task</label>
210
+ <select id="taskSelect"></select>
211
+ </div>
212
+ <div class="meta" id="taskMeta"></div>
213
+ <button id="resetBtn">Start / Reset Episode</button>
214
+ <div>
215
+ <label for="messageInput">Your reply</label>
216
+ <textarea id="messageInput" placeholder="Type the agent response here..."></textarea>
217
+ </div>
218
+ <button id="stepBtn" disabled>Send Step</button>
219
+ <button id="stateBtn" class="secondary">Refresh Public State</button>
220
+ <div class="status" id="status"></div>
221
+ <div class="footer">
222
+ Tip: keep replies short, warm, and stage-aware. The hard task only
223
+ succeeds if you eventually include real-world safety support.
224
+ </div>
225
+ </div>
226
+ <div class="panel">
227
+ <div class="meta" id="episodeMeta"></div>
228
+ <div class="chat" id="chat"></div>
229
+ </div>
230
+ </div>
231
+ </div>
232
+ <script>
233
+ const taskSelect = document.getElementById("taskSelect");
234
+ const taskMeta = document.getElementById("taskMeta");
235
+ const episodeMeta = document.getElementById("episodeMeta");
236
+ const chat = document.getElementById("chat");
237
+ const statusEl = document.getElementById("status");
238
+ const messageInput = document.getElementById("messageInput");
239
+ const resetBtn = document.getElementById("resetBtn");
240
+ const stepBtn = document.getElementById("stepBtn");
241
+ const stateBtn = document.getElementById("stateBtn");
242
+
243
+ let currentDone = true;
244
+
245
+ function setStatus(text, isError = false) {
246
+ statusEl.textContent = text || "";
247
+ statusEl.className = isError ? "status error" : "status";
248
+ }
249
+
250
+ function renderBadge(label, value) {
251
+ return `<span class="badge"><strong>${label}:</strong> ${value}</span>`;
252
+ }
253
+
254
+ function renderEpisodeMeta(obs, reward = null, done = null) {
255
+ const parts = [
256
+ renderBadge("Task", obs.task_id),
257
+ renderBadge("Stage", obs.stage_hint),
258
+ renderBadge("Turn", obs.turn),
259
+ renderBadge("Remaining", obs.remaining_turns),
260
+ ];
261
+ if (reward !== null) parts.push(renderBadge("Reward", `<span class="reward">${reward.toFixed(2)}</span>`));
262
+ if (done !== null) parts.push(renderBadge("Done", done ? "true" : "false"));
263
+ episodeMeta.innerHTML = parts.join("");
264
+ }
265
+
266
+ function addMessage(role, text) {
267
+ const node = document.createElement("div");
268
+ node.className = `msg ${role}`;
269
+ node.innerHTML = `<small>${role === "agent" ? "Agent" : "Seeker"}</small>${text}`;
270
+ chat.appendChild(node);
271
+ chat.scrollTop = chat.scrollHeight;
272
+ }
273
+
274
+ async function loadTasks() {
275
+ const res = await fetch("/tasks");
276
+ const data = await res.json();
277
+ taskSelect.innerHTML = "";
278
+ data.tasks.forEach((task) => {
279
+ const option = document.createElement("option");
280
+ option.value = task.id;
281
+ option.textContent = `${task.id} (${task.difficulty})`;
282
+ option.dataset.meta = JSON.stringify(task);
283
+ taskSelect.appendChild(option);
284
+ });
285
+ updateTaskMeta();
286
+ }
287
+
288
+ function updateTaskMeta() {
289
+ const selected = taskSelect.options[taskSelect.selectedIndex];
290
+ if (!selected) return;
291
+ const task = JSON.parse(selected.dataset.meta);
292
+ taskMeta.innerHTML = [
293
+ renderBadge("Difficulty", task.difficulty),
294
+ renderBadge("Max turns", task.max_turns),
295
+ renderBadge("Success threshold", task.success_threshold),
296
+ ].join("");
297
+ }
298
+
299
+ async function resetEpisode() {
300
+ setStatus("Starting episode...");
301
+ chat.innerHTML = "";
302
+ const res = await fetch("/reset", {
303
+ method: "POST",
304
+ headers: { "Content-Type": "application/json" },
305
+ body: JSON.stringify({ task_id: taskSelect.value }),
306
+ });
307
+ const data = await res.json();
308
+ currentDone = false;
309
+ stepBtn.disabled = false;
310
+ renderEpisodeMeta(data.observation);
311
+ addMessage("seeker", data.observation.seeker_utterance);
312
+ setStatus(data.observation.scenario_brief);
313
+ }
314
+
315
+ async function sendStep() {
316
+ const message = messageInput.value.trim();
317
+ if (!message) {
318
+ setStatus("Write a reply before sending.", true);
319
+ return;
320
+ }
321
+ if (currentDone) {
322
+ setStatus("Episode is finished. Reset before sending another step.", true);
323
+ return;
324
+ }
325
+
326
+ stepBtn.disabled = true;
327
+ setStatus("Sending step...");
328
+ addMessage("agent", message);
329
+ const res = await fetch("/step", {
330
+ method: "POST",
331
+ headers: { "Content-Type": "application/json" },
332
+ body: JSON.stringify({ action: { message } }),
333
+ });
334
+
335
+ if (!res.ok) {
336
+ const err = await res.text();
337
+ setStatus(err, true);
338
+ stepBtn.disabled = false;
339
+ return;
340
+ }
341
+
342
+ const data = await res.json();
343
+ messageInput.value = "";
344
+ addMessage("seeker", data.observation.seeker_utterance);
345
+ renderEpisodeMeta(data.observation, data.reward, data.done);
346
+ currentDone = Boolean(data.done);
347
+ stepBtn.disabled = currentDone;
348
+ if (data.done) {
349
+ const final = data.info && data.info.final ? data.info.final : null;
350
+ if (final) {
351
+ setStatus(`Episode finished. score=${final.score.toFixed(3)} success=${final.success >= 1.0}`);
352
+ } else {
353
+ setStatus("Episode finished.");
354
+ }
355
+ } else {
356
+ setStatus(`Step accepted. reward=${data.reward.toFixed(2)}`);
357
+ }
358
+ }
359
+
360
+ async function refreshState() {
361
+ const res = await fetch("/state");
362
+ if (!res.ok) {
363
+ setStatus("No active episode yet. Reset first.", true);
364
+ return;
365
+ }
366
+ const data = await res.json();
367
+ setStatus(`Public state refreshed. turn=${data.turn} cumulative_reward=${data.cumulative_reward.toFixed(3)}`);
368
+ }
369
+
370
+ taskSelect.addEventListener("change", updateTaskMeta);
371
+ resetBtn.addEventListener("click", resetEpisode);
372
+ stepBtn.addEventListener("click", sendStep);
373
+ stateBtn.addEventListener("click", refreshState);
374
+ loadTasks().catch((err) => setStatus(String(err), true));
375
+ </script>
376
+ </body>
377
+ </html>
378
+ """
379
 
380
 
381
  def _urlsafe_b64encode(data: bytes) -> str:
 
422
  return _decode_env(token)
423
 
424
 
425
+ def _root_payload() -> dict:
 
426
  return {
427
  "name": "emotional-support-conversations",
428
  "version": "0.1.0",
429
+ "endpoints": ["/reset", "/step", "/state", "/tasks", "/ui"],
430
  "tasks": [t["id"] for t in ESCEnv.list_tasks()],
431
  }
432
 
433
 
434
+ @app.get("/")
435
+ def root(request: Request):
436
+ accept = request.headers.get("accept", "")
437
+ if "text/html" in accept:
438
+ return HTMLResponse(UI_HTML)
439
+ return _root_payload()
440
+
441
+
442
+ @app.get("/ui", response_class=HTMLResponse)
443
+ def ui() -> str:
444
+ return UI_HTML
445
+
446
+
447
  @app.get("/tasks")
448
  def list_tasks() -> dict:
449
  return {"tasks": ESCEnv.list_tasks()}