feat: unify card runtime and event-driven web ui
This commit is contained in:
parent
0edf8c3fef
commit
4dfb7ca3cc
105 changed files with 17382 additions and 8505 deletions
169
routes/inbox.py
Normal file
169
routes/inbox.py
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
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})
|
||||
Loading…
Add table
Add a link
Reference in a new issue