Merge pull request #114 from IRONICBo/feat/support-tavily-websearch

This commit is contained in:
crispyberry
2026-03-06 00:12:32 +08:00
committed by GitHub
10 changed files with 447 additions and 35 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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タスク

View File

@@ -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"

View File

@@ -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";
@@ -299,6 +302,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 +540,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;
}
@@ -589,6 +611,137 @@ 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;
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;
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;
}
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);
return (err == ESP_OK) ? 0 : 1;
}
/* --- restart command --- */
static int cmd_restart(int argc, char **argv)
{
@@ -805,6 +958,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, "<key>", "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, "<host>", "Proxy host/IP");
proxy_args.port = arg_int1(NULL, NULL, "<port>", "Proxy port");
@@ -866,6 +1030,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, "<query>", "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",

View File

@@ -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"

View File

@@ -33,3 +33,5 @@
/* Brave Search API */
#define MIMI_SECRET_SEARCH_KEY ""
/* Tavily Search API */
#define MIMI_SECRET_TAVILY_KEY ""

View File

@@ -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\"}},"

View File

@@ -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 <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;
}
@@ -113,26 +139,81 @@ 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++;
}
}
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;
if (off >= output_size - 1) break;
cJSON *title = cJSON_GetObjectItem(item, "title");
cJSON *url = cJSON_GetObjectItem(item, "url");
cJSON *content = cJSON_GetObjectItem(item, "content");
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 (written < 0) break;
if ((size_t)written >= output_size - off) {
off = output_size - 1;
break;
}
off += (size_t)written;
idx++;
}
}
static char *build_tavily_payload(const char *query)
{
cJSON *root = cJSON_CreateObject();
if (!root) return NULL;
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 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,
@@ -147,7 +228,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);
@@ -163,7 +244,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;
@@ -175,7 +256,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);
@@ -222,12 +303,121 @@ static esp_err_t 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");
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);
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"
"Authorization: Bearer %s\r\n"
"Content-Length: %d\r\n"
"Connection: close\r\n\r\n",
s_tavily_key, (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)
{
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;
}
@@ -247,15 +437,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);
@@ -267,12 +455,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) {
@@ -290,7 +489,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));
@@ -305,7 +508,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;
}

View File

@@ -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);