Emalawi19 commited on
Commit
e48e33b
Β·
verified Β·
1 Parent(s): 24cf705

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +85 -225
server.js CHANGED
@@ -1,291 +1,150 @@
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
-
8
- async function loadModel() {
9
- console.log("Loading coding model...");
10
- generator = await pipeline('text-generation', MODEL_NAME, { dtype: 'q4' });
11
- console.log("Model loaded successfully!");
12
- }
13
-
14
- // ── WIKIPEDIA ────────────────────────────────────────────────────────────────
15
- async function searchWikipedia(query) {
16
- try {
17
- const url = `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(query)}&srlimit=1&format=json&origin=*`;
18
- const res = await fetch(url);
19
- const data = await res.json();
20
- const results = data?.query?.search;
21
- if (!results || results.length === 0) return null;
22
- return results[0].title;
23
- } catch (e) {
24
- console.error("Wikipedia search error:", e.message);
25
- return null;
26
- }
27
- }
28
-
29
- async function fetchWikipediaSummary(title) {
30
- try {
31
- const url = `https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(title)}`;
32
- const res = await fetch(url);
33
- const data = await res.json();
34
- if (data.extract) return data.extract.slice(0, 800);
35
- return null;
36
- } catch (e) {
37
- console.error("Wikipedia fetch error:", e.message);
38
- return null;
39
  }
 
40
  }
41
 
42
- async function getWikipediaContext(prompt) {
43
- try {
44
- const title = await searchWikipedia(prompt);
45
- if (!title) return null;
46
- const summary = await fetchWikipediaSummary(title);
47
- if (!summary) return null;
48
- console.log(`Wikipedia context fetched: "${title}"`);
49
- return { title, summary };
50
- } catch (e) {
51
- console.error("Wikipedia context error:", e.message);
52
- return null;
53
  }
 
54
  }
55
 
56
- // ── CODING DETECTION ─────────────────────────────────────────────────────────
57
- const CODING_PHRASES = [
58
- 'generate code', 'write code', 'create code',
59
- 'generate a website', 'build a website', 'create a website', 'make a website',
60
- 'generate a webpage', 'build a webpage',
61
- 'generate a login', 'create a login', 'build a login',
62
- 'generate a form', 'create a form', 'build a form',
63
- 'generate a page', 'create a page', 'build a page',
64
- 'generate an app', 'create an app', 'build an app',
65
- 'write a function', 'write a script', 'write a program',
66
- 'create a function', 'create a script',
67
- 'build an api', 'create an api', 'generate an api',
68
- 'node.js', 'express.js', 'express server',
69
- 'show me the code', 'give me the code', 'write html',
70
- 'write css', 'write javascript', 'write js',
71
- 'create a backend', 'build a backend',
72
- 'create a frontend', 'build a frontend',
73
- 'create a server', 'build a server',
74
- 'how do i code', 'how to code', 'code for',
75
- 'sample code', 'example code', '```'
76
- ];
77
-
78
- const NON_CODING_CONTEXTS = [
79
- 'function of', 'functions of', 'what is the function',
80
- 'what are the functions', 'purpose of', 'role of',
81
- 'explain', 'what is', 'what are', 'tell me about',
82
- 'history of', 'meaning of', 'definition of',
83
- 'describe', 'how does', 'why is', 'benefits of',
84
- 'importance of', 'effects of', 'causes of',
85
- 'who is', 'who was', 'when did', 'when was',
86
- 'where is', 'where was', 'which is', 'which was'
87
- ];
88
-
89
- function isCodingRequest(prompt) {
90
- const lower = prompt.toLowerCase().trim();
91
- if (NON_CODING_CONTEXTS.some(ctx => lower.includes(ctx))) return false;
92
- return CODING_PHRASES.some(phrase => lower.includes(phrase));
93
- }
94
-
95
- function isWebsiteRequest(prompt) {
96
- const lower = prompt.toLowerCase();
97
- return ['website', 'webpage', 'web page', 'html page', 'login page',
98
- 'register page', 'landing page', 'form page', 'signup page',
99
- 'homepage', 'home page', 'portfolio', 'dashboard'].some(w => lower.includes(w));
100
  }
101
 
