Files
chat-gpz/lib/tools/web-search.ts
T
Zacharias-Brohn bcebaed78f changes
2026-01-14 22:36:22 +01:00

128 lines
3.4 KiB
TypeScript

/**
* Web Search tool - search the internet using DuckDuckGo or SearXNG
*/
import { Tool } from 'ollama';
import { ToolHandler, ToolResult } from './types';
export const webSearchTool: Tool = {
type: 'function',
function: {
name: 'web_search',
description:
'Search the internet for current information. Returns a list of relevant search results with titles, URLs, and snippets.',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'The search query',
},
num_results: {
type: 'number',
description: 'Number of results to return (default: 5, max: 10)',
},
},
required: ['query'],
},
},
};
interface SearchResult {
title: string;
url: string;
snippet: string;
}
// DuckDuckGo HTML search (no API key needed)
async function searchDuckDuckGo(query: string, numResults: number): Promise<SearchResult[]> {
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
const response = await fetch(url, {
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
},
signal: AbortSignal.timeout(10000),
});
if (!response.ok) {
throw new Error(`Search failed: HTTP ${response.status}`);
}
const html = await response.text();
const results: SearchResult[] = [];
// Parse results from DDG HTML
// Results are in <div class="result"> elements
const resultRegex =
/<a[^>]+class="result__a"[^>]*href="([^"]+)"[^>]*>([^<]+)<\/a>[\s\S]*?<a[^>]+class="result__snippet"[^>]*>([^<]*(?:<[^>]+>[^<]*)*)<\/a>/gi;
let match;
while ((match = resultRegex.exec(html)) !== null && results.length < numResults) {
const url = match[1];
const title = match[2].trim();
const snippet = match[3].replace(/<[^>]+>/g, '').trim();
if (url && title && !url.startsWith('/')) {
results.push({ title, url, snippet });
}
}
// Fallback: try alternative parsing if no results
if (results.length === 0) {
const altRegex =
/<div class="result[^"]*"[\s\S]*?<a[^>]+href="([^"]+)"[^>]*>[\s\S]*?<\/a>[\s\S]*?class="result__title"[^>]*>([^<]+)/gi;
while ((match = altRegex.exec(html)) !== null && results.length < numResults) {
const url = match[1];
const title = match[2].trim();
if (url && title && !url.startsWith('/')) {
results.push({ title, url, snippet: '' });
}
}
}
return results;
}
export const webSearchHandler: ToolHandler = async (args): Promise<ToolResult> => {
const query = args.query as string;
const numResults = Math.min((args.num_results as number) || 5, 10);
if (!query) {
return {
success: false,
error: 'No search query provided',
};
}
try {
const results = await searchDuckDuckGo(query, numResults);
if (results.length === 0) {
return {
success: true,
result: `No search results found for: "${query}"`,
};
}
const formatted = results
.map(
(r, i) => `${i + 1}. ${r.title}\n URL: ${r.url}${r.snippet ? `\n ${r.snippet}` : ''}`
)
.join('\n\n');
return {
success: true,
result: `Search results for "${query}":\n\n${formatted}`,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Search failed',
};
}
};