Skip to content

Commit cc2b39e

Browse files
Programcodeclaude
andcommitted
fix: Add timeout safeguards to deep research for Generate and Brainstorm
Generate and Brainstorm now use light research (1 search round, 2 URL reads, 20s timeout) instead of full deep research which caused edge function timeouts. Factoids keeps deep research with a 45s safety timeout. All research gracefully falls back to empty context on timeout. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 390883e commit cc2b39e

1 file changed

Lines changed: 72 additions & 30 deletions

File tree

‎supabase/functions/ai/index.ts‎

Lines changed: 72 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,24 @@ interface ResearchContext {
4040
verifiedUrls: Set<string>;
4141
}
4242

43+
interface ResearchOptions {
44+
maxFollowUpQueries: number; // 0 = skip AI follow-up entirely
45+
maxUrlsToRead: number; // How many pages to read in full
46+
overallTimeoutMs: number; // Hard timeout for entire research
47+
}
48+
49+
const LIGHT_RESEARCH: ResearchOptions = {
50+
maxFollowUpQueries: 0,
51+
maxUrlsToRead: 2,
52+
overallTimeoutMs: 20000,
53+
};
54+
55+
const DEEP_RESEARCH: ResearchOptions = {
56+
maxFollowUpQueries: 3,
57+
maxUrlsToRead: 5,
58+
overallTimeoutMs: 45000,
59+
};
60+
4361
// Generate unique request ID for tracing
4462
function generateRequestId(): string {
4563
return `edge_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
@@ -384,50 +402,51 @@ async function generateFollowUpQueries(
384402
}
385403
}
386404

387-
async function deepResearch(
405+
async function deepResearchInner(
388406
claim: string,
389407
region: string,
390408
provider: Provider,
391-
model: string
409+
model: string,
410+
options: ResearchOptions
392411
): Promise<ResearchContext> {
393412
const context: ResearchContext = {
394413
searches: [],
395414
readContent: [],
396415
verifiedUrls: new Set(),
397416
};
398417

399-
console.log("[DEEP RESEARCH] Starting for claim:", claim);
418+
console.log(`[RESEARCH] Starting (mode: ${options.maxFollowUpQueries > 0 ? "deep" : "light"}) for: ${claim}`);
400419

401420
// Round 1: Initial search
402-
const initialQuery = `${claim} ${region} statistics data`;
421+
const initialQuery = `${claim} ${region} statistics data`.trim();
403422
const round1Results = await jinaSearch(initialQuery);
404423
const round1Text = formatSearchResults(round1Results);
405424
context.searches.push({ query: initialQuery, results: round1Text });
406-
console.log(`[DEEP RESEARCH] Round 1: ${round1Results.length} results`);
425+
console.log(`[RESEARCH] Round 1: ${round1Results.length} results`);
407426

408427
// Extract URLs for reading
409428
const urlsToRead = round1Results.slice(0, 3).map((r) => r.url).filter(Boolean) as string[];
410429

411-
// Round 2: AI generates follow-up queries
412-
const followUpQueries = await generateFollowUpQueries(claim, round1Text, provider, model);
413-
console.log(`[DEEP RESEARCH] Follow-up queries: ${followUpQueries.join(", ")}`);
414-
415-
// Execute follow-up searches in parallel
416-
const followUpPromises = followUpQueries.map(async (query) => {
417-
const results = await jinaSearch(query);
418-
const text = formatSearchResults(results);
419-
context.searches.push({ query, results: text });
420-
// Add more URLs to read
421-
results.slice(0, 2).forEach((r) => {
422-
if (r.url && !urlsToRead.includes(r.url)) urlsToRead.push(r.url);
430+
// Round 2: AI generates follow-up queries (skipped in light mode)
431+
if (options.maxFollowUpQueries > 0) {
432+
const followUpQueries = await generateFollowUpQueries(claim, round1Text, provider, model);
433+
console.log(`[RESEARCH] Follow-up queries: ${followUpQueries.join(", ")}`);
434+
435+
const followUpPromises = followUpQueries.slice(0, options.maxFollowUpQueries).map(async (query) => {
436+
const results = await jinaSearch(query);
437+
const text = formatSearchResults(results);
438+
context.searches.push({ query, results: text });
439+
results.slice(0, 2).forEach((r) => {
440+
if (r.url && !urlsToRead.includes(r.url)) urlsToRead.push(r.url);
441+
});
442+
return results.length;
423443
});
424-
return results.length;
425-
});
426-
await Promise.all(followUpPromises);
427-
console.log(`[DEEP RESEARCH] Round 2 complete, ${context.searches.length} total searches`);
444+
await Promise.all(followUpPromises);
445+
console.log(`[RESEARCH] Round 2 complete, ${context.searches.length} total searches`);
446+
}
428447

429448
// Round 3: Read top URLs for full content
430-
const readPromises = urlsToRead.slice(0, 5).map(async (url) => {
449+
const readPromises = urlsToRead.slice(0, options.maxUrlsToRead).map(async (url) => {
431450
const content = await jinaRead(url);
432451
if (content) {
433452
context.readContent.push({ url, content });
@@ -436,11 +455,34 @@ async function deepResearch(
436455
return content.length > 0;
437456
});
438457
await Promise.all(readPromises);
439-
console.log(`[DEEP RESEARCH] Read ${context.readContent.length} pages`);
458+
console.log(`[RESEARCH] Read ${context.readContent.length} pages`);
440459

441460
return context;
442461
}
443462

463+
// Wrapper with overall timeout - returns partial results if research exceeds time limit
464+
async function deepResearch(
465+
claim: string,
466+
region: string,
467+
provider: Provider,
468+
model: string,
469+
options: ResearchOptions = DEEP_RESEARCH
470+
): Promise<ResearchContext> {
471+
try {
472+
const result = await Promise.race([
473+
deepResearchInner(claim, region, provider, model, options),
474+
new Promise<never>((_, reject) =>
475+
setTimeout(() => reject(new Error("Research timeout")), options.overallTimeoutMs)
476+
),
477+
]);
478+
return result;
479+
} catch (err) {
480+
console.warn(`[RESEARCH] Timed out or failed after ${options.overallTimeoutMs}ms:`, err);
481+
// Return empty context so the AI call still proceeds without research
482+
return { searches: [], readContent: [], verifiedUrls: new Set() };
483+
}
484+
}
485+
444486
function buildResearchContext(research: ResearchContext): string {
445487
let ctx = "=== WEB RESEARCH RESULTS ===\n\n";
446488

@@ -832,7 +874,7 @@ Deno.serve(async (req) => {
832874
const region = String(sanitizedInput.region || "");
833875

834876
// Perform deep research with multiple search rounds
835-
const research = await deepResearch(claim, region, provider, model);
877+
const research = await deepResearch(claim, region, provider, model, DEEP_RESEARCH);
836878
const searchContext = buildResearchContext(research);
837879

838880
const userPrompt = buildUserPrompt(feature, sanitizedInput, searchContext);
@@ -888,9 +930,9 @@ Deno.serve(async (req) => {
888930
const motion = String(sanitizedInput.motion || sanitizedInput.topic || "");
889931
const side = String(sanitizedInput.side || "pro");
890932

891-
// Perform research to find supporting evidence
892-
console.log("[GENERATE] Starting deep research for motion:", motion);
893-
const research = await deepResearch(motion, "", provider, model);
933+
// Perform light research to find supporting evidence (avoids timeout)
934+
console.log("[GENERATE] Starting light research for motion:", motion);
935+
const research = await deepResearch(motion, "", provider, model, LIGHT_RESEARCH);
894936
const searchContext = buildResearchContext(research);
895937

896938
// Build prompt with research context
@@ -927,9 +969,9 @@ Deno.serve(async (req) => {
927969
if (feature === "brainstorm") {
928970
const motion = String(sanitizedInput.motion || sanitizedInput.topic || "");
929971

930-
// Perform research to find relevant context
931-
console.log("[BRAINSTORM] Starting deep research for motion:", motion);
932-
const research = await deepResearch(motion, "", provider, model);
972+
// Perform light research to find relevant context (avoids timeout)
973+
console.log("[BRAINSTORM] Starting light research for motion:", motion);
974+
const research = await deepResearch(motion, "", provider, model, LIGHT_RESEARCH);
933975
const searchContext = buildResearchContext(research);
934976

935977
// Build prompt with research context

0 commit comments

Comments
 (0)