feat: consult embeddings UI, vector search FLOAT32 fix, and base64 compartment decode

- Add Consult Embeddings sub-menu with chat-like Q&A interface
- Fix VECTOR_DISTANCE FLOAT32/FLOAT64 mismatch (array 'd' → 'f')
- Decode base64 compartment_id in _resolve_embed_config
- Sidebar sub-item navigation for Embeddings hierarchy
- Fallback to raw document display when no GenAI config available
This commit is contained in:
nogueiraguh
2026-03-11 03:06:53 -03:00
parent 2a56712665
commit 749f4e479b
3 changed files with 136 additions and 8 deletions

View File

@@ -2419,7 +2419,7 @@ def _resolve_embed_config(oci_config_id: str = None, genai_cfg: dict = None) ->
"oci_config_id": oc["id"],
"genai_region": oc["region"],
"endpoint": f"https://inference.generativeai.{oc['region']}.oci.oraclecloud.com",
"compartment_id": (dict(oc).get("compartment_id") or oc["tenancy_ocid"]),
"compartment_id": _safe_dec(dict(oc).get("compartment_id") or oc["tenancy_ocid"]),
}
# Last resort: any genai config
with db() as c:
@@ -2432,7 +2432,7 @@ def _resolve_embed_config(oci_config_id: str = None, genai_cfg: dict = None) ->
"oci_config_id": oc["id"],
"genai_region": oc["region"],
"endpoint": f"https://inference.generativeai.{oc['region']}.oci.oraclecloud.com",
"compartment_id": (dict(oc).get("compartment_id") or oc["tenancy_ocid"]),
"compartment_id": _safe_dec(dict(oc).get("compartment_id") or oc["tenancy_ocid"]),
}
raise HTTPException(400, "Nenhuma credencial OCI configurada para gerar embeddings.")
@@ -2466,7 +2466,7 @@ def _vector_search(cfg: dict, query_embedding: list, top_k: int = 5, table_name:
conn = _get_adb_connection(cfg)
try:
cur = conn.cursor()
vec = array.array('d', query_embedding)
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, ..."
@@ -3319,6 +3319,83 @@ async def embed_upload_url(
_audit(u["id"], u["username"], "embed_url", url, f"{len(documents)} chunks")
return {"ok": True, "message": f"Embedding de {len(documents)} chunks iniciado", "chunks": len(documents), "url": url}
class ConsultQuery(BaseModel):
query: str
table_name: str = ""
top_k: int = 10
@app.post("/api/embeddings/consult")
async def consult_embeddings(req: ConsultQuery, u=Depends(current_user)):
"""Query embeddings via vector search + GenAI to get a formatted answer."""
if not req.query.strip():
raise HTTPException(400, "Query não pode ser vazia")
adb_configs = _get_active_adb_configs(u["id"])
if not adb_configs:
raise HTTPException(400, "Nenhuma conexão ADB ativa configurada")
# Collect results from all active ADB configs + tables
all_docs = []
for adb_cfg in adb_configs:
log.info(f"Consult: processing ADB {adb_cfg['id']} ({adb_cfg.get('config_name','')})")
try:
emb_genai = _resolve_embed_config(oci_config_id=adb_cfg.get("oci_config_id"))
log.info(f"Consult: resolved embed config, compartment={emb_genai.get('compartment_id','?')[:30]}, region={emb_genai.get('genai_region','?')}")
except Exception as e:
log.warning(f"Consult: resolve config failed for {adb_cfg['id']}: {e}")
continue
emb_model = adb_cfg.get("embedding_model_id", "")
if not emb_model:
log.warning(f"Consult: no embedding_model_id for {adb_cfg['id']}")
continue
try:
query_embedding = _embed_text(req.query, emb_genai, emb_model)
log.info(f"Consult: embedded query, dims={len(query_embedding)}")
except Exception as e:
log.warning(f"Consult: embed failed for {adb_cfg['id']}: {e}")
continue
tables = _get_tables_for_config(adb_cfg["id"], active_only=True)
log.info(f"Consult: {len(tables)} active tables, filter='{req.table_name}'")
if req.table_name:
tables = [t for t in tables if t["table_name"] == req.table_name]
for tbl in tables:
try:
docs = _vector_search(adb_cfg, query_embedding, top_k=req.top_k, table_name=tbl["table_name"])
for d in docs:
d["source"] = f"{tbl['table_name']}"
all_docs.extend(docs)
except Exception as e:
log.warning(f"Consult: search failed on {tbl['table_name']}: {type(e).__name__}: {e}")
if not all_docs:
return {"answer": "Nenhum resultado encontrado nas bases vetoriais.", "documents": [], "total": 0}
# Sort by distance and take top results
all_docs.sort(key=lambda d: d.get("distance", 999))
top_docs = all_docs[:req.top_k]
# Build context and call GenAI
rag_context = _build_rag_context(top_docs)
augmented = RAG_CONTEXT_TEMPLATE.format(context=rag_context, question=req.query)
# Get GenAI config for answering
with db() as c:
gc_row = c.execute("SELECT * FROM genai_configs WHERE is_default=1 ORDER BY created_at DESC").fetchone()
if not gc_row:
gc_row = c.execute("SELECT * FROM genai_configs ORDER BY created_at DESC").fetchone()
if not gc_row:
# No GenAI config — return raw documents formatted as answer
parts = []
for i, d in enumerate(top_docs, 1):
content = d.get("content", "")
if len(content) > 800: content = content[:800] + "..."
parts.append(f"**Documento {i}** — `{d.get('source', '?')}` (distância: {d.get('distance', 0):.4f})\n\n{content}")
raw_answer = "**Resultados da busca vetorial** (sem GenAI configurado para sumarizar):\n\n---\n\n" + "\n\n---\n\n".join(parts)
doc_list = [{"content": d.get("content", "")[:500], "source": d.get("source", ""), "distance": round(d.get("distance", 0), 4), "metadata": d.get("metadata", "")} for d in top_docs]
return {"answer": raw_answer, "documents": doc_list, "total": len(all_docs)}
gc = dict(gc_row)
try:
answer, _, _ = _call_genai(gc, [{"role": "user", "content": augmented}], system_prompt=RAG_DEFAULT_SYSTEM_PROMPT)
except Exception as e:
log.error(f"Consult GenAI error: {e}")
answer = f"Erro ao consultar GenAI: {str(e)[:300]}"
doc_list = [{"content": d.get("content", "")[:500], "source": d.get("source", ""), "distance": round(d.get("distance", 0), 4), "metadata": d.get("metadata", "")} for d in top_docs]
return {"answer": answer, "documents": doc_list, "total": len(all_docs)}
@app.get("/api/embeddings/{vid}/list")
async def list_embeddings(vid: str, table_name: str = Query(""), limit: int = Query(50), offset: int = Query(0), u=Depends(current_user)):
with db() as c: