/* RAG Document Assistant — retrieval engine (client-side port). * * JavaScript port of the Python `rag.py` TF-IDF path: chunk -> embed (TF-IDF) * -> cosine retrieve -> grounded answer with citations, and an honest * "I don't know" below a similarity threshold. Runs entirely in the browser, * so the Hugging Face Static Space is free and needs no key or server. * * Source: https://github.com/furqunali (full Python version adds neural * sentence-transformers embeddings + optional LLM generation). */ (function (global) { "use strict"; function chunkText(text, source, chunkSize, overlap) { chunkSize = chunkSize || 600; overlap = overlap == null ? 100 : overlap; text = String(text || "").replace(/\s+/g, " ").trim(); if (!text) return []; const sentences = text.split(/(?<=[.!?])\s+/); const chunks = []; let buf = "", idx = 0; for (const sent of sentences) { if (buf.length + sent.length + 1 <= chunkSize || !buf) { buf = (buf + " " + sent).trim(); } else { chunks.push({ text: buf, source, index: idx++ }); const tail = overlap ? buf.slice(-overlap) : ""; buf = (tail + " " + sent).trim(); } } if (buf) chunks.push({ text: buf, source, index: idx }); return chunks; } const tokenize = (s) => (String(s || "").toLowerCase().match(/[a-z0-9]+/g) || []); function TfidfEmbedder() { this.vocab = {}; this.idf = null; } TfidfEmbedder.prototype.fit = function (corpus) { const df = {}; corpus.forEach((d) => { new Set(tokenize(d)).forEach((t) => df[t] = (df[t] || 0) + 1); }); const terms = Object.keys(df).sort(); this.vocab = {}; terms.forEach((t, i) => this.vocab[t] = i); const n = corpus.length; this.idf = terms.map((t) => Math.log((1 + n) / (1 + df[t])) + 1.0); return this; }; TfidfEmbedder.prototype.embed = function (texts) { const V = this.idf.length; return texts.map((text) => { const vec = new Float64Array(V), toks = tokenize(text), counts = {}; toks.forEach((t) => { const j = this.vocab[t]; if (j !== undefined) counts[j] = (counts[j] || 0) + 1; }); if (toks.length) for (const j in counts) vec[j] = (counts[j] / toks.length) * this.idf[j]; return vec; }); }; function cosine(a, b) { let dot = 0, na = 0, nb = 0; for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; na += a[i] * a[i]; nb += b[i] * b[i]; } return dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-9); } function Retriever(chunks, embedder) { this.chunks = chunks; this.embedder = embedder; this.matrix = chunks.length ? embedder.embed(chunks.map((c) => c.text)) : []; } Retriever.prototype.query = function (question, k) { k = k || 4; if (!this.chunks.length) return []; const q = this.embedder.embed([question])[0]; const scored = this.matrix.map((v, i) => ({ chunk: this.chunks[i], score: cosine(v, q) })); scored.sort((a, b) => b.score - a.score); return scored.slice(0, k); }; const IDK = "I don't have enough information in the provided documents to answer that. " + "Try rephrasing, or add a document that covers this topic."; function answer(question, hits, threshold) { threshold = threshold == null ? 0.15 : threshold; const top = hits.length ? hits[0].score : 0.0; if (!hits.length || top < threshold) return { answer: IDK, grounded: false, citations: [], top_score: round(top) }; const citations = hits.map((h) => ({ source: h.chunk.source, chunk: h.chunk.index, score: round(h.score), preview: h.chunk.text.slice(0, 200) + (h.chunk.text.length > 200 ? "…" : ""), })); return { answer: hits[0].chunk.text, grounded: true, mode: "extractive", citations, top_score: round(top) }; } const round = (x) => Math.round(x * 1000) / 1000; function buildRetriever(namedTexts) { let chunks = []; namedTexts.forEach(([name, text]) => { chunks = chunks.concat(chunkText(text, name)); }); if (!chunks.length) return null; const emb = new TfidfEmbedder().fit(chunks.map((c) => c.text)); return new Retriever(chunks, emb); } const SAMPLE_DOCS = [["company_handbook.md", `# Acme Demo Co. Customer Handbook (Fictional Sample) ## Refunds and Returns Customers may request a refund within 30 days of purchase. To be eligible, the item must be unused and in its original packaging. Refunds are processed to the original payment method within 5 to 7 business days after we receive the returned item. Shipping fees are non-refundable except when the return is due to our error. ## Password Reset To reset your password, go to the login page and click "Forgot password". Enter the email associated with your account and you will receive a reset link that is valid for 60 minutes. If the link expires, simply request a new one. For security, we never send your existing password by email. ## Support Hours Customer support is available Monday to Friday, 9:00 AM to 6:00 PM Central Time. Weekend support is limited to email only, with responses on the next business day. Priority phone support is available to Premium plan customers 24/7. ## Loyalty Program Any customer with an active account is eligible for the Acme Rewards loyalty program. Members earn 1 point per dollar spent and can redeem 100 points for a 5 dollar credit. Points expire 12 months after they are earned. Premium plan members earn points at double the standard rate. ## Shipping Standard shipping takes 3 to 5 business days within the continental US. Expedited shipping of 1 to 2 business days is available at checkout for an additional fee. We currently do not ship to international addresses. ## Data Privacy We store only the account information necessary to provide the service. Customer data is never sold to third parties. You can request deletion of your account and associated data at any time by contacting support.`]]; const API = { chunkText, TfidfEmbedder, cosine, Retriever, answer, buildRetriever, SAMPLE_DOCS }; if (typeof module !== "undefined" && module.exports) module.exports = API; else global.RAG = API; })(typeof window !== "undefined" ? window : globalThis);