Emalawi19 commited on
Commit
5548e36
Β·
verified Β·
1 Parent(s): 1419a0e

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +290 -211
server.js CHANGED
@@ -1,273 +1,352 @@
1
- import { pipeline, TextStreamer } from '@huggingface/transformers';
2
  import http from 'http';
3
- import fs from 'fs';
4
- import path from 'path';
5
 
6
- const PORT = 7860;
7
- const MODEL_NAME = 'onnx-community/Qwen2.5-0.5B-Instruct';
8
- const KNOWLEDGE_DIR = './knowledge';
9
  let generator;
10
- let knowledgeBase = [];
11
-
12
- // ── KNOWLEDGE ─────────────────────────────────────────────────────────────────
13
- function loadKnowledge() {
14
- if (!fs.existsSync(KNOWLEDGE_DIR)) { fs.mkdirSync(KNOWLEDGE_DIR); return; }
15
- const files = fs.readdirSync(KNOWLEDGE_DIR).filter(f => f.endsWith('.txt'));
16
- knowledgeBase = [];
17
- for (const file of files) {
18
- const content = fs.readFileSync(path.join(KNOWLEDGE_DIR, file), 'utf-8');
19
- const chunks = splitChunks(content, 800, 80);
20
- chunks.forEach(c => knowledgeBase.push({ source: file, text: c }));
21
- }
22
- console.log(`Knowledge loaded: ${files.length} files, ${knowledgeBase.length} chunks`);
23
- }
24
-
25
- function splitChunks(text, size, overlap) {
26
- const chunks = [];
27
- let start = 0;
28
- while (start < text.length) {
29
- chunks.push(text.slice(start, start + size));
30
- start += size - overlap;
31
- }
32
- return chunks;
33
- }
34
 
35
- function retrieveContext(prompt, topK = 5) {
36
- if (knowledgeBase.length === 0) return '';
 
 
37
 
38
- const words = prompt.toLowerCase()
39
- .split(/\W+/)
40
- .filter(w => w.length > 2);
41
-
42
- const scored = knowledgeBase.map(chunk => {
43
- const lower = chunk.text.toLowerCase();
44
- let score = 0;
45
- for (const w of words) {
46
- const matches = (lower.match(new RegExp(w, 'g')) || []).length;
47
- score += matches;
48
- }
49
- return { ...chunk, score };
50
  });
 
51
 
52
- const top = scored
53
- .filter(c => c.score > 0)
54
- .sort((a, b) => b.score - a.score)
55
- .slice(0, topK);
 
 
 
 
 
 
 
 
 
56
 
57
- if (top.length === 0) return '';
 
 
58
 
59
- return top.map(c => c.text).join('\n\n---\n\n');
 
 
 
 
60
  }
61
 
62
- function buildMessages(prompt) {
63
- const ragContext = retrieveContext(prompt, 5);
64
- console.log(`RAG chunks found: ${ragContext.length} chars`);
65
-
66
- const systemPrompt = ragContext
67
- ? `You are Mlimi Connect AI, a free agricultural advisor for Malawian farmers.
68
- KNOWLEDGE BASE β€” THIS IS YOUR ONLY SOURCE OF INFORMATION. USE ONLY THIS:
69
- ===START OF KNOWLEDGE===
70
- ${ragContext}
71
- ===END OF KNOWLEDGE===
72
- STRICT RULES:
73
- 1. Answer ONLY using the knowledge provided above between ===START=== and ===END===.
74
- 2. Do NOT add information from outside the knowledge base.
75
- 3. Do NOT be vague. Give specific details: variety names, exact spacing, fertilizer amounts, timing.
76
- 4. Structure your answer clearly with numbered steps.
77
- 5. If the knowledge above does not contain the answer, say: "I don't have specific information on that in my knowledge base."
78
- 6. ONLY answer agriculture questions. For anything else say: "I can only help with farming questions."`
79
-
80
- : `You are Mlimi Connect AI, a free agricultural advisor for Malawian farmers.
81
- I don't have specific notes on that topic in my knowledge base yet.
82
- Give a brief, honest answer based on general Malawian agricultural knowledge.
83
- Keep it practical and specific to Malawi's conditions.
84
- ONLY answer agriculture questions.`;
85
-
86
- return {
87
- messages: [
88
- { role: 'system', content: systemPrompt },
89
- { role: 'user', content: prompt }
90
- ],
91
- ragContext
92
- };
93
  }
94
 
95
  // ── MODEL ─────────────────────────────────────────────────────────────────────
