Spaces:
Sleeping
Sleeping
| /** | |
| * Express server for the drug-interaction backend. | |
| * GET /api/health | |
| * GET /api/search?q=... | |
| * POST /api/check body: { brands: [...] } | |
| * | |
| * Run: | |
| * npm start # production | |
| * npm run dev # auto-reload on file changes | |
| */ | |
| ; | |
| require('dotenv').config(); | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const https = require('https'); | |
| const express = require('express'); | |
| const cors = require('cors'); | |
| const { InteractionEngine } = require('./src/engine'); | |
| const { Gap1Cache } = require('./src/gap1Cache'); | |
| const { Gap3Cache } = require('./src/gap3Cache'); | |
| const { GeminiEnricher } = require('./src/gemini'); | |
| const PORT = process.env.PORT || 3001; | |
| const DATA_DIR = process.env.DATA_DIR || path.resolve(__dirname, '..', 'data'); | |
| const REPO_DATA_DIR = path.resolve(__dirname, '..', 'data'); | |
| const GEMINI_API_KEY = process.env.GEMINI_API_KEY || ''; | |
| // Download dataset files from HF via direct HTTPS (resolve URL bypasses LFS pointer issue) | |
| const HF_DATASET_FILES = [ | |
| 'interactions.csv', | |
| '1mg_medicines_normalized.csv', | |
| 'canonical_generics.csv', | |
| 'gap1_cache.csv', | |
| 'gap3_cache.csv', | |
| ]; | |
| const HF_BASE = 'https://huggingface.co/datasets/amulyakh/drug-interactions-data/resolve/main'; | |
| const MIN_BYTES = 10_000; // anything smaller is treated as a stale stub and re-downloaded | |
| function downloadOne(url, dest) { | |
| return new Promise((resolve, reject) => { | |
| const file = fs.createWriteStream(dest); | |
| const req = https.get(url, res => { | |
| if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { | |
| file.close(); | |
| fs.unlinkSync(dest); | |
| const next = new URL(res.headers.location, url).toString(); | |
| return downloadOne(next, dest).then(resolve, reject); | |
| } | |
| if (res.statusCode !== 200) { | |
| file.close(); | |
| fs.unlinkSync(dest); | |
| return reject(new Error(`HTTP ${res.statusCode} for ${url}`)); | |
| } | |
| res.pipe(file); | |
| file.on('finish', () => file.close(() => resolve())); | |
| }); | |
| req.on('error', err => { | |
| file.close(); | |
| if (fs.existsSync(dest)) fs.unlinkSync(dest); | |
| reject(err); | |
| }); | |
| }); | |
| } | |
| async function ensureData() { | |
| if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true }); | |
| for (const f of HF_DATASET_FILES) { | |
| const dest = path.join(DATA_DIR, f); | |
| const existing = fs.existsSync(dest) ? fs.statSync(dest).size : 0; | |
| if (existing >= MIN_BYTES) { | |
| console.log(`[startup] ${f} already present (${existing} bytes)`); | |
| continue; | |
| } | |
| if (existing > 0) { | |
| console.log(`[startup] ${f} is stub (${existing} bytes) β re-downloading`); | |
| fs.unlinkSync(dest); | |
| } | |
| console.log(`[startup] downloading ${f}...`); | |
| await downloadOne(`${HF_BASE}/${f}`, dest); | |
| const size = fs.statSync(dest).size; | |
| console.log(`[startup] ${f} β ${size} bytes`); | |
| } | |
| } | |
| // First-boot bootstrap: if DATA_DIR points at an empty persistent disk, | |
| // copy the read-only CSVs shipped in the repo into it. | |
| if (DATA_DIR !== REPO_DATA_DIR && fs.existsSync(REPO_DATA_DIR)) { | |
| if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true }); | |
| for (const f of fs.readdirSync(REPO_DATA_DIR)) { | |
| const src = path.join(REPO_DATA_DIR, f); | |
| const dst = path.join(DATA_DIR, f); | |
| if (!fs.existsSync(dst) && fs.statSync(src).isFile()) { | |
| fs.copyFileSync(src, dst); | |
| console.log('[bootstrap] copied', f, 'β', DATA_DIR); | |
| } | |
| } | |
| } | |
| console.log('[startup] GEMINI_API_KEY:', GEMINI_API_KEY ? 'β set' : 'β not set'); | |
| const app = express(); | |
| app.use(cors()); | |
| app.use(express.json()); | |
| let engine; | |
| (async () => { | |
| await ensureData(); | |
| console.log('Loading interaction engine from', DATA_DIR); | |
| engine = new InteractionEngine(DATA_DIR); | |
| engine.load(); | |
| const gap1Cache = new Gap1Cache(); | |
| const gap3Cache = new Gap3Cache(); | |
| let geminiEnricher = null; | |
| if (GEMINI_API_KEY) { | |
| geminiEnricher = new GeminiEnricher(GEMINI_API_KEY); | |
| console.log('[gap1] Gemini enricher active'); | |
| } else { | |
| console.log('[gap1] No GEMINI_API_KEY β Gap 1 fallback disabled (set env var to enable)'); | |
| } | |
| app.get('/api/health', (req, res) => { | |
| res.json({ | |
| status: 'ok', | |
| medicines: engine.medicines.length, | |
| pairs: engine.pairIndex.size, | |
| ddinter_drugs: engine.knownDDInterDrugs.size, | |
| gap1_cached: gap1Cache.medicines.size, | |
| gap3_cached: gap3Cache.ingredients.size, | |
| gemini_active: !!geminiEnricher, | |
| }); | |
| }); | |
| app.get('/api/search', (req, res) => { | |
| const q = String(req.query.q || ''); | |
| const limit = Math.min(parseInt(req.query.limit || '10', 10), 50); | |
| res.json({ results: engine.search(q, limit) }); | |
| }); | |
| app.post('/api/check', async (req, res) => { | |
| const brands = Array.isArray(req.body && req.body.brands) ? req.body.brands : null; | |
| if (!brands) return res.status(400).json({ error: 'Body must be { brands: [...] }' }); | |
| console.log('[/api/check] brands:', brands, 'geminiEnricher:', !!geminiEnricher); | |
| try { | |
| if (geminiEnricher) { | |
| console.log('[/api/check] using Gap 1 & Gap 3 fallback...'); | |
| const result = await engine.checkWithGap1Fallback(brands, gap1Cache, geminiEnricher, gap3Cache); | |
| console.log('[/api/check] result unresolved:', result.unresolved_brands); | |
| return res.json(result); | |
| } | |
| console.log('[/api/check] no geminiEnricher, using plain check'); | |
| res.json(engine.check(brands)); | |
| } catch (e) { | |
| console.error('/api/check error:', e); | |
| res.status(500).json({ error: e.message }); | |
| } | |
| }); | |
| app.listen(PORT, () => { | |
| console.log(`β Backend listening on http://localhost:${PORT}`); | |
| }); | |
| })(); | |