nanobot-voice-interface/routes/inbox.py

170 lines
5.9 KiB
Python
Raw Normal View History

from __future__ import annotations
from fastapi import APIRouter, Depends, Request
from fastapi.responses import JSONResponse
from app_dependencies import get_runtime
from message_pipeline import typed_message_from_api_notification
from nanobot_api_client import run_nanobot_agent_turn
from route_helpers import read_json_request
from web_runtime import WebAppRuntime
router = APIRouter()
@router.get("/inbox")
async def get_inbox(
request: Request, runtime: WebAppRuntime = Depends(get_runtime)
) -> JSONResponse:
status_filter = str(request.query_params.get("status", "") or "").strip() or None
kind_filter = str(request.query_params.get("kind", "") or "").strip() or None
include_closed = str(request.query_params.get("include_closed", "") or "").strip().lower() in {
"1",
"true",
"yes",
"on",
}
limit_raw = str(request.query_params.get("limit", "") or "").strip()
try:
limit = max(1, min(int(limit_raw), 50)) if limit_raw else None
except ValueError:
return JSONResponse({"error": "limit must be an integer"}, status_code=400)
tags_raw = request.query_params.getlist("tag")
tags: list[str] = []
for raw in tags_raw:
for part in str(raw).split(","):
part_clean = part.strip()
if part_clean:
tags.append(part_clean)
try:
items = runtime.inbox_service.list_items(
status_filter=status_filter,
kind_filter=kind_filter,
include_closed=include_closed,
limit=limit,
tags=tags,
)
except ValueError as exc:
return JSONResponse({"error": str(exc)}, status_code=400)
except Exception as exc:
return JSONResponse({"error": f"failed to load inbox: {exc}"}, status_code=500)
return JSONResponse({"items": items})
@router.post("/inbox/capture")
async def capture_inbox_item(
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", "") or "")
text = str(payload.get("text", "") or "")
kind = str(payload.get("kind", "unknown") or "unknown")
source = str(payload.get("source", "web-ui") or "web-ui")
due = str(payload.get("due", "") or "")
body = str(payload.get("body", "") or "")
session_id = str(payload.get("session_id", "") or "")
raw_tags = payload.get("tags", [])
tags = [str(tag) for tag in raw_tags] if isinstance(raw_tags, list) else []
confidence_value = payload.get("confidence")
confidence = confidence_value if isinstance(confidence_value, (int, float)) else None
try:
item = runtime.inbox_service.capture_item(
title=title,
text=text,
kind=kind,
source=source,
due=due,
body=body,
session_id=session_id,
tags=tags,
confidence=confidence,
)
except ValueError as exc:
return JSONResponse({"error": str(exc)}, status_code=400)
except Exception as exc:
return JSONResponse({"error": f"failed to capture inbox item: {exc}"}, status_code=500)
await runtime.publish_inbox_changed()
return JSONResponse({"status": "ok", "item": item}, status_code=201)
@router.post("/inbox/accept-task")
async def accept_inbox_item_as_task(
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)
item = str(payload.get("item", "") or "").strip()
lane = str(payload.get("lane", "backlog") or "backlog").strip()
title = str(payload.get("title", "") or "")
due = str(payload.get("due", "") or "")
body = payload.get("body")
body_text = str(body) if isinstance(body, str) else None
raw_tags = payload.get("tags")
tags = [str(tag) for tag in raw_tags] if isinstance(raw_tags, list) else None
if not item:
return JSONResponse({"error": "item is required"}, status_code=400)
try:
result = await runtime.inbox_service.accept_item_as_task(
item=item,
lane=lane,
title=title,
due=due,
body_text=body_text,
tags=tags,
run_nanobot_turn=run_nanobot_agent_turn,
notification_to_event=lambda obj: typed_message_from_api_notification(
obj,
default_chat_id="inbox-groomer",
),
)
except (FileNotFoundError, ValueError) as exc:
return JSONResponse({"error": str(exc)}, status_code=400)
except RuntimeError as exc:
return JSONResponse({"error": str(exc)}, status_code=503)
except Exception as exc:
return JSONResponse({"error": f"failed to accept inbox item: {exc}"}, status_code=500)
await runtime.publish_inbox_changed()
await runtime.publish_cards_changed()
return JSONResponse(result)
@router.post("/inbox/dismiss")
async def dismiss_inbox_item(
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)
item = str(payload.get("item", "") or "").strip()
if not item:
return JSONResponse({"error": "item is required"}, status_code=400)
try:
item_payload = runtime.inbox_service.dismiss_item(item)
except (FileNotFoundError, ValueError) as exc:
return JSONResponse({"error": str(exc)}, status_code=400)
except Exception as exc:
return JSONResponse({"error": f"failed to dismiss inbox item: {exc}"}, status_code=500)
await runtime.publish_inbox_changed()
return JSONResponse({"status": "ok", "item": item_payload})