From 59f17403ab55c7dd529516626760d09c0d186b0c Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Sun, 1 Feb 2026 04:31:19 -0300 Subject: [PATCH] atlasbot: emphasize key facts and validate runbooks --- atlasbot/engine/answerer.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/atlasbot/engine/answerer.py b/atlasbot/engine/answerer.py index ace5488..710581e 100644 --- a/atlasbot/engine/answerer.py +++ b/atlasbot/engine/answerer.py @@ -4,6 +4,7 @@ import logging import math import re import time +import difflib from dataclasses import dataclass from typing import Any, Callable @@ -254,6 +255,7 @@ class AnswerEngine: chunks = _chunk_lines(summary_lines, plan.chunk_lines) scored = await _score_chunks(call_llm, chunks, normalized, sub_questions, plan) selected = _select_chunks(chunks, scored, plan, keyword_tokens) + key_facts = _key_fact_lines(summary_lines, keyword_tokens) if self._settings.debug_pipeline: scored_preview = sorted( [{"id": c["id"], "score": scored.get(c["id"], 0.0), "summary": c["summary"]} for c in chunks], @@ -268,6 +270,8 @@ class AnswerEngine: }, ) snapshot_context = "ClusterSnapshot:\n" + "\n".join([chunk["text"] for chunk in selected]) + if key_facts: + snapshot_context = "KeyFacts:\n" + "\n".join(key_facts) + "\n\n" + snapshot_context context = _join_context( [kb_summary, _format_runbooks(runbooks), snapshot_context, history_ctx if classify.get("follow_up") else ""] @@ -372,6 +376,8 @@ class AnswerEngine: ) resolver = _parse_json_block(resolver_raw, fallback={}) candidate = resolver.get("path") if isinstance(resolver.get("path"), str) else None + if not (candidate and candidate in runbook_paths): + candidate = _best_runbook_match(invalid[0], runbook_paths) if candidate and candidate in runbook_paths: enforce_prompt = prompts.RUNBOOK_ENFORCE_PROMPT.format(path=candidate) reply = await call_llm( @@ -845,6 +851,22 @@ def _summary_lines(snapshot: dict[str, Any] | None) -> list[str]: return [line for line in text.splitlines() if line.strip()] +def _key_fact_lines(lines: list[str], keywords: list[str] | None, limit: int = 6) -> list[str]: + if not lines or not keywords: + return [] + lowered = [kw.lower() for kw in keywords if kw] + if not lowered: + return [] + matches: list[str] = [] + for line in lines: + line_lower = line.lower() + if any(kw in line_lower for kw in lowered): + matches.append(line) + if len(matches) >= limit: + break + return matches + + def _lexicon_context(summary: dict[str, Any]) -> str: if not isinstance(summary, dict): return "" @@ -1086,6 +1108,19 @@ def _needs_runbook_reference(question: str, allowed: list[str], reply: str) -> b return True +def _best_runbook_match(candidate: str, allowed: list[str]) -> str | None: + if not candidate or not allowed: + return None + best = None + best_score = 0.0 + for path in allowed: + score = difflib.SequenceMatcher(a=candidate.lower(), b=path.lower()).ratio() + if score > best_score: + best_score = score + best = path + return best if best_score >= 0.4 else None + + def _resolve_path(data: Any, path: str) -> Any | None: cursor = data for part in re.split(r"\.(?![^\[]*\])", path):