mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 00:40:45 +08:00
fix(scheduler): clarify interval units in schedule tools
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
e3828418d4
commit
f3152acb88
2 changed files with 97 additions and 6 deletions
|
|
@ -35,6 +35,32 @@ from runtime.types import normalize_interaction_mode, normalize_requested_specia
|
||||||
from svc.persistence.assistant_store import get_assistant_store
|
from svc.persistence.assistant_store import get_assistant_store
|
||||||
|
|
||||||
|
|
||||||
|
def _interval_human(schedule_kind: str, schedule_expr: str, lang: str = "zh") -> str:
|
||||||
|
"""Return a human-readable interval string for LLM consumption."""
|
||||||
|
if normalize_schedule_kind(schedule_kind) != "interval":
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
secs = int(schedule_expr)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return ""
|
||||||
|
is_en = str(lang or "").lower().startswith("en")
|
||||||
|
if secs < 60:
|
||||||
|
return f"{secs} seconds" if is_en else f"{secs} 秒"
|
||||||
|
if secs < 3600:
|
||||||
|
mins = secs // 60
|
||||||
|
rem = secs % 60
|
||||||
|
if rem == 0:
|
||||||
|
return f"every {mins} minute{'s' if mins != 1 else ''}" if is_en else f"每 {mins} 分钟"
|
||||||
|
return (
|
||||||
|
f"every {mins}m {rem}s" if is_en else f"每 {mins} 分 {rem} 秒"
|
||||||
|
)
|
||||||
|
hrs = secs // 3600
|
||||||
|
rem_m = (secs % 3600) // 60
|
||||||
|
if rem_m == 0:
|
||||||
|
return f"every {hrs} hour{'s' if hrs != 1 else ''}" if is_en else f"每 {hrs} 小时"
|
||||||
|
return f"every {hrs}h {rem_m}m" if is_en else f"每 {hrs} 小时 {rem_m} 分钟"
|
||||||
|
|
||||||
|
|
||||||
def _require(s: str, name: str) -> str:
|
def _require(s: str, name: str) -> str:
|
||||||
v = (s or "").strip()
|
v = (s or "").strip()
|
||||||
if not v:
|
if not v:
|
||||||
|
|
@ -161,8 +187,23 @@ def schedule_propose_tool() -> ToolSpec:
|
||||||
"session_id": {"type": "string", "description": "Auto-filled from session."},
|
"session_id": {"type": "string", "description": "Auto-filled from session."},
|
||||||
"name": {"type": "string"},
|
"name": {"type": "string"},
|
||||||
"recipe": _RECIPE_PARAM,
|
"recipe": _RECIPE_PARAM,
|
||||||
"schedule_kind": {"type": "string", "enum": ["cron", "once", "interval"]},
|
"schedule_kind": {
|
||||||
"schedule_expr": {"type": "string"},
|
"type": "string",
|
||||||
|
"enum": ["cron", "once", "interval"],
|
||||||
|
"description": (
|
||||||
|
"cron=standard cron expression; "
|
||||||
|
"once=ISO-8601 datetime; "
|
||||||
|
"interval=repeat every N **seconds** (integer string, e.g. '300' = every 5 minutes)."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"schedule_expr": {
|
||||||
|
"type": "string",
|
||||||
|
"description": (
|
||||||
|
"For interval: integer number of **seconds** between runs (e.g. '300' for every 5 minutes, '3600' for hourly). "
|
||||||
|
"For cron: standard 5-field cron expression. "
|
||||||
|
"For once: ISO-8601 datetime string."
|
||||||
|
),
|
||||||
|
},
|
||||||
"timezone": {"type": "string"},
|
"timezone": {"type": "string"},
|
||||||
"lang": {"type": "string"},
|
"lang": {"type": "string"},
|
||||||
},
|
},
|
||||||
|
|
@ -285,7 +326,15 @@ def schedule_create_tool() -> ToolSpec:
|
||||||
created_by_user_id=owner_user_id,
|
created_by_user_id=owner_user_id,
|
||||||
source="chat",
|
source="chat",
|
||||||
)
|
)
|
||||||
return {"ok": True, "job": store.scheduled_job_to_dict(row)}
|
job_dict = store.scheduled_job_to_dict(row)
|
||||||
|
human = _interval_human(
|
||||||
|
schedule_kind=schedule_kind,
|
||||||
|
schedule_expr=schedule_expr,
|
||||||
|
lang=str(args.get("lang") or "zh"),
|
||||||
|
)
|
||||||
|
if human:
|
||||||
|
job_dict["schedule_expr_human"] = human
|
||||||
|
return {"ok": True, "job": job_dict}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"ok": False, "error": f"{type(e).__name__}: {e}"}
|
return {"ok": False, "error": f"{type(e).__name__}: {e}"}
|
||||||
|
|
||||||
|
|
@ -311,8 +360,23 @@ def schedule_create_tool() -> ToolSpec:
|
||||||
"description": "Short summary / reminder intent. For playbooks, prefer recipe.goal.",
|
"description": "Short summary / reminder intent. For playbooks, prefer recipe.goal.",
|
||||||
},
|
},
|
||||||
"recipe": _RECIPE_PARAM,
|
"recipe": _RECIPE_PARAM,
|
||||||
"schedule_kind": {"type": "string", "enum": ["cron", "once", "interval"]},
|
"schedule_kind": {
|
||||||
"schedule_expr": {"type": "string"},
|
"type": "string",
|
||||||
|
"enum": ["cron", "once", "interval"],
|
||||||
|
"description": (
|
||||||
|
"cron=standard cron expression; "
|
||||||
|
"once=ISO-8601 datetime; "
|
||||||
|
"interval=repeat every N **seconds** (integer string, e.g. '300' = every 5 minutes)."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"schedule_expr": {
|
||||||
|
"type": "string",
|
||||||
|
"description": (
|
||||||
|
"For interval: integer number of **seconds** between runs (e.g. '300' for every 5 minutes, '3600' for hourly). "
|
||||||
|
"For cron: standard 5-field cron expression. "
|
||||||
|
"For once: ISO-8601 datetime string."
|
||||||
|
),
|
||||||
|
},
|
||||||
"timezone": {"type": "string", "description": "IANA timezone; defaults to the host system timezone."},
|
"timezone": {"type": "string", "description": "IANA timezone; defaults to the host system timezone."},
|
||||||
"interaction_mode": {"type": "string"},
|
"interaction_mode": {"type": "string"},
|
||||||
"specialist": {"type": "string"},
|
"specialist": {"type": "string"},
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,11 @@ import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from runtime.tools.experts.productivity.schedule_tools import schedule_create_tool, schedule_list_tool
|
from runtime.tools.experts.productivity.schedule_tools import (
|
||||||
|
_interval_human,
|
||||||
|
schedule_create_tool,
|
||||||
|
schedule_list_tool,
|
||||||
|
)
|
||||||
from svc.persistence.assistant_store import reset_assistant_store_singleton
|
from svc.persistence.assistant_store import reset_assistant_store_singleton
|
||||||
from svc.persistence.sqlite_store import SqliteStore
|
from svc.persistence.sqlite_store import SqliteStore
|
||||||
|
|
||||||
|
|
@ -54,6 +58,29 @@ class ScheduleToolsTests(unittest.TestCase):
|
||||||
self.assertTrue(listed.get("ok"))
|
self.assertTrue(listed.get("ok"))
|
||||||
self.assertEqual(len(listed.get("items") or []), 1)
|
self.assertEqual(len(listed.get("items") or []), 1)
|
||||||
|
|
||||||
|
def test_interval_human_uses_seconds_storage_but_readable_output(self) -> None:
|
||||||
|
self.assertEqual(_interval_human("interval", "300", "zh"), "每 5 分钟")
|
||||||
|
self.assertEqual(_interval_human("interval", "90", "en"), "every 1m 30s")
|
||||||
|
self.assertEqual(_interval_human("cron", "*/5 * * * *", "zh"), "")
|
||||||
|
|
||||||
|
def test_schedule_create_returns_human_interval_hint(self) -> None:
|
||||||
|
create = schedule_create_tool()
|
||||||
|
out = create.handler(
|
||||||
|
{
|
||||||
|
"tenant_id": self.tenant_id,
|
||||||
|
"owner_user_id": self.user_id,
|
||||||
|
"name": "Break reminder",
|
||||||
|
"prompt_text": "Stand up and stretch",
|
||||||
|
"schedule_kind": "interval",
|
||||||
|
"schedule_expr": "300",
|
||||||
|
"lang": "zh",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertTrue(out.get("ok"), out)
|
||||||
|
job = out.get("job") or {}
|
||||||
|
self.assertEqual(job.get("schedule_expr"), "300")
|
||||||
|
self.assertEqual(job.get("schedule_expr_human"), "每 5 分钟")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue