mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 04:40:45 +08:00
Cut WA alarm re-query loops, auto-deliver xlsx, and add ops schedule templates.
TTL-cache UME alarm/diagnostics MCP calls, mark write_xlsx deliverable on WhatsApp unless explicitly false, and expose built-in recipe_template_id playbooks for daily tally / critical xlsx / license checks. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
7373e516f0
commit
d2693d1c63
9 changed files with 431 additions and 5 deletions
|
|
@ -542,6 +542,51 @@ def _resolve_creator_specialist(ctx: ToolExecutionContext) -> str:
|
|||
return "generalist"
|
||||
|
||||
|
||||
def _truthy_flag(raw: Any) -> bool | None:
|
||||
"""Return True/False when the flag is explicit; None when unset."""
|
||||
if raw is None:
|
||||
return None
|
||||
if isinstance(raw, bool):
|
||||
return raw
|
||||
text = str(raw).strip().lower()
|
||||
if not text:
|
||||
return None
|
||||
if text in {"1", "true", "yes", "on"}:
|
||||
return True
|
||||
if text in {"0", "false", "no", "off"}:
|
||||
return False
|
||||
return None
|
||||
|
||||
|
||||
def _maybe_auto_mark_xlsx_deliverable(
|
||||
*,
|
||||
tool_name: str,
|
||||
tool_args: dict[str, Any],
|
||||
result: dict[str, Any],
|
||||
inbound_metadata: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""On WhatsApp/WeChat, auto-mark successful write_xlsx as deliverable unless explicitly false."""
|
||||
if not isinstance(result, dict) or result.get("ok") is False:
|
||||
return result
|
||||
if str(tool_name or "").strip() != "write_xlsx":
|
||||
return result
|
||||
channel = str(inbound_metadata.get("channel") or "").strip().lower()
|
||||
if channel not in {"whatsapp", "wechat", "wecom"}:
|
||||
return result
|
||||
explicit = _truthy_flag(tool_args.get("deliverable")) if isinstance(tool_args, dict) else None
|
||||
if explicit is False:
|
||||
return result
|
||||
if result.get("deliverable") is True:
|
||||
return result
|
||||
out = dict(result)
|
||||
out["deliverable"] = True
|
||||
out["auto_deliverable"] = True
|
||||
out["hint"] = (
|
||||
"Excel auto-marked deliverable for channel outbound — file will attach on WhatsApp/WeChat."
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ToolExecutionConfig:
|
||||
max_workers: int = 8
|
||||
|
|
@ -788,6 +833,12 @@ class ToolExecutor:
|
|||
else:
|
||||
result = _call()
|
||||
out = normalize_tool_result(result)
|
||||
out = _maybe_auto_mark_xlsx_deliverable(
|
||||
tool_name=str(tc.name or ""),
|
||||
tool_args=tool_args if isinstance(tool_args, dict) else {},
|
||||
result=out,
|
||||
inbound_metadata=ctx.inbound_metadata if isinstance(ctx.inbound_metadata, dict) else {},
|
||||
)
|
||||
dur_ms = int((time.perf_counter() - t0) * 1000)
|
||||
try:
|
||||
_tool_exec_log.info(
|
||||
|
|
|
|||
|
|
@ -324,9 +324,113 @@ def load_recipe_from_job(job: Any) -> dict[str, Any]:
|
|||
return normalize_recipe(data if isinstance(data, dict) else {})
|
||||
|
||||
|
||||
# Built-in ops playbooks for WhatsApp field schedules (English-first).
|
||||
_OPS_RECIPE_TEMPLATE_ALIASES: dict[str, str] = {
|
||||
"alarm_tally": "ume_alarm_tally_daily",
|
||||
"alarm_tally_daily": "ume_alarm_tally_daily",
|
||||
"critical_xlsx": "ume_critical_xlsx_daily",
|
||||
"critical_xlsx_daily": "ume_critical_xlsx_daily",
|
||||
"license_check": "ne_license_check_weekly",
|
||||
"license_weekly": "ne_license_check_weekly",
|
||||
}
|
||||
|
||||
OPS_RECIPE_TEMPLATES: dict[str, dict[str, Any]] = {
|
||||
"ume_alarm_tally_daily": {
|
||||
"version": 1,
|
||||
"goal": "Post daily UME open-alarm tally to the ops WhatsApp group",
|
||||
"steps": [
|
||||
"Call runUmeDiagnostics or aggregateUmeAlarms for current open alarms",
|
||||
"Summarize by_severity, top NEs, and freshness in concise English",
|
||||
"Post a short WhatsApp update (skip xlsx unless counts are very large)",
|
||||
],
|
||||
"constraints": [
|
||||
"Prefer English for WhatsApp field ops",
|
||||
"Do not re-list CLI targets/inventory unless required",
|
||||
"Do not blind-retry identical failing tool calls",
|
||||
],
|
||||
"success_criteria": [
|
||||
"Group receives a severity tally that includes freshness",
|
||||
],
|
||||
"output": {"need_attachments": False},
|
||||
},
|
||||
"ume_critical_xlsx_daily": {
|
||||
"version": 1,
|
||||
"goal": "Send daily critical UME alarm Excel to the ops WhatsApp group",
|
||||
"steps": [
|
||||
"Call ume_alarm_xlsx_report(mode=aggregate_by_host, severity=critical, deliverable=true)",
|
||||
"If that tool is unavailable: aggregateUmeAlarms then write_xlsx(deliverable=true)",
|
||||
"Confirm the file is marked deliverable and summarize top hosts in English",
|
||||
],
|
||||
"constraints": [
|
||||
"Prefer ume_alarm_xlsx_report over multi-step query+xlsx",
|
||||
"Never claim a file was sent without deliverable marking",
|
||||
"Prefer English for WhatsApp field ops",
|
||||
],
|
||||
"success_criteria": [
|
||||
"WhatsApp group receives an xlsx attachment of critical alarms by host",
|
||||
],
|
||||
"output": {"need_attachments": True},
|
||||
},
|
||||
"ne_license_check_weekly": {
|
||||
"version": 1,
|
||||
"goal": "Weekly NE license/capacity check summary for ops WhatsApp",
|
||||
"steps": [
|
||||
"Resolve target NEs via listManagedNe or known constants (avoid repeated listCliTargets)",
|
||||
"Run execManagedNe license/capacity show commands with read_timeout_sec>=60",
|
||||
"Summarize near-limit or failed NEs in English; attach xlsx only if many rows",
|
||||
],
|
||||
"constraints": [
|
||||
"Prefer English for WhatsApp field ops",
|
||||
"On timeout/unreachable, classify failure and do not blind-retry identical args",
|
||||
"Keep the group update short and actionable",
|
||||
],
|
||||
"success_criteria": [
|
||||
"Group receives a license/capacity status summary for the target set",
|
||||
],
|
||||
"output": {"need_attachments": False},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _normalize_template_id(template_id: str) -> str:
|
||||
tid = str(template_id or "").strip().lower().replace("-", "_")
|
||||
return _OPS_RECIPE_TEMPLATE_ALIASES.get(tid, tid)
|
||||
|
||||
|
||||
def resolve_ops_recipe_template(template_id: str) -> dict[str, Any] | None:
|
||||
"""Return a normalized recipe for a built-in ops template id, or None."""
|
||||
tid = _normalize_template_id(template_id)
|
||||
raw = OPS_RECIPE_TEMPLATES.get(tid)
|
||||
if not raw:
|
||||
return None
|
||||
recipe = normalize_recipe(raw)
|
||||
src = dict(recipe.get("source") or {})
|
||||
src["template_id"] = tid
|
||||
recipe["source"] = src
|
||||
return recipe
|
||||
|
||||
|
||||
def list_ops_recipe_templates() -> list[dict[str, Any]]:
|
||||
"""List built-in ops recipe templates (id + goal + attachment hint)."""
|
||||
items: list[dict[str, Any]] = []
|
||||
for tid, raw in OPS_RECIPE_TEMPLATES.items():
|
||||
recipe = normalize_recipe(raw)
|
||||
items.append(
|
||||
{
|
||||
"id": tid,
|
||||
"goal": str(recipe.get("goal") or ""),
|
||||
"need_attachments": bool((recipe.get("output") or {}).get("need_attachments")),
|
||||
"aliases": sorted(k for k, v in _OPS_RECIPE_TEMPLATE_ALIASES.items() if v == tid),
|
||||
}
|
||||
)
|
||||
return items
|
||||
|
||||
|
||||
__all__ = [
|
||||
"COMPLEX_PROMPT_HINTS",
|
||||
"OPS_RECIPE_TEMPLATES",
|
||||
"compile_playbook_instruction",
|
||||
"list_ops_recipe_templates",
|
||||
"load_recipe_from_job",
|
||||
"looks_like_complex_schedule_prompt",
|
||||
"normalize_recipe",
|
||||
|
|
@ -336,4 +440,5 @@ __all__ = [
|
|||
"recipe_has_playbook",
|
||||
"recipe_is_empty",
|
||||
"recipe_missing_fields",
|
||||
"resolve_ops_recipe_template",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from runtime.scheduler.job_delete import (
|
|||
resolve_scheduled_jobs_by_id,
|
||||
)
|
||||
from runtime.scheduler.recipe import (
|
||||
list_ops_recipe_templates,
|
||||
looks_like_complex_schedule_prompt,
|
||||
normalize_recipe,
|
||||
parse_recipe_arg,
|
||||
|
|
@ -21,6 +22,7 @@ from runtime.scheduler.recipe import (
|
|||
prompt_summary_from_recipe,
|
||||
recipe_has_playbook,
|
||||
recipe_missing_fields,
|
||||
resolve_ops_recipe_template,
|
||||
)
|
||||
from runtime.scheduler.service import run_scheduled_job_now
|
||||
from runtime.scheduler.system_timezone import default_system_timezone
|
||||
|
|
@ -234,6 +236,18 @@ def schedule_create_tool() -> ToolSpec:
|
|||
prompt_text = str(args.get("prompt_text") or "").strip()
|
||||
recipe_raw = parse_recipe_arg(args.get("recipe"))
|
||||
recipe = normalize_recipe(recipe_raw) if recipe_raw is not None else {}
|
||||
template_id = str(args.get("recipe_template_id") or args.get("template_id") or "").strip()
|
||||
if template_id and not recipe_has_playbook(recipe):
|
||||
tmpl = resolve_ops_recipe_template(template_id)
|
||||
if tmpl is None:
|
||||
return {
|
||||
"ok": False,
|
||||
"error": "unknown_recipe_template",
|
||||
"template_id": template_id,
|
||||
"available": list_ops_recipe_templates(),
|
||||
"hint": "Use a known recipe_template_id (e.g. ume_alarm_tally_daily) or pass a full recipe.",
|
||||
}
|
||||
recipe = tmpl
|
||||
if recipe_has_playbook(recipe):
|
||||
prompt_text = prompt_summary_from_recipe(recipe, fallback=prompt_text or name)
|
||||
if not prompt_text:
|
||||
|
|
@ -345,7 +359,9 @@ def schedule_create_tool() -> ToolSpec:
|
|||
"Simple reminders may use prompt_text only. "
|
||||
"Complex / multi-step / '刚才那件事做成定时' jobs MUST include a self-contained recipe "
|
||||
"(goal + >=2 concrete steps + success_criteria; no chat-dependent phrasing); "
|
||||
"call schedule_propose and get user confirmation first. "
|
||||
"or pass recipe_template_id for a built-in ops playbook "
|
||||
"(ume_alarm_tally_daily / ume_critical_xlsx_daily / ne_license_check_weekly). "
|
||||
"Call schedule_propose and get user confirmation first when drafting custom recipes. "
|
||||
"Delivery follows the current chat channel unless delivery is set explicitly."
|
||||
),
|
||||
parameters={
|
||||
|
|
@ -360,6 +376,14 @@ def schedule_create_tool() -> ToolSpec:
|
|||
"description": "Short summary / reminder intent. For playbooks, prefer recipe.goal.",
|
||||
},
|
||||
"recipe": _RECIPE_PARAM,
|
||||
"recipe_template_id": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"Built-in ops playbook id: ume_alarm_tally_daily, ume_critical_xlsx_daily, "
|
||||
"ne_license_check_weekly (aliases: alarm_tally, critical_xlsx, license_check). "
|
||||
"Used when recipe is omitted/incomplete."
|
||||
),
|
||||
},
|
||||
"schedule_kind": {
|
||||
"type": "string",
|
||||
"enum": ["cron", "once", "interval"],
|
||||
|
|
|
|||
|
|
@ -30,11 +30,17 @@ _MCP_TOOL_TIMEOUT_OVERRIDES_S: dict[str, float] = {
|
|||
"aggregateUmeAlarms": 60.0,
|
||||
}
|
||||
|
||||
# Read-mostly inventory/list tools that agents re-call in tight self-loops on WhatsApp.
|
||||
# Read-mostly inventory/list/alarm tools that agents re-call in tight self-loops on WhatsApp.
|
||||
_MCP_LIST_CACHE_TTL_S: dict[str, float] = {
|
||||
"listCliTargets": 120.0,
|
||||
"listManagedNe": 120.0,
|
||||
"queryUmeNeInventory": 90.0,
|
||||
# Short TTL: cut identical alarm/diagnostics re-query loops in the same turn.
|
||||
"queryUmeAlarms": 45.0,
|
||||
"queryUmeAlarmsRaw": 45.0,
|
||||
"aggregateUmeAlarms": 45.0,
|
||||
"aggregateUmeAlarmsRaw": 45.0,
|
||||
"runUmeDiagnostics": 60.0,
|
||||
}
|
||||
|
||||
_MCP_LIST_CACHE_LOCK = threading.Lock()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue