Spaces:
Sleeping
Sleeping
IZERE HIRWA Roger
commited on
Commit
·
b84fe3f
1
Parent(s):
d4d8805
- app.py +1 -1
- chatbot/app.js +128 -61
- 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 |
-
|
| 173 |
-
const
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
}
|
| 178 |
-
|
|
|
|
| 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 |
-
//
|
| 274 |
-
if (
|
| 275 |
-
appendMessage("assistant",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
} else {
|
| 277 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
}
|
| 279 |
|
| 280 |
// Risk assessment is handled in backend only (no display)
|
|
@@ -381,48 +430,64 @@
|
|
| 381 |
disableComposer(true);
|
| 382 |
showUploadPreview(file);
|
| 383 |
|
| 384 |
-
|
| 385 |
-
const
|
| 386 |
-
|
|
|
|
|
|
|
| 387 |
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
}
|
| 409 |
-
} catch (err) {
|
| 410 |
-
appendMessage("bot", "Upload parsing error");
|
| 411 |
-
}
|
| 412 |
-
};
|
| 413 |
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
};
|
| 418 |
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
|
|
|
|
|
|
| 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
|
| 901 |
method: 'POST',
|
| 902 |
headers: { 'Content-Type': 'application/json' },
|
| 903 |
body: JSON.stringify({
|
|
@@ -907,7 +974,7 @@
|
|
| 907 |
})
|
| 908 |
});
|
| 909 |
|
| 910 |
-
const data =
|
| 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
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
-
|
| 55 |
-
-
|
| 56 |
-
-
|
| 57 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|