From d8c451dc803c689db0defd95a38fa8f7f0a5177d Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Mon, 30 Mar 2026 17:32:46 -0300 Subject: [PATCH] atlasbot: prefer qualified longhorn node subsets --- services/comms/scripts/atlasbot/bot.py | 49 +++++++++++++++++++ .../scripts/tests/test_atlasbot_modes.py | 29 +++++++++++ 2 files changed, 78 insertions(+) diff --git a/services/comms/scripts/atlasbot/bot.py b/services/comms/scripts/atlasbot/bot.py index bc5eb7d4..cc3ba003 100644 --- a/services/comms/scripts/atlasbot/bot.py +++ b/services/comms/scripts/atlasbot/bot.py @@ -4112,6 +4112,7 @@ 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: @@ -4122,11 +4123,59 @@ def _fallback_fact_answer(prompt: str, context: str) -> str: cp_nodes = _find_value("control_plane_nodes") if cp_nodes: return f"Control-plane nodes: {cp_nodes}." + if wants_longhorn: + for hw in include_hw: + best_nodes: list[str] = [] + best_val = "" + for _fact, key, val in parsed_facts: + if not key or not val: + continue + key_tokens = set(_tokens(key)) + if "longhorn" not in key_tokens: + continue + if hw not in key_tokens: + continue + nodes = _extract_titan_nodes(val) + if nodes and len(nodes) > len(best_nodes): + best_nodes = nodes + best_val = val + elif not best_nodes and val: + best_val = val + if best_nodes: + return f"{hw} longhorn nodes: {', '.join(best_nodes)}." + if best_val: + return f"{hw} longhorn nodes: {best_val}." for hw in include_hw: + if wants_longhorn: + continue hw_line = _find_value(hw) if hw_line: return f"{hw} nodes: {hw_line}." + if list_intent and "longhorn" in tokens: + best_nodes: list[str] = [] + best_key = "" + best_val = "" + for _fact, key, val in parsed_facts: + if not key or not val: + continue + key_tokens = set(_tokens(key)) + if "longhorn" not in key_tokens: + continue + nodes = _extract_titan_nodes(val) + if nodes and len(nodes) > len(best_nodes): + best_nodes = nodes + best_key = key + best_val = val + elif not best_nodes and val: + best_key = key + best_val = val + if best_nodes: + return f"Longhorn nodes: {', '.join(best_nodes)}." + if best_val: + label = (best_key or "Longhorn nodes").replace("_", " ").strip() + return f"{label.capitalize()}: {best_val}." + if list_intent and "control" in q: cp_nodes = _find_value("control_plane_nodes") if cp_nodes: diff --git a/services/comms/scripts/tests/test_atlasbot_modes.py b/services/comms/scripts/tests/test_atlasbot_modes.py index 895b8da0..f2886386 100644 --- a/services/comms/scripts/tests/test_atlasbot_modes.py +++ b/services/comms/scripts/tests/test_atlasbot_modes.py @@ -162,3 +162,32 @@ class AtlasbotModeTests(TestCase): self.assertIn("worker nodes", reply.lower()) self.assertIn("Confidence:", reply) + + def test_longhorn_rpi4_subset_beats_generic_rpi4_list(self): + fact_lines = [ + "rpi4: titan-12, titan-13, titan-14, titan-15, titan-17, titan-18, titan-19", + "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, "_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)