File size: 3,614 Bytes
d48ab29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
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" };
}