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:
nogueiraguh
2026-03-09 01:10:33 -03:00
parent 6fb2eecddf
commit a5b8f6903a
2 changed files with 164 additions and 2 deletions

View File

@@ -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__":

View File

@@ -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