atlasbot: improve runbook and hardware snapshot

This commit is contained in:
Brad Stein 2026-02-01 03:37:52 -03:00
parent 2386d5c832
commit 90688c244d
3 changed files with 53 additions and 0 deletions

View File

@ -297,6 +297,18 @@ class AnswerEngine:
runbook_fix = _needs_runbook_fix(reply, runbook_paths) runbook_fix = _needs_runbook_fix(reply, runbook_paths)
runbook_needed = _needs_runbook_reference(normalized, runbook_paths, reply) runbook_needed = _needs_runbook_reference(normalized, runbook_paths, reply)
needs_evidence = _needs_evidence_fix(reply, classify) needs_evidence = _needs_evidence_fix(reply, classify)
resolved_runbook = None
if runbook_paths and (runbook_fix or runbook_needed):
resolver_prompt = prompts.RUNBOOK_SELECT_PROMPT + "\nQuestion: " + normalized
resolver_raw = await call_llm(
prompts.RUNBOOK_SELECT_SYSTEM,
resolver_prompt,
context="AllowedRunbooks:\n" + "\n".join(runbook_paths),
model=plan.fast_model,
tag="runbook_select",
)
resolver = _parse_json_block(resolver_raw, fallback={})
resolved_runbook = resolver.get("path") if isinstance(resolver.get("path"), str) else None
if (snapshot_context and needs_evidence) or unknown_nodes or unknown_namespaces or runbook_fix or runbook_needed: if (snapshot_context and needs_evidence) or unknown_nodes or unknown_namespaces or runbook_fix or runbook_needed:
if observer: if observer:
observer("evidence_fix", "repairing missing evidence") observer("evidence_fix", "repairing missing evidence")
@ -307,6 +319,8 @@ class AnswerEngine:
extra_bits.append("UnknownNamespaces: " + ", ".join(sorted(unknown_namespaces))) extra_bits.append("UnknownNamespaces: " + ", ".join(sorted(unknown_namespaces)))
if runbook_paths: if runbook_paths:
extra_bits.append("AllowedRunbooks: " + ", ".join(runbook_paths)) extra_bits.append("AllowedRunbooks: " + ", ".join(runbook_paths))
if resolved_runbook:
extra_bits.append("ResolvedRunbook: " + resolved_runbook)
if allowed_nodes: if allowed_nodes:
extra_bits.append("AllowedNodes: " + ", ".join(allowed_nodes)) extra_bits.append("AllowedNodes: " + ", ".join(allowed_nodes))
if allowed_namespaces: if allowed_namespaces:
@ -847,11 +861,14 @@ def _needs_evidence_fix(reply: str, classify: dict[str, Any]) -> bool:
"need to", "need to",
"would need", "would need",
"does not provide", "does not provide",
"does not mention",
"not mention",
"not provided", "not provided",
"not in context", "not in context",
"not referenced", "not referenced",
"missing", "missing",
"no specific", "no specific",
"no information",
) )
if classify.get("needs_snapshot") and any(marker in lowered for marker in missing_markers): if classify.get("needs_snapshot") and any(marker in lowered for marker in missing_markers):
return True return True

View File

@ -104,6 +104,17 @@ EVIDENCE_FIX_PROMPT = (
"documentation or checklist questions and do not invent new paths." "documentation or checklist questions and do not invent new paths."
) )
RUNBOOK_SELECT_SYSTEM = (
CLUSTER_SYSTEM
+ " Select the single best runbook path from the allowed list. "
+ "Return JSON only."
)
RUNBOOK_SELECT_PROMPT = (
"Pick the best runbook path for the question from the AllowedRunbooks list. "
"Return JSON with field: path. If none apply, return {\"path\": \"\"}."
)
DRAFT_SELECT_PROMPT = ( DRAFT_SELECT_PROMPT = (
"Pick the best draft for accuracy, clarity, and helpfulness. " "Pick the best draft for accuracy, clarity, and helpfulness. "
"Return JSON with field: best (1-based index)." "Return JSON with field: best (1-based index)."

View File

@ -625,6 +625,21 @@ def _append_hardware(lines: list[str], summary: dict[str, Any]) -> None:
lines.append("hardware: " + "; ".join(sorted(parts))) lines.append("hardware: " + "; ".join(sorted(parts)))
def _append_hardware_groups(lines: list[str], summary: dict[str, Any]) -> None:
hardware = summary.get("hardware") if isinstance(summary.get("hardware"), dict) else {}
if not hardware:
return
parts = []
for key, names in hardware.items():
if not isinstance(names, list):
continue
name_list = _format_names([str(name) for name in names if name])
if name_list:
parts.append(f"{key}={name_list}")
if parts:
lines.append("hardware_nodes: " + "; ".join(sorted(parts)))
def _append_node_ages(lines: list[str], summary: dict[str, Any]) -> None: def _append_node_ages(lines: list[str], summary: dict[str, Any]) -> None:
ages = summary.get("node_ages") if isinstance(summary.get("node_ages"), list) else [] ages = summary.get("node_ages") if isinstance(summary.get("node_ages"), list) else []
if not ages: if not ages:
@ -1308,6 +1323,15 @@ def _append_postgres(lines: list[str], summary: dict[str, Any]) -> None:
hottest=hottest, hottest=hottest,
) )
) )
used = postgres.get("used")
max_conn = postgres.get("max")
if used is not None or max_conn is not None:
lines.append(
"postgres_connections_total: used={used}, max={max}".format(
used=_format_float(used),
max=_format_float(max_conn),
)
)
by_db = postgres.get("by_db") by_db = postgres.get("by_db")
if isinstance(by_db, list) and by_db: if isinstance(by_db, list) and by_db:
parts = [] parts = []
@ -1805,6 +1829,7 @@ def summary_text(snapshot: dict[str, Any] | None) -> str:
lines.append("snapshot: " + ", ".join(bits)) lines.append("snapshot: " + ", ".join(bits))
_append_nodes(lines, summary) _append_nodes(lines, summary)
_append_hardware(lines, summary) _append_hardware(lines, summary)
_append_hardware_groups(lines, summary)
_append_lexicon(lines, summary) _append_lexicon(lines, summary)
_append_pressure(lines, summary) _append_pressure(lines, summary)
_append_node_facts(lines, summary) _append_node_facts(lines, summary)