feat: custom Oracle AI robot logo
This commit is contained in:
3
.idea/misc.xml
generated
3
.idea/misc.xml
generated
@@ -1,4 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.10 (oci-cis-agent)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (oci-cis-agent)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
373
backend/app.py
373
backend/app.py
@@ -1,10 +1,11 @@
|
||||
"""
|
||||
OCI CIS AI Agent - Backend API v2
|
||||
FastAPI with JWT auth, TOTP MFA, RBAC, OCI GenAI integration (Cohere/Meta/xAI/Google),
|
||||
MCP Server registry, Autonomous DB vector storage, CIS reports, chat agent, audit log.
|
||||
OCI CIS AI Agent - Backend API v1.1
|
||||
FastAPI with JWT auth, TOTP MFA, RBAC, OCI GenAI (exact SDK pattern),
|
||||
OCI Account Explorer, MCP Server registry with VectorDB tool integration,
|
||||
Autonomous DB vector storage, CIS reports, chat agent, audit log.
|
||||
"""
|
||||
import os, json, uuid, hashlib, hmac, time, base64, struct, secrets, subprocess
|
||||
import shutil, asyncio, sqlite3, logging, socket, re, importlib, sys
|
||||
import shutil, asyncio, sqlite3, logging, socket, re
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Dict, Any
|
||||
@@ -30,6 +31,7 @@ OCI_DIR = DATA / "oci_configs"
|
||||
REPORTS = DATA / "reports"
|
||||
MCP_DIR = DATA / "mcp_servers"
|
||||
WALLET_DIR = DATA / "wallets"
|
||||
VERSION = "1.1"
|
||||
|
||||
for d in [DATA, OCI_DIR, REPORTS, MCP_DIR, WALLET_DIR]:
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
@@ -37,7 +39,7 @@ for d in [DATA, OCI_DIR, REPORTS, MCP_DIR, WALLET_DIR]:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
log = logging.getLogger("agent")
|
||||
|
||||
app = FastAPI(title="OCI CIS AI Agent", version="2.0.0")
|
||||
app = FastAPI(title="OCI CIS AI Agent", version=VERSION)
|
||||
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True,
|
||||
allow_methods=["*"], allow_headers=["*"])
|
||||
security = HTTPBearer()
|
||||
@@ -104,16 +106,21 @@ def init_db():
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS genai_configs (
|
||||
id TEXT PRIMARY KEY, user_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT 'default',
|
||||
oci_config_id TEXT NOT NULL,
|
||||
model_id TEXT NOT NULL,
|
||||
model_ocid TEXT,
|
||||
compartment_id TEXT NOT NULL,
|
||||
genai_region TEXT NOT NULL,
|
||||
endpoint TEXT NOT NULL,
|
||||
serving_type TEXT DEFAULT 'ON_DEMAND',
|
||||
endpoint_id TEXT,
|
||||
temperature REAL DEFAULT 0.7,
|
||||
max_tokens INTEGER DEFAULT 2048,
|
||||
top_p REAL DEFAULT 0.9,
|
||||
top_k INTEGER DEFAULT -1,
|
||||
dedicated_endpoint_id TEXT,
|
||||
temperature REAL DEFAULT 1,
|
||||
max_tokens INTEGER DEFAULT 6000,
|
||||
top_p REAL DEFAULT 0.95,
|
||||
top_k INTEGER DEFAULT 1,
|
||||
frequency_penalty REAL DEFAULT 0,
|
||||
presence_penalty REAL DEFAULT 0,
|
||||
is_default INTEGER DEFAULT 0,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (oci_config_id) REFERENCES oci_configs(id)
|
||||
@@ -146,6 +153,8 @@ def init_db():
|
||||
server_type TEXT NOT NULL DEFAULT 'stdio',
|
||||
command TEXT, args TEXT, env_vars TEXT,
|
||||
url TEXT, module_path TEXT,
|
||||
tools TEXT,
|
||||
linked_adb_id TEXT,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
@@ -187,6 +196,12 @@ def _make_token(uid,role,sid):
|
||||
def _enc(v): return base64.b64encode(v.encode()).decode()
|
||||
def _dec(v): return base64.b64decode(v.encode()).decode()
|
||||
|
||||
# ── OCI SDK Client Helper ─────────────────────────────────────────────────────
|
||||
def _get_oci_config(oci_config_id: str) -> dict:
|
||||
import oci
|
||||
config_path = str(OCI_DIR / oci_config_id / "config")
|
||||
return oci.config.from_file(config_path, "DEFAULT")
|
||||
|
||||
# ── Auth deps ─────────────────────────────────────────────────────────────────
|
||||
async def current_user(cred: HTTPAuthorizationCredentials = Depends(security)):
|
||||
try: p = pyjwt.decode(cred.credentials, APP_SECRET, algorithms=[JWT_ALG])
|
||||
@@ -221,13 +236,17 @@ class ChangePwReq(BaseModel):
|
||||
class UserUpdateReq(BaseModel):
|
||||
email: Optional[str] = None; role: Optional[str] = None; is_active: Optional[bool] = None
|
||||
class ChatMsg(BaseModel):
|
||||
message: str; session_id: Optional[str] = None; model_id: Optional[str] = None; genai_config_id: Optional[str] = None
|
||||
message: str; session_id: Optional[str] = None; genai_config_id: Optional[str] = None
|
||||
class RunReportReq(BaseModel):
|
||||
config_id: str; mcp_server_id: Optional[str] = None; regions: Optional[List[str]] = None
|
||||
class GenAIConfigReq(BaseModel):
|
||||
oci_config_id: str; model_id: str; compartment_id: str; genai_region: str
|
||||
serving_type: str = "ON_DEMAND"; endpoint_id: Optional[str] = None
|
||||
temperature: float = 0.7; max_tokens: int = 2048; top_p: float = 0.9; top_k: int = -1
|
||||
name: str = "default"
|
||||
oci_config_id: str; model_id: str; model_ocid: Optional[str] = None
|
||||
compartment_id: str; genai_region: str
|
||||
endpoint: Optional[str] = None
|
||||
serving_type: str = "ON_DEMAND"; dedicated_endpoint_id: Optional[str] = None
|
||||
temperature: float = 1; max_tokens: int = 6000; top_p: float = 0.95
|
||||
top_k: int = 1; frequency_penalty: float = 0; presence_penalty: float = 0
|
||||
is_default: bool = False
|
||||
class ADBVectorReq(BaseModel):
|
||||
config_name: str; dsn: str; username: str; password: str
|
||||
@@ -235,7 +254,9 @@ class ADBVectorReq(BaseModel):
|
||||
class MCPServerReq(BaseModel):
|
||||
name: str; description: Optional[str] = None; server_type: str = "stdio"
|
||||
command: Optional[str] = None; args: Optional[List[str]] = None
|
||||
env_vars: Optional[Dict[str,str]] = None; url: Optional[str] = None; module_path: Optional[str] = None
|
||||
env_vars: Optional[Dict[str,str]] = None; url: Optional[str] = None
|
||||
module_path: Optional[str] = None; tools: Optional[List[str]] = None
|
||||
linked_adb_id: Optional[str] = None
|
||||
|
||||
# ── Auth endpoints ────────────────────────────────────────────────────────────
|
||||
@app.post("/api/auth/login")
|
||||
@@ -314,9 +335,9 @@ async def update_user(uid: str, req: UserUpdateReq, adm=Depends(require("admin")
|
||||
if req.role not in ("admin","user","viewer"): raise HTTPException(400, "Role inválida")
|
||||
sets.append("role=?"); vals.append(req.role)
|
||||
if req.is_active is not None: sets.append("is_active=?"); vals.append(int(req.is_active))
|
||||
if sets: vals.append(uid); c_ = db()
|
||||
with db() as c:
|
||||
if sets: c.execute(f"UPDATE users SET {','.join(sets)} WHERE id=?", vals)
|
||||
if sets:
|
||||
vals.append(uid)
|
||||
with db() as c: c.execute(f"UPDATE users SET {','.join(sets)} WHERE id=?", vals)
|
||||
_audit(adm["id"], adm["username"], "update_user", uid)
|
||||
return {"ok": True}
|
||||
|
||||
@@ -340,9 +361,11 @@ async def save_oci(
|
||||
kp = cdir / "oci_api_key.pem"
|
||||
kp.write_bytes(await private_key.read()); kp.chmod(0o600)
|
||||
if public_key: (cdir / "oci_api_key_public.pem").write_bytes(await public_key.read())
|
||||
(cdir / "config").write_text(
|
||||
cfg_file = cdir / "config"
|
||||
cfg_file.write_text(
|
||||
f"[DEFAULT]\nuser={user_ocid}\nfingerprint={fingerprint}\n"
|
||||
f"tenancy={tenancy_ocid}\nregion={region}\nkey_file={kp}\n")
|
||||
cfg_file.chmod(0o600)
|
||||
with db() as c:
|
||||
c.execute("INSERT INTO oci_configs (id,user_id,tenancy_name,tenancy_ocid,user_ocid,fingerprint,region,key_file_path,compartment_id) VALUES (?,?,?,?,?,?,?,?,?)",
|
||||
(cid, u["id"], tenancy_name, tenancy_ocid, user_ocid, fingerprint, region, str(kp), compartment_id or None))
|
||||
@@ -375,11 +398,98 @@ async def test_oci(cid: str, u=Depends(require("admin","user"))):
|
||||
r = subprocess.run(["oci","iam","region","list","--config-file",str(cp),"--output","json"], capture_output=True, text=True, timeout=30)
|
||||
if r.returncode == 0: return {"status":"success","message":"Conexão OK","data":json.loads(r.stdout)}
|
||||
return {"status":"error","message":r.stderr[:500]}
|
||||
except FileNotFoundError: return {"status":"error","message":"OCI CLI não instalado. Instale via admin panel."}
|
||||
except FileNotFoundError: return {"status":"error","message":"OCI CLI não disponível no container."}
|
||||
except subprocess.TimeoutExpired: return {"status":"error","message":"Timeout na conexão"}
|
||||
except Exception as e: return {"status":"error","message":str(e)}
|
||||
|
||||
# ── OCI GenAI Config ──────────────────────────────────────────────────────────
|
||||
# ── OCI Account Explorer ──────────────────────────────────────────────────────
|
||||
@app.get("/api/oci/explore/{cid}/compartments")
|
||||
async def explore_compartments(cid: str, u=Depends(current_user)):
|
||||
try:
|
||||
import oci
|
||||
config = _get_oci_config(cid)
|
||||
identity = oci.identity.IdentityClient(config)
|
||||
with db() as c:
|
||||
cfg = c.execute("SELECT tenancy_ocid,compartment_id FROM oci_configs WHERE id=?", (cid,)).fetchone()
|
||||
root = cfg["compartment_id"] or cfg["tenancy_ocid"]
|
||||
comps = identity.list_compartments(root, compartment_id_in_subtree=True).data
|
||||
return [{"id":cp.id,"name":cp.name,"lifecycle_state":cp.lifecycle_state,"description":cp.description or ""} for cp in comps if cp.lifecycle_state == "ACTIVE"]
|
||||
except Exception as e:
|
||||
return {"error": str(e)[:500]}
|
||||
|
||||
@app.get("/api/oci/explore/{cid}/regions")
|
||||
async def explore_regions(cid: str, u=Depends(current_user)):
|
||||
try:
|
||||
import oci
|
||||
config = _get_oci_config(cid)
|
||||
identity = oci.identity.IdentityClient(config)
|
||||
with db() as c:
|
||||
cfg = c.execute("SELECT tenancy_ocid FROM oci_configs WHERE id=?", (cid,)).fetchone()
|
||||
regions = identity.list_region_subscriptions(cfg["tenancy_ocid"]).data
|
||||
return [{"name":r.region_name,"key":r.region_key,"status":r.status,"is_home":r.is_home_region} for r in regions]
|
||||
except Exception as e:
|
||||
return {"error": str(e)[:500]}
|
||||
|
||||
@app.get("/api/oci/explore/{cid}/vcns")
|
||||
async def explore_vcns(cid: str, compartment_id: str = Query(None), u=Depends(current_user)):
|
||||
try:
|
||||
import oci
|
||||
config = _get_oci_config(cid)
|
||||
vn = oci.core.VirtualNetworkClient(config)
|
||||
with db() as c:
|
||||
cfg = c.execute("SELECT tenancy_ocid,compartment_id FROM oci_configs WHERE id=?", (cid,)).fetchone()
|
||||
comp = compartment_id or cfg["compartment_id"] or cfg["tenancy_ocid"]
|
||||
vcns = vn.list_vcns(comp).data
|
||||
return [{"id":v.id,"display_name":v.display_name,"cidr_blocks":v.cidr_blocks,"lifecycle_state":v.lifecycle_state} for v in vcns]
|
||||
except Exception as e:
|
||||
return {"error": str(e)[:500]}
|
||||
|
||||
@app.get("/api/oci/explore/{cid}/instances")
|
||||
async def explore_instances(cid: str, compartment_id: str = Query(None), u=Depends(current_user)):
|
||||
try:
|
||||
import oci
|
||||
config = _get_oci_config(cid)
|
||||
compute = oci.core.ComputeClient(config)
|
||||
with db() as c:
|
||||
cfg = c.execute("SELECT tenancy_ocid,compartment_id FROM oci_configs WHERE id=?", (cid,)).fetchone()
|
||||
comp = compartment_id or cfg["compartment_id"] or cfg["tenancy_ocid"]
|
||||
insts = compute.list_instances(comp).data
|
||||
return [{"id":i.id,"display_name":i.display_name,"shape":i.shape,"lifecycle_state":i.lifecycle_state,"region":i.region,"time_created":str(i.time_created)} for i in insts]
|
||||
except Exception as e:
|
||||
return {"error": str(e)[:500]}
|
||||
|
||||
@app.get("/api/oci/explore/{cid}/databases")
|
||||
async def explore_databases(cid: str, compartment_id: str = Query(None), u=Depends(current_user)):
|
||||
try:
|
||||
import oci
|
||||
config = _get_oci_config(cid)
|
||||
db_client = oci.database.DatabaseClient(config)
|
||||
with db() as c:
|
||||
cfg = c.execute("SELECT tenancy_ocid,compartment_id FROM oci_configs WHERE id=?", (cid,)).fetchone()
|
||||
comp = compartment_id or cfg["compartment_id"] or cfg["tenancy_ocid"]
|
||||
adbs = db_client.list_autonomous_databases(comp).data
|
||||
return [{"id":a.id,"display_name":a.display_name,"db_name":a.db_name,"lifecycle_state":a.lifecycle_state,
|
||||
"cpu_core_count":a.cpu_core_count,"data_storage_size_in_tbs":a.data_storage_size_in_tbs,
|
||||
"is_free_tier":a.is_free_tier} for a in adbs]
|
||||
except Exception as e:
|
||||
return {"error": str(e)[:500]}
|
||||
|
||||
@app.get("/api/oci/explore/{cid}/buckets")
|
||||
async def explore_buckets(cid: str, compartment_id: str = Query(None), u=Depends(current_user)):
|
||||
try:
|
||||
import oci
|
||||
config = _get_oci_config(cid)
|
||||
os_client = oci.object_storage.ObjectStorageClient(config)
|
||||
namespace = os_client.get_namespace().data
|
||||
with db() as c:
|
||||
cfg = c.execute("SELECT tenancy_ocid,compartment_id FROM oci_configs WHERE id=?", (cid,)).fetchone()
|
||||
comp = compartment_id or cfg["compartment_id"] or cfg["tenancy_ocid"]
|
||||
buckets = os_client.list_buckets(namespace, comp).data
|
||||
return [{"name":b.name,"namespace":b.namespace,"time_created":str(b.time_created)} for b in buckets]
|
||||
except Exception as e:
|
||||
return {"error": str(e)[:500]}
|
||||
|
||||
# ── 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}
|
||||
@@ -387,15 +497,20 @@ async def list_genai_models(u=Depends(current_user)):
|
||||
@app.post("/api/genai/config")
|
||||
async def save_genai(req: GenAIConfigReq, u=Depends(require("admin","user"))):
|
||||
gid = str(uuid.uuid4())
|
||||
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(
|
||||
"INSERT INTO genai_configs (id,user_id,oci_config_id,model_id,compartment_id,genai_region,serving_type,endpoint_id,temperature,max_tokens,top_p,top_k,is_default) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
(gid, u["id"], req.oci_config_id, req.model_id, req.compartment_id, req.genai_region,
|
||||
req.serving_type, req.endpoint_id, req.temperature, req.max_tokens, req.top_p, req.top_k, int(req.is_default)))
|
||||
"""INSERT INTO genai_configs (id,user_id,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) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(gid, u["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)))
|
||||
_audit(u["id"], u["username"], "save_genai_config", gid, req.model_id)
|
||||
return {"id": gid, "model_id": req.model_id}
|
||||
return {"id": gid, "model_id": req.model_id, "endpoint": ep}
|
||||
|
||||
@app.get("/api/genai/configs")
|
||||
async def list_genai(u=Depends(current_user)):
|
||||
@@ -411,79 +526,123 @@ async def del_genai(gid: str, u=Depends(require("admin","user"))):
|
||||
|
||||
@app.post("/api/genai/test/{gid}")
|
||||
async def test_genai(gid: str, u=Depends(require("admin","user"))):
|
||||
"""Test GenAI connection by sending a simple prompt"""
|
||||
with db() as c:
|
||||
gc = c.execute("SELECT g.*,o.key_file_path,o.tenancy_ocid,o.user_ocid,o.fingerprint FROM genai_configs g JOIN oci_configs o ON g.oci_config_id=o.id WHERE g.id=?",(gid,)).fetchone()
|
||||
gc = c.execute("SELECT * FROM genai_configs WHERE id=?",(gid,)).fetchone()
|
||||
if not gc: raise HTTPException(404)
|
||||
try:
|
||||
resp = await _call_genai(dict(gc), "Say 'connection successful' in one sentence.")
|
||||
return {"status":"success","message":"GenAI OK","response":resp[:200]}
|
||||
resp = _call_genai(dict(gc), "Say 'connection successful' in one short sentence.")
|
||||
return {"status":"success","message":"GenAI OK","response":resp[:300]}
|
||||
except Exception as e:
|
||||
return {"status":"error","message":str(e)[:500]}
|
||||
|
||||
async def _call_genai(gc: dict, message: str, history: list = None) -> str:
|
||||
"""Call OCI Generative AI chat endpoint using OCI SDK"""
|
||||
try:
|
||||
import oci
|
||||
except ImportError:
|
||||
return "❌ OCI SDK não instalado. Execute: pip install oci"
|
||||
def _call_genai(gc: dict, message: str, history: list = None) -> str:
|
||||
"""
|
||||
Call OCI Generative AI using the exact SDK pattern from chat_demo.py.
|
||||
Uses oci.generative_ai_inference with proper Message/TextContent objects.
|
||||
"""
|
||||
import oci
|
||||
|
||||
config_path = str(Path(gc["key_file_path"]).parent / "config")
|
||||
try:
|
||||
config = oci.config.from_file(config_path, "DEFAULT")
|
||||
except Exception:
|
||||
config = {
|
||||
"user": gc["user_ocid"], "tenancy": gc["tenancy_ocid"],
|
||||
"fingerprint": gc["fingerprint"], "key_file": gc["key_file_path"],
|
||||
"region": gc["genai_region"]
|
||||
}
|
||||
# Load OCI config from stored credentials (same as ~/.oci/config)
|
||||
config_path = str(OCI_DIR / gc["oci_config_id"] / "config")
|
||||
config = oci.config.from_file(config_path, "DEFAULT")
|
||||
|
||||
endpoint = f"https://inference.generativeai.{gc['genai_region']}.oci.oraclecloud.com"
|
||||
client = oci.generative_ai_inference.GenerativeAiInferenceClient(config, service_endpoint=endpoint)
|
||||
# Service endpoint - built from region
|
||||
endpoint = gc["endpoint"]
|
||||
|
||||
# Create inference client with retry strategy and timeout
|
||||
generative_ai_inference_client = oci.generative_ai_inference.GenerativeAiInferenceClient(
|
||||
config=config,
|
||||
service_endpoint=endpoint,
|
||||
retry_strategy=oci.retry.NoneRetryStrategy(),
|
||||
timeout=(10, 240)
|
||||
)
|
||||
|
||||
# Build ChatDetails
|
||||
chat_detail = oci.generative_ai_inference.models.ChatDetails()
|
||||
|
||||
# Determine API format from model catalog
|
||||
model_info = GENAI_MODELS.get(gc["model_id"], {})
|
||||
api_format = model_info.get("api_format", "GENERIC")
|
||||
|
||||
if gc["serving_type"] == "DEDICATED" and gc.get("endpoint_id"):
|
||||
serving_mode = oci.generative_ai_inference.models.DedicatedServingMode(endpoint_id=gc["endpoint_id"])
|
||||
else:
|
||||
serving_mode = oci.generative_ai_inference.models.OnDemandServingMode(model_id=gc["model_id"])
|
||||
|
||||
if api_format == "COHERE":
|
||||
chat_request = oci.generative_ai_inference.models.CohereChatRequest(
|
||||
message=message, max_tokens=gc.get("max_tokens", 2048),
|
||||
temperature=gc.get("temperature", 0.7), top_p=gc.get("top_p", 0.9),
|
||||
api_format="COHERE"
|
||||
)
|
||||
# ── Cohere models (CohereChatRequest) ──
|
||||
chat_request = oci.generative_ai_inference.models.CohereChatRequest()
|
||||
chat_request.api_format = oci.generative_ai_inference.models.BaseChatRequest.API_FORMAT_COHERE
|
||||
chat_request.message = message
|
||||
chat_request.max_tokens = int(gc.get("max_tokens", 6000))
|
||||
chat_request.temperature = float(gc.get("temperature", 1))
|
||||
chat_request.frequency_penalty = float(gc.get("frequency_penalty", 0))
|
||||
chat_request.presence_penalty = float(gc.get("presence_penalty", 0))
|
||||
chat_request.top_p = float(gc.get("top_p", 0.95))
|
||||
chat_request.top_k = int(gc.get("top_k", 1))
|
||||
if history:
|
||||
chat_history = []
|
||||
for h in history:
|
||||
entry = oci.generative_ai_inference.models.CohereMessage()
|
||||
entry.role = "USER" if h["role"] == "user" else "CHATBOT"
|
||||
entry.message = h["content"]
|
||||
chat_history.append(entry)
|
||||
chat_request.chat_history = chat_history
|
||||
else:
|
||||
msgs = []
|
||||
# ── Generic format (Meta Llama, Google, xAI) - exact chat_demo.py pattern ──
|
||||
chat_request = oci.generative_ai_inference.models.GenericChatRequest()
|
||||
chat_request.api_format = oci.generative_ai_inference.models.BaseChatRequest.API_FORMAT_GENERIC
|
||||
chat_request.max_tokens = int(gc.get("max_tokens", 6000))
|
||||
chat_request.temperature = float(gc.get("temperature", 1))
|
||||
chat_request.frequency_penalty = float(gc.get("frequency_penalty", 0))
|
||||
chat_request.presence_penalty = float(gc.get("presence_penalty", 0))
|
||||
chat_request.top_p = float(gc.get("top_p", 0.95))
|
||||
chat_request.top_k = int(gc.get("top_k", 1))
|
||||
|
||||
messages = []
|
||||
if history:
|
||||
for h in history:
|
||||
role = "USER" if h["role"] == "user" else "ASSISTANT"
|
||||
msgs.append(oci.generative_ai_inference.models.GenericChatMessage(
|
||||
role=role, content=[oci.generative_ai_inference.models.TextContent(text=h["content"])]
|
||||
))
|
||||
msgs.append(oci.generative_ai_inference.models.GenericChatMessage(
|
||||
role="USER", content=[oci.generative_ai_inference.models.TextContent(text=message)]
|
||||
))
|
||||
chat_request = oci.generative_ai_inference.models.GenericChatRequest(
|
||||
messages=msgs, max_tokens=gc.get("max_tokens", 2048),
|
||||
temperature=gc.get("temperature", 0.7), top_p=gc.get("top_p", 0.9),
|
||||
api_format="GENERIC"
|
||||
content = oci.generative_ai_inference.models.TextContent()
|
||||
content.text = h["content"]
|
||||
msg = oci.generative_ai_inference.models.Message()
|
||||
msg.role = "USER" if h["role"] == "user" else "ASSISTANT"
|
||||
msg.content = [content]
|
||||
messages.append(msg)
|
||||
|
||||
# Current user message
|
||||
content = oci.generative_ai_inference.models.TextContent()
|
||||
content.text = message
|
||||
user_message = oci.generative_ai_inference.models.Message()
|
||||
user_message.role = "USER"
|
||||
user_message.content = [content]
|
||||
messages.append(user_message)
|
||||
|
||||
chat_request.messages = messages
|
||||
|
||||
# Serving mode - supports model_id or model_ocid
|
||||
model_ref = gc.get("model_ocid") or gc["model_id"]
|
||||
if gc.get("serving_type") == "DEDICATED" and gc.get("dedicated_endpoint_id"):
|
||||
chat_detail.serving_mode = oci.generative_ai_inference.models.DedicatedServingMode(
|
||||
endpoint_id=gc["dedicated_endpoint_id"]
|
||||
)
|
||||
else:
|
||||
chat_detail.serving_mode = oci.generative_ai_inference.models.OnDemandServingMode(
|
||||
model_id=model_ref
|
||||
)
|
||||
|
||||
chat_detail = oci.generative_ai_inference.models.ChatDetails(
|
||||
compartment_id=gc["compartment_id"], serving_mode=serving_mode, chat_request=chat_request
|
||||
)
|
||||
response = client.chat(chat_detail)
|
||||
chat_response = response.data.chat_response
|
||||
chat_detail.chat_request = chat_request
|
||||
chat_detail.compartment_id = gc["compartment_id"]
|
||||
|
||||
# Execute
|
||||
chat_response = generative_ai_inference_client.chat(chat_detail)
|
||||
|
||||
# Extract text from response
|
||||
resp = chat_response.data.chat_response
|
||||
if api_format == "COHERE":
|
||||
return chat_response.text if hasattr(chat_response, 'text') else str(chat_response)
|
||||
return resp.text if hasattr(resp, 'text') else str(resp)
|
||||
else:
|
||||
if hasattr(chat_response, 'choices') and chat_response.choices:
|
||||
return chat_response.choices[0].message.content[0].text if chat_response.choices[0].message.content else ""
|
||||
return str(chat_response)
|
||||
if hasattr(resp, 'choices') and resp.choices:
|
||||
choice = resp.choices[0]
|
||||
if hasattr(choice, 'message') and choice.message and hasattr(choice.message, 'content'):
|
||||
contents = choice.message.content
|
||||
if contents and len(contents) > 0:
|
||||
return contents[0].text
|
||||
return str(resp)
|
||||
|
||||
# ── MCP Servers ───────────────────────────────────────────────────────────────
|
||||
@app.post("/api/mcp/servers")
|
||||
@@ -491,10 +650,11 @@ async def register_mcp(req: MCPServerReq, u=Depends(require("admin","user"))):
|
||||
mid = str(uuid.uuid4())
|
||||
with db() as c:
|
||||
c.execute(
|
||||
"INSERT INTO mcp_servers (id,user_id,name,description,server_type,command,args,env_vars,url,module_path) VALUES (?,?,?,?,?,?,?,?,?,?)",
|
||||
"INSERT INTO mcp_servers (id,user_id,name,description,server_type,command,args,env_vars,url,module_path,tools,linked_adb_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
(mid, u["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, req.module_path))
|
||||
json.dumps(req.env_vars) if req.env_vars else None, req.url, req.module_path,
|
||||
json.dumps(req.tools) if req.tools else None, req.linked_adb_id))
|
||||
_audit(u["id"], u["username"], "register_mcp", mid, req.name)
|
||||
return {"id": mid, "name": req.name, "server_type": req.server_type}
|
||||
|
||||
@@ -506,8 +666,10 @@ async def list_mcp(u=Depends(current_user)):
|
||||
res = []
|
||||
for r in rows:
|
||||
d = dict(r)
|
||||
if d.get("args"): d["args"] = json.loads(d["args"])
|
||||
if d.get("env_vars"): d["env_vars"] = json.loads(d["env_vars"])
|
||||
for k in ("args","env_vars","tools"):
|
||||
if d.get(k):
|
||||
try: d[k] = json.loads(d[k])
|
||||
except: pass
|
||||
res.append(d)
|
||||
return res
|
||||
|
||||
@@ -526,14 +688,17 @@ async def toggle_mcp(mid: str, u=Depends(require("admin","user"))):
|
||||
|
||||
@app.post("/api/mcp/servers/{mid}/upload")
|
||||
async def upload_mcp_file(mid: str, file: UploadFile = File(...), u=Depends(require("admin","user"))):
|
||||
"""Upload a Python MCP server script"""
|
||||
sdir = MCP_DIR / mid; sdir.mkdir(parents=True, exist_ok=True)
|
||||
fp = sdir / file.filename
|
||||
fp.write_bytes(await file.read())
|
||||
with db() as c:
|
||||
c.execute("UPDATE mcp_servers SET module_path=? WHERE id=?", (str(fp), mid))
|
||||
with db() as c: c.execute("UPDATE mcp_servers SET module_path=? WHERE id=?", (str(fp), mid))
|
||||
return {"ok": True, "path": str(fp)}
|
||||
|
||||
@app.put("/api/mcp/servers/{mid}/link-adb")
|
||||
async def link_mcp_adb(mid: str, adb_id: str = Query(...), u=Depends(require("admin","user"))):
|
||||
with db() as c: c.execute("UPDATE mcp_servers SET linked_adb_id=? WHERE id=?", (adb_id, mid))
|
||||
return {"ok": True, "mcp_id": mid, "linked_adb_id": adb_id}
|
||||
|
||||
# ── ADB Vector DB Config ─────────────────────────────────────────────────────
|
||||
@app.post("/api/adb/config")
|
||||
async def save_adb(req: ADBVectorReq, u=Depends(require("admin","user"))):
|
||||
@@ -580,7 +745,7 @@ async def test_adb(vid: str, u=Depends(require("admin","user"))):
|
||||
cur = conn.cursor(); cur.execute("SELECT 1 FROM DUAL"); cur.close(); conn.close()
|
||||
return {"status":"success","message":"Conexão Autonomous DB OK!"}
|
||||
except ImportError:
|
||||
return {"status":"error","message":"python-oracledb não instalado. Execute: pip install oracledb"}
|
||||
return {"status":"error","message":"python-oracledb não disponível no container."}
|
||||
except Exception as e:
|
||||
return {"status":"error","message":str(e)[:500]}
|
||||
|
||||
@@ -614,7 +779,13 @@ async def _exec_report(rid, cfg, regions, mcp_server_id):
|
||||
if mcp_server_id:
|
||||
with db() as c:
|
||||
mcp = c.execute("SELECT * FROM mcp_servers WHERE id=?",(mcp_server_id,)).fetchone()
|
||||
if mcp and mcp["module_path"]: cmd += ["--mcp-module", mcp["module_path"]]
|
||||
if mcp:
|
||||
if mcp["module_path"]: cmd += ["--mcp-module", mcp["module_path"]]
|
||||
if mcp.get("linked_adb_id"):
|
||||
adb_cfg = c.execute("SELECT * FROM adb_vector_configs WHERE id=?",(mcp["linked_adb_id"],)).fetchone()
|
||||
if adb_cfg:
|
||||
cmd += ["--adb-dsn", adb_cfg["dsn"], "--adb-user", adb_cfg["username"]]
|
||||
if adb_cfg.get("wallet_dir"): cmd += ["--adb-wallet", adb_cfg["wallet_dir"]]
|
||||
proc = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
||||
stdout, stderr = await proc.communicate()
|
||||
jp = rdir / "report.json"; hp = rdir / "report.html"
|
||||
@@ -668,36 +839,30 @@ async def chat(msg: ChatMsg, u=Depends(current_user)):
|
||||
sid = msg.session_id or str(uuid.uuid4())
|
||||
with db() as c:
|
||||
c.execute("INSERT INTO chat_messages (id,session_id,user_id,role,content,model_id) VALUES (?,?,?,?,?,?)",
|
||||
(str(uuid.uuid4()), sid, u["id"], "user", msg.message, msg.model_id))
|
||||
(str(uuid.uuid4()), sid, u["id"], "user", msg.message, None))
|
||||
|
||||
genai_cfg = None
|
||||
if msg.genai_config_id:
|
||||
with db() as c:
|
||||
genai_cfg = c.execute(
|
||||
"SELECT g.*,o.key_file_path,o.tenancy_ocid,o.user_ocid,o.fingerprint FROM genai_configs g JOIN oci_configs o ON g.oci_config_id=o.id WHERE g.id=?",
|
||||
(msg.genai_config_id,)).fetchone()
|
||||
elif msg.model_id:
|
||||
with db() as c:
|
||||
genai_cfg = c.execute(
|
||||
"SELECT g.*,o.key_file_path,o.tenancy_ocid,o.user_ocid,o.fingerprint FROM genai_configs g JOIN oci_configs o ON g.oci_config_id=o.id WHERE g.user_id=? AND g.model_id=? LIMIT 1",
|
||||
(u["id"], msg.model_id)).fetchone()
|
||||
genai_cfg = c.execute("SELECT * FROM genai_configs WHERE id=?", (msg.genai_config_id,)).fetchone()
|
||||
|
||||
if genai_cfg:
|
||||
try:
|
||||
history = []
|
||||
with db() as c:
|
||||
prev = c.execute("SELECT role,content FROM chat_messages WHERE session_id=? ORDER BY created_at ASC", (sid,)).fetchall()
|
||||
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 = await _call_genai(dict(genai_cfg), msg.message, history[:-1] if history else None)
|
||||
resp = _call_genai(dict(genai_cfg), msg.message, history[:-1] if len(history) > 1 else None)
|
||||
except Exception as e:
|
||||
resp = f"❌ Erro GenAI: {str(e)[:300]}"
|
||||
resp = f"❌ Erro GenAI: {str(e)[:400]}"
|
||||
else:
|
||||
resp = _agent_respond(msg.message, u)
|
||||
|
||||
mid = genai_cfg["model_id"] if genai_cfg else None
|
||||
with db() as c:
|
||||
c.execute("INSERT INTO chat_messages (id,session_id,user_id,role,content,model_id) VALUES (?,?,?,?,?,?)",
|
||||
(str(uuid.uuid4()), sid, u["id"], "assistant", resp, msg.model_id))
|
||||
return {"session_id": sid, "response": resp, "model_id": msg.model_id}
|
||||
(str(uuid.uuid4()), sid, u["id"], "assistant", resp, mid))
|
||||
return {"session_id": sid, "response": resp, "model_id": mid}
|
||||
|
||||
def _agent_respond(msg, user):
|
||||
m = msg.lower().strip()
|
||||
@@ -709,7 +874,7 @@ def _agent_respond(msg, user):
|
||||
nu=c.execute("SELECT COUNT(*) n FROM users WHERE is_active=1").fetchone()["n"]
|
||||
nm=c.execute("SELECT COUNT(*) n FROM mcp_servers WHERE is_active=1").fetchone()["n"]
|
||||
ng=c.execute("SELECT COUNT(*) n FROM genai_configs").fetchone()["n"]
|
||||
return (f"📊 **Status do Sistema**\n\n• OCI CLI: {cli}\n• Configs OCI: {nc}\n• GenAI Models: {ng}\n• MCP Servers: {nm}\n• Relatórios: {nr}\n• Usuários ativos: {nu}\n• Servidor: ✅ Online")
|
||||
return (f"📊 **Status do Sistema**\n\n• OCI CLI: {cli}\n• Configs OCI: {nc}\n• GenAI Models: {ng}\n• MCP Servers: {nm}\n• Relatórios: {nr}\n• Usuários ativos: {nu}\n• Servidor: ✅ Online\n• Versão: v{VERSION}")
|
||||
if any(k in m for k in ["ajuda","help","comandos"]):
|
||||
return ("🤖 **Comandos disponíveis:**\n\n• `status` — Status do sistema\n• `listar configs` — Configurações OCI\n• `verificar cis` — Checks CIS 3.0\n• `modelos` — Modelos GenAI disponíveis\n• `ajuda` — Esta mensagem\n\n💡 Configure um modelo GenAI para chat com IA.")
|
||||
if any(k in m for k in ["modelo","modelos","genai"]):
|
||||
@@ -720,7 +885,7 @@ def _agent_respond(msg, user):
|
||||
return "\n".join(lines)
|
||||
if any(k in m for k in ["cis","benchmark","checks","verificar"]):
|
||||
return ("🔒 **CIS OCI Foundations Benchmark 3.0**\n\n54 controles em 8 domínios:\n• IAM: 17 controles\n• Networking: 8 controles\n• Compute: 3 controles\n• Logging & Monitoring: 18 controles\n• Storage: 6 controles\n• Asset Management: 2 controles\n\nConfigure OCI e execute um relatório na aba **Report**.")
|
||||
return ("Sou o **OCI CIS AI Agent**. Sem modelo GenAI configurado, uso respostas locais.\n\n"
|
||||
return ("Sou o **OCI CIS AI Agent v" + VERSION + "**. Sem modelo GenAI configurado, uso respostas locais.\n\n"
|
||||
"Para chat com IA:\n1. Configure **OCI Credentials**\n2. Configure **GenAI** com modelo e região\n3. Selecione o modelo no chat\n\nDigite **ajuda** para ver os comandos.")
|
||||
|
||||
@app.delete("/api/chat/{sid}")
|
||||
@@ -758,12 +923,12 @@ async def audit_log(limit:int=Query(100,le=500), u=Depends(require("admin"))):
|
||||
# ── Health ────────────────────────────────────────────────────────────────────
|
||||
@app.get("/api/health")
|
||||
async def health():
|
||||
return {"status":"ok","ts":datetime.utcnow().isoformat(),"version":"2.0.0"}
|
||||
return {"status":"ok","ts":datetime.utcnow().isoformat(),"version":VERSION}
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
init_db()
|
||||
log.info("OCI CIS AI Agent v2 API started")
|
||||
log.info(f"OCI CIS AI Agent v{VERSION} API started")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
@@ -9,6 +9,7 @@ services:
|
||||
- APP_SECRET=${APP_SECRET:-change-me-in-production-use-a-long-random-string}
|
||||
- JWT_EXPIRY_HOURS=${JWT_EXPIRY_HOURS:-12}
|
||||
- DATA_DIR=/data
|
||||
- OCI_CLI_SUPPRESS_FILE_PERMISSIONS_WARNING=True
|
||||
volumes:
|
||||
- agent-data:/data
|
||||
ports:
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:var(--fs);background:var(--bg0);color:var(--t1);min-height:100vh}
|
||||
.app{display:flex;min-height:100vh}
|
||||
/* ── Sidebar ── */
|
||||
.sb{width:250px;background:var(--bg1);border-right:1px solid var(--bd);display:flex;flex-direction:column;position:fixed;top:0;left:0;bottom:0;z-index:100}
|
||||
.sb-h{padding:1.15rem 1rem;border-bottom:1px solid var(--bd);background:linear-gradient(135deg,#c74634 0%,#a53727 100%)}
|
||||
.sb-h h1{font-size:.88rem;font-weight:700;color:#fff;font-family:var(--fm);letter-spacing:-.02em;display:flex;align-items:center;gap:.4rem}
|
||||
@@ -28,66 +27,59 @@ body{font-family:var(--fs);background:var(--bg0);color:var(--t1);min-height:100v
|
||||
.ui{display:flex;align-items:center;gap:.5rem}
|
||||
.ua{width:28px;height:28px;background:var(--ac);border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:.7rem;color:#fff}
|
||||
.un{font-weight:600;color:var(--t1);font-size:.78rem}.ur{color:var(--tm);font-size:.63rem;font-family:var(--fm)}
|
||||
/* ── Main ── */
|
||||
.mc{flex:1;margin-left:250px;min-height:100vh}
|
||||
.tb{height:50px;background:var(--bg1);border-bottom:1px solid var(--bd);display:flex;align-items:center;justify-content:space-between;padding:0 1.15rem;position:sticky;top:0;z-index:50}
|
||||
.tb-t{font-weight:600;font-size:.88rem;color:var(--t1)}.tb-a{display:flex;align-items:center;gap:.5rem}
|
||||
.pc{padding:1.15rem}
|
||||
.cd{background:var(--bg1);border:1px solid var(--bd);border-radius:var(--rl);padding:1.15rem;margin-bottom:.65rem;box-shadow:var(--sh)}
|
||||
.ct{font-weight:600;font-size:.88rem;margin-bottom:.65rem;display:flex;align-items:center;gap:.35rem;color:var(--t1)}
|
||||
/* ── Buttons ── */
|
||||
.btn{display:inline-flex;align-items:center;gap:.35rem;padding:.42rem .8rem;border-radius:var(--r);font-size:.76rem;font-weight:600;cursor:pointer;border:1px solid transparent;transition:all .15s;font-family:var(--fs)}
|
||||
.bp{background:var(--ac);color:#fff;border-color:var(--ac)}.bp:hover{background:var(--ah)}
|
||||
.bs{background:var(--bg1);color:var(--t2);border-color:var(--bd)}.bs:hover{border-color:var(--tm);color:var(--t1)}
|
||||
.bd{background:var(--rdl);color:var(--rd);border-color:rgba(199,70,52,.2)}.bd:hover{background:var(--rd);color:#fff}
|
||||
.bsm{padding:.28rem .55rem;font-size:.7rem}
|
||||
.btn:disabled{opacity:.5;cursor:not-allowed}
|
||||
/* ── Form ── */
|
||||
.ig{margin-bottom:.65rem}.ig label{display:block;font-size:.76rem;font-weight:600;color:var(--t2);margin-bottom:.2rem}
|
||||
.ig .ht{font-size:.65rem;color:var(--tm);margin-bottom:.15rem}
|
||||
input[type=text],input[type=password],input[type=email],input[type=number],select,textarea{width:100%;padding:.45rem .6rem;background:var(--bg1);border:1px solid var(--bd);border-radius:var(--r);color:var(--t1);font-size:.78rem;font-family:var(--fm);transition:border .15s}
|
||||
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--ac);box-shadow:0 0 0 2px rgba(199,70,52,.12)}
|
||||
input[type=file]{font-family:var(--fm);font-size:.73rem;color:var(--t2)}
|
||||
/* ── Badges ── */
|
||||
.badge{padding:.15rem .5rem;border-radius:12px;font-size:.66rem;font-weight:600;font-family:var(--fm)}
|
||||
.b-pass{background:var(--gnl);color:var(--gn)}.b-fail{background:var(--rdl);color:var(--rd)}
|
||||
.b-pend{background:var(--yll);color:var(--yl)}.b-rev{background:var(--bll);color:var(--bl)}
|
||||
.b-admin{background:var(--ppl);color:var(--pp)}.b-user{background:var(--bll);color:var(--bl)}.b-viewer{background:var(--bg3);color:var(--tm)}
|
||||
.b-on{background:var(--gnl);color:var(--gn)}.b-off{background:var(--bg3);color:var(--tm)}
|
||||
/* ── Table ── */
|
||||
table{width:100%;border-collapse:collapse;font-size:.76rem}
|
||||
th{text-align:left;padding:.5rem .6rem;font-size:.65rem;text-transform:uppercase;letter-spacing:.05em;color:var(--tm);border-bottom:2px solid var(--bd);font-weight:700;background:var(--bg2)}
|
||||
td{padding:.5rem .6rem;border-bottom:1px solid var(--bg3);color:var(--t2)}
|
||||
tr:hover td{background:var(--acl)}
|
||||
.g2{display:grid;grid-template-columns:1fr 1fr;gap:.65rem}
|
||||
.g3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:.65rem}
|
||||
/* ── Chat ── */
|
||||
.ch-c{display:flex;flex-direction:column;height:calc(100vh - 50px - 2.3rem)}
|
||||
.ch-m{flex:1;overflow-y:auto;padding:.65rem;background:var(--bg2);border:1px solid var(--bd);border-radius:var(--rl) var(--rl) 0 0}
|
||||
.cm{margin-bottom:.65rem;display:flex;gap:.5rem}
|
||||
.cm-u{justify-content:flex-end}
|
||||
.cm-u .cb{background:var(--ac);color:#fff;border-radius:var(--rl) var(--rl) 4px var(--rl);border-color:var(--ac)}
|
||||
.cm{margin-bottom:.65rem;display:flex;gap:.5rem}.cm-user{justify-content:flex-end}
|
||||
.cm-user .cb{background:var(--ac);color:#fff;border-radius:var(--rl) var(--rl) 4px var(--rl);border-color:var(--ac)}
|
||||
.cb{max-width:75%;padding:.55rem .75rem;border-radius:var(--rl) var(--rl) var(--rl) 4px;background:var(--bg1);font-size:.8rem;line-height:1.55;border:1px solid var(--bd);white-space:pre-wrap;box-shadow:var(--sh)}
|
||||
.cb code{font-family:var(--fm);font-size:.73rem;background:var(--bg3);padding:.1rem .2rem;border-radius:3px}
|
||||
.cb strong{color:var(--ac)}
|
||||
.ch-i{display:flex;gap:.35rem;padding:.55rem;background:var(--bg1);border:1px solid var(--bd);border-top:none;border-radius:0 0 var(--rl) var(--rl)}
|
||||
.ch-i input{flex:1}
|
||||
.ch-t{display:flex;gap:.35rem;align-items:center;padding:.35rem .55rem;background:var(--bg2);border:1px solid var(--bd);border-bottom:none;border-radius:var(--rl) var(--rl) 0 0;font-size:.7rem}
|
||||
.ch-t{display:flex;gap:.35rem;align-items:center;padding:.35rem .55rem;background:var(--bg2);border:1px solid var(--bd);border-bottom:none;border-radius:var(--rl) var(--rl) 0 0;font-size:.7rem;flex-wrap:wrap}
|
||||
.ch-t select{max-width:240px;font-size:.72rem;padding:.25rem .4rem}
|
||||
.ch-t label{font-weight:600;color:var(--tm);font-size:.68rem}
|
||||
/* ── Report iframe ── */
|
||||
.rif{width:100%;height:calc(100vh - 50px - 5.5rem);border:1px solid var(--bd);border-radius:var(--rl);background:#fff}
|
||||
/* ── Login ── */
|
||||
.lp{min-height:100vh;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#fef2f0 0%,#f0f2f5 50%,#e6f1fa 100%)}
|
||||
.lc{background:var(--bg1);border:1px solid var(--bd);border-radius:var(--rl);padding:2rem;width:100%;max-width:370px;box-shadow:0 4px 24px rgba(0,0,0,.08)}
|
||||
.lc h2{font-size:1.1rem;font-family:var(--fm);color:var(--ac)}.lc .st{color:var(--tm);font-size:.76rem;margin-bottom:1.15rem}
|
||||
/* ── Alerts ── */
|
||||
.al{padding:.55rem .75rem;border-radius:var(--r);font-size:.76rem;margin-bottom:.65rem}
|
||||
.al-e{background:var(--rdl);color:var(--rd);border:1px solid rgba(199,70,52,.2)}
|
||||
.al-s{background:var(--gnl);color:var(--gn);border:1px solid rgba(13,138,65,.2)}
|
||||
.al-i{background:var(--bll);color:var(--bl);border:1px solid rgba(11,114,185,.2)}
|
||||
.tag{display:inline-block;padding:.1rem .35rem;background:var(--acl);color:var(--ac);border-radius:4px;font-size:.65rem;font-family:var(--fm);font-weight:500}
|
||||
.emp{text-align:center;padding:2rem;color:var(--tm)}.emp .eic{font-size:2rem;margin-bottom:.4rem}
|
||||
.exp-r{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:.5rem;margin-top:.5rem}
|
||||
.exp-c{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r);padding:.65rem;font-size:.76rem}
|
||||
.exp-c strong{color:var(--t1);font-size:.8rem}.exp-c .em{font-family:var(--fm);font-size:.63rem;color:var(--tm);word-break:break-all}
|
||||
@keyframes fi{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.fi{animation:fi .2s ease}
|
||||
@keyframes pu{0%,100%{opacity:1}50%{opacity:.5}}.ld{animation:pu 1.5s infinite;color:var(--tm);font-size:.8rem;text-align:center;padding:1.2rem}
|
||||
::-webkit-scrollbar{width:5px}::-webkit-scrollbar-track{background:var(--bg0)}::-webkit-scrollbar-thumb{background:var(--bd);border-radius:3px}
|
||||
@@ -97,7 +89,8 @@ tr:hover td{background:var(--acl)}
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script>
|
||||
const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],genaiCfg:[],adbCfg:[],mcpSvr:[],users:[],auditLogs:[],models:{},regions:[],selModel:'',selGenai:''};
|
||||
const V='1.1';
|
||||
const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],genaiCfg:[],adbCfg:[],mcpSvr:[],users:[],auditLogs:[],models:{},regions:[],selGenai:'',expData:null};
|
||||
const API='/api';
|
||||
async function $api(p,o={}){const h={...(o.headers||{})};if(S.token)h['Authorization']='Bearer '+S.token;
|
||||
if(o.body&&!(o.body instanceof FormData)){h['Content-Type']='application/json';o.body=JSON.stringify(o.body)}
|
||||
@@ -112,14 +105,14 @@ 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{S.adbCfg=await $api('/adb/configs')}catch(e){S.adbCfg=[]}
|
||||
if(S.user.role==='admin'){S.users=await $api('/users')}
|
||||
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 switchTab(t){S.tab=t;R();if(t==='audit'&&S.user.role==='admin')loadAudit();if(t==='downloads')refreshDl()}
|
||||
function switchTab(t){S.tab=t;S.expData=null;R();if(t==='audit'&&S.user.role==='admin')loadAudit();if(t==='downloads')refreshDl()}
|
||||
|
||||
function rLogin(){return`<div class="lp"><div class="lc fi">
|
||||
<div style="text-align:center;margin-bottom:1.15rem"><div style="font-size:1.6rem;margin-bottom:.3rem">🛡️</div>
|
||||
<h2>OCI CIS Agent</h2><div class="st">Oracle Cloud Infrastructure · Security</div></div>
|
||||
<div style="text-align:center;margin-bottom:1.15rem"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="48" height="48"><rect x="2" y="5" width="32" height="26" rx="13" ry="13" fill="rgba(199,70,52,0.1)" stroke="#C74634" stroke-width="2"/><rect x="11" y="11" width="14" height="14" rx="4" fill="#fff" stroke="#C74634" stroke-width="0.5"/><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.3" stroke="#C74634" stroke-width="0.8"/><line x1="11" y1="18" x2="6" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.5"/><line x1="25" y1="18" x2="30" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.5"/><circle cx="5.5" cy="18" r="1" fill="#C74634" opacity="0.4"/><circle cx="30.5" cy="18" r="1" fill="#C74634" opacity="0.4"/></svg>
|
||||
<h2>OCI CIS Agent</h2><div class="st">Oracle Cloud Infrastructure · Security · v${V}</div></div>
|
||||
<div id="le"></div>
|
||||
<div id="mfa-s" style="display:none"><div class="al al-i">MFA obrigatório. Insira o código do autenticador.</div>
|
||||
<div class="ig"><label>Código MFA</label><input type="text" id="mfa-c" placeholder="000000" maxlength="6" style="text-align:center;font-size:1rem;letter-spacing:.25em"></div>
|
||||
@@ -131,11 +124,11 @@ function rLogin(){return`<div class="lp"><div class="lc fi">
|
||||
|
||||
function rApp(){return`<div class="app">${rSb()}<div class="mc">${rTb()}<div class="pc fi" id="pg">${rPg()}</div></div></div>`}
|
||||
function rSb(){
|
||||
const tabs=[['chat','💬','Chat Agent'],['report','📊','Report'],['downloads','📁','Downloads']];
|
||||
const tabs=[['chat','💬','Chat Agent'],['explorer','🔍','OCI Explorer'],['report','📊','Report'],['downloads','📁','Downloads']];
|
||||
const ctabs=[['oci-config','☁️','OCI Credentials'],['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();
|
||||
return`<div class="sb"><div class="sb-h"><h1>🛡️ OCI CIS Agent</h1><div class="st">Infrastructure & Security Engineer</div></div>
|
||||
return`<div class="sb"><div class="sb-h"><h1><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="28" height="28" style="vertical-align:middle;flex-shrink:0"><rect x="2" y="5" width="32" height="26" rx="13" ry="13" fill="rgba(255,255,255,0.15)" 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> OCI CIS Agent</h1><div class="st">Infrastructure & Security · v${V}</div></div>
|
||||
<div class="nav"><div class="nl">Principal</div>
|
||||
${tabs.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 class="nl" style="margin-top:.5rem">Configuração</div>
|
||||
@@ -145,16 +138,16 @@ ${atabs.map(t=>`<div class="ni ${S.tab===t[0]?'on':''}" onclick="switchTab('${t[
|
||||
<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>
|
||||
<button class="btn bs bsm" style="margin-left:auto" onclick="doLogout()" title="Sair">⏻</button></div></div></div>`}
|
||||
|
||||
function rTb(){const t={'chat':'💬 AI Agent Chat','report':'📊 Compliance Report','downloads':'📁 Downloads','oci-config':'☁️ OCI Credentials','genai':'🧠 OCI Generative AI','mcp':'🔌 MCP Servers','adb':'🗄️ Autonomous DB Vector','users':'👥 Usuários','mfa':'🔐 MFA','audit':'📋 Audit Log'};
|
||||
return`<div class="tb"><div class="tb-t">${t[S.tab]||''}</div><div class="tb-a"><span class="tag">v2.0</span></div></div>`}
|
||||
function rTb(){const t={'chat':'💬 AI Agent Chat','explorer':'🔍 OCI Account Explorer','report':'📊 Compliance Report','downloads':'📁 Downloads','oci-config':'☁️ OCI Credentials','genai':'🧠 OCI Generative AI','mcp':'🔌 MCP Servers','adb':'🗄️ Autonomous DB Vector','users':'👥 Usuários','mfa':'🔐 MFA','audit':'📋 Audit Log'};
|
||||
return`<div class="tb"><div class="tb-t">${t[S.tab]||''}</div><div class="tb-a"><span class="tag">v${V}</span></div></div>`}
|
||||
|
||||
function rPg(){switch(S.tab){case'chat':return rChat();case'report':return rReport();case'downloads':return rDl();case'oci-config':return rOci();case'genai':return rGenAI();case'mcp':return rMCP();case'adb':return rADB();case'users':return rUsers();case'mfa':return rMfa();case'audit':return rAudit();default:return''}}
|
||||
function rPg(){switch(S.tab){case'chat':return rChat();case'explorer':return rExplorer();case'report':return rReport();case'downloads':return rDl();case'oci-config':return rOci();case'genai':return rGenAI();case'mcp':return rMCP();case'adb':return rADB();case'users':return rUsers();case'mfa':return rMfa();case'audit':return rAudit();default:return''}}
|
||||
|
||||
// ── Chat ──
|
||||
function rChat(){
|
||||
const ms=S.msgs.length===0?'<div class="emp"><div class="eic">🤖</div><p>Envie uma mensagem para iniciar.</p><p style="font-size:.72rem;margin-top:.3rem">Selecione um modelo GenAI acima ou use o agente local.</p></div>'
|
||||
:S.msgs.map(m=>`<div class="cm cm-${m.r}"><div class="cb">${fm(m.c)}</div></div>`).join('');
|
||||
const gcOpts=S.genaiCfg.map(g=>{const mi=S.models[g.model_id];return`<option value="${g.id}" ${S.selGenai===g.id?'selected':''}>${mi?mi.name:g.model_id} (${g.genai_region})</option>`}).join('');
|
||||
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>
|
||||
<div style="flex:1"></div><button class="btn bd bsm" onclick="clrChat()">Limpar</button></div>
|
||||
@@ -170,20 +163,40 @@ async function sChat(){const el=document.getElementById('chi');const m=el.value.
|
||||
function scCh(){setTimeout(()=>{const e=document.getElementById('chm');if(e)e.scrollTop=e.scrollHeight},50)}
|
||||
async function clrChat(){if(S.sid)try{await $api('/chat/'+S.sid,{method:'DELETE'})}catch(e){}S.msgs=[];S.sid=null;R()}
|
||||
|
||||
// ── OCI Explorer ──
|
||||
function rExplorer(){
|
||||
const cfgSel=S.ociCfg.map(c=>`<option value="${c.id}">${c.tenancy_name} (${c.region})</option>`).join('');
|
||||
if(!S.ociCfg.length)return`<div class="cd"><div class="emp"><div class="eic">☁️</div><p>Nenhuma credencial OCI configurada.</p><p style="font-size:.72rem;margin-top:.3rem">Vá em <strong>OCI Credentials</strong> para adicionar.</p></div></div>`;
|
||||
return`<div class="cd"><div class="ct">🔍 Explorar Conta OCI</div>
|
||||
<div class="g3"><div class="ig"><label>Conexão OCI</label><select id="exCfg">${cfgSel}</select></div>
|
||||
<div class="ig"><label>Recurso</label><select id="exRes"><option value="compartments">Compartments</option><option value="regions">Regions</option><option value="instances">Compute Instances</option><option value="vcns">VCNs</option><option value="databases">Autonomous DBs</option><option value="buckets">Object Storage Buckets</option></select></div>
|
||||
<div class="ig"><label> </label><button class="btn bp" onclick="doExplore()">🔍 Explorar</button></div></div></div>
|
||||
<div class="cd"><div class="ct" id="exTitle">Resultados</div><div id="exOut">${S.expData?renderExpData(S.expData):'<div class="emp"><p>Selecione uma conexão e recurso acima.</p></div>'}</div></div>`}
|
||||
async function doExplore(){const cid=document.getElementById('exCfg').value;const res=document.getElementById('exRes').value;
|
||||
document.getElementById('exOut').innerHTML='<div class="ld">Consultando OCI...</div>';
|
||||
try{const d=await $api('/oci/explore/'+cid+'/'+res);S.expData=d;
|
||||
document.getElementById('exTitle').textContent='Resultados: '+res;
|
||||
document.getElementById('exOut').innerHTML=renderExpData(d)}catch(e){document.getElementById('exOut').innerHTML='<div class="al al-e">'+e.message+'</div>'}}
|
||||
function renderExpData(d){if(d.error)return`<div class="al al-e">${d.error}</div>`;if(!Array.isArray(d)||!d.length)return'<div class="emp"><p>Nenhum resultado.</p></div>';
|
||||
return`<div style="font-size:.72rem;color:var(--tm);margin-bottom:.4rem">${d.length} item(ns)</div><div class="exp-r">${d.map(i=>{
|
||||
const keys=Object.keys(i).filter(k=>k!=='id');
|
||||
const name=i.display_name||i.name||i.db_name||'—';
|
||||
return`<div class="exp-c"><strong>${name}</strong>${i.lifecycle_state?` <span class="badge ${i.lifecycle_state==='RUNNING'||i.lifecycle_state==='ACTIVE'||i.lifecycle_state==='AVAILABLE'?'b-pass':'b-pend'}">${i.lifecycle_state}</span>`:''}<br>
|
||||
${keys.filter(k=>!['display_name','name','db_name','lifecycle_state'].includes(k)).map(k=>`<span style="color:var(--tm);font-size:.65rem">${k}:</span> <span class="em">${typeof i[k]==='object'?JSON.stringify(i[k]):i[k]}</span>`).join('<br>')}</div>`}).join('')}</div>`}
|
||||
|
||||
// ── Report ──
|
||||
function rReport(){const comp=S.reports.filter(r=>r.status==='completed');
|
||||
if(!comp.length)return`<div class="cd"><div class="ct">📊 Report</div><div class="emp"><div class="eic">📄</div><p>Nenhum relatório.</p></div></div>${rRunRpt()}`;
|
||||
const lt=comp[0];
|
||||
return`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.4rem"><span style="font-size:.76rem;color:var(--tm)">Report HTML — ${lt.tenancy_name}</span>
|
||||
<div style="display:flex;gap:.35rem"><select id="rsel" onchange="ldRpt(this.value)" style="max-width:260px">
|
||||
${comp.map(r=>`<option value="${r.id}">${r.tenancy_name} — ${r.created_at}</option>`).join('')}</select></div></div>
|
||||
<select id="rsel" onchange="ldRpt(this.value)" style="max-width:260px">${comp.map(r=>`<option value="${r.id}">${r.tenancy_name} — ${r.created_at}</option>`).join('')}</select></div>
|
||||
<iframe class="rif" id="rfr" src="${API}/reports/${lt.id}/html"></iframe>${rRunRpt()}`}
|
||||
function rRunRpt(){if(S.user?.role==='viewer')return'';
|
||||
return`<div class="cd" style="margin-top:.65rem"><div class="ct">🚀 Executar Relatório</div>
|
||||
<div class="g3"><div class="ig"><label>Config OCI</label><select id="rc"><option value="">Selecione...</option>
|
||||
${S.ociCfg.map(c=>`<option value="${c.id}">${c.tenancy_name} (${c.region})</option>`).join('')}</select></div>
|
||||
<div class="ig"><label>MCP Server (opcional)</label><select id="rmcp"><option value="">Padrão (stub)</option>
|
||||
${S.mcpSvr.filter(m=>m.is_active).map(m=>`<option value="${m.id}">${m.name}</option>`).join('')}</select></div>
|
||||
${S.mcpSvr.filter(m=>m.is_active).map(m=>`<option value="${m.id}">${m.name}${m.linked_adb_id?' 🗄️':''}</option>`).join('')}</select></div>
|
||||
<div class="ig"><label>Regiões (opcional)</label><input type="text" id="rr" placeholder="sa-saopaulo-1, us-ashburn-1"></div></div>
|
||||
<button class="btn bp" onclick="runRpt()">▶ Executar CIS Compliance</button></div>`}
|
||||
function ldRpt(id){const f=document.getElementById('rfr');if(f)f.src=API+'/reports/'+id+'/html'}
|
||||
@@ -196,17 +209,15 @@ async function runRpt(){const c=document.getElementById('rc').value;if(!c)return
|
||||
// ── Downloads ──
|
||||
function rDl(){return`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.4rem">
|
||||
<span style="font-size:.76rem;color:var(--tm)">${S.reports.length} relatório(s)</span>
|
||||
<div style="display:flex;gap:.35rem"><input type="text" id="dlf" placeholder="Filtrar..." style="max-width:240px" oninput="fDl()">
|
||||
<button class="btn bp bsm" onclick="refreshDl()">🔄</button></div></div>
|
||||
<button class="btn bp bsm" onclick="refreshDl()">🔄</button></div>
|
||||
<div class="cd"><table><thead><tr><th>Tenancy</th><th>Status</th><th>Criado</th><th>Concluído</th><th>Ações</th></tr></thead>
|
||||
<tbody id="dlt">${S.reports.map(r=>`<tr data-n="${r.tenancy_name.toLowerCase()}"><td><strong>${r.tenancy_name}</strong></td>
|
||||
<tbody>${S.reports.map(r=>`<tr><td><strong>${r.tenancy_name}</strong></td>
|
||||
<td><span class="badge ${r.status==='completed'?'b-pass':r.status==='failed'?'b-fail':'b-pend'}">${r.status}</span></td>
|
||||
<td>${r.created_at||'—'}</td><td>${r.completed_at||'—'}</td>
|
||||
<td>${r.status==='completed'?`<button class="btn bs bsm" onclick="dlRpt('${r.id}','json')">JSON</button> <button class="btn bs bsm" onclick="dlRpt('${r.id}','html')">HTML</button>`:'—'}</td></tr>`).join('')}</tbody></table>
|
||||
${!S.reports.length?'<div class="emp"><div class="eic">📂</div><p>Nenhum relatório.</p></div>':''}</div>`}
|
||||
function dlRpt(id,f){window.open(API+'/reports/'+id+'/download?fmt='+f,'_blank')}
|
||||
async function refreshDl(){S.reports=await $api('/reports');R()}
|
||||
function fDl(){const q=document.getElementById('dlf').value.toLowerCase();document.querySelectorAll('#dlt tr').forEach(tr=>{tr.style.display=tr.dataset.n?.includes(q)?'':'none'})}
|
||||
|
||||
// ── OCI Config ──
|
||||
function rOci(){return`<div class="cd"><div class="ct">☁️ Credenciais OCI</div>
|
||||
@@ -234,69 +245,92 @@ async function sOci(){const fd=new FormData();fd.append('tenancy_name',document.
|
||||
try{await $api('/oci/config',{method:'POST',body:fd,headers:{}});S.ociCfg=await $api('/oci/configs');sm('om','✅ Salvo!','s');R()}catch(e){sm('om',e.message,'e')}}
|
||||
async function tOci(id){try{const d=await $api('/oci/test/'+id,{method:'POST'});alert(d.status==='success'?'✅ Conexão OK!':'❌ '+d.message)}catch(e){alert(e.message)}}
|
||||
async function dOci(id){if(!confirm('Excluir?'))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])=>`<option value="${k}">${v.name} (${v.provider})</option>`).join('');
|
||||
const rOpts=S.regions.map(r=>`<option value="${r}">${r}</option>`).join('');
|
||||
const rOpts=S.regions.map(r=>`<option value="${r}" ${r==='us-ashburn-1'?'selected':''}>${r}</option>`).join('');
|
||||
return`<div class="cd"><div class="ct">🧠 Configurações GenAI</div>
|
||||
<table><thead><tr><th>Modelo</th><th>Provider</th><th>Região</th><th>Serving</th><th>Default</th><th>Ações</th></tr></thead><tbody>
|
||||
${S.genaiCfg.map(g=>{const mi=S.models[g.model_id]||{};return`<tr><td><strong>${mi.name||g.model_id}</strong><br><span style="font-size:.63rem;color:var(--tm)">${g.model_id}</span></td>
|
||||
<td><span class="tag">${mi.provider||'?'}</span></td><td>${g.genai_region}</td><td>${g.serving_type}</td>
|
||||
<td>${g.is_default?'⭐':'—'}</td>
|
||||
<table><thead><tr><th>Nome</th><th>Modelo</th><th>Endpoint</th><th>Temp</th><th>Max Tokens</th><th>Ações</th></tr></thead><tbody>
|
||||
${S.genaiCfg.map(g=>{const mi=S.models[g.model_id]||{};return`<tr><td><strong>${g.name||'—'}</strong>${g.is_default?' ⭐':''}<br><span class="tag">${mi.provider||'?'}</span></td>
|
||||
<td>${mi.name||g.model_id}<br><span style="font-size:.6rem;color:var(--tm)">${g.model_ocid||g.model_id}</span></td>
|
||||
<td style="font-size:.63rem;font-family:var(--fm)">${g.endpoint||'—'}</td>
|
||||
<td>${g.temperature}</td><td>${g.max_tokens}</td>
|
||||
<td><button class="btn bs bsm" onclick="tGenai('${g.id}')">Testar</button> <button class="btn bd bsm" onclick="dGenai('${g.id}')">Excluir</button></td></tr>`}).join('')}</tbody></table>
|
||||
${!S.genaiCfg.length?'<div class="emp" style="padding:.65rem"><p>Nenhum modelo configurado. Adicione abaixo.</p></div>':''}</div>
|
||||
<div class="cd"><div class="ct">➕ Novo Modelo GenAI</div><div id="gm"></div>
|
||||
<div class="g3"><div class="ig"><label>Config OCI (credenciais)</label><select id="goci"><option value="">Selecione...</option>
|
||||
${!S.genaiCfg.length?'<div class="emp" style="padding:.65rem"><p>Nenhum modelo configurado.</p></div>':''}</div>
|
||||
<div class="cd"><div class="ct">➕ Novo Modelo GenAI</div>
|
||||
<p style="font-size:.72rem;color:var(--tm);margin-bottom:.65rem">Configura conexão via <strong>OCI SDK</strong> com <code>GenerativeAiInferenceClient</code>. Endpoint é gerado automaticamente pela região.</p>
|
||||
<div id="gm"></div>
|
||||
<div class="g3"><div class="ig"><label>Nome da Config</label><input type="text" id="gname" placeholder="Llama Produção"></div>
|
||||
<div class="ig"><label>Credencial OCI</label><select id="goci"><option value="">Selecione...</option>
|
||||
${S.ociCfg.map(c=>`<option value="${c.id}">${c.tenancy_name}</option>`).join('')}</select></div>
|
||||
<div class="ig"><label>Modelo</label><select id="gmod">${mOpts}</select></div>
|
||||
<div class="ig"><label>Região GenAI</label><select id="greg">${rOpts}</select></div></div>
|
||||
<div class="g3"><div class="ig"><label>Compartment OCID</label><input type="text" id="gcmp" placeholder="ocid1.compartment.oc1.."></div>
|
||||
<div class="ig"><label>Serving Type</label><select id="gsrv"><option value="ON_DEMAND">On-Demand</option><option value="DEDICATED">Dedicated</option></select></div>
|
||||
<div class="ig"><label>Endpoint OCID (dedicado)</label><input type="text" id="gep" placeholder="(opcional)"></div></div>
|
||||
<div class="g3"><div class="ig"><label>Temperature</label><input type="number" id="gtemp" value="0.7" step="0.1" min="0" max="2"></div>
|
||||
<div class="ig"><label>Max Tokens</label><input type="number" id="gmt" value="2048" min="1" max="128000"></div>
|
||||
<div class="ig"><label>Top P</label><input type="number" id="gtp" value="0.9" step="0.05" min="0" max="1"></div></div>
|
||||
<div class="ig"><label>Modelo (catálogo)</label><select id="gmod">${mOpts}</select></div></div>
|
||||
<div class="g3"><div class="ig"><label>Região GenAI</label><select id="greg">${rOpts}</select></div>
|
||||
<div class="ig"><label>Compartment OCID</label><input type="text" id="gcmp" placeholder="ocid1.compartment.oc1.."></div>
|
||||
<div class="ig"><label>Model OCID (opcional)</label><div class="ht">Se diferente do catálogo (ex: modelo dedicado)</div><input type="text" id="gocid" placeholder="ocid1.generativeaimodel.oc1..."></div></div>
|
||||
|
||||
<div style="font-size:.72rem;font-weight:600;color:var(--t2);margin:.5rem 0 .3rem">Parâmetros do Chat</div>
|
||||
<div class="g3"><div class="ig"><label>Temperature</label><input type="number" id="gtemp" value="1" step="0.1" min="0" max="2"></div>
|
||||
<div class="ig"><label>Max Tokens</label><input type="number" id="gmt" value="6000" min="1" max="128000"></div>
|
||||
<div class="ig"><label>Top P</label><input type="number" id="gtp" value="0.95" step="0.05" min="0" max="1"></div></div>
|
||||
<div class="g3"><div class="ig"><label>Top K</label><input type="number" id="gtk" value="1" min="-1" max="500"></div>
|
||||
<div class="ig"><label>Frequency Penalty</label><input type="number" id="gfp" value="0" step="0.1" min="0" max="2"></div>
|
||||
<div class="ig"><label>Presence Penalty</label><input type="number" id="gpp" value="0" step="0.1" min="0" max="2"></div></div>
|
||||
<button class="btn bp" onclick="sGenai()">Salvar Modelo</button></div>`}
|
||||
async function sGenai(){try{await $api('/genai/config',{method:'POST',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,
|
||||
compartment_id:document.getElementById('gcmp').value,genai_region:document.getElementById('greg').value,
|
||||
serving_type:document.getElementById('gsrv').value,endpoint_id:document.getElementById('gep').value||null,
|
||||
endpoint:null,
|
||||
serving_type:'ON_DEMAND',dedicated_endpoint_id:null,
|
||||
temperature:parseFloat(document.getElementById('gtemp').value),max_tokens:parseInt(document.getElementById('gmt').value),
|
||||
top_p:parseFloat(document.getElementById('gtp').value),is_default:S.genaiCfg.length===0}});
|
||||
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!','s');R()}catch(e){sm('gm',e.message,'e')}}
|
||||
async function tGenai(id){try{const d=await $api('/genai/test/'+id,{method:'POST'});alert(d.status==='success'?'✅ '+d.message+'\n'+d.response:'❌ '+d.message)}catch(e){alert(e.message)}}
|
||||
async function dGenai(id){if(!confirm('Excluir?'))return;await $api('/genai/configs/'+id,{method:'DELETE'});S.genaiCfg=await $api('/genai/configs');R()}
|
||||
|
||||
// ── MCP Servers ──
|
||||
function rMCP(){return`<div class="cd"><div class="ct">🔌 MCP Servers Registrados</div>
|
||||
<table><thead><tr><th>Nome</th><th>Tipo</th><th>Comando/URL</th><th>Status</th><th>Ações</th></tr></thead><tbody>
|
||||
${S.mcpSvr.map(m=>`<tr><td><strong>${m.name}</strong><br><span style="font-size:.63rem;color:var(--tm)">${m.description||''}</span></td>
|
||||
function rMCP(){const adbOpts=S.adbCfg.map(a=>`<option value="${a.id}">${a.config_name} (${a.dsn})</option>`).join('');
|
||||
return`<div class="cd"><div class="ct">🔌 MCP Servers</div>
|
||||
<table><thead><tr><th>Nome</th><th>Tipo</th><th>Cmd/URL</th><th>ADB Link</th><th>Status</th><th>Ações</th></tr></thead><tbody>
|
||||
${S.mcpSvr.map(m=>{const adb=m.linked_adb_id?S.adbCfg.find(a=>a.id===m.linked_adb_id):null;
|
||||
return`<tr><td><strong>${m.name}</strong><br><span style="font-size:.63rem;color:var(--tm)">${m.description||''}</span></td>
|
||||
<td><span class="tag">${m.server_type}</span></td>
|
||||
<td style="font-family:var(--fm);font-size:.65rem">${m.command||m.url||m.module_path||'—'}</td>
|
||||
<td>${adb?`<span class="badge b-pass">${adb.config_name}</span>`:'<span style="color:var(--tm);font-size:.65rem">—</span>'}</td>
|
||||
<td><span class="badge ${m.is_active?'b-on':'b-off'}">${m.is_active?'Ativo':'Inativo'}</span></td>
|
||||
<td><button class="btn bs bsm" onclick="tgMcp('${m.id}')">${m.is_active?'Desativar':'Ativar'}</button>
|
||||
<button class="btn bd bsm" onclick="dMcp('${m.id}')">Excluir</button></td></tr>`).join('')}</tbody></table>
|
||||
${!S.mcpSvr.length?'<div class="emp" style="padding:.65rem"><p>Nenhum MCP server. Registre abaixo para usar em relatórios.</p></div>':''}</div>
|
||||
<td><button class="btn bs bsm" onclick="tgMcp('${m.id}')">${m.is_active?'Off':'On'}</button>
|
||||
<button class="btn bd bsm" onclick="dMcp('${m.id}')">Del</button></td></tr>`}).join('')}</tbody></table>
|
||||
${!S.mcpSvr.length?'<div class="emp" style="padding:.65rem"><p>Nenhum MCP server.</p></div>':''}</div>
|
||||
<div class="cd"><div class="ct">➕ Registrar MCP Server</div><div id="mcm"></div>
|
||||
<div class="g2"><div class="ig"><label>Nome</label><input type="text" id="mn" placeholder="CIS Benchmark Server"></div>
|
||||
<div class="ig"><label>Descrição</label><input type="text" id="md" placeholder="Executa checks CIS 3.0"></div></div>
|
||||
<div class="g3"><div class="ig"><label>Tipo</label><select id="mt"><option value="stdio">stdio</option><option value="sse">SSE (HTTP)</option><option value="module">Python Module</option></select></div>
|
||||
<div class="ig"><label>Comando (stdio)</label><input type="text" id="mcmd" placeholder="python3 server.py"></div>
|
||||
<div class="ig"><label>URL (SSE)</label><input type="text" id="murl" placeholder="http://localhost:8001/sse"></div></div>
|
||||
<div class="ig"><label>Args (JSON array)</label><input type="text" id="margs" placeholder='["--config", "/path"]'></div>
|
||||
<div class="g2"><div class="ig"><label>🗄️ Vincular ADB Vector (tool)</label><div class="ht">MCP server usará este banco como ferramenta</div><select id="madb"><option value="">Nenhum</option>${adbOpts}</select></div>
|
||||
<div class="ig"><label>Args (JSON array)</label><input type="text" id="margs" placeholder='["--config", "/path"]'></div></div>
|
||||
<button class="btn bp" onclick="sMcp()">Registrar Server</button></div>
|
||||
<div class="cd"><div class="ct">📤 Upload Script MCP</div>
|
||||
<div class="g2"><div class="ig"><label>MCP Server</label><select id="mup">${S.mcpSvr.map(m=>`<option value="${m.id}">${m.name}</option>`).join('')}</select></div>
|
||||
<div class="ig"><label>Arquivo .py</label><input type="file" id="muf" accept=".py"></div></div>
|
||||
<div class="cd"><div class="ct">📤 Upload Script / 🔗 Link ADB</div>
|
||||
<div class="g3"><div class="ig"><label>MCP Server</label><select id="mup">${S.mcpSvr.map(m=>`<option value="${m.id}">${m.name}</option>`).join('')}</select></div>
|
||||
<div class="ig"><label>Arquivo .py</label><input type="file" id="muf" accept=".py"></div>
|
||||
<div class="ig"><label>Vincular ADB</label><select id="mlnk"><option value="">Nenhum</option>${adbOpts}</select>
|
||||
<button class="btn bs bsm" style="margin-top:.3rem" onclick="lnkMcp()">Vincular</button></div></div>
|
||||
<button class="btn bs" onclick="uMcp()">Upload Script</button></div>`}
|
||||
async function sMcp(){try{let args=null;const a=document.getElementById('margs').value;if(a)try{args=JSON.parse(a)}catch(e){}
|
||||
await $api('/mcp/servers',{method:'POST',body:{name:document.getElementById('mn').value,description:document.getElementById('md').value,
|
||||
server_type:document.getElementById('mt').value,command:document.getElementById('mcmd').value||null,url:document.getElementById('murl').value||null,args}});
|
||||
server_type:document.getElementById('mt').value,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')}}
|
||||
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?'))return;await $api('/mcp/servers/'+id,{method:'DELETE'});S.mcpSvr=await $api('/mcp/servers');R()}
|
||||
async function uMcp(){const fd=new FormData();const f=document.getElementById('muf').files[0];if(!f)return alert('Selecione um arquivo');
|
||||
fd.append('file',f);const id=document.getElementById('mup').value;if(!id)return alert('Selecione um server');
|
||||
try{await $api('/mcp/servers/'+id+'/upload',{method:'POST',body:fd,headers:{}});alert('Upload OK!')}catch(e){alert(e.message)}}
|
||||
async function lnkMcp(){const mid=document.getElementById('mup').value;const aid=document.getElementById('mlnk').value;if(!mid)return alert('Selecione MCP');
|
||||
try{await $api('/mcp/servers/'+mid+'/link-adb?adb_id='+(aid||''),{method:'PUT'});S.mcpSvr=await $api('/mcp/servers');alert('Vinculado!');R()}catch(e){alert(e.message)}}
|
||||
|
||||
// ── ADB Vector ──
|
||||
function rADB(){return`<div class="cd"><div class="ct">🗄️ Conexões Autonomous Database (Vector)</div>
|
||||
@@ -307,13 +341,12 @@ ${S.adbCfg.map(c=>`<tr><td><strong>${c.config_name}</strong></td><td style="font
|
||||
<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>
|
||||
${!S.adbCfg.length?'<div class="emp" style="padding:.65rem"><p>Nenhuma conexão ADB.</p></div>':''}</div>
|
||||
<div class="cd"><div class="ct">➕ Nova Conexão Autonomous DB</div>
|
||||
<p style="font-size:.76rem;color:var(--tm);margin-bottom:.65rem">Conexão via <strong>python-oracledb</strong> (Thin mode) com Wallet (mTLS) para Oracle Autonomous Database.</p>
|
||||
<div id="am"></div>
|
||||
<p style="font-size:.72rem;color:var(--tm);margin-bottom:.65rem">Conexão via <strong>python-oracledb</strong> Thin mode com Wallet (mTLS) para Oracle Autonomous Database.</p><div id="am"></div>
|
||||
<div class="g2"><div class="ig"><label>Nome da Conexão</label><input type="text" id="an" placeholder="Produção ADB"></div>
|
||||
<div class="ig"><label>DSN (TNS Name)</label><div class="ht">Ex: myatp_high, myatp_medium (do tnsnames.ora)</div><input type="text" id="adsn" placeholder="myatp_high"></div>
|
||||
<div class="ig"><label>DSN (TNS Name)</label><div class="ht">Ex: myatp_high, myatp_medium</div><input type="text" id="adsn" placeholder="myatp_high"></div>
|
||||
<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" placeholder="••••••••"></div>
|
||||
<div class="ig"><label>Wallet Password</label><div class="ht">Senha do ewallet.pem (definida ao baixar wallet)</div><input type="password" id="awpw" placeholder="(opcional)"></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>
|
||||
<button class="btn bp" onclick="sAdb()">Salvar Conexão</button></div>
|
||||
<div class="cd"><div class="ct">📤 Upload Wallet (.zip)</div>
|
||||
|
||||
21
logo.svg
Normal file
21
logo.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="128" height="128">
|
||||
<!-- Oracle-style rounded rectangle -->
|
||||
<rect x="2" y="5" width="32" height="26" rx="13" ry="13" fill="rgba(199,70,52,0.08)" stroke="#C74634" stroke-width="2"/>
|
||||
<!-- Robot head -->
|
||||
<rect x="11" y="11" width="14" height="14" rx="4" fill="#fff" stroke="#C74634" stroke-width="0.5"/>
|
||||
<!-- Eyes -->
|
||||
<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"/>
|
||||
<!-- Mouth -->
|
||||
<rect x="14" y="21" width="8" height="1.5" rx="0.75" fill="#C74634" opacity="0.6"/>
|
||||
<!-- Antenna -->
|
||||
<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.3" stroke="#C74634" stroke-width="0.8"/>
|
||||
<!-- Ears/signal -->
|
||||
<line x1="11" y1="18" x2="6" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.5"/>
|
||||
<line x1="25" y1="18" x2="30" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.5"/>
|
||||
<circle cx="5.5" cy="18" r="1" fill="#C74634" opacity="0.4"/>
|
||||
<circle cx="30.5" cy="18" r="1" fill="#C74634" opacity="0.4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
Reference in New Issue
Block a user