-
-
Notifications
You must be signed in to change notification settings - Fork 445
feat(webapp): add browser observability console #216
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dnhkng
wants to merge
2
commits into
main
Choose a base branch
from
feat/webapp-console
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| # GLaDOS configuration with the webapp observability console enabled. | ||
| # | ||
| # Start it with: `uv run glados webapp --config configs/glados_webapp_config.yaml` | ||
| # Then open http://127.0.0.1:8050/ in a browser. | ||
| # | ||
| # The console is an in-process HTTP server (stdlib only) that streams the | ||
| # engine's live state - minds/slots/subagents/MCP/emotion/audio/lanes and the | ||
| # ObservabilityBus event log - over Server-Sent-Events plus a JSON snapshot API. | ||
| Glados: | ||
| llm_model: "llama3.2" | ||
| completion_url: "http://localhost:11434/api/chat" | ||
| api_key: null # Add your API key here if needed! | ||
| interruptible: true | ||
| audio_io: "sounddevice" # local hardware. For a browser-mic setup use "websocket". | ||
| input_mode: "audio" # audio, text, or both | ||
| tts_enabled: true | ||
| asr_muted: false | ||
| tui_theme: "aperture" | ||
| asr_engine: "tdt" | ||
| llm_headers: null # Optional extra headers (e.g., OpenRouter HTTP-Referer, X-Title) | ||
| wake_word: null | ||
| voice: "glados" | ||
| announcement: "All neural network modules are now loaded. System Operational." | ||
|
|
||
| # --- webapp observability console --------------------------------------- | ||
| # Default OFF. Enable here, or set GLADOS_WEBAPP_ENABLED=1 / _PORT / _HOST | ||
| # environment variables to switch it on without editing YAML. The demo is | ||
| # intentionally loopback-only because it has no authentication boundary. | ||
| webapp: | ||
| enabled: true | ||
| host: "127.0.0.1" # Listen address | ||
| port: 8050 # Listen port; open http://127.0.0.1:8050/ | ||
|
|
||
| autonomy: | ||
| enabled: true | ||
| tick_interval_s: 10 | ||
| cooldown_s: 20 | ||
| autonomy_parallel_calls: 2 # parallel autonomy LLM workers -> the console's "Autonomy" lane | ||
| autonomy_queue_max: null | ||
| coalesce_ticks: true | ||
| jobs: | ||
| enabled: true | ||
| poll_interval_s: 1 | ||
| hacker_news: | ||
| enabled: false | ||
| interval_s: 1800 | ||
| top_n: 5 | ||
| min_score: 200 | ||
| weather: | ||
| enabled: false | ||
| interval_s: 3600 | ||
| latitude: null | ||
| longitude: null | ||
| timezone: "auto" | ||
| temp_change_c: 4 | ||
| wind_alert_kmh: 40 | ||
|
|
||
| mcp_servers: | ||
| - name: "system_info" | ||
| transport: "stdio" | ||
| command: "python" | ||
| args: ["-m", "glados.mcp.system_info_server"] | ||
|
|
||
| personality_preprompt: | ||
| - system: "You are GLaDOS, a sarcastic AI assistant." |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| # Webapp Console | ||
|
|
||
| An in-process "mission control" for the running GLaDOS engine. It streams the | ||
| live parallel state — the two autonomy lanes, subagent contexts, tool state, | ||
| PAD emotion, audio/MCP health — to a browser over HTTP + Server-Sent Events. | ||
| No separate service to run; no new runtime dependencies. | ||
|
|
||
| ## Decoupled launcher (key design) | ||
|
|
||
| The webapp console is **not** part of the core engine. It is started by a | ||
| dedicated CLI command — `glados webapp` — that mirrors how `glados tui` works: | ||
| it loads the config, builds a `Glados` engine, starts the in-process | ||
| `WebappServer` on its own port, then runs the engine loop and shuts the server | ||
| down when the loop exits. The engine itself holds no webapp knowledge. | ||
|
|
||
| The observable state (`ObservabilityBus`, `MindRegistry`, `TaskSlotStore`, | ||
| subagent memory, interaction/emotion state) only exists inside the running | ||
| `Glados` object, so the console server runs in the same process and reads those | ||
| objects directly — exactly the pattern the WebSocket audio backend uses. This | ||
| keeps it dependency-light and side-effect free for every engine entry point | ||
| (`start`, `tui`, `say`). | ||
|
|
||
| The webapp and the TUI are **mutually exclusive UI options** — you run either | ||
| `glados webapp` or `glados tui`, never both; the core engine stays agnostic to | ||
| which one is attached. | ||
|
|
||
| ## Running it | ||
|
|
||
| The console is **off by default**. Enable it, then start it with the webapp | ||
| launcher: | ||
|
|
||
| 1) YAML config: | ||
|
|
||
| ```yaml | ||
| webapp: | ||
| enabled: true | ||
| host: 127.0.0.1 | ||
| port: 8050 | ||
| ``` | ||
|
|
||
| 2) Environment variables (no config edit): | ||
|
|
||
| ```bash | ||
| GLADOS_WEBAPP_ENABLED=1 GLADOS_WEBAPP_PORT=8050 uv run glados webapp | ||
| ``` | ||
|
|
||
| Both can be combined with `--config`, `--input-mode`, `--tts-enabled`/ | ||
| `--tts-disabled`, and `--asr-muted`/`--asr-unmuted`: | ||
|
|
||
| ```bash | ||
| uv run glados webapp --config ./configs/glados_webapp_config.yaml | ||
| ``` | ||
|
|
||
| Then open `http://127.0.0.1:8050/`. | ||
|
|
||
| > **Dummy fallback.** If the page is opened without a live engine — e.g. | ||
| > `examples/webapp/index.html` or a directly-opened static file — the page | ||
| > detects the missing API and falls back to simulated data so it can be | ||
| > previewed and styled. | ||
|
|
||
| ## Endpoints | ||
|
|
||
| | Method | Path | Purpose | | ||
| | ------ | --------------------- | ------- | | ||
| | GET | `/` | Static console (`static/index.html`). | | ||
| | GET | `/api/snapshot` | Aggregate JSON snapshot (minds, agents, slots, lanes, audio, emotion, MCP, interaction, vision). | | ||
| | GET | `/api/state` | Lightweight state JSON for the live gauges. | | ||
| | GET | `/api/stream` | SSE stream (see contract below). | | ||
| | GET | `/api/minds` | Registered mind statuses. | | ||
| | GET | `/api/minds/{id}` | Single mind status. | | ||
| | GET | `/api/minds/{id}/memory` | That agent's private jsonlines memory entries. | | ||
| | GET | `/api/slots` | Task slots (summary fields). | | ||
| | GET | `/api/slots/{id}` | Full slot including the on-demand report. | | ||
| | GET | `/api/agents` | Subagent statuses (`agent_id, title, running, tick_count, last_tick`). | | ||
|
|
||
| The API is deliberately read-only and loopback-only. It does not enable CORS, | ||
| rejects cross-origin browser requests, and accepts only `127.0.0.1` or | ||
| `localhost` as its configured host. Remote control needs a separately designed | ||
| authentication and authorization boundary; this demo does not pretend to | ||
| provide one. | ||
|
|
||
| ## SSE contract — `/api/stream` | ||
|
|
||
| Each connection first replays the last 100 events from the bus, then receives: | ||
|
|
||
| - `obs` events — the same shape the TUI `ObsScreen` renders: | ||
|
|
||
| ```json | ||
| {"timestamp": 1750000000.0, "source": "autonomy", "kind": "slot.update", | ||
| "level": "info", "message": "weather brief -> done", "meta": {"slot": "s_weather"}} | ||
| ``` | ||
|
|
||
| Real `source`/`kind` combos include `llm.request`, `llm.queue`, | ||
| `llm.tool_calls`, `autonomy.dispatch`, `autonomy.slot.update`, | ||
| `subagent.start/stop`, `tool.start/finish/error/timeout`, `tts.*`, | ||
| `mcp.*`, `vision.update`, `text.user_input`. | ||
|
|
||
| - `state` events — every ~0.5 s, mirroring `/api/state`, so gauges, clock, and | ||
| lane chips stay live without re-sending the whole snapshot. | ||
|
|
||
| ### Multi-consumer (the important bit) | ||
|
|
||
| The TUI's `ObservabilityScreen` consumes the bus via `drain()`, which is a | ||
| *single-consumer* FIFO. The webapp never calls `drain()`. Instead every SSE | ||
| connection registers its own private `subscribe()` queue on | ||
| `ObservabilityBus`, so multiple browsers each get their own copy and never | ||
| steal events from the TUI or from each other. This is fully backward-compatible: | ||
| `drain()` and `snapshot()` keep their existing behavior. | ||
|
|
||
| ## Failure behavior | ||
|
|
||
| Because the webapp is the point of the `glados webapp` launcher, a disabled | ||
| console or a bind failure (port in use) is **fatal for that command**: the | ||
| launcher logs an error and exits rather than running the engine without the | ||
| console. This does not affect other entry points — `tui`, `start`, and `say` | ||
| are completely independent of the webapp and never bind a port. | ||
|
|
||
| - Request exceptions return JSON error bodies and never crash a worker thread. | ||
| - Client disconnects free their subscription. | ||
|
|
||
| ## Lifecycle | ||
|
|
||
| The `glados webapp` command orchestrates the whole lifecycle: | ||
|
|
||
| 1. Load `GladosConfig.from_yaml` and apply CLI overrides. | ||
| 2. Refuse to start if `webapp.enabled` is false. | ||
| 3. Build `Glados.from_config`, then `WebappServer(engine, host, port).start()`. | ||
| 4. Run `engine.run()`; on any exit (`try/finally`), shut the server down. | ||
|
|
||
| The core engine stays decoupled and side-effect free. This mirrors the TUI's | ||
| launcher pattern: the same `GLADOS_WEBAPP_*` environment variables live on | ||
| `GladosConfig.webapp`, so the launcher reads one merged config. | ||
| `GladosConfig.webapp` stays a field on the shared config model, but the engine | ||
| never reads it at runtime — the decoupling is at the launcher boundary. | ||
|
|
||
| ## Development | ||
|
|
||
| - Console page: `src/glados/webapp/static/index.html` — single self-contained | ||
| file (Aperture/GLaDOS theme, no build step). `examples/webapp/index.html` is | ||
| the standalone mockup it derives from. | ||
| - Server: `src/glados/webapp/server.py`; serializers: `serializers.py`; | ||
| config: `config.py`. | ||
| - Tests: `tests/test_webapp.py` — bus fan-out, serializers, and an in-process | ||
| HTTP smoke test against a stub engine. | ||
| - Example config: `configs/glados_webapp_config.yaml`. | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
statepings are not guaranteed every 0.5 s.The doc states that
stateevents arrive every ~0.5 s. Insrc/glados/webapp/server.py_stream, thestateframe is emitted only in the_queue.Emptybranch ofsub.get(timeout=0.5). While observability events arrive faster than one per 0.5 s, the timeout never fires and nostateframe is sent. The browser gauges then freeze exactly when the engine is busiest.Two options:
stateframe when 0.5 s has elapsed, regardless of which branch ran.The first option matches the stated intent.
🤖 Prompt for AI Agents