feat: Chat Agent RAG improvements — auto-detect model, tenancy filter, source hierarchy

- 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
This commit is contained in:
nogueiraguh
2026-03-21 13:31:19 -03:00
parent 8db3e42d31
commit 87fa92e8cf

View File

@@ -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 **13 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:** <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: