Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions app/api/web-search/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
*
* POST /api/web-search
* Simple JSON request/response using Tavily search.
* When pdfText is provided, enhances the query using LLM to extract
* meaningful keywords from the document.
*/

import { searchWithTavily, formatSearchResultsAsContext } from '@/lib/web-search/tavily';
import { resolveWebSearchApiKey } from '@/lib/server/provider-config';
import { enhanceSearchQuery } from '@/lib/web-search/query-enhancer';
import { createLogger } from '@/lib/logger';
import { apiError, apiSuccess } from '@/lib/server/api-response';

Expand All @@ -15,9 +18,22 @@ const log = createLogger('WebSearch');
export async function POST(req: Request) {
try {
const body = await req.json();
const { query, apiKey: clientApiKey } = body as {
const {
query,
apiKey: clientApiKey,
pdfText,
modelConfig,
} = body as {
query?: string;
apiKey?: string;
pdfText?: string;
modelConfig?: {
modelString?: string;
apiKey?: string;
baseUrl?: string;
providerType?: string;
requiresApiKey?: boolean;
};
};

if (!query || !query.trim()) {
Expand All @@ -33,7 +49,10 @@ export async function POST(req: Request) {
);
}

const result = await searchWithTavily({ query: query.trim(), apiKey });
// Enhance query with PDF context if available
const effectiveQuery = await enhanceSearchQuery(query.trim(), pdfText, modelConfig);

const result = await searchWithTavily({ query: effectiveQuery, apiKey });
const context = formatSearchResultsAsContext(result);

return apiSuccess({
Expand Down
11 changes: 11 additions & 0 deletions app/generation-preview/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -307,12 +307,23 @@ function GenerationPreviewContent() {
const wsSettings = useSettingsStore.getState();
const wsApiKey =
wsSettings.webSearchProvidersConfig?.[wsSettings.webSearchProviderId]?.apiKey;
const wsModelConfig = getCurrentModelConfig();
const res = await fetch('/api/web-search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: currentSession.requirements.requirement,
apiKey: wsApiKey || undefined,
pdfText: currentSession.pdfText || undefined,
modelConfig: currentSession.pdfText
? {
modelString: wsModelConfig.modelString,
apiKey: wsModelConfig.apiKey,
baseUrl: wsModelConfig.baseUrl,
providerType: wsModelConfig.providerType,
requiresApiKey: wsModelConfig.requiresApiKey,
}
: undefined,
}),
signal,
});
Expand Down
4 changes: 3 additions & 1 deletion lib/server/classroom-generation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { parseModelString } from '@/lib/ai/providers';
import { resolveApiKey, resolveWebSearchApiKey } from '@/lib/server/provider-config';
import { resolveModel } from '@/lib/server/resolve-model';
import { searchWithTavily, formatSearchResultsAsContext } from '@/lib/web-search/tavily';
import { enhanceSearchQuery } from '@/lib/web-search/query-enhancer';
import { persistClassroom } from '@/lib/server/classroom-storage';
import {
generateMediaForClassroom,
Expand Down Expand Up @@ -241,7 +242,8 @@ export async function generateClassroom(
if (tavilyKey) {
try {
log.info('Running web search for requirement context...');
const searchResult = await searchWithTavily({ query: requirement, apiKey: tavilyKey });
const searchQuery = await enhanceSearchQuery(requirement, pdfText);
const searchResult = await searchWithTavily({ query: searchQuery, apiKey: tavilyKey });
researchContext = formatSearchResultsAsContext(searchResult);
if (researchContext) {
log.info(`Web search returned ${searchResult.sources.length} sources`);
Expand Down
86 changes: 86 additions & 0 deletions lib/web-search/query-enhancer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* Search Query Enhancer
*
* When a PDF is uploaded but the user requirement is too vague
* (e.g. "讲讲这篇论文"), uses LLM to extract meaningful keywords
* from the document text for a better web search query.
*/

import { callLLM } from '@/lib/ai/llm';
import { resolveModel, type ResolvedModel } from '@/lib/server/resolve-model';
import { createLogger } from '@/lib/logger';

const log = createLogger('QueryEnhancer');

/**
* Enhance a search query using PDF text context.
*
* If pdfText is provided, uses a lightweight LLM call to extract
* the document's title, key topics, and important terms, then
* constructs a concise search query.
*
* Returns the original query if no PDF is available or if
* enhancement fails.
*
* @param modelConfig - Optional model config for the LLM call.
* If not provided, uses server-configured default model.
*/
export async function enhanceSearchQuery(
query: string,
pdfText: string | undefined,
modelConfig?: {
modelString?: string;
apiKey?: string;
baseUrl?: string;
providerType?: string;
requiresApiKey?: boolean;
},
): Promise<string> {
if (!pdfText) return query;

try {
let resolved: ResolvedModel;
try {
resolved = resolveModel(modelConfig || {});
} catch {
log.warn('No model available for query enhancement, using original query');
return query;
}

const snippet = pdfText.slice(0, 2000);

const result = await callLLM(
{
model: resolved.model,
messages: [
{
role: 'system',
content:
'You extract search keywords from documents. Return ONLY a concise search query string (under 200 characters), no explanation.',
},
{
role: 'user',
content: `The user uploaded a document and said: "${query}"

Extract the document's title, key topics, and important terms from the text below, then combine them into a concise web search query (under 200 characters) that would find relevant supplementary materials.

Document text (first 2000 chars):
${snippet}`,
},
],
maxOutputTokens: resolved.modelInfo?.outputWindow,
},
'enhance-search-query',
);

const enhanced = result.text.trim();
if (enhanced && enhanced.length > 5 && enhanced.length <= 400) {
log.info(`Enhanced search query: "${query}" → "${enhanced}"`);
return enhanced;
}
} catch (e) {
log.warn('Search query enhancement failed, using original query:', e);
}

return query;
}
Loading