Skip to content
Open
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
27 changes: 27 additions & 0 deletions App/memmy-agent/src/core/agent-runtime/tools/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,8 @@ export class WebSearchTool extends Tool {
return this.config.apiKey || process.env.KAGI_API_KEY ? "kagi" : "duckduckgo";
if (provider === "olostep")
return this.config.apiKey || process.env.OLOSTEP_API_KEY ? "olostep" : "duckduckgo";
if (provider === "youcom")
return this.config.apiKey || process.env.YDC_API_KEY ? "youcom" : "duckduckgo";
return provider;
}

Expand All @@ -497,6 +499,7 @@ export class WebSearchTool extends Tool {
.toLowerCase() || "brave";

if (provider === "olostep") return this.searchOlostep(query, n);
if (provider === "youcom") return this.searchYoucom(query, n);
if (provider === "duckduckgo") return this.searchDuckduckgo(query, n);
if (provider === "tavily") return this.searchTavily(query, n);
if (provider === "searxng") return this.searchSearxng(query, n);
Expand Down Expand Up @@ -691,6 +694,30 @@ export class WebSearchTool extends Tool {
return `Olostep search error: ${(err as Error).message}`;
}
}

async searchYoucom(query: string, n: number): Promise<string> {
const apiKey = this.config.apiKey || process.env.YDC_API_KEY || "";
if (!apiKey) return this.searchDuckduckgo(query, n);
try {
const data = await requestJson("https://ydc-index.io/v1/search", {
method: "POST",
headers: {
"X-API-Key": apiKey,
"Content-Type": "application/json",
"User-Agent": this.userAgent,
},
body: JSON.stringify({ query, count: n }),
});
const items = ((data.results?.web ?? []) as Array<Record<string, any>>).map((item) => ({
title: item.title ?? "",
url: item.url ?? "",
content: item.snippets?.join(" ") || item.description || "",
}));
return formatResults(query, items, n);
} catch (err) {
return `Error: ${(err as Error).message}`;
}
}
}

export class WebFetchTool extends Tool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const WEB_SEARCH_PROVIDER_OPTIONS = [
{ name: "jina", label: "Jina", credential: "api_key" },
{ name: "kagi", label: "Kagi", credential: "api_key" },
{ name: "olostep", label: "Olostep", credential: "api_key" },
{ name: "youcom", label: "You.com", credential: "api_key" },
] as const;

const WEB_SEARCH_PROVIDER_BY_NAME: Map<string, (typeof WEB_SEARCH_PROVIDER_OPTIONS)[number]> = new Map(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ afterEach(() => {
delete process.env.KAGI_API_KEY;
delete process.env.JINA_API_KEY;
delete process.env.TAVILY_API_KEY;
delete process.env.YDC_API_KEY;
});

describe("web_search providers", () => {
Expand Down Expand Up @@ -482,6 +483,106 @@ describe("web_search providers", () => {
expect(result).toContain("Duck Result");
});

it("formats You.com search results", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async (url: string, init: RequestInit) => {
expect(url).toBe("https://ydc-index.io/v1/search");
expect(init.method).toBe("POST");
expect((init.headers as Record<string, string>)["X-API-Key"]).toBe("ydc-key");
expect((init.headers as Record<string, string>)["User-Agent"]).toBe("memmy-search-test");
expect(JSON.parse(String(init.body))).toEqual({ query: "test", count: 2 });
return jsonResponse({
results: {
web: [
{
title: "You.com Result",
url: "https://you.com",
snippets: ["Cited search"],
description: "AI search engine",
},
],
},
});
}),
);

const result = await tool({
provider: "youcom",
apiKey: "ydc-key",
userAgent: "memmy-search-test",
}).execute({
query: "test",
count: 2,
});

expect(result).toContain("You.com Result");
expect(result).toContain("https://you.com");
expect(result).toContain("Cited search");
});

it("prefers You.com snippets over descriptions", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () =>
jsonResponse({
results: {
web: [
{
title: "Snippet Result",
url: "https://example.com/snippet",
description: "fallback description",
snippets: ["primary snippet"],
},
],
},
}),
),
);

const result = await tool({ provider: "youcom", apiKey: "ydc-key" }).execute({ query: "test" });

expect(result).toContain("primary snippet");
expect(result).not.toContain("fallback description");
});

it("falls back to You.com descriptions when snippets are missing", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () =>
jsonResponse({
results: {
web: [{ title: "Description Result", url: "https://example.com", description: "description body" }],
},
}),
),
);

const result = await tool({ provider: "youcom", apiKey: "ydc-key" }).execute({ query: "test" });

expect(result).toContain("description body");
});

it("reports You.com search errors clearly", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => jsonResponse({ error: "unauthorized" }, 401)),
);

const result = await tool({ provider: "youcom", apiKey: "ydc-key" }).execute({ query: "test" });

expect(result).toContain("Error");
expect(result).toContain("401");
});

it("falls back to DuckDuckGo when You.com has no key", async () => {
stubDuckDuckGo();

const result = await tool({ provider: "youcom", apiKey: "" }).execute({ query: "test" });

expect(result).toContain("Duck Result");
});

it("reports unknown providers", async () => {
const result = await tool({ provider: "unknown" }).execute({ query: "test" });

Expand Down
2 changes: 2 additions & 0 deletions docs/cn/tools/mcp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ tools:
```

内置工具覆盖文件系统、shell、Web 搜索与抓取、MCP、定时任务、消息发送、图像生成、长任务、自省、运行时状态和上下文管理。

`web.search.provider` 支持 `duckduckgo`(默认,无需密钥)、`brave`、`tavily`、`searxng`、`jina`、`kagi`、`olostep` 和 `youcom`。需要密钥的提供方从 `tools.web.search.apiKey` 或对应的环境变量读取密钥(例如 `BRAVE_API_KEY`、`TAVILY_API_KEY`、You.com 使用 `YDC_API_KEY`),未配置密钥时回退到 DuckDuckGo。
2 changes: 2 additions & 0 deletions docs/en/tools/mcp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ tools:
```

Built-in tools cover the filesystem, shell, web search and fetching, MCP, scheduled tasks, message sending, image generation, long-running tasks, introspection, runtime status, and context management.

`web.search.provider` supports `duckduckgo` (default, no key required), `brave`, `tavily`, `searxng`, `jina`, `kagi`, `olostep`, and `youcom`. Key-based providers read their key from `tools.web.search.apiKey` or the matching environment variable (e.g. `BRAVE_API_KEY`, `TAVILY_API_KEY`, `YDC_API_KEY` for You.com) and fall back to DuckDuckGo when no key is configured.
Loading