Spaces:
Running
Running
stanlee47 Claude Opus 4.6 commited on
Commit ·
69e7273
1
Parent(s): 8414ec8
Add missing wearable alert endpoints + fix ML prediction pipeline
Browse files- Add GET /api/wearable/alerts/latest endpoint for Flutter app polling
- Add GET /api/wearable/alerts endpoint for all unacknowledged alerts
- Add POST /api/wearable/alerts/acknowledge endpoint
- Fix batch endpoints to run ML inference after saving readings
- Return ML prediction result in device data response
- Set condition/dri_score columns in update_ml_prediction
- Save BDI crisis flag to database for admin monitoring
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- app.py +9 -1
- database.py +4 -2
- wearable.py +177 -6
app.py
CHANGED
|
@@ -745,7 +745,15 @@ def handle_full_beck_protocol(current_state: str, user_message: str, session_id:
|
|
| 745 |
|
| 746 |
# Check for crisis signal
|
| 747 |
elif "[CRISIS_FLAG]" in response_text:
|
| 748 |
-
# Item 9 indicated suicidal thoughts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 749 |
crisis_response = get_crisis_response(user_name)
|
| 750 |
|
| 751 |
return jsonify({
|
|
|
|
| 745 |
|
| 746 |
# Check for crisis signal
|
| 747 |
elif "[CRISIS_FLAG]" in response_text:
|
| 748 |
+
# Item 9 indicated suicidal thoughts - flag for admin monitoring
|
| 749 |
+
db.flag_crisis(
|
| 750 |
+
user_id=user_id,
|
| 751 |
+
user_name=user_name,
|
| 752 |
+
user_email=request.current_user.get("email", ""),
|
| 753 |
+
session_id=session_id,
|
| 754 |
+
message_content="BDI Item 9 (Suicidal Thoughts) scored >= 2",
|
| 755 |
+
trigger_word="BDI_SUICIDAL_IDEATION"
|
| 756 |
+
)
|
| 757 |
crisis_response = get_crisis_response(user_name)
|
| 758 |
|
| 759 |
return jsonify({
|
database.py
CHANGED
|
@@ -1191,11 +1191,13 @@ class Database:
|
|
| 1191 |
|
| 1192 |
def update_ml_prediction(self, record_id: str, prediction: str, confidence: float, risk_level: int):
|
| 1193 |
"""Update a wearable data record with ML prediction results."""
|
|
|
|
| 1194 |
self.conn.execute(
|
| 1195 |
"""UPDATE wearable_data
|
| 1196 |
-
SET ml_prediction = ?, ml_confidence = ?, risk_level = ?
|
|
|
|
| 1197 |
WHERE id = ?""",
|
| 1198 |
-
(prediction, confidence, risk_level, record_id)
|
| 1199 |
)
|
| 1200 |
self.conn.commit()
|
| 1201 |
|
|
|
|
| 1191 |
|
| 1192 |
def update_ml_prediction(self, record_id: str, prediction: str, confidence: float, risk_level: int):
|
| 1193 |
"""Update a wearable data record with ML prediction results."""
|
| 1194 |
+
condition = prediction # prediction is already "NORMAL", "MILD_STRESS", or "HIGH_STRESS"
|
| 1195 |
self.conn.execute(
|
| 1196 |
"""UPDATE wearable_data
|
| 1197 |
+
SET ml_prediction = ?, ml_confidence = ?, risk_level = ?,
|
| 1198 |
+
condition = ?, dri_score = ?
|
| 1199 |
WHERE id = ?""",
|
| 1200 |
+
(prediction, confidence, risk_level, condition, confidence, record_id)
|
| 1201 |
)
|
| 1202 |
self.conn.commit()
|
| 1203 |
|
wearable.py
CHANGED
|
@@ -26,9 +26,10 @@ def run_ml_inference_and_alert(user_id: str, record_id: str, db):
|
|
| 26 |
"""
|
| 27 |
Run ML inference on recent sensor data and trigger alerts if needed.
|
| 28 |
This runs after each new sensor reading is saved.
|
|
|
|
| 29 |
"""
|
| 30 |
if not ML_ENABLED:
|
| 31 |
-
return
|
| 32 |
|
| 33 |
try:
|
| 34 |
# Get recent readings (need at least 25 for 5 windows of 5 samples)
|
|
@@ -36,13 +37,13 @@ def run_ml_inference_and_alert(user_id: str, record_id: str, db):
|
|
| 36 |
|
| 37 |
if len(recent_readings) < 25:
|
| 38 |
# Not enough data yet
|
| 39 |
-
return
|
| 40 |
|
| 41 |
# Run ML prediction
|
| 42 |
prediction_result = predict_risk(recent_readings)
|
| 43 |
|
| 44 |
if not prediction_result:
|
| 45 |
-
return
|
| 46 |
|
| 47 |
prediction = prediction_result["prediction"]
|
| 48 |
confidence = prediction_result["confidence"]
|
|
@@ -94,10 +95,13 @@ def run_ml_inference_and_alert(user_id: str, record_id: str, db):
|
|
| 94 |
db.end_depression_episode(active_episode["id"])
|
| 95 |
print(f"✅ Depression episode ended for user {user_id}")
|
| 96 |
|
|
|
|
|
|
|
| 97 |
except Exception as e:
|
| 98 |
print(f"❌ Error in ML inference: {str(e)}")
|
| 99 |
import traceback
|
| 100 |
traceback.print_exc()
|
|
|
|
| 101 |
|
| 102 |
|
| 103 |
# ==================== SENSOR DATA ENDPOINTS ====================
|
|
@@ -228,6 +232,19 @@ def receive_batch_data():
|
|
| 228 |
except (ValueError, TypeError) as e:
|
| 229 |
errors.append(f"Reading {i}: {str(e)}")
|
| 230 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
return jsonify({
|
| 232 |
"success": True,
|
| 233 |
"saved_count": saved_count,
|
|
@@ -492,12 +509,16 @@ def receive_device_data():
|
|
| 492 |
)
|
| 493 |
|
| 494 |
# Run ML inference and check for depression risk
|
| 495 |
-
run_ml_inference_and_alert(user["id"], record_id, db)
|
| 496 |
|
| 497 |
-
|
| 498 |
"success": True,
|
| 499 |
"record_id": record_id
|
| 500 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 501 |
|
| 502 |
except ValueError as e:
|
| 503 |
return jsonify({"error": f"Invalid data format: {str(e)}"}), 400
|
|
@@ -506,6 +527,143 @@ def receive_device_data():
|
|
| 506 |
return jsonify({"error": "Failed to save sensor data"}), 500
|
| 507 |
|
| 508 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 509 |
@wearable_bp.route("/api/wearable/ml/status", methods=["GET"])
|
| 510 |
@token_required
|
| 511 |
def get_ml_status():
|
|
@@ -636,6 +794,19 @@ def receive_device_batch():
|
|
| 636 |
except (ValueError, TypeError) as e:
|
| 637 |
errors.append(f"Reading {i}: {str(e)}")
|
| 638 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 639 |
return jsonify({
|
| 640 |
"success": True,
|
| 641 |
"saved_count": saved_count,
|
|
|
|
| 26 |
"""
|
| 27 |
Run ML inference on recent sensor data and trigger alerts if needed.
|
| 28 |
This runs after each new sensor reading is saved.
|
| 29 |
+
Returns the prediction result dict or None.
|
| 30 |
"""
|
| 31 |
if not ML_ENABLED:
|
| 32 |
+
return None
|
| 33 |
|
| 34 |
try:
|
| 35 |
# Get recent readings (need at least 25 for 5 windows of 5 samples)
|
|
|
|
| 37 |
|
| 38 |
if len(recent_readings) < 25:
|
| 39 |
# Not enough data yet
|
| 40 |
+
return {"status": "waiting", "readings_count": len(recent_readings), "needed": 25}
|
| 41 |
|
| 42 |
# Run ML prediction
|
| 43 |
prediction_result = predict_risk(recent_readings)
|
| 44 |
|
| 45 |
if not prediction_result:
|
| 46 |
+
return None
|
| 47 |
|
| 48 |
prediction = prediction_result["prediction"]
|
| 49 |
confidence = prediction_result["confidence"]
|
|
|
|
| 95 |
db.end_depression_episode(active_episode["id"])
|
| 96 |
print(f"✅ Depression episode ended for user {user_id}")
|
| 97 |
|
| 98 |
+
return prediction_result
|
| 99 |
+
|
| 100 |
except Exception as e:
|
| 101 |
print(f"❌ Error in ML inference: {str(e)}")
|
| 102 |
import traceback
|
| 103 |
traceback.print_exc()
|
| 104 |
+
return None
|
| 105 |
|
| 106 |
|
| 107 |
# ==================== SENSOR DATA ENDPOINTS ====================
|
|
|
|
| 232 |
except (ValueError, TypeError) as e:
|
| 233 |
errors.append(f"Reading {i}: {str(e)}")
|
| 234 |
|
| 235 |
+
# Run ML inference after batch save (uses latest 25+ readings)
|
| 236 |
+
if saved_count > 0:
|
| 237 |
+
try:
|
| 238 |
+
last_record_id = db.conn.execute(
|
| 239 |
+
"""SELECT id FROM wearable_data
|
| 240 |
+
WHERE user_id = ? ORDER BY recorded_at DESC LIMIT 1""",
|
| 241 |
+
(user["id"],)
|
| 242 |
+
).fetchone()
|
| 243 |
+
if last_record_id:
|
| 244 |
+
run_ml_inference_and_alert(user["id"], last_record_id[0], db)
|
| 245 |
+
except Exception as ml_err:
|
| 246 |
+
print(f"ML inference after batch failed: {str(ml_err)}")
|
| 247 |
+
|
| 248 |
return jsonify({
|
| 249 |
"success": True,
|
| 250 |
"saved_count": saved_count,
|
|
|
|
| 509 |
)
|
| 510 |
|
| 511 |
# Run ML inference and check for depression risk
|
| 512 |
+
ml_result = run_ml_inference_and_alert(user["id"], record_id, db)
|
| 513 |
|
| 514 |
+
response_data = {
|
| 515 |
"success": True,
|
| 516 |
"record_id": record_id
|
| 517 |
+
}
|
| 518 |
+
if ml_result:
|
| 519 |
+
response_data["ml_prediction"] = ml_result
|
| 520 |
+
|
| 521 |
+
return jsonify(response_data)
|
| 522 |
|
| 523 |
except ValueError as e:
|
| 524 |
return jsonify({"error": f"Invalid data format: {str(e)}"}), 400
|
|
|
|
| 527 |
return jsonify({"error": "Failed to save sensor data"}), 500
|
| 528 |
|
| 529 |
|
| 530 |
+
# ==================== ALERT ENDPOINTS ====================
|
| 531 |
+
|
| 532 |
+
@wearable_bp.route("/api/wearable/alerts/latest", methods=["GET"])
|
| 533 |
+
@token_required
|
| 534 |
+
def get_latest_alert():
|
| 535 |
+
"""
|
| 536 |
+
Get the latest unacknowledged HIGH_STRESS or MILD_STRESS alert.
|
| 537 |
+
Used by the Flutter app for polling-based alert detection.
|
| 538 |
+
"""
|
| 539 |
+
try:
|
| 540 |
+
user = request.current_user
|
| 541 |
+
db = get_db()
|
| 542 |
+
|
| 543 |
+
# Find the latest unacknowledged stress reading
|
| 544 |
+
result = db.conn.execute(
|
| 545 |
+
"""SELECT id, risk_level, ml_confidence, ml_prediction, ppg, gsr,
|
| 546 |
+
recorded_at, condition
|
| 547 |
+
FROM wearable_data
|
| 548 |
+
WHERE user_id = ?
|
| 549 |
+
AND risk_level >= 1
|
| 550 |
+
AND (acknowledged = 0 OR acknowledged IS NULL)
|
| 551 |
+
ORDER BY recorded_at DESC
|
| 552 |
+
LIMIT 1""",
|
| 553 |
+
(user["id"],)
|
| 554 |
+
).fetchone()
|
| 555 |
+
|
| 556 |
+
if not result:
|
| 557 |
+
return jsonify({"has_alert": False, "alert": None})
|
| 558 |
+
|
| 559 |
+
alert = {
|
| 560 |
+
"id": result[0],
|
| 561 |
+
"dri_score": result[2] if result[2] else 0.0,
|
| 562 |
+
"condition": result[7] if result[7] else ("HIGH_STRESS" if result[1] == 2 else "MILD_STRESS"),
|
| 563 |
+
"ppg": result[4],
|
| 564 |
+
"gsr": result[5],
|
| 565 |
+
"recorded_at": result[6],
|
| 566 |
+
}
|
| 567 |
+
|
| 568 |
+
return jsonify({"has_alert": True, "alert": alert})
|
| 569 |
+
|
| 570 |
+
except Exception as e:
|
| 571 |
+
print(f"Error fetching latest alert: {str(e)}")
|
| 572 |
+
return jsonify({"error": "Failed to fetch alert"}), 500
|
| 573 |
+
|
| 574 |
+
|
| 575 |
+
@wearable_bp.route("/api/wearable/alerts", methods=["GET"])
|
| 576 |
+
@token_required
|
| 577 |
+
def get_all_alerts():
|
| 578 |
+
"""
|
| 579 |
+
Get all unacknowledged stress alerts for the user.
|
| 580 |
+
"""
|
| 581 |
+
try:
|
| 582 |
+
user = request.current_user
|
| 583 |
+
db = get_db()
|
| 584 |
+
|
| 585 |
+
results = db.conn.execute(
|
| 586 |
+
"""SELECT id, risk_level, ml_confidence, ml_prediction, ppg, gsr,
|
| 587 |
+
recorded_at, condition
|
| 588 |
+
FROM wearable_data
|
| 589 |
+
WHERE user_id = ?
|
| 590 |
+
AND risk_level >= 1
|
| 591 |
+
AND (acknowledged = 0 OR acknowledged IS NULL)
|
| 592 |
+
ORDER BY recorded_at DESC
|
| 593 |
+
LIMIT 50""",
|
| 594 |
+
(user["id"],)
|
| 595 |
+
).fetchall()
|
| 596 |
+
|
| 597 |
+
alerts = []
|
| 598 |
+
high_count = 0
|
| 599 |
+
mild_count = 0
|
| 600 |
+
for r in results:
|
| 601 |
+
condition = r[7] if r[7] else ("HIGH_STRESS" if r[1] == 2 else "MILD_STRESS")
|
| 602 |
+
alerts.append({
|
| 603 |
+
"id": r[0],
|
| 604 |
+
"dri_score": r[2] if r[2] else 0.0,
|
| 605 |
+
"condition": condition,
|
| 606 |
+
"ppg": r[4],
|
| 607 |
+
"gsr": r[5],
|
| 608 |
+
"recorded_at": r[6],
|
| 609 |
+
})
|
| 610 |
+
if r[1] == 2:
|
| 611 |
+
high_count += 1
|
| 612 |
+
elif r[1] == 1:
|
| 613 |
+
mild_count += 1
|
| 614 |
+
|
| 615 |
+
return jsonify({
|
| 616 |
+
"alerts": alerts,
|
| 617 |
+
"count": len(alerts),
|
| 618 |
+
"high_stress_count": high_count,
|
| 619 |
+
"mild_stress_count": mild_count,
|
| 620 |
+
"has_critical": high_count > 0,
|
| 621 |
+
})
|
| 622 |
+
|
| 623 |
+
except Exception as e:
|
| 624 |
+
print(f"Error fetching alerts: {str(e)}")
|
| 625 |
+
return jsonify({"error": "Failed to fetch alerts"}), 500
|
| 626 |
+
|
| 627 |
+
|
| 628 |
+
@wearable_bp.route("/api/wearable/alerts/acknowledge", methods=["POST"])
|
| 629 |
+
@token_required
|
| 630 |
+
def acknowledge_alerts():
|
| 631 |
+
"""
|
| 632 |
+
Acknowledge stress alerts. If alert_id is provided, acknowledge that specific alert.
|
| 633 |
+
Otherwise, acknowledge all unacknowledged alerts for the user.
|
| 634 |
+
"""
|
| 635 |
+
try:
|
| 636 |
+
user = request.current_user
|
| 637 |
+
db = get_db()
|
| 638 |
+
|
| 639 |
+
data = request.json or {}
|
| 640 |
+
alert_id = data.get("alert_id")
|
| 641 |
+
|
| 642 |
+
if alert_id:
|
| 643 |
+
db.conn.execute(
|
| 644 |
+
"""UPDATE wearable_data SET acknowledged = 1
|
| 645 |
+
WHERE id = ? AND user_id = ?""",
|
| 646 |
+
(alert_id, user["id"])
|
| 647 |
+
)
|
| 648 |
+
else:
|
| 649 |
+
db.conn.execute(
|
| 650 |
+
"""UPDATE wearable_data SET acknowledged = 1
|
| 651 |
+
WHERE user_id = ? AND risk_level >= 1
|
| 652 |
+
AND (acknowledged = 0 OR acknowledged IS NULL)""",
|
| 653 |
+
(user["id"],)
|
| 654 |
+
)
|
| 655 |
+
|
| 656 |
+
db.conn.commit()
|
| 657 |
+
|
| 658 |
+
return jsonify({"success": True, "message": "Alerts acknowledged"})
|
| 659 |
+
|
| 660 |
+
except Exception as e:
|
| 661 |
+
print(f"Error acknowledging alerts: {str(e)}")
|
| 662 |
+
return jsonify({"error": "Failed to acknowledge alerts"}), 500
|
| 663 |
+
|
| 664 |
+
|
| 665 |
+
# ==================== ML STATUS ENDPOINTS ====================
|
| 666 |
+
|
| 667 |
@wearable_bp.route("/api/wearable/ml/status", methods=["GET"])
|
| 668 |
@token_required
|
| 669 |
def get_ml_status():
|
|
|
|
| 794 |
except (ValueError, TypeError) as e:
|
| 795 |
errors.append(f"Reading {i}: {str(e)}")
|
| 796 |
|
| 797 |
+
# Run ML inference after batch save (uses latest 25+ readings)
|
| 798 |
+
if saved_count > 0:
|
| 799 |
+
try:
|
| 800 |
+
last_record_id = db.conn.execute(
|
| 801 |
+
"""SELECT id FROM wearable_data
|
| 802 |
+
WHERE user_id = ? ORDER BY recorded_at DESC LIMIT 1""",
|
| 803 |
+
(user["id"],)
|
| 804 |
+
).fetchone()
|
| 805 |
+
if last_record_id:
|
| 806 |
+
run_ml_inference_and_alert(user["id"], last_record_id[0], db)
|
| 807 |
+
except Exception as ml_err:
|
| 808 |
+
print(f"ML inference after device batch failed: {str(ml_err)}")
|
| 809 |
+
|
| 810 |
return jsonify({
|
| 811 |
"success": True,
|
| 812 |
"saved_count": saved_count,
|