- Remove 5 Cohere chat models (not available in us-ashburn-1) - Remove 9 legacy Cohere embedding models (v3.0), keep embed-v4.0 - Update default embedding model to cohere.embed-v4.0 - Skip presence_penalty, frequency_penalty, top_k for xAI models - Remove Cohere from chat provider order in frontend
1733 lines
100 KiB
Python
1733 lines
100 KiB
Python
"""
|
|
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
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
from typing import Optional, List, Dict, Any
|
|
from contextlib import contextmanager
|
|
|
|
from fastapi import (
|
|
FastAPI, HTTPException, Depends, Request, UploadFile, File, Form,
|
|
Query, BackgroundTasks
|
|
)
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import JSONResponse, FileResponse
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
from pydantic import BaseModel
|
|
import jwt as pyjwt
|
|
|
|
# ── Config ────────────────────────────────────────────────────────────────────
|
|
APP_SECRET = os.environ.get("APP_SECRET", secrets.token_hex(32))
|
|
JWT_ALG = "HS256"
|
|
JWT_EXP_H = int(os.environ.get("JWT_EXPIRY_HOURS", "12"))
|
|
DATA = Path(os.environ.get("DATA_DIR", "/data"))
|
|
DB_PATH = DATA / "agent.db"
|
|
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)
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
log = logging.getLogger("agent")
|
|
|
|
app = FastAPI(title="OCI CIS AI Agent", version=VERSION)
|
|
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True,
|
|
allow_methods=["*"], allow_headers=["*"])
|
|
security = HTTPBearer()
|
|
|
|
# ── OCI GenAI Models Catalog ──────────────────────────────────────────────────
|
|
# OCIDs are region-specific; "ocids" maps genai_region → OCID.
|
|
# _call_genai resolves the OCID for the configured region at runtime.
|
|
GENAI_MODELS = {
|
|
# ── Meta ──
|
|
"meta.llama-4-maverick-17b-128e-instruct-fp8": {"provider":"meta","name":"Meta Llama 4 Maverick","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyah6tjdejjashngznsylutuhhvufukzb2g2ls54g2flsfq"}},
|
|
"meta.llama-4-scout-17b-16e-instruct": {"provider":"meta","name":"Meta Llama 4 Scout","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaw23hfc7mtvv5wef3gwvvaguyzqmhb5lx4r5s3y2xzc4a"}},
|
|
"meta.llama-guard-4-12b": {"provider":"meta","name":"Meta Llama Guard 4 (12B)","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyab5ggfxf4zs33lb5skxemyudnfxangjl4557toy3yapea"}},
|
|
# ── Google ──
|
|
"google.gemini-2.5-pro": {"provider":"google","name":"Google Gemini 2.5 Pro","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyargceyuaysrjzo2metq2rinavayxqmpu7tkm6mmfojcvq"}},
|
|
"google.gemini-2.5-flash": {"provider":"google","name":"Google Gemini 2.5 Flash","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaeo4ehrn25guuats5s45hnvswlhxo6riop275l2bkr2vq"}},
|
|
"google.gemini-2.5-flash-lite": {"provider":"google","name":"Google Gemini 2.5 Flash-Lite","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaqk3p6ljepyguyo4ff5dwjw3ecij3x4l2j32e3gz66wtq"}},
|
|
# ── OpenAI ──
|
|
"openai.gpt-5.3-codex": {"provider":"openai","name":"OpenAI GPT-5.3 Codex","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyayatqadc4zh74l6mh7sb3sb5olk7jnhq62bfnxgjwfb5a"}},
|
|
"openai.gpt-5.2-codex": {"provider":"openai","name":"OpenAI GPT-5.2 Codex","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyarqt3ngs42jvevvgunlvkb2ksxlnotqymbm4duy3phy4q"}},
|
|
"openai.gpt-5.2-pro": {"provider":"openai","name":"OpenAI GPT-5.2 Pro","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaw6eprz3l3db3w3y5udndb6fjairuepn5chunmmgwueba"}},
|
|
"openai.gpt-5.2-pro-2025-12-11": {"provider":"openai","name":"OpenAI GPT-5.2 Pro (2025-12-11)","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya5s4tallv2x5rh7l2zc4kaxsbkihcnhd2w6lqf3a7ty2a"}},
|
|
"openai.gpt-5.2": {"provider":"openai","name":"OpenAI GPT-5.2","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya4fw3p5fddnexbfcurnz7spgkqb4mq4a6y5ubyv7777sa"}},
|
|
"openai.gpt-5.2-2025-12-11": {"provider":"openai","name":"OpenAI GPT-5.2 (2025-12-11)","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyawclpjz3ar2psi2vjqk3zwzesnq2u6uli37djpdn2zaha"}},
|
|
"openai.gpt-5.2-chat-latest": {"provider":"openai","name":"OpenAI GPT-5.2 Chat Latest","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyafvkojauwkg3b6nps2rogu5lmigedrkel65j6qtk7l2uq"}},
|
|
"openai.gpt-5.1-codex-max": {"provider":"openai","name":"OpenAI GPT-5.1 Codex Max","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaqy3hrasco26ocvumkr5canmnzkvkhgoyw6ntyvyeamrq"}},
|
|
"openai.gpt-5.1-codex": {"provider":"openai","name":"OpenAI GPT-5.1 Codex","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyar76rnxb66b4bkhlpn62jdffjedmeijbbh3h3v4e6xrxa"}},
|
|
"openai.gpt-5.1-codex-mini": {"provider":"openai","name":"OpenAI GPT-5.1 Codex Mini","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyashim6rmq4irtdxw5osv4flw6ueggq5sppyzmv3qw7tha"}},
|
|
"openai.gpt-5.1": {"provider":"openai","name":"OpenAI GPT-5.1","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya3darth2ozqcfssb2bats5jitpgigllccajasdyqljnkq"}},
|
|
"openai.gpt-5.1-2025-11-13": {"provider":"openai","name":"OpenAI GPT-5.1 (2025-11-13)","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya5xtecezkpnkloj5wsmgtpbz7y4n7dsryanaj3l3npk5q"}},
|
|
"openai.gpt-5.1-chat-latest": {"provider":"openai","name":"OpenAI GPT-5.1 Chat Latest","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyavtwm6mlbmpmkkvejoprbt5cpflx2bq66lgy6yr2hxsba"}},
|
|
"openai.gpt-5-codex": {"provider":"openai","name":"OpenAI GPT-5 Codex","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyasddbvtsh44glecj4bbyghxdbjabmvb76pvrkjefeqgpa"}},
|
|
"openai.gpt-5": {"provider":"openai","name":"OpenAI GPT-5","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyathaswupt2vqykuxzinzbu776zpydos343fokoddyywma"}},
|
|
"openai.gpt-5-2025-08-07": {"provider":"openai","name":"OpenAI GPT-5 (2025-08-07)","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyahr5xoj3itcnd2ppl45w3isil2brd4obx2x2qslckfxsq"}},
|
|
"openai.gpt-5-mini": {"provider":"openai","name":"OpenAI GPT-5 Mini","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya3eain4n6v3edm4ryjvze5hnjouujd4vralxntfalwjaq"}},
|
|
"openai.gpt-5-mini-2025-08-07": {"provider":"openai","name":"OpenAI GPT-5 Mini (2025-08-07)","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyakzv5rbuziucahmfkbrvnrzwepgfzuyio4fq4goueda2a"}},
|
|
"openai.gpt-5-nano": {"provider":"openai","name":"OpenAI GPT-5 Nano","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyactz4ikvxbjchjlkdw4jtyftsopfwk6prltzarznudqxq"}},
|
|
"openai.gpt-5-nano-2025-08-07": {"provider":"openai","name":"OpenAI GPT-5 Nano (2025-08-07)","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyafyjacj4tcfrcvxpte4j5v5dhzrxs4ir7cz3hu3ztatlq"}},
|
|
"openai.gpt-4.1": {"provider":"openai","name":"OpenAI GPT-4.1","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaa6g75r2qqmtzjaooqtlxv4lxkpcqp2jdd6plpq7yq7ea"}},
|
|
"openai.gpt-4.1-2025-04-14": {"provider":"openai","name":"OpenAI GPT-4.1 (2025-04-14)","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaba4ewqm32w7nsbal4od2unrtvhr6qybzfra5y2q7yhra"}},
|
|
"openai.gpt-4.1-mini": {"provider":"openai","name":"OpenAI GPT-4.1 Mini","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya6pk3sxishpiexm2rb5sf4ytb5tsbz4to2g3g23smidaa"}},
|
|
"openai.gpt-4.1-mini-2025-04-14": {"provider":"openai","name":"OpenAI GPT-4.1 Mini (2025-04-14)","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyageakxq5b2haozdcjwalu6sdxsswsw3gsx4hxqaerpqvq"}},
|
|
"openai.gpt-4.1-nano": {"provider":"openai","name":"OpenAI GPT-4.1 Nano","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyacxqiaijwxalbynhst6oyg4wttzagz3dai4y2rnwh6wrq"}},
|
|
"openai.gpt-4.1-nano-2025-04-14": {"provider":"openai","name":"OpenAI GPT-4.1 Nano (2025-04-14)","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaa3ubnmts4tfcwslpvuiuj74qdkyyfpb7vxo334nzzfoa"}},
|
|
"openai.gpt-4o": {"provider":"openai","name":"OpenAI GPT-4o","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyah7slrtboxdbfdy5cdspsfts62yumoclpdgwydopse7za"}},
|
|
"openai.gpt-4o-2024-08-06": {"provider":"openai","name":"OpenAI GPT-4o (2024-08-06)","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyahw7ffo4apa3z5nrlfn3hjglirdasqn7ydwbe6o66tela"}},
|
|
"openai.gpt-4o-2024-11-20": {"provider":"openai","name":"OpenAI GPT-4o (2024-11-20)","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyagxncxsf5vkooaj2pcrgcgjrxhexifnp2bghtnjstssga"}},
|
|
"openai.gpt-4o-mini": {"provider":"openai","name":"OpenAI GPT-4o Mini","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya5jba2auifbnsmrfjqmigut4aq6gei4kxyfqmwsyjo54a"}},
|
|
"openai.gpt-4o-mini-search-preview": {"provider":"openai","name":"OpenAI GPT-4o Mini Search Preview","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyauin4xlovatfjhxp67e3pxfmjp26sxide2yobsnxn5xyq"}},
|
|
"openai.gpt-4o-mini-search-preview-2025-03-11": {"provider":"openai","name":"OpenAI GPT-4o Mini Search (2025-03-11)","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyabx7ppfcp5uyt2gmfc6mrfy74lnzenayxiyfpbvo5b7ka"}},
|
|
"openai.gpt-4o-search-preview": {"provider":"openai","name":"OpenAI GPT-4o Search Preview","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyar2iwiwsqlrrdw5wmppchuz56nmkrp2soyohx3boc37mq"}},
|
|
"openai.gpt-4o-search-preview-2025-03-11": {"provider":"openai","name":"OpenAI GPT-4o Search (2025-03-11)","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya7huzy6zziboyhuwfqmbhsjrz57xat5hclnjnqrnan7ha"}},
|
|
"openai.gpt-image-1.5": {"provider":"openai","name":"OpenAI GPT Image 1.5","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyauvxywwfyxnqouoltmp2oe5srzspxnnflmxpkznbwgaea"}},
|
|
"openai.gpt-image-1": {"provider":"openai","name":"OpenAI GPT Image 1","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaykmvlptihcu4iumplkemkp3u3mcw7atypidcpfpminoq"}},
|
|
"openai.gpt-audio": {"provider":"openai","name":"OpenAI GPT Audio","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyabnxjhfcolefqhio7twnnqvkxbkphpy6f5h2mo4xqkdvq"}},
|
|
"openai.o4-mini": {"provider":"openai","name":"OpenAI o4-mini","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya5ivfqp4fxlajoeg2cahlqtuuswagiv6a7dggpigy23bq"}},
|
|
"openai.o4-mini-2025-04-16": {"provider":"openai","name":"OpenAI o4-mini (2025-04-16)","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyatheqm7c6zhrtgwccbwyopoi5dzrzrclw7kx5igfrqniq"}},
|
|
"openai.o3": {"provider":"openai","name":"OpenAI o3","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyalgnrukpjk6wm5zsf4jzkoneahgswhrk7kukkoagwnzma"}},
|
|
"openai.o3-2025-04-16": {"provider":"openai","name":"OpenAI o3 (2025-04-16)","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyah6nkmpmrgb67u4zuxs77uc3kg3vsgmfdig5zznbtcqoq"}},
|
|
"openai.o3-mini": {"provider":"openai","name":"OpenAI o3-mini","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyayrbuyw3robmloay76lh7j2l3mk65aoy64mhcgmwzn5yq"}},
|
|
"openai.o3-mini-2025-01-31": {"provider":"openai","name":"OpenAI o3-mini (2025-01-31)","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaeo3yslwf6uhxvfhbop7ukixto35432w7vir53mvw2vza"}},
|
|
"openai.o1": {"provider":"openai","name":"OpenAI o1","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya674efirmykrx77pde5ftnuihvpn45vyhn2ecj6dnl5ca"}},
|
|
"openai.o1-2024-12-17": {"provider":"openai","name":"OpenAI o1 (2024-12-17)","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyarak5fe624z3kudi7p4svnlif7bgg5gpoqcxcyeoajjcq"}},
|
|
"openai.gpt-oss-120b": {"provider":"openai","name":"OpenAI GPT-oss (120B)","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyachl5cngrvo4tna3bj3yqqtfvgtjarbgidmy76z7ojy6a"}},
|
|
"openai.gpt-oss-20b": {"provider":"openai","name":"OpenAI GPT-oss (20B)","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaillcx47jg3axfefkaruzmcvmse66t4nmoxuwpaqbf4kq"}},
|
|
# ── xAI ──
|
|
"xai.grok-4": {"provider":"xai","name":"xAI Grok 4","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaldmhg25is4nouena4oa2pj4nvwgfeempo4syiaazukia"}},
|
|
"xai.grok-4-1-fast-reasoning": {"provider":"xai","name":"xAI Grok 4.1 Fast Reasoning","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaocp3cooe7jkje7irb7dsrspgobjtkxakutz76gjo3k3a"}},
|
|
"xai.grok-4-1-fast-non-reasoning": {"provider":"xai","name":"xAI Grok 4.1 Fast Non-Reasoning","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaklmfkhxw5lwo5ifhqo2dxu5ymbeyzdr4nnpv3paigsoq"}},
|
|
"xai.grok-4-fast-reasoning": {"provider":"xai","name":"xAI Grok 4 Fast Reasoning","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyamqkpviarwzvo3wwuhnhqloa224fneaukac5megifuaba"}},
|
|
"xai.grok-4-fast-non-reasoning": {"provider":"xai","name":"xAI Grok 4 Fast Non-Reasoning","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaj2m67qex5dzgi7gi5blfdztnvbwggvibaxi6ac3jthha"}},
|
|
"xai.grok-3": {"provider":"xai","name":"xAI Grok 3","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyag3w2xk76vlahjujj2gdfeuzhflt25gbo3bxidlsqfjla"}},
|
|
"xai.grok-3-mini": {"provider":"xai","name":"xAI Grok 3 Mini","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyar77g5dtex3loxuluziukznryr7kowvncea33ml7vrlda"}},
|
|
"xai.grok-3-fast": {"provider":"xai","name":"xAI Grok 3 Fast","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyazdu5a5ruzfds34kwb2zwc65p6fes5uwddmkq7dyisvxq"}},
|
|
"xai.grok-3-mini-fast": {"provider":"xai","name":"xAI Grok 3 Mini Fast","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyauvjoll2repj5pbtkk7pinwj57ex3lkehzpxd6v6rxscq"}},
|
|
"xai.grok-code-fast-1": {"provider":"xai","name":"xAI Grok Code Fast 1","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaayp5ccahfbpht56x7meyc26gyvojowmrbrr4xj626enq"}},
|
|
# ── ProtectAI ──
|
|
"protectai.deberta-v3-base-prompt-injection-v2": {"provider":"protectai","name":"ProtectAI DeBERTa Prompt Injection v2","api_format":"GENERIC",
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyasj4kxlosqvmyaxu53lrc7iddmo2owc6bizoa2y5w47oq"}},
|
|
}
|
|
|
|
EMBEDDING_MODELS = {
|
|
"cohere.embed-v4.0": {"name":"Cohere Embed v4.0 (Multimodal)","dims":1536,
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyahw4vlsxm7newcqtlgmristnwxlrxox3h7bcnlomjpgwa"}},
|
|
"openai.text-embedding-3-large": {"name":"OpenAI Text Embedding 3 Large","dims":3072,
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya3i6o2p5h2mij6unya5bsyc46aqey5hwy3icncawo3vcq"}},
|
|
"openai.text-embedding-3-small": {"name":"OpenAI Text Embedding 3 Small","dims":1536,
|
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyarjpjyniixp4tf7kyhr5bfajxmky4sjmbki2hp55ns2pq"}},
|
|
}
|
|
|
|
GENAI_REGIONS = [
|
|
"us-chicago-1","us-ashburn-1","us-phoenix-1","uk-london-1",
|
|
"eu-frankfurt-1","ap-tokyo-1","ap-osaka-1","sa-saopaulo-1",
|
|
"ca-toronto-1","ap-melbourne-1","ap-mumbai-1","eu-amsterdam-1",
|
|
"me-jeddah-1","ap-singapore-1","ap-seoul-1","sa-vinhedo-1",
|
|
]
|
|
|
|
# ── Database ──────────────────────────────────────────────────────────────────
|
|
@contextmanager
|
|
def db():
|
|
conn = sqlite3.connect(str(DB_PATH))
|
|
conn.row_factory = sqlite3.Row
|
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
try:
|
|
yield conn
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
|
|
def init_db():
|
|
with db() as c:
|
|
c.executescript("""
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id TEXT PRIMARY KEY,
|
|
first_name TEXT NOT NULL,
|
|
last_name TEXT NOT NULL,
|
|
username TEXT UNIQUE NOT NULL,
|
|
email TEXT,
|
|
password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'viewer',
|
|
mfa_secret TEXT, mfa_enabled INTEGER DEFAULT 0,
|
|
is_active INTEGER DEFAULT 1,
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
last_login TEXT
|
|
);
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
id TEXT PRIMARY KEY, user_id TEXT NOT NULL,
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
expires_at TEXT NOT NULL, is_active INTEGER DEFAULT 1
|
|
);
|
|
CREATE TABLE IF NOT EXISTS oci_configs (
|
|
id TEXT PRIMARY KEY, user_id TEXT NOT NULL,
|
|
tenancy_name TEXT NOT NULL, tenancy_ocid TEXT NOT NULL,
|
|
user_ocid TEXT NOT NULL, fingerprint TEXT NOT NULL,
|
|
region TEXT NOT NULL, key_file_path TEXT NOT NULL,
|
|
compartment_id TEXT,
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
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',
|
|
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)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS reports (
|
|
id TEXT PRIMARY KEY, user_id TEXT NOT NULL,
|
|
tenancy_name TEXT NOT NULL, config_id TEXT,
|
|
mcp_server_id TEXT,
|
|
status TEXT DEFAULT 'pending', report_data TEXT,
|
|
html_path TEXT, json_path TEXT,
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
completed_at TEXT, error_msg TEXT
|
|
);
|
|
CREATE TABLE IF NOT EXISTS adb_vector_configs (
|
|
id TEXT PRIMARY KEY, user_id TEXT NOT NULL,
|
|
config_name TEXT NOT NULL,
|
|
dsn TEXT NOT NULL,
|
|
username TEXT NOT NULL,
|
|
password_enc TEXT NOT NULL,
|
|
wallet_dir TEXT,
|
|
wallet_password_enc TEXT,
|
|
table_name TEXT DEFAULT 'CIS_EMBEDDINGS',
|
|
use_mtls INTEGER DEFAULT 1,
|
|
is_active INTEGER DEFAULT 1,
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
CREATE TABLE IF NOT EXISTS mcp_servers (
|
|
id TEXT PRIMARY KEY, user_id TEXT NOT NULL,
|
|
name TEXT NOT NULL, description TEXT,
|
|
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'))
|
|
);
|
|
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
id TEXT PRIMARY KEY, session_id TEXT NOT NULL,
|
|
user_id TEXT NOT NULL, role TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
model_id TEXT,
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
CREATE TABLE IF NOT EXISTS audit_log (
|
|
id TEXT PRIMARY KEY, user_id TEXT, username TEXT,
|
|
action TEXT NOT NULL, resource TEXT, details TEXT,
|
|
ip TEXT, created_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
CREATE TABLE IF NOT EXISTS config_logs (
|
|
id TEXT PRIMARY KEY,
|
|
config_type TEXT NOT NULL,
|
|
config_id TEXT NOT NULL,
|
|
config_name TEXT,
|
|
severity TEXT NOT NULL,
|
|
action TEXT NOT NULL,
|
|
message TEXT NOT NULL,
|
|
user_id TEXT,
|
|
username TEXT,
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
""")
|
|
c.execute("DELETE FROM config_logs WHERE created_at < datetime('now', '-30 days')")
|
|
# ── Migrations ──
|
|
for col in ["genai_config_id TEXT", "embedding_model_id TEXT DEFAULT 'cohere.embed-v4.0'"]:
|
|
try:
|
|
c.execute(f"ALTER TABLE adb_vector_configs ADD COLUMN {col}")
|
|
except sqlite3.OperationalError:
|
|
pass
|
|
adm = c.execute("SELECT id FROM users WHERE username='admin'").fetchone()
|
|
if not adm:
|
|
c.execute(
|
|
"INSERT INTO users (id,first_name,last_name,username,email,password_hash,role) VALUES (?,?,?,?,?,?,?)",
|
|
(str(uuid.uuid4()), "Admin", "Sistema", "admin", "admin@local", _hash_pw("admin123"), "admin")
|
|
)
|
|
log.info("Default admin created: admin / admin123")
|
|
|
|
# ── Crypto helpers ────────────────────────────────────────────────────────────
|
|
def _hash_pw(pw): salt=secrets.token_hex(16); h=hashlib.pbkdf2_hmac("sha256",pw.encode(),salt.encode(),100_000); return f"{salt}:{h.hex()}"
|
|
def _verify_pw(pw,stored): salt,h=stored.split(":"); return hmac.compare_digest(hashlib.pbkdf2_hmac("sha256",pw.encode(),salt.encode(),100_000).hex(),h)
|
|
def _totp_secret(): return base64.b32encode(secrets.token_bytes(20)).decode()
|
|
def _totp_verify(secret,code,window=1):
|
|
key=base64.b32decode(secret); now=int(time.time())//30
|
|
for off in range(-window,window+1):
|
|
msg=struct.pack(">Q",now+off); h=hmac.new(key,msg,hashlib.sha1).digest(); o=h[-1]&0x0F
|
|
c=str((struct.unpack(">I",h[o:o+4])[0]&0x7FFFFFFF)%1_000_000).zfill(6)
|
|
if hmac.compare_digest(c,code): return True
|
|
return False
|
|
def _totp_uri(secret,user): return f"otpauth://totp/OCI-CIS-Agent:{user}?secret={secret}&issuer=OCI-CIS-Agent"
|
|
def _make_token(uid,role,sid):
|
|
return pyjwt.encode({"sub":uid,"role":role,"sid":sid,"exp":datetime.utcnow()+timedelta(hours=JWT_EXP_H),"iat":datetime.utcnow()},APP_SECRET,algorithm=JWT_ALG)
|
|
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])
|
|
except pyjwt.ExpiredSignatureError: raise HTTPException(401, "Token expirado")
|
|
except pyjwt.InvalidTokenError: raise HTTPException(401, "Token inválido")
|
|
with db() as c:
|
|
u = c.execute("SELECT * FROM users WHERE id=? AND is_active=1", (p["sub"],)).fetchone()
|
|
s = c.execute("SELECT * FROM sessions WHERE id=? AND is_active=1", (p["sid"],)).fetchone()
|
|
if not u or not s: raise HTTPException(401, "Sessão inválida")
|
|
return dict(u)
|
|
|
|
def require(*roles):
|
|
async def dep(u=Depends(current_user)):
|
|
if u["role"] not in roles: raise HTTPException(403, f"Requer role: {', '.join(roles)}")
|
|
return u
|
|
return dep
|
|
|
|
def _audit(uid, uname, action, resource=None, details=None, ip=None):
|
|
with db() as c:
|
|
c.execute("INSERT INTO audit_log (id,user_id,username,action,resource,details,ip) VALUES (?,?,?,?,?,?,?)",
|
|
(str(uuid.uuid4()), uid, uname, action, resource, details, ip))
|
|
|
|
def _config_log(config_type, config_id, config_name, severity, action, message, uid=None, uname=None):
|
|
with db() as c:
|
|
c.execute("INSERT INTO config_logs (id,config_type,config_id,config_name,severity,action,message,user_id,username) VALUES (?,?,?,?,?,?,?,?,?)",
|
|
(str(uuid.uuid4()), config_type, config_id, config_name or "", severity, action, str(message)[:2000], uid, uname))
|
|
|
|
# ── Models ────────────────────────────────────────────────────────────────────
|
|
class LoginReq(BaseModel):
|
|
username: str; password: str; totp_code: Optional[str] = None
|
|
class RegisterReq(BaseModel):
|
|
first_name: str; last_name: str; username: str; email: str; password: str; role: str = "viewer"
|
|
class TOTPVerify(BaseModel):
|
|
totp_code: str
|
|
class ChangePwReq(BaseModel):
|
|
current_password: str; new_password: str
|
|
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
|
|
# Option A: saved preset
|
|
genai_config_id: Optional[str] = None
|
|
# Option B: direct model selection (inline)
|
|
model_id: Optional[str] = None; oci_config_id: Optional[str] = None
|
|
genai_region: Optional[str] = None; compartment_id: Optional[str] = None
|
|
temperature: Optional[float] = None; max_tokens: Optional[int] = None
|
|
top_p: Optional[float] = None; top_k: Optional[int] = None
|
|
frequency_penalty: Optional[float] = None; presence_penalty: Optional[float] = None
|
|
class RunReportReq(BaseModel):
|
|
config_id: str; mcp_server_id: Optional[str] = None; regions: Optional[List[str]] = None
|
|
class GenAIConfigReq(BaseModel):
|
|
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
|
|
wallet_password: Optional[str] = None; table_name: str = "CIS_EMBEDDINGS"; use_mtls: bool = True
|
|
genai_config_id: Optional[str] = None; embedding_model_id: str = "cohere.embed-v4.0"
|
|
class IngestDocReq(BaseModel):
|
|
adb_config_id: str; documents: List[Dict[str, Any]]
|
|
class MCPServerReq(BaseModel):
|
|
name: str; description: Optional[str] = None; server_type: str = "stdio"
|
|
command: Optional[str] = None; args: Optional[List[str]] = None
|
|
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")
|
|
async def login(req: LoginReq, request: Request):
|
|
with db() as c:
|
|
u = c.execute("SELECT * FROM users WHERE username=? AND is_active=1", (req.username,)).fetchone()
|
|
if not u or not _verify_pw(req.password, u["password_hash"]): raise HTTPException(401, "Credenciais inválidas")
|
|
u = dict(u)
|
|
if u["mfa_enabled"]:
|
|
if not req.totp_code: return {"mfa_required": True, "message": "Código MFA necessário"}
|
|
if not _totp_verify(u["mfa_secret"], req.totp_code): raise HTTPException(401, "Código MFA inválido")
|
|
sid = str(uuid.uuid4()); exp = (datetime.utcnow()+timedelta(hours=JWT_EXP_H)).isoformat()
|
|
with db() as c:
|
|
c.execute("INSERT INTO sessions (id,user_id,expires_at) VALUES (?,?,?)", (sid, u["id"], exp))
|
|
c.execute("UPDATE users SET last_login=datetime('now') WHERE id=?", (u["id"],))
|
|
_audit(u["id"], u["username"], "login", ip=request.client.host if request.client else None)
|
|
return {"token":_make_token(u["id"],u["role"],sid),
|
|
"user":{"id":u["id"],"first_name":u["first_name"],"last_name":u["last_name"],"username":u["username"],"email":u["email"],"role":u["role"],"mfa_enabled":bool(u["mfa_enabled"])}}
|
|
|
|
@app.post("/api/auth/logout")
|
|
async def logout_ep(u=Depends(current_user)):
|
|
with db() as c: c.execute("UPDATE sessions SET is_active=0 WHERE user_id=?", (u["id"],))
|
|
return {"ok": True}
|
|
|
|
@app.post("/api/auth/register")
|
|
async def register(req: RegisterReq, adm=Depends(require("admin"))):
|
|
if req.role not in ("admin","user","viewer"): raise HTTPException(400, "Role inválida")
|
|
uid = str(uuid.uuid4())
|
|
with db() as c:
|
|
if c.execute("SELECT 1 FROM users WHERE username=?", (req.username,)).fetchone(): raise HTTPException(400, "Usuário já existe")
|
|
c.execute("INSERT INTO users (id,first_name,last_name,username,email,password_hash,role) VALUES (?,?,?,?,?,?,?)",
|
|
(uid, req.first_name, req.last_name, req.username, req.email, _hash_pw(req.password), req.role))
|
|
_audit(adm["id"], adm["username"], "create_user", uid, f"user={req.username} role={req.role}")
|
|
return {"id": uid, "username": req.username, "role": req.role}
|
|
|
|
@app.post("/api/auth/change-password")
|
|
async def change_pw(req: ChangePwReq, u=Depends(current_user)):
|
|
if not _verify_pw(req.current_password, u["password_hash"]): raise HTTPException(400, "Senha atual incorreta")
|
|
with db() as c: c.execute("UPDATE users SET password_hash=? WHERE id=?", (_hash_pw(req.new_password), u["id"]))
|
|
return {"ok": True}
|
|
|
|
# ── MFA ───────────────────────────────────────────────────────────────────────
|
|
@app.post("/api/mfa/setup")
|
|
async def mfa_setup(u=Depends(current_user)):
|
|
sec = _totp_secret()
|
|
with db() as c: c.execute("UPDATE users SET mfa_secret=? WHERE id=?", (sec, u["id"]))
|
|
return {"secret": sec, "uri": _totp_uri(sec, u["username"])}
|
|
|
|
@app.post("/api/mfa/verify")
|
|
async def mfa_verify(req: TOTPVerify, u=Depends(current_user)):
|
|
if not u.get("mfa_secret"): raise HTTPException(400, "Chame /api/mfa/setup primeiro")
|
|
if not _totp_verify(u["mfa_secret"], req.totp_code): raise HTTPException(400, "Código inválido")
|
|
with db() as c: c.execute("UPDATE users SET mfa_enabled=1 WHERE id=?", (u["id"],))
|
|
return {"ok": True, "message": "MFA ativado"}
|
|
|
|
@app.post("/api/mfa/disable/{user_id}")
|
|
async def mfa_disable(user_id: str, adm=Depends(require("admin"))):
|
|
with db() as c: c.execute("UPDATE users SET mfa_enabled=0,mfa_secret=NULL WHERE id=?", (user_id,))
|
|
_audit(adm["id"], adm["username"], "disable_mfa", user_id)
|
|
return {"ok": True}
|
|
|
|
# ── Users ─────────────────────────────────────────────────────────────────────
|
|
@app.get("/api/users")
|
|
async def list_users(u=Depends(require("admin"))):
|
|
with db() as c: rows = c.execute("SELECT id,first_name,last_name,username,email,role,mfa_enabled,is_active,created_at,last_login FROM users").fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
@app.get("/api/users/me")
|
|
async def me(u=Depends(current_user)):
|
|
return {k: u[k] for k in ("id","first_name","last_name","username","email","role","mfa_enabled")}
|
|
|
|
@app.put("/api/users/{uid}")
|
|
async def update_user(uid: str, req: UserUpdateReq, adm=Depends(require("admin"))):
|
|
sets, vals = [], []
|
|
if req.email is not None: sets.append("email=?"); vals.append(req.email)
|
|
if req.role is not None:
|
|
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)
|
|
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}
|
|
|
|
@app.delete("/api/users/{uid}")
|
|
async def del_user(uid: str, adm=Depends(require("admin"))):
|
|
if uid == adm["id"]: raise HTTPException(400, "Não pode desativar a si mesmo")
|
|
with db() as c: c.execute("UPDATE users SET is_active=0 WHERE id=?", (uid,))
|
|
_audit(adm["id"], adm["username"], "deactivate_user", uid)
|
|
return {"ok": True}
|
|
|
|
# ── OCI Config ────────────────────────────────────────────────────────────────
|
|
@app.post("/api/oci/config")
|
|
async def save_oci(
|
|
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: UploadFile = File(...), public_key: Optional[UploadFile] = File(None),
|
|
u = Depends(require("admin","user"))
|
|
):
|
|
cid = str(uuid.uuid4()); cdir = OCI_DIR / cid; cdir.mkdir(parents=True)
|
|
kp = cdir / "oci_api_key.pem"
|
|
key_bytes = await private_key.read()
|
|
kp.write_bytes(key_bytes); kp.chmod(0o600)
|
|
# Detect encrypted keys that require passphrase
|
|
key_text = key_bytes.decode("utf-8", errors="ignore")
|
|
key_is_encrypted = "ENCRYPTED" in key_text or "Proc-Type: 4,ENCRYPTED" in key_text
|
|
if key_is_encrypted and not key_passphrase:
|
|
# Clean up and warn
|
|
shutil.rmtree(cdir, ignore_errors=True)
|
|
raise HTTPException(400, "A chave privada está criptografada (ENCRYPTED). Informe a Key Passphrase.")
|
|
if public_key: (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("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))
|
|
_audit(u["id"], u["username"], "save_oci_config", cid, f"tenancy={tenancy_name}")
|
|
_config_log("oci", cid, tenancy_name, "success", "save", f"Credencial salva: {tenancy_name} ({region})", u["id"], u["username"])
|
|
return {"id": cid, "tenancy_name": tenancy_name, "region": region}
|
|
|
|
@app.get("/api/oci/configs")
|
|
async def list_oci(u=Depends(current_user)):
|
|
with db() as c:
|
|
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}")
|
|
async def del_oci(cid: str, u=Depends(require("admin","user"))):
|
|
with db() as c:
|
|
cfg=c.execute("SELECT * FROM oci_configs WHERE id=?",(cid,)).fetchone()
|
|
if not cfg: raise HTTPException(404)
|
|
if u["role"]!="admin" and cfg["user_id"]!=u["id"]: raise HTTPException(403)
|
|
c.execute("DELETE FROM oci_configs WHERE id=?",(cid,))
|
|
d=OCI_DIR/cid
|
|
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"
|
|
cname = None
|
|
with db() as c:
|
|
row = c.execute("SELECT tenancy_name FROM oci_configs WHERE id=?", (cid,)).fetchone()
|
|
if row: cname = row["tenancy_name"]
|
|
if not cp.exists():
|
|
_config_log("oci", cid, cname, "error", "test", "Config não encontrada", u["id"], u["username"])
|
|
return {"status":"error","message":"Config não encontrada"}
|
|
try:
|
|
r = subprocess.run(["oci","iam","region","list","--config-file",str(cp),"--output","json"],
|
|
capture_output=True, text=True, timeout=30, stdin=subprocess.DEVNULL)
|
|
if r.returncode == 0:
|
|
_config_log("oci", cid, cname, "success", "test", "Conexão OK", u["id"], u["username"])
|
|
return {"status":"success","message":"Conexão OK","data":json.loads(r.stdout)}
|
|
msg = r.stderr[:500]
|
|
if "passphrase" in msg.lower() or "getpass" in msg.lower():
|
|
msg = "A chave privada requer passphrase. Recadastre a credencial informando a Key Passphrase."
|
|
_config_log("oci", cid, cname, "error", "test", msg, u["id"], u["username"])
|
|
return {"status":"error","message":msg}
|
|
except FileNotFoundError:
|
|
_config_log("oci", cid, cname, "error", "test", "OCI CLI não disponível no container.", u["id"], u["username"])
|
|
return {"status":"error","message":"OCI CLI não disponível no container."}
|
|
except subprocess.TimeoutExpired:
|
|
_config_log("oci", cid, cname, "error", "test", "Timeout na conexão", u["id"], u["username"])
|
|
return {"status":"error","message":"Timeout na conexão"}
|
|
except Exception as e:
|
|
_config_log("oci", cid, cname, "error", "test", str(e)[:500], u["id"], u["username"])
|
|
return {"status":"error","message":str(e)[:500]}
|
|
|
|
# ── 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, "embedding_models": EMBEDDING_MODELS}
|
|
|
|
@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,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)
|
|
_config_log("genai", gid, req.name, "success", "save", f"Modelo salvo: {req.model_id} ({req.genai_region})", u["id"], u["username"])
|
|
return {"id": gid, "model_id": req.model_id, "endpoint": ep}
|
|
|
|
@app.get("/api/genai/configs")
|
|
async def list_genai(u=Depends(current_user)):
|
|
with db() as c:
|
|
if u["role"]=="admin": rows=c.execute("SELECT * FROM genai_configs").fetchall()
|
|
else: rows=c.execute("SELECT * FROM genai_configs WHERE user_id=?",(u["id"],)).fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
@app.delete("/api/genai/configs/{gid}")
|
|
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:
|
|
gc = c.execute("SELECT * FROM genai_configs WHERE id=?",(gid,)).fetchone()
|
|
if not gc: raise HTTPException(404)
|
|
gname = gc["name"]
|
|
try:
|
|
resp = _call_genai(dict(gc), "Say 'connection successful' in one short sentence.")
|
|
_config_log("genai", gid, gname, "success", "test", f"GenAI OK: {resp[:200]}", u["id"], u["username"])
|
|
return {"status":"success","message":"GenAI OK","response":resp[:300]}
|
|
except Exception as e:
|
|
msg = str(e)[:500]
|
|
_config_log("genai", gid, gname, "error", "test", msg, u["id"], u["username"])
|
|
return {"status":"error","message":msg}
|
|
|
|
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
|
|
|
|
# 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")
|
|
|
|
# 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 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:
|
|
# ── Generic format (Meta Llama, Google, xAI, OpenAI) ──
|
|
chat_request = oci.generative_ai_inference.models.GenericChatRequest()
|
|
chat_request.api_format = oci.generative_ai_inference.models.BaseChatRequest.API_FORMAT_GENERIC
|
|
provider = model_info.get("provider", "")
|
|
is_openai = provider == "openai"
|
|
is_xai = provider == "xai"
|
|
# OpenAI models use max_completion_tokens instead of max_tokens
|
|
# and do not support top_k
|
|
if is_openai:
|
|
chat_request.max_completion_tokens = int(gc.get("max_tokens", 6000))
|
|
else:
|
|
chat_request.max_tokens = int(gc.get("max_tokens", 6000))
|
|
if not is_xai:
|
|
chat_request.top_k = int(gc.get("top_k", 1))
|
|
chat_request.temperature = float(gc.get("temperature", 1))
|
|
if not is_xai:
|
|
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))
|
|
|
|
messages = []
|
|
if history:
|
|
for h in history:
|
|
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 - resolve OCID: explicit model_ocid > catalog ocid for region > short model_id
|
|
model_ref = gc.get("model_ocid") or ""
|
|
if not model_ref:
|
|
region = gc.get("genai_region", "")
|
|
ocids = model_info.get("ocids", {})
|
|
model_ref = ocids.get(region) or gc["model_id"]
|
|
log.info(f"GenAI call: model_id={gc.get('model_id')}, model_ref={model_ref[:60]}...")
|
|
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.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 resp.text if hasattr(resp, 'text') else str(resp)
|
|
else:
|
|
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)
|
|
|
|
# ── RAG Helpers ───────────────────────────────────────────────────────────────
|
|
RAG_SYSTEM_PROMPT = """You are the OCI CIS AI Agent, an expert assistant for Oracle Cloud Infrastructure security and compliance.
|
|
|
|
You have been provided with relevant context documents retrieved from a knowledge base. Use this context to provide accurate, specific answers. If the context does not contain relevant information for the question, say so and answer based on your general knowledge.
|
|
|
|
Always cite the source documents when using information from the context.
|
|
|
|
--- RETRIEVED CONTEXT ---
|
|
{context}
|
|
--- END CONTEXT ---
|
|
|
|
User question: {question}"""
|
|
|
|
def _get_adb_connection(cfg: dict):
|
|
"""Create an oracledb connection from an adb_vector_configs row."""
|
|
import oracledb
|
|
params = {"user": cfg["username"], "password": _dec(cfg["password_enc"]), "dsn": cfg["dsn"]}
|
|
if cfg["use_mtls"] and cfg.get("wallet_dir"):
|
|
params["config_dir"] = cfg["wallet_dir"]
|
|
params["wallet_location"] = cfg["wallet_dir"]
|
|
if cfg.get("wallet_password_enc"):
|
|
params["wallet_password"] = _dec(cfg["wallet_password_enc"])
|
|
return oracledb.connect(**params)
|
|
|
|
def _ensure_embeddings_table(cfg: dict):
|
|
"""Create the embeddings table in ADB if it doesn't exist."""
|
|
table_name = cfg.get("table_name", "CIS_EMBEDDINGS")
|
|
emb_model = cfg.get("embedding_model_id", "cohere.embed-v4.0")
|
|
dims = EMBEDDING_MODELS.get(emb_model, {}).get("dims", 1024)
|
|
conn = _get_adb_connection(cfg)
|
|
try:
|
|
cur = conn.cursor()
|
|
cur.execute("SELECT COUNT(*) FROM user_tables WHERE table_name = :1", (table_name.upper(),))
|
|
if cur.fetchone()[0] == 0:
|
|
cur.execute(f"""
|
|
CREATE TABLE {table_name} (
|
|
ID VARCHAR2(100) PRIMARY KEY,
|
|
CONTENT CLOB,
|
|
EMBEDDING VECTOR({dims}, FLOAT64),
|
|
METADATA VARCHAR2(4000),
|
|
SOURCE VARCHAR2(500),
|
|
CREATED_AT TIMESTAMP DEFAULT SYSTIMESTAMP
|
|
)
|
|
""")
|
|
conn.commit()
|
|
log.info(f"Created embeddings table: {table_name} (dims={dims})")
|
|
cur.close()
|
|
finally:
|
|
conn.close()
|
|
|
|
def _embed_text(text: str, genai_cfg: dict, embedding_model_id: str) -> list:
|
|
"""Generate embedding using OCI GenAI embed endpoint."""
|
|
import oci
|
|
config_path = str(OCI_DIR / genai_cfg["oci_config_id"] / "config")
|
|
config = oci.config.from_file(config_path, "DEFAULT")
|
|
endpoint = genai_cfg["endpoint"]
|
|
client = oci.generative_ai_inference.GenerativeAiInferenceClient(
|
|
config=config, service_endpoint=endpoint,
|
|
retry_strategy=oci.retry.NoneRetryStrategy(), timeout=(10, 120)
|
|
)
|
|
embed_detail = oci.generative_ai_inference.models.EmbedTextDetails()
|
|
embed_detail.inputs = [text]
|
|
# Resolve OCID for embedding model by region
|
|
emb_info = EMBEDDING_MODELS.get(embedding_model_id, {})
|
|
region = genai_cfg.get("genai_region", "")
|
|
emb_ref = emb_info.get("ocids", {}).get(region) or embedding_model_id
|
|
embed_detail.serving_mode = oci.generative_ai_inference.models.OnDemandServingMode(model_id=emb_ref)
|
|
embed_detail.compartment_id = genai_cfg["compartment_id"]
|
|
embed_detail.truncate = "NONE"
|
|
embed_detail.input_type = "SEARCH_QUERY"
|
|
response = client.embed_text(embed_detail)
|
|
return response.data.embeddings[0]
|
|
|
|
def _vector_search(cfg: dict, query_embedding: list, top_k: int = 5) -> list:
|
|
"""Search ADB vector store using cosine similarity. Returns top-K documents."""
|
|
import array
|
|
table_name = cfg.get("table_name", "CIS_EMBEDDINGS")
|
|
conn = _get_adb_connection(cfg)
|
|
try:
|
|
cur = conn.cursor()
|
|
vec = array.array('d', query_embedding)
|
|
cur.execute(f"""
|
|
SELECT ID, CONTENT, METADATA, SOURCE,
|
|
VECTOR_DISTANCE(EMBEDDING, :1, COSINE) AS distance
|
|
FROM {table_name}
|
|
ORDER BY distance ASC
|
|
FETCH FIRST :2 ROWS ONLY
|
|
""", [vec, top_k])
|
|
results = []
|
|
for row in cur:
|
|
content = row[1]
|
|
if hasattr(content, 'read'):
|
|
content = content.read()
|
|
results.append({
|
|
"id": row[0], "content": content or "",
|
|
"metadata": row[2], "source": row[3], "distance": float(row[4])
|
|
})
|
|
cur.close()
|
|
return results
|
|
finally:
|
|
conn.close()
|
|
|
|
def _build_rag_context(documents: list) -> str:
|
|
"""Format retrieved documents into a context string for the LLM prompt."""
|
|
if not documents:
|
|
return ""
|
|
parts = []
|
|
for i, doc in enumerate(documents, 1):
|
|
source = doc.get("source", "unknown")
|
|
content = doc.get("content", "")
|
|
if len(content) > 2000:
|
|
content = content[:2000] + "..."
|
|
parts.append(f"[Document {i} | Source: {source}]\n{content}")
|
|
return "\n\n---\n\n".join(parts)
|
|
|
|
def _get_active_adb_config(user_id: str) -> dict | None:
|
|
"""Get the first active ADB vector config with a linked GenAI config."""
|
|
with db() as c:
|
|
cfg = c.execute(
|
|
"SELECT * FROM adb_vector_configs WHERE user_id=? AND is_active=1 AND genai_config_id IS NOT NULL ORDER BY created_at DESC LIMIT 1",
|
|
(user_id,)
|
|
).fetchone()
|
|
if not cfg:
|
|
cfg = c.execute(
|
|
"SELECT * FROM adb_vector_configs WHERE is_active=1 AND genai_config_id IS NOT NULL ORDER BY created_at DESC LIMIT 1"
|
|
).fetchone()
|
|
return dict(cfg) if cfg else None
|
|
|
|
# ── MCP Servers ───────────────────────────────────────────────────────────────
|
|
@app.post("/api/mcp/servers")
|
|
async def register_mcp(req: MCPServerReq, u=Depends(require("admin","user"))):
|
|
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,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.tools) if req.tools else None, req.linked_adb_id))
|
|
_audit(u["id"], u["username"], "register_mcp", mid, req.name)
|
|
_config_log("mcp", mid, req.name, "success", "save", f"MCP registrado: {req.name} ({req.server_type})", u["id"], u["username"])
|
|
return {"id": mid, "name": req.name, "server_type": req.server_type}
|
|
|
|
@app.get("/api/mcp/servers")
|
|
async def list_mcp(u=Depends(current_user)):
|
|
with db() as c:
|
|
if u["role"]=="admin": rows=c.execute("SELECT * FROM mcp_servers ORDER BY created_at DESC").fetchall()
|
|
else: rows=c.execute("SELECT * FROM mcp_servers WHERE user_id=? ORDER BY created_at DESC",(u["id"],)).fetchall()
|
|
res = []
|
|
for r in rows:
|
|
d = dict(r)
|
|
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
|
|
|
|
@app.delete("/api/mcp/servers/{mid}")
|
|
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:
|
|
cur = c.execute("SELECT is_active FROM mcp_servers WHERE id=?",(mid,)).fetchone()
|
|
if not cur: raise HTTPException(404)
|
|
c.execute("UPDATE mcp_servers SET is_active=? WHERE id=?",(0 if cur["is_active"] else 1, mid))
|
|
return {"ok": True, "is_active": not cur["is_active"]}
|
|
|
|
@app.post("/api/mcp/servers/{mid}/upload")
|
|
async def upload_mcp_file(mid: str, file: UploadFile = File(...), u=Depends(require("admin","user"))):
|
|
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))
|
|
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/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(
|
|
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-v4.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"], 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"))):
|
|
cname = None
|
|
with db() as c:
|
|
row = c.execute("SELECT config_name FROM adb_vector_configs WHERE id=?", (vid,)).fetchone()
|
|
if row: cname = row["config_name"]
|
|
try:
|
|
wdir = WALLET_DIR / vid; wdir.mkdir(parents=True, exist_ok=True)
|
|
zp = wdir / "wallet.zip"
|
|
zp.write_bytes(await wallet.read())
|
|
import zipfile
|
|
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))
|
|
files = os.listdir(str(wdir))
|
|
_config_log("adb", vid, cname, "success", "upload", f"Wallet enviada: {', '.join(files)}", u["id"], u["username"])
|
|
return {"ok": True, "wallet_dir": str(wdir), "files": files}
|
|
except Exception as e:
|
|
_config_log("adb", vid, cname, "error", "upload", str(e)[:500], u["id"], u["username"])
|
|
raise HTTPException(500, str(e)[:500])
|
|
|
|
@app.get("/api/adb/configs")
|
|
async def list_adb(u=Depends(current_user)):
|
|
with db() as c:
|
|
cols = "id,config_name,dsn,username,table_name,use_mtls,is_active,wallet_dir,genai_config_id,embedding_model_id,created_at"
|
|
if u["role"]=="admin": rows=c.execute(f"SELECT {cols} FROM adb_vector_configs").fetchall()
|
|
else: rows=c.execute(f"SELECT {cols} FROM adb_vector_configs WHERE user_id=?",(u["id"],)).fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
@app.post("/api/adb/test/{vid}")
|
|
async def test_adb(vid: str, u=Depends(require("admin","user"))):
|
|
with db() as c:
|
|
cfg = c.execute("SELECT * FROM adb_vector_configs WHERE id=?",(vid,)).fetchone()
|
|
if not cfg: raise HTTPException(404)
|
|
cname = cfg["config_name"]
|
|
try:
|
|
conn = _get_adb_connection(dict(cfg))
|
|
cur = conn.cursor(); cur.execute("SELECT 1 FROM DUAL"); cur.close(); conn.close()
|
|
_config_log("adb", vid, cname, "success", "test", "Conexão Autonomous DB OK!", u["id"], u["username"])
|
|
return {"status":"success","message":"Conexão Autonomous DB OK!"}
|
|
except ImportError:
|
|
_config_log("adb", vid, cname, "error", "test", "python-oracledb não disponível no container.", u["id"], u["username"])
|
|
return {"status":"error","message":"python-oracledb não disponível no container."}
|
|
except Exception as e:
|
|
msg = str(e)[:500]
|
|
_config_log("adb", vid, cname, "error", "test", msg, u["id"], u["username"])
|
|
return {"status":"error","message":msg}
|
|
|
|
@app.delete("/api/adb/configs/{vid}")
|
|
async def del_adb(vid: str, u=Depends(require("admin","user"))):
|
|
with db() as c: c.execute("DELETE FROM adb_vector_configs WHERE id=?", (vid,))
|
|
d = WALLET_DIR / vid
|
|
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-v4.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:
|
|
cfg = c.execute("SELECT * FROM adb_vector_configs WHERE id=?", (vid,)).fetchone()
|
|
if not cfg: raise HTTPException(404)
|
|
try:
|
|
_ensure_embeddings_table(dict(cfg))
|
|
return {"ok": True, "table": cfg["table_name"]}
|
|
except Exception as e:
|
|
raise HTTPException(500, f"Falha ao criar tabela: {str(e)[:500]}")
|
|
|
|
@app.post("/api/adb/{vid}/ingest")
|
|
async def ingest_documents(vid: str, req: IngestDocReq, bg: BackgroundTasks, u=Depends(require("admin","user"))):
|
|
with db() as c:
|
|
cfg = c.execute("SELECT * FROM adb_vector_configs WHERE id=?", (vid,)).fetchone()
|
|
if not cfg: raise HTTPException(404, "ADB config not found")
|
|
cfg = dict(cfg)
|
|
if not cfg.get("genai_config_id"):
|
|
raise HTTPException(400, "GenAI config not linked to this ADB connection")
|
|
with db() as c:
|
|
gc = c.execute("SELECT * FROM genai_configs WHERE id=?", (cfg["genai_config_id"],)).fetchone()
|
|
if not gc: raise HTTPException(400, "Linked GenAI config not found")
|
|
bg.add_task(_ingest_documents_task, cfg, dict(gc), req.documents, u["id"], u["username"])
|
|
return {"ok": True, "message": f"Ingestão iniciada para {len(req.documents)} documentos", "config_id": vid}
|
|
|
|
def _ingest_documents_task(cfg: dict, genai_cfg: dict, documents: list, user_id: str, username: str):
|
|
"""Background task: embed and insert documents into ADB via OCI GenAI."""
|
|
import array
|
|
emb_model = cfg.get("embedding_model_id", "cohere.embed-v4.0")
|
|
table_name = cfg.get("table_name", "CIS_EMBEDDINGS")
|
|
_ensure_embeddings_table(cfg)
|
|
conn = _get_adb_connection(cfg)
|
|
try:
|
|
cur = conn.cursor()
|
|
inserted = 0
|
|
for doc in documents:
|
|
try:
|
|
content = doc.get("content", "")
|
|
if not content: continue
|
|
embedding = _embed_text(content, genai_cfg, emb_model)
|
|
vec = array.array('d', embedding)
|
|
cur.execute(f"""
|
|
INSERT INTO {table_name} (ID, CONTENT, EMBEDDING, METADATA, SOURCE)
|
|
VALUES (:1, :2, :3, :4, :5)
|
|
""", [str(uuid.uuid4()), content, vec, doc.get("metadata", ""), doc.get("source", "manual_upload")])
|
|
inserted += 1
|
|
except Exception as e:
|
|
log.error(f"Failed to ingest document: {e}")
|
|
conn.commit()
|
|
cur.close()
|
|
log.info(f"Ingested {inserted}/{len(documents)} documents into {table_name}")
|
|
_audit(user_id, username, "ingest_documents", cfg["id"], f"{inserted} documents")
|
|
_config_log("adb", cfg["id"], cfg.get("config_name"), "success", "ingest", f"{inserted}/{len(documents)} documentos ingeridos em {table_name}", user_id, username)
|
|
except Exception as e:
|
|
log.error(f"Ingestion task failed: {e}")
|
|
_config_log("adb", cfg["id"], cfg.get("config_name"), "error", "ingest", str(e)[:500], user_id, username)
|
|
finally:
|
|
conn.close()
|
|
|
|
# ── Embeddings ────────────────────────────────────────────────────────────────
|
|
def _chunk_report_by_section(report_data: dict) -> list:
|
|
"""Chunk a CIS report into documents grouped by section."""
|
|
if isinstance(report_data, str):
|
|
report_data = json.loads(report_data)
|
|
findings = report_data.get("findings", {})
|
|
tenancy = report_data.get("tenancy", "unknown")
|
|
generated_at = report_data.get("generated_at", "")
|
|
sections = {}
|
|
for cid, check in findings.items():
|
|
sec = check.get("section", "Other")
|
|
sections.setdefault(sec, [])
|
|
sections[sec].append(check)
|
|
documents = []
|
|
for section_name, checks in sections.items():
|
|
passed = sum(1 for c in checks if c.get("status") == "PASS")
|
|
failed = sum(1 for c in checks if c.get("status") == "FAIL")
|
|
review = sum(1 for c in checks if c.get("status") == "REVIEW")
|
|
lines = [f"Section: {section_name}", f"Total checks: {len(checks)}, Passed: {passed}, Failed: {failed}, Review: {review}", ""]
|
|
for c in checks:
|
|
status = c.get("status", "REVIEW")
|
|
lines.append(f"- [{c.get('id', '')}] {c.get('title', '')} — Status: {status}")
|
|
if c.get("findings"):
|
|
for f in c["findings"]:
|
|
lines.append(f" Finding: {f}")
|
|
documents.append({
|
|
"content": "\n".join(lines),
|
|
"source": f"CIS Report - {tenancy} - {generated_at}",
|
|
"metadata": f"section: {section_name}, total: {len(checks)}, passed: {passed}, failed: {failed}, review: {review}"
|
|
})
|
|
return documents
|
|
|
|
def _chunk_text_file(text: str, filename: str, chunk_size: int = 1000) -> list:
|
|
"""Split text into chunks by paragraphs or fixed size."""
|
|
paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
|
|
documents = []
|
|
current_chunk = ""
|
|
chunk_num = 1
|
|
for para in paragraphs:
|
|
if len(current_chunk) + len(para) + 2 > chunk_size and current_chunk:
|
|
documents.append({"content": current_chunk, "source": filename, "metadata": f"chunk: {chunk_num}"})
|
|
chunk_num += 1
|
|
current_chunk = para
|
|
else:
|
|
current_chunk = current_chunk + "\n\n" + para if current_chunk else para
|
|
if current_chunk:
|
|
documents.append({"content": current_chunk, "source": filename, "metadata": f"chunk: {chunk_num}"})
|
|
return documents
|
|
|
|
def _get_adb_and_genai(vid: str):
|
|
"""Load ADB config and its linked GenAI config. Returns (adb_cfg, genai_cfg) or raises."""
|
|
with db() as c:
|
|
cfg = c.execute("SELECT * FROM adb_vector_configs WHERE id=?", (vid,)).fetchone()
|
|
if not cfg: raise HTTPException(404, "ADB config not found")
|
|
cfg = dict(cfg)
|
|
if not cfg.get("genai_config_id"):
|
|
raise HTTPException(400, "GenAI config not linked to this ADB connection")
|
|
with db() as c:
|
|
gc = c.execute("SELECT * FROM genai_configs WHERE id=?", (cfg["genai_config_id"],)).fetchone()
|
|
if not gc: raise HTTPException(400, "Linked GenAI config not found")
|
|
return cfg, dict(gc)
|
|
|
|
@app.post("/api/embeddings/report/{rid}")
|
|
async def embed_report(rid: str, req: dict, bg: BackgroundTasks, u=Depends(require("admin","user"))):
|
|
vid = req.get("adb_config_id")
|
|
if not vid: raise HTTPException(400, "adb_config_id is required")
|
|
with db() as c:
|
|
r = c.execute("SELECT report_data,tenancy_name FROM reports WHERE id=? AND status='completed'", (rid,)).fetchone()
|
|
if not r: raise HTTPException(404, "Report not found or not completed")
|
|
report_data = r["report_data"]
|
|
if isinstance(report_data, str):
|
|
try: report_data = json.loads(report_data)
|
|
except: raise HTTPException(400, "Invalid report data")
|
|
documents = _chunk_report_by_section(report_data)
|
|
if not documents: raise HTTPException(400, "No sections found in report")
|
|
cfg, gc = _get_adb_and_genai(vid)
|
|
bg.add_task(_ingest_documents_task, cfg, gc, documents, u["id"], u["username"])
|
|
_audit(u["id"], u["username"], "embed_report", rid, f"{len(documents)} sections")
|
|
return {"ok": True, "message": f"Embedding de {len(documents)} seções iniciado", "sections": len(documents)}
|
|
|
|
@app.post("/api/embeddings/upload")
|
|
async def embed_upload(adb_config_id: str = Form(...), file: UploadFile = File(...), bg: BackgroundTasks = None, u=Depends(require("admin","user"))):
|
|
if not file.filename.lower().endswith(".txt"):
|
|
raise HTTPException(400, "Only .txt files are supported")
|
|
content = (await file.read()).decode("utf-8", errors="replace")
|
|
if not content.strip(): raise HTTPException(400, "File is empty")
|
|
documents = _chunk_text_file(content, file.filename)
|
|
if not documents: raise HTTPException(400, "No content chunks found")
|
|
cfg, gc = _get_adb_and_genai(adb_config_id)
|
|
bg.add_task(_ingest_documents_task, cfg, gc, documents, u["id"], u["username"])
|
|
_audit(u["id"], u["username"], "embed_upload", file.filename, f"{len(documents)} chunks")
|
|
return {"ok": True, "message": f"Embedding de {len(documents)} chunks iniciado", "chunks": len(documents), "filename": file.filename}
|
|
|
|
@app.get("/api/embeddings/{vid}/list")
|
|
async def list_embeddings(vid: str, limit: int = Query(50), offset: int = Query(0), u=Depends(current_user)):
|
|
with db() as c:
|
|
cfg = c.execute("SELECT * FROM adb_vector_configs WHERE id=?", (vid,)).fetchone()
|
|
if not cfg: raise HTTPException(404)
|
|
try:
|
|
conn = _get_adb_connection(dict(cfg))
|
|
cur = conn.cursor()
|
|
table_name = cfg["table_name"] or "CIS_EMBEDDINGS"
|
|
cur.execute(f"SELECT COUNT(*) FROM {table_name}")
|
|
total = cur.fetchone()[0]
|
|
cur.execute(f"""
|
|
SELECT ID, SOURCE, METADATA, CREATED_AT FROM {table_name}
|
|
ORDER BY CREATED_AT DESC
|
|
OFFSET :1 ROWS FETCH NEXT :2 ROWS ONLY
|
|
""", [offset, limit])
|
|
rows = []
|
|
for row in cur:
|
|
rows.append({"id": row[0], "source": row[1], "metadata": row[2],
|
|
"created_at": str(row[3]) if row[3] else None})
|
|
cur.close(); conn.close()
|
|
return {"total": total, "offset": offset, "limit": limit, "documents": rows}
|
|
except Exception as e:
|
|
raise HTTPException(500, f"Erro ao listar embeddings: {str(e)[:500]}")
|
|
|
|
@app.delete("/api/embeddings/{vid}/{doc_id}")
|
|
async def delete_embedding(vid: str, doc_id: str, u=Depends(require("admin","user"))):
|
|
with db() as c:
|
|
cfg = c.execute("SELECT * FROM adb_vector_configs WHERE id=?", (vid,)).fetchone()
|
|
if not cfg: raise HTTPException(404)
|
|
try:
|
|
conn = _get_adb_connection(dict(cfg))
|
|
cur = conn.cursor()
|
|
table_name = cfg["table_name"] or "CIS_EMBEDDINGS"
|
|
cur.execute(f"DELETE FROM {table_name} WHERE ID = :1", [doc_id])
|
|
conn.commit()
|
|
cur.close(); conn.close()
|
|
return {"ok": True}
|
|
except Exception as e:
|
|
raise HTTPException(500, f"Erro ao deletar: {str(e)[:500]}")
|
|
|
|
# ── Reports ───────────────────────────────────────────────────────────────────
|
|
@app.post("/api/reports/run")
|
|
async def run_report(req: RunReportReq, bg: BackgroundTasks, u=Depends(require("admin","user"))):
|
|
with db() as c:
|
|
cfg = c.execute("SELECT * FROM oci_configs WHERE id=?",(req.config_id,)).fetchone()
|
|
if not cfg: raise HTTPException(404, "Config não encontrada")
|
|
rid = str(uuid.uuid4())
|
|
c.execute("INSERT INTO reports (id,user_id,tenancy_name,config_id,mcp_server_id,status) VALUES (?,?,?,?,?,?)",
|
|
(rid, u["id"], cfg["tenancy_name"], req.config_id, req.mcp_server_id, "running"))
|
|
bg.add_task(_exec_report, rid, dict(cfg), req.regions, req.mcp_server_id)
|
|
_audit(u["id"], u["username"], "run_report", rid)
|
|
return {"report_id": rid, "status": "running"}
|
|
|
|
async def _exec_report(rid, cfg, regions, mcp_server_id):
|
|
rdir = REPORTS / rid; rdir.mkdir(parents=True, exist_ok=True)
|
|
config_path = str(OCI_DIR / cfg["id"] / "config")
|
|
try:
|
|
cmd = ["python3", "/app/cis_runner.py", "--config", config_path,
|
|
"--output", str(rdir), "--tenancy-name", cfg["tenancy_name"]]
|
|
if regions: cmd += ["--regions", ",".join(regions)]
|
|
if mcp_server_id:
|
|
with db() as c:
|
|
mcp = c.execute("SELECT * FROM mcp_servers WHERE id=?",(mcp_server_id,)).fetchone()
|
|
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"
|
|
with db() as c:
|
|
if proc.returncode == 0 and jp.exists():
|
|
c.execute("UPDATE reports SET status='completed',report_data=?,html_path=?,json_path=?,completed_at=datetime('now') WHERE id=?",
|
|
(jp.read_text()[:500_000], str(hp) if hp.exists() else None, str(jp), rid))
|
|
_config_log("oci", cfg["id"], cfg["tenancy_name"], "success", "report", f"Relatório concluído: {rid}")
|
|
else:
|
|
err = (stderr.decode() if stderr else "Unknown")[:2000]
|
|
c.execute("UPDATE reports SET status='failed',error_msg=?,completed_at=datetime('now') WHERE id=?", (err, rid))
|
|
_config_log("oci", cfg["id"], cfg["tenancy_name"], "error", "report", err)
|
|
except Exception as e:
|
|
with db() as c:
|
|
c.execute("UPDATE reports SET status='failed',error_msg=?,completed_at=datetime('now') WHERE id=?", (str(e)[:2000], rid))
|
|
_config_log("oci", cfg["id"], cfg["tenancy_name"], "error", "report", str(e)[:2000])
|
|
|
|
@app.get("/api/reports")
|
|
async def list_reports(u=Depends(current_user)):
|
|
with db() as c:
|
|
q = "SELECT id,user_id,tenancy_name,status,mcp_server_id,created_at,completed_at,error_msg FROM reports"
|
|
rows = c.execute(q+" ORDER BY created_at DESC").fetchall() if u["role"]=="admin" \
|
|
else c.execute(q+" WHERE user_id=? ORDER BY created_at DESC",(u["id"],)).fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
@app.get("/api/reports/{rid}")
|
|
async def get_report(rid, u=Depends(current_user)):
|
|
with db() as c: r=c.execute("SELECT * FROM reports WHERE id=?",(rid,)).fetchone()
|
|
if not r: raise HTTPException(404)
|
|
if u["role"]!="admin" and r["user_id"]!=u["id"]: raise HTTPException(403)
|
|
d=dict(r)
|
|
if d.get("report_data"):
|
|
try: d["report_data"]=json.loads(d["report_data"])
|
|
except: pass
|
|
return d
|
|
|
|
@app.get("/api/reports/{rid}/html")
|
|
async def report_html(rid):
|
|
with db() as c: r=c.execute("SELECT html_path FROM reports WHERE id=?",(rid,)).fetchone()
|
|
if not r or not r["html_path"] or not Path(r["html_path"]).exists(): raise HTTPException(404)
|
|
return FileResponse(r["html_path"], media_type="text/html")
|
|
|
|
@app.get("/api/reports/{rid}/download")
|
|
async def report_dl(rid, fmt: str = Query("json"), u=Depends(current_user)):
|
|
with db() as c: r=c.execute("SELECT * FROM reports WHERE id=?",(rid,)).fetchone()
|
|
if not r: raise HTTPException(404)
|
|
p = r["json_path"] if fmt=="json" else r["html_path"]
|
|
if not p or not Path(p).exists(): raise HTTPException(404)
|
|
return FileResponse(p, filename=f"cis_{r['tenancy_name']}_{rid[:8]}.{fmt}")
|
|
|
|
# ── Chat Agent ────────────────────────────────────────────────────────────────
|
|
@app.post("/api/chat")
|
|
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, None))
|
|
|
|
genai_cfg = None
|
|
if msg.genai_config_id:
|
|
with db() as c:
|
|
row = c.execute("SELECT * FROM genai_configs WHERE id=?", (msg.genai_config_id,)).fetchone()
|
|
if row:
|
|
genai_cfg = dict(row)
|
|
# Override params from inline chat settings if provided
|
|
if msg.temperature is not None: genai_cfg["temperature"] = msg.temperature
|
|
if msg.max_tokens is not None: genai_cfg["max_tokens"] = msg.max_tokens
|
|
if msg.top_p is not None: genai_cfg["top_p"] = msg.top_p
|
|
if msg.top_k is not None: genai_cfg["top_k"] = msg.top_k
|
|
if msg.frequency_penalty is not None: genai_cfg["frequency_penalty"] = msg.frequency_penalty
|
|
if msg.presence_penalty is not None: genai_cfg["presence_penalty"] = msg.presence_penalty
|
|
elif msg.model_id and msg.oci_config_id:
|
|
# Direct model mode: build synthetic config dict
|
|
with db() as c:
|
|
oci_row = c.execute("SELECT * FROM oci_configs WHERE id=?", (msg.oci_config_id,)).fetchone()
|
|
if not oci_row:
|
|
raise HTTPException(400, "OCI config not found")
|
|
region = msg.genai_region or oci_row["region"]
|
|
compartment = msg.compartment_id or oci_row.get("compartment_id") or ""
|
|
if not compartment:
|
|
raise HTTPException(400, "compartment_id required")
|
|
genai_cfg = {
|
|
"oci_config_id": msg.oci_config_id,
|
|
"model_id": msg.model_id,
|
|
"model_ocid": None,
|
|
"compartment_id": compartment,
|
|
"genai_region": region,
|
|
"endpoint": f"https://inference.generativeai.{region}.oci.oraclecloud.com",
|
|
"serving_type": "ON_DEMAND",
|
|
"dedicated_endpoint_id": None,
|
|
"temperature": msg.temperature if msg.temperature is not None else 1.0,
|
|
"max_tokens": msg.max_tokens if msg.max_tokens is not None else 6000,
|
|
"top_p": msg.top_p if msg.top_p is not None else 0.95,
|
|
"top_k": msg.top_k if msg.top_k is not None else 1,
|
|
"frequency_penalty": msg.frequency_penalty if msg.frequency_penalty is not None else 0.0,
|
|
"presence_penalty": msg.presence_penalty if msg.presence_penalty is not None else 0.0,
|
|
}
|
|
|
|
if genai_cfg:
|
|
try:
|
|
history = []
|
|
with db() as c:
|
|
prev = c.execute("SELECT role,content FROM chat_messages WHERE session_id=? AND role IN ('user','assistant') ORDER BY created_at ASC", (sid,)).fetchall()
|
|
history = [{"role":r["role"],"content":r["content"]} for r in prev]
|
|
|
|
# ── RAG: augment with vector context if ADB config is active ──
|
|
rag_context = ""
|
|
adb_cfg = _get_active_adb_config(u["id"])
|
|
if adb_cfg:
|
|
try:
|
|
with db() as c:
|
|
emb_genai = c.execute("SELECT * FROM genai_configs WHERE id=?", (adb_cfg["genai_config_id"],)).fetchone()
|
|
if emb_genai:
|
|
emb_model = adb_cfg.get("embedding_model_id", "cohere.embed-v4.0")
|
|
query_embedding = _embed_text(msg.message, dict(emb_genai), emb_model)
|
|
documents = _vector_search(adb_cfg, query_embedding, top_k=5)
|
|
if documents:
|
|
rag_context = _build_rag_context(documents)
|
|
log.info(f"RAG: Retrieved {len(documents)} documents for query")
|
|
except Exception as e:
|
|
log.warning(f"RAG retrieval failed (non-fatal): {e}")
|
|
|
|
augmented_message = RAG_SYSTEM_PROMPT.format(context=rag_context, question=msg.message) if rag_context else msg.message
|
|
resp = _call_genai(dict(genai_cfg), augmented_message, history[:-1] if len(history) > 1 else None)
|
|
except Exception as e:
|
|
resp = f"❌ Erro GenAI: {str(e)[:400]}"
|
|
else:
|
|
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, mid))
|
|
return {"session_id": sid, "response": resp, "model_id": mid}
|
|
|
|
def _agent_respond(msg, user):
|
|
m = msg.lower().strip()
|
|
if any(k in m for k in ["status","health","como está","saúde"]):
|
|
cli = "✅ Instalado" if shutil.which("oci") else "❌ Não instalado"
|
|
with db() as c:
|
|
nc=c.execute("SELECT COUNT(*) n FROM oci_configs").fetchone()["n"]
|
|
nr=c.execute("SELECT COUNT(*) n FROM reports").fetchone()["n"]
|
|
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\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"]):
|
|
lines = ["🧠 **Modelos OCI Generative AI disponíveis:**\n"]
|
|
for mid, info in GENAI_MODELS.items():
|
|
lines.append(f"• `{mid}` — {info['name']} ({info['provider']})")
|
|
lines.append("\nConfigure em **GenAI Config** para usar no chat.")
|
|
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 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}")
|
|
async def clear_chat(sid, u=Depends(current_user)):
|
|
with db() as c: c.execute("DELETE FROM chat_messages WHERE session_id=? AND user_id=?", (sid, u["id"]))
|
|
return {"ok": True}
|
|
|
|
# ── Downloads ─────────────────────────────────────────────────────────────────
|
|
@app.get("/api/downloads")
|
|
async def list_dl(u=Depends(current_user)):
|
|
with db() as c:
|
|
q="SELECT id,tenancy_name,status,created_at,completed_at FROM reports WHERE status='completed'"
|
|
rows=c.execute(q+" ORDER BY created_at DESC").fetchall() if u["role"]=="admin" \
|
|
else c.execute(q+" AND user_id=? ORDER BY created_at DESC",(u["id"],)).fetchall()
|
|
res=[]
|
|
for r in rows:
|
|
d=dict(r); d["files"]=[]
|
|
rd=REPORTS/d["id"]
|
|
if rd.exists(): d["files"]=[{"name":f.name,"size":f.stat().st_size} for f in rd.iterdir() if f.is_file()]
|
|
res.append(d)
|
|
return res
|
|
|
|
@app.get("/api/downloads/{rid}/{fname}")
|
|
async def dl_file(rid, fname, u=Depends(current_user)):
|
|
p=REPORTS/rid/fname
|
|
if not p.exists(): raise HTTPException(404)
|
|
return FileResponse(p, filename=fname)
|
|
|
|
# ── Audit ─────────────────────────────────────────────────────────────────────
|
|
@app.get("/api/audit-log")
|
|
async def audit_log(limit:int=Query(100,le=500), u=Depends(require("admin"))):
|
|
with db() as c: rows=c.execute("SELECT * FROM audit_log ORDER BY created_at DESC LIMIT ?",(limit,)).fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
# ── Config Logs ───────────────────────────────────────────────────────────────
|
|
@app.get("/api/config-logs")
|
|
async def get_config_logs(
|
|
config_type: str = Query(None), config_id: str = Query(None),
|
|
severity: str = Query(None), limit: int = Query(50, le=200),
|
|
u=Depends(current_user)
|
|
):
|
|
query = "SELECT * FROM config_logs WHERE 1=1"
|
|
params = []
|
|
if config_type: query += " AND config_type=?"; params.append(config_type)
|
|
if config_id: query += " AND config_id=?"; params.append(config_id)
|
|
if severity: query += " AND severity=?"; params.append(severity)
|
|
if u["role"] != "admin": query += " AND user_id=?"; params.append(u["id"])
|
|
query += " ORDER BY created_at DESC LIMIT ?"
|
|
params.append(limit)
|
|
with db() as c: rows = c.execute(query, params).fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
@app.delete("/api/config-logs")
|
|
async def clear_config_logs(
|
|
config_type: str = Query(None), config_id: str = Query(None),
|
|
u=Depends(require("admin"))
|
|
):
|
|
query = "DELETE FROM config_logs WHERE 1=1"
|
|
params = []
|
|
if config_type: query += " AND config_type=?"; params.append(config_type)
|
|
if config_id: query += " AND config_id=?"; params.append(config_id)
|
|
with db() as c: c.execute(query, params)
|
|
return {"ok": True}
|
|
|
|
# ── Health ────────────────────────────────────────────────────────────────────
|
|
@app.get("/api/health")
|
|
async def health():
|
|
return {"status":"ok","ts":datetime.utcnow().isoformat(),"version":VERSION}
|
|
|
|
@app.on_event("startup")
|
|
async def startup():
|
|
init_db()
|
|
log.info(f"OCI CIS AI Agent v{VERSION} API started")
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|