From 58d430c90490f37c0402fc3f03864be69e9c5f67 Mon Sep 17 00:00:00 2001 From: nogueiraguh Date: Wed, 11 Mar 2026 13:55:59 -0300 Subject: [PATCH] feat: terraform prompt generator, provider parameter matrix, and model penalties - Add Terraform Prompt Generator: AI-powered structured prompt generation with dedicated chat UI, curated model selection (7 models), conversation history with persistent sessions, and OCI TF resource reference context - Provider parameter matrix: segment API params per model/provider, explicitly null unsupported params (freq/pres penalty, temperature) to prevent OCI SDK serialization errors - Add penalties flag to model catalog: only GPT-4.1/4.1-mini/4o support frequency_penalty and presence_penalty - Flat workspace enforcement: prohibit module blocks and duplicate variable declarations in Terraform system prompt - Prompt Generator history: sidebar panel with session persistence, restore, rename/delete (agent_type tf-prompt) --- README.md | 22 +++- backend/app.py | 239 +++++++++++++++++++++++++++++++++++++------- frontend/index.html | 159 ++++++++++++++++++++++++----- 3 files changed, 355 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 8a5508d..bf6c45d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

- Version + Version Python FastAPI OCI @@ -41,7 +41,8 @@ The platform combines security compliance scanning, AI-powered chat with **RAG ( - 15 chat models + 3 embedding models across 5 providers: **Meta** (Llama 4), **Google** (Gemini 2.5), **OpenAI** (GPT-5.2/5.1/5 Mini/4.1/4o, o3/o4-mini), **xAI** (Grok 4/3) - 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 -- Full parameter control: temperature, max_tokens, top_p, top_k, frequency/presence penalty +- **Provider parameter matrix**: automatic parameter segmentation per model — reasoning models (o3/o4-mini) only get `max_completion_tokens` + `reasoning_effort`, OpenAI GPT-4.x gets freq/pres penalties, Google/Meta get `top_k`, xAI gets basic params. Unsupported parameters are explicitly nulled to prevent SDK serialization errors +- Full parameter control: temperature, max_tokens, top_p, top_k, frequency/presence penalty (when supported by model) - Toggle MCP tools on/off per chat session - Conversation history with session management - On-Demand and Dedicated serving modes @@ -71,6 +72,18 @@ The platform combines security compliance scanning, AI-powered chat with **RAG ( - **Multi-region provider management**: auto-detects provider aliases and region variables in generated code, generates `provider.tf` with credentials and correct region references (`var.region` instead of hardcoded values) - **System prompt sync**: Terraform system prompt in code is automatically synced to DB on every restart — single source of truth - Compact system prompt optimized for OCI Terraform provider best practices (100K max_tokens) +- **Flat workspace enforcement**: system prompt prohibits `module` blocks (workspace has no subdirectories) and enforces single-file variable declarations (`variables.tf` only) + +### 📝 Terraform Prompt Generator +- **AI-powered prompt generation** for OCI infrastructure — generates structured, optimized prompts to feed into the Terraform Agent +- Dedicated chat interface (sub-menu under Terraform) matching Terraform Agent layout +- **Curated model selection**: only 7 recommended models (GPT-4.1, o3, o4-mini, GPT-5.1, GPT-5.2, Gemini 2.5 Pro, Gemini 2.5 Flash), grouped by provider +- **OCI Terraform Resource Reference**: prompt generation includes the full OCI provider resource catalog as context, ensuring accurate resource names and attributes +- **Conversation history**: persistent sessions (agent_type `tf-prompt`) with sidebar history panel, rename/delete, session restore +- **Contextual follow-up**: conversation history sent to GenAI for iterative prompt refinement +- **Copy & Send**: action buttons on assistant messages to copy the prompt or send directly to the Terraform Agent input +- Robust system prompt with OCI Provider constraints, inference rules, architecture templates, and workspace restrictions +- Example prompts as clickable buttons for quick start ### ⚡ OCI Resource Actions - **Start/Stop Compute Instances** directly from OCI Account Explorer with one click @@ -396,14 +409,14 @@ Allow group to read buckets in compartment ``` oci-cis-agent/ ├── backend/ -│ ├── app.py # FastAPI application (~5300 lines) +│ ├── app.py # FastAPI application (~5500 lines) │ ├── cis_reports.py # Oracle CIS Benchmark checker (6660 lines, report engine) │ ├── mcp_cis_server.py # MCP server with 12 granular CIS tools (~700 lines) │ ├── gen_tf_reference.py # OCI Terraform provider resource catalog generator │ ├── Dockerfile # Python 3.12 + OCI CLI + Terraform CLI │ └── requirements.txt # Dependencies ├── frontend/ -│ └── index.html # SPA with Oracle Dark Premium theme (~2820 lines) +│ └── index.html # SPA with Oracle Dark Premium theme (~2950 lines) ├── nginx/ │ └── default.conf # Reverse proxy config ├── docker-compose.yml # Orchestration @@ -560,6 +573,7 @@ oci-cis-agent/ | POST | `/api/terraform/workspaces/{wid}/cancel` | Cancel running Terraform operation | | DELETE | `/api/terraform/workspaces/{wid}` | Delete workspace | | POST | `/api/terraform/refresh-reference` | Regenerate OCI Terraform resource reference (UI button) | +| POST | `/api/terraform/generate-prompt` | Generate structured Terraform prompt via AI (Prompt Generator) | ### Chat & Reports diff --git a/backend/app.py b/backend/app.py index bf86885..0e33e56 100644 --- a/backend/app.py +++ b/backend/app.py @@ -62,38 +62,38 @@ security = HTTPBearer() # _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", + "meta.llama-4-maverick-17b-128e-instruct-fp8": {"provider":"meta","name":"Meta Llama 4 Maverick","api_format":"GENERIC","max_tokens":8192, "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", + "meta.llama-4-scout-17b-16e-instruct": {"provider":"meta","name":"Meta Llama 4 Scout","api_format":"GENERIC","max_tokens":8192, "ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaw23hfc7mtvv5wef3gwvvaguyzqmhb5lx4r5s3y2xzc4a"}}, # ── Google ── - "google.gemini-2.5-pro": {"provider":"google","name":"Google Gemini 2.5 Pro","api_format":"GENERIC", + "google.gemini-2.5-pro": {"provider":"google","name":"Google Gemini 2.5 Pro","api_format":"GENERIC","max_tokens":65536, "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", + "google.gemini-2.5-flash": {"provider":"google","name":"Google Gemini 2.5 Flash","api_format":"GENERIC","max_tokens":8192, "ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaeo4ehrn25guuats5s45hnvswlhxo6riop275l2bkr2vq"}}, # ── OpenAI ── - "openai.gpt-5.2": {"provider":"openai","name":"OpenAI GPT-5.2","api_format":"GENERIC", + "openai.gpt-5.2": {"provider":"openai","name":"OpenAI GPT-5.2","api_format":"GENERIC","max_tokens":32768, "ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya4fw3p5fddnexbfcurnz7spgkqb4mq4a6y5ubyv7777sa"}}, - "openai.gpt-5.1": {"provider":"openai","name":"OpenAI GPT-5.1","api_format":"GENERIC", + "openai.gpt-5.1": {"provider":"openai","name":"OpenAI GPT-5.1","api_format":"GENERIC","max_tokens":32768, "ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya3darth2ozqcfssb2bats5jitpgigllccajasdyqljnkq"}}, - "openai.gpt-5-mini": {"provider":"openai","name":"OpenAI GPT-5 Mini","api_format":"GENERIC", + "openai.gpt-5-mini": {"provider":"openai","name":"OpenAI GPT-5 Mini","api_format":"GENERIC","max_tokens":16384, "ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya3eain4n6v3edm4ryjvze5hnjouujd4vralxntfalwjaq"}}, - "openai.gpt-4.1": {"provider":"openai","name":"OpenAI GPT-4.1 (Padrão)","api_format":"GENERIC", + "openai.gpt-4.1": {"provider":"openai","name":"OpenAI GPT-4.1 (Padrão)","api_format":"GENERIC","max_tokens":32768,"penalties":True, "ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaa6g75r2qqmtzjaooqtlxv4lxkpcqp2jdd6plpq7yq7ea"}}, - "openai.gpt-4.1-mini": {"provider":"openai","name":"OpenAI GPT-4.1 Mini","api_format":"GENERIC", + "openai.gpt-4.1-mini": {"provider":"openai","name":"OpenAI GPT-4.1 Mini","api_format":"GENERIC","max_tokens":16384,"penalties":True, "ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya6pk3sxishpiexm2rb5sf4ytb5tsbz4to2g3g23smidaa"}}, - "openai.gpt-4o": {"provider":"openai","name":"OpenAI GPT-4o","api_format":"GENERIC", + "openai.gpt-4o": {"provider":"openai","name":"OpenAI GPT-4o","api_format":"GENERIC","max_tokens":16384,"penalties":True, "ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyah7slrtboxdbfdy5cdspsfts62yumoclpdgwydopse7za"}}, - "openai.o4-mini": {"provider":"openai","name":"OpenAI o4-mini","api_format":"GENERIC", + "openai.o4-mini": {"provider":"openai","name":"OpenAI o4-mini","api_format":"GENERIC","reasoning":True,"max_tokens":100000, "ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya5ivfqp4fxlajoeg2cahlqtuuswagiv6a7dggpigy23bq"}}, - "openai.o3": {"provider":"openai","name":"OpenAI o3","api_format":"GENERIC", + "openai.o3": {"provider":"openai","name":"OpenAI o3","api_format":"GENERIC","reasoning":True,"max_tokens":100000, "ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyalgnrukpjk6wm5zsf4jzkoneahgswhrk7kukkoagwnzma"}}, # ── xAI ── - "xai.grok-4": {"provider":"xai","name":"xAI Grok 4","api_format":"GENERIC", + "xai.grok-4": {"provider":"xai","name":"xAI Grok 4","api_format":"GENERIC","max_tokens":131072, "ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaldmhg25is4nouena4oa2pj4nvwgfeempo4syiaazukia"}}, - "xai.grok-3": {"provider":"xai","name":"xAI Grok 3","api_format":"GENERIC", + "xai.grok-3": {"provider":"xai","name":"xAI Grok 3","api_format":"GENERIC","max_tokens":131072, "ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyag3w2xk76vlahjujj2gdfeuzhflt25gbo3bxidlsqfjla"}}, - "xai.grok-3-mini-fast": {"provider":"xai","name":"xAI Grok 3 Mini Fast","api_format":"GENERIC", + "xai.grok-3-mini-fast": {"provider":"xai","name":"xAI Grok 3 Mini Fast","api_format":"GENERIC","max_tokens":131072, "ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyauvjoll2repj5pbtkk7pinwj57ex3lkehzpxd6v6rxscq"}}, } @@ -197,6 +197,7 @@ def init_db(): top_k INTEGER DEFAULT 1, frequency_penalty REAL DEFAULT 0, presence_penalty REAL DEFAULT 0, + reasoning_effort TEXT, is_default INTEGER DEFAULT 0, system_prompt TEXT DEFAULT '', created_at TEXT DEFAULT (datetime('now')), @@ -353,7 +354,7 @@ def init_db(): c.execute("DELETE FROM config_logs WHERE created_at < datetime('now', '-30 days')") c.execute("DELETE FROM chat_logs WHERE created_at < datetime('now', '-30 days')") # ── Migrations ── - for col in ["system_prompt TEXT DEFAULT ''"]: + for col in ["system_prompt TEXT DEFAULT ''", "reasoning_effort TEXT"]: try: c.execute(f"ALTER TABLE genai_configs ADD COLUMN {col}") except sqlite3.OperationalError: @@ -518,6 +519,7 @@ class ChatMsg(BaseModel): 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 + reasoning_effort: Optional[str] = None use_tools: Optional[bool] = True class RunReportReq(BaseModel): config_id: str; regions: Optional[List[str]] = None @@ -530,6 +532,7 @@ class GenAIConfigReq(BaseModel): 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 + reasoning_effort: Optional[str] = None is_default: bool = False class IngestDocReq(BaseModel): adb_config_id: str; documents: List[Dict[str, Any]]; table_name: Optional[str] = None @@ -1638,11 +1641,11 @@ async def save_genai(req: GenAIConfigReq, u=Depends(require("admin","user"))): 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 (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", + frequency_penalty,presence_penalty,reasoning_effort,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))) + req.frequency_penalty, req.presence_penalty, req.reasoning_effort, 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} @@ -1672,11 +1675,11 @@ async def update_genai(gid: str, req: GenAIConfigReq, u=Depends(require("admin", 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=?""", + top_p=?,top_k=?,frequency_penalty=?,presence_penalty=?,reasoning_effort=?,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)) + req.frequency_penalty, req.presence_penalty, req.reasoning_effort, 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} @@ -1846,7 +1849,8 @@ def _call_genai(gc: dict, message: str, history: list = None, tools: list = None if system_prompt: chat_request.preamble_override = system_prompt chat_request.message = message - chat_request.max_tokens = int(gc.get("max_tokens", 6000)) + cohere_max = model_info.get("max_tokens", 4096) + chat_request.max_tokens = min(int(gc.get("max_tokens", 4096)), cohere_max) 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)) @@ -1887,20 +1891,54 @@ def _call_genai(gc: dict, message: str, history: list = None, tools: list = None 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)) + is_reasoning = model_info.get("reasoning", False) + # Clamp max_tokens to model limit + model_max = model_info.get("max_tokens", 16384) + requested_tokens = int(gc.get("max_tokens", 6000)) + clamped_tokens = min(requested_tokens, model_max) + + # ── Parameter matrix per provider ── + # OpenAI reasoning (o3, o4-mini): max_completion_tokens + reasoning_effort only + if is_reasoning: + chat_request.max_completion_tokens = clamped_tokens + re = gc.get("reasoning_effort") + if re and hasattr(chat_request, "reasoning_effort"): + chat_request.reasoning_effort = re + # Explicitly unset unsupported params + chat_request.temperature = None + chat_request.top_p = None + chat_request.frequency_penalty = None + chat_request.presence_penalty = None + # OpenAI standard (GPT-4.1, GPT-5.x, GPT-4o): max_completion_tokens, temperature, top_p + # freq/pres penalty only for models with "penalties":True (GPT-4.1, GPT-4.1-mini, GPT-4o) + elif is_openai: + chat_request.max_completion_tokens = clamped_tokens + chat_request.temperature = float(gc.get("temperature", 1)) + chat_request.top_p = float(gc.get("top_p", 0.95)) + if model_info.get("penalties"): + chat_request.frequency_penalty = float(gc.get("frequency_penalty", 0)) + chat_request.presence_penalty = float(gc.get("presence_penalty", 0)) + else: + chat_request.frequency_penalty = None + chat_request.presence_penalty = None + # xAI Grok: max_tokens, temperature, top_p (no top_k, no freq/pres penalty) + elif provider == "xai": + chat_request.max_tokens = clamped_tokens + chat_request.temperature = float(gc.get("temperature", 1)) + chat_request.top_p = float(gc.get("top_p", 0.95)) + # Explicitly unset unsupported params to prevent SDK serialization + chat_request.frequency_penalty = None + chat_request.presence_penalty = None + # Meta Llama: max_tokens, temperature, top_p, top_k (no freq/pres penalty) + # Google Gemini: max_tokens, temperature, top_p, top_k (no freq/pres penalty) 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)) + chat_request.max_tokens = clamped_tokens + chat_request.temperature = float(gc.get("temperature", 1)) + chat_request.top_p = float(gc.get("top_p", 0.95)) + chat_request.top_k = int(gc.get("top_k", 1)) + # Explicitly unset unsupported params to prevent SDK serialization + chat_request.frequency_penalty = None + chat_request.presence_penalty = None messages = [] if system_prompt: @@ -2164,6 +2202,11 @@ Se pedirem AWS, Azure, GCP ou outro provider, recuse educadamente. - Remote peering: APENAS UM LADO do par define `peer_id` e `peer_region_name`. O outro lado é criado SEM `peer_id`. Nunca gere dois RPCs apontando um para o outro (causa Cycle error). Exemplo: RPC_mad1 (sem peer_id) e RPC_mad3 (com peer_id = RPC_mad1.id, peer_region_name = var.region). - Associação de route table com subnet deve ser via `route_table_id` na subnet ou `oci_core_route_table_attachment`. - `oci_network_firewall_network_firewall` exige `network_firewall_policy_id`. + +### Proibições absolutas — módulos e variáveis +- NUNCA use `module` blocks. Este workspace é flat (um único diretório). Não existem subdiretórios `modules/`. Toda a infraestrutura deve ser definida diretamente com `resource` e `data` blocks. +- NUNCA declare a mesma `variable` em mais de um arquivo. Todas as variáveis devem estar em `variables.tf` e SOMENTE lá. Os outros arquivos (.tf) apenas referenciam `var.nome`. +- Antes de gerar, verifique se uma variável já foi declarada. Se já existe em `variables.tf`, NÃO redeclare. """ # ── Terraform OCI Resource Reference (generated at build time, updatable at runtime) ── @@ -3821,6 +3864,7 @@ def _chat_start(msg: ChatMsg, u, attachments: list = None, agent_type: str = "ch 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 + if msg.reasoning_effort is not None: genai_cfg["reasoning_effort"] = msg.reasoning_effort elif msg.model_id and msg.oci_config_id: with db() as c: oci_row = c.execute("SELECT * FROM oci_configs WHERE id=?", (msg.oci_config_id,)).fetchone() @@ -3845,6 +3889,7 @@ def _chat_start(msg: ChatMsg, u, attachments: list = None, agent_type: str = "ch "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, + "reasoning_effort": msg.reasoning_effort, } if not genai_cfg: @@ -3976,9 +4021,11 @@ async def _chat_background(mid: str, sid: str, msg: ChatMsg, user: dict, genai_c base_prompt = cfg_dict.get("system_prompt", "") cfg_dict["system_prompt"] = f"{base_prompt}\n\n{config_hint}" if base_prompt else config_hint - # ── Terraform agent: boost max_tokens for complex infra code ── + # ── Terraform agent: boost max_tokens to model limit ── if agent_type == "terraform": - cfg_dict["max_tokens"] = max(int(cfg_dict.get("max_tokens", 6000)), 100000) + tf_model_info = GENAI_MODELS.get(cfg_dict.get("model_id", ""), {}) + tf_model_max = tf_model_info.get("max_tokens", 32768) + cfg_dict["max_tokens"] = tf_model_max # ── Inject existing OCI resources for terraform agent ── if agent_type == "terraform" and active_oci_id: @@ -4184,6 +4231,7 @@ async def chat_with_files( top_k: Optional[int] = Form(None), frequency_penalty: Optional[float] = Form(None), presence_penalty: Optional[float] = Form(None), + reasoning_effort: Optional[str] = Form(None), files: List[UploadFile] = File(default=[]), u=Depends(current_user), ): @@ -4226,6 +4274,7 @@ async def chat_with_files( top_k=top_k, frequency_penalty=frequency_penalty, presence_penalty=presence_penalty, + reasoning_effort=reasoning_effort, ) sid, mid_or_result, genai_cfg = _chat_start(msg, u, attachments=attachments if attachments else None) @@ -4510,6 +4559,120 @@ async def tf_list_resources(oci_config_id: str = Query(...), compartment_id: str raise HTTPException(500, str(e)[:500]) +TFP_SYSTEM_PROMPT = """Você é um arquiteto sênior de infraestrutura OCI com profundo conhecimento do Terraform OCI Provider. +O usuário vai descrever a infraestrutura desejada em linguagem natural. Sua tarefa é gerar um **prompt detalhado e estruturado** que será enviado a um agente Terraform para criar o código HCL. + +### Formato do Prompt Gerado +Retorne APENAS o prompt estruturado (não gere código Terraform). Use markdown: + +1. **Título**: Uma linha descrevendo o projeto (ex: "## Infraestrutura Multi-Region HA para Aplicação Web") +2. **Arquitetura**: Diagrama textual ou descrição da topologia +3. **Recursos por Categoria**: Organizados em seções ## (Networking, Compute, Storage, Database, Security, Observability) + - Para cada recurso: nome, especificações técnicas, relacionamentos +4. **Networking**: Sempre inclua CIDRs (10.0.0.0/16 para VCN, /24 para subnets), gateways necessários, route tables, security lists/NSGs +5. **Dependências**: Liste dependências entre recursos (ex: "Compute depende de Subnet, que depende de VCN") +6. **Requisitos Técnicos**: Seção final com regras para o agente Terraform: + - Separação em arquivos (provider.tf, variables.tf, vcn.tf, subnets.tf, compute.tf, outputs.tf) + - Variáveis obrigatórias (compartment_id, region, ssh_public_key, etc.) + - Naming convention (ex: proj-env-resource) + - Outputs necessários (IPs, OCIDs, endpoints) + - Tags padrão (Environment, Project, ManagedBy=Terraform) + +### Regras de Inferência +- Se pedir VCN, inclua automaticamente: subnets (pública + privada), internet gateway, NAT gateway, service gateway, route tables, security lists +- Se pedir Compute, inclua: boot volume, NSG, cloud-init básico, shape e imagem +- Se pedir Database, inclua: subnet privada dedicada, NSG com porta do DB, backup policy +- Se pedir OKE, inclua: VCN com topologia para K8s (API endpoint subnet, workers subnet, pods subnet, services LB subnet) +- Se mencionar multi-region, use providers aliased e detalhe RPC/DRG +- Se mencionar HA/DR, inclua redundância cross-AD ou cross-region +- Se mencionar segurança/CIS, inclua Vault, Cloud Guard, WAF, logging, encryption at rest + +### Restrições do OCI Provider +- RPC (Remote Peering Connection): NÃO usar oci_core_drg_attachment para RPC. O attachment é criado automaticamente pelo recurso oci_core_remote_peering_connection +- Cada VCN suporta apenas 1 DRG attachment — não criar duplicados +- DRG attachment type para VCN é "VCN", não "REMOTE_PEERING_CONNECTION" + +### Restrições do Workspace +- NUNCA sugira o uso de `module` blocks. O workspace é flat (diretório único, sem subdiretórios). Toda infraestrutura deve ser definida com `resource` e `data` blocks diretamente. +- Variáveis devem ser declaradas SOMENTE em `variables.tf`. Nunca duplicar declarações de variáveis entre arquivos. + +### Tom +- Seja técnico e preciso +- Use português brasileiro +- Não gere código, apenas o prompt estruturado""" + +class TfPromptReq(BaseModel): + message: str + genai_config: Optional[dict] = None + history: Optional[list] = None + session_id: Optional[str] = None + +@app.post("/api/terraform/generate-prompt") +async def tf_generate_prompt(req: TfPromptReq, u=Depends(current_user)): + gc = None + if req.genai_config: + if req.genai_config.get("genai_config_id"): + with db() as c: + row = c.execute("SELECT * FROM genai_configs WHERE id=?", (req.genai_config["genai_config_id"],)).fetchone() + if row: gc = dict(row) + elif req.genai_config.get("oci_config_id"): + oci_id = req.genai_config["oci_config_id"] + with db() as c: + oc = c.execute("SELECT * FROM oci_configs WHERE id=?", (oci_id,)).fetchone() + if oc: + region = req.genai_config.get("genai_region") or oc["region"] + compartment = _safe_dec(dict(oc).get("compartment_id") or oc["tenancy_ocid"]) + gc = { + "oci_config_id": oci_id, "model_id": req.genai_config.get("model_id", "openai.gpt-4.1"), + "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": 0.7, "max_tokens": 4000, "top_p": 0.9, + } + if not gc: + with db() as c: + row = c.execute("SELECT * FROM genai_configs WHERE is_default=1").fetchone() + if not row: + row = c.execute("SELECT * FROM genai_configs LIMIT 1").fetchone() + if row: gc = dict(row) + if not gc: + raise HTTPException(400, "Nenhuma configuração GenAI disponível") + # Session persistence + is_new = not req.session_id + sid = req.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,status) VALUES (?,?,?,?,?,?,?)", + (str(uuid.uuid4()), sid, u["id"], "user", req.message, gc.get("model_id"), "done")) + if is_new: + title = (req.message or "Prompt Generator")[:80].strip() + c.execute("INSERT OR IGNORE INTO chat_sessions (id,user_id,agent_type,title) VALUES (?,?,?,?)", + (sid, u["id"], "tf-prompt", title)) + else: + c.execute("UPDATE chat_sessions SET updated_at=datetime('now') WHERE id=?", (sid,)) + # Load TF resource reference for context + tf_ref = _load_tf_resource_reference() + system_with_ref = TFP_SYSTEM_PROMPT + if tf_ref: + system_with_ref += f"\n\n### Referência de Recursos OCI Terraform Disponíveis\nUse esta referência para garantir que os recursos mencionados no prompt existam no provider OCI:\n\n{tf_ref}" + gc["system_prompt"] = system_with_ref + gc["temperature"] = 0.7 + gc["max_tokens"] = 4000 + # Build history from conversation (normalize roles to lowercase for _call_genai) + hist = None + if req.history: + hist = [{"role": "user" if m.get("role","").upper() in ("USER","user") else "assistant", + "content": m.get("content", "")} for m in req.history] + try: + answer, _, _ = _call_genai(gc, req.message, history=hist) + # Save assistant response + 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", answer, gc.get("model_id"), "done")) + return {"prompt": answer, "session_id": sid} + except Exception as e: + log.error(f"tf_generate_prompt error: {e}") + raise HTTPException(500, str(e)[:500]) + @app.post("/api/terraform/chat") async def terraform_chat(msg: ChatMsg, bg: BackgroundTasks, u=Depends(current_user)): sid, mid_or_result, genai_cfg = _chat_start(msg, u, agent_type="terraform") @@ -5276,12 +5439,12 @@ async def import_config(file: UploadFile = File(...), u=Depends(require("admin") for cfg in data.get("genai_configs", []): if c.execute("SELECT 1 FROM genai_configs WHERE id=?", (cfg["id"],)).fetchone(): counts["skipped"] += 1; continue - 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,system_prompt,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", + 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,reasoning_effort,is_default,system_prompt,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", (cfg["id"], cfg.get("user_id", u["id"]), cfg.get("name",""), cfg.get("oci_config_id",""), cfg.get("model_id",""), cfg.get("model_ocid"), cfg.get("compartment_id",""), cfg.get("genai_region",""), cfg.get("endpoint",""), cfg.get("serving_type","ON_DEMAND"), cfg.get("dedicated_endpoint_id"), cfg.get("temperature",1), cfg.get("max_tokens",6000), cfg.get("top_p",0.95), cfg.get("top_k",1), cfg.get("frequency_penalty",0), - cfg.get("presence_penalty",0), cfg.get("is_default",0), cfg.get("system_prompt",""), cfg.get("created_at"))) + cfg.get("presence_penalty",0), cfg.get("reasoning_effort"), cfg.get("is_default",0), cfg.get("system_prompt",""), cfg.get("created_at"))) counts["genai_configs"] += 1 # MCP servers for srv in data.get("mcp_servers", []): diff --git a/frontend/index.html b/frontend/index.html index 1c36bc9..e11f3ac 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -379,6 +379,10 @@ html .spinner,html [class*="kpi-bar-fill"],html canvas{transition:none!important .tf-chat{flex:1;display:flex;flex-direction:column;overflow:hidden;min-height:0} .tf-toolbar{display:flex;align-items:center;gap:.5rem;padding:.45rem .7rem;border-bottom:1px solid var(--bd);flex-wrap:wrap;background:var(--bg)} .tf-msgs{flex:1;overflow-y:auto;padding:.7rem} +.tfp-act{display:inline-flex;align-items:center;gap:.25rem;padding:.2rem .5rem;font-size:.6rem;border-radius:5px;border:1px solid var(--bd);background:var(--bg2);color:var(--t2);cursor:pointer;transition:all .15s;font-family:inherit;line-height:1.3} +.tfp-act:hover{background:var(--bg3);border-color:var(--t4)} +.tfp-act-tf{background:#7b42bc18;border-color:#7b42bc55;color:#7b42bc} +.tfp-act-tf:hover{background:#7b42bc30;border-color:#7b42bc} .tf-input{display:flex;gap:.5rem;padding:.5rem .7rem;border-top:1px solid var(--bd)} .tf-input textarea,.tf-input input{flex:1;font-family:inherit;font-size:inherit;line-height:1.4;padding:.45rem .65rem;border:1.5px solid var(--bd);border-radius:10px;background:var(--bg2);min-height:36px;max-height:200px} .tf-resize{height:5px;cursor:row-resize;background:var(--bd);transition:background .15s;position:relative;flex-shrink:0} @@ -523,14 +527,16 @@ const LOGO_R=`${rSb()}

${rTb()}

${LOGO_W} OCI CIS Agent

Infrastructure & Security · v${V}
`} -function rTb(){const t={'chat':IC.chat+' AI Agent Chat','terraform':TF_IC+' Terraform Agent','explorer':IC.search+' OCI Account Explorer','report':IC.chart+' Compliance Reports','downloads':IC.folder+' Downloads','oci-config':IC.cloud+' Credenciais OCI','genai':IC.brain+' OCI Generative AI','mcp':IC.plug+' MCP Servers','adb':IC.db+' Autonomous DB Vector','embeddings':IC.dna+' Embeddings','emb-consult':IC.search+' Consultar Embeddings','users':IC.users+' Gerenciar Usuários','mfa':IC.lock+' Autenticação MFA','audit':IC.log+' Audit Log'}; +function rTb(){const t={'chat':IC.chat+' AI Agent Chat','terraform':TF_IC+' Terraform Agent','tf-prompt':IC.edit+' Prompt Generator','explorer':IC.search+' OCI Account Explorer','report':IC.chart+' Compliance Reports','downloads':IC.folder+' Downloads','oci-config':IC.cloud+' Credenciais OCI','genai':IC.brain+' OCI Generative AI','mcp':IC.plug+' MCP Servers','adb':IC.db+' Autonomous DB Vector','embeddings':IC.dna+' Embeddings','emb-consult':IC.search+' Consultar Embeddings','users':IC.users+' Gerenciar Usuários','mfa':IC.lock+' Autenticação MFA','audit':IC.log+' Audit Log'}; return`
${t[S.tab]||''}
v${V}
`} -function rPg(){switch(S.tab){case'chat':return rChat();case'terraform':return rTerraform();case'explorer':return rExplorer();case'report':return rReport();case'downloads':return rDl();case'oci-config':return rOci();case'genai':return rGenAI();case'mcp':return rMCP();case'adb':return rADB();case'embeddings':return rEmbeddings();case'emb-consult':return rEmbConsultPage();case'users':return rUsers();case'mfa':return rMfa();case'audit':return rAudit();default:return''}} +function rPg(){switch(S.tab){case'chat':return rChat();case'terraform':return rTerraform();case'tf-prompt':return rTfPrompt();case'explorer':return rExplorer();case'report':return rReport();case'downloads':return rDl();case'oci-config':return rOci();case'genai':return rGenAI();case'mcp':return rMCP();case'adb':return rADB();case'embeddings':return rEmbeddings();case'emb-consult':return rEmbConsultPage();case'users':return rUsers();case'mfa':return rMfa();case'audit':return rAudit();default:return''}} /* ── Chat ── */ function rChat(){ @@ -672,16 +678,27 @@ ${S.chatFiles.length?`
${IC.clip}
`} +function _getModelInfo(){ + let mid=''; + if(S.chatModel.startsWith('cfg:')){const g=S.genaiCfg.find(x=>x.id===S.chatModel.slice(4));if(g)mid=g.model_id} + else mid=S.chatModel; + return S.models[mid]||{}} +function _isReasoningModel(){return _getModelInfo().reasoning||false} function rChatConfig(isDirect,toolCount){ - const pp=S.chatParams; + const pp=S.chatParams;const mi=_getModelInfo();const pv=mi.provider||'';const isR=mi.reasoning||false; + const maxLim=mi.max_tokens||128000; + const hasTopK=pv==='meta'||pv==='google'||pv===''; + const hasPenalties=mi.penalties===true; return`
${IC.settings} Configurações
-
-
+${isR?`
+
` +:`
+
-
-
-
+${hasTopK?`
`:''} +${hasPenalties?`
+
`:''}`}
@@ -753,6 +770,7 @@ async function sChat(){const el=document.getElementById('chi');const m=el.value. fd.append('temperature',S.chatParams.temperature);fd.append('max_tokens',S.chatParams.max_tokens); fd.append('top_p',S.chatParams.top_p);fd.append('top_k',S.chatParams.top_k); fd.append('frequency_penalty',S.chatParams.frequency_penalty);fd.append('presence_penalty',S.chatParams.presence_penalty); + if(_isReasoningModel()&&S.chatParams.reasoning_effort)fd.append('reasoning_effort',S.chatParams.reasoning_effort); S.chatFiles.forEach(f=>fd.append('files',f.file)); d=await $api('/chat/upload',{method:'POST',body:fd});S.chatFiles=[] }else{ @@ -760,12 +778,14 @@ async function sChat(){const el=document.getElementById('chi');const m=el.value. if(S.chatModel.startsWith('cfg:')){ body.genai_config_id=S.chatModel.slice(4); body.temperature=S.chatParams.temperature;body.max_tokens=S.chatParams.max_tokens;body.top_p=S.chatParams.top_p; - 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; + if(_isReasoningModel()&&S.chatParams.reasoning_effort)body.reasoning_effort=S.chatParams.reasoning_effort} else if(S.chatModel){ if(!S.chatOci){S.msgs.pop();S.msgs.push({r:'assistant',c:IC.warn+' Selecione uma credencial OCI.'});R();el.disabled=false;btns.forEach(b=>{b.disabled=false});if(sendBtn)sendBtn.textContent=sendBtn.dataset.origText;return} body.model_id=S.chatModel;body.oci_config_id=S.chatOci;body.genai_region=S.chatRegion;body.compartment_id=S.chatCompartment; body.temperature=S.chatParams.temperature;body.max_tokens=S.chatParams.max_tokens;body.top_p=S.chatParams.top_p; - 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; + if(_isReasoningModel()&&S.chatParams.reasoning_effort)body.reasoning_effort=S.chatParams.reasoning_effort} d=await $api('/chat',{method:'POST',body})} S.sid=d.session_id; if(S.chatHistOpen)loadHistory('chat'); @@ -846,14 +866,16 @@ async function tfRefreshRef(){ async function loadHistory(type){ try{const h=await $api('/chat/sessions?agent_type='+type+'&limit=50'); - if(type==='chat')S.chatHistory=h;else S.tfHistory=h} - catch(e){if(type==='chat')S.chatHistory=[];else S.tfHistory=[]} + if(type==='chat')S.chatHistory=h;else if(type==='tf-prompt')S.tfpHistory=h;else S.tfHistory=h} + catch(e){if(type==='chat')S.chatHistory=[];else if(type==='tf-prompt')S.tfpHistory=[];else S.tfHistory=[]} R(); } async function loadSession(sid,type){ try{const d=await $api('/chat/sessions/'+sid+'/messages'); if(type==='chat'){ S.sid=sid;S.msgs=d.messages.map(m=>({r:m.role,c:m.content,t:m.created_at?.slice(11,16)||''}));R();scCh(); + }else if(type==='tf-prompt'){ + S.tfpSid=sid;S.tfpMsgs=d.messages.map(m=>({r:m.role,c:m.content,_raw:m.role==='assistant'?m.content:undefined,t:m.created_at?.slice(11,16)||''}));R(); }else{ S.tfSid=sid;S.tfMsgs=d.messages.map(m=>({r:m.role,c:m.content,t:m.created_at?.slice(11,16)||''})); S.tfFiles=[];S.tfCode='';S.tfPlan=[];S.tfPlanOut='';S.tfApplyOut='';S.tfDestroyOut=''; @@ -903,6 +925,7 @@ async function delSession(sid,type){ if(!confirm('Excluir esta conversa?'))return; try{await $api('/chat/'+sid,{method:'DELETE'})}catch(e){} if(type==='chat'){if(S.sid===sid)newChat();S.chatHistory=S.chatHistory.filter(s=>s.id!==sid)} + else if(type==='tf-prompt'){if(S.tfpSid===sid){S.tfpMsgs=[];S.tfpSid=null}S.tfpHistory=S.tfpHistory.filter(s=>s.id!==sid)} else{if(S.tfSid===sid)newTfChat();S.tfHistory=S.tfHistory.filter(s=>s.id!==sid)} R(); } @@ -1160,16 +1183,106 @@ function fmTf(t){ } function tfCopyBlock(i){const b=S.tfFiles[i];if(b)navigator.clipboard.writeText(b.content).then(()=>alert('Copiado!'))} +/* ── Terraform Prompt Generator ── */ +const TFP_EXAMPLES=[ + 'VCN com subnets pública e privada, internet gateway, NAT gateway e 2 compute instances Ubuntu', + 'Ambiente multi-region MAD1 + MAD3 com DRG, RPC e VCNs espelhadas', + 'Cluster OKE com 3 node pools, load balancer e container registry', + 'Banco Autonomous Database com vault, encryption key e bastion para acesso', + 'Infraestrutura completa: VCN, compute, block storage, object storage, IAM policies e monitoring', +]; +function rTfPrompt(){ + const msgs=S.tfpMsgs; + const msHtml=msgs.length===0 + ?`
${IC.edit}

Descreva a infraestrutura OCI que você precisa

O agente vai gerar um prompt estruturado e otimizado para o Terraform Agent.

+
${TFP_EXAMPLES.map(e=>``).join('')}
` + :msgs.map((m,i)=>`
${fm(m.c)}
${m._raw?`
+ +
`:''}
`).join(''); + // Model dropdown — only recommended models, grouped by provider + const tfpAllowed=['openai.gpt-4.1','openai.o3','openai.o4-mini','openai.gpt-5.1','openai.gpt-5.2','google.gemini-2.5-pro','google.gemini-2.5-flash']; + const tfpProvs={};tfpAllowed.forEach(mid=>{const mi=S.models[mid];if(mi){const p=mi.provider||'other';if(!tfpProvs[p])tfpProvs[p]=[];tfpProvs[p].push({id:mid,name:mi.name})}}); + const tfpProvOrder=['openai','google']; + let ddItems=''; + if(S.genaiCfg.length){const cfgFiltered=S.genaiCfg.filter(g=>tfpAllowed.includes(g.model_id));if(cfgFiltered.length){ddItems+='
Configs Salvas
';cfgFiltered.forEach(g=>{const mi=S.models[g.model_id];ddItems+=`
${g.name||mi?.name||g.model_id}
`})}} + tfpProvOrder.filter(p=>tfpProvs[p]).forEach(p=>{ddItems+=`
${p[0].toUpperCase()+p.slice(1)}
`;tfpProvs[p].forEach(m=>{ddItems+=`
${m.name}
`})}); + let curLabel='Selecione modelo...'; + if(S.tfpModel.startsWith('cfg:')){const g=S.genaiCfg.find(x=>x.id===S.tfpModel.slice(4));curLabel=g?(g.name||g.model_id):'Config...'} + else if(S.tfpModel){const mi=S.models[S.tfpModel];curLabel=mi?mi.name:S.tfpModel} + const isDirect=S.tfpModel&&!S.tfpModel.startsWith('cfg:'); + const ociSel=isDirect?``:''; + return`
+
+
+
${IC.history} Histórico
+ Nova Conversa
+
${rHistList('tf-prompt',S.tfpHistory,S.tfpSid)}
+
+
+
+
+ ${IC.edit} +
${curLabel}
+
${ddItems}
+
+ ${ociSel} +
+
+
${msHtml}${S.tfpLoading?'
Gerando prompt...
':''}
+
+ + +
+
+
`} +function tfpToggleModelDrop(){const dd=document.getElementById('tfpmdd');if(!dd)return;dd.classList.toggle('open');if(dd.classList.contains('open'))document.getElementById('tfpmds')?.focus()} +function tfpFilterModels(q){const l=document.getElementById('tfpmdl');if(!l)return;l.querySelectorAll('.itm').forEach(el=>{el.style.display=el.textContent.toLowerCase().includes(q.toLowerCase())?'':'none'})} +async function tfpSend(){ + const el=document.getElementById('tfpi');const m=(el?.value||'').trim();if(!m||S.tfpLoading)return; + el.value='';el.style.height='auto'; + S.tfpMsgs.push({r:'user',c:m});S.tfpLoading=true;R(); + const mc=document.getElementById('tfpchm');if(mc)mc.scrollTop=mc.scrollHeight; + try{ + const gc=await _tfpResolveGenai(); + const hist=S.tfpMsgs.slice(0,-1).filter(x=>x.r==='user'||x.r==='assistant').map(x=>({role:x.r==='user'?'USER':'CHATBOT',content:x._raw||x.c})); + const body={message:m,genai_config:gc,history:hist.length?hist:null}; + if(S.tfpSid)body.session_id=S.tfpSid; + const d=await $api('/terraform/generate-prompt',{method:'POST',body}); + if(d.session_id)S.tfpSid=d.session_id; + S.tfpMsgs.push({r:'assistant',c:d.prompt,_raw:d.prompt}); + }catch(e){S.tfpMsgs.push({r:'assistant',c:IC.err+' Erro: '+e.message})} + S.tfpLoading=false;R(); + const mc2=document.getElementById('tfpchm');if(mc2)mc2.scrollTop=mc2.scrollHeight} +function tfpPickModel(v){S.tfpModel=v;S.tfpMdOpen=false; + if(v&&!v.startsWith('cfg:')&&!S.tfpOci&&S.ociCfg.length){S.tfpOci=S.ociCfg[0].id;S.tfpRegion=S.ociCfg[0].region;S.tfpCompartment=S.ociCfg[0].compartment_id||''} + else if(v&&v.startsWith('cfg:')){const g=S.genaiCfg.find(x=>x.id===v.slice(4));if(g&&g.oci_config_id&&!S.tfpOci){S.tfpOci=g.oci_config_id;const c=S.ociCfg.find(x=>x.id===g.oci_config_id);if(c){S.tfpRegion=c.region;S.tfpCompartment=c.compartment_id||''}}} + R()} +async function _tfpResolveGenai(){ + if(S.tfpModel.startsWith('cfg:'))return{genai_config_id:S.tfpModel.slice(4)}; + if(S.tfpModel&&S.tfpOci)return{oci_config_id:S.tfpOci,model_id:S.tfpModel,genai_region:S.tfpRegion,compartment_id:S.tfpCompartment}; + const def=S.genaiCfg.find(g=>g.is_default)||S.genaiCfg[0]; + if(def)return{genai_config_id:def.id}; + if(!S.ociCfg.length)return{}; + const oc=S.ociCfg[0]; + return{oci_config_id:oc.id,model_id:'openai.gpt-4.1',genai_region:oc.region,compartment_id:oc.compartment_id}} +function tfpCopy(idx){ + const m=S.tfpMsgs[idx];if(!m||!m._raw)return; + navigator.clipboard.writeText(m._raw).then(()=>{m._copied=true;R();setTimeout(()=>{m._copied=false;R()},2000)})} +function tfpSendToTf(idx){ + const m=S.tfpMsgs[idx];if(!m||!m._raw)return; + S.tab='terraform'; + setTimeout(()=>{const el=document.getElementById('tfi');if(el){el.value=m._raw;el.style.height='auto';el.style.height=el.scrollHeight+'px'}R()},100)} + function rTerraform(){ const ms=S.tfMsgs.length===0 ?`
${TF_ICON}

Descreva a infraestrutura OCI desejada

Ex: "Crie uma VCN com subnets pública e privada, internet gateway e um compute instance"

` :S.tfMsgs.map(m=>`
${m.r==='assistant'?fmTf(m.c):fm(m.c)}
${m.t?`
${m.t}
`:''}
`).join(''); - // Model dropdown - const provs={};for(const[mid,info]of Object.entries(S.models)){const p=info.provider||'other';if(!provs[p])provs[p]=[];provs[p].push({id:mid,name:info.name})} - const provOrder=['openai','google','meta','xai']; + // Model dropdown — same filtered models as Prompt Generator, grouped by provider + const tfAllowed=['openai.gpt-4.1','openai.o3','openai.o4-mini','openai.gpt-5.1','openai.gpt-5.2','google.gemini-2.5-pro','google.gemini-2.5-flash']; + const provs={};tfAllowed.forEach(mid=>{const mi=S.models[mid];if(mi){const p=mi.provider||'other';if(!provs[p])provs[p]=[];provs[p].push({id:mid,name:mi.name})}}); + const provOrder=['openai','google']; let ddItems=''; - if(S.genaiCfg.length){ddItems+='
Configs Salvas
';S.genaiCfg.forEach(g=>{const mi=S.models[g.model_id];ddItems+=`
${g.name||mi?.name||g.model_id}
`})} + if(S.genaiCfg.length){const cfgF=S.genaiCfg.filter(g=>tfAllowed.includes(g.model_id));if(cfgF.length){ddItems+='
Configs Salvas
';cfgF.forEach(g=>{const mi=S.models[g.model_id];ddItems+=`
${g.name||mi?.name||g.model_id}
`})}} provOrder.filter(p=>provs[p]).forEach(p=>{ddItems+=`
${p[0].toUpperCase()+p.slice(1)}
`;provs[p].forEach(m=>{ddItems+=`
${m.name}
`})}); let curLabel='Selecione modelo...'; if(S.tfModel.startsWith('cfg:')){const g=S.genaiCfg.find(x=>x.id===S.tfModel.slice(4));curLabel=g?(g.name||g.model_id):'Config...'} @@ -1216,7 +1329,7 @@ function rTerraform(){
${ms}
- +
@@ -1904,7 +2017,7 @@ ${S.rptRselOpen?`
Nenhum relatório encontrado
'} `:''} -${rKpi()}${rRunRpt()}${rReportHistory()}`} +
${rKpi()}
${rRunRpt()}${rReportHistory()}`} function rRunRpt(){if(S.user?.role==='viewer')return''; const selTags=S.rptSelRegions.map(r=>`${r}×`).join(' '); const f=S.rptRegionFilter.toLowerCase(); @@ -1968,7 +2081,7 @@ function rptPickRegion(r){S.rptSelRegions.push(r);S.rptRegionFilter='';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 rptPickRsel(id){S.rptRselVal=id;S.rptRselOpen=false;S.rptRselFilter='';const f=document.getElementById('rfr');if(f)f.src=API+'/reports/'+id+'/html';loadRptKpi(id);R()} -async function loadRptKpi(rid){S.rptKpiLoading=true;S.rptKpi=null;R();try{S.rptKpi=await $api('/reports/'+rid+'/summary')}catch(e){S.rptKpi=null}S.rptKpiLoading=false;R()} +async function loadRptKpi(rid){S.rptKpiLoading=true;S.rptKpi=null;const w=document.getElementById('kpi-wrap');if(w)w.innerHTML=rKpi();try{S.rptKpi=await $api('/reports/'+rid+'/summary')}catch(e){S.rptKpi=null}S.rptKpiLoading=false;const w2=document.getElementById('kpi-wrap');if(w2)w2.innerHTML=rKpi()} let _kpiCharts={}; function _destroyKpiCharts(){Object.values(_kpiCharts).forEach(c=>{try{c.destroy()}catch(e){}});_kpiCharts={}} function _initKpiCharts(){