From 87fa92e8cf7639b4ce8f2ed54f82fe68b38ad764 Mon Sep 17 00:00:00 2001 From: nogueiraguh Date: Sat, 21 Mar 2026 13:31:19 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20Chat=20Agent=20RAG=20improvements=20?= =?UTF-8?q?=E2=80=94=20auto-detect=20model,=20tenancy=20filter,=20source?= =?UTF-8?q?=20hierarchy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Auto-detect embedding model per table dimension (1536/3072) for search queries - Embedding cache: reuse same embedding for tables with same model (avoid redundant API calls) - Tenancy filter: strict JSON_VALUE match, no IS NULL fallback (prevents old data leaking) - Global tables (cisrecom, engineerknowledgebase): no tenancy filter (generic knowledge) - ADB connection timeout: 15s connect, 30s query - RAG context: includes extract_date per document, relevance score, source table name - Context size: 12000 chars max, distributed proportionally across top 8 docs - ADB offline handling: LLM informed when bases are unavailable - System prompt updated: clear hierarchy (findings > cisrecom > engineerknowledgebase) - RAG vs MCP Tools differentiation: stored data vs real-time scan instructions - Temporal awareness: model prioritizes recent data, supports date comparison --- backend/app.py | 170 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 128 insertions(+), 42 deletions(-) diff --git a/backend/app.py b/backend/app.py index 8d31aea..58f2911 100644 --- a/backend/app.py +++ b/backend/app.py @@ -2463,20 +2463,58 @@ RAG_DEFAULT_SYSTEM_PROMPT = """Você é um assistente RAG especializado em Oracl - Se a pergunta não for desse escopo, recuse educadamente e peça uma pergunta dentro do tema. ### Bases vetorizadas disponíveis (contexto recuperado automaticamente) -- **CIS_REPORT**: report coletado na tenancy (compliance, findings, critérios/auditoria, evidências). -- **CIS_RECOMMENDATIONS**: recomendações práticas de correção/remediação para itens não compliant. -- **Inventários OCI** (itens não compliance): inventidentityandaccess, inventassetmanagement, inventnetworking, inventstorageblock, inventfilestorageservice, inventobjectstorage, inventcomputeinstances, inventloggingandmonitoring. -### Resolução de divergências -- Para **critérios, exigência e auditoria**, priorize documentos do **CIS_REPORT**. -- Para **passos de remediação (como corrigir)**, priorize documentos do **CIS_RECOMMENDATIONS**. -- Se houver conflito, declare: "Há divergência entre bases; para auditoria usei CIS_REPORT e para correção usei CIS_RECOMMENDATIONS", citando evidências. +**Tabelas de findings (dados do scan da tenancy):** +- **summaryreportcsvvector**: resumo geral do CIS report (compliance scores por seção) +- **identityandaccess**: findings de IAM (users, policies, MFA, API keys) +- **networking**: findings de rede (security lists, NSGs, VCNs) +- **computeinstances**: findings de compute (instances, metadata, boot) +- **loggingandmonitoring**: findings de logging (alarms, events, notifications) +- **objectstorage**: findings de Object Storage (buckets, visibility, encryption) +- **storageblockvolume**: findings de Block Volume (encryption, CMK) +- **filestorageservice**: findings de File Storage (encryption, CMK) +- **assetmanagement**: findings de Asset Management (compartments, tagging) + +**Tabelas de referência (conhecimento genérico):** +- **cisrecom**: recomendações oficiais do CIS Benchmark — contém passos detalhados de remediação, auditoria e rationale para cada controle CIS +- **engineerknowledgebase**: base de conhecimento complementar (blogs, documentações, PDFs) + +### Quando usar RAG (dados armazenados) vs MCP Tools (tempo real) + +**Use RAG (contexto recuperado abaixo) quando o usuário:** +- Perguntar sobre **relatórios já gerados**, resultados de scans anteriores, histórico +- Quiser saber **como corrigir** um problema (remediação do cisrecom) +- Pedir **resumo de compliance**, scores, comparações entre datas +- Perguntar sobre **documentação, boas práticas, referências** (engineerknowledgebase) +- Palavras-chave: "no último scan", "no report", "segundo o CIS", "como corrigir", "remediação", "recomendação" + +**Use MCP Tools (scan em tempo real) quando o usuário:** +- Pedir para **verificar agora**, **validar agora**, **checar o estado atual** +- Quiser dados **atualizados/em tempo real** da tenancy +- Palavras-chave: "verifica agora", "valida agora", "estado atual", "scan", "checa", "como está hoje" +- Se tiver dúvida se os dados do RAG estão desatualizados + +**Se não tiver certeza:** pergunte ao usuário se quer consultar os dados armazenados (mais rápido) ou verificar em tempo real (scan ao vivo). + +### Hierarquia de fontes RAG (IMPORTANTE) +1. Para **identificar problemas e status**: use as tabelas de findings (identityandaccess, networking, etc.) +2. Para **como corrigir (remediação)**: use SEMPRE a tabela **cisrecom** como fonte principal — ela contém os passos oficiais do CIS +3. Para **informações complementares**: use **engineerknowledgebase** como plus, se tiver algo relevante a acrescentar +4. Para **resumo de compliance**: use **summaryreportcsvvector** +- Se houver conflito entre fontes, priorize: findings (dados reais) > cisrecom (recomendações oficiais) > engineerknowledgebase (complementar) + +### Tratamento temporal (datas de extração) +- Cada documento possui um campo **date** no cabeçalho indicando quando os dados foram coletados. +- **Por padrão, priorize SEMPRE os dados mais recentes** (data mais nova). +- Se o usuário pedir comparação ou evolução, apresente os dados de ambas as datas lado a lado, indicando claramente qual é o mais recente e qual é o anterior. +- Informe a data de extração nas respostas para que o usuário saiba de quando são os dados. ### Regras de fidelidade - Responda **somente** com informações suportadas por evidências do contexto recuperado. - Se não houver evidência suficiente: **"Não encontrei nas minhas bases"** e peça dados para refinar (Recommendation #, seção, recurso, OCID, etc.). - **Não invente** páginas, comandos, políticas, valores ou números. - Use somente **1–3 linhas** de evidência por item para manter respostas compactas. +- **Sempre informe a data de extração** dos dados apresentados. ### Formato de resposta (compacto, porém completo) - **Tenancy:** @@ -2796,6 +2834,7 @@ def _get_adb_connection(cfg: dict): params["config_dir"] = cfg["wallet_dir"] params["wallet_location"] = cfg["wallet_dir"] params["wallet_password"] = _dec(cfg["wallet_password_enc"]) if cfg.get("wallet_password_enc") else "" + params["tcp_connect_timeout"] = 15 # 15s connection timeout return oracledb.connect(**params) def _resolve_embed_config(oci_config_id: str = None, genai_cfg: dict = None) -> dict: @@ -2874,36 +2913,37 @@ def _get_table_embedding_dim(cfg: dict, table_name: str) -> int: finally: conn.close() +_GLOBAL_TABLES = {"cisrecom", "engineerknowledgebase"} # Tables without tenancy filter (generic knowledge) + def _vector_search(cfg: dict, query_embedding: list, top_k: int = 5, table_name: str = None, tenancy: str = None) -> list: """Search ADB vector store using cosine similarity. Returns top-K documents. - If tenancy is provided, filters METADATA JSON to match that tenancy only.""" + If tenancy is provided and table is not global, filters by tenancy.""" import array table_name = table_name or cfg.get("table_name", "") conn = _get_adb_connection(cfg) try: + conn.call_timeout = 30000 # 30s query timeout (milliseconds) cur = conn.cursor() vec = array.array('f', query_embedding) - if tenancy: - # Filter by tenancy in METADATA JSON field using LIKE for broad compatibility - # Matches both structured JSON {"tenancy":"X",...} and legacy "tenancy: X, ..." - tenancy_filter = f'%"tenancy":"{tenancy}"%' - tenancy_filter_legacy = f'%tenancy: {tenancy}%' + limit = int(top_k) + use_tenancy_filter = tenancy and table_name.lower() not in _GLOBAL_TABLES + if use_tenancy_filter: cur.execute(f""" SELECT ID, TEXT, METADATA, VECTOR_DISTANCE(EMBEDDING, :1, COSINE) AS distance FROM "{table_name}" - WHERE (METADATA LIKE :3 OR METADATA LIKE :4) + WHERE JSON_VALUE(METADATA, '$.tenancy') = :2 ORDER BY distance ASC - FETCH FIRST :2 ROWS ONLY - """, [vec, top_k, tenancy_filter, tenancy_filter_legacy]) + FETCH FIRST {limit} ROWS ONLY + """, [vec, tenancy]) else: cur.execute(f""" SELECT ID, TEXT, METADATA, VECTOR_DISTANCE(EMBEDDING, :1, COSINE) AS distance FROM "{table_name}" ORDER BY distance ASC - FETCH FIRST :2 ROWS ONLY - """, [vec, top_k]) + FETCH FIRST {limit} ROWS ONLY + """, [vec]) results = [] for row in cur: content = row[1] @@ -2950,17 +2990,36 @@ def _enrich_doc_content(doc: dict) -> str: content = " | ".join(extra) + "\n" + content return content -def _build_rag_context(documents: list) -> str: - """Format retrieved documents into a context string for the LLM prompt.""" +def _build_rag_context(documents: list, max_total_chars: int = 12000) -> str: + """Format retrieved documents into a context string for the LLM prompt. + Includes extract_date from metadata for temporal awareness. + Limits total context to max_total_chars to prevent token overflow.""" if not documents: return "" parts = [] + total = 0 + max_per_doc = max_total_chars // min(len(documents), 8) for i, doc in enumerate(documents, 1): source = doc.get("source", "unknown") + dist = doc.get("distance", 0) + # Extract date from metadata for temporal context + extract_date = "" + meta_raw = doc.get("metadata", "") + if isinstance(meta_raw, str) and meta_raw: + try: + meta_obj = json.loads(meta_raw) + extract_date = meta_obj.get("extract_date", "") or meta_obj.get("report_date", "") + except Exception: + pass content = _enrich_doc_content(doc) - if len(content) > 2000: - content = content[:2000] + "..." - parts.append(f"[Document {i} | Source: {source}]\n{content}") + if len(content) > max_per_doc: + content = content[:max_per_doc] + "..." + date_tag = f" | date: {extract_date}" if extract_date else "" + part = f"[Doc {i} | {source}{date_tag} | relevance: {1 - dist:.2f}]\n{content}" + if total + len(part) > max_total_chars: + break + parts.append(part) + total += len(part) return "\n\n---\n\n".join(parts) def _get_active_adb_configs(user_id: str) -> list[dict]: @@ -5762,6 +5821,7 @@ async def _chat_background(mid: str, sid: str, msg: ChatMsg, user: dict, genai_c rag_context = "" 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: @@ -5772,29 +5832,55 @@ async def _chat_background(mid: str, sid: str, msg: ChatMsg, user: dict, genai_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) - if emb_genai: - emb_model = adb_cfg.get("embedding_model_id", "cohere.embed-v4.0") - query_embedding = _embed_text(msg.message, emb_genai, emb_model) - tables = _get_tables_for_config(adb_cfg["id"], active_only=True) - if not tables: - tables = [{"table_name": adb_cfg.get("table_name", "")}] - for tbl in tables: + 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", "")}] + # Cache embeddings by dimension to avoid re-computing + embedding_cache: dict[str, list] = {} + for tbl in tables: + tbl_name = tbl["table_name"] + if not tbl_name: + continue + try: + # Auto-detect model by table dimension + emb_model = default_model try: - documents = _vector_search(adb_cfg, query_embedding, top_k=5, table_name=tbl["table_name"], tenancy=rag_tenancy) - if documents: - for doc in documents: - doc["source"] = f"{doc.get('source', 'unknown')} [{tbl['table_name']}]" - all_documents.extend(documents) - log.info(f"RAG: Retrieved {len(documents)} docs from {tbl['table_name']}") - except Exception as te: - log.warning(f"RAG search failed for table {tbl['table_name']}: {te}") - _chat_log(sid, mid, user["id"], "warning", "rag", "search_failed", f"{tbl['table_name']}: {te}") + actual_dim = _get_table_embedding_dim(adb_cfg, tbl_name) + if actual_dim and actual_dim in _DIM_TO_MODEL: + emb_model = _DIM_TO_MODEL[actual_dim] + except Exception: + pass + # Reuse cached embedding if same model + if emb_model not in embedding_cache: + embedding_cache[emb_model] = _embed_text(msg.message, emb_genai, emb_model) + query_embedding = embedding_cache[emb_model] + documents = _vector_search(adb_cfg, query_embedding, top_k=3, table_name=tbl_name, tenancy=rag_tenancy) + if documents: + for doc in documents: + doc["source"] = tbl_name + all_documents.extend(documents) + log.info(f"RAG: {len(documents)} docs from {tbl_name} (dim={emb_model})") + except Exception as te: + err_short = str(te)[:100] + log.warning(f"RAG search failed for {tbl_name}: {err_short}") + if "DPY-6001" in str(te) or "DPY-6005" in str(te) or "timeout" in str(te).lower(): + rag_errors.append(f"ADB offline ou timeout ({adb_cfg.get('config_name','?')})") except Exception as e: - log.warning(f"RAG retrieval failed for {adb_cfg.get('config_name','?')} (non-fatal): {e}") - _chat_log(sid, mid, user["id"], "warning", "rag", "retrieval_failed", f"{adb_cfg.get('config_name','?')}: {e}") + err_short = str(e)[:100] + 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"]) - rag_context = _build_rag_context(all_documents[:10]) + rag_context = _build_rag_context(all_documents[:8]) + # 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: