File size: 3,306 Bytes
f56a29b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | /**
* 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 { 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.',
);
}
// 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, 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);
}
}
|