Addyk24 commited on
Commit
6e2f008
·
1 Parent(s): e99bc86

Added fastapi server for deployment space

Browse files
Files changed (2) hide show
  1. server/__init__.py +0 -0
  2. server/app.py +375 -0
server/__init__.py ADDED
File without changes
server/app.py ADDED
@@ -0,0 +1,375 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI server exposing the Project Polymath Workspace Environment.
3
+ Endpoints:
4
+ GET / — Command Center UI (HTML)
5
+ GET /docs — Interactive OpenAPI (Swagger UI)
6
+ GET /health — Liveness probe (JSON)
7
+ POST /reset — Start a new negotiation
8
+ POST /step — Apply an agent action
9
+ GET /state — Read current state without advancing
10
+ """
11
+
12
+ from __future__ import annotations
13
+ import os
14
+ import sys
15
+
16
+ # Ensure Python can find your 'envs' and 'models' folders
17
+ ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
18
+ if ROOT_DIR not in sys.path:
19
+ sys.path.insert(0, ROOT_DIR)
20
+
21
+ from typing import Any, Optional
22
+ from fastapi import FastAPI
23
+ from fastapi.responses import HTMLResponse
24
+ from pydantic import BaseModel, ConfigDict, Field
25
+ import uvicorn
26
+
27
+ # Project Polymath Imports
28
+ from envs.environment import WorkSpaceEnvironment
29
+ from models.schemas import WorkSpaceAction
30
+
31
+ # ── OpenAPI Documentation Setup ──────────────────────────────────────────────
32
+
33
+ _OPENAPI_TAGS = [
34
+ {
35
+ "name": "Environment",
36
+ "description": "Episode lifecycle for Polymath: call **reset** before **step**.",
37
+ },
38
+ {
39
+ "name": "Interface",
40
+ "description": "Browser UI for manual debugging (HTML, not JSON).",
41
+ }
42
+ ]
43
+
44
+ app = FastAPI(
45
+ title="Project Polymath: OpenEnv",
46
+ version="1.0.0",
47
+ openapi_tags=_OPENAPI_TAGS,
48
+ description=(
49
+ "Multi-agent negotiation environment. "
50
+ "Agent must balance constraints from Finance, Security, and UX."
51
+ ),
52
+ )
53
+
54
+ # Force the environment into the mode you want the judges to see by default
55
+ os.environ["BASELINE_ENV_MODE"] = "easy"
56
+ _env = WorkSpaceEnvironment()
57
+
58
+ # ── request / response schemas ───────────────────────────────────────────────
59
+
60
+ class ResetRequest(BaseModel):
61
+ """Start a new episode."""
62
+ model_config = ConfigDict(
63
+ json_schema_extra={
64
+ "examples": [{"topic": "Draft the new Mobile App PRD"}]
65
+ }
66
+ )
67
+ topic: str = Field(
68
+ default="Draft the new Mobile App PRD",
69
+ description="The core task the PM must complete."
70
+ )
71
+
72
+ class StepRequest(BaseModel):
73
+ """Apply one agent action."""
74
+ action: WorkSpaceAction = Field(
75
+ description="The agent's action payload."
76
+ )
77
+
78
+ class EnvResponse(BaseModel):
79
+ """Observation and reward after reset, step, or state."""
80
+ observation: dict[str, Any] | None = Field(description="Environment feedback and turn count.")
81
+ reward: float = Field(description="Reward for the last transition.")
82
+ done: bool = Field(description="True after the episode concludes.")
83
+ info: dict[str, Any] = Field(default_factory=dict)
84
+
85
+ def _format_obs(obs: Any) -> dict[str, Any] | None:
86
+ if obs is None:
87
+ return None
88
+ # obs is a WorkspaceObservation
89
+ return {
90
+ "feedback": getattr(obs, "feedback", "Episode Terminated."),
91
+ "current_turn": getattr(obs, "current_turn", 0)
92
+ }
93
+
94
+ # ── FRONTEND HTML/CSS/JS PAYLOAD ─────────────────────────────────────────────
95
+
96
+ _DEBUG_UI_HTML = """<!DOCTYPE html>
97
+ <html lang="en">
98
+ <head>
99
+ <meta charset="utf-8" />
100
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
101
+ <title>Polymath Command Center</title>
102
+ <style>
103
+ :root {
104
+ --bg: #0f172a; --surface: #1e293b; --border: #334155; --text: #f8fafc;
105
+ --muted: #94a3b8; --accent: #8b5cf6; --exec: #8b5cf6; --exec-hover: #7c3aed;
106
+ --card-blue: #3b82f6; --card-orange: #f97316; --card-green: #10b981; --card-grey: #475569;
107
+ --mono: ui-monospace, "Cascadia Code", monospace;
108
+ }
109
+ * { box-sizing: border-box; }
110
+ body { margin: 0; min-height: 100vh; font-family: system-ui, sans-serif; background: var(--bg); color: var(--text); }
111
+ .wrap { max-width: 60rem; margin: 0 auto; padding: 2rem 1rem; }
112
+
113
+ .page-head { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 1.5rem; }
114
+ h1 { font-size: 1.5rem; margin: 0 0 0.25rem; }
115
+ .sub { color: var(--muted); font-size: 0.9rem; margin: 0; }
116
+ .btn-mini { background: #fdfd96; color: #111; padding: 0.35rem 0.65rem; border-radius: 6px; text-decoration: none; font-weight: bold; font-size: 0.75rem; text-transform: uppercase; }
117
+
118
+ .panel { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem; margin-bottom: 1.25rem; }
119
+ .metrics { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 1.5rem; }
120
+ .metric { padding: 1rem; border-radius: 8px; color: #fff; box-shadow: 0 4px 6px rgba(0,0,0,0.3); border-top: 4px solid; background: #1e293b; }
121
+ .metric .m-label { font-size: 0.7rem; text-transform: uppercase; color: var(--muted); font-weight: bold; }
122
+ .metric .m-val { font-size: 1.5rem; font-weight: bold; margin-top: 0.25rem; }
123
+
124
+ label { font-size: 0.8rem; color: var(--muted); font-weight: bold; display: block; margin-bottom: 0.4rem; text-transform: uppercase; }
125
+ select, input, textarea { width: 100%; padding: 0.75rem; border-radius: 6px; border: 1px solid var(--border); background: #0f172a; color: var(--text); font-family: inherit; margin-bottom: 1rem;}
126
+ textarea { min-height: 80px; resize: vertical; }
127
+
128
+ .btn-row { display: flex; gap: 0.75rem; flex-wrap: wrap; margin-top: 0.5rem; }
129
+ button { padding: 0.75rem 1.25rem; border: none; border-radius: 6px; font-weight: bold; cursor: pointer; transition: 0.2s; text-transform: uppercase; font-size: 0.8rem; }
130
+ .btn-exec { background: var(--exec); color: #fff; flex: 2; }
131
+ .btn-exec:hover { background: var(--exec-hover); }
132
+ .btn-sec { background: var(--card-grey); color: #fff; flex: 1; }
133
+ .btn-sec:hover { background: #334155; }
134
+
135
+ .world-text { background: #0f172a; padding: 1rem; border-radius: 6px; border: 1px solid var(--border); line-height: 1.5; color: #e2e8f0; white-space: pre-wrap; font-family: var(--mono); font-size: 0.85rem;}
136
+
137
+ .timeline { max-height: 400px; overflow-y: auto; display: flex; flex-direction: column; gap: 0.75rem; padding-right: 0.5rem; }
138
+ .tl-item { background: #0f172a; border: 1px solid var(--border); padding: 1rem; border-radius: 6px; }
139
+ .tl-header { display: flex; justify-content: space-between; margin-bottom: 0.5rem; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem;}
140
+ .tl-reward { font-weight: bold; font-family: var(--mono); }
141
+ .tl-pos { color: #10b981; } .tl-neg { color: #ef4444; }
142
+ .tl-body { font-size: 0.85rem; line-height: 1.4; color: var(--muted); }
143
+ .tl-body strong { color: #e2e8f0; }
144
+
145
+ #toast { position: fixed; top: 1rem; right: 1rem; background: #ef4444; color: white; padding: 1rem; border-radius: 6px; display: none; z-index: 1000; box-shadow: 0 4px 12px rgba(0,0,0,0.5); }
146
+ </style>
147
+ </head>
148
+ <body>
149
+ <div id="toast"></div>
150
+ <div class="wrap">
151
+
152
+ <header class="page-head">
153
+ <div>
154
+ <h1>Project Polymath</h1>
155
+ <p class="sub">Multi-Agent Negotiation Environment • <a href="/health">/health</a></p>
156
+ </div>
157
+ <a class="btn-mini" href="/docs" target="_blank">Docs</a>
158
+ </header>
159
+
160
+ <div class="metrics">
161
+ <div class="metric" style="border-color: var(--card-blue);">
162
+ <div class="m-label">Step Reward</div><div class="m-val" id="mStep">0.00</div>
163
+ </div>
164
+ <div class="metric" style="border-color: var(--card-orange);">
165
+ <div class="m-label">Total Reward</div><div class="m-val" id="mTotal">0.00</div>
166
+ </div>
167
+ <div class="metric" style="border-color: var(--card-green);">
168
+ <div class="m-label">Turn Count</div><div class="m-val" id="mTurn">0</div>
169
+ </div>
170
+ <div class="metric" style="border-color: var(--card-grey);">
171
+ <div class="m-label">Status</div><div class="m-val" id="mStatus" style="font-size: 1.1rem; padding-top: 0.4rem;">STANDBY</div>
172
+ </div>
173
+ </div>
174
+
175
+ <div class="panel">
176
+ <label>Environment Feedback</label>
177
+ <div class="world-text" id="oppText">Initialize the environment to begin.</div>
178
+ </div>
179
+
180
+ <div class="panel">
181
+ <label>Topic / Seed</label>
182
+ <input type="text" id="iTopic" value="Draft the new Mobile App PRD">
183
+
184
+ <div style="display: flex; gap: 1rem;">
185
+ <div style="flex: 1;">
186
+ <label>Action Type</label>
187
+ <select id="iType" onchange="toggleTarget()">
188
+ <option value="message_expert">message_expert</option>
189
+ <option value="propose_draft">propose_draft</option>
190
+ <option value="submit_final">submit_final</option>
191
+ </select>
192
+ </div>
193
+ <div style="flex: 1;">
194
+ <label>Target</label>
195
+ <select id="iTarget">
196
+ <option value="Finance">Finance</option>
197
+ <option value="Security">Security</option>
198
+ <option value="UX">UX</option>
199
+ <option value="All">All</option>
200
+ </select>
201
+ </div>
202
+ </div>
203
+
204
+ <label>Content Payload</label>
205
+ <textarea id="iArg" placeholder="Message the expert or paste the PRD..."></textarea>
206
+
207
+ <div class="btn-row">
208
+ <button class="btn-exec" onclick="doStep()">Execute Step</button>
209
+ <button class="btn-sec" onclick="doState()">Refresh State</button>
210
+ <button class="btn-sec" onclick="doReset()">Reset Env</button>
211
+ </div>
212
+ </div>
213
+
214
+ <div class="panel">
215
+ <label>Action Timeline</label>
216
+ <div class="timeline" id="timeline">
217
+ <div style="color: var(--muted); text-align: center; padding: 2rem;">No actions yet. Click Reset to begin.</div>
218
+ </div>
219
+ </div>
220
+
221
+ </div>
222
+
223
+ <script>
224
+ let totalReward = 0;
225
+ let isDone = false;
226
+
227
+ const $ = id => document.getElementById(id);
228
+
229
+ function showToast(msg) {
230
+ const t = $('toast'); t.innerText = msg; t.style.display = 'block';
231
+ setTimeout(() => t.style.display = 'none', 4000);
232
+ }
233
+
234
+ function toggleTarget() {
235
+ const type = $('iType').value;
236
+ $('iTarget').disabled = (type === 'submit_final');
237
+ }
238
+
239
+ function updateUI(data, isReset = false) {
240
+ if(isReset) { totalReward = 0; $('timeline').innerHTML = ''; }
241
+
242
+ const obs = data.observation || {};
243
+ const reward = data.reward || 0;
244
+ totalReward += reward;
245
+ isDone = data.done;
246
+
247
+ $('mStep').innerText = reward.toFixed(2);
248
+ $('mTotal').innerText = totalReward.toFixed(2);
249
+ $('mTurn').innerText = obs.current_turn || 0;
250
+ $('mStatus').innerText = data.done ? "TERMINATED" : "ACTIVE";
251
+ $('oppText').innerText = obs.feedback || "Episode Terminated.";
252
+
253
+ if(data.action_taken) {
254
+ const colorCls = reward >= 0 ? "tl-pos" : "tl-neg";
255
+ const sign = reward > 0 ? "+" : "";
256
+ const html = `
257
+ <div class="tl-item">
258
+ <div class="tl-header">
259
+ <strong style="color: #fff;">${data.action_taken.action_type} &rarr; ${data.action_taken.target || "None"}</strong>
260
+ <span class="tl-reward ${colorCls}">${sign}${reward.toFixed(2)}</span>
261
+ </div>
262
+ <div class="tl-body">
263
+ <div><strong>Agent:</strong> ${data.action_taken.content}</div>
264
+ </div>
265
+ </div>`;
266
+ $('timeline').insertAdjacentHTML('beforeend', html);
267
+ $('timeline').scrollTop = $('timeline').scrollHeight;
268
+ }
269
+ }
270
+
271
+ async function apiCall(endpoint, payload) {
272
+ try {
273
+ const res = await fetch(endpoint, {
274
+ method: payload ? "POST" : "GET",
275
+ headers: { "Content-Type": "application/json" },
276
+ body: payload ? JSON.stringify(payload) : null
277
+ });
278
+ const data = await res.json();
279
+ if(!res.ok) throw new Error(data.detail || "API Error");
280
+ return data;
281
+ } catch (e) { showToast(e.message); throw e; }
282
+ }
283
+
284
+ async function doReset() {
285
+ const data = await apiCall("/reset", { topic: $('iTopic').value });
286
+ updateUI(data, true);
287
+ $('iArg').value = "";
288
+ }
289
+
290
+ async function doState() {
291
+ const data = await apiCall("/state");
292
+ updateUI(data);
293
+ }
294
+
295
+ async function doStep() {
296
+ if(isDone || $('mStatus').innerText === "STANDBY") {
297
+ showToast("Please Reset the environment first!"); return;
298
+ }
299
+
300
+ let targetVal = $('iTarget').value;
301
+ if ($('iType').value === "submit_final") targetVal = null;
302
+
303
+ const action = {
304
+ action_type: $('iType').value,
305
+ target: targetVal,
306
+ content: $('iArg').value
307
+ };
308
+
309
+ const data = await apiCall("/step", { action });
310
+ data.action_taken = action;
311
+ updateUI(data);
312
+ $('iArg').value = "";
313
+ }
314
+ </script>
315
+ </body>
316
+ </html>
317
+ """
318
+
319
+ # ── routes ───────────────────────────────────────────────────────────────────
320
+
321
+ @app.get("/", response_class=HTMLResponse, tags=["Interface"])
322
+ def ui() -> HTMLResponse:
323
+ """Browser debug UI (HTML)."""
324
+ return HTMLResponse(content=_DEBUG_UI_HTML)
325
+
326
+ @app.get("/health", tags=["System"])
327
+ async def health() -> dict[str, str]:
328
+ """Return `{"status": "ok"}` when the server is up."""
329
+ return {"status": "ok"}
330
+
331
+ @app.post("/reset", response_model=EnvResponse, tags=["Environment"])
332
+ async def reset(req: ResetRequest | None = None) -> dict[str, Any]:
333
+ if req is None:
334
+ req = ResetRequest()
335
+ obs = _env.reset(req.topic)
336
+ return {
337
+ "observation": _format_obs(obs),
338
+ "reward": 0.0,
339
+ "done": False,
340
+ "info": {}
341
+ }
342
+
343
+ @app.post("/step", response_model=EnvResponse, tags=["Environment"])
344
+ async def step(req: StepRequest) -> dict[str, Any]:
345
+ obs = _env.step(req.action)
346
+ return {
347
+ "observation": _format_obs(obs),
348
+ "reward": float(obs.reward),
349
+ "done": obs.done,
350
+ "info": {}
351
+ }
352
+
353
+ @app.get("/state", response_model=EnvResponse, tags=["Environment"])
354
+ async def state() -> dict[str, Any]:
355
+ try:
356
+ obs = _env._state # Direct access for API state check
357
+ if obs is None:
358
+ return {"observation": None, "reward": 0.0, "done": False, "info": {}}
359
+ return {
360
+ "observation": {
361
+ "feedback": "Current state fetched.",
362
+ "current_turn": obs.turn_count
363
+ },
364
+ "reward": 0.0,
365
+ "done": obs.is_done,
366
+ "info": {}
367
+ }
368
+ except Exception:
369
+ return {"observation": None, "reward": 0.0, "done": False, "info": {}}
370
+
371
+ def main() -> None:
372
+ uvicorn.run(app, host="0.0.0.0", port=7860)
373
+
374
+ if __name__ == "__main__":
375
+ main()