Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

<div align="center">

![logo](assets/logo.png)

**Decouple LLM, Tools, Agent, and Channels—then pack them onto a single ESP32-S3.**

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) ![ESP32-S3](https://img.shields.io/badge/MCU-ESP32--S3-ff6a00) ![ESP-IDF](https://img.shields.io/badge/ESP--IDF-v5.5.2-00979D) ![LLM](https://img.shields.io/badge/LLM-Qwen%20via%20DashScope-0f766e) ![Channel](https://img.shields.io/badge/Channel-Feishu%20%7C%20WebSocket%20%7C%20QQBot-2563eb) ![Search](https://img.shields.io/badge/Search-Tavily-111827)
Expand All @@ -18,7 +20,7 @@

This project draws on the ideas and direction of:

- [OpenClaw](https://github.com/OpenClawAI/OpenClaw)
- [OpenClaw](https://github.com/openclaw/openclaw)
- [MimiClaw](https://github.com/memovai/mimiclaw)

EmbedClaw keeps the goal of running a full AI Agent on low-power hardware but focuses the architecture on **decoupling LLM, Tools, Agent, and Channels**.
Expand Down Expand Up @@ -69,10 +71,10 @@ It’s a working “embedded Agent base” you can extend.
| Chat Channel | Feishu, WebSocket, QQBot | Feishu long connection, local WebSocket chat, official QQBot gateway |
| Agent | ReAct tool loop | Model can call tools, read results, then continue |
| Long-term memory | `/spiffs/memory/MEMORY.md` | User profile, preferences, stable facts |
| Short-term memory | `/spiffs/session/se_<hash>.jsonl` | Recent conversation for current session |
| Short-term memory | `/spiffs/session/se_<hash>.jsonl` | Conversation history including tool call traces |
| Daily notes | `/spiffs/memory/<YYYY-MM-DD>.md` | Recent events and daily context |
| Skills | Built-in + SPIFFS | Task instructions as Markdown |
| Tools | Files, time, search, cron | Exposed to LLM via JSON schema |
| Skills | SPIFFS pre-installed + runtime | Task instructions as Markdown |
| Tools | Files, time, search, cron, GPIO | Exposed to LLM via JSON schema |

### Registered tools

Expand All @@ -87,10 +89,11 @@ It’s a working “embedded Agent base” you can extend.
| `cron_add` | Add periodic or one-shot scheduled tasks |
| `cron_list` | List scheduled tasks |
| `cron_remove` | Remove scheduled tasks |
| `gpio_control` | Control ESP32 GPIO pins (on, off, set, toggle, get) |

### Built-in skills
### Pre-installed skills

These are installed at startup:
These are pre-installed as Markdown files in `spiffs_data/skills/` and deployed with the SPIFFS image:

- `weather`
- `daily-briefing`
Expand All @@ -115,6 +118,7 @@ flowchart LR
T --> S2[File Tools]
T --> S3[Time Tool]
T --> S4[Cron Tool]
T --> S5[GPIO Tool]
A --> M1[Session Memory]
A --> M2[Long-term Memory]
A --> K[Skill Loader]
Expand All @@ -137,6 +141,9 @@ flowchart LR
│ ├── embed_claw.c # System startup entry
│ └── ec_config_internal.h # Built-in defaults; local overrides live in main/ec_config.h
├── spiffs_data/ # Default SPIFFS image content
│ ├── config/ # SOUL.md, USER.md
│ ├── memory/ # MEMORY.md
│ └── skills/ # Pre-installed skill files
└── scripts/ # WebSocket test script and test-app helpers
```

Expand Down Expand Up @@ -540,7 +547,6 @@ Skills are Markdown task descriptions, not code. You can:

- Write them at runtime via tools to `/spiffs/skills/<name>.md`
- Or put default skills in `spiffs_data/skills/` so they’re in the SPIFFS image
- Or add built-in skills in `ec_skill_loader.c`

Suggested format:

Expand Down
18 changes: 12 additions & 6 deletions README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

<div align="center">

![logo](assets/logo.png)

**把 LLM、Tools、Agent、Channel 彻底拆开,再把它们装进一块 ESP32-S3。**

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) ![ESP32-S3](https://img.shields.io/badge/MCU-ESP32--S3-ff6a00) ![ESP-IDF](https://img.shields.io/badge/ESP--IDF-v5.5.2-00979D) ![LLM](https://img.shields.io/badge/LLM-Qwen%20via%20DashScope-0f766e) ![Channel](https://img.shields.io/badge/Channel-Feishu%20%7C%20WebSocket%20%7C%20QQBot-2563eb) ![Search](https://img.shields.io/badge/Search-Tavily-111827)
Expand All @@ -17,7 +19,7 @@

本仓库在理念和方向上参考了以下优秀项目:

- [OpenClaw](https://github.com/OpenClawAI/OpenClaw)
- [OpenClaw](https://github.com/openclaw/openclaw)
- [MimiClaw](https://github.com/memovai/mimiclaw)

EmbedClaw 延续了“在低功耗硬件上运行完整 AI Agent”的思路,但把架构重点放在了 **LLM、Tools、Agent、Channel 的解耦** 上。
Expand Down Expand Up @@ -68,9 +70,9 @@ EmbedClaw 延续了“在低功耗硬件上运行完整 AI Agent”的思路,
| Chat Channel | Feishu、WebSocket、QQBot | 飞书长连接、本地 WebSocket 对话、官方 QQBot gateway |
| Agent | ReAct Tool Loop | 支持模型调用工具、再读工具结果、再继续推理 |
| 长期记忆 | `/spiffs/memory/MEMORY.md` | 用户画像、长期偏好、稳定事实 |
| 短期记忆 | `/spiffs/session/se_<hash>.jsonl` | 最近对话历史,供当前会话上下文使用 |
| 短期记忆 | `/spiffs/session/se_<hash>.jsonl` | 对话历史(含完整工具调用链),供当前会话上下文使用 |
| 每日笔记 | `/spiffs/memory/<YYYY-MM-DD>.md` | 记录近期事件与每日上下文 |
| Skills | 内置 + SPIFFS 动态技能 | 任务指令可持久化为 Markdown |
| Skills | SPIFFS 预置 + 运行时动态添加 | 任务指令可持久化为 Markdown |
| Tools | 文件、时间、搜索、定时任务 | 通过统一 JSON Schema 暴露给 LLM |

### 已注册 Tools
Expand All @@ -86,10 +88,11 @@ EmbedClaw 延续了“在低功耗硬件上运行完整 AI Agent”的思路,
| `cron_add` | 创建周期任务或单次任务 |
| `cron_list` | 查看当前定时任务 |
| `cron_remove` | 删除定时任务 |
| `gpio_control` | 控制 ESP32 GPIO 引脚(on/off/set/toggle/get) |

### 已启用 Skills
### 预置 Skills

系统启动时会自动安装内置 Skills
以下技能以 Markdown 文件形式预置在 `spiffs_data/skills/` 中,随 SPIFFS 镜像一同烧录

- `weather`
- `daily-briefing`
Expand All @@ -114,6 +117,7 @@ flowchart LR
T --> S2[File Tools]
T --> S3[Time Tool]
T --> S4[Cron Tool]
T --> S5[GPIO Tool]
A --> M1[Session Memory]
A --> M2[Long-term Memory]
A --> K[Skill Loader]
Expand All @@ -136,6 +140,9 @@ flowchart LR
│ ├── embed_claw.c # 系统统一启动入口
│ └── ec_config_internal.h # 仓库内置默认配置,项目覆盖放在 main/ec_config.h
├── spiffs_data/ # 默认写入 SPIFFS 的系统文件
│ ├── config/ # SOUL.md、USER.md
│ ├── memory/ # MEMORY.md
│ └── skills/ # 预置 Skill 文件
└── scripts/ # WebSocket 测试脚本与测试构建辅助脚本
```

Expand Down Expand Up @@ -569,7 +576,6 @@ Skill 是最轻量的扩展方式。它不是代码,而是任务说明书。

- 在运行时通过 Tool 把 Skill 写入 `/spiffs/skills/<name>.md`
- 或者把默认 Skill 放进 `spiffs_data/skills/`,随 SPIFFS 镜像一同烧录
- 或者在 `ec_skill_loader.c` 中增加新的内置 Skill

建议格式:

Expand Down
3 changes: 3 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- [x] Web search
- [ ] Script interpreter
- [ ] Peripherals (GPIO, I2C, etc.)
- [X] GPIO
- [ ] Camera

## Channel integration
Expand All @@ -33,3 +34,5 @@
- [x] test and ci/cd
- [ ] Support more filesystems (beyond SPIFFS)
- [ ] SD card support
- [ ] Support ESP32C3

2 changes: 2 additions & 0 deletions TODO_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- [x] 对接web搜索
- [ ] 对接脚本语言解释器
- [ ] 对接外设
- [X] 对接GPIO
- [ ] 对接摄像头

## Channel对接
Expand All @@ -33,3 +34,4 @@
- [x] 测试和ci/cd
- [ ] 更改SPIFFS为更多的文件系统
- [ ] 支持 SD card
- [ ] 支持 ESP32C3
Binary file added assets/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions components/embed_claw/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ idf_component_register(
INCLUDE_DIRS
"."
REQUIRES
driver
esp_http_client
esp_http_server
esp_netif
Expand Down
69 changes: 40 additions & 29 deletions components/embed_claw/core/ec_agent.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,15 @@
#define EC_AGENT_CHANNEL_DELIVERY_TASK_STACK (8 * 1024)
#define EC_AGENT_CHANNEL_DELIVERY_TASK_PRIO 5

#define EC_AGENT_SYSTEM_PROMPT_STR \
#define EC_AGENT_SYSTEM_PROMPT_HEAD \
"You are EmbedClaw, a helpful and concise AI assistant running on an ESP32 device.\n"\
"You communicate via Feishu and WebSocket.\n"\
"Reply briefly to short messages (e.g. 你好, 在吗, 谢谢).\n"\
"# Tools\n"\
"- web_search: search current information.\n"\
"- get_current_time: get date/time.\n"\
"- read_file: read /spiffs/ files (path must start with " EC_FS_BASE "/).\n"\
"- write_file: Write file.\n"\
"- edit_file: edit file.\n"\
"- list_dir: list files.\n"\
"- cron_add: schedule task.\n"\
"- cron_list: list tasks.\n"\
"- cron_remove: remove task.\n\n"\
"When using cron_add to reply later in the same conversation, reuse the current channel, chat_type, and chat_id.\n\n"\
"Available tools:\n"

#define EC_AGENT_SYSTEM_PROMPT_TAIL \
"\nWhen using cron_add to reply later in the same conversation, reuse the current channel, chat_type, and chat_id.\n\n"\
"Use tools when needed. Provide your final answer as text after using tools.\n\n"\
"## Memory\n"\
"You have persistent memory stored on local flash:\n"\
Expand Down Expand Up @@ -328,19 +322,34 @@ static esp_err_t context_build_system_prompt(char *buf, size_t size)
size_t off = 0;
size_t cap = size - 1;

off += snprintf(buf + off, size - off, EC_AGENT_SYSTEM_PROMPT_STR);
off += snprintf(buf + off, size - off, EC_AGENT_SYSTEM_PROMPT_HEAD);
if (off > cap) {
off = cap;
}

/* Bootstrap files */
off = append_file(buf, size, off, EC_SOUL_FILE, "Personality");
off = append_file(buf, size, off, EC_USER_FILE, "User Info");

scratch = calloc(1, EC_AGENT_PROMPT_SCRATCH_SIZE);
if (!scratch) {
ESP_LOGW(TAG, "Skipping optional prompt sections: out of memory");
} else {
size_t tools_len = ec_tools_build_summary(scratch, EC_AGENT_PROMPT_SCRATCH_SIZE);
if (off < cap && tools_len > 0) {
off += snprintf(buf + off, size - off, "%s", scratch);
if (off > cap) {
off = cap;
}
}
}

off += snprintf(buf + off, size - off, EC_AGENT_SYSTEM_PROMPT_TAIL);
if (off > cap) {
off = cap;
}

/* Bootstrap files */
off = append_file(buf, size, off, EC_SOUL_FILE, "Personality");
off = append_file(buf, size, off, EC_USER_FILE, "User Info");

if (scratch) {
if (off < cap && ec_memory_read_long_term(scratch, EC_AGENT_PROMPT_SCRATCH_SIZE) == ESP_OK && scratch[0]) {
off += snprintf(buf + off, size - off, "\n## Long-term Memory\n\n%s\n", scratch);
if (off > cap) {
Expand Down Expand Up @@ -377,7 +386,6 @@ static esp_err_t context_build_system_prompt(char *buf, size_t size)
}

ESP_LOGI(TAG, "System prompt built: %d bytes", (int)off);
ESP_LOGI(TAG, "prompt:%s", buf);

return ESP_OK;
}
Expand Down Expand Up @@ -455,6 +463,8 @@ static void agent_loop_task(void *arg)
if (!messages) {
messages = cJSON_CreateArray();
}
int history_count = cJSON_GetArraySize(messages);

cJSON *user_msg = cJSON_CreateObject();
cJSON_AddStringToObject(user_msg, "role", "user");
cJSON_AddStringToObject(user_msg, "content", msg.content);
Expand Down Expand Up @@ -488,22 +498,18 @@ static void agent_loop_task(void *arg)
}

if (!resp.tool_use) {
// 正常对话结束,保存最终文本并退出循环
if (resp.text && resp.text_len > 0) {
final_text = strdup(resp.text);
}
ec_llm_response_free(&resp);
break;
}


// 构建助手消息,包含文本回复和工具调用信息,供工具执行结果使用
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);

// 执行工具并将结果追加到消息数组中,供下一轮 LLM 调用使用
cJSON *tool_results = build_tool_results(&resp, &msg, tool_output, EC_AGENT_TOOL_OUTPUT_SIZE);
cJSON *result_msg = cJSON_CreateObject();
cJSON_AddStringToObject(result_msg, "role", "user");
Expand All @@ -514,14 +520,19 @@ static void agent_loop_task(void *arg)
}

if (final_text && final_text[0]) {
// 保存用户消息和助手回复到会话历史中
esp_err_t save_user = ec_session_append(session_key, "user", msg.content);
esp_err_t save_asst = ec_session_append(session_key, "assistant", final_text);
if (save_user != ESP_OK || save_asst != ESP_OK) {
ESP_LOGW(TAG, "Session save failed for %s:%s:%s (user=%s, assistant=%s)",
msg.channel, msg.chat_type, msg.chat_id,
esp_err_to_name(save_user),
esp_err_to_name(save_asst));
int total = cJSON_GetArraySize(messages);
bool save_ok = true;
for (int k = history_count; k < total; k++) {
if (ec_session_append_msg(session_key, cJSON_GetArrayItem(messages, k)) != ESP_OK) {
save_ok = false;
}
}
if (ec_session_append(session_key, "assistant", final_text) != ESP_OK) {
save_ok = false;
}
if (!save_ok) {
ESP_LOGW(TAG, "Session save failed for %s:%s:%s",
msg.channel, msg.chat_type, msg.chat_id);
} else {
ESP_LOGI(TAG, "Session saved for %s:%s:%s", msg.channel, msg.chat_type, msg.chat_id);
}
Expand Down
Loading
Loading