feat: add memory store and session manager
MEMORY.md for long-term memory, daily YYYY-MM-DD.md notes. JSONL session files per chat_id with ring buffer history (max 20 messages). All persisted on SPIFFS. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
107
main/memory/memory_store.c
Normal file
107
main/memory/memory_store.c
Normal file
@@ -0,0 +1,107 @@
|
||||
#include "memory_store.h"
|
||||
#include "mimi_config.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <sys/stat.h>
|
||||
#include "esp_log.h"
|
||||
|
||||
static const char *TAG = "memory";
|
||||
|
||||
static void get_date_str(char *buf, size_t size, int days_ago)
|
||||
{
|
||||
time_t now;
|
||||
time(&now);
|
||||
now -= days_ago * 86400;
|
||||
struct tm tm;
|
||||
localtime_r(&now, &tm);
|
||||
strftime(buf, size, "%Y-%m-%d", &tm);
|
||||
}
|
||||
|
||||
esp_err_t memory_store_init(void)
|
||||
{
|
||||
/* SPIFFS is flat — no real directory creation needed.
|
||||
Just verify we can open the base path. */
|
||||
ESP_LOGI(TAG, "Memory store initialized at %s", MIMI_SPIFFS_BASE);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t memory_read_long_term(char *buf, size_t size)
|
||||
{
|
||||
FILE *f = fopen(MIMI_MEMORY_FILE, "r");
|
||||
if (!f) {
|
||||
buf[0] = '\0';
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
size_t n = fread(buf, 1, size - 1, f);
|
||||
buf[n] = '\0';
|
||||
fclose(f);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t memory_write_long_term(const char *content)
|
||||
{
|
||||
FILE *f = fopen(MIMI_MEMORY_FILE, "w");
|
||||
if (!f) {
|
||||
ESP_LOGE(TAG, "Cannot write %s", MIMI_MEMORY_FILE);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
fputs(content, f);
|
||||
fclose(f);
|
||||
ESP_LOGI(TAG, "Long-term memory updated (%d bytes)", (int)strlen(content));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t memory_append_today(const char *note)
|
||||
{
|
||||
char date_str[16];
|
||||
get_date_str(date_str, sizeof(date_str), 0);
|
||||
|
||||
char path[64];
|
||||
snprintf(path, sizeof(path), "%s/%s.md", MIMI_SPIFFS_MEMORY_DIR, date_str);
|
||||
|
||||
FILE *f = fopen(path, "a");
|
||||
if (!f) {
|
||||
/* Try creating — if file doesn't exist yet, write header */
|
||||
f = fopen(path, "w");
|
||||
if (!f) {
|
||||
ESP_LOGE(TAG, "Cannot open %s", path);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
fprintf(f, "# %s\n\n", date_str);
|
||||
}
|
||||
|
||||
fprintf(f, "%s\n", note);
|
||||
fclose(f);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t memory_read_recent(char *buf, size_t size, int days)
|
||||
{
|
||||
size_t offset = 0;
|
||||
buf[0] = '\0';
|
||||
|
||||
for (int i = 0; i < days && offset < size - 1; i++) {
|
||||
char date_str[16];
|
||||
get_date_str(date_str, sizeof(date_str), i);
|
||||
|
||||
char path[64];
|
||||
snprintf(path, sizeof(path), "%s/%s.md", MIMI_SPIFFS_MEMORY_DIR, date_str);
|
||||
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) continue;
|
||||
|
||||
if (offset > 0 && offset < size - 4) {
|
||||
offset += snprintf(buf + offset, size - offset, "\n---\n");
|
||||
}
|
||||
|
||||
size_t n = fread(buf + offset, 1, size - offset - 1, f);
|
||||
offset += n;
|
||||
buf[offset] = '\0';
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
31
main/memory/memory_store.h
Normal file
31
main/memory/memory_store.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stddef.h>
|
||||
|
||||
/**
|
||||
* Initialize memory store. Ensures SPIFFS directories exist.
|
||||
*/
|
||||
esp_err_t memory_store_init(void);
|
||||
|
||||
/**
|
||||
* Read long-term memory (MEMORY.md) into buffer.
|
||||
* @return ESP_OK on success, ESP_ERR_NOT_FOUND if file missing
|
||||
*/
|
||||
esp_err_t memory_read_long_term(char *buf, size_t size);
|
||||
|
||||
/**
|
||||
* Write content to long-term memory (MEMORY.md).
|
||||
*/
|
||||
esp_err_t memory_write_long_term(const char *content);
|
||||
|
||||
/**
|
||||
* Append a note to today's daily memory file (YYYY-MM-DD.md).
|
||||
*/
|
||||
esp_err_t memory_append_today(const char *note);
|
||||
|
||||
/**
|
||||
* Read recent daily memories (last N days) into buffer.
|
||||
* @param days Number of days to look back (default 3)
|
||||
*/
|
||||
esp_err_t memory_read_recent(char *buf, size_t size, int days);
|
||||
165
main/memory/session_mgr.c
Normal file
165
main/memory/session_mgr.c
Normal file
@@ -0,0 +1,165 @@
|
||||
#include "session_mgr.h"
|
||||
#include "mimi_config.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <dirent.h>
|
||||
#include <time.h>
|
||||
#include "esp_log.h"
|
||||
#include "cJSON.h"
|
||||
|
||||
static const char *TAG = "session";
|
||||
|
||||
static void session_path(const char *chat_id, char *buf, size_t size)
|
||||
{
|
||||
snprintf(buf, size, "%s/tg_%s.jsonl", MIMI_SPIFFS_SESSION_DIR, chat_id);
|
||||
}
|
||||
|
||||
esp_err_t session_mgr_init(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Session manager initialized at %s", MIMI_SPIFFS_SESSION_DIR);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t session_append(const char *chat_id, const char *role, const char *content)
|
||||
{
|
||||
char path[64];
|
||||
session_path(chat_id, path, sizeof(path));
|
||||
|
||||
FILE *f = fopen(path, "a");
|
||||
if (!f) {
|
||||
ESP_LOGE(TAG, "Cannot open session file %s", path);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
cJSON *obj = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(obj, "role", role);
|
||||
cJSON_AddStringToObject(obj, "content", content);
|
||||
cJSON_AddNumberToObject(obj, "ts", (double)time(NULL));
|
||||
|
||||
char *line = cJSON_PrintUnformatted(obj);
|
||||
cJSON_Delete(obj);
|
||||
|
||||
if (line) {
|
||||
fprintf(f, "%s\n", line);
|
||||
free(line);
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t session_get_history_json(const char *chat_id, char *buf, size_t size, int max_msgs)
|
||||
{
|
||||
char path[64];
|
||||
session_path(chat_id, path, sizeof(path));
|
||||
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) {
|
||||
/* No history yet */
|
||||
snprintf(buf, size, "[]");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Read all lines into a ring buffer of cJSON objects */
|
||||
cJSON *messages[MIMI_SESSION_MAX_MSGS];
|
||||
int count = 0;
|
||||
int write_idx = 0;
|
||||
|
||||
char line[2048];
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
/* Strip newline */
|
||||
size_t len = strlen(line);
|
||||
if (len > 0 && line[len - 1] == '\n') line[len - 1] = '\0';
|
||||
if (line[0] == '\0') continue;
|
||||
|
||||
cJSON *obj = cJSON_Parse(line);
|
||||
if (!obj) continue;
|
||||
|
||||
/* Ring buffer: overwrite oldest if full */
|
||||
if (count >= max_msgs) {
|
||||
cJSON_Delete(messages[write_idx]);
|
||||
}
|
||||
messages[write_idx] = obj;
|
||||
write_idx = (write_idx + 1) % max_msgs;
|
||||
if (count < max_msgs) count++;
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
/* Build JSON array with only role + content */
|
||||
cJSON *arr = cJSON_CreateArray();
|
||||
int start = (count < max_msgs) ? 0 : write_idx;
|
||||
for (int i = 0; i < count; i++) {
|
||||
int idx = (start + i) % max_msgs;
|
||||
cJSON *src = messages[idx];
|
||||
|
||||
cJSON *entry = cJSON_CreateObject();
|
||||
cJSON *role = cJSON_GetObjectItem(src, "role");
|
||||
cJSON *content = cJSON_GetObjectItem(src, "content");
|
||||
if (role && content) {
|
||||
cJSON_AddStringToObject(entry, "role", role->valuestring);
|
||||
cJSON_AddStringToObject(entry, "content", content->valuestring);
|
||||
}
|
||||
cJSON_AddItemToArray(arr, entry);
|
||||
}
|
||||
|
||||
/* Cleanup ring buffer */
|
||||
int cleanup_start = (count < max_msgs) ? 0 : write_idx;
|
||||
for (int i = 0; i < count; i++) {
|
||||
int idx = (cleanup_start + i) % max_msgs;
|
||||
cJSON_Delete(messages[idx]);
|
||||
}
|
||||
|
||||
char *json_str = cJSON_PrintUnformatted(arr);
|
||||
cJSON_Delete(arr);
|
||||
|
||||
if (json_str) {
|
||||
strncpy(buf, json_str, size - 1);
|
||||
buf[size - 1] = '\0';
|
||||
free(json_str);
|
||||
} else {
|
||||
snprintf(buf, size, "[]");
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t session_clear(const char *chat_id)
|
||||
{
|
||||
char path[64];
|
||||
session_path(chat_id, path, sizeof(path));
|
||||
|
||||
if (remove(path) == 0) {
|
||||
ESP_LOGI(TAG, "Session %s cleared", chat_id);
|
||||
return ESP_OK;
|
||||
}
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
void session_list(void)
|
||||
{
|
||||
DIR *dir = opendir(MIMI_SPIFFS_SESSION_DIR);
|
||||
if (!dir) {
|
||||
/* SPIFFS is flat, so list all files matching pattern */
|
||||
dir = opendir(MIMI_SPIFFS_BASE);
|
||||
if (!dir) {
|
||||
ESP_LOGW(TAG, "Cannot open SPIFFS directory");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
struct dirent *entry;
|
||||
int count = 0;
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
if (strstr(entry->d_name, "tg_") && strstr(entry->d_name, ".jsonl")) {
|
||||
ESP_LOGI(TAG, " Session: %s", entry->d_name);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
|
||||
if (count == 0) {
|
||||
ESP_LOGI(TAG, " No sessions found");
|
||||
}
|
||||
}
|
||||
39
main/memory/session_mgr.h
Normal file
39
main/memory/session_mgr.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stddef.h>
|
||||
|
||||
/**
|
||||
* Initialize session manager.
|
||||
*/
|
||||
esp_err_t session_mgr_init(void);
|
||||
|
||||
/**
|
||||
* Append a message to a session file (JSONL format).
|
||||
* @param chat_id Session identifier (e.g., "12345")
|
||||
* @param role "user" or "assistant"
|
||||
* @param content Message text
|
||||
*/
|
||||
esp_err_t session_append(const char *chat_id, const char *role, const char *content);
|
||||
|
||||
/**
|
||||
* Load session history as a JSON array string suitable for LLM messages.
|
||||
* Returns the last max_msgs messages as:
|
||||
* [{"role":"user","content":"..."},{"role":"assistant","content":"..."},...]
|
||||
*
|
||||
* @param chat_id Session identifier
|
||||
* @param buf Output buffer (caller allocates)
|
||||
* @param size Buffer size
|
||||
* @param max_msgs Maximum number of messages to return
|
||||
*/
|
||||
esp_err_t session_get_history_json(const char *chat_id, char *buf, size_t size, int max_msgs);
|
||||
|
||||
/**
|
||||
* Clear a session (delete the file).
|
||||
*/
|
||||
esp_err_t session_clear(const char *chat_id);
|
||||
|
||||
/**
|
||||
* List all session files (prints to log).
|
||||
*/
|
||||
void session_list(void);
|
||||
Reference in New Issue
Block a user