ligbox-ops-platform/projects/ops-desk/api/app/agents/chat_context_builder.py
Ligbox Spec Hub 6c4b063f76 Implement Spec 027 access matrix preview and agent bindings enforcement.
Adds RBAC matrix API/UI behind feature flags, agent×role toggles with audit,
and extends Agentic Ops with streaming chat, Kimi LLM support, and UI updates.
2026-06-20 22:02:03 +00:00

240 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Contexto operacional rico para chat investigativo — Spec 030+."""
from __future__ import annotations
import sqlite3
from app.agents.catalog import AGENT_CATALOG
from app.agents import store
AGENT_INVESTIGATION_LENS: dict[str, str] = {
"A0": "Visão global: correlacionar incidentes, delegar aos agentes certos, priorizar por severidade.",
"A1": "Investigar: VM112 wizard/Carbonio, Proxmox cluster, CPU/RAM, containers down, restarts.",
"A2": "Investigar: DNS Cloudflare, Traefik CT114, certificados LE, SNI, pfSense API/regras.",
"A3": "Investigar: SPF/DKIM/DMARC, entregabilidade mail, registos DNS por domínio tenant.",
"A4": "Investigar: filas mail, amavis/clamav, spam, quarentena, bloqueios de envio.",
"A5": "Investigar: alertas Wazuh VM104, correlação SIEM, timeline segurança, runbooks R0/R1.",
"A6": "Investigar: tickets Desk, onboarding stuck, findings abertos, contexto cliente, runbooks KB.",
"A7": "Investigar: runbooks aprovados pós-incidente, remediação R0R3, aprovações pendentes.",
"sentinel": "Investigar: health checks T0 (desk, wizard, pfSense, proxmox, ollama, VM123 stack).",
}
def _fmt_incident(inc: dict) -> str:
action = (inc.get("suggested_human_action") or "Investigar manualmente.")[:200]
return (
f"- [{inc.get('severity', '?').upper()}] {inc.get('title')} "
f"(cenário `{inc.get('scenario_id')}`, agente {inc.get('agent_name', inc.get('primary_agent'))}, "
f"visto {inc.get('occurrence_count', 1)}×, última vez {inc.get('last_seen_at', '?')})\n"
f" → Acção sugerida: {action}"
)
def build_ops_context(
conn: sqlite3.Connection,
question: str,
target_agent: str,
*,
include_findings: bool = True,
kb_snippets: list[str] | None = None,
) -> str:
"""Monta dossiê operacional para respostas investigativas."""
sections: list[str] = []
ov = store.get_overview(conn)
open_inc = ov.get("incidents_open") or {}
sections.append(
"### Estado do ambiente (dados reais do Agentic Ops)\n"
f"- Provider LLM: {ov.get('provider')} / {ov.get('model')}\n"
f"- Último tick worker: {ov.get('last_tick_at') or 'desconhecido'} — status `{ov.get('last_tick_status', '?')}`\n"
f"- Cenários vigilância: **{ov.get('scenarios_ok', 0)}/{ov.get('scenarios_total', 0)} OK**\n"
f"- Incidentes abertos: {open_inc.get('critical', 0)} crít · "
f"{open_inc.get('high', 0)} alto · {open_inc.get('warn', 0)} aviso"
)
lens = AGENT_INVESTIGATION_LENS.get(target_agent, AGENT_INVESTIGATION_LENS["A6"])
profile = AGENT_CATALOG.get(target_agent, AGENT_CATALOG["A6"])
sections.append(
f"### Lente investigativa do agente {profile.name} ({target_agent})\n{lens}\n"
f"Competências: {', '.join(profile.reads[:4])}"
)
incidents = store.list_incidents(conn, status="open", limit=15)
if target_agent not in ("A0", "A6", "ALL"):
agent_first = [i for i in incidents if i.get("primary_agent") == target_agent]
other = [i for i in incidents if i.get("primary_agent") != target_agent][:5]
incidents = agent_first + other
if incidents:
sections.append(
"### Incidentes activos (deduplicados por cenário)\n"
+ "\n".join(_fmt_incident(i) for i in incidents[:8])
)
else:
sections.append("### Incidentes activos\nNenhum incidente aberto no momento.")
scenarios = store.list_scenarios(conn)
failing = [s for s in scenarios if s.get("last_run_status") not in (None, "ok")]
if failing:
fail_lines = []
for s in failing[:8]:
fail_lines.append(
f"- `{s['id']}` — {s.get('title', s['id'])}: status **{s.get('last_run_status')}** "
f"(último run {s.get('last_run_at', '?')})"
)
sections.append("### Cenários com falha recente\n" + "\n".join(fail_lines))
if include_findings:
findings = store.list_findings(conn, limit=10, open_only=True)
if findings:
flines = []
for f in findings[:8]:
detail = (f.get("detail_md") or f.get("title") or "")[:180]
flines.append(
f"- [{f.get('severity')}] **{f.get('title')}** — {detail}\n"
f" → Humano: {(f.get('suggested_human_action') or 'verificar')[:160]}"
)
sections.append("### Findings T0/T1 abertos\n" + "\n".join(flines))
if kb_snippets:
sections.append(
"### Base de conhecimento (RAG — specs/runbooks)\n"
+ "\n---\n".join(kb_snippets[:6])[:4500]
)
sections.append(
"### Infra de referência Ligbox\n"
"- Proxmox host · pfSense WAN/LAN · VM112 wizard/onboard · VM122 Desk/API · VM123 finance/Ollama\n"
"- API pfSense via Traefik: `firewall.itecnologys.com` · Desk: `desk.ligbox.com.br`"
)
return "\n\n".join(sections)
RESPONSE_FORMAT_INSTRUCTION = """
## Formato OBRIGATÓRIO da resposta (markdown)
Estruture SEMPRE assim — respostas planas ou genéricas são proibidas:
### 🔍 Diagnóstico
Sintetize o que os **dados do contexto** indicam sobre a pergunta. Cite incidentes/findings/cenários relevantes por nome.
### 🧪 Hipóteses (ordem de probabilidade)
1. **Hipótese mais provável** — porquê, o que corrobora
2. **Hipótese alternativa** — o que verificar para confirmar/descartar
3. (opcional) terceira hipótese se aplicável
### ✅ Plano de acção proposto
| # | Prioridade | Acção concreta | Onde / comando |
|---|------------|----------------|----------------|
| 1 | Alta/Média/Baixa | passo executável | `comando` ou painel/link |
| 2 | ... | ... | ... |
Inclua **comandos shell** ou **URLs/painéis** quando souber (sem inventar credenciais).
### ⚠️ Riscos e impacto
- O que pode piorar se ignorarmos
- Dependências (ex.: reiniciar X afecta Y)
### ▶️ Próximo passo imediato
Uma frase directa: **faça isto agora** — a acção única de maior valor.
---
Regras: português BR · baseie-se no contexto injectado · se faltar dado, diga **exactamente** o que o operador deve correr/verificar · não invente estados de serviços.
"""
def _question_relevance(question: str, *texts: str) -> int:
q = question.lower()
score = 0
for t in texts:
if not t:
continue
tl = str(t).lower()
for word in q.split():
if len(word) > 3 and word in tl:
score += 2
if tl in q or q in tl:
score += 3
return score
def build_suggested_actions(
conn: sqlite3.Connection,
question: str,
target_agent: str,
*,
limit: int = 6,
) -> list[dict]:
"""Acções executáveis no Desk — checks T0 e ack de incidentes."""
actions: list[dict] = []
seen_scenarios: set[str] = set()
seen_incidents: set[int] = set()
def add_run(scenario_id: str, label: str, severity: str = "info") -> None:
if scenario_id in seen_scenarios or len(actions) >= limit:
return
if not store.get_scenario(conn, scenario_id):
return
seen_scenarios.add(scenario_id)
actions.append({
"type": "run_scenario",
"scenario_id": scenario_id,
"label": label,
"severity": severity,
})
def add_ack(incident_id: int, label: str, severity: str, scenario_id: str) -> None:
if incident_id in seen_incidents or len(actions) >= limit:
return
seen_incidents.add(incident_id)
actions.append({
"type": "ack_incident",
"incident_id": incident_id,
"scenario_id": scenario_id,
"label": label,
"severity": severity,
})
incidents = store.list_incidents(conn, status="open", limit=15)
ranked_inc = sorted(
incidents,
key=lambda i: (
-{"critical": 4, "high": 3, "warn": 2, "info": 1}.get(i.get("severity", "info"), 0),
-_question_relevance(question, i.get("title", ""), i.get("scenario_id", "")),
),
)
for inc in ranked_inc[:5]:
sid = inc.get("scenario_id") or ""
title = inc.get("title") or sid
sev = inc.get("severity") or "warn"
rel = _question_relevance(question, title, sid)
if rel > 0 or len(actions) < 2:
add_run(sid, f"▶ Executar check: {title[:48]}", sev)
if rel > 0 and inc.get("id"):
add_ack(int(inc["id"]), f"✓ Ack #{inc['id']} · {title[:32]}", sev, sid)
for s in store.list_scenarios(conn):
if s.get("last_run_status") not in (None, "ok"):
add_run(
s["id"],
f"▶ Re-validar: {(s.get('title') or s['id'])[:42]}",
"warn" if s.get("last_run_status") == "fail" else "info",
)
profile = AGENT_CATALOG.get(target_agent)
if profile:
for sid in profile.scenarios:
sc = store.get_scenario(conn, sid)
if sc:
add_run(sid, f"▶ Check: {(sc.get('title') or sid)[:40]}", "info")
if len(actions) < 2 and target_agent in ("A6", "A0", "sentinel"):
for sid, lbl in (
("desk.api.health", "▶ Health Desk API"),
("proxmox.cluster", "▶ Health Proxmox"),
("ollama.vm123.health", "▶ Health Ollama VM123"),
):
add_run(sid, lbl, "info")
return actions[:limit]