Spaces:
Sleeping
Sleeping
| """ | |
| FastAPI server exposing the Project Polymath Workspace Environment. | |
| Endpoints: | |
| GET / — Command Center UI (HTML) | |
| GET /docs — Interactive OpenAPI (Swagger UI) | |
| GET /health — Liveness probe (JSON) | |
| POST /reset — Start a new negotiation | |
| POST /step — Apply an agent action | |
| GET /state — Read current state without advancing | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import sys | |
| # Ensure Python can find your 'envs' and 'models' folders | |
| ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) | |
| if ROOT_DIR not in sys.path: | |
| sys.path.insert(0, ROOT_DIR) | |
| from typing import Any, Optional | |
| from fastapi import FastAPI | |
| from fastapi.responses import HTMLResponse | |
| from pydantic import BaseModel, ConfigDict, Field | |
| import uvicorn | |
| # Project Polymath Imports | |
| from envs.environment import WorkSpaceEnvironment | |
| from models.schemas import WorkSpaceAction | |
| # ── OpenAPI Documentation Setup ────────────────────────────────────────────── | |
| _OPENAPI_TAGS = [ | |
| { | |
| "name": "Environment", | |
| "description": "Episode lifecycle for Polymath: call **reset** before **step**.", | |
| }, | |
| { | |
| "name": "Interface", | |
| "description": "Browser UI for manual debugging (HTML, not JSON).", | |
| } | |
| ] | |
| app = FastAPI( | |
| title="Project Polymath: OpenEnv", | |
| version="1.0.0", | |
| openapi_tags=_OPENAPI_TAGS, | |
| description=( | |
| "Multi-agent negotiation environment. " | |
| "Agent must balance constraints from Finance, Security, and UX." | |
| ), | |
| ) | |
| # Force the environment into the mode you want the judges to see by default | |
| os.environ["BASELINE_ENV_MODE"] = "easy" | |
| _env = WorkSpaceEnvironment() | |
| # ── request / response schemas ─────────────────────────────────────────────── | |
| class ResetRequest(BaseModel): | |
| """Start a new episode.""" | |
| model_config = ConfigDict( | |
| json_schema_extra={ | |
| "examples": [{"topic": "Draft the new Mobile App PRD"}] | |
| } | |
| ) | |
| topic: str = Field( | |
| default="Draft the new Mobile App PRD", | |
| description="The core task the PM must complete." | |
| ) | |
| class StepRequest(BaseModel): | |
| """Apply one agent action.""" | |
| action: WorkSpaceAction = Field( | |
| description="The agent's action payload." | |
| ) | |
| class EnvResponse(BaseModel): | |
| """Observation and reward after reset, step, or state.""" | |
| observation: dict[str, Any] | None = Field(description="Environment feedback and turn count.") | |
| reward: float = Field(description="Reward for the last transition.") | |
| done: bool = Field(description="True after the episode concludes.") | |
| info: dict[str, Any] = Field(default_factory=dict) | |
| def _format_obs(obs: Any) -> dict[str, Any] | None: | |
| if obs is None: | |
| return None | |
| # obs is a WorkspaceObservation | |
| return { | |
| "feedback": getattr(obs, "feedback", "Episode Terminated."), | |
| "current_turn": getattr(obs, "current_turn", 0) | |
| } | |
| # ── FRONTEND HTML/CSS/JS PAYLOAD ───────────────────────────────────────────── | |
| _DEBUG_UI_HTML = """<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>Polymath Command Center</title> | |
| <style> | |
| :root { | |
| --bg: #0f172a; --surface: #1e293b; --border: #334155; --text: #f8fafc; | |
| --muted: #94a3b8; --accent: #8b5cf6; --exec: #8b5cf6; --exec-hover: #7c3aed; | |
| --card-blue: #3b82f6; --card-orange: #f97316; --card-green: #10b981; --card-grey: #475569; | |
| --mono: ui-monospace, "Cascadia Code", monospace; | |
| } | |
| * { box-sizing: border-box; } | |
| body { margin: 0; min-height: 100vh; font-family: system-ui, sans-serif; background: var(--bg); color: var(--text); } | |
| .wrap { max-width: 60rem; margin: 0 auto; padding: 2rem 1rem; } | |
| .page-head { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 1.5rem; } | |
| h1 { font-size: 1.5rem; margin: 0 0 0.25rem; } | |
| .sub { color: var(--muted); font-size: 0.9rem; margin: 0; } | |
| .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; } | |
| .panel { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem; margin-bottom: 1.25rem; } | |
| .metrics { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 1.5rem; } | |
| .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; } | |
| .metric .m-label { font-size: 0.7rem; text-transform: uppercase; color: var(--muted); font-weight: bold; } | |
| .metric .m-val { font-size: 1.5rem; font-weight: bold; margin-top: 0.25rem; } | |
| label { font-size: 0.8rem; color: var(--muted); font-weight: bold; display: block; margin-bottom: 0.4rem; text-transform: uppercase; } | |
| 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;} | |
| textarea { min-height: 80px; resize: vertical; } | |
| .btn-row { display: flex; gap: 0.75rem; flex-wrap: wrap; margin-top: 0.5rem; } | |
| 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; } | |
| .btn-exec { background: var(--exec); color: #fff; flex: 2; } | |
| .btn-exec:hover { background: var(--exec-hover); } | |
| .btn-sec { background: var(--card-grey); color: #fff; flex: 1; } | |
| .btn-sec:hover { background: #334155; } | |
| .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;} | |
| .timeline { max-height: 400px; overflow-y: auto; display: flex; flex-direction: column; gap: 0.75rem; padding-right: 0.5rem; } | |
| .tl-item { background: #0f172a; border: 1px solid var(--border); padding: 1rem; border-radius: 6px; } | |
| .tl-header { display: flex; justify-content: space-between; margin-bottom: 0.5rem; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem;} | |
| .tl-reward { font-weight: bold; font-family: var(--mono); } | |
| .tl-pos { color: #10b981; } .tl-neg { color: #ef4444; } | |
| .tl-body { font-size: 0.85rem; line-height: 1.4; color: var(--muted); } | |
| .tl-body strong { color: #e2e8f0; } | |
| #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); } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="toast"></div> | |
| <div class="wrap"> | |
| <header class="page-head"> | |
| <div> | |
| <h1>Project Polymath</h1> | |
| <p class="sub">Multi-Agent Negotiation Environment • <a href="/health">/health</a></p> | |
| </div> | |
| <a class="btn-mini" href="/docs" target="_blank">Docs</a> | |
| </header> | |
| <div class="metrics"> | |
| <div class="metric" style="border-color: var(--card-blue);"> | |
| <div class="m-label">Step Reward</div><div class="m-val" id="mStep">0.00</div> | |
| </div> | |
| <div class="metric" style="border-color: var(--card-orange);"> | |
| <div class="m-label">Total Reward</div><div class="m-val" id="mTotal">0.00</div> | |
| </div> | |
| <div class="metric" style="border-color: var(--card-green);"> | |
| <div class="m-label">Turn Count</div><div class="m-val" id="mTurn">0</div> | |
| </div> | |
| <div class="metric" style="border-color: var(--card-grey);"> | |
| <div class="m-label">Status</div><div class="m-val" id="mStatus" style="font-size: 1.1rem; padding-top: 0.4rem;">STANDBY</div> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <label>Environment Feedback</label> | |
| <div class="world-text" id="oppText">Initialize the environment to begin.</div> | |
| </div> | |
| <div class="panel"> | |
| <label>Topic / Seed</label> | |
| <input type="text" id="iTopic" value="Draft the new Mobile App PRD"> | |
| <div style="display: flex; gap: 1rem;"> | |
| <div style="flex: 1;"> | |
| <label>Action Type</label> | |
| <select id="iType" onchange="toggleTarget()"> | |
| <option value="message_expert">message_expert</option> | |
| <option value="propose_draft">propose_draft</option> | |
| <option value="submit_final">submit_final</option> | |
| </select> | |
| </div> | |
| <div style="flex: 1;"> | |
| <label>Target</label> | |
| <select id="iTarget"> | |
| <option value="Finance">Finance</option> | |
| <option value="Security">Security</option> | |
| <option value="UX">UX</option> | |
| <option value="All">All</option> | |
| </select> | |
| </div> | |
| </div> | |
| <label>Content Payload</label> | |
| <textarea id="iArg" placeholder="Message the expert or paste the PRD..."></textarea> | |
| <div class="btn-row"> | |
| <button class="btn-exec" onclick="doStep()">Execute Step</button> | |
| <button class="btn-sec" onclick="doState()">Refresh State</button> | |
| <button class="btn-sec" onclick="doReset()">Reset Env</button> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <label>Action Timeline</label> | |
| <div class="timeline" id="timeline"> | |
| <div style="color: var(--muted); text-align: center; padding: 2rem;">No actions yet. Click Reset to begin.</div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let totalReward = 0; | |
| let isDone = false; | |
| const $ = id => document.getElementById(id); | |
| function showToast(msg) { | |
| const t = $('toast'); t.innerText = msg; t.style.display = 'block'; | |
| setTimeout(() => t.style.display = 'none', 4000); | |
| } | |
| function toggleTarget() { | |
| const type = $('iType').value; | |
| $('iTarget').disabled = (type === 'submit_final'); | |
| } | |
| function updateUI(data, isReset = false) { | |
| if(isReset) { totalReward = 0; $('timeline').innerHTML = ''; } | |
| const obs = data.observation || {}; | |
| const reward = data.reward || 0; | |
| totalReward += reward; | |
| isDone = data.done; | |
| $('mStep').innerText = reward.toFixed(2); | |
| $('mTotal').innerText = totalReward.toFixed(2); | |
| $('mTurn').innerText = obs.current_turn || 0; | |
| $('mStatus').innerText = data.done ? "TERMINATED" : "ACTIVE"; | |
| $('oppText').innerText = obs.feedback || "Episode Terminated."; | |
| if(data.action_taken) { | |
| const colorCls = reward >= 0 ? "tl-pos" : "tl-neg"; | |
| const sign = reward > 0 ? "+" : ""; | |
| const html = ` | |
| <div class="tl-item"> | |
| <div class="tl-header"> | |
| <strong style="color: #fff;">${data.action_taken.action_type} → ${data.action_taken.target || "None"}</strong> | |
| <span class="tl-reward ${colorCls}">${sign}${reward.toFixed(2)}</span> | |
| </div> | |
| <div class="tl-body"> | |
| <div><strong>Agent:</strong> ${data.action_taken.content}</div> | |
| </div> | |
| </div>`; | |
| $('timeline').insertAdjacentHTML('beforeend', html); | |
| $('timeline').scrollTop = $('timeline').scrollHeight; | |
| } | |
| } | |
| async function apiCall(endpoint, payload) { | |
| try { | |
| const res = await fetch(endpoint, { | |
| method: payload ? "POST" : "GET", | |
| headers: { "Content-Type": "application/json" }, | |
| body: payload ? JSON.stringify(payload) : null | |
| }); | |
| const data = await res.json(); | |
| if(!res.ok) throw new Error(data.detail || "API Error"); | |
| return data; | |
| } catch (e) { showToast(e.message); throw e; } | |
| } | |
| async function doReset() { | |
| const data = await apiCall("/reset", { topic: $('iTopic').value }); | |
| updateUI(data, true); | |
| $('iArg').value = ""; | |
| } | |
| async function doState() { | |
| const data = await apiCall("/state"); | |
| updateUI(data); | |
| } | |
| async function doStep() { | |
| if(isDone || $('mStatus').innerText === "STANDBY") { | |
| showToast("Please Reset the environment first!"); return; | |
| } | |
| let targetVal = $('iTarget').value; | |
| if ($('iType').value === "submit_final") targetVal = null; | |
| const action = { | |
| action_type: $('iType').value, | |
| target: targetVal, | |
| content: $('iArg').value | |
| }; | |
| const data = await apiCall("/step", { action }); | |
| data.action_taken = action; | |
| updateUI(data); | |
| $('iArg').value = ""; | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| # ── routes ─────────────────────────────────────────────────────────────────── | |
| def ui() -> HTMLResponse: | |
| """Browser debug UI (HTML).""" | |
| return HTMLResponse(content=_DEBUG_UI_HTML) | |
| async def health() -> dict[str, str]: | |
| """Return `{"status": "ok"}` when the server is up.""" | |
| return {"status": "ok"} | |
| async def reset(req: ResetRequest | None = None) -> dict[str, Any]: | |
| if req is None: | |
| req = ResetRequest() | |
| obs = _env.reset(req.topic) | |
| return { | |
| "observation": _format_obs(obs), | |
| "reward": 0.0, | |
| "done": False, | |
| "info": {} | |
| } | |
| async def step(req: StepRequest) -> dict[str, Any]: | |
| obs = _env.step(req.action) | |
| return { | |
| "observation": _format_obs(obs), | |
| "reward": float(obs.reward), | |
| "done": obs.done, | |
| "info": {} | |
| } | |
| async def state() -> dict[str, Any]: | |
| try: | |
| obs = _env._state # Direct access for API state check | |
| if obs is None: | |
| return {"observation": None, "reward": 0.0, "done": False, "info": {}} | |
| return { | |
| "observation": { | |
| "feedback": "Current state fetched.", | |
| "current_turn": obs.turn_count | |
| }, | |
| "reward": 0.0, | |
| "done": obs.is_done, | |
| "info": {} | |
| } | |
| except Exception: | |
| return {"observation": None, "reward": 0.0, "done": False, "info": {}} | |
| def main() -> None: | |
| uvicorn.run(app, host="0.0.0.0", port=7860) | |
| if __name__ == "__main__": | |
| main() |