feat: async background chat, performance scaling, and dead code cleanup
Async chat processing eliminates 504 timeouts - POST returns immediately, backend processes in background, frontend polls for results with timestamps. Scale to ~12 simultaneous chats via 8 uvicorn workers + 16-thread executor. Parallelized MCP data collection, 2h session cache, 5min tool timeout. Full dead code cleanup across backend, frontend, MCP, docker, and nginx.
This commit is contained in:
@@ -9,7 +9,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://img.shields.io/badge/version-1.9-C74634?style=flat-square" alt="Version">
|
<img src="https://img.shields.io/badge/version-2.0-C74634?style=flat-square" alt="Version">
|
||||||
<img src="https://img.shields.io/badge/python-3.12-3776AB?style=flat-square" alt="Python">
|
<img src="https://img.shields.io/badge/python-3.12-3776AB?style=flat-square" alt="Python">
|
||||||
<img src="https://img.shields.io/badge/FastAPI-0.115-009688?style=flat-square" alt="FastAPI">
|
<img src="https://img.shields.io/badge/FastAPI-0.115-009688?style=flat-square" alt="FastAPI">
|
||||||
<img src="https://img.shields.io/badge/OCI-GenAI-C74634?style=flat-square" alt="OCI">
|
<img src="https://img.shields.io/badge/OCI-GenAI-C74634?style=flat-square" alt="OCI">
|
||||||
@@ -35,7 +35,8 @@ The platform combines security compliance scanning, AI-powered chat with **RAG (
|
|||||||
- **MCP Tool Use (Function Calling)**: GenAI models can call tools from registered MCP servers during chat — supports both Cohere and Generic (OpenAI-style) function calling formats with automatic tool execution loop (max 5 iterations)
|
- **MCP Tool Use (Function Calling)**: GenAI models can call tools from registered MCP servers during chat — supports both Cohere and Generic (OpenAI-style) function calling formats with automatic tool execution loop (max 5 iterations)
|
||||||
- **Chat Memory Compaction**: automatic summarization of older messages when conversation exceeds ~8000 tokens — keeps 6 recent messages intact and generates an LLM-based summary of older context, similar to Claude Code's context compression
|
- **Chat Memory Compaction**: automatic summarization of older messages when conversation exceeds ~8000 tokens — keeps 6 recent messages intact and generates an LLM-based summary of older context, similar to Claude Code's context compression
|
||||||
- **Multimodal Chat**: upload images (PNG/JPG/GIF/WebP), PDFs, and text files directly in the chat for AI analysis — supports up to 5 files per message via OCI GenAI `ImageContent` and `DocumentContent`
|
- **Multimodal Chat**: upload images (PNG/JPG/GIF/WebP), PDFs, and text files directly in the chat for AI analysis — supports up to 5 files per message via OCI GenAI `ImageContent` and `DocumentContent`
|
||||||
- **Thinking Indicator**: button disables and shows spinner + "Pensando..." while waiting for GenAI response
|
- **Async Background Processing**: chat requests return immediately, GenAI + MCP tools process in background via dedicated thread pool (16 threads), frontend polls for results — eliminates 504 timeouts on long-running scans
|
||||||
|
- **Thinking Indicator**: button disables and shows spinner + "Pensando..." while waiting for GenAI response, with message timestamps
|
||||||
- 69 chat models + 11 embedding models across 6 providers: **Cohere**, **Meta**, **Google**, **OpenAI** (GPT-5.3/5.2/5.1/5/4.1/4o, Codex, Image, Audio, o1/o3/o4-mini, GPT-oss), **xAI** (Grok 4.1/4/3), **ProtectAI**
|
- 69 chat models + 11 embedding models across 6 providers: **Cohere**, **Meta**, **Google**, **OpenAI** (GPT-5.3/5.2/5.1/5/4.1/4o, Codex, Image, Audio, o1/o3/o4-mini, GPT-oss), **xAI** (Grok 4.1/4/3), **ProtectAI**
|
||||||
- OCID-based model resolution: catalog maps model IDs to OCI resource IDs per region for reliable API calls
|
- OCID-based model resolution: catalog maps model IDs to OCI resource IDs per region for reliable API calls
|
||||||
- 16 OCI regions supported with auto-generated endpoints
|
- 16 OCI regions supported with auto-generated endpoints
|
||||||
@@ -76,7 +77,8 @@ The platform combines security compliance scanning, AI-powered chat with **RAG (
|
|||||||
- `cis_get_scan_status` / `cis_invalidate_cache` — session status and cache management
|
- `cis_get_scan_status` / `cis_invalidate_cache` — session status and cache management
|
||||||
- **Per-section data collection**: each scan tool collects only the OCI data needed for that section, avoiding unnecessary API calls
|
- **Per-section data collection**: each scan tool collects only the OCI data needed for that section, avoiding unnecessary API calls
|
||||||
- **Region-specific scanning**: all scan tools accept optional `regions` parameter to target specific OCI regions (e.g., `["us-ashburn-1"]`) instead of scanning the entire tenancy
|
- **Region-specific scanning**: all scan tools accept optional `regions` parameter to target specific OCI regions (e.g., `["us-ashburn-1"]`) instead of scanning the entire tenancy
|
||||||
- **Session caching**: collected data is cached per config+regions scope, so subsequent scans on different sections reuse shared prerequisites (compartments, identity domains)
|
- **Session caching**: collected data is cached per config+regions scope (2-hour TTL), so subsequent scans on different sections reuse shared prerequisites (compartments, identity domains)
|
||||||
|
- **Parallelized data collection**: base collectors and regional collectors run in parallel thread pools (up to 8 workers), with 5-minute timeout per tool call
|
||||||
- Based on Oracle's official `cis_reports.py` (6660 lines, 48 CIS + 11 OBP checks)
|
- Based on Oracle's official `cis_reports.py` (6660 lines, 48 CIS + 11 OBP checks)
|
||||||
|
|
||||||
### 🔌 MCP Server Registry + Tool Discovery
|
### 🔌 MCP Server Registry + Tool Discovery
|
||||||
@@ -579,6 +581,7 @@ All models include OCID mapping for `us-ashburn-1`. For other regions, use the "
|
|||||||
|
|
||||||
| Version | Date | Changes |
|
| Version | Date | Changes |
|
||||||
|---------|------|---------|
|
|---------|------|---------|
|
||||||
|
| **v2.0** | 2026-03 | Async background chat processing (no more 504 timeouts), frontend polling with timestamps, 8 uvicorn workers + 16-thread chat executor for ~12 simultaneous chats, parallelized MCP data collection (5-thread base + 8-thread regional), 2-hour MCP session cache, 5-min tool timeout, full dead code cleanup across backend/frontend/MCP |
|
||||||
| **v1.9** | 2026-03 | Multimodal chat (image/PDF/text file upload with OCI GenAI ImageContent/DocumentContent), region-specific MCP scanning (`regions` param on all scan tools), orphaned report auto-detection on progress poll, nginx timeout increased to 15min, improved API error handling for non-JSON responses |
|
| **v1.9** | 2026-03 | Multimodal chat (image/PDF/text file upload with OCI GenAI ImageContent/DocumentContent), region-specific MCP scanning (`regions` param on all scan tools), orphaned report auto-detection on progress poll, nginx timeout increased to 15min, improved API error handling for non-JSON responses |
|
||||||
| **v1.8** | 2026-03 | CIS Engine auto-update from Oracle GitHub with automatic patch reapplication, version check UI card (admin), new `/api/cis-engine/*` endpoints, report file listing and individual download endpoints, reorganized Reports tab (execution history + status) and Downloads tab (file browser only with expandable cards per report), CIS Level description tooltip, persistent log expand during report generation |
|
| **v1.8** | 2026-03 | CIS Engine auto-update from Oracle GitHub with automatic patch reapplication, version check UI card (admin), new `/api/cis-engine/*` endpoints, report file listing and individual download endpoints, reorganized Reports tab (execution history + status) and Downloads tab (file browser only with expandable cards per report), CIS Level description tooltip, persistent log expand during report generation |
|
||||||
| **v1.7** | 2026-03 | Oracle official CIS report engine (replaces lightweight checker), granular report parameters (Level, OBP, Raw Data, Redact), per-report file storage with category browser, tenancy filter in Downloads, individual file download |
|
| **v1.7** | 2026-03 | Oracle official CIS report engine (replaces lightweight checker), granular report parameters (Level, OBP, Raw Data, Redact), per-report file storage with category browser, tenancy filter in Downloads, individual file download |
|
||||||
|
|||||||
@@ -22,4 +22,4 @@ RUN mkdir -p /data
|
|||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
|
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "8"]
|
||||||
|
|||||||
215
backend/app.py
215
backend/app.py
@@ -5,11 +5,12 @@ OCI Account Explorer, MCP Server registry with VectorDB tool integration,
|
|||||||
Autonomous DB vector storage, CIS reports, chat agent, audit log.
|
Autonomous DB vector storage, CIS reports, chat agent, audit log.
|
||||||
"""
|
"""
|
||||||
import os, json, uuid, hashlib, hmac, time, base64, struct, secrets, subprocess
|
import os, json, uuid, hashlib, hmac, time, base64, struct, secrets, subprocess
|
||||||
import shutil, asyncio, sqlite3, logging, socket, re
|
import shutil, asyncio, sqlite3, logging, re, concurrent.futures
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, List, Dict, Any
|
from typing import Optional, List, Dict, Any
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
FastAPI, HTTPException, Depends, Request, UploadFile, File, Form,
|
FastAPI, HTTPException, Depends, Request, UploadFile, File, Form,
|
||||||
@@ -37,6 +38,7 @@ for d in [DATA, OCI_DIR, REPORTS, MCP_DIR, WALLET_DIR]:
|
|||||||
d.mkdir(parents=True, exist_ok=True)
|
d.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
_running_reports: dict[str, asyncio.subprocess.Process] = {} # rid → subprocess
|
_running_reports: dict[str, asyncio.subprocess.Process] = {} # rid → subprocess
|
||||||
|
_chat_executor = concurrent.futures.ThreadPoolExecutor(max_workers=16, thread_name_prefix="chat")
|
||||||
|
|
||||||
# ── Chat Memory Compaction Settings ──
|
# ── Chat Memory Compaction Settings ──
|
||||||
COMPACT_TOKEN_THRESHOLD = 8000 # estimated tokens before triggering compaction
|
COMPACT_TOKEN_THRESHOLD = 8000 # estimated tokens before triggering compaction
|
||||||
@@ -199,9 +201,10 @@ OCI_REGIONS = {
|
|||||||
# ── Database ──────────────────────────────────────────────────────────────────
|
# ── Database ──────────────────────────────────────────────────────────────────
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def db():
|
def db():
|
||||||
conn = sqlite3.connect(str(DB_PATH))
|
conn = sqlite3.connect(str(DB_PATH), timeout=30, check_same_thread=False)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
conn.execute("PRAGMA journal_mode=WAL")
|
conn.execute("PRAGMA journal_mode=WAL")
|
||||||
|
conn.execute("PRAGMA busy_timeout=30000")
|
||||||
conn.execute("PRAGMA foreign_keys=ON")
|
conn.execute("PRAGMA foreign_keys=ON")
|
||||||
try:
|
try:
|
||||||
yield conn
|
yield conn
|
||||||
@@ -379,6 +382,11 @@ def init_db():
|
|||||||
c.execute(f"ALTER TABLE reports ADD COLUMN {col}")
|
c.execute(f"ALTER TABLE reports ADD COLUMN {col}")
|
||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
pass
|
pass
|
||||||
|
for col in ["status TEXT DEFAULT 'done'"]:
|
||||||
|
try:
|
||||||
|
c.execute(f"ALTER TABLE chat_messages ADD COLUMN {col}")
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass
|
||||||
# Migrate legacy table_name from adb_vector_configs into adb_vector_tables
|
# Migrate legacy table_name from adb_vector_configs into adb_vector_tables
|
||||||
try:
|
try:
|
||||||
for cfg_row in c.execute("SELECT id, table_name FROM adb_vector_configs WHERE table_name IS NOT NULL AND table_name != ''").fetchall():
|
for cfg_row in c.execute("SELECT id, table_name FROM adb_vector_configs WHERE table_name IS NOT NULL AND table_name != ''").fetchall():
|
||||||
@@ -496,10 +504,6 @@ class GenAIConfigReq(BaseModel):
|
|||||||
temperature: float = 1; max_tokens: int = 6000; top_p: float = 0.95
|
temperature: float = 1; max_tokens: int = 6000; top_p: float = 0.95
|
||||||
top_k: int = 1; frequency_penalty: float = 0; presence_penalty: float = 0
|
top_k: int = 1; frequency_penalty: float = 0; presence_penalty: float = 0
|
||||||
is_default: bool = False
|
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):
|
class IngestDocReq(BaseModel):
|
||||||
adb_config_id: str; documents: List[Dict[str, Any]]; table_name: Optional[str] = None
|
adb_config_id: str; documents: List[Dict[str, Any]]; table_name: Optional[str] = None
|
||||||
class MCPServerReq(BaseModel):
|
class MCPServerReq(BaseModel):
|
||||||
@@ -1035,7 +1039,7 @@ def _call_genai(gc: dict, message: str, history: list = None, tools: list = None
|
|||||||
config=config,
|
config=config,
|
||||||
service_endpoint=endpoint,
|
service_endpoint=endpoint,
|
||||||
retry_strategy=oci.retry.NoneRetryStrategy(),
|
retry_strategy=oci.retry.NoneRetryStrategy(),
|
||||||
timeout=(10, 240)
|
timeout=(10, 600)
|
||||||
)
|
)
|
||||||
|
|
||||||
# System prompt
|
# System prompt
|
||||||
@@ -1733,8 +1737,7 @@ async def update_adb(
|
|||||||
_config_log("adb", vid, config_name, "success", "save", f"Conexão atualizada: {config_name} ({dsn})", u["id"], u["username"])
|
_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}
|
return {"id": vid, "config_name": config_name}
|
||||||
|
|
||||||
import re as _re
|
_TABLE_NAME_RE = re.compile(r'^[A-Z][A-Z0-9_]{0,127}$')
|
||||||
_TABLE_NAME_RE = _re.compile(r'^[A-Z][A-Z0-9_]{0,127}$')
|
|
||||||
|
|
||||||
@app.get("/api/adb/{vid}/tables")
|
@app.get("/api/adb/{vid}/tables")
|
||||||
async def list_adb_tables(vid: str, u=Depends(current_user)):
|
async def list_adb_tables(vid: str, u=Depends(current_user)):
|
||||||
@@ -1917,12 +1920,15 @@ def _get_adb_and_genai(vid: str):
|
|||||||
async def preview_report_chunks(rid: str, u=Depends(current_user)):
|
async def preview_report_chunks(rid: str, u=Depends(current_user)):
|
||||||
"""Preview the chunks that will be generated from a CIS report before embedding."""
|
"""Preview the chunks that will be generated from a CIS report before embedding."""
|
||||||
with db() as c:
|
with db() as c:
|
||||||
r = c.execute("SELECT report_data,tenancy_name FROM reports WHERE id=? AND status='completed'", (rid,)).fetchone()
|
r = c.execute("SELECT json_path,tenancy_name FROM reports WHERE id=? AND status='completed'", (rid,)).fetchone()
|
||||||
if not r: raise HTTPException(404, "Report not found or not completed")
|
if not r: raise HTTPException(404, "Report not found or not completed")
|
||||||
report_data = r["report_data"]
|
json_path = r["json_path"]
|
||||||
if isinstance(report_data, str):
|
if not json_path or not Path(json_path).exists():
|
||||||
try: report_data = json.loads(report_data)
|
raise HTTPException(400, "Report JSON file not found")
|
||||||
except: raise HTTPException(400, "Invalid report data")
|
try:
|
||||||
|
report_data = json.loads(Path(json_path).read_text())
|
||||||
|
except Exception:
|
||||||
|
raise HTTPException(400, "Invalid report data")
|
||||||
documents = _chunk_report_by_section(report_data)
|
documents = _chunk_report_by_section(report_data)
|
||||||
return {"tenancy": report_data.get("tenancy", "unknown"),
|
return {"tenancy": report_data.get("tenancy", "unknown"),
|
||||||
"regions": report_data.get("regions", []),
|
"regions": report_data.get("regions", []),
|
||||||
@@ -1935,12 +1941,15 @@ async def embed_report(rid: str, req: dict, bg: BackgroundTasks, u=Depends(requi
|
|||||||
vid = req.get("adb_config_id")
|
vid = req.get("adb_config_id")
|
||||||
if not vid: raise HTTPException(400, "adb_config_id is required")
|
if not vid: raise HTTPException(400, "adb_config_id is required")
|
||||||
with db() as c:
|
with db() as c:
|
||||||
r = c.execute("SELECT report_data,tenancy_name FROM reports WHERE id=? AND status='completed'", (rid,)).fetchone()
|
r = c.execute("SELECT json_path,tenancy_name FROM reports WHERE id=? AND status='completed'", (rid,)).fetchone()
|
||||||
if not r: raise HTTPException(404, "Report not found or not completed")
|
if not r: raise HTTPException(404, "Report not found or not completed")
|
||||||
report_data = r["report_data"]
|
json_path = r["json_path"]
|
||||||
if isinstance(report_data, str):
|
if not json_path or not Path(json_path).exists():
|
||||||
try: report_data = json.loads(report_data)
|
raise HTTPException(400, "Report JSON file not found")
|
||||||
except: raise HTTPException(400, "Invalid report data")
|
try:
|
||||||
|
report_data = json.loads(Path(json_path).read_text())
|
||||||
|
except Exception:
|
||||||
|
raise HTTPException(400, "Invalid report data")
|
||||||
documents = _chunk_report_by_section(report_data)
|
documents = _chunk_report_by_section(report_data)
|
||||||
if not documents: raise HTTPException(400, "No sections found in report")
|
if not documents: raise HTTPException(400, "No sections found in report")
|
||||||
cfg, gc = _get_adb_and_genai(vid)
|
cfg, gc = _get_adb_and_genai(vid)
|
||||||
@@ -2150,10 +2159,7 @@ async def get_report(rid, u=Depends(current_user)):
|
|||||||
with db() as c: r=c.execute("SELECT * FROM reports WHERE id=?",(rid,)).fetchone()
|
with db() as c: r=c.execute("SELECT * FROM reports WHERE id=?",(rid,)).fetchone()
|
||||||
if not r: raise HTTPException(404)
|
if not r: raise HTTPException(404)
|
||||||
if u["role"]!="admin" and r["user_id"]!=u["id"]: raise HTTPException(403)
|
if u["role"]!="admin" and r["user_id"]!=u["id"]: raise HTTPException(403)
|
||||||
d=dict(r)
|
return dict(r)
|
||||||
d.pop("report_data", None)
|
|
||||||
d.pop("mcp_server_id", None)
|
|
||||||
return d
|
|
||||||
|
|
||||||
@app.get("/api/reports/{rid}/files")
|
@app.get("/api/reports/{rid}/files")
|
||||||
async def list_report_files(rid: str, u=Depends(current_user)):
|
async def list_report_files(rid: str, u=Depends(current_user)):
|
||||||
@@ -2196,12 +2202,13 @@ async def report_dl(rid, fmt: str = Query("json"), u=Depends(current_user)):
|
|||||||
# ── Chat Agent ────────────────────────────────────────────────────────────────
|
# ── Chat Agent ────────────────────────────────────────────────────────────────
|
||||||
# (endpoints defined below _chat_core, after _agent_respond)
|
# (endpoints defined below _chat_core, after _agent_respond)
|
||||||
|
|
||||||
async def _chat_core_impl(msg: ChatMsg, u, attachments: list = None):
|
def _chat_start(msg: ChatMsg, u, attachments: list = None):
|
||||||
"""Internal chat implementation — called by both /api/chat and /api/chat/upload."""
|
"""Start a chat: save user msg, resolve config, return (sid, mid, genai_cfg) or immediate response.
|
||||||
|
If genai_cfg is None, returns immediate fallback response in mid field as dict."""
|
||||||
sid = msg.session_id or str(uuid.uuid4())
|
sid = msg.session_id or str(uuid.uuid4())
|
||||||
with db() as c:
|
with db() as c:
|
||||||
c.execute("INSERT INTO chat_messages (id,session_id,user_id,role,content,model_id) VALUES (?,?,?,?,?,?)",
|
c.execute("INSERT INTO chat_messages (id,session_id,user_id,role,content,model_id,status) VALUES (?,?,?,?,?,?,?)",
|
||||||
(str(uuid.uuid4()), sid, u["id"], "user", msg.message, None))
|
(str(uuid.uuid4()), sid, u["id"], "user", msg.message, None, "done"))
|
||||||
|
|
||||||
genai_cfg = None
|
genai_cfg = None
|
||||||
if msg.genai_config_id:
|
if msg.genai_config_id:
|
||||||
@@ -2209,7 +2216,6 @@ async def _chat_core_impl(msg: ChatMsg, u, attachments: list = None):
|
|||||||
row = c.execute("SELECT * FROM genai_configs WHERE id=?", (msg.genai_config_id,)).fetchone()
|
row = c.execute("SELECT * FROM genai_configs WHERE id=?", (msg.genai_config_id,)).fetchone()
|
||||||
if row:
|
if row:
|
||||||
genai_cfg = dict(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.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.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_p is not None: genai_cfg["top_p"] = msg.top_p
|
||||||
@@ -2217,7 +2223,6 @@ async def _chat_core_impl(msg: ChatMsg, u, attachments: list = None):
|
|||||||
if msg.frequency_penalty is not None: genai_cfg["frequency_penalty"] = msg.frequency_penalty
|
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
|
if msg.presence_penalty is not None: genai_cfg["presence_penalty"] = msg.presence_penalty
|
||||||
elif msg.model_id and msg.oci_config_id:
|
elif msg.model_id and msg.oci_config_id:
|
||||||
# Direct model mode: build synthetic config dict
|
|
||||||
with db() as c:
|
with db() as c:
|
||||||
oci_row = c.execute("SELECT * FROM oci_configs WHERE id=?", (msg.oci_config_id,)).fetchone()
|
oci_row = c.execute("SELECT * FROM oci_configs WHERE id=?", (msg.oci_config_id,)).fetchone()
|
||||||
if not oci_row:
|
if not oci_row:
|
||||||
@@ -2243,16 +2248,34 @@ async def _chat_core_impl(msg: ChatMsg, u, attachments: list = None):
|
|||||||
"presence_penalty": msg.presence_penalty if msg.presence_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:
|
if not genai_cfg:
|
||||||
|
# No GenAI config — return immediate fallback
|
||||||
|
resp = _agent_respond(msg.message, u)
|
||||||
|
with db() as c:
|
||||||
|
c.execute("INSERT INTO chat_messages (id,session_id,user_id,role,content,model_id,status) VALUES (?,?,?,?,?,?,?)",
|
||||||
|
(str(uuid.uuid4()), sid, u["id"], "assistant", resp, None, "done"))
|
||||||
|
return sid, {"session_id": sid, "response": resp, "model_id": None, "status": "done"}, None
|
||||||
|
|
||||||
|
# Create placeholder assistant message for background processing
|
||||||
|
mid = str(uuid.uuid4())
|
||||||
|
with db() as c:
|
||||||
|
c.execute("INSERT INTO chat_messages (id,session_id,user_id,role,content,model_id,status) VALUES (?,?,?,?,?,?,?)",
|
||||||
|
(mid, sid, u["id"], "assistant", "", genai_cfg.get("model_id"), "processing"))
|
||||||
|
return sid, mid, genai_cfg
|
||||||
|
|
||||||
|
|
||||||
|
async def _chat_background(mid: str, sid: str, msg: ChatMsg, user: dict, genai_cfg: dict, attachments: list = None):
|
||||||
|
"""Background worker — processes GenAI chat, updates DB when done."""
|
||||||
|
log.info(f"Chat background started: mid={mid}, sid={sid}")
|
||||||
try:
|
try:
|
||||||
history = []
|
history = []
|
||||||
with db() as c:
|
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()
|
prev = c.execute("SELECT role,content FROM chat_messages WHERE session_id=? AND role IN ('user','assistant') AND status='done' ORDER BY created_at ASC", (sid,)).fetchall()
|
||||||
history = [{"role":r["role"],"content":r["content"]} for r in prev]
|
history = [{"role":r["role"],"content":r["content"]} for r in prev]
|
||||||
|
|
||||||
# ── RAG: augment with vector context from ALL active ADB configs ──
|
# ── RAG: augment with vector context from ALL active ADB configs ──
|
||||||
rag_context = ""
|
rag_context = ""
|
||||||
adb_cfgs = _get_active_adb_configs(u["id"])
|
adb_cfgs = _get_active_adb_configs(user["id"])
|
||||||
if adb_cfgs:
|
if adb_cfgs:
|
||||||
all_documents = []
|
all_documents = []
|
||||||
for adb_cfg in adb_cfgs:
|
for adb_cfg in adb_cfgs:
|
||||||
@@ -2282,11 +2305,9 @@ async def _chat_core_impl(msg: ChatMsg, u, attachments: list = None):
|
|||||||
rag_context = _build_rag_context(all_documents[:10])
|
rag_context = _build_rag_context(all_documents[:10])
|
||||||
|
|
||||||
cfg_dict = dict(genai_cfg)
|
cfg_dict = dict(genai_cfg)
|
||||||
# Global system prompt from system_prompts table
|
|
||||||
with db() as c:
|
with db() as c:
|
||||||
sp_row = c.execute("SELECT content FROM system_prompts WHERE agent='chat' AND is_active=1 LIMIT 1").fetchone()
|
sp_row = c.execute("SELECT content FROM system_prompts WHERE agent='chat' AND is_active=1 LIMIT 1").fetchone()
|
||||||
global_prompt = sp_row["content"] if sp_row and sp_row["content"] else ""
|
global_prompt = sp_row["content"] if sp_row and sp_row["content"] else ""
|
||||||
# If RAG context found, wrap user message with context
|
|
||||||
if rag_context:
|
if rag_context:
|
||||||
augmented_message = RAG_CONTEXT_TEMPLATE.format(context=rag_context, question=msg.message)
|
augmented_message = RAG_CONTEXT_TEMPLATE.format(context=rag_context, question=msg.message)
|
||||||
cfg_dict["system_prompt"] = global_prompt or RAG_DEFAULT_SYSTEM_PROMPT
|
cfg_dict["system_prompt"] = global_prompt or RAG_DEFAULT_SYSTEM_PROMPT
|
||||||
@@ -2294,28 +2315,28 @@ async def _chat_core_impl(msg: ChatMsg, u, attachments: list = None):
|
|||||||
augmented_message = msg.message
|
augmented_message = msg.message
|
||||||
if global_prompt:
|
if global_prompt:
|
||||||
cfg_dict["system_prompt"] = global_prompt
|
cfg_dict["system_prompt"] = global_prompt
|
||||||
# Collect MCP tools if enabled
|
|
||||||
mcp_tools = []
|
mcp_tools = []
|
||||||
tool_defs = None
|
tool_defs = None
|
||||||
if msg.use_tools:
|
if msg.use_tools:
|
||||||
mcp_tools = _get_active_mcp_tools(u["id"])
|
mcp_tools = _get_active_mcp_tools(user["id"])
|
||||||
if mcp_tools:
|
if mcp_tools:
|
||||||
tool_defs = [t["tool"] for t in mcp_tools]
|
tool_defs = [t["tool"] for t in mcp_tools]
|
||||||
log.info(f"Chat with {len(tool_defs)} MCP tools available")
|
log.info(f"Chat with {len(tool_defs)} MCP tools available")
|
||||||
|
|
||||||
# ── Memory compaction: summarize old messages if history is too long ──
|
|
||||||
if history and _should_compact(history):
|
if history and _should_compact(history):
|
||||||
log.info(f"Compaction triggered: {len(history)} msgs, ~{_estimate_history_tokens(history)} est tokens")
|
log.info(f"Compaction triggered: {len(history)} msgs, ~{_estimate_history_tokens(history)} est tokens")
|
||||||
history = _compact_history(sid, u["id"], cfg_dict, history)
|
history = _compact_history(sid, user["id"], cfg_dict, history)
|
||||||
log.info(f"Post-compaction: {len(history)} msgs, ~{_estimate_history_tokens(history)} est tokens")
|
log.info(f"Post-compaction: {len(history)} msgs, ~{_estimate_history_tokens(history)} est tokens")
|
||||||
|
|
||||||
hist = history[:-1] if len(history) > 1 else None
|
hist = history[:-1] if len(history) > 1 else None
|
||||||
resp_text, tool_calls, tool_calls_raw = _call_genai(cfg_dict, augmented_message, hist, tools=tool_defs, attachments=attachments)
|
loop = asyncio.get_event_loop()
|
||||||
|
resp_text, tool_calls, tool_calls_raw = await loop.run_in_executor(
|
||||||
|
_chat_executor, partial(_call_genai, cfg_dict, augmented_message, hist,
|
||||||
|
tool_defs, None, None, attachments))
|
||||||
|
|
||||||
# Tool use loop (max 5 iterations)
|
|
||||||
# Accumulate extra_messages so the model sees the full tool use conversation
|
|
||||||
all_tool_results = []
|
all_tool_results = []
|
||||||
accumulated_msgs = [] # raw OCI SDK message objects for Generic format
|
accumulated_msgs = []
|
||||||
iterations = 0
|
iterations = 0
|
||||||
api_format = GENAI_MODELS.get(genai_cfg.get("model_id", ""), {}).get("api_format", "GENERIC")
|
api_format = GENAI_MODELS.get(genai_cfg.get("model_id", ""), {}).get("api_format", "GENERIC")
|
||||||
while tool_calls and iterations < 5:
|
while tool_calls and iterations < 5:
|
||||||
@@ -2336,7 +2357,6 @@ async def _chat_core_impl(msg: ChatMsg, u, attachments: list = None):
|
|||||||
iteration_results.append({"tool_call_id": tc["id"], "name": tc["name"], "content": result})
|
iteration_results.append({"tool_call_id": tc["id"], "name": tc["name"], "content": result})
|
||||||
all_tool_results.extend(iteration_results)
|
all_tool_results.extend(iteration_results)
|
||||||
|
|
||||||
# Build tool results in the appropriate format and call again
|
|
||||||
if api_format == "COHERE":
|
if api_format == "COHERE":
|
||||||
import oci
|
import oci
|
||||||
cohere_results = []
|
cohere_results = []
|
||||||
@@ -2348,12 +2368,11 @@ async def _chat_core_impl(msg: ChatMsg, u, attachments: list = None):
|
|||||||
tr_obj.call = tc_obj
|
tr_obj.call = tc_obj
|
||||||
tr_obj.outputs = [{"result": tr["content"]}]
|
tr_obj.outputs = [{"result": tr["content"]}]
|
||||||
cohere_results.append(tr_obj)
|
cohere_results.append(tr_obj)
|
||||||
resp_text, tool_calls, tool_calls_raw = _call_genai(
|
resp_text, tool_calls, tool_calls_raw = await loop.run_in_executor(
|
||||||
cfg_dict, augmented_message, hist, tools=tool_defs,
|
_chat_executor, partial(_call_genai, cfg_dict, augmented_message, hist,
|
||||||
tool_results_cohere=cohere_results)
|
tool_defs, cohere_results))
|
||||||
else:
|
else:
|
||||||
import oci
|
import oci
|
||||||
# Accumulate: add assistant message with tool_calls + tool result messages
|
|
||||||
assistant_msg = oci.generative_ai_inference.models.AssistantMessage()
|
assistant_msg = oci.generative_ai_inference.models.AssistantMessage()
|
||||||
assistant_msg.tool_calls = tool_calls_raw
|
assistant_msg.tool_calls = tool_calls_raw
|
||||||
accumulated_msgs.append(assistant_msg)
|
accumulated_msgs.append(assistant_msg)
|
||||||
@@ -2364,27 +2383,23 @@ async def _chat_core_impl(msg: ChatMsg, u, attachments: list = None):
|
|||||||
tool_content.text = tr["content"]
|
tool_content.text = tr["content"]
|
||||||
tool_msg.content = [tool_content]
|
tool_msg.content = [tool_content]
|
||||||
accumulated_msgs.append(tool_msg)
|
accumulated_msgs.append(tool_msg)
|
||||||
|
resp_text, tool_calls, tool_calls_raw = await loop.run_in_executor(
|
||||||
resp_text, tool_calls, tool_calls_raw = _call_genai(
|
_chat_executor, partial(_call_genai, cfg_dict, augmented_message, hist,
|
||||||
cfg_dict, augmented_message, hist, tools=tool_defs,
|
tool_defs, None, accumulated_msgs))
|
||||||
extra_messages=accumulated_msgs)
|
|
||||||
|
|
||||||
resp = resp_text
|
resp = resp_text
|
||||||
except Exception as e:
|
|
||||||
resp = f"❌ Erro GenAI: {str(e)[:400]}"
|
|
||||||
all_tool_results = []
|
|
||||||
else:
|
|
||||||
resp = _agent_respond(msg.message, u)
|
|
||||||
all_tool_results = []
|
|
||||||
|
|
||||||
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))
|
|
||||||
result = {"session_id": sid, "response": resp, "model_id": mid}
|
|
||||||
if all_tool_results:
|
if all_tool_results:
|
||||||
result["tools_used"] = [{"name": tr["name"], "result_preview": tr["content"][:200]} for tr in all_tool_results]
|
tools_info = '\n\n🔧 **Tools utilizadas:** ' + ', '.join(tr["name"] for tr in all_tool_results)
|
||||||
return result
|
resp += tools_info
|
||||||
|
|
||||||
|
with db() as c:
|
||||||
|
c.execute("UPDATE chat_messages SET content=?, status='done' WHERE id=?", (resp, mid))
|
||||||
|
log.info(f"Chat {mid} completed successfully")
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Chat {mid} failed: {e}")
|
||||||
|
with db() as c:
|
||||||
|
c.execute("UPDATE chat_messages SET content=?, status='failed' WHERE id=?",
|
||||||
|
(f"❌ Erro GenAI: {str(e)[:400]}", mid))
|
||||||
|
|
||||||
MAX_UPLOAD_FILES = 5
|
MAX_UPLOAD_FILES = 5
|
||||||
MAX_UPLOAD_SIZE = 10 * 1024 * 1024 # 10MB
|
MAX_UPLOAD_SIZE = 10 * 1024 * 1024 # 10MB
|
||||||
@@ -2394,6 +2409,7 @@ DOC_MIMES = {"application/pdf"}
|
|||||||
|
|
||||||
@app.post("/api/chat/upload")
|
@app.post("/api/chat/upload")
|
||||||
async def chat_with_files(
|
async def chat_with_files(
|
||||||
|
bg: BackgroundTasks,
|
||||||
message: str = Form(""),
|
message: str = Form(""),
|
||||||
session_id: Optional[str] = Form(None),
|
session_id: Optional[str] = Form(None),
|
||||||
genai_config_id: Optional[str] = Form(None),
|
genai_config_id: Optional[str] = Form(None),
|
||||||
@@ -2414,7 +2430,6 @@ async def chat_with_files(
|
|||||||
if len(files) > MAX_UPLOAD_FILES:
|
if len(files) > MAX_UPLOAD_FILES:
|
||||||
raise HTTPException(400, f"Máximo {MAX_UPLOAD_FILES} arquivos por mensagem")
|
raise HTTPException(400, f"Máximo {MAX_UPLOAD_FILES} arquivos por mensagem")
|
||||||
|
|
||||||
# Process uploaded files into attachments
|
|
||||||
attachments = []
|
attachments = []
|
||||||
file_names = []
|
file_names = []
|
||||||
for f in files:
|
for f in files:
|
||||||
@@ -2429,7 +2444,6 @@ async def chat_with_files(
|
|||||||
elif mime in DOC_MIMES:
|
elif mime in DOC_MIMES:
|
||||||
attachments.append({"type": "document", "mime": mime, "data_uri": data_uri})
|
attachments.append({"type": "document", "mime": mime, "data_uri": data_uri})
|
||||||
else:
|
else:
|
||||||
# Text-based files: read content and append to message
|
|
||||||
try:
|
try:
|
||||||
text = data.decode("utf-8", errors="replace")
|
text = data.decode("utf-8", errors="replace")
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -2437,7 +2451,6 @@ async def chat_with_files(
|
|||||||
message = f"{message}\n\n--- Conteúdo de {f.filename} ---\n{text[:50000]}"
|
message = f"{message}\n\n--- Conteúdo de {f.filename} ---\n{text[:50000]}"
|
||||||
file_names.append(f.filename)
|
file_names.append(f.filename)
|
||||||
|
|
||||||
# Build a synthetic ChatMsg and delegate to the chat logic
|
|
||||||
msg = ChatMsg(
|
msg = ChatMsg(
|
||||||
message=message or "Analise os arquivos anexados.",
|
message=message or "Analise os arquivos anexados.",
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
@@ -2455,19 +2468,35 @@ async def chat_with_files(
|
|||||||
presence_penalty=presence_penalty,
|
presence_penalty=presence_penalty,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Store attachments in request state for _call_genai
|
sid, mid_or_result, genai_cfg = _chat_start(msg, u, attachments=attachments if attachments else None)
|
||||||
result = await _chat_core_impl(msg, u, attachments=attachments if attachments else None)
|
if genai_cfg is None:
|
||||||
|
return mid_or_result # immediate fallback response
|
||||||
|
|
||||||
if file_names:
|
if file_names:
|
||||||
# Save file reference in user message
|
|
||||||
with db() as c:
|
with db() as c:
|
||||||
c.execute("UPDATE chat_messages SET content = content || ? WHERE session_id=? AND role='user' ORDER BY created_at DESC LIMIT 1",
|
c.execute("UPDATE chat_messages SET content = content || ? WHERE session_id=? AND role='user' AND status='done' ORDER BY created_at DESC LIMIT 1",
|
||||||
(f"\n[📎 {', '.join(file_names)}]", result["session_id"]))
|
(f"\n[📎 {', '.join(file_names)}]", sid))
|
||||||
return result
|
|
||||||
|
bg.add_task(_chat_background, mid_or_result, sid, msg, dict(u), genai_cfg, attachments if attachments else None)
|
||||||
|
return {"message_id": mid_or_result, "session_id": sid, "status": "processing"}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/chat")
|
@app.post("/api/chat")
|
||||||
async def chat(msg: ChatMsg, u=Depends(current_user)):
|
async def chat(msg: ChatMsg, bg: BackgroundTasks, u=Depends(current_user)):
|
||||||
return await _chat_core_impl(msg, u)
|
sid, mid_or_result, genai_cfg = _chat_start(msg, u)
|
||||||
|
if genai_cfg is None:
|
||||||
|
return mid_or_result # immediate fallback response
|
||||||
|
bg.add_task(_chat_background, mid_or_result, sid, msg, dict(u), genai_cfg)
|
||||||
|
return {"message_id": mid_or_result, "session_id": sid, "status": "processing"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/chat/{mid}/status")
|
||||||
|
async def chat_message_status(mid: str, u=Depends(current_user)):
|
||||||
|
with db() as c:
|
||||||
|
r = c.execute("SELECT id, session_id, role, content, model_id, status FROM chat_messages WHERE id=?", (mid,)).fetchone()
|
||||||
|
if not r:
|
||||||
|
raise HTTPException(404, "Message not found")
|
||||||
|
return dict(r)
|
||||||
|
|
||||||
|
|
||||||
def _agent_respond(msg, user):
|
def _agent_respond(msg, user):
|
||||||
@@ -2499,27 +2528,6 @@ 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"]))
|
with db() as c: c.execute("DELETE FROM chat_messages WHERE session_id=? AND user_id=?", (sid, u["id"]))
|
||||||
return {"ok": True}
|
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 ─────────────────────────────────────────────────────────────────────
|
# ── Audit ─────────────────────────────────────────────────────────────────────
|
||||||
@app.get("/api/audit-log")
|
@app.get("/api/audit-log")
|
||||||
async def audit_log(limit:int=Query(100,le=500), u=Depends(require("admin"))):
|
async def audit_log(limit:int=Query(100,le=500), u=Depends(require("admin"))):
|
||||||
@@ -2544,19 +2552,6 @@ async def get_config_logs(
|
|||||||
with db() as c: rows = c.execute(query, params).fetchall()
|
with db() as c: rows = c.execute(query, params).fetchall()
|
||||||
return [dict(r) for r in rows]
|
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 Settings ──────────────────────────────────────────────────────────────
|
# ── App Settings ──────────────────────────────────────────────────────────────
|
||||||
@app.get("/api/settings/{key}")
|
@app.get("/api/settings/{key}")
|
||||||
async def get_setting(key: str, u=Depends(current_user)):
|
async def get_setting(key: str, u=Depends(current_user)):
|
||||||
|
|||||||
@@ -11,13 +11,10 @@ import concurrent.futures
|
|||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
from mcp.server import Server
|
from mcp.server import Server
|
||||||
from mcp.server.stdio import stdio_server
|
from mcp.server.stdio import stdio_server
|
||||||
from mcp.types import Tool, TextContent
|
from mcp.types import Tool, TextContent
|
||||||
@@ -27,7 +24,8 @@ server = Server("cis-compliance")
|
|||||||
|
|
||||||
# session_key -> {checker, sections_collected: set, sections_analyzed: set, ...}
|
# session_key -> {checker, sections_collected: set, sections_analyzed: set, ...}
|
||||||
_sessions: dict = {}
|
_sessions: dict = {}
|
||||||
SESSION_TTL = 1800
|
SESSION_TTL = 7200 # 2 hours — reduce cold starts
|
||||||
|
TOOL_TIMEOUT = 300 # 5 min max per tool call
|
||||||
|
|
||||||
|
|
||||||
def _session_key(config_id: str, regions: list[str] | None = None) -> str:
|
def _session_key(config_id: str, regions: list[str] | None = None) -> str:
|
||||||
@@ -42,15 +40,6 @@ OCI_CONFIGS_DIR = os.environ.get("OCI_CONFIGS_DIR", "/data/oci_configs")
|
|||||||
# Per-section data collectors: which private methods to call
|
# Per-section data collectors: which private methods to call
|
||||||
# ──────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# Home-region functions (run once, needed by most sections)
|
|
||||||
HOME_BASE = [
|
|
||||||
"_CIS_Report__identity_read_compartments",
|
|
||||||
"_CIS_Report__cloud_guard_read_cloud_guard_configuration",
|
|
||||||
"_CIS_Report__identity_read_domains",
|
|
||||||
"_CIS_Report__identity_read_groups",
|
|
||||||
"_CIS_Report__identity_read_availability_domains",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Section -> (home_region_collectors, regional_collectors, analysis_methods)
|
# Section -> (home_region_collectors, regional_collectors, analysis_methods)
|
||||||
SECTION_DEPS = {
|
SECTION_DEPS = {
|
||||||
"iam": {
|
"iam": {
|
||||||
@@ -217,12 +206,20 @@ def _ensure_base(session):
|
|||||||
if session["base_collected"]:
|
if session["base_collected"]:
|
||||||
return
|
return
|
||||||
checker = session["checker"]
|
checker = session["checker"]
|
||||||
# Sequential: compartments + cloud_guard first, then domains, then groups
|
base_methods = [
|
||||||
getattr(checker, "_CIS_Report__identity_read_compartments")()
|
"_CIS_Report__identity_read_compartments",
|
||||||
getattr(checker, "_CIS_Report__cloud_guard_read_cloud_guard_configuration")()
|
"_CIS_Report__cloud_guard_read_cloud_guard_configuration",
|
||||||
getattr(checker, "_CIS_Report__identity_read_domains")()
|
"_CIS_Report__identity_read_domains",
|
||||||
getattr(checker, "_CIS_Report__identity_read_groups")()
|
"_CIS_Report__identity_read_groups",
|
||||||
getattr(checker, "_CIS_Report__identity_read_availability_domains")()
|
"_CIS_Report__identity_read_availability_domains",
|
||||||
|
]
|
||||||
|
def _run_base(fn_name):
|
||||||
|
try:
|
||||||
|
getattr(checker, fn_name)()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: {fn_name} failed: {e}")
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as ex:
|
||||||
|
list(ex.map(_run_base, base_methods))
|
||||||
session["base_collected"] = True
|
session["base_collected"] = True
|
||||||
|
|
||||||
|
|
||||||
@@ -236,23 +233,21 @@ def _collect_section(session, section: str):
|
|||||||
if not deps:
|
if not deps:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Home region collectors (sequential for safety)
|
def _run_method(fn_name):
|
||||||
for method_name in deps["home"]:
|
|
||||||
try:
|
|
||||||
getattr(checker, method_name)()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Warning: {method_name} failed: {e}")
|
|
||||||
|
|
||||||
# Regional collectors (parallel with thread pool)
|
|
||||||
if deps["regional"]:
|
|
||||||
def _run(fn_name):
|
|
||||||
try:
|
try:
|
||||||
getattr(checker, fn_name)()
|
getattr(checker, fn_name)()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Warning: {fn_name} failed: {e}")
|
print(f"Warning: {fn_name} failed: {e}")
|
||||||
|
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as ex:
|
# Home region collectors (parallel)
|
||||||
list(ex.map(_run, deps["regional"]))
|
if deps["home"]:
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=len(deps["home"])) as ex:
|
||||||
|
list(ex.map(_run_method, deps["home"]))
|
||||||
|
|
||||||
|
# Regional collectors (parallel with thread pool)
|
||||||
|
if deps["regional"]:
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as ex:
|
||||||
|
list(ex.map(_run_method, deps["regional"]))
|
||||||
|
|
||||||
session["sections_collected"].add(section)
|
session["sections_collected"].add(section)
|
||||||
|
|
||||||
@@ -627,8 +622,13 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
|||||||
return [TextContent(type="text", text=json.dumps({"error": f"Tool '{name}' not found"}))]
|
return [TextContent(type="text", text=json.dumps({"error": f"Tool '{name}' not found"}))]
|
||||||
try:
|
try:
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
result = await loop.run_in_executor(None, partial(handler, arguments))
|
result = await asyncio.wait_for(
|
||||||
|
loop.run_in_executor(None, partial(handler, arguments)),
|
||||||
|
timeout=TOOL_TIMEOUT
|
||||||
|
)
|
||||||
return [TextContent(type="text", text=json.dumps(result, default=str, ensure_ascii=False))]
|
return [TextContent(type="text", text=json.dumps(result, default=str, ensure_ascii=False))]
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return [TextContent(type="text", text=json.dumps({"error": f"Tool '{name}' timed out after {TOOL_TIMEOUT}s"}))]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return [TextContent(type="text", text=json.dumps({"error": str(e), "traceback": traceback.format_exc()}))]
|
return [TextContent(type="text", text=json.dumps({"error": str(e), "traceback": traceback.format_exc()}))]
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ services:
|
|||||||
- OCI_CLI_SUPPRESS_FILE_PERMISSIONS_WARNING=True
|
- OCI_CLI_SUPPRESS_FILE_PERMISSIONS_WARNING=True
|
||||||
volumes:
|
volumes:
|
||||||
- agent-data:/data
|
- agent-data:/data
|
||||||
ports:
|
|
||||||
- "8000:8000" # Optional: direct API access for debugging
|
|
||||||
networks:
|
networks:
|
||||||
- agent-net
|
- agent-net
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
--gn:#16a34a;--gnl:rgba(22,163,74,.08);--rd:#dc2626;--rdl:rgba(220,38,38,.06);
|
--gn:#16a34a;--gnl:rgba(22,163,74,.08);--rd:#dc2626;--rdl:rgba(220,38,38,.06);
|
||||||
--bl:#2563eb;--bll:rgba(37,99,235,.06);--yl:#ca8a04;--yll:rgba(202,138,4,.06);
|
--bl:#2563eb;--bll:rgba(37,99,235,.06);--yl:#ca8a04;--yll:rgba(202,138,4,.06);
|
||||||
--pp:#7c3aed;--ppl:rgba(124,58,237,.06);
|
--pp:#7c3aed;--ppl:rgba(124,58,237,.06);
|
||||||
|
--ok:#16a34a;--w:#ca8a04;--err:#dc2626;
|
||||||
|
--tx2:#78716c;--p:#1c1917;--pl:rgba(199,70,52,.06);
|
||||||
|
--b2:#ddd9d2;--b3:#c5c0b8;
|
||||||
--r:12px;--rl:16px;--rr:20px;
|
--r:12px;--rl:16px;--rr:20px;
|
||||||
--sh1:0 1px 2px rgba(28,25,23,.04),0 1px 3px rgba(28,25,23,.06);
|
--sh1:0 1px 2px rgba(28,25,23,.04),0 1px 3px rgba(28,25,23,.06);
|
||||||
--sh2:0 4px 6px -1px rgba(28,25,23,.05),0 2px 4px -2px rgba(28,25,23,.04);
|
--sh2:0 4px 6px -1px rgba(28,25,23,.05),0 2px 4px -2px rgba(28,25,23,.04);
|
||||||
@@ -143,6 +146,7 @@ tbody tr:last-child td{border-bottom:none}
|
|||||||
.cm-user .cb strong{color:#ffe0db}
|
.cm-user .cb strong{color:#ffe0db}
|
||||||
.cb{max-width:72%;padding:.7rem .95rem;border-radius:var(--rl) var(--rl) var(--rl) 6px;background:var(--bg1);font-size:.82rem;
|
.cb{max-width:72%;padding:.7rem .95rem;border-radius:var(--rl) var(--rl) var(--rl) 6px;background:var(--bg1);font-size:.82rem;
|
||||||
line-height:1.6;border:1px solid var(--bd);white-space:pre-wrap;box-shadow:var(--sh1)}
|
line-height:1.6;border:1px solid var(--bd);white-space:pre-wrap;box-shadow:var(--sh1)}
|
||||||
|
.cm-ts{font-size:.65rem;color:#999;margin-top:3px;opacity:.7}
|
||||||
.cb code{font-family:var(--fm);font-size:.73rem;background:var(--bg3);padding:.12rem .3rem;border-radius:5px}
|
.cb code{font-family:var(--fm);font-size:.73rem;background:var(--bg3);padding:.12rem .3rem;border-radius:5px}
|
||||||
.cb strong{color:var(--ac)}
|
.cb strong{color:var(--ac)}
|
||||||
.ch-i{display:flex;gap:.45rem;padding:.7rem .85rem;background:var(--bg1);border:1px solid var(--bd);border-top:none;
|
.ch-i{display:flex;gap:.45rem;padding:.7rem .85rem;background:var(--bg1);border:1px solid var(--bd);border-top:none;
|
||||||
@@ -212,7 +216,7 @@ const V='1.1';
|
|||||||
const LOGO_W=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="26" height="26" style="flex-shrink:0"><rect x="2" y="5" width="32" height="26" rx="13" ry="13" fill="rgba(255,255,255,0.12)" stroke="#fff" stroke-width="2"/><rect x="11" y="11" width="14" height="14" rx="4" fill="#fff" opacity="0.95"/><circle cx="15" cy="17" r="2" fill="#C74634"/><circle cx="21" cy="17" r="2" fill="#C74634"/><circle cx="15" cy="16.7" r="0.7" fill="#fff"/><circle cx="21" cy="16.7" r="0.7" fill="#fff"/><rect x="14" y="21" width="8" height="1.5" rx="0.75" fill="#C74634" opacity="0.6"/><line x1="18" y1="11" x2="18" y2="6" stroke="#fff" stroke-width="1.2" stroke-linecap="round"/><circle cx="18" cy="5.5" r="1.5" fill="#fff" opacity="0.9"/><line x1="11" y1="18" x2="6" y2="18" stroke="#fff" stroke-width="1" stroke-linecap="round" opacity="0.7"/><line x1="25" y1="18" x2="30" y2="18" stroke="#fff" stroke-width="1" stroke-linecap="round" opacity="0.7"/><circle cx="5.5" cy="18" r="1" fill="#fff" opacity="0.7"/><circle cx="30.5" cy="18" r="1" fill="#fff" opacity="0.7"/></svg>`;
|
const LOGO_W=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="26" height="26" style="flex-shrink:0"><rect x="2" y="5" width="32" height="26" rx="13" ry="13" fill="rgba(255,255,255,0.12)" stroke="#fff" stroke-width="2"/><rect x="11" y="11" width="14" height="14" rx="4" fill="#fff" opacity="0.95"/><circle cx="15" cy="17" r="2" fill="#C74634"/><circle cx="21" cy="17" r="2" fill="#C74634"/><circle cx="15" cy="16.7" r="0.7" fill="#fff"/><circle cx="21" cy="16.7" r="0.7" fill="#fff"/><rect x="14" y="21" width="8" height="1.5" rx="0.75" fill="#C74634" opacity="0.6"/><line x1="18" y1="11" x2="18" y2="6" stroke="#fff" stroke-width="1.2" stroke-linecap="round"/><circle cx="18" cy="5.5" r="1.5" fill="#fff" opacity="0.9"/><line x1="11" y1="18" x2="6" y2="18" stroke="#fff" stroke-width="1" stroke-linecap="round" opacity="0.7"/><line x1="25" y1="18" x2="30" y2="18" stroke="#fff" stroke-width="1" stroke-linecap="round" opacity="0.7"/><circle cx="5.5" cy="18" r="1" fill="#fff" opacity="0.7"/><circle cx="30.5" cy="18" r="1" fill="#fff" opacity="0.7"/></svg>`;
|
||||||
const LOGO_R=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="52" height="52"><rect x="2" y="5" width="32" height="26" rx="13" ry="13" fill="rgba(199,70,52,0.08)" stroke="#C74634" stroke-width="1.8"/><rect x="11" y="11" width="14" height="14" rx="4" fill="#fff" stroke="#C74634" stroke-width="0.4"/><circle cx="15" cy="17" r="2" fill="#C74634"/><circle cx="21" cy="17" r="2" fill="#C74634"/><circle cx="15" cy="16.7" r="0.7" fill="#fff"/><circle cx="21" cy="16.7" r="0.7" fill="#fff"/><rect x="14" y="21" width="8" height="1.5" rx="0.75" fill="#C74634" opacity="0.6"/><line x1="18" y1="11" x2="18" y2="6" stroke="#C74634" stroke-width="1.2" stroke-linecap="round"/><circle cx="18" cy="5.5" r="1.5" fill="#C74634" opacity="0.25" stroke="#C74634" stroke-width="0.7"/><line x1="11" y1="18" x2="6" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.4"/><line x1="25" y1="18" x2="30" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.4"/><circle cx="5.5" cy="18" r="1" fill="#C74634" opacity="0.35"/><circle cx="30.5" cy="18" r="1" fill="#C74634" opacity="0.35"/></svg>`;
|
const LOGO_R=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="52" height="52"><rect x="2" y="5" width="32" height="26" rx="13" ry="13" fill="rgba(199,70,52,0.08)" stroke="#C74634" stroke-width="1.8"/><rect x="11" y="11" width="14" height="14" rx="4" fill="#fff" stroke="#C74634" stroke-width="0.4"/><circle cx="15" cy="17" r="2" fill="#C74634"/><circle cx="21" cy="17" r="2" fill="#C74634"/><circle cx="15" cy="16.7" r="0.7" fill="#fff"/><circle cx="21" cy="16.7" r="0.7" fill="#fff"/><rect x="14" y="21" width="8" height="1.5" rx="0.75" fill="#C74634" opacity="0.6"/><line x1="18" y1="11" x2="18" y2="6" stroke="#C74634" stroke-width="1.2" stroke-linecap="round"/><circle cx="18" cy="5.5" r="1.5" fill="#C74634" opacity="0.25" stroke="#C74634" stroke-width="0.7"/><line x1="11" y1="18" x2="6" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.4"/><line x1="25" y1="18" x2="30" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.4"/><circle cx="5.5" cy="18" r="1" fill="#C74634" opacity="0.35"/><circle cx="30.5" cy="18" r="1" fill="#C74634" opacity="0.35"/></svg>`;
|
||||||
|
|
||||||
const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],genaiCfg:[],adbCfg:[],mcpSvr:[],users:[],auditLogs:[],models:{},regions:[],embModels:{},selGenai:'',expData:null,editing:null,
|
const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],genaiCfg:[],adbCfg:[],mcpSvr:[],users:[],models:{},regions:[],embModels:{},expData:null,editing:null,
|
||||||
chatModel:'',chatOci:'',chatRegion:'',chatCompartment:'',
|
chatModel:'',chatOci:'',chatRegion:'',chatCompartment:'',
|
||||||
chatParams:{temperature:1,max_tokens:6000,top_p:0.95,top_k:1,frequency_penalty:0,presence_penalty:0},chatParamsOpen:false,chatUseTools:true,
|
chatParams:{temperature:1,max_tokens:6000,top_p:0.95,top_k:1,frequency_penalty:0,presence_penalty:0},chatParamsOpen:false,chatUseTools:true,
|
||||||
chatPrompts:[],editingPrompt:null,trackingReportId:null,ociRegions:{},rptSelRegions:[],rptRegionsOpen:false,rptRegionFilter:'',
|
chatPrompts:[],editingPrompt:null,trackingReportId:null,ociRegions:{},rptSelRegions:[],rptRegionsOpen:false,rptRegionFilter:'',
|
||||||
@@ -240,7 +244,7 @@ async function loadData(){try{
|
|||||||
if(S.user.role==='admin'){try{S.users=await $api('/users')}catch(e){}}
|
if(S.user.role==='admin'){try{S.users=await $api('/users')}catch(e){}}
|
||||||
try{S.cisVer=await $api('/cis-engine/version')}catch(e){}
|
try{S.cisVer=await $api('/cis-engine/version')}catch(e){}
|
||||||
}catch(e){console.error(e)}}
|
}catch(e){console.error(e)}}
|
||||||
function R(){document.getElementById('app').innerHTML=S.user?rApp():rLogin();bind();if(S.editing?.type==='mcp')setTimeout(mcpTypeFields,0)}
|
function R(){document.getElementById('app').innerHTML=S.user?rApp():rLogin();if(S.editing?.type==='mcp')setTimeout(mcpTypeFields,0)}
|
||||||
function switchTab(t){S.tab=t;S.expData=null;R();if(t==='audit'&&S.user.role==='admin')loadAudit();if(t==='downloads')refreshDl();
|
function switchTab(t){S.tab=t;S.expData=null;R();if(t==='audit'&&S.user.role==='admin')loadAudit();if(t==='downloads')refreshDl();
|
||||||
const tm={'oci-config':'oci','genai':'genai','adb':'adb','mcp':'mcp'};if(tm[t])setTimeout(()=>refreshCLogs(tm[t]),100)}
|
const tm={'oci-config':'oci','genai':'genai','adb':'adb','mcp':'mcp'};if(tm[t])setTimeout(()=>refreshCLogs(tm[t]),100)}
|
||||||
|
|
||||||
@@ -284,7 +288,7 @@ function rPg(){switch(S.tab){case'chat':return rChat();case'explorer':return rEx
|
|||||||
/* ── Chat ── */
|
/* ── Chat ── */
|
||||||
function rChat(){
|
function rChat(){
|
||||||
const ms=S.msgs.length===0?'<div class="emp"><div class="eic">🤖</div><p>Inicie uma conversa com o agente.</p><p style="font-size:.74rem;margin-top:.35rem">Selecione um modelo para começar.</p></div>'
|
const ms=S.msgs.length===0?'<div class="emp"><div class="eic">🤖</div><p>Inicie uma conversa com o agente.</p><p style="font-size:.74rem;margin-top:.35rem">Selecione um modelo para começar.</p></div>'
|
||||||
:S.msgs.map(m=>`<div class="cm cm-${m.r}"><div class="cb">${fm(m.c)}</div></div>`).join('');
|
:S.msgs.map(m=>`<div class="cm cm-${m.r}"><div class="cb">${fm(m.c)}</div>${m.t?`<div class="cm-ts">${m.t}</div>`:''}</div>`).join('');
|
||||||
// Build custom dropdown items
|
// Build custom dropdown items
|
||||||
// Skip non-chat models: Codex (completions only), Image, Audio, Guard, ProtectAI, GPT-oss, Pro (Responses API only)
|
// Skip non-chat models: Codex (completions only), Image, Audio, Guard, ProtectAI, GPT-oss, Pro (Responses API only)
|
||||||
const skip=new Set(['openai.gpt-image-1','openai.gpt-image-1.5','openai.gpt-audio','meta.llama-guard-4-12b','protectai.deberta-v3-base-prompt-injection-v2',
|
const skip=new Set(['openai.gpt-image-1','openai.gpt-image-1.5','openai.gpt-audio','meta.llama-guard-4-12b','protectai.deberta-v3-base-prompt-injection-v2',
|
||||||
@@ -354,6 +358,7 @@ function addChatFiles(input){for(const f of input.files){if(S.chatFiles.length>=
|
|||||||
if(entry.type==='image'){const r=new FileReader();r.onload=e=>{entry.preview=e.target.result;R()};r.readAsDataURL(f)}
|
if(entry.type==='image'){const r=new FileReader();r.onload=e=>{entry.preview=e.target.result;R()};r.readAsDataURL(f)}
|
||||||
S.chatFiles.push(entry)}input.value='';R()}
|
S.chatFiles.push(entry)}input.value='';R()}
|
||||||
function rmChatFile(i){S.chatFiles.splice(i,1);R()}
|
function rmChatFile(i){S.chatFiles.splice(i,1);R()}
|
||||||
|
function tstamp(){const d=new Date();return d.toLocaleTimeString('pt-BR',{hour:'2-digit',minute:'2-digit'})}
|
||||||
async function sChat(){const el=document.getElementById('chi');const m=el.value.trim();
|
async function sChat(){const el=document.getElementById('chi');const m=el.value.trim();
|
||||||
if(!m&&!S.chatFiles.length)return;
|
if(!m&&!S.chatFiles.length)return;
|
||||||
if(!S.chatModel){S.msgs.push({r:'assistant',c:'⚠️ Selecione um modelo antes de enviar uma mensagem.'});R();return}
|
if(!S.chatModel){S.msgs.push({r:'assistant',c:'⚠️ Selecione um modelo antes de enviar uma mensagem.'});R();return}
|
||||||
@@ -362,8 +367,7 @@ async function sChat(){const el=document.getElementById('chi');const m=el.value.
|
|||||||
const fileNames=S.chatFiles.map(f=>f.name);
|
const fileNames=S.chatFiles.map(f=>f.name);
|
||||||
let userDisplay=m||'';
|
let userDisplay=m||'';
|
||||||
if(hasFiles)userDisplay+=(userDisplay?'\n':'')+'📎 '+fileNames.join(', ');
|
if(hasFiles)userDisplay+=(userDisplay?'\n':'')+'📎 '+fileNames.join(', ');
|
||||||
S.msgs.push({r:'user',c:userDisplay});R();scCh();
|
S.msgs.push({r:'user',c:userDisplay,t:tstamp()});R();scCh();
|
||||||
// Disable input and show thinking indicator
|
|
||||||
const btns=document.querySelectorAll('.ch-i .btn');
|
const btns=document.querySelectorAll('.ch-i .btn');
|
||||||
el.disabled=true;btns.forEach(b=>{b.disabled=true});
|
el.disabled=true;btns.forEach(b=>{b.disabled=true});
|
||||||
const sendBtn=btns[btns.length-1];if(sendBtn){sendBtn.dataset.origText=sendBtn.textContent;sendBtn.innerHTML='<span class="spinner" style="width:14px;height:14px;border-width:2px;margin-right:6px"></span>Pensando...'}
|
const sendBtn=btns[btns.length-1];if(sendBtn){sendBtn.dataset.origText=sendBtn.textContent;sendBtn.innerHTML='<span class="spinner" style="width:14px;height:14px;border-width:2px;margin-right:6px"></span>Pensando...'}
|
||||||
@@ -396,12 +400,23 @@ async function sChat(){const el=document.getElementById('chi');const m=el.value.
|
|||||||
body.top_k=S.chatParams.top_k;body.frequency_penalty=S.chatParams.frequency_penalty;body.presence_penalty=S.chatParams.presence_penalty}
|
body.top_k=S.chatParams.top_k;body.frequency_penalty=S.chatParams.frequency_penalty;body.presence_penalty=S.chatParams.presence_penalty}
|
||||||
d=await $api('/chat',{method:'POST',body})}
|
d=await $api('/chat',{method:'POST',body})}
|
||||||
S.sid=d.session_id;
|
S.sid=d.session_id;
|
||||||
|
if(d.status==='processing'&&d.message_id){
|
||||||
|
await pollChatResult(d.message_id)
|
||||||
|
}else{
|
||||||
S.msgs=S.msgs.filter(x=>!x.thinking);
|
S.msgs=S.msgs.filter(x=>!x.thinking);
|
||||||
let resp=d.response;
|
let resp=d.response;
|
||||||
if(d.tools_used&&d.tools_used.length){resp+='\n\n🔧 **Tools utilizadas:** '+d.tools_used.map(t=>t.name).join(', ')}
|
if(d.tools_used&&d.tools_used.length){resp+='\n\n🔧 **Tools utilizadas:** '+d.tools_used.map(t=>t.name).join(', ')}
|
||||||
S.msgs.push({r:'assistant',c:resp});R();scCh()}
|
S.msgs.push({r:'assistant',c:resp,t:tstamp()});R();scCh()}}
|
||||||
catch(e){S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:'❌ Erro: '+e.message});R()}
|
catch(e){S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:'❌ Erro: '+e.message,t:tstamp()});R()}
|
||||||
finally{el.disabled=false;el.focus();btns.forEach(b=>{b.disabled=false});if(sendBtn)sendBtn.textContent=sendBtn.dataset.origText||'Enviar →'}}
|
finally{el.disabled=false;el.focus();btns.forEach(b=>{b.disabled=false});if(sendBtn)sendBtn.textContent=sendBtn.dataset.origText||'Enviar →'}}
|
||||||
|
async function pollChatResult(mid){
|
||||||
|
for(let i=0;i<1200;i++){
|
||||||
|
await new Promise(r=>setTimeout(r,i<10?1000:3000));
|
||||||
|
try{const r=await $api('/chat/'+mid+'/status');
|
||||||
|
if(r.status==='done'){S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:r.content,t:tstamp()});R();scCh();return}
|
||||||
|
if(r.status==='failed'){S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:'❌ '+r.content,t:tstamp()});R();scCh();return}
|
||||||
|
}catch(e){}}
|
||||||
|
S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:'⏰ Timeout: a resposta está demorando muito. Tente novamente.',t:tstamp()});R()}
|
||||||
function scCh(){setTimeout(()=>{const e=document.getElementById('chm');if(e)e.scrollTop=e.scrollHeight},50)}
|
function scCh(){setTimeout(()=>{const e=document.getElementById('chm');if(e)e.scrollTop=e.scrollHeight},50)}
|
||||||
async function clrChat(){if(S.sid)try{await $api('/chat/'+S.sid,{method:'DELETE'})}catch(e){}S.msgs=[];S.sid=null;R()}
|
async function clrChat(){if(S.sid)try{await $api('/chat/'+S.sid,{method:'DELETE'})}catch(e){}S.msgs=[];S.sid=null;R()}
|
||||||
|
|
||||||
@@ -513,7 +528,6 @@ ${status}</div></div>`}
|
|||||||
async function cisLoadVersion(){try{S.cisVer=await $api('/cis-engine/version');R()}catch(e){console.error('cisVer',e)}}
|
async function cisLoadVersion(){try{S.cisVer=await $api('/cis-engine/version');R()}catch(e){console.error('cisVer',e)}}
|
||||||
async function cisCheckUpdate(){S.cisCheckResult=null;R();try{S.cisCheckResult=await $api('/cis-engine/check-update');R()}catch(e){alert('Erro ao verificar: '+e.message)}}
|
async function cisCheckUpdate(){S.cisCheckResult=null;R();try{S.cisCheckResult=await $api('/cis-engine/check-update');R()}catch(e){alert('Erro ao verificar: '+e.message)}}
|
||||||
async function cisDoUpdate(){if(!confirm('Atualizar CIS Engine para a versão mais recente do GitHub?'))return;S.cisUpdating=true;R();try{const d=await $api('/cis-engine/update',{method:'POST'});alert(`Atualizado: ${d.old_version} → ${d.new_version}\nPatches aplicados: ${d.patches_applied.join(', ')||'nenhum'}`);S.cisCheckResult=null;S.cisUpdating=false;await cisLoadVersion()}catch(e){S.cisUpdating=false;R();alert('Erro: '+e.message)}}
|
async function cisDoUpdate(){if(!confirm('Atualizar CIS Engine para a versão mais recente do GitHub?'))return;S.cisUpdating=true;R();try{const d=await $api('/cis-engine/update',{method:'POST'});alert(`Atualizado: ${d.old_version} → ${d.new_version}\nPatches aplicados: ${d.patches_applied.join(', ')||'nenhum'}`);S.cisCheckResult=null;S.cisUpdating=false;await cisLoadVersion()}catch(e){S.cisUpdating=false;R();alert('Erro: '+e.message)}}
|
||||||
function ldRpt(id){const f=document.getElementById('rfr');if(f)f.src=API+'/reports/'+id+'/html'}
|
|
||||||
function rptPickRegion(r){S.rptSelRegions.push(r);S.rptRegionFilter='';R()}
|
function rptPickRegion(r){S.rptSelRegions.push(r);S.rptRegionFilter='';R()}
|
||||||
function rptRemoveRegion(r){S.rptSelRegions=S.rptSelRegions.filter(x=>x!==r);R()}
|
function rptRemoveRegion(r){S.rptSelRegions=S.rptSelRegions.filter(x=>x!==r);R()}
|
||||||
function rptPickOci(id){S.rptOciVal=id;S.rptOciOpen=false;S.rptOciFilter='';R()}
|
function rptPickOci(id){S.rptOciVal=id;S.rptOciOpen=false;S.rptOciFilter='';R()}
|
||||||
@@ -1128,7 +1142,6 @@ async function hLogin(){const u=document.getElementById('lu').value,p=document.g
|
|||||||
try{const d=await doLogin(u,p);if(d&&d.mfa_required){document.getElementById('lf').style.display='none';document.getElementById('mfa-s').style.display='block'}}
|
try{const d=await doLogin(u,p);if(d&&d.mfa_required){document.getElementById('lf').style.display='none';document.getElementById('mfa-s').style.display='block'}}
|
||||||
catch(e){document.getElementById('le').innerHTML=`<div class="al al-e">${e.message}</div>`}}
|
catch(e){document.getElementById('le').innerHTML=`<div class="al al-e">${e.message}</div>`}}
|
||||||
async function hMfa(){try{await doLogin(_lu,_lp,document.getElementById('mfa-c').value)}catch(e){document.getElementById('le').innerHTML=`<div class="al al-e">${e.message}</div>`}}
|
async function hMfa(){try{await doLogin(_lu,_lp,document.getElementById('mfa-c').value)}catch(e){document.getElementById('le').innerHTML=`<div class="al al-e">${e.message}</div>`}}
|
||||||
function bind(){}
|
|
||||||
(async()=>{await checkAuth();R()})();
|
(async()=>{await checkAuth();R()})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ server {
|
|||||||
index index.html;
|
index index.html;
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
add_header Pragma "no-cache";
|
|
||||||
add_header Expires 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
location /api/ {
|
location /api/ {
|
||||||
|
|||||||
Reference in New Issue
Block a user