Files
beaver_project/app-instance/backend-old/nanobot/agent/tools/cron_action.py

117 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""结构化 cron 生命周期控制工具。
cron 任务不是普通用户对话,它经常需要在运行完成后主动告诉调度器:
- 这个任务已经可以删掉;
- 今天这一轮先结束,下一天再继续;
- 下次应该改成新的时间表。
这个工具就是让模型把这些决策显式写成结构化数据,而不是只留在自然语言里。
"""
from __future__ import annotations
from typing import Any
from nanobot.agent.tools.base import Tool
from nanobot.cron.types import CronAction
class CronActionTool(Tool):
"""捕获模型输出的机器可读 cron 控制决策。"""
def __init__(self, job_id: str):
# `job_id` 仅用于回显和审计,不参与决策本身。
self.job_id = job_id
# `_decision` 在本轮 agent 执行期间最多被写一次,外部在结束后读取。
self._decision: CronAction | None = None
@property
def name(self) -> str:
return "cron_action"
@property
def description(self) -> str:
return "Record a structured lifecycle action for the currently running cron job."
@property
def parameters(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["none", "remove", "disable", "complete_today", "reschedule"],
"description": "Lifecycle action for the current cron job",
},
"reason": {
"type": "string",
"description": "Short reason for audit logs",
},
"every_seconds": {
"type": "integer",
"description": "Required when action=reschedule and using fixed interval",
},
"cron_expr": {
"type": "string",
"description": "Required when action=reschedule and using cron expression",
},
"tz": {
"type": "string",
"description": "Optional timezone for cron_expr reschedules",
},
"at": {
"type": "string",
"description": "Required when action=reschedule and using one-time ISO datetime",
},
},
"required": ["action"],
}
@property
def decision(self) -> CronAction | None:
# 暴露最终结构化决策给 cron runtime便于后处理调度状态。
return self._decision
async def execute(
self,
action: str,
reason: str | None = None,
every_seconds: int | None = None,
cron_expr: str | None = None,
tz: str | None = None,
at: str | None = None,
**_kwargs: Any,
) -> str:
# 统一做小写规范化,避免模型传入 `Remove` / `REMOVE` 之类大小写变体。
normalized = (action or "").strip().lower()
allowed_actions = {"none", "remove", "disable", "complete_today", "reschedule"}
if normalized not in allowed_actions:
return f"Error: unsupported cron action '{action}'"
# 非重排任务不允许额外携带调度字段,避免出现“说 remove 但又传 cron_expr”的脏数据。
if normalized != "reschedule" and any(value is not None for value in (every_seconds, cron_expr, tz, at)):
return "Error: schedule fields can only be used when action='reschedule'"
if normalized == "reschedule":
# 重新排期必须在三种时间表达方式里三选一,不能都不传,也不能混传。
options = int(every_seconds is not None) + int(bool(cron_expr)) + int(bool(at))
if options != 1:
return "Error: reschedule requires exactly one of every_seconds, cron_expr, or at"
# 时区只有 cron 表达式才有意义。
if tz and not cron_expr:
return "Error: tz can only be used with cron_expr"
# 校验通过后,把本轮决策固化为 dataclass交给 runtime 在执行后统一消费。
self._decision = CronAction(
action=normalized or "none",
reason=(reason or "").strip() or None,
every_seconds=every_seconds,
cron_expr=cron_expr,
tz=tz,
at=at,
)
# 返回给模型/日志的是一条可读确认文本,方便工具调用结果出现在上下文里。
detail = f" for job {self.job_id}"
if self._decision.reason:
detail += f" ({self._decision.reason})"
return f"Recorded cron_action={self._decision.action}{detail}"