atlasbot: make cluster answers more narrative
This commit is contained in:
parent
1b04e6cb00
commit
e87fa4369c
@ -181,6 +181,27 @@ CLUSTER_HINT_WORDS = {
|
|||||||
"arm64",
|
"arm64",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_INSIGHT_HINT_WORDS = {
|
||||||
|
"interesting",
|
||||||
|
"unconventional",
|
||||||
|
"surprising",
|
||||||
|
"weird",
|
||||||
|
"odd",
|
||||||
|
"fun",
|
||||||
|
"cool",
|
||||||
|
"unique",
|
||||||
|
"notable",
|
||||||
|
}
|
||||||
|
|
||||||
|
_OVERVIEW_HINT_WORDS = {
|
||||||
|
"overview",
|
||||||
|
"summary",
|
||||||
|
"describe",
|
||||||
|
"explain",
|
||||||
|
"tell me about",
|
||||||
|
"what do you know",
|
||||||
|
}
|
||||||
|
|
||||||
_OLLAMA_LOCK = threading.Lock()
|
_OLLAMA_LOCK = threading.Lock()
|
||||||
|
|
||||||
HARDWARE_HINTS = {
|
HARDWARE_HINTS = {
|
||||||
@ -1408,7 +1429,18 @@ def _nodes_summary_line(inventory: list[dict[str, Any]], snapshot: dict[str, Any
|
|||||||
not_ready = len([n for n in inventory if n.get("ready") is False])
|
not_ready = len([n for n in inventory if n.get("ready") is False])
|
||||||
if total is None:
|
if total is None:
|
||||||
return ""
|
return ""
|
||||||
return f"Atlas cluster has {total} nodes ({ready} ready, {not_ready} not ready)."
|
if not_ready:
|
||||||
|
names = []
|
||||||
|
summary_names = summary.get("not_ready_names") if isinstance(summary, dict) else []
|
||||||
|
if isinstance(summary_names, list):
|
||||||
|
names = [name for name in summary_names if isinstance(name, str)]
|
||||||
|
if not names and snapshot:
|
||||||
|
details = snapshot.get("nodes_detail") if isinstance(snapshot.get("nodes_detail"), list) else []
|
||||||
|
names = [node.get("name") for node in details if isinstance(node, dict) and node.get("ready") is False]
|
||||||
|
names = [name for name in names if isinstance(name, str) and name]
|
||||||
|
suffix = f" (not ready: {', '.join(names)})" if names else ""
|
||||||
|
return f"Atlas has {total} nodes; {ready} ready, {not_ready} not ready{suffix}."
|
||||||
|
return f"Atlas has {total} nodes and all are Ready."
|
||||||
|
|
||||||
|
|
||||||
def _hardware_mix_line(inventory: list[dict[str, Any]]) -> str:
|
def _hardware_mix_line(inventory: list[dict[str, Any]]) -> str:
|
||||||
@ -1422,7 +1454,7 @@ def _hardware_mix_line(inventory: list[dict[str, Any]]) -> str:
|
|||||||
parts.append(f"{key}={len(nodes)}")
|
parts.append(f"{key}={len(nodes)}")
|
||||||
if not parts:
|
if not parts:
|
||||||
return ""
|
return ""
|
||||||
return "Hardware mix: " + ", ".join(parts) + "."
|
return "Hardware mix includes " + ", ".join(parts) + "."
|
||||||
|
|
||||||
|
|
||||||
def _os_mix_line(snapshot: dict[str, Any] | None) -> str:
|
def _os_mix_line(snapshot: dict[str, Any] | None) -> str:
|
||||||
@ -1449,6 +1481,8 @@ def _pods_summary_line(metrics: dict[str, Any]) -> str:
|
|||||||
pending = metrics.get("pods_pending")
|
pending = metrics.get("pods_pending")
|
||||||
failed = metrics.get("pods_failed")
|
failed = metrics.get("pods_failed")
|
||||||
succeeded = metrics.get("pods_succeeded")
|
succeeded = metrics.get("pods_succeeded")
|
||||||
|
if running is None and pending is None and failed is None and succeeded is None:
|
||||||
|
return ""
|
||||||
parts: list[str] = []
|
parts: list[str] = []
|
||||||
if running is not None:
|
if running is not None:
|
||||||
parts.append(f"{running:.0f} running")
|
parts.append(f"{running:.0f} running")
|
||||||
@ -1458,9 +1492,7 @@ def _pods_summary_line(metrics: dict[str, Any]) -> str:
|
|||||||
parts.append(f"{failed:.0f} failed")
|
parts.append(f"{failed:.0f} failed")
|
||||||
if succeeded is not None:
|
if succeeded is not None:
|
||||||
parts.append(f"{succeeded:.0f} succeeded")
|
parts.append(f"{succeeded:.0f} succeeded")
|
||||||
if not parts:
|
return "There are " + ", ".join(parts) + " pods."
|
||||||
return ""
|
|
||||||
return "Pods: " + ", ".join(parts) + "."
|
|
||||||
|
|
||||||
|
|
||||||
def _postgres_summary_line(metrics: dict[str, Any]) -> str:
|
def _postgres_summary_line(metrics: dict[str, Any]) -> str:
|
||||||
@ -1481,7 +1513,7 @@ def _postgres_summary_line(metrics: dict[str, Any]) -> str:
|
|||||||
parts.append(f"hottest {hottest.get('label')} ({hot_val_str})")
|
parts.append(f"hottest {hottest.get('label')} ({hot_val_str})")
|
||||||
if not parts:
|
if not parts:
|
||||||
return ""
|
return ""
|
||||||
return "Postgres: " + ", ".join(parts) + "."
|
return "Postgres is at " + ", ".join(parts) + "."
|
||||||
|
|
||||||
|
|
||||||
def _hottest_summary_line(metrics: dict[str, Any]) -> str:
|
def _hottest_summary_line(metrics: dict[str, Any]) -> str:
|
||||||
@ -1504,7 +1536,101 @@ def _hottest_summary_line(metrics: dict[str, Any]) -> str:
|
|||||||
parts.append(f"{key.upper()} {node} ({value_fmt})")
|
parts.append(f"{key.upper()} {node} ({value_fmt})")
|
||||||
if not parts:
|
if not parts:
|
||||||
return ""
|
return ""
|
||||||
return "Hottest nodes: " + "; ".join(parts) + "."
|
return "Hot spots: " + "; ".join(parts) + "."
|
||||||
|
|
||||||
|
|
||||||
|
def _is_insight_query(query: str) -> bool:
|
||||||
|
q = normalize_query(query)
|
||||||
|
if not q:
|
||||||
|
return False
|
||||||
|
if any(word in q for word in _INSIGHT_HINT_WORDS):
|
||||||
|
return True
|
||||||
|
if "most" in q and any(word in q for word in ("unusual", "odd", "weird", "unconventional")):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _is_overview_query(query: str) -> bool:
|
||||||
|
q = normalize_query(query)
|
||||||
|
if not q:
|
||||||
|
return False
|
||||||
|
return any(word in q for word in _OVERVIEW_HINT_WORDS)
|
||||||
|
|
||||||
|
|
||||||
|
def _doc_intent(query: str) -> bool:
|
||||||
|
q = normalize_query(query)
|
||||||
|
if not q:
|
||||||
|
return False
|
||||||
|
return any(
|
||||||
|
phrase in q
|
||||||
|
for phrase in (
|
||||||
|
"runbook",
|
||||||
|
"documentation",
|
||||||
|
"docs",
|
||||||
|
"guide",
|
||||||
|
"how do i",
|
||||||
|
"how to",
|
||||||
|
"instructions",
|
||||||
|
"playbook",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _insight_candidates(
|
||||||
|
inventory: list[dict[str, Any]],
|
||||||
|
snapshot: dict[str, Any] | None,
|
||||||
|
) -> list[tuple[str, str, str]]:
|
||||||
|
metrics = _snapshot_metrics(snapshot)
|
||||||
|
candidates: list[tuple[str, str, str]] = []
|
||||||
|
|
||||||
|
nodes_line = _nodes_summary_line(inventory, snapshot)
|
||||||
|
if nodes_line and "not ready" in nodes_line.lower():
|
||||||
|
candidates.append(("availability", nodes_line, "high"))
|
||||||
|
|
||||||
|
hottest = metrics.get("hottest_nodes") if isinstance(metrics.get("hottest_nodes"), dict) else {}
|
||||||
|
if hottest:
|
||||||
|
cpu = hottest.get("cpu") if isinstance(hottest.get("cpu"), dict) else {}
|
||||||
|
if cpu.get("node") and cpu.get("value") is not None:
|
||||||
|
value_fmt = _format_metric_value(str(cpu.get("value")), percent=True)
|
||||||
|
candidates.append(("cpu", f"The busiest CPU right now is {cpu.get('node')} at about {value_fmt}.", "high"))
|
||||||
|
ram = hottest.get("ram") if isinstance(hottest.get("ram"), dict) else {}
|
||||||
|
if ram.get("node") and ram.get("value") is not None:
|
||||||
|
value_fmt = _format_metric_value(str(ram.get("value")), percent=True)
|
||||||
|
candidates.append(("ram", f"RAM usage peaks on {ram.get('node')} at about {value_fmt}.", "high"))
|
||||||
|
|
||||||
|
postgres_line = _postgres_summary_line(metrics)
|
||||||
|
if postgres_line:
|
||||||
|
candidates.append(("postgres", postgres_line, "high"))
|
||||||
|
|
||||||
|
hardware_line = _hardware_mix_line(inventory)
|
||||||
|
if hardware_line:
|
||||||
|
candidates.append(("hardware", hardware_line, "medium"))
|
||||||
|
|
||||||
|
pods_line = _pods_summary_line(metrics)
|
||||||
|
if pods_line:
|
||||||
|
candidates.append(("pods", pods_line, "high"))
|
||||||
|
|
||||||
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
|
def _select_insight(
|
||||||
|
prompt: str,
|
||||||
|
candidates: list[tuple[str, str, str]],
|
||||||
|
) -> tuple[str, str] | None:
|
||||||
|
if not candidates:
|
||||||
|
return None
|
||||||
|
q = normalize_query(prompt)
|
||||||
|
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 ("another", "else", "different", "other")) and len(candidates) > 1:
|
||||||
|
return candidates[1][1], candidates[1][2]
|
||||||
|
if prefer_keys:
|
||||||
|
for key, text, conf in candidates:
|
||||||
|
if key in prefer_keys:
|
||||||
|
return text, conf
|
||||||
|
key, text, conf = candidates[0]
|
||||||
|
return text, conf
|
||||||
|
|
||||||
|
|
||||||
def cluster_overview_answer(
|
def cluster_overview_answer(
|
||||||
@ -1517,31 +1643,21 @@ def cluster_overview_answer(
|
|||||||
return ""
|
return ""
|
||||||
q = normalize_query(prompt)
|
q = normalize_query(prompt)
|
||||||
metrics = _snapshot_metrics(snapshot)
|
metrics = _snapshot_metrics(snapshot)
|
||||||
lines: list[str] = []
|
sentences: list[str] = []
|
||||||
|
|
||||||
nodes_line = _nodes_summary_line(inventory, snapshot)
|
nodes_line = _nodes_summary_line(inventory, snapshot)
|
||||||
if nodes_line:
|
if nodes_line:
|
||||||
lines.append(nodes_line)
|
sentences.append(nodes_line)
|
||||||
|
|
||||||
if any(word in q for word in ("hardware", "architecture", "nodes", "node", "cluster", "atlas", "titan", "lab")):
|
wants_overview = _is_overview_query(q) or any(word in q for word in ("atlas", "cluster", "titan", "lab"))
|
||||||
hw_line = _hardware_mix_line(inventory)
|
wants_hardware = any(word in q for word in ("hardware", "architecture", "nodes", "node")) or wants_overview
|
||||||
if hw_line:
|
wants_metrics = any(
|
||||||
lines.append(hw_line)
|
|
||||||
os_line = _os_mix_line(snapshot)
|
|
||||||
if os_line:
|
|
||||||
lines.append(os_line)
|
|
||||||
|
|
||||||
if any(
|
|
||||||
word in q
|
word in q
|
||||||
for word in (
|
for word in (
|
||||||
"interesting",
|
|
||||||
"status",
|
"status",
|
||||||
"health",
|
"health",
|
||||||
"overview",
|
"overview",
|
||||||
"summary",
|
"summary",
|
||||||
"tell me",
|
|
||||||
"what do you know",
|
|
||||||
"about",
|
|
||||||
"pods",
|
"pods",
|
||||||
"postgres",
|
"postgres",
|
||||||
"connections",
|
"connections",
|
||||||
@ -1558,20 +1674,32 @@ def cluster_overview_answer(
|
|||||||
"usage",
|
"usage",
|
||||||
"utilization",
|
"utilization",
|
||||||
)
|
)
|
||||||
):
|
) or wants_overview
|
||||||
|
|
||||||
|
if wants_hardware:
|
||||||
|
hw_line = _hardware_mix_line(inventory)
|
||||||
|
if hw_line:
|
||||||
|
sentences.append(hw_line)
|
||||||
|
os_line = _os_mix_line(snapshot)
|
||||||
|
if os_line:
|
||||||
|
sentences.append(os_line)
|
||||||
|
|
||||||
|
if wants_metrics:
|
||||||
pods_line = _pods_summary_line(metrics)
|
pods_line = _pods_summary_line(metrics)
|
||||||
if pods_line:
|
if pods_line:
|
||||||
lines.append(pods_line)
|
sentences.append(pods_line)
|
||||||
hottest_line = _hottest_summary_line(metrics)
|
|
||||||
if hottest_line:
|
|
||||||
lines.append(hottest_line)
|
|
||||||
postgres_line = _postgres_summary_line(metrics)
|
postgres_line = _postgres_summary_line(metrics)
|
||||||
if postgres_line:
|
if postgres_line:
|
||||||
lines.append(postgres_line)
|
sentences.append(postgres_line)
|
||||||
|
hottest_line = _hottest_summary_line(metrics)
|
||||||
|
if hottest_line:
|
||||||
|
sentences.append(hottest_line)
|
||||||
|
|
||||||
if not lines:
|
if not sentences:
|
||||||
return ""
|
return ""
|
||||||
return "Based on the snapshot, " + "\n".join(lines)
|
if len(sentences) > 3 and not wants_overview:
|
||||||
|
sentences = sentences[:3]
|
||||||
|
return "Based on the latest snapshot, " + " ".join(sentences)
|
||||||
|
|
||||||
|
|
||||||
def cluster_answer(
|
def cluster_answer(
|
||||||
@ -1582,6 +1710,12 @@ def cluster_answer(
|
|||||||
workloads: list[dict[str, Any]] | None,
|
workloads: list[dict[str, Any]] | None,
|
||||||
) -> str:
|
) -> str:
|
||||||
metrics_summary = snapshot_context(prompt, snapshot)
|
metrics_summary = snapshot_context(prompt, snapshot)
|
||||||
|
if _is_insight_query(prompt):
|
||||||
|
candidates = _insight_candidates(inventory, snapshot)
|
||||||
|
selected = _select_insight(prompt, candidates)
|
||||||
|
if selected:
|
||||||
|
text, confidence = selected
|
||||||
|
return _format_confidence(text, confidence)
|
||||||
structured = structured_answer(
|
structured = structured_answer(
|
||||||
prompt,
|
prompt,
|
||||||
inventory=inventory,
|
inventory=inventory,
|
||||||
@ -1602,7 +1736,7 @@ def cluster_answer(
|
|||||||
|
|
||||||
overview = cluster_overview_answer(prompt, inventory=inventory, snapshot=snapshot)
|
overview = cluster_overview_answer(prompt, inventory=inventory, snapshot=snapshot)
|
||||||
if overview:
|
if overview:
|
||||||
kb_titles = kb_retrieve_titles(prompt, limit=4) if _knowledge_intent(prompt) else ""
|
kb_titles = kb_retrieve_titles(prompt, limit=4) if _doc_intent(prompt) else ""
|
||||||
if kb_titles:
|
if kb_titles:
|
||||||
overview = overview + "\n" + kb_titles
|
overview = overview + "\n" + kb_titles
|
||||||
return _format_confidence(overview, "medium")
|
return _format_confidence(overview, "medium")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user