From 2c3fb724bf8ef0ba05df6d81741f7f0848a5f388 Mon Sep 17 00:00:00 2001 From: nogueiraguh Date: Mon, 2 Mar 2026 19:48:53 -0300 Subject: [PATCH] feat: add RAG pipeline with OCI GenAI embeddings and ADB vector search Integrate Retrieval-Augmented Generation into the chat flow using OCI GenAI embed_text API and Oracle Autonomous Database vector storage. The chat automatically queries ADB for relevant context when an active ADB config with a linked GenAI config exists. - Add EMBEDDING_MODELS catalog (Cohere Embed v3.0/light) - Add _embed_text() using OCI GenAI SDK embed endpoint - Add _vector_search() with VECTOR_DISTANCE cosine similarity - Add _get_adb_connection(), _ensure_embeddings_table(), _build_rag_context() - Add document ingestion endpoint (POST /api/adb/{vid}/ingest) - Add table creation endpoint (POST /api/adb/{vid}/ensure-table) - Modify chat endpoint with automatic RAG augmentation (non-fatal) - Add GenAI config and embedding model selectors to ADB UI - Add RAG status indicator in chat toolbar - Add document ingestion section in ADB tab --- backend/app.py | 260 ++++++++++++++++++++++++++++++++++++++++---- frontend/index.html | 127 +++++++++++++++++----- 2 files changed, 340 insertions(+), 47 deletions(-) diff --git a/backend/app.py b/backend/app.py index 065793c..d2eb039 100644 --- a/backend/app.py +++ b/backend/app.py @@ -60,6 +60,13 @@ GENAI_MODELS = { "xai.grok-3": {"provider":"xai","name":"xAI Grok 3","api_format":"GENERIC"}, } +EMBEDDING_MODELS = { + "cohere.embed-english-v3.0": {"name":"Cohere Embed English v3.0","dims":1024}, + "cohere.embed-multilingual-v3.0": {"name":"Cohere Embed Multilingual v3.0","dims":1024}, + "cohere.embed-english-light-v3.0": {"name":"Cohere Embed English Light v3.0","dims":384}, + "cohere.embed-multilingual-light-v3.0": {"name":"Cohere Embed Multilingual Light v3.0","dims":384}, +} + GENAI_REGIONS = [ "us-chicago-1","us-ashburn-1","us-phoenix-1","uk-london-1", "eu-frankfurt-1","ap-tokyo-1","ap-osaka-1","sa-saopaulo-1", @@ -84,7 +91,11 @@ def init_db(): with db() as c: c.executescript(""" CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, username TEXT UNIQUE NOT NULL, email TEXT, + id TEXT PRIMARY KEY, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + username TEXT UNIQUE NOT NULL, + email TEXT, password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'viewer', mfa_secret TEXT, mfa_enabled INTEGER DEFAULT 0, is_active INTEGER DEFAULT 1, @@ -171,11 +182,17 @@ def init_db(): ip TEXT, created_at TEXT DEFAULT (datetime('now')) ); """) + # ── Migrations ── + for col in ["genai_config_id TEXT", "embedding_model_id TEXT DEFAULT 'cohere.embed-multilingual-v3.0'"]: + try: + c.execute(f"ALTER TABLE adb_vector_configs ADD COLUMN {col}") + except sqlite3.OperationalError: + pass adm = c.execute("SELECT id FROM users WHERE username='admin'").fetchone() if not adm: c.execute( - "INSERT INTO users (id,username,email,password_hash,role) VALUES (?,?,?,?,?)", - (str(uuid.uuid4()), "admin", "admin@local", _hash_pw("admin123"), "admin") + "INSERT INTO users (id,first_name,last_name,username,email,password_hash,role) VALUES (?,?,?,?,?,?,?)", + (str(uuid.uuid4()), "Admin", "Sistema", "admin", "admin@local", _hash_pw("admin123"), "admin") ) log.info("Default admin created: admin / admin123") @@ -228,7 +245,7 @@ def _audit(uid, uname, action, resource=None, details=None, ip=None): class LoginReq(BaseModel): username: str; password: str; totp_code: Optional[str] = None class RegisterReq(BaseModel): - username: str; email: str; password: str; role: str = "viewer" + first_name: str; last_name: str; username: str; email: str; password: str; role: str = "viewer" class TOTPVerify(BaseModel): totp_code: str class ChangePwReq(BaseModel): @@ -251,6 +268,9 @@ class GenAIConfigReq(BaseModel): class ADBVectorReq(BaseModel): config_name: str; dsn: str; username: str; password: str wallet_password: Optional[str] = None; table_name: str = "CIS_EMBEDDINGS"; use_mtls: bool = True + genai_config_id: Optional[str] = None; embedding_model_id: str = "cohere.embed-multilingual-v3.0" +class IngestDocReq(BaseModel): + adb_config_id: str; documents: List[Dict[str, Any]] class MCPServerReq(BaseModel): name: str; description: Optional[str] = None; server_type: str = "stdio" command: Optional[str] = None; args: Optional[List[str]] = None @@ -274,7 +294,7 @@ async def login(req: LoginReq, request: Request): c.execute("UPDATE users SET last_login=datetime('now') WHERE id=?", (u["id"],)) _audit(u["id"], u["username"], "login", ip=request.client.host if request.client else None) return {"token":_make_token(u["id"],u["role"],sid), - "user":{"id":u["id"],"username":u["username"],"email":u["email"],"role":u["role"],"mfa_enabled":bool(u["mfa_enabled"])}} + "user":{"id":u["id"],"first_name":u["first_name"],"last_name":u["last_name"],"username":u["username"],"email":u["email"],"role":u["role"],"mfa_enabled":bool(u["mfa_enabled"])}} @app.post("/api/auth/logout") async def logout_ep(u=Depends(current_user)): @@ -287,7 +307,8 @@ async def register(req: RegisterReq, adm=Depends(require("admin"))): uid = str(uuid.uuid4()) with db() as c: if c.execute("SELECT 1 FROM users WHERE username=?", (req.username,)).fetchone(): raise HTTPException(400, "Usuário já existe") - c.execute("INSERT INTO users (id,username,email,password_hash,role) VALUES (?,?,?,?,?)", (uid, req.username, req.email, _hash_pw(req.password), req.role)) + c.execute("INSERT INTO users (id,first_name,last_name,username,email,password_hash,role) VALUES (?,?,?,?,?,?,?)", + (uid, req.first_name, req.last_name, req.username, req.email, _hash_pw(req.password), req.role)) _audit(adm["id"], adm["username"], "create_user", uid, f"user={req.username} role={req.role}") return {"id": uid, "username": req.username, "role": req.role} @@ -320,12 +341,12 @@ async def mfa_disable(user_id: str, adm=Depends(require("admin"))): # ── Users ───────────────────────────────────────────────────────────────────── @app.get("/api/users") async def list_users(u=Depends(require("admin"))): - with db() as c: rows = c.execute("SELECT id,username,email,role,mfa_enabled,is_active,created_at,last_login FROM users").fetchall() + with db() as c: rows = c.execute("SELECT id,first_name,last_name,username,email,role,mfa_enabled,is_active,created_at,last_login FROM users").fetchall() return [dict(r) for r in rows] @app.get("/api/users/me") async def me(u=Depends(current_user)): - return {k: u[k] for k in ("id","username","email","role","mfa_enabled")} + return {k: u[k] for k in ("id","first_name","last_name","username","email","role","mfa_enabled")} @app.put("/api/users/{uid}") async def update_user(uid: str, req: UserUpdateReq, adm=Depends(require("admin"))): @@ -492,7 +513,7 @@ async def explore_buckets(cid: str, compartment_id: str = Query(None), u=Depends # ── OCI GenAI Config & Chat ─────────────────────────────────────────────────── @app.get("/api/genai/models") async def list_genai_models(u=Depends(current_user)): - return {"models": GENAI_MODELS, "regions": GENAI_REGIONS} + return {"models": GENAI_MODELS, "regions": GENAI_REGIONS, "embedding_models": EMBEDDING_MODELS} @app.post("/api/genai/config") async def save_genai(req: GenAIConfigReq, u=Depends(require("admin","user"))): @@ -644,6 +665,130 @@ def _call_genai(gc: dict, message: str, history: list = None) -> str: return contents[0].text return str(resp) +# ── RAG Helpers ─────────────────────────────────────────────────────────────── +RAG_SYSTEM_PROMPT = """You are the OCI CIS AI Agent, an expert assistant for Oracle Cloud Infrastructure security and compliance. + +You have been provided with relevant context documents retrieved from a knowledge base. Use this context to provide accurate, specific answers. If the context does not contain relevant information for the question, say so and answer based on your general knowledge. + +Always cite the source documents when using information from the context. + +--- RETRIEVED CONTEXT --- +{context} +--- END CONTEXT --- + +User question: {question}""" + +def _get_adb_connection(cfg: dict): + """Create an oracledb connection from an adb_vector_configs row.""" + import oracledb + params = {"user": cfg["username"], "password": _dec(cfg["password_enc"]), "dsn": cfg["dsn"]} + if cfg["use_mtls"] and cfg.get("wallet_dir"): + params["config_dir"] = cfg["wallet_dir"] + params["wallet_location"] = cfg["wallet_dir"] + if cfg.get("wallet_password_enc"): + params["wallet_password"] = _dec(cfg["wallet_password_enc"]) + return oracledb.connect(**params) + +def _ensure_embeddings_table(cfg: dict): + """Create the embeddings table in ADB if it doesn't exist.""" + table_name = cfg.get("table_name", "CIS_EMBEDDINGS") + emb_model = cfg.get("embedding_model_id", "cohere.embed-multilingual-v3.0") + dims = EMBEDDING_MODELS.get(emb_model, {}).get("dims", 1024) + conn = _get_adb_connection(cfg) + try: + cur = conn.cursor() + cur.execute("SELECT COUNT(*) FROM user_tables WHERE table_name = :1", (table_name.upper(),)) + if cur.fetchone()[0] == 0: + cur.execute(f""" + CREATE TABLE {table_name} ( + ID VARCHAR2(100) PRIMARY KEY, + CONTENT CLOB, + EMBEDDING VECTOR({dims}, FLOAT64), + METADATA VARCHAR2(4000), + SOURCE VARCHAR2(500), + CREATED_AT TIMESTAMP DEFAULT SYSTIMESTAMP + ) + """) + conn.commit() + log.info(f"Created embeddings table: {table_name} (dims={dims})") + cur.close() + finally: + conn.close() + +def _embed_text(text: str, genai_cfg: dict, embedding_model_id: str) -> list: + """Generate embedding using OCI GenAI embed endpoint.""" + import oci + config_path = str(OCI_DIR / genai_cfg["oci_config_id"] / "config") + config = oci.config.from_file(config_path, "DEFAULT") + endpoint = genai_cfg["endpoint"] + client = oci.generative_ai_inference.GenerativeAiInferenceClient( + config=config, service_endpoint=endpoint, + retry_strategy=oci.retry.NoneRetryStrategy(), timeout=(10, 120) + ) + embed_detail = oci.generative_ai_inference.models.EmbedTextDetails() + embed_detail.inputs = [text] + embed_detail.serving_mode = oci.generative_ai_inference.models.OnDemandServingMode(model_id=embedding_model_id) + embed_detail.compartment_id = genai_cfg["compartment_id"] + embed_detail.truncate = "NONE" + embed_detail.input_type = "SEARCH_QUERY" + response = client.embed_text(embed_detail) + return response.data.embeddings[0] + +def _vector_search(cfg: dict, query_embedding: list, top_k: int = 5) -> list: + """Search ADB vector store using cosine similarity. Returns top-K documents.""" + import array + table_name = cfg.get("table_name", "CIS_EMBEDDINGS") + conn = _get_adb_connection(cfg) + try: + cur = conn.cursor() + vec = array.array('d', query_embedding) + cur.execute(f""" + SELECT ID, CONTENT, METADATA, SOURCE, + VECTOR_DISTANCE(EMBEDDING, :1, COSINE) AS distance + FROM {table_name} + ORDER BY distance ASC + FETCH FIRST :2 ROWS ONLY + """, [vec, top_k]) + results = [] + for row in cur: + content = row[1] + if hasattr(content, 'read'): + content = content.read() + results.append({ + "id": row[0], "content": content or "", + "metadata": row[2], "source": row[3], "distance": float(row[4]) + }) + cur.close() + return results + finally: + conn.close() + +def _build_rag_context(documents: list) -> str: + """Format retrieved documents into a context string for the LLM prompt.""" + if not documents: + return "" + parts = [] + for i, doc in enumerate(documents, 1): + source = doc.get("source", "unknown") + content = doc.get("content", "") + if len(content) > 2000: + content = content[:2000] + "..." + parts.append(f"[Document {i} | Source: {source}]\n{content}") + return "\n\n---\n\n".join(parts) + +def _get_active_adb_config(user_id: str) -> dict | None: + """Get the first active ADB vector config with a linked GenAI config.""" + with db() as c: + cfg = c.execute( + "SELECT * FROM adb_vector_configs WHERE user_id=? AND is_active=1 AND genai_config_id IS NOT NULL ORDER BY created_at DESC LIMIT 1", + (user_id,) + ).fetchone() + if not cfg: + cfg = c.execute( + "SELECT * FROM adb_vector_configs WHERE is_active=1 AND genai_config_id IS NOT NULL ORDER BY created_at DESC LIMIT 1" + ).fetchone() + return dict(cfg) if cfg else None + # ── MCP Servers ─────────────────────────────────────────────────────────────── @app.post("/api/mcp/servers") async def register_mcp(req: MCPServerReq, u=Depends(require("admin","user"))): @@ -705,9 +850,10 @@ async def save_adb(req: ADBVectorReq, u=Depends(require("admin","user"))): vid = str(uuid.uuid4()) with db() as c: c.execute( - "INSERT INTO adb_vector_configs (id,user_id,config_name,dsn,username,password_enc,wallet_password_enc,table_name,use_mtls) VALUES (?,?,?,?,?,?,?,?,?)", + "INSERT INTO adb_vector_configs (id,user_id,config_name,dsn,username,password_enc,wallet_password_enc,table_name,use_mtls,genai_config_id,embedding_model_id) VALUES (?,?,?,?,?,?,?,?,?,?,?)", (vid, u["id"], req.config_name, req.dsn, req.username, _enc(req.password), - _enc(req.wallet_password) if req.wallet_password else None, req.table_name, int(req.use_mtls))) + _enc(req.wallet_password) if req.wallet_password else None, req.table_name, int(req.use_mtls), + req.genai_config_id, req.embedding_model_id)) _audit(u["id"], u["username"], "save_adb_config", vid, req.config_name) return {"id": vid, "config_name": req.config_name} @@ -724,8 +870,9 @@ async def upload_wallet(vid: str, wallet: UploadFile = File(...), u=Depends(requ @app.get("/api/adb/configs") async def list_adb(u=Depends(current_user)): with db() as c: - if u["role"]=="admin": rows=c.execute("SELECT id,config_name,dsn,username,table_name,use_mtls,is_active,wallet_dir,created_at FROM adb_vector_configs").fetchall() - else: rows=c.execute("SELECT id,config_name,dsn,username,table_name,use_mtls,is_active,wallet_dir,created_at FROM adb_vector_configs WHERE user_id=?",(u["id"],)).fetchall() + cols = "id,config_name,dsn,username,table_name,use_mtls,is_active,wallet_dir,genai_config_id,embedding_model_id,created_at" + if u["role"]=="admin": rows=c.execute(f"SELECT {cols} FROM adb_vector_configs").fetchall() + else: rows=c.execute(f"SELECT {cols} FROM adb_vector_configs WHERE user_id=?",(u["id"],)).fetchall() return [dict(r) for r in rows] @app.post("/api/adb/test/{vid}") @@ -734,14 +881,7 @@ async def test_adb(vid: str, u=Depends(require("admin","user"))): cfg = c.execute("SELECT * FROM adb_vector_configs WHERE id=?",(vid,)).fetchone() if not cfg: raise HTTPException(404) try: - import oracledb - params = {"user": cfg["username"], "password": _dec(cfg["password_enc"]), "dsn": cfg["dsn"]} - if cfg["use_mtls"] and cfg.get("wallet_dir"): - params["config_dir"] = cfg["wallet_dir"] - params["wallet_location"] = cfg["wallet_dir"] - if cfg.get("wallet_password_enc"): - params["wallet_password"] = _dec(cfg["wallet_password_enc"]) - conn = oracledb.connect(**params) + conn = _get_adb_connection(dict(cfg)) cur = conn.cursor(); cur.execute("SELECT 1 FROM DUAL"); cur.close(); conn.close() return {"status":"success","message":"Conexão Autonomous DB OK!"} except ImportError: @@ -756,6 +896,63 @@ async def del_adb(vid: str, u=Depends(require("admin","user"))): if d.exists(): shutil.rmtree(d) return {"ok": True} +@app.post("/api/adb/{vid}/ensure-table") +async def ensure_table(vid: str, u=Depends(require("admin","user"))): + with db() as c: + cfg = c.execute("SELECT * FROM adb_vector_configs WHERE id=?", (vid,)).fetchone() + if not cfg: raise HTTPException(404) + try: + _ensure_embeddings_table(dict(cfg)) + return {"ok": True, "table": cfg["table_name"]} + except Exception as e: + raise HTTPException(500, f"Falha ao criar tabela: {str(e)[:500]}") + +@app.post("/api/adb/{vid}/ingest") +async def ingest_documents(vid: str, req: IngestDocReq, bg: BackgroundTasks, u=Depends(require("admin","user"))): + with db() as c: + cfg = c.execute("SELECT * FROM adb_vector_configs WHERE id=?", (vid,)).fetchone() + if not cfg: raise HTTPException(404, "ADB config not found") + cfg = dict(cfg) + if not cfg.get("genai_config_id"): + raise HTTPException(400, "GenAI config not linked to this ADB connection") + with db() as c: + gc = c.execute("SELECT * FROM genai_configs WHERE id=?", (cfg["genai_config_id"],)).fetchone() + if not gc: raise HTTPException(400, "Linked GenAI config not found") + bg.add_task(_ingest_documents_task, cfg, dict(gc), req.documents, u["id"], u["username"]) + return {"ok": True, "message": f"Ingestão iniciada para {len(req.documents)} documentos", "config_id": vid} + +def _ingest_documents_task(cfg: dict, genai_cfg: dict, documents: list, user_id: str, username: str): + """Background task: embed and insert documents into ADB via OCI GenAI.""" + import array + emb_model = cfg.get("embedding_model_id", "cohere.embed-multilingual-v3.0") + table_name = cfg.get("table_name", "CIS_EMBEDDINGS") + _ensure_embeddings_table(cfg) + conn = _get_adb_connection(cfg) + try: + cur = conn.cursor() + inserted = 0 + for doc in documents: + try: + content = doc.get("content", "") + if not content: continue + embedding = _embed_text(content, genai_cfg, emb_model) + vec = array.array('d', embedding) + cur.execute(f""" + INSERT INTO {table_name} (ID, CONTENT, EMBEDDING, METADATA, SOURCE) + VALUES (:1, :2, :3, :4, :5) + """, [str(uuid.uuid4()), content, vec, doc.get("metadata", ""), doc.get("source", "manual_upload")]) + inserted += 1 + except Exception as e: + log.error(f"Failed to ingest document: {e}") + conn.commit() + cur.close() + log.info(f"Ingested {inserted}/{len(documents)} documents into {table_name}") + _audit(user_id, username, "ingest_documents", cfg["id"], f"{inserted} documents") + except Exception as e: + log.error(f"Ingestion task failed: {e}") + finally: + conn.close() + # ── Reports ─────────────────────────────────────────────────────────────────── @app.post("/api/reports/run") async def run_report(req: RunReportReq, bg: BackgroundTasks, u=Depends(require("admin","user"))): @@ -852,7 +1049,26 @@ async def chat(msg: ChatMsg, u=Depends(current_user)): with db() as c: prev = c.execute("SELECT role,content FROM chat_messages WHERE session_id=? AND role IN ('user','assistant') ORDER BY created_at ASC", (sid,)).fetchall() history = [{"role":r["role"],"content":r["content"]} for r in prev] - resp = _call_genai(dict(genai_cfg), msg.message, history[:-1] if len(history) > 1 else None) + + # ── RAG: augment with vector context if ADB config is active ── + rag_context = "" + adb_cfg = _get_active_adb_config(u["id"]) + if adb_cfg: + try: + with db() as c: + emb_genai = c.execute("SELECT * FROM genai_configs WHERE id=?", (adb_cfg["genai_config_id"],)).fetchone() + if emb_genai: + emb_model = adb_cfg.get("embedding_model_id", "cohere.embed-multilingual-v3.0") + query_embedding = _embed_text(msg.message, dict(emb_genai), emb_model) + documents = _vector_search(adb_cfg, query_embedding, top_k=5) + if documents: + rag_context = _build_rag_context(documents) + log.info(f"RAG: Retrieved {len(documents)} documents for query") + except Exception as e: + log.warning(f"RAG retrieval failed (non-fatal): {e}") + + augmented_message = RAG_SYSTEM_PROMPT.format(context=rag_context, question=msg.message) if rag_context else msg.message + resp = _call_genai(dict(genai_cfg), augmented_message, history[:-1] if len(history) > 1 else None) except Exception as e: resp = f"❌ Erro GenAI: {str(e)[:400]}" else: diff --git a/frontend/index.html b/frontend/index.html index 2b325c1..910f93c 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -194,7 +194,7 @@ const V='1.1'; const LOGO_W=``; const LOGO_R=``; -const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],genaiCfg:[],adbCfg:[],mcpSvr:[],users:[],auditLogs:[],models:{},regions:[],selGenai:'',expData:null}; +const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],genaiCfg:[],adbCfg:[],mcpSvr:[],users:[],auditLogs:[],models:{},regions:[],embModels:{},selGenai:'',expData:null}; const API='/api'; async function $api(p,o={}){const h={...(o.headers||{})};if(S.token)h['Authorization']='Bearer '+S.token; @@ -208,7 +208,7 @@ async function doLogout(){try{await $api('/auth/logout',{method:'POST'})}catch(e async function checkAuth(){const t=localStorage.getItem('t');if(!t)return;S.token=t;try{S.user=await $api('/users/me');await loadData()}catch(e){S.token=null;localStorage.removeItem('t')}} async function loadData(){try{ [S.ociCfg,S.reports,S.genaiCfg,S.mcpSvr]=await Promise.all([$api('/oci/configs'),$api('/reports'),$api('/genai/configs'),$api('/mcp/servers')]); - try{const m=await $api('/genai/models');S.models=m.models;S.regions=m.regions}catch(e){} + try{const m=await $api('/genai/models');S.models=m.models;S.regions=m.regions;S.embModels=m.embedding_models||{}}catch(e){} try{S.adbCfg=await $api('/adb/configs')}catch(e){S.adbCfg=[]} if(S.user.role==='admin'){try{S.users=await $api('/users')}catch(e){}} }catch(e){console.error(e)}} @@ -235,7 +235,7 @@ function rSb(){ const tabs=[['chat','💬','Chat Agent'],['explorer','🔍','OCI Explorer'],['report','📊','Reports'],['downloads','📁','Downloads']]; const ctabs=[['oci-config','☁️','Credenciais OCI'],['genai','🧠','GenAI Config'],['mcp','🔌','MCP Servers'],['adb','🗄️','ADB Vector']]; const atabs=[['users','👥','Usuários'],['mfa','🔐','MFA'],['audit','📋','Audit Log']]; - const i=(S.user?.username||'?')[0].toUpperCase(); + const i=(S.user?.first_name||S.user?.username||'?')[0].toUpperCase(); return`

${LOGO_W} OCI CIS Agent

Infrastructure & Security · v${V}
-
${i}
${S.user?.username}
${S.user?.role}
+
${i}
${S.user?.first_name?S.user.first_name+' '+S.user.last_name:S.user?.username}
${S.user?.role}
`} function rTb(){const t={'chat':'💬 AI Agent Chat','explorer':'🔍 OCI Account Explorer','report':'📊 Compliance Reports','downloads':'📁 Downloads','oci-config':'☁️ Credenciais OCI','genai':'🧠 OCI Generative AI','mcp':'🔌 MCP Servers','adb':'🗄️ Autonomous DB Vector','users':'👥 Gerenciar Usuários','mfa':'🔐 Autenticação MFA','audit':'📋 Audit Log'}; @@ -259,6 +259,7 @@ function rChat(){ const gcOpts=S.genaiCfg.map(g=>{const mi=S.models[g.model_id];return``}).join(''); return`
+${S.adbCfg.some(c=>c.genai_config_id&&c.is_active)?'RAG Ativo':''}
${ms}
@@ -442,11 +443,12 @@ async function lnkMcp(){const mid=document.getElementById('mup').value;const aid /* ── ADB Vector ── */ function rADB(){return`
🗄️ Conexões Registradas
- -${S.adbCfg.map(c=>` +
NomeDSNUserTabelamTLSWalletAções
${c.config_name}${c.dsn}
+${S.adbCfg.map(c=>{const em=S.embModels[c.embedding_model_id];return` -`).join('')}
NomeDSNUserTabelamTLSWalletEmbedAções
${c.config_name}${c.dsn} ${c.username}${c.table_name} ${c.use_mtls?'✅':'—'}${c.wallet_dir?'✅':'❌'}
+${c.genai_config_id?''+(em?em.name:c.embedding_model_id)+'':''} + `}).join('')} ${!S.adbCfg.length?'

Nenhuma conexão ADB registrada.

':''}
➕ Nova Conexão
Conexão via python-oracledb Thin mode com Wallet (mTLS) para Oracle Autonomous Database.
@@ -455,40 +457,115 @@ ${!S.adbCfg.length?'

Nenhuma conexão

-
+
+
Credenciais OCI usadas para gerar embeddings via GenAI.
+
+
Modelo OCI GenAI para gerar vetores.
+
📤 Upload Wallet
-
`} +
+
📄 Ingerir Documentos
+
Gere embeddings e insira documentos na tabela ADB para uso com RAG.
+
+
+
+
Array de objetos: [{"content":"texto","source":"fonte","metadata":"info"}]
+
+
`} async function sAdb(){try{await $api('/adb/config',{method:'POST',body:{config_name:document.getElementById('an').value,dsn:document.getElementById('adsn').value, username:document.getElementById('auser').value,password:document.getElementById('apw').value, - wallet_password:document.getElementById('awpw').value||null,table_name:document.getElementById('atbl').value}}); + wallet_password:document.getElementById('awpw').value||null,table_name:document.getElementById('atbl').value, + genai_config_id:document.getElementById('agcfg').value||null, + embedding_model_id:document.getElementById('aemb').value||'cohere.embed-multilingual-v3.0'}}); S.adbCfg=await $api('/adb/configs');sm('am','✅ Conexão salva!','s');R()}catch(e){sm('am',e.message,'e')}} async function tAdb(id){try{const d=await $api('/adb/test/'+id,{method:'POST'});alert(d.status==='success'?'✅ '+d.message:'❌ '+d.message)}catch(e){alert(e.message)}} async function dAdb(id){if(!confirm('Excluir conexão?'))return;await $api('/adb/configs/'+id,{method:'DELETE'});S.adbCfg=await $api('/adb/configs');R()} async function uWallet(){const fd=new FormData();const f=document.getElementById('awf').files[0];if(!f)return alert('Selecione wallet ZIP'); fd.append('wallet',f);const id=document.getElementById('awsel').value;if(!id)return alert('Selecione conexão'); try{const d=await $api('/adb/'+id+'/upload-wallet',{method:'POST',body:fd,headers:{}});alert('Wallet enviada! Arquivos: '+d.files.join(', '))}catch(e){alert(e.message)}} +async function ensureTable(id){try{const d=await $api('/adb/'+id+'/ensure-table',{method:'POST'});alert('Tabela criada/verificada: '+d.table)}catch(e){alert('Erro: '+e.message)}} +async function ingestDocs(){const id=document.getElementById('aisel').value; + if(!id)return alert('Selecione conexão ADB com GenAI configurado'); + let docs;try{docs=JSON.parse(document.getElementById('adocs').value)}catch(e){return alert('JSON inválido')} + if(!Array.isArray(docs)||!docs.length)return alert('Forneça array de documentos'); + try{const d=await $api('/adb/'+id+'/ingest',{method:'POST',body:{adb_config_id:id,documents:docs}});alert(d.message)}catch(e){alert('Erro: '+e.message)}} /* ── Users ── */ -function rUsers(){return`
👥 Usuários
- -${S.users.map(u=>` - - +function rUsers(){return`
👥 Gerenciamento de Usuários
+
UsuárioEmailRoleMFAStatusÚltimo LoginAções
${u.username}${u.email||'—'}${u.role}${u.mfa_enabled?'✅':'❌'}${u.is_active?'Ativo':'Inativo'}
+${S.users.map(u=>` + + + + + + -`).join('')}
NomeUsuárioEmailRoleMFAStatusÚltimo LoginAções
${u.first_name||''} ${u.last_name||''}${u.username}${u.email||'—'}${u.role}${u.mfa_enabled?'':'—'}${u.is_active?'Ativo':'Inativo'} ${u.last_login||'Nunca'} -${u.is_active?` `:''}
-
➕ Criar Usuário
-
-
-
-
+ +${u.username!=='admin'&&u.is_active?` `:''}`).join('')} +
+
➕ Criar Novo Usuário
+
+
+
+
+
+
+
`} -async function cUser(){try{await $api('/auth/register',{method:'POST',body:{username:document.getElementById('nu').value,email:document.getElementById('ne').value, - password:document.getElementById('np').value,role:document.getElementById('nr').value}});S.users=await $api('/users');sm('um','✅ Usuário criado!','s');R()}catch(e){sm('um',e.message,'e')}} + +async function cUser(){ + const firstName = document.getElementById('nfn').value.trim(); + const lastName = document.getElementById('nln').value.trim(); + const username = document.getElementById('nu').value.trim(); + const email = document.getElementById('ne').value.trim(); + const password = document.getElementById('np').value; + const role = document.getElementById('nr').value; + + if (!firstName || !lastName) { + sm('um2', '❌ Nome e sobrenome são obrigatórios', 'e'); + return; + } + if (!username) { + sm('um2', '❌ Usuário (login) é obrigatório', 'e'); + return; + } + if (!password) { + sm('um2', '❌ Senha é obrigatória', 'e'); + return; + } + + try { + await $api('/auth/register', { + method: 'POST', + body: { + first_name: firstName, + last_name: lastName, + username: username, + email: email, + password: password, + role: role + } + }); + S.users = await $api('/users'); + sm('um2', '✅ Usuário criado com sucesso!', 's'); + document.getElementById('nfn').value = ''; + document.getElementById('nln').value = ''; + document.getElementById('nu').value = ''; + document.getElementById('ne').value = ''; + document.getElementById('np').value = ''; + R(); + } catch(e) { + sm('um2', e.message, 'e'); + } +} + async function uRole(id,r){try{await $api('/users/'+id,{method:'PUT',body:{role:r}});S.users=await $api('/users')}catch(e){alert(e.message)}} async function deact(id){if(!confirm('Desativar usuário?'))return;await $api('/users/'+id,{method:'DELETE'});S.users=await $api('/users');R()}