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
This commit is contained in:
175
backend/app.py
175
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:
|
||||
|
||||
Reference in New Issue
Block a user