File size: 2,384 Bytes
f56a29b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | /**
* Tavily Web Search Integration
*
* Uses raw REST API via proxyFetch for reliable proxy support.
* Tavily search endpoint: POST https://api.tavily.com/search
*/
import { proxyFetch } from '@/lib/server/proxy-fetch';
import type { WebSearchResult, WebSearchSource } from '@/lib/types/web-search';
const TAVILY_API_URL = 'https://api.tavily.com/search';
const TAVILY_MAX_QUERY_LENGTH = 400;
/**
* Search the web using Tavily REST API and return structured results.
*/
export async function searchWithTavily(params: {
query: string;
apiKey: string;
maxResults?: number;
}): Promise<WebSearchResult> {
const { query, apiKey, maxResults = 5 } = params;
// Tavily rejects queries over 400 characters with a 400 error
const truncatedQuery = query.slice(0, TAVILY_MAX_QUERY_LENGTH);
const res = await proxyFetch(TAVILY_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
query: truncatedQuery,
search_depth: 'basic',
max_results: maxResults,
include_answer: 'basic',
}),
});
if (!res.ok) {
const errorText = await res.text().catch(() => '');
throw new Error(`Tavily API error (${res.status}): ${errorText || res.statusText}`);
}
const data = (await res.json()) as {
answer?: string;
query: string;
response_time: number;
results: Array<{
title: string;
url: string;
content: string;
score: number;
}>;
};
const sources: WebSearchSource[] = (data.results || []).map((r) => ({
title: r.title,
url: r.url,
content: r.content,
score: r.score,
}));
return {
answer: data.answer || '',
sources,
query: data.query,
responseTime: data.response_time,
};
}
/**
* Format search results into a markdown context block for LLM prompts.
*/
export function formatSearchResultsAsContext(result: WebSearchResult): string {
if (!result.answer && result.sources.length === 0) {
return '';
}
const lines: string[] = [];
if (result.answer) {
lines.push(result.answer);
lines.push('');
}
if (result.sources.length > 0) {
lines.push('Sources:');
for (const src of result.sources) {
lines.push(`- [${src.title}](${src.url}): ${src.content.slice(0, 200)}`);
}
}
return lines.join('\n');
}
|