From 4e6d4f43b2551f7175179f4cfc6da6aa40fb1c89 Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Tue, 27 Jan 2026 18:43:03 -0300 Subject: [PATCH] atlasbot: improve insight voice and avoid repeats --- services/comms/atlasbot-deployment.yaml | 2 +- services/comms/scripts/atlasbot/bot.py | 86 ++++++++++++++++++++----- 2 files changed, 70 insertions(+), 18 deletions(-) diff --git a/services/comms/atlasbot-deployment.yaml b/services/comms/atlasbot-deployment.yaml index 03e9dc2..dc1b0bb 100644 --- a/services/comms/atlasbot-deployment.yaml +++ b/services/comms/atlasbot-deployment.yaml @@ -16,7 +16,7 @@ spec: labels: app: atlasbot annotations: - checksum/atlasbot-configmap: manual-atlasbot-54 + checksum/atlasbot-configmap: manual-atlasbot-55 vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/role: "comms" vault.hashicorp.com/agent-inject-secret-turn-secret: "kv/data/atlas/comms/turn-shared-secret" diff --git a/services/comms/scripts/atlasbot/bot.py b/services/comms/scripts/atlasbot/bot.py index a446a10..2616cb1 100644 --- a/services/comms/scripts/atlasbot/bot.py +++ b/services/comms/scripts/atlasbot/bot.py @@ -1640,27 +1640,49 @@ def _hardware_insight(inventory: list[dict[str, Any]]) -> str: rpi5 = groups.get("rpi5") or [] rpi4 = groups.get("rpi4") or [] amd64 = groups.get("amd64") or [] + parts: list[str] = [] + if rpi5: + parts.append(f"rpi5={len(rpi5)}") + if rpi4: + parts.append(f"rpi4={len(rpi4)}") 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 "" + parts.append(f"jetson={len(jetsons)} ({jetson_names})") + if amd64: + parts.append(f"amd64={len(amd64)}") + return ", ".join(parts) + + +def _recent_insight_keys(history_lines: list[str]) -> set[str]: + used: set[str] = set() + for line in history_lines[-10:]: + lower = normalize_query(line) + if not lower: + continue + if "postgres" in lower or "connections" in lower: + used.add("postgres") + if "atlas mixes" in lower or "hardware" in lower or "rpi" in lower or "jetson" in lower: + used.add("hardware") + if "busiest cpu" in lower or "cpu right now" in lower or "cpu " in lower: + used.add("cpu") + if "ram usage" in lower or "memory" in lower: + used.add("ram") + if "pods" in lower: + used.add("pods") + if "not ready" in lower: + used.add("availability") + return used def _select_insight( prompt: str, candidates: list[tuple[str, str, str]], + *, + used_keys: set[str] | None = None, ) -> tuple[str, str, str] | None: if not candidates: return None + used = used_keys or set() q = normalize_query(prompt) prefer_keys: list[str] = [] if any(word in q for word in ("unconventional", "weird", "odd", "unique", "surprising")): @@ -1668,11 +1690,21 @@ def _select_insight( 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: + for candidate in candidates: + if candidate[0] not in used: + return candidate return candidates[1] if prefer_keys: + for key, text, conf in candidates: + if key in prefer_keys and key not in used: + return key, text, conf for key, text, conf in candidates: if key in prefer_keys: return key, text, conf + if used: + for candidate in candidates: + if candidate[0] not in used: + return candidate return candidates[0] @@ -1681,29 +1713,45 @@ def _format_insight_text(key: str, text: str) -> str: if not cleaned: return "" if key == "hardware": - counts = cleaned.replace("Hardware mix includes ", "") - return f"Atlas mixes Raspberry Pi, Jetson, and AMD64 nodes ({counts})." + counts = ( + cleaned.replace("Hardware mix includes ", "") + .replace("Atlas mixes tiny ", "") + .replace("Atlas mixes ", "") + .replace("which is unusual for a homelab cluster", "") + .strip() + .strip(".") + ) + return f"the mixed hardware stack ({counts}) is a bit unconventional for a homelab." if key == "postgres": detail = cleaned.replace("Postgres is at ", "") - return f"Postgres looks healthy at {detail}." + return f"Postgres looks healthy at {detail}; that suggests moderate load." if key == "pods": detail = cleaned.replace("There are ", "") return f"Pods look stable with {detail}." if key == "availability": return cleaned + "." if key in ("cpu", "ram"): - return cleaned + "." + suffix = " That likely marks the busiest workload right now." if key == "cpu" else " That box is carrying the heaviest memory load." + return cleaned + "." + suffix return cleaned + "." def _insight_prefix(prompt: str) -> str: q = normalize_query(prompt) + if "coolest" in q: + return "If I had to pick the coolest detail, it's " + if "favorite" in q or "favourite" in q: + return "My favorite detail is " + if "trivia" in q: + return "A bit of trivia I like: " + if "most interesting" in q: + return "The most interesting detail to me is " if any(word in q for word in ("another", "else", "different", "other")): return "Another interesting detail: " if any(word in q for word in ("unconventional", "weird", "odd", "unique", "surprising")): return "What stands out is that " if any(word in q for word in ("interesting", "notable", "fun", "cool")): - return "One notable detail: " + return "One thing I'd highlight is " return "" @@ -1782,11 +1830,13 @@ def cluster_answer( inventory: list[dict[str, Any]], snapshot: dict[str, Any] | None, workloads: list[dict[str, Any]] | None, + history_lines: list[str] | None = None, ) -> str: metrics_summary = snapshot_context(prompt, snapshot) if _is_insight_query(prompt): candidates = _insight_candidates(inventory, snapshot) - selected = _select_insight(prompt, candidates) + used_keys = _recent_insight_keys(history_lines or []) + selected = _select_insight(prompt, candidates, used_keys=used_keys) if selected: key, raw_text, confidence = selected formatted = _format_insight_text(key, raw_text) @@ -2363,6 +2413,7 @@ class _AtlasbotHandler(BaseHTTPRequestHandler): inventory=inventory, snapshot=snapshot, workloads=workloads, + history_lines=history_lines, ) if not answer: answer = fallback @@ -2843,6 +2894,7 @@ def sync_loop(token: str, room_id: str): inventory=inventory, snapshot=snapshot, workloads=workloads, + history_lines=history[hist_key], ) if not reply: reply = fallback