nanobot-voice-interface/app.py
kacper 4dfb7ca3cc
Some checks failed
CI / Backend Checks (push) Failing after 36s
CI / Frontend Checks (push) Failing after 40s
feat: unify card runtime and event-driven web ui
2026-04-06 15:42:53 -04:00

103 lines
3.1 KiB
Python

from __future__ import annotations
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from card_store import CARD_TEMPLATES_DIR, NANOBOT_WORKSPACE
from routes import (
cards_router,
inbox_router,
messages_router,
rtc_router,
sessions_router,
templates_router,
tools_router,
workbench_router,
)
from session_store import list_web_sessions, normalize_session_chat_id
from web_runtime import create_web_runtime
BASE_DIR = Path(__file__).resolve().parent
DIST_DIR = BASE_DIR / "frontend" / "dist"
NANOBOT_REPO_DIR = BASE_DIR.parent / "nanobot"
NANOBOT_RUNTIME_WORKSPACE = (
(NANOBOT_WORKSPACE / "workspace")
if (NANOBOT_WORKSPACE / "workspace").exists()
else NANOBOT_WORKSPACE
)
INBOX_DIR = NANOBOT_RUNTIME_WORKSPACE / "inbox"
TOOL_JOB_TIMEOUT_SECONDS = 300.0
TOOL_JOB_RETENTION_SECONDS = 15 * 60
@asynccontextmanager
async def app_lifespan(app: FastAPI):
runtime = create_web_runtime(
repo_dir=NANOBOT_REPO_DIR,
inbox_dir=INBOX_DIR,
tool_job_timeout_seconds=TOOL_JOB_TIMEOUT_SECONDS,
tool_job_retention_seconds=TOOL_JOB_RETENTION_SECONDS,
list_sessions=list_web_sessions,
normalize_chat_id=normalize_session_chat_id,
)
app.state.runtime = runtime
try:
yield
finally:
await runtime.shutdown()
def create_app() -> FastAPI:
app = FastAPI(title="Nanobot SuperTonic Wisper Web", lifespan=app_lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/health")
async def health() -> JSONResponse:
return JSONResponse({"status": "ok"})
app.include_router(tools_router)
app.include_router(sessions_router)
app.include_router(cards_router)
app.include_router(workbench_router)
app.include_router(inbox_router)
app.include_router(templates_router)
app.include_router(messages_router)
app.include_router(rtc_router)
if DIST_DIR.exists():
assets_dir = DIST_DIR / "assets"
if assets_dir.exists():
app.mount("/assets", StaticFiles(directory=str(assets_dir)), name="assets")
if CARD_TEMPLATES_DIR.exists():
app.mount(
"/card-templates",
StaticFiles(directory=str(CARD_TEMPLATES_DIR)),
name="card-templates",
)
@app.get("/{full_path:path}")
async def spa_fallback(full_path: str) -> FileResponse:
candidate = DIST_DIR / full_path
if candidate.is_file():
return FileResponse(str(candidate))
response = FileResponse(str(DIST_DIR / "index.html"))
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
return response
return app
app = create_app()