feat: add file tools (read/write/edit/list_dir) for agent memory writes
Enable the agent to persist memories by adding 4 SPIFFS file tools (read_file, write_file, edit_file, list_dir) with path validation, and update the system prompt with memory guidelines pointing to /spiffs/memory/MEMORY.md and daily notes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ idf_component_register(
|
||||
"tools/tool_registry.c"
|
||||
"tools/tool_web_search.c"
|
||||
"tools/tool_get_time.c"
|
||||
"tools/tool_files.c"
|
||||
INCLUDE_DIRS
|
||||
"."
|
||||
REQUIRES
|
||||
|
||||
@@ -39,8 +39,17 @@ esp_err_t context_build_system_prompt(char *buf, size_t size)
|
||||
"- web_search: Search the web for current information. "
|
||||
"Use this when you need up-to-date facts, news, weather, or anything beyond your training data.\n"
|
||||
"- get_current_time: Get the current date and time. "
|
||||
"You do NOT have an internal clock — always use this tool when you need to know the time or date.\n\n"
|
||||
"Use tools when needed. Provide your final answer as text after using tools.\n");
|
||||
"You do NOT have an internal clock — always use this tool when you need to know the time or date.\n"
|
||||
"- read_file: Read a file from SPIFFS (path must start with /spiffs/).\n"
|
||||
"- write_file: Write/overwrite a file on SPIFFS.\n"
|
||||
"- edit_file: Find-and-replace edit a file on SPIFFS.\n"
|
||||
"- list_dir: List files on SPIFFS, optionally filter by prefix.\n\n"
|
||||
"Use tools when needed. Provide your final answer as text after using tools.\n\n"
|
||||
"## Memory Guidelines\n"
|
||||
"Your long-term memory is at /spiffs/memory/MEMORY.md — use write_file or edit_file to update it.\n"
|
||||
"Daily notes are at /spiffs/memory/daily/<YYYY-MM-DD>.md — use get_current_time for today's date, then write_file to create/append.\n"
|
||||
"When you learn something important about the user or need to remember something, persist it to memory.\n"
|
||||
"Read /spiffs/memory/MEMORY.md first before writing, so you can append or edit without losing existing content.\n");
|
||||
|
||||
/* Bootstrap files */
|
||||
off = append_file(buf, size, off, MIMI_SOUL_FILE, "Personality");
|
||||
|
||||
259
main/tools/tool_files.c
Normal file
259
main/tools/tool_files.c
Normal file
@@ -0,0 +1,259 @@
|
||||
#include "tools/tool_files.h"
|
||||
#include "mimi_config.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include "esp_log.h"
|
||||
#include "cJSON.h"
|
||||
|
||||
static const char *TAG = "tool_files";
|
||||
|
||||
#define MAX_FILE_SIZE (32 * 1024)
|
||||
|
||||
/**
|
||||
* Validate that a path starts with /spiffs/ and contains no ".." traversal.
|
||||
*/
|
||||
static bool validate_path(const char *path)
|
||||
{
|
||||
if (!path) return false;
|
||||
if (strncmp(path, "/spiffs/", 8) != 0) return false;
|
||||
if (strstr(path, "..") != NULL) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ── read_file ─────────────────────────────────────────────── */
|
||||
|
||||
esp_err_t tool_read_file_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 *path = cJSON_GetStringValue(cJSON_GetObjectItem(root, "path"));
|
||||
if (!validate_path(path)) {
|
||||
snprintf(output, output_size, "Error: path must start with /spiffs/ and must not contain '..'");
|
||||
cJSON_Delete(root);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) {
|
||||
snprintf(output, output_size, "Error: file not found: %s", path);
|
||||
cJSON_Delete(root);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
size_t max_read = output_size - 1;
|
||||
if (max_read > MAX_FILE_SIZE) max_read = MAX_FILE_SIZE;
|
||||
|
||||
size_t n = fread(output, 1, max_read, f);
|
||||
output[n] = '\0';
|
||||
fclose(f);
|
||||
|
||||
ESP_LOGI(TAG, "read_file: %s (%d bytes)", path, (int)n);
|
||||
cJSON_Delete(root);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ── write_file ────────────────────────────────────────────── */
|
||||
|
||||
esp_err_t tool_write_file_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 *path = cJSON_GetStringValue(cJSON_GetObjectItem(root, "path"));
|
||||
const char *content = cJSON_GetStringValue(cJSON_GetObjectItem(root, "content"));
|
||||
|
||||
if (!validate_path(path)) {
|
||||
snprintf(output, output_size, "Error: path must start with /spiffs/ and must not contain '..'");
|
||||
cJSON_Delete(root);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (!content) {
|
||||
snprintf(output, output_size, "Error: missing 'content' field");
|
||||
cJSON_Delete(root);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f) {
|
||||
snprintf(output, output_size, "Error: cannot open file for writing: %s", path);
|
||||
cJSON_Delete(root);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
size_t len = strlen(content);
|
||||
size_t written = fwrite(content, 1, len, f);
|
||||
fclose(f);
|
||||
|
||||
if (written != len) {
|
||||
snprintf(output, output_size, "Error: wrote %d of %d bytes to %s", (int)written, (int)len, path);
|
||||
cJSON_Delete(root);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
snprintf(output, output_size, "OK: wrote %d bytes to %s", (int)written, path);
|
||||
ESP_LOGI(TAG, "write_file: %s (%d bytes)", path, (int)written);
|
||||
cJSON_Delete(root);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ── edit_file ─────────────────────────────────────────────── */
|
||||
|
||||
esp_err_t tool_edit_file_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 *path = cJSON_GetStringValue(cJSON_GetObjectItem(root, "path"));
|
||||
const char *old_str = cJSON_GetStringValue(cJSON_GetObjectItem(root, "old_string"));
|
||||
const char *new_str = cJSON_GetStringValue(cJSON_GetObjectItem(root, "new_string"));
|
||||
|
||||
if (!validate_path(path)) {
|
||||
snprintf(output, output_size, "Error: path must start with /spiffs/ and must not contain '..'");
|
||||
cJSON_Delete(root);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (!old_str || !new_str) {
|
||||
snprintf(output, output_size, "Error: missing 'old_string' or 'new_string' field");
|
||||
cJSON_Delete(root);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* Read existing file */
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) {
|
||||
snprintf(output, output_size, "Error: file not found: %s", path);
|
||||
cJSON_Delete(root);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
fseek(f, 0, SEEK_END);
|
||||
long file_size = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
|
||||
if (file_size <= 0 || file_size > MAX_FILE_SIZE) {
|
||||
snprintf(output, output_size, "Error: file too large or empty (%ld bytes)", file_size);
|
||||
fclose(f);
|
||||
cJSON_Delete(root);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
/* Allocate buffer for the result (old content + possible expansion) */
|
||||
size_t old_len = strlen(old_str);
|
||||
size_t new_len = strlen(new_str);
|
||||
size_t max_result = file_size + (new_len > old_len ? new_len - old_len : 0) + 1;
|
||||
char *buf = malloc(file_size + 1);
|
||||
char *result = malloc(max_result);
|
||||
if (!buf || !result) {
|
||||
free(buf);
|
||||
free(result);
|
||||
fclose(f);
|
||||
snprintf(output, output_size, "Error: out of memory");
|
||||
cJSON_Delete(root);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
size_t n = fread(buf, 1, file_size, f);
|
||||
buf[n] = '\0';
|
||||
fclose(f);
|
||||
|
||||
/* Find and replace first occurrence */
|
||||
char *pos = strstr(buf, old_str);
|
||||
if (!pos) {
|
||||
snprintf(output, output_size, "Error: old_string not found in %s", path);
|
||||
free(buf);
|
||||
free(result);
|
||||
cJSON_Delete(root);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
size_t prefix_len = pos - buf;
|
||||
memcpy(result, buf, prefix_len);
|
||||
memcpy(result + prefix_len, new_str, new_len);
|
||||
size_t suffix_start = prefix_len + old_len;
|
||||
size_t suffix_len = n - suffix_start;
|
||||
memcpy(result + prefix_len + new_len, buf + suffix_start, suffix_len);
|
||||
size_t total = prefix_len + new_len + suffix_len;
|
||||
result[total] = '\0';
|
||||
|
||||
free(buf);
|
||||
|
||||
/* Write back */
|
||||
f = fopen(path, "w");
|
||||
if (!f) {
|
||||
snprintf(output, output_size, "Error: cannot open file for writing: %s", path);
|
||||
free(result);
|
||||
cJSON_Delete(root);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
fwrite(result, 1, total, f);
|
||||
fclose(f);
|
||||
free(result);
|
||||
|
||||
snprintf(output, output_size, "OK: edited %s (replaced %d bytes with %d bytes)", path, (int)old_len, (int)new_len);
|
||||
ESP_LOGI(TAG, "edit_file: %s", path);
|
||||
cJSON_Delete(root);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ── list_dir ──────────────────────────────────────────────── */
|
||||
|
||||
esp_err_t tool_list_dir_execute(const char *input_json, char *output, size_t output_size)
|
||||
{
|
||||
cJSON *root = cJSON_Parse(input_json);
|
||||
const char *prefix = NULL;
|
||||
if (root) {
|
||||
cJSON *pfx = cJSON_GetObjectItem(root, "prefix");
|
||||
if (pfx && cJSON_IsString(pfx)) {
|
||||
prefix = pfx->valuestring;
|
||||
}
|
||||
}
|
||||
|
||||
DIR *dir = opendir(MIMI_SPIFFS_BASE);
|
||||
if (!dir) {
|
||||
snprintf(output, output_size, "Error: cannot open /spiffs directory");
|
||||
cJSON_Delete(root);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
size_t off = 0;
|
||||
struct dirent *ent;
|
||||
int count = 0;
|
||||
|
||||
while ((ent = readdir(dir)) != NULL && off < output_size - 1) {
|
||||
/* Build full path: SPIFFS entries are just filenames with embedded slashes */
|
||||
char full_path[512];
|
||||
snprintf(full_path, sizeof(full_path), "%s/%s", MIMI_SPIFFS_BASE, ent->d_name);
|
||||
|
||||
if (prefix && strncmp(full_path, prefix, strlen(prefix)) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
off += snprintf(output + off, output_size - off, "%s\n", full_path);
|
||||
count++;
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
|
||||
if (count == 0) {
|
||||
snprintf(output, output_size, "(no files found)");
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "list_dir: %d files (prefix=%s)", count, prefix ? prefix : "(none)");
|
||||
cJSON_Delete(root);
|
||||
return ESP_OK;
|
||||
}
|
||||
28
main/tools/tool_files.h
Normal file
28
main/tools/tool_files.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stddef.h>
|
||||
|
||||
/**
|
||||
* Read a file from SPIFFS.
|
||||
* Input JSON: {"path": "/spiffs/..."}
|
||||
*/
|
||||
esp_err_t tool_read_file_execute(const char *input_json, char *output, size_t output_size);
|
||||
|
||||
/**
|
||||
* Write/overwrite a file on SPIFFS.
|
||||
* Input JSON: {"path": "/spiffs/...", "content": "..."}
|
||||
*/
|
||||
esp_err_t tool_write_file_execute(const char *input_json, char *output, size_t output_size);
|
||||
|
||||
/**
|
||||
* Find-and-replace edit a file on SPIFFS.
|
||||
* Input JSON: {"path": "/spiffs/...", "old_string": "...", "new_string": "..."}
|
||||
*/
|
||||
esp_err_t tool_edit_file_execute(const char *input_json, char *output, size_t output_size);
|
||||
|
||||
/**
|
||||
* List files on SPIFFS, optionally filtered by path prefix.
|
||||
* Input JSON: {"prefix": "/spiffs/..."} (prefix is optional)
|
||||
*/
|
||||
esp_err_t tool_list_dir_execute(const char *input_json, char *output, size_t output_size);
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "tool_registry.h"
|
||||
#include "tools/tool_web_search.h"
|
||||
#include "tools/tool_get_time.h"
|
||||
#include "tools/tool_files.h"
|
||||
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
@@ -78,6 +79,57 @@ esp_err_t tool_registry_init(void)
|
||||
};
|
||||
register_tool(>);
|
||||
|
||||
/* Register read_file */
|
||||
mimi_tool_t rf = {
|
||||
.name = "read_file",
|
||||
.description = "Read a file from SPIFFS storage. Path must start with /spiffs/.",
|
||||
.input_schema_json =
|
||||
"{\"type\":\"object\","
|
||||
"\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Absolute path starting with /spiffs/\"}},"
|
||||
"\"required\":[\"path\"]}",
|
||||
.execute = tool_read_file_execute,
|
||||
};
|
||||
register_tool(&rf);
|
||||
|
||||
/* Register write_file */
|
||||
mimi_tool_t wf = {
|
||||
.name = "write_file",
|
||||
.description = "Write or overwrite a file on SPIFFS storage. Path must start with /spiffs/.",
|
||||
.input_schema_json =
|
||||
"{\"type\":\"object\","
|
||||
"\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Absolute path starting with /spiffs/\"},"
|
||||
"\"content\":{\"type\":\"string\",\"description\":\"File content to write\"}},"
|
||||
"\"required\":[\"path\",\"content\"]}",
|
||||
.execute = tool_write_file_execute,
|
||||
};
|
||||
register_tool(&wf);
|
||||
|
||||
/* Register edit_file */
|
||||
mimi_tool_t ef = {
|
||||
.name = "edit_file",
|
||||
.description = "Find and replace text in a file on SPIFFS. Replaces first occurrence of old_string with new_string.",
|
||||
.input_schema_json =
|
||||
"{\"type\":\"object\","
|
||||
"\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Absolute path starting with /spiffs/\"},"
|
||||
"\"old_string\":{\"type\":\"string\",\"description\":\"Text to find\"},"
|
||||
"\"new_string\":{\"type\":\"string\",\"description\":\"Replacement text\"}},"
|
||||
"\"required\":[\"path\",\"old_string\",\"new_string\"]}",
|
||||
.execute = tool_edit_file_execute,
|
||||
};
|
||||
register_tool(&ef);
|
||||
|
||||
/* Register list_dir */
|
||||
mimi_tool_t ld = {
|
||||
.name = "list_dir",
|
||||
.description = "List files on SPIFFS storage, optionally filtered by path prefix.",
|
||||
.input_schema_json =
|
||||
"{\"type\":\"object\","
|
||||
"\"properties\":{\"prefix\":{\"type\":\"string\",\"description\":\"Optional path prefix filter, e.g. /spiffs/memory/\"}},"
|
||||
"\"required\":[]}",
|
||||
.execute = tool_list_dir_execute,
|
||||
};
|
||||
register_tool(&ld);
|
||||
|
||||
build_tools_json();
|
||||
|
||||
ESP_LOGI(TAG, "Tool registry initialized");
|
||||
|
||||
Reference in New Issue
Block a user