route.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. /**
  2. * Web Search API
  3. *
  4. * POST /api/web-search
  5. * Simple JSON request/response using Tavily search.
  6. */
  7. import { NextRequest } from 'next/server';
  8. import { callLLM } from '@/lib/ai/llm';
  9. import { searchWithTavily, formatSearchResultsAsContext } from '@/lib/web-search/tavily';
  10. import { resolveWebSearchApiKey } from '@/lib/server/provider-config';
  11. import { createLogger } from '@/lib/logger';
  12. import { apiError, apiSuccess } from '@/lib/server/api-response';
  13. import {
  14. buildSearchQuery,
  15. SEARCH_QUERY_REWRITE_EXCERPT_LENGTH,
  16. } from '@/lib/server/search-query-builder';
  17. import { resolveModelFromHeaders } from '@/lib/server/resolve-model';
  18. import type { AICallFn } from '@/lib/generation/pipeline-types';
  19. const log = createLogger('WebSearch');
  20. export async function POST(req: NextRequest) {
  21. let query: string | undefined;
  22. try {
  23. const body = await req.json();
  24. const {
  25. query: requestQuery,
  26. pdfText,
  27. apiKey: clientApiKey,
  28. } = body as {
  29. query?: string;
  30. pdfText?: string;
  31. apiKey?: string;
  32. };
  33. query = requestQuery;
  34. if (!query || !query.trim()) {
  35. return apiError('MISSING_REQUIRED_FIELD', 400, 'query is required');
  36. }
  37. const apiKey = resolveWebSearchApiKey(clientApiKey);
  38. if (!apiKey) {
  39. return apiError(
  40. 'MISSING_API_KEY',
  41. 400,
  42. 'Tavily API key is not configured. Set it in Settings → Web Search or set TAVILY_API_KEY env var.',
  43. );
  44. }
  45. // Clamp rewrite input at the route boundary; framework body limits still apply to total request size.
  46. const boundedPdfText = pdfText?.slice(0, SEARCH_QUERY_REWRITE_EXCERPT_LENGTH);
  47. let aiCall: AICallFn | undefined;
  48. try {
  49. const { model: languageModel } = await resolveModelFromHeaders(req);
  50. aiCall = async (systemPrompt, userPrompt) => {
  51. const result = await callLLM(
  52. {
  53. model: languageModel,
  54. messages: [
  55. { role: 'system', content: systemPrompt },
  56. { role: 'user', content: userPrompt },
  57. ],
  58. maxOutputTokens: 256,
  59. },
  60. 'web-search-query-rewrite',
  61. );
  62. return result.text;
  63. };
  64. } catch (error) {
  65. log.warn('Search query rewrite model unavailable, falling back to raw requirement:', error);
  66. }
  67. const searchQuery = await buildSearchQuery(query, boundedPdfText, aiCall);
  68. log.info('Running web search API request', {
  69. hasPdfContext: searchQuery.hasPdfContext,
  70. rawRequirementLength: searchQuery.rawRequirementLength,
  71. rewriteAttempted: searchQuery.rewriteAttempted,
  72. finalQueryLength: searchQuery.finalQueryLength,
  73. });
  74. const result = await searchWithTavily({ query: searchQuery.query, apiKey });
  75. const context = formatSearchResultsAsContext(result);
  76. return apiSuccess({
  77. answer: result.answer,
  78. sources: result.sources,
  79. context,
  80. query: result.query,
  81. responseTime: result.responseTime,
  82. });
  83. } catch (err) {
  84. log.error(`Web search failed [query="${query?.substring(0, 60) ?? 'unknown'}"]:`, err);
  85. const message = err instanceof Error ? err.message : 'Web search failed';
  86. return apiError('INTERNAL_ERROR', 500, message);
  87. }
  88. }