Files
MoviePilot/app/agent/tools/impl/create_agent_task.py
jxxghp 7e851dbfa7 refactor(chain): 处理链功能域 mixin 化,清理未使用导入并根治兼容层循环导入
- ChainBase 拆分为 RecognitionMixin/MessageProcessingMixin/NotificationMixin
- TransferChain 拆分为 7 个功能 mixin(_mixins.py),SubscribeChain 音乐订阅域拆出 _music.py
- 斜杠命令交互四件套收敛为 InteractionChainMixin 委托,会话管理器移至 application 层,chain 层不再 re-export
- 模块基础类收敛到 app/modules/_base(notification/mediaserver 语义重命名)
- 清理 app/chain/__init__.py 24 个未使用导入,修正 49 处测试 patch 目标到实际命名空间
- 兼容层 legacy 符号不再并入 __all__,根治 schemas 初始化反向拉起 application.transfer 的循环导入
- 修复 bangumi 集数为字符串时 set_bangumi_info 抛 TypeError
- 新增重复代码等架构门禁测试;capability 清单校验排除下划线内部目录
2026-08-16 16:30:16 +08:00

161 lines
6.4 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.
import json
from datetime import datetime, timedelta
from typing import Literal, Optional, Type
import pytz
from pydantic import BaseModel, Field, model_validator
from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.runtime.config import settings
from app.db.oper.agentchat import AgentChatOper
from app.db.oper.agenttask import AgentTaskOper
from app.runtime.scheduling import TimerUtils
class CreateAgentTaskInput(BaseModel):
"""创建 Agent 自主定时任务的输入参数。"""
name: str = Field(
...,
min_length=1,
max_length=100,
description="Short task name shown in task management and execution reports.",
)
content: str = Field(
...,
min_length=1,
max_length=10000,
description="Complete instructions that the agent must execute when the task fires.",
)
trigger_type: Literal["date", "cron"] = Field(
...,
description="Use 'date' for one exact future run or 'cron' for recurring work.",
)
trigger: Optional[str] = Field(
None,
min_length=1,
max_length=200,
description=(
"For date, an ISO 8601 local or timezone-aware time such as "
"2026-07-19 20:30:00; for cron, a standard five-field expression "
"(minute hour day month weekday). The MoviePilot system timezone is used."
),
)
delay_minutes: Optional[int] = Field(
None,
ge=1,
le=525600,
description=(
"For a one-time date task expressed as 'in N minutes', provide this instead "
"of trigger. MoviePilot calculates and persists the exact future run time."
),
)
@model_validator(mode="after")
def validate_trigger(self) -> "CreateAgentTaskInput":
"""校验任务触发配置并统一格式。"""
self.name = self.name.strip()
self.content = self.content.strip()
if not self.name or not self.content:
raise ValueError("name 和 content 不能只包含空白字符")
if self.trigger_type == "date":
if self.delay_minutes is not None:
# LangChain 会在 run() 前后各校验一次,延迟时间在持久化前统一计算。
self.trigger = None
return self
if self.trigger is None:
raise ValueError("date 任务必须提供 trigger 或 delay_minutes")
elif self.trigger is None or self.delay_minutes is not None:
raise ValueError("cron 任务必须提供 trigger,且不能提供 delay_minutes")
self.trigger_type, self.trigger = TimerUtils.normalize_schedule_trigger(
trigger_type=self.trigger_type,
trigger_value=self.trigger,
timezone_name=settings.TZ,
require_future=True,
)
return self
class CreateAgentTaskTool(MoviePilotTool):
"""创建可精确唤醒当前 Agent 会话的自主定时任务。"""
name: str = "create_agent_task"
tags: list[str] = [ToolTag.Write, ToolTag.AgentTask, ToolTag.Admin]
description: str = (
"Create a persistent autonomous agent task only when the user explicitly asks "
"for delayed, scheduled, recurring, reminder, or monitoring work. Use trigger_type "
"'date' with delay_minutes for requests such as 'check in 30 minutes', an exact "
"trigger time for other one-time work, and 'cron' for recurring schedules. When "
"fired, MoviePilot wakes the agent in this conversation, executes content, and "
"broadcasts user-facing messages through the configured notification channels."
)
args_schema: Type[BaseModel] = CreateAgentTaskInput
require_admin: bool = True
def get_tool_message(self, **kwargs: object) -> Optional[str]:
"""生成创建定时任务的提示消息。"""
return f"创建自主定时任务:{kwargs.get('name', '')}"
def _create_task(self, payload: CreateAgentTaskInput) -> dict:
"""持久化任务并立即注册到运行时调度器。"""
from app.application.scheduling import update_agent_task_job
trigger_value = payload.trigger
if payload.trigger_type == "date" and payload.delay_minutes is not None:
timezone = pytz.timezone(settings.TZ)
trigger_value = (
datetime.now(timezone) + timedelta(minutes=payload.delay_minutes)
).isoformat(timespec="seconds")
_, trigger_value = TimerUtils.normalize_schedule_trigger(
trigger_type=payload.trigger_type,
trigger_value=trigger_value,
timezone_name=settings.TZ,
require_future=True,
)
chat = AgentChatOper().get(
session_id=self._session_id,
user_id=self._user_id,
)
task = AgentTaskOper().add(
name=payload.name.strip(),
content=payload.content.strip(),
trigger_type=payload.trigger_type,
cron_expression=trigger_value if payload.trigger_type == "cron" else None,
run_at=trigger_value if payload.trigger_type == "date" else None,
user_id=str(self._user_id),
username=self._username or (chat.username if chat else None),
session_id=str(self._session_id),
channel=self._channel or (chat.channel if chat else None),
source=self._source or (chat.source if chat else None),
original_chat_id=chat.original_chat_id if chat else None,
)
next_run_at = update_agent_task_job(task.id)
return AgentTaskOper.to_dict(
task,
next_run_at=next_run_at,
timezone=settings.TZ,
)
async def run(
self,
name: str,
content: str,
trigger_type: str,
trigger: Optional[str] = None,
delay_minutes: Optional[int] = None,
**kwargs: object,
) -> str:
"""创建 Agent 自主定时任务。"""
if not settings.AI_AGENT_ENABLE:
return "AI Agent 未启用,无法创建自主定时任务"
payload = CreateAgentTaskInput(
name=name,
content=content,
trigger_type=trigger_type,
trigger=trigger,
delay_minutes=delay_minutes,
)
task = await self.run_blocking("db", self._create_task, payload)
return json.dumps(task, ensure_ascii=False, indent=2)