Repository navigation
✨ Adaptive Minds v0.1 — Next.js UI, four modes, one-command Docker, demo + docs overhaul - #2
Merged
Merged
Conversation
Restructure the repo around a single `adaptive_minds/` package that
implements the framework described in the ICML 2026 paper "Adaptive
Minds: Empowering Agents with LoRAs as Tools".
Runtime:
* Router (paper §5.2) — single-step semantic routing
* Agent (paper §5.4) — ReAct loop with adapters-as-tools plus 5
external tools (calc, code, shell, web, LP)
* FastAPI server (thin: no LangGraph / torch / transformers) with
/health, /adapters, /route, /agent, /chat endpoints
* Streamlit chat UI with mode toggle + trace expander
* Typer CLI: serve / server / ui / route / agent / list
One-command deploy: `docker compose up -d` brings up
prefetch (downloads requested LoRAs)
→ vllm (vllm/vllm-openai:v0.6.3, base + LoRAs by name)
→ server (FastAPI on :8765)
→ ui (Streamlit on :8501)
Catalog:
* catalogs/qwen25_30.yaml — 30-adapter library targeting
pavan01729/adaptive-minds-loras/qwen2.5-7b/* (paper Table 1)
* catalogs/qwen25_smoke.yaml — 2-adapter smoke catalog for
docker-compose first-run
Reproduction:
* Table 1 — evals/routing_table1.py + 151-query hand-labeled gold set
* Table 1 keyword baseline — evals/routing_keyword_baseline.py
* Table 2 — training/train_sft.py (shared SFT recipe) + benchmark map
* Table 3 — evals/mmlu_three_way.py (vanilla/router/agent on MMLU)
OSS hygiene:
* GitHub Actions CI (pytest matrix on 3.10/3.11/3.12)
* CONTRIBUTING.md (dev setup, add-adapter, add-tool, PR checklist)
* 18 pytest cases (catalog loader + FastAPI server routes), no GPU
or network required to run
Battle-tested with `docker compose up -d` on a single L40S:
prefetch / vllm / server / ui all reach healthy state. End-to-end
inference verified up to the vLLM 0.6.3 chat-template issue — the
prefetch script now strips adapter-side tokenizer files so the base
model's chat template is used, and router/agent pull the base model id
from the catalog via `base_model_id` instead of the old hard-coded
"base". Full end-to-end inference verification deferred to a follow-up
once vllm is bumped or a chat-template flag is wired through.
Deferred to v0.2 (README "Roadmap"): entropy H(Q) mode classifier
(paper §5.5), SSE streaming, PEFT in-process runtime, GRPO recipes,
Appendix C MCQ harness, adapter-fusion experiments.
- common.py: remove vllm_chat_stream (unreferenced) and unused typing.Any
import; preserves vllm_chat as the sole HTTP entry point.
- router.py: remove run_manual + run_auto (no callers in server, agent,
CLI, tests, or evals). run_router is now the only public function plus
the keyword_pick baseline used by evaluations.
- __init__.py: replace 'accepted to ICML 2026' with arXiv 2510.15416 ref
(the conference has not happened; only the arXiv citation is honest for
v0.1). Update __all__ to match the trimmed surface.
- common.vllm_chat: friendlier ConnectionError ('vLLM unreachable at
<url>; run docker compose up -d') in place of the bare requests stack
trace.
Net: -98 lines, no behaviour change. 18/18 tests pass; ruff clean.
A self-contained reference implementation of the framework:
catalog YAML loader + vLLM HTTP client + router + agent loop + 2 example
tool handlers + __main__ that runs 'route' or 'agent'. Only deps: stdlib
+ requests + PyYAML.
Purpose: a reader can 'cat nanoam.py' and grasp the framework in 5 min.
The production runtime stays in adaptive_minds/ (FastAPI, sandboxed
tools, evals, docker).
Smoke-tested helpers (no vLLM needed):
- parse_actions('CALL: x | y\nFINAL: z') → 2 actions parsed cleanly
- tool_calculator('2**16+17') → '65553'
- tool_python('print("hello")') → 'hello'
- load_catalog() → chemistry, sql from smoke YAML
- keyword_pick(...) on chemistry query → 'chemistry'
Friendly error when vLLM is unreachable.
* tests/test_cli.py (8): typer CliRunner over root --help, list,
serve (incl. base-model override + missing-base-model failure),
route, agent; covers happy + sad paths for the subcommands a fresh
user runs first.
* tests/test_prefetch.py (3): import docker/prefetch.py as a module;
mock huggingface_hub.snapshot_download; verify
- empty LORA_SUBDIRS is a no-op,
- subdir parsing splits on commas and triggers one download each,
- allow_patterns excludes tokenizer files (the chat-template
override bug we shipped a fix for in bee4715),
- LORA_OUT_DIR defaults to /loras (matches docker-compose mount).
* tests/test_catalog_smoke.py (3): the 2-adapter smoke catalog parses,
has the canonical {chemistry, sql} ids with non-empty system prompts
+ keywords, and router_cfg() surfaces base_model_id +
{query}/{domain_list} placeholders.
All 32 tests hermetic — no vLLM, no GPU, no network. Total runtime
0.65 s. Ruff clean.
Before this commit, 'adaptive-minds server --port 8765' would crash with uvicorn's opaque 'OSError: [Errno 98] address already in use' when a co-tenant process holds the port. We now do a one-shot SO_REUSEADDR bind probe first and exit with a one-line message that names the port and points to the AM_SERVER_PORT override in .env. Matches the friendly vLLM-unreachable error in adaptive_minds.common.vllm_chat.
…se section banners * agent.py 295 → 284: dropped the duplicate 'sequential CALL' example from DEFAULT_AGENT_PROMPT. The multi-call example is the load-bearing one (it teaches batching); sequential dispatch happens naturally without an explicit example. Retry path + commit-FINAL pass intact. * external_tools.py 290 → 280: collapsed five 3-line section banners (# ----- / # Title / # -----) into single-line comments. No content change. The pulp handler is kept — it's the most distinctive demo in the tool set and documented in README+ARCHITECTURE. Net: -21 lines, no behaviour change. 32/32 tests pass; ruff clean.
* Add a 'Read nanoam.py to grasp the framework in 5 minutes' callout at the top of the Architecture section. * Update the test-count badge mention 18 → 32; replace the 600-line claim (stale) with '~1.4 k lines across nine .py files' (accurate after the Phase-4 cleanup). * Add a citation block: short prose line + arXiv-style BibTeX stub. Honesty contract: arXiv 2510.15416 is the only public ref for v0.1. No ICML/PMLR/Proceedings until the conference happens. * Add 'Next.js chat UI' as the first v0.2 roadmap item — telegraphs the planned port of the am-ui frontend (Tailwind + framer-motion + xyflow) without overpromising v0.1. * Add a 'nanoam.py' row to the features table.
Drops Streamlit (`ui/app.py`) and replaces it with a Next.js 14 chat
UI under `ui/`. Same backend surface (`/health`, `/adapters`,
`/chat`) — the browser hits FastAPI directly since the server already
runs with CORS open (`allow_origins=["*"]`), so no Next.js
server-side proxy is needed.
Stack (matches the am-ui frontend the user wanted ported):
- Next.js 14.2.35 App Router + React 18 + TypeScript 5
- Tailwind CSS 3.4
- framer-motion 12 (turn animations)
- @xyflow/react 12 (router-decision visualisation)
- react-markdown + rehype-highlight + remark-gfm (response rendering)
Layout: header with mode toggle (Router / Agent) and health pill;
sidebar with adapter list; chat with user + assistant bubbles; for
router-mode replies an embedded RouterFlow xyflow node graph shows
`user → base · router → `<adapter>``; trace expander surfaces the
agent's step chips with tone-coloured tool/expert/brain pills.
Wiring:
- `NEXT_PUBLIC_AM_API_BASE` (browser-side) defaults to
`http://localhost:8765`; compose bakes it in at build time as
`http://localhost:${AM_SERVER_PORT:-8765}` so a port override in
`.env` automatically flows through.
- `NEXT_PUBLIC_DEFAULT_MODE` controls the initial mode (router/agent).
- No server-side proxy routes (`app/api/...`); CORS handles browser →
FastAPI directly.
docker/Dockerfile.ui — rewritten as a multi-stage Node build:
- builder: `node:20-alpine` + `npm ci || npm install` + `npm run
build` with `output: 'standalone'`.
- runner: `node:20-alpine` copies only `.next/standalone`,
`.next/static`, `public`. Final image ~80 MB instead of ~800 MB.
- Healthcheck wgets `http://localhost:3000`.
docker-compose.yml — `ui` service now publishes
`${AM_UI_PORT:-3000}:3000` (was 8501); drops `AM_API_BASE` env
(browser uses build-baked NEXT_PUBLIC_AM_API_BASE).
pyproject.toml — dropped the `[ui]` optional dep (no streamlit); the
chat UI lives in `ui/` and is built via docker or `npm`.
adaptive_minds/cli.py — dropped the `adaptive-minds ui` Streamlit
launcher; README documents `cd ui && npm run dev` for local dev.
tests/test_cli.py — removed `'ui'` from the help-subcommand
assertion. 32/32 tests still pass.
README — Streamlit → Next.js everywhere; port 8501 → 3000; v0.2
roadmap drops the 'Next.js port' bullet (it's done).
NOT TESTED on this host (no docker, no Node). Build verification is on
the user's dev machine: `cd ui && npm install && npm run build` to
catch any TS errors locally before `docker compose build ui`.
Backend (adaptive_minds/):
* New auto.py — heuristic dispatcher between Router and Agent. Stamps
the result with auto_decision = {picked, reason} so the UI shows
which path was chosen and why (operators / sequencing / long /
multi-sentence / single-domain).
* New langgraph_agent.py — same ReAct loop as agent.py but expressed
as a LangGraph StateGraph (plan → dispatch → synthesise nodes,
conditional edges). Same return shape; same dispatch reuses
_parse_actions + _dispatch_call from agent.py so the behaviour is
identical, just observable as graph node visits.
* server.py: ChatRequest.mode literal now Router | Agent | Auto |
LangGraph; /chat resolves the runner dynamically (so tests can
monkeypatch run_router / run_agent / run_auto). LangGraph runner is
imported lazily — returns 501 with a friendly install hint if the
langgraph dep isn't present.
* pyproject.toml: added langgraph>=0.2 to the [serve] extra.
* New tests/test_modes.py (9 tests): needs_agent heuristic parametrised
+ /chat dispatches all four modes + rejects unknown mode (422 from
pydantic). 32 → 41 hermetic tests, 0.69 s.
UI (ui/, replaces Streamlit):
* Four-tab mode nav under the header with icon + label + per-mode
one-line blurb on the right; active tab gets an emerald ring.
* api.ts MODES table is the single source of truth for tab metadata.
* RouterFlow xyflow viz shown for router + auto→router replies; new
LangGraphFlow xyflow viz shown for langgraph + auto→agent replies,
with plan/dispatch/synthesise nodes coloured by whether the trace
visited them.
* Auto-mode replies get an amber 'auto → <picked> (<reason>)' pill so
the dispatcher's decision is visible without opening the trace.
* Polished input chrome: gradient Send button, animated 'running …'
pulse dot, sample chips per active mode.
* Default UI port now 7007 (was 3000) — set in package.json start
script, Dockerfile.ui (ENV PORT, EXPOSE, HEALTHCHECK), and
docker-compose.yml ui.ports. .env.example AM_UI_PORT default
updated; README quickstart points at http://localhost:7007 and the
Architecture ASCII says 'port 7007'.
* ui/.env.example documents all four NEXT_PUBLIC_DEFAULT_MODE values.
No Streamlit anywhere — confirmed by grep. Tests 41/41 pass; ruff
clean. NOT smoke-tested in a browser (no docker / node on this host);
build verification is on the user's dev machine via 'cd ui && npm
install && npm run build' before 'docker compose build ui'.
* Drop examples/basic_usage.py + examples/interactive_demo.py — they
duplicate basic_router.py / basic_agent.py with verbose pre-v0.1
docstrings; not referenced in examples/README.md. Saves 284 lines.
Three runnable examples remain: basic_router.py, basic_agent.py,
curl_examples.sh — all listed in examples/README.md.
* New scripts/capture_demo.py (~120 lines) — Playwright + Chromium
drives the running Next.js UI (port 7007), records a 25-s demo of
Router + Agent flows, writes docs/{router,agent_trace,screenshot}.png
plus docs/demo.gif (if ffmpeg is on PATH). The user runs this after
'docker compose up -d' is healthy. No live testing needed in the OSS
repo itself — the script is hermetic Python that won't be exercised
in CI.
* Add scripts/README.md — what each one does + how to run.
* README:
- Drop the 'Why Adaptive Minds' bullet list; replace with one
three-sentence prose paragraph that reads in 10 s (the karpathy
hook style).
- Update the 'demo' caption to mention scripts/capture_demo.py
and the four modes (Router / Agent / Auto / LangGraph).
- Features table: replace 'Router mode / Agent mode / 5 tools'
rows with one row per mode (now four), each with its emoji,
matching the UI's tab nav. Tightens the cognitive map.
- 'CI + tests': 32 → 41 (added test_modes.py in the 4-modes ship).
* .gitignore: add .ruff_cache/ so a fresh dev clone stays clean.
Tests 41/41 pass; ruff clean. Net -267 lines.
Architecture diagram had only run_router/run_agent; auto + langgraph modes shipped in 0fffaa6 but weren't shown. Line-count blurb still said "nine .py files / ~1.4k lines" pre-port.
The shell example contained a bare `{print "too long"}` which Python's
str.format() treated as a placeholder, raising KeyError on every /agent
request. Escape as `{{...}}` and add a hermetic regression test.
Three changes that make `docker compose up -d` work end-to-end out of
the box, regardless of where the stack is hosted:
1. Default to the 30-adapter catalog
- docker-compose: vllm `--lora-modules` lists all 30; prefetch
LORA_SUBDIRS covers all 30
- Dockerfile.server: shell-form CMD so $AM_CATALOG is interpolated
at runtime (the hardcoded smoke path was overriding the env)
- .env.example + Dockerfile.server ENV default → qwen25_30.yaml
2. Same-origin API proxy (fixes "Failed to fetch" on remote hosts)
- ui/src/lib/api.ts default → "/api/am"
- ui/next.config.mjs adds rewrite /api/am/* → server:8765/*
- Dockerfile.ui ARG default emptied (no more localhost:8765 baked
into the bundle; same image works on localhost, public IP, or
behind a reverse proxy)
3. Stop excluding ui/src/lib/ from git
- The Python `lib/` ignore was swallowing the UI's typed api client;
`page.tsx` imports `@/lib/api` so OSS clones would fail to build.
- Add negation rule and commit the file.
Verified end-to-end against the live stack: vLLM serves 31 models
(base + 30 LoRAs), server reports n_adapters=30, browser proxy hits
all four modes (router, agent, auto, langgraph) without CORS issues.
Dockerfile.ui sets `ARG NEXT_PUBLIC_AM_API_BASE=` (empty string) so the proxy path is the default. But the bundle was reading `process.env.NEXT_PUBLIC_AM_API_BASE ?? "/api/am"` — and `??` only falls back on null/undefined, not empty string. Result: API_BASE got baked in as `""` and fetches went to `/health`, `/chat`, `/adapters` on the UI host — Next.js has no such routes, so the browser saw 404 HTML in the trace pane while curl `…/api/am/chat` worked. Switch both call sites to `||` so empty-string envs also fall back.
…button - Plain Enter sends; Shift+Enter inserts newline (matches ChatGPT). IME composition is guarded so multi-byte input methods don't fire submit mid-character. - Chat persists to localStorage (`am-chat-v1`); hydrates on mount and re-syncs on every turn. A separate `hydrated` flag prevents the empty initial state from wiping storage before we've read it. - "🗑 Clear" button in the header — disabled when chat is empty, prompts for confirm before wiping. - Auto-growing textarea (caps at ~240 px, then scrolls) replaces the fixed-height box. - Centered empty state with stacked sample-query buttons (clicking one fills the input and focuses). - Copy-to-clipboard button on each assistant response (top-right of the header row). - Spinning send-button affordance during in-flight requests; auto-focus returns to the textarea after a turn completes. - Footer hint updated; sidebar reminds users history is local.
The README's hero pointed at docs/screenshot.png which never existed — broken image on GitHub. Capture the real running stack instead: - scripts/capture_demo.py rewritten to drive all four modes (Router → Agent → Auto → LangGraph) with human-paced typing, Enter-to-send, and trace expansion, then render docs/demo.mp4 (HQ, linked) + docs/demo.gif (README hero, ~4 MB, sped 1.3x) via ffmpeg two-pass palette + gifsicle. - README hero now shows docs/demo.gif with a link to the MP4. - docs/screenshot.png shipped as a static fallback (Router + decision graph). - scripts/README.md updated to match the new outputs. Captured against the live 30-adapter docker-compose stack on :7007.
…o/outro Upgrade the demo from a raw screen-grab to a branded marketing video: - video/ : new Remotion project. Intro (animated brain logo, gradient wordmark, mode chips) and outro (headline, 98.3%/30/4 stat cards, 'Star us on GitHub' CTA, repo + arXiv links) composited around the real app footage via OffthreadVideo at 1.35x. - scripts/capture_demo.py now records 1080p footage to video/public/app.mp4 (instead of producing the final media itself). - scripts/build_demo.sh : one-shot capture → Remotion render → GIF. - docs/demo.mp4 regenerated at 1080p (h264, ~8.7 MB, 60s). - docs/demo.gif re-encoded higher quality (two-pass palette + sierra2_4a dither, ~7.2 MB). - .gitignore excludes video/node_modules, video/out, the captured app.mp4, and docs/.rec (all regenerable). - README hero caption + project layout + scripts/README updated. Captured against the live 30-adapter docker-compose stack on :7007.
CI only triggered on main/release/** — the active v2 dev branch was never tested, so green local runs never surfaced in CI and a release branch had to be cut to see it (where an older lint violation failed the ruff step). Add v2 to push + pull_request triggers so the working branch is verified on every push. Current code is clean: ruff passes and 42 tests pass in a fresh .[dev,serve] env matching the CI matrix.
UI: - Add a light theme across the whole app (Tailwind darkMode:class). Dark stays the default (html ships .dark; no-flash inline script in layout.tsx reads localStorage[am-theme]); a header sun/moon toggle flips it and persists. - Rewrite page.tsx light-first with dark: variants for every surface, border, text, pill, chip, and the input. Thread a theme prop into the xyflow graph (nodeStyle/edges/Background) since inline styles can't use dark:. Also fixes the dark graph-controls white-box blemish via CSS. - globals.css: CSS-var tokens for light/.dark, light-first body/scrollbar/ .panel, light github.css highlight base + compact .dark .hljs overrides, themed code blocks. Demo video: - capture_demo.py records LIGHT mode, zoomed in (1440x810 viewport @ 2x DSF, recorded at crisp 1080p) so UI text is clearly readable. - Demo.tsx adds a subtle background-music bed (<Audio>, ~35% with fade in/out). build_demo.sh stages docs/bgm.mp3 (falls back to silence). - Regenerated docs/demo.mp4 (1080p H.264 + AAC, 6.8 MB) and docs/demo.gif (light, zoomed, 5.9 MB) + light docs/screenshot.png. - Raw docs/bgm.mp3 gitignored (licensing/size); rendered video carries the music. Aylex attribution noted in video/README.md. 42 tests still pass; UI changes don't touch Python.
The footage only filled the top-left because record_video_size (1080p) didn't match the viewport (1440x810) — Playwright only scales DOWN to fit, so it pinned the page to the top-left with blank margins. - Capture now uses viewport == record_video_size (1280x720, zoomed) and lanczos-upscales to 1080p in ffmpeg, so Remotion uses it 1:1 and it fills the whole frame. - Trim the script to a punchy two-mode run: a simple Router route, then a multi-step LangGraph run (state graph + answer). - Add a guard that fails the capture if any response shows a transient server error, so a flaky run never gets baked into the demo (a LangGraph call 500'd mid-capture once; it's reliable — re-captured clean). - Regenerated docs/demo.mp4 (1080p H.264+AAC, 4.1 MB, 37s) + docs/demo.gif (4.7 MB) + light docs/screenshot.png.
Replace the plain GIF + MP4 link with a polished hero: the autoplaying GIF preview is now clickable and a prominent '▶ Watch the full demo — with sound' badge links to the GitHub-hosted MP4 (plays inline with audio). Keeps the silent GIF for at-a-glance, adds a one-click path to the full 1080p tour with music.
Dockerfile.ui prefers `npm ci` when a lockfile is present; committing it pins the Next.js UI dependency tree so the image is reproducible.
The user-attachments asset linked in the hero turned out to be a different project's computer-vision clip, not the Adaptive Minds demo — so the 'Watch' button was opening the wrong video. Point the GIF + the '▶ Watch with sound' button at the committed docs/demo.mp4 (the current full-screen 1080p Router→LangGraph tour, with audio), which GitHub plays inline in the blob viewer. Always tracks the latest committed demo.
Swap the hero to the user-attachments video (uploaded to issue #1): the bare URL renders an inline GitHub player that plays the full-screen 1080p Router→LangGraph tour with sound, plus a '▶ Watch with sound' badge. Replaces the earlier wrong/computer-vision asset.
- Headline subtitle → 'Empowering Agents with LoRA-as-Tools' (matches the paper title). - Replace the ASCII architecture diagram with a colour-coded Mermaid flowchart: User → Next.js UI → FastAPI → the four orchestration modes → vLLM (base + LoRA adapters), with external tools and the YAML catalog as single source of truth. Renders natively on GitHub.
README full restructure for clarity and credibility: - Simplify the demo hero — inline player with audio + one-line caption, drop the redundant 'Watch with sound' badge. - New 'Key results' section with the paper's headline numbers (98.3% routing, +4.6→+84pp specialists within ±5pp of direct, 3.1× faster) and the three findings. - New 'How it works' centerpiece: the three mode figures from docs/ (Router/Agent/Auto) + grounded explanations; LangGraph in text. - Replace the Mermaid block with the unified auto_mode_arch.png as the system overview (per request: real figures, not Mermaid). - Add an honest 'When it helps (and when it doesn't)' section. - Enrich 'bring your own adapter' with the LoRA training recipe; fix the test count (41 → 42) and a few stale lines. ARCHITECTURE.md: add an Operating-modes intro with the three figures, expand the module table to all four modes (auto.py, langgraph_agent.py) plus server/cli, and add Auto + LangGraph deep-dives. No Mermaid. Commit the three figures (am_routing_v4, agent_architecture_2, auto_mode_arch).
- Reframe Roadmap around the three things that matter: adapters as truly first-class tools (incl. train-as-a-tool), scaling routing to 100s–1000s of adapters, and better adapter-training recipes. Tuck the small infra items into one line. - Surface the core thesis near the top: adapters are tools, their quality matters most — train good adapters and the framework handles the rest. - Add an Acknowledgements section and a few thank-you/please touches (vLLM, HF, LangGraph, Next.js + the community).
- Rename 'What is this?' → '🤔 Why Adaptive Minds?' (start with why) and drop the trailing callout line. - Rewrite CONTRIBUTING.md to be warmer (please/thanks, emoji headers, a friendly PR note + closing thanks) and fix a real bug: the dev-install used a nonexistent '[ui]' extra → now '.[dev,serve,tools]'.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Brings
v2up to a polished, paper-aligned v0.1. 29 commits, CI green across Python 3.10 / 3.11 / 3.12.Highlights
Runtime & modes
fix(agent): escaped anawkliteral inDEFAULT_AGENT_PROMPTthat 500'd every/agentrequest (.formatKeyError) — with a regression test.Deploy / DX
docker compose up -dboots vLLM + server + UI with the full 30-adapter catalog by default./api/am/*→ server) so the same image works on localhost, a public IP, or behind a reverse proxy (fix(ui):||vs??so the empty build-arg falls back correctly).UI
@xyflow/reactdecision + state-graph viz, ChatGPT-style input (Enter to send, persisted history, clear, copy).Demo video
Docs
docs/ARCHITECTURE.mdrefreshed (all four modes + figures);CONTRIBUTING.mdwarmed up and a bad[ui]install extra fixed.CI
testsworkflow now also runs onv2+ PRs; 42 hermetic pytest cases.Notes
docs/bgm.mp3is gitignored (licensing/size); the rendereddocs/demo.mp4carries the music.