gemma-webgpu-thinking-engine / garden-bridge.js
LJTSG's picture
Upload garden-bridge.js with huggingface_hub
29facf9 verified
const http = require('http');
const WebSocket = require('ws');
const fs = require('fs');
const path = require('path');
const PORT = 8160;
let pendingRequests = [];
let wsClient = null;
let requestId = 0;
const callbacks = new Map();
const ENTITY_DIR = path.join(__dirname, '..', 'entity');
async function extractKeywords(text) {
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 from: "${text}". Return JSON: {"keywords":["k1","k2"]}` }],
temperature: 0.1, max_tokens: 60,
}),
});
if (resp.ok) {
const data = await resp.json();
const match = data.choices?.[0]?.message?.content?.match(/\{[\s\S]*"keywords"[\s\S]*\}/);
if (match) return JSON.parse(match[0]).keywords.map(k => k.toLowerCase());
}
} catch (e) {}
const stop = new Set(['what','that','this','with','from','have','your','been','does','were','they','about','would','could','should','there','where','which']);
return (text.toLowerCase().match(/[a-z]{4,}/g) || []).filter(w => !stop.has(w)).slice(0, 5);
}
function grepEntity(keywords) {
const results = [];
function scan(dir) {
try {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const fp = path.join(dir, e.name);
if (e.isDirectory()) { scan(fp); continue; }
if (!/\.(md|json|txt)$/.test(e.name)) continue;
try {
const lines = fs.readFileSync(fp, 'utf8').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();
let score = 0;
for (const kw of keywords) { if (lower.includes(kw)) score++; }
if (score > 0) results.push({ line, score, file: path.relative(ENTITY_DIR, fp) });
}
} catch (e) {}
}
} catch (e) {}
}
scan(ENTITY_DIR);
results.sort((a, b) => b.score - a.score);
return results.slice(0, 5);
}
const server = http.createServer(async (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
if (req.url === '/v1/chat/completions' && req.method === 'POST') {
let body = '';
req.on('data', c => body += c);
req.on('end', async () => {
try {
const data = JSON.parse(body);
const userMsg = data.messages?.find(m => m.role === 'user')?.content || '';
let entity = data.entity || '';
if (!entity) {
const sysMsg = data.messages?.find(m => m.role === 'system')?.content || '';
const match = sysMsg.match(/^You are (\w[\w-]*)/i);
if (match) entity = match[1].toLowerCase();
}
if (!entity) entity = 'grandma';
const sysContent = data.messages?.find(m => m.role === 'system')?.content || '';
console.log(`[bridge] entity=${entity} | user="${userMsg.slice(0,60)}" | sys=${sysContent.length} chars | from-body=${!!data.entity}`);
const id = ++requestId;
const promise = new Promise((resolve, reject) => {
callbacks.set(id, { resolve, reject, timer: setTimeout(() => { callbacks.delete(id); reject(new Error('timeout')); }, 120000) });
});
if (wsClient && wsClient.readyState === 1) {
wsClient.send(JSON.stringify({ id, entity, message: userMsg, systemContext: sysContent, timestamp: Date.now() }));
} else {
pendingRequests.push({ id, entity, message: userMsg, keywords, grepResults });
}
const result = await promise;
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
choices: [{ message: { role: 'assistant', content: result.text } }],
entity, model: 'gemma-26b-thinking-layer',
}));
} catch (e) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: e.message }));
}
});
return;
}
if (req.url === '/api/keywords' && req.method === 'POST') {
let body = '';
req.on('data', c => body += c);
req.on('end', async () => {
const { text } = JSON.parse(body);
const keywords = await extractKeywords(text);
const grepResults = grepEntity(keywords);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ keywords, grepResults }));
});
return;
}
if (req.url === '/api/save-memory' && req.method === 'POST') {
let body = '';
req.on('data', c => body += c);
req.on('end', () => {
const { entity, turn } = JSON.parse(body);
const memPath = path.join(ENTITY_DIR, 'memory', 'running.md');
try {
const ts = new Date().toISOString().slice(0, 19);
fs.appendFileSync(memPath, `\n[${ts}] [${entity}] ${turn}\n`);
res.writeHead(200); res.end('ok');
} catch (e) {
res.writeHead(500); res.end(e.message);
}
});
return;
}
if (req.url === '/status') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ connected: wsClient?.readyState === 1, pending: pendingRequests.length }));
return;
}
// GET /api/recent?entity=anima&lines=20
if (req.url.startsWith('/api/recent') && req.method === 'GET') {
const params = new URL(req.url, 'http://localhost').searchParams;
const entity = params.get('entity') || 'all';
const lines = parseInt(params.get('lines') || '30');
const memPath = path.join(ENTITY_DIR, 'memory', 'running.md');
try {
const content = fs.readFileSync(memPath, 'utf8');
const allLines = content.split('\n').filter(l => l.trim());
const filtered = entity === 'all' ? allLines : allLines.filter(l => l.toLowerCase().includes(`[${entity}]`));
const recent = filtered.slice(-lines);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ entity, lines: recent.length, memory: recent }));
} catch (e) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ entity, lines: 0, memory: [] }));
}
return;
}
res.writeHead(404); res.end('not found');
});
const wss = new WebSocket.Server({ server });
wss.on('connection', (ws) => {
console.log('[bridge] WebGPU webapp connected');
wsClient = ws;
for (const req of pendingRequests) ws.send(JSON.stringify(req));
pendingRequests = [];
ws.on('message', (data) => {
const msg = JSON.parse(data);
if (msg.id && callbacks.has(msg.id)) {
const cb = callbacks.get(msg.id);
clearTimeout(cb.timer);
callbacks.delete(msg.id);
cb.resolve({ text: msg.text });
}
});
ws.on('close', () => { console.log('[bridge] webapp disconnected'); wsClient = null; });
});
server.listen(PORT, () => {
console.log(`[garden-bridge] HTTP + WebSocket on :${PORT}`);
console.log(`[garden-bridge] OpenAI-compatible: POST http://localhost:${PORT}/v1/chat/completions`);
console.log(`[garden-bridge] Pass { "entity": "esh" } in body to select entity`);
});