amulyakh's picture
Add backend code + Docker config (data excluded)
cb558b3
'use strict';
const fs = require('fs');
const path = require('path');
const { parse } = require('csv-parse/sync');
const { stringify } = require('csv-stringify/sync');
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, '../../data');
const CACHE_FILE = path.join(DATA_DIR, 'gap1_cache.csv');
const HEADERS = ['brand_name_lc','generic_name','active_ingredients','drug_a','drug_b','severity','side_effects','mechanism','clinical_action','confidence','created_at'];
class Gap1Cache {
constructor() {
// brand_lc → { generic_name, active_ingredients, interactions: Map(drug_b → {severity, ...}) }
this.medicines = new Map();
// 'drug_a|drug_b' → { severity, side_effects, mechanism, clinical_action }
this.pairIndex = new Map();
this._load();
}
_load() {
if (!fs.existsSync(CACHE_FILE)) {
fs.writeFileSync(CACHE_FILE, HEADERS.join(',') + '\n');
return;
}
try {
const raw = fs.readFileSync(CACHE_FILE);
const rows = parse(raw, { columns: true, skip_empty_lines: true });
for (const r of rows) {
this._indexRow(r);
}
console.log(`[gap1Cache] loaded ${rows.length} cached rows`);
} catch (e) {
console.error('[gap1Cache] load error:', e.message);
}
}
_indexRow(r) {
const brandLc = r.brand_name_lc;
if (!this.medicines.has(brandLc)) {
this.medicines.set(brandLc, {
generic_name: r.generic_name,
active_ingredients: r.active_ingredients ? r.active_ingredients.split('|') : [],
interactions: new Map(),
});
}
if (r.drug_b) {
this.medicines.get(brandLc).interactions.set(r.drug_b, {
severity: r.severity,
side_effects: r.side_effects,
mechanism: r.mechanism,
clinical_action: r.clinical_action,
});
const [da, db] = r.drug_a < r.drug_b ? [r.drug_a, r.drug_b] : [r.drug_b, r.drug_a];
this.pairIndex.set(`${da}|${db}`, {
severity: r.severity,
side_effects: r.side_effects,
mechanism: r.mechanism,
clinical_action: r.clinical_action,
});
}
}
getMedicine(brandName) {
return this.medicines.get(brandName.toLowerCase()) || null;
}
getPair(drugA, drugB) {
const [da, db] = drugA < drugB ? [drugA, drugB] : [drugB, drugA];
return this.pairIndex.get(`${da}|${db}`) || null;
}
save(brandName, geminiData) {
const brandLc = brandName.toLowerCase();
const activeIngs = geminiData.active_ingredients || [];
const allInteractions = [
...(geminiData.prescription_interactions || []),
...(geminiData.common_interactions || []),
];
const rows = [];
const now = new Date().toISOString().split('T')[0];
if (allInteractions.length === 0) {
// Save medicine with no interactions found
rows.push({
brand_name_lc: brandLc,
generic_name: geminiData.generic_name || '',
active_ingredients: activeIngs.join('|'),
drug_a: brandLc,
drug_b: '',
severity: '',
side_effects: '',
mechanism: '',
clinical_action: '',
confidence: geminiData.confidence || 0,
created_at: now,
});
} else {
for (const it of allInteractions) {
if (!it.drug || it.severity === 'Safe') continue;
rows.push({
brand_name_lc: brandLc,
generic_name: geminiData.generic_name || '',
active_ingredients: activeIngs.join('|'),
drug_a: brandLc,
drug_b: it.drug.toLowerCase(),
severity: it.severity,
side_effects: it.side_effects || '',
mechanism: it.mechanism || '',
clinical_action: it.clinical_action || '',
confidence: geminiData.confidence || 0,
created_at: now,
});
}
// If every interaction was filtered out, still register the medicine
// so future queries hit the cache and the brand resolves correctly.
if (rows.length === 0) {
rows.push({
brand_name_lc: brandLc,
generic_name: geminiData.generic_name || '',
active_ingredients: activeIngs.join('|'),
drug_a: brandLc,
drug_b: '',
severity: '',
side_effects: '',
mechanism: '',
clinical_action: '',
confidence: geminiData.confidence || 0,
created_at: now,
});
}
}
// Append to CSV
const csv = stringify(rows, { header: false, columns: HEADERS });
fs.appendFileSync(CACHE_FILE, csv);
// Index in memory
for (const r of rows) this._indexRow(r);
console.log(`[gap1Cache] saved ${rows.length} rows for "${brandName}"`);
return rows.length;
}
}
module.exports = { Gap1Cache };