102
- function cleanHTML(html) {
103
- if (!html.trimStart().toLowerCase().startsWith('<!doctype')) {
104
- if (html.trimStart().toLowerCase().startsWith('<html')) {
105
- html = '<!DOCTYPE html>\n' + html.trimStart();
106
- } else {
107
- html = '<!DOCTYPE html>\n<html lang="en">\n' + html.trimStart();
108
- }
109
- }
110
- if (!html.trimEnd().toLowerCase().endsWith('</html>')) {
111
- if (html.toLowerCase().includes('</body>')) {
112
- html = html.replace(/<\/body>\s*$/i, '</body>\n</html>');
113
- } else {
114
- html = html.trimEnd() + '\n</body>\n</html>';
115
- }
116
- }
117
- return html.trim();
118
- }
119
-
120
- function enforceHTMLStructure(text) {
121
- const codeBlockMatch = text.match(/```html\s*([\s\S]*?)```/i);
122
- if (codeBlockMatch) {
123
- return '```html\n' + cleanHTML(codeBlockMatch[1].trim()) + '\n```';
124
- }
125
- const htmlStart = text.search(/<(!DOCTYPE|html)/i);
126
- if (htmlStart !== -1) {
127
- let html = text.slice(htmlStart);
128
- const htmlEnd = html.search(/<\/html>/i);
129
- if (htmlEnd !== -1) html = html.slice(0, htmlEnd + 7);
130
- return '```html\n' + cleanHTML(html) + '\n```';
131
- }
132
- return text;
133
  }
134
 
135
- // ── CORE GENERATION (non-streaming, reliable) ─────────────────────────────────
136
- async function generateResponse(messages, coding) {
137
  const output = await generator(messages, {
138
- max_new_tokens: coding ? 800 : 400,
139
- temperature: coding ? 0.2 : 0.5,
140
  repetition_penalty: 1.2,
141
- do_sample: !coding
142
  });
143
  return output[0].generated_text.at(-1).content || '';
144
  }
145
 
