atlasbot: improve insight voice and avoid repeats
This commit is contained in:
parent
58dab1ca79
commit
4e6d4f43b2
@ -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"
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user