LJTSG commited on
Commit
29facf9
·
verified ·
1 Parent(s): 561dff9

Upload garden-bridge.js with huggingface_hub

Browse files
Files changed (1) hide show
  1. 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
+ });