146
- // ── HTTP SERVER ───────────────────────────────────────────────────────────────
147
  const server = http.createServer(async (req, res) => {
148
  res.setHeader('Access-Control-Allow-Origin', '*');
149
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
150
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
151
 
152
- if (req.method === 'OPTIONS') {
153
- res.writeHead(200);
154
- return res.end();
155
- }
156
 
157
  const pathname = req.url.split('?')[0];
158
 
159
  if (pathname === '/' && req.method === 'GET') {
160
  res.setHeader('Content-Type', 'application/json');
161
  res.writeHead(200);
162
- res.end(JSON.stringify({ status: "Backend is running", model: MODEL_NAME }));
163
- return;
 
 
 
 
 
 
164
  }
165
 
166
  if (pathname === '/generate' && req.method === 'POST') {
167
  let body = '';
168
- req.on('data', chunk => { body += chunk.toString(); });
169
  req.on('end', async () => {
170
  try {
171
- const { prompt } = JSON.parse(body);
172
-
173
  if (!generator) {
174
  res.setHeader('Content-Type', 'application/json');
175
  res.writeHead(503);
176
- return res.end(JSON.stringify({ error: "Model is still loading..." }));
177
- }
178
-
179
- const coding = isCodingRequest(prompt);
180
- const websiteReq = isWebsiteRequest(prompt);
181
-
182
- // ── Fetch Wikipedia for non-coding questions ──────────────────
183
- let wikipediaContext = '';
184
- let wikiTitle = null;
185
- if (!coding) {
186
- const wiki = await getWikipediaContext(prompt);
187
- if (wiki) {
188
- wikiTitle = wiki.title;
189
- wikipediaContext = `\n\nWIKIPEDIA REFERENCE (article: "${wiki.title}"):\n${wiki.summary}\n\nIMPORTANT: Use this Wikipedia data to answer accurately. Rewrite it in your own words and add helpful context.`;
190
- }
191
  }
192
 
193
- const finalPrompt = coding
194
- ? `You are a coding assistant. Write complete, working code. Never refuse: ${prompt}`
195
- : prompt;
196
-
197
- const systemPrompt = coding
198
- ? `You are Gini AI, a full-stack web development AI assistant created by Emalawi19, founded by Professor Roosevelt Chinkwende.
199
-
200
- IDENTITY: Your name is Gini AI. Company: Emalawi19. Founder: Professor Roosevelt Chinkwende.
201
-
202
- CODING RULES:
203
- 1. ALWAYS write code when asked. NEVER refuse.
204
- 2. Complete, real, copy-paste ready code only β€” no placeholders.
205
- 3. Default: JavaScript/HTML/CSS. Never Python unless asked.
206
- 4. Add comments inside the code.
207
-
208
- HTML RULES:
209
- 1. Start with <!DOCTYPE html> then <html lang="en">
210
- 2. Include proper <head> with meta tags, title, and <style>
211
- 3. End with </body> then </html>
212
- 4. All CSS in <style>, all JS in <script> at bottom of body.
213
- 5. Wrap in \`\`\`html code block. Never cut off mid-code.
214
 
215
- STEP-BY-STEP: List all steps first, then one step at a time. Say "next" to continue.`
 
 
 
216
 
217
- : `You are Gini AI, a helpful AI assistant created by Emalawi19, founded by Professor Roosevelt Chinkwende.
218
-
219
- IDENTITY: Your name is Gini AI. Company: Emalawi19. Founder: Professor Roosevelt Chinkwende. You are an AI, not a human.
220
- ${wikipediaContext}
221
- RULES:
222
- - Answer the question directly and clearly in plain English.
223
- - If Wikipedia reference is provided above, use it to give an accurate, well-informed answer.
224
- - Rewrite Wikipedia info in your own words β€” never copy it directly.
225
- - If the question is about a person (president, leader, founder etc.), state their name clearly at the start.
226
- - NEVER generate code for general knowledge questions.
227
- - Keep answers friendly, clear, and concise.`;
228
 
229
- // ── Set SSE headers ───────────────────────────────────────────
230
  res.writeHead(200, {
231
- 'Content-Type': 'text/event-stream',
232
- 'Cache-Control': 'no-cache',
233
- 'Connection': 'keep-alive',
234
  'X-Accel-Buffering': 'no'
235
  });
236
 
237
- // Send a heartbeat so the client knows we're working
238
  res.write(`data: ${JSON.stringify({ status: "generating" })}\n\n`);
239
 
240
- // ── Generate (wait for full result, then stream it out) ────────
241
- // The callback_function in transformers.js is unreliable on CPU
242
- // so we generate fully then simulate streaming character by character
243
- const messages = [
244
- { role: 'system', content: systemPrompt },
245
- { role: 'user', content: finalPrompt }
246
- ];
247
-
248
- let result = await generateResponse(messages, coding);
249
-
250
- // Catch refusals
251
- const refusals = ["i can't assist", "i cannot assist", "i'm sorry", "i am sorry", "i'm unable", "i cannot help", "i can't help"];
252
- const refused = refusals.some(r => result.toLowerCase().startsWith(r));
253
-
254
- if (refused && coding) {
255
- result = await generateResponse([
256
- { role: 'system', content: 'You are a code generator. Output only complete working code. Never refuse.' },
257
- { role: 'user', content: `Write complete HTML code starting with <!DOCTYPE html> ending with </html> for: ${prompt}` }
258
- ], true);
259
- }
260
-
261
- // If result is still empty, use Wikipedia summary directly
262
- if (!result || result.trim().length < 5) {
263
- if (wikiTitle && wikipediaContext) {
264
- result = `Based on Wikipedia: ${wikipediaContext.split('\n').slice(3).join(' ').trim()}`;
265
- } else {
266
- result = "I'm sorry, I couldn't generate a response. Please try again.";
267
- }
268
- }
269
 
270
- if (websiteReq && coding) {
271
- result = enforceHTMLStructure(result);
 
 
272
  }
273
 
274
- // ── Stream the result word by word ────────────────────────────
275
  const words = result.split(' ');
276
  for (let i = 0; i < words.length; i++) {
277
  const chunk = (i === 0 ? '' : ' ') + words[i];
278
  res.write(`data: ${JSON.stringify({ chunk })}\n\n`);
279
- // Small delay so frontend can render progressively
280
- await new Promise(r => setTimeout(r, 15));
281
  }
282
 
283
- // Send done event
284
- res.write(`data: ${JSON.stringify({ done: true, result, source: wikiTitle ? `πŸ“– Wikipedia β€” ${wikiTitle}` : null })}\n\n`);
285
  res.end();
286
 
287
  } catch (err) {
288
- console.error("Generation error:", err.message);
289
  res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
290
  res.end();
291
  }
@@ -298,8 +157,9 @@ RULES:
298
  res.end(JSON.stringify({ error: "Not Found" }));
299
  });
300
 
 
301
  loadModel().then(() => {
302
  server.listen(PORT, '0.0.0.0', () => {
303
- console.log(`Server running at http://0.0.0.0:${PORT}`);
304
  });
305
  });
 
1
  import { pipeline } 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
+ // ── LOAD KNOWLEDGE FILES ──────────────────────────────────────────────────────
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, 600, 60);
20
+ chunks.forEach(c => knowledgeBase.push({ source: file, text: c }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  }
22
+ console.log(`Knowledge: ${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 = 4) {
36
+ if (knowledgeBase.length === 0) return '';
37
+ const words = prompt.toLowerCase().split(/\W+/).filter(w => w.length > 2);
38
+ const scored = knowledgeBase.map(chunk => ({
39
+ ...chunk,
40
+ score: words.reduce((acc, w) => acc + (chunk.text.toLowerCase().includes(w) ? 1 : 0), 0)
41
+ }));
42
+ return scored
43
+ .filter(c => c.score > 0)
44
+ .sort((a, b) => b.score - a.score)
45
+ .slice(0, topK)
46
+ .map(c => `[${c.source}]\n${c.text}`)
47
+ .join('\n\n---\n\n');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  }
49
 
50
+ // ── LOAD MODEL ────────────────────────────────────────────────────────────────
51
+ async function loadModel() {
52
+ console.log("Loading model...");
53
+ generator = await pipeline('text-generation', MODEL_NAME, { dtype: 'q4' });
54
+ console.log("Model ready!");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  }
56
 
57
+ async function generateResponse(messages) {
 
58
  const output = await generator(messages, {
59
+ max_new_tokens: 500,
60
+ temperature: 0.3,
61
  repetition_penalty: 1.2,
62
+ do_sample: false
63
  });
64
  return output[0].generated_text.at(-1).content || '';
65
  }
66
 
67
+ // ── SERVER ────────────────────────────────────────────────────────────────────
68
  const server = http.createServer(async (req, res) => {
69
  res.setHeader('Access-Control-Allow-Origin', '*');
70
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
71
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
72
 
73
+ if (req.method === 'OPTIONS') { res.writeHead(200); return res.end(); }
 
 
 
74
 
75
  const pathname = req.url.split('?')[0];
76
 
77
  if (pathname === '/' && req.method === 'GET') {
78
  res.setHeader('Content-Type', 'application/json');
79
  res.writeHead(200);
80
+ return res.end(JSON.stringify({ status: "running", model: MODEL_NAME, knowledge_chunks: knowledgeBase.length }));
81
+ }
82
+
83
+ if (pathname === '/reload-knowledge' && req.method === 'POST') {
84
+ loadKnowledge();
85
+ res.setHeader('Content-Type', 'application/json');
86
+ res.writeHead(200);
87
+ return res.end(JSON.stringify({ message: `Reloaded: ${knowledgeBase.length} chunks` }));
88
  }
89
 
90
  if (pathname === '/generate' && req.method === 'POST') {
91
  let body = '';
92
+ req.on('data', c => { body += c.toString(); });
93
  req.on('end', async () => {
94
  try {
95
+ const { prompt, system } = JSON.parse(body);
 
96
  if (!generator) {
97
  res.setHeader('Content-Type', 'application/json');
98
  res.writeHead(503);
99
+ return res.end(JSON.stringify({ error: "Model loading..." }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  }
101
 
102
+ // Retrieve from knowledge files
103
+ const ragContext = retrieveContext(prompt, 4);
104
+ const ragSection = ragContext
105
+ ? `\n\nKNOWLEDGE BASE (use this for your answer):\n${ragContext}\n`
106
+ : '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
+ // Use system prompt from frontend if provided, else default
109
+ const finalSystem = system
110
+ ? system + ragSection
111
+ : `You are Mlimi Connect AI, a free agricultural advisor for Malawian farmers. Only answer agriculture questions.${ragSection}`;
112
 
113
+ const messages = [
114
+ { role: 'system', content: finalSystem },
115
+ { role: 'user', content: prompt }
116
+ ];
 
 
 
 
 
 
 
117
 
118
+ // SSE streaming
119
  res.writeHead(200, {
120
+ 'Content-Type': 'text/event-stream',
121
+ 'Cache-Control': 'no-cache',
122
+ 'Connection': 'keep-alive',
123
  'X-Accel-Buffering': 'no'
124
  });
125
 
 
126
  res.write(`data: ${JSON.stringify({ status: "generating" })}\n\n`);
127
 
128
+ let result = await generateResponse(messages);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
+ if (!result || result.trim().length < 3) {
131
+ result = ragContext
132
+ ? `Based on available knowledge:\n\n${ragContext.slice(0, 400)}`
133
+ : "I don't have specific information on that. Please consult your local agricultural extension officer.";
134
  }
135
 
136
+ // Stream word by word
137
  const words = result.split(' ');
138
  for (let i = 0; i < words.length; i++) {
139
  const chunk = (i === 0 ? '' : ' ') + words[i];
140
  res.write(`data: ${JSON.stringify({ chunk })}\n\n`);
141
+ await new Promise(r => setTimeout(r, 12));
 
142
  }
143
 
144
+ res.write(`data: ${JSON.stringify({ done: true, result })}\n\n`);
 
145
  res.end();
146
 
147
  } catch (err) {
 
148
  res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
149
  res.end();
150
  }
 
157
  res.end(JSON.stringify({ error: "Not Found" }));
158
  });
159
 
160
+ loadKnowledge();
161
  loadModel().then(() => {
162
  server.listen(PORT, '0.0.0.0', () => {
163
+ console.log(`Mlimi Connect backend running on port ${PORT}`);
164
  });
165
  });