atlasbot: improve insight voice and avoid repeats

This commit is contained in:
Brad Stein 2026-01-27 18:43:03 -03:00
parent 58dab1ca79
commit 4e6d4f43b2
2 changed files with 70 additions and 18 deletions

View File

@ -16,7 +16,7 @@ spec:
labels: labels:
app: atlasbot app: atlasbot
annotations: annotations:
checksum/atlasbot-configmap: manual-atlasbot-54 checksum/atlasbot-configmap: manual-atlasbot-55
vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "comms" vault.hashicorp.com/role: "comms"
vault.hashicorp.com/agent-inject-secret-turn-secret: "kv/data/atlas/comms/turn-shared-secret" vault.hashicorp.com/agent-inject-secret-turn-secret: "kv/data/atlas/comms/turn-shared-secret"

View File

@ -1640,27 +1640,49 @@ def _hardware_insight(inventory: list[dict[str, Any]]) -> str:
rpi5 = groups.get("rpi5") or [] rpi5 = groups.get("rpi5") or []
rpi4 = groups.get("rpi4") or [] rpi4 = groups.get("rpi4") or []
amd64 = groups.get("amd64") 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: if jetsons:
jetson_names = ", ".join(jetsons[:2]) jetson_names = ", ".join(jetsons[:2])
return ( parts.append(f"jetson={len(jetsons)} ({jetson_names})")
f"Atlas mixes tiny Raspberry Pi nodes with Jetson accelerators ({jetson_names}) " if amd64:
f"and AMD64 servers, which is unusual for a homelab cluster." parts.append(f"amd64={len(amd64)}")
) return ", ".join(parts)
if amd64 and (rpi5 or rpi4):
return (
"Atlas mixes small ARM boards with a couple of AMD64 machines, " def _recent_insight_keys(history_lines: list[str]) -> set[str]:
"so workloads can land on either low-power or high-power nodes." used: set[str] = set()
) for line in history_lines[-10:]:
line = _hardware_mix_line(inventory) lower = normalize_query(line)
return line.replace("Hardware mix includes ", "Atlas mixes ") if line else "" 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( def _select_insight(
prompt: str, prompt: str,
candidates: list[tuple[str, str, str]], candidates: list[tuple[str, str, str]],
*,
used_keys: set[str] | None = None,
) -> tuple[str, str, str] | None: ) -> tuple[str, str, str] | None:
if not candidates: if not candidates:
return None return None
used = used_keys or set()
q = normalize_query(prompt) q = normalize_query(prompt)
prefer_keys: list[str] = [] prefer_keys: list[str] = []
if any(word in q for word in ("unconventional", "weird", "odd", "unique", "surprising")): 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")): if any(word in q for word in ("coolest", "favorite", "favourite", "trivia", "fun")):
prefer_keys.extend(["hardware", "cpu", "ram"]) prefer_keys.extend(["hardware", "cpu", "ram"])
if any(word in q for word in ("another", "else", "different", "other")) and len(candidates) > 1: 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] return candidates[1]
if prefer_keys: 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: for key, text, conf in candidates:
if key in prefer_keys: if key in prefer_keys:
return key, text, conf return key, text, conf
if used:
for candidate in candidates:
if candidate[0] not in used:
return candidate
return candidates[0] return candidates[0]
@ -1681,29 +1713,45 @@ def _format_insight_text(key: str, text: str) -> str:
if not cleaned: if not cleaned:
return "" return ""
if key == "hardware": if key == "hardware":
counts = cleaned.replace("Hardware mix includes ", "") counts = (
return f"Atlas mixes Raspberry Pi, Jetson, and AMD64 nodes ({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": if key == "postgres":
detail = cleaned.replace("Postgres is at ", "") 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": if key == "pods":
detail = cleaned.replace("There are ", "") detail = cleaned.replace("There are ", "")
return f"Pods look stable with {detail}." return f"Pods look stable with {detail}."
if key == "availability": if key == "availability":
return cleaned + "." return cleaned + "."
if key in ("cpu", "ram"): 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 + "." return cleaned + "."
def _insight_prefix(prompt: str) -> str: def _insight_prefix(prompt: str) -> str:
q = normalize_query(prompt) 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")): if any(word in q for word in ("another", "else", "different", "other")):
return "Another interesting detail: " return "Another interesting detail: "
if any(word in q for word in ("unconventional", "weird", "odd", "unique", "surprising")): if any(word in q for word in ("unconventional", "weird", "odd", "unique", "surprising")):
return "What stands out is that " return "What stands out is that "
if any(word in q for word in ("interesting", "notable", "fun", "cool")): if any(word in q for word in ("interesting", "notable", "fun", "cool")):
return "One notable detail: " return "One thing I'd highlight is "
return "" return ""
@ -1782,11 +1830,13 @@ def cluster_answer(
inventory: list[dict[str, Any]], inventory: list[dict[str, Any]],
snapshot: dict[str, Any] | None, snapshot: dict[str, Any] | None,
workloads: list[dict[str, Any]] | None, workloads: list[dict[str, Any]] | None,
history_lines: list[str] | None = None,
) -> str: ) -> str:
metrics_summary = snapshot_context(prompt, snapshot) metrics_summary = snapshot_context(prompt, snapshot)
if _is_insight_query(prompt): if _is_insight_query(prompt):
candidates = _insight_candidates(inventory, snapshot) 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: if selected:
key, raw_text, confidence = selected key, raw_text, confidence = selected
formatted = _format_insight_text(key, raw_text) formatted = _format_insight_text(key, raw_text)
@ -2363,6 +2413,7 @@ class _AtlasbotHandler(BaseHTTPRequestHandler):
inventory=inventory, inventory=inventory,
snapshot=snapshot, snapshot=snapshot,
workloads=workloads, workloads=workloads,
history_lines=history_lines,
) )
if not answer: if not answer:
answer = fallback answer = fallback
@ -2843,6 +2894,7 @@ def sync_loop(token: str, room_id: str):
inventory=inventory, inventory=inventory,
snapshot=snapshot, snapshot=snapshot,
workloads=workloads, workloads=workloads,
history_lines=history[hist_key],
) )
if not reply: if not reply:
reply = fallback reply = fallback