| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697 |
- /**
- * 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');
- }
|