mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 00:40:45 +08:00
Block CLI/inventory until ume_alarm_xlsx_report (or aggregate) succeeds, and return job_id for big NE batches via get_ne_exec_job. Co-authored-by: Cursor <cursoragent@cursor.com>
161 lines
4.9 KiB
Python
161 lines
4.9 KiB
Python
"""Report-first gate for ops short intents: CLI/inventory only after report path."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from typing import Any
|
||
|
||
from runtime.application.gateway.ops_short_intent import ops_short_intent_should_filter_tools
|
||
from runtime.tools.playbook_contracts import playbook_example_for_tool, short_intent_first_step
|
||
|
||
# Tools blocked until a report-path tool succeeds this turn.
|
||
_CLI_BEFORE_REPORT_TOOLS = frozenset(
|
||
{
|
||
"listclitargets",
|
||
"listmanagedne",
|
||
"getmanagedne",
|
||
"execmanagedne",
|
||
"queryumeneinventory",
|
||
"getumene",
|
||
"findtopologypaths",
|
||
"sqlqueryume",
|
||
"netx_list_managed_ne",
|
||
"netx_get_managed_ne",
|
||
"netx_exec_managed_ne",
|
||
"netx_sql_query_ume",
|
||
"netx_list_cli_targets",
|
||
}
|
||
)
|
||
|
||
# Successful call to any of these unlocks CLI for the rest of the turn.
|
||
_REPORT_PATH_TOOLS = frozenset(
|
||
{
|
||
"ume_alarm_xlsx_report",
|
||
"aggregateumealarms",
|
||
"aggregateumealarmsraw",
|
||
}
|
||
)
|
||
|
||
|
||
def _tool_key(name: str) -> str:
|
||
raw = str(name or "").strip()
|
||
if not raw:
|
||
return ""
|
||
if "__" in raw:
|
||
raw = raw.rsplit("__", 1)[-1]
|
||
return raw.strip().lower().replace("-", "_")
|
||
|
||
|
||
def is_cli_before_report_tool(tool_name: str) -> bool:
|
||
return _tool_key(tool_name) in _CLI_BEFORE_REPORT_TOOLS
|
||
|
||
|
||
def is_report_path_tool(tool_name: str) -> bool:
|
||
return _tool_key(tool_name) in _REPORT_PATH_TOOLS
|
||
|
||
|
||
def _parse_json_obj(raw: Any) -> dict[str, Any]:
|
||
if isinstance(raw, dict):
|
||
return dict(raw)
|
||
if isinstance(raw, str) and raw.strip():
|
||
try:
|
||
data = json.loads(raw)
|
||
return data if isinstance(data, dict) else {}
|
||
except Exception:
|
||
return {}
|
||
return {}
|
||
|
||
|
||
def turn_has_successful_report_path(
|
||
store: Any,
|
||
*,
|
||
session_id: str,
|
||
turn_uuid: str,
|
||
) -> bool:
|
||
"""True if this turn already has a successful report/aggregate tool result."""
|
||
tu = str(turn_uuid or "").strip()
|
||
sid = str(session_id or "").strip()
|
||
if not tu or not sid or store is None:
|
||
return False
|
||
try:
|
||
rows = store.get_messages(session_id=sid, limit=500)
|
||
except Exception:
|
||
return False
|
||
for m in rows or []:
|
||
if str(getattr(m, "role", "") or "").strip().lower() != "tool":
|
||
continue
|
||
if str(getattr(m, "turn_uuid", "") or "").strip() != tu:
|
||
continue
|
||
ep = _parse_json_obj(getattr(m, "event_payload", None))
|
||
name = str(ep.get("tool_name") or "").strip()
|
||
if not name:
|
||
tc = _parse_json_obj(getattr(m, "tool_calls", None))
|
||
name = str(tc.get("name") or "").strip()
|
||
if not is_report_path_tool(name):
|
||
continue
|
||
if ep.get("ok") is True:
|
||
return True
|
||
try:
|
||
payload = json.loads(str(getattr(m, "content", "") or "") or "{}")
|
||
except Exception:
|
||
payload = {}
|
||
if isinstance(payload, dict) and payload.get("ok") is True:
|
||
return True
|
||
return False
|
||
|
||
|
||
def report_first_block_payload(*, intent: str | None, lang: str = "en") -> dict[str, Any]:
|
||
en = str(lang or "").strip().lower().startswith("en")
|
||
step = short_intent_first_step(intent)
|
||
tool = "ume_alarm_xlsx_report"
|
||
example = playbook_example_for_tool(tool, intent=intent) or {"mode": "list", "deliverable": True}
|
||
if step:
|
||
tool, example = step[0], step[1]
|
||
hint = (
|
||
f"Report-first gate: call {tool} successfully before CLI/inventory this turn. "
|
||
"Device confirmation via execManagedNe is allowed only after the report path returns ok."
|
||
if en
|
||
else f"报表优先:本轮先成功调用 {tool},再允许 CLI/清单。"
|
||
"设备核实(execManagedNe)仅在报表路径 ok 之后。"
|
||
)
|
||
return {
|
||
"ok": False,
|
||
"error_code": "report_first_required",
|
||
"failure_class": "report_first",
|
||
"error": "report_first_required",
|
||
"hint": hint,
|
||
"next_tool": tool,
|
||
"example": example,
|
||
"intent": str(intent or ""),
|
||
}
|
||
|
||
|
||
def maybe_block_cli_before_report(
|
||
*,
|
||
tool_name: str,
|
||
intent: str | None,
|
||
store: Any,
|
||
session_id: str,
|
||
turn_uuid: str,
|
||
lang: str = "en",
|
||
local_report_ok: bool = False,
|
||
) -> dict[str, Any] | None:
|
||
"""Return a block payload when CLI/inventory is used before report on short intents."""
|
||
if not ops_short_intent_should_filter_tools(intent):
|
||
return None
|
||
if not is_cli_before_report_tool(tool_name):
|
||
return None
|
||
if local_report_ok:
|
||
return None
|
||
if turn_has_successful_report_path(store, session_id=session_id, turn_uuid=turn_uuid):
|
||
return None
|
||
return report_first_block_payload(intent=intent, lang=lang)
|
||
|
||
|
||
__all__ = [
|
||
"is_cli_before_report_tool",
|
||
"is_report_path_tool",
|
||
"maybe_block_cli_before_report",
|
||
"report_first_block_payload",
|
||
"turn_has_successful_report_path",
|
||
]
|