Spaces:
Sleeping
Sleeping
| # Visualizer β Game-engine contract | |
| This document describes what the React visualizer in `visualizer/` expects from | |
| the rest of the project. The viewer can render episodes from two sources: | |
| 1. **Live streaming** (default): the FastAPI server in | |
| `server/visualizer_server.py` runs LLM- or rule-based agents through the | |
| parliamentary loop and streams per-round records over Server-Sent Events. | |
| 2. **Static JSON files**: drag-and-drop a `.json` file (or click | |
| "Load episode JSON") with the shape described below. | |
| The two paths share the same per-round schema, so anything the live runner | |
| produces can be dropped onto disk and replayed later. | |
| --- | |
| ## 1. The episode JSON contract | |
| Top-level shape: | |
| ```jsonc | |
| { | |
| "episode_id": "string", | |
| "seed": 42, | |
| "policy": "string e.g. 'gpt-4o-mini' or 'PPO/checkpoint-12000'", | |
| "config": { | |
| "sectors": [ | |
| { "name": "Social", "full_name": "Social/Municipal", "baseline": 60 }, | |
| ... | |
| ], | |
| "pop_0": 1000000, | |
| "initial_treasury": 1000, | |
| "baseline_tax": 100, | |
| "productivity_bounds": [0.5, 2.0], | |
| "max_rounds": 50 | |
| }, | |
| "rounds": [ /* see Β§1.1 below */ ], | |
| "summary": { | |
| "rounds_survived": 50, | |
| "total_reward": 12345.6, | |
| "final_treasury": 2200, | |
| "final_prosperity": 0.0021, | |
| "final_productivity": 1.42, | |
| "final_population": 1080000, | |
| "termination_reason": "MAX_ROUNDS" | |
| } | |
| } | |
| ``` | |
| ### 1.1 Per-round shape | |
| Each entry in `rounds[]` is one full step (Phases 1β9 of the spec): | |
| ```jsonc | |
| { | |
| "round_num": 1, | |
| "year": 1, | |
| "quarter": 1, | |
| // Phase 1 β events | |
| "events": [ | |
| { | |
| "round": 1, | |
| "id": "war", | |
| "name": "War", | |
| "severity": 4, | |
| "affected_sectors": { "Defense": 2.5, "Agriculture": 1.3 }, | |
| "category": "moderate", | |
| "narrative": "Enemy forcesβ¦", | |
| "treasury_injection": 0, | |
| "is_positive": false | |
| } | |
| ], | |
| "crisis_occurred": false, | |
| "treasury_injection": 0, | |
| // Phase 2 β debate | |
| "debate": [ | |
| { "agent_id": "Health", "department": "Health", "message": "β¦" } | |
| ], | |
| // Phases 3β4 β proposals + voting | |
| "proposal_order": ["Defense", "Health", "Education", "Commerce", "Social", "Agriculture"], | |
| "proposals": [ | |
| { | |
| "proposal_id": "r1_Defense", | |
| "agent_id": "Defense", | |
| "department": "Defense", | |
| "amount": 320, | |
| "justification": "β¦", | |
| "status": "approved", | |
| "rejection_reason": null, | |
| "votes": { "Health": "YES", "Defense": "ABSTAIN", ... } | |
| } | |
| ], | |
| "votes": [ | |
| { | |
| "proposal_id": "r1_Defense", | |
| "agent_id": "Health", | |
| "department": "Health", | |
| "vote": "YES" | |
| } | |
| ], | |
| "vote_results": [ | |
| { | |
| "proposal_id": "r1_Defense", | |
| "department": "Defense", | |
| "amount": 320, | |
| "yes": 4, "no": 1, "abstain": 1, | |
| "status": "APPROVED" | |
| } | |
| ], | |
| // Phases 5β8 β execution + revenue + surplus | |
| "allocations": { "Defense": 320, "Health": 95, ... }, | |
| "consumptions": { "Defense": 250, "Health": 90, ... }, | |
| "revenues": { "Defense": 480, "Health": 130, ... }, | |
| "revenue_factors": { "Defense": 1.50, "Health": 1.36, ... }, | |
| "thresholds": { | |
| "Defense": { "critical": 100, "demand": 250, "surplus": 375, "wastage": 625 }, | |
| ... | |
| }, | |
| "event_multipliers": { "Defense": 2.5, "Agriculture": 1.3, ... }, | |
| "treasury_before": 1000.0, | |
| "treasury_after": 1180.4, | |
| "total_allocation": 740, | |
| "total_revenue": 1020.4, | |
| "surplus_returned": 60.0, | |
| "population": 1004000, | |
| "productivity": 1.04, | |
| "avg_revenue_factor":1.21, | |
| "prosperity": 0.00102, | |
| // Phase 9 β reward + termination | |
| "reward": { | |
| "base_reward": 0.00102, | |
| "productivity_bonus": 2.0, | |
| "survival_bonus": 10.0, | |
| "over_alloc_penalty": 0, | |
| "under_alloc_penalty": -10, | |
| "critical_penalty": 0, | |
| "total": 2.001 | |
| }, | |
| "cumulative_reward": 2.001, | |
| "done": false, | |
| "termination_reason": null, | |
| "critical_failure_in_budget": false | |
| } | |
| ``` | |
| ### 1.2 Validation rules the loader enforces | |
| `App.jsx::onLoadFile` requires: | |
| - `parsed.rounds` is an array (may be empty for a freshly-started live run). | |
| - `parsed.config.sectors` exists. | |
| Everything else is run through `src/utils/normalizeEpisode.js`, which derives | |
| the optional fields below from the spec formulas in | |
| `specification/04_ECONOMY_MODEL.md`. The engine's lean output is enough. | |
| ### 1.3 Required vs derived fields (per round) | |
| | Field | Required | If missing, derived as | | |
| |---|---|---| | |
| | `round_num` | β (or implied by index) | `index + 1` | | |
| | `treasury` *or* `treasury_after` | β | uses whichever is present | | |
| | `treasury_before` | β | previous round's `treasury_after` (`config.initial_treasury` for round 1) | | |
| | `allocations[s]`, `revenues[s]`, `revenue_factors[s]`, `consumptions[s]` | β | β | | |
| | `demands[s]` *or* `thresholds[s]` | β (one of) | thresholds derived as `{0.4, 1.0, 1.5, 2.5} Γ demand` (spec 04) | | |
| | `event_multipliers[s]` | β | aggregated multiplicatively from `events[].affected_sectors` | | |
| | `total_allocation`, `total_revenue`, `surplus_returned` | β | summed from per-sector dicts | | |
| | `prosperity` | β | `total_revenue / population` (spec 09 base reward) | | |
| | `avg_revenue_factor` | β | mean of `revenue_factors[s]` | | |
| | `cumulative_reward` | β | running sum of `reward.total` | | |
| | `events[].is_positive` | β | true iff `treasury_injection > 0` or `category == "positive"` or every multiplier `< 1` | | |
| | `events[].treasury_injection` | β | `0` | | |
| | `debate`, `proposal_order`, `proposals`, `votes`, `vote_results` | β | empty (panel renders gracefully) | | |
| | `done`, `termination_reason`, `reward.*` | β | falsy / zero | | |
| **Critical-failure semantics** (per `core/game.py::_terminate()` + spec 04): | |
| the engine returns the round with `revenues = 0`, `revenue_factors = 0`, | |
| `consumptions = 0`, `surplus_returned = 0` for every sector and **does not | |
| debit the treasury** (the check fires in Phase 5a, before Phase 5b debit). | |
| The visualizer detects this with `done && termination_reason.startsWith("CRITICAL_FAILURE")`, | |
| flags the offending sector(s) (those with `allocation < critical`), greys out | |
| the rest, and labels the treasury cell as "frozen β failure before debit". | |
| --- | |
| ## 2. Live streaming | |
| The visualizer talks to `server/visualizer_server.py` over a tiny REST + SSE | |
| surface. The Vite dev server already proxies `/api/*` to | |
| `http://127.0.0.1:8001` (override with `VIZ_BACKEND_URL`). | |
| ### 2.1 Endpoints | |
| | Method | Path | Purpose | | |
| |---|---|---| | |
| | `GET` | `/api/health` | Liveness check. | | |
| | `GET` | `/api/config` | Visualizer-shaped `config` block (sectors, baselines, β¦). | | |
| | `GET` | `/api/modes` | Available inference modes (`llm` + rule-based baselines). | | |
| | `GET` | `/api/runs` | List recent runs. | | |
| | `POST` | `/api/runs` | Start a new run. Body: `{mode, model_id?, seed, max_rounds, temperature}`. Returns `{run_id, ...}`. | | |
| | `GET` | `/api/runs/{id}/snapshot` | Full episode-so-far in the Β§1 shape. | | |
| | `GET` | `/api/runs/{id}/stream` | SSE stream β see Β§2.2. | | |
| ### 2.2 SSE event types | |
| Each event arrives as `event: <type>\ndata: <json>\n\n`. The browser client in | |
| `src/utils/api.js` dispatches them into typed callbacks. | |
| | `event` | Payload | Notes | | |
| |---|---|---| | |
| | `start` | `{run_id, policy, mode, seed, config, max_rounds}` | Sent once on connection. | | |
| | `round` | A single per-round record (Β§1.1). | One per completed round. | | |
| | `summary` | The Β§1 `summary` block. | Sent once when the episode finishes. | | |
| | `error` | `{message: string}` | Adapter or engine failure. | | |
| | `done` | `{reason: "complete" \| "error" \| "already_closed"}` | Always the last event. | | |
| Late subscribers receive the full event history first (so refreshing the page | |
| mid-run replays from the beginning), then live updates. | |
| ### 2.3 Inference modes | |
| Modes returned from `/api/modes`: | |
| - `llm` β drives every minister with a Hugging Face Inference API model. | |
| Requires `HF_TOKEN` in the server `.env`; reads `HF_MODEL_ID` as the default | |
| model id (the form lets you override it per-run). | |
| - `equal_split`, `optimal_zone`, `conservative`, `greedy` β rule-based | |
| baselines from `agents/rule_based/`. Useful for offline demos. | |
| ### 2.4 Running locally | |
| Two terminals: | |
| ```bash | |
| # Terminal 1 β backend (FastAPI + SSE on :8001) | |
| python -m scripts.run_visualizer_server | |
| # Terminal 2 β frontend (Vite dev server on :5173) | |
| cd visualizer | |
| npm install # one time | |
| npm run dev | |
| ``` | |
| Open <http://localhost:5173>, pick a policy in the **Live inference** card, and | |
| press **Start run**. Rounds will stream into the dashboard as they're produced. | |
| --- | |
| ## 3. Static JSON files | |
| The static loader is unchanged. To export a recorded run, hit | |
| `GET /api/runs/{id}/snapshot` and save the response β the file matches the | |
| shape in Β§1 and can be dragged back into the visualizer at any time. | |
| --- | |
| ## 4. Development notes | |
| - `vite.config.js` has `server.watch.usePolling: true` because the project | |
| lives on a WSL-mounted Windows drive where `fs.watch()` throws `EISDIR`. | |
| Remove it on a normal filesystem to lower idle CPU. | |
| - For the very first run on Windows + WSL, install dependencies *inside* WSL | |
| (`wsl npm install`) to avoid `EPERM`/`EISDIR` cleanup errors, then run the | |
| build/dev server from either side. | |
| - The proxy block in `vite.config.js` strips response buffering for the | |
| `/stream` endpoint so SSE chunks arrive immediately even through the dev | |
| proxy. | |