mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-08 23:33:16 +08:00
Synthesize playbooks from structured prompts and inject prior-run context.
Legacy jobs with empty recipes (e.g. dying-gasp algorithms in prompt_text) now compile into durable playbooks on create/enqueue, and each run gets a short previous-run summary for continuity. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
4898adf7d0
commit
51eef09801
8 changed files with 516 additions and 27 deletions
|
|
@ -104,6 +104,46 @@ class RecipeHelpersTests(unittest.TestCase):
|
|||
self.assertIn("工作流", scheduled_turn_system_suffix(lang="zh", playbook=True))
|
||||
self.assertIn("提醒", scheduled_turn_system_suffix(lang="zh", playbook=False))
|
||||
|
||||
def test_synthesize_step_headers_and_previous_run(self) -> None:
|
||||
from runtime.scheduler.recipe import synthesize_recipe_from_prompt
|
||||
from runtime.scheduler.turn_text import append_previous_run_context
|
||||
|
||||
prompt = (
|
||||
'Run "unmanaged by dying gasp" report.\n\n'
|
||||
"CRITICAL — Follow this exact algorithm:\n\n"
|
||||
"Step 1 — Query BN EMS failures:\n"
|
||||
"- Query active alarms: native_probable_cause = \"BN EMS alarm NE communication failure\"\n\n"
|
||||
"Step 2 — Query Remote dying gasp:\n"
|
||||
"- Query active alarms: native_probable_cause LIKE \"%Remote dying gasp%\"\n\n"
|
||||
"Step 3 — Join and deliver XLSX via save_deliverable_attachment.\n"
|
||||
)
|
||||
recipe = synthesize_recipe_from_prompt(prompt)
|
||||
assert recipe is not None
|
||||
self.assertTrue(recipe_has_playbook(recipe))
|
||||
self.assertGreaterEqual(len(recipe["steps"]), 3)
|
||||
self.assertEqual((recipe.get("source") or {}).get("compiled_from"), "prompt_text")
|
||||
self.assertTrue((recipe.get("output") or {}).get("need_attachments"))
|
||||
instr = build_scheduled_turn_instruction(
|
||||
prompt_text=prompt,
|
||||
mode="scheduled",
|
||||
lang="en",
|
||||
recipe=recipe,
|
||||
previous_run={
|
||||
"status": "success",
|
||||
"finished_at": "2026-08-10T06:00:00+00:00",
|
||||
"reply_text": "Found 12 candidates; 3 confirmed.",
|
||||
},
|
||||
)
|
||||
self.assertIn("Scheduled playbook", instr)
|
||||
self.assertIn("Previous run context", instr)
|
||||
self.assertIn("Found 12 candidates", instr)
|
||||
with_prev = append_previous_run_context(
|
||||
"base",
|
||||
previous_run={"status": "failed", "error": "timeout on CLI"},
|
||||
lang="en",
|
||||
)
|
||||
self.assertIn("timeout on CLI", with_prev)
|
||||
|
||||
|
||||
class ScheduleRecipeToolTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
|
|
@ -220,6 +260,29 @@ class ScheduleRecipeToolTests(unittest.TestCase):
|
|||
self.assertEqual((recipe.get("source") or {}).get("template_id"), "ume_alarm_tally_daily")
|
||||
self.assertIn("UME", str(job.get("prompt_text") or ""))
|
||||
|
||||
def test_create_auto_compiles_structured_prompt(self) -> None:
|
||||
prompt = (
|
||||
"Daily unmanaged dying-gasp correlation.\n\n"
|
||||
"Step 1 — Query BN EMS communication failures.\n"
|
||||
"Step 2 — Query remote dying gasp alarms.\n"
|
||||
"Step 3 — Correlate and attach XLSX report.\n"
|
||||
)
|
||||
out = schedule_create_tool().handler(
|
||||
{
|
||||
"tenant_id": self.tenant_id,
|
||||
"owner_user_id": self.user_id,
|
||||
"name": "dying-gasp-daily",
|
||||
"prompt_text": prompt,
|
||||
"schedule_kind": "cron",
|
||||
"schedule_expr": "0 14 * * *",
|
||||
"lang": "en",
|
||||
}
|
||||
)
|
||||
self.assertTrue(out.get("ok"), out)
|
||||
recipe = (out.get("job") or {}).get("recipe") or {}
|
||||
self.assertTrue(recipe_has_playbook(recipe))
|
||||
self.assertGreaterEqual(len(recipe.get("steps") or []), 3)
|
||||
|
||||
def test_simple_reminder_still_works(self) -> None:
|
||||
out = schedule_create_tool().handler(
|
||||
{
|
||||
|
|
|
|||
120
tests/test_scheduler_playbook_backfill.py
Normal file
120
tests/test_scheduler_playbook_backfill.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from runtime.scheduler.service import enqueue_scheduled_job_run
|
||||
from svc.persistence.assistant_store import reset_assistant_store_singleton
|
||||
from svc.persistence.sqlite_store import SqliteStore
|
||||
|
||||
|
||||
class EnqueuePlaybookBackfillTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self._tmp = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
|
||||
self.db = Path(self._tmp.name) / "enqueue.sqlite"
|
||||
os.environ["OPS_ASSISTANT_DB_PATH"] = str(self.db)
|
||||
os.environ["AIA_ASSISTANT_DB_BACKEND"] = "sqlite"
|
||||
reset_assistant_store_singleton()
|
||||
self.store = SqliteStore(str(self.db))
|
||||
t = self.store.create_tenant("Team")
|
||||
self.tenant_id = str(t["id"])
|
||||
user = self.store.create_user_account(
|
||||
tenant_id=self.tenant_id,
|
||||
username="administrator",
|
||||
display_name="Admin",
|
||||
role="owner",
|
||||
password_hash="x",
|
||||
is_active=True,
|
||||
)
|
||||
self.user_id = str(user["id"])
|
||||
|
||||
def tearDown(self) -> None:
|
||||
reset_assistant_store_singleton()
|
||||
self._tmp.cleanup()
|
||||
|
||||
def test_enqueue_synthesizes_recipe_and_injects_previous_run(self) -> None:
|
||||
prompt = (
|
||||
"Hourly dying-gasp check.\n\n"
|
||||
"Step 1 — Query BN EMS failures.\n"
|
||||
"Step 2 — Query remote dying gasp.\n"
|
||||
"Step 3 — Correlate and report.\n"
|
||||
)
|
||||
job = self.store.scheduled_job_create(
|
||||
tenant_id=self.tenant_id,
|
||||
name="dying-gasp",
|
||||
prompt_text=prompt,
|
||||
schedule_kind="interval",
|
||||
schedule_expr="3600",
|
||||
timezone_name="UTC",
|
||||
lang="en",
|
||||
delivery={"channel": "admin_chat"},
|
||||
recipe={},
|
||||
created_by_user_id=self.user_id,
|
||||
)
|
||||
prev = self.store.scheduled_job_run_create(
|
||||
job_id=job.id,
|
||||
tenant_id=self.tenant_id,
|
||||
status="queued",
|
||||
)
|
||||
self.store.scheduled_job_run_update(
|
||||
run_id=prev.id,
|
||||
tenant_id=self.tenant_id,
|
||||
patch={
|
||||
"status": "success",
|
||||
"reply_text": "Prior: 4 unmanaged NEs.",
|
||||
"finished_at": "2026-08-10T06:00:00+00:00",
|
||||
},
|
||||
)
|
||||
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
def _capture_create(**kwargs: object) -> MagicMock:
|
||||
captured.update(kwargs)
|
||||
return MagicMock(id="task-1")
|
||||
|
||||
self.store.oclaw_task_create = _capture_create # type: ignore[method-assign]
|
||||
|
||||
from runtime.scheduler import service as sched_service
|
||||
from runtime.scheduler.session_resolver import ResolvedSession
|
||||
|
||||
resolved = ResolvedSession(
|
||||
session_id="sess-sched-1",
|
||||
tenant_id=self.tenant_id,
|
||||
user_id=self.user_id,
|
||||
channel="admin_chat",
|
||||
account_id="",
|
||||
external_chat_id="",
|
||||
external_user_id="",
|
||||
is_group=False,
|
||||
)
|
||||
original_resolve = sched_service.resolve_scheduled_session
|
||||
original_worker = sched_service.ensure_worker_started
|
||||
try:
|
||||
sched_service.resolve_scheduled_session = MagicMock(return_value=resolved)
|
||||
sched_service.ensure_worker_started = MagicMock(return_value="worker-1")
|
||||
out = enqueue_scheduled_job_run(self.store, job=job, mode="scheduled")
|
||||
finally:
|
||||
sched_service.resolve_scheduled_session = original_resolve
|
||||
sched_service.ensure_worker_started = original_worker
|
||||
|
||||
self.assertTrue(out.get("ok"), out)
|
||||
payload = captured.get("payload")
|
||||
self.assertIsInstance(payload, dict)
|
||||
assert isinstance(payload, dict)
|
||||
self.assertTrue((payload.get("metadata") or {}).get("scheduled_playbook"))
|
||||
text = str(payload.get("text") or "")
|
||||
self.assertIn("Scheduled playbook", text)
|
||||
self.assertIn("Previous run context", text)
|
||||
self.assertIn("4 unmanaged", text)
|
||||
|
||||
refreshed = self.store.scheduled_job_get(job_id=job.id, tenant_id=self.tenant_id)
|
||||
assert refreshed is not None
|
||||
self.assertIn("Query BN EMS", refreshed.recipe_json)
|
||||
self.assertIn("compiled_from", refreshed.recipe_json)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -69,6 +69,9 @@ class ResolveScheduledViewerUsernameTests(unittest.TestCase):
|
|||
job.specialist = "generalist"
|
||||
job.created_by_user_id = str(self.member["id"])
|
||||
job.schedule_kind = "interval"
|
||||
job.recipe_json = "{}"
|
||||
job.source_session_id = ""
|
||||
job.name = "stretch"
|
||||
|
||||
run = MagicMock()
|
||||
run.id = "run-1"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue