divine-wisdom / server.js
AdronX's picture
Upload 5 files
4f87349 verified
Raw History Blame Contribute Delete
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);
});