From a0b6cb34cf70e238152c2a8dac06c37018213cb6 Mon Sep 17 00:00:00 2001 From: Asklv Date: Wed, 18 Feb 2026 19:16:00 +0800 Subject: [PATCH 01/12] chore(search): add Tavily key config placeholders Signed-off-by: Asklv --- main/mimi_config.h | 4 ++++ main/tools/tool_web_search.h | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/main/mimi_config.h b/main/mimi_config.h index 0b90860..9be7c08 100644 --- a/main/mimi_config.h +++ b/main/mimi_config.h @@ -43,6 +43,9 @@ #ifndef MIMI_SECRET_FEISHU_APP_SECRET #define MIMI_SECRET_FEISHU_APP_SECRET "" #endif +#ifndef MIMI_SECRET_TAVILY_KEY +#define MIMI_SECRET_TAVILY_KEY "" +#endif /* WiFi */ #define MIMI_WIFI_MAX_RETRY 10 @@ -141,6 +144,7 @@ #define MIMI_NVS_KEY_FEISHU_APP_ID "app_id" #define MIMI_NVS_KEY_FEISHU_APP_SECRET "app_secret" #define MIMI_NVS_KEY_API_KEY "api_key" +#define MIMI_NVS_KEY_TAVILY_KEY "tavily_key" #define MIMI_NVS_KEY_MODEL "model" #define MIMI_NVS_KEY_PROVIDER "provider" #define MIMI_NVS_KEY_PROXY_HOST "host" diff --git a/main/tools/tool_web_search.h b/main/tools/tool_web_search.h index ba5b87b..28b46f3 100644 --- a/main/tools/tool_web_search.h +++ b/main/tools/tool_web_search.h @@ -22,3 +22,8 @@ esp_err_t tool_web_search_execute(const char *input_json, char *output, size_t o * Save Brave Search API key to NVS. */ esp_err_t tool_web_search_set_key(const char *api_key); + +/** + * Save Tavily API key to NVS. + */ +esp_err_t tool_web_search_set_tavily_key(const char *api_key); From 06de1fe05bad9c07eb91daeea9ec290283b0fd1f Mon Sep 17 00:00:00 2001 From: Asklv Date: Wed, 18 Feb 2026 19:16:00 +0800 Subject: [PATCH 02/12] refactor(search): track Brave/Tavily providers and key loading Signed-off-by: Asklv --- main/tools/tool_web_search.c | 40 +++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/main/tools/tool_web_search.c b/main/tools/tool_web_search.c index d1b8ecf..f42bc8a 100644 --- a/main/tools/tool_web_search.c +++ b/main/tools/tool_web_search.c @@ -13,7 +13,15 @@ static const char *TAG = "web_search"; -static char s_search_key[128] = {0}; +typedef enum { + SEARCH_PROVIDER_NONE = 0, + SEARCH_PROVIDER_BRAVE, + SEARCH_PROVIDER_TAVILY, +} search_provider_t; + +static char s_brave_key[128] = {0}; +static char s_tavily_key[128] = {0}; +static search_provider_t s_provider = SEARCH_PROVIDER_NONE; #define SEARCH_BUF_SIZE (16 * 1024) #define SEARCH_RESULT_COUNT 5 @@ -44,9 +52,12 @@ static esp_err_t http_event_handler(esp_http_client_event_t *evt) esp_err_t tool_web_search_init(void) { - /* Start with build-time default */ + /* Start with build-time defaults */ if (MIMI_SECRET_SEARCH_KEY[0] != '\0') { - strncpy(s_search_key, MIMI_SECRET_SEARCH_KEY, sizeof(s_search_key) - 1); + strncpy(s_brave_key, MIMI_SECRET_SEARCH_KEY, sizeof(s_brave_key) - 1); + } + if (MIMI_SECRET_TAVILY_KEY[0] != '\0') { + strncpy(s_tavily_key, MIMI_SECRET_TAVILY_KEY, sizeof(s_tavily_key) - 1); } /* NVS overrides take highest priority (set via CLI) */ @@ -55,15 +66,30 @@ esp_err_t tool_web_search_init(void) char tmp[128] = {0}; size_t len = sizeof(tmp); if (nvs_get_str(nvs, MIMI_NVS_KEY_API_KEY, tmp, &len) == ESP_OK && tmp[0]) { - strncpy(s_search_key, tmp, sizeof(s_search_key) - 1); + strncpy(s_brave_key, tmp, sizeof(s_brave_key) - 1); + } + memset(tmp, 0, sizeof(tmp)); + len = sizeof(tmp); + if (nvs_get_str(nvs, MIMI_NVS_KEY_TAVILY_KEY, tmp, &len) == ESP_OK && tmp[0]) { + strncpy(s_tavily_key, tmp, sizeof(s_tavily_key) - 1); } nvs_close(nvs); } - if (s_search_key[0]) { - ESP_LOGI(TAG, "Web search initialized (key configured)"); + if (s_tavily_key[0]) { + s_provider = SEARCH_PROVIDER_TAVILY; + } else if (s_brave_key[0]) { + s_provider = SEARCH_PROVIDER_BRAVE; } else { - ESP_LOGW(TAG, "No search API key. Use CLI: set_search_key "); + s_provider = SEARCH_PROVIDER_NONE; + } + + if (s_provider == SEARCH_PROVIDER_TAVILY) { + ESP_LOGI(TAG, "Web search initialized (provider=tavily)"); + } else if (s_provider == SEARCH_PROVIDER_BRAVE) { + ESP_LOGI(TAG, "Web search initialized (provider=brave)"); + } else { + ESP_LOGW(TAG, "No search API key. Use CLI: set_search_key or set_tavily_key"); } return ESP_OK; } From 34fde9c82c17315cc608fb59236471b779124ca2 Mon Sep 17 00:00:00 2001 From: Asklv Date: Wed, 18 Feb 2026 19:16:00 +0800 Subject: [PATCH 03/12] refactor(search): isolate Brave HTTP paths for multi-provider support Signed-off-by: Asklv --- main/tools/tool_web_search.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main/tools/tool_web_search.c b/main/tools/tool_web_search.c index f42bc8a..8df63be 100644 --- a/main/tools/tool_web_search.c +++ b/main/tools/tool_web_search.c @@ -158,7 +158,7 @@ static void format_results(cJSON *root, char *output, size_t output_size) /* ── Direct HTTPS request ─────────────────────────────────────── */ -static esp_err_t search_direct(const char *url, search_buf_t *sb) +static esp_err_t brave_search_direct(const char *url, search_buf_t *sb) { esp_http_client_config_t config = { .url = url, @@ -173,7 +173,7 @@ static esp_err_t search_direct(const char *url, search_buf_t *sb) if (!client) return ESP_FAIL; esp_http_client_set_header(client, "Accept", "application/json"); - esp_http_client_set_header(client, "X-Subscription-Token", s_search_key); + esp_http_client_set_header(client, "X-Subscription-Token", s_brave_key); esp_err_t err = esp_http_client_perform(client); int status = esp_http_client_get_status_code(client); @@ -189,7 +189,7 @@ static esp_err_t search_direct(const char *url, search_buf_t *sb) /* ── Proxy HTTPS request ──────────────────────────────────────── */ -static esp_err_t search_via_proxy(const char *path, search_buf_t *sb) +static esp_err_t brave_search_via_proxy(const char *path, search_buf_t *sb) { proxy_conn_t *conn = proxy_conn_open("api.search.brave.com", 443, 15000); if (!conn) return ESP_ERR_HTTP_CONNECT; @@ -201,7 +201,7 @@ static esp_err_t search_via_proxy(const char *path, search_buf_t *sb) "Accept: application/json\r\n" "X-Subscription-Token: %s\r\n" "Connection: close\r\n\r\n", - path, s_search_key); + path, s_brave_key); if (proxy_conn_write(conn, header, hlen) < 0) { proxy_conn_close(conn); From 2ad5101a7736dc79f6f576e52b213fcbf1946983 Mon Sep 17 00:00:00 2001 From: Asklv Date: Wed, 18 Feb 2026 19:16:00 +0800 Subject: [PATCH 04/12] feat(search): add Tavily result formatter Signed-off-by: Asklv --- main/tools/tool_web_search.c | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/main/tools/tool_web_search.c b/main/tools/tool_web_search.c index 8df63be..3a293f8 100644 --- a/main/tools/tool_web_search.c +++ b/main/tools/tool_web_search.c @@ -156,6 +156,36 @@ static void format_results(cJSON *root, char *output, size_t output_size) } } +static void format_tavily_results(cJSON *root, char *output, size_t output_size) +{ + cJSON *results = cJSON_GetObjectItem(root, "results"); + if (!results || !cJSON_IsArray(results) || cJSON_GetArraySize(results) == 0) { + snprintf(output, output_size, "No web results found."); + return; + } + + size_t off = 0; + int idx = 0; + cJSON *item; + cJSON_ArrayForEach(item, results) { + if (idx >= SEARCH_RESULT_COUNT) break; + + cJSON *title = cJSON_GetObjectItem(item, "title"); + cJSON *url = cJSON_GetObjectItem(item, "url"); + cJSON *content = cJSON_GetObjectItem(item, "content"); + + off += snprintf(output + off, output_size - off, + "%d. %s\n %s\n %s\n\n", + idx + 1, + (title && cJSON_IsString(title)) ? title->valuestring : "(no title)", + (url && cJSON_IsString(url)) ? url->valuestring : "", + (content && cJSON_IsString(content)) ? content->valuestring : ""); + + if (off >= output_size - 1) break; + idx++; + } +} + /* ── Direct HTTPS request ─────────────────────────────────────── */ static esp_err_t brave_search_direct(const char *url, search_buf_t *sb) From 3a76f02f2f65edcbcdf552a4fe56914429c44be2 Mon Sep 17 00:00:00 2001 From: Asklv Date: Wed, 18 Feb 2026 19:16:00 +0800 Subject: [PATCH 05/12] feat(search): implement Tavily HTTP client for direct and proxy modes Signed-off-by: Asklv --- main/tools/tool_web_search.c | 118 +++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/main/tools/tool_web_search.c b/main/tools/tool_web_search.c index 3a293f8..b405e00 100644 --- a/main/tools/tool_web_search.c +++ b/main/tools/tool_web_search.c @@ -186,6 +186,20 @@ static void format_tavily_results(cJSON *root, char *output, size_t output_size) } } +static char *build_tavily_payload(const char *query) +{ + cJSON *root = cJSON_CreateObject(); + if (!root) return NULL; + cJSON_AddStringToObject(root, "api_key", s_tavily_key); + cJSON_AddStringToObject(root, "query", query); + cJSON_AddNumberToObject(root, "max_results", SEARCH_RESULT_COUNT); + cJSON_AddBoolToObject(root, "include_answer", false); + cJSON_AddStringToObject(root, "search_depth", "basic"); + char *payload = cJSON_PrintUnformatted(root); + cJSON_Delete(root); + return payload; +} + /* ── Direct HTTPS request ─────────────────────────────────────── */ static esp_err_t brave_search_direct(const char *url, search_buf_t *sb) @@ -278,6 +292,110 @@ static esp_err_t brave_search_via_proxy(const char *path, search_buf_t *sb) return ESP_OK; } +static esp_err_t tavily_search_direct(const char *query, search_buf_t *sb) +{ + char *payload = build_tavily_payload(query); + if (!payload) return ESP_ERR_NO_MEM; + + esp_http_client_config_t config = { + .url = "https://api.tavily.com/search", + .event_handler = http_event_handler, + .user_data = sb, + .timeout_ms = 15000, + .buffer_size = 4096, + .crt_bundle_attach = esp_crt_bundle_attach, + }; + + esp_http_client_handle_t client = esp_http_client_init(&config); + if (!client) { + free(payload); + return ESP_FAIL; + } + + esp_http_client_set_method(client, HTTP_METHOD_POST); + esp_http_client_set_header(client, "Accept", "application/json"); + esp_http_client_set_header(client, "Content-Type", "application/json"); + esp_http_client_set_post_field(client, payload, strlen(payload)); + + esp_err_t err = esp_http_client_perform(client); + int status = esp_http_client_get_status_code(client); + esp_http_client_cleanup(client); + free(payload); + + if (err != ESP_OK) return err; + if (status != 200) { + ESP_LOGE(TAG, "Tavily API returned %d", status); + return ESP_FAIL; + } + return ESP_OK; +} + +static esp_err_t tavily_search_via_proxy(const char *query, search_buf_t *sb) +{ + proxy_conn_t *conn = proxy_conn_open("api.tavily.com", 443, 15000); + if (!conn) return ESP_ERR_HTTP_CONNECT; + + char *payload = build_tavily_payload(query); + if (!payload) { + proxy_conn_close(conn); + return ESP_ERR_NO_MEM; + } + + char header[768]; + int hlen = snprintf(header, sizeof(header), + "POST /search HTTP/1.1\r\n" + "Host: api.tavily.com\r\n" + "Accept: application/json\r\n" + "Content-Type: application/json\r\n" + "Content-Length: %d\r\n" + "Connection: close\r\n\r\n", + (int)strlen(payload)); + + if (proxy_conn_write(conn, header, hlen) < 0 || + proxy_conn_write(conn, payload, strlen(payload)) < 0) { + free(payload); + proxy_conn_close(conn); + return ESP_ERR_HTTP_WRITE_DATA; + } + free(payload); + + char tmp[4096]; + size_t total = 0; + while (1) { + int n = proxy_conn_read(conn, tmp, sizeof(tmp), 15000); + if (n <= 0) break; + size_t copy = (total + n < sb->cap - 1) ? (size_t)n : sb->cap - 1 - total; + if (copy > 0) { + memcpy(sb->data + total, tmp, copy); + total += copy; + } + } + sb->data[total] = '\0'; + sb->len = total; + proxy_conn_close(conn); + + int status = 0; + if (total > 5 && strncmp(sb->data, "HTTP/", 5) == 0) { + const char *sp = strchr(sb->data, ' '); + if (sp) status = atoi(sp + 1); + } + + char *body = strstr(sb->data, "\r\n\r\n"); + if (body) { + body += 4; + size_t blen = total - (body - sb->data); + memmove(sb->data, body, blen); + sb->len = blen; + sb->data[sb->len] = '\0'; + } + + if (status != 200) { + ESP_LOGE(TAG, "Tavily API returned %d via proxy", status); + return ESP_FAIL; + } + return ESP_OK; +} + /* ── Execute ──────────────────────────────────────────────────── */ esp_err_t tool_web_search_execute(const char *input_json, char *output, size_t output_size) From 2de7087e2b7bf6c84b5cb8f515d7aa770f7d7fd0 Mon Sep 17 00:00:00 2001 From: Asklv Date: Wed, 18 Feb 2026 19:16:00 +0800 Subject: [PATCH 06/12] feat(search): route web_search through Tavily when key is configured Signed-off-by: Asklv --- main/tools/tool_web_search.c | 59 +++++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/main/tools/tool_web_search.c b/main/tools/tool_web_search.c index b405e00..fc0bc5f 100644 --- a/main/tools/tool_web_search.c +++ b/main/tools/tool_web_search.c @@ -400,8 +400,9 @@ static esp_err_t tavily_search_via_proxy(const char *query, search_buf_t *sb) esp_err_t tool_web_search_execute(const char *input_json, char *output, size_t output_size) { - if (s_search_key[0] == '\0') { - snprintf(output, output_size, "Error: No search API key configured. Set MIMI_SECRET_SEARCH_KEY in mimi_secrets.h"); + if (s_provider == SEARCH_PROVIDER_NONE) { + snprintf(output, output_size, + "Error: No search API key configured. Set MIMI_SECRET_TAVILY_KEY or MIMI_SECRET_SEARCH_KEY in mimi_secrets.h"); return ESP_ERR_INVALID_STATE; } @@ -421,15 +422,13 @@ esp_err_t tool_web_search_execute(const char *input_json, char *output, size_t o ESP_LOGI(TAG, "Searching: %s", query->valuestring); - /* Build URL */ + /* Build URL/query fields */ char encoded_query[256]; url_encode(query->valuestring, encoded_query, sizeof(encoded_query)); + char query_copy[256]; + snprintf(query_copy, sizeof(query_copy), "%s", query->valuestring); cJSON_Delete(input); - char path[384]; - snprintf(path, sizeof(path), - "/res/v1/web/search?q=%s&count=%d", encoded_query, SEARCH_RESULT_COUNT); - /* Allocate response buffer from PSRAM */ search_buf_t sb = {0}; sb.data = heap_caps_calloc(1, SEARCH_BUF_SIZE, MALLOC_CAP_SPIRAM); @@ -441,12 +440,23 @@ esp_err_t tool_web_search_execute(const char *input_json, char *output, size_t o /* Make HTTP request */ esp_err_t err; - if (http_proxy_is_enabled()) { - err = search_via_proxy(path, &sb); + if (s_provider == SEARCH_PROVIDER_TAVILY) { + if (http_proxy_is_enabled()) { + err = tavily_search_via_proxy(query_copy, &sb); + } else { + err = tavily_search_direct(query_copy, &sb); + } } else { - char url[512]; - snprintf(url, sizeof(url), "https://api.search.brave.com%s", path); - err = search_direct(url, &sb); + char path[384]; + snprintf(path, sizeof(path), + "/res/v1/web/search?q=%s&count=%d", encoded_query, SEARCH_RESULT_COUNT); + if (http_proxy_is_enabled()) { + err = brave_search_via_proxy(path, &sb); + } else { + char url[512]; + snprintf(url, sizeof(url), "https://api.search.brave.com%s", path); + err = brave_search_direct(url, &sb); + } } if (err != ESP_OK) { @@ -464,7 +474,11 @@ esp_err_t tool_web_search_execute(const char *input_json, char *output, size_t o return ESP_FAIL; } - format_results(root, output, output_size); + if (s_provider == SEARCH_PROVIDER_TAVILY) { + format_tavily_results(root, output, output_size); + } else { + format_results(root, output, output_size); + } cJSON_Delete(root); ESP_LOGI(TAG, "Search complete, %d bytes result", (int)strlen(output)); @@ -479,7 +493,24 @@ esp_err_t tool_web_search_set_key(const char *api_key) ESP_ERROR_CHECK(nvs_commit(nvs)); nvs_close(nvs); - strncpy(s_search_key, api_key, sizeof(s_search_key) - 1); + strncpy(s_brave_key, api_key, sizeof(s_brave_key) - 1); + if (s_provider == SEARCH_PROVIDER_NONE) { + s_provider = SEARCH_PROVIDER_BRAVE; + } ESP_LOGI(TAG, "Search API key saved"); return ESP_OK; } + +esp_err_t tool_web_search_set_tavily_key(const char *api_key) +{ + nvs_handle_t nvs; + ESP_ERROR_CHECK(nvs_open(MIMI_NVS_SEARCH, NVS_READWRITE, &nvs)); + ESP_ERROR_CHECK(nvs_set_str(nvs, MIMI_NVS_KEY_TAVILY_KEY, api_key)); + ESP_ERROR_CHECK(nvs_commit(nvs)); + nvs_close(nvs); + + strncpy(s_tavily_key, api_key, sizeof(s_tavily_key) - 1); + s_provider = SEARCH_PROVIDER_TAVILY; + ESP_LOGI(TAG, "Tavily API key saved"); + return ESP_OK; +} From c4c66dda9ee0ca209f099b78b78174e6f57305ff Mon Sep 17 00:00:00 2001 From: Asklv Date: Wed, 18 Feb 2026 19:16:00 +0800 Subject: [PATCH 07/12] feat(cli): add set_tavily_key command and config visibility Signed-off-by: Asklv --- main/cli/serial_cli.c | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/main/cli/serial_cli.c b/main/cli/serial_cli.c index 49fed0a..32c3ce3 100644 --- a/main/cli/serial_cli.c +++ b/main/cli/serial_cli.c @@ -299,6 +299,24 @@ static int cmd_set_search_key(int argc, char **argv) return 0; } +/* --- set_tavily_key command --- */ +static struct { + struct arg_str *key; + struct arg_end *end; +} tavily_key_args; + +static int cmd_set_tavily_key(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&tavily_key_args); + if (nerrors != 0) { + arg_print_errors(stderr, tavily_key_args.end, argv[0]); + return 1; + } + tool_web_search_set_tavily_key(tavily_key_args.key->sval[0]); + printf("Tavily API key saved.\n"); + return 0; +} + /* --- wifi_scan command --- */ static int cmd_wifi_scan(int argc, char **argv) { @@ -519,6 +537,7 @@ static int cmd_config_show(int argc, char **argv) print_config("Proxy Host", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_HOST, MIMI_SECRET_PROXY_HOST, false); print_config("Proxy Port", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_PORT, MIMI_SECRET_PROXY_PORT, false); print_config("Search Key", MIMI_NVS_SEARCH, MIMI_NVS_KEY_API_KEY, MIMI_SECRET_SEARCH_KEY, true); + print_config("Tavily Key", MIMI_NVS_SEARCH, MIMI_NVS_KEY_TAVILY_KEY, MIMI_SECRET_TAVILY_KEY, true); printf("=============================\n"); return 0; } @@ -805,6 +824,17 @@ esp_err_t serial_cli_init(void) }; esp_console_cmd_register(&search_key_cmd); + /* set_tavily_key */ + tavily_key_args.key = arg_str1(NULL, NULL, "", "Tavily Search API key"); + tavily_key_args.end = arg_end(1); + esp_console_cmd_t tavily_key_cmd = { + .command = "set_tavily_key", + .help = "Set Tavily API key for web_search tool", + .func = &cmd_set_tavily_key, + .argtable = &tavily_key_args, + }; + esp_console_cmd_register(&tavily_key_cmd); + /* set_proxy */ proxy_args.host = arg_str1(NULL, NULL, "", "Proxy host/IP"); proxy_args.port = arg_int1(NULL, NULL, "", "Proxy port"); From a7b3d67ecb49c0a5884173ddfef3b8371787dda8 Mon Sep 17 00:00:00 2001 From: Asklv Date: Wed, 18 Feb 2026 19:16:00 +0800 Subject: [PATCH 08/12] chore(agent): document Tavily-first behavior for web_search Signed-off-by: Asklv --- main/agent/context_builder.c | 2 +- main/tools/tool_registry.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/main/agent/context_builder.c b/main/agent/context_builder.c index e8a5625..5f8d503 100644 --- a/main/agent/context_builder.c +++ b/main/agent/context_builder.c @@ -36,7 +36,7 @@ esp_err_t context_build_system_prompt(char *buf, size_t size) "Be helpful, accurate, and concise.\n\n" "## Available Tools\n" "You have access to the following tools:\n" - "- web_search: Search the web for current information. " + "- web_search: Search the web for current information (Tavily preferred, Brave fallback when configured). " "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" diff --git a/main/tools/tool_registry.c b/main/tools/tool_registry.c index 6323ef1..6c82a3e 100644 --- a/main/tools/tool_registry.c +++ b/main/tools/tool_registry.c @@ -60,7 +60,7 @@ esp_err_t tool_registry_init(void) mimi_tool_t ws = { .name = "web_search", - .description = "Search the web for current information. Use this when you need up-to-date facts, news, weather, or anything beyond your training data.", + .description = "Search the web for current information via Tavily (preferred) or Brave when configured.", .input_schema_json = "{\"type\":\"object\"," "\"properties\":{\"query\":{\"type\":\"string\",\"description\":\"The search query\"}}," From b75370a290f16c3e6cac2d8b47ae1bfd71177379 Mon Sep 17 00:00:00 2001 From: Asklv Date: Wed, 18 Feb 2026 19:16:00 +0800 Subject: [PATCH 09/12] docs(search): add Tavily setup and CLI usage examples Signed-off-by: Asklv --- README.md | 6 ++++-- README_CN.md | 6 ++++-- README_JA.md | 6 ++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 44c8b79..d46d2a8 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,7 @@ Edit `main/mimi_secrets.h`: #define MIMI_SECRET_API_KEY "sk-ant-api03-xxxxx" #define MIMI_SECRET_MODEL_PROVIDER "anthropic" // "anthropic" or "openai" #define MIMI_SECRET_SEARCH_KEY "" // optional: Brave Search API key +#define MIMI_SECRET_TAVILY_KEY "" // optional: Tavily API key (preferred) #define MIMI_SECRET_PROXY_HOST "" // optional: e.g. "10.0.0.1" #define MIMI_SECRET_PROXY_PORT "" // optional: e.g. "7897" ``` @@ -173,6 +174,7 @@ mimi> set_model gpt-4o # change LLM model mimi> set_proxy 127.0.0.1 7897 # set HTTP proxy mimi> clear_proxy # remove proxy mimi> set_search_key BSA... # set Brave Search API key +mimi> set_tavily_key tvly-... # set Tavily API key (preferred) mimi> config_show # show all config (masked) mimi> config_reset # clear NVS, revert to build-time defaults ``` @@ -252,13 +254,13 @@ MimiClaw supports tool calling for both Anthropic and OpenAI — the LLM can cal | Tool | Description | |------|-------------| -| `web_search` | Search the web via Brave Search API for current information | +| `web_search` | Search the web via Tavily (preferred) or Brave for current information | | `get_current_time` | Fetch current date/time via HTTP and set the system clock | | `cron_add` | Schedule a recurring or one-shot task (the LLM creates cron jobs on its own) | | `cron_list` | List all scheduled cron jobs | | `cron_remove` | Remove a cron job by ID | -To enable web search, set a [Brave Search API key](https://brave.com/search/api/) via `MIMI_SECRET_SEARCH_KEY` in `mimi_secrets.h`. +To enable web search, set a [Tavily API key](https://app.tavily.com/home) via `MIMI_SECRET_TAVILY_KEY` (preferred), or a [Brave Search API key](https://brave.com/search/api/) via `MIMI_SECRET_SEARCH_KEY` in `mimi_secrets.h`. ## Cron Tasks diff --git a/README_CN.md b/README_CN.md index 0401ac5..572905c 100644 --- a/README_CN.md +++ b/README_CN.md @@ -130,6 +130,7 @@ cp main/mimi_secrets.h.example main/mimi_secrets.h #define MIMI_SECRET_API_KEY "sk-ant-api03-xxxxx" #define MIMI_SECRET_MODEL_PROVIDER "anthropic" // "anthropic" 或 "openai" #define MIMI_SECRET_SEARCH_KEY "" // 可选:Brave Search API key +#define MIMI_SECRET_TAVILY_KEY "" // 可选:Tavily API key(优先) #define MIMI_SECRET_PROXY_HOST "10.0.0.1" // 可选:代理地址 #define MIMI_SECRET_PROXY_PORT "7897" // 可选:代理端口 ``` @@ -188,6 +189,7 @@ mimi> set_model gpt-4o # 换模型 mimi> set_proxy 192.168.1.83 7897 # 设置代理 mimi> clear_proxy # 清除代理 mimi> set_search_key BSA... # 设置 Brave Search API Key +mimi> set_tavily_key tvly-... # 设置 Tavily API Key(优先) mimi> config_show # 查看所有配置(脱敏显示) mimi> config_reset # 清除 NVS,恢复编译时默认值 ``` @@ -267,13 +269,13 @@ MimiClaw 同时支持 Anthropic 和 OpenAI 的工具调用 — LLM 在对话中 | 工具 | 说明 | |------|------| -| `web_search` | 通过 Brave Search API 搜索网页,获取实时信息 | +| `web_search` | 通过 Tavily(优先)或 Brave 搜索网页,获取实时信息 | | `get_current_time` | 通过 HTTP 获取当前日期和时间,并设置系统时钟 | | `cron_add` | 创建定时或一次性任务(LLM 自主创建 cron 任务) | | `cron_list` | 列出所有已调度的 cron 任务 | | `cron_remove` | 按 ID 删除 cron 任务 | -启用网页搜索需要在 `mimi_secrets.h` 中设置 [Brave Search API key](https://brave.com/search/api/)(`MIMI_SECRET_SEARCH_KEY`)。 +启用网页搜索可在 `mimi_secrets.h` 中设置 [Tavily API key](https://app.tavily.com/home)(优先,`MIMI_SECRET_TAVILY_KEY`),或 [Brave Search API key](https://brave.com/search/api/)(`MIMI_SECRET_SEARCH_KEY`)。 ## 定时任务(Cron) diff --git a/README_JA.md b/README_JA.md index 6b7b2a8..4ba5777 100644 --- a/README_JA.md +++ b/README_JA.md @@ -130,6 +130,7 @@ cp main/mimi_secrets.h.example main/mimi_secrets.h #define MIMI_SECRET_API_KEY "sk-ant-api03-xxxxx" #define MIMI_SECRET_MODEL_PROVIDER "anthropic" // "anthropic" または "openai" #define MIMI_SECRET_SEARCH_KEY "" // 任意:Brave Search APIキー +#define MIMI_SECRET_TAVILY_KEY "" // 任意:Tavily APIキー(優先) #define MIMI_SECRET_PROXY_HOST "" // 任意:例 "10.0.0.1" #define MIMI_SECRET_PROXY_PORT "" // 任意:例 "7897" ``` @@ -173,6 +174,7 @@ mimi> set_model gpt-4o # LLMモデルを変更 mimi> set_proxy 127.0.0.1 7897 # HTTPプロキシを設定 mimi> clear_proxy # プロキシを削除 mimi> set_search_key BSA... # Brave Search APIキーを設定 +mimi> set_tavily_key tvly-... # Tavily APIキーを設定(優先) mimi> config_show # 全設定を表示(マスク付き) mimi> config_reset # NVSをクリア、ビルド時デフォルトに戻す ``` @@ -252,13 +254,13 @@ MimiClawはAnthropicとOpenAI両方のツール呼び出しをサポート — L | ツール | 説明 | |--------|------| -| `web_search` | Brave Search APIでウェブ検索、最新情報を取得 | +| `web_search` | Tavily(優先)またはBraveでウェブ検索し、最新情報を取得 | | `get_current_time` | HTTP経由で現在の日時を取得し、システムクロックを設定 | | `cron_add` | 定期または単発タスクをスケジュール(LLMが自律的にcronジョブを作成) | | `cron_list` | スケジュール済みのcronジョブを一覧表示 | | `cron_remove` | IDでcronジョブを削除 | -ウェブ検索を有効にするには、`mimi_secrets.h`で[Brave Search APIキー](https://brave.com/search/api/)(`MIMI_SECRET_SEARCH_KEY`)を設定してください。 +ウェブ検索を有効にするには、`mimi_secrets.h`で[Tavily APIキー](https://app.tavily.com/home)(優先、`MIMI_SECRET_TAVILY_KEY`)または[Brave Search APIキー](https://brave.com/search/api/)(`MIMI_SECRET_SEARCH_KEY`)を設定してください。 ## Cronタスク From 443cb97f59d90be778fa1eefda6c65290235ad26 Mon Sep 17 00:00:00 2001 From: Asklv Date: Tue, 3 Mar 2026 01:20:01 +0800 Subject: [PATCH 10/12] feat(search): add CLI web_search command and Tavily bearer auth Signed-off-by: Asklv --- main/cli/serial_cli.c | 83 ++++++++++++++++++++++++++++++++++++ main/tools/tool_web_search.c | 7 ++- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/main/cli/serial_cli.c b/main/cli/serial_cli.c index 32c3ce3..91e04a3 100644 --- a/main/cli/serial_cli.c +++ b/main/cli/serial_cli.c @@ -608,6 +608,78 @@ static int cmd_tool_exec(int argc, char **argv) return (err == ESP_OK) ? 0 : 1; } +/* --- web_search command --- */ +static struct { + struct arg_str *query; + struct arg_end *end; +} web_search_args; + +static bool json_escape_string(const char *in, char *out, size_t out_size) +{ + if (!in || !out || out_size == 0) return false; + size_t o = 0; + for (size_t i = 0; in[i] != '\0'; ++i) { + const char c = in[i]; + const char *esc = NULL; + switch (c) { + case '\\': esc = "\\\\"; break; + case '\"': esc = "\\\""; break; + case '\n': esc = "\\n"; break; + case '\r': esc = "\\r"; break; + case '\t': esc = "\\t"; break; + default: break; + } + if (esc) { + size_t n = strlen(esc); + if (o + n >= out_size) return false; + memcpy(&out[o], esc, n); + o += n; + continue; + } + if ((unsigned char)c < 0x20) { + continue; + } + if (o + 1 >= out_size) return false; + out[o++] = c; + } + out[o] = '\0'; + return true; +} + +static int cmd_web_search(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&web_search_args); + if (nerrors != 0) { + arg_print_errors(stderr, web_search_args.end, argv[0]); + return 1; + } + + char escaped_query[512]; + if (!json_escape_string(web_search_args.query->sval[0], escaped_query, sizeof(escaped_query))) { + printf("Query too long.\n"); + return 1; + } + + char input_json[640]; + int n = snprintf(input_json, sizeof(input_json), "{\"query\":\"%s\"}", escaped_query); + if (n <= 0 || n >= (int)sizeof(input_json)) { + printf("Query too long.\n"); + return 1; + } + + char *output = calloc(1, 4096); + if (!output) { + printf("Out of memory.\n"); + return 1; + } + + esp_err_t err = tool_web_search_execute(input_json, output, 4096); + printf("web_search status: %s\n", esp_err_to_name(err)); + printf("%s\n", output[0] ? output : "(empty)"); + free(output); + return (err == ESP_OK) ? 0 : 1; +} + /* --- restart command --- */ static int cmd_restart(int argc, char **argv) { @@ -896,6 +968,17 @@ esp_err_t serial_cli_init(void) }; esp_console_cmd_register(&tool_exec_cmd); + /* web_search */ + web_search_args.query = arg_str1(NULL, NULL, "", "Search query"); + web_search_args.end = arg_end(1); + esp_console_cmd_t web_search_cmd = { + .command = "web_search", + .help = "Run web search tool directly (e.g. web_search \"latest esp-idf\")", + .func = &cmd_web_search, + .argtable = &web_search_args, + }; + esp_console_cmd_register(&web_search_cmd); + /* restart */ esp_console_cmd_t restart_cmd = { .command = "restart", diff --git a/main/tools/tool_web_search.c b/main/tools/tool_web_search.c index fc0bc5f..f8bb2dc 100644 --- a/main/tools/tool_web_search.c +++ b/main/tools/tool_web_search.c @@ -190,7 +190,6 @@ static char *build_tavily_payload(const char *query) { cJSON *root = cJSON_CreateObject(); if (!root) return NULL; - cJSON_AddStringToObject(root, "api_key", s_tavily_key); cJSON_AddStringToObject(root, "query", query); cJSON_AddNumberToObject(root, "max_results", SEARCH_RESULT_COUNT); cJSON_AddBoolToObject(root, "include_answer", false); @@ -315,6 +314,9 @@ static esp_err_t tavily_search_direct(const char *query, search_buf_t *sb) esp_http_client_set_method(client, HTTP_METHOD_POST); esp_http_client_set_header(client, "Accept", "application/json"); esp_http_client_set_header(client, "Content-Type", "application/json"); + char auth[192]; + snprintf(auth, sizeof(auth), "Bearer %s", s_tavily_key); + esp_http_client_set_header(client, "Authorization", auth); esp_http_client_set_post_field(client, payload, strlen(payload)); esp_err_t err = esp_http_client_perform(client); @@ -347,9 +349,10 @@ static esp_err_t tavily_search_via_proxy(const char *query, search_buf_t *sb) "Host: api.tavily.com\r\n" "Accept: application/json\r\n" "Content-Type: application/json\r\n" + "Authorization: Bearer %s\r\n" "Content-Length: %d\r\n" "Connection: close\r\n\r\n", - (int)strlen(payload)); + s_tavily_key, (int)strlen(payload)); if (proxy_conn_write(conn, header, hlen) < 0 || proxy_conn_write(conn, payload, strlen(payload)) < 0) { From 7edcd31f8a89212f388ea55b2ea105400184cd89 Mon Sep 17 00:00:00 2001 From: Asklv Date: Tue, 3 Mar 2026 01:25:30 +0800 Subject: [PATCH 11/12] fix(search): prevent web_search output overflow and expose tavily secret template Signed-off-by: Asklv --- main/mimi_secrets.h.example | 2 ++ main/tools/tool_web_search.c | 20 ++++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/main/mimi_secrets.h.example b/main/mimi_secrets.h.example index ac087b0..ecebf54 100644 --- a/main/mimi_secrets.h.example +++ b/main/mimi_secrets.h.example @@ -33,3 +33,5 @@ /* Brave Search API */ #define MIMI_SECRET_SEARCH_KEY "" +/* Tavily Search API */ +#define MIMI_SECRET_TAVILY_KEY "" diff --git a/main/tools/tool_web_search.c b/main/tools/tool_web_search.c index f8bb2dc..c67b5c8 100644 --- a/main/tools/tool_web_search.c +++ b/main/tools/tool_web_search.c @@ -139,19 +139,25 @@ static void format_results(cJSON *root, char *output, size_t output_size) cJSON *item; cJSON_ArrayForEach(item, results) { if (idx >= SEARCH_RESULT_COUNT) break; + if (off >= output_size - 1) break; cJSON *title = cJSON_GetObjectItem(item, "title"); cJSON *url = cJSON_GetObjectItem(item, "url"); cJSON *desc = cJSON_GetObjectItem(item, "description"); - off += snprintf(output + off, output_size - off, + int written = snprintf(output + off, output_size - off, "%d. %s\n %s\n %s\n\n", idx + 1, (title && cJSON_IsString(title)) ? title->valuestring : "(no title)", (url && cJSON_IsString(url)) ? url->valuestring : "", (desc && cJSON_IsString(desc)) ? desc->valuestring : ""); - if (off >= output_size - 1) break; + if (written < 0) break; + if ((size_t)written >= output_size - off) { + off = output_size - 1; + break; + } + off += (size_t)written; idx++; } } @@ -169,19 +175,25 @@ static void format_tavily_results(cJSON *root, char *output, size_t output_size) cJSON *item; cJSON_ArrayForEach(item, results) { if (idx >= SEARCH_RESULT_COUNT) break; + if (off >= output_size - 1) break; cJSON *title = cJSON_GetObjectItem(item, "title"); cJSON *url = cJSON_GetObjectItem(item, "url"); cJSON *content = cJSON_GetObjectItem(item, "content"); - off += snprintf(output + off, output_size - off, + int written = snprintf(output + off, output_size - off, "%d. %s\n %s\n %s\n\n", idx + 1, (title && cJSON_IsString(title)) ? title->valuestring : "(no title)", (url && cJSON_IsString(url)) ? url->valuestring : "", (content && cJSON_IsString(content)) ? content->valuestring : ""); - if (off >= output_size - 1) break; + if (written < 0) break; + if ((size_t)written >= output_size - off) { + off = output_size - 1; + break; + } + off += (size_t)written; idx++; } } From 1ed434e5c2dcaae81e531f4e51094a0b8115edd3 Mon Sep 17 00:00:00 2001 From: Asklv Date: Tue, 3 Mar 2026 01:39:12 +0800 Subject: [PATCH 12/12] fix(cli): run web_search in dedicated task to avoid REPL stack overflow Signed-off-by: Asklv --- main/cli/serial_cli.c | 64 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/main/cli/serial_cli.c b/main/cli/serial_cli.c index 91e04a3..4968ff7 100644 --- a/main/cli/serial_cli.c +++ b/main/cli/serial_cli.c @@ -24,6 +24,9 @@ #include "nvs_flash.h" #include "nvs.h" #include "argtable3/argtable3.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" static const char *TAG = "cli"; @@ -614,6 +617,22 @@ static struct { struct arg_end *end; } web_search_args; +typedef struct { + const char *input_json; + char *output; + size_t output_size; + esp_err_t err; + SemaphoreHandle_t done; +} web_search_task_ctx_t; + +static void web_search_task(void *arg) +{ + web_search_task_ctx_t *task_ctx = (web_search_task_ctx_t *)arg; + task_ctx->err = tool_web_search_execute(task_ctx->input_json, task_ctx->output, task_ctx->output_size); + xSemaphoreGive(task_ctx->done); + vTaskDelete(NULL); +} + static bool json_escape_string(const char *in, char *out, size_t out_size) { if (!in || !out || out_size == 0) return false; @@ -673,7 +692,50 @@ static int cmd_web_search(int argc, char **argv) return 1; } - esp_err_t err = tool_web_search_execute(input_json, output, 4096); + web_search_task_ctx_t *ctx = calloc(1, sizeof(*ctx)); + char *input_copy = strdup(input_json); + if (!ctx || !input_copy) { + free(input_copy); + free(ctx); + free(output); + printf("Out of memory.\n"); + return 1; + } + + ctx->input_json = input_copy; + ctx->output = output; + ctx->output_size = 4096; + ctx->done = xSemaphoreCreateBinary(); + if (!ctx->done) { + free(input_copy); + free(ctx); + free(output); + printf("Out of memory.\n"); + return 1; + } + + if (xTaskCreate(web_search_task, "cli_web_search", 20 * 1024, ctx, 5, NULL) != pdPASS) { + vSemaphoreDelete(ctx->done); + free(input_copy); + free(ctx); + free(output); + printf("Failed to start web_search task.\n"); + return 1; + } + + if (xSemaphoreTake(ctx->done, pdMS_TO_TICKS(45000)) != pdTRUE) { + printf("web_search status: timeout\n"); + vSemaphoreDelete(ctx->done); + free(input_copy); + free(ctx); + free(output); + return 1; + } + esp_err_t err = ctx->err; + vSemaphoreDelete(ctx->done); + free(input_copy); + free(ctx); + printf("web_search status: %s\n", esp_err_to_name(err)); printf("%s\n", output[0] ? output : "(empty)"); free(output);