Files
A-Team-Security-Infra-Agent…/backend/routes/chat.py
nogueiraguh 1135e9d6a9 refactor: decompose monolith — app.py 10,600→143 lines, 30+ modules
Fase 0 complete: extract all business logic, auth, database, and 176
endpoints from monolithic app.py into dedicated modules.

Structure:
- app.py: FastAPI hub (CORS, routers, startup/shutdown)
- models.py: 13 Pydantic request models
- utils.py: shared utilities (embed status, upload validation, process registries)
- config.py: constants, env vars, model catalogs
- auth/: crypto, jwt_auth, oidc, rate_limit
- database/: db(), init_db(), schema DDL
- services/: genai, compliance, chat, terraform, embeddings, cis_reports, mcp
- routes/: 13 APIRouter modules (auth, users, oci_config, oci_explorer,
  genai, mcp, adb, embeddings, reports, chat, terraform, settings, cis_engine)

Also: README updated with OCIR auth instructions for manual docker run.

86 tests passing.
2026-04-06 15:20:10 -03:00

201 lines
10 KiB
Python

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