@@ -12,7 +12,6 @@
|
|||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_heap_caps.h"
|
#include "esp_heap_caps.h"
|
||||||
#include "esp_random.h"
|
|
||||||
#include "cJSON.h"
|
#include "cJSON.h"
|
||||||
|
|
||||||
static const char *TAG = "agent";
|
static const char *TAG = "agent";
|
||||||
@@ -54,17 +53,107 @@ static cJSON *build_assistant_content(const llm_response_t *resp)
|
|||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void json_set_string(cJSON *obj, const char *key, const char *value)
|
||||||
|
{
|
||||||
|
if (!obj || !key || !value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cJSON_DeleteItemFromObject(obj, key);
|
||||||
|
cJSON_AddStringToObject(obj, key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void append_turn_context_prompt(char *prompt, size_t size, const mimi_msg_t *msg)
|
||||||
|
{
|
||||||
|
if (!prompt || size == 0 || !msg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t off = strnlen(prompt, size - 1);
|
||||||
|
if (off >= size - 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int n = snprintf(
|
||||||
|
prompt + off, size - off,
|
||||||
|
"\n## Current Turn Context\n"
|
||||||
|
"- source_channel: %s\n"
|
||||||
|
"- source_chat_id: %s\n"
|
||||||
|
"- If using cron_add for Telegram in this turn, set channel='telegram' and chat_id to source_chat_id.\n"
|
||||||
|
"- Never use chat_id 'cron' for Telegram messages.\n",
|
||||||
|
msg->channel[0] ? msg->channel : "(unknown)",
|
||||||
|
msg->chat_id[0] ? msg->chat_id : "(empty)");
|
||||||
|
|
||||||
|
if (n < 0 || (size_t)n >= (size - off)) {
|
||||||
|
prompt[size - 1] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *patch_tool_input_with_context(const llm_tool_call_t *call, const mimi_msg_t *msg)
|
||||||
|
{
|
||||||
|
if (!call || !msg || strcmp(call->name, "cron_add") != 0) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *root = cJSON_Parse(call->input ? call->input : "{}");
|
||||||
|
if (!root || !cJSON_IsObject(root)) {
|
||||||
|
cJSON_Delete(root);
|
||||||
|
root = cJSON_CreateObject();
|
||||||
|
}
|
||||||
|
if (!root) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
cJSON *channel_item = cJSON_GetObjectItem(root, "channel");
|
||||||
|
const char *channel = cJSON_IsString(channel_item) ? channel_item->valuestring : NULL;
|
||||||
|
|
||||||
|
if ((!channel || channel[0] == '\0') && msg->channel[0] != '\0') {
|
||||||
|
json_set_string(root, "channel", msg->channel);
|
||||||
|
channel = msg->channel;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel && strcmp(channel, MIMI_CHAN_TELEGRAM) == 0 &&
|
||||||
|
strcmp(msg->channel, MIMI_CHAN_TELEGRAM) == 0 && msg->chat_id[0] != '\0') {
|
||||||
|
cJSON *chat_item = cJSON_GetObjectItem(root, "chat_id");
|
||||||
|
const char *chat_id = cJSON_IsString(chat_item) ? chat_item->valuestring : NULL;
|
||||||
|
if (!chat_id || chat_id[0] == '\0' || strcmp(chat_id, "cron") == 0) {
|
||||||
|
json_set_string(root, "chat_id", msg->chat_id);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
char *patched = NULL;
|
||||||
|
if (changed) {
|
||||||
|
patched = cJSON_PrintUnformatted(root);
|
||||||
|
if (patched) {
|
||||||
|
ESP_LOGI(TAG, "Patched cron_add target to %s:%s", msg->channel, msg->chat_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_Delete(root);
|
||||||
|
return patched;
|
||||||
|
}
|
||||||
|
|
||||||
/* Build the user message with tool_result blocks */
|
/* Build the user message with tool_result blocks */
|
||||||
static cJSON *build_tool_results(const llm_response_t *resp, char *tool_output, size_t tool_output_size)
|
static cJSON *build_tool_results(const llm_response_t *resp, const mimi_msg_t *msg,
|
||||||
|
char *tool_output, size_t tool_output_size)
|
||||||
{
|
{
|
||||||
cJSON *content = cJSON_CreateArray();
|
cJSON *content = cJSON_CreateArray();
|
||||||
|
|
||||||
for (int i = 0; i < resp->call_count; i++) {
|
for (int i = 0; i < resp->call_count; i++) {
|
||||||
const llm_tool_call_t *call = &resp->calls[i];
|
const llm_tool_call_t *call = &resp->calls[i];
|
||||||
|
const char *tool_input = call->input ? call->input : "{}";
|
||||||
|
char *patched_input = patch_tool_input_with_context(call, msg);
|
||||||
|
if (patched_input) {
|
||||||
|
tool_input = patched_input;
|
||||||
|
}
|
||||||
|
|
||||||
/* Execute tool */
|
/* Execute tool */
|
||||||
tool_output[0] = '\0';
|
tool_output[0] = '\0';
|
||||||
tool_registry_execute(call->name, call->input, tool_output, tool_output_size);
|
tool_registry_execute(call->name, tool_input, tool_output, tool_output_size);
|
||||||
|
free(patched_input);
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Tool %s result: %d bytes", call->name, (int)strlen(tool_output));
|
ESP_LOGI(TAG, "Tool %s result: %d bytes", call->name, (int)strlen(tool_output));
|
||||||
|
|
||||||
@@ -105,6 +194,8 @@ static void agent_loop_task(void *arg)
|
|||||||
|
|
||||||
/* 1. Build system prompt */
|
/* 1. Build system prompt */
|
||||||
context_build_system_prompt(system_prompt, MIMI_CONTEXT_BUF_SIZE);
|
context_build_system_prompt(system_prompt, MIMI_CONTEXT_BUF_SIZE);
|
||||||
|
append_turn_context_prompt(system_prompt, MIMI_CONTEXT_BUF_SIZE, &msg);
|
||||||
|
ESP_LOGI(TAG, "LLM turn context: channel=%s chat_id=%s", msg.channel, msg.chat_id);
|
||||||
|
|
||||||
/* 2. Load session history into cJSON array */
|
/* 2. Load session history into cJSON array */
|
||||||
session_get_history_json(msg.chat_id, history_json,
|
session_get_history_json(msg.chat_id, history_json,
|
||||||
@@ -122,27 +213,22 @@ static void agent_loop_task(void *arg)
|
|||||||
/* 4. ReAct loop */
|
/* 4. ReAct loop */
|
||||||
char *final_text = NULL;
|
char *final_text = NULL;
|
||||||
int iteration = 0;
|
int iteration = 0;
|
||||||
|
bool sent_working_status = false;
|
||||||
|
|
||||||
while (iteration < MIMI_AGENT_MAX_TOOL_ITER) {
|
while (iteration < MIMI_AGENT_MAX_TOOL_ITER) {
|
||||||
/* Send "working" indicator before each API call */
|
/* Send "working" indicator before each API call */
|
||||||
#if MIMI_AGENT_SEND_WORKING_STATUS
|
#if MIMI_AGENT_SEND_WORKING_STATUS
|
||||||
{
|
if (!sent_working_status && strcmp(msg.channel, MIMI_CHAN_SYSTEM) != 0) {
|
||||||
static const char *working_phrases[] = {
|
|
||||||
"mimi\xF0\x9F\x98\x97is working...",
|
|
||||||
"mimi\xF0\x9F\x90\xBE is thinking...",
|
|
||||||
"mimi\xF0\x9F\x92\xAD is pondering...",
|
|
||||||
"mimi\xF0\x9F\x8C\x99 is on it...",
|
|
||||||
"mimi\xE2\x9C\xA8 is cooking...",
|
|
||||||
};
|
|
||||||
const int phrase_count = sizeof(working_phrases) / sizeof(working_phrases[0]);
|
|
||||||
mimi_msg_t status = {0};
|
mimi_msg_t status = {0};
|
||||||
strncpy(status.channel, msg.channel, sizeof(status.channel) - 1);
|
strncpy(status.channel, msg.channel, sizeof(status.channel) - 1);
|
||||||
strncpy(status.chat_id, msg.chat_id, sizeof(status.chat_id) - 1);
|
strncpy(status.chat_id, msg.chat_id, sizeof(status.chat_id) - 1);
|
||||||
status.content = strdup(working_phrases[esp_random() % phrase_count]);
|
status.content = strdup("\xF0\x9F\x90\xB1mimi is working...");
|
||||||
if (status.content) {
|
if (status.content) {
|
||||||
if (message_bus_push_outbound(&status) != ESP_OK) {
|
if (message_bus_push_outbound(&status) != ESP_OK) {
|
||||||
ESP_LOGW(TAG, "Outbound queue full, drop working status");
|
ESP_LOGW(TAG, "Outbound queue full, drop working status");
|
||||||
free(status.content);
|
free(status.content);
|
||||||
|
} else {
|
||||||
|
sent_working_status = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,7 +260,7 @@ static void agent_loop_task(void *arg)
|
|||||||
cJSON_AddItemToArray(messages, asst_msg);
|
cJSON_AddItemToArray(messages, asst_msg);
|
||||||
|
|
||||||
/* Execute tools and append results */
|
/* Execute tools and append results */
|
||||||
cJSON *tool_results = build_tool_results(&resp, tool_output, TOOL_OUTPUT_SIZE);
|
cJSON *tool_results = build_tool_results(&resp, &msg, tool_output, TOOL_OUTPUT_SIZE);
|
||||||
cJSON *result_msg = cJSON_CreateObject();
|
cJSON *result_msg = cJSON_CreateObject();
|
||||||
cJSON_AddStringToObject(result_msg, "role", "user");
|
cJSON_AddStringToObject(result_msg, "role", "user");
|
||||||
cJSON_AddItemToObject(result_msg, "content", tool_results);
|
cJSON_AddItemToObject(result_msg, "content", tool_results);
|
||||||
@@ -189,8 +275,16 @@ static void agent_loop_task(void *arg)
|
|||||||
/* 5. Send response */
|
/* 5. Send response */
|
||||||
if (final_text && final_text[0]) {
|
if (final_text && final_text[0]) {
|
||||||
/* Save to session (only user text + final assistant text) */
|
/* Save to session (only user text + final assistant text) */
|
||||||
session_append(msg.chat_id, "user", msg.content);
|
esp_err_t save_user = session_append(msg.chat_id, "user", msg.content);
|
||||||
session_append(msg.chat_id, "assistant", final_text);
|
esp_err_t save_asst = session_append(msg.chat_id, "assistant", final_text);
|
||||||
|
if (save_user != ESP_OK || save_asst != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "Session save failed for chat %s (user=%s, assistant=%s)",
|
||||||
|
msg.chat_id,
|
||||||
|
esp_err_to_name(save_user),
|
||||||
|
esp_err_to_name(save_asst));
|
||||||
|
} else {
|
||||||
|
ESP_LOGI(TAG, "Session saved for chat %s", msg.chat_id);
|
||||||
|
}
|
||||||
|
|
||||||
/* Push response to outbound */
|
/* Push response to outbound */
|
||||||
mimi_msg_t out = {0};
|
mimi_msg_t out = {0};
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ esp_err_t context_build_system_prompt(char *buf, size_t size)
|
|||||||
"- cron_add: Schedule a recurring or one-shot task. The message will trigger an agent turn when the job fires.\n"
|
"- cron_add: Schedule a recurring or one-shot task. The message will trigger an agent turn when the job fires.\n"
|
||||||
"- cron_list: List all scheduled cron jobs.\n"
|
"- cron_list: List all scheduled cron jobs.\n"
|
||||||
"- cron_remove: Remove a scheduled cron job by ID.\n\n"
|
"- cron_remove: Remove a scheduled cron job by ID.\n\n"
|
||||||
|
"When using cron_add for Telegram delivery, always set channel='telegram' and a valid numeric chat_id.\n\n"
|
||||||
"Use tools when needed. Provide your final answer as text after using tools.\n\n"
|
"Use tools when needed. Provide your final answer as text after using tools.\n\n"
|
||||||
"## Memory\n"
|
"## Memory\n"
|
||||||
"You have persistent memory stored on local flash:\n"
|
"You have persistent memory stored on local flash:\n"
|
||||||
|
|||||||
@@ -20,6 +20,36 @@ static cron_job_t s_jobs[MAX_CRON_JOBS];
|
|||||||
static int s_job_count = 0;
|
static int s_job_count = 0;
|
||||||
static TaskHandle_t s_cron_task = NULL;
|
static TaskHandle_t s_cron_task = NULL;
|
||||||
|
|
||||||
|
static esp_err_t cron_save_jobs(void);
|
||||||
|
|
||||||
|
static bool cron_sanitize_destination(cron_job_t *job)
|
||||||
|
{
|
||||||
|
bool changed = false;
|
||||||
|
if (!job) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (job->channel[0] == '\0') {
|
||||||
|
strncpy(job->channel, MIMI_CHAN_SYSTEM, sizeof(job->channel) - 1);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(job->channel, MIMI_CHAN_TELEGRAM) == 0) {
|
||||||
|
if (job->chat_id[0] == '\0' || strcmp(job->chat_id, "cron") == 0) {
|
||||||
|
ESP_LOGW(TAG, "Cron job %s has invalid telegram chat_id, fallback to system:cron",
|
||||||
|
job->id[0] ? job->id : "<new>");
|
||||||
|
strncpy(job->channel, MIMI_CHAN_SYSTEM, sizeof(job->channel) - 1);
|
||||||
|
strncpy(job->chat_id, "cron", sizeof(job->chat_id) - 1);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
} else if (job->chat_id[0] == '\0') {
|
||||||
|
strncpy(job->chat_id, "cron", sizeof(job->chat_id) - 1);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Persistence ──────────────────────────────────────────────── */
|
/* ── Persistence ──────────────────────────────────────────────── */
|
||||||
|
|
||||||
static void cron_generate_id(char *id_buf)
|
static void cron_generate_id(char *id_buf)
|
||||||
@@ -77,6 +107,7 @@ static esp_err_t cron_load_jobs(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
s_job_count = 0;
|
s_job_count = 0;
|
||||||
|
bool repaired = false;
|
||||||
cJSON *item;
|
cJSON *item;
|
||||||
cJSON_ArrayForEach(item, jobs_arr) {
|
cJSON_ArrayForEach(item, jobs_arr) {
|
||||||
if (s_job_count >= MAX_CRON_JOBS) break;
|
if (s_job_count >= MAX_CRON_JOBS) break;
|
||||||
@@ -100,6 +131,9 @@ static esp_err_t cron_load_jobs(void)
|
|||||||
sizeof(job->channel) - 1);
|
sizeof(job->channel) - 1);
|
||||||
strncpy(job->chat_id, chat_id ? chat_id : "cron",
|
strncpy(job->chat_id, chat_id ? chat_id : "cron",
|
||||||
sizeof(job->chat_id) - 1);
|
sizeof(job->chat_id) - 1);
|
||||||
|
if (cron_sanitize_destination(job)) {
|
||||||
|
repaired = true;
|
||||||
|
}
|
||||||
|
|
||||||
cJSON *enabled_j = cJSON_GetObjectItem(item, "enabled");
|
cJSON *enabled_j = cJSON_GetObjectItem(item, "enabled");
|
||||||
job->enabled = enabled_j ? cJSON_IsTrue(enabled_j) : true;
|
job->enabled = enabled_j ? cJSON_IsTrue(enabled_j) : true;
|
||||||
@@ -133,6 +167,9 @@ static esp_err_t cron_load_jobs(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
cJSON_Delete(root);
|
cJSON_Delete(root);
|
||||||
|
if (repaired) {
|
||||||
|
cron_save_jobs();
|
||||||
|
}
|
||||||
ESP_LOGI(TAG, "Loaded %d cron jobs", s_job_count);
|
ESP_LOGI(TAG, "Loaded %d cron jobs", s_job_count);
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
@@ -354,13 +391,8 @@ esp_err_t cron_add_job(cron_job_t *job)
|
|||||||
/* Generate ID */
|
/* Generate ID */
|
||||||
cron_generate_id(job->id);
|
cron_generate_id(job->id);
|
||||||
|
|
||||||
/* Set defaults for channel/chat_id if empty */
|
/* Validate/sanitize channel and chat_id before storing. */
|
||||||
if (job->channel[0] == '\0') {
|
cron_sanitize_destination(job);
|
||||||
strncpy(job->channel, MIMI_CHAN_SYSTEM, sizeof(job->channel) - 1);
|
|
||||||
}
|
|
||||||
if (job->chat_id[0] == '\0') {
|
|
||||||
strncpy(job->chat_id, "cron", sizeof(job->chat_id) - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Compute initial next_run */
|
/* Compute initial next_run */
|
||||||
job->enabled = true;
|
job->enabled = true;
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
#define MIMI_AGENT_MAX_HISTORY 20
|
#define MIMI_AGENT_MAX_HISTORY 20
|
||||||
#define MIMI_AGENT_MAX_TOOL_ITER 10
|
#define MIMI_AGENT_MAX_TOOL_ITER 10
|
||||||
#define MIMI_MAX_TOOL_CALLS 4
|
#define MIMI_MAX_TOOL_CALLS 4
|
||||||
#define MIMI_AGENT_SEND_WORKING_STATUS 0
|
#define MIMI_AGENT_SEND_WORKING_STATUS 1
|
||||||
|
|
||||||
/* Timezone (POSIX TZ format) */
|
/* Timezone (POSIX TZ format) */
|
||||||
#define MIMI_TIMEZONE "PST8PDT,M3.2.0,M11.1.0"
|
#define MIMI_TIMEZONE "PST8PDT,M3.2.0,M11.1.0"
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
#define MIMI_LLM_API_VERSION "2023-06-01"
|
#define MIMI_LLM_API_VERSION "2023-06-01"
|
||||||
#define MIMI_LLM_STREAM_BUF_SIZE (32 * 1024)
|
#define MIMI_LLM_STREAM_BUF_SIZE (32 * 1024)
|
||||||
#define MIMI_LLM_LOG_VERBOSE_PAYLOAD 0
|
#define MIMI_LLM_LOG_VERBOSE_PAYLOAD 0
|
||||||
#define MIMI_LLM_LOG_PREVIEW_BYTES 256
|
#define MIMI_LLM_LOG_PREVIEW_BYTES 160
|
||||||
|
|
||||||
/* Message Bus */
|
/* Message Bus */
|
||||||
#define MIMI_BUS_QUEUE_LEN 16
|
#define MIMI_BUS_QUEUE_LEN 16
|
||||||
|
|||||||
Reference in New Issue
Block a user