Upload garden-bridge.js with huggingface_hub
Browse files- garden-bridge.js +191 -0
garden-bridge.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const http = require('http');
|
| 2 |
+
const WebSocket = require('ws');
|
| 3 |
+
const fs = require('fs');
|
| 4 |
+
const path = require('path');
|
| 5 |
+
const PORT = 8160;
|
| 6 |
+
|
| 7 |
+
let pendingRequests = [];
|
| 8 |
+
let wsClient = null;
|
| 9 |
+
let requestId = 0;
|
| 10 |
+
const callbacks = new Map();
|
| 11 |
+
|
| 12 |
+
const ENTITY_DIR = path.join(__dirname, '..', 'entity');
|
| 13 |
+
|
| 14 |
+
async function extractKeywords(text) {
|
| 15 |
+
try {
|
| 16 |
+
const resp = await fetch('http://127.0.0.1:8093/v1/chat/completions', {
|
| 17 |
+
method: 'POST',
|
| 18 |
+
headers: { 'Content-Type': 'application/json' },
|
| 19 |
+
body: JSON.stringify({
|
| 20 |
+
model: 'xlam', messages: [{ role: 'user', content: `Extract 3-5 keyword phrases from: "${text}". Return JSON: {"keywords":["k1","k2"]}` }],
|
| 21 |
+
temperature: 0.1, max_tokens: 60,
|
| 22 |
+
}),
|
| 23 |
+
});
|
| 24 |
+
if (resp.ok) {
|
| 25 |
+
const data = await resp.json();
|
| 26 |
+
const match = data.choices?.[0]?.message?.content?.match(/\{[\s\S]*"keywords"[\s\S]*\}/);
|
| 27 |
+
if (match) return JSON.parse(match[0]).keywords.map(k => k.toLowerCase());
|
| 28 |
+
}
|
| 29 |
+
} catch (e) {}
|
| 30 |
+
const stop = new Set(['what','that','this','with','from','have','your','been','does','were','they','about','would','could','should','there','where','which']);
|
| 31 |
+
return (text.toLowerCase().match(/[a-z]{4,}/g) || []).filter(w => !stop.has(w)).slice(0, 5);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
function grepEntity(keywords) {
|
| 35 |
+
const results = [];
|
| 36 |
+
function scan(dir) {
|
| 37 |
+
try {
|
| 38 |
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
| 39 |
+
const fp = path.join(dir, e.name);
|
| 40 |
+
if (e.isDirectory()) { scan(fp); continue; }
|
| 41 |
+
if (!/\.(md|json|txt)$/.test(e.name)) continue;
|
| 42 |
+
try {
|
| 43 |
+
const lines = fs.readFileSync(fp, 'utf8').split('\n');
|
| 44 |
+
for (let i = 0; i < lines.length; i++) {
|
| 45 |
+
const line = lines[i].trim();
|
| 46 |
+
if (line.length < 10 || line.length > 300) continue;
|
| 47 |
+
const lower = line.toLowerCase();
|
| 48 |
+
let score = 0;
|
| 49 |
+
for (const kw of keywords) { if (lower.includes(kw)) score++; }
|
| 50 |
+
if (score > 0) results.push({ line, score, file: path.relative(ENTITY_DIR, fp) });
|
| 51 |
+
}
|
| 52 |
+
} catch (e) {}
|
| 53 |
+
}
|
| 54 |
+
} catch (e) {}
|
| 55 |
+
}
|
| 56 |
+
scan(ENTITY_DIR);
|
| 57 |
+
results.sort((a, b) => b.score - a.score);
|
| 58 |
+
return results.slice(0, 5);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
const server = http.createServer(async (req, res) => {
|
| 62 |
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
| 63 |
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
| 64 |
+
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
|
| 65 |
+
|
| 66 |
+
if (req.url === '/v1/chat/completions' && req.method === 'POST') {
|
| 67 |
+
let body = '';
|
| 68 |
+
req.on('data', c => body += c);
|
| 69 |
+
req.on('end', async () => {
|
| 70 |
+
try {
|
| 71 |
+
const data = JSON.parse(body);
|
| 72 |
+
const userMsg = data.messages?.find(m => m.role === 'user')?.content || '';
|
| 73 |
+
let entity = data.entity || '';
|
| 74 |
+
if (!entity) {
|
| 75 |
+
const sysMsg = data.messages?.find(m => m.role === 'system')?.content || '';
|
| 76 |
+
const match = sysMsg.match(/^You are (\w[\w-]*)/i);
|
| 77 |
+
if (match) entity = match[1].toLowerCase();
|
| 78 |
+
}
|
| 79 |
+
if (!entity) entity = 'grandma';
|
| 80 |
+
const sysContent = data.messages?.find(m => m.role === 'system')?.content || '';
|
| 81 |
+
console.log(`[bridge] entity=${entity} | user="${userMsg.slice(0,60)}" | sys=${sysContent.length} chars | from-body=${!!data.entity}`);
|
| 82 |
+
|
| 83 |
+
const id = ++requestId;
|
| 84 |
+
const promise = new Promise((resolve, reject) => {
|
| 85 |
+
callbacks.set(id, { resolve, reject, timer: setTimeout(() => { callbacks.delete(id); reject(new Error('timeout')); }, 120000) });
|
| 86 |
+
});
|
| 87 |
+
|
| 88 |
+
if (wsClient && wsClient.readyState === 1) {
|
| 89 |
+
wsClient.send(JSON.stringify({ id, entity, message: userMsg, systemContext: sysContent, timestamp: Date.now() }));
|
| 90 |
+
} else {
|
| 91 |
+
pendingRequests.push({ id, entity, message: userMsg, keywords, grepResults });
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
const result = await promise;
|
| 95 |
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
| 96 |
+
res.end(JSON.stringify({
|
| 97 |
+
choices: [{ message: { role: 'assistant', content: result.text } }],
|
| 98 |
+
entity, model: 'gemma-26b-thinking-layer',
|
| 99 |
+
}));
|
| 100 |
+
} catch (e) {
|
| 101 |
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
| 102 |
+
res.end(JSON.stringify({ error: e.message }));
|
| 103 |
+
}
|
| 104 |
+
});
|
| 105 |
+
return;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
if (req.url === '/api/keywords' && req.method === 'POST') {
|
| 109 |
+
let body = '';
|
| 110 |
+
req.on('data', c => body += c);
|
| 111 |
+
req.on('end', async () => {
|
| 112 |
+
const { text } = JSON.parse(body);
|
| 113 |
+
const keywords = await extractKeywords(text);
|
| 114 |
+
const grepResults = grepEntity(keywords);
|
| 115 |
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
| 116 |
+
res.end(JSON.stringify({ keywords, grepResults }));
|
| 117 |
+
});
|
| 118 |
+
return;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
if (req.url === '/api/save-memory' && req.method === 'POST') {
|
| 122 |
+
let body = '';
|
| 123 |
+
req.on('data', c => body += c);
|
| 124 |
+
req.on('end', () => {
|
| 125 |
+
const { entity, turn } = JSON.parse(body);
|
| 126 |
+
const memPath = path.join(ENTITY_DIR, 'memory', 'running.md');
|
| 127 |
+
try {
|
| 128 |
+
const ts = new Date().toISOString().slice(0, 19);
|
| 129 |
+
fs.appendFileSync(memPath, `\n[${ts}] [${entity}] ${turn}\n`);
|
| 130 |
+
res.writeHead(200); res.end('ok');
|
| 131 |
+
} catch (e) {
|
| 132 |
+
res.writeHead(500); res.end(e.message);
|
| 133 |
+
}
|
| 134 |
+
});
|
| 135 |
+
return;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
if (req.url === '/status') {
|
| 139 |
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
| 140 |
+
res.end(JSON.stringify({ connected: wsClient?.readyState === 1, pending: pendingRequests.length }));
|
| 141 |
+
return;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
// GET /api/recent?entity=anima&lines=20
|
| 145 |
+
if (req.url.startsWith('/api/recent') && req.method === 'GET') {
|
| 146 |
+
const params = new URL(req.url, 'http://localhost').searchParams;
|
| 147 |
+
const entity = params.get('entity') || 'all';
|
| 148 |
+
const lines = parseInt(params.get('lines') || '30');
|
| 149 |
+
const memPath = path.join(ENTITY_DIR, 'memory', 'running.md');
|
| 150 |
+
try {
|
| 151 |
+
const content = fs.readFileSync(memPath, 'utf8');
|
| 152 |
+
const allLines = content.split('\n').filter(l => l.trim());
|
| 153 |
+
const filtered = entity === 'all' ? allLines : allLines.filter(l => l.toLowerCase().includes(`[${entity}]`));
|
| 154 |
+
const recent = filtered.slice(-lines);
|
| 155 |
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
| 156 |
+
res.end(JSON.stringify({ entity, lines: recent.length, memory: recent }));
|
| 157 |
+
} catch (e) {
|
| 158 |
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
| 159 |
+
res.end(JSON.stringify({ entity, lines: 0, memory: [] }));
|
| 160 |
+
}
|
| 161 |
+
return;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
res.writeHead(404); res.end('not found');
|
| 165 |
+
});
|
| 166 |
+
|
| 167 |
+
const wss = new WebSocket.Server({ server });
|
| 168 |
+
wss.on('connection', (ws) => {
|
| 169 |
+
console.log('[bridge] WebGPU webapp connected');
|
| 170 |
+
wsClient = ws;
|
| 171 |
+
for (const req of pendingRequests) ws.send(JSON.stringify(req));
|
| 172 |
+
pendingRequests = [];
|
| 173 |
+
|
| 174 |
+
ws.on('message', (data) => {
|
| 175 |
+
const msg = JSON.parse(data);
|
| 176 |
+
if (msg.id && callbacks.has(msg.id)) {
|
| 177 |
+
const cb = callbacks.get(msg.id);
|
| 178 |
+
clearTimeout(cb.timer);
|
| 179 |
+
callbacks.delete(msg.id);
|
| 180 |
+
cb.resolve({ text: msg.text });
|
| 181 |
+
}
|
| 182 |
+
});
|
| 183 |
+
|
| 184 |
+
ws.on('close', () => { console.log('[bridge] webapp disconnected'); wsClient = null; });
|
| 185 |
+
});
|
| 186 |
+
|
| 187 |
+
server.listen(PORT, () => {
|
| 188 |
+
console.log(`[garden-bridge] HTTP + WebSocket on :${PORT}`);
|
| 189 |
+
console.log(`[garden-bridge] OpenAI-compatible: POST http://localhost:${PORT}/v1/chat/completions`);
|
| 190 |
+
console.log(`[garden-bridge] Pass { "entity": "esh" } in body to select entity`);
|
| 191 |
+
});
|