96
  async function loadModel() {
97
  console.log("Loading model...");
98
- generator = await pipeline('text-generation', MODEL_NAME, { dtype: 'q4' });
 
99
  console.log("Model ready!");
100
  }
101
 
102
- // Non-streaming (kept for backward compatibility with /generate)
103
- async function generateResponse(messages) {
104
- const output = await generator(messages, {
105
- max_new_tokens: 600,
106
- temperature: 0.2,
107
- repetition_penalty: 1.15,
108
- do_sample: false
109
- });
110
- const generated = output[0].generated_text;
111
- if (Array.isArray(generated)) return generated.at(-1)?.content || '';
112
- return String(generated || '');
 
 
 
 
 
 
 
 
 
 
113
  }
114
 
115
- // Streaming version β€” calls onToken(token) for every generated token as it arrives
116
- async function generateResponseStream(messages, onToken) {
117
- const tokenizer = generator.tokenizer;
118
-
119
- const streamer = new TextStreamer(tokenizer, {
120
- skip_prompt: true,
121
- skip_special_tokens: true,
122
- callback_function: (text) => {
123
- if (text) onToken(text);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  }
125
- });
126
 
127
- const output = await generator(messages, {
128
- max_new_tokens: 600,
129
- temperature: 0.2,
130
- repetition_penalty: 1.15,
131
- do_sample: false,
132
- streamer: streamer
133
- });
 
 
 
 
 
 
 
 
 
 
 
134
 
135
- const generated = output[0].generated_text;
136
- if (Array.isArray(generated)) return generated.at(-1)?.content || '';
137
- return String(generated || '');
138
- }
139
 
