"""Chat routes — chat upload, chat JSON, message status, sessions CRUD.""" import base64 import shutil from typing import Optional, List from fastapi import ( APIRouter, HTTPException, Depends, Query, BackgroundTasks, UploadFile, File, Form, Body, ) from fastapi.security import HTTPBearer from config import VERSION, GENAI_MODELS, TERRAFORM_DIR, log from database import db from auth.jwt_auth import current_user, _audit from models import ChatMsg from services.chat_background import _chat_start, _chat_background router = APIRouter() # ── Upload constants ────────────────────────────────────────────────────────── MAX_UPLOAD_FILES = 5 MAX_UPLOAD_SIZE = 10 * 1024 * 1024 # 10MB IMAGE_MIMES = {"image/png", "image/jpeg", "image/gif", "image/webp", "image/bmp"} DOC_MIMES = {"application/pdf"} # ── Chat endpoints ──────────────────────────────────────────────────────────── @router.post("/api/chat/upload") async def chat_with_files( bg: BackgroundTasks, message: str = Form(""), session_id: Optional[str] = Form(None), genai_config_id: Optional[str] = Form(None), model_id: Optional[str] = Form(None), oci_config_id: Optional[str] = Form(None), genai_region: Optional[str] = Form(None), compartment_id: Optional[str] = Form(None), use_tools: Optional[bool] = Form(True), temperature: Optional[float] = Form(None), max_tokens: Optional[int] = Form(None), top_p: Optional[float] = Form(None), top_k: Optional[int] = Form(None), frequency_penalty: Optional[float] = Form(None), presence_penalty: Optional[float] = Form(None), reasoning_effort: Optional[str] = Form(None), files: List[UploadFile] = File(default=[]), u=Depends(current_user), ): if len(files) > MAX_UPLOAD_FILES: raise HTTPException(400, f"Máximo {MAX_UPLOAD_FILES} arquivos por mensagem") attachments = [] file_names = [] for f in files: data = await f.read() if len(data) > MAX_UPLOAD_SIZE: raise HTTPException(400, f"Arquivo {f.filename} excede 10MB") mime = f.content_type or "application/octet-stream" b64 = base64.b64encode(data).decode() data_uri = f"data:{mime};base64,{b64}" if mime in IMAGE_MIMES: attachments.append({"type": "image", "mime": mime, "data_uri": data_uri}) elif mime in DOC_MIMES: attachments.append({"type": "document", "mime": mime, "data_uri": data_uri}) else: try: text = data.decode("utf-8", errors="replace") except Exception as e: log.warning(f"UTF-8 decode failed for file attachment, falling back to latin-1: {e}") text = data.decode("latin-1", errors="replace") message = f"{message}\n\n--- Conteúdo de {f.filename} ---\n{text[:50000]}" file_names.append(f.filename) msg = ChatMsg( message=message or "Analise os arquivos anexados.", session_id=session_id, genai_config_id=genai_config_id, model_id=model_id, oci_config_id=oci_config_id, genai_region=genai_region, compartment_id=compartment_id, use_tools=use_tools if use_tools is not None else True, temperature=temperature, max_tokens=max_tokens, top_p=top_p, top_k=top_k, frequency_penalty=frequency_penalty, presence_penalty=presence_penalty, reasoning_effort=reasoning_effort, ) sid, mid_or_result, genai_cfg = _chat_start(msg, u, attachments=attachments if attachments else None) if genai_cfg is None: return mid_or_result # immediate fallback response if file_names: with db() as c: c.execute("UPDATE chat_messages SET content = content || ? WHERE session_id=? AND role='user' AND status='done' ORDER BY created_at DESC LIMIT 1", (f"\n[📎 {', '.join(file_names)}]", sid)) bg.add_task(_chat_background, mid_or_result, sid, msg, dict(u), genai_cfg, attachments if attachments else None) return {"message_id": mid_or_result, "session_id": sid, "status": "processing"} @router.post("/api/chat") async def chat(msg: ChatMsg, bg: BackgroundTasks, u=Depends(current_user)): sid, mid_or_result, genai_cfg = _chat_start(msg, u) if genai_cfg is None: return mid_or_result # immediate fallback response bg.add_task(_chat_background, mid_or_result, sid, msg, dict(u), genai_cfg) return {"message_id": mid_or_result, "session_id": sid, "status": "processing"} @router.get("/api/chat/{mid}/status") async def chat_message_status(mid: str, u=Depends(current_user)): with db() as c: r = c.execute("SELECT id, session_id, role, content, model_id, status FROM chat_messages WHERE id=?", (mid,)).fetchone() if not r: raise HTTPException(404, "Message not found") return dict(r) # ── Local fallback agent (no GenAI configured) ─────────────────────────────── def _agent_respond(msg, user): m = msg.lower().strip() if any(k in m for k in ["status","health","como está","saúde"]): cli = "✅ Instalado" if shutil.which("oci") else "❌ Não instalado" with db() as c: nc=c.execute("SELECT COUNT(*) n FROM oci_configs").fetchone()["n"] nr=c.execute("SELECT COUNT(*) n FROM reports").fetchone()["n"] nu=c.execute("SELECT COUNT(*) n FROM users WHERE is_active=1").fetchone()["n"] nm=c.execute("SELECT COUNT(*) n FROM mcp_servers WHERE is_active=1").fetchone()["n"] ng=c.execute("SELECT COUNT(*) n FROM genai_configs").fetchone()["n"] return (f"📊 **Status do Sistema**\n\n• OCI CLI: {cli}\n• Configs OCI: {nc}\n• GenAI Models: {ng}\n• MCP Servers: {nm}\n• Relatórios: {nr}\n• Usuários ativos: {nu}\n• Servidor: ✅ Online\n• Versão: v{VERSION}") if any(k in m for k in ["ajuda","help","comandos"]): return ("🤖 **Comandos disponíveis:**\n\n• `status` — Status do sistema\n• `listar configs` — Configurações OCI\n• `verificar cis` — Checks CIS 3.0\n• `modelos` — Modelos GenAI disponíveis\n• `ajuda` — Esta mensagem\n\n💡 Configure um modelo GenAI para chat com IA.") if any(k in m for k in ["modelo","modelos","genai"]): lines = ["🧠 **Modelos OCI Generative AI disponíveis:**\n"] for mid, info in GENAI_MODELS.items(): lines.append(f"• `{mid}` — {info['name']} ({info['provider']})") lines.append("\nConfigure em **GenAI Config** para usar no chat.") return "\n".join(lines) if any(k in m for k in ["cis","benchmark","checks","verificar"]): return ("🔒 **CIS OCI Foundations Benchmark 3.0**\n\n54 controles em 8 domínios:\n• IAM: 17 controles\n• Networking: 8 controles\n• Compute: 3 controles\n• Logging & Monitoring: 18 controles\n• Storage: 6 controles\n• Asset Management: 2 controles\n\nConfigure OCI e execute um relatório na aba **Report**.") return ("Sou o **AI Agent v" + VERSION + "** — Infrastructure & Security Engineer. Sem modelo GenAI configurado, uso respostas locais.\n\n" "Para chat com IA:\n1. Configure **OCI Credentials**\n2. Configure **GenAI** com modelo e região\n3. Selecione o modelo no chat\n\nDigite **ajuda** para ver os comandos.") # ── Chat sessions ───────────────────────────────────────────────────────────── @router.get("/api/chat/sessions") async def list_chat_sessions(agent_type: str = "chat", limit: int = 50, u=Depends(current_user)): with db() as c: rows = c.execute( """SELECT cs.id, cs.title, cs.agent_type, cs.created_at, cs.updated_at, (SELECT COUNT(*) FROM chat_messages cm WHERE cm.session_id=cs.id) as message_count FROM chat_sessions cs WHERE cs.user_id=? AND cs.agent_type=? ORDER BY cs.updated_at DESC LIMIT ?""", (u["id"], agent_type, limit)).fetchall() return [dict(r) for r in rows] @router.get("/api/chat/sessions/{sid}/messages") async def get_session_messages(sid: str, skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=500), u=Depends(current_user)): with db() as c: session = c.execute("SELECT * FROM chat_sessions WHERE id=? AND user_id=?", (sid, u["id"])).fetchone() if not session: raise HTTPException(404, "Session not found") total = c.execute( "SELECT COUNT(*) as cnt FROM chat_messages " "WHERE session_id=? AND status='done'", (sid,)).fetchone()["cnt"] msgs = c.execute( "SELECT id, role, content, model_id, status, created_at FROM chat_messages " "WHERE session_id=? AND status='done' ORDER BY created_at ASC LIMIT ? OFFSET ?", (sid, limit, skip)).fetchall() return {"session": dict(session), "messages": [dict(m) for m in msgs], "total": total} @router.put("/api/chat/sessions/{sid}/title") async def rename_session(sid: str, req: dict = Body(...), u=Depends(current_user)): with db() as c: c.execute("UPDATE chat_sessions SET title=? WHERE id=? AND user_id=?", (req.get("title", "")[:120], sid, u["id"])) return {"ok": True} @router.delete("/api/chat/{sid}") async def clear_chat(sid, u=Depends(current_user)): with db() as c: # Delete associated terraform workspaces and their files ws_rows = c.execute("SELECT id FROM terraform_workspaces WHERE session_id=? AND user_id=?", (sid, u["id"])).fetchall() for ws in ws_rows: wdir = TERRAFORM_DIR / ws["id"] if wdir.exists(): shutil.rmtree(wdir, ignore_errors=True) c.execute("DELETE FROM terraform_workspaces WHERE session_id=? AND user_id=?", (sid, u["id"])) c.execute("DELETE FROM chat_messages WHERE session_id=? AND user_id=?", (sid, u["id"])) c.execute("DELETE FROM chat_sessions WHERE id=? AND user_id=?", (sid, u["id"])) return {"ok": True}