Compare commits
8 Commits
aed2606601
...
eaba83a033
| Author | SHA1 | Date | |
|---|---|---|---|
| eaba83a033 | |||
| c0fd606f43 | |||
| 4d15bdd466 | |||
| 9d495b4f1e | |||
| d4262d66ad | |||
| f4b4d6afe8 | |||
| 6d6039c8d6 | |||
| cc3739efe4 |
@ -201,6 +201,8 @@ class AnswerEngine:
|
|||||||
classify.setdefault("needs_snapshot", True)
|
classify.setdefault("needs_snapshot", True)
|
||||||
classify.setdefault("answer_style", "direct")
|
classify.setdefault("answer_style", "direct")
|
||||||
classify.setdefault("follow_up", False)
|
classify.setdefault("follow_up", False)
|
||||||
|
classify.setdefault("focus_entity", "unknown")
|
||||||
|
classify.setdefault("focus_metric", "unknown")
|
||||||
_debug_log("route_parsed", {"classify": classify, "normalized": normalized})
|
_debug_log("route_parsed", {"classify": classify, "normalized": normalized})
|
||||||
cluster_terms = (
|
cluster_terms = (
|
||||||
"atlas",
|
"atlas",
|
||||||
@ -227,14 +229,27 @@ class AnswerEngine:
|
|||||||
)
|
)
|
||||||
if any(term in normalized.lower() for term in cluster_terms):
|
if any(term in normalized.lower() for term in cluster_terms):
|
||||||
classify["needs_snapshot"] = True
|
classify["needs_snapshot"] = True
|
||||||
|
lowered_norm = normalized.lower()
|
||||||
|
if (
|
||||||
|
("namespace" in lowered_norm and ("pod" in lowered_norm or "pods" in lowered_norm))
|
||||||
|
or re.search(r"\bmost\s+pods\b", lowered_norm)
|
||||||
|
or re.search(r"\bpods\s+running\b", lowered_norm)
|
||||||
|
):
|
||||||
|
classify["question_type"] = "metric"
|
||||||
|
classify["needs_snapshot"] = True
|
||||||
if re.search(r"\b(how many|count|number of|list)\b", normalized.lower()):
|
if re.search(r"\b(how many|count|number of|list)\b", normalized.lower()):
|
||||||
classify["question_type"] = "metric"
|
classify["question_type"] = "metric"
|
||||||
hottest_terms = ("hottest", "highest", "lowest", "most")
|
hottest_terms = ("hottest", "highest", "lowest", "most")
|
||||||
metric_terms = ("cpu", "ram", "memory", "net", "network", "io", "disk", "load", "usage")
|
metric_terms = ("cpu", "ram", "memory", "net", "network", "io", "disk", "load", "usage", "pod", "pods", "namespace")
|
||||||
lowered_question = normalized.lower()
|
lowered_question = f"{question} {normalized}".lower()
|
||||||
if any(term in lowered_question for term in hottest_terms) and any(term in lowered_question for term in metric_terms):
|
if any(term in lowered_question for term in hottest_terms) and any(term in lowered_question for term in metric_terms):
|
||||||
classify["question_type"] = "metric"
|
classify["question_type"] = "metric"
|
||||||
|
|
||||||
|
if not classify.get("follow_up") and state and state.claims:
|
||||||
|
follow_terms = ("there", "that", "those", "these", "it", "them", "that one", "this", "former", "latter")
|
||||||
|
if any(term in lowered_question for term in follow_terms) or len(normalized.split()) <= 6:
|
||||||
|
classify["follow_up"] = True
|
||||||
|
|
||||||
if classify.get("follow_up") and state and state.claims:
|
if classify.get("follow_up") and state and state.claims:
|
||||||
if observer:
|
if observer:
|
||||||
observer("followup", "answering follow-up")
|
observer("followup", "answering follow-up")
|
||||||
@ -257,6 +272,8 @@ class AnswerEngine:
|
|||||||
sub_questions = _select_subquestions(parts, normalized, plan.max_subquestions)
|
sub_questions = _select_subquestions(parts, normalized, plan.max_subquestions)
|
||||||
_debug_log("decompose_parsed", {"sub_questions": sub_questions})
|
_debug_log("decompose_parsed", {"sub_questions": sub_questions})
|
||||||
keyword_tokens = _extract_keywords(question, normalized, sub_questions=sub_questions, keywords=keywords)
|
keyword_tokens = _extract_keywords(question, normalized, sub_questions=sub_questions, keywords=keywords)
|
||||||
|
focus_entity = str(classify.get("focus_entity") or "unknown").lower()
|
||||||
|
focus_metric = str(classify.get("focus_metric") or "unknown").lower()
|
||||||
|
|
||||||
snapshot_context = ""
|
snapshot_context = ""
|
||||||
if classify.get("needs_snapshot"):
|
if classify.get("needs_snapshot"):
|
||||||
@ -282,12 +299,15 @@ class AnswerEngine:
|
|||||||
hardware_facts = [seg.strip() for seg in line.split(" | ") if seg.strip().startswith("hardware_usage_avg:")]
|
hardware_facts = [seg.strip() for seg in line.split(" | ") if seg.strip().startswith("hardware_usage_avg:")]
|
||||||
break
|
break
|
||||||
metric_facts = [line for line in key_facts if re.search(r"\d", line)]
|
metric_facts = [line for line in key_facts if re.search(r"\d", line)]
|
||||||
if hardware_facts:
|
if focus_entity == "node" and hottest_facts:
|
||||||
|
metric_facts = hottest_facts
|
||||||
|
key_facts = _merge_fact_lines(metric_facts, key_facts)
|
||||||
|
elif hardware_facts:
|
||||||
metric_facts = hardware_facts
|
metric_facts = hardware_facts
|
||||||
key_facts = _merge_fact_lines(metric_facts, key_facts)
|
key_facts = _merge_fact_lines(metric_facts, key_facts)
|
||||||
if classify.get("question_type") in {"metric", "diagnostic"}:
|
if classify.get("question_type") in {"metric", "diagnostic"}:
|
||||||
lowered_q = f"{question} {normalized}".lower()
|
lowered_q = f"{question} {normalized}".lower()
|
||||||
if any(tok in lowered_q for tok in ("hardware", "class", "type", "rpi", "jetson", "amd64", "arm64")) and any(
|
if focus_entity != "node" and any(tok in lowered_q for tok in ("hardware", "class", "type", "rpi", "jetson", "amd64", "arm64")) and any(
|
||||||
tok in lowered_q for tok in ("average", "avg", "mean", "ram", "memory", "cpu", "load")
|
tok in lowered_q for tok in ("average", "avg", "mean", "ram", "memory", "cpu", "load")
|
||||||
):
|
):
|
||||||
hw_top = None
|
hw_top = None
|
||||||
@ -300,7 +320,7 @@ class AnswerEngine:
|
|||||||
if hw_top:
|
if hw_top:
|
||||||
metric_facts = [hw_top]
|
metric_facts = [hw_top]
|
||||||
key_facts = _merge_fact_lines(metric_facts, key_facts)
|
key_facts = _merge_fact_lines(metric_facts, key_facts)
|
||||||
if hottest_facts and not hardware_facts:
|
if hottest_facts and not hardware_facts and focus_entity != "class":
|
||||||
metric_facts = hottest_facts
|
metric_facts = hottest_facts
|
||||||
key_facts = _merge_fact_lines(metric_facts, key_facts)
|
key_facts = _merge_fact_lines(metric_facts, key_facts)
|
||||||
if classify.get("question_type") in {"metric", "diagnostic"} and not hottest_facts and not hardware_facts:
|
if classify.get("question_type") in {"metric", "diagnostic"} and not hottest_facts and not hardware_facts:
|
||||||
@ -312,15 +332,24 @@ class AnswerEngine:
|
|||||||
if self._settings.debug_pipeline:
|
if self._settings.debug_pipeline:
|
||||||
_debug_log("metric_facts_selected", {"facts": metric_facts})
|
_debug_log("metric_facts_selected", {"facts": metric_facts})
|
||||||
if classify.get("question_type") in {"metric", "diagnostic"} and not metric_facts:
|
if classify.get("question_type") in {"metric", "diagnostic"} and not metric_facts:
|
||||||
for line in summary_lines:
|
lowered_q = f"{question} {normalized}".lower()
|
||||||
if "hardware_usage_top:" in line:
|
if "namespace" in lowered_q and "pod" in lowered_q:
|
||||||
metric_facts = [seg.strip() for seg in line.split(" | ") if seg.strip().startswith("hardware_usage_top:")]
|
|
||||||
break
|
|
||||||
if not metric_facts:
|
|
||||||
for line in summary_lines:
|
for line in summary_lines:
|
||||||
if "hardware_usage_avg:" in line:
|
if line.startswith("namespaces_top:"):
|
||||||
metric_facts = [seg.strip() for seg in line.split(" | ") if seg.strip().startswith("hardware_usage_avg:")]
|
metric_facts = [line]
|
||||||
break
|
break
|
||||||
|
if not metric_facts:
|
||||||
|
hardware_tokens = ("hardware", "class", "type", "rpi", "jetson", "amd64", "arm64")
|
||||||
|
if any(tok in lowered_q for tok in hardware_tokens):
|
||||||
|
for line in summary_lines:
|
||||||
|
if "hardware_usage_top:" in line:
|
||||||
|
metric_facts = [seg.strip() for seg in line.split(" | ") if seg.strip().startswith("hardware_usage_top:")]
|
||||||
|
break
|
||||||
|
if not metric_facts:
|
||||||
|
for line in summary_lines:
|
||||||
|
if "hardware_usage_avg:" in line:
|
||||||
|
metric_facts = [seg.strip() for seg in line.split(" | ") if seg.strip().startswith("hardware_usage_avg:")]
|
||||||
|
break
|
||||||
if metric_facts:
|
if metric_facts:
|
||||||
key_facts = _merge_fact_lines(metric_facts, key_facts)
|
key_facts = _merge_fact_lines(metric_facts, key_facts)
|
||||||
if self._settings.debug_pipeline:
|
if self._settings.debug_pipeline:
|
||||||
@ -513,6 +542,61 @@ class AnswerEngine:
|
|||||||
if classify.get("question_type") in {"metric", "diagnostic"} and metric_facts:
|
if classify.get("question_type") in {"metric", "diagnostic"} and metric_facts:
|
||||||
reply = _metric_fact_guard(reply, metric_facts, keyword_tokens)
|
reply = _metric_fact_guard(reply, metric_facts, keyword_tokens)
|
||||||
|
|
||||||
|
if classify.get("question_type") in {"metric", "diagnostic"}:
|
||||||
|
lowered_q = f"{question} {normalized}".lower()
|
||||||
|
if any(tok in lowered_q for tok in ("how many", "count", "number of")) and any(
|
||||||
|
tok in lowered_q for tok in ("jetson", "rpi4", "rpi5", "amd64", "arm64", "rpi")
|
||||||
|
):
|
||||||
|
hw_line = next((line for line in summary_lines if line.startswith("hardware:")), None)
|
||||||
|
hw_nodes_line = next((line for line in summary_lines if line.startswith("hardware_nodes:")), None)
|
||||||
|
if hw_line:
|
||||||
|
def _find_value(key: str, line: str) -> str | None:
|
||||||
|
match = re.search(rf"{re.escape(key)}=([^;|]+)", line)
|
||||||
|
return match.group(1).strip() if match else None
|
||||||
|
|
||||||
|
target = None
|
||||||
|
if "jetson" in lowered_q:
|
||||||
|
target = "jetson"
|
||||||
|
elif "rpi5" in lowered_q:
|
||||||
|
target = "rpi5"
|
||||||
|
elif "rpi4" in lowered_q:
|
||||||
|
target = "rpi4"
|
||||||
|
elif "amd64" in lowered_q:
|
||||||
|
target = "amd64"
|
||||||
|
elif "arm64" in lowered_q:
|
||||||
|
target = "arm64"
|
||||||
|
elif "rpi" in lowered_q:
|
||||||
|
target = "rpi"
|
||||||
|
if target:
|
||||||
|
count = _find_value(target, hw_line)
|
||||||
|
nodes = _find_value(target, hw_nodes_line or "")
|
||||||
|
if count:
|
||||||
|
if nodes and "(" not in count:
|
||||||
|
reply = f"From the latest snapshot: {target}={count} ({nodes})."
|
||||||
|
else:
|
||||||
|
reply = f"From the latest snapshot: {target}={count}."
|
||||||
|
if ("io" in lowered_q or "i/o" in lowered_q) and ("node" in lowered_q or "nodes" in lowered_q):
|
||||||
|
io_facts = _extract_hottest_facts(summary_lines, lowered_q)
|
||||||
|
io_line = next((fact for fact in io_facts if fact.startswith("hottest_io_node")), None)
|
||||||
|
if io_line:
|
||||||
|
reply = f"From the latest snapshot: {io_line}."
|
||||||
|
if "namespace" in lowered_q and "pod" in lowered_q:
|
||||||
|
ns_line = None
|
||||||
|
for line in summary_lines:
|
||||||
|
if line.startswith("namespaces_top:"):
|
||||||
|
ns_line = line
|
||||||
|
break
|
||||||
|
cpu_line = None
|
||||||
|
if any(tok in lowered_q for tok in ("cpu", "hottest", "highest cpu", "highest")):
|
||||||
|
cpu_facts = _extract_hottest_facts(summary_lines, lowered_q)
|
||||||
|
cpu_line = next((fact for fact in cpu_facts if fact.startswith("hottest_cpu_node")), None)
|
||||||
|
if ns_line:
|
||||||
|
if cpu_line:
|
||||||
|
reply = f"From the latest snapshot: {cpu_line}; {ns_line}."
|
||||||
|
else:
|
||||||
|
reply = f"From the latest snapshot: {ns_line}."
|
||||||
|
# do not fall through to other overrides
|
||||||
|
|
||||||
if classify.get("question_type") in {"metric", "diagnostic"}:
|
if classify.get("question_type") in {"metric", "diagnostic"}:
|
||||||
lowered_q = f"{question} {normalized}".lower()
|
lowered_q = f"{question} {normalized}".lower()
|
||||||
if any(tok in lowered_q for tok in ("hardware", "class", "type", "rpi", "jetson", "amd64", "arm64")) and any(
|
if any(tok in lowered_q for tok in ("hardware", "class", "type", "rpi", "jetson", "amd64", "arm64")) and any(
|
||||||
@ -724,6 +808,7 @@ class AnswerEngine:
|
|||||||
tag="followup_fix",
|
tag="followup_fix",
|
||||||
)
|
)
|
||||||
reply = await self._dedup_reply(reply, plan, call_llm, tag="dedup_followup")
|
reply = await self._dedup_reply(reply, plan, call_llm, tag="dedup_followup")
|
||||||
|
reply = _strip_followup_meta(reply)
|
||||||
return reply
|
return reply
|
||||||
|
|
||||||
async def _select_claims(
|
async def _select_claims(
|
||||||
@ -771,6 +856,26 @@ class AnswerEngine:
|
|||||||
self._store.cleanup()
|
self._store.cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_followup_meta(reply: str) -> str:
|
||||||
|
cleaned = reply.strip()
|
||||||
|
if not cleaned:
|
||||||
|
return cleaned
|
||||||
|
prefixes = [
|
||||||
|
"The draft is correct based on the provided context.",
|
||||||
|
"The draft is correct based on the context.",
|
||||||
|
"The draft is correct based on the provided evidence.",
|
||||||
|
"The draft is correct.",
|
||||||
|
"Based on the provided context,",
|
||||||
|
"Based on the context,",
|
||||||
|
"Based on the provided evidence,",
|
||||||
|
]
|
||||||
|
for prefix in prefixes:
|
||||||
|
if cleaned.lower().startswith(prefix.lower()):
|
||||||
|
cleaned = cleaned[len(prefix) :].lstrip(" .")
|
||||||
|
break
|
||||||
|
return cleaned
|
||||||
|
|
||||||
|
|
||||||
def _build_meta(
|
def _build_meta(
|
||||||
mode: str,
|
mode: str,
|
||||||
call_count: int,
|
call_count: int,
|
||||||
@ -1091,6 +1196,8 @@ def _extract_hottest_facts(lines: list[str], question: str) -> list[str]:
|
|||||||
if not facts:
|
if not facts:
|
||||||
return []
|
return []
|
||||||
wanted = []
|
wanted = []
|
||||||
|
if ("io" in lowered or "i/o" in lowered) and ("disk" in lowered or "storage" in lowered):
|
||||||
|
return [fact for fact in facts if fact.startswith("hottest_io_node")]
|
||||||
if any(term in lowered for term in ("cpu", "processor")):
|
if any(term in lowered for term in ("cpu", "processor")):
|
||||||
wanted.append("hottest_cpu_node")
|
wanted.append("hottest_cpu_node")
|
||||||
if any(term in lowered for term in ("ram", "memory")):
|
if any(term in lowered for term in ("ram", "memory")):
|
||||||
@ -1156,6 +1263,13 @@ def _metric_candidate_lines(lines: list[str], keywords: list[str] | None, limit:
|
|||||||
"p95",
|
"p95",
|
||||||
"percent",
|
"percent",
|
||||||
"pressure",
|
"pressure",
|
||||||
|
"pod",
|
||||||
|
"pods",
|
||||||
|
"namespace",
|
||||||
|
"anomaly",
|
||||||
|
"anomalies",
|
||||||
|
"pvc",
|
||||||
|
"storage",
|
||||||
}
|
}
|
||||||
candidates: list[str] = []
|
candidates: list[str] = []
|
||||||
for line in lines:
|
for line in lines:
|
||||||
|
|||||||
@ -30,7 +30,8 @@ ROUTE_SYSTEM = (
|
|||||||
|
|
||||||
ROUTE_PROMPT = (
|
ROUTE_PROMPT = (
|
||||||
"Return JSON with fields: needs_snapshot (bool), needs_kb (bool), needs_tool (bool), "
|
"Return JSON with fields: needs_snapshot (bool), needs_kb (bool), needs_tool (bool), "
|
||||||
"answer_style (direct|insightful), follow_up (bool), question_type (metric|diagnostic|planning|open_ended)."
|
"answer_style (direct|insightful), follow_up (bool), question_type (metric|diagnostic|planning|open_ended), "
|
||||||
|
"focus_entity (node|class|namespace|service|cluster|unknown), focus_metric (cpu|ram|net|io|disk|load|pods|storage|unknown)."
|
||||||
)
|
)
|
||||||
|
|
||||||
DECOMPOSE_SYSTEM = (
|
DECOMPOSE_SYSTEM = (
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user