| 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; |
| } |
|
|
| |
| 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`); |
| }); |
|
|