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
This commit is contained in:
nogueiraguh
2026-03-02 19:48:53 -03:00
parent 11343af59a
commit 2c3fb724bf
2 changed files with 340 additions and 47 deletions

View File

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

View File

@@ -194,7 +194,7 @@ const V='1.1';
const LOGO_W=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="26" height="26" style="flex-shrink:0"><rect x="2" y="5" width="32" height="26" rx="13" ry="13" fill="rgba(255,255,255,0.12)" stroke="#fff" stroke-width="2"/><rect x="11" y="11" width="14" height="14" rx="4" fill="#fff" opacity="0.95"/><circle cx="15" cy="17" r="2" fill="#C74634"/><circle cx="21" cy="17" r="2" fill="#C74634"/><circle cx="15" cy="16.7" r="0.7" fill="#fff"/><circle cx="21" cy="16.7" r="0.7" fill="#fff"/><rect x="14" y="21" width="8" height="1.5" rx="0.75" fill="#C74634" opacity="0.6"/><line x1="18" y1="11" x2="18" y2="6" stroke="#fff" stroke-width="1.2" stroke-linecap="round"/><circle cx="18" cy="5.5" r="1.5" fill="#fff" opacity="0.9"/><line x1="11" y1="18" x2="6" y2="18" stroke="#fff" stroke-width="1" stroke-linecap="round" opacity="0.7"/><line x1="25" y1="18" x2="30" y2="18" stroke="#fff" stroke-width="1" stroke-linecap="round" opacity="0.7"/><circle cx="5.5" cy="18" r="1" fill="#fff" opacity="0.7"/><circle cx="30.5" cy="18" r="1" fill="#fff" opacity="0.7"/></svg>`;
const LOGO_R=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="52" height="52"><rect x="2" y="5" width="32" height="26" rx="13" ry="13" fill="rgba(199,70,52,0.08)" stroke="#C74634" stroke-width="1.8"/><rect x="11" y="11" width="14" height="14" rx="4" fill="#fff" stroke="#C74634" stroke-width="0.4"/><circle cx="15" cy="17" r="2" fill="#C74634"/><circle cx="21" cy="17" r="2" fill="#C74634"/><circle cx="15" cy="16.7" r="0.7" fill="#fff"/><circle cx="21" cy="16.7" r="0.7" fill="#fff"/><rect x="14" y="21" width="8" height="1.5" rx="0.75" fill="#C74634" opacity="0.6"/><line x1="18" y1="11" x2="18" y2="6" stroke="#C74634" stroke-width="1.2" stroke-linecap="round"/><circle cx="18" cy="5.5" r="1.5" fill="#C74634" opacity="0.25" stroke="#C74634" stroke-width="0.7"/><line x1="11" y1="18" x2="6" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.4"/><line x1="25" y1="18" x2="30" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.4"/><circle cx="5.5" cy="18" r="1" fill="#C74634" opacity="0.35"/><circle cx="30.5" cy="18" r="1" fill="#C74634" opacity="0.35"/></svg>`;
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`<div class="sb">
<div class="sb-h"><h1>${LOGO_W} OCI CIS Agent</h1><div class="st">Infrastructure & Security · v${V}</div></div>
<div class="nav"><div class="nl">Principal</div>
@@ -244,7 +244,7 @@ ${tabs.map(t=>`<div class="ni ${S.tab===t[0]?'on':''}" onclick="switchTab('${t[0
${ctabs.map(t=>`<div class="ni ${S.tab===t[0]?'on':''}" onclick="switchTab('${t[0]}')"><span class="ic">${t[1]}</span>${t[2]}</div>`).join('')}
${S.user?.role==='admin'?`<div class="nl">Administração</div>
${atabs.map(t=>`<div class="ni ${S.tab===t[0]?'on':''}" onclick="switchTab('${t[0]}')"><span class="ic">${t[1]}</span>${t[2]}</div>`).join('')}`:''}</div>
<div class="sb-f"><div class="ui"><div class="ua">${i}</div><div><div class="un">${S.user?.username}</div><div class="ur">${S.user?.role}</div></div>
<div class="sb-f"><div class="ui"><div class="ua">${i}</div><div><div class="un">${S.user?.first_name?S.user.first_name+' '+S.user.last_name:S.user?.username}</div><div class="ur">${S.user?.role}</div></div>
<div class="lo-btn" onclick="doLogout()" title="Sair">⏻</div></div></div></div>`}
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`<option value="${g.id}" ${S.selGenai===g.id?'selected':''}>${g.name||mi?.name||g.model_id} · ${g.genai_region}</option>`}).join('');
return`<div class="ch-c">
<div class="ch-t"><label>🧠 Modelo:</label><select id="gsel" onchange="S.selGenai=this.value"><option value="">Agente Local (sem IA)</option>${gcOpts}</select>
${S.adbCfg.some(c=>c.genai_config_id&&c.is_active)?'<span class="tag" style="background:var(--gnl);color:var(--gn);font-size:.62rem;margin-left:.5rem">RAG Ativo</span>':''}
<div style="flex:1"></div><button class="btn bd bsm" onclick="clrChat()">Limpar</button></div>
<div class="ch-m" id="chm">${ms}</div>
<div class="ch-i"><input type="text" id="chi" placeholder="Digite sua mensagem..." onkeydown="if(event.key==='Enter')sChat()">
@@ -442,11 +443,12 @@ async function lnkMcp(){const mid=document.getElementById('mup').value;const aid
/* ── ADB Vector ── */
function rADB(){return`<div class="cd"><div class="ct">🗄️ Conexões Registradas</div>
<table><thead><tr><th>Nome</th><th>DSN</th><th>User</th><th>Tabela</th><th>mTLS</th><th>Wallet</th><th>Ações</th></tr></thead><tbody>
${S.adbCfg.map(c=>`<tr><td><strong>${c.config_name}</strong></td><td style="font-family:var(--fm);font-size:.68rem;color:var(--t4)">${c.dsn}</td>
<table><thead><tr><th>Nome</th><th>DSN</th><th>User</th><th>Tabela</th><th>mTLS</th><th>Wallet</th><th>Embed</th><th>Ações</th></tr></thead><tbody>
${S.adbCfg.map(c=>{const em=S.embModels[c.embedding_model_id];return`<tr><td><strong>${c.config_name}</strong></td><td style="font-family:var(--fm);font-size:.68rem;color:var(--t4)">${c.dsn}</td>
<td>${c.username}</td><td><span class="tag">${c.table_name}</span></td>
<td>${c.use_mtls?'✅':'—'}</td><td>${c.wallet_dir?'✅':'❌'}</td>
<td><button class="btn bs bsm" onclick="tAdb('${c.id}')">Testar</button> <button class="btn bd bsm" onclick="dAdb('${c.id}')">Excluir</button></td></tr>`).join('')}</tbody></table>
<td>${c.genai_config_id?'<span class="tag" style="background:var(--gnl);color:var(--gn)">'+(em?em.name:c.embedding_model_id)+'</span>':'<span style="color:var(--t4)">—</span>'}</td>
<td><button class="btn bs bsm" onclick="tAdb('${c.id}')">Testar</button> <button class="btn bs bsm" onclick="ensureTable('${c.id}')">Criar Tabela</button> <button class="btn bd bsm" onclick="dAdb('${c.id}')">Excluir</button></td></tr>`}).join('')}</tbody></table>
${!S.adbCfg.length?'<div class="emp" style="padding:.85rem"><p>Nenhuma conexão ADB registrada.</p></div>':''}</div>
<div class="cd"><div class="ct"> Nova Conexão</div>
<div class="cdesc">Conexão via <code style="font-size:.72rem">python-oracledb</code> Thin mode com Wallet (mTLS) para Oracle Autonomous Database.</div><div id="am"></div>
@@ -455,40 +457,115 @@ ${!S.adbCfg.length?'<div class="emp" style="padding:.85rem"><p>Nenhuma conexão
<div class="ig"><label>Username</label><input type="text" id="auser" placeholder="ADMIN"></div>
<div class="ig"><label>Password</label><input type="password" id="apw"></div>
<div class="ig"><label>Wallet Password</label><input type="password" id="awpw" placeholder="(opcional)"></div>
<div class="ig"><label>Tabela de Embeddings</label><input type="text" id="atbl" value="CIS_EMBEDDINGS"></div></div>
<div class="ig"><label>Tabela de Embeddings</label><input type="text" id="atbl" value="CIS_EMBEDDINGS"></div>
<div class="ig"><label>Config GenAI (Embeddings)</label><div class="ht">Credenciais OCI usadas para gerar embeddings via GenAI.</div>
<select id="agcfg"><option value="">Nenhum (sem RAG)</option>${S.genaiCfg.map(g=>'<option value="'+g.id+'">'+(g.name||g.model_id)+' · '+g.genai_region+'</option>').join('')}</select></div>
<div class="ig"><label>Modelo de Embedding</label><div class="ht">Modelo OCI GenAI para gerar vetores.</div>
<select id="aemb">${Object.entries(S.embModels).map(([k,v])=>'<option value="'+k+'">'+v.name+' ('+v.dims+'d)</option>').join('')}</select></div></div>
<button class="btn bp" onclick="sAdb()">Salvar Conexão</button></div>
<div class="cd"><div class="ct">📤 Upload Wallet</div>
<div class="g2"><div class="ig"><label>Conexão ADB</label><select id="awsel">${S.adbCfg.map(c=>`<option value="${c.id}">${c.config_name}</option>`).join('')}</select></div>
<div class="ig"><label>Wallet ZIP</label><input type="file" id="awf" accept=".zip"></div></div>
<button class="btn bs" onclick="uWallet()">Upload Wallet</button></div>`}
<button class="btn bs" onclick="uWallet()">Upload Wallet</button></div>
<div class="cd"><div class="ct">📄 Ingerir Documentos</div>
<div class="cdesc">Gere embeddings e insira documentos na tabela ADB para uso com RAG.</div>
<div class="g2"><div class="ig"><label>Conexão ADB</label>
<select id="aisel">${S.adbCfg.filter(c=>c.genai_config_id).map(c=>'<option value="'+c.id+'">'+c.config_name+'</option>').join('')}</select></div>
<div class="ig"><label>Documentos (JSON)</label>
<div class="ht">Array de objetos: [{"content":"texto","source":"fonte","metadata":"info"}]</div>
<textarea id="adocs" rows="6" style="font-family:var(--fm);font-size:.72rem;width:100%;padding:.5rem;border:1px solid var(--bd);border-radius:var(--r)" placeholder='[{"content":"...","source":"doc.pdf","metadata":"page 1"}]'></textarea></div></div>
<button class="btn bp" onclick="ingestDocs()">Ingerir Documentos</button></div>`}
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`<div class="cd"><div class="ct">👥 Usuários</div>
<table><thead><tr><th>Usuário</th><th>Email</th><th>Role</th><th>MFA</th><th>Status</th><th>Último Login</th><th>Ações</th></tr></thead><tbody>
${S.users.map(u=>`<tr><td><strong>${u.username}</strong></td><td>${u.email||'—'}</td>
<td><span class="badge b-${u.role}">${u.role}</span></td><td>${u.mfa_enabled?'✅':''}</td>
<td><span class="badge ${u.is_active?'b-pass':'b-fail'}">${u.is_active?'Ativo':'Inativo'}</span></td>
function rUsers(){return`<div class="cd"><div class="ct">👥 Gerenciamento de Usuários</div><div id="um"></div>
<table style="margin-top:.65rem"><thead><tr><th>Nome</th><th>Usuário</th><th>Email</th><th>Role</th><th>MFA</th><th>Status</th><th>Último Login</th><th>Ações</th></tr></thead><tbody>
${S.users.map(u=>`<tr>
<td><strong>${u.first_name||''} ${u.last_name||''}</strong></td>
<td style="font-family:var(--fm);font-size:.7rem">${u.username}</td>
<td style="font-size:.72rem">${u.email||'—'}</td>
<td><span class="badge b-${u.role}">${u.role}</span></td>
<td>${u.mfa_enabled?'<span class="tag" style="background:var(--gnl);color:var(--gn)">✓</span>':'—'}</td>
<td>${u.is_active?'<span class="tag" style="background:var(--gnl);color:var(--gn)">Ativo</span>':'<span class="tag">Inativo</span>'}</td>
<td style="font-size:.72rem;color:var(--t4)">${u.last_login||'Nunca'}</td>
<td><select onchange="uRole('${u.id}',this.value)" style="max-width:90px;padding:.22rem .35rem;font-size:.72rem"><option value="admin"${u.role==='admin'?' selected':''}>Admin</option>
<option value="user"${u.role==='user'?' selected':''}>User</option><option value="viewer"${u.role==='viewer'?' selected':''}>Viewer</option></select>
${u.is_active?` <button class="btn bd bsm" onclick="deact('${u.id}')">Desativar</button>`:''}</td></tr>`).join('')}</tbody></table></div>
<div class="cd"><div class="ct"> Criar Usuário</div><div id="um"></div>
<div class="g3"><div class="ig"><label>Usuário</label><input type="text" id="nu"></div>
<div class="ig"><label>Email</label><input type="email" id="ne"></div>
<div class="ig"><label>Senha</label><input type="password" id="np"></div></div>
<div class="ig" style="max-width:180px"><label>Role</label><select id="nr"><option value="viewer">Viewer</option><option value="user">User</option><option value="admin">Admin</option></select></div>
<td><select onchange="uRole('${u.id}',this.value)" style="max-width:90px;padding:.22rem .35rem;font-size:.72rem" ${u.username==='admin'?'disabled':''}>
<option value="admin"${u.role==='admin'?' selected':''}>Admin</option>
<option value="user"${u.role==='user'?' selected':''}>User</option>
<option value="viewer"${u.role==='viewer'?' selected':''}>Viewer</option></select>
${u.username!=='admin'&&u.is_active?` <button class="btn bd bsm" onclick="deact('${u.id}')">Desativar</button>`:''}</td></tr>`).join('')}
</tbody></table></div>
<div class="cd"><div class="ct"> Criar Novo Usuário</div><div id="um2"></div>
<div class="g3">
<div class="ig"><label>Nome</label><input type="text" id="nfn" placeholder="João"></div>
<div class="ig"><label>Sobrenome</label><input type="text" id="nln" placeholder="Silva"></div>
<div class="ig"><label>Usuário (login)</label><input type="text" id="nu" placeholder="joao.silva"></div></div>
<div class="g3"><div class="ig"><label>Email</label><input type="email" id="ne" placeholder="joao@empresa.com"></div>
<div class="ig"><label>Senha</label><input type="password" id="np"></div>
<div class="ig"><label>Role</label><select id="nr"><option value="viewer">Viewer</option><option value="user">User</option><option value="admin">Admin</option></select></div></div>
<button class="btn bp" onclick="cUser()">Criar Usuário</button></div>`}
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()}