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:
oliver 2026-08-10 23:08:05 +08:00
parent 7373e516f0
commit d2693d1c63
9 changed files with 431 additions and 5 deletions

View file

@ -109,6 +109,35 @@ class AdminScheduledJobsApiTests(unittest.TestCase):
self.assertEqual(str(job.get("schedule_expr") or ""), "7200")
self.assertEqual(str(job.get("prompt_text") or ""), "Stand up hourly")
def test_recipe_templates_meta_and_create(self) -> None:
token = self._login()
headers = {"authorization": f"Bearer {token}"}
meta = self.client.get("/admin/api/scheduled-jobs/meta/recipe-templates", headers=headers)
self.assertEqual(meta.status_code, 200, meta.text)
items = meta.json().get("items") or []
self.assertTrue(any(str(x.get("id") or "") == "ume_alarm_tally_daily" for x in items))
create = self.client.post(
"/admin/api/scheduled-jobs",
headers=headers,
json={
"name": "Daily alarm tally",
"schedule_kind": "cron",
"schedule_expr": "0 8 * * *",
"recipe_template_id": "alarm_tally",
"delivery": {"channel": "whatsapp", "chat_id": "ops@g.us"},
"specialist": "ops",
},
)
self.assertEqual(create.status_code, 200, create.text)
body = create.json()
self.assertTrue(body.get("ok"), body)
job = body.get("job") or {}
self.assertEqual(str(job.get("lang") or ""), "en")
recipe = job.get("recipe") if isinstance(job.get("recipe"), dict) else {}
self.assertIn("UME", str(recipe.get("goal") or job.get("prompt_text") or ""))
self.assertEqual(str((recipe.get("source") or {}).get("template_id") or ""), "ume_alarm_tally_daily")
if __name__ == "__main__":
unittest.main()

View file

@ -108,6 +108,64 @@ class McpTimeoutAndCacheTests(unittest.TestCase):
self.assertEqual(second.get("data", {}).get("items", [])[0]["ne_id"], "1")
clear_list_cli_targets_cache()
def test_alarm_query_ttl_cache(self) -> None:
clear_list_cli_targets_cache()
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as td:
store = SqliteStore(str(Path(td) / "ops.sqlite"))
store.upsert_mcp_server(
server_id="netx",
source_type="github",
source_ref="local",
entry_command="python",
entry_args=["-m", "netx_mcp"],
enabled=True,
)
store.replace_mcp_server_tools(
server_id="netx",
tools=[
{
"tool_name": "queryUmeAlarms",
"description": "alarms",
"parameters": {"type": "object", "properties": {}},
},
{
"tool_name": "aggregateUmeAlarms",
"description": "agg",
"parameters": {"type": "object", "properties": {}},
},
{
"tool_name": "runUmeDiagnostics",
"description": "diag",
"parameters": {"type": "object", "properties": {}},
},
],
)
specs = {s.name: s for s in materialize_mcp_tools(store)}
calls = {"n": 0}
def fake_call_tool(self, tool_name, arguments=None): # type: ignore[no-untyped-def]
calls["n"] += 1
return {"ok": True, "data": {"tool": tool_name, "n": calls["n"]}}
with patch("runtime.tools.mcp.adapter.McpProcessRuntime.call_tool", fake_call_tool):
q = specs["mcp__netx__queryUmeAlarms"]
a = specs["mcp__netx__aggregateUmeAlarms"]
d = specs["mcp__netx__runUmeDiagnostics"]
q1 = q.handler({"severity": "critical"})
q2 = q.handler({"severity": "critical"})
a1 = a.handler({"top_ne": 10})
a2 = a.handler({"top_ne": 10})
d1 = d.handler({})
d2 = d.handler({})
self.assertEqual(calls["n"], 3)
self.assertFalse(q1.get("cache_hit"))
self.assertTrue(q2.get("cache_hit"))
self.assertTrue(a2.get("cache_hit"))
self.assertTrue(d2.get("cache_hit"))
self.assertEqual(a1.get("data", {}).get("tool"), "aggregateUmeAlarms")
self.assertEqual(d1.get("data", {}).get("tool"), "runUmeDiagnostics")
clear_list_cli_targets_cache()
class InvalidArgFormatTests(unittest.TestCase):
def test_format_includes_example(self) -> None:

View file

