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:
154
backend/app.py
154
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.
|
- 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)
|
### 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
|
**Tabelas de findings (dados do scan da tenancy):**
|
||||||
- Para **critérios, exigência e auditoria**, priorize documentos do **CIS_REPORT**.
|
- **summaryreportcsvvector**: resumo geral do CIS report (compliance scores por seção)
|
||||||
- Para **passos de remediação (como corrigir)**, priorize documentos do **CIS_RECOMMENDATIONS**.
|
- **identityandaccess**: findings de IAM (users, policies, MFA, API keys)
|
||||||
- Se houver conflito, declare: "Há divergência entre bases; para auditoria usei CIS_REPORT e para correção usei CIS_RECOMMENDATIONS", citando evidências.
|
- **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
|
### Regras de fidelidade
|
||||||
- Responda **somente** com informações suportadas por evidências do contexto recuperado.
|
- 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.).
|
- 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.
|
- **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.
|
- 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)
|
### Formato de resposta (compacto, porém completo)
|
||||||
- **Tenancy:** <tenancy>
|
- **Tenancy:** <tenancy>
|
||||||
@@ -2796,6 +2834,7 @@ def _get_adb_connection(cfg: dict):
|
|||||||
params["config_dir"] = cfg["wallet_dir"]
|
params["config_dir"] = cfg["wallet_dir"]
|
||||||
params["wallet_location"] = 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["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)
|
return oracledb.connect(**params)
|
||||||
|
|
||||||
def _resolve_embed_config(oci_config_id: str = None, genai_cfg: dict = None) -> dict:
|
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:
|
finally:
|
||||||
conn.close()
|
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:
|
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.
|
"""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
|
import array
|
||||||
table_name = table_name or cfg.get("table_name", "")
|
table_name = table_name or cfg.get("table_name", "")
|
||||||
conn = _get_adb_connection(cfg)
|
conn = _get_adb_connection(cfg)
|
||||||
try:
|
try:
|
||||||
|
conn.call_timeout = 30000 # 30s query timeout (milliseconds)
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
vec = array.array('f', query_embedding)
|
vec = array.array('f', query_embedding)
|
||||||
if tenancy:
|
limit = int(top_k)
|
||||||
# Filter by tenancy in METADATA JSON field using LIKE for broad compatibility
|
use_tenancy_filter = tenancy and table_name.lower() not in _GLOBAL_TABLES
|
||||||
# Matches both structured JSON {"tenancy":"X",...} and legacy "tenancy: X, ..."
|
if use_tenancy_filter:
|
||||||
tenancy_filter = f'%"tenancy":"{tenancy}"%'
|
|
||||||
tenancy_filter_legacy = f'%tenancy: {tenancy}%'
|
|
||||||
cur.execute(f"""
|
cur.execute(f"""
|
||||||
SELECT ID, TEXT, METADATA,
|
SELECT ID, TEXT, METADATA,
|
||||||
VECTOR_DISTANCE(EMBEDDING, :1, COSINE) AS distance
|
VECTOR_DISTANCE(EMBEDDING, :1, COSINE) AS distance
|
||||||
FROM "{table_name}"
|
FROM "{table_name}"
|
||||||
WHERE (METADATA LIKE :3 OR METADATA LIKE :4)
|
WHERE JSON_VALUE(METADATA, '$.tenancy') = :2
|
||||||
ORDER BY distance ASC
|
ORDER BY distance ASC
|
||||||
FETCH FIRST :2 ROWS ONLY
|
FETCH FIRST {limit} ROWS ONLY
|
||||||
""", [vec, top_k, tenancy_filter, tenancy_filter_legacy])
|
""", [vec, tenancy])
|
||||||
else:
|
else:
|
||||||
cur.execute(f"""
|
cur.execute(f"""
|
||||||
SELECT ID, TEXT, METADATA,
|
SELECT ID, TEXT, METADATA,
|
||||||
VECTOR_DISTANCE(EMBEDDING, :1, COSINE) AS distance
|
VECTOR_DISTANCE(EMBEDDING, :1, COSINE) AS distance
|
||||||
FROM "{table_name}"
|
FROM "{table_name}"
|
||||||
ORDER BY distance ASC
|
ORDER BY distance ASC
|
||||||
FETCH FIRST :2 ROWS ONLY
|
FETCH FIRST {limit} ROWS ONLY
|
||||||
""", [vec, top_k])
|
""", [vec])
|
||||||
results = []
|
results = []
|
||||||
for row in cur:
|
for row in cur:
|
||||||
content = row[1]
|
content = row[1]
|
||||||
@@ -2950,17 +2990,36 @@ def _enrich_doc_content(doc: dict) -> str:
|
|||||||
content = " | ".join(extra) + "\n" + content
|
content = " | ".join(extra) + "\n" + content
|
||||||
return content
|
return content
|
||||||
|
|
||||||
def _build_rag_context(documents: list) -> str:
|
def _build_rag_context(documents: list, max_total_chars: int = 12000) -> str:
|
||||||
"""Format retrieved documents into a context string for the LLM prompt."""
|
"""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:
|
if not documents:
|
||||||
return ""
|
return ""
|
||||||
parts = []
|
parts = []
|
||||||
|
total = 0
|
||||||
|
max_per_doc = max_total_chars // min(len(documents), 8)
|
||||||
for i, doc in enumerate(documents, 1):
|
for i, doc in enumerate(documents, 1):
|
||||||
source = doc.get("source", "unknown")
|
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)
|
content = _enrich_doc_content(doc)
|
||||||
if len(content) > 2000:
|
if len(content) > max_per_doc:
|
||||||
content = content[:2000] + "..."
|
content = content[:max_per_doc] + "..."
|
||||||
parts.append(f"[Document {i} | Source: {source}]\n{content}")
|
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)
|
return "\n\n---\n\n".join(parts)
|
||||||
|
|
||||||
def _get_active_adb_configs(user_id: str) -> list[dict]:
|
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 = ""
|
rag_context = ""
|
||||||
adb_cfgs = _get_active_adb_configs(user["id"]) if agent_type != "terraform" else []
|
adb_cfgs = _get_active_adb_configs(user["id"]) if agent_type != "terraform" else []
|
||||||
|
rag_errors = []
|
||||||
if adb_cfgs:
|
if adb_cfgs:
|
||||||
all_documents = []
|
all_documents = []
|
||||||
for adb_cfg in adb_cfgs:
|
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()
|
row = c.execute("SELECT * FROM genai_configs WHERE id=?", (adb_cfg["genai_config_id"],)).fetchone()
|
||||||
if row: genai_linked = dict(row)
|
if row: genai_linked = dict(row)
|
||||||
emb_genai = _resolve_embed_config(oci_config_id=active_oci_for_rag, genai_cfg=genai_linked)
|
emb_genai = _resolve_embed_config(oci_config_id=active_oci_for_rag, genai_cfg=genai_linked)
|
||||||
if emb_genai:
|
if not emb_genai:
|
||||||
emb_model = adb_cfg.get("embedding_model_id", "cohere.embed-v4.0")
|
continue
|
||||||
query_embedding = _embed_text(msg.message, emb_genai, emb_model)
|
default_model = adb_cfg.get("embedding_model_id", "cohere.embed-v4.0")
|
||||||
tables = _get_tables_for_config(adb_cfg["id"], active_only=True)
|
tables = _get_tables_for_config(adb_cfg["id"], active_only=True)
|
||||||
if not tables:
|
if not tables:
|
||||||
tables = [{"table_name": adb_cfg.get("table_name", "")}]
|
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:
|
for tbl in tables:
|
||||||
|
tbl_name = tbl["table_name"]
|
||||||
|
if not tbl_name:
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
documents = _vector_search(adb_cfg, query_embedding, top_k=5, table_name=tbl["table_name"], tenancy=rag_tenancy)
|
# Auto-detect model by table dimension
|
||||||
|
emb_model = default_model
|
||||||
|
try:
|
||||||
|
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:
|
if documents:
|
||||||
for doc in documents:
|
for doc in documents:
|
||||||
doc["source"] = f"{doc.get('source', 'unknown')} [{tbl['table_name']}]"
|
doc["source"] = tbl_name
|
||||||
all_documents.extend(documents)
|
all_documents.extend(documents)
|
||||||
log.info(f"RAG: Retrieved {len(documents)} docs from {tbl['table_name']}")
|
log.info(f"RAG: {len(documents)} docs from {tbl_name} (dim={emb_model})")
|
||||||
except Exception as te:
|
except Exception as te:
|
||||||
log.warning(f"RAG search failed for table {tbl['table_name']}: {te}")
|
err_short = str(te)[:100]
|
||||||
_chat_log(sid, mid, user["id"], "warning", "rag", "search_failed", f"{tbl['table_name']}: {te}")
|
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:
|
except Exception as e:
|
||||||
log.warning(f"RAG retrieval failed for {adb_cfg.get('config_name','?')} (non-fatal): {e}")
|
err_short = str(e)[:100]
|
||||||
_chat_log(sid, mid, user["id"], "warning", "rag", "retrieval_failed", f"{adb_cfg.get('config_name','?')}: {e}")
|
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:
|
if all_documents:
|
||||||
all_documents.sort(key=lambda d: d["distance"])
|
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)
|
cfg_dict = dict(genai_cfg)
|
||||||
with db() as c:
|
with db() as c:
|
||||||
|
|||||||
Reference in New Issue
Block a user