feat: resource type validation, 100K max tokens, and auto-split improvements
- Add SQLite-based resource type validation with suggestions for invalid types - Increase Terraform max_tokens from 32K to 100K for large infra generation - Generate resource reference at startup to survive volume mount overwrites - Add invalid resource type examples to system prompt (RPC peer, subnet route table) - Add prompt size logging for Terraform agent - Translate remaining Portuguese code comment to English
This commit is contained in:
164
backend/app.py
164
backend/app.py
@@ -335,6 +335,13 @@ def init_db():
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS tf_valid_types (
|
||||
name TEXT PRIMARY KEY,
|
||||
kind TEXT NOT NULL DEFAULT 'resource',
|
||||
category TEXT DEFAULT '',
|
||||
required_args TEXT DEFAULT '',
|
||||
blocks TEXT DEFAULT ''
|
||||
);
|
||||
""")
|
||||
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')")
|
||||
@@ -2036,6 +2043,37 @@ Abaixo estão os nomes corretos de resource types do provider oracle/oci. O mode
|
||||
}
|
||||
```
|
||||
- **Remote Peering Connection**: `oci_core_remote_peering_connection`
|
||||
- NÃO EXISTE `oci_core_remote_peering_connection_peer`. Para conectar dois RPCs, use `peer_id` e `peer_region_name` DENTRO do próprio `oci_core_remote_peering_connection`:
|
||||
```hcl
|
||||
resource "oci_core_remote_peering_connection" "rpc_initiator" {
|
||||
compartment_id = var.compartment_id
|
||||
drg_id = oci_core_drg.drg_a.id
|
||||
peer_id = oci_core_remote_peering_connection.rpc_accepter.id
|
||||
peer_region_name = var.region_b
|
||||
}
|
||||
```
|
||||
- **Route Table em Subnet**: NÃO EXISTE `oci_core_subnet_route_table_association`. Para associar route table a subnet, use:
|
||||
- Opção 1: `route_table_id` DENTRO do `oci_core_subnet`
|
||||
- Opção 2: `oci_core_route_table_attachment` (requer `route_table_id` e `subnet_id` OBRIGATÓRIOS):
|
||||
```hcl
|
||||
resource "oci_core_route_table_attachment" "example" {
|
||||
route_table_id = oci_core_route_table.example_rt.id
|
||||
subnet_id = oci_core_subnet.example.id
|
||||
}
|
||||
```
|
||||
- **Network Firewall** (`oci_network_firewall_network_firewall`):
|
||||
- O argumento `network_firewall_policy_id` é OBRIGATÓRIO. Crie primeiro a policy:
|
||||
```hcl
|
||||
resource "oci_network_firewall_network_firewall_policy" "policy" {
|
||||
compartment_id = var.compartment_id
|
||||
display_name = "fw-policy"
|
||||
}
|
||||
resource "oci_network_firewall_network_firewall" "fw" {
|
||||
compartment_id = var.compartment_id
|
||||
network_firewall_policy_id = oci_network_firewall_network_firewall_policy.policy.id
|
||||
subnet_id = oci_core_subnet.fw_subnet.id
|
||||
}
|
||||
```
|
||||
- **DRG Route Table**: `oci_core_drg_route_table`
|
||||
- **DRG Route Distribution**: `oci_core_drg_route_distribution`
|
||||
- **DRG Route Table Route Rule**: `oci_core_drg_route_table_route_rule`
|
||||
@@ -2103,6 +2141,93 @@ Antes de entregar qualquer código (novo ou corrigido), valide TODOS os itens ab
|
||||
_TF_RESOURCE_REF_PATH = "/data/oci_tf_resource_reference.txt"
|
||||
_tf_resource_ref_cache: dict = {"content": None, "mtime": 0}
|
||||
|
||||
|
||||
def _populate_tf_valid_types():
|
||||
"""Parse the OCI TF resource reference file and populate tf_valid_types table in SQLite."""
|
||||
p = Path(_TF_RESOURCE_REF_PATH)
|
||||
if not p.exists():
|
||||
return 0
|
||||
content = p.read_text()
|
||||
import re as _re
|
||||
entries = []
|
||||
current_category = ""
|
||||
for line in content.split('\n'):
|
||||
if line.startswith('## '):
|
||||
current_category = line[3:].strip()
|
||||
continue
|
||||
# Resource lines: - **oci_xxx** required: ... blocks: ...
|
||||
m = _re.match(r'^- \*\*(\w+)\*\*\s*(.*)', line)
|
||||
if m:
|
||||
name = m.group(1)
|
||||
rest = m.group(2).strip()
|
||||
required = ""
|
||||
blocks = ""
|
||||
# Split by "blocks:" to separate required from blocks
|
||||
blk_parts = rest.split('blocks:')
|
||||
if blk_parts[0].strip().startswith('required:'):
|
||||
required = blk_parts[0].replace('required:', '').strip()
|
||||
if len(blk_parts) > 1:
|
||||
blocks = blk_parts[1].strip()
|
||||
entries.append((name, 'resource', current_category, required, blocks))
|
||||
continue
|
||||
# Data source lines (in "All Resource Types" or "All Data Sources" section): - oci_xxx
|
||||
m2 = _re.match(r'^- (\w+)\s*$', line)
|
||||
if m2:
|
||||
name = m2.group(1)
|
||||
if current_category.startswith('All Data'):
|
||||
entries.append((name, 'data', '', '', ''))
|
||||
elif current_category.startswith('All Resource'):
|
||||
entries.append((name, 'resource', '', '', ''))
|
||||
if not entries:
|
||||
return 0
|
||||
with db() as c:
|
||||
c.execute("DELETE FROM tf_valid_types")
|
||||
# Insert detailed entries first (with required_args), then bulk lists with IGNORE to not overwrite
|
||||
detailed = [e for e in entries if e[3] or e[4]] # has required_args or blocks
|
||||
bulk = [e for e in entries if not e[3] and not e[4]]
|
||||
if detailed:
|
||||
c.executemany(
|
||||
"INSERT OR REPLACE INTO tf_valid_types (name, kind, category, required_args, blocks) VALUES (?,?,?,?,?)",
|
||||
detailed)
|
||||
if bulk:
|
||||
c.executemany(
|
||||
"INSERT OR IGNORE INTO tf_valid_types (name, kind, category, required_args, blocks) VALUES (?,?,?,?,?)",
|
||||
bulk)
|
||||
log.info(f"Populated tf_valid_types: {len(entries)} entries")
|
||||
return len(entries)
|
||||
|
||||
|
||||
def _validate_tf_resource_types(tf_code: str) -> list:
|
||||
"""Validate resource/data types in generated TF code against the tf_valid_types table.
|
||||
Returns list of dicts with invalid types and suggestions."""
|
||||
import re as _re
|
||||
from difflib import get_close_matches
|
||||
# Extract all resource and data type declarations
|
||||
used_types = set()
|
||||
for m in _re.finditer(r'(?:resource|data)\s+"(\w+)"', tf_code):
|
||||
used_types.add(m.group(1))
|
||||
if not used_types:
|
||||
return []
|
||||
# Load valid types from DB
|
||||
with db() as c:
|
||||
rows = c.execute("SELECT name, kind, required_args FROM tf_valid_types").fetchall()
|
||||
valid_names = {r["name"] for r in rows}
|
||||
valid_required = {r["name"]: r["required_args"] for r in rows}
|
||||
if not valid_names:
|
||||
return [] # No reference loaded
|
||||
# Check each used type
|
||||
errors = []
|
||||
for t in sorted(used_types):
|
||||
if t not in valid_names:
|
||||
# Find closest matches
|
||||
suggestions = get_close_matches(t, valid_names, n=3, cutoff=0.6)
|
||||
errors.append({
|
||||
"type": t,
|
||||
"suggestions": suggestions,
|
||||
"message": f"Resource type '{t}' NÃO EXISTE no provider oracle/oci."
|
||||
})
|
||||
return errors
|
||||
|
||||
def _load_tf_resource_reference() -> str:
|
||||
"""Load the OCI Terraform resource reference file, cached by mtime."""
|
||||
try:
|
||||
@@ -3227,7 +3352,7 @@ async def _chat_background(mid: str, sid: str, msg: ChatMsg, user: dict, genai_c
|
||||
|
||||
# ── Terraform agent: boost max_tokens for complex infra code ──
|
||||
if agent_type == "terraform":
|
||||
cfg_dict["max_tokens"] = max(int(cfg_dict.get("max_tokens", 6000)), 32000)
|
||||
cfg_dict["max_tokens"] = max(int(cfg_dict.get("max_tokens", 6000)), 100000)
|
||||
|
||||
# ── Inject existing OCI resources for terraform agent ──
|
||||
if agent_type == "terraform" and active_oci_id:
|
||||
@@ -3363,6 +3488,23 @@ async def _chat_background(mid: str, sid: str, msg: ChatMsg, user: dict, genai_c
|
||||
resp = f"⚠️ O modelo **{model_id}** retornou uma resposta vazia. Isso pode ocorrer com alguns modelos. Tente novamente ou selecione outro modelo (ex: Gemini, Llama)."
|
||||
log.warning(f"Chat {mid}: empty response from model {model_id}, returning fallback message")
|
||||
|
||||
# Validate terraform resource types against DB
|
||||
if agent_type == "terraform" and resp and '```' in resp:
|
||||
try:
|
||||
tf_errors = _validate_tf_resource_types(resp)
|
||||
if tf_errors:
|
||||
warning_lines = ["\n\n⚠️ **Validação automática — Resource types inválidos detectados:**"]
|
||||
for err in tf_errors:
|
||||
line = f"- `{err['type']}` — NÃO EXISTE no provider oracle/oci."
|
||||
if err['suggestions']:
|
||||
line += f" Sugestões: {', '.join('`' + s + '`' for s in err['suggestions'])}"
|
||||
warning_lines.append(line)
|
||||
warning_lines.append("\n**Clique em 'Re-plan' após o modelo corrigir, ou peça correção no chat.**")
|
||||
resp += '\n'.join(warning_lines)
|
||||
log.warning(f"Chat {mid}: {len(tf_errors)} invalid TF resource types detected: {[e['type'] for e in tf_errors]}")
|
||||
except Exception as e:
|
||||
log.warning(f"TF resource type validation failed: {e}")
|
||||
|
||||
with db() as c:
|
||||
c.execute("UPDATE chat_messages SET content=?, status='done' WHERE id=?", (resp, mid))
|
||||
log.info(f"Chat {mid} completed successfully")
|
||||
@@ -4426,6 +4568,26 @@ async def startup():
|
||||
except Exception as e:
|
||||
log.warning(f"Could not read CIS engine version: {e}")
|
||||
|
||||
# Generate TF resource reference if not present (volume mount may hide build-time file)
|
||||
ref_path = Path(_TF_RESOURCE_REF_PATH)
|
||||
if not ref_path.exists():
|
||||
try:
|
||||
result = subprocess.run(["python", "/app/gen_tf_reference.py"], capture_output=True, text=True, timeout=300)
|
||||
if result.returncode == 0 and ref_path.exists():
|
||||
log.info(f"Generated TF resource reference at startup: {ref_path.stat().st_size} bytes")
|
||||
else:
|
||||
log.warning(f"TF reference generation failed at startup: {result.stderr[:200] if result.stderr else 'unknown'}")
|
||||
except Exception as e:
|
||||
log.warning(f"Could not generate TF reference at startup: {e}")
|
||||
# Populate tf_valid_types table from reference file
|
||||
if ref_path.exists():
|
||||
try:
|
||||
count = _populate_tf_valid_types()
|
||||
if count:
|
||||
log.info(f"TF valid types DB populated: {count} entries")
|
||||
except Exception as e:
|
||||
log.warning(f"Could not populate tf_valid_types: {e}")
|
||||
|
||||
log.info(f"OCI CIS AI Agent v{VERSION} API started")
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -327,7 +327,7 @@ def _finding_in_compartment(finding, compartment_id):
|
||||
if fcomp == compartment_id:
|
||||
return True
|
||||
if fcomp is None:
|
||||
return True # recurso sem compartment (IAM tenancy-level) → incluir
|
||||
return True # resource without compartment (IAM tenancy-level) → include
|
||||
return False
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user