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:
nogueiraguh
2026-04-06 15:20:10 -03:00
parent 426bde563e
commit 1135e9d6a9
36 changed files with 11179 additions and 10514 deletions

View File

@@ -0,0 +1,469 @@
"""Chat Agent background processing — GenAI call, RAG, MCP tool use."""
import os, json, uuid, time, re, asyncio, concurrent.futures
from datetime import datetime, timedelta
from typing import Optional, List, Dict, Any
from fastapi import HTTPException
from config import (
DATA, OCI_DIR, REPORTS, log, _chat_executor,
COMPACT_TOKEN_THRESHOLD, COMPACT_KEEP_RECENT,
)
from database import db
from auth.crypto import _make_token, _safe_dec
from auth.jwt_auth import _audit, _chat_log, _verify_config_access
from services.genai import (
_call_genai, _embed_text, _build_rag_context,
_get_active_adb_configs, _get_tables_for_config,
_resolve_embed_config, _DIM_TO_MODEL, _get_table_embedding_dim,
_vector_search_multi, _relevant_tables,
_estimate_tokens, _should_compact, _compact_history,
RAG_CONTEXT_TEMPLATE, RAG_DEFAULT_SYSTEM_PROMPT,
)
def _chat_start(msg: "ChatMsg", u, attachments: list = None, agent_type: str = "chat"):
"""Start a chat: save user msg, resolve config, return (sid, mid, genai_cfg) or immediate response.
If genai_cfg is None, returns immediate fallback response in mid field as dict."""
is_new = not msg.session_id
sid = msg.session_id or str(uuid.uuid4())
with db() as c:
c.execute("INSERT INTO chat_messages (id,session_id,user_id,role,content,model_id,status) VALUES (?,?,?,?,?,?,?)",
(str(uuid.uuid4()), sid, u["id"], "user", msg.message, None, "done"))
if is_new:
title = (msg.message or "Nova conversa")[:80].strip()
c.execute("INSERT OR IGNORE INTO chat_sessions (id,user_id,agent_type,title) VALUES (?,?,?,?)",
(sid, u["id"], agent_type, title))
else:
c.execute("UPDATE chat_sessions SET updated_at=datetime('now') WHERE id=?", (sid,))
genai_cfg = None
if msg.genai_config_id:
_verify_config_access("genai", msg.genai_config_id, u)
with db() as c:
row = c.execute("SELECT * FROM genai_configs WHERE id=?", (msg.genai_config_id,)).fetchone()
if row:
genai_cfg = dict(row)
if msg.temperature is not None: genai_cfg["temperature"] = msg.temperature
if msg.max_tokens is not None: genai_cfg["max_tokens"] = msg.max_tokens
if msg.top_p is not None: genai_cfg["top_p"] = msg.top_p
if msg.top_k is not None: genai_cfg["top_k"] = msg.top_k
if msg.frequency_penalty is not None: genai_cfg["frequency_penalty"] = msg.frequency_penalty
if msg.presence_penalty is not None: genai_cfg["presence_penalty"] = msg.presence_penalty
if msg.reasoning_effort is not None: genai_cfg["reasoning_effort"] = msg.reasoning_effort
elif msg.model_id and msg.oci_config_id:
_verify_config_access("oci", msg.oci_config_id, u)
with db() as c:
oci_row = c.execute("SELECT * FROM oci_configs WHERE id=?", (msg.oci_config_id,)).fetchone()
if not oci_row:
raise HTTPException(400, "OCI config not found")
region = msg.genai_region or oci_row["region"]
compartment = _safe_dec(oci_row["compartment_id"]) if oci_row["compartment_id"] else ""
if not compartment:
raise HTTPException(400, "compartment_id required")
genai_cfg = {
"oci_config_id": msg.oci_config_id,
"model_id": msg.model_id,
"model_ocid": None,
"compartment_id": compartment,
"genai_region": region,
"endpoint": f"https://inference.generativeai.{region}.oci.oraclecloud.com",
"serving_type": "ON_DEMAND",
"dedicated_endpoint_id": None,
"temperature": msg.temperature if msg.temperature is not None else 1.0,
"max_tokens": msg.max_tokens if msg.max_tokens is not None else 6000,
"top_p": msg.top_p if msg.top_p is not None else 0.95,
"top_k": msg.top_k if msg.top_k is not None else 1,
"frequency_penalty": msg.frequency_penalty if msg.frequency_penalty is not None else 0.0,
"presence_penalty": msg.presence_penalty if msg.presence_penalty is not None else 0.0,
"reasoning_effort": msg.reasoning_effort,
}
if not genai_cfg:
# No GenAI config — return immediate fallback
resp = _agent_respond(msg.message, u)
with db() as c:
c.execute("INSERT INTO chat_messages (id,session_id,user_id,role,content,model_id,status) VALUES (?,?,?,?,?,?,?)",
(str(uuid.uuid4()), sid, u["id"], "assistant", resp, None, "done"))
return sid, {"session_id": sid, "response": resp, "model_id": None, "status": "done"}, None
# Create placeholder assistant message for background processing
mid = str(uuid.uuid4())
with db() as c:
c.execute("INSERT INTO chat_messages (id,session_id,user_id,role,content,model_id,status) VALUES (?,?,?,?,?,?,?)",
(mid, sid, u["id"], "assistant", "", genai_cfg.get("model_id"), "processing"))
return sid, mid, genai_cfg
async def _chat_background(mid: str, sid: str, msg: "ChatMsg", user: dict, genai_cfg: dict, attachments: list = None, agent_type: str = "chat"):
"""Background worker — processes GenAI chat, updates DB when done."""
log.info(f"Chat background started: mid={mid}, sid={sid}")
try:
history = []
with db() as c:
prev = c.execute("SELECT role,content FROM chat_messages WHERE session_id=? AND role IN ('user','assistant') AND status='done' ORDER BY created_at ASC", (sid,)).fetchall()
history = [{"role":r["role"],"content":r["content"]} for r in prev]
# ── RAG: augment with vector context from ALL active ADB configs ──
# Resolve active tenancy for filtered vector search
rag_tenancy = None
active_oci_for_rag = genai_cfg.get("oci_config_id") or (msg.oci_config_id if hasattr(msg, 'oci_config_id') and msg.oci_config_id else None)
if active_oci_for_rag:
with db() as c:
oci_for_rag = c.execute("SELECT tenancy_name FROM oci_configs WHERE id=?", (active_oci_for_rag,)).fetchone()
if oci_for_rag:
rag_tenancy = oci_for_rag["tenancy_name"]
log.info(f"RAG: filtering by tenancy '{rag_tenancy}'")
rag_context = ""
# For short follow-up questions, enrich with previous context for better RAG search
import re as _re_chat
rag_query = msg.message
if len(msg.message.split()) <= 8 and history:
# Short question — get previous user messages (excluding current which is already in history)
prev_user_msgs = [h["content"] for h in history if h["role"] == "user" and h["content"] != msg.message]
if prev_user_msgs:
rag_query = prev_user_msgs[-1] + " " + msg.message
log.info(f"RAG: enriched short query → '{rag_query[:100]}'")
elif len(history) >= 2:
# Fallback: use last assistant response for context
last_assistant = [h["content"][:200] for h in history if h["role"] == "assistant"]
if last_assistant:
rag_query = last_assistant[-1] + " " + msg.message
log.info(f"RAG: enriched from assistant context")
# Detect CIS recommendation number for exact text filtering
cis_chat_match = _re_chat.search(r'(?:cis|recommendation)\s*(\d+\.\d+)', rag_query, _re_chat.IGNORECASE)
cis_chat_filter = f"CIS Recommendation: {cis_chat_match.group(1)}" if cis_chat_match else None
if cis_chat_filter:
log.info(f"RAG: detected CIS filter '{cis_chat_filter}'")
adb_cfgs = _get_active_adb_configs(user["id"]) if agent_type != "terraform" else []
rag_errors = []
if adb_cfgs:
all_documents = []
for adb_cfg in adb_cfgs:
try:
genai_linked = None
if adb_cfg.get("genai_config_id"):
with db() as c:
row = c.execute("SELECT * FROM genai_configs WHERE id=?", (adb_cfg["genai_config_id"],)).fetchone()
if row: genai_linked = dict(row)
emb_genai = _resolve_embed_config(oci_config_id=active_oci_for_rag, genai_cfg=genai_linked, user_id=user["id"])
if not emb_genai:
continue
default_model = adb_cfg.get("embedding_model_id", "cohere.embed-v4.0")
tables = _get_tables_for_config(adb_cfg["id"], active_only=True)
if not tables:
tables = [{"table_name": adb_cfg.get("table_name", "")}]
all_table_names = [t["table_name"] for t in tables if t["table_name"]]
# Smart skip: only search relevant tables
relevant = _relevant_tables(rag_query, all_table_names)
skipped = set(all_table_names) - set(relevant)
if skipped:
log.info(f"RAG: skipped {', '.join(skipped)} (not relevant)")
# Auto-detect embedding model (use first table's dim as representative)
emb_model = default_model
for tbl_name in relevant:
try:
dim = _get_table_embedding_dim(adb_cfg, tbl_name)
if dim and dim in _DIM_TO_MODEL:
emb_model = _DIM_TO_MODEL[dim]
break
except Exception:
pass
query_embedding = _embed_text(rag_query, emb_genai, emb_model)
# Single connection, multi-table search
tbl_top_k = 10 if cis_chat_filter else 3
docs = _vector_search_multi(adb_cfg, query_embedding, relevant,
top_k_per_table=tbl_top_k, tenancy=rag_tenancy,
text_filter=cis_chat_filter, user_id=user["id"])
if docs:
all_documents.extend(docs)
sources = {}
for d in docs:
sources[d["source"]] = sources.get(d["source"], 0) + 1
log.info(f"RAG: {len(docs)} docs — {', '.join(f'{k}:{v}' for k,v in sources.items())}")
except Exception as e:
err_short = str(e)[:150]
log.warning(f"RAG retrieval failed for {adb_cfg.get('config_name','?')}: {err_short}")
if "DPY-6001" in str(e) or "DPY-6005" in str(e) or "timeout" in str(e).lower() or "connect" in str(e).lower():
rag_errors.append(f"Não foi possível conectar ao ADB ({adb_cfg.get('config_name','?')})")
if all_documents:
all_documents.sort(key=lambda d: d["distance"])
top_limit = 15 if cis_chat_filter else 8
rag_context = _build_rag_context(all_documents[:top_limit], max_total_chars=16000 if cis_chat_filter else 12000)
# Append connection errors to context so LLM can inform the user
if rag_errors and not rag_context:
rag_context = "⚠️ AVISO: " + "; ".join(set(rag_errors)) + ". A base de conhecimento não está disponível no momento. Responda com base no seu conhecimento geral, informando que os dados do ADB não puderam ser consultados."
elif rag_errors:
rag_context += "\n\n⚠️ AVISO: Algumas bases não puderam ser consultadas: " + "; ".join(set(rag_errors))
cfg_dict = dict(genai_cfg)
with db() as c:
sp_row = c.execute("SELECT content FROM system_prompts WHERE agent=? AND is_active=1 LIMIT 1", (agent_type,)).fetchone()
global_prompt = sp_row["content"] if sp_row and sp_row["content"] else ""
if rag_context:
augmented_message = RAG_CONTEXT_TEMPLATE.format(context=rag_context, question=msg.message)
cfg_dict["system_prompt"] = global_prompt or RAG_DEFAULT_SYSTEM_PROMPT
else:
augmented_message = msg.message
if global_prompt:
cfg_dict["system_prompt"] = global_prompt
# ── Inject all config context into system prompt so model auto-resolves IDs ──
ctx_parts = []
active_oci_id = cfg_dict.get("oci_config_id") or (msg.oci_config_id if msg.oci_config_id else None)
with db() as c:
oci_cfgs = c.execute("SELECT id,tenancy_name,region,compartment_id FROM oci_configs WHERE user_id=?", (user["id"],)).fetchall()
genai_cfgs = c.execute("SELECT id,name,model_id,genai_region,compartment_id,oci_config_id FROM genai_configs WHERE user_id=?", (user["id"],)).fetchall()
adb_cfgs = c.execute("SELECT id,config_name,dsn,table_name,is_active,genai_config_id,embedding_model_id FROM adb_vector_configs WHERE user_id=? AND is_active=1", (user["id"],)).fetchall()
mcp_srvs = c.execute("SELECT id,name,description,is_active FROM mcp_servers WHERE is_active=1 AND (user_id=? OR EXISTS (SELECT 1 FROM users WHERE id=? AND role='admin'))", (user["id"], user["id"])).fetchall()
# ── Active OCI config (selected by user in this session) ──
if active_oci_id:
for oc in oci_cfgs:
if oc["id"] == active_oci_id:
comp = _safe_dec(oc["compartment_id"]) if oc["compartment_id"] else "N/A"
ctx_parts.append(
f"⚡ CONFIG OCI ATIVA (usar como config_id em TODAS as tools que precisarem): "
f"config_id=\"{oc['id']}\" tenancy=\"{oc['tenancy_name']}\" region=\"{oc['region']}\" compartment_id=\"{comp}\""
)
break
if oci_cfgs:
ctx_parts.append("\nTodas as configurações OCI disponíveis (use 'id' como config_id):")
for oc in oci_cfgs:
comp = _safe_dec(oc["compartment_id"]) if oc["compartment_id"] else "N/A"
active_tag = " ← ATIVA" if oc["id"] == active_oci_id else ""
ctx_parts.append(f" - id=\"{oc['id']}\" tenancy=\"{oc['tenancy_name']}\" region=\"{oc['region']}\" compartment_id=\"{comp}\"{active_tag}")
if genai_cfgs:
ctx_parts.append("\nConfigurações GenAI disponíveis (use 'id' como genai_config_id):")
for gc in genai_cfgs:
comp = _safe_dec(gc["compartment_id"]) if gc["compartment_id"] else "N/A"
ctx_parts.append(f" - id=\"{gc['id']}\" name=\"{gc['name']}\" model=\"{gc['model_id']}\" region=\"{gc['genai_region']}\" compartment_id=\"{comp}\"")
if adb_cfgs:
ctx_parts.append("\nConfigurações ADB Vector Store disponíveis (use 'id' como adb_config_id):")
for ac in adb_cfgs:
emb = ac["embedding_model_id"] if ac["embedding_model_id"] else "N/A"
ctx_parts.append(f" - id=\"{ac['id']}\" name=\"{ac['config_name']}\" table=\"{ac['table_name']}\" embedding_model=\"{emb}\"")
if mcp_srvs:
ctx_parts.append("\nMCP Servers ativos (tools disponíveis para uso):")
for ms in mcp_srvs:
desc = ms["description"] or ""
ctx_parts.append(f" - id=\"{ms['id']}\" name=\"{ms['name']}\" desc=\"{desc}\"")
if ctx_parts:
ctx_parts.append("\nIMPORTANTE: Use automaticamente a config OCI ATIVA como config_id em todas as tools. NUNCA peça config_id, tenancy ou IDs ao usuário — já estão definidos acima.")
config_hint = "\n".join(ctx_parts)
base_prompt = cfg_dict.get("system_prompt", "")
cfg_dict["system_prompt"] = f"{base_prompt}\n\n{config_hint}" if base_prompt else config_hint
# ── Terraform agent: boost max_tokens (capped at 65K to avoid context overflow) + force reasoning_effort=high ──
if agent_type == "terraform":
tf_model_info = GENAI_MODELS.get(cfg_dict.get("model_id", ""), {})
tf_model_max = tf_model_info.get("max_tokens", 32768)
cfg_dict["max_tokens"] = min(tf_model_max, 65000)
if tf_model_info.get("reasoning") and not cfg_dict.get("reasoning_effort"):
cfg_dict["reasoning_effort"] = "HIGH"
# ── Inject existing OCI resources for terraform agent ──
if agent_type == "terraform" and active_oci_id:
try:
# Determine compartment: from msg or from OCI config
tf_compartment = getattr(msg, 'compartment_id', None) or None
if not tf_compartment:
for oc in oci_cfgs:
if oc["id"] == active_oci_id:
tf_compartment = _safe_dec(oc["compartment_id"]) if oc["compartment_id"] else None
break
if tf_compartment:
tf_region = None
for oc in oci_cfgs:
if oc["id"] == active_oci_id:
tf_region = oc["region"]
break
loop = asyncio.get_event_loop()
resources = await loop.run_in_executor(
_chat_executor, partial(_fetch_compartment_resources, active_oci_id, tf_compartment, tf_region))
resource_ctx = _build_resource_context(resources)
cfg_dict["system_prompt"] = cfg_dict.get("system_prompt", "") + "\n\n" + resource_ctx
log.info(f"Terraform: injected resource context for compartment {tf_compartment[:20]}...")
except Exception as e:
log.warning(f"Failed to inject terraform resource context: {e}")
# ── Inject OCI Terraform resource reference for correct resource names ──
if agent_type == "terraform":
tf_ref = _load_tf_resource_reference()
if tf_ref:
# Extract only the categorized sections (not the full 900+ resource list)
# to keep prompt size manageable
sections = []
for line in tf_ref.split('\n'):
if line.startswith('## All Resource Types'):
break
sections.append(line)
ref_compact = '\n'.join(sections).strip()
if ref_compact:
cfg_dict["system_prompt"] = cfg_dict.get("system_prompt", "") + \
"\n\n### Referência de Recursos OCI Terraform (gerado do provider schema)\n" + \
"Use EXATAMENTE estes nomes de resource types. Se o recurso não estiver nesta lista, ele NÃO EXISTE no provider.\n\n" + \
ref_compact
log.info(f"Terraform: injected resource reference ({len(ref_compact)} chars)")
# ── Inject official Terraform resource docs (Example Usage + Arguments) ──
if agent_type == "terraform":
try:
# Detect resource types from user message + recent conversation history
detect_text = msg.message if hasattr(msg, 'message') else ""
if history:
# Include last 3 messages for context
for h in history[-3:]:
detect_text += "\n" + (h.get("content", "") or "")
resource_types = _detect_tf_resource_types(detect_text)
if resource_types:
loop = asyncio.get_event_loop()
docs_ctx = await loop.run_in_executor(
_chat_executor, partial(_get_tf_docs_for_resources, resource_types))
if docs_ctx:
cfg_dict["system_prompt"] = cfg_dict.get("system_prompt", "") + "\n\n" + docs_ctx
log.info(f"Terraform: injected {len(resource_types)} resource docs ({len(docs_ctx)} chars)")
except Exception as e:
log.warning(f"Failed to inject terraform resource docs: {e}")
# Log total prompt sizes for debugging
if agent_type == "terraform":
sp_len = len(cfg_dict.get("system_prompt", ""))
msg_len = len(msg.message) if hasattr(msg, 'message') else 0
log.info(f"Terraform prompt sizes: system_prompt={sp_len} chars (~{sp_len//4} tokens), user_msg={msg_len} chars (~{msg_len//4} tokens), max_tokens={cfg_dict.get('max_tokens')}")
mcp_tools = []
tool_defs = None
if msg.use_tools:
mcp_tools = _get_active_mcp_tools(user["id"])
if mcp_tools:
tool_defs = [t["tool"] for t in mcp_tools]
log.info(f"Chat with {len(tool_defs)} MCP tools available")
if history and _should_compact(history):
log.info(f"Compaction triggered: {len(history)} msgs, ~{_estimate_history_tokens(history)} est tokens")
history = _compact_history(sid, user["id"], cfg_dict, history)
log.info(f"Post-compaction: {len(history)} msgs, ~{_estimate_history_tokens(history)} est tokens")
hist = history[:-1] if len(history) > 1 else None
loop = asyncio.get_event_loop()
try:
resp_text, tool_calls, tool_calls_raw = await asyncio.wait_for(
loop.run_in_executor(
_chat_executor, partial(_call_genai, cfg_dict, augmented_message, hist,
tool_defs, None, None, attachments)),
timeout=300)
except asyncio.TimeoutError:
log.error(f"GenAI call timed out after 300s for session {sid}")
raise HTTPException(504, "O modelo de IA demorou demais para responder. Tente novamente.")
all_tool_results = []
accumulated_msgs = []
iterations = 0
api_format = GENAI_MODELS.get(genai_cfg.get("model_id", ""), {}).get("api_format", "GENERIC")
while tool_calls and iterations < 5:
iterations += 1
log.info(f"Tool use iteration {iterations}: {len(tool_calls)} tool call(s)")
iteration_results = []
for tc in tool_calls:
mcp_match = next((m for m in mcp_tools if m["tool"]["name"] == tc["name"]), None)
if mcp_match:
try:
result = await _execute_mcp_tool(mcp_match["server"], tc["name"], tc["arguments"])
log.info(f"Tool {tc['name']} executed successfully ({len(result)} chars)")
_chat_log(sid, mid, user["id"], "info", "tool", "tool_success", f"{tc['name']} ({len(result)} chars)")
except Exception as te:
result = f"Erro ao executar tool {tc['name']}: {str(te)[:300]}"
log.warning(f"Tool {tc['name']} failed: {te}")
_chat_log(sid, mid, user["id"], "error", "tool", "tool_error", f"{tc['name']}: {te}")
else:
result = f"Tool {tc['name']} não encontrada nos MCP servers ativos"
_chat_log(sid, mid, user["id"], "error", "tool", "tool_not_found", tc["name"])
iteration_results.append({"tool_call_id": tc["id"], "name": tc["name"], "content": result})
all_tool_results.extend(iteration_results)
if api_format == "COHERE":
import oci
cohere_results = []
for tr in iteration_results:
tc_obj = oci.generative_ai_inference.models.CohereToolCall()
tc_obj.name = tr["name"]
tc_obj.parameters = {}
tr_obj = oci.generative_ai_inference.models.CohereToolResult()
tr_obj.call = tc_obj
tr_obj.outputs = [{"result": tr["content"]}]
cohere_results.append(tr_obj)
try:
resp_text, tool_calls, tool_calls_raw = await asyncio.wait_for(
loop.run_in_executor(
_chat_executor, partial(_call_genai, cfg_dict, augmented_message, hist,
tool_defs, cohere_results)),
timeout=300)
except asyncio.TimeoutError:
log.error(f"GenAI tool-use call timed out after 300s (Cohere, iteration {iterations})")
raise HTTPException(504, "O modelo de IA demorou demais para responder. Tente novamente.")
else:
import oci
assistant_msg = oci.generative_ai_inference.models.AssistantMessage()
assistant_msg.tool_calls = tool_calls_raw
accumulated_msgs.append(assistant_msg)
for tr in iteration_results:
tool_msg = oci.generative_ai_inference.models.ToolMessage()
tool_msg.tool_call_id = tr["tool_call_id"]
tool_content = oci.generative_ai_inference.models.TextContent()
tool_content.text = tr["content"]
tool_msg.content = [tool_content]
accumulated_msgs.append(tool_msg)
try:
resp_text, tool_calls, tool_calls_raw = await asyncio.wait_for(
loop.run_in_executor(
_chat_executor, partial(_call_genai, cfg_dict, augmented_message, hist,
tool_defs, None, accumulated_msgs)),
timeout=300)
except asyncio.TimeoutError:
log.error(f"GenAI tool-use call timed out after 300s (Generic, iteration {iterations})")
raise HTTPException(504, "O modelo de IA demorou demais para responder. Tente novamente.")
resp = resp_text
if all_tool_results:
tools_info = '\n\n🔧 **Tools utilizadas:** ' + ', '.join(tr["name"] for tr in all_tool_results)
resp += tools_info
if not resp or not resp.strip():
model_id = genai_cfg.get("model_id", "unknown")
resp = f"⚠️ O modelo **{model_id}** retornou uma resposta vazia. Isso pode ocorrer com alguns modelos. Tente novamente ou selecione outro modelo (ex: Gemini, Llama)."
log.warning(f"Chat {mid}: empty response from model {model_id}, returning fallback message")
# Validate terraform resource types against DB
if agent_type == "terraform" and resp and '```' in resp:
try:
tf_errors = _validate_tf_resource_types(resp)
if tf_errors:
warning_lines = ["\n\n⚠️ **Validação automática — Resource types inválidos detectados:**"]
for err in tf_errors:
line = f"- `{err['type']}` — NÃO EXISTE no provider oracle/oci."
if err['suggestions']:
line += f" Sugestões: {', '.join('`' + s + '`' for s in err['suggestions'])}"
warning_lines.append(line)
warning_lines.append("\n**Clique em 'Re-plan' após o modelo corrigir, ou peça correção no chat.**")
resp += '\n'.join(warning_lines)
log.warning(f"Chat {mid}: {len(tf_errors)} invalid TF resource types detected: {[e['type'] for e in tf_errors]}")
except Exception as e:
log.warning(f"TF resource type validation failed: {e}")
with db() as c:
c.execute("UPDATE chat_messages SET content=?, status='done' WHERE id=?", (resp, mid))
log.info(f"Chat {mid} completed successfully")
except Exception as e:
log.error(f"Chat {mid} failed: {e}")
_chat_log(sid, mid, user["id"], "error", "genai", "genai_failed", str(e))
with db() as c:
c.execute("UPDATE chat_messages SET content=?, status='failed' WHERE id=?",
(f"❌ Erro GenAI: {str(e)[:400]}", mid))
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"}