@ -7,12 +7,14 @@ from pathlib import Path
from runtime.scheduler.recipe import (
compile_playbook_instruction,
list_ops_recipe_templates,
looks_like_complex_schedule_prompt,
normalize_recipe,
preview_markdown,
prompt_summary_from_recipe,
recipe_has_playbook,
recipe_missing_fields,
resolve_ops_recipe_template,
)
from runtime.scheduler.turn_text import build_scheduled_turn_instruction, scheduled_turn_system_suffix
from runtime.tools.experts.productivity.schedule_tools import (
@ -67,6 +69,18 @@ class RecipeHelpersTests(unittest.TestCase):
self.assertTrue(looks_like_complex_schedule_prompt("继续刚才那个生成 PDF 流程"))
self.assertFalse(looks_like_complex_schedule_prompt("提醒喝水"))
def test_ops_recipe_templates(self) -> None:
items = list_ops_recipe_templates()
ids = {str(x.get("id") or "") for x in items}
self.assertIn("ume_alarm_tally_daily", ids)
self.assertIn("ume_critical_xlsx_daily", ids)
self.assertIn("ne_license_check_weekly", ids)
tmpl = resolve_ops_recipe_template("alarm_tally")
assert tmpl is not None
self.assertTrue(recipe_has_playbook(tmpl))
self.assertEqual((tmpl.get("source") or {}).get("template_id"), "ume_alarm_tally_daily")
self.assertIsNone(resolve_ops_recipe_template("nope"))
def test_turn_instruction_modes(self) -> None:
reminder = build_scheduled_turn_instruction(prompt_text="喝水", mode="scheduled", lang="zh")
self.assertIn("提醒意图", reminder)
@ -183,6 +197,25 @@ class ScheduleRecipeToolTests(unittest.TestCase):
self.assertFalse(out.get("ok"))
self.assertEqual(out.get("error"), "recipe_required")
def test_create_from_ops_recipe_template(self) -> None:
out = schedule_create_tool().handler(
{
"tenant_id": self.tenant_id,
"owner_user_id": self.user_id,
"name": "Daily tally",
"recipe_template_id": "ume_alarm_tally_daily",
"schedule_kind": "cron",
"schedule_expr": "0 8 * * *",
"lang": "en",
}
)
self.assertTrue(out.get("ok"), out)
job = out.get("job") or {}
recipe = job.get("recipe") or {}
self.assertTrue(recipe_has_playbook(recipe))
self.assertEqual((recipe.get("source") or {}).get("template_id"), "ume_alarm_tally_daily")
self.assertIn("UME", str(job.get("prompt_text") or ""))
def test_simple_reminder_still_works(self) -> None:
out = schedule_create_tool().handler(
{

View file

@ -0,0 +1,79 @@
from __future__ import annotations
from pathlib import Path
from runtime.chat.tool_runtime import ToolExecutionContext, ToolExecutor, _maybe_auto_mark_xlsx_deliverable
from runtime.tools.base import ToolRegistry, ToolSpec
from svc.llm.chat_models import LLMToolCall
from svc.persistence.sqlite_store import SqliteStore
def test_helper_marks_on_whatsapp() -> None:
out = _maybe_auto_mark_xlsx_deliverable(
tool_name="write_xlsx",
tool_args={},
result={"ok": True, "attachment_id": "a1", "name": "r.xlsx"},
inbound_metadata={"channel": "whatsapp"},
)
assert out.get("deliverable") is True
assert out.get("auto_deliverable") is True
def test_helper_respects_explicit_false() -> None:
out = _maybe_auto_mark_xlsx_deliverable(
tool_name="write_xlsx",
tool_args={"deliverable": False},
result={"ok": True, "attachment_id": "a1"},
inbound_metadata={"channel": "whatsapp"},
)
assert out.get("deliverable") is not True
assert out.get("auto_deliverable") is not True
def test_helper_skips_non_channel() -> None:
out = _maybe_auto_mark_xlsx_deliverable(
tool_name="write_xlsx",
tool_args={},
result={"ok": True, "attachment_id": "a1"},
inbound_metadata={"channel": "web"},
)
assert out.get("deliverable") is not True
def test_executor_auto_marks_write_xlsx_on_whatsapp(tmp_path: Path) -> None:
store = SqliteStore(str(tmp_path / "ops.sqlite"))
sess = store.create_session("t1")
def handler(_args: dict) -> dict:
return {
"ok": True,
"attachment_id": "att-xlsx",
"name": "report.xlsx",
"mime": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
}
reg = ToolRegistry(
[
ToolSpec(
name="write_xlsx",
description="xlsx",
parameters={"type": "object", "properties": {}},
handler=handler,
)
]
)
ctx = ToolExecutionContext(
store=store,
tools=reg,
session_id=sess.id,
lang="en",
inbound_metadata={"channel": "whatsapp"},
turn_uuid="turn-xlsx-1",
)
out, _dur = ToolExecutor()._execute_tool(
ctx,
LLMToolCall(id="c1", name="write_xlsx", arguments={}),
)
assert out.get("ok") is True
assert out.get("deliverable") is True
assert out.get("auto_deliverable") is True