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>

Files changed (3) hide show
  1. app.py +9 -1
  2. database.py +4 -2
  3. 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
- return jsonify({
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,