IZERE HIRWA Roger commited on
Commit
b84fe3f
·
1 Parent(s): d4d8805
Files changed (3) hide show
  1. app.py +1 -1
  2. chatbot/app.js +128 -61
  3. run_aimhsa.py +44 -8
app.py CHANGED
@@ -531,7 +531,7 @@ class RiskDetector:
531
  def __init__(self):
532
  # Risk indicators patterns
533
  self.critical_indicators = [
534
- r'\b(suicide|kill myself|end it all'
535
  ]
536
  self.high_risk_indicators = [
537
  r'\b(hopeless|worthless|burden|better off without)\b',
 
531
  def __init__(self):
532
  # Risk indicators patterns
533
  self.critical_indicators = [
534
+ r'\b(suicide|kill myself|end it all)\b',
535
  ]
536
  self.high_risk_indicators = [
537
  r'\b(hopeless|worthless|burden|better off without)\b',
chatbot/app.js CHANGED
@@ -169,13 +169,38 @@
169
  }
170
 
171
  async function api(path, opts) {
172
- const url = API_BASE_URL + path;
173
- const res = await fetch(url, opts);
174
- if (!res.ok) {
175
- const txt = await res.text();
176
- throw new Error(txt || res.statusText);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  }
178
- return res.json();
 
179
  }
180
 
181
  async function initSession(useAccount = false) {
@@ -227,6 +252,10 @@
227
  ensureScroll();
228
  } catch (err) {
229
  console.error("history load error", err);
 
 
 
 
230
  }
231
  }
232
 
@@ -270,11 +299,31 @@
270
 
271
  removeTypingIndicator();
272
 
273
- // Ensure we got a valid response
274
- if (!resp.answer || resp.answer.trim() === '') {
275
- appendMessage("assistant", "I'm here to help. Could you please rephrase your question?");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  } else {
277
- appendMessage("assistant", resp.answer);
 
 
 
 
 
278
  }
279
 
280
  // Risk assessment is handled in backend only (no display)
@@ -381,48 +430,64 @@
381
  disableComposer(true);
382
  showUploadPreview(file);
383
 
384
- const url = API_BASE_URL + "/upload_pdf";
385
- const xhr = new XMLHttpRequest();
386
- xhr.open("POST", url, true);
 
 
387
 
388
- xhr.upload.onprogress = function(e) {
389
- if (e.lengthComputable) {
390
- const pct = Math.round((e.loaded / e.total) * 100);
391
- updateUploadProgress(pct);
392
- }
393
- };
394
 
395
- xhr.onload = function() {
396
- disableComposer(false);
397
- try {
398
- const resText = xhr.responseText || "{}";
399
- const data = JSON.parse(resText);
400
- if (xhr.status >= 200 && xhr.status < 300) {
401
- convId = data.id;
402
- localStorage.setItem("aimhsa_conv", convId);
403
- appendMessage("bot", `Uploaded ${data.filename}. What would you like to know about this document?`);
404
- clearUploadPreview();
405
- if (account) updateHistoryList();
406
- } else {
407
- appendMessage("bot", "PDF upload failed: " + (data.error || xhr.statusText));
408
- }
409
- } catch (err) {
410
- appendMessage("bot", "Upload parsing error");
411
- }
412
- };
413
 
414
- xhr.onerror = function() {
415
- disableComposer(false);
416
- appendMessage("bot", "PDF upload error");
 
 
 
 
 
 
 
 
 
417
  };
418
 
419
- const fd = new FormData();
420
- fd.append("file", file, file.name);
421
- if (convId) fd.append("id", convId);
422
- if (account) fd.append("account", account);
423
- const model = getSelectedModel();
424
- if (model) fd.append("model", model);
425
- xhr.send(fd);
 
 
 
 
 
 
 
 
 
 
426
  }
427
 
