nanobot-voice-interface/routes/sessions.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

132 lines
4.4 KiB
Python

from __future__ import annotations
import asyncio
import uuid
from fastapi import APIRouter, Depends, Request
from fastapi.responses import JSONResponse, StreamingResponse
from app_dependencies import get_runtime
from message_pipeline import encode_sse_data
from route_helpers import read_json_request
from session_store import (
create_session,
delete_session,
is_web_session_chat_id,
list_web_sessions,
load_session_payload,
normalize_session_chat_id,
rename_session,
)
from web_runtime import WebAppRuntime
router = APIRouter()
@router.get("/events")
async def stream_ui_events(request: Request, runtime: WebAppRuntime = Depends(get_runtime)):
chat_id = normalize_session_chat_id(str(request.query_params.get("chat_id", "web") or "web"))
if not chat_id:
return JSONResponse({"error": "invalid chat id"}, status_code=400)
subscription_id, queue = await runtime.event_bus.subscribe(chat_id)
async def stream_events():
try:
yield ": stream-open\n\n"
yield encode_sse_data({"type": "events.ready", "chat_id": chat_id})
while True:
if await request.is_disconnected():
break
try:
payload = await asyncio.wait_for(queue.get(), timeout=20.0)
except asyncio.TimeoutError:
yield ": keepalive\n\n"
continue
yield encode_sse_data(payload)
finally:
await runtime.event_bus.unsubscribe(subscription_id)
return StreamingResponse(
stream_events(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
},
)
@router.get("/sessions")
async def get_sessions() -> JSONResponse:
return JSONResponse({"sessions": list_web_sessions()})
@router.get("/sessions/{chat_id}")
async def get_session(chat_id: str) -> JSONResponse:
session_chat_id = normalize_session_chat_id(chat_id)
if not session_chat_id or not is_web_session_chat_id(session_chat_id):
return JSONResponse({"error": "invalid session id"}, status_code=400)
payload = load_session_payload(session_chat_id)
if payload is None:
return JSONResponse({"error": "session not found"}, status_code=404)
return JSONResponse(payload)
@router.post("/sessions")
async def create_session_route(
request: Request,
runtime: WebAppRuntime = Depends(get_runtime),
) -> JSONResponse:
try:
payload = await read_json_request(request)
except ValueError as exc:
return JSONResponse({"error": str(exc)}, status_code=400)
title = str(payload.get("title", "")).strip()
chat_id = f"web-{uuid.uuid4().hex[:8]}"
summary = create_session(chat_id, title=title)
await runtime.publish_sessions_changed()
return JSONResponse({"session": summary}, status_code=201)
@router.patch("/sessions/{chat_id}")
async def rename_session_route(
chat_id: str,
request: Request,
runtime: WebAppRuntime = Depends(get_runtime),
) -> JSONResponse:
session_chat_id = normalize_session_chat_id(chat_id)
if not session_chat_id or not is_web_session_chat_id(session_chat_id):
return JSONResponse({"error": "invalid session id"}, status_code=400)
try:
payload = await read_json_request(request)
except ValueError as exc:
return JSONResponse({"error": str(exc)}, status_code=400)
title = str(payload.get("title", "")).strip()
summary = rename_session(session_chat_id, title)
if summary is None:
return JSONResponse({"error": "session not found"}, status_code=404)
await runtime.publish_sessions_changed()
return JSONResponse({"session": summary})
@router.delete("/sessions/{chat_id}")
async def delete_session_route(
chat_id: str,
runtime: WebAppRuntime = Depends(get_runtime),
) -> JSONResponse:
session_chat_id = normalize_session_chat_id(chat_id)
if not session_chat_id or not is_web_session_chat_id(session_chat_id):
return JSONResponse({"error": "invalid session id"}, status_code=400)
if not delete_session(session_chat_id, runtime.delete_cards_for_chat):
return JSONResponse({"error": "session not found"}, status_code=404)
await runtime.publish_sessions_changed()
await runtime.publish_cards_changed()
return JSONResponse({"status": "ok", "chat_id": session_chat_id})