Spaces:
Sleeping
Sleeping
| // ---------------------------------------------------------------------------- | |
| // oAI backend proxy. | |
| // | |
| // Endpoints: | |
| // GET /health -> { ok: true } | |
| // POST /search { query, maxResults } -> { sources:[{title,url}], notes } | |
| // POST /ingest { name, ext, base64 } -> { docId, chunks } | |
| // POST /retrieve { docIds, query, k } -> { context, chunks } | |
| // | |
| // The on-device GGUF model still generates every answer. This server only does | |
| // the parts a phone is bad at: reliable web search and PDF/DOCX text extraction | |
| // + embeddings for RAG. | |
| // ---------------------------------------------------------------------------- | |
| import express from 'express'; | |
| import cors from 'cors'; | |
| import crypto from 'node:crypto'; | |
| import { webSearch, buildSourceNotes } from './src/search.js'; | |
| import { extractText } from './src/extract.js'; | |
| import { embed, embedBatch, warmup } from './src/embed.js'; | |
| import { recursiveSplit, putDoc, hasDoc, retrieve } from './src/store.js'; | |
| const app = express(); | |
| app.use(cors()); | |
| app.use(express.json({ limit: '30mb' })); // base64 file uploads can be large | |
| const PORT = process.env.PORT || 8787; | |
| app.get('/health', (_req, res) => res.json({ ok: true })); | |
| // ---- web search ------------------------------------------------------------ | |
| app.post('/search', async (req, res) => { | |
| const { query, maxResults = 6 } = req.body || {}; | |
| if (!query || typeof query !== 'string') { | |
| return res.status(400).json({ error: 'query required' }); | |
| } | |
| try { | |
| const results = await webSearch(query, maxResults); | |
| // FIX (BUG-1): buildSourceNotes now needs the query so it can score | |
| // page sentences against the query keywords (like the Python reference). | |
| const notes = await buildSourceNotes(query, results); | |
| res.json({ | |
| sources: results.map((r) => ({ title: r.title, url: r.url })), | |
| notes, | |
| }); | |
| } catch (e) { | |
| console.error('[/search]', e); | |
| res.status(502).json({ error: 'search_failed', message: String(e.message || e) }); | |
| } | |
| }); | |
| // ---- ingest a file: extract -> chunk -> embed -> store --------------------- | |
| app.post('/ingest', async (req, res) => { | |
| const { name, ext, base64 } = req.body || {}; | |
| if (!base64 || typeof base64 !== 'string') { | |
| return res.status(400).json({ error: 'base64 required' }); | |
| } | |
| try { | |
| // dedupe identical uploads by content hash | |
| const docId = crypto.createHash('sha256').update(base64).digest('hex').slice(0, 24); | |
| if (hasDoc(docId)) { | |
| return res.json({ docId, cached: true }); | |
| } | |
| const buffer = Buffer.from(base64, 'base64'); | |
| const text = await extractText(ext || guessExt(name), buffer); | |
| if (!text || text.trim().length === 0) { | |
| return res.status(422).json({ error: 'no_text', message: 'Could not extract text from file.' }); | |
| } | |
| const chunks = recursiveSplit(text); | |
| const vectors = await embedBatch(chunks); | |
| putDoc(docId, chunks, vectors); | |
| res.json({ docId, chunks: chunks.length }); | |
| } catch (e) { | |
| console.error('[/ingest]', e); | |
| res.status(500).json({ error: 'ingest_failed', message: String(e.message || e) }); | |
| } | |
| }); | |
| // ---- retrieve top-k context for a query ------------------------------------ | |
| app.post('/retrieve', async (req, res) => { | |
| const { docIds, query, k = 4 } = req.body || {}; | |
| if (!Array.isArray(docIds) || docIds.length === 0) { | |
| return res.status(400).json({ error: 'docIds required' }); | |
| } | |
| if (!query || typeof query !== 'string') { | |
| return res.status(400).json({ error: 'query required' }); | |
| } | |
| try { | |
| const qvec = await embed(query); | |
| const top = retrieve(docIds, qvec, k); | |
| const context = top.map((c, i) => `[chunk ${i + 1}]\n${c}`).join('\n\n'); | |
| res.json({ context, chunks: top.length }); | |
| } catch (e) { | |
| console.error('[/retrieve]', e); | |
| res.status(500).json({ error: 'retrieve_failed', message: String(e.message || e) }); | |
| } | |
| }); | |
| function guessExt(name) { | |
| return String(name || '').split('.').pop()?.toLowerCase() || 'txt'; | |
| } | |
| app.listen(PORT, () => { | |
| console.log(`oAI backend listening on :${PORT}`); | |
| // warm the embedding model so the first /ingest isn't slow | |
| warmup() | |
| .then(() => console.log('embedding model ready')) | |
| .catch((e) => console.warn('warmup failed (will lazy-load):', e.message)); | |
| }); |