atlasbot: improve metric detection and counts
This commit is contained in:
parent
67ca0d451d
commit
7b1c891e70
@ -120,6 +120,7 @@ OPERATION_HINTS = {
|
|||||||
"count": ("how many", "count", "number", "total"),
|
"count": ("how many", "count", "number", "total"),
|
||||||
"list": ("list", "which", "what are", "show", "names"),
|
"list": ("list", "which", "what are", "show", "names"),
|
||||||
"top": ("top", "hottest", "highest", "most", "largest", "max", "maximum", "busiest", "busy"),
|
"top": ("top", "hottest", "highest", "most", "largest", "max", "maximum", "busiest", "busy"),
|
||||||
|
"bottom": ("lowest", "least", "minimum", "min", "smallest"),
|
||||||
"status": ("ready", "not ready", "unready", "down", "missing", "status"),
|
"status": ("ready", "not ready", "unready", "down", "missing", "status"),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -568,6 +569,14 @@ def _detect_operation(q: str) -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _detect_metric(q: str) -> str | None:
|
def _detect_metric(q: str) -> str | None:
|
||||||
|
q = normalize_query(q)
|
||||||
|
if _has_any(q, ("disk", "storage")):
|
||||||
|
return "io"
|
||||||
|
if _has_any(q, ("io",)) and not _has_any(q, METRIC_HINTS["net"]):
|
||||||
|
return "io"
|
||||||
|
for metric, phrases in METRIC_HINTS.items():
|
||||||
|
if _has_any(q, phrases):
|
||||||
|
return metric
|
||||||
tokens = set(_tokens(q))
|
tokens = set(_tokens(q))
|
||||||
expanded: set[str] = set(tokens)
|
expanded: set[str] = set(tokens)
|
||||||
for token in list(tokens):
|
for token in list(tokens):
|
||||||
@ -1237,6 +1246,34 @@ def _node_usage_top(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _node_usage_bottom(
|
||||||
|
usage: list[dict[str, Any]],
|
||||||
|
*,
|
||||||
|
allowed_nodes: set[str] | None,
|
||||||
|
) -> tuple[str, float] | None:
|
||||||
|
best_node: str | None = None
|
||||||
|
best_val: float | None = None
|
||||||
|
for item in usage:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
node = item.get("node")
|
||||||
|
if not node or not isinstance(node, str):
|
||||||
|
continue
|
||||||
|
if allowed_nodes and node not in allowed_nodes:
|
||||||
|
continue
|
||||||
|
value = item.get("value")
|
||||||
|
try:
|
||||||
|
numeric = float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
if best_val is None or numeric < best_val:
|
||||||
|
best_val = numeric
|
||||||
|
best_node = node
|
||||||
|
if best_node and best_val is not None:
|
||||||
|
return best_node, best_val
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def snapshot_metric_answer(
|
def snapshot_metric_answer(
|
||||||
prompt: str,
|
prompt: str,
|
||||||
*,
|
*,
|
||||||
@ -1267,18 +1304,20 @@ def snapshot_metric_answer(
|
|||||||
)
|
)
|
||||||
allowed_nodes = {node["name"] for node in filtered} if filtered else None
|
allowed_nodes = {node["name"] for node in filtered} if filtered else None
|
||||||
|
|
||||||
if metric in {"cpu", "ram", "net", "io"} and op in {"top", "status", None}:
|
if metric in {"cpu", "ram", "net", "io"} and op in {"top", "bottom", "status", None}:
|
||||||
usage = metrics.get("node_usage", {}).get(metric, [])
|
usage = metrics.get("node_usage", {}).get(metric, [])
|
||||||
top = _node_usage_top(usage, allowed_nodes=allowed_nodes)
|
pick = _node_usage_bottom if op == "bottom" else _node_usage_top
|
||||||
if top:
|
chosen = pick(usage, allowed_nodes=allowed_nodes)
|
||||||
node, val = top
|
if chosen:
|
||||||
|
node, val = chosen
|
||||||
percent = metric in {"cpu", "ram"}
|
percent = metric in {"cpu", "ram"}
|
||||||
value = _format_metric_value(str(val), percent=percent, rate=metric in {"net", "io"})
|
value = _format_metric_value(str(val), percent=percent, rate=metric in {"net", "io"})
|
||||||
scope = ""
|
scope = ""
|
||||||
if include_hw:
|
if include_hw:
|
||||||
scope = f" among {' and '.join(sorted(include_hw))}"
|
scope = f" among {' and '.join(sorted(include_hw))}"
|
||||||
answer = f"Hottest node{scope}: {node} ({value})."
|
label = "Lowest" if op == "bottom" else "Hottest"
|
||||||
if allowed_nodes and len(allowed_nodes) != len(inventory):
|
answer = f"{label} node{scope}: {node} ({value})."
|
||||||
|
if allowed_nodes and len(allowed_nodes) != len(inventory) and op != "bottom":
|
||||||
overall = _node_usage_top(usage, allowed_nodes=None)
|
overall = _node_usage_top(usage, allowed_nodes=None)
|
||||||
if overall and overall[0] != node:
|
if overall and overall[0] != node:
|
||||||
overall_val = _format_metric_value(
|
overall_val = _format_metric_value(
|
||||||
@ -1314,6 +1353,10 @@ def snapshot_metric_answer(
|
|||||||
failed = metrics.get("pods_failed")
|
failed = metrics.get("pods_failed")
|
||||||
succeeded = metrics.get("pods_succeeded")
|
succeeded = metrics.get("pods_succeeded")
|
||||||
status_terms = ("running", "pending", "failed", "succeeded", "completed")
|
status_terms = ("running", "pending", "failed", "succeeded", "completed")
|
||||||
|
if "total" in q or "sum" in q:
|
||||||
|
values = [v for v in (running, pending, failed, succeeded) if isinstance(v, (int, float))]
|
||||||
|
if values:
|
||||||
|
return _format_confidence(f"Total pods: {sum(values):.0f}.", "high")
|
||||||
if "not running" in q or "not in running" in q or "non running" in q:
|
if "not running" in q or "not in running" in q or "non running" in q:
|
||||||
parts = [v for v in (pending, failed, succeeded) if isinstance(v, (int, float))]
|
parts = [v for v in (pending, failed, succeeded) if isinstance(v, (int, float))]
|
||||||
if parts:
|
if parts:
|
||||||
@ -1468,7 +1511,8 @@ def structured_answer(
|
|||||||
node, val = _primary_series_metric(res)
|
node, val = _primary_series_metric(res)
|
||||||
if node and val is not None:
|
if node and val is not None:
|
||||||
percent = _metric_expr_uses_percent(entry)
|
percent = _metric_expr_uses_percent(entry)
|
||||||
value_fmt = _format_metric_value(val or "", percent=percent)
|
rate = metric in {"net", "io"}
|
||||||
|
value_fmt = _format_metric_value(val or "", percent=percent, rate=rate)
|
||||||
metric_label = (metric or "").upper()
|
metric_label = (metric or "").upper()
|
||||||
label = f"{metric_label} node" if metric_label else "node"
|
label = f"{metric_label} node" if metric_label else "node"
|
||||||
answer = f"Hottest {label}: {node} ({value_fmt})."
|
answer = f"Hottest {label}: {node} ({value_fmt})."
|
||||||
@ -1495,7 +1539,8 @@ def structured_answer(
|
|||||||
scoped_node, scoped_val = _primary_series_metric(res)
|
scoped_node, scoped_val = _primary_series_metric(res)
|
||||||
if base_node and scoped_node and base_node != scoped_node:
|
if base_node and scoped_node and base_node != scoped_node:
|
||||||
percent = _metric_expr_uses_percent(entry)
|
percent = _metric_expr_uses_percent(entry)
|
||||||
base_val_fmt = _format_metric_value(base_val or "", percent=percent)
|
rate = metric in {"net", "io"}
|
||||||
|
base_val_fmt = _format_metric_value(base_val or "", percent=percent, rate=rate)
|
||||||
overall_note = f" Overall hottest node: {base_node} ({base_val_fmt})."
|
overall_note = f" Overall hottest node: {base_node} ({base_val_fmt})."
|
||||||
return _format_confidence(f"Among {scope} nodes, {answer}{overall_note}", "high")
|
return _format_confidence(f"Among {scope} nodes, {answer}{overall_note}", "high")
|
||||||
return _format_confidence(answer, "high")
|
return _format_confidence(answer, "high")
|
||||||
@ -1525,9 +1570,14 @@ def structured_answer(
|
|||||||
names = [node["name"] for node in filtered]
|
names = [node["name"] for node in filtered]
|
||||||
|
|
||||||
if op == "status":
|
if op == "status":
|
||||||
|
scope_label = "nodes"
|
||||||
|
if include_hw:
|
||||||
|
scope_label = f"{' and '.join(sorted(include_hw))} nodes"
|
||||||
|
elif only_workers:
|
||||||
|
scope_label = "worker nodes"
|
||||||
if "missing" in q and ("ready" in q or "readiness" in q):
|
if "missing" in q and ("ready" in q or "readiness" in q):
|
||||||
return _format_confidence(
|
return _format_confidence(
|
||||||
"Not ready nodes: " + (", ".join(names) if names else "none") + ".",
|
f"Not ready {scope_label}: " + (", ".join(names) if names else "none") + ".",
|
||||||
"high",
|
"high",
|
||||||
)
|
)
|
||||||
if "missing" in q and expected_workers:
|
if "missing" in q and expected_workers:
|
||||||
@ -1538,16 +1588,21 @@ def structured_answer(
|
|||||||
)
|
)
|
||||||
if only_ready is False:
|
if only_ready is False:
|
||||||
return _format_confidence(
|
return _format_confidence(
|
||||||
"Not ready nodes: " + (", ".join(names) if names else "none") + ".",
|
f"Not ready {scope_label}: " + (", ".join(names) if names else "none") + ".",
|
||||||
"high",
|
"high",
|
||||||
)
|
)
|
||||||
if only_ready is True:
|
if only_ready is True:
|
||||||
return _format_confidence(
|
return _format_confidence(
|
||||||
f"Ready nodes ({len(names)}): " + (", ".join(names) if names else "none") + ".",
|
f"Ready {scope_label} ({len(names)}): " + (", ".join(names) if names else "none") + ".",
|
||||||
"high",
|
"high",
|
||||||
)
|
)
|
||||||
|
|
||||||
if op == "count":
|
if op == "count":
|
||||||
|
scope_label = "nodes"
|
||||||
|
if include_hw:
|
||||||
|
scope_label = f"{' and '.join(sorted(include_hw))} nodes"
|
||||||
|
elif only_workers:
|
||||||
|
scope_label = "worker nodes"
|
||||||
if only_workers and "ready" in q and ("total" in q or "vs" in q or "versus" in q):
|
if only_workers and "ready" in q and ("total" in q or "vs" in q or "versus" in q):
|
||||||
total_workers = _inventory_filter(
|
total_workers = _inventory_filter(
|
||||||
inventory,
|
inventory,
|
||||||
@ -1576,9 +1631,9 @@ def structured_answer(
|
|||||||
msg += f" Missing: {', '.join(missing)}."
|
msg += f" Missing: {', '.join(missing)}."
|
||||||
return _format_confidence(msg, "high")
|
return _format_confidence(msg, "high")
|
||||||
if only_ready is True:
|
if only_ready is True:
|
||||||
return _format_confidence(f"Ready nodes: {len(names)}.", "high")
|
return _format_confidence(f"Ready {scope_label}: {len(names)}.", "high")
|
||||||
if only_ready is False:
|
if only_ready is False:
|
||||||
return _format_confidence(f"Not ready nodes: {len(names)}.", "high")
|
return _format_confidence(f"Not ready {scope_label}: {len(names)}.", "high")
|
||||||
if not (include_hw or exclude_hw or nodes_in_query or only_workers or role_filters):
|
if not (include_hw or exclude_hw or nodes_in_query or only_workers or role_filters):
|
||||||
return _format_confidence(f"Atlas has {len(names)} nodes.", "high")
|
return _format_confidence(f"Atlas has {len(names)} nodes.", "high")
|
||||||
return _format_confidence(f"Matching nodes: {len(names)}.", "high")
|
return _format_confidence(f"Matching nodes: {len(names)}.", "high")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user