| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899 |
- /**
- * Web Search API
- *
- * POST /api/web-search
- * Simple JSON request/response using Tavily search.
- */
- 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 { resolveModelFromHeaders } 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.',
- );
- }
- // Clamp rewrite input at the route boundary; framework body limits still apply to total request size.
- const boundedPdfText = pdfText?.slice(0, SEARCH_QUERY_REWRITE_EXCERPT_LENGTH);
- let aiCall: AICallFn | undefined;
- try {
- const { model: languageModel } = await resolveModelFromHeaders(req);
- 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',
- );
- 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);
- }
- }
|