428
  function disableComposer(disabled) {
@@ -500,18 +565,20 @@
500
 
501
  appendMessage("bot", "Chat history cleared. How can I help you?");
502
 
503
- // Start a new conversation
504
- const payload = { account };
505
- const resp = await api("/conversations", {
506
- method: "POST",
507
- headers: { "Content-Type": "application/json" },
508
- body: JSON.stringify(payload)
509
- });
510
-
511
- if (resp && resp.id) {
512
- convId = resp.id;
513
- localStorage.setItem("aimhsa_conv", convId);
514
- await updateHistoryList();
 
 
515
  }
516
  } catch (err) {
517
  console.error("Failed to clear chat history", err);
@@ -897,7 +964,7 @@
897
 
898
  async function handleBookingResponse(response) {
899
  try {
900
- const res = await fetch(API_ROOT + '/booking_response', {
901
  method: 'POST',
902
  headers: { 'Content-Type': 'application/json' },
903
  body: JSON.stringify({
@@ -907,7 +974,7 @@
907
  })
908
  });
909
 
910
- const data = await res.json();
911
 
912
  if (response === 'yes' && data.booking) {
913
  // Show booking confirmation
 
169
  }
170
 
171
  async function api(path, opts) {
172
+ // Try multiple endpoint patterns to handle both app.py and run_aimhsa.py
173
+ const endpoints = [
174
+ API_BASE_URL + path, // Direct path (app.py style)
175
+ API_BASE_URL + '/api' + path // API prefixed path (run_aimhsa.py style)
176
+ ];
177
+
178
+ let lastError;
179
+ for (const url of endpoints) {
180
+ try {
181
+ const res = await fetch(url, opts);
182
+ if (res.ok) {
183
+ return res.json();
184
+ }
185
+ if (res.status === 404 && url === endpoints[0]) {
186
+ // Try the next endpoint
187
+ continue;
188
+ }
189
+ // If not 404 or this is the last endpoint, handle the error
190
+ const txt = await res.text();
191
+ throw new Error(txt || res.statusText);
192
+ } catch (error) {
193
+ lastError = error;
194
+ if (url === endpoints[0] && error.message.includes('404')) {
195
+ // Try the next endpoint
196
+ continue;
197
+ }
198
+ // If not a 404 or this is the last endpoint, throw the error
199
+ throw error;
200
+ }
201
  }
202
+ // If we get here, all endpoints failed
203
+ throw lastError || new Error('All API endpoints failed');
204
  }
205
 
206
  async function initSession(useAccount = false) {
 
252
  ensureScroll();
253
  } catch (err) {
254
  console.error("history load error", err);
255
+ // If history fails to load, just show a welcome message
256
+ if (messagesEl.children.length === 0) {
257
+ appendMessage("bot", "Welcome! How can I help you today?");
258
+ }
259
  }
260
  }
261
 
 
299
 
300
  removeTypingIndicator();
301
 
302
+ // Handle scope rejection with special styling
303
+ if (resp.scope_rejection) {
304
+ const botMessage = appendMessage("assistant", resp.answer);
305
+ botMessage.classList.add("scope-rejection");
306
+ // Add visual indicator for scope rejection
307
+ const indicator = document.createElement("div");
308
+ indicator.className = "scope-indicator";
309
+ indicator.innerHTML = "🎯 Mental Health Focus";
310
+ indicator.style.cssText = `
311
+ font-size: 12px;
312
+ color: #f59e0b;
313
+ background: rgba(245, 158, 11, 0.1);
314
+ padding: 4px 8px;
315
+ border-radius: 4px;
316
+ margin-top: 8px;
317
+ display: inline-block;
318
+ `;
319
+ botMessage.querySelector('.msg-content').appendChild(indicator);
320
  } else {
321
+ // Ensure we got a valid response
322
+ if (!resp.answer || resp.answer.trim() === '') {
323
+ appendMessage("assistant", "I'm here to help. Could you please rephrase your question?");
324
+ } else {
325
+ appendMessage("assistant", resp.answer);
326
+ }
327
  }
328
 
329
  // Risk assessment is handled in backend only (no display)
 
430
  disableComposer(true);
431
  showUploadPreview(file);
432
 
433
+ // Try both endpoint patterns
434
+ const tryUpload = (url) => {
435
+ return new Promise((resolve, reject) => {
436
+ const xhr = new XMLHttpRequest();
437
+ xhr.open("POST", url, true);
438
 
439
+ xhr.upload.onprogress = function(e) {
440
+ if (e.lengthComputable) {
441
+ const pct = Math.round((e.loaded / e.total) * 100);
442
+ updateUploadProgress(pct);
443
+ }
444
+ };
445
 
446
+ xhr.onload = function() {
447
+ try {
448
+ const resText = xhr.responseText || "{}";
449
+ const data = JSON.parse(resText);
450
+ if (xhr.status >= 200 && xhr.status < 300) {
451
+ resolve(data);
452
+ } else {
453
+ reject(new Error(data.error || xhr.statusText));
454
+ }
455
+ } catch (err) {
456
+ reject(new Error("Upload parsing error"));
457
+ }
458
+ };
 
 
 
 
 
459
 
460
+ xhr.onerror = function() {
461
+ reject(new Error("Upload network error"));
462
+ };
463
+
464
+ const fd = new FormData();
465
+ fd.append("file", file, file.name);
466
+ if (convId) fd.append("id", convId);
467
+ if (account) fd.append("account", account);
468
+ const model = getSelectedModel();
469
+ if (model) fd.append("model", model);
470
+ xhr.send(fd);
471
+ });
472
  };
473
 
474
+ // Try upload_pdf endpoint first, then api/upload_pdf as fallback
475
+ tryUpload(API_BASE_URL + "/upload_pdf")
476
+ .catch(() => tryUpload(API_BASE_URL + "/api/upload_pdf"))
477
+ .then((data) => {
478
+ disableComposer(false);
479
+ convId = data.id;
480
+ localStorage.setItem("aimhsa_conv", convId);
481
+ appendMessage("bot", `Uploaded ${data.filename}. What would you like to know about this document?`);
482
+ clearUploadPreview();
483
+ if (account) updateHistoryList();
484
+ })
485
+ .catch((error) => {
486
+ disableComposer(false);
487
+ console.error("PDF upload failed:", error);
488
+ appendMessage("bot", "PDF upload failed: " + error.message);
489
+ clearUploadPreview();
490
+ });
491
  }
492
 
493
  function disableComposer(disabled) {
 
565
 
566
  appendMessage("bot", "Chat history cleared. How can I help you?");
567
 
568
+ // Start a new conversation if account exists
569
+ if (account && account !== 'null') {
570
+ const payload = { account };
571
+ const resp = await api("/conversations", {
572
+ method: "POST",
573
+ headers: { "Content-Type": "application/json" },
574
+ body: JSON.stringify(payload)
575
+ });
576
+
577
+ if (resp && resp.id) {
578
+ convId = resp.id;
579
+ localStorage.setItem("aimhsa_conv", convId);
580
+ await updateHistoryList();
581
+ }
582
  }
583
  } catch (err) {
584
  console.error("Failed to clear chat history", err);
 
964
 
965
  async function handleBookingResponse(response) {
966
  try {
967
+ const res = await api('/booking_response', {
968
  method: 'POST',
969
  headers: { 'Content-Type': 'application/json' },
970
  body: JSON.stringify({
 
974
  })
975
  });
976
 
977
+ const data = res; // api() already returns parsed JSON
978
 
979
  if (response === 'yes' && data.booking) {
980
  // Show booking confirmation
run_aimhsa.py CHANGED
@@ -48,13 +48,27 @@ openai_client = OpenAI(
48
  )
49
 
50
  # System prompt for AIMHSA
51
- SYSTEM_PROMPT = """You are AIMHSA, a supportive mental-health companion for Rwanda.
52
- - Be warm, brief, and evidence-informed. Use simple English (or Kinyarwanda if the user uses it).
53
- - Do NOT diagnose or prescribe medications. Encourage professional care when appropriate.
54
- - If the user mentions self-harm or immediate danger, express care and advise contacting local emergency services right away.
55
- - Ground answers in the provided CONTEXT. If context is insufficient, say what is known and unknown, and offer general coping strategies.
56
- - Only answer in English!
57
- - Also keep it brief except when details are required.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  """
59
 
60
  # Global variables for embeddings
@@ -241,12 +255,34 @@ def healthz():
241
 
242
  @app.route('/ask', methods=['POST'])
243
  def ask():
244
- """Main chat endpoint"""
245
  data = request.get_json(force=True)
246
  query = (data.get("query") or "").strip()
247
  if not query:
248
  return jsonify({"error": "Missing 'query'"}), 400
249
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  # Conversation ID handling
251
  conv_id = data.get("id")
252
  new_conv = False
 
48
  )
49
 
50
  # System prompt for AIMHSA
51
+ SYSTEM_PROMPT = """You are AIMHSA, a professional mental health support assistant for Rwanda.
52
+
53
+ ## CRITICAL SCOPE ENFORCEMENT - REJECT OFF-TOPIC QUERIES
54
+ - You ONLY provide mental health, emotional well-being, and psychological support
55
+ - IMMEDIATELY REJECT any questions about: technology, politics, sports, entertainment, cooking, general knowledge, science, business, or any non-mental health topics
56
+ - For rejected queries, respond with: "I'm a mental health support assistant and can only help with emotional well-being and mental health concerns. Let's focus on how you're feeling today - is there anything causing you stress, anxiety, or affecting your mood?"
57
+ - NEVER provide detailed answers to non-mental health questions
58
+ - Always redirect to mental health topics after rejection
59
+
60
+ ## Professional Guidelines
61
+ - Be warm, empathetic, and culturally sensitive
62
+ - Provide evidence-based information from the context when available
63
+ - Do NOT diagnose or prescribe medications
64
+ - Encourage professional care when appropriate
65
+ - For emergencies, always mention Rwanda's Mental Health Hotline: 105
66
+ - Keep responses professional, concise, and helpful
67
+ - Use the provided context to give accurate, relevant information
68
+ - Maintain a natural, conversational tone
69
+ - Ensure professional mental health support standards
70
+
71
+ Remember: You are a professional mental health support system. ALWAYS enforce scope boundaries by rejecting non-mental health queries and redirecting to emotional well-being topics.
72
  """
73
 
74
  # Global variables for embeddings
 
255
 
256
  @app.route('/ask', methods=['POST'])
257
  def ask():
258
+ """Main chat endpoint with scope validation"""
259
  data = request.get_json(force=True)
260
  query = (data.get("query") or "").strip()
261
  if not query:
262
  return jsonify({"error": "Missing 'query'"}), 400
263
 
264
+ # Simple scope validation for non-mental health topics
265
+ query_lower = query.lower()
266
+ non_mental_health_indicators = [
267
+ 'computer', 'technology', 'programming', 'politics', 'sports', 'football',
268
+ 'recipe', 'cooking', 'weather', 'mathematics', 'history', 'business',
269
+ 'movie', 'music', 'travel', 'shopping', 'news', 'science'
270
+ ]
271
+
272
+ # Check if query is clearly outside mental health scope
273
+ if any(indicator in query_lower for indicator in non_mental_health_indicators):
274
+ rejection_message = "I'm a mental health support assistant and can only help with emotional well-being and mental health concerns. Let's focus on how you're feeling today - is there anything causing you stress, anxiety, or affecting your mood?"
275
+
276
+ conv_id = data.get("id") or str(uuid.uuid4())
277
+ save_message(conv_id, "user", query)
278
+ save_message(conv_id, "assistant", rejection_message)
279
+
280
+ return jsonify({
281
+ "answer": rejection_message,
282
+ "id": conv_id,
283
+ "scope_rejection": True
284
+ })
285
+
286
  # Conversation ID handling
287
  conv_id = data.get("id")
288
  new_conv = False