From 09371cb6758a0d0ab4fe92e7374597be78e44308 Mon Sep 17 00:00:00 2001 From: nogueiraguh Date: Tue, 3 Mar 2026 19:02:26 -0300 Subject: [PATCH] feat: add in-place config editing, OpenAI GPT-oss models, and Cohere Embed v4.0 - Add PUT endpoints for editing OCI, GenAI, ADB, and MCP configurations - Add edit buttons in all config tables with inline form pre-fill - Add OpenAI GPT-oss 120B and 20B models to GenAI catalog - Add Cohere Embed v4.0 (Multimodal, 1536d) to embedding catalog - Update OCI list endpoint to return user_ocid and fingerprint for editing - Add S.editing state management with cancel support - Bump version to 1.4 --- README.md | 13 ++- backend/app.py | 175 ++++++++++++++++++++++++++++++++++++--- frontend/index.html | 193 ++++++++++++++++++++++++++------------------ 3 files changed, 291 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index d979ed0..8f3f746 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

- Version + Version Python FastAPI OCI @@ -32,7 +32,7 @@ The platform combines security compliance scanning, AI-powered chat with **RAG ( ### 🤖 AI Chat Agent with RAG - **OCI Generative AI** integration via official SDK (`oci.generative_ai_inference`) - **RAG (Retrieval-Augmented Generation)**: automatically queries ADB vector store for relevant context before generating responses -- 12 models across 4 providers: **Cohere** (Command A/R/R+), **Meta** (Llama 4/3.3/3.2/3.1), **Google** (Gemini 2.5), **xAI** (Grok 3/4) +- 14 models across 5 providers: **Cohere** (Command A/R/R+), **Meta** (Llama 4/3.3/3.2/3.1), **Google** (Gemini 2.5), **xAI** (Grok 3/4), **OpenAI** (GPT-oss 120B/20B) - 16 OCI regions supported with auto-generated endpoints - Full parameter control: temperature, max_tokens, top_p, top_k, frequency/presence penalty - Conversation history with session management @@ -309,6 +309,7 @@ oci-cis-agent/ |--------|----------|-------------| | POST | `/api/oci/config` | Save OCI credentials (multipart) | | GET | `/api/oci/configs` | List OCI credentials | +| PUT | `/api/oci/configs/{id}` | Update OCI credential (multipart) | | POST | `/api/oci/test/{id}` | Test OCI connection | | DELETE | `/api/oci/configs/{id}` | Delete OCI credential | @@ -330,6 +331,7 @@ oci-cis-agent/ | GET | `/api/genai/models` | List available models and regions | | POST | `/api/genai/config` | Save GenAI configuration | | GET | `/api/genai/configs` | List GenAI configurations | +| PUT | `/api/genai/configs/{id}` | Update GenAI configuration | | POST | `/api/genai/test/{id}` | Test GenAI connection | | DELETE | `/api/genai/configs/{id}` | Delete GenAI config | @@ -339,6 +341,7 @@ oci-cis-agent/ |--------|----------|-------------| | POST | `/api/mcp/servers` | Register MCP server | | GET | `/api/mcp/servers` | List MCP servers | +| PUT | `/api/mcp/servers/{id}` | Update MCP server | | PUT | `/api/mcp/servers/{id}/toggle` | Activate/deactivate | | POST | `/api/mcp/servers/{id}/upload` | Upload script file | | PUT | `/api/mcp/servers/{id}/link-adb` | Link to ADB Vector | @@ -350,6 +353,8 @@ oci-cis-agent/ |--------|----------|-------------| | POST | `/api/adb/config` | Save ADB connection (with GenAI config + embedding model) | | GET | `/api/adb/configs` | List ADB connections | +| PUT | `/api/adb/configs/{id}` | Update ADB connection (multipart) | +| POST | `/api/adb/parse-wallet` | Parse wallet ZIP and extract DSN names | | POST | `/api/adb/{id}/upload-wallet` | Upload wallet ZIP | | POST | `/api/adb/test/{id}` | Test ADB connection | | POST | `/api/adb/{id}/ensure-table` | Create embeddings table in ADB | @@ -410,6 +415,8 @@ oci-cis-agent/ | Google | Gemini 2.5 Flash | GENERIC | | xAI | Grok 4 | GENERIC | | xAI | Grok 3 | GENERIC | +| OpenAI | GPT-oss (120B) | GENERIC | +| OpenAI | GPT-oss (20B) | GENERIC | ### Embedding Models @@ -419,6 +426,7 @@ oci-cis-agent/ | Cohere | Embed Multilingual v3.0 | 1024 | | Cohere | Embed English Light v3.0 | 384 | | Cohere | Embed Multilingual Light v3.0 | 384 | +| Cohere | Embed v4.0 (Multimodal) | 1536 | ### GenAI Regions @@ -446,6 +454,7 @@ oci-cis-agent/ | Version | Date | Changes | |---------|------|---------| +| **v1.4** | 2026-03 | In-place config editing (OCI, GenAI, ADB, MCP), OpenAI GPT-oss models (120B/20B), Cohere Embed v4.0 multimodal, wallet auto-parse with DSN extraction | | **v1.3** | 2026-03 | Persistent config logs per tab, GenAI auto-fill from OCI credentials, inline UX feedback with loading spinners, MCP type-switch fix, encrypted key passphrase support, Docker stdin hang fix | | **v1.2** | 2026-03 | RAG pipeline (OCI GenAI embeddings + ADB vector search), dedicated Embeddings tab, CIS report chunking, file upload embedding | | **v1.1** | 2025-02 | OCI SDK GenAI (exact pattern), OCI Account Explorer, MCP↔ADB linking, full chat parameters | diff --git a/backend/app.py b/backend/app.py index 7d05291..534f582 100644 --- a/backend/app.py +++ b/backend/app.py @@ -58,6 +58,8 @@ GENAI_MODELS = { "google.gemini-2.5-flash": {"provider":"google","name":"Google Gemini 2.5 Flash","api_format":"GENERIC"}, "xai.grok-4": {"provider":"xai","name":"xAI Grok 4","api_format":"GENERIC"}, "xai.grok-3": {"provider":"xai","name":"xAI Grok 3","api_format":"GENERIC"}, + "openai.gpt-oss-120b": {"provider":"openai","name":"OpenAI GPT-oss (120B)","api_format":"GENERIC"}, + "openai.gpt-oss-20b": {"provider":"openai","name":"OpenAI GPT-oss (20B)","api_format":"GENERIC"}, } EMBEDDING_MODELS = { @@ -65,6 +67,7 @@ EMBEDDING_MODELS = { "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}, + "cohere.embed-v4.0": {"name":"Cohere Embed v4.0 (Multimodal)","dims":1536}, } GENAI_REGIONS = [ @@ -426,8 +429,9 @@ async def save_oci( @app.get("/api/oci/configs") async def list_oci(u=Depends(current_user)): with db() as c: - if u["role"]=="admin": rows=c.execute("SELECT id,user_id,tenancy_name,tenancy_ocid,region,compartment_id,created_at FROM oci_configs").fetchall() - else: rows=c.execute("SELECT id,user_id,tenancy_name,tenancy_ocid,region,compartment_id,created_at FROM oci_configs WHERE user_id=?",(u["id"],)).fetchall() + cols = "id,user_id,tenancy_name,tenancy_ocid,user_ocid,fingerprint,region,compartment_id,created_at" + if u["role"]=="admin": rows=c.execute(f"SELECT {cols} FROM oci_configs").fetchall() + else: rows=c.execute(f"SELECT {cols} FROM oci_configs WHERE user_id=?",(u["id"],)).fetchall() return [dict(r) for r in rows] @app.delete("/api/oci/configs/{cid}") @@ -441,6 +445,43 @@ async def del_oci(cid: str, u=Depends(require("admin","user"))): if d.exists(): shutil.rmtree(d) return {"ok": True} +@app.put("/api/oci/configs/{cid}") +async def update_oci( + cid: str, + tenancy_name: str = Form(...), tenancy_ocid: str = Form(...), + user_ocid: str = Form(...), fingerprint: str = Form(...), + region: str = Form(...), compartment_id: str = Form(""), + key_passphrase: str = Form(""), + private_key: Optional[UploadFile] = File(None), public_key: Optional[UploadFile] = File(None), + u = Depends(require("admin","user")) +): + with db() as c: + existing = c.execute("SELECT * FROM oci_configs WHERE id=?", (cid,)).fetchone() + if not existing: raise HTTPException(404) + if u["role"] != "admin" and existing["user_id"] != u["id"]: raise HTTPException(403) + cdir = OCI_DIR / cid; cdir.mkdir(parents=True, exist_ok=True) + kp = cdir / "oci_api_key.pem" + if private_key and private_key.filename: + key_bytes = await private_key.read() + kp.write_bytes(key_bytes); kp.chmod(0o600) + key_text = key_bytes.decode("utf-8", errors="ignore") + if ("ENCRYPTED" in key_text or "Proc-Type: 4,ENCRYPTED" in key_text) and not key_passphrase: + raise HTTPException(400, "A chave privada está criptografada (ENCRYPTED). Informe a Key Passphrase.") + if public_key and public_key.filename: + (cdir / "oci_api_key_public.pem").write_bytes(await public_key.read()) + cfg_file = cdir / "config" + cfg_content = (f"[DEFAULT]\nuser={user_ocid}\nfingerprint={fingerprint}\n" + f"tenancy={tenancy_ocid}\nregion={region}\nkey_file={kp}\n") + if key_passphrase: + cfg_content += f"pass_phrase={key_passphrase}\n" + cfg_file.write_text(cfg_content); cfg_file.chmod(0o600) + with db() as c: + c.execute("UPDATE oci_configs SET tenancy_name=?,tenancy_ocid=?,user_ocid=?,fingerprint=?,region=?,compartment_id=? WHERE id=?", + (tenancy_name, tenancy_ocid, user_ocid, fingerprint, region, compartment_id or None, cid)) + _audit(u["id"], u["username"], "update_oci_config", cid, f"tenancy={tenancy_name}") + _config_log("oci", cid, tenancy_name, "success", "save", f"Credencial atualizada: {tenancy_name} ({region})", u["id"], u["username"]) + return {"id": cid, "tenancy_name": tenancy_name, "region": region} + @app.post("/api/oci/test/{cid}") async def test_oci(cid: str, u=Depends(require("admin","user"))): cp = OCI_DIR / cid / "config" @@ -595,6 +636,28 @@ async def del_genai(gid: str, u=Depends(require("admin","user"))): with db() as c: c.execute("DELETE FROM genai_configs WHERE id=?", (gid,)) return {"ok": True} +@app.put("/api/genai/configs/{gid}") +async def update_genai(gid: str, req: GenAIConfigReq, u=Depends(require("admin","user"))): + with db() as c: + existing = c.execute("SELECT * FROM genai_configs WHERE id=?", (gid,)).fetchone() + if not existing: raise HTTPException(404) + if u["role"] != "admin" and existing["user_id"] != u["id"]: raise HTTPException(403) + ep = req.endpoint or f"https://inference.generativeai.{req.genai_region}.oci.oraclecloud.com" + with db() as c: + if req.is_default: + c.execute("UPDATE genai_configs SET is_default=0 WHERE user_id=?", (u["id"],)) + c.execute( + """UPDATE genai_configs SET name=?,oci_config_id=?,model_id=?,model_ocid=?,compartment_id=?, + genai_region=?,endpoint=?,serving_type=?,dedicated_endpoint_id=?,temperature=?,max_tokens=?, + top_p=?,top_k=?,frequency_penalty=?,presence_penalty=?,is_default=? WHERE id=?""", + (req.name, req.oci_config_id, req.model_id, req.model_ocid, + req.compartment_id, req.genai_region, ep, req.serving_type, req.dedicated_endpoint_id, + req.temperature, req.max_tokens, req.top_p, req.top_k, + req.frequency_penalty, req.presence_penalty, int(req.is_default), gid)) + _audit(u["id"], u["username"], "update_genai_config", gid, req.model_id) + _config_log("genai", gid, req.name, "success", "save", f"Modelo atualizado: {req.model_id} ({req.genai_region})", u["id"], u["username"]) + return {"id": gid, "model_id": req.model_id, "endpoint": ep} + @app.post("/api/genai/test/{gid}") async def test_genai(gid: str, u=Depends(require("admin","user"))): with db() as c: @@ -878,6 +941,23 @@ async def del_mcp(mid: str, u=Depends(require("admin","user"))): with db() as c: c.execute("DELETE FROM mcp_servers WHERE id=?", (mid,)) return {"ok": True} +@app.put("/api/mcp/servers/{mid}") +async def update_mcp(mid: str, req: MCPServerReq, u=Depends(require("admin","user"))): + with db() as c: + existing = c.execute("SELECT * FROM mcp_servers WHERE id=?", (mid,)).fetchone() + if not existing: raise HTTPException(404) + if u["role"] != "admin" and existing["user_id"] != u["id"]: raise HTTPException(403) + with db() as c: + c.execute( + "UPDATE mcp_servers SET name=?,description=?,server_type=?,command=?,args=?,env_vars=?,url=?,tools=?,linked_adb_id=? WHERE id=?", + (req.name, req.description, req.server_type, req.command, + json.dumps(req.args) if req.args else None, + json.dumps(req.env_vars) if req.env_vars else None, req.url, + json.dumps(req.tools) if req.tools else None, req.linked_adb_id, mid)) + _audit(u["id"], u["username"], "update_mcp", mid, req.name) + _config_log("mcp", mid, req.name, "success", "save", f"MCP atualizado: {req.name} ({req.server_type})", u["id"], u["username"]) + return {"id": mid, "name": req.name, "server_type": req.server_type} + @app.put("/api/mcp/servers/{mid}/toggle") async def toggle_mcp(mid: str, u=Depends(require("admin","user"))): with db() as c: @@ -900,18 +980,57 @@ async def link_mcp_adb(mid: str, adb_id: str = Query(...), u=Depends(require("ad return {"ok": True, "mcp_id": mid, "linked_adb_id": adb_id} # ── ADB Vector DB Config ───────────────────────────────────────────────────── +@app.post("/api/adb/parse-wallet") +async def parse_wallet(wallet: UploadFile = File(...), u=Depends(require("admin","user"))): + """Extract DSN names from tnsnames.ora inside wallet ZIP (temporary parse, no save).""" + import zipfile, tempfile + try: + data = await wallet.read() + with tempfile.TemporaryDirectory() as tmp: + zp = Path(tmp) / "wallet.zip" + zp.write_bytes(data) + with zipfile.ZipFile(str(zp), 'r') as z: + z.extractall(tmp) + tns = Path(tmp) / "tnsnames.ora" + if not tns.exists(): + raise HTTPException(400, "Wallet ZIP não contém tnsnames.ora") + tns_text = tns.read_text(errors="ignore") + dsn_names = re.findall(r'^(\w[\w\-.]*)\s*=', tns_text, re.MULTILINE) + if not dsn_names: + raise HTTPException(400, "Nenhum DSN encontrado no tnsnames.ora") + return {"dsn_names": dsn_names, "files": [n for n in os.listdir(tmp) if n != "wallet.zip"]} + except zipfile.BadZipFile: + raise HTTPException(400, "Arquivo não é um ZIP válido") + @app.post("/api/adb/config") -async def save_adb(req: ADBVectorReq, u=Depends(require("admin","user"))): +async def save_adb( + config_name: str = Form(...), dsn: str = Form(...), username: str = Form(...), + password: str = Form(...), wallet_password: str = Form(""), + table_name: str = Form("CIS_EMBEDDINGS"), use_mtls: str = Form("true"), + genai_config_id: str = Form(""), embedding_model_id: str = Form("cohere.embed-multilingual-v3.0"), + wallet: Optional[UploadFile] = File(None), + u=Depends(require("admin","user")) +): vid = str(uuid.uuid4()) + use_mtls_bool = use_mtls.lower() in ("true", "1", "yes") 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,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), - req.genai_config_id, req.embedding_model_id)) - _audit(u["id"], u["username"], "save_adb_config", vid, req.config_name) - _config_log("adb", vid, req.config_name, "success", "save", f"Conexão salva: {req.config_name} ({req.dsn})", u["id"], u["username"]) - return {"id": vid, "config_name": req.config_name} + (vid, u["id"], config_name, dsn, username, _enc(password), + _enc(wallet_password) if wallet_password else None, table_name, int(use_mtls_bool), + genai_config_id or None, embedding_model_id)) + # Auto-save wallet if provided + if wallet and wallet.filename: + import zipfile + wdir = WALLET_DIR / vid; wdir.mkdir(parents=True, exist_ok=True) + zp = wdir / "wallet.zip" + zp.write_bytes(await wallet.read()) + with zipfile.ZipFile(str(zp), 'r') as z: z.extractall(str(wdir)) + with db() as c: c.execute("UPDATE adb_vector_configs SET wallet_dir=? WHERE id=?", (str(wdir), vid)) + _config_log("adb", vid, config_name, "success", "upload", f"Wallet enviada com a conexão", u["id"], u["username"]) + _audit(u["id"], u["username"], "save_adb_config", vid, config_name) + _config_log("adb", vid, config_name, "success", "save", f"Conexão salva: {config_name} ({dsn})", u["id"], u["username"]) + return {"id": vid, "config_name": config_name} @app.post("/api/adb/{vid}/upload-wallet") async def upload_wallet(vid: str, wallet: UploadFile = File(...), u=Depends(require("admin","user"))): @@ -967,6 +1086,44 @@ async def del_adb(vid: str, u=Depends(require("admin","user"))): if d.exists(): shutil.rmtree(d) return {"ok": True} +@app.put("/api/adb/configs/{vid}") +async def update_adb( + vid: str, + config_name: str = Form(...), dsn: str = Form(...), username: str = Form(...), + password: str = Form(""), wallet_password: str = Form(""), + table_name: str = Form("CIS_EMBEDDINGS"), use_mtls: str = Form("true"), + genai_config_id: str = Form(""), embedding_model_id: str = Form("cohere.embed-multilingual-v3.0"), + wallet: Optional[UploadFile] = File(None), + u=Depends(require("admin","user")) +): + with db() as c: + existing = c.execute("SELECT * FROM adb_vector_configs WHERE id=?", (vid,)).fetchone() + if not existing: raise HTTPException(404) + if u["role"] != "admin" and existing["user_id"] != u["id"]: raise HTTPException(403) + use_mtls_bool = use_mtls.lower() in ("true", "1", "yes") + sets = "config_name=?,dsn=?,username=?,table_name=?,use_mtls=?,genai_config_id=?,embedding_model_id=?" + vals = [config_name, dsn, username, table_name, int(use_mtls_bool), genai_config_id or None, embedding_model_id] + if password: + sets += ",password_enc=?" + vals.append(_enc(password)) + if wallet_password: + sets += ",wallet_password_enc=?" + vals.append(_enc(wallet_password)) + vals.append(vid) + with db() as c: + c.execute(f"UPDATE adb_vector_configs SET {sets} WHERE id=?", vals) + if wallet and wallet.filename: + import zipfile + wdir = WALLET_DIR / vid; wdir.mkdir(parents=True, exist_ok=True) + zp = wdir / "wallet.zip" + zp.write_bytes(await wallet.read()) + with zipfile.ZipFile(str(zp), 'r') as z: z.extractall(str(wdir)) + with db() as c: c.execute("UPDATE adb_vector_configs SET wallet_dir=? WHERE id=?", (str(wdir), vid)) + _config_log("adb", vid, config_name, "success", "upload", "Wallet atualizado", u["id"], u["username"]) + _audit(u["id"], u["username"], "update_adb_config", vid, config_name) + _config_log("adb", vid, config_name, "success", "save", f"Conexão atualizada: {config_name} ({dsn})", u["id"], u["username"]) + return {"id": vid, "config_name": config_name} + @app.post("/api/adb/{vid}/ensure-table") async def ensure_table(vid: str, u=Depends(require("admin","user"))): with db() as c: diff --git a/frontend/index.html b/frontend/index.html index 68e08f8..3f3d338 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:[],embModels:{},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,editing:null}; const API='/api'; async function $api(p,o={}){const h={...(o.headers||{})};if(S.token)h['Authorization']='Bearer '+S.token; @@ -212,7 +212,7 @@ async function loadData(){try{ 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)}} -function R(){document.getElementById('app').innerHTML=S.user?rApp():rLogin();bind()} +function R(){document.getElementById('app').innerHTML=S.user?rApp():rLogin();bind();if(S.editing?.type==='mcp')setTimeout(mcpTypeFields,0)} function switchTab(t){S.tab=t;S.expData=null;R();if(t==='audit'&&S.user.role==='admin')loadAudit();if(t==='downloads')refreshDl(); const tm={'oci-config':'oci','genai':'genai','adb':'adb','mcp':'mcp'};if(tm[t])setTimeout(()=>refreshCLogs(tm[t]),100)} @@ -330,69 +330,81 @@ ${!S.reports.length?'

📂

Nenhum relat function dlRpt(id,f){window.open(API+'/reports/'+id+'/download?fmt='+f,'_blank')} async function refreshDl(){S.reports=await $api('/reports');R()} +/* ── Edit helpers ── */ +function editCfg(type,id){S.editing={type,id};R(); + const form=document.querySelector('.cd:has(#obtn), .cd:has(#gbtn), .cd:has(#abtn), .cd:has(#mcbtn)');if(form)form.scrollIntoView({behavior:'smooth',block:'start'})} +function cancelEdit(){S.editing=null;R()} + /* ── OCI Config ── */ -function rOci(){return`

☁️ Credenciais Registradas
+function rOci(){const eo=S.editing?.type==='oci'?S.ociCfg.find(c=>c.id===S.editing.id):null; + return`
☁️ Credenciais Registradas
-${S.ociCfg.map(c=>` +${S.ociCfg.map(c=>` -`).join('')}
TenancyRegionCompartmentCriadoAções
${c.tenancy_name}${c.region}${c.tenancy_name}${c.region} ${c.compartment_id?(c.compartment_id.slice(0,22)+'…'):'—'}${c.created_at}
+ `).join('')} ${!S.ociCfg.length?'

Nenhuma credencial registrada.

':''}
-
➕ Nova Credencial
-
Adicione as credenciais da sua conta OCI para executar reports e acessar serviços.
+
${eo?'✏️ Editar Credencial':'➕ Nova Credencial'}
+
${eo?'Editando: '+eo.tenancy_name+'':'Adicione as credenciais da sua conta OCI para executar reports e acessar serviços.'}
-
-
-
-
-
-
-
+
+
+
+
+
+
+
${eo?'
Deixe vazio para manter a chave atual
':''}
Apenas se a chave privada for protegida por senha
-
+
${eo?'':''}
`+rConfigLogs('oci')} async function sOci(){const fd=new FormData();fd.append('tenancy_name',document.getElementById('otn').value);fd.append('tenancy_ocid',document.getElementById('oto').value); fd.append('user_ocid',document.getElementById('ouo').value);fd.append('fingerprint',document.getElementById('ofp').value); fd.append('region',document.getElementById('org').value);fd.append('compartment_id',document.getElementById('ocp').value); fd.append('key_passphrase',document.getElementById('okp').value||''); - const sk=document.getElementById('osk').files[0];if(!sk)return sm('om','Selecione a chave privada','e');fd.append('private_key',sk); + const sk=document.getElementById('osk').files[0]; + const isEdit=S.editing?.type==='oci'; + if(!isEdit&&!sk)return sm('om','Selecione a chave privada','e'); + if(sk)fd.append('private_key',sk); const pk=document.getElementById('opk').files[0];if(pk)fd.append('public_key',pk); const btn=document.getElementById('obtn');btn.disabled=true;btn.textContent='Salvando...';sm('om',' Salvando credencial...','i'); - try{await $api('/oci/config',{method:'POST',body:fd,headers:{}});S.ociCfg=await $api('/oci/configs');sm('om','✅ Credencial salva com sucesso!','s');R()}catch(e){sm('om',e.message,'e');btn.disabled=false;btn.textContent='Salvar Credencial'}} + try{const url=isEdit?'/oci/configs/'+S.editing.id:'/oci/config';const method=isEdit?'PUT':'POST'; + await $api(url,{method,body:fd,headers:{}});S.editing=null;S.ociCfg=await $api('/oci/configs');sm('om','✅ Credencial '+(isEdit?'atualizada':'salva')+' com sucesso!','s');R()}catch(e){sm('om',e.message,'e');btn.disabled=false;btn.textContent=isEdit?'Salvar Alterações':'Salvar Credencial'}} async function tOci(id){try{const d=await $api('/oci/test/'+id,{method:'POST'});sm('ocm',d.status==='success'?'✅ Conexão OK!':'❌ '+d.message,d.status==='success'?'s':'e');refreshCLogs('oci')}catch(e){sm('ocm',e.message,'e')}} async function dOci(id){if(!confirm('Excluir credencial?'))return;await $api('/oci/configs/'+id,{method:'DELETE'});S.ociCfg=await $api('/oci/configs');R()} /* ── GenAI Config ── */ -function rGenAI(){const mOpts=Object.entries(S.models).map(([k,v])=>``).join(''); - const rOpts=S.regions.map(r=>``).join(''); +function rGenAI(){const eg=S.editing?.type==='genai'?S.genaiCfg.find(g=>g.id===S.editing.id):null; + const mOpts=Object.entries(S.models).map(([k,v])=>``).join(''); + const rOpts=S.regions.map(r=>``).join(''); return`
🧠 Modelos Configurados
-${S.genaiCfg.map(g=>{const mi=S.models[g.model_id]||{};return` +${S.genaiCfg.map(g=>{const mi=S.models[g.model_id]||{};return` -`}).join('')}
ConfigModeloRegiãoTempTokensAções
${g.name||'—'}${g.is_default?' ⭐':''}${g.name||'—'}${g.is_default?' ⭐':''} ${mi.name||g.model_id}
${mi.provider||'?'}
${g.genai_region} ${g.temperature}${g.max_tokens}
+ `}).join('')} ${!S.genaiCfg.length?'

Nenhum modelo configurado.

':''}
-
➕ Novo Modelo GenAI
-
Configura conexão via OCI SDK (GenerativeAiInferenceClient). O endpoint é gerado automaticamente pela região selecionada.
-
+
${eg?'✏️ Editar Modelo GenAI':'➕ Novo Modelo GenAI'}
+
${eg?'Editando: '+eg.name+'':'Configura conexão via OCI SDK (GenerativeAiInferenceClient). O endpoint é gerado automaticamente pela região selecionada.'}
+
+${S.ociCfg.map(c=>``).join('')}
-
-
Somente se diferente do catálogo
+
+
Somente se diferente do catálogo
Parâmetros
-
-
-
-
-
-
-
+
+
+
+
+
+
+
${eg?'':''}
`+rConfigLogs('genai')} -async function sGenai(){const btn=document.getElementById('gbtn');btn.disabled=true;btn.textContent='Salvando...';sm('gm',' Salvando modelo...','i');try{await $api('/genai/config',{method:'POST',body:{ +async function sGenai(){const btn=document.getElementById('gbtn');btn.disabled=true;btn.textContent='Salvando...';sm('gm',' Salvando modelo...','i'); + const isEdit=S.editing?.type==='genai';const body={ name:document.getElementById('gname').value||'default', oci_config_id:document.getElementById('goci').value,model_id:document.getElementById('gmod').value, model_ocid:document.getElementById('gocid').value||null, @@ -401,8 +413,9 @@ async function sGenai(){const btn=document.getElementById('gbtn');btn.disabled=t temperature:parseFloat(document.getElementById('gtemp').value),max_tokens:parseInt(document.getElementById('gmt').value), top_p:parseFloat(document.getElementById('gtp').value),top_k:parseInt(document.getElementById('gtk').value), frequency_penalty:parseFloat(document.getElementById('gfp').value),presence_penalty:parseFloat(document.getElementById('gpp').value), - is_default:S.genaiCfg.length===0}}); - S.genaiCfg=await $api('/genai/configs');sm('gm','✅ Modelo salvo com sucesso!','s');R()}catch(e){sm('gm',e.message,'e');btn.disabled=false;btn.textContent='Salvar Modelo'}} + is_default:isEdit?(S.genaiCfg.find(g=>g.id===S.editing.id)?.is_default||false):S.genaiCfg.length===0}; + try{const url=isEdit?'/genai/configs/'+S.editing.id:'/genai/config';const method=isEdit?'PUT':'POST'; + await $api(url,{method,body});S.editing=null;S.genaiCfg=await $api('/genai/configs');sm('gm','✅ Modelo '+(isEdit?'atualizado':'salvo')+' com sucesso!','s');R()}catch(e){sm('gm',e.message,'e');btn.disabled=false;btn.textContent=isEdit?'Salvar Alterações':'Salvar Modelo'}} function fillGenaiFromOci(){const sel=document.getElementById('goci');const cfg=S.ociCfg.find(c=>c.id===sel.value);if(!cfg)return; const greg=document.getElementById('greg');if(greg){for(let i=0;i``).join(''); +function rMCP(){const em=S.editing?.type==='mcp'?S.mcpSvr.find(m=>m.id===S.editing.id):null; + const adbOpts=S.adbCfg.map(a=>``).join(''); return`
🔌 Servidores Registrados
MCP servers disponíveis para integração com ferramentas e execução de tarefas.
${S.mcpSvr.length?S.mcpSvr.map(m=>{const adb=m.linked_adb_id?S.adbCfg.find(a=>a.id===m.linked_adb_id):null; - return`
+ return`
${m.name} ${m.server_type} ${m.is_active?'Ativo':'Inativo'}
-
+
${m.description?`
${m.description}
`:''}
@@ -440,34 +454,38 @@ ${m.module_path?`
Módulo:Sem ADB${adbOpts}
`}).join('') :'

Nenhum MCP server registrado.

'}
-
➕ Registrar Novo Server
-
Configure um novo servidor MCP. Os campos se ajustam ao tipo selecionado.
-
-
+
${em?'✏️ Editar Server':'➕ Registrar Novo Server'}
+
${em?'Editando: '+em.name+'':'Configure um novo servidor MCP. Os campos se ajustam ao tipo selecionado.'}
+
+
+ ⌨️ stdio + 🌐 SSE (HTTP)
+ 🐍 Python Module
Comando para iniciar o servidor
-
+
+
Array de argumentos passados ao comando
-
+
Opcional — o MCP server poderá usar este banco como ferramenta.
-
- + +
${em?'':''}
`+rConfigLogs('mcp')} -async function sMcp(){const btn=document.getElementById('mcbtn');btn.disabled=true;btn.textContent='Registrando...';sm('mcm',' Registrando servidor...','i');try{let args=null;const a=document.getElementById('margs')?.value;if(a)try{args=JSON.parse(a)}catch(e){} +async function sMcp(){const btn=document.getElementById('mcbtn');const isEdit=S.editing?.type==='mcp'; + btn.disabled=true;btn.textContent='Salvando...';sm('mcm',' '+(isEdit?'Atualizando':'Registrando')+' servidor...','i'); + try{let args=null;const a=document.getElementById('margs')?.value;if(a)try{args=JSON.parse(a)}catch(e){} const mt=document.querySelector('input[name="mt"]:checked')?.value||'stdio'; - await $api('/mcp/servers',{method:'POST',body:{name:document.getElementById('mn').value,description:document.getElementById('md').value, + const body={name:document.getElementById('mn').value,description:document.getElementById('md').value, server_type:mt,command:document.getElementById('mcmd')?.value||null,url:document.getElementById('murl')?.value||null,args, - linked_adb_id:document.getElementById('madb').value||null}}); - S.mcpSvr=await $api('/mcp/servers');sm('mcm','✅ Registrado!','s');R()}catch(e){sm('mcm',e.message,'e');btn.disabled=false;btn.textContent='Registrar Server'}} + linked_adb_id:document.getElementById('madb').value||null}; + const url=isEdit?'/mcp/servers/'+S.editing.id:'/mcp/servers';const method=isEdit?'PUT':'POST'; + await $api(url,{method,body});S.editing=null; + S.mcpSvr=await $api('/mcp/servers');sm('mcm','✅ '+(isEdit?'Atualizado':'Registrado')+'!','s');R()}catch(e){sm('mcm',e.message,'e');btn.disabled=false;btn.textContent=isEdit?'Salvar Alterações':'Registrar Server'}} async function tgMcp(id){await $api('/mcp/servers/'+id+'/toggle',{method:'PUT'});S.mcpSvr=await $api('/mcp/servers');R()} async function dMcp(id){if(!confirm('Excluir server?'))return;await $api('/mcp/servers/'+id,{method:'DELETE'});S.mcpSvr=await $api('/mcp/servers');R()} async function uMcp(id){const fd=new FormData();const f=document.getElementById('muf_'+id)?.files[0];if(!f)return sm('mcpm_'+id,'Selecione um arquivo .py','e'); @@ -478,41 +496,58 @@ async function lnkMcp(id){const aid=document.getElementById('mlnk_'+id)?.value|| try{await $api('/mcp/servers/'+id+'/link-adb?adb_id='+aid,{method:'PUT'});S.mcpSvr=await $api('/mcp/servers');sm('mcpm_'+id,'✅ '+(aid?'Vinculado ao ADB!':'ADB desvinculado!'),'s');R()}catch(e){sm('mcpm_'+id,e.message,'e');btn.disabled=false;btn.textContent='Vincular ADB'}} /* ── ADB Vector ── */ -function rADB(){return`
🗄️ Conexões Registradas
+function rADB(){const ea=S.editing?.type==='adb'?S.adbCfg.find(c=>c.id===S.editing.id):null; + return`
🗄️ Conexões Registradas
-${S.adbCfg.map(c=>{const em=S.embModels[c.embedding_model_id];return` +${S.adbCfg.map(c=>{const em=S.embModels[c.embedding_model_id];return` -`}).join('')}
NomeDSNUserTabelamTLSWalletEmbedAções
${c.config_name}${c.dsn}${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.
-
-
Ex: myatp_high, myatp_medium
-
-
+
${ea?'✏️ Editar Conexão':'➕ Nova Conexão'}
+
${ea?'Editando: '+ea.config_name+'':'Conexão via python-oracledb Thin mode com Wallet (mTLS) para Oracle Autonomous Database. Envie o Wallet ZIP para preencher o DSN automaticamente.'}
+
+
${ea?'Envie novo wallet ou deixe vazio para manter o atual':'Envie o wallet para extrair os DSNs do tnsnames.ora'}
+
+
Selecione ou digite o DSN. Ex: myatp_high
+
+
+
-
+
Credenciais OCI usadas para gerar embeddings via GenAI.
-
+
Modelo OCI GenAI para gerar vetores.
-
-
-
📤 Upload Wallet
+
+
${ea?'':''}
+${S.adbCfg.length?`
📤 Atualizar Wallet
-
-
+
+`:''} `+rConfigLogs('adb')} -async function sAdb(){const btn=document.getElementById('abtn');btn.disabled=true;btn.textContent='Salvando...';sm('am',' Salvando conexão...','i');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, - 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');btn.disabled=false;btn.textContent='Salvar Conexão'}} +async function parseWallet(){const f=document.getElementById('awf').files[0];if(!f)return sm('am','Selecione um arquivo wallet ZIP.','e'); + const btn=document.getElementById('wpbtn');btn.disabled=true;btn.textContent='...'; + try{const fd=new FormData();fd.append('wallet',f);const d=await $api('/adb/parse-wallet',{method:'POST',body:fd,headers:{}}); + const wrap=document.getElementById('adsn-wrap'); + wrap.innerHTML=''; + sm('am','✅ Wallet analisado! '+d.dsn_names.length+' DSN(s) encontrado(s): '+d.dsn_names.join(', ')+'. Arquivos: '+d.files.join(', '),'s'); + }catch(e){sm('am',e.message,'e')}finally{btn.disabled=false;btn.textContent='Analisar'}} +async function sAdb(){const btn=document.getElementById('abtn');btn.disabled=true;btn.textContent='Salvando...';sm('am',' Salvando conexão...','i'); + const isEdit=S.editing?.type==='adb'; + try{const fd=new FormData();fd.append('config_name',document.getElementById('an').value); + fd.append('dsn',document.getElementById('adsn').value);fd.append('username',document.getElementById('auser').value); + fd.append('password',document.getElementById('apw').value);fd.append('wallet_password',document.getElementById('awpw').value||''); + fd.append('table_name',document.getElementById('atbl').value);fd.append('use_mtls','true'); + fd.append('genai_config_id',document.getElementById('agcfg').value||''); + fd.append('embedding_model_id',document.getElementById('aemb').value||'cohere.embed-multilingual-v3.0'); + const wf=document.getElementById('awf').files[0];if(wf)fd.append('wallet',wf); + const url=isEdit?'/adb/configs/'+S.editing.id:'/adb/config';const method=isEdit?'PUT':'POST'; + await $api(url,{method,body:fd,headers:{}});S.editing=null; + S.adbCfg=await $api('/adb/configs');sm('am','✅ Conexão '+(isEdit?'atualizada':'salva')+'!'+(wf?' Wallet incluído.':''),'s');R()}catch(e){sm('am',e.message,'e');btn.disabled=false;btn.textContent=isEdit?'Salvar Alterações':'Salvar Conexão'}} async function tAdb(id){try{const d=await $api('/adb/test/'+id,{method:'POST'});sm('am',d.status==='success'?'✅ '+d.message:'❌ '+d.message,d.status==='success'?'s':'e');refreshCLogs('adb')}catch(e){sm('am',e.message,'e')}} 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 sm('wm','Selecione um arquivo wallet ZIP.','e'); +async function uWallet(){const fd=new FormData();const f=document.getElementById('awf2').files[0];if(!f)return sm('wm','Selecione um arquivo wallet ZIP.','e'); fd.append('wallet',f);const id=document.getElementById('awsel').value;if(!id)return sm('wm','Selecione uma conexão ADB.','e'); const btn=document.getElementById('wbtn');btn.disabled=true;btn.textContent='Enviando...';sm('wm',' Enviando wallet...','i'); try{const d=await $api('/adb/'+id+'/upload-wallet',{method:'POST',body:fd,headers:{}});S.adbCfg=await $api('/adb/configs');sm('wm','✅ Wallet enviada! Arquivos: '+d.files.join(', '),'s');R()}catch(e){sm('wm',e.message,'e');btn.disabled=false;btn.textContent='Upload Wallet'}}