From fd0d03b0dd799cee365cb649942d3296bfaeec06 Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Mon, 30 Mar 2026 17:38:20 -0300 Subject: [PATCH] atlasbot: enforce longhorn-qualified fallback matches --- services/comms/scripts/atlasbot/bot.py | 21 +++++++++++- .../scripts/tests/test_atlasbot_modes.py | 34 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/services/comms/scripts/atlasbot/bot.py b/services/comms/scripts/atlasbot/bot.py index cc3ba003..d9883747 100644 --- a/services/comms/scripts/atlasbot/bot.py +++ b/services/comms/scripts/atlasbot/bot.py @@ -2125,6 +2125,9 @@ def _doc_intent(query: str) -> bool: "triage", "recover", "remediate", + "longhorn", + "astreae", + "asteria", ) ) @@ -4061,6 +4064,7 @@ def _fallback_fact_answer(prompt: str, context: str) -> str: hottest_intent = any(word in q for word in ("hottest", "highest", "most", "top", "busiest")) metric = _detect_metric(q) include_hw, _exclude_hw = _detect_hardware_filters(q) + wants_longhorn = any(word in tokens for word in ("longhorn", "astreae", "asteria")) if hottest_intent and metric in {"cpu", "ram", "net", "io"}: hottest_val = _find_value(f"hottest_{metric}") @@ -4099,6 +4103,18 @@ def _fallback_fact_answer(prompt: str, context: str) -> str: return f"Not ready nodes: {match.group(1)}." if count_intent and include_hw: + if wants_longhorn: + for hw in include_hw: + for fact, key, val in parsed_facts: + key_tokens = set(_tokens(key or fact)) + if "longhorn" not in key_tokens: + continue + if hw not in key_tokens: + continue + nodes = _extract_titan_nodes(val or fact) + if nodes: + return f"{hw} longhorn nodes: {len(nodes)}." + return "" counts_line = _find_value("nodes_by_hardware_count") if counts_line: counts = _parse_counts(counts_line) @@ -4112,7 +4128,6 @@ def _fallback_fact_answer(prompt: str, context: str) -> str: return f"{hw} nodes: {len(items)}." if list_intent and include_hw: - wants_longhorn = "longhorn" in tokens if "control" in q: cp_by_hw = _find_value("control_plane_by_hardware") if cp_by_hw: @@ -4206,6 +4221,10 @@ def _fallback_fact_answer(prompt: str, context: str) -> str: best_fact = "" best_score = -1 for fact in facts: + if wants_longhorn: + fact_tokens = set(_tokens(fact)) + if not ({"longhorn", "astreae", "asteria"} & fact_tokens): + continue key_match = re.match(r"^([\w\s/.-]+):\s*(.+)$", fact) if not key_match: key_match = re.match(r"^([\w\s/.-]+)=\s*(.+)$", fact) diff --git a/services/comms/scripts/tests/test_atlasbot_modes.py b/services/comms/scripts/tests/test_atlasbot_modes.py index f2886386..15f7d536 100644 --- a/services/comms/scripts/tests/test_atlasbot_modes.py +++ b/services/comms/scripts/tests/test_atlasbot_modes.py @@ -191,3 +191,37 @@ class AtlasbotModeTests(TestCase): self.assertNotIn("titan-14", reply) self.assertNotIn("titan-18", reply) self.assertIn("Confidence:", reply) + + def test_longhorn_query_uses_kb_context_when_snapshot_only_has_generic_rpi4(self): + fact_lines = [ + "rpi4: titan-12, titan-13, titan-14, titan-15, titan-17, titan-18, titan-19", + ] + kb_detail = ( + "Atlas KB (retrieved):\n" + "- metis (knowledge/metis.md)\n" + "rpi4 armbian longhorn: titan-13/15/17/19" + ) + + with ( + mock.patch.object(self.bot, "_fact_pack_lines", return_value=fact_lines), + mock.patch.object(self.bot, "kb_retrieve", return_value=kb_detail), + mock.patch.object(self.bot, "_ollama_call", side_effect=AssertionError("LLM should not be called")), + ): + reply = self.bot.open_ended_answer( + "which nodes in titan are the rpi4 longhorn nodes?", + inventory=[], + snapshot=None, + workloads=[], + history_lines=[], + mode="smart", + allow_tools=True, + ) + + self.assertIn("titan-13", reply) + self.assertIn("titan-15", reply) + self.assertIn("titan-17", reply) + self.assertIn("titan-19", reply) + self.assertNotIn("titan-12", reply) + self.assertNotIn("titan-14", reply) + self.assertNotIn("titan-18", reply) + self.assertIn("Confidence:", reply)