140
- // ── SERVER ────────────────────────────────────────────────────────────────────
141
- const server = http.createServer(async (req, res) => {
142
- res.setHeader('Access-Control-Allow-Origin', '*');
143
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
144
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
 
 
 
145
 
146
- if (req.method === 'OPTIONS') { res.writeHead(200); return res.end(); }
 
 
147
 
148
- const pathname = req.url.split('?')[0];
 
 
 
 
 
149
 
150
- if (pathname === '/' && req.method === 'GET') {
151
- res.setHeader('Content-Type', 'application/json');
152
- res.writeHead(200);
153
- return res.end(JSON.stringify({
154
- status: "running",
155
- model: MODEL_NAME,
156
- knowledge_chunks: knowledgeBase.length
157
- }));
158
  }
159
 
160
- if (pathname === '/reload-knowledge' && req.method === 'POST') {
161
- loadKnowledge();
162
- res.setHeader('Content-Type', 'application/json');
163
- res.writeHead(200);
164
- return res.end(JSON.stringify({ message: `Reloaded: ${knowledgeBase.length} chunks` }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  }
 
166
 
167
- // ── Existing non-streaming endpoint (unchanged behavior) ──────────────
168
- if (pathname === '/generate' && req.method === 'POST') {
169
- let body = '';
170
- req.on('data', c => { body += c.toString(); });
171
- req.on('end', async () => {
172
- res.setHeader('Content-Type', 'application/json');
173
- try {
174
- const { prompt } = JSON.parse(body);
175
 
176
- if (!generator) {
177
- res.writeHead(503);
178
- return res.end(JSON.stringify({ error: "Model still loading..." }));
179
- }
 
180
 
181
- console.log(`Query: "${prompt}"`);
 
 
 
 
 
 
182
 
183
- const { messages, ragContext } = buildMessages(prompt);
184
- let result = await generateResponse(messages);
 
185
 
186
- if (!result || result.trim().length < 10) {
187
- result = ragContext
188
- ? `Here is what my knowledge base says:\n\n${ragContext.slice(0, 800)}`
189
- : "I don't have specific information on that topic. Please ask your local agricultural extension officer.";
190
- }
191
 
192
- console.log(`Response: ${result.slice(0, 80)}...`);
193
- res.writeHead(200);
194
- res.end(JSON.stringify({ result }));
195
 
196
- } catch (err) {
197
- console.error("Error:", err.message);
198
- res.writeHead(500);
199
- res.end(JSON.stringify({ error: err.message }));
200
- }
 
 
201
  });
202
- return;
203
  }
204
 
205
- // ── NEW: streaming endpoint (SSE) ──────────────────────────────────────
206
- if (pathname === '/generate-stream' && req.method === 'POST') {
 
 
 
 
 
 
 
207
  let body = '';
208
  req.on('data', c => { body += c.toString(); });
209
  req.on('end', async () => {
210
- try {
211
- const { prompt } = JSON.parse(body);
212
-
213
- if (!generator) {
214
- res.writeHead(503, { 'Content-Type': 'application/json' });
215
- return res.end(JSON.stringify({ error: "Model still loading..." }));
216
- }
217
-
218
- console.log(`Query (stream): "${prompt}"`);
219
-
220
- res.writeHead(200, {
221
- 'Content-Type': 'text/event-stream',
222
- 'Cache-Control': 'no-cache',
223
- 'Connection': 'keep-alive',
224
- 'X-Accel-Buffering': 'no' // disable proxy buffering (nginx etc.)
225
- });
226
 
227
- const { messages, ragContext } = buildMessages(prompt);
228
-
229
- let fullText = '';
230
- let sentAnything = false;
231
-
232
- await generateResponseStream(messages, (token) => {
233
- fullText += token;
234
- sentAnything = true;
235
- res.write(`data: ${JSON.stringify({ token })}\n\n`);
236
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
 
238
- // Fallback if the model produced nothing usable
239
- if (!sentAnything || fullText.trim().length < 10) {
240
- const fallback = ragContext
241
- ? `Here is what my knowledge base says:\n\n${ragContext.slice(0, 800)}`
242
- : "I don't have specific information on that topic. Please ask your local agricultural extension officer.";
243
- res.write(`data: ${JSON.stringify({ token: fallback })}\n\n`);
 
 
 
 
 
244
  }
 
245
 
246
- res.write('data: [DONE]\n\n');
247
- res.end();
 
 
 
 
 
 
248
 
 
 
 
 
 
249
  } catch (err) {
250
- console.error("Error:", err.message);
251
- // If headers already sent (mid-stream), send an SSE error event instead
252
- if (!res.headersSent) {
253
- res.writeHead(500, { 'Content-Type': 'application/json' });
254
- return res.end(JSON.stringify({ error: err.message }));
255
- }
256
- res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
257
  res.end();
258
  }
259
  });
260
  return;
261
  }
262
 
263
- res.setHeader('Content-Type', 'application/json');
264
- res.writeHead(404);
265
- res.end(JSON.stringify({ error: "Not Found" }));
266
  });
267
 
268
- loadKnowledge();
269
  loadModel().then(() => {
270
  server.listen(PORT, '0.0.0.0', () => {
271
- console.log(`Mlimi Connect backend on port ${PORT}`);
 
 
 
 
 
272
  });
273
  });
 
1
+ import { pipeline } from '@huggingface/transformers';
2
  import http from 'http';
 
 
3
 
4
+ const PORT = 7860;
5
+ const MODEL_NAME = 'onnx-community/Qwen2.5-0.5B-Instruct';
 
6
  let generator;
7
+ let modelReady = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
+ // ── QUEUE SYSTEM ──────────────────────────────────────────────────────────────
10
+ const queue = []; // pending requests
11
+ const MAX_PARALLEL = 2; // max concurrent generations
12
+ let activeCount = 0;
13
 
14
+ function enqueue(task) {
15
+ return new Promise((resolve, reject) => {
16
+ queue.push({ task, resolve, reject });
17
+ processQueue();
 
 
 
 
 
 
 
 
18
  });
19
+ }
20
 
21
+ async function processQueue() {
22
+ if (activeCount >= MAX_PARALLEL || queue.length === 0) return;
23
+ activeCount++;
24
+ const { task, resolve, reject } = queue.shift();
25
+ try {
26
+ resolve(await task());
27
+ } catch (err) {
28
+ reject(err);
29
+ } finally {
30
+ activeCount--;
31
+ processQueue();
32
+ }
33
+ }
34
 
35
+ function getQueuePosition() {
36
+ return queue.length;
37
+ }
38
 
39
+ // ── CORS ──────────────────────────────────────────────────────────────────────
40
+ function setCORS(res) {
41
+ res.setHeader('Access-Control-Allow-Origin', '*');
42
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
43
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
44
  }
45
 
46
+ function sendJSON(res, status, data) {
47
+ setCORS(res);
48
+ res.setHeader('Content-Type', 'application/json');
49
+ res.writeHead(status);
50
+ res.end(JSON.stringify(data));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  }
52
 
53
  // ── MODEL ─────────────────────────────────────────────────────────────────────
54
  async function loadModel() {
55
  console.log("Loading model...");
56
+ generator = await pipeline('text-generation', MODEL_NAME, { dtype: 'q4' });
57
+ modelReady = true;
58
  console.log("Model ready!");
59
  }
60
 
61
+ // ── SINGLE CHUNK GENERATION ───────────────────────────────────────────────────
62
+ // Generates one chunk with retry on failure
63
+ async function generateChunk(messages, maxTokens, attempt = 0) {
64
+ try {
65
+ const output = await generator(messages, {
66
+ max_new_tokens: maxTokens,
67
+ temperature: 0.3,
68
+ repetition_penalty: 1.15,
69
+ do_sample: false
70
+ });
71
+ const generated = output[0].generated_text;
72
+ if (Array.isArray(generated)) return generated.at(-1)?.content || '';
73
+ return String(generated || '');
74
+ } catch (err) {
75
+ if (attempt < 2) {
76
+ console.log(`Chunk failed, retrying (attempt ${attempt + 1})...`);
77
+ await new Promise(r => setTimeout(r, 1000));
78
+ return generateChunk(messages, maxTokens, attempt + 1);
79
+ }
80
+ throw err;
81
+ }
82
  }
83
 
84
+ // ── SLIDING WINDOW STREAMING ──────────────────────────────────────────────────
85
+ // Breaks generation into chunks, streams each chunk, auto-resumes with context
86
+ async function slidingWindowStream(res, systemPrompt, userPrompt, totalTokens) {
87
+ const CHUNK_TOKENS = 250; // tokens per chunk
88
+ const CONTEXT_CHARS = 300; // chars of previous output to use as context
89
+ const totalChunks = Math.ceil(totalTokens / CHUNK_TOKENS);
90
+
91
+ let fullOutput = '';
92
+ let previousContext = '';
93
+
94
+ for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
95
+ const isFirst = chunkIndex === 0;
96
+ const isLast = chunkIndex === totalChunks - 1;
97
+ const chunkNum = chunkIndex + 1;
98
+
99
+ // Send chunk start event
100
+ res.write(`data: ${JSON.stringify({
101
+ type: 'chunk_start',
102
+ chunk: chunkNum,
103
+ totalChunks: totalChunks
104
+ })}\n\n`);
105
+
106
+ // Build messages for this chunk
107
+ let chunkUserPrompt;
108
+ if (isFirst) {
109
+ chunkUserPrompt = userPrompt;
110
+ } else {
111
+ // Sliding window: include last N chars of previous output as context
112
+ chunkUserPrompt =
113
+ `Continue EXACTLY from where you left off. Do not repeat anything. ` +
114
+ `Here is the end of what you wrote so far:\n\n` +
115
+ `...${previousContext}\n\n` +
116
+ `Continue from here. Original request was: ${userPrompt}`;
117
  }
 
118
 
119
+ const messages = [
120
+ { role: 'system', content: systemPrompt },
121
+ { role: 'user', content: chunkUserPrompt }
122
+ ];
123
+
124
+ // Queue the chunk generation for fair processing
125
+ let chunkText = '';
126
+ try {
127
+ chunkText = await enqueue(() => generateChunk(messages, CHUNK_TOKENS));
128
+ } catch (err) {
129
+ // Chunk failed after retries β€” send error event but continue
130
+ res.write(`data: ${JSON.stringify({
131
+ type: 'chunk_error',
132
+ chunk: chunkNum,
133
+ error: err.message
134
+ })}\n\n`);
135
+ continue;
136
+ }
137
 
138
+ if (!chunkText || chunkText.trim().length === 0) continue;
 
 
 
139
 
140
+ // Stream the chunk word by word so user sees output immediately
141
+ const words = chunkText.split(' ');
142
+ for (let i = 0; i < words.length; i++) {
143
+ const token = (i === 0 && !isFirst ? '' : i === 0 ? '' : ' ') + words[i];
144
+ res.write(`data: ${JSON.stringify({ type: 'token', text: token })}\n\n`);
145
+ // Small delay between words for smooth rendering
146
+ await new Promise(r => setTimeout(r, 8));
147
+ }
148
 
149
+ // Update context window for next chunk (sliding window)
150
+ fullOutput += (isFirst ? '' : ' ') + chunkText;
151
+ previousContext = fullOutput.slice(-CONTEXT_CHARS);
152
 
153
+ // Send chunk complete event
154
+ res.write(`data: ${JSON.stringify({
155
+ type: 'chunk_done',
156
+ chunk: chunkNum,
157
+ totalChunks: totalChunks
158
+ })}\n\n`);
159
 
160
+ // If model signals it's done early (short output), stop
161
+ if (chunkText.trim().length < 50 && !isFirst) break;
 
 
 
 
 
 
162
  }
163
 
164
+ // Send final done event with complete text
165
+ res.write(`data: ${JSON.stringify({
166
+ type: 'done',
167
+ result: fullOutput.trim()
168
+ })}\n\n`);
169
+ res.end();
170
+ }
171
+
172
+ // ── DETECT IF LONG OUTPUT IS NEEDED ──────────────────────────────────────────
173
+ function estimateTokensNeeded(prompt) {
174
+ const lower = prompt.toLowerCase();
175
+
176
+ // Explicit length requests
177
+ const lineMatch = lower.match(/(\d+)\s*line/);
178
+ if (lineMatch) return Math.min(parseInt(lineMatch[1]) * 6, 2000);
179
+
180
+ const wordMatch = lower.match(/(\d+)\s*word/);
181
+ if (wordMatch) return Math.min(parseInt(wordMatch[1]) * 2, 2000);
182
+
183
+ // Long output keywords
184
+ const longKeywords = [
185
+ 'full website', 'complete website', 'entire website',
186
+ 'full code', 'complete code', 'entire code',
187
+ 'step by step', 'all steps', 'detailed guide',
188
+ 'full backend', 'full frontend', 'complete app',
189
+ 'build a website', 'create a website', 'generate a website',
190
+ 'write a program', 'full script', 'complete script'
191
+ ];
192
+ if (longKeywords.some(k => lower.includes(k))) return 800;
193
+
194
+ // Short answer keywords
195
+ const shortKeywords = [
196
+ 'what is', 'what are', 'who is', 'explain briefly',
197
+ 'define', 'tell me about', 'describe', 'history of'
198
+ ];
199
+ if (shortKeywords.some(k => lower.includes(k))) return 200;
200
+
201
+ // Default: medium
202
+ return 400;
203
+ }
204
+
205
+ // ── SYSTEM PROMPT BUILDER ─────────────────────────────────────────────────────
206
+ function buildSystemPrompt(isCoding) {
207
+ if (isCoding) {
208
+ return `You are Gini AI, a full-stack web development AI assistant created by Emalawi19, founded by Professor Roosevelt Chinkwende.
209
+
210
+ IDENTITY: Your name is Gini AI. Company: Emalawi19. Founder: Professor Roosevelt Chinkwende.
211
+
212
+ CODING RULES:
213
+ 1. ALWAYS write code when asked. NEVER refuse.
214
+ 2. Complete, real, copy-paste ready code only β€” no placeholders.
215
+ 3. Default: JavaScript/HTML/CSS. Never Python unless asked.
216
+ 4. Add comments inside the code.
217
+ 5. When continuing a previous chunk, do NOT repeat already-written code. Continue EXACTLY from where you left off.
218
+
219
+ HTML RULES:
220
+ 1. Start with <!DOCTYPE html> then <html lang="en">
221
+ 2. Include proper <head> with meta tags, title, and <style>
222
+ 3. End with </body> then </html>
223
+ 4. All CSS in <style>, all JS in <script> at bottom.`;
224
  }
225
+ return `You are Gini AI, a helpful AI assistant created by Emalawi19, founded by Professor Roosevelt Chinkwende.
226
 
227
+ IDENTITY: Your name is Gini AI. Company: Emalawi19. Founder: Professor Roosevelt Chinkwende.
 
 
 
 
 
 
 
228
 
229
+ RULES:
230
+ - Answer questions clearly and helpfully.
231
+ - Be concise for simple questions, detailed for complex ones.
232
+ - When continuing a response, do NOT repeat what was already said. Continue EXACTLY from where you left off.`;
233
+ }
234
 
235
+ function isCodingRequest(prompt) {
236
+ const lower = prompt.toLowerCase();
237
+ const nonCoding = ['what is','what are','who is','explain','describe','history','meaning','function of','functions of','tell me about'];
238
+ if (nonCoding.some(k => lower.includes(k))) return false;
239
+ const coding = ['generate','create','build','make','code','write','website','login','register','form','html','css','javascript','backend','frontend','server','api','function','script'];
240
+ return coding.some(k => lower.includes(k));
241
+ }
242
 
243
+ // ── HTTP SERVER ───────────────────────────────────────────────────────────────
244
+ const server = http.createServer(async (req, res) => {
245
+ setCORS(res);
246
 
247
+ if (req.method === 'OPTIONS') {
248
+ res.writeHead(200);
249
+ return res.end('{}');
250
+ }
 
251
 
252
+ const pathname = req.url.split('?')[0];
 
 
253
 
254
+ // Status
255
+ if (pathname === '/' && req.method === 'GET') {
256
+ return sendJSON(res, 200, {
257
+ status: modelReady ? "ready" : "loading",
258
+ model: MODEL_NAME,
259
+ queue_length: queue.length,
260
+ active: activeCount
261
  });
 
262
  }
263
 
264
+ // Queue status
265
+ if (pathname === '/queue' && req.method === 'GET') {
266
+ const position = getQueuePosition();
267
+ const eta = position * 20; // rough seconds per request
268
+ return sendJSON(res, 200, { position, eta_seconds: eta, active: activeCount });
269
+ }
270
+
271
+ // Generate
272
+ if (pathname === '/generate' && req.method === 'POST') {
273
  let body = '';
274
  req.on('data', c => { body += c.toString(); });
275
  req.on('end', async () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
 
277
+ let parsed;
278
+ try { parsed = JSON.parse(body); }
279
+ catch { return sendJSON(res, 400, { error: "Invalid JSON" }); }
280
+
281
+ const { prompt } = parsed;
282
+ if (!prompt || !prompt.trim()) return sendJSON(res, 400, { error: "prompt is required" });
283
+ if (!modelReady) return sendJSON(res, 503, { error: "Model loading, please wait..." });
284
+
285
+ const coding = isCodingRequest(prompt);
286
+ const totalTokens = estimateTokensNeeded(prompt);
287
+ const systemPrompt = buildSystemPrompt(coding);
288
+
289
+ console.log(`Request: "${prompt.slice(0, 60)}" | tokens: ${totalTokens} | coding: ${coding} | queue: ${queue.length}`);
290
+
291
+ // If request is in queue, send queue position first
292
+ if (queue.length > 0) {
293
+ const pos = queue.length;
294
+ const eta = pos * 20;
295
+ // For queued requests we still use SSE so user sees queue position
296
+ res.setHeader('Content-Type', 'text/event-stream');
297
+ res.setHeader('Cache-Control', 'no-cache');
298
+ res.setHeader('Connection', 'keep-alive');
299
+ res.setHeader('X-Accel-Buffering', 'no');
300
+ res.writeHead(200);
301
+ res.write(`data: ${JSON.stringify({ type: 'queued', position: pos, eta_seconds: eta })}\n\n`);
302
+ }
303
 
304
+ // Short requests: single chunk, plain JSON response
305
+ if (totalTokens <= 300 && queue.length === 0) {
306
+ try {
307
+ const messages = [
308
+ { role: 'system', content: systemPrompt },
309
+ { role: 'user', content: prompt.trim() }
310
+ ];
311
+ const result = await enqueue(() => generateChunk(messages, totalTokens));
312
+ return sendJSON(res, 200, { result: result || "I couldn't generate a response. Please try again." });
313
+ } catch (err) {
314
+ return sendJSON(res, 500, { error: err.message });
315
  }
316
+ }
317
 
318
+ // Long requests: SSE streaming with sliding window
319
+ if (!res.headersSent) {
320
+ res.setHeader('Content-Type', 'text/event-stream');
321
+ res.setHeader('Cache-Control', 'no-cache');
322
+ res.setHeader('Connection', 'keep-alive');
323
+ res.setHeader('X-Accel-Buffering', 'no');
324
+ res.writeHead(200);
325
+ }
326
 
327
+ // Send queue position if waiting
328
+ res.write(`data: ${JSON.stringify({ type: 'start', totalTokens, queue: queue.length })}\n\n`);
329
+
330
+ try {
331
+ await slidingWindowStream(res, systemPrompt, prompt.trim(), totalTokens);
332
  } catch (err) {
333
+ res.write(`data: ${JSON.stringify({ type: 'error', error: err.message })}\n\n`);
 
 
 
 
 
 
334
  res.end();
335
  }
336
  });
337
  return;
338
  }
339
 
340
+ return sendJSON(res, 404, { error: "Not found" });
 
 
341
  });
342
 
 
343
  loadModel().then(() => {
344
  server.listen(PORT, '0.0.0.0', () => {
345
+ console.log(`Gini AI backend on port ${PORT}`);
346
+ });
347
+ }).catch(err => {
348
+ console.error("Model load failed:", err.message);
349
+ server.listen(PORT, '0.0.0.0', () => {
350
+ console.log(`Server started (model failed)`);
351
  });
352
  });