tavily.ts 2.3 KB

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