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.
This commit is contained in:
200
backend/routes/chat.py
Normal file
200
backend/routes/chat.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""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}
|
||||
Reference in New Issue
Block a user