"""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 R0–R3, 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]