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:
469
backend/services/chat_background.py
Normal file
469
backend/services/chat_background.py
Normal 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"}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user