File size: 5,822 Bytes
f510c6d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | const http = require('http'), fs = require('fs'), path = require('path');
const fetch = globalThis.fetch || require('node-fetch'); // Node 18+ has built-in fetch
const PORT = 8150;
const ROOT = __dirname;
const SPLITS = path.join(ROOT, 'model_splits');
const MIME = {
'.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript',
'.json': 'application/json', '.gguf': 'application/octet-stream',
'.wasm': 'application/wasm', '.ts': 'text/javascript',
};
const ENTITY_DIR = path.join(ROOT, '..', 'entity');
async function extractKeywords(text) {
// Try local xLAM at :8093 for intent-based keyword extraction
try {
const resp = await fetch('http://127.0.0.1:8093/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'xlam',
messages: [{ role: 'user', content: `Extract 3-5 keyword phrases (1-3 words each) from this message that capture the user's intent. Return JSON: {"keywords":["k1","k2","k3"]}\n\nMessage: "${text}"` }],
temperature: 0.1,
max_tokens: 60,
}),
});
if (resp.ok) {
const data = await resp.json();
const content = data.choices?.[0]?.message?.content || '';
const match = content.match(/\{[\s\S]*"keywords"[\s\S]*\}/);
if (match) {
const parsed = JSON.parse(match[0]);
if (Array.isArray(parsed.keywords)) return parsed.keywords.map(k => k.toLowerCase());
}
}
} catch (e) { /* xLAM not available, fall back */ }
// Fallback: extract names and nouns (4+ chars, skip common words)
const stop = new Set(['what','that','this','with','from','have','your','been','does','were','they','their','about','would','could','should','there','where','which','these','those','before','after','other','being','still','never','always','remember']);
return (text.toLowerCase().match(/[a-z]{4,}/g) || []).filter(w => !stop.has(w)).slice(0, 5);
}
function grepEntity(text, keywords) {
const unique = [...new Set(keywords)];
if (unique.length === 0) return [];
const results = [];
function scanDir(dir) {
try {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const e of entries) {
const fp = path.join(dir, e.name);
if (e.isDirectory()) { scanDir(fp); continue; }
if (!/\.(md|json|txt)$/.test(e.name)) continue;
try {
const content = fs.readFileSync(fp, 'utf8');
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.length < 10 || line.length > 300) continue;
const lower = line.toLowerCase();
for (const kw of unique) {
if (lower.includes(kw)) {
results.push(line);
break;
}
}
}
} catch (e) {}
}
} catch (e) {}
}
scanDir(ENTITY_DIR);
return [...new Set(results)].slice(0, 6);
}
http.createServer((req, res) => {
// Handle grep API
if (req.method === 'POST' && req.url === '/api/grep') {
let body = '';
req.on('data', c => body += c);
req.on('end', async () => {
try {
const { text } = JSON.parse(body);
const keywords = await extractKeywords(text);
console.log('[grep] keywords:', keywords);
const results = grepEntity(text, keywords);
console.log('[grep] found:', results.length, 'snippets');
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
res.end(JSON.stringify({ results, keywords }));
} catch (e) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: e.message }));
}
});
return;
}
if (req.method === 'OPTIONS') {
res.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,HEAD',
'Access-Control-Allow-Headers': 'Content-Type,Range',
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin',
});
return res.end();
}
let p = decodeURIComponent(req.url.split('?')[0]);
if (p === '/') p = '/index.html';
let fp;
if (p.startsWith('/model/')) {
fp = path.join(SPLITS, p.slice('/model/'.length));
} else {
// Resolve relative to project root, allowing ../mamba_webgpu/ paths
fp = path.resolve(ROOT, '.' + p);
}
fs.stat(fp, (e, st) => {
if (e) { res.writeHead(404); return res.end('not found: ' + fp); }
const total = st.size;
const range = req.headers.range;
const ct = MIME[path.extname(fp)] || 'application/octet-stream';
const headers = {
'Content-Type': ct,
'Accept-Ranges': 'bytes',
'Access-Control-Allow-Origin': '*',
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin',
};
if (range) {
const m = range.match(/bytes=(\d+)-(\d*)/);
if (m) {
const start = parseInt(m[1]);
const end = m[2] ? parseInt(m[2]) : total - 1;
headers['Content-Range'] = 'bytes ' + start + '-' + end + '/' + total;
headers['Content-Length'] = end - start + 1;
res.writeHead(206, headers);
fs.createReadStream(fp, { start, end }).pipe(res);
return;
}
}
headers['Content-Length'] = total;
res.writeHead(200, headers);
fs.createReadStream(fp).pipe(res);
});
}).listen(PORT, () => {
console.log('Gemma WebGPU on :' + PORT);
console.log('Model splits: ' + SPLITS);
const files = fs.readdirSync(SPLITS).filter(f => f.endsWith('.gguf'));
console.log('Split files: ' + files.length);
console.log('CORS + COEP/COOP headers enabled for wllama multi-threading');
});
|