Spaces:
Sleeping
Sleeping
Download server.js from AdronX/divine-wisdom: direct link, hf CLI and curl.
- Browser
- Download file 18.6 kB
-
https://huggingface.co/spaces/AdronX/divine-wisdom/resolve/main/server.js
- Command line
-
hf download hf://spaces/AdronX/divine-wisdom/server.js
-
curl -L -o server.js https://huggingface.co/spaces/AdronX/divine-wisdom/resolve/main/server.js
18.6 kB
| const express = require("express"); | |
| const path = require("path"); | |
| const app = express(); | |
| app.use(express.json({ limit: "10mb" })); | |
| app.use(express.static(path.join(__dirname, "public"))); | |
| const PORT = process.env.PORT || 7860; | |
| const ADMIN_SECRET = process.env.ADMIN_SECRET || "admin123"; | |
| const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "sacred2024"; | |
| // ββ SAFE HELPERS β no more .includes() crashes ββββββββββββ | |
| const S = v => (v == null ? "" : String(v)); | |
| const A = v => (Array.isArray(v) ? v : []); | |
| const N = v => (isFinite(v) ? parseInt(v) : 0); | |
| // ββ Upstash Redis βββββββββββββββββββββββββββββββββββββββββ | |
| async function rGet(key) { | |
| const {UPSTASH_REDIS_REST_URL:url, UPSTASH_REDIS_REST_TOKEN:tok} = process.env; | |
| if (!url || !tok) throw new Error("UPSTASH not configured"); | |
| const r = await fetch(`${url}/get/${encodeURIComponent(S(key))}`, { headers:{Authorization:`Bearer ${tok}`} }); | |
| const d = await r.json(); | |
| if (d.result == null) return null; | |
| try { return JSON.parse(d.result); } catch { return d.result; } | |
| } | |
| async function rSet(key, val) { | |
| const {UPSTASH_REDIS_REST_URL:url, UPSTASH_REDIS_REST_TOKEN:tok} = process.env; | |
| if (!url || !tok) throw new Error("UPSTASH not configured"); | |
| await fetch(`${url}/set/${encodeURIComponent(S(key))}`, { | |
| method:"POST", | |
| headers:{Authorization:`Bearer ${tok}`,"Content-Type":"application/json"}, | |
| body: JSON.stringify(typeof val === "string" ? val : JSON.stringify(val)), | |
| }); | |
| } | |
| async function rDel(key) { | |
| const {UPSTASH_REDIS_REST_URL:url, UPSTASH_REDIS_REST_TOKEN:tok} = process.env; | |
| if (!url || !tok) return; | |
| await fetch(`${url}/del/${encodeURIComponent(S(key))}`, { method:"POST", headers:{Authorization:`Bearer ${tok}`} }); | |
| } | |
| // ββ Auth ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function auth(req, res, next) { | |
| if (S(req.headers["x-admin-token"]) !== ADMIN_PASSWORD) return res.status(401).json({error:"Unauthorized"}); | |
| next(); | |
| } | |
| // ββ Tradition detection βββββββββββββββββββββββββββββββββββ | |
| const TMAP = { | |
| quran:"Islam",hadith:"Islam",islam:"Islam",muhammad:"Islam", | |
| bible:"Christianity",testament:"Christianity",christian:"Christianity",jesus:"Christianity",gospel:"Christianity",psalm:"Christianity", | |
| gita:"Hinduism",geeta:"Hinduism",veda:"Hinduism",hindu:"Hinduism",upanishad:"Hinduism",ramayan:"Hinduism",mahabharata:"Hinduism",krishna:"Hinduism",shiva:"Hinduism", | |
| tripitaka:"Buddhism",dhammapada:"Buddhism",buddha:"Buddhism",pali:"Buddhism", | |
| torah:"Judaism",talmud:"Judaism",jewish:"Judaism",hebrew:"Judaism", | |
| granth:"Sikhism",sikh:"Sikhism",waheguru:"Sikhism",guru:"Sikhism", | |
| }; | |
| function tradition(name) { | |
| const fl = S(name).toLowerCase().replace(/[_\-\.]+/g," "); | |
| for (const [k,v] of Object.entries(TMAP)) if (fl.indexOf(k) !== -1) return v; | |
| return "Scripture"; | |
| } | |
| // ββ AI providers ββββββββββββββββββββββββββββββββββββββββββ | |
| async function ai_claude(k,m,sys,msgs,mx) { | |
| const r = await fetch("https://api.anthropic.com/v1/messages",{method:"POST",headers:{"Content-Type":"application/json","x-api-key":k,"anthropic-version":"2023-06-01"},body:JSON.stringify({model:m||"claude-sonnet-4-20250514",max_tokens:mx,system:sys,messages:msgs})}); | |
| const d = await r.json(); if(d.error) throw new Error(d.error.message); return S(d.content&&d.content[0]&&d.content[0].text); | |
| } | |
| async function ai_openai(k,m,sys,msgs,mx) { | |
| const r = await fetch("https://api.openai.com/v1/chat/completions",{method:"POST",headers:{"Content-Type":"application/json","Authorization":`Bearer ${k}`},body:JSON.stringify({model:m||"gpt-4o",max_tokens:mx,messages:[{role:"system",content:sys},...msgs]})}); | |
| const d = await r.json(); if(d.error) throw new Error(d.error.message); return S(d.choices&&d.choices[0]&&d.choices[0].message&&d.choices[0].message.content); | |
| } | |
| async function ai_gemini(k,m,sys,msgs,mx) { | |
| const r = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${m||"gemini-2.0-flash"}:generateContent?key=${k}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({systemInstruction:{parts:[{text:sys}]},contents:msgs.map(x=>({role:x.role==="assistant"?"model":"user",parts:[{text:S(x.content)}]})),generationConfig:{maxOutputTokens:mx}})}); | |
| const d = await r.json(); if(d.error) throw new Error(d.error.message); return S(d.candidates&&d.candidates[0]&&d.candidates[0].content&&d.candidates[0].content.parts&&d.candidates[0].content.parts[0]&&d.candidates[0].content.parts[0].text); | |
| } | |
| async function ai_groq(k,m,sys,msgs,mx) { | |
| const r = await fetch("https://api.groq.com/openai/v1/chat/completions",{method:"POST",headers:{"Content-Type":"application/json","Authorization":`Bearer ${k}`},body:JSON.stringify({model:m||"llama-3.3-70b-versatile",max_tokens:mx,messages:[{role:"system",content:sys},...msgs]})}); | |
| const d = await r.json(); if(d.error) throw new Error(d.error.message); return S(d.choices&&d.choices[0]&&d.choices[0].message&&d.choices[0].message.content); | |
| } | |
| async function ai_deepseek(k,m,sys,msgs,mx) { | |
| const r = await fetch("https://api.deepseek.com/chat/completions",{method:"POST",headers:{"Content-Type":"application/json","Authorization":`Bearer ${k}`},body:JSON.stringify({model:m||"deepseek-chat",max_tokens:mx,messages:[{role:"system",content:sys},...msgs]})}); | |
| const d = await r.json(); if(d.error) throw new Error(d.error.message); return S(d.choices&&d.choices[0]&&d.choices[0].message&&d.choices[0].message.content); | |
| } | |
| async function callAI(sys, msgs, mx=2000) { | |
| const providers = [ | |
| {name:"Claude", key:process.env.ANTHROPIC_API_KEY, model:process.env.CLAUDE_MODEL, fn:ai_claude}, | |
| {name:"OpenAI", key:process.env.OPENAI_API_KEY, model:process.env.OPENAI_MODEL, fn:ai_openai}, | |
| {name:"Gemini", key:process.env.GEMINI_API_KEY, model:process.env.GEMINI_MODEL, fn:ai_gemini}, | |
| {name:"Groq", key:process.env.GROQ_API_KEY, model:process.env.GROQ_MODEL, fn:ai_groq}, | |
| {name:"DeepSeek",key:process.env.DEEPSEEK_API_KEY, model:process.env.DEEPSEEK_MODEL, fn:ai_deepseek}, | |
| ].filter(p => S(p.key).trim()); | |
| for (const p of providers) { | |
| try { const raw=await p.fn(p.key,p.model,sys,msgs,mx); if(raw) return {raw,provider:p.name}; } | |
| catch(e) { console.error(p.name+" failed:",e.message); } | |
| } | |
| return null; | |
| } | |
| // ββ Smart search across ALL books βββββββββββββββββββββββββ | |
| function searchChunks(question, books, topN=12) { | |
| const q = S(question).toLowerCase(); | |
| if (!q) return []; | |
| const words = q.split(/\s+/).filter(w=>w.length>2); | |
| const long = words.filter(w=>w.length>4); | |
| const results = []; | |
| for (const bk of Object.values(books||{})) { | |
| const txt = S(bk&&bk.text); | |
| if (!txt) continue; | |
| const bName = S(bk.name)||"Unknown"; | |
| const bTrad = S(bk.tradition)||"Scripture"; | |
| for (const chunk of txt.split(/\n{2,}/).filter(c=>c.trim().length>40)) { | |
| const cl = S(chunk).toLowerCase(); | |
| const pm = chunk.match(/\[PAGE (\d+)\]/); | |
| const t = chunk.replace(/\[PAGE \d+\]\n?/,"").trim(); | |
| if (t.length < 30) continue; | |
| let score = 0; | |
| for (const w of long) if (cl.indexOf(w) !== -1) score += 3; | |
| for (const w of words) if (cl.indexOf(w) !== -1) score += 1; | |
| if (q.length>10 && cl.indexOf(q.substring(0,15)) !== -1) score += 10; | |
| if (score > 0) results.push({ | |
| book: bName, tradition: bTrad, | |
| text: t.substring(0,600), fullChunk: chunk, | |
| page: S(pm&&pm[1])||"β", score, | |
| }); | |
| } | |
| } | |
| results.sort((a,b)=>b.score-a.score); | |
| const seen={}, top=[]; | |
| for (const r of results) { | |
| seen[r.book] = (seen[r.book]||0); | |
| if (seen[r.book]<3) { top.push(r); seen[r.book]++; } | |
| if (top.length>=topN) break; | |
| } | |
| return top; | |
| } | |
| function textFallback(question, books) { | |
| const top = searchChunks(question, books, 5); | |
| if (!top.length) return {answer:"π No relevant passages found. Admin should upload related books.",source:"β",fallback:true}; | |
| return { | |
| answer: "π Relevant passages:\n\n"+top.map(r=>`**π ${r.book}** (Page ${r.page})\n"${r.text.substring(0,350)}..."`).join("\n\n---\n\n"), | |
| source: top.map(r=>`${r.book} p.${r.page}`).join(", "), | |
| fallback:true, passages:top, | |
| }; | |
| } | |
| // ββ Load books from Redis βββββββββββββββββββββββββββββββββ | |
| async function loadBooks() { | |
| try { | |
| const meta = await rGet("books-meta"); | |
| if (!meta) return {}; | |
| const books = {}; | |
| for (const name of A(meta.books)) { | |
| try { | |
| const d = await rGet("book-"+S(name)); | |
| if (d && S(d.text)) books[S(name)] = d; | |
| } catch {} | |
| } | |
| return books; | |
| } catch(e) { console.error("loadBooks:",e.message); return {}; } | |
| } | |
| // ββ Store book helper βββββββββββββββββββββββββββββββββββββ | |
| async function storeBook({bookName, text, totalPages, wordCount, fileId, modifiedTime}) { | |
| const name = S(bookName).trim().replace(/[_\-]+/g," ")||"Unknown Book"; | |
| const txt = S(text).trim(); | |
| if (!txt || txt.length < 30) throw new Error(`No text extracted from "${name}"`); | |
| const trad = tradition(name); | |
| const safe = txt.length>4000000 ? txt.substring(0,4000000) : txt; | |
| await rSet("book-"+name, {name,tradition:trad,text:safe,totalPages:N(totalPages),wordCount:N(wordCount),syncedAt:new Date().toISOString()}); | |
| let meta = {books:[],synced:{},totalBooks:0}; | |
| try { meta = (await rGet("books-meta"))||meta; } catch {} | |
| meta.books = A(meta.books); | |
| meta.synced = meta.synced||{}; | |
| if (meta.books.indexOf(name)===-1) meta.books.push(name); | |
| if (fileId) meta.synced[S(fileId)] = S(modifiedTime); | |
| meta.totalBooks = meta.books.length; | |
| meta.lastSync = new Date().toISOString(); | |
| await rSet("books-meta", meta); | |
| return {name, tradition:trad, totalPages:N(totalPages), wordCount:N(wordCount)}; | |
| } | |
| // ββ Admin page ββββββββββββββββββββββββββββββββββββββββββββ | |
| app.get("/admin-"+ADMIN_SECRET, (req,res) => res.sendFile(path.join(__dirname,"public","admin.html"))); | |
| // ββ Chat ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.post("/api/chat", async (req,res) => { | |
| try { | |
| const question = S(req.body&&req.body.question).trim(); | |
| const messages = A(req.body&&req.body.messages); | |
| const lang = S(req.body&&req.body.lang)||"en"; | |
| if (!question) return res.status(400).json({error:"Question required"}); | |
| const langInstr = {hi:"Answer in Hindi.",ur:"Answer in Urdu.",ar:"Answer in Arabic.",en:"Answer in English."}[lang]||"Answer in English."; | |
| const books = await loadBooks(); | |
| const bookCount = Object.keys(books).length; | |
| const chunks = bookCount>0 ? searchChunks(question,books,12) : []; | |
| let ctx = ""; | |
| if (chunks.length>0) { | |
| const byBook={}; | |
| for (const c of chunks) { if(!byBook[c.book]) byBook[c.book]=[]; byBook[c.book].push(c); } | |
| ctx = Object.entries(byBook).map(([n,cs])=>`=== ${n} ===\n`+cs.map(c=>S(c.fullChunk)).join("\n\n")).join("\n\n"); | |
| } | |
| const bookList = bookCount>0 | |
| ? Object.values(books).map(b=>`"${S(b.name)}" (${S(b.tradition)||"Scripture"})`).join(", ") | |
| : "none"; | |
| const system = `You are a scholar with access to a sacred text library. | |
| ${langInstr} | |
| Library (${bookCount} books): ${bookList} | |
| ${chunks.length>0 ? "RELEVANT PASSAGES:\n"+ctx+"\nQuote exact text with page numbers." : bookCount>0 ? "No keyword match. Answer from knowledge." : "No books uploaded. Answer from general knowledge."} | |
| Rules: cite book+page always. Translate if needed. Add commentary. | |
| Return ONLY valid JSON: | |
| {"answer":"text","quotes":[{"book":"name","tradition":"religion","original":"text","page":"Page X","translation":"translation","commentary":"notes"}],"source":"Book p.X"}`; | |
| const ai = await callAI(system, [...messages.slice(-4),{role:"user",content:question}], 2000); | |
| if (ai) { | |
| let parsed; | |
| try { parsed = JSON.parse(S(ai.raw).replace(/^```json\s*/i,"").replace(/^```\s*/i,"").replace(/\s*```$/i,"").trim()); } | |
| catch { parsed = {answer:S(ai.raw),quotes:[],source:"AI"}; } | |
| return res.json({...parsed,provider:ai.provider,fallback:false,bookCount}); | |
| } | |
| return res.json({...textFallback(question,books),bookCount}); | |
| } catch(e) { console.error("chat:",e); res.status(500).json({error:e.message}); } | |
| }); | |
| // ββ Admin login βββββββββββββββββββββββββββββββββββββββββββ | |
| app.post("/api/admin/login", (req,res) => { | |
| if (S(req.body&&req.body.password)===ADMIN_PASSWORD) return res.json({success:true,token:ADMIN_PASSWORD}); | |
| res.status(401).json({error:"Wrong password"}); | |
| }); | |
| // ββ Admin status ββββββββββββββββββββββββββββββββββββββββββ | |
| app.get("/api/admin/status", auth, async (req,res) => { | |
| try { | |
| let meta={books:[],lastSync:null,totalBooks:0}; | |
| try { meta=(await rGet("books-meta"))||meta; } catch {} | |
| meta.books = A(meta.books); | |
| const bookDetails=[]; | |
| for (const name of meta.books) { | |
| try { | |
| const bk=await rGet("book-"+S(name)); | |
| if(bk) bookDetails.push({name:S(bk.name),tradition:S(bk.tradition)||"Scripture",totalPages:N(bk.totalPages),wordCount:N(bk.wordCount),uploadedAt:S(bk.syncedAt)}); | |
| } catch {} | |
| } | |
| res.json({meta,bookDetails,config:{ | |
| hasClaude: !!process.env.ANTHROPIC_API_KEY, | |
| hasOpenAI: !!process.env.OPENAI_API_KEY, | |
| hasGemini: !!process.env.GEMINI_API_KEY, | |
| hasGroq: !!process.env.GROQ_API_KEY, | |
| hasDeepSeek:!!process.env.DEEPSEEK_API_KEY, | |
| hasUpstash: !!(process.env.UPSTASH_REDIS_REST_URL&&process.env.UPSTASH_REDIS_REST_TOKEN), | |
| hasDrive: !!(process.env.GOOGLE_DRIVE_API_KEY&&process.env.GOOGLE_DRIVE_FOLDER_ID), | |
| }}); | |
| } catch(e) { res.status(500).json({error:e.message}); } | |
| }); | |
| // ββ Upload (browser-parsed) βββββββββββββββββββββββββββββββ | |
| app.post("/api/admin/upload-book", auth, async (req,res) => { | |
| try { | |
| const result = await storeBook(req.body||{}); | |
| res.json({success:true,...result}); | |
| } catch(e) { res.status(400).json({error:e.message}); } | |
| }); | |
| // ββ Google Drive: list files ββββββββββββββββββββββββββββββ | |
| app.post("/api/admin/drive-list", auth, async (req,res) => { | |
| const KEY=process.env.GOOGLE_DRIVE_API_KEY, FOLDER=process.env.GOOGLE_DRIVE_FOLDER_ID; | |
| if (!KEY||!FOLDER) return res.status(400).json({error:"Set GOOGLE_DRIVE_API_KEY and GOOGLE_DRIVE_FOLDER_ID in HF Secrets"}); | |
| try { | |
| const url=`https://www.googleapis.com/drive/v3/files?q='${FOLDER}'+in+parents+and+mimeType='application/pdf'+and+trashed=false&fields=files(id,name,modifiedTime,size)&key=${KEY}&pageSize=100`; | |
| const r=await fetch(url); | |
| if (!r.ok) throw new Error("Drive API HTTP "+r.status); | |
| const d=await r.json(); | |
| if (d.error) throw new Error("Drive API: "+S(d.error.message||d.error)); | |
| // Load synced map safely β triple-guard against any null/undefined | |
| let synced = {}; | |
| try { | |
| const meta = await rGet("books-meta"); | |
| if (meta && typeof meta === "object" && meta.synced && typeof meta.synced === "object") { | |
| synced = meta.synced; | |
| } | |
| } catch {} | |
| // Filter out any null/undefined entries from Drive response | |
| const rawFiles = Array.isArray(d.files) ? d.files.filter(f => f && S(f.id)) : []; | |
| const files = rawFiles.map(f => { | |
| const id = S(f.id); | |
| const name = S(f.name); | |
| const modifiedTime = S(f.modifiedTime); | |
| const bookName = name.replace(/\.pdf$/i,"").trim().replace(/[_\-]+/g," ")||"Unknown"; | |
| // Safe synced check β never access property on undefined | |
| let alreadySynced = false; | |
| try { alreadySynced = (synced[id] === modifiedTime && modifiedTime !== ""); } catch {} | |
| return { id, name, bookName, modifiedTime, size:N(f.size), alreadySynced }; | |
| }); | |
| res.json({success:true, files, driveApiKey:KEY}); | |
| } catch(e) { | |
| console.error("drive-list error:", e.message); | |
| res.status(500).json({error:e.message}); | |
| } | |
| }); | |
| // ββ Google Drive: store parsed text ββββββββββββββββββββββ | |
| app.post("/api/admin/drive-store", auth, async (req,res) => { | |
| try { | |
| const result = await storeBook(req.body||{}); | |
| res.json({success:true,...result}); | |
| } catch(e) { res.status(400).json({error:e.message}); } | |
| }); | |
| // ββ Delete book βββββββββββββββββββββββββββββββββββββββββββ | |
| app.delete("/api/admin/delete-book", auth, async (req,res) => { | |
| try { | |
| const name=S(req.body&&req.body.bookName).trim(); | |
| if(!name) return res.status(400).json({error:"bookName required"}); | |
| await rDel("book-"+name); | |
| let meta={books:[]}; | |
| try { meta=(await rGet("books-meta"))||meta; } catch {} | |
| meta.books=A(meta.books).filter(b=>b!==name); | |
| meta.totalBooks=meta.books.length; | |
| await rSet("books-meta",meta); | |
| res.json({success:true}); | |
| } catch(e) { res.status(500).json({error:e.message}); } | |
| }); | |
| // ββ Clear all βββββββββββββββββββββββββββββββββββββββββββββ | |
| app.delete("/api/admin/clear-all", auth, async (req,res) => { | |
| try { | |
| let meta={books:[]}; | |
| try { meta=(await rGet("books-meta"))||meta; } catch {} | |
| for (const n of A(meta.books)) { try { await rDel("book-"+S(n)); } catch {} } | |
| await rSet("books-meta",{books:[],totalBooks:0,lastSync:null,synced:{}}); | |
| res.json({success:true}); | |
| } catch(e) { res.status(500).json({error:e.message}); } | |
| }); | |
| app.get("*",(req,res)=>res.sendFile(path.join(__dirname,"public","index.html"))); | |
| app.listen(PORT,"0.0.0.0",()=>{ | |
| console.log("Divine Wisdom running on port "+PORT); | |
| console.log("Admin: /admin-"+ADMIN_SECRET); | |
| }); | |