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})