| |
| |
| |
| |
| |
| |
|
|
| import { NextRequest } from 'next/server'; |
| import { callLLM } from '@/lib/ai/llm'; |
| import { searchWithTavily, formatSearchResultsAsContext } from '@/lib/web-search/tavily'; |
| import { resolveWebSearchApiKey } from '@/lib/server/provider-config'; |
| import { createLogger } from '@/lib/logger'; |
| import { apiError, apiSuccess } from '@/lib/server/api-response'; |
| import { |
| buildSearchQuery, |
| SEARCH_QUERY_REWRITE_EXCERPT_LENGTH, |
| } from '@/lib/server/search-query-builder'; |
| import { resolveModelFromRequest } from '@/lib/server/resolve-model'; |
| import type { AICallFn } from '@/lib/generation/pipeline-types'; |
|
|
| const log = createLogger('WebSearch'); |
|
|
| export async function POST(req: NextRequest) { |
| let query: string | undefined; |
| try { |
| const body = await req.json(); |
| const { |
| query: requestQuery, |
| pdfText, |
| apiKey: clientApiKey, |
| } = body as { |
| query?: string; |
| pdfText?: string; |
| apiKey?: string; |
| }; |
| query = requestQuery; |
|
|
| if (!query || !query.trim()) { |
| return apiError('MISSING_REQUIRED_FIELD', 400, 'query is required'); |
| } |
|
|
| const apiKey = resolveWebSearchApiKey(clientApiKey); |
| if (!apiKey) { |
| return apiError( |
| 'MISSING_API_KEY', |
| 400, |
| 'Tavily API key is not configured. Set it in Settings → Web Search or set TAVILY_API_KEY env var.', |
| ); |
| } |
|
|
| |
| const boundedPdfText = pdfText?.slice(0, SEARCH_QUERY_REWRITE_EXCERPT_LENGTH); |
|
|
| let aiCall: AICallFn | undefined; |
| try { |
| const { model: languageModel, thinkingConfig } = await resolveModelFromRequest(req, body); |
| aiCall = async (systemPrompt, userPrompt) => { |
| const result = await callLLM( |
| { |
| model: languageModel, |
| messages: [ |
| { role: 'system', content: systemPrompt }, |
| { role: 'user', content: userPrompt }, |
| ], |
| maxOutputTokens: 256, |
| }, |
| 'web-search-query-rewrite', |
| undefined, |
| thinkingConfig, |
| ); |
| return result.text; |
| }; |
| } catch (error) { |
| log.warn('Search query rewrite model unavailable, falling back to raw requirement:', error); |
| } |
|
|
| const searchQuery = await buildSearchQuery(query, boundedPdfText, aiCall); |
|
|
| log.info('Running web search API request', { |
| hasPdfContext: searchQuery.hasPdfContext, |
| rawRequirementLength: searchQuery.rawRequirementLength, |
| rewriteAttempted: searchQuery.rewriteAttempted, |
| finalQueryLength: searchQuery.finalQueryLength, |
| }); |
|
|
| const result = await searchWithTavily({ query: searchQuery.query, apiKey }); |
| const context = formatSearchResultsAsContext(result); |
|
|
| return apiSuccess({ |
| answer: result.answer, |
| sources: result.sources, |
| context, |
| query: result.query, |
| responseTime: result.responseTime, |
| }); |
| } catch (err) { |
| log.error(`Web search failed [query="${query?.substring(0, 60) ?? 'unknown'}"]:`, err); |
| const message = err instanceof Error ? err.message : 'Web search failed'; |
| return apiError('INTERNAL_ERROR', 500, message); |
| } |
| } |
|
|