# retoor import logging import uuid_utils from fastapi import APIRouter, Request, WebSocket, WebSocketDisconnect from fastapi.responses import JSONResponse, RedirectResponse from devplacepy import stealth from devplacepy.constants import DEVII_GUEST_COOKIE from devplacepy.database import get_int_setting, mark_notifications_read_by_target from devplacepy.seo import base_seo_context, site_url from devplacepy.services.manager import service_manager from devplacepy.responses import respond from devplacepy.schemas import DeviiPageOut from devplacepy.utils import ( _user_from_api_key, _user_from_session, get_current_user, is_admin, is_primary_admin, require_user, ) from devplacepy.services.audit import record as audit logger = logging.getLogger("devii.router") router = APIRouter() GUEST_COOKIE = DEVII_GUEST_COOKIE GUEST_COOKIE_MAX_AGE = 31536000 CHANNELS = frozenset({"main", "docs"}) def _service(): return service_manager.get_service("devii") def _bearer_key(headers) -> str: key = headers.get("x-api-key", "") if key: return key.strip() scheme, _, credentials = headers.get("authorization", "").partition(" ") if scheme.lower() == "bearer": return credentials.strip() return "" def _resolve_ws_owner(websocket: WebSocket): user = _user_from_session(websocket) if not user: key = _bearer_key(websocket.headers) if key: user = _user_from_api_key(key) if user: return ( "user", user["uid"], user.get("username", ""), user.get("api_key", ""), is_admin(user), is_primary_admin(user), ) guest_id = websocket.cookies.get(GUEST_COOKIE) or "" return "guest", guest_id, "guest", "", False, False def _owner_from_request(request: Request): user = get_current_user(request) if user: return "user", user["uid"], user.get("username", "") guest_id = request.cookies.get(GUEST_COOKIE) or "" return "guest", guest_id, "guest" @router.get("/") async def devii_page(request: Request): user = get_current_user(request) if user: mark_notifications_read_by_target(user["uid"], "/devii") seo_ctx = base_seo_context( request, title="Devii", description="Your personal AI development assistant on DevPlace.", robots="noindex,nofollow", ) response = respond( request, "devii.html", { **seo_ctx, "request": request, "user": user, }, model=DeviiPageOut, ) if not user and not request.cookies.get(GUEST_COOKIE): response.set_cookie( GUEST_COOKIE, uuid_utils.uuid7().hex, httponly=True, samesite="lax", max_age=GUEST_COOKIE_MAX_AGE, ) return response @router.get("/session") async def devii_session(request: Request): user = get_current_user(request) svc = _service() payload = { "loggedIn": bool(user), "username": user.get("username") if user else "guest", "enabled": bool(svc and svc.is_enabled()), "guestsEnabled": bool(svc and svc.guests_enabled()), "baseUrl": site_url(request), } response = JSONResponse(payload) if not user and not request.cookies.get(GUEST_COOKIE): response.set_cookie( GUEST_COOKIE, uuid_utils.uuid7().hex, httponly=True, samesite="lax", max_age=GUEST_COOKIE_MAX_AGE, ) return response def _validate_target(value: str) -> str: if value.startswith("/") and not value.startswith("//"): return value return "/" @router.get("/adopt") async def devii_adopt(request: Request): # The terminal's agent logged in; adopt the real session it minted into the browser so both # share one session, then return to where the user was. target = _validate_target(request.query_params.get("next", "/")) response = RedirectResponse(target, status_code=303) svc = _service() if svc is None: return response owner_kind, owner_id, _ = _owner_from_request(request) session = svc.hub().find(owner_kind, owner_id) if owner_id else None token = session.take_pending_session() if session else None if token: max_age = max(1, get_int_setting("session_remember_days", 30)) * 86400 response.set_cookie( "session", token, max_age=max_age, httponly=True, samesite="lax" ) return response @router.get("/usage") async def devii_usage(request: Request): svc = _service() owner_kind, owner_id, _ = _owner_from_request(request) if svc is None: return JSONResponse({"used_pct": 0.0, "turns_today": 0}) viewer_is_admin = is_admin(get_current_user(request)) spent = svc.spent_24h(owner_kind, owner_id) if owner_id else 0.0 limit = svc.daily_limit_for(owner_kind, viewer_is_admin) turns = svc.hub().ledger.turns_24h(owner_kind, owner_id) if owner_id else 0 used_pct = round(min(100.0, spent / limit * 100), 1) if limit > 0 else 0.0 payload = { "used_pct": used_pct, "turns_today": turns, "owner_kind": owner_kind, } if viewer_is_admin: payload["spent_24h"] = round(spent, 6) payload["limit"] = limit return JSONResponse(payload) @router.post("/clippy/ai/chat") async def clippy_proxy(request: Request): user = require_user(request) svc = _service() if svc is None or not svc.is_enabled(): return JSONResponse({"error": "Devii is unavailable"}, status_code=503) cfg = svc.effective_config() body = await request.body() headers = { "Content-Type": "application/json", "X-App-Reference": "devplace-devii-v-1-0-0", } if cfg.get("devii_ai_key"): headers["Authorization"] = f"Bearer {cfg['devii_ai_key']}" async with stealth.stealth_async_client(timeout=45.0) as client: upstream = await client.post(cfg["devii_ai_url"], content=body, headers=headers) audit.record( request, "ai.clippy.chat", summary="clippy AI chat", metadata={"status_code": upstream.status_code}, ) return JSONResponse( content=upstream.json() if upstream.headers.get("content-type", "").startswith("application/json") else {"raw": upstream.text}, status_code=upstream.status_code, ) @router.websocket("/ws") async def devii_ws(websocket: WebSocket): await websocket.accept() svc = _service() if svc is None or not svc.is_enabled(): await websocket.close(code=1013) return if not service_manager.owns_lock(): await websocket.close(code=4013) return channel = websocket.query_params.get("channel", "main") if channel not in CHANNELS: channel = "main" owner_kind, owner_id, username, api_key, owner_is_admin, owner_is_primary_admin = ( _resolve_ws_owner(websocket) ) if owner_kind == "guest" and not svc.guests_enabled(): await websocket.send_json( {"type": "error", "text": "Guest access to Devii is disabled."} ) await websocket.close(code=1008) return if owner_kind == "guest" and not owner_id: await websocket.send_json( {"type": "error", "text": "Open Devii from the page to start a session."} ) await websocket.close(code=1008) return session = svc.hub().get_or_create( owner_kind, owner_id, username, api_key, svc.instance_base_url(), is_admin=owner_is_admin, is_primary_admin=owner_is_primary_admin, channel=channel, ) session.attach(websocket) try: await session.send_bootstrap(websocket) while True: data = await websocket.receive_json() kind = data.get("type") if kind == "input": text = str(data.get("text", "")).strip() if not text: continue command = text.lower().lstrip("/").strip() if command == "stop": await session.stop() continue if command == "reset": await session.reset() continue if await session.try_answer_interaction(text): continue if svc.quota_exceeded(owner_kind, owner_id, owner_is_admin): limit = svc.daily_limit_for(owner_kind, owner_is_admin) audit.record_system( "ai.quota.exceeded", actor_kind=owner_kind if owner_kind == "guest" else "user", actor_uid=owner_id if owner_kind == "user" else None, actor_username=username, actor_role="admin" if owner_is_admin else ("guest" if owner_kind == "guest" else "member"), origin="devii", via_agent=1, result="denied", summary=f"AI request by {username or owner_id} blocked - 24h quota reached", metadata={"limit_usd": limit, "owner_kind": owner_kind}, ) await websocket.send_json( { "type": "error", "text": "Daily AI quota reached (100%). Try again later.", } ) continue session.spawn_turn(text) elif kind == "reset": await session.reset() elif kind == "stop": await session.stop() elif kind == "clear": await session.reset_conversation() elif kind == "visibility": session.set_visibility( websocket, bool(data.get("visible", True)), bool(data.get("focused", False)), ) elif kind == "clientinfo": offset = data.get("offset") session.set_clientinfo( str(data.get("timezone", "") or "").strip(), int(offset) if isinstance(offset, (int, float)) else None, ) elif kind in ("avatar_result", "client_result"): session.resolve_query(str(data.get("id", "")), data.get("result")) elif kind == "interaction_result": session.resolve_interaction( str(data.get("id", data.get("interaction_id", ""))), data.get("result") or data, ) except WebSocketDisconnect: pass except Exception: # noqa: BLE001 - never let the socket loop crash the worker logger.exception("Devii websocket loop failed for %s/%s", owner_kind, owner_id) finally: session.detach(websocket)