mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-10 09:10:46 +08:00
Remove plan mode, skill toolcall path, and inline netx tools.
Wave C subtraction: keep skills as prompt-only, route netx via MCP with a shared netx_http client for xlsx/context inject, and delete plan_agent_v2 from the gateway. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
628a9dffd2
commit
efa72df362
40 changed files with 169 additions and 3033 deletions
|
|
@ -13,7 +13,6 @@ from runtime.tools.base import ToolRegistry, ToolSpec
|
|||
from runtime.tools.expert_registry import materialize_tools_for_expert
|
||||
from runtime.tools.mcp.adapter import materialize_mcp_tools_for_specialist
|
||||
from runtime.tools.public_registry import materialize_public_tools
|
||||
from runtime.tools.skills_runtime.materialize_skill_tools import materialize_executable_skill_tools
|
||||
from runtime.skills import SkillSpec, materialize_skills_from_tool_specs
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -48,21 +47,6 @@ def _plugin_tools_enabled(store: Any | None = None) -> bool:
|
|||
return True
|
||||
|
||||
|
||||
def _skill_toolcall_enabled(store: Any | None) -> bool:
|
||||
try:
|
||||
raw_env = str(os.getenv("AIA_SKILL_TOOLCALL_ENABLED") or "").strip()
|
||||
if raw_env:
|
||||
return _is_truthy(raw_env)
|
||||
if store is not None:
|
||||
raw = str(store.get_setting("AIA_SKILL_TOOLCALL_ENABLED") or "").strip()
|
||||
if raw:
|
||||
return _is_truthy(raw)
|
||||
except Exception:
|
||||
pass
|
||||
# Default off: skill uses prompt-injection path, not toolcall path.
|
||||
return False
|
||||
|
||||
|
||||
def _apply_declared_tool_policy(
|
||||
tools: list[ToolSpec],
|
||||
*,
|
||||
|
|
@ -206,19 +190,6 @@ def materialize_tool_specs(
|
|||
except Exception as exc:
|
||||
logger.warning("expert tool load skipped: %s", exc)
|
||||
|
||||
# collect: skill runtime
|
||||
if _skill_toolcall_enabled(store):
|
||||
try:
|
||||
for spec in materialize_executable_skill_tools(store=store):
|
||||
if not isinstance(spec, ToolSpec):
|
||||
continue
|
||||
if _hidden_from_model(str(spec.name or "")):
|
||||
logger.info("skill runtime tool hidden from model registry: %s", str(spec.name or ""))
|
||||
continue
|
||||
collected.append(("skill_runtime", spec))
|
||||
except Exception as exc:
|
||||
logger.warning("skill runtime tool load skipped: %s", exc)
|
||||
|
||||
# MCP tools are role-bound and should be materialized before model injection.
|
||||
# Fine-grained penalty/visibility is still applied by wire policy in direct_loop.
|
||||
mcp_enabled = True
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ from __future__ import annotations
|
|||
import importlib.util
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
|
@ -20,22 +19,12 @@ _DEPRECATED_TOOL_NAMES: set[str] = {
|
|||
# Deprecated internal tool from legacy src/tools chain.
|
||||
"get_weather",
|
||||
}
|
||||
|
||||
|
||||
def netx_builtin_tools_enabled() -> bool:
|
||||
"""When false, skip ``network_ops/netx_tools.py`` factories (use MCP ``mcp__netx__*`` instead)."""
|
||||
return str(os.getenv("OCLAW_NETX_BUILTIN_TOOLS") or "0").strip().lower() not in {
|
||||
"0",
|
||||
"false",
|
||||
"no",
|
||||
"off",
|
||||
}
|
||||
# Helpers / non-tool modules under experts/ (no *_tool factories expected).
|
||||
_SKIP_EXPERT_MODULES: frozenset[str] = frozenset({"netx_tools", "netx_http"})
|
||||
|
||||
|
||||
def _skip_expert_module(module_path: Path) -> bool:
|
||||
if module_path.stem == "netx_tools" and not netx_builtin_tools_enabled():
|
||||
return True
|
||||
return False
|
||||
return module_path.stem in _SKIP_EXPERT_MODULES
|
||||
|
||||
|
||||
def _load_module_from_path(module_path: Path, module_name: str) -> Any | None:
|
||||
|
|
|
|||
104
runtime/tools/experts/network_ops/netx_http.py
Normal file
104
runtime/tools/experts/network_ops/netx_http.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
"""Shared netx REST client for oclaw (context inject + ume_alarm_xlsx_report).
|
||||
|
||||
Aligns base URL with netx-mcp: ``NETX_API_URL`` then ``OCLAW_NETX_BASE_URL``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextvars
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
# Set by ToolExecutor so responses match session language.
|
||||
NETX_TOOL_LANG: contextvars.ContextVar[str] = contextvars.ContextVar("netx_tool_lang", default="zh")
|
||||
|
||||
_PROTOCOL_KEY_ZH_TO_EN: dict[str, str] = {
|
||||
"其他": "Other",
|
||||
"时钟": "Clock",
|
||||
"OTN/光": "OTN/Optical",
|
||||
"电源": "Power",
|
||||
}
|
||||
|
||||
|
||||
def _netx_base_url() -> str:
|
||||
return (
|
||||
os.getenv("NETX_API_URL") or os.getenv("OCLAW_NETX_BASE_URL") or "http://127.0.0.1:8890"
|
||||
).strip().rstrip("/")
|
||||
|
||||
|
||||
def _netx_headers() -> dict[str, str]:
|
||||
h = {"accept": "application/json"}
|
||||
tok = (os.getenv("OCLAW_NETX_API_TOKEN") or os.getenv("NETX_API_TOKEN") or "").strip()
|
||||
if tok:
|
||||
h["authorization"] = f"Bearer {tok}"
|
||||
return h
|
||||
|
||||
|
||||
def _netx_lang_query_params() -> dict[str, str]:
|
||||
lang = str(NETX_TOOL_LANG.get() or "zh").strip().lower()
|
||||
if lang.startswith("en"):
|
||||
return {"lang": "en"}
|
||||
return {}
|
||||
|
||||
|
||||
def _localize_netx_payload(data: dict[str, Any], *, lang: str) -> dict[str, Any]:
|
||||
"""Map legacy Chinese protocol bucket labels to English for en sessions."""
|
||||
if not str(lang or "").strip().lower().startswith("en"):
|
||||
return data
|
||||
proto = data.get("protocol_summary")
|
||||
if isinstance(proto, list):
|
||||
for row in proto:
|
||||
if isinstance(row, dict):
|
||||
k = str(row.get("key") or "")
|
||||
if k in _PROTOCOL_KEY_ZH_TO_EN:
|
||||
row["key"] = _PROTOCOL_KEY_ZH_TO_EN[k]
|
||||
return data
|
||||
|
||||
|
||||
def _http_post_json(path: str, body: dict[str, Any], *, timeout: float = 180.0) -> dict[str, Any]:
|
||||
base = _netx_base_url()
|
||||
url = f"{base}{path}"
|
||||
try:
|
||||
with httpx.Client(timeout=timeout, trust_env=False) as client:
|
||||
resp = client.post(url, json=body, headers=_netx_headers())
|
||||
text = resp.text
|
||||
if not resp.is_success:
|
||||
return {"ok": False, "error": f"netx_http_{resp.status_code}", "detail": text[:800]}
|
||||
data = resp.json() if text else {}
|
||||
if isinstance(data, dict):
|
||||
data = _localize_netx_payload(data, lang=str(NETX_TOOL_LANG.get() or "zh"))
|
||||
return {"ok": True, "data": data if isinstance(data, dict) else {"raw": data}}
|
||||
except Exception as exc:
|
||||
return {"ok": False, "error": "netx_request_failed", "detail": str(exc)[:800]}
|
||||
|
||||
|
||||
def _http_json(method: str, path: str, *, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
base = _netx_base_url()
|
||||
url = f"{base}{path}"
|
||||
merged: dict[str, Any] = dict(_netx_lang_query_params())
|
||||
if params:
|
||||
merged.update(params)
|
||||
try:
|
||||
with httpx.Client(timeout=45.0, trust_env=False) as client:
|
||||
resp = client.request(method, url, params=merged or None, headers=_netx_headers())
|
||||
text = resp.text
|
||||
if not resp.is_success:
|
||||
return {"ok": False, "error": f"netx_http_{resp.status_code}", "detail": text[:800]}
|
||||
data = resp.json() if text else {}
|
||||
if isinstance(data, dict):
|
||||
data = _localize_netx_payload(data, lang=str(NETX_TOOL_LANG.get() or "zh"))
|
||||
return {"ok": True, "data": data if isinstance(data, dict) else {"raw": data}}
|
||||
except Exception as exc:
|
||||
return {"ok": False, "error": "netx_request_failed", "detail": str(exc)[:800]}
|
||||
|
||||
|
||||
__all__ = [
|
||||
"NETX_TOOL_LANG",
|
||||
"_http_json",
|
||||
"_http_post_json",
|
||||
"_localize_netx_payload",
|
||||
"_netx_base_url",
|
||||
"_netx_headers",
|
||||
]
|
||||
|
|
@ -1,145 +1,40 @@
|
|||
"""netx ops helpers: runtime context inject + optional legacy inline HTTP tools.
|
||||
"""netx ops helpers: runtime context inject for ops specialist.
|
||||
|
||||
Default: tools are exposed via stdio MCP (``pip install -e packages/netx-mcp`` then ``python -m netx_mcp``).
|
||||
Set ``OCLAW_NETX_BUILTIN_TOOLS=1`` to re-register inline ``netx_*`` expert tools.
|
||||
Alarm/NE tools are exposed only via stdio MCP (``mcp__netx__*``).
|
||||
HTTP helpers live in ``netx_http`` (shared with ``ume_alarm_xlsx_report``).
|
||||
|
||||
Configure via environment:
|
||||
- ``OCLAW_NETX_BASE_URL`` (default ``http://127.0.0.1:8890``) — runtime anchor HTTP probe
|
||||
- ``OCLAW_NETX_API_TOKEN`` (optional) → sent as ``Authorization: Bearer …`` if set.
|
||||
- ``NETX_API_URL`` or ``OCLAW_NETX_BASE_URL`` (default ``http://127.0.0.1:8890``)
|
||||
- ``OCLAW_NETX_API_TOKEN`` / ``NETX_API_TOKEN`` (optional Bearer)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextvars
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from runtime.tools.experts.network_ops.netx_http import (
|
||||
NETX_TOOL_LANG,
|
||||
_http_json,
|
||||
_http_post_json,
|
||||
_localize_netx_payload,
|
||||
_netx_base_url,
|
||||
_netx_headers,
|
||||
)
|
||||
|
||||
from runtime.tools.base import ToolSpec
|
||||
|
||||
# Set by ToolExecutor for netx_* tools so responses match session language.
|
||||
NETX_TOOL_LANG: contextvars.ContextVar[str] = contextvars.ContextVar("netx_tool_lang", default="zh")
|
||||
|
||||
_PROTOCOL_KEY_ZH_TO_EN: dict[str, str] = {
|
||||
"其他": "Other",
|
||||
"时钟": "Clock",
|
||||
"OTN/光": "OTN/Optical",
|
||||
"电源": "Power",
|
||||
}
|
||||
|
||||
|
||||
_UME_RAW_GROUP_FIELDS = [
|
||||
"alarm_alarm_key",
|
||||
"alarm_host_name",
|
||||
"alarm_ne_id",
|
||||
"alarm_object_name",
|
||||
"alarm_event_type",
|
||||
"alarm_native_probable_cause",
|
||||
"alarm_perceived_severity",
|
||||
"alarm_is_cleared",
|
||||
"alarm_time_created",
|
||||
"alarm_root_cause_alarm_indication",
|
||||
"ne_ne_id",
|
||||
"ne_ne_name",
|
||||
"ne_user_label",
|
||||
"ne_ip_address",
|
||||
"ne_ipv6_address",
|
||||
"ne_ne_type",
|
||||
"ne_device_level",
|
||||
"ne_host_name",
|
||||
"ne_location",
|
||||
"ne_hardware_version",
|
||||
"ne_loopback",
|
||||
"ne_consistent_state",
|
||||
"ne_interface_version",
|
||||
"ne_mac",
|
||||
"ne_admin_status",
|
||||
"ne_address_type",
|
||||
"ne_connection_status",
|
||||
"ne_maintain_status",
|
||||
"ne_net_mask",
|
||||
"ne_create_time",
|
||||
"ne_creator",
|
||||
"ne_vendor",
|
||||
"ne_source_type",
|
||||
"ne_exists",
|
||||
__all__ = [
|
||||
"NETX_TOOL_LANG",
|
||||
"ops_netx_system_context_extension",
|
||||
"_http_json",
|
||||
"_http_post_json",
|
||||
"_localize_netx_payload",
|
||||
"_netx_base_url",
|
||||
"_netx_headers",
|
||||
]
|
||||
|
||||
|
||||
def _netx_base_url() -> str:
|
||||
return (os.getenv("OCLAW_NETX_BASE_URL") or "http://127.0.0.1:8890").strip().rstrip("/")
|
||||
|
||||
|
||||
def _netx_headers() -> dict[str, str]:
|
||||
h = {"accept": "application/json"}
|
||||
tok = (os.getenv("OCLAW_NETX_API_TOKEN") or "").strip()
|
||||
if tok:
|
||||
h["authorization"] = f"Bearer {tok}"
|
||||
return h
|
||||
|
||||
|
||||
def _netx_lang_query_params() -> dict[str, str]:
|
||||
lang = str(NETX_TOOL_LANG.get() or "zh").strip().lower()
|
||||
if lang.startswith("en"):
|
||||
return {"lang": "en"}
|
||||
return {}
|
||||
|
||||
|
||||
def _localize_netx_payload(data: dict[str, Any], *, lang: str) -> dict[str, Any]:
|
||||
"""Map legacy Chinese protocol bucket labels to English for en sessions."""
|
||||
if not str(lang or "").strip().lower().startswith("en"):
|
||||
return data
|
||||
proto = data.get("protocol_summary")
|
||||
if isinstance(proto, list):
|
||||
for row in proto:
|
||||
if isinstance(row, dict):
|
||||
k = str(row.get("key") or "")
|
||||
if k in _PROTOCOL_KEY_ZH_TO_EN:
|
||||
row["key"] = _PROTOCOL_KEY_ZH_TO_EN[k]
|
||||
return data
|
||||
|
||||
|
||||
def _http_post_json(path: str, body: dict[str, Any], *, timeout: float = 180.0) -> dict[str, Any]:
|
||||
base = _netx_base_url()
|
||||
url = f"{base}{path}"
|
||||
try:
|
||||
with httpx.Client(timeout=timeout, trust_env=False) as client:
|
||||
resp = client.post(url, json=body, headers=_netx_headers())
|
||||
text = resp.text
|
||||
if not resp.is_success:
|
||||
return {"ok": False, "error": f"netx_http_{resp.status_code}", "detail": text[:800]}
|
||||
data = resp.json() if text else {}
|
||||
if isinstance(data, dict):
|
||||
data = _localize_netx_payload(data, lang=str(NETX_TOOL_LANG.get() or "zh"))
|
||||
return {"ok": True, "data": data if isinstance(data, dict) else {"raw": data}}
|
||||
except Exception as exc:
|
||||
return {"ok": False, "error": "netx_request_failed", "detail": str(exc)[:800]}
|
||||
|
||||
|
||||
def _http_json(method: str, path: str, *, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
base = _netx_base_url()
|
||||
url = f"{base}{path}"
|
||||
merged: dict[str, Any] = dict(_netx_lang_query_params())
|
||||
if params:
|
||||
merged.update(params)
|
||||
try:
|
||||
# Do not inherit system proxy settings for local netx calls.
|
||||
with httpx.Client(timeout=45.0, trust_env=False) as client:
|
||||
resp = client.request(method, url, params=merged or None, headers=_netx_headers())
|
||||
text = resp.text
|
||||
if not resp.is_success:
|
||||
return {"ok": False, "error": f"netx_http_{resp.status_code}", "detail": text[:800]}
|
||||
data = resp.json() if text else {}
|
||||
if isinstance(data, dict):
|
||||
data = _localize_netx_payload(data, lang=str(NETX_TOOL_LANG.get() or "zh"))
|
||||
return {"ok": True, "data": data if isinstance(data, dict) else {"raw": data}}
|
||||
except Exception as exc:
|
||||
return {"ok": False, "error": "netx_request_failed", "detail": str(exc)[:800]}
|
||||
|
||||
|
||||
def _resolve_ume_anchor() -> dict[str, Any]:
|
||||
"""Resolve current UME alarm anchor from netx sync status."""
|
||||
r = _http_json("GET", "/v1/ume/sync/status", params={"page": 1, "page_size": 20})
|
||||
|
|
@ -259,584 +154,3 @@ def ops_netx_system_context_extension(*, lang: str = "zh") -> str:
|
|||
with _OPS_NETX_SYS_CTX_LOCK:
|
||||
_OPS_NETX_SYS_CTX_CACHE[lk] = (store_ts, text)
|
||||
return text
|
||||
|
||||
|
||||
def netx_query_ume_alarms_tool() -> ToolSpec:
|
||||
"""Paginated UME current alarms from netx."""
|
||||
|
||||
def handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||
# Guardrail: avoid accidental full scans by endless paging.
|
||||
page = max(1, int(args.get("page") or 1))
|
||||
if page > 2:
|
||||
page = 2
|
||||
page_size = min(500, max(1, int(args.get("page_size") or 50)))
|
||||
params: dict[str, Any] = {"page": page, "page_size": page_size}
|
||||
if str(args.get("severity") or "").strip():
|
||||
params["severity"] = str(args.get("severity")).strip()
|
||||
ne_name = str(args.get("ne_name") or "").strip()
|
||||
keyword = str(args.get("keyword") or "").strip()
|
||||
if keyword:
|
||||
params["keyword"] = keyword
|
||||
elif ne_name:
|
||||
params["keyword"] = ne_name
|
||||
if str(args.get("ne_id") or "").strip():
|
||||
params["ne_id"] = str(args.get("ne_id")).strip()
|
||||
return _http_json("GET", "/v1/ume/alarms", params=params)
|
||||
|
||||
return ToolSpec(
|
||||
name="netx_query_ume_alarms",
|
||||
description=(
|
||||
"读取 netx UME 当前告警明细(实时表);每条含 host_name(网元主展示键,同步时已写入告警表)。"
|
||||
"支持 severity/ne_id/keyword 与分页。"
|
||||
),
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"severity": {"type": "string"},
|
||||
"ne_id": {"type": "string"},
|
||||
"ne_name": {"type": "string", "description": "兼容参数,会映射到 keyword"},
|
||||
"keyword": {"type": "string", "description": "按网元名/标签/IP/对象名等关键字检索"},
|
||||
"page": {"type": "integer", "minimum": 1, "default": 1},
|
||||
"page_size": {"type": "integer", "minimum": 1, "maximum": 500, "default": 50},
|
||||
},
|
||||
"required": [],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
handler=handler,
|
||||
tags=frozenset({"netx", "ops", "ume", "alarms", "read_only"}),
|
||||
risk_level="low",
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
|
||||
def netx_aggregate_ume_alarms_tool() -> ToolSpec:
|
||||
"""Aggregate UME current alarms from netx."""
|
||||
|
||||
def handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = args
|
||||
return _http_json("GET", "/v1/ume/alarms/aggregate", params=None)
|
||||
|
||||
return ToolSpec(
|
||||
name="netx_aggregate_ume_alarms",
|
||||
description="读取 netx UME 当前告警聚合(by_severity/by_ne)。",
|
||||
parameters={"type": "object", "properties": {}, "required": [], "additionalProperties": False},
|
||||
handler=handler,
|
||||
tags=frozenset({"netx", "ops", "ume", "alarms", "aggregate", "read_only"}),
|
||||
risk_level="low",
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
|
||||
def netx_run_ume_diagnostics_tool() -> ToolSpec:
|
||||
"""Diagnostics summary for UME current alarms."""
|
||||
|
||||
def handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = args
|
||||
return _http_json("GET", "/v1/ume/diagnostics", params=None)
|
||||
|
||||
return ToolSpec(
|
||||
name="netx_run_ume_diagnostics",
|
||||
description="读取 netx UME 告警诊断摘要(级别分布、Top 告警码、Top 网元、协议归类)。",
|
||||
parameters={"type": "object", "properties": {}, "required": [], "additionalProperties": False},
|
||||
handler=handler,
|
||||
tags=frozenset({"netx", "ops", "ume", "diagnostics", "read_only"}),
|
||||
risk_level="low",
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
|
||||
def netx_query_ume_ne_inventory_tool() -> ToolSpec:
|
||||
"""Paged UME NE inventory synced in netx (PostgreSQL-backed)."""
|
||||
|
||||
def handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||
page = max(1, int(args.get("page") or 1))
|
||||
page_size = min(500, max(1, int(args.get("page_size") or 50)))
|
||||
params: dict[str, Any] = {"page": page, "page_size": page_size}
|
||||
if str(args.get("keyword") or "").strip():
|
||||
params["keyword"] = str(args.get("keyword")).strip()
|
||||
return _http_json("GET", "/v1/ume/inventory/ne", params=params)
|
||||
|
||||
return ToolSpec(
|
||||
name="netx_query_ume_ne_inventory",
|
||||
description=(
|
||||
"查询 netx 已同步的 UME 网元清单(读 /v1/ume/inventory/ne,与 netx Web「网元清单」同源)。"
|
||||
"keyword 可选:匹配 ne_id / ne_name / user_label / ip_address / host_name(主机名)包含。"
|
||||
"返回 total、page、page_size、items(含 host_name、在线状态、地址、类型等)。"
|
||||
),
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"keyword": {"type": "string", "description": "关键字过滤(可选)"},
|
||||
"page": {"type": "integer", "minimum": 1, "default": 1},
|
||||
"page_size": {"type": "integer", "minimum": 1, "maximum": 500, "default": 50},
|
||||
},
|
||||
"required": [],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
handler=handler,
|
||||
tags=frozenset({"netx", "ops", "ume", "inventory", "ne", "read_only"}),
|
||||
risk_level="low",
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
|
||||
def netx_get_ume_ne_tool() -> ToolSpec:
|
||||
"""Single UME NE detail by ne_id from netx."""
|
||||
|
||||
def handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||
from urllib.parse import quote
|
||||
|
||||
ne_id = str(args.get("ne_id") or "").strip()
|
||||
if not ne_id:
|
||||
return {"ok": False, "error": "ne_id_required", "error_code": "ne_id_required"}
|
||||
safe = quote(ne_id, safe="")
|
||||
return _http_json("GET", f"/v1/ume/inventory/ne/{safe}", params=None)
|
||||
|
||||
return ToolSpec(
|
||||
name="netx_get_ume_ne",
|
||||
description=(
|
||||
"按网元 UUID(ne_id)读取 netx 中单条 UME 网元详情(GET /v1/ume/inventory/ne/{ne_id})。"
|
||||
"含 vendor、source_type、raw_json 等;404 时上游返回 ume_ne_not_found。"
|
||||
),
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ne_id": {"type": "string", "description": "网元 UUID(与清单中 ne_id 一致)"},
|
||||
},
|
||||
"required": ["ne_id"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
handler=handler,
|
||||
tags=frozenset({"netx", "ops", "ume", "inventory", "ne", "read_only"}),
|
||||
risk_level="low",
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
|
||||
def netx_query_ume_alarms_raw_tool() -> ToolSpec:
|
||||
"""Power query UME current alarms with full alarm+NE fields."""
|
||||
|
||||
presets: dict[str, list[str]] = {
|
||||
"brief": [
|
||||
"alarm_alarm_key",
|
||||
"alarm_host_name",
|
||||
"alarm_perceived_severity",
|
||||
"alarm_event_type",
|
||||
"alarm_last_seen_at",
|
||||
"ne_host_name",
|
||||
"ne_user_label",
|
||||
"ne_ne_name",
|
||||
"ne_ip_address",
|
||||
"ne_exists",
|
||||
],
|
||||
"evidence": [
|
||||
"alarm_alarm_key",
|
||||
"alarm_host_name",
|
||||
"alarm_object_name",
|
||||
"alarm_event_type",
|
||||
"alarm_native_probable_cause",
|
||||
"alarm_perceived_severity",
|
||||
"alarm_is_cleared",
|
||||
"alarm_time_created",
|
||||
"alarm_last_seen_at",
|
||||
"ne_host_name",
|
||||
"ne_user_label",
|
||||
"ne_ne_name",
|
||||
"ne_ip_address",
|
||||
"ne_connection_status",
|
||||
"ne_exists",
|
||||
],
|
||||
"ne_debug": [
|
||||
"alarm_alarm_key",
|
||||
"alarm_ne_id",
|
||||
"alarm_perceived_severity",
|
||||
"alarm_last_seen_at",
|
||||
"ne_user_label",
|
||||
"ne_ne_name",
|
||||
"ne_ip_address",
|
||||
"ne_ipv6_address",
|
||||
"ne_device_level",
|
||||
"ne_host_name",
|
||||
"ne_connection_status",
|
||||
"ne_admin_status",
|
||||
"ne_address_type",
|
||||
"ne_maintain_status",
|
||||
"ne_exists",
|
||||
],
|
||||
}
|
||||
|
||||
def handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||
page = max(1, int(args.get("page") or 1))
|
||||
page_size = min(500, max(1, int(args.get("page_size") or 50)))
|
||||
params: dict[str, Any] = {"page": page, "page_size": page_size}
|
||||
for k in ("severity", "is_cleared", "ne_id", "event_type", "keyword", "time_from", "time_to", "order_by", "order"):
|
||||
v = str(args.get(k) or "").strip()
|
||||
if v:
|
||||
params[k] = v
|
||||
sf = args.get("select_fields")
|
||||
fields: list[str] = []
|
||||
if isinstance(sf, list):
|
||||
fields = [str(x).strip() for x in sf if str(x).strip()]
|
||||
if not fields:
|
||||
preset = str(args.get("field_preset") or "").strip().lower()
|
||||
fields = list(presets.get(preset) or [])
|
||||
if fields:
|
||||
params["select_fields"] = ",".join(fields)
|
||||
return _http_json("GET", "/v1/ume/alarms/raw", params=params)
|
||||
|
||||
return ToolSpec(
|
||||
name="netx_query_ume_alarms_raw",
|
||||
description=(
|
||||
"自由查询 netx UME 当前告警原始视图,返回 alarm_* + ne_* 全字段。"
|
||||
"可按 severity/is_cleared/ne_id/event_type/keyword/time_from/time_to 过滤,支持排序分页。"
|
||||
"select_fields 可按需指定返回字段,降低输出体积。"
|
||||
"field_preset 可快速选用默认字段集(brief/evidence/ne_debug)。"
|
||||
"建议先调用 netx_list_ume_alarm_fields 查看可用字段。"
|
||||
),
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"severity": {"type": "string"},
|
||||
"is_cleared": {"type": "string"},
|
||||
"ne_id": {"type": "string"},
|
||||
"event_type": {"type": "string"},
|
||||
"keyword": {"type": "string"},
|
||||
"time_from": {"type": "string", "description": "ISO8601 时间下界(按 last_seen_at)"},
|
||||
"time_to": {"type": "string", "description": "ISO8601 时间上界(按 last_seen_at)"},
|
||||
"order_by": {"type": "string", "enum": ["last_seen_at", "time_created", "perceived_severity", "event_type", "ne_id"]},
|
||||
"order": {"type": "string", "enum": ["asc", "desc"]},
|
||||
"select_fields": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "可选返回字段,如 alarm_alarm_key/ne_user_label/ne_exists",
|
||||
},
|
||||
"field_preset": {
|
||||
"type": "string",
|
||||
"enum": ["brief", "evidence", "ne_debug"],
|
||||
"description": "字段集预设;当未传 select_fields 时生效",
|
||||
},
|
||||
"page": {"type": "integer", "minimum": 1, "default": 1},
|
||||
"page_size": {"type": "integer", "minimum": 1, "maximum": 500, "default": 50},
|
||||
},
|
||||
"required": [],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
handler=handler,
|
||||
tags=frozenset({"netx", "ops", "ume", "alarms", "power_query", "read_only"}),
|
||||
risk_level="low",
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
|
||||
def netx_list_ume_alarm_fields_tool() -> ToolSpec:
|
||||
"""List field names for UME raw alarm query."""
|
||||
|
||||
def handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = args
|
||||
return _http_json("GET", "/v1/ume/alarms/fields", params=None)
|
||||
|
||||
return ToolSpec(
|
||||
name="netx_list_ume_alarm_fields",
|
||||
description="列出 UME 当前告警 raw 查询可用字段(alarm_fields/ne_fields/order_by_allowed)。",
|
||||
parameters={"type": "object", "properties": {}, "required": [], "additionalProperties": False},
|
||||
handler=handler,
|
||||
tags=frozenset({"netx", "ops", "ume", "alarms", "schema", "read_only"}),
|
||||
risk_level="low",
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
|
||||
def netx_sql_query_ume_tool() -> ToolSpec:
|
||||
"""Execute read-only SQL on UME tables in netx."""
|
||||
|
||||
def handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||
sql = str(args.get("sql") or "").strip()
|
||||
limit = max(1, min(2000, int(args.get("limit") or 200)))
|
||||
statement_timeout_ms = max(0, min(30000, int(args.get("statement_timeout_ms") or 0)))
|
||||
if not sql:
|
||||
return {"ok": False, "error": "sql_required"}
|
||||
base = _netx_base_url()
|
||||
url = f"{base}/v1/sql/ume_query"
|
||||
try:
|
||||
# Do not inherit system proxy settings for local netx calls.
|
||||
with httpx.Client(timeout=60.0, trust_env=False) as client:
|
||||
resp = client.post(
|
||||
url,
|
||||
json={"sql": sql, "limit": limit, "statement_timeout_ms": statement_timeout_ms},
|
||||
headers=_netx_headers(),
|
||||
)
|
||||
text = resp.text
|
||||
if not resp.is_success:
|
||||
return {"ok": False, "error": f"netx_http_{resp.status_code}", "detail": text[:800]}
|
||||
data = resp.json() if text else {}
|
||||
return {"ok": True, "data": data if isinstance(data, dict) else {"raw": data}}
|
||||
except Exception as exc:
|
||||
return {"ok": False, "error": "netx_request_failed", "detail": str(exc)[:800]}
|
||||
|
||||
return ToolSpec(
|
||||
name="netx_sql_query_ume",
|
||||
description=(
|
||||
"在 netx 上执行 UME 只读 SQL(服务端强制 SELECT-only、单语句、限制表为 "
|
||||
"ume_alarms_current/ume_inventory_ne,并强制 limit)。"
|
||||
"推荐默认模板:设置 statement_timeout_ms=8000,且 SQL 带时间窗过滤(last_seen_at >= now() - interval '30 minutes')。"
|
||||
),
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sql": {"type": "string", "description": "只读 SELECT SQL;仅允许 UME 当前告警与网元表"},
|
||||
"limit": {"type": "integer", "minimum": 1, "maximum": 2000, "default": 200},
|
||||
"statement_timeout_ms": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 30000,
|
||||
"default": 0,
|
||||
"description": "可选查询超时(ms);0 表示使用数据库默认超时",
|
||||
},
|
||||
},
|
||||
"required": ["sql"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
handler=handler,
|
||||
tags=frozenset({"netx", "ops", "ume", "sql", "power_query", "read_only"}),
|
||||
risk_level="low",
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
|
||||
def netx_aggregate_ume_alarms_raw_tool() -> ToolSpec:
|
||||
"""Dynamic aggregation on UME raw fields."""
|
||||
|
||||
def handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||
params: dict[str, Any] = {}
|
||||
for k in (
|
||||
"group_by",
|
||||
"group_by2",
|
||||
"severity",
|
||||
"is_cleared",
|
||||
"ne_id",
|
||||
"event_type",
|
||||
"keyword",
|
||||
"time_from",
|
||||
"time_to",
|
||||
"limit",
|
||||
):
|
||||
v = args.get(k)
|
||||
if v is None:
|
||||
continue
|
||||
sv = str(v).strip()
|
||||
if sv:
|
||||
params[k] = sv
|
||||
return _http_json("GET", "/v1/ume/alarms/aggregate/raw", params=params)
|
||||
|
||||
return ToolSpec(
|
||||
name="netx_aggregate_ume_alarms_raw",
|
||||
description=(
|
||||
"按 UME raw 字段做动态聚合(group_by/group_by2),支持与 raw 同口径过滤条件。"
|
||||
"group_by 需使用 alarm_*/ne_* 字段,建议先 netx_list_ume_alarm_fields。"
|
||||
),
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"group_by": {
|
||||
"type": "string",
|
||||
"enum": _UME_RAW_GROUP_FIELDS,
|
||||
"description": "主分组字段(网元主键优先 alarm_host_name 或 ne_host_name;勿用 alarm_ne_id/ne_ne_id)",
|
||||
},
|
||||
"group_by2": {"type": "string", "enum": _UME_RAW_GROUP_FIELDS, "description": "可选第二分组字段"},
|
||||
"severity": {"type": "string"},
|
||||
"is_cleared": {"type": "string"},
|
||||
"ne_id": {"type": "string"},
|
||||
"event_type": {"type": "string"},
|
||||
"keyword": {"type": "string"},
|
||||
"time_from": {"type": "string"},
|
||||
"time_to": {"type": "string"},
|
||||
"limit": {"type": "integer", "minimum": 1, "maximum": 2000, "default": 200},
|
||||
},
|
||||
"required": ["group_by"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
handler=handler,
|
||||
tags=frozenset({"netx", "ops", "ume", "alarms", "aggregate", "power_query", "read_only"}),
|
||||
risk_level="low",
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
|
||||
def netx_list_managed_ne_tool() -> ToolSpec:
|
||||
"""List netx managed NEs (inventory for CLI login targets)."""
|
||||
|
||||
def handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||
page = max(1, int(args.get("page") or 1))
|
||||
page_size = min(500, max(1, int(args.get("page_size") or 50)))
|
||||
params: dict[str, Any] = {"page": page, "page_size": page_size}
|
||||
if str(args.get("keyword") or "").strip():
|
||||
params["keyword"] = str(args.get("keyword")).strip()
|
||||
if str(args.get("vendor") or "").strip():
|
||||
params["vendor"] = str(args.get("vendor")).strip()
|
||||
if str(args.get("connect_status") or "").strip():
|
||||
params["connect_status"] = str(args.get("connect_status")).strip()
|
||||
return _http_json("GET", "/v1/managed-ne", params=params)
|
||||
|
||||
return ToolSpec(
|
||||
name="netx_list_managed_ne",
|
||||
description=(
|
||||
"列出 netx「网元管理」中已纳管的设备(GET /v1/managed-ne)。"
|
||||
"返回 id、name、ip、vendor、device_type、connect_status 等(不含密码)。"
|
||||
"必须至少提供 keyword / vendor / connect_status 之一;keyword 可匹配名称/IP/用户名/标签,且至少 2 个字符。"
|
||||
"登录查配置前先用本工具定位 ne_id,再 netx_get_managed_ne / netx_exec_managed_ne。"
|
||||
),
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"keyword": {"type": "string", "description": "名称/IP/用户名/标签包含(可选,若提供至少 2 个字符)"},
|
||||
"vendor": {"type": "string", "description": "厂商过滤(可选)"},
|
||||
"connect_status": {
|
||||
"type": "string",
|
||||
"enum": ["unknown", "testing", "pass", "fail"],
|
||||
"description": "连通性状态过滤(可选)",
|
||||
},
|
||||
"page": {"type": "integer", "minimum": 1, "default": 1},
|
||||
"page_size": {"type": "integer", "minimum": 1, "maximum": 100, "default": 20},
|
||||
},
|
||||
"required": [],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
handler=handler,
|
||||
tags=frozenset({"netx", "ops", "managed_ne", "inventory", "read_only"}),
|
||||
risk_level="low",
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
|
||||
def netx_get_managed_ne_tool() -> ToolSpec:
|
||||
"""Single managed NE metadata from netx."""
|
||||
|
||||
def handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||
ne_id = str(args.get("ne_id") or "").strip()
|
||||
if not ne_id:
|
||||
return {"ok": False, "error": "ne_id_required", "error_code": "ne_id_required"}
|
||||
return _http_json("GET", f"/v1/managed-ne/{ne_id}", params=None)
|
||||
|
||||
return ToolSpec(
|
||||
name="netx_get_managed_ne",
|
||||
description=(
|
||||
"读取 netx 单条纳管网元详情(GET /v1/managed-ne/{ne_id})。"
|
||||
"含 connect_status、connect_message、connect_detail(连通测试日志)、跳板 hop_* 配置摘要。"
|
||||
),
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ne_id": {"type": "string", "description": "网元 UUID(列表 items[].id)"},
|
||||
},
|
||||
"required": ["ne_id"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
handler=handler,
|
||||
tags=frozenset({"netx", "ops", "managed_ne", "read_only"}),
|
||||
risk_level="low",
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
|
||||
def _netx_exec_max_commands() -> int:
|
||||
"""Mirror netx NETX_NE_EXEC_MAX_COMMANDS (default 5, hard cap 50)."""
|
||||
try:
|
||||
raw = int(os.getenv("NETX_NE_EXEC_MAX_COMMANDS") or 5)
|
||||
except ValueError:
|
||||
raw = 5
|
||||
return max(1, min(50, raw))
|
||||
|
||||
|
||||
def netx_exec_managed_ne_tool() -> ToolSpec:
|
||||
"""Run read-only CLI on a managed NE via netx."""
|
||||
max_cmds = _netx_exec_max_commands()
|
||||
|
||||
def handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||
ne_id = str(args.get("ne_id") or "").strip()
|
||||
if not ne_id:
|
||||
return {"ok": False, "error": "ne_id_required", "error_code": "ne_id_required"}
|
||||
raw_cmds = args.get("commands")
|
||||
if not isinstance(raw_cmds, list) or not raw_cmds:
|
||||
return {"ok": False, "error": "commands_required", "error_code": "commands_required"}
|
||||
commands = [str(c).strip() for c in raw_cmds if str(c).strip()]
|
||||
if not commands:
|
||||
return {"ok": False, "error": "commands_required", "error_code": "commands_required"}
|
||||
if len(commands) > _netx_exec_max_commands():
|
||||
return {"ok": False, "error": "too_many_commands", "error_code": "too_many_commands"}
|
||||
body: dict[str, Any] = {"ne_id": ne_id, "commands": commands}
|
||||
rts = args.get("read_timeout_sec")
|
||||
if rts is not None:
|
||||
body["read_timeout_sec"] = int(rts)
|
||||
out = _http_post_json("/v1/managed-ne/exec", body, timeout=300.0)
|
||||
if not out.get("ok"):
|
||||
return out
|
||||
data = out.get("data") or {}
|
||||
if isinstance(data, dict) and data.get("ok") is False:
|
||||
return {"ok": False, "data": data, "error": str(data.get("error") or "exec_failed")}
|
||||
return {"ok": True, "data": data}
|
||||
|
||||
return ToolSpec(
|
||||
name="netx_exec_managed_ne",
|
||||
description=(
|
||||
"经 netx 登录「网元管理」中的设备并执行只读 CLI(POST /v1/managed-ne/exec)。"
|
||||
"每条命令须以 show / display / ping / ping6 / traceroute / tracert / trace / trace6 开头;"
|
||||
f"允许白名单管道过滤;禁止分号及改配置类命令;单次最多 {max_cmds} 条"
|
||||
"(NETX_NE_EXEC_MAX_COMMANDS,硬上限 50);默认读超时 60s。"
|
||||
"返回合并输出(含命令回显);失败时含 error/detail。"
|
||||
"先 netx_list_managed_ne 解析 ne_id;若 connect_status 非 pass 可先 netx_get_managed_ne 看 connect_detail。"
|
||||
),
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ne_id": {"type": "string", "description": "纳管网元 UUID"},
|
||||
"commands": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"minItems": 1,
|
||||
"maxItems": max_cmds,
|
||||
"description": "只读 CLI 列表,如 show version、display interface brief",
|
||||
},
|
||||
"read_timeout_sec": {
|
||||
"type": "integer",
|
||||
"minimum": 10,
|
||||
"maximum": 120,
|
||||
"description": "单条命令 Netmiko 读超时(秒),默认 60",
|
||||
},
|
||||
},
|
||||
"required": ["ne_id", "commands"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
handler=handler,
|
||||
tags=frozenset({"netx", "ops", "managed_ne", "cli", "exec"}),
|
||||
risk_level="medium",
|
||||
read_only=False,
|
||||
)
|
||||
|
||||
|
||||
__all__: list[str] = []
|
||||
|
||||
_LEGACY_EXPORTS = [
|
||||
"netx_query_ume_alarms_tool",
|
||||
"netx_aggregate_ume_alarms_tool",
|
||||
"netx_run_ume_diagnostics_tool",
|
||||
"netx_query_ume_ne_inventory_tool",
|
||||
"netx_get_ume_ne_tool",
|
||||
"netx_query_ume_alarms_raw_tool",
|
||||
"netx_aggregate_ume_alarms_raw_tool",
|
||||
"netx_list_ume_alarm_fields_tool",
|
||||
"netx_sql_query_ume_tool",
|
||||
"netx_list_managed_ne_tool",
|
||||
"netx_get_managed_ne_tool",
|
||||
"netx_exec_managed_ne_tool",
|
||||
]
|
||||
|
||||
|
||||
def _apply_legacy_exports() -> None:
|
||||
import os as _os
|
||||
|
||||
if str(_os.getenv("OCLAW_NETX_BUILTIN_TOOLS") or "0").strip().lower() in {"0", "false", "no", "off"}:
|
||||
return
|
||||
globals()["__all__"] = list(_LEGACY_EXPORTS)
|
||||
|
||||
|
||||
_apply_legacy_exports()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""Always-on ops tool: query UME alarms → xlsx → optional channel deliverable in one call.
|
||||
|
||||
Independent of ``OCLAW_NETX_BUILTIN_TOOLS`` (MCP remains the primary query path; this
|
||||
collapses the WhatsApp “export Excel” loop that previously needed 3–12 tool calls).
|
||||
Uses shared ``netx_http`` (same REST base as MCP). MCP remains the primary interactive
|
||||
query path; this collapses the WhatsApp “export Excel” loop into one tool call.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -9,7 +9,7 @@ from __future__ import annotations
|
|||
from typing import Any
|
||||
|
||||
from runtime.tools.base import ToolSpec
|
||||
from runtime.tools.experts.network_ops import netx_tools as nt
|
||||
from runtime.tools.experts.network_ops import netx_http as nt
|
||||
from runtime.tools.public.write_xlsx_tool import write_xlsx_tool
|
||||
|
||||
_LIST_FIELDS = [
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from runtime.skills import discover_workspace_skill_manifests
|
||||
from runtime.tools.base import ToolSpec
|
||||
|
||||
|
||||
def materialize_executable_skill_tools(*, store: Any | None = None) -> list[ToolSpec]:
|
||||
"""Convert installed skill manifests with runtime into ToolSpec.
|
||||
|
||||
Tool name equals skill name so the model can call it directly.
|
||||
"""
|
||||
_ = store
|
||||
out: list[ToolSpec] = []
|
||||
for m in discover_workspace_skill_manifests():
|
||||
rt = dict(m.runtime or {}) if isinstance(m.runtime, dict) else {}
|
||||
if not rt:
|
||||
continue
|
||||
tp = str(rt.get("type") or "").strip().lower()
|
||||
entry = str(rt.get("entry") or "").strip()
|
||||
if not tp or not entry:
|
||||
continue
|
||||
schema = rt.get("schema") if isinstance(rt.get("schema"), dict) else {"type": "object", "additionalProperties": True}
|
||||
name = str(m.name or "").strip()
|
||||
if not name:
|
||||
continue
|
||||
|
||||
def _handler(args: dict[str, Any], *, _manifest=m, _rt=rt) -> dict[str, Any]:
|
||||
from runtime.tools.skills_runtime.subprocess_exec import run_skill_runtime_entry
|
||||
|
||||
source_meta = {}
|
||||
if isinstance(getattr(_manifest, "metadata_oclaw", None), dict):
|
||||
source_raw = _manifest.metadata_oclaw.get("source")
|
||||
if isinstance(source_raw, dict):
|
||||
source_meta = {
|
||||
"provider": str(source_raw.get("provider") or ""),
|
||||
"version": str(source_raw.get("version") or ""),
|
||||
"kind": str(source_raw.get("kind") or ""),
|
||||
}
|
||||
return run_skill_runtime_entry(
|
||||
skill_name=str(_manifest.name or ""),
|
||||
skill_dir=str(_manifest.skill_dir or ""),
|
||||
runtime={**dict(_rt), "__source_meta": source_meta},
|
||||
args=dict(args or {}),
|
||||
)
|
||||
|
||||
out.append(
|
||||
ToolSpec(
|
||||
name=name,
|
||||
description=str(m.description or ""),
|
||||
parameters=dict(schema),
|
||||
handler=_handler,
|
||||
tags=frozenset({"skill", "oclaw", "runtime"}),
|
||||
risk_level="high",
|
||||
timeout_s=60.0,
|
||||
)
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
__all__ = ["materialize_executable_skill_tools"]
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue