"""AI Copilot routes — query, execute, history. .. deprecated:: Phase 2 (ARCH-059) Legacy module, not wired into main.py and without frontend consumers. Scheduled for removal together with app/services/ai_copilot_service.py and the ``ai_conversations`` tables (see service docstring for plan). """ from __future__ import annotations import uuid from fastapi import APIRouter, Depends, HTTPException, Query, Request, status from sqlalchemy.ext.asyncio import AsyncSession from app.core.db import get_db from app.core.rate_limit import RateLimitPolicy, check_rate_limit_policy from app.deps import require_permission from app.schemas.ai_copilot import ( CopilotExecuteRequest, CopilotQueryRequest, ) from app.services import ai_copilot_service router = APIRouter(prefix="/api/v1/ai/copilot", tags=["ai-copilot"]) @router.post("/query") async def copilot_query( body: CopilotQueryRequest, request: Request, db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("ai:write")), ): """Process a natural language query and return proposed actions. Returns proposed_actions array for user confirmation. Does NOT execute any actions — user must call /execute to confirm. """ tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) # Rate limit — AI policy (cost-sensitive LLM call) await check_rate_limit_policy( f"rate:ai:copilot:{tenant_id}:{user_id}", RateLimitPolicy.AI, ) result = await ai_copilot_service.process_query( db, tenant_id, user_id, query=body.query, conversation_id=body.conversation_id, context=body.context, ) if "error" in result and result.get("status_code") == 404: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={"detail": result["error"], "code": "conversation_not_found"}, ) return result @router.post("/execute") async def copilot_execute( body: CopilotExecuteRequest, request: Request, db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("ai:write")), ): """Execute a proposed action after user confirmation. RBAC is enforced — the user must have permission for the action. Returns 200 with execution result or 403 if RBAC blocks. """ tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) # Rate limit — AI policy (cost-sensitive LLM call) await check_rate_limit_policy( f"rate:ai:copilot:{tenant_id}:{user_id}", RateLimitPolicy.AI, ) resolved = { "permissions": current_user.get("permissions", []), "denied": current_user.get("denied_permissions", []), "field_permissions": current_user.get("field_permissions", {}), "is_system_admin": current_user.get("is_system_admin", False), } result = await ai_copilot_service.execute_action( db, tenant_id, user_id, resolved, conversation_id=body.conversation_id, action=body.action.model_dump(), ) if result.get("status_code") == 404: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={"detail": result.get("error", "Not found"), "code": "not_found"}, ) if result.get("status_code") == 403: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail={"detail": result.get("error", "Insufficient permissions"), "code": "forbidden"}, ) return result @router.get("/history") async def copilot_history( page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("ai:read")), ): """Get paginated conversation history for the current user.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) return await ai_copilot_service.get_history( db, tenant_id, user_id, page=page, page_size=page_size, )