Merge pull request #7 from memovai/skill
This commit is contained in:
@@ -4,3 +4,6 @@ cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(mimiclaw)
|
||||
|
||||
# Pre-flash a valid SPIFFS image so first boot does not need runtime formatting.
|
||||
spiffs_create_partition_image(spiffs spiffs_data FLASH_IN_PROJECT)
|
||||
|
||||
@@ -26,9 +26,7 @@ idf_component_register(
|
||||
"tools/tool_web_search.c"
|
||||
"tools/tool_get_time.c"
|
||||
"tools/tool_files.c"
|
||||
"tools/tool_cron.c"
|
||||
"cron/cron_service.c"
|
||||
"heartbeat/heartbeat.c"
|
||||
"skills/skill_loader.c"
|
||||
INCLUDE_DIRS
|
||||
"."
|
||||
EMBED_FILES
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "context_builder.h"
|
||||
#include "mimi_config.h"
|
||||
#include "memory/memory_store.h"
|
||||
#include "skills/skill_loader.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
@@ -59,11 +60,10 @@ esp_err_t context_build_system_prompt(char *buf, size_t size)
|
||||
"- Use get_current_time to know today's date before writing daily notes.\n"
|
||||
"- Keep MEMORY.md concise and organized — summarize, don't dump raw conversation.\n"
|
||||
"- You should proactively save memory without being asked. If the user tells you their name, preferences, or important facts, persist them immediately.\n\n"
|
||||
"## Heartbeat\n"
|
||||
"The file /spiffs/config/HEARTBEAT.md contains periodic tasks.\n"
|
||||
"When triggered by heartbeat, read the file and execute any pending tasks.\n"
|
||||
"If nothing needs attention, reply with just: HEARTBEAT_OK\n"
|
||||
"You can also write to HEARTBEAT.md to schedule tasks for yourself.\n");
|
||||
"## Skills\n"
|
||||
"Skills are specialized instruction files stored in /spiffs/skills/.\n"
|
||||
"When a task matches a skill, read the full skill file for detailed instructions.\n"
|
||||
"You can create new skills using write_file to /spiffs/skills/<name>.md.\n");
|
||||
|
||||
/* Bootstrap files */
|
||||
off = append_file(buf, size, off, MIMI_SOUL_FILE, "Personality");
|
||||
@@ -81,6 +81,16 @@ esp_err_t context_build_system_prompt(char *buf, size_t size)
|
||||
off += snprintf(buf + off, size - off, "\n## Recent Notes\n\n%s\n", recent_buf);
|
||||
}
|
||||
|
||||
/* Skills */
|
||||
char skills_buf[2048];
|
||||
size_t skills_len = skill_loader_build_summary(skills_buf, sizeof(skills_buf));
|
||||
if (skills_len > 0) {
|
||||
off += snprintf(buf + off, size - off,
|
||||
"\n## Available Skills\n\n"
|
||||
"Available skills (use read_file to load full instructions):\n%s\n",
|
||||
skills_buf);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "System prompt built: %d bytes", (int)off);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
#include "memory/session_mgr.h"
|
||||
#include "proxy/http_proxy.h"
|
||||
#include "tools/tool_web_search.h"
|
||||
#include "tools/tool_registry.h"
|
||||
#include "cron/cron_service.h"
|
||||
#include "heartbeat/heartbeat.h"
|
||||
#include "skills/skill_loader.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <ctype.h>
|
||||
#include <dirent.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_console.h"
|
||||
#include "esp_system.h"
|
||||
@@ -253,6 +253,173 @@ static int cmd_wifi_scan(int argc, char **argv)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --- skill_list command --- */
|
||||
static int cmd_skill_list(int argc, char **argv)
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
char *buf = malloc(4096);
|
||||
if (!buf) {
|
||||
printf("Out of memory.\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t n = skill_loader_build_summary(buf, 4096);
|
||||
if (n == 0) {
|
||||
printf("No skills found under /spiffs/skills/.\n");
|
||||
} else {
|
||||
printf("=== Skills ===\n%s", buf);
|
||||
}
|
||||
free(buf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --- skill_show command --- */
|
||||
static struct {
|
||||
struct arg_str *name;
|
||||
struct arg_end *end;
|
||||
} skill_show_args;
|
||||
|
||||
static bool has_md_suffix(const char *name)
|
||||
{
|
||||
size_t len = strlen(name);
|
||||
return (len >= 3) && strcmp(name + len - 3, ".md") == 0;
|
||||
}
|
||||
|
||||
static bool build_skill_path(const char *name, char *out, size_t out_size)
|
||||
{
|
||||
if (!name || !name[0]) return false;
|
||||
if (strstr(name, "..") != NULL) return false;
|
||||
if (strchr(name, '/') != NULL || strchr(name, '\\') != NULL) return false;
|
||||
|
||||
if (has_md_suffix(name)) {
|
||||
snprintf(out, out_size, "/spiffs/skills/%s", name);
|
||||
} else {
|
||||
snprintf(out, out_size, "/spiffs/skills/%s.md", name);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static int cmd_skill_show(int argc, char **argv)
|
||||
{
|
||||
int nerrors = arg_parse(argc, argv, (void **)&skill_show_args);
|
||||
if (nerrors != 0) {
|
||||
arg_print_errors(stderr, skill_show_args.end, argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
char path[128];
|
||||
if (!build_skill_path(skill_show_args.name->sval[0], path, sizeof(path))) {
|
||||
printf("Invalid skill name.\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) {
|
||||
printf("Skill not found: %s\n", path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("=== %s ===\n", path);
|
||||
char line[256];
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
fputs(line, stdout);
|
||||
}
|
||||
fclose(f);
|
||||
printf("\n============\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --- skill_search command --- */
|
||||
static struct {
|
||||
struct arg_str *keyword;
|
||||
struct arg_end *end;
|
||||
} skill_search_args;
|
||||
|
||||
static bool contains_nocase(const char *text, const char *keyword)
|
||||
{
|
||||
if (!text || !keyword || !keyword[0]) return false;
|
||||
|
||||
size_t key_len = strlen(keyword);
|
||||
for (const char *p = text; *p; p++) {
|
||||
size_t i = 0;
|
||||
while (i < key_len && p[i] &&
|
||||
tolower((unsigned char)p[i]) == tolower((unsigned char)keyword[i])) {
|
||||
i++;
|
||||
}
|
||||
if (i == key_len) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static int cmd_skill_search(int argc, char **argv)
|
||||
{
|
||||
int nerrors = arg_parse(argc, argv, (void **)&skill_search_args);
|
||||
if (nerrors != 0) {
|
||||
arg_print_errors(stderr, skill_search_args.end, argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *keyword = skill_search_args.keyword->sval[0];
|
||||
DIR *dir = opendir("/spiffs");
|
||||
if (!dir) {
|
||||
printf("Cannot open /spiffs.\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *prefix = "skills/";
|
||||
const size_t prefix_len = strlen(prefix);
|
||||
int matches = 0;
|
||||
|
||||
struct dirent *ent;
|
||||
while ((ent = readdir(dir)) != NULL) {
|
||||
const char *name = ent->d_name;
|
||||
size_t name_len = strlen(name);
|
||||
|
||||
if (strncmp(name, prefix, prefix_len) != 0) continue;
|
||||
if (name_len < prefix_len + 4) continue;
|
||||
if (strcmp(name + name_len - 3, ".md") != 0) continue;
|
||||
|
||||
char full_path[296];
|
||||
snprintf(full_path, sizeof(full_path), "/spiffs/%s", name);
|
||||
|
||||
bool file_matched = contains_nocase(name, keyword);
|
||||
int matched_line = 0;
|
||||
|
||||
FILE *f = fopen(full_path, "r");
|
||||
if (!f) continue;
|
||||
|
||||
char line[256];
|
||||
int line_no = 0;
|
||||
while (!file_matched && fgets(line, sizeof(line), f)) {
|
||||
line_no++;
|
||||
if (contains_nocase(line, keyword)) {
|
||||
file_matched = true;
|
||||
matched_line = line_no;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
if (file_matched) {
|
||||
matches++;
|
||||
if (matched_line > 0) {
|
||||
printf("- %s (matched at line %d)\n", full_path, matched_line);
|
||||
} else {
|
||||
printf("- %s (matched in filename)\n", full_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
if (matches == 0) {
|
||||
printf("No skills matched keyword: %s\n", keyword);
|
||||
} else {
|
||||
printf("Total matches: %d\n", matches);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --- config_show command --- */
|
||||
static void print_config(const char *label, const char *ns, const char *key,
|
||||
const char *build_val, bool mask)
|
||||
@@ -463,6 +630,36 @@ esp_err_t serial_cli_init(void)
|
||||
};
|
||||
esp_console_cmd_register(&provider_cmd);
|
||||
|
||||
/* skill_list */
|
||||
esp_console_cmd_t skill_list_cmd = {
|
||||
.command = "skill_list",
|
||||
.help = "List installed skills from /spiffs/skills/",
|
||||
.func = &cmd_skill_list,
|
||||
};
|
||||
esp_console_cmd_register(&skill_list_cmd);
|
||||
|
||||
/* skill_show */
|
||||
skill_show_args.name = arg_str1(NULL, NULL, "<name>", "Skill name (e.g. weather or weather.md)");
|
||||
skill_show_args.end = arg_end(1);
|
||||
esp_console_cmd_t skill_show_cmd = {
|
||||
.command = "skill_show",
|
||||
.help = "Print full content of one skill file",
|
||||
.func = &cmd_skill_show,
|
||||
.argtable = &skill_show_args,
|
||||
};
|
||||
esp_console_cmd_register(&skill_show_cmd);
|
||||
|
||||
/* skill_search */
|
||||
skill_search_args.keyword = arg_str1(NULL, NULL, "<keyword>", "Keyword to search in skills");
|
||||
skill_search_args.end = arg_end(1);
|
||||
esp_console_cmd_t skill_search_cmd = {
|
||||
.command = "skill_search",
|
||||
.help = "Search skill files by keyword (filename + content)",
|
||||
.func = &cmd_skill_search,
|
||||
.argtable = &skill_search_args,
|
||||
};
|
||||
esp_console_cmd_register(&skill_search_cmd);
|
||||
|
||||
/* memory_read */
|
||||
esp_console_cmd_t mem_read_cmd = {
|
||||
.command = "memory_read",
|
||||
|
||||
@@ -13,13 +13,12 @@
|
||||
|
||||
static const char *TAG = "llm";
|
||||
|
||||
#define LLM_API_KEY_MAX_LEN 256
|
||||
#define LLM_API_KEY_MAX_LEN 320
|
||||
#define LLM_MODEL_MAX_LEN 64
|
||||
#define LLM_PROVIDER_MAX_LEN 16
|
||||
|
||||
static char s_api_key[LLM_API_KEY_MAX_LEN] = {0};
|
||||
static char s_model[LLM_MODEL_MAX_LEN] = MIMI_LLM_DEFAULT_MODEL;
|
||||
static char s_provider[LLM_PROVIDER_MAX_LEN] = MIMI_LLM_PROVIDER_DEFAULT;
|
||||
static char s_provider[16] = MIMI_LLM_PROVIDER_DEFAULT;
|
||||
|
||||
static void safe_copy(char *dst, size_t dst_size, const char *src)
|
||||
{
|
||||
@@ -28,7 +27,9 @@ static void safe_copy(char *dst, size_t dst_size, const char *src)
|
||||
dst[0] = '\0';
|
||||
return;
|
||||
}
|
||||
snprintf(dst, dst_size, "%s", src);
|
||||
size_t n = strnlen(src, dst_size - 1);
|
||||
memcpy(dst, src, n);
|
||||
dst[n] = '\0';
|
||||
}
|
||||
|
||||
/* ── Response buffer ──────────────────────────────────────────── */
|
||||
@@ -127,15 +128,12 @@ esp_err_t llm_proxy_init(void)
|
||||
if (nvs_get_str(nvs, MIMI_NVS_KEY_API_KEY, tmp, &len) == ESP_OK && tmp[0]) {
|
||||
safe_copy(s_api_key, sizeof(s_api_key), tmp);
|
||||
}
|
||||
}
|
||||
|
||||
char model_tmp[LLM_MODEL_MAX_LEN] = {0};
|
||||
len = sizeof(model_tmp);
|
||||
if (nvs_get_str(nvs, MIMI_NVS_KEY_MODEL, model_tmp, &len) == ESP_OK && model_tmp[0]) {
|
||||
safe_copy(s_model, sizeof(s_model), model_tmp);
|
||||
}
|
||||
|
||||
char provider_tmp[LLM_PROVIDER_MAX_LEN] = {0};
|
||||
char provider_tmp[16] = {0};
|
||||
len = sizeof(provider_tmp);
|
||||
if (nvs_get_str(nvs, MIMI_NVS_KEY_PROVIDER, provider_tmp, &len) == ESP_OK && provider_tmp[0]) {
|
||||
safe_copy(s_provider, sizeof(s_provider), provider_tmp);
|
||||
@@ -172,7 +170,7 @@ static esp_err_t llm_http_direct(const char *post_data, resp_buf_t *rb, int *out
|
||||
esp_http_client_set_header(client, "Content-Type", "application/json");
|
||||
if (provider_is_openai()) {
|
||||
if (s_api_key[0]) {
|
||||
char auth[192];
|
||||
char auth[LLM_API_KEY_MAX_LEN + 16];
|
||||
snprintf(auth, sizeof(auth), "Bearer %s", s_api_key);
|
||||
esp_http_client_set_header(client, "Authorization", auth);
|
||||
}
|
||||
@@ -196,7 +194,7 @@ static esp_err_t llm_http_via_proxy(const char *post_data, resp_buf_t *rb, int *
|
||||
if (!conn) return ESP_ERR_HTTP_CONNECT;
|
||||
|
||||
int body_len = strlen(post_data);
|
||||
char header[512];
|
||||
char header[1024];
|
||||
int hlen = 0;
|
||||
if (provider_is_openai()) {
|
||||
hlen = snprintf(header, sizeof(header),
|
||||
|
||||
@@ -26,8 +26,7 @@
|
||||
#include "ui/config_screen.h"
|
||||
#include "imu/imu_manager.h"
|
||||
#include "rgb/rgb.h"
|
||||
#include "cron/cron_service.h"
|
||||
#include "heartbeat/heartbeat.h"
|
||||
#include "skills/skill_loader.h"
|
||||
|
||||
static const char *TAG = "mimi";
|
||||
|
||||
@@ -122,6 +121,7 @@ void app_main(void)
|
||||
/* Initialize subsystems */
|
||||
ESP_ERROR_CHECK(message_bus_init());
|
||||
ESP_ERROR_CHECK(memory_store_init());
|
||||
ESP_ERROR_CHECK(skill_loader_init());
|
||||
ESP_ERROR_CHECK(session_mgr_init());
|
||||
ESP_ERROR_CHECK(wifi_manager_init());
|
||||
ESP_ERROR_CHECK(http_proxy_init());
|
||||
|
||||
@@ -84,14 +84,8 @@
|
||||
#define MIMI_CONTEXT_BUF_SIZE (16 * 1024)
|
||||
#define MIMI_SESSION_MAX_MSGS 20
|
||||
|
||||
/* Cron Service */
|
||||
#define MIMI_CRON_FILE "/spiffs/config/cron.json"
|
||||
#define MIMI_CRON_CHECK_INTERVAL_MS (30 * 1000)
|
||||
#define MIMI_CRON_MAX_JOBS 8
|
||||
|
||||
/* Heartbeat */
|
||||
#define MIMI_HEARTBEAT_FILE "/spiffs/config/HEARTBEAT.md"
|
||||
#define MIMI_HEARTBEAT_INTERVAL_MS (30 * 60 * 1000)
|
||||
/* Skills */
|
||||
#define MIMI_SKILLS_PREFIX "/spiffs/skills/"
|
||||
|
||||
/* WebSocket Gateway */
|
||||
#define MIMI_WS_PORT 18789
|
||||
|
||||
262
main/skills/skill_loader.c
Normal file
262
main/skills/skill_loader.c
Normal file
@@ -0,0 +1,262 @@
|
||||
#include "skills/skill_loader.h"
|
||||
#include "mimi_config.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <dirent.h>
|
||||
#include "esp_log.h"
|
||||
|
||||
static const char *TAG = "skills";
|
||||
|
||||
/* ── Built-in skill contents ─────────────────────────────────── */
|
||||
|
||||
#define BUILTIN_WEATHER \
|
||||
"# Weather\n" \
|
||||
"\n" \
|
||||
"Get current weather and forecasts using web_search.\n" \
|
||||
"\n" \
|
||||
"## When to use\n" \
|
||||
"When the user asks about weather, temperature, or forecasts.\n" \
|
||||
"\n" \
|
||||
"## How to use\n" \
|
||||
"1. Use get_current_time to know the current date\n" \
|
||||
"2. Use web_search with a query like \"weather in [city] today\"\n" \
|
||||
"3. Extract temperature, conditions, and forecast from results\n" \
|
||||
"4. Present in a concise, friendly format\n" \
|
||||
"\n" \
|
||||
"## Example\n" \
|
||||
"User: \"What's the weather in Tokyo?\"\n" \
|
||||
"→ get_current_time\n" \
|
||||
"→ web_search \"weather Tokyo today February 2026\"\n" \
|
||||
"→ \"Tokyo: 8°C, partly cloudy. High 12°C, low 4°C. Light wind from the north.\"\n"
|
||||
|
||||
#define BUILTIN_DAILY_BRIEFING \
|
||||
"# Daily Briefing\n" \
|
||||
"\n" \
|
||||
"Compile a personalized daily briefing for the user.\n" \
|
||||
"\n" \
|
||||
"## When to use\n" \
|
||||
"When the user asks for a daily briefing, morning update, or \"what's new today\".\n" \
|
||||
"Also useful as a heartbeat/cron task.\n" \
|
||||
"\n" \
|
||||
"## How to use\n" \
|
||||
"1. Use get_current_time for today's date\n" \
|
||||
"2. Read /spiffs/memory/MEMORY.md for user preferences and context\n" \
|
||||
"3. Read today's daily note if it exists\n" \
|
||||
"4. Use web_search for relevant news based on user interests\n" \
|
||||
"5. Compile a concise briefing covering:\n" \
|
||||
" - Date and time\n" \
|
||||
" - Weather (if location known from USER.md)\n" \
|
||||
" - Relevant news/updates based on user interests\n" \
|
||||
" - Any pending tasks from memory\n" \
|
||||
" - Any scheduled cron jobs\n" \
|
||||
"\n" \
|
||||
"## Format\n" \
|
||||
"Keep it brief — 5-10 bullet points max. Use the user's preferred language.\n"
|
||||
|
||||
#define BUILTIN_SKILL_CREATOR \
|
||||
"# Skill Creator\n" \
|
||||
"\n" \
|
||||
"Create new skills for MimiClaw.\n" \
|
||||
"\n" \
|
||||
"## When to use\n" \
|
||||
"When the user asks to create a new skill, teach the bot something, or add a new capability.\n" \
|
||||
"\n" \
|
||||
"## How to create a skill\n" \
|
||||
"1. Choose a short, descriptive name (lowercase, hyphens ok)\n" \
|
||||
"2. Write a SKILL.md file with this structure:\n" \
|
||||
" - `# Title` — clear name\n" \
|
||||
" - Brief description paragraph\n" \
|
||||
" - `## When to use` — trigger conditions\n" \
|
||||
" - `## How to use` — step-by-step instructions\n" \
|
||||
" - `## Example` — concrete example (optional but helpful)\n" \
|
||||
"3. Save to `/spiffs/skills/<name>.md` using write_file\n" \
|
||||
"4. The skill will be automatically available after the next conversation\n" \
|
||||
"\n" \
|
||||
"## Best practices\n" \
|
||||
"- Keep skills concise — the context window is limited\n" \
|
||||
"- Focus on WHAT to do, not HOW (the agent is smart)\n" \
|
||||
"- Include specific tool calls the agent should use\n" \
|
||||
"- Test by asking the agent to use the new skill\n" \
|
||||
"\n" \
|
||||
"## Example\n" \
|
||||
"To create a \"translate\" skill:\n" \
|
||||
"write_file path=\"/spiffs/skills/translate.md\" content=\"# Translate\\n\\nTranslate text between languages.\\n\\n" \
|
||||
"## When to use\\nWhen the user asks to translate text.\\n\\n" \
|
||||
"## How to use\\n1. Identify source and target languages\\n" \
|
||||
"2. Translate directly using your language knowledge\\n" \
|
||||
"3. For specialized terms, use web_search to verify\\n\"\n"
|
||||
|
||||
/* Built-in skill registry */
|
||||
typedef struct {
|
||||
const char *filename; /* e.g. "weather" */
|
||||
const char *content;
|
||||
} builtin_skill_t;
|
||||
|
||||
static const builtin_skill_t s_builtins[] = {
|
||||
{ "weather", BUILTIN_WEATHER },
|
||||
{ "daily-briefing", BUILTIN_DAILY_BRIEFING },
|
||||
{ "skill-creator", BUILTIN_SKILL_CREATOR },
|
||||
};
|
||||
|
||||
#define NUM_BUILTINS (sizeof(s_builtins) / sizeof(s_builtins[0]))
|
||||
|
||||
/* ── Install built-in skills if missing ──────────────────────── */
|
||||
|
||||
static void install_builtin(const builtin_skill_t *skill)
|
||||
{
|
||||
char path[64];
|
||||
snprintf(path, sizeof(path), "%s%s.md", MIMI_SKILLS_PREFIX, skill->filename);
|
||||
|
||||
/* Check if already exists */
|
||||
FILE *f = fopen(path, "r");
|
||||
if (f) {
|
||||
fclose(f);
|
||||
ESP_LOGD(TAG, "Skill exists: %s", path);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Write built-in skill */
|
||||
f = fopen(path, "w");
|
||||
if (!f) {
|
||||
ESP_LOGE(TAG, "Cannot write skill: %s", path);
|
||||
return;
|
||||
}
|
||||
|
||||
fputs(skill->content, f);
|
||||
fclose(f);
|
||||
ESP_LOGI(TAG, "Installed built-in skill: %s", path);
|
||||
}
|
||||
|
||||
esp_err_t skill_loader_init(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Initializing skills system");
|
||||
|
||||
for (size_t i = 0; i < NUM_BUILTINS; i++) {
|
||||
install_builtin(&s_builtins[i]);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Skills system ready (%d built-in)", (int)NUM_BUILTINS);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ── Build skills summary for system prompt ──────────────────── */
|
||||
|
||||
/**
|
||||
* Parse first line as title: expects "# Title"
|
||||
* Returns pointer past "# " or the line itself if no prefix.
|
||||
*/
|
||||
static const char *extract_title(const char *line, size_t len, char *out, size_t out_size)
|
||||
{
|
||||
const char *start = line;
|
||||
if (len >= 2 && line[0] == '#' && line[1] == ' ') {
|
||||
start = line + 2;
|
||||
len -= 2;
|
||||
}
|
||||
|
||||
/* Trim trailing whitespace/newline */
|
||||
while (len > 0 && (start[len - 1] == '\n' || start[len - 1] == '\r' || start[len - 1] == ' ')) {
|
||||
len--;
|
||||
}
|
||||
|
||||
size_t copy = len < out_size - 1 ? len : out_size - 1;
|
||||
memcpy(out, start, copy);
|
||||
out[copy] = '\0';
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract description: text between the first line and the first blank line.
|
||||
*/
|
||||
static void extract_description(FILE *f, char *out, size_t out_size)
|
||||
{
|
||||
size_t off = 0;
|
||||
char line[256];
|
||||
|
||||
while (fgets(line, sizeof(line), f) && off < out_size - 1) {
|
||||
size_t len = strlen(line);
|
||||
|
||||
/* Stop at blank line or section header */
|
||||
if (len == 0 || (len == 1 && line[0] == '\n') ||
|
||||
(len >= 2 && line[0] == '#' && line[1] == '#')) {
|
||||
break;
|
||||
}
|
||||
|
||||
/* Skip leading blank lines */
|
||||
if (off == 0 && line[0] == '\n') continue;
|
||||
|
||||
/* Trim trailing newline for concatenation */
|
||||
if (line[len - 1] == '\n') {
|
||||
line[len - 1] = ' ';
|
||||
}
|
||||
|
||||
size_t copy = len < out_size - off - 1 ? len : out_size - off - 1;
|
||||
memcpy(out + off, line, copy);
|
||||
off += copy;
|
||||
}
|
||||
|
||||
/* Trim trailing space */
|
||||
while (off > 0 && out[off - 1] == ' ') off--;
|
||||
out[off] = '\0';
|
||||
}
|
||||
|
||||
size_t skill_loader_build_summary(char *buf, size_t size)
|
||||
{
|
||||
DIR *dir = opendir(MIMI_SPIFFS_BASE);
|
||||
if (!dir) {
|
||||
ESP_LOGW(TAG, "Cannot open SPIFFS for skill enumeration");
|
||||
buf[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t off = 0;
|
||||
struct dirent *ent;
|
||||
/* SPIFFS readdir returns filenames relative to the mount point (e.g. "skills/weather.md").
|
||||
We match entries that start with "skills/" and end with ".md". */
|
||||
const char *skills_subdir = "skills/";
|
||||
const size_t subdir_len = strlen(skills_subdir);
|
||||
|
||||
while ((ent = readdir(dir)) != NULL && off < size - 1) {
|
||||
const char *name = ent->d_name;
|
||||
|
||||
/* Match files under skills/ with .md extension */
|
||||
if (strncmp(name, skills_subdir, subdir_len) != 0) continue;
|
||||
|
||||
size_t name_len = strlen(name);
|
||||
if (name_len < subdir_len + 4) continue; /* at least "skills/x.md" */
|
||||
if (strcmp(name + name_len - 3, ".md") != 0) continue;
|
||||
|
||||
/* Build full path */
|
||||
char full_path[296];
|
||||
snprintf(full_path, sizeof(full_path), "%s/%s", MIMI_SPIFFS_BASE, name);
|
||||
|
||||
FILE *f = fopen(full_path, "r");
|
||||
if (!f) continue;
|
||||
|
||||
/* Read first line for title */
|
||||
char first_line[128];
|
||||
if (!fgets(first_line, sizeof(first_line), f)) {
|
||||
fclose(f);
|
||||
continue;
|
||||
}
|
||||
|
||||
char title[64];
|
||||
extract_title(first_line, strlen(first_line), title, sizeof(title));
|
||||
|
||||
/* Read description (until blank line) */
|
||||
char desc[256];
|
||||
extract_description(f, desc, sizeof(desc));
|
||||
fclose(f);
|
||||
|
||||
/* Append to summary */
|
||||
off += snprintf(buf + off, size - off,
|
||||
"- **%s**: %s (read with: read_file %s)\n",
|
||||
title, desc, full_path);
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
|
||||
buf[off] = '\0';
|
||||
ESP_LOGI(TAG, "Skills summary: %d bytes", (int)off);
|
||||
return off;
|
||||
}
|
||||
20
main/skills/skill_loader.h
Normal file
20
main/skills/skill_loader.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stddef.h>
|
||||
|
||||
/**
|
||||
* Initialize skills system.
|
||||
* Installs built-in skill files to SPIFFS if they don't already exist.
|
||||
*/
|
||||
esp_err_t skill_loader_init(void);
|
||||
|
||||
/**
|
||||
* Build a summary of all available skills for the system prompt.
|
||||
* Lists each skill with its title and description.
|
||||
*
|
||||
* @param buf Output buffer
|
||||
* @param size Buffer size
|
||||
* @return Number of bytes written (0 if no skills found)
|
||||
*/
|
||||
size_t skill_loader_build_summary(char *buf, size_t size);
|
||||
@@ -9,7 +9,6 @@
|
||||
#include <sys/stat.h>
|
||||
#include "esp_log.h"
|
||||
#include "cJSON.h"
|
||||
#include <stdbool.h>
|
||||
|
||||
static const char *TAG = "tool_files";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user