Spaces:
Sleeping
Sleeping
| // ---------------------------------------------------------------------------- | |
| // Web search provider β Tavily (https://tavily.com). | |
| // | |
| // WHY THIS CHANGED: the old version scraped DuckDuckGo directly. DuckDuckGo | |
| // blocks datacenter IPs (Hugging Face, Render, Fly, etc.), so in production it | |
| // returned ZERO results ({"sources":[],"notes":""}). Tavily is a search API | |
| // built for LLMs: it returns clean, relevant content snippets per result, so we | |
| // no longer scrape or fetch pages ourselves β it's faster and reliable. | |
| // | |
| // SETUP (one-time): | |
| // 1. Get a free API key at https://app.tavily.com (1,000 searches/month, | |
| // no credit card). It looks like: tvly-xxxxxxxxxxxxxxxx | |
| // 2. Add it to your Hugging Face Space as a SECRET named TAVILY_API_KEY | |
| // (Space β Settings β Variables and secrets β New secret). | |
| // | |
| // The exported interface (webSearch, buildSourceNotes, domainOf) is unchanged, | |
| // so server.js keeps working without edits. | |
| // ---------------------------------------------------------------------------- | |
| const TAVILY_API_KEY = process.env.TAVILY_API_KEY || ''; | |
| const TAVILY_URL = 'https://api.tavily.com/search'; | |
| async function fetchWithTimeout(url, init = {}, timeoutMs = 15000) { | |
| const controller = new AbortController(); | |
| const id = setTimeout(() => controller.abort(), timeoutMs); | |
| try { | |
| return await fetch(url, { ...init, signal: controller.signal }); | |
| } finally { | |
| clearTimeout(id); | |
| } | |
| } | |
| export function domainOf(url) { | |
| return String(url).replace(/^https?:\/\//, '').split('/')[0]; | |
| } | |
| // Query Tavily -> [{ title, url, snippet }] | |
| export async function webSearch(query, maxResults = 6) { | |
| const q = String(query || '').trim(); | |
| if (!q) return []; | |
| if (!TAVILY_API_KEY) { | |
| console.warn('[search] TAVILY_API_KEY is not set β web search disabled.'); | |
| return []; | |
| } | |
| try { | |
| const res = await fetchWithTimeout(TAVILY_URL, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| Authorization: `Bearer ${TAVILY_API_KEY}`, | |
| }, | |
| body: JSON.stringify({ | |
| query: q, | |
| max_results: Math.min(Math.max(maxResults, 1), 10), | |
| search_depth: 'basic', | |
| include_answer: false, | |
| include_raw_content: false, | |
| }), | |
| }); | |
| if (!res.ok) { | |
| const text = await res.text().catch(() => ''); | |
| throw new Error(`tavily HTTP ${res.status} ${text.slice(0, 200)}`); | |
| } | |
| const data = await res.json(); | |
| const results = Array.isArray(data.results) ? data.results : []; | |
| return results.map((r) => ({ | |
| title: r.title || domainOf(r.url || ''), | |
| url: r.url || '', | |
| snippet: String(r.content || '').trim(), | |
| })); | |
| } catch (e) { | |
| console.warn('[search] tavily failed:', e.message); | |
| return []; | |
| } | |
| } | |
| // Turn results into numbered "source notes" the on-device model cites as [1],[2]. | |
| // Tavily already returns the relevant content per result, so there is nothing to | |
| // fetch β we just format. (Extra args kept for signature compatibility.) | |
| export async function buildSourceNotes( | |
| query, | |
| results, | |
| _fetchTopK = 4, | |
| _maxFactsPerSource = 4, | |
| maxTotalChars = 9000, | |
| ) { | |
| const blocks = (results || []).map((r, i) => { | |
| const facts = String(r.snippet || '').replace(/\s+/g, ' ').trim(); | |
| return ( | |
| `[${i + 1}] Title: ${r.title}\nURL: ${r.url}\nExtracted facts:\n` + | |
| `- ${facts || '(no snippet returned)'}` | |
| ); | |
| }); | |
| return blocks.join('\n\n').slice(0, maxTotalChars); | |
| } | |