import { tavily } from "@tavily/core"; const CACHE_TTL_DAYS = 4; const client = tavily({ apiKey: process.env.TAVILY_API_KEY }); // ───────────────────────────────────────────── // CACHE HELPERS // ───────────────────────────────────────────── function isStale(createdAt) { return (Date.now() - new Date(createdAt).getTime()) / 86400000 > CACHE_TTL_DAYS; } async function getCache(supabase, query) { try { // Use Supabase full text search against the stored query column. // This tolerates word order differences, plurals, and minor phrasing changes. const { data } = await supabase .from("research_cache") .select("result, created_at") .textSearch("query", query, { type: "websearch", config: "english" }) .order("created_at", { ascending: false }) .limit(1) .single(); if (!data || isStale(data.created_at)) return null; return data.result; } catch { return null; } } async function setCache(supabase, query, result) { try { await supabase.from("research_cache").insert({ query, result, created_at: new Date().toISOString(), }); } catch {} } // ───────────────────────────────────────────── // TAVILY CALL // ───────────────────────────────────────────── async function fetchFromTavily(query, deep = false) { const res = await client.search(query, { searchDepth: deep ? "advanced" : "basic", maxResults: 5, includeAnswer: true, }); const answer = res.answer || ""; const sources = (res.results || []) .map((r, i) => `[${i + 1}] ${r.title}\n${r.url}\n${r.content?.slice(0, 400)}`) .join("\n\n"); let extract = ""; if (deep) { const firstUrl = (res.results || [])[0]?.url; if (firstUrl) { try { const extracted = await client.extract([firstUrl]); extract = `\n\nDEEP EXTRACT [${firstUrl}]:\n${extracted.results?.[0]?.rawContent?.slice(0, 1000) || ""}`; } catch {} } } return `ANSWER:\n${answer}\n\nSOURCES:\n${sources}${extract}`; } // ───────────────────────────────────────────── // MAIN EXPORT // ───────────────────────────────────────────── /** * search({ query, urgent, deep, supabase }) * * urgent: true → skip cache entirely (breaking news, real-time queries) * deep: true → use Tavily advanced depth + extract first URL (for docs) * * Returns: { result: string, source: "cache" | "tavily" } */ export async function search({ query, urgent = false, deep = false, supabase }) { if (!query?.trim()) throw new Error("research.search: query is required"); if (!urgent) { const cached = await getCache(supabase, query); if (cached) { console.log(`[Research] Cache hit: "${query.slice(0, 60)}"`); return { result: cached, source: "cache" }; } } else { console.log(`[Research] Urgent — bypassing cache: "${query.slice(0, 60)}"`); } console.log(`[Research] Calling Tavily: "${query.slice(0, 60)}"`); const result = await fetchFromTavily(query, deep); await setCache(supabase, query, result); return { result, source: "tavily" }; }