feat: add cron scheduled task service with tool_use integration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
186
main/tools/tool_cron.c
Normal file
186
main/tools/tool_cron.c
Normal file
@@ -0,0 +1,186 @@
|
||||
#include "tools/tool_cron.h"
|
||||
#include "cron/cron_service.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include "esp_log.h"
|
||||
#include "cJSON.h"
|
||||
|
||||
static const char *TAG = "tool_cron";
|
||||
|
||||
/* ── cron_add ─────────────────────────────────────────────────── */
|
||||
|
||||
esp_err_t tool_cron_add_execute(const char *input_json, char *output, size_t output_size)
|
||||
{
|
||||
cJSON *root = cJSON_Parse(input_json);
|
||||
if (!root) {
|
||||
snprintf(output, output_size, "Error: invalid JSON input");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
const char *name = cJSON_GetStringValue(cJSON_GetObjectItem(root, "name"));
|
||||
const char *schedule_type = cJSON_GetStringValue(cJSON_GetObjectItem(root, "schedule_type"));
|
||||
const char *message = cJSON_GetStringValue(cJSON_GetObjectItem(root, "message"));
|
||||
|
||||
if (!name || !schedule_type || !message) {
|
||||
snprintf(output, output_size, "Error: missing required fields (name, schedule_type, message)");
|
||||
cJSON_Delete(root);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
if (strlen(message) == 0) {
|
||||
snprintf(output, output_size, "Error: message must not be empty");
|
||||
cJSON_Delete(root);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
cron_job_t job;
|
||||
memset(&job, 0, sizeof(job));
|
||||
strncpy(job.name, name, sizeof(job.name) - 1);
|
||||
strncpy(job.message, message, sizeof(job.message) - 1);
|
||||
|
||||
/* Optional channel and chat_id */
|
||||
const char *channel = cJSON_GetStringValue(cJSON_GetObjectItem(root, "channel"));
|
||||
const char *chat_id = cJSON_GetStringValue(cJSON_GetObjectItem(root, "chat_id"));
|
||||
if (channel) strncpy(job.channel, channel, sizeof(job.channel) - 1);
|
||||
if (chat_id) strncpy(job.chat_id, chat_id, sizeof(job.chat_id) - 1);
|
||||
|
||||
if (strcmp(schedule_type, "every") == 0) {
|
||||
job.kind = CRON_KIND_EVERY;
|
||||
cJSON *interval = cJSON_GetObjectItem(root, "interval_s");
|
||||
if (!interval || !cJSON_IsNumber(interval) || interval->valuedouble <= 0) {
|
||||
snprintf(output, output_size, "Error: 'every' schedule requires positive 'interval_s'");
|
||||
cJSON_Delete(root);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
job.interval_s = (uint32_t)interval->valuedouble;
|
||||
job.delete_after_run = false;
|
||||
} else if (strcmp(schedule_type, "at") == 0) {
|
||||
job.kind = CRON_KIND_AT;
|
||||
cJSON *at_epoch = cJSON_GetObjectItem(root, "at_epoch");
|
||||
if (!at_epoch || !cJSON_IsNumber(at_epoch)) {
|
||||
snprintf(output, output_size, "Error: 'at' schedule requires 'at_epoch' (unix timestamp)");
|
||||
cJSON_Delete(root);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
job.at_epoch = (int64_t)at_epoch->valuedouble;
|
||||
|
||||
/* Check if already in the past */
|
||||
time_t now = time(NULL);
|
||||
if (job.at_epoch <= now) {
|
||||
snprintf(output, output_size, "Error: at_epoch %lld is in the past (now=%lld)",
|
||||
(long long)job.at_epoch, (long long)now);
|
||||
cJSON_Delete(root);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* Default: delete one-shot jobs after run */
|
||||
cJSON *delete_j = cJSON_GetObjectItem(root, "delete_after_run");
|
||||
job.delete_after_run = delete_j ? cJSON_IsTrue(delete_j) : true;
|
||||
} else {
|
||||
snprintf(output, output_size, "Error: schedule_type must be 'every' or 'at'");
|
||||
cJSON_Delete(root);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
|
||||
esp_err_t err = cron_add_job(&job);
|
||||
if (err != ESP_OK) {
|
||||
snprintf(output, output_size, "Error: failed to add job (%s)", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
/* Format success response */
|
||||
if (job.kind == CRON_KIND_EVERY) {
|
||||
snprintf(output, output_size,
|
||||
"OK: Added recurring job '%s' (id=%s), runs every %lu seconds. Next run at epoch %lld.",
|
||||
job.name, job.id, (unsigned long)job.interval_s, (long long)job.next_run);
|
||||
} else {
|
||||
snprintf(output, output_size,
|
||||
"OK: Added one-shot job '%s' (id=%s), fires at epoch %lld.%s",
|
||||
job.name, job.id, (long long)job.at_epoch,
|
||||
job.delete_after_run ? " Will be deleted after firing." : "");
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "cron_add: %s", output);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ── cron_list ────────────────────────────────────────────────── */
|
||||
|
||||
esp_err_t tool_cron_list_execute(const char *input_json, char *output, size_t output_size)
|
||||
{
|
||||
(void)input_json;
|
||||
|
||||
const cron_job_t *jobs;
|
||||
int count;
|
||||
cron_list_jobs(&jobs, &count);
|
||||
|
||||
if (count == 0) {
|
||||
snprintf(output, output_size, "No cron jobs scheduled.");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
size_t off = 0;
|
||||
off += snprintf(output + off, output_size - off,
|
||||
"Scheduled jobs (%d):\n", count);
|
||||
|
||||
for (int i = 0; i < count && off < output_size - 1; i++) {
|
||||
const cron_job_t *j = &jobs[i];
|
||||
|
||||
if (j->kind == CRON_KIND_EVERY) {
|
||||
off += snprintf(output + off, output_size - off,
|
||||
" %d. [%s] \"%s\" — every %lus, %s, next=%lld, last=%lld, ch=%s:%s\n",
|
||||
i + 1, j->id, j->name,
|
||||
(unsigned long)j->interval_s,
|
||||
j->enabled ? "enabled" : "disabled",
|
||||
(long long)j->next_run, (long long)j->last_run,
|
||||
j->channel, j->chat_id);
|
||||
} else {
|
||||
off += snprintf(output + off, output_size - off,
|
||||
" %d. [%s] \"%s\" — at %lld, %s, last=%lld, ch=%s:%s%s\n",
|
||||
i + 1, j->id, j->name,
|
||||
(long long)j->at_epoch,
|
||||
j->enabled ? "enabled" : "disabled",
|
||||
(long long)j->last_run,
|
||||
j->channel, j->chat_id,
|
||||
j->delete_after_run ? " (auto-delete)" : "");
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "cron_list: %d jobs", count);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ── cron_remove ──────────────────────────────────────────────── */
|
||||
|
||||
esp_err_t tool_cron_remove_execute(const char *input_json, char *output, size_t output_size)
|
||||
{
|
||||
cJSON *root = cJSON_Parse(input_json);
|
||||
if (!root) {
|
||||
snprintf(output, output_size, "Error: invalid JSON input");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
const char *job_id = cJSON_GetStringValue(cJSON_GetObjectItem(root, "job_id"));
|
||||
if (!job_id || strlen(job_id) == 0) {
|
||||
snprintf(output, output_size, "Error: missing 'job_id' field");
|
||||
cJSON_Delete(root);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
esp_err_t err = cron_remove_job(job_id);
|
||||
cJSON_Delete(root);
|
||||
|
||||
if (err == ESP_OK) {
|
||||
snprintf(output, output_size, "OK: Removed cron job %s", job_id);
|
||||
} else if (err == ESP_ERR_NOT_FOUND) {
|
||||
snprintf(output, output_size, "Error: job '%s' not found", job_id);
|
||||
} else {
|
||||
snprintf(output, output_size, "Error: failed to remove job (%s)", esp_err_to_name(err));
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "cron_remove: %s -> %s", job_id, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
22
main/tools/tool_cron.h
Normal file
22
main/tools/tool_cron.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stddef.h>
|
||||
|
||||
/**
|
||||
* Add a scheduled cron job.
|
||||
* Input JSON: { name, schedule_type ("every"/"at"), interval_s, at_epoch, message, channel?, chat_id? }
|
||||
*/
|
||||
esp_err_t tool_cron_add_execute(const char *input_json, char *output, size_t output_size);
|
||||
|
||||
/**
|
||||
* List all scheduled cron jobs.
|
||||
* Input JSON: {} (no required fields)
|
||||
*/
|
||||
esp_err_t tool_cron_list_execute(const char *input_json, char *output, size_t output_size);
|
||||
|
||||
/**
|
||||
* Remove a scheduled cron job by ID.
|
||||
* Input JSON: { job_id }
|
||||
*/
|
||||
esp_err_t tool_cron_remove_execute(const char *input_json, char *output, size_t output_size);
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "tools/tool_web_search.h"
|
||||
#include "tools/tool_get_time.h"
|
||||
#include "tools/tool_files.h"
|
||||
#include "tools/tool_cron.h"
|
||||
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
@@ -9,7 +10,7 @@
|
||||
|
||||
static const char *TAG = "tools";
|
||||
|
||||
#define MAX_TOOLS 8
|
||||
#define MAX_TOOLS 12
|
||||
|
||||
static mimi_tool_t s_tools[MAX_TOOLS];
|
||||
static int s_tool_count = 0;
|
||||
@@ -130,6 +131,50 @@ esp_err_t tool_registry_init(void)
|
||||
};
|
||||
register_tool(&ld);
|
||||
|
||||
/* Register cron_add */
|
||||
mimi_tool_t ca = {
|
||||
.name = "cron_add",
|
||||
.description = "Schedule a recurring or one-shot task. The message will trigger an agent turn when the job fires.",
|
||||
.input_schema_json =
|
||||
"{\"type\":\"object\","
|
||||
"\"properties\":{"
|
||||
"\"name\":{\"type\":\"string\",\"description\":\"Short name for the job\"},"
|
||||
"\"schedule_type\":{\"type\":\"string\",\"description\":\"'every' for recurring interval or 'at' for one-shot at a unix timestamp\"},"
|
||||
"\"interval_s\":{\"type\":\"integer\",\"description\":\"Interval in seconds (required for 'every')\"},"
|
||||
"\"at_epoch\":{\"type\":\"integer\",\"description\":\"Unix timestamp to fire at (required for 'at')\"},"
|
||||
"\"message\":{\"type\":\"string\",\"description\":\"Message to inject when the job fires, triggering an agent turn\"},"
|
||||
"\"channel\":{\"type\":\"string\",\"description\":\"Optional reply channel (e.g. 'telegram'). Defaults to 'system'\"},"
|
||||
"\"chat_id\":{\"type\":\"string\",\"description\":\"Optional reply chat_id. Defaults to 'cron'\"}"
|
||||
"},"
|
||||
"\"required\":[\"name\",\"schedule_type\",\"message\"]}",
|
||||
.execute = tool_cron_add_execute,
|
||||
};
|
||||
register_tool(&ca);
|
||||
|
||||
/* Register cron_list */
|
||||
mimi_tool_t cl = {
|
||||
.name = "cron_list",
|
||||
.description = "List all scheduled cron jobs with their status, schedule, and IDs.",
|
||||
.input_schema_json =
|
||||
"{\"type\":\"object\","
|
||||
"\"properties\":{},"
|
||||
"\"required\":[]}",
|
||||
.execute = tool_cron_list_execute,
|
||||
};
|
||||
register_tool(&cl);
|
||||
|
||||
/* Register cron_remove */
|
||||
mimi_tool_t cr = {
|
||||
.name = "cron_remove",
|
||||
.description = "Remove a scheduled cron job by its ID.",
|
||||
.input_schema_json =
|
||||
"{\"type\":\"object\","
|
||||
"\"properties\":{\"job_id\":{\"type\":\"string\",\"description\":\"The 8-character job ID to remove\"}},"
|
||||
"\"required\":[\"job_id\"]}",
|
||||
.execute = tool_cron_remove_execute,
|
||||
};
|
||||
register_tool(&cr);
|
||||
|
||||
build_tools_json();
|
||||
|
||||
ESP_LOGI(TAG, "Tool registry initialized");
|
||||
|
||||
Reference in New Issue
Block a user