From 113bcdededcc6687eb88481fcb337f09e8b9bb02 Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Tue, 27 Jan 2026 18:32:27 -0300 Subject: [PATCH] atlasbot: use history for subjective follow-ups --- services/comms/scripts/atlasbot/bot.py | 94 ++++++++++++++++++++++++-- 1 file changed, 89 insertions(+), 5 deletions(-) diff --git a/services/comms/scripts/atlasbot/bot.py b/services/comms/scripts/atlasbot/bot.py index ada8dd7..a446a10 100644 --- a/services/comms/scripts/atlasbot/bot.py +++ b/services/comms/scripts/atlasbot/bot.py @@ -191,6 +191,10 @@ _INSIGHT_HINT_WORDS = { "cool", "unique", "notable", + "coolest", + "favorite", + "favourite", + "trivia", } _OVERVIEW_HINT_WORDS = { @@ -1550,6 +1554,21 @@ def _is_insight_query(query: str) -> bool: return False +def _is_subjective_query(query: str) -> bool: + q = normalize_query(query) + if not q: + return False + return any(word in q for word in _INSIGHT_HINT_WORDS) or any( + phrase in q + for phrase in ( + "what do you think", + "your favorite", + "your favourite", + "your opinion", + ) + ) + + def _is_overview_query(query: str) -> bool: q = normalize_query(query) if not q: @@ -1602,9 +1621,9 @@ def _insight_candidates( if postgres_line: candidates.append(("postgres", postgres_line, "high")) - hardware_line = _hardware_mix_line(inventory) - if hardware_line: - candidates.append(("hardware", hardware_line, "medium")) + hardware_insight = _hardware_insight(inventory) + if hardware_insight: + candidates.append(("hardware", hardware_insight, "medium")) pods_line = _pods_summary_line(metrics) if pods_line: @@ -1613,6 +1632,29 @@ def _insight_candidates( return candidates +def _hardware_insight(inventory: list[dict[str, Any]]) -> str: + if not inventory: + return "" + groups = _group_nodes(inventory) + jetsons = groups.get("jetson") or [] + rpi5 = groups.get("rpi5") or [] + rpi4 = groups.get("rpi4") or [] + amd64 = groups.get("amd64") or [] + if jetsons: + jetson_names = ", ".join(jetsons[:2]) + return ( + f"Atlas mixes tiny Raspberry Pi nodes with Jetson accelerators ({jetson_names}) " + f"and AMD64 servers, which is unusual for a homelab cluster." + ) + if amd64 and (rpi5 or rpi4): + return ( + "Atlas mixes small ARM boards with a couple of AMD64 machines, " + "so workloads can land on either low-power or high-power nodes." + ) + line = _hardware_mix_line(inventory) + return line.replace("Hardware mix includes ", "Atlas mixes ") if line else "" + + def _select_insight( prompt: str, candidates: list[tuple[str, str, str]], @@ -1623,6 +1665,8 @@ def _select_insight( prefer_keys: list[str] = [] if any(word in q for word in ("unconventional", "weird", "odd", "unique", "surprising")): prefer_keys.extend(["hardware", "availability"]) + if any(word in q for word in ("coolest", "favorite", "favourite", "trivia", "fun")): + prefer_keys.extend(["hardware", "cpu", "ram"]) if any(word in q for word in ("another", "else", "different", "other")) and len(candidates) > 1: return candidates[1] if prefer_keys: @@ -2284,7 +2328,24 @@ class _AtlasbotHandler(BaseHTTPRequestHandler): snapshot = _snapshot_state() inventory = _snapshot_inventory(snapshot) or node_inventory_live() workloads = _snapshot_workloads(snapshot) - cluster_query = _is_cluster_query(cleaned, inventory=inventory, workloads=workloads) + history_payload = payload.get("history") or [] + history_lines: list[str] = [] + if isinstance(history_payload, list): + for item in history_payload[-10:]: + if isinstance(item, dict): + content = item.get("content") or item.get("message") or "" + if isinstance(content, str) and content.strip(): + history_lines.append(content.strip()) + elif isinstance(item, str) and item.strip(): + history_lines.append(item.strip()) + history_cluster = _history_mentions_cluster( + history_lines, + inventory=inventory, + workloads=workloads, + ) + cluster_query = _is_cluster_query(cleaned, inventory=inventory, workloads=workloads) or ( + _is_subjective_query(cleaned) and history_cluster + ) context = "" if cluster_query: context = build_context( @@ -2329,6 +2390,22 @@ history = collections.defaultdict(list) # (room_id, sender|None) -> list[str] ( def key_for(room_id: str, sender: str, is_dm: bool): return (room_id, None) if is_dm else (room_id, sender) + +def _history_mentions_cluster( + history_lines: list[str], + *, + inventory: list[dict[str, Any]] | None = None, + workloads: list[dict[str, Any]] | None = None, +) -> bool: + recent = [line for line in history_lines[-8:] if isinstance(line, str)] + for line in recent: + cleaned = normalize_query(line) + if not cleaned: + continue + if _is_cluster_query(cleaned, inventory=inventory, workloads=workloads): + return True + return False + def build_context( prompt: str, *, @@ -2734,7 +2811,14 @@ def sync_loop(token: str, room_id: str): if not inventory: inventory = _snapshot_inventory(snapshot) workloads = _snapshot_workloads(snapshot) - cluster_query = _is_cluster_query(cleaned_body, inventory=inventory, workloads=workloads) + history_cluster = _history_mentions_cluster( + history[hist_key], + inventory=inventory, + workloads=workloads, + ) + cluster_query = _is_cluster_query(cleaned_body, inventory=inventory, workloads=workloads) or ( + _is_subjective_query(cleaned_body) and history_cluster + ) context = "" if cluster_query: context = build_context(