nanobot-voice-interface/routes/messages.py

115 lines
4 KiB
Python
Raw Normal View History

from __future__ import annotations
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, Request
from fastapi.responses import JSONResponse, StreamingResponse
from app_dependencies import get_runtime
from message_pipeline import (
context_label_from_message_metadata,
encode_sse_data,
typed_message_from_api_notification,
)
from nanobot_api_client import stream_nanobot_agent_turn
from route_helpers import read_json_request
from session_store import normalize_session_chat_id
from web_runtime import WebAppRuntime
router = APIRouter()
@router.post("/message")
async def post_message(
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)
text = str(payload.get("text", "")).strip()
metadata = payload.get("metadata", {})
chat_id = normalize_session_chat_id(str(payload.get("chat_id", "web") or "web"))
if not text:
return JSONResponse({"error": "empty message"}, status_code=400)
if not chat_id:
return JSONResponse({"error": "invalid chat id"}, status_code=400)
if not isinstance(metadata, dict):
metadata = {}
try:
await runtime.gateway.send_user_message(text, metadata=metadata, chat_id=chat_id)
except RuntimeError as exc:
return JSONResponse({"error": str(exc)}, status_code=503)
return JSONResponse({"status": "ok"})
@router.post("/message/stream")
async def stream_message(
request: Request,
runtime: WebAppRuntime = Depends(get_runtime),
):
try:
payload = await read_json_request(request)
except ValueError as exc:
return JSONResponse({"error": str(exc)}, status_code=400)
text = str(payload.get("text", "")).strip()
metadata = payload.get("metadata", {})
chat_id = normalize_session_chat_id(str(payload.get("chat_id", "web") or "web"))
if not text:
return JSONResponse({"error": "empty message"}, status_code=400)
if not chat_id:
return JSONResponse({"error": "invalid chat id"}, status_code=400)
if not isinstance(metadata, dict):
metadata = {}
context_label = context_label_from_message_metadata(metadata)
async def stream_turn():
try:
yield ": stream-open\n\n"
yield encode_sse_data(
{
"type": "message",
"role": "user",
"content": text,
"is_progress": False,
"is_tool_hint": False,
"timestamp": datetime.now(timezone.utc).isoformat(),
"context_label": context_label or None,
}
)
async for typed_event in stream_nanobot_agent_turn(
text,
chat_id=chat_id,
metadata=metadata,
timeout_seconds=60.0,
notification_to_event=lambda obj: typed_message_from_api_notification(
obj,
default_chat_id=chat_id,
),
):
if typed_event.get("type") == "card":
await runtime.publish_cards_changed(
typed_event.get("chat_id") if isinstance(typed_event, dict) else chat_id
)
elif typed_event.get("type") == "workbench":
await runtime.publish_workbench_changed(
typed_event.get("chat_id") if isinstance(typed_event, dict) else chat_id
)
yield encode_sse_data(typed_event)
await runtime.publish_sessions_changed()
except RuntimeError as exc:
yield encode_sse_data({"type": "error", "error": str(exc)})
return StreamingResponse(
stream_turn(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
},
)