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)
This commit is contained in:
22
README.md
22
README.md
@@ -9,7 +9,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://img.shields.io/badge/version-2.3-C74634?style=flat-square" alt="Version">
|
<img src="https://img.shields.io/badge/version-2.4-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">
|
||||||
@@ -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)
|
- 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
|
- 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
|
||||||
- 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
|
- Toggle MCP tools on/off per chat session
|
||||||
- Conversation history with session management
|
- Conversation history with session management
|
||||||
- On-Demand and Dedicated serving modes
|
- 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)
|
- **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
|
- **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)
|
- 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
|
### ⚡ OCI Resource Actions
|
||||||
- **Start/Stop Compute Instances** directly from OCI Account Explorer with one click
|
- **Start/Stop Compute Instances** directly from OCI Account Explorer with one click
|
||||||
@@ -396,14 +409,14 @@ Allow group <group-name> to read buckets in compartment <compartment-name>
|
|||||||
```
|
```
|
||||||
oci-cis-agent/
|
oci-cis-agent/
|
||||||
├── backend/
|
├── backend/
|
||||||
│ ├── app.py # FastAPI application (~5300 lines)
|
│ ├── app.py # FastAPI application (~5500 lines)
|
||||||
│ ├── cis_reports.py # Oracle CIS Benchmark checker (6660 lines, report engine)
|
│ ├── cis_reports.py # Oracle CIS Benchmark checker (6660 lines, report engine)
|
||||||
│ ├── mcp_cis_server.py # MCP server with 12 granular CIS tools (~700 lines)
|
│ ├── mcp_cis_server.py # MCP server with 12 granular CIS tools (~700 lines)
|
||||||
│ ├── gen_tf_reference.py # OCI Terraform provider resource catalog generator
|
│ ├── gen_tf_reference.py # OCI Terraform provider resource catalog generator
|
||||||
│ ├── Dockerfile # Python 3.12 + OCI CLI + Terraform CLI
|
│ ├── Dockerfile # Python 3.12 + OCI CLI + Terraform CLI
|
||||||
│ └── requirements.txt # Dependencies
|
│ └── requirements.txt # Dependencies
|
||||||
├── frontend/
|
├── frontend/
|
||||||
│ └── index.html # SPA with Oracle Dark Premium theme (~2820 lines)
|
│ └── index.html # SPA with Oracle Dark Premium theme (~2950 lines)
|
||||||
├── nginx/
|
├── nginx/
|
||||||
│ └── default.conf # Reverse proxy config
|
│ └── default.conf # Reverse proxy config
|
||||||
├── docker-compose.yml # Orchestration
|
├── docker-compose.yml # Orchestration
|
||||||
@@ -560,6 +573,7 @@ oci-cis-agent/
|
|||||||
| POST | `/api/terraform/workspaces/{wid}/cancel` | Cancel running Terraform operation |
|
| POST | `/api/terraform/workspaces/{wid}/cancel` | Cancel running Terraform operation |
|
||||||
| DELETE | `/api/terraform/workspaces/{wid}` | Delete workspace |
|
| DELETE | `/api/terraform/workspaces/{wid}` | Delete workspace |
|
||||||
| POST | `/api/terraform/refresh-reference` | Regenerate OCI Terraform resource reference (UI button) |
|
| 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
|
### Chat & Reports
|
||||||
|
|
||||||
|
|||||||
233
backend/app.py
233
backend/app.py
@@ -62,38 +62,38 @@ security = HTTPBearer()
|
|||||||
# _call_genai resolves the OCID for the configured region at runtime.
|
# _call_genai resolves the OCID for the configured region at runtime.
|
||||||
GENAI_MODELS = {
|
GENAI_MODELS = {
|
||||||
# ── Meta ──
|
# ── 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"}},
|
"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"}},
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaw23hfc7mtvv5wef3gwvvaguyzqmhb5lx4r5s3y2xzc4a"}},
|
||||||
# ── Google ──
|
# ── 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"}},
|
"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"}},
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaeo4ehrn25guuats5s45hnvswlhxo6riop275l2bkr2vq"}},
|
||||||
# ── OpenAI ──
|
# ── 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"}},
|
"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"}},
|
"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"}},
|
"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"}},
|
"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"}},
|
"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"}},
|
"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"}},
|
"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"}},
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyalgnrukpjk6wm5zsf4jzkoneahgswhrk7kukkoagwnzma"}},
|
||||||
# ── xAI ──
|
# ── 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"}},
|
"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"}},
|
"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"}},
|
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyauvjoll2repj5pbtkk7pinwj57ex3lkehzpxd6v6rxscq"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,6 +197,7 @@ def init_db():
|
|||||||
top_k INTEGER DEFAULT 1,
|
top_k INTEGER DEFAULT 1,
|
||||||
frequency_penalty REAL DEFAULT 0,
|
frequency_penalty REAL DEFAULT 0,
|
||||||
presence_penalty REAL DEFAULT 0,
|
presence_penalty REAL DEFAULT 0,
|
||||||
|
reasoning_effort TEXT,
|
||||||
is_default INTEGER DEFAULT 0,
|
is_default INTEGER DEFAULT 0,
|
||||||
system_prompt TEXT DEFAULT '',
|
system_prompt TEXT DEFAULT '',
|
||||||
created_at TEXT DEFAULT (datetime('now')),
|
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 config_logs WHERE created_at < datetime('now', '-30 days')")
|
||||||
c.execute("DELETE FROM chat_logs WHERE created_at < datetime('now', '-30 days')")
|
c.execute("DELETE FROM chat_logs WHERE created_at < datetime('now', '-30 days')")
|
||||||
# ── Migrations ──
|
# ── Migrations ──
|
||||||
for col in ["system_prompt TEXT DEFAULT ''"]:
|
for col in ["system_prompt TEXT DEFAULT ''", "reasoning_effort TEXT"]:
|
||||||
try:
|
try:
|
||||||
c.execute(f"ALTER TABLE genai_configs ADD COLUMN {col}")
|
c.execute(f"ALTER TABLE genai_configs ADD COLUMN {col}")
|
||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
@@ -518,6 +519,7 @@ class ChatMsg(BaseModel):
|
|||||||
temperature: Optional[float] = None; max_tokens: Optional[int] = None
|
temperature: Optional[float] = None; max_tokens: Optional[int] = None
|
||||||
top_p: Optional[float] = None; top_k: Optional[int] = None
|
top_p: Optional[float] = None; top_k: Optional[int] = None
|
||||||
frequency_penalty: Optional[float] = None; presence_penalty: Optional[float] = None
|
frequency_penalty: Optional[float] = None; presence_penalty: Optional[float] = None
|
||||||
|
reasoning_effort: Optional[str] = None
|
||||||
use_tools: Optional[bool] = True
|
use_tools: Optional[bool] = True
|
||||||
class RunReportReq(BaseModel):
|
class RunReportReq(BaseModel):
|
||||||
config_id: str; regions: Optional[List[str]] = None
|
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
|
serving_type: str = "ON_DEMAND"; dedicated_endpoint_id: Optional[str] = None
|
||||||
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
|
||||||
|
reasoning_effort: Optional[str] = None
|
||||||
is_default: bool = False
|
is_default: bool = False
|
||||||
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
|
||||||
@@ -1638,11 +1641,11 @@ async def save_genai(req: GenAIConfigReq, u=Depends(require("admin","user"))):
|
|||||||
c.execute(
|
c.execute(
|
||||||
"""INSERT INTO genai_configs (id,user_id,name,oci_config_id,model_id,model_ocid,compartment_id,
|
"""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,
|
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,
|
(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.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.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)
|
_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"])
|
_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}
|
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(
|
c.execute(
|
||||||
"""UPDATE genai_configs SET name=?,oci_config_id=?,model_id=?,model_ocid=?,compartment_id=?,
|
"""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=?,
|
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.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.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.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)
|
_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"])
|
_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}
|
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:
|
if system_prompt:
|
||||||
chat_request.preamble_override = system_prompt
|
chat_request.preamble_override = system_prompt
|
||||||
chat_request.message = message
|
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.temperature = float(gc.get("temperature", 1))
|
||||||
chat_request.frequency_penalty = float(gc.get("frequency_penalty", 0))
|
chat_request.frequency_penalty = float(gc.get("frequency_penalty", 0))
|
||||||
chat_request.presence_penalty = float(gc.get("presence_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
|
chat_request.api_format = oci.generative_ai_inference.models.BaseChatRequest.API_FORMAT_GENERIC
|
||||||
provider = model_info.get("provider", "")
|
provider = model_info.get("provider", "")
|
||||||
is_openai = provider == "openai"
|
is_openai = provider == "openai"
|
||||||
is_xai = provider == "xai"
|
is_reasoning = model_info.get("reasoning", False)
|
||||||
# OpenAI models use max_completion_tokens instead of max_tokens
|
# Clamp max_tokens to model limit
|
||||||
# and do not support top_k
|
model_max = model_info.get("max_tokens", 16384)
|
||||||
if is_openai:
|
requested_tokens = int(gc.get("max_tokens", 6000))
|
||||||
chat_request.max_completion_tokens = int(gc.get("max_tokens", 6000))
|
clamped_tokens = min(requested_tokens, model_max)
|
||||||
else:
|
|
||||||
chat_request.max_tokens = int(gc.get("max_tokens", 6000))
|
# ── Parameter matrix per provider ──
|
||||||
if not is_xai:
|
# OpenAI reasoning (o3, o4-mini): max_completion_tokens + reasoning_effort only
|
||||||
chat_request.top_k = int(gc.get("top_k", 1))
|
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.temperature = float(gc.get("temperature", 1))
|
||||||
if not is_xai:
|
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.frequency_penalty = float(gc.get("frequency_penalty", 0))
|
||||||
chat_request.presence_penalty = float(gc.get("presence_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))
|
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 = 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 = []
|
messages = []
|
||||||
if system_prompt:
|
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).
|
- 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`.
|
- 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`.
|
- `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) ──
|
# ── 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.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.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
|
||||||
|
if msg.reasoning_effort is not None: genai_cfg["reasoning_effort"] = msg.reasoning_effort
|
||||||
elif msg.model_id and msg.oci_config_id:
|
elif msg.model_id and msg.oci_config_id:
|
||||||
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()
|
||||||
@@ -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,
|
"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,
|
"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,
|
"presence_penalty": msg.presence_penalty if msg.presence_penalty is not None else 0.0,
|
||||||
|
"reasoning_effort": msg.reasoning_effort,
|
||||||
}
|
}
|
||||||
|
|
||||||
if not genai_cfg:
|
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", "")
|
base_prompt = cfg_dict.get("system_prompt", "")
|
||||||
cfg_dict["system_prompt"] = f"{base_prompt}\n\n{config_hint}" if base_prompt else config_hint
|
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":
|
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 ──
|
# ── Inject existing OCI resources for terraform agent ──
|
||||||
if agent_type == "terraform" and active_oci_id:
|
if agent_type == "terraform" and active_oci_id:
|
||||||
@@ -4184,6 +4231,7 @@ async def chat_with_files(
|
|||||||
top_k: Optional[int] = Form(None),
|
top_k: Optional[int] = Form(None),
|
||||||
frequency_penalty: Optional[float] = Form(None),
|
frequency_penalty: Optional[float] = Form(None),
|
||||||
presence_penalty: Optional[float] = Form(None),
|
presence_penalty: Optional[float] = Form(None),
|
||||||
|
reasoning_effort: Optional[str] = Form(None),
|
||||||
files: List[UploadFile] = File(default=[]),
|
files: List[UploadFile] = File(default=[]),
|
||||||
u=Depends(current_user),
|
u=Depends(current_user),
|
||||||
):
|
):
|
||||||
@@ -4226,6 +4274,7 @@ async def chat_with_files(
|
|||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
frequency_penalty=frequency_penalty,
|
frequency_penalty=frequency_penalty,
|
||||||
presence_penalty=presence_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)
|
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])
|
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")
|
@app.post("/api/terraform/chat")
|
||||||
async def terraform_chat(msg: ChatMsg, bg: BackgroundTasks, u=Depends(current_user)):
|
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")
|
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", []):
|
for cfg in data.get("genai_configs", []):
|
||||||
if c.execute("SELECT 1 FROM genai_configs WHERE id=?", (cfg["id"],)).fetchone():
|
if c.execute("SELECT 1 FROM genai_configs WHERE id=?", (cfg["id"],)).fetchone():
|
||||||
counts["skipped"] += 1; continue
|
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["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("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("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("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
|
counts["genai_configs"] += 1
|
||||||
# MCP servers
|
# MCP servers
|
||||||
for srv in data.get("mcp_servers", []):
|
for srv in data.get("mcp_servers", []):
|
||||||
|
|||||||
@@ -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-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-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}
|
.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{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-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}
|
.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=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width=
|
|||||||
|
|
||||||
const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],genaiCfg:[],adbCfg:[],mcpSvr:[],users:[],models:{},regions:[],embModels:{},expData:null,expCfg:'',expSelRegions:[],expRegDdOpen:false,expTree:[],expSelComp:'',expCat:'Compute',expResType:'instances',expTreeOpen:{},expLoading:false,expRegions:[],expCounts:{},expTreeW: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,expCfg:'',expSelRegions:[],expRegDdOpen:false,expTree:[],expSelComp:'',expCat:'Compute',expResType:'instances',expTreeOpen:{},expLoading:false,expRegions:[],expCounts:{},expTreeW:null,editing:null,
|
||||||
chatModel:'',chatOci:'',chatRegion:'',chatCompartment:'',chatHistOpen:false,chatHistory:[],tfHistOpen:false,tfHistory:[],
|
chatModel:'',chatOci:'',chatRegion:'',chatCompartment:'',chatHistOpen:false,chatHistory:[],tfHistOpen:false,tfHistory:[],
|
||||||
chatParams:{temperature:1,max_tokens:6000,top_p:0.95,top_k:1,frequency_penalty:0,presence_penalty:0},chatPanel:'',chatUseTools:true,
|
chatParams:{temperature:1,max_tokens:6000,top_p:0.95,top_k:1,frequency_penalty:0,presence_penalty:0,reasoning_effort:'medium'},chatPanel:'',chatUseTools:true,
|
||||||
chatPrompts:[],editingPrompt:null,trackingReportId:null,ociRegions:{},rptSelRegions:[],rptRegionsOpen:false,rptRegionFilter:'',
|
chatPrompts:[],editingPrompt:null,trackingReportId:null,ociRegions:{},rptSelRegions:[],rptRegionsOpen:false,rptRegionFilter:'',
|
||||||
rptOciOpen:false,rptOciFilter:'',rptOciVal:'',rptRselOpen:false,rptRselFilter:'',rptRselVal:'',ociFormRegOpen:false,ociFormRegFilter:'',ociFormRegVal:'',
|
rptOciOpen:false,rptOciFilter:'',rptOciVal:'',rptRselOpen:false,rptRselFilter:'',rptRselVal:'',ociFormRegOpen:false,ociFormRegFilter:'',ociFormRegVal:'',
|
||||||
rptLevel:2,rptObp:false,rptRaw:false,rptRedact:false,reportFiles:{},dlExpandedRid:null,dlTenancyFilter:'',dlSectionFilter:'',
|
rptLevel:2,rptObp:false,rptRaw:false,rptRedact:false,reportFiles:{},dlExpandedRid:null,dlTenancyFilter:'',dlSectionFilter:'',
|
||||||
cisVer:null,cisCheckResult:null,cisUpdating:false,rptHistFilter:'',rptKpi:null,rptKpiLoading:false,chatFiles:[],
|
cisVer:null,cisCheckResult:null,cisUpdating:false,rptHistFilter:'',rptKpi:null,rptKpiLoading:false,chatFiles:[],
|
||||||
tfMsgs:[],tfSid:null,tfModel:'',tfOci:'',tfRegion:'',tfCompartment:'',tfPlan:[],tfCode:'',tfPanel:'',
|
tfMsgs:[],tfSid:null,tfModel:'',tfOci:'',tfRegion:'',tfCompartment:'',tfPlan:[],tfCode:'',tfPanel:'',
|
||||||
tfWs:null,tfWsList:[],tfPlanOut:'',tfApplyOut:'',tfDestroyOut:'',tfStatus:'draft',tfRunning:false,tfConfirmDest:false,tfMdOpen:false,
|
tfWs:null,tfWsList:[],tfPlanOut:'',tfApplyOut:'',tfDestroyOut:'',tfStatus:'draft',tfRunning:false,tfConfirmDest:false,tfMdOpen:false,
|
||||||
tfComps:[],tfCompLoading:false,tfFiles:[],tfBtab:'files',tfEditIdx:-1,tfResources:null,tfResLoading:false,tfRefStatus:'',tfBottomH:null};
|
tfComps:[],tfCompLoading:false,tfFiles:[],tfBtab:'files',tfEditIdx:-1,tfResources:null,tfResLoading:false,tfRefStatus:'',tfBottomH:null,
|
||||||
|
tfpMsgs:[],tfpLoading:false,tfpCopied:false,tfpModel:'',tfpMdOpen:false,tfpOci:'',tfpRegion:'',tfpCompartment:'',
|
||||||
|
tfpHistOpen:false,tfpHistory:[],tfpSid:null};
|
||||||
const API='/api';
|
const API='/api';
|
||||||
|
|
||||||
async function $api(p,o={}){const h={...(o.headers||{})};if(S.token)h['Authorization']='Bearer '+S.token;
|
async function $api(p,o={}){const h={...(o.headers||{})};if(S.token)h['Authorization']='Bearer '+S.token;
|
||||||
@@ -612,14 +618,14 @@ function rApp(){return`<div class="app">${rSb()}<div class="mc">${rTb()}<div cla
|
|||||||
|
|
||||||
const TF_IC='<svg viewBox="0 0 16 16" width="14" height="14" style="vertical-align:-2px;margin-right:2px"><path d="M5.6 1v4.2l3.6 2.1V3.1L5.6 1zm4.4 2.1v4.2l3.6-2.1V1L10 3.1zM1.4 3.5v4.2l3.6 2.1V5.6L1.4 3.5zM5.6 8.1v4.2L9.2 14.4V10.2L5.6 8.1z" fill="#7b42bc"/></svg>';
|
const TF_IC='<svg viewBox="0 0 16 16" width="14" height="14" style="vertical-align:-2px;margin-right:2px"><path d="M5.6 1v4.2l3.6 2.1V3.1L5.6 1zm4.4 2.1v4.2l3.6-2.1V1L10 3.1zM1.4 3.5v4.2l3.6 2.1V5.6L1.4 3.5zM5.6 8.1v4.2L9.2 14.4V10.2L5.6 8.1z" fill="#7b42bc"/></svg>';
|
||||||
function rSb(){
|
function rSb(){
|
||||||
const tabs=[['chat',IC.chat,'Chat Agent'],['terraform',TF_IC,'Terraform'],['explorer',IC.search,'OCI Explorer'],['report',IC.chart,'Reports'],['downloads',IC.folder,'Downloads']];
|
const tabs=[['chat',IC.chat,'Chat Agent'],['terraform',TF_IC,'Terraform'],['tf-prompt',IC.edit,'Prompt Generator','sub'],['explorer',IC.search,'OCI Explorer'],['report',IC.chart,'Reports'],['downloads',IC.folder,'Downloads']];
|
||||||
const ctabs=[['oci-config',IC.cloud,'Credenciais OCI'],['genai',IC.brain,'GenAI Config'],['mcp',IC.plug,'MCP Servers'],['adb',IC.db,'ADB Vector'],['embeddings',IC.dna,'Embeddings'],['emb-consult',IC.search,'Consultar Embeddings','sub']];
|
const ctabs=[['oci-config',IC.cloud,'Credenciais OCI'],['genai',IC.brain,'GenAI Config'],['mcp',IC.plug,'MCP Servers'],['adb',IC.db,'ADB Vector'],['embeddings',IC.dna,'Embeddings'],['emb-consult',IC.search,'Consultar Embeddings','sub']];
|
||||||
const atabs=[['users',IC.users,'Usuários'],['mfa',IC.lock,'MFA'],['audit',IC.log,'Audit Log']];
|
const atabs=[['users',IC.users,'Usuários'],['mfa',IC.lock,'MFA'],['audit',IC.log,'Audit Log']];
|
||||||
const i=(S.user?.first_name||S.user?.username||'?')[0].toUpperCase();
|
const i=(S.user?.first_name||S.user?.username||'?')[0].toUpperCase();
|
||||||
return`<div class="sb">
|
return`<div class="sb">
|
||||||
<div class="sb-h"><h1>${LOGO_W} OCI CIS Agent</h1><div class="st">Infrastructure & Security · v${V}</div></div>
|
<div class="sb-h"><h1>${LOGO_W} OCI CIS Agent</h1><div class="st">Infrastructure & Security · v${V}</div></div>
|
||||||
<div class="nav"><div class="nl">Principal</div>
|
<div class="nav"><div class="nl">Principal</div>
|
||||||
${tabs.map(t=>`<div class="ni ${S.tab===t[0]?'on':''}" onclick="switchTab('${t[0]}')"><span class="ic">${t[1]}</span>${t[2]}</div>`).join('')}
|
${tabs.map(t=>`<div class="ni ${S.tab===t[0]?'on':''}" onclick="switchTab('${t[0]}')" ${t[3]==='sub'?'style="padding-left:2.2rem;font-size:.78rem;opacity:.9"':''}><span class="ic">${t[1]}</span>${t[2]}</div>`).join('')}
|
||||||
<div class="nl">Configuração</div>
|
<div class="nl">Configuração</div>
|
||||||
${ctabs.map(t=>`<div class="ni ${S.tab===t[0]?'on':''}" onclick="switchTab('${t[0]}')" ${t[3]==='sub'?'style="padding-left:2.2rem;font-size:.78rem;opacity:.9"':''}><span class="ic">${t[1]}</span>${t[2]}</div>`).join('')}
|
${ctabs.map(t=>`<div class="ni ${S.tab===t[0]?'on':''}" onclick="switchTab('${t[0]}')" ${t[3]==='sub'?'style="padding-left:2.2rem;font-size:.78rem;opacity:.9"':''}><span class="ic">${t[1]}</span>${t[2]}</div>`).join('')}
|
||||||
${S.user?.role==='admin'?`<div class="nl">Administração</div>
|
${S.user?.role==='admin'?`<div class="nl">Administração</div>
|
||||||
@@ -628,10 +634,10 @@ ${atabs.map(t=>`<div class="ni ${S.tab===t[0]?'on':''}" onclick="switchTab('${t[
|
|||||||
<div class="lo-btn" onclick="toggleTheme()" title="Alternar tema">${document.documentElement.classList.contains('dark')?IC.sun:IC.moon}</div>
|
<div class="lo-btn" onclick="toggleTheme()" title="Alternar tema">${document.documentElement.classList.contains('dark')?IC.sun:IC.moon}</div>
|
||||||
<div class="lo-btn" onclick="doLogout()" title="Sair">⏻</div></div></div></div>`}
|
<div class="lo-btn" onclick="doLogout()" title="Sair">⏻</div></div></div></div>`}
|
||||||
|
|
||||||
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`<div class="tb"><div class="tb-t">${t[S.tab]||''}</div><div class="tb-a"><span class="tag">v${V}</span></div></div>`}
|
return`<div class="tb"><div class="tb-t">${t[S.tab]||''}</div><div class="tb-a"><span class="tag">v${V}</span></div></div>`}
|
||||||
|
|
||||||
function rPg(){switch(S.tab){case'chat':return rChat();case'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 ── */
|
/* ── Chat ── */
|
||||||
function rChat(){
|
function rChat(){
|
||||||
@@ -672,16 +678,27 @@ ${S.chatFiles.length?`<div style="display:flex;gap:.4rem;flex-wrap:wrap;padding:
|
|||||||
<button class="btn bs" onclick="document.getElementById('chf').click()" title="Anexar arquivo" style="font-size:1rem;padding:.3rem .5rem">${IC.clip}</button>
|
<button class="btn bs" onclick="document.getElementById('chf').click()" title="Anexar arquivo" style="font-size:1rem;padding:.3rem .5rem">${IC.clip}</button>
|
||||||
<textarea id="chi" placeholder="Digite sua mensagem... (Shift+Enter para nova linha)" rows="1" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sChat()}" oninput="autoGrow(this)" onpaste="setTimeout(()=>autoGrow(this),0)" style="resize:none;overflow-y:auto"></textarea>
|
<textarea id="chi" placeholder="Digite sua mensagem... (Shift+Enter para nova linha)" rows="1" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sChat()}" oninput="autoGrow(this)" onpaste="setTimeout(()=>autoGrow(this),0)" style="resize:none;overflow-y:auto"></textarea>
|
||||||
<button class="btn bp" onclick="sChat()">Enviar →</button></div></div></div>`}
|
<button class="btn bp" onclick="sChat()">Enviar →</button></div></div></div>`}
|
||||||
|
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){
|
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`<div class="ch-side-title">${IC.settings} Configurações</div>
|
return`<div class="ch-side-title">${IC.settings} Configurações</div>
|
||||||
<div class="ch-side-section"><div class="g2">
|
<div class="ch-side-section"><div class="g2">
|
||||||
<div><label>Temperature</label><input type="number" value="${pp.temperature}" step="0.1" min="0" max="2" onchange="S.chatParams.temperature=+this.value"></div>
|
${isR?`<div><label>Max Tokens <span style="font-size:.6rem;color:var(--t4)">(max ${maxLim.toLocaleString()})</span></label><input type="number" value="${pp.max_tokens}" min="1" max="${maxLim}" onchange="S.chatParams.max_tokens=+this.value"></div>
|
||||||
<div><label>Max Tokens</label><input type="number" value="${pp.max_tokens}" min="1" max="128000" onchange="S.chatParams.max_tokens=+this.value"></div>
|
<div><label>Reasoning Effort</label><select onchange="S.chatParams.reasoning_effort=this.value"><option value="low"${pp.reasoning_effort==='low'?' selected':''}>Low</option><option value="medium"${pp.reasoning_effort==='medium'?' selected':''}>Medium</option><option value="high"${pp.reasoning_effort==='high'?' selected':''}>High</option></select></div>`
|
||||||
|
:`<div><label>Temperature</label><input type="number" value="${pp.temperature}" step="0.1" min="0" max="2" onchange="S.chatParams.temperature=+this.value"></div>
|
||||||
|
<div><label>Max Tokens <span style="font-size:.6rem;color:var(--t4)">(max ${maxLim.toLocaleString()})</span></label><input type="number" value="${pp.max_tokens}" min="1" max="${maxLim}" onchange="S.chatParams.max_tokens=+this.value"></div>
|
||||||
<div><label>Top P</label><input type="number" value="${pp.top_p}" step="0.05" min="0" max="1" onchange="S.chatParams.top_p=+this.value"></div>
|
<div><label>Top P</label><input type="number" value="${pp.top_p}" step="0.05" min="0" max="1" onchange="S.chatParams.top_p=+this.value"></div>
|
||||||
<div><label>Top K</label><input type="number" value="${pp.top_k}" min="-1" max="500" onchange="S.chatParams.top_k=+this.value"></div>
|
${hasTopK?`<div><label>Top K</label><input type="number" value="${pp.top_k}" min="-1" max="500" onchange="S.chatParams.top_k=+this.value"></div>`:''}
|
||||||
<div><label>Freq Penalty</label><input type="number" value="${pp.frequency_penalty}" step="0.1" min="0" max="2" onchange="S.chatParams.frequency_penalty=+this.value"></div>
|
${hasPenalties?`<div><label>Freq Penalty</label><input type="number" value="${pp.frequency_penalty}" step="0.1" min="0" max="2" onchange="S.chatParams.frequency_penalty=+this.value"></div>
|
||||||
<div><label>Pres Penalty</label><input type="number" value="${pp.presence_penalty}" step="0.1" min="0" max="2" onchange="S.chatParams.presence_penalty=+this.value"></div>
|
<div><label>Pres Penalty</label><input type="number" value="${pp.presence_penalty}" step="0.1" min="0" max="2" onchange="S.chatParams.presence_penalty=+this.value"></div>`:''}`}
|
||||||
</div></div>
|
</div></div>
|
||||||
<div class="ch-side-section" style="border-top:1px solid var(--bd);padding-top:.65rem">
|
<div class="ch-side-section" style="border-top:1px solid var(--bd);padding-top:.65rem">
|
||||||
<label style="display:flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.76rem"><input type="checkbox" ${S.chatUseTools?'checked':''} onchange="S.chatUseTools=this.checked"> ${IC.plug} MCP Tools</label>
|
<label style="display:flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.76rem"><input type="checkbox" ${S.chatUseTools?'checked':''} onchange="S.chatUseTools=this.checked"> ${IC.plug} MCP Tools</label>
|
||||||
@@ -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('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('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);
|
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));
|
S.chatFiles.forEach(f=>fd.append('files',f.file));
|
||||||
d=await $api('/chat/upload',{method:'POST',body:fd});S.chatFiles=[]
|
d=await $api('/chat/upload',{method:'POST',body:fd});S.chatFiles=[]
|
||||||
}else{
|
}else{
|
||||||
@@ -760,12 +778,14 @@ async function sChat(){const el=document.getElementById('chi');const m=el.value.
|
|||||||
if(S.chatModel.startsWith('cfg:')){
|
if(S.chatModel.startsWith('cfg:')){
|
||||||
body.genai_config_id=S.chatModel.slice(4);
|
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.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){
|
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}
|
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.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.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})}
|
d=await $api('/chat',{method:'POST',body})}
|
||||||
S.sid=d.session_id;
|
S.sid=d.session_id;
|
||||||
if(S.chatHistOpen)loadHistory('chat');
|
if(S.chatHistOpen)loadHistory('chat');
|
||||||
@@ -846,14 +866,16 @@ async function tfRefreshRef(){
|
|||||||
|
|
||||||
async function loadHistory(type){
|
async function loadHistory(type){
|
||||||
try{const h=await $api('/chat/sessions?agent_type='+type+'&limit=50');
|
try{const h=await $api('/chat/sessions?agent_type='+type+'&limit=50');
|
||||||
if(type==='chat')S.chatHistory=h;else S.tfHistory=h}
|
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 S.tfHistory=[]}
|
catch(e){if(type==='chat')S.chatHistory=[];else if(type==='tf-prompt')S.tfpHistory=[];else S.tfHistory=[]}
|
||||||
R();
|
R();
|
||||||
}
|
}
|
||||||
async function loadSession(sid,type){
|
async function loadSession(sid,type){
|
||||||
try{const d=await $api('/chat/sessions/'+sid+'/messages');
|
try{const d=await $api('/chat/sessions/'+sid+'/messages');
|
||||||
if(type==='chat'){
|
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();
|
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{
|
}else{
|
||||||
S.tfSid=sid;S.tfMsgs=d.messages.map(m=>({r:m.role,c:m.content,t:m.created_at?.slice(11,16)||''}));
|
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='';
|
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;
|
if(!confirm('Excluir esta conversa?'))return;
|
||||||
try{await $api('/chat/'+sid,{method:'DELETE'})}catch(e){}
|
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)}
|
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)}
|
else{if(S.tfSid===sid)newTfChat();S.tfHistory=S.tfHistory.filter(s=>s.id!==sid)}
|
||||||
R();
|
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!'))}
|
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
|
||||||
|
?`<div class="tf-empty">${IC.edit}<p>Descreva a infraestrutura OCI que você precisa</p><p style="font-size:.68rem;opacity:.6">O agente vai gerar um prompt estruturado e otimizado para o Terraform Agent.</p>
|
||||||
|
<div style="display:flex;flex-wrap:wrap;gap:.35rem;margin-top:.75rem;justify-content:center">${TFP_EXAMPLES.map(e=>`<button class="btn bs bsm" onclick="document.getElementById('tfpi').value='${e.replace(/'/g,"\\'")}';document.getElementById('tfpi').focus()" style="font-size:.66rem;white-space:normal;text-align:left;max-width:280px;line-height:1.35">${e}</button>`).join('')}</div></div>`
|
||||||
|
:msgs.map((m,i)=>`<div class="cm cm-${m.r}"><div class="cb">${fm(m.c)}</div>${m._raw?`<div style="display:flex;gap:.3rem;margin-top:.35rem;padding-top:.3rem;border-top:1px solid var(--bd)">
|
||||||
|
<button class="tfp-act" onclick="tfpCopy(${i})">${m._copied?IC.ok+' Copiado':IC.clip+' Copiar'}</button>
|
||||||
|
<button class="tfp-act tfp-act-tf" onclick="tfpSendToTf(${i})">${TF_IC} Terraform</button></div>`:''}</div>`).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+='<div class="grp">Configs Salvas</div>';cfgFiltered.forEach(g=>{const mi=S.models[g.model_id];ddItems+=`<div class="itm${S.tfpModel==='cfg:'+g.id?' sel':''}" onclick="tfpPickModel('cfg:${g.id}')">${g.name||mi?.name||g.model_id}</div>`})}}
|
||||||
|
tfpProvOrder.filter(p=>tfpProvs[p]).forEach(p=>{ddItems+=`<div class="grp">${p[0].toUpperCase()+p.slice(1)}</div>`;tfpProvs[p].forEach(m=>{ddItems+=`<div class="itm${S.tfpModel===m.id?' sel':''}" onclick="tfpPickModel('${m.id}')">${m.name}</div>`})});
|
||||||
|
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?`<select onchange="S.tfpOci=this.value;const c=S.ociCfg.find(x=>x.id===this.value);if(c){S.tfpRegion=c.region;S.tfpCompartment=c.compartment_id||''}" style="max-width:180px"><option value="">OCI Config...</option>${S.ociCfg.map(c=>`<option value="${c.id}"${S.tfpOci===c.id?' selected':''}>${c.tenancy_name} (${c.region})</option>`).join('')}</select>`:'';
|
||||||
|
return`<div class="tf-wrap">
|
||||||
|
<div style="display:flex;flex:1;min-height:0;overflow:hidden">
|
||||||
|
<div class="ch-hp${S.tfpHistOpen?' open':''}"><div class="ch-hp-inner">
|
||||||
|
<div class="ch-hp-hdr"><span>${IC.history} Histórico</span><div class="ch-hp-new" onclick="S.tfpMsgs=[];S.tfpSid=null;R()">+ Nova Conversa</div></div>
|
||||||
|
<div class="ch-hp-list">${rHistList('tf-prompt',S.tfpHistory,S.tfpSid)}</div>
|
||||||
|
</div></div>
|
||||||
|
<div class="tf-chat">
|
||||||
|
<div class="tf-toolbar">
|
||||||
|
<div class="ch-icon-btn${S.tfpHistOpen?' active':''}" onclick="S.tfpHistOpen=!S.tfpHistOpen;if(S.tfpHistOpen)loadHistory('tf-prompt');R()" title="Histórico" style="width:24px;height:24px;font-size:.85rem">☰</div>
|
||||||
|
${IC.edit}
|
||||||
|
<div class="mdrop"><div class="mdrop-btn" onclick="tfpToggleModelDrop()">${curLabel} <span class="arr">▼</span></div>
|
||||||
|
<div class="mdrop-dd" id="tfpmdd"><input type="text" id="tfpmds" placeholder="Buscar..." oninput="tfpFilterModels(this.value)"><div class="mdrop-list" id="tfpmdl">${ddItems}</div></div>
|
||||||
|
</div>
|
||||||
|
${ociSel}
|
||||||
|
<div style="flex:1"></div>
|
||||||
|
</div>
|
||||||
|
<div class="tf-msgs" id="tfpchm">${msHtml}${S.tfpLoading?'<div class="cm cm-assistant"><div class="cb"><span class="spinner" style="width:16px;height:16px;border-width:2px;vertical-align:middle"></span> Gerando prompt...</div></div>':''}</div>
|
||||||
|
<div class="tf-input">
|
||||||
|
<textarea id="tfpi" placeholder="Descreva a infraestrutura desejada... (Shift+Enter para nova linha)" rows="1" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();tfpSend()}" oninput="autoGrow(this)" onpaste="setTimeout(()=>autoGrow(this),0)" style="resize:none;overflow-y:auto"></textarea>
|
||||||
|
<button class="btn bp" onclick="tfpSend()" style="background:#7b42bc;border-color:#7b42bc">Enviar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div></div>`}
|
||||||
|
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(){
|
function rTerraform(){
|
||||||
const ms=S.tfMsgs.length===0
|
const ms=S.tfMsgs.length===0
|
||||||
?`<div class="tf-empty">${TF_ICON}<p>Descreva a infraestrutura OCI desejada</p><p style="font-size:.68rem;opacity:.6">Ex: "Crie uma VCN com subnets pública e privada, internet gateway e um compute instance"</p></div>`
|
?`<div class="tf-empty">${TF_ICON}<p>Descreva a infraestrutura OCI desejada</p><p style="font-size:.68rem;opacity:.6">Ex: "Crie uma VCN com subnets pública e privada, internet gateway e um compute instance"</p></div>`
|
||||||
:S.tfMsgs.map(m=>`<div class="cm cm-${m.r}"><div class="cb">${m.r==='assistant'?fmTf(m.c):fm(m.c)}</div>${m.t?`<div class="cm-ts">${m.t}</div>`:''}</div>`).join('');
|
:S.tfMsgs.map(m=>`<div class="cm cm-${m.r}"><div class="cb">${m.r==='assistant'?fmTf(m.c):fm(m.c)}</div>${m.t?`<div class="cm-ts">${m.t}</div>`:''}</div>`).join('');
|
||||||
|
|
||||||
// Model dropdown
|
// Model dropdown — same filtered models as Prompt Generator, grouped by provider
|
||||||
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 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 provOrder=['openai','google','meta','xai'];
|
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='';
|
let ddItems='';
|
||||||
if(S.genaiCfg.length){ddItems+='<div class="grp">Configs Salvas</div>';S.genaiCfg.forEach(g=>{const mi=S.models[g.model_id];ddItems+=`<div class="itm${S.tfModel==='cfg:'+g.id?' sel':''}" onclick="tfPickModel('cfg:${g.id}')">${g.name||mi?.name||g.model_id}</div>`})}
|
if(S.genaiCfg.length){const cfgF=S.genaiCfg.filter(g=>tfAllowed.includes(g.model_id));if(cfgF.length){ddItems+='<div class="grp">Configs Salvas</div>';cfgF.forEach(g=>{const mi=S.models[g.model_id];ddItems+=`<div class="itm${S.tfModel==='cfg:'+g.id?' sel':''}" onclick="tfPickModel('cfg:${g.id}')">${g.name||mi?.name||g.model_id}</div>`})}}
|
||||||
provOrder.filter(p=>provs[p]).forEach(p=>{ddItems+=`<div class="grp">${p[0].toUpperCase()+p.slice(1)}</div>`;provs[p].forEach(m=>{ddItems+=`<div class="itm${S.tfModel===m.id?' sel':''}" onclick="tfPickModel('${m.id}')">${m.name}</div>`})});
|
provOrder.filter(p=>provs[p]).forEach(p=>{ddItems+=`<div class="grp">${p[0].toUpperCase()+p.slice(1)}</div>`;provs[p].forEach(m=>{ddItems+=`<div class="itm${S.tfModel===m.id?' sel':''}" onclick="tfPickModel('${m.id}')">${m.name}</div>`})});
|
||||||
let curLabel='Selecione modelo...';
|
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...'}
|
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(){
|
|||||||
<div class="tf-msgs" id="tfchm">${ms}</div>
|
<div class="tf-msgs" id="tfchm">${ms}</div>
|
||||||
<div class="tf-input">
|
<div class="tf-input">
|
||||||
<textarea id="tfi" placeholder="Descreva a infraestrutura desejada... (Shift+Enter para nova linha)" rows="1" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();tfSend()}" oninput="autoGrow(this)" onpaste="setTimeout(()=>autoGrow(this),0)" style="resize:none;overflow-y:auto"></textarea>
|
<textarea id="tfi" placeholder="Descreva a infraestrutura desejada... (Shift+Enter para nova linha)" rows="1" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();tfSend()}" oninput="autoGrow(this)" onpaste="setTimeout(()=>autoGrow(this),0)" style="resize:none;overflow-y:auto"></textarea>
|
||||||
<button class="btn bp" onclick="tfSend()" style="background:#7b42bc;border-color:#7b42bc">Gerar</button>
|
<button class="btn bp" onclick="tfSend()" style="background:#7b42bc;border-color:#7b42bc">Enviar</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1904,7 +2017,7 @@ ${S.rptRselOpen?`<div class="mdrop-list" style="position:absolute;top:100%;left:
|
|||||||
${rselDdItems||'<div style="padding:.5rem;color:var(--t4);font-size:.72rem">Nenhum relatório encontrado</div>'}
|
${rselDdItems||'<div style="padding:.5rem;color:var(--t4);font-size:.72rem">Nenhum relatório encontrado</div>'}
|
||||||
</div>`:''}
|
</div>`:''}
|
||||||
</div></div>
|
</div></div>
|
||||||
${rKpi()}<iframe class="rif" id="rfr" src="${API}/reports/${(selRpt||lt).id}/html"></iframe>${rRunRpt()}${rReportHistory()}`}
|
<div id="kpi-wrap">${rKpi()}</div><iframe class="rif" id="rfr" src="${API}/reports/${(selRpt||lt).id}/html"></iframe>${rRunRpt()}${rReportHistory()}`}
|
||||||
function rRunRpt(){if(S.user?.role==='viewer')return'';
|
function rRunRpt(){if(S.user?.role==='viewer')return'';
|
||||||
const selTags=S.rptSelRegions.map(r=>`<span class="tag" style="background:var(--b2);font-size:.62rem;display:inline-flex;align-items:center;gap:.2rem">${r}<span onclick="rptRemoveRegion('${r}')" style="cursor:pointer;font-weight:700;color:var(--err)">×</span></span>`).join(' ');
|
const selTags=S.rptSelRegions.map(r=>`<span class="tag" style="background:var(--b2);font-size:.62rem;display:inline-flex;align-items:center;gap:.2rem">${r}<span onclick="rptRemoveRegion('${r}')" style="cursor:pointer;font-weight:700;color:var(--err)">×</span></span>`).join(' ');
|
||||||
const f=S.rptRegionFilter.toLowerCase();
|
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 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()}
|
||||||
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()}
|
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={};
|
let _kpiCharts={};
|
||||||
function _destroyKpiCharts(){Object.values(_kpiCharts).forEach(c=>{try{c.destroy()}catch(e){}});_kpiCharts={}}
|
function _destroyKpiCharts(){Object.values(_kpiCharts).forEach(c=>{try{c.destroy()}catch(e){}});_kpiCharts={}}
|
||||||
function _initKpiCharts(){
|
function _initKpiCharts(){
|
||||||
|
|||||||
Reference in New Issue
Block a user