Spaces:
Sleeping
Architecture
This document describes how Calculus Animator is wired together at runtime โ the process model, IPC shape, module responsibilities, and lifecycle. It is the doc to read first when you want to understand or modify the system; the README focuses on what the app does for users.
Process Model
The running app spans three Python processes plus the embedded WebView, which together form a strict hub-and-spoke topology with the PyWebView shell as the user-facing root.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Process A โ PyWebView Shell (run.py โ window.py) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Embedded WebView (browser engine, OS-native) โ โ
โ โ Loads ui/index.html โ ui/js/app.js + modules/* โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Python side: api.bridge.CalculusAPI (exposed to JS as `pywebview.api`) โ
โ โ โ โ
โ โ โโโ HTTP โโโบ Process B (AI tutor backend) โ
โ โโโ stdin/stdout JSON โโโโโบ Process C (slide render worker) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Process B โ FastAPI AI Tutor Backend Process C โ Pygame Render Worker
(ai_tutor/main.py) (api/slide_render_worker.py)
Bound to 127.0.0.1 Persistent subprocess
HTTP/JSON Line-delimited JSON over stdin/stdout
JSON access logs with request_id stderr drained to parent logger
- Process A owns the WebView and is the user-facing entrypoint. It instantiates
CalculusAPIand registers it as the JavaScript-callable bridge. It also spawns and supervises Processes B and C. - Process B is a FastAPI server providing AI tutoring (RAG + LLM provider routing + vision). It is bound to loopback only and has no external network surface.
- Process C is a long-lived pygame-based slide-render worker. The bridge talks to it via line-delimited JSON over stdin/stdout. There is at most one render worker; the bridge serializes calls with a render lock.
Workers other than slide-render (e.g., capacity-test) are spawned on demand and torn down per call rather than kept persistent.
IPC Shapes
Bridge โ Render Worker
- Transport: pipes (stdin write, stdout read, stderr read).
- Encoding: one JSON object per line in both directions.
- Reliability: stdout reads happen via a queue-fed reader thread with a configurable timeout (
CALC_ANIM_RENDER_TIMEOUT_SEC, default 60s). On timeout the bridge kills the worker, restarts it on the next call, and returns a structured error to the caller. - Stderr: drained continuously by
_drain_stream_to_loggerdaemon thread into the project logger at WARNING level with a[render-worker]label. Pipe buffer can never fill. - Crash detection:
_watchdog_looppollsproc.poll()every_WATCHDOG_INTERVAL_SEC(default 2s); on crash it restarts the worker. Restart storms are bounded by_MAX_CONSECUTIVE_RESTART_FAILURES(default 3); once exhausted, subsequent calls returncapability_unavailableinstead of starting a restart loop.
Bridge โ AI Tutor Backend
- Transport: HTTP over loopback (
127.0.0.1:<port>). - Encoding: JSON request/response.
- Middleware order (outer โ inner):
RequestIdMiddlewareโAccessLogMiddlewareโCORSMiddlewareโMaxBodySizeMiddlewareโ app. - Request IDs:
RequestIdMiddlewarehonours an inboundX-Request-Idheader (so the bridge can propagate its own correlation ID) or generates a UUID4 fresh. Always echoed in the response. The ID is bound to acontextvars.ContextVarso everylogger.*call within the request automatically includes it. - Errors: all exceptions flow through three exception handlers (
HTTPException,RequestValidationError,Exception) and emit a structured envelope:{"error": {"type", "message", "request_id"[, "details"]}}. No traceback or stack-trace text leaks into HTTP response bodies. - Outbound LLM calls: each provider call site validates the target URL against an allowlist before firing (SSRF protection).
WebView โ Bridge
- Transport: PyWebView's native JS-to-Python binding (no network).
- API: methods on
CalculusAPIare exposed underpywebview.api.*to the WebView JavaScript context. - Concurrency: JavaScript calls into the bridge can interleave; the bridge serializes worker stdin via
_render_worker_lock. Per-call queue instances prevent reader-thread sentinels from leaking across worker generations.
Module Responsibilities
| Path | Responsibility |
|---|---|
run.py |
Canonical user-facing launcher. Drains backend stderr to the project logger, supervises the FastAPI subprocess, instantiates CalculusAPI, calls webview.create_window. Use python run.py --help / --version to inspect without launching. |
window.py |
PyWebView shell helper. Importable; not a separate user-facing entrypoint. |
scripts/build_release.py |
PyInstaller artifact builder. Release-only. Install build deps via pip install -e .[build]. |
scripts/smoke_test.py |
Three-check non-interactive verification (backend constructs, bridge probe, end-to-end render pipeline). Exit 0 on pass. |
api/bridge.py |
CalculusAPI โ JS-callable methods, render-worker lifecycle, capacity probes. Owns the watchdog and stderr-drain machinery. |
api/slide_render_worker.py |
Pygame-based persistent render worker. JSON-line protocol on stdin/stdout. |
ai_tutor/main.py |
FastAPI app. Wires middleware, exception handlers, lifespan startup (calls configure_logging()), and route routers. |
ai_tutor/middleware.py |
RequestIdMiddleware, MaxBodySizeMiddleware, AccessLogMiddleware. |
ai_tutor/logging_config.py |
request_id_var ContextVar, RequestIdFilter, JSON formatter, configure_logging() (idempotent). |
ai_tutor/providers/router.py |
Multi-provider LLM dispatch (DeepSeek, Google, OpenAI, Anthropic, Ollama). SSRF allowlist + per-provider URL validation. |
ai_tutor/rag/ |
Curriculum retrieval (ChromaDB-backed). Optional dependency; the package imports cleanly without chromadb installed. |
core/ |
Symbolic math: parser, detector, extractor, solver, step generator, animation engine. SymPy under the hood. |
slide_renderer/ |
Pygame slide rendering primitives (auxiliary package; not on the OSS POC critical path). |
ui/index.html, ui/js/app.js |
WebView entrypoint and main JS module. |
ui/js/modules/renderer.js |
Renderer module โ math + slide rendering. Uses safe DOM APIs throughout; katex invoked with trust: false. Uses _setSafe / _ensureChild to prevent prototype-pollution writes. |
ui/vendor/katex/, ui/vendor/mathlive/ |
Vendored math libraries (KaTeX 0.16.11, MathLive 0.101.0) for offline rendering. |
Lifecycle
- User runs
python run.py. run.pychecks/installs missing runtime deps, then spawns Process B (FastAPI backend) as a subprocess. Backendstderris drained into the project logger via a daemon reader thread.run.pyinstantiatesCalculusAPI(which spawns Process C โ the render worker โ lazily on first call).- PyWebView opens the desktop window pointing at
ui/index.html. - The frontend's
app.jscallspywebview.api.boot(...)(or equivalent) to wire bridge โ UI. - User flows: solve / render / tutor calls flow through the bridge into the appropriate process.
- Shutdown: ctrl+C or window-close terminates Process A; the bridge's watchdog terminates Process C; FastAPI's lifespan shutdown handler drains in-flight requests on Process B.
Configuration Surface
All runtime configuration is via environment variables (see .env.example):
| Variable | Default | Purpose |
|---|---|---|
CALCANIM_MAX_REQUEST_BYTES |
10485760 (10 MiB) | Maximum request body size accepted by the FastAPI backend. |
CALC_ANIM_RENDER_TIMEOUT_SEC |
60 | Render worker readline timeout. |
_STARTUP_TIMEOUT_SEC |
5 | Render worker startup timeout. |
_WATCHDOG_INTERVAL_SEC |
2 | Watchdog poll interval. |
LLM_PROVIDER |
deepseek |
Active LLM provider for the AI tutor. |
DEEPSEEK_API_KEY, GOOGLE_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY |
โ | Provider API keys (whichever provider is active). |
Threading and Concurrency Model
- Process A (PyWebView shell): main thread runs the WebView event loop. Daemon threads handle: (a) backend
stderrdrain, (b) render-workerstderrdrain, (c) render-worker stdout queue feeder, (d) watchdog poll loop. - Process B (FastAPI): default uvicorn event loop. Per-request
contextvars.ContextVarcarries the request ID through async call chains. - Process C (render worker): single-threaded pygame loop, blocks on stdin between calls.
Cross-process state is intentionally minimal โ there is no shared memory or shared filesystem state beyond the read-only data/ curriculum and the read-only ui/ assets.
Why This Shape
- PyWebView over Electron: smaller distribution (~80MB vs ~200MB), single Python codebase, native OS integration without Chromium bundle.
- Subprocess workers over threads: pygame can hang or crash; isolation protects the main process. Memory is isolated from the renderer's leaks.
- FastAPI bridge over direct PyWebView binding for AI tutor: language-agnostic, future-proofing for web/mobile ports, async-native for concurrent solve+render+chat, OpenAPI documentation for free.
- Loopback-only over real auth: this is a single-user desktop app. The host machine is trusted; the network boundary is the loopback interface.
- Vendored math libraries over CDN: clients running offline (e.g., classrooms, restricted networks) need the app to work without network access.