Emalawi19 commited on
Commit
f45e523
Β·
verified Β·
1 Parent(s): 8504fa4

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +77 -54
server.js CHANGED
@@ -3,20 +3,20 @@ 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 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 loaded: ${files.length} files, ${knowledgeBase.length} chunks`);
@@ -24,7 +24,7 @@ function loadKnowledge() {
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;
@@ -32,19 +32,32 @@ function splitChunks(text, size, overlap) {
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
  // ── MODEL ─────────────────────────────────────────────────────────────────────
@@ -56,22 +69,19 @@ async function loadModel() {
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
- // Extract only the assistant reply content
65
  const generated = output[0].generated_text;
66
- if (Array.isArray(generated)) {
67
- return generated.at(-1)?.content || '';
68
- }
69
  return String(generated || '');
70
  }
71
 
72
  // ── SERVER ────────────────────────────────────────────────────────────────────
73
  const server = http.createServer(async (req, res) => {
74
- res.setHeader('Access-Control-Allow-Origin', '*');
75
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
76
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
77
 
@@ -79,18 +89,16 @@ const server = http.createServer(async (req, res) => {
79
 
80
  const pathname = req.url.split('?')[0];
81
 
82
- // Status check
83
  if (pathname === '/' && req.method === 'GET') {
84
  res.setHeader('Content-Type', 'application/json');
85
  res.writeHead(200);
86
  return res.end(JSON.stringify({
87
  status: "running",
88
- model: MODEL_NAME,
89
  knowledge_chunks: knowledgeBase.length
90
  }));
91
  }
92
 
93
- // Reload knowledge
94
  if (pathname === '/reload-knowledge' && req.method === 'POST') {
95
  loadKnowledge();
96
  res.setHeader('Content-Type', 'application/json');
@@ -98,57 +106,72 @@ const server = http.createServer(async (req, res) => {
98
  return res.end(JSON.stringify({ message: `Reloaded: ${knowledgeBase.length} chunks` }));
99
  }
100
 
101
- // Main generate endpoint β€” returns plain JSON (no SSE, no streaming delays)
102
  if (pathname === '/generate' && req.method === 'POST') {
103
  let body = '';
104
  req.on('data', c => { body += c.toString(); });
105
  req.on('end', async () => {
106
  res.setHeader('Content-Type', 'application/json');
107
-
108
  try {
109
- const { prompt, system } = JSON.parse(body);
110
 
111
  if (!generator) {
112
  res.writeHead(503);
113
- return res.end(JSON.stringify({ error: "Model still loading, please wait..." }));
114
  }
115
 
116
- // RAG: retrieve relevant knowledge chunks
117
- const ragContext = retrieveContext(prompt, 4);
118
- const ragSection = ragContext
119
- ? `\n\nKNOWLEDGE BASE β€” use ONLY this information to answer:\n${ragContext}\n`
120
- : '';
121
 
122
- // Build final system prompt
123
- const finalSystem = (system || `You are Mlimi Connect AI, a free agricultural advisor for Malawian farmers. Only answer agriculture questions.`) + ragSection;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
  const messages = [
126
- { role: 'system', content: finalSystem },
127
- { role: 'user', content: prompt }
128
  ];
129
 
130
- console.log(`Generating response for: "${prompt.slice(0, 60)}..."`);
131
-
132
  let result = await generateResponse(messages);
133
 
134
- // Fallback if model returns empty
135
- if (!result || result.trim().length < 5) {
136
- if (ragContext) {
137
- result = `Here is what I know about this topic:\n\n${ragContext.slice(0, 600)}`;
138
- } else {
139
- result = "I don't have specific information on that topic. Please consult your local agricultural extension officer (AEO) for advice.";
140
- }
141
  }
142
 
143
- console.log(`Response ready: ${result.length} chars`);
144
-
145
  res.writeHead(200);
146
  res.end(JSON.stringify({ result }));
147
 
148
  } catch (err) {
149
- console.error("Generation error:", err.message);
150
  res.writeHead(500);
151
- res.end(JSON.stringify({ error: err.message || "Generation failed" }));
152
  }
153
  });
154
  return;
@@ -162,6 +185,6 @@ const server = http.createServer(async (req, res) => {
162
  loadKnowledge();
163
  loadModel().then(() => {
164
  server.listen(PORT, '0.0.0.0', () => {
165
- console.log(`Mlimi Connect backend running on port ${PORT}`);
166
  });
167
  });
 
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`);
 
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;
 
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
+ // Count every occurrence not just presence β€” better scoring
47
+ const matches = (lower.match(new RegExp(w, 'g')) || []).length;
48
+ score += matches;
49
+ }
50
+ return { ...chunk, score };
51
+ });
52
+
53
+ const top = scored
54
  .filter(c => c.score > 0)
55
  .sort((a, b) => b.score - a.score)
56
+ .slice(0, topK);
57
+
58
+ if (top.length === 0) return '';
59
+
60
+ return top.map(c => c.text).join('\n\n---\n\n');
61
  }
62
 
63
  // ── MODEL ─────────────────────────────────────────────────────────────────────
 
69
 
70
  async function generateResponse(messages) {
71
  const output = await generator(messages, {
72
+ max_new_tokens: 600,
73
+ temperature: 0.2,
74
+ repetition_penalty: 1.15,
75
  do_sample: false
76
  });
 
77
  const generated = output[0].generated_text;
78
+ if (Array.isArray(generated)) return generated.at(-1)?.content || '';
 
 
79
  return String(generated || '');
80
  }
81
 
82
  // ── SERVER ────────────────────────────────────────────────────────────────────
83
  const server = http.createServer(async (req, res) => {
84
+ res.setHeader('Access-Control-Allow-Origin', '*');
85
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
86
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
87
 
 
89
 
90
  const pathname = req.url.split('?')[0];
91
 
 
92
  if (pathname === '/' && req.method === 'GET') {
93
  res.setHeader('Content-Type', 'application/json');
94
  res.writeHead(200);
95
  return res.end(JSON.stringify({
96
  status: "running",
97
+ model: MODEL_NAME,
98
  knowledge_chunks: knowledgeBase.length
99
  }));
100
  }
101
 
 
102
  if (pathname === '/reload-knowledge' && req.method === 'POST') {
103
  loadKnowledge();
104
  res.setHeader('Content-Type', 'application/json');
 
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(); });
112
  req.on('end', async () => {
113
  res.setHeader('Content-Type', 'application/json');
 
114
  try {
115
+ const { prompt } = JSON.parse(body);
116
 
117
  if (!generator) {
118
  res.writeHead(503);
119
+ return res.end(JSON.stringify({ error: "Model still loading..." }));
120
  }
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)}`
164
+ : "I don't have specific information on that topic. Please ask your local agricultural extension officer.";
 
 
165
  }
166
 
167
+ console.log(`Response: ${result.slice(0, 80)}...`);
 
168
  res.writeHead(200);
169
  res.end(JSON.stringify({ result }));
170
 
171
  } catch (err) {
172
+ console.error("Error:", err.message);
173
  res.writeHead(500);
174
+ res.end(JSON.stringify({ error: err.message }));
175
  }
176
  });
177
  return;
 
185
  loadKnowledge();
186
  loadModel().then(() => {
187
  server.listen(PORT, '0.0.0.0', () => {
188
+ console.log(`Mlimi Connect backend on port ${PORT}`);
189
  });
190
  });