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:
170
backend/app.py
170
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:** <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:
|
||||
|
||||
Reference in New Issue
Block a user