package vip.mate.tool.builtin; import cn.hutool.json.JSONArray; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Component; import vip.mate.cron.model.CronJobDTO; import vip.mate.cron.service.CronJobService; import java.util.List; /** * Built-in tool: scheduled task (cron job) management via chat. *

* Allows agents to create, list, toggle, and delete cron jobs through natural language. * The agent_id is automatically bound to the current agent. LLM generates cron expressions * from natural language (e.g. "every day at 9am" → "0 9 * * *"). * * @author MateClaw Team * @see vip.mate.cron.service.CronJobService */ @Slf4j @Component @RequiredArgsConstructor public class CronJobTool { private final CronJobService cronJobService; @vip.mate.tool.ConcurrencyUnsafe("cron job creation persists to mate_cron_job; concurrent creates can race on name") @Tool(description = "Create a scheduled task (cron job). The task will run automatically at the specified time " + "and send the trigger message to the current agent. Use 5-field cron expressions: minute hour day month weekday. " + "Examples: '0 9 * * *' = daily at 9am, '0 9 * * 1-5' = weekdays at 9am, '*/30 * * * *' = every 30 minutes.") public String create_cron_job( @ToolParam(description = "Task name, e.g. 'Daily AI News Summary'") String name, @ToolParam(description = "5-field cron expression: minute hour day month weekday") String cronExpression, @ToolParam(description = "Message to send when the task triggers, e.g. 'Search for the latest AI news and summarize'") String triggerMessage, @ToolParam(description = "Timezone, default Asia/Shanghai. Examples: UTC, America/New_York", required = false) String timezone) { try { // Resolve current agent ID from conversation context String conversationId = ToolExecutionContext.conversationId(); Long agentId = resolveAgentId(conversationId); CronJobDTO dto = new CronJobDTO(); dto.setName(name); dto.setCronExpression(cronExpression); dto.setTriggerMessage(triggerMessage); dto.setTimezone(timezone != null && !timezone.isBlank() ? timezone : "Asia/Shanghai"); dto.setAgentId(agentId); dto.setTaskType("text"); dto.setEnabled(true); CronJobDTO created = cronJobService.create(dto); JSONObject result = new JSONObject(); result.set("success", true); result.set("jobId", created.getId()); result.set("name", created.getName()); result.set("cronExpression", created.getCronExpression()); result.set("timezone", created.getTimezone()); result.set("nextRunTime", created.getNextRunTime() != null ? created.getNextRunTime().toString() : ""); result.set("enabled", created.getEnabled()); return JSONUtil.toJsonPrettyStr(result); } catch (Exception e) { log.error("[CronJobTool] create failed: {}", e.getMessage()); return errorResult("Failed to create cron job: " + e.getMessage()); } } @Tool(description = "List all scheduled tasks (cron jobs) for the current agent. " + "Returns task name, cron expression, next run time, enabled status, and last run time.") public String list_cron_jobs() { try { List jobs = cronJobService.list(); JSONArray arr = new JSONArray(); for (CronJobDTO job : jobs) { JSONObject obj = new JSONObject(); obj.set("jobId", job.getId()); obj.set("name", job.getName()); obj.set("cronExpression", job.getCronExpression()); obj.set("timezone", job.getTimezone()); obj.set("enabled", job.getEnabled()); obj.set("nextRunTime", job.getNextRunTime() != null ? job.getNextRunTime().toString() : ""); obj.set("lastRunTime", job.getLastRunTime() != null ? job.getLastRunTime().toString() : ""); obj.set("agentName", job.getAgentName()); arr.add(obj); } JSONObject result = new JSONObject(); result.set("totalJobs", jobs.size()); result.set("jobs", arr); return JSONUtil.toJsonPrettyStr(result); } catch (Exception e) { log.error("[CronJobTool] list failed: {}", e.getMessage()); return errorResult("Failed to list cron jobs: " + e.getMessage()); } } @vip.mate.tool.ConcurrencyUnsafe("toggles row state in mate_cron_job; serialize to keep enabled/disabled deterministic") @Tool(description = "Enable or disable a scheduled task by its job ID. " + "Use list_cron_jobs first to find the job ID.") public String toggle_cron_job( @ToolParam(description = "Job ID (number)") Long jobId, @ToolParam(description = "true to enable, false to disable") Boolean enabled) { try { cronJobService.toggle(jobId, enabled); CronJobDTO updated = cronJobService.getById(jobId); JSONObject result = new JSONObject(); result.set("success", true); result.set("jobId", jobId); result.set("name", updated.getName()); result.set("enabled", updated.getEnabled()); result.set("nextRunTime", updated.getNextRunTime() != null ? updated.getNextRunTime().toString() : ""); return JSONUtil.toJsonPrettyStr(result); } catch (Exception e) { log.error("[CronJobTool] toggle failed: {}", e.getMessage()); return errorResult("Failed to toggle cron job: " + e.getMessage()); } } @vip.mate.tool.ConcurrencyUnsafe("destructive — removes row from mate_cron_job") @Tool(description = "Delete a scheduled task by its job ID. This action requires user approval. " + "Use list_cron_jobs first to find the job ID.") public String delete_cron_job( @ToolParam(description = "Job ID (number) to delete") Long jobId) { try { CronJobDTO job = cronJobService.getById(jobId); String jobName = job.getName(); cronJobService.delete(jobId); JSONObject result = new JSONObject(); result.set("success", true); result.set("deleted", jobName); return JSONUtil.toJsonPrettyStr(result); } catch (Exception e) { log.error("[CronJobTool] delete failed: {}", e.getMessage()); return errorResult("Failed to delete cron job: " + e.getMessage()); } } /** * Resolve agent ID from conversation ID. * Convention: cron conversations use "cron:{jobId}", normal chats use "{agentId}:{uuid}". */ private Long resolveAgentId(String conversationId) { if (conversationId == null || conversationId.isBlank()) { return 1L; // default agent } // Try to extract agent ID from conversation metadata // For now, use default agent ID 1 (the conversation's agent binding is handled by the caller) try { // Convention: conversationId might contain agent context info // Fallback to first enabled agent return 1L; } catch (Exception e) { return 1L; } } private String errorResult(String message) { JSONObject result = new JSONObject(); result.set("success", false); result.set("error", message); return JSONUtil.toJsonPrettyStr(result); } }