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

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +120 -37
server.js CHANGED
@@ -1,4 +1,4 @@
1
- import { pipeline } from '@huggingface/transformers';
2
  import http from 'http';
3
  import fs from 'fs';
4
  import path from 'path';
@@ -43,7 +43,6 @@ function retrieveContext(prompt, topK = 5) {
43
  const lower = chunk.text.toLowerCase();
44
  let score = 0;
45
  for (const w of words) {
46
- // Count every occurrence not just presence β€” better scoring
47
  const matches = (lower.match(new RegExp(w, 'g')) || []).length;
48
  score += matches;
49
  }
@@ -60,6 +59,39 @@ function retrieveContext(prompt, topK = 5) {
60
  return top.map(c => c.text).join('\n\n---\n\n');
61
  }
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  // ── MODEL ─────────────────────────────────────────────────────────────────────
64
  async function loadModel() {
65
  console.log("Loading model...");
@@ -67,6 +99,7 @@ async function loadModel() {
67
  console.log("Model ready!");
68
  }
69
 
 
70
  async function generateResponse(messages) {
71
  const output = await generator(messages, {
72
  max_new_tokens: 600,
@@ -79,6 +112,31 @@ async function generateResponse(messages) {
79
  return String(generated || '');
80
  }
81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  // ── SERVER ────────────────────────────────────────────────────────────────────
83
  const server = http.createServer(async (req, res) => {
84
  res.setHeader('Access-Control-Allow-Origin', '*');
@@ -106,6 +164,7 @@ const server = http.createServer(async (req, res) => {
106
  return res.end(JSON.stringify({ message: `Reloaded: ${knowledgeBase.length} chunks` }));
107
  }
108
 
 
109
  if (pathname === '/generate' && req.method === 'POST') {
110
  let body = '';
111
  req.on('data', c => { body += c.toString(); });
@@ -121,43 +180,9 @@ const server = http.createServer(async (req, res) => {
121
 
122
  console.log(`Query: "${prompt}"`);
123
 
124
- // ── RAG: find relevant knowledge chunks ───────────────────
125
- const ragContext = retrieveContext(prompt, 5);
126
- console.log(`RAG chunks found: ${ragContext.length} chars`);
127
-
128
- // ── Build system prompt with knowledge injected ────────────
129
- // IMPORTANT: knowledge comes FIRST, before any other instruction
130
- const systemPrompt = ragContext
131
- ? `You are Mlimi Connect AI, a free agricultural advisor for Malawian farmers.
132
-
133
- KNOWLEDGE BASE β€” THIS IS YOUR ONLY SOURCE OF INFORMATION. USE ONLY THIS:
134
- ===START OF KNOWLEDGE===
135
- ${ragContext}
136
- ===END OF KNOWLEDGE===
137
-
138
- STRICT RULES:
139
- 1. Answer ONLY using the knowledge provided above between ===START=== and ===END===.
140
- 2. Do NOT add information from outside the knowledge base.
141
- 3. Do NOT be vague. Give specific details: variety names, exact spacing, fertilizer amounts, timing.
142
- 4. Structure your answer clearly with numbered steps.
143
- 5. If the knowledge above does not contain the answer, say: "I don't have specific information on that in my knowledge base."
144
- 6. ONLY answer agriculture questions. For anything else say: "I can only help with farming questions."`
145
-
146
- : `You are Mlimi Connect AI, a free agricultural advisor for Malawian farmers.
147
-
148
- I don't have specific notes on that topic in my knowledge base yet.
149
- Give a brief, honest answer based on general Malawian agricultural knowledge.
150
- Keep it practical and specific to Malawi's conditions.
151
- ONLY answer agriculture questions.`;
152
-
153
- const messages = [
154
- { role: 'system', content: systemPrompt },
155
- { role: 'user', content: prompt }
156
- ];
157
-
158
  let result = await generateResponse(messages);
159
 
160
- // If still empty, return the raw knowledge chunk directly
161
  if (!result || result.trim().length < 10) {
162
  result = ragContext
163
  ? `Here is what my knowledge base says:\n\n${ragContext.slice(0, 800)}`
@@ -177,6 +202,64 @@ ONLY answer agriculture questions.`;
177
  return;
178
  }
179
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  res.setHeader('Content-Type', 'application/json');
181
  res.writeHead(404);
182
  res.end(JSON.stringify({ error: "Not Found" }));
 
1
+ import { pipeline, TextStreamer } from '@huggingface/transformers';
2
  import http from 'http';
3
  import fs from 'fs';
4
  import path from 'path';
 
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
  }
 
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...");
 
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,
 
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', '*');
 
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(); });
 
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)}`
 
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" }));