From 19c6298749b33ae77621d557e2d7ee76828808aa Mon Sep 17 00:00:00 2001 From: thura-aung Date: Sun, 26 Jul 2026 12:00:02 +0000 Subject: [PATCH] feat(tools): add You.com web search tool Add youcomSearch tool wrapping You.com Search API (POST https://ydc-index.io/v1/search) following the weather.tools.ts pattern with fetchJsonWithRetry and optional YOUCOM_API_KEY env var. Registered in AgentChatCompletionService.buildTools() with needsApproval=false and added to skipApproval set. - youcom-search.tools.ts: new tool using Vercel ai SDK tool() with zod schema (query, maxResults) - index.ts: export new tool - agent-chat-completion.service.ts: import, register, add to skipApproval set Co-Authored-By: Claude Opus 4.6 --- .../@buildingai/ai-toolkit/src/tools/index.ts | 1 + .../src/tools/youcom-search.tools.ts | 116 ++++++++++++++++++ .../services/agent-chat-completion.service.ts | 3 + 3 files changed, 120 insertions(+) create mode 100644 packages/@buildingai/ai-toolkit/src/tools/youcom-search.tools.ts diff --git a/packages/@buildingai/ai-toolkit/src/tools/index.ts b/packages/@buildingai/ai-toolkit/src/tools/index.ts index f9fba4c95..0c229d6e0 100644 --- a/packages/@buildingai/ai-toolkit/src/tools/index.ts +++ b/packages/@buildingai/ai-toolkit/src/tools/index.ts @@ -4,3 +4,4 @@ export * from "./openai-research.tools.js"; export * from "./read-attached-file.tools.js"; export * from "./request-execution-plan.tools.js"; export * from "./weather.tools.js"; +export * from "./youcom-search.tools.js"; diff --git a/packages/@buildingai/ai-toolkit/src/tools/youcom-search.tools.ts b/packages/@buildingai/ai-toolkit/src/tools/youcom-search.tools.ts new file mode 100644 index 000000000..1d1550ec9 --- /dev/null +++ b/packages/@buildingai/ai-toolkit/src/tools/youcom-search.tools.ts @@ -0,0 +1,116 @@ +import { tool } from "ai"; +import { z } from "zod"; + +const YOUCOM_SEARCH_URL = "https://ydc-index.io/v1/search"; + +interface SearchResult { + title: string; + url: string; + snippet: string; +} + +interface YoucomSearchResponse { + results?: SearchResult[]; +} + +async function fetchJsonWithRetry( + url: string, + options?: RequestInit, + retries = 2, + timeoutMs = 15000, +): Promise { + let lastError: unknown; + + for (let attempt = 0; attempt <= retries; attempt++) { + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + + try { + const response = await fetch(url, { + ...options, + signal: controller.signal, + }); + + if (!response.ok) { + throw new Error(`HTTP_${response.status}`); + } + + return await response.json(); + } finally { + clearTimeout(timeoutId); + } + } catch (error) { + lastError = error; + } + } + + throw lastError instanceof Error ? lastError : new Error("FETCH_FAILED"); +} + +export const youcomSearch = tool({ + description: + "Search the web for up-to-date information using You.com Search API. Use this tool when you need to find current events, latest news, real-time data, or any information that may not be in your training data. Input should be a self-contained search query.", + inputSchema: z.object({ + query: z.string().describe("The search query — be specific and include key terms"), + maxResults: z + .number() + .int() + .min(1) + .max(20) + .default(10) + .optional() + .describe("Maximum number of results to return, between 1 and 20"), + }), + needsApproval: false, + execute: async (input) => { + const apiKey = process.env.YOUCOM_API_KEY; + + const payload: Record = { + query: input.query, + count: input.maxResults ?? 10, + }; + + const headers: Record = { + "Content-Type": "application/json", + Accept: "application/json", + }; + if (apiKey) { + headers["X-API-Key"] = apiKey; + } + + let data: YoucomSearchResponse; + try { + data = (await fetchJsonWithRetry( + YOUCOM_SEARCH_URL, + { + method: "POST", + headers, + body: JSON.stringify(payload), + }, + 2, + 15000, + )) as YoucomSearchResponse; + } catch (error) { + return { + success: false, + error: `You.com Search request failed: ${error instanceof Error ? error.message : "Unknown error" + }`, + }; + } + + const results = data?.results; + if (!results || results.length === 0) { + return { success: true, results: [], message: "No results found" }; + } + + return { + success: true, + results: results.slice(0, input.maxResults ?? 10).map((r) => ({ + title: r.title, + url: r.url, + snippet: r.snippet, + })), + }; + }, +}); diff --git a/packages/api/src/modules/ai/agents/services/agent-chat-completion.service.ts b/packages/api/src/modules/ai/agents/services/agent-chat-completion.service.ts index 8f456c782..e4c79c1d0 100644 --- a/packages/api/src/modules/ai/agents/services/agent-chat-completion.service.ts +++ b/packages/api/src/modules/ai/agents/services/agent-chat-completion.service.ts @@ -18,6 +18,7 @@ import { createReadAttachedFileTool, createRequestExecutionPlanTool, getWeather, + youcomSearch, } from "@buildingai/ai-toolkit/tools"; import { parseFile, @@ -726,6 +727,7 @@ export class AgentChatCompletionService { const tools: Record = { ...mcpTools }; tools.getWeather = getWeather; + tools.youcomSearch = youcomSearch; if (documentContents?.length) { tools.read_attached_file = createReadAttachedFileTool({ documents: documentContents }); @@ -818,6 +820,7 @@ export class AgentChatCompletionService { "getWeather", "read_attached_file", "request_execution_plan", + "youcomSearch", ]); return Object.fromEntries(