From 2f1d3983b89bb508901ff646d686ceb820ea25da Mon Sep 17 00:00:00 2001 From: oliver Date: Wed, 12 Aug 2026 22:36:57 +0800 Subject: [PATCH] Polish ops skills from field Q&A and tighten fiber_cut vs optical-power presets. Co-authored-by: Cursor --- .../network_ops/ume_alarm_xlsx_report.py | 53 +++++++++++++--- .../ops/ops-netx-managed-ne-playbook/SKILL.md | 9 ++- .../ops/ops-netx-ume-playbook/SKILL.md | 63 ++++++++++++++++--- .../ops/ops-netx-ume-playbook/reference.md | 28 +++++++-- tests/test_ume_alarm_xlsx_report.py | 14 ++++- 5 files changed, 143 insertions(+), 24 deletions(-) diff --git a/runtime/tools/experts/network_ops/ume_alarm_xlsx_report.py b/runtime/tools/experts/network_ops/ume_alarm_xlsx_report.py index 896bd659..c789d808 100644 --- a/runtime/tools/experts/network_ops/ume_alarm_xlsx_report.py +++ b/runtime/tools/experts/network_ops/ume_alarm_xlsx_report.py @@ -24,9 +24,32 @@ _LIST_FIELDS = [ ] # Server keyword is single-substring; presets filter client-side after a wider pull. +# fiber_cut: do NOT use bare "optical" — that pulls optical-power threshold (field CSV noise). +# Default server keyword (when caller omits keyword) biases the page toward real cut/LOS rows. _PRESET_CLIENT_TERMS: dict[str, tuple[str, ...]] = { - "fiber_cut": ("los", "fiber", "断纤", "光缆", "光路", "optical"), - "offline": ("离线", "offline", "通信中断", "单板离线", "ne communication"), + "fiber_cut": ( + "los", + "fiber break", + "fiber", + "断纤", + "光缆", + "光路", + "missing laser", + "optical module is faulty", + ), + "offline": ( + "离线", + "offline", + "通信中断", + "单板离线", + "ne communication", + "bn ems", + "communication failure", + ), +} +_PRESET_DEFAULT_KEYWORD: dict[str, str] = { + "fiber_cut": "LOS", + "offline": "BN EMS", } _MODE_DEFAULT_NAMES: dict[str, str] = { @@ -196,9 +219,14 @@ def ume_alarm_xlsx_report_tool() -> ToolSpec: time_from = str(args.get("time_from") or "").strip() time_to = str(args.get("time_to") or "").strip() page_size = max(1, min(500, int(args.get("page_size") or args.get("limit") or 100))) + # Preset modes: bias API keyword so the page is not drowned by PW/BGP noise (~80k rows). + preset_kw_applied = False + if mode in _PRESET_DEFAULT_KEYWORD and not keyword: + keyword = _PRESET_DEFAULT_KEYWORD[mode] + preset_kw_applied = True # Preset modes pull a wider page then filter client-side (API keyword is single substring). fetch_size = page_size - if mode in _PRESET_CLIENT_TERMS and not keyword: + if mode in _PRESET_CLIENT_TERMS: fetch_size = max(page_size, min(500, page_size * 3)) deliverable = _truthy(args.get("deliverable"), default=True) filename = str(args.get("name") or args.get("filename") or "").strip() or _MODE_DEFAULT_NAMES[mode] @@ -206,7 +234,12 @@ def ume_alarm_xlsx_report_tool() -> ToolSpec: filename = f"{filename}.xlsx" sheet_name = "alarms" - summary_meta: dict[str, Any] = {"mode": mode, "keyword": keyword or None, "severity": severity or None} + summary_meta: dict[str, Any] = { + "mode": mode, + "keyword": keyword or None, + "severity": severity or None, + "preset_default_keyword": preset_kw_applied, + } if mode == "aggregate_by_host": upstream = _fetch_aggregate_by_host( @@ -235,7 +268,7 @@ def ume_alarm_xlsx_report_tool() -> ToolSpec: return {"ok": False, "error": "ume_query_failed", "upstream": upstream} data = upstream.get("data") if isinstance(upstream.get("data"), dict) else {} items = data.get("items") if isinstance(data.get("items"), list) else [] - if mode in _PRESET_CLIENT_TERMS and not keyword: + if mode in _PRESET_CLIENT_TERMS: items = _filter_preset_items(mode, items)[:page_size] headers, rows = _rows_from_list_items(items) summary_meta["total"] = data.get("total") @@ -278,6 +311,8 @@ def ume_alarm_xlsx_report_tool() -> ToolSpec: description=( "One-shot UME alarm Excel for WhatsApp ops: query netx alarms and build .xlsx " "(optionally mark deliverable). Modes: list, aggregate_by_host, fiber_cut, offline. " + "fiber_cut defaults keyword=LOS (ETPI LOS / Fiber Break family; not optical-power threshold). " + "offline defaults keyword=BN EMS. Pass keyword=Fiber Break for explicit fiber-break rows. " "Prefer this over query+write_xlsx+save_deliverable_attachment for short WA requests." ), parameters={ @@ -289,7 +324,8 @@ def ume_alarm_xlsx_report_tool() -> ToolSpec: "default": "list", "description": ( "list=raw alarm rows; aggregate_by_host=count by host_name; " - "fiber_cut/offline=preset keyword filters for common WA intents." + "fiber_cut defaults keyword=LOS (not optical-power threshold); " + "offline defaults keyword=BN EMS." ), }, "severity": { @@ -298,7 +334,10 @@ def ume_alarm_xlsx_report_tool() -> ToolSpec: }, "keyword": { "type": "string", - "description": "Optional keyword; fiber_cut/offline apply defaults when omitted.", + "description": ( + "Optional keyword. If omitted: fiber_cut→LOS, offline→BN EMS. " + "Pass Fiber Break for explicit fiber-break rows." + ), }, "time_from": {"type": "string", "description": "ISO time; filters last_seen_at >="}, "time_to": {"type": "string", "description": "ISO time; filters last_seen_at <="}, diff --git a/skills/_workspace/ops/ops-netx-managed-ne-playbook/SKILL.md b/skills/_workspace/ops/ops-netx-managed-ne-playbook/SKILL.md index 4f474a74..d8433fbc 100644 --- a/skills/_workspace/ops/ops-netx-managed-ne-playbook/SKILL.md +++ b/skills/_workspace/ops/ops-netx-managed-ne-playbook/SKILL.md @@ -35,12 +35,17 @@ description: 面向 ops 专家的 netx 纳管网元(网元管理)作业手 ### Capacity / optical power between two names -When user says **capacity**, **bandwidth between A and B**, or **optical power A <> B** (prod vocabulary): +When user says **capacity**, **bandwidth between A and B**, **optical power A <> B**, or site pairs (`SEMBAWA <> ANGKATAN_EP`, `SMD-PSB <> SMD-PNTE`): 1. Resolve nicknames → real `host_name` via inventory/wiki (`SEMBAWA` → e.g. `PLG-SMW-EN1-…`). 2. Find interconnect: `findTopologyPaths` and/or LLDP (`show lldp …` / vendor equivalent) — identify **both ports**. 3. Read optics on **both** ends with the correct vendor command (below). Summarize: interface, RX/TX power, threshold, whether link is up. -4. Do **not** answer with only UME bandwidth-usage-rate alarms unless the user asked for congestion alarms. +4. Do **not** answer with only UME bandwidth-usage-rate **or** optical-power-threshold alarm tallies unless the user asked for those alarm lists. +5. CRC + optical on a link: same path — resolve ports once, then optic CLI (+ CRC counters if allowlisted) on **both** ends in **one** batch when possible (`targets` if vendors differ). + +### Area optical-power **alarm** list (UME only) + +`optical power threshold crossed` under area BPP/PBR/PAL/… → UME keyword=`optical power` + hostname prefix — **not** this CLI recipe and **not** fiber_cut. ### ZTE optical CLI (prod corrections) diff --git a/skills/_workspace/ops/ops-netx-ume-playbook/SKILL.md b/skills/_workspace/ops/ops-netx-ume-playbook/SKILL.md index 77a99e4e..38c1f9fc 100644 --- a/skills/_workspace/ops/ops-netx-ume-playbook/SKILL.md +++ b/skills/_workspace/ops/ops-netx-ume-playbook/SKILL.md @@ -59,15 +59,20 @@ Prefer these fixed paths for short group/DM asks (EN first; ZH aliases still wor | User says (examples) | Recipe | |----------------------|--------| -| fiber cut / LOS / cable cut / 断纤 | Prefer `ume_alarm_xlsx_report(mode=fiber_cut)` (xlsx + deliver); or summarize via `queryUmeAlarmsRaw` | -| offline NE / board offline / 离线 | Prefer `ume_alarm_xlsx_report(mode=offline)` | +| fiber cut / LOS / cable cut / 断纤 / sitelist | Prefer `ume_alarm_xlsx_report(mode=fiber_cut)` (defaults keyword=`LOS` → ETPI LOS; not optical-power threshold). For explicit **Fiber Break** rows also/instead `keyword=Fiber Break` or `queryUmeAlarmsRaw(keyword=Fiber Break)`. Reply with **host_name list** + counts | +| offline NE / board offline / unmanaged / 离线 | Prefer `ume_alarm_xlsx_report(mode=offline)` (defaults `BN EMS` / NE communication failure). Unmanaged ME list → same family + clarify BN EMS / unreachable | | Critical Top / alarm tally | ① `aggregateUmeAlarms(severity=critical, top_ne=20)`; for file: `ume_alarm_xlsx_report(mode=aggregate_by_host, severity=critical)` | -| how many alarms / tally | ① `runUmeDiagnostics` or `aggregateUmeAlarms`; ② report by_severity + freshness | +| how many alarms / tally / 现网告警数量 | ① `runUmeDiagnostics` or `aggregateUmeAlarms`; ② report by_severity + freshness | | export Excel / send spreadsheet | `ume_alarm_xlsx_report` **or** `write_xlsx(..., deliverable=true)`; never split into 3 steps | | CRC in area PAD / ACH / … | `queryUmeAlarmsRaw(keyword=CRC)` then keep rows whose `alarm_host_name` / `ne_host_name` starts with area prefix (`PAD-`, `ACH-`, …). Optional xlsx via `write_xlsx(deliverable=true)` | -| bandwidth / congestion / usage rate (+ area) | keyword=`bandwidth` (do **not** require event_type unless user asks); filter hostname prefix for area; if CLI confirm false positives: top 3–5 NEs in **one** batch — same show → `ume_ne_ids=[…]`+`commands`; mixed vendors → `targets=[{ume_ne_id, commands},…]` — never one-NE loops | -| BN EMS / dying gasp / unmanaged (+ area) | keyword or native cause match (`BN EMS` / `dying gasp`); filter area prefix; short EN summary + optional xlsx | -| power / temperature / fan alarms (+ area/NE) | keyword=`power` / `temperature` / `fan`; scope to host or area prefix | +| bandwidth / congestion / usage rate (+ area) | keyword=`bandwidth` (matches *Send/Receive bandwidth usage rate threshold crossed*; do **not** require event_type). Filter hostname prefix for area; if CLI confirm: top 3–5 NEs in **one** batch | +| optical power **threshold** in area (BPP/PBR/PAL/…) | keyword=`optical power` (or `Input optical power`) + keep `AREA-` hosts. **Not** fiber_cut mode. Distinct from capacity A<>B CLI | +| BN EMS / dying gasp / unmanaged (+ area) | See **Dying gasp / BN EMS correlation** below — do not stop at one NE | +| power / temperature / fan / undervoltage / System Power off | keyword=`power` / `temperature` / `fan` / `undervoltage` / `Power off`; scope to host or area prefix. Optical *power(dBm)* ≠ board voltage | +| license | keyword=`License` (causes: *Permanent license abnormal*, *No enough license resource*) | +| BGP / OSPF / ISIS / LDP / PW / Tunnel on host | `queryUmeAlarmsRaw(host or keyword=BGP\|OSPF\|LDP\|…)` on named host(s); for peer correlation see below | +| Port down / ETPI / which segment cut? | keyword=`Port down` or `LOS` on the named host; use `object_name` + `findTopologyPaths` / LLDP to name the far end | +| alarm code NNNN | `queryUmeAlarmsRaw` / diagnostics `top_alarm_codes`; keyword or raw filter on code; return **host_name** list | | alarm on **one hostname** (e.g. `MDN-PLSP`, `MKS-SWBP-EN1`) | `queryUmeAlarms` / `queryUmeAlarmsRaw` with `host_name` / keyword=hostname. **Never** start a scheduled License/daily playbook | | alarm history / time range (e.g. `17.50-18.15`) | Resolve **WIB (UTC+7)** wall clock → `time_from`/`time_to` on `last_seen_at` / history fields; first check freshness; name hosts exactly (`MKS-KIM-CN1`) | | is NE rebooted? / alarm history for NE | Host-scoped alarm history (reboot/reload/power related causes); answer yes/no + evidence times | @@ -80,11 +85,49 @@ Delivery rules: ### Field vocabulary (prod-learned; enforce) -- **Area** = hostname **prefix** before first extra segment: `BTM-`, `ACH-`, `MKS-`, `PAD-`, `MDN-`, `KND-`, `SMD-`, `MDO-`, `PLG-`, … Case-insensitive starts-with. -- **Capacity / bandwidth between A and B** (user correction in field): means **SFP/optical link capacity between two hostnames**, not UME bandwidth-usage-rate alarms alone. Resolve both NEs → interconnect ports (`findTopologyPaths` / LLDP) → optical/SFP CLI. See `ops-netx-managed-ne-playbook`. -- **Site nicknames** (SEMBAWA, ANGKATAN_EP, …): resolve via inventory/wiki/`queryUmeNeInventory(keyword=…)` **before** CLI; never invent hostnames. +- **Area** = hostname **prefix** before first `-`: `MDN-`, `LPG-`, `MKS-`, `PLG-`, `BJM-`, `PTK-`, `ACH-`, `PBR-`, `MDO-`, `SMD-`, `PAD-`, `BTM-`, `PLK-`, `BPP-`, `BKL-`, `JBI-`, `KND-`, `PAL-`, `GRO-`, `JAP-`, … Case-insensitive starts-with. +- **Capacity / bandwidth between A and B** / `A <> B` / site nicknames (SEMBAWA, ANGKATAN_EP): means **SFP/optical link** on the interconnect — **not** UME *bandwidth usage rate* alarms alone. Resolve both NEs → ports (`findTopologyPaths` / LLDP) → optic CLI. See `ops-netx-managed-ne-playbook`. +- **Optical power threshold crossed** (area list): UME cause *Input/Output optical power(dBm) threshold crossed* — keyword=`optical power`; **not** `mode=fiber_cut`. +- **Fiber cut / LOS sitelist**: causes *Ethernet physical (ETPI) LOS*, *Fiber Break*, *Missing laser module* — report `mode=fiber_cut` (LOS-biased) and/or `keyword=Fiber Break`. +- **Site nicknames**: resolve via inventory/wiki/`queryUmeNeInventory(keyword=…)` **before** CLI; never invent hostnames. - **Local clock phrases** (`17.50`, `today`, `yesterday`): treat as **Asia/Jakarta (WIB, UTC+7)** unless user says otherwise. +### Field cause cheat-sheet (2026-08 snapshot vocabulary) + +Use these as `keyword` / evidence labels (exact strings appear in `native_probable_cause`): + +| Intent | Typical cause substrings | +|--------|---------------------------| +| Fiber / LOS | `ETPI) LOS`, `Fiber Break`, `Missing laser module` | +| Optical threshold | `Input optical power(dBm) threshold crossed`, `Output optical power` | +| Congestion | `Send bandwidth usage rate`, `Receive bandwidth usage rate` | +| CRC | `Receive CRC error frames`, `Received CRC error packet` | +| Offline / unmanaged | `BN EMS alarm NE communication failure` | +| Dying gasp | `Remote dying gasp event` | +| License | `Permanent license abnormal`, `No enough license resource` | +| Power / env | `System Power off`, `Input undervoltage`, `temperature`, `Fan module` | +| Control-plane (noisy) | `BGP Neighbour down`, `OSPF Neighbour`, `ISIS Neighbour`, `LDP Neighbour`, `State of PW in L2VPN`, `Tunnel down`, `NTP server` | + +Do **not** treat PW/BGP volume leaders as “fiber cut” unless the user asked for those families. + +### Dying gasp / BN EMS correlation (field-mandated) + +When user mentions **dying gasp** (or correlates BN EMS with a port): + +1. On the named NE: `queryUmeAlarmsRaw` with keyword=`dying gasp` (and/or host_name) — note `object_name` / slot-port and `last_seen_at`. +2. Find peer: `findTopologyPaths` and/or LLDP/CLI on that port; identify far-end `host_name`. +3. On the **peer**: look for **BN EMS** / `NE communication failure` (and related offline) with **near timestamp** (± window from step 1). +4. Reply with both sides + times + whether correlation holds. Save this as the default dying-gasp playbook — do not answer only one NE. + +### Peer / protocol correlation (BGP·OSPF·LDP) + +Field pattern: alarms on `HOST-A` with peer IP → confirm on `HOST-B` (or peer from topology). + +1. Query both hosts (or keyword + both host filters) for the protocol family. +2. Align **occurrence / clear** times when asked. +3. Peer match: prefer exact peer / router-id; if user says so, also match **identical 3rd+4th octet** of the peer address from the alarm text. +4. Keep answers scoped to the named link (`A <> B`); do not dump unrelated area noise. + ### Anti-patterns seen in field (do not repeat) 1. **Wrong playbook hijack** — User: `query alarm on MDN-AHJ-AN1` → must NOT run License/daily scheduled playbook. Answer that host’s current alarms only. @@ -93,6 +136,8 @@ Delivery rules: 4. **Apology loops** — If user asks “are you still running / why no response?”, resume the **quoted task** immediately; one short status line, then results. Do not ask what a Run ID might mean if `schedule_list` / job tools can answer. 5. **Group noise** — Pure emoji / mention-only / “hi” with no ops ask: stay minimal or silent per group policy; do not give a long “how can I help” menu. 6. **Blind CLI retries** — Wrong ZTE optic command once → switch to `show opticalinfo brief` (see managed-ne skill); do not retry the failed spelling. +7. **fiber_cut vs optical power** — Area “optical power threshold” lists must **not** use `mode=fiber_cut` (that is LOS/Fiber Break biased). +8. **Unfiltered dump** — Snapshot has ~80k uncleared rows; never list without severity/keyword/host/area/time. ### Answer shape (WhatsApp EN) — strict ops bot diff --git a/skills/_workspace/ops/ops-netx-ume-playbook/reference.md b/skills/_workspace/ops/ops-netx-ume-playbook/reference.md index 433f7d21..9963dc16 100644 --- a/skills/_workspace/ops/ops-netx-ume-playbook/reference.md +++ b/skills/_workspace/ops/ops-netx-ume-playbook/reference.md @@ -31,10 +31,13 @@ |------|------| | Critical Top | `aggregateUmeAlarms(severity=critical, top_ne=20)` | | 按 host 统计+Excel | `ume_alarm_xlsx_report(mode=aggregate_by_host, severity=critical)` | -| 断纤/离线清单+Excel | `ume_alarm_xlsx_report(mode=fiber_cut\|offline)` | +| 断纤/LOS 清单+Excel | `ume_alarm_xlsx_report(mode=fiber_cut)`(默认 keyword=`LOS`);纯 Fiber Break 再加 `keyword=Fiber Break` | +| 离线/BN EMS+Excel | `ume_alarm_xlsx_report(mode=offline)`(默认 `BN EMS`) | +| 区域光功率门限 | `queryUmeAlarmsRaw(keyword=optical power)` → 保留 `AREA-` 前缀;**不要** fiber_cut | | 发 Excel(已有表数据) | `write_xlsx(..., deliverable=true)` | -| 区域 + 关键字(CRC/bandwidth/power) | `queryUmeAlarmsRaw(keyword=…)` → 过滤 `host` 前缀 `AREA-` | +| 区域 + 关键字(CRC/bandwidth/license) | `queryUmeAlarmsRaw(keyword=…)` → 过滤 `host` 前缀 `AREA-` | | 单网元当前告警 | `queryUmeAlarms(host_name=…)` — **禁止**误跑 License 定时 playbook | +| dying gasp | 本端 dying gasp → 对端 BN EMS(近时间窗)+ 端口/拓扑;见 SKILL | | 两端 capacity/optical | 解析两端 hostname → `findTopologyPaths` / LLDP → CLI optic(见 managed-ne) | | 时间窗历史(WIB) | freshness → `time_from`/`time_to`(按 Asia/Jakarta) | @@ -42,11 +45,28 @@ | 用户说法 | 正确理解 | |----------|----------| -| congestion / bandwidth usage in ACH | UME keyword bandwidth + hostname `ACH-`;要验真再 CLI top N | -| capacity A to B / optical power A <> B | **链路口 SFP/光功率**,不是单独告警 tally | +| congestion / bandwidth usage in ACH | UME keyword=`bandwidth` + hostname `ACH-`;要验真再 CLI top N | +| optical power threshold in BPP/PBR/PAL | keyword=`optical power` + 区域前缀;≠ fiber cut | +| fiber cut / LOS sitelist | `mode=fiber_cut` / keyword=`LOS` 或 `Fiber Break`;回 host 列表 | +| capacity A to B / optical power A <> B / SEMBAWA <> ANGKATAN | **链路口 SFP/光功率 CLI**,不是单独告警 tally | +| dying gasp on HOST + port | 关联对端 **BN EMS** near timestamp(现场强制配方) | +| which segment cut? + LOS host | `object_name` + topology/LLDP 找对端 | +| BGP/OSPF/LDP on HOST / A <> B | 双端协议告警 + 时间对齐;peer 可按后两段 octet | | site SEMBAWA / ANGKATAN_EP | 先 inventory/wiki 解析成真实 `host_name` | | `17.50 - 18.15` | WIB 当天 17:50–18:15 | | check alarm on MDN-xxx | **仅该 host**;勿触发 daily license 等无关 playbook | +| alarm code 4758 | 按 code 过滤;列出 **host_name** | + +## 3d) 现场 cause 关键字(CSV 高频) + +- LOS / Fiber Break / Missing laser → 断纤类 +- Input/Output optical power(dBm) threshold → 光功率门限(区域清单) +- bandwidth usage rate threshold → 拥塞 +- CRC error → CRC +- BN EMS … communication failure → 离线/非管 +- Remote dying gasp → 临终掉电类,必做对端关联 +- Permanent license / No enough license → license +- BGP/OSPF/ISIS/LDP Neighbour down、State of PW、Tunnel down → 控制面/伪线噪声,勿当断纤 ## 4) 诊断 diff --git a/tests/test_ume_alarm_xlsx_report.py b/tests/test_ume_alarm_xlsx_report.py index 6f90178b..c3fb76bf 100644 --- a/tests/test_ume_alarm_xlsx_report.py +++ b/tests/test_ume_alarm_xlsx_report.py @@ -113,12 +113,22 @@ def test_filter_preset_and_row_helpers() -> None: items = [ {"alarm_event_type": "Communication LOS", "alarm_host_name": "A"}, {"alarm_event_type": "fan fail", "alarm_host_name": "B"}, + { + "alarm_native_probable_cause": "Ethernet physical (ETPI) Input optical power(dBm) threshold crossed", + "alarm_host_name": "C", + }, + {"alarm_native_probable_cause": "Fiber Break", "alarm_host_name": "D"}, + {"alarm_native_probable_cause": "BN EMS alarm NE communication failure", "alarm_host_name": "E"}, ] filtered = _filter_preset_items("fiber_cut", items) - assert len(filtered) == 1 + hosts = {r["alarm_host_name"] for r in filtered} + assert hosts == {"A", "D"} + assert "C" not in hosts # optical-power threshold is not fiber_cut + offline = _filter_preset_items("offline", items) + assert {r["alarm_host_name"] for r in offline} == {"E"} headers, rows = _rows_from_list_items(filtered) assert headers[0] == "host_name" - assert rows[0][0] == "A" + assert rows[0][0] in {"A", "D"} _, agg_rows, meta = _rows_from_aggregate_buckets( {"buckets": [{"key": "H1", "count": 2}], "total": 2, "by_ne_missing": 0} )