import { type Static, Type } from "@sinclair/typebox"; import type { ToolDefinition } from "@mariozechner/pi-coding-agent"; import { SearchClient, type ImageItem, type SearchResponse, type WebItem } from "coze-coding-dev-sdk"; import { formatCozeError } from "../common/format-coze-error.js"; type SearchResultItem = { title: string; url?: string; siteName?: string; snippet?: string; content?: string; publishTime?: string; imageUrl?: string; imageWidth?: number; imageHeight?: number; }; type SearchResult = { query: string; type: "web" | "image"; summary?: string; items: SearchResultItem[]; }; function mapWebItem(item: WebItem): SearchResultItem { return { title: item.title, url: item.url, siteName: item.site_name, snippet: item.snippet, content: item.content, publishTime: item.publish_time, }; } function mapImageItem(item: ImageItem): SearchResultItem { return { title: item.title ?? "Untitled", url: item.url, siteName: item.site_name, publishTime: item.publish_time, imageUrl: item.image?.url, imageWidth: item.image?.width, imageHeight: item.image?.height, }; } type SearchInput = { query: string; type?: "web" | "image"; count?: number; timeRange?: string; sites?: string; blockHosts?: string; needSummary?: boolean; needContent?: boolean; }; async function searchWeb(input: SearchInput): Promise { const client = new SearchClient(); const type = input.type ?? "web"; const count = input.count ?? 10; let response: SearchResponse; if (type === "image") { response = await client.imageSearch(input.query, count); return { query: input.query, type, items: (response.image_items ?? []).map(mapImageItem), }; } const hasAdvanced = Boolean(input.timeRange || input.sites || input.blockHosts || input.needContent); if (hasAdvanced) { response = await client.advancedSearch(input.query, { searchType: "web", count, timeRange: input.timeRange, sites: input.sites, blockHosts: input.blockHosts, needSummary: input.needSummary, needContent: input.needContent, }); } else { response = await client.webSearch(input.query, count, input.needSummary); } return { query: input.query, type, summary: response.summary, items: (response.web_items ?? []).map(mapWebItem), }; } const SearchToolSchema = Type.Object( { query: Type.String({ description: "Search query." }), type: Type.Optional( Type.Unsafe<"web" | "image">({ type: "string", enum: ["web", "image"], description: "Search type. Defaults to web.", }), ), count: Type.Optional( Type.Integer({ description: "Number of results to return.", minimum: 1, maximum: 20, }), ), timeRange: Type.Optional( Type.Unsafe<"1d" | "1w" | "1m">({ type: "string", enum: ["1d", "1w", "1m"], description: "Recency filter for web search.", }), ), sites: Type.Optional( Type.String({ description: "Comma-separated domains to include." }), ), blockHosts: Type.Optional( Type.String({ description: "Comma-separated domains to exclude." }), ), needSummary: Type.Optional( Type.Boolean({ description: "Whether to include Coze summary output." }), ), needContent: Type.Optional( Type.Boolean({ description: "Whether to include extracted page content." }), ), }, { additionalProperties: false }, ); function buildSearchText( result: SearchResult, options: { includeContent: boolean }, ): string { const lines = [`Coze web search: ${result.query}`]; if (result.summary) { lines.push("", `Summary: ${result.summary}`); } lines.push("", `Results (${result.items.length})`); for (const [index, item] of result.items.entries()) { lines.push(`${index + 1}. ${item.title}`); if (item.url) { lines.push(` URL: ${item.url}`); } if (item.imageUrl) { lines.push(` Image: ${item.imageUrl}`); } if (item.siteName) { lines.push(` Source: ${item.siteName}`); } if (item.publishTime) { lines.push(` Published: ${item.publishTime}`); } if (item.snippet) { lines.push(` ${item.snippet}`); } if (options.includeContent && item.content) { lines.push(" Content:"); lines.push(` ${item.content}`); } } return lines.join("\n"); } export const cozeWebSearchTool: ToolDefinition = { name: "coze_web_search", label: "Coze Web Search", description: "Search the web or images through Coze. Supports summaries, recency filters, and site restrictions.", parameters: SearchToolSchema, async execute(_toolCallId, rawParams) { const params = rawParams as Static; console.log("[coze_web_search] query=%s type=%s count=%s", params.query, params.type ?? "web", params.count ?? 10); try { const result = await searchWeb(params); console.log("[coze_web_search] done, %d results", result.items.length); return { content: [ { type: "text", text: buildSearchText(result, { includeContent: params.needContent === true }), }, ], details: { query: result.query, type: result.type, summary: result.summary, count: result.items.length, }, }; } catch (error) { const message = formatCozeError(error); console.error("[coze_web_search] error:", message); return { content: [{ type: "text", text: `Error: ${message}` }], details: { error: true, message }, }; } }, };