362 lines
13 KiB
C
362 lines
13 KiB
C
#include "agent_loop.h"
|
|
#include "agent/context_builder.h"
|
|
#include "mimi_config.h"
|
|
#include "bus/message_bus.h"
|
|
#include "llm/llm_proxy.h"
|
|
#include "memory/session_mgr.h"
|
|
#include "tools/tool_registry.h"
|
|
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
#include "esp_log.h"
|
|
#include "esp_heap_caps.h"
|
|
#include "cJSON.h"
|
|
|
|
static const char *TAG = "agent";
|
|
|
|
#define TOOL_OUTPUT_SIZE (8 * 1024)
|
|
|
|
/* Build the assistant content array from llm_response_t for the messages history.
|
|
* Returns a cJSON array with text and tool_use blocks. */
|
|
static cJSON *build_assistant_content(const llm_response_t *resp)
|
|
{
|
|
cJSON *content = cJSON_CreateArray();
|
|
|
|
/* Text block */
|
|
if (resp->text && resp->text_len > 0) {
|
|
cJSON *text_block = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(text_block, "type", "text");
|
|
cJSON_AddStringToObject(text_block, "text", resp->text);
|
|
cJSON_AddItemToArray(content, text_block);
|
|
}
|
|
|
|
/* Tool use blocks */
|
|
for (int i = 0; i < resp->call_count; i++) {
|
|
const llm_tool_call_t *call = &resp->calls[i];
|
|
cJSON *tool_block = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(tool_block, "type", "tool_use");
|
|
cJSON_AddStringToObject(tool_block, "id", call->id);
|
|
cJSON_AddStringToObject(tool_block, "name", call->name);
|
|
|
|
cJSON *input = cJSON_Parse(call->input);
|
|
if (input) {
|
|
cJSON_AddItemToObject(tool_block, "input", input);
|
|
} else {
|
|
cJSON_AddItemToObject(tool_block, "input", cJSON_CreateObject());
|
|
}
|
|
|
|
cJSON_AddItemToArray(content, tool_block);
|
|
}
|
|
|
|
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 */
|
|
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();
|
|
|
|
for (int i = 0; i < resp->call_count; 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 */
|
|
tool_output[0] = '\0';
|
|
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));
|
|
|
|
/* Build tool_result block */
|
|
cJSON *result_block = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(result_block, "type", "tool_result");
|
|
cJSON_AddStringToObject(result_block, "tool_use_id", call->id);
|
|
cJSON_AddStringToObject(result_block, "content", tool_output);
|
|
cJSON_AddItemToArray(content, result_block);
|
|
}
|
|
|
|
return content;
|
|
}
|
|
|
|
static void agent_loop_task(void *arg)
|
|
{
|
|
ESP_LOGI(TAG, "Agent loop started on core %d", xPortGetCoreID());
|
|
|
|
char *system_prompt = calloc(1, MIMI_CONTEXT_BUF_SIZE);
|
|
char *history_json = calloc(1, MIMI_LLM_STREAM_BUF_SIZE);
|
|
char *tool_output = calloc(1, TOOL_OUTPUT_SIZE);
|
|
|
|
if (!system_prompt || !history_json || !tool_output) {
|
|
ESP_LOGE(TAG, "Failed to allocate runtime buffers");
|
|
vTaskDelete(NULL);
|
|
return;
|
|
}
|
|
|
|
const char *tools_json = tool_registry_get_tools_json();
|
|
|
|
while (1) {
|
|
mimi_msg_t msg;
|
|
esp_err_t err = message_bus_pop_inbound(&msg, UINT32_MAX);
|
|
if (err != ESP_OK) continue;
|
|
|
|
ESP_LOGI(TAG, "Processing message from %s:%s", msg.channel, msg.chat_id);
|
|
|
|
/* 1. Build system prompt */
|
|
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 */
|
|
session_get_history_json(msg.chat_id, history_json,
|
|
MIMI_LLM_STREAM_BUF_SIZE, MIMI_AGENT_MAX_HISTORY);
|
|
|
|
cJSON *messages = cJSON_Parse(history_json);
|
|
if (!messages) messages = cJSON_CreateArray();
|
|
|
|
/* 3. Append current user message */
|
|
cJSON *user_msg = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(user_msg, "role", "user");
|
|
cJSON_AddStringToObject(user_msg, "content", msg.content);
|
|
cJSON_AddItemToArray(messages, user_msg);
|
|
|
|
/* 4. ReAct loop */
|
|
char *final_text = NULL;
|
|
int iteration = 0;
|
|
bool sent_working_status = false;
|
|
|
|
while (iteration < MIMI_AGENT_MAX_TOOL_ITER) {
|
|
/* Send "working" indicator before each API call */
|
|
#if MIMI_AGENT_SEND_WORKING_STATUS
|
|
if (!sent_working_status && strcmp(msg.channel, MIMI_CHAN_SYSTEM) != 0) {
|
|
mimi_msg_t status = {0};
|
|
strncpy(status.channel, msg.channel, sizeof(status.channel) - 1);
|
|
strncpy(status.chat_id, msg.chat_id, sizeof(status.chat_id) - 1);
|
|
status.content = strdup("\xF0\x9F\x90\xB1mimi is working...");
|
|
if (status.content) {
|
|
if (message_bus_push_outbound(&status) != ESP_OK) {
|
|
ESP_LOGW(TAG, "Outbound queue full, drop working status");
|
|
free(status.content);
|
|
} else {
|
|
sent_working_status = true;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
llm_response_t resp;
|
|
err = llm_chat_tools(system_prompt, messages, tools_json, &resp);
|
|
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "LLM call failed: %s", esp_err_to_name(err));
|
|
break;
|
|
}
|
|
|
|
if (!resp.tool_use) {
|
|
/* Normal completion — save final text and break */
|
|
if (resp.text && resp.text_len > 0) {
|
|
final_text = strdup(resp.text);
|
|
}
|
|
llm_response_free(&resp);
|
|
break;
|
|
}
|
|
|
|
ESP_LOGI(TAG, "Tool use iteration %d: %d calls", iteration + 1, resp.call_count);
|
|
|
|
/* Append assistant message with content array */
|
|
cJSON *asst_msg = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(asst_msg, "role", "assistant");
|
|
cJSON_AddItemToObject(asst_msg, "content", build_assistant_content(&resp));
|
|
cJSON_AddItemToArray(messages, asst_msg);
|
|
|
|
/* Execute tools and append results */
|
|
cJSON *tool_results = build_tool_results(&resp, &msg, tool_output, TOOL_OUTPUT_SIZE);
|
|
cJSON *result_msg = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(result_msg, "role", "user");
|
|
cJSON_AddItemToObject(result_msg, "content", tool_results);
|
|
cJSON_AddItemToArray(messages, result_msg);
|
|
|
|
llm_response_free(&resp);
|
|
iteration++;
|
|
}
|
|
|
|
cJSON_Delete(messages);
|
|
|
|
/* 5. Send response */
|
|
if (final_text && final_text[0]) {
|
|
/* Save to session (only user text + final assistant text) */
|
|
esp_err_t save_user = session_append(msg.chat_id, "user", msg.content);
|
|
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 */
|
|
mimi_msg_t out = {0};
|
|
strncpy(out.channel, msg.channel, sizeof(out.channel) - 1);
|
|
strncpy(out.chat_id, msg.chat_id, sizeof(out.chat_id) - 1);
|
|
out.content = final_text; /* transfer ownership */
|
|
ESP_LOGI(TAG, "Queue final response to %s:%s (%d bytes)",
|
|
out.channel, out.chat_id, (int)strlen(final_text));
|
|
if (message_bus_push_outbound(&out) != ESP_OK) {
|
|
ESP_LOGW(TAG, "Outbound queue full, drop final response");
|
|
free(final_text);
|
|
} else {
|
|
final_text = NULL;
|
|
}
|
|
} else {
|
|
/* Error or empty response */
|
|
free(final_text);
|
|
mimi_msg_t out = {0};
|
|
strncpy(out.channel, msg.channel, sizeof(out.channel) - 1);
|
|
strncpy(out.chat_id, msg.chat_id, sizeof(out.chat_id) - 1);
|
|
out.content = strdup("Sorry, I encountered an error.");
|
|
if (out.content) {
|
|
if (message_bus_push_outbound(&out) != ESP_OK) {
|
|
ESP_LOGW(TAG, "Outbound queue full, drop error response");
|
|
free(out.content);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Free inbound message content */
|
|
free(msg.content);
|
|
|
|
/* Log memory status */
|
|
ESP_LOGI(TAG, "Free internal heap: %d bytes",
|
|
(int)heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
|
|
}
|
|
}
|
|
|
|
esp_err_t agent_loop_init(void)
|
|
{
|
|
ESP_LOGI(TAG, "Agent loop initialized");
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t agent_loop_start(void)
|
|
{
|
|
const uint32_t stack_candidates[] = {
|
|
MIMI_AGENT_STACK,
|
|
20 * 1024,
|
|
16 * 1024,
|
|
14 * 1024,
|
|
12 * 1024,
|
|
};
|
|
|
|
for (size_t i = 0; i < (sizeof(stack_candidates) / sizeof(stack_candidates[0])); i++) {
|
|
uint32_t stack_size = stack_candidates[i];
|
|
BaseType_t ret = xTaskCreatePinnedToCore(
|
|
agent_loop_task, "agent_loop",
|
|
stack_size, NULL,
|
|
MIMI_AGENT_PRIO, NULL, MIMI_AGENT_CORE);
|
|
|
|
if (ret == pdPASS) {
|
|
ESP_LOGI(TAG, "agent_loop task created with stack=%u bytes", (unsigned)stack_size);
|
|
return ESP_OK;
|
|
}
|
|
|
|
ESP_LOGW(TAG,
|
|
"agent_loop create failed (stack=%u, free_internal=%u, largest_internal=%u), retrying...",
|
|
(unsigned)stack_size,
|
|
(unsigned)heap_caps_get_free_size(MALLOC_CAP_INTERNAL),
|
|
(unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL));
|
|
}
|
|
|
|
return ESP_FAIL;
|
|
}
|