From dff8901ae31126ea58abf05cbd4d00659ee9d1ae Mon Sep 17 00:00:00 2001 From: oliver Date: Mon, 10 Aug 2026 23:45:30 +0800 Subject: [PATCH] Hide inventory/CLI tools on WhatsApp report short-intents. Fiber/offline/alarm/excel/license/congestion turns drop list/exec/sql/run_command so the model must use ume_alarm_xlsx_report instead of inventory self-loops. Co-authored-by: Cursor --- .../application/gateway/ops_short_intent.py | 105 ++++++++++++++---- runtime/gateway.py | 36 +++++- tests/test_ops_short_intent_and_exec_hints.py | 40 +++++++ 3 files changed, 157 insertions(+), 24 deletions(-) diff --git a/runtime/application/gateway/ops_short_intent.py b/runtime/application/gateway/ops_short_intent.py index 6936b501..26ef6fab 100644 --- a/runtime/application/gateway/ops_short_intent.py +++ b/runtime/application/gateway/ops_short_intent.py @@ -12,36 +12,39 @@ _BOT_MENTION_RE = re.compile(r"@\S+") # intent -> (en hint, zh hint) _HINTS: dict[str, tuple[str, str]] = { "fiber_cut": ( - "[Ops short-intent: fiber/LOS. Prefer ume_alarm_xlsx_report(mode=fiber_cut) in ≤3 tool calls; " - "do not paginate or re-list CLI targets first.]", - "[短指令:断纤/LOS。优先 ume_alarm_xlsx_report(mode=fiber_cut),≤3 次工具;勿先翻页或反复 listCliTargets。]", + "[Ops short-intent: fiber/LOS. Call ume_alarm_xlsx_report(mode=fiber_cut, deliverable=true) now. " + "Inventory/CLI tools are hidden this turn — do not try listCliTargets/execManagedNe.]", + "[短指令:断纤/LOS。立即 ume_alarm_xlsx_report(mode=fiber_cut, deliverable=true)。" + "本轮已隐藏清单/CLI 工具,勿调用 listCliTargets/execManagedNe。]", ), "offline": ( - "[Ops short-intent: offline NE. Prefer ume_alarm_xlsx_report(mode=offline) in ≤3 tool calls.]", - "[短指令:离线网元。优先 ume_alarm_xlsx_report(mode=offline),≤3 次工具。]", + "[Ops short-intent: offline NE. Call ume_alarm_xlsx_report(mode=offline, deliverable=true) now. " + "Inventory/CLI tools are hidden this turn.]", + "[短指令:离线网元。立即 ume_alarm_xlsx_report(mode=offline, deliverable=true)。本轮已隐藏清单/CLI。]", ), "alarm_tally": ( - "[Ops short-intent: alarm tally/top. Prefer aggregateUmeAlarms or " - "ume_alarm_xlsx_report(mode=aggregate_by_host); ≤3 tool calls.]", - "[短指令:告警统计/Top。优先 aggregateUmeAlarms 或 ume_alarm_xlsx_report(mode=aggregate_by_host);≤3 次工具。]", + "[Ops short-intent: alarm tally/top. Prefer ume_alarm_xlsx_report(mode=aggregate_by_host) or " + "aggregateUmeAlarms; inventory/CLI tools are hidden this turn.]", + "[短指令:告警统计/Top。优先 ume_alarm_xlsx_report(mode=aggregate_by_host) 或 aggregateUmeAlarms;" + "本轮已隐藏清单/CLI。]", ), "excel_export": ( - "[Ops short-intent: Excel export. Prefer ume_alarm_xlsx_report or write_xlsx(deliverable=true); " - "do not build xlsx via run_command.]", - "[短指令:导出 Excel。优先 ume_alarm_xlsx_report 或 write_xlsx(deliverable=true);禁止 run_command 造表。]", + "[Ops short-intent: Excel export. Prefer ume_alarm_xlsx_report or write_xlsx(deliverable=true). " + "Inventory/CLI/run_command are hidden this turn — do not build xlsx via shell.]", + "[短指令:导出 Excel。优先 ume_alarm_xlsx_report 或 write_xlsx(deliverable=true);" + "本轮已隐藏清单/CLI/run_command。]", ), "license": ( - "[Ops short-intent: license/capacity. Prefer aggregateUmeAlarms / queryUmeAlarmsRaw with license keywords, " - "or ume_alarm_xlsx_report(mode=list, keyword=license); ≤3 tool calls — no CLI spam.]", - "[短指令:License/容量。优先 aggregateUmeAlarms / queryUmeAlarmsRaw(license 关键字)" - "或 ume_alarm_xlsx_report(mode=list, keyword=license);≤3 次工具,勿刷 CLI。]", + "[Ops short-intent: license/capacity. Prefer ume_alarm_xlsx_report(mode=list, keyword=license) or " + "aggregateUmeAlarms/queryUmeAlarmsRaw; CLI/inventory tools are hidden this turn.]", + "[短指令:License/容量。优先 ume_alarm_xlsx_report(mode=list, keyword=license) 或 " + "aggregate/queryUmeAlarmsRaw;本轮已隐藏 CLI/清单。]", ), "congestion": ( - "[Ops short-intent: bandwidth congestion. Prefer aggregateUmeAlarms / queryUmeAlarmsRaw " - "(bandwidth/utilization/congestion keywords) or ume_alarm_xlsx_report(mode=list); " - "≤3 tool calls — no CLI spam / no sqlQueryUme unless scoped.]", - "[短指令:带宽拥塞。优先 aggregateUmeAlarms / queryUmeAlarmsRaw(带宽/利用率/拥塞)" - "或 ume_alarm_xlsx_report(mode=list);≤3 次工具,勿刷 CLI / 勿先 sqlQueryUme。]", + "[Ops short-intent: bandwidth congestion. Prefer ume_alarm_xlsx_report(mode=list) or " + "aggregateUmeAlarms/queryUmeAlarmsRaw; CLI/inventory/sql are hidden this turn.]", + "[短指令:带宽拥塞。优先 ume_alarm_xlsx_report(mode=list) 或 aggregate/query;" + "本轮已隐藏 CLI/清单/sql。]", ), "continue": ( "[Ops short-intent: continue/confirm. Resume the unfinished prior task immediately; " @@ -50,6 +53,65 @@ _HINTS: dict[str, tuple[str, str]] = { ), } +# Report-style short intents: hide inventory/CLI loops that dominate WA tool spam. +_REPORT_TOOL_FILTER_INTENTS = frozenset( + {"fiber_cut", "offline", "alarm_tally", "excel_export", "license", "congestion"} +) + +# Match bare tool names and mcp__netx__* / legacy netx_* aliases. +_SUPPRESSED_TOOL_NAMES = frozenset( + { + "listclitargets", + "listmanagedne", + "getmanagedne", + "execmanagedne", + "queryumeneinventory", + "getumene", + "findtopologypaths", + "sqlqueryume", + "run_command", + "netx_list_managed_ne", + "netx_get_managed_ne", + "netx_exec_managed_ne", + "netx_sql_query_ume", + "netx_list_cli_targets", + } +) + + +def _tool_name_key(name: str) -> str: + raw = str(name or "").strip() + if not raw: + return "" + # mcp__netx__listManagedNe -> listManagedNe + if "__" in raw: + raw = raw.rsplit("__", 1)[-1] + return raw.strip().lower() + + +def ops_short_intent_should_filter_tools(intent: str | None) -> bool: + return str(intent or "").strip() in _REPORT_TOOL_FILTER_INTENTS + + +def is_ops_short_intent_suppressed_tool(tool_name: str, *, intent: str | None) -> bool: + if not ops_short_intent_should_filter_tools(intent): + return False + key = _tool_name_key(tool_name) + return bool(key) and key in _SUPPRESSED_TOOL_NAMES + + +def filter_tool_specs_for_ops_short_intent(tools: list[Any], *, intent: str | None) -> list[Any]: + """Drop inventory/CLI tools for report-style short intents (keep alarm/xlsx path).""" + if not ops_short_intent_should_filter_tools(intent): + return list(tools or []) + out: list[Any] = [] + for spec in tools or []: + name = str(getattr(spec, "name", "") or "") + if is_ops_short_intent_suppressed_tool(name, intent=intent): + continue + out.append(spec) + return out + def normalize_ops_user_text(text: str) -> str: s = str(text or "").strip() @@ -180,8 +242,11 @@ __all__ = [ "build_group_mention_nudge_text", "build_ops_short_intent_hint", "detect_ops_short_intent", + "filter_tool_specs_for_ops_short_intent", + "is_ops_short_intent_suppressed_tool", "maybe_ops_short_intent_system_hint", "normalize_ops_user_text", + "ops_short_intent_should_filter_tools", "reset_group_mention_nudge_throttle_for_tests", "should_send_group_mention_nudge", ] diff --git a/runtime/gateway.py b/runtime/gateway.py index 88e15c23..2334c30b 100644 --- a/runtime/gateway.py +++ b/runtime/gateway.py @@ -1216,6 +1216,34 @@ class OclawGateway: tools = tools_override if tools_override is not None else getattr(selected_executor, "tools", None) if model is None or tools is None: raise RuntimeError("executor missing model/tools") + if self._is_channel_delivery_channel(msg) and isinstance(tools, ToolRegistry): + from runtime.application.gateway.ops_short_intent import ( + detect_ops_short_intent, + filter_tool_specs_for_ops_short_intent, + ops_short_intent_should_filter_tools, + ) + + md_intent = msg.metadata if isinstance(msg.metadata, dict) else {} + short_intent = detect_ops_short_intent( + str(msg.text or md_intent.get("raw_inbound_text") or "") + ) + if ops_short_intent_should_filter_tools(short_intent): + before_n = len(tools.list()) + filtered_specs = filter_tool_specs_for_ops_short_intent( + tools.list(), intent=short_intent + ) + if len(filtered_specs) < before_n: + tools_override = ToolRegistry(filtered_specs) + tools = tools_override + _trace_local( + event_type="ops_short_intent_tools_filtered", + payload={ + "intent": short_intent, + "before_count": before_n, + "after_count": len(filtered_specs), + }, + started_at=t0, + ) sys_prompt = system_prompt_override or str(getattr(selected_executor, "system_prompt", "") or "") if self._is_channel_delivery_channel(msg): ch_hint = self._channel_file_delivery_system_hint(lang) @@ -1224,10 +1252,10 @@ class OclawGateway: focus_hint = self._group_focus_system_hint(msg, lang) if focus_hint: sys_prompt = f"{sys_prompt}\n\n{focus_hint}".strip() - if str(manager_specialist or requested_specialist or "").strip().lower() == "ops": - intent_hint = self._ops_short_intent_system_hint(msg, lang) - if intent_hint: - sys_prompt = f"{sys_prompt}\n\n{intent_hint}".strip() + # Always inject short-intent recipe on WA/weixin (not only when specialist==ops). + intent_hint = self._ops_short_intent_system_hint(msg, lang) + if intent_hint: + sys_prompt = f"{sys_prompt}\n\n{intent_hint}".strip() if self._has_tabular_ref_attachments(msg): sys_prompt = f"{sys_prompt}\n\n{self._tabular_query_system_hint(lang)}".strip() if self._has_text_ref_attachments(msg): diff --git a/tests/test_ops_short_intent_and_exec_hints.py b/tests/test_ops_short_intent_and_exec_hints.py index 4518b5ea..856dd48c 100644 --- a/tests/test_ops_short_intent_and_exec_hints.py +++ b/tests/test_ops_short_intent_and_exec_hints.py @@ -3,10 +3,13 @@ from __future__ import annotations from runtime.application.gateway.ops_short_intent import ( build_group_mention_nudge_text, detect_ops_short_intent, + filter_tool_specs_for_ops_short_intent, + is_ops_short_intent_suppressed_tool, maybe_ops_short_intent_system_hint, reset_group_mention_nudge_throttle_for_tests, should_send_group_mention_nudge, ) +from runtime.tools.base import ToolSpec from runtime.tools.tool_error_hints import enrich_exec_managed_ne_error @@ -43,6 +46,43 @@ def test_group_mention_nudge_throttle() -> None: ) +def test_ops_short_intent_suppresses_inventory_cli_tools() -> None: + assert is_ops_short_intent_suppressed_tool("mcp__netx__listCliTargets", intent="fiber_cut") + assert is_ops_short_intent_suppressed_tool("mcp__netx__execManagedNe", intent="offline") + assert is_ops_short_intent_suppressed_tool("run_command", intent="excel_export") + assert not is_ops_short_intent_suppressed_tool("ume_alarm_xlsx_report", intent="fiber_cut") + assert not is_ops_short_intent_suppressed_tool("mcp__netx__queryUmeAlarmsRaw", intent="alarm_tally") + assert not is_ops_short_intent_suppressed_tool("mcp__netx__execManagedNe", intent="continue") + + +def test_filter_tool_specs_for_ops_short_intent_keeps_report_path() -> None: + def _spec(name: str) -> ToolSpec: + return ToolSpec( + name=name, + description="t", + parameters={"type": "object", "properties": {}}, + handler=lambda _a: {"ok": True}, + ) + + tools = [ + _spec("mcp__netx__listCliTargets"), + _spec("mcp__netx__execManagedNe"), + _spec("ume_alarm_xlsx_report"), + _spec("mcp__netx__aggregateUmeAlarms"), + _spec("write_xlsx"), + _spec("run_command"), + ] + kept = filter_tool_specs_for_ops_short_intent(tools, intent="fiber_cut") + names = {t.name for t in kept} + assert "ume_alarm_xlsx_report" in names + assert "mcp__netx__aggregateUmeAlarms" in names + assert "write_xlsx" in names + assert "mcp__netx__listCliTargets" not in names + assert "mcp__netx__execManagedNe" not in names + assert "run_command" not in names + assert len(filter_tool_specs_for_ops_short_intent(tools, intent="continue")) == len(tools) + + def test_ops_short_intent_hint_english_default() -> None: hint = maybe_ops_short_intent_system_hint(text="LOS on these sites", lang="en") assert "fiber" in hint.lower() or "LOS" in hint