Spaces:
Sleeping
Sleeping
Upload 3 files
Browse files- app.py +1306 -0
- dexcom_sandbox_oauth.py +790 -0
- unified_data_manager.py +575 -0
app.py
ADDED
|
@@ -0,0 +1,1306 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
GlycoAI - AI-Powered Glucose Insights
|
| 4 |
+
Complete application with Demo Users + Dexcom Sandbox OAuth
|
| 5 |
+
IMPROVED UI VERSION - Clean, readable design with blue theme
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import gradio as gr
|
| 9 |
+
import plotly.graph_objects as go
|
| 10 |
+
import plotly.express as px
|
| 11 |
+
from datetime import datetime, timedelta
|
| 12 |
+
import pandas as pd
|
| 13 |
+
from typing import Optional, Tuple, List
|
| 14 |
+
import logging
|
| 15 |
+
import os
|
| 16 |
+
|
| 17 |
+
# Load environment variables from .env file
|
| 18 |
+
from dotenv import load_dotenv
|
| 19 |
+
load_dotenv()
|
| 20 |
+
|
| 21 |
+
# Import the Mistral chat class and unified data manager
|
| 22 |
+
from mistral_chat import GlucoBuddyMistralChat, validate_environment
|
| 23 |
+
from unified_data_manager import UnifiedDataManager
|
| 24 |
+
|
| 25 |
+
# Setup logging
|
| 26 |
+
logging.basicConfig(level=logging.INFO)
|
| 27 |
+
logger = logging.getLogger(__name__)
|
| 28 |
+
|
| 29 |
+
# Import our custom functions
|
| 30 |
+
from apifunctions import (
|
| 31 |
+
DexcomAPI,
|
| 32 |
+
GlucoseAnalyzer,
|
| 33 |
+
DEMO_USERS,
|
| 34 |
+
format_glucose_data_for_display
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
# Import Dexcom Sandbox OAuth
|
| 38 |
+
try:
|
| 39 |
+
from dexcom_sandbox_oauth import DexcomSandboxIntegration, DexcomSandboxUser
|
| 40 |
+
DEXCOM_SANDBOX_AVAILABLE = True
|
| 41 |
+
logger.info("β
Dexcom Sandbox OAuth available")
|
| 42 |
+
except ImportError as e:
|
| 43 |
+
DEXCOM_SANDBOX_AVAILABLE = False
|
| 44 |
+
logger.warning(f"β οΈ Dexcom Sandbox OAuth not available: {e}")
|
| 45 |
+
|
| 46 |
+
class GlycoAIApp:
|
| 47 |
+
"""Main application class for GlycoAI with demo users AND Dexcom Sandbox OAuth"""
|
| 48 |
+
|
| 49 |
+
def __init__(self):
|
| 50 |
+
# Validate environment before initializing
|
| 51 |
+
if not validate_environment():
|
| 52 |
+
raise ValueError("Environment validation failed - check your .env file or environment variables")
|
| 53 |
+
|
| 54 |
+
# Single data manager for consistency
|
| 55 |
+
self.data_manager = UnifiedDataManager()
|
| 56 |
+
|
| 57 |
+
# Chat interface (will use data manager's context)
|
| 58 |
+
self.mistral_chat = GlucoBuddyMistralChat()
|
| 59 |
+
|
| 60 |
+
# Dexcom Sandbox OAuth API
|
| 61 |
+
self.dexcom_sandbox = DexcomSandboxIntegration() if DEXCOM_SANDBOX_AVAILABLE else None
|
| 62 |
+
|
| 63 |
+
# UI state
|
| 64 |
+
self.chat_history = []
|
| 65 |
+
self.current_user_type = None # "demo" or "dexcom_sandbox"
|
| 66 |
+
|
| 67 |
+
def select_demo_user(self, user_key: str) -> Tuple[str, str]:
|
| 68 |
+
"""Handle demo user selection and load data consistently"""
|
| 69 |
+
if user_key not in DEMO_USERS:
|
| 70 |
+
return "β Invalid user selection", gr.update(visible=False)
|
| 71 |
+
|
| 72 |
+
try:
|
| 73 |
+
# Load data through unified manager
|
| 74 |
+
load_result = self.data_manager.load_user_data(user_key)
|
| 75 |
+
|
| 76 |
+
if not load_result['success']:
|
| 77 |
+
return f"β {load_result['message']}", gr.update(visible=False)
|
| 78 |
+
|
| 79 |
+
user = self.data_manager.current_user
|
| 80 |
+
self.current_user_type = "demo"
|
| 81 |
+
|
| 82 |
+
# Update Mistral chat with the same context
|
| 83 |
+
self._sync_chat_with_data_manager()
|
| 84 |
+
|
| 85 |
+
# Clear chat history when switching users
|
| 86 |
+
self.chat_history = []
|
| 87 |
+
self.mistral_chat.clear_conversation()
|
| 88 |
+
|
| 89 |
+
return (
|
| 90 |
+
f"β
Connected: {user.name} ({user.device_type}) - Demo Data",
|
| 91 |
+
gr.update(visible=True)
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
except Exception as e:
|
| 95 |
+
logger.error(f"Demo user selection failed: {str(e)}")
|
| 96 |
+
return f"β Connection failed: {str(e)}", gr.update(visible=False)
|
| 97 |
+
|
| 98 |
+
def initialize_chat_with_prompts(self) -> List:
|
| 99 |
+
"""Initialize chat with demo prompts as conversation bubbles"""
|
| 100 |
+
if not self.data_manager.current_user:
|
| 101 |
+
return [
|
| 102 |
+
[None, "π Welcome to GlycoAI! Please select a demo user or connect Dexcom Sandbox to get started."],
|
| 103 |
+
[None, "π‘ Once you load your glucose data, I'll provide personalized insights about your patterns and trends."]
|
| 104 |
+
]
|
| 105 |
+
|
| 106 |
+
templates = self.get_template_prompts()
|
| 107 |
+
|
| 108 |
+
# Create initial conversation with demo prompts
|
| 109 |
+
initial_chat = [
|
| 110 |
+
[None, f"π Hi! I'm ready to analyze {self.data_manager.current_user.name}'s glucose data. Here are some quick ways to get started:"],
|
| 111 |
+
[None, f"π― **{templates[0] if templates else 'Analyze my recent glucose patterns and trends'}**"],
|
| 112 |
+
[None, f"β‘ **{templates[1] if len(templates) > 1 else 'What can I do to improve my glucose control?'}**"],
|
| 113 |
+
[None, f"π½οΈ **What are some meal management strategies for better glucose control?**"],
|
| 114 |
+
[None, "π¬ You can click on any of these questions above, or ask me anything about glucose management!"]
|
| 115 |
+
]
|
| 116 |
+
|
| 117 |
+
return initial_chat
|
| 118 |
+
|
| 119 |
+
def handle_demo_prompt_click(self, prompt_text: str, history: List) -> Tuple[str, List]:
|
| 120 |
+
"""Handle clicking on demo prompts in chat"""
|
| 121 |
+
# Remove the emoji and formatting from the prompt
|
| 122 |
+
clean_prompt = prompt_text.replace("π― **", "").replace("β‘ **", "").replace("π½οΈ **", "").replace("**", "")
|
| 123 |
+
|
| 124 |
+
# Process the prompt as if user typed it
|
| 125 |
+
return self.chat_with_mistral(clean_prompt, history)
|
| 126 |
+
|
| 127 |
+
def start_dexcom_sandbox_oauth(self) -> str:
|
| 128 |
+
"""Start Dexcom Sandbox OAuth process"""
|
| 129 |
+
if not DEXCOM_SANDBOX_AVAILABLE:
|
| 130 |
+
return """
|
| 131 |
+
β **Dexcom Sandbox OAuth Not Available**
|
| 132 |
+
|
| 133 |
+
The Dexcom Sandbox authentication module is not properly configured.
|
| 134 |
+
Please ensure:
|
| 135 |
+
1. dexcom_sandbox_oauth.py exists and imports correctly
|
| 136 |
+
2. You have valid Dexcom developer credentials
|
| 137 |
+
3. All dependencies are installed
|
| 138 |
+
|
| 139 |
+
For now, please use the demo users above for instant access to realistic glucose data.
|
| 140 |
+
"""
|
| 141 |
+
|
| 142 |
+
try:
|
| 143 |
+
# Start OAuth flow for Dexcom Sandbox
|
| 144 |
+
auth_url = self.dexcom_sandbox.oauth.generate_auth_url()
|
| 145 |
+
|
| 146 |
+
# Try to open browser automatically
|
| 147 |
+
try:
|
| 148 |
+
import webbrowser
|
| 149 |
+
webbrowser.open(auth_url)
|
| 150 |
+
browser_status = "β
Browser opened automatically"
|
| 151 |
+
except:
|
| 152 |
+
browser_status = "β οΈ Please open the URL manually"
|
| 153 |
+
|
| 154 |
+
return f"""
|
| 155 |
+
π **Dexcom Sandbox OAuth Started**
|
| 156 |
+
|
| 157 |
+
{browser_status}
|
| 158 |
+
|
| 159 |
+
**π OAuth URL:** {auth_url}
|
| 160 |
+
|
| 161 |
+
**Step-by-Step Instructions:**
|
| 162 |
+
1. Browser should open automatically (or open URL above)
|
| 163 |
+
2. Select a sandbox user from the dropdown (SandboxUser6 recommended)
|
| 164 |
+
3. Click "Authorize" to grant access
|
| 165 |
+
4. **You will get a 404 error - THIS IS EXPECTED!**
|
| 166 |
+
5. Copy the COMPLETE callback URL from address bar
|
| 167 |
+
|
| 168 |
+
**Example callback URL:**
|
| 169 |
+
`http://localhost:7860/callback?code=ABC123XYZ&state=sandbox_test`
|
| 170 |
+
|
| 171 |
+
**Important:** Copy the entire URL (not just the code part)!
|
| 172 |
+
"""
|
| 173 |
+
|
| 174 |
+
except Exception as e:
|
| 175 |
+
logger.error(f"Dexcom Sandbox OAuth start error: {e}")
|
| 176 |
+
return f"β OAuth error: {str(e)}"
|
| 177 |
+
|
| 178 |
+
def complete_dexcom_sandbox_oauth(self, callback_url_input: str) -> Tuple[str, str]:
|
| 179 |
+
"""Complete Dexcom Sandbox OAuth with full callback URL"""
|
| 180 |
+
if not DEXCOM_SANDBOX_AVAILABLE:
|
| 181 |
+
return "β Dexcom Sandbox OAuth not available", gr.update(visible=False)
|
| 182 |
+
|
| 183 |
+
if not callback_url_input or not callback_url_input.strip():
|
| 184 |
+
return "β Please paste the complete callback URL", gr.update(visible=False)
|
| 185 |
+
|
| 186 |
+
try:
|
| 187 |
+
callback_url = callback_url_input.strip()
|
| 188 |
+
|
| 189 |
+
logger.info(f"Processing Dexcom Sandbox callback: {callback_url[:50]}...")
|
| 190 |
+
|
| 191 |
+
# Use Dexcom Sandbox OAuth completion
|
| 192 |
+
status_message, show_interface = self.dexcom_sandbox.complete_oauth(callback_url)
|
| 193 |
+
|
| 194 |
+
if show_interface:
|
| 195 |
+
logger.info("β
Dexcom Sandbox OAuth successful")
|
| 196 |
+
|
| 197 |
+
# Load Dexcom Sandbox data into data manager
|
| 198 |
+
sandbox_data_result = self._load_dexcom_sandbox_data()
|
| 199 |
+
|
| 200 |
+
if sandbox_data_result['success']:
|
| 201 |
+
self.current_user_type = "dexcom_sandbox"
|
| 202 |
+
|
| 203 |
+
# Update chat context
|
| 204 |
+
self._sync_chat_with_data_manager()
|
| 205 |
+
|
| 206 |
+
# Clear chat history for new user
|
| 207 |
+
self.chat_history = []
|
| 208 |
+
self.mistral_chat.clear_conversation()
|
| 209 |
+
|
| 210 |
+
return (
|
| 211 |
+
f"β
Connected: Dexcom Sandbox User - OAuth Authenticated",
|
| 212 |
+
gr.update(visible=True)
|
| 213 |
+
)
|
| 214 |
+
else:
|
| 215 |
+
return f"β Dexcom Sandbox data loading failed: {sandbox_data_result['message']}", gr.update(visible=False)
|
| 216 |
+
else:
|
| 217 |
+
logger.error(f"Dexcom Sandbox OAuth failed: {status_message}")
|
| 218 |
+
return f"β {status_message}", gr.update(visible=False)
|
| 219 |
+
|
| 220 |
+
except Exception as e:
|
| 221 |
+
logger.error(f"Dexcom Sandbox OAuth completion error: {e}")
|
| 222 |
+
return f"β OAuth completion failed: {str(e)}", gr.update(visible=False)
|
| 223 |
+
|
| 224 |
+
def _load_dexcom_sandbox_data(self) -> dict:
|
| 225 |
+
"""Load Dexcom Sandbox data through the unified data manager"""
|
| 226 |
+
try:
|
| 227 |
+
# Get Dexcom Sandbox user profile
|
| 228 |
+
sandbox_profile = self.dexcom_sandbox.get_user_profile()
|
| 229 |
+
|
| 230 |
+
if not sandbox_profile:
|
| 231 |
+
return {
|
| 232 |
+
'success': False,
|
| 233 |
+
'message': 'No Dexcom Sandbox user profile available'
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
# Set in data manager (compatible with existing structure)
|
| 237 |
+
self.data_manager.current_user = sandbox_profile
|
| 238 |
+
self.data_manager.data_source = "dexcom_sandbox_oauth"
|
| 239 |
+
self.data_manager.data_loaded_at = datetime.now()
|
| 240 |
+
|
| 241 |
+
logger.info("β
Dexcom Sandbox data integrated with data manager")
|
| 242 |
+
|
| 243 |
+
return {
|
| 244 |
+
'success': True,
|
| 245 |
+
'message': 'Dexcom Sandbox user profile loaded successfully'
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
except Exception as e:
|
| 249 |
+
logger.error(f"Failed to load Dexcom Sandbox data: {e}")
|
| 250 |
+
return {
|
| 251 |
+
'success': False,
|
| 252 |
+
'message': f'Failed to load OAuth data: {str(e)}'
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
def load_glucose_data(self) -> Tuple[str, go.Figure, str]:
|
| 256 |
+
"""Load and display glucose data using unified manager with notifications"""
|
| 257 |
+
if not self.data_manager.current_user:
|
| 258 |
+
return "Please select a user first (demo or Dexcom Sandbox)", None, ""
|
| 259 |
+
|
| 260 |
+
try:
|
| 261 |
+
# For Dexcom Sandbox users, load real data via OAuth
|
| 262 |
+
if self.current_user_type == "dexcom_sandbox":
|
| 263 |
+
overview, chart = self._load_dexcom_sandbox_glucose_data()
|
| 264 |
+
else:
|
| 265 |
+
# For demo users, force reload data to ensure freshness
|
| 266 |
+
load_result = self.data_manager.load_user_data(
|
| 267 |
+
self._get_current_user_key(),
|
| 268 |
+
force_reload=True
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
if not load_result['success']:
|
| 272 |
+
return load_result['message'], None, ""
|
| 273 |
+
|
| 274 |
+
# Get unified stats and build display
|
| 275 |
+
overview, chart = self._build_glucose_display()
|
| 276 |
+
|
| 277 |
+
# Create notification message based on user and data quality
|
| 278 |
+
notification = self._create_data_loaded_notification()
|
| 279 |
+
|
| 280 |
+
return overview, chart, notification
|
| 281 |
+
|
| 282 |
+
except Exception as e:
|
| 283 |
+
logger.error(f"Failed to load glucose data: {str(e)}")
|
| 284 |
+
return f"Failed to load glucose data: {str(e)}", None, ""
|
| 285 |
+
|
| 286 |
+
def _create_data_loaded_notification(self) -> str:
|
| 287 |
+
"""Create appropriate notification based on loaded data"""
|
| 288 |
+
if not self.data_manager.current_user or not self.data_manager.calculated_stats:
|
| 289 |
+
return ""
|
| 290 |
+
|
| 291 |
+
user_name = self.data_manager.current_user.name
|
| 292 |
+
stats = self.data_manager.calculated_stats
|
| 293 |
+
|
| 294 |
+
tir = stats.get('time_in_range_70_180', 0)
|
| 295 |
+
cv = stats.get('cv', 0)
|
| 296 |
+
avg_glucose = stats.get('average_glucose', 0)
|
| 297 |
+
total_readings = stats.get('total_readings', 0)
|
| 298 |
+
|
| 299 |
+
# Special handling for Sarah (unstable patterns)
|
| 300 |
+
if user_name == "Sarah Thompson":
|
| 301 |
+
if tir < 50 and cv > 40:
|
| 302 |
+
notification = f"""
|
| 303 |
+
π¨ **DATA LOADED - CONCERNING PATTERNS DETECTED**
|
| 304 |
+
|
| 305 |
+
**Patient:** {user_name} ({total_readings:,} readings analyzed)
|
| 306 |
+
|
| 307 |
+
**β οΈ Critical Findings:**
|
| 308 |
+
β’ Time in Range: {tir:.1f}% (Target: >70%)
|
| 309 |
+
β’ High Variability: CV {cv:.1f}% (Target: <36%)
|
| 310 |
+
β’ Average Glucose: {avg_glucose:.1f} mg/dL
|
| 311 |
+
|
| 312 |
+
**π₯ Immediate Action Required**
|
| 313 |
+
β’ Frequent hypoglycemia detected
|
| 314 |
+
β’ Severe glucose instability
|
| 315 |
+
β’ Healthcare provider consultation recommended
|
| 316 |
+
|
| 317 |
+
*AI analysis ready - Click Chat tab for urgent insights*
|
| 318 |
+
"""
|
| 319 |
+
else:
|
| 320 |
+
notification = f"""
|
| 321 |
+
β
**DATA LOADED SUCCESSFULLY**
|
| 322 |
+
|
| 323 |
+
**Patient:** {user_name} ({total_readings:,} readings analyzed)
|
| 324 |
+
**Time in Range:** {tir:.1f}% | **Average:** {avg_glucose:.1f} mg/dL
|
| 325 |
+
|
| 326 |
+
*14-day analysis complete - Ready for AI insights*
|
| 327 |
+
"""
|
| 328 |
+
else:
|
| 329 |
+
# For other users with better control
|
| 330 |
+
if tir >= 70:
|
| 331 |
+
notification = f"""
|
| 332 |
+
β
**DATA LOADED - EXCELLENT CONTROL**
|
| 333 |
+
|
| 334 |
+
**Patient:** {user_name} ({total_readings:,} readings analyzed)
|
| 335 |
+
**Time in Range:** {tir:.1f}% β
| **CV:** {cv:.1f}%
|
| 336 |
+
|
| 337 |
+
*Great glucose management - AI ready to help maintain control*
|
| 338 |
+
"""
|
| 339 |
+
else:
|
| 340 |
+
notification = f"""
|
| 341 |
+
π **DATA LOADED SUCCESSFULLY**
|
| 342 |
+
|
| 343 |
+
**Patient:** {user_name} ({total_readings:,} readings analyzed)
|
| 344 |
+
**Time in Range:** {tir:.1f}% | **Average:** {avg_glucose:.1f} mg/dL
|
| 345 |
+
|
| 346 |
+
*Analysis complete - AI ready to provide insights*
|
| 347 |
+
"""
|
| 348 |
+
|
| 349 |
+
return notification
|
| 350 |
+
|
| 351 |
+
def _load_dexcom_sandbox_glucose_data(self) -> Tuple[str, go.Figure]:
|
| 352 |
+
"""Load Dexcom Sandbox glucose data via OAuth"""
|
| 353 |
+
if not self.dexcom_sandbox.authenticated:
|
| 354 |
+
return "β Dexcom Sandbox not authenticated. Please complete OAuth first.", None
|
| 355 |
+
|
| 356 |
+
try:
|
| 357 |
+
# Load 14 days of data from Dexcom Sandbox
|
| 358 |
+
data_result = self.dexcom_sandbox.load_glucose_data(days=14)
|
| 359 |
+
|
| 360 |
+
if not data_result['success']:
|
| 361 |
+
return f"β {data_result['error']}", None
|
| 362 |
+
|
| 363 |
+
# Convert Dexcom Sandbox data to data manager format
|
| 364 |
+
self._convert_dexcom_sandbox_to_dataframe()
|
| 365 |
+
|
| 366 |
+
return self._build_glucose_display()
|
| 367 |
+
|
| 368 |
+
except Exception as e:
|
| 369 |
+
logger.error(f"Failed to load Dexcom Sandbox data: {e}")
|
| 370 |
+
return f"β Failed to load Dexcom Sandbox data: {str(e)}", None
|
| 371 |
+
|
| 372 |
+
def _convert_dexcom_sandbox_to_dataframe(self):
|
| 373 |
+
"""Convert Dexcom Sandbox glucose data to DataFrame format"""
|
| 374 |
+
try:
|
| 375 |
+
glucose_data = self.dexcom_sandbox.get_glucose_data_for_ui()
|
| 376 |
+
|
| 377 |
+
if not glucose_data:
|
| 378 |
+
raise Exception("No glucose data available from Dexcom Sandbox")
|
| 379 |
+
|
| 380 |
+
# Convert to DataFrame
|
| 381 |
+
df = pd.DataFrame(glucose_data)
|
| 382 |
+
|
| 383 |
+
# Ensure proper datetime conversion
|
| 384 |
+
df['systemTime'] = pd.to_datetime(df['systemTime'])
|
| 385 |
+
df['displayTime'] = pd.to_datetime(df['displayTime'])
|
| 386 |
+
df['value'] = pd.to_numeric(df['value'], errors='coerce')
|
| 387 |
+
|
| 388 |
+
# Sort by time
|
| 389 |
+
df = df.sort_values('systemTime')
|
| 390 |
+
|
| 391 |
+
# Set in data manager
|
| 392 |
+
self.data_manager.processed_glucose_data = df
|
| 393 |
+
|
| 394 |
+
# Calculate statistics using existing analyzer
|
| 395 |
+
self.data_manager.calculated_stats = self.data_manager._calculate_unified_stats()
|
| 396 |
+
self.data_manager.identified_patterns = GlucoseAnalyzer.identify_patterns(df)
|
| 397 |
+
|
| 398 |
+
logger.info(f"β
Converted {len(df)} Dexcom Sandbox readings to DataFrame")
|
| 399 |
+
|
| 400 |
+
except Exception as e:
|
| 401 |
+
logger.error(f"Failed to convert Dexcom Sandbox data: {e}")
|
| 402 |
+
raise
|
| 403 |
+
|
| 404 |
+
def _build_glucose_display(self) -> Tuple[str, go.Figure]:
|
| 405 |
+
"""Build glucose data display (common for demo and Dexcom Sandbox)"""
|
| 406 |
+
# Get unified stats
|
| 407 |
+
stats = self.data_manager.get_stats_for_ui()
|
| 408 |
+
chart_data = self.data_manager.get_chart_data()
|
| 409 |
+
|
| 410 |
+
# Sync chat with fresh data
|
| 411 |
+
self._sync_chat_with_data_manager()
|
| 412 |
+
|
| 413 |
+
if chart_data is None or chart_data.empty:
|
| 414 |
+
return "No glucose data available", None
|
| 415 |
+
|
| 416 |
+
# Build data summary with CONSISTENT metrics
|
| 417 |
+
user = self.data_manager.current_user
|
| 418 |
+
data_points = stats.get('total_readings', 0)
|
| 419 |
+
avg_glucose = stats.get('average_glucose', 0)
|
| 420 |
+
std_glucose = stats.get('std_glucose', 0)
|
| 421 |
+
min_glucose = stats.get('min_glucose', 0)
|
| 422 |
+
max_glucose = stats.get('max_glucose', 0)
|
| 423 |
+
|
| 424 |
+
time_in_range = stats.get('time_in_range_70_180', 0)
|
| 425 |
+
time_below_range = stats.get('time_below_70', 0)
|
| 426 |
+
time_above_range = stats.get('time_above_180', 0)
|
| 427 |
+
|
| 428 |
+
gmi = stats.get('gmi', 0)
|
| 429 |
+
cv = stats.get('cv', 0)
|
| 430 |
+
|
| 431 |
+
# Calculate date range
|
| 432 |
+
end_date = datetime.now()
|
| 433 |
+
start_date = end_date - timedelta(days=14)
|
| 434 |
+
|
| 435 |
+
# Determine data source
|
| 436 |
+
if self.current_user_type == "dexcom_sandbox":
|
| 437 |
+
data_source = "Dexcom Sandbox OAuth"
|
| 438 |
+
oauth_status = "β
Authenticated Dexcom Sandbox with working OAuth"
|
| 439 |
+
else:
|
| 440 |
+
data_source = "Demo Data"
|
| 441 |
+
oauth_status = "π Using demo data for testing"
|
| 442 |
+
|
| 443 |
+
data_summary = f"""
|
| 444 |
+
## π Data Summary for {user.name}
|
| 445 |
+
|
| 446 |
+
### Basic Information
|
| 447 |
+
β’ **Data Type:** {data_source}
|
| 448 |
+
β’ **Analysis Period:** {start_date.strftime('%B %d, %Y')} to {end_date.strftime('%B %d, %Y')} (14 days)
|
| 449 |
+
β’ **Total Readings:** {data_points:,} glucose measurements
|
| 450 |
+
β’ **Device:** {user.device_type}
|
| 451 |
+
|
| 452 |
+
### Glucose Statistics
|
| 453 |
+
β’ **Average Glucose:** {avg_glucose:.1f} mg/dL
|
| 454 |
+
β’ **Standard Deviation:** {std_glucose:.1f} mg/dL
|
| 455 |
+
β’ **Coefficient of Variation:** {cv:.1f}%
|
| 456 |
+
β’ **Glucose Range:** {min_glucose:.0f} - {max_glucose:.0f} mg/dL
|
| 457 |
+
β’ **GMI (Glucose Management Indicator):** {gmi:.1f}%
|
| 458 |
+
|
| 459 |
+
### Time in Range Analysis
|
| 460 |
+
β’ **Time in Range (70-180 mg/dL):** {time_in_range:.1f}%
|
| 461 |
+
β’ **Time Below Range (<70 mg/dL):** {time_below_range:.1f}%
|
| 462 |
+
β’ **Time Above Range (>180 mg/dL):** {time_above_range:.1f}%
|
| 463 |
+
|
| 464 |
+
### Clinical Targets
|
| 465 |
+
β’ **Target Time in Range:** >70% (Current: {time_in_range:.1f}%)
|
| 466 |
+
β’ **Target Time Below Range:** <4% (Current: {time_below_range:.1f}%)
|
| 467 |
+
β’ **Target CV:** <36% (Current: {cv:.1f}%)
|
| 468 |
+
|
| 469 |
+
### Authentication Status
|
| 470 |
+
β’ **User Type:** {self.current_user_type.upper() if self.current_user_type else 'Unknown'}
|
| 471 |
+
β’ **OAuth Status:** {oauth_status}
|
| 472 |
+
"""
|
| 473 |
+
|
| 474 |
+
chart = self.create_glucose_chart()
|
| 475 |
+
|
| 476 |
+
return data_summary, chart
|
| 477 |
+
|
| 478 |
+
def _sync_chat_with_data_manager(self):
|
| 479 |
+
"""Ensure chat uses the same data as the UI"""
|
| 480 |
+
try:
|
| 481 |
+
# Get context from unified data manager
|
| 482 |
+
context = self.data_manager.get_context_for_agent()
|
| 483 |
+
|
| 484 |
+
# Update chat's internal data to match
|
| 485 |
+
if not context.get("error"):
|
| 486 |
+
self.mistral_chat.current_user = self.data_manager.current_user
|
| 487 |
+
self.mistral_chat.current_glucose_data = self.data_manager.processed_glucose_data
|
| 488 |
+
self.mistral_chat.current_stats = self.data_manager.calculated_stats
|
| 489 |
+
self.mistral_chat.current_patterns = self.data_manager.identified_patterns
|
| 490 |
+
|
| 491 |
+
logger.info(f"Synced chat with data manager - TIR: {self.data_manager.calculated_stats.get('time_in_range_70_180', 0):.1f}%")
|
| 492 |
+
|
| 493 |
+
except Exception as e:
|
| 494 |
+
logger.error(f"Failed to sync chat with data manager: {e}")
|
| 495 |
+
|
| 496 |
+
def _get_current_user_key(self) -> str:
|
| 497 |
+
"""Get the current user key"""
|
| 498 |
+
if not self.data_manager.current_user:
|
| 499 |
+
return ""
|
| 500 |
+
|
| 501 |
+
# Find the key for current user
|
| 502 |
+
for key, user in DEMO_USERS.items():
|
| 503 |
+
if user == self.data_manager.current_user:
|
| 504 |
+
return key
|
| 505 |
+
return ""
|
| 506 |
+
|
| 507 |
+
def get_template_prompts(self) -> List[str]:
|
| 508 |
+
"""Get template prompts based on current user data"""
|
| 509 |
+
if not self.data_manager.current_user or not self.data_manager.calculated_stats:
|
| 510 |
+
return [
|
| 511 |
+
"What should I know about managing my diabetes?",
|
| 512 |
+
"How can I improve my glucose control?"
|
| 513 |
+
]
|
| 514 |
+
|
| 515 |
+
stats = self.data_manager.calculated_stats
|
| 516 |
+
time_in_range = stats.get('time_in_range_70_180', 0)
|
| 517 |
+
time_below_70 = stats.get('time_below_70', 0)
|
| 518 |
+
|
| 519 |
+
templates = []
|
| 520 |
+
|
| 521 |
+
if time_in_range < 70:
|
| 522 |
+
templates.append(f"My time in range is {time_in_range:.1f}% which is below the 70% target. What specific strategies can help me improve it?")
|
| 523 |
+
else:
|
| 524 |
+
templates.append(f"My time in range is {time_in_range:.1f}% which meets the target. How can I maintain this level of control?")
|
| 525 |
+
|
| 526 |
+
if time_below_70 > 4:
|
| 527 |
+
templates.append(f"I'm experiencing {time_below_70:.1f}% time below 70 mg/dL. What can I do to prevent these low episodes?")
|
| 528 |
+
else:
|
| 529 |
+
templates.append("What are the best practices for preventing hypoglycemia in my situation?")
|
| 530 |
+
|
| 531 |
+
# Add data source specific template
|
| 532 |
+
if self.current_user_type == "dexcom_sandbox":
|
| 533 |
+
templates.append("This is my Dexcom Sandbox OAuth-authenticated data. What insights can you provide about these glucose patterns?")
|
| 534 |
+
else:
|
| 535 |
+
templates.append("Based on this demo data, what would you recommend for someone with similar patterns?")
|
| 536 |
+
|
| 537 |
+
return templates
|
| 538 |
+
|
| 539 |
+
def chat_with_mistral(self, message: str, history: List) -> Tuple[str, List]:
|
| 540 |
+
"""Handle chat interaction with Mistral using unified data"""
|
| 541 |
+
if not message.strip():
|
| 542 |
+
return "", history
|
| 543 |
+
|
| 544 |
+
if not self.data_manager.current_user:
|
| 545 |
+
response = "Please select a user first (demo or Dexcom Sandbox) to get personalized insights about glucose data."
|
| 546 |
+
history.append([message, response])
|
| 547 |
+
return "", history
|
| 548 |
+
|
| 549 |
+
try:
|
| 550 |
+
# Ensure chat is synced with latest data
|
| 551 |
+
self._sync_chat_with_data_manager()
|
| 552 |
+
|
| 553 |
+
# Send message to Mistral chat
|
| 554 |
+
result = self.mistral_chat.chat_with_mistral(message)
|
| 555 |
+
|
| 556 |
+
if result['success']:
|
| 557 |
+
response = result['response']
|
| 558 |
+
|
| 559 |
+
# Add data consistency note
|
| 560 |
+
validation = self.data_manager.validate_data_consistency()
|
| 561 |
+
if validation.get('valid'):
|
| 562 |
+
data_age = validation.get('data_age_minutes', 0)
|
| 563 |
+
if data_age > 10: # Warn if data is old
|
| 564 |
+
response += f"\n\nπ *Note: Analysis based on data from {data_age} minutes ago. Reload data for most current insights.*"
|
| 565 |
+
|
| 566 |
+
# Add data source context
|
| 567 |
+
if self.current_user_type == "dexcom_sandbox":
|
| 568 |
+
response += f"\n\nπ *This analysis is based on your OAuth-authenticated Dexcom Sandbox data.*"
|
| 569 |
+
else:
|
| 570 |
+
response += f"\n\nπ *This analysis is based on demo data for testing purposes.*"
|
| 571 |
+
|
| 572 |
+
# Add context note if no user data was included
|
| 573 |
+
if not result.get('context_included', True):
|
| 574 |
+
response += f"\n\nπ‘ *For more personalized advice, make sure your glucose data is loaded.*"
|
| 575 |
+
else:
|
| 576 |
+
response = f"I apologize, but I encountered an error: {result.get('error', 'Unknown error')}. Please try again or rephrase your question."
|
| 577 |
+
|
| 578 |
+
history.append([message, response])
|
| 579 |
+
return "", history
|
| 580 |
+
|
| 581 |
+
except Exception as e:
|
| 582 |
+
logger.error(f"Chat error: {str(e)}")
|
| 583 |
+
error_response = f"I apologize, but I encountered an error while processing your question: {str(e)}. Please try rephrasing your question."
|
| 584 |
+
history.append([message, error_response])
|
| 585 |
+
return "", history
|
| 586 |
+
|
| 587 |
+
def clear_chat_history(self) -> List:
|
| 588 |
+
"""Clear chat history"""
|
| 589 |
+
self.chat_history = []
|
| 590 |
+
self.mistral_chat.clear_conversation()
|
| 591 |
+
return []
|
| 592 |
+
|
| 593 |
+
def create_glucose_chart(self) -> Optional[go.Figure]:
|
| 594 |
+
"""Create an interactive glucose chart using unified data"""
|
| 595 |
+
chart_data = self.data_manager.get_chart_data()
|
| 596 |
+
|
| 597 |
+
if chart_data is None or chart_data.empty:
|
| 598 |
+
return None
|
| 599 |
+
|
| 600 |
+
fig = go.Figure()
|
| 601 |
+
|
| 602 |
+
# Color code based on glucose ranges
|
| 603 |
+
colors = []
|
| 604 |
+
for value in chart_data['value']:
|
| 605 |
+
if value < 70:
|
| 606 |
+
colors.append('#E74C3C') # Red for low
|
| 607 |
+
elif value > 180:
|
| 608 |
+
colors.append('#F39C12') # Orange for high
|
| 609 |
+
else:
|
| 610 |
+
colors.append('#3498DB') # Blue for in range
|
| 611 |
+
|
| 612 |
+
fig.add_trace(go.Scatter(
|
| 613 |
+
x=chart_data['systemTime'],
|
| 614 |
+
y=chart_data['value'],
|
| 615 |
+
mode='lines+markers',
|
| 616 |
+
name='Glucose',
|
| 617 |
+
line=dict(color='#2980B9', width=2),
|
| 618 |
+
marker=dict(size=4, color=colors),
|
| 619 |
+
hovertemplate='<b>%{y} mg/dL</b><br>%{x}<extra></extra>'
|
| 620 |
+
))
|
| 621 |
+
|
| 622 |
+
# Add target range shading
|
| 623 |
+
fig.add_hrect(
|
| 624 |
+
y0=70, y1=180,
|
| 625 |
+
fillcolor="rgba(52, 152, 219, 0.1)",
|
| 626 |
+
layer="below",
|
| 627 |
+
line_width=0,
|
| 628 |
+
annotation_text="Target Range",
|
| 629 |
+
annotation_position="top left"
|
| 630 |
+
)
|
| 631 |
+
|
| 632 |
+
# Add reference lines
|
| 633 |
+
fig.add_hline(y=70, line_dash="dash", line_color="#E67E22",
|
| 634 |
+
annotation_text="Low (70 mg/dL)", annotation_position="right")
|
| 635 |
+
fig.add_hline(y=180, line_dash="dash", line_color="#E67E22",
|
| 636 |
+
annotation_text="High (180 mg/dL)", annotation_position="right")
|
| 637 |
+
fig.add_hline(y=54, line_dash="dot", line_color="#E74C3C",
|
| 638 |
+
annotation_text="Severe Low (54 mg/dL)", annotation_position="right")
|
| 639 |
+
fig.add_hline(y=250, line_dash="dot", line_color="#E74C3C",
|
| 640 |
+
annotation_text="Severe High (250 mg/dL)", annotation_position="right")
|
| 641 |
+
|
| 642 |
+
# Get current stats for title
|
| 643 |
+
stats = self.data_manager.get_stats_for_ui()
|
| 644 |
+
tir = stats.get('time_in_range_70_180', 0)
|
| 645 |
+
|
| 646 |
+
if self.current_user_type == "dexcom_sandbox":
|
| 647 |
+
data_type = "Dexcom Sandbox"
|
| 648 |
+
else:
|
| 649 |
+
data_type = "Demo Data"
|
| 650 |
+
|
| 651 |
+
fig.update_layout(
|
| 652 |
+
title={
|
| 653 |
+
'text': f"14-Day Glucose Trends - {self.data_manager.current_user.name} ({data_type} - TIR: {tir:.1f}%)",
|
| 654 |
+
'x': 0.5,
|
| 655 |
+
'xanchor': 'center'
|
| 656 |
+
},
|
| 657 |
+
xaxis_title="Time",
|
| 658 |
+
yaxis_title="Glucose (mg/dL)",
|
| 659 |
+
hovermode='x unified',
|
| 660 |
+
height=500,
|
| 661 |
+
showlegend=False,
|
| 662 |
+
plot_bgcolor='rgba(0,0,0,0)',
|
| 663 |
+
paper_bgcolor='rgba(0,0,0,0)',
|
| 664 |
+
font=dict(size=12),
|
| 665 |
+
margin=dict(l=60, r=60, t=80, b=60)
|
| 666 |
+
)
|
| 667 |
+
|
| 668 |
+
fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='rgba(128,128,128,0.2)')
|
| 669 |
+
fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='rgba(128,128,128,0.2)')
|
| 670 |
+
|
| 671 |
+
return fig
|
| 672 |
+
|
| 673 |
+
|
| 674 |
+
def create_interface():
|
| 675 |
+
"""Create the Gradio interface with improved, cleaner design"""
|
| 676 |
+
app = GlycoAIApp()
|
| 677 |
+
|
| 678 |
+
# Clean blue-themed CSS
|
| 679 |
+
custom_css = """
|
| 680 |
+
/* Main header styling */
|
| 681 |
+
.main-header {
|
| 682 |
+
text-align: center;
|
| 683 |
+
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
|
| 684 |
+
color: white;
|
| 685 |
+
padding: 2rem;
|
| 686 |
+
border-radius: 12px;
|
| 687 |
+
margin-bottom: 2rem;
|
| 688 |
+
box-shadow: 0 4px 20px rgba(52, 152, 219, 0.3);
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
/* Demo user buttons - consistent size and light blue */
|
| 692 |
+
.demo-user-btn {
|
| 693 |
+
background: linear-gradient(135deg, #85c1e9 0%, #5dade2 100%) !important;
|
| 694 |
+
border: none !important;
|
| 695 |
+
border-radius: 8px !important;
|
| 696 |
+
padding: 1rem !important;
|
| 697 |
+
font-size: 0.95rem !important;
|
| 698 |
+
font-weight: 600 !important;
|
| 699 |
+
color: white !important;
|
| 700 |
+
box-shadow: 0 3px 12px rgba(93, 173, 226, 0.3) !important;
|
| 701 |
+
transition: all 0.3s ease !important;
|
| 702 |
+
min-height: 80px !important;
|
| 703 |
+
text-align: center !important;
|
| 704 |
+
width: 100% !important;
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
.demo-user-btn:hover {
|
| 708 |
+
transform: translateY(-2px) !important;
|
| 709 |
+
box-shadow: 0 6px 20px rgba(93, 173, 226, 0.4) !important;
|
| 710 |
+
background: linear-gradient(135deg, #7fb3d3 0%, #5499c7 100%) !important;
|
| 711 |
+
}
|
| 712 |
+
|
| 713 |
+
/* Dexcom OAuth button - smaller and distinct */
|
| 714 |
+
.dexcom-oauth-btn {
|
| 715 |
+
background: linear-gradient(135deg, #2980b9 0%, #1f618d 100%) !important;
|
| 716 |
+
border: none !important;
|
| 717 |
+
border-radius: 8px !important;
|
| 718 |
+
padding: 0.8rem 1.5rem !important;
|
| 719 |
+
font-size: 0.9rem !important;
|
| 720 |
+
font-weight: 600 !important;
|
| 721 |
+
color: white !important;
|
| 722 |
+
box-shadow: 0 3px 12px rgba(41, 128, 185, 0.3) !important;
|
| 723 |
+
transition: all 0.3s ease !important;
|
| 724 |
+
text-align: center !important;
|
| 725 |
+
}
|
| 726 |
+
|
| 727 |
+
.dexcom-oauth-btn:hover {
|
| 728 |
+
transform: translateY(-1px) !important;
|
| 729 |
+
box-shadow: 0 5px 16px rgba(41, 128, 185, 0.4) !important;
|
| 730 |
+
}
|
| 731 |
+
|
| 732 |
+
/* Prominent load data button */
|
| 733 |
+
.load-data-btn {
|
| 734 |
+
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%) !important;
|
| 735 |
+
border: none !important;
|
| 736 |
+
border-radius: 12px !important;
|
| 737 |
+
padding: 1.5rem 2rem !important;
|
| 738 |
+
font-size: 1.1rem !important;
|
| 739 |
+
font-weight: bold !important;
|
| 740 |
+
color: white !important;
|
| 741 |
+
box-shadow: 0 6px 24px rgba(52, 152, 219, 0.4) !important;
|
| 742 |
+
transition: all 0.3s ease !important;
|
| 743 |
+
min-height: 80px !important;
|
| 744 |
+
text-align: center !important;
|
| 745 |
+
}
|
| 746 |
+
|
| 747 |
+
.load-data-btn:hover {
|
| 748 |
+
transform: translateY(-2px) !important;
|
| 749 |
+
box-shadow: 0 8px 32px rgba(52, 152, 219, 0.5) !important;
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
/* Tab styling - more visible */
|
| 753 |
+
.gradio-tabs .tab-nav {
|
| 754 |
+
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%) !important;
|
| 755 |
+
border-radius: 8px !important;
|
| 756 |
+
padding: 0.5rem !important;
|
| 757 |
+
margin-bottom: 1rem !important;
|
| 758 |
+
}
|
| 759 |
+
|
| 760 |
+
.gradio-tabs .tab-nav button {
|
| 761 |
+
background: white !important;
|
| 762 |
+
border: 1px solid #90caf9 !important;
|
| 763 |
+
border-radius: 6px !important;
|
| 764 |
+
margin: 0 0.25rem !important;
|
| 765 |
+
padding: 0.75rem 1.5rem !important;
|
| 766 |
+
font-weight: 600 !important;
|
| 767 |
+
color: #1565c0 !important;
|
| 768 |
+
transition: all 0.3s ease !important;
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
.gradio-tabs .tab-nav button:hover {
|
| 772 |
+
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%) !important;
|
| 773 |
+
transform: translateY(-1px) !important;
|
| 774 |
+
}
|
| 775 |
+
|
| 776 |
+
.gradio-tabs .tab-nav button.selected {
|
| 777 |
+
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%) !important;
|
| 778 |
+
color: white !important;
|
| 779 |
+
border-color: #2980b9 !important;
|
| 780 |
+
box-shadow: 0 3px 12px rgba(52, 152, 219, 0.3) !important;
|
| 781 |
+
}
|
| 782 |
+
|
| 783 |
+
/* Chat bubble styling for demo prompts */
|
| 784 |
+
.demo-prompt-bubble {
|
| 785 |
+
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
|
| 786 |
+
border: 1px solid #90caf9;
|
| 787 |
+
border-radius: 15px;
|
| 788 |
+
padding: 0.75rem 1rem;
|
| 789 |
+
margin: 0.5rem 0;
|
| 790 |
+
color: #1565c0;
|
| 791 |
+
font-size: 0.9rem;
|
| 792 |
+
cursor: pointer;
|
| 793 |
+
transition: all 0.2s ease;
|
| 794 |
+
display: inline-block;
|
| 795 |
+
max-width: 80%;
|
| 796 |
+
}
|
| 797 |
+
|
| 798 |
+
.demo-prompt-bubble:hover {
|
| 799 |
+
background: linear-gradient(135deg, #bbdefb 0%, #90caf9 100%);
|
| 800 |
+
transform: translateY(-1px);
|
| 801 |
+
box-shadow: 0 3px 8px rgba(52, 152, 219, 0.2);
|
| 802 |
+
}
|
| 803 |
+
|
| 804 |
+
/* Toggle styling */
|
| 805 |
+
.oauth-toggle {
|
| 806 |
+
background: #f8f9fa;
|
| 807 |
+
border: 1px solid #e3f2fd;
|
| 808 |
+
border-radius: 6px;
|
| 809 |
+
padding: 0.5rem;
|
| 810 |
+
}
|
| 811 |
+
|
| 812 |
+
/* Notification styling */
|
| 813 |
+
.notification-success {
|
| 814 |
+
background: white !important;
|
| 815 |
+
border: 2px solid #27ae60 !important;
|
| 816 |
+
border-radius: 8px !important;
|
| 817 |
+
padding: 1rem !important;
|
| 818 |
+
margin: 1rem 0 !important;
|
| 819 |
+
box-shadow: 0 4px 12px rgba(39, 174, 96, 0.2) !important;
|
| 820 |
+
animation: slideIn 0.5s ease-out !important;
|
| 821 |
+
}
|
| 822 |
+
|
| 823 |
+
.notification-warning {
|
| 824 |
+
background: white !important;
|
| 825 |
+
border: 2px solid #f39c12 !important;
|
| 826 |
+
border-radius: 8px !important;
|
| 827 |
+
padding: 1rem !important;
|
| 828 |
+
margin: 1rem 0 !important;
|
| 829 |
+
box-shadow: 0 4px 12px rgba(243, 156, 18, 0.2) !important;
|
| 830 |
+
animation: slideIn 0.5s ease-out !important;
|
| 831 |
+
}
|
| 832 |
+
|
| 833 |
+
.notification-critical {
|
| 834 |
+
background: white !important;
|
| 835 |
+
border: 2px solid #e74c3c !important;
|
| 836 |
+
border-radius: 8px !important;
|
| 837 |
+
padding: 1rem !important;
|
| 838 |
+
margin: 1rem 0 !important;
|
| 839 |
+
box-shadow: 0 4px 12px rgba(231, 76, 60, 0.2) !important;
|
| 840 |
+
animation: slideIn 0.5s ease-out !important;
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
+
@keyframes slideIn {
|
| 844 |
+
from {
|
| 845 |
+
opacity: 0;
|
| 846 |
+
transform: translateY(-20px);
|
| 847 |
+
}
|
| 848 |
+
to {
|
| 849 |
+
opacity: 1;
|
| 850 |
+
transform: translateY(0);
|
| 851 |
+
}
|
| 852 |
+
}
|
| 853 |
+
|
| 854 |
+
/* Group styling */
|
| 855 |
+
.user-selection-group {
|
| 856 |
+
background: #f8f9fa;
|
| 857 |
+
border: 1px solid #e3f2fd;
|
| 858 |
+
border-radius: 8px;
|
| 859 |
+
padding: 1.5rem;
|
| 860 |
+
margin-bottom: 1rem;
|
| 861 |
+
}
|
| 862 |
+
|
| 863 |
+
/* Connection status */
|
| 864 |
+
.connection-status {
|
| 865 |
+
background: #e3f2fd;
|
| 866 |
+
border: 1px solid #bbdefb;
|
| 867 |
+
border-radius: 6px;
|
| 868 |
+
padding: 1rem;
|
| 869 |
+
color: #1565c0;
|
| 870 |
+
font-weight: 500;
|
| 871 |
+
}
|
| 872 |
+
"""
|
| 873 |
+
|
| 874 |
+
with gr.Blocks(
|
| 875 |
+
title="GlycoAI - AI Glucose Insights",
|
| 876 |
+
theme=gr.themes.Soft(
|
| 877 |
+
primary_hue="blue",
|
| 878 |
+
secondary_hue="blue",
|
| 879 |
+
neutral_hue="slate"
|
| 880 |
+
),
|
| 881 |
+
css=custom_css
|
| 882 |
+
) as interface:
|
| 883 |
+
|
| 884 |
+
# Clean Header
|
| 885 |
+
with gr.Row():
|
| 886 |
+
with gr.Column():
|
| 887 |
+
gr.HTML("""
|
| 888 |
+
<div class="main-header">
|
| 889 |
+
<div style="display: flex; align-items: center; justify-content: center; gap: 1rem;">
|
| 890 |
+
<div style="width: 50px; height: 50px; background: white; border-radius: 50%; display: flex; align-items: center; justify-content: center;">
|
| 891 |
+
<span style="color: #3498db; font-size: 20px; font-weight: bold;">π©Ί</span>
|
| 892 |
+
</div>
|
| 893 |
+
<div>
|
| 894 |
+
<h1 style="margin: 0; font-size: 2rem; color: white;">GlycoAI</h1>
|
| 895 |
+
<p style="margin: 0; font-size: 1rem; color: white; opacity: 0.9;">AI-Powered Glucose Insights</p>
|
| 896 |
+
</div>
|
| 897 |
+
</div>
|
| 898 |
+
<p style="margin-top: 1rem; font-size: 0.9rem; color: white; opacity: 0.8;">
|
| 899 |
+
Demo Users + Dexcom Sandbox OAuth β’ Chat with AI for personalized glucose insights
|
| 900 |
+
</p>
|
| 901 |
+
</div>
|
| 902 |
+
""")
|
| 903 |
+
|
| 904 |
+
# User Selection Section - Cleaner Layout
|
| 905 |
+
with gr.Row():
|
| 906 |
+
with gr.Column():
|
| 907 |
+
gr.Markdown("### π₯ Choose Your Data Source")
|
| 908 |
+
|
| 909 |
+
# Demo Users Section
|
| 910 |
+
with gr.Group():
|
| 911 |
+
gr.Markdown("#### π Demo Users")
|
| 912 |
+
gr.Markdown("*Instant access to realistic glucose data for testing*")
|
| 913 |
+
|
| 914 |
+
with gr.Row():
|
| 915 |
+
with gr.Column(scale=1):
|
| 916 |
+
sarah_btn = gr.Button(
|
| 917 |
+
"Sarah Thompson\nG7 Mobile - β οΈ Unstable Control",
|
| 918 |
+
elem_classes=["demo-user-btn"]
|
| 919 |
+
)
|
| 920 |
+
with gr.Column(scale=1):
|
| 921 |
+
marcus_btn = gr.Button(
|
| 922 |
+
"Marcus Rodriguez\nONE+ Mobile - Type 2",
|
| 923 |
+
elem_classes=["demo-user-btn"]
|
| 924 |
+
)
|
| 925 |
+
with gr.Column(scale=1):
|
| 926 |
+
jennifer_btn = gr.Button(
|
| 927 |
+
"Jennifer Chen\nG6 Mobile - Athletic",
|
| 928 |
+
elem_classes=["demo-user-btn"]
|
| 929 |
+
)
|
| 930 |
+
with gr.Column(scale=1):
|
| 931 |
+
robert_btn = gr.Button(
|
| 932 |
+
"Robert Williams\nG6 Receiver - Experienced",
|
| 933 |
+
elem_classes=["demo-user-btn"]
|
| 934 |
+
)
|
| 935 |
+
|
| 936 |
+
# Show/Hide OAuth Toggle
|
| 937 |
+
with gr.Row():
|
| 938 |
+
with gr.Column(scale=4):
|
| 939 |
+
pass
|
| 940 |
+
with gr.Column(scale=2):
|
| 941 |
+
show_oauth_toggle = gr.Checkbox(
|
| 942 |
+
label="Show Dexcom OAuth Options",
|
| 943 |
+
value=False,
|
| 944 |
+
container=False,
|
| 945 |
+
elem_classes=["oauth-toggle"]
|
| 946 |
+
)
|
| 947 |
+
|
| 948 |
+
# Dexcom Sandbox OAuth Section (Collapsible)
|
| 949 |
+
with gr.Group(visible=False) as oauth_section:
|
| 950 |
+
if DEXCOM_SANDBOX_AVAILABLE:
|
| 951 |
+
gr.Markdown("#### π Dexcom Sandbox OAuth")
|
| 952 |
+
gr.Markdown("*Connect with OAuth-authenticated sandbox data*")
|
| 953 |
+
|
| 954 |
+
with gr.Row():
|
| 955 |
+
with gr.Column(scale=2):
|
| 956 |
+
dexcom_sandbox_btn = gr.Button(
|
| 957 |
+
"π Connect Dexcom Sandbox",
|
| 958 |
+
elem_classes=["dexcom-oauth-btn"]
|
| 959 |
+
)
|
| 960 |
+
with gr.Column(scale=3):
|
| 961 |
+
oauth_instructions = gr.Markdown(
|
| 962 |
+
"Click to start Dexcom Sandbox authentication",
|
| 963 |
+
visible=True
|
| 964 |
+
)
|
| 965 |
+
|
| 966 |
+
with gr.Row(visible=False) as oauth_completion_row:
|
| 967 |
+
with gr.Column():
|
| 968 |
+
callback_url_input = gr.Textbox(
|
| 969 |
+
label="Paste Complete Callback URL",
|
| 970 |
+
placeholder="http://localhost:7860/callback?code=ABC123XYZ&state=sandbox_test",
|
| 971 |
+
lines=2
|
| 972 |
+
)
|
| 973 |
+
complete_oauth_btn = gr.Button(
|
| 974 |
+
"β
Complete OAuth",
|
| 975 |
+
elem_classes=["dexcom-oauth-btn"]
|
| 976 |
+
)
|
| 977 |
+
else:
|
| 978 |
+
gr.Markdown("#### π Dexcom Sandbox OAuth")
|
| 979 |
+
gr.Markdown("*Not configured - demo users available*")
|
| 980 |
+
|
| 981 |
+
gr.Button(
|
| 982 |
+
"π Dexcom Sandbox Not Available",
|
| 983 |
+
interactive=False,
|
| 984 |
+
elem_classes=["dexcom-oauth-btn"]
|
| 985 |
+
)
|
| 986 |
+
|
| 987 |
+
# Create dummy variables for consistency
|
| 988 |
+
oauth_instructions = gr.Markdown("", visible=False)
|
| 989 |
+
callback_url_input = gr.Textbox(visible=False)
|
| 990 |
+
complete_oauth_btn = gr.Button(visible=False)
|
| 991 |
+
oauth_completion_row = gr.Row(visible=False)
|
| 992 |
+
|
| 993 |
+
# Connection Status
|
| 994 |
+
with gr.Row():
|
| 995 |
+
with gr.Column():
|
| 996 |
+
connection_status = gr.Textbox(
|
| 997 |
+
label="Connection Status",
|
| 998 |
+
value="No user selected - Choose a demo user or connect Dexcom Sandbox",
|
| 999 |
+
interactive=False,
|
| 1000 |
+
elem_classes=["connection-status"]
|
| 1001 |
+
)
|
| 1002 |
+
|
| 1003 |
+
# Section Divider
|
| 1004 |
+
gr.HTML('<div class="section-divider"></div>')
|
| 1005 |
+
|
| 1006 |
+
# Update button description for Sarah's unstable patterns
|
| 1007 |
+
with gr.Group(visible=False) as main_interface:
|
| 1008 |
+
# Prominent Load Data Button
|
| 1009 |
+
with gr.Row():
|
| 1010 |
+
with gr.Column(scale=1):
|
| 1011 |
+
pass # Left spacer
|
| 1012 |
+
with gr.Column(scale=2):
|
| 1013 |
+
load_data_btn = gr.Button(
|
| 1014 |
+
"π Load 14-Day Glucose Data\nπ Start Analysis & Enable AI Chat",
|
| 1015 |
+
elem_classes=["load-data-btn"]
|
| 1016 |
+
)
|
| 1017 |
+
with gr.Column(scale=1):
|
| 1018 |
+
pass # Right spacer
|
| 1019 |
+
|
| 1020 |
+
# Notification area for data loading feedback
|
| 1021 |
+
with gr.Row():
|
| 1022 |
+
notification_area = gr.Markdown(
|
| 1023 |
+
"",
|
| 1024 |
+
visible=False,
|
| 1025 |
+
elem_classes=["notification-success"]
|
| 1026 |
+
)
|
| 1027 |
+
|
| 1028 |
+
# Section Divider
|
| 1029 |
+
gr.HTML('<div class="section-divider"></div>')
|
| 1030 |
+
|
| 1031 |
+
# Main Content Tabs - Reordered with Chat first
|
| 1032 |
+
with gr.Tabs():
|
| 1033 |
+
# Chat Tab - FIRST for priority
|
| 1034 |
+
with gr.TabItem("π¬ Chat with AI"):
|
| 1035 |
+
with gr.Column():
|
| 1036 |
+
gr.Markdown("### π€ Chat with GlycoAI")
|
| 1037 |
+
|
| 1038 |
+
# Chat Interface with integrated demo prompts
|
| 1039 |
+
chatbot = gr.Chatbot(
|
| 1040 |
+
label="π¬ Chat with GlycoAI",
|
| 1041 |
+
height=450,
|
| 1042 |
+
show_label=False,
|
| 1043 |
+
container=True,
|
| 1044 |
+
bubble_full_width=False,
|
| 1045 |
+
avatar_images=(None, "π©Ί")
|
| 1046 |
+
)
|
| 1047 |
+
|
| 1048 |
+
# Chat Input
|
| 1049 |
+
with gr.Row():
|
| 1050 |
+
chat_input = gr.Textbox(
|
| 1051 |
+
placeholder="Ask about your glucose patterns, trends, or management strategies...",
|
| 1052 |
+
label="Your Question",
|
| 1053 |
+
lines=2,
|
| 1054 |
+
scale=4
|
| 1055 |
+
)
|
| 1056 |
+
send_btn = gr.Button(
|
| 1057 |
+
"Send",
|
| 1058 |
+
variant="primary",
|
| 1059 |
+
scale=1
|
| 1060 |
+
)
|
| 1061 |
+
|
| 1062 |
+
# Chat Controls
|
| 1063 |
+
with gr.Row():
|
| 1064 |
+
clear_chat_btn = gr.Button(
|
| 1065 |
+
"ποΈ Clear Chat",
|
| 1066 |
+
size="sm"
|
| 1067 |
+
)
|
| 1068 |
+
gr.Markdown("*AI responses are for informational purposes only. Always consult your healthcare provider.*")
|
| 1069 |
+
|
| 1070 |
+
# Data Overview Tab - SECOND
|
| 1071 |
+
with gr.TabItem("π Data Overview"):
|
| 1072 |
+
with gr.Column():
|
| 1073 |
+
gr.Markdown("### π Comprehensive Data Analysis")
|
| 1074 |
+
|
| 1075 |
+
data_display = gr.Markdown(
|
| 1076 |
+
"Load your glucose data to see detailed statistics and insights",
|
| 1077 |
+
container=True
|
| 1078 |
+
)
|
| 1079 |
+
|
| 1080 |
+
# Glucose Chart Tab - THIRD
|
| 1081 |
+
with gr.TabItem("π Glucose Chart"):
|
| 1082 |
+
with gr.Column():
|
| 1083 |
+
gr.Markdown("### π Interactive 14-Day Glucose Analysis")
|
| 1084 |
+
|
| 1085 |
+
glucose_chart = gr.Plot(
|
| 1086 |
+
label="Interactive Glucose Trends",
|
| 1087 |
+
container=True
|
| 1088 |
+
)
|
| 1089 |
+
|
| 1090 |
+
# Event Handlers
|
| 1091 |
+
def handle_demo_user_selection(user_key):
|
| 1092 |
+
status, interface_visibility = app.select_demo_user(user_key)
|
| 1093 |
+
initial_chat = app.initialize_chat_with_prompts()
|
| 1094 |
+
return status, interface_visibility, initial_chat
|
| 1095 |
+
|
| 1096 |
+
def handle_load_data():
|
| 1097 |
+
overview, chart, notification = app.load_glucose_data()
|
| 1098 |
+
|
| 1099 |
+
# Determine notification class based on content
|
| 1100 |
+
if "CONCERNING PATTERNS" in notification or "CRITICAL" in notification:
|
| 1101 |
+
notification_class = "notification-critical"
|
| 1102 |
+
elif "EXCELLENT CONTROL" in notification:
|
| 1103 |
+
notification_class = "notification-success"
|
| 1104 |
+
elif notification:
|
| 1105 |
+
notification_class = "notification-warning"
|
| 1106 |
+
else:
|
| 1107 |
+
notification_class = "notification-success"
|
| 1108 |
+
|
| 1109 |
+
# Show notification with appropriate styling
|
| 1110 |
+
notification_update = gr.update(
|
| 1111 |
+
value=notification,
|
| 1112 |
+
visible=bool(notification),
|
| 1113 |
+
elem_classes=[notification_class]
|
| 1114 |
+
)
|
| 1115 |
+
|
| 1116 |
+
return overview, chart, notification_update
|
| 1117 |
+
|
| 1118 |
+
def handle_chat_submit(message, history):
|
| 1119 |
+
return app.chat_with_mistral(message, history)
|
| 1120 |
+
|
| 1121 |
+
def handle_enter_key(message, history):
|
| 1122 |
+
if message.strip():
|
| 1123 |
+
return app.chat_with_mistral(message, history)
|
| 1124 |
+
return "", history
|
| 1125 |
+
|
| 1126 |
+
def handle_chatbot_click(history, evt: gr.SelectData):
|
| 1127 |
+
"""Handle clicking on chat bubbles (demo prompts)"""
|
| 1128 |
+
if evt.index is not None and len(history) > evt.index[0]:
|
| 1129 |
+
clicked_message = history[evt.index[0]][1] # Get AI message
|
| 1130 |
+
|
| 1131 |
+
# Check if it's a demo prompt (contains ** formatting)
|
| 1132 |
+
if "**" in clicked_message and ("π―" in clicked_message or "β‘" in clicked_message or "π½οΈ" in clicked_message):
|
| 1133 |
+
return app.handle_demo_prompt_click(clicked_message, history)
|
| 1134 |
+
|
| 1135 |
+
return "", history
|
| 1136 |
+
|
| 1137 |
+
# Toggle OAuth section visibility
|
| 1138 |
+
show_oauth_toggle.change(
|
| 1139 |
+
lambda show: gr.update(visible=show),
|
| 1140 |
+
inputs=[show_oauth_toggle],
|
| 1141 |
+
outputs=[oauth_section]
|
| 1142 |
+
)
|
| 1143 |
+
|
| 1144 |
+
# Connect Event Handlers for Demo Users
|
| 1145 |
+
sarah_btn.click(
|
| 1146 |
+
lambda: handle_demo_user_selection("sarah_g7"),
|
| 1147 |
+
outputs=[connection_status, main_interface, chatbot]
|
| 1148 |
+
)
|
| 1149 |
+
|
| 1150 |
+
marcus_btn.click(
|
| 1151 |
+
lambda: handle_demo_user_selection("marcus_one"),
|
| 1152 |
+
outputs=[connection_status, main_interface, chatbot]
|
| 1153 |
+
)
|
| 1154 |
+
|
| 1155 |
+
jennifer_btn.click(
|
| 1156 |
+
lambda: handle_demo_user_selection("jennifer_g6"),
|
| 1157 |
+
outputs=[connection_status, main_interface, chatbot]
|
| 1158 |
+
)
|
| 1159 |
+
|
| 1160 |
+
robert_btn.click(
|
| 1161 |
+
lambda: handle_demo_user_selection("robert_receiver"),
|
| 1162 |
+
outputs=[connection_status, main_interface, chatbot]
|
| 1163 |
+
)
|
| 1164 |
+
|
| 1165 |
+
# Connect Event Handlers for Dexcom Sandbox OAuth
|
| 1166 |
+
if DEXCOM_SANDBOX_AVAILABLE:
|
| 1167 |
+
dexcom_sandbox_btn.click(
|
| 1168 |
+
app.start_dexcom_sandbox_oauth,
|
| 1169 |
+
outputs=[oauth_instructions]
|
| 1170 |
+
).then(
|
| 1171 |
+
lambda: gr.update(visible=True),
|
| 1172 |
+
outputs=[oauth_completion_row]
|
| 1173 |
+
)
|
| 1174 |
+
|
| 1175 |
+
complete_oauth_btn.click(
|
| 1176 |
+
app.complete_dexcom_sandbox_oauth,
|
| 1177 |
+
inputs=[callback_url_input],
|
| 1178 |
+
outputs=[connection_status, main_interface]
|
| 1179 |
+
).then(
|
| 1180 |
+
app.initialize_chat_with_prompts, # Initialize chat with prompts after OAuth
|
| 1181 |
+
outputs=[chatbot]
|
| 1182 |
+
)
|
| 1183 |
+
|
| 1184 |
+
# Data Loading
|
| 1185 |
+
load_data_btn.click(
|
| 1186 |
+
handle_load_data,
|
| 1187 |
+
outputs=[data_display, glucose_chart, notification_area]
|
| 1188 |
+
)
|
| 1189 |
+
|
| 1190 |
+
# Chat Handlers
|
| 1191 |
+
send_btn.click(
|
| 1192 |
+
handle_chat_submit,
|
| 1193 |
+
inputs=[chat_input, chatbot],
|
| 1194 |
+
outputs=[chat_input, chatbot]
|
| 1195 |
+
)
|
| 1196 |
+
|
| 1197 |
+
chat_input.submit(
|
| 1198 |
+
handle_enter_key,
|
| 1199 |
+
inputs=[chat_input, chatbot],
|
| 1200 |
+
outputs=[chat_input, chatbot]
|
| 1201 |
+
)
|
| 1202 |
+
|
| 1203 |
+
# Handle clicking on chat bubbles (demo prompts)
|
| 1204 |
+
chatbot.select(
|
| 1205 |
+
handle_chatbot_click,
|
| 1206 |
+
inputs=[chatbot],
|
| 1207 |
+
outputs=[chat_input, chatbot]
|
| 1208 |
+
)
|
| 1209 |
+
|
| 1210 |
+
# Clear Chat
|
| 1211 |
+
clear_chat_btn.click(
|
| 1212 |
+
app.clear_chat_history,
|
| 1213 |
+
outputs=[chatbot]
|
| 1214 |
+
)
|
| 1215 |
+
|
| 1216 |
+
# Clean Footer
|
| 1217 |
+
with gr.Row():
|
| 1218 |
+
gr.HTML(f"""
|
| 1219 |
+
<div style="text-align: center; padding: 1.5rem; margin-top: 2rem; border-top: 1px solid #e3f2fd; color: #546e7a;">
|
| 1220 |
+
<p><strong>β οΈ Medical Disclaimer</strong></p>
|
| 1221 |
+
<p style="font-size: 0.9rem;">GlycoAI is for informational and educational purposes only. Always consult your healthcare provider
|
| 1222 |
+
before making any changes to your diabetes management plan.</p>
|
| 1223 |
+
<p style="margin-top: 1rem; font-size: 0.85rem; color: #78909c;">
|
| 1224 |
+
π Data processed securely β’ π‘ Powered by Dexcom API & Mistral AI<br>
|
| 1225 |
+
π Demo: Available β’ π Dexcom Sandbox: {"Available" if DEXCOM_SANDBOX_AVAILABLE else "Not configured"}
|
| 1226 |
+
</p>
|
| 1227 |
+
</div>
|
| 1228 |
+
""")
|
| 1229 |
+
|
| 1230 |
+
return interface
|
| 1231 |
+
|
| 1232 |
+
|
| 1233 |
+
def main():
|
| 1234 |
+
"""Main function to launch the application"""
|
| 1235 |
+
print("π Starting GlycoAI - AI-Powered Glucose Insights...")
|
| 1236 |
+
|
| 1237 |
+
# Check OAuth availability
|
| 1238 |
+
oauth_status = "β
Available" if DEXCOM_SANDBOX_AVAILABLE else "β Not configured"
|
| 1239 |
+
print(f"π― Dexcom Sandbox OAuth: {oauth_status}")
|
| 1240 |
+
|
| 1241 |
+
# Validate environment before starting
|
| 1242 |
+
print("π Validating environment configuration...")
|
| 1243 |
+
if not validate_environment():
|
| 1244 |
+
print("β Environment validation failed!")
|
| 1245 |
+
print("Please check your .env file or environment variables.")
|
| 1246 |
+
return
|
| 1247 |
+
|
| 1248 |
+
print("β
Environment validation passed!")
|
| 1249 |
+
|
| 1250 |
+
try:
|
| 1251 |
+
# Create and launch the interface
|
| 1252 |
+
demo = create_interface()
|
| 1253 |
+
|
| 1254 |
+
print("π― GlycoAI Features:")
|
| 1255 |
+
print("π Clean UI with blue theme, consistent button sizes, improved readability")
|
| 1256 |
+
print("π Demo users: 4 realistic profiles for instant testing")
|
| 1257 |
+
if DEXCOM_SANDBOX_AVAILABLE:
|
| 1258 |
+
print("β
Dexcom Sandbox: Available - OAuth authentication ready")
|
| 1259 |
+
else:
|
| 1260 |
+
print("π Dexcom Sandbox: Not configured - demo users only")
|
| 1261 |
+
|
| 1262 |
+
# Launch with custom settings
|
| 1263 |
+
demo.launch(
|
| 1264 |
+
server_name="0.0.0.0", # Allow external access
|
| 1265 |
+
server_port=7860, # Your port
|
| 1266 |
+
share=True, # Set to True for public sharing (tunneling)
|
| 1267 |
+
debug=os.getenv("DEBUG", "false").lower() == "true",
|
| 1268 |
+
show_error=True, # Show errors in the interface
|
| 1269 |
+
auth=None, # No authentication required
|
| 1270 |
+
favicon_path=None, # Use default favicon
|
| 1271 |
+
ssl_verify=False # Disable SSL verification for development
|
| 1272 |
+
)
|
| 1273 |
+
|
| 1274 |
+
except Exception as e:
|
| 1275 |
+
logger.error(f"Failed to launch GlycoAI application: {e}")
|
| 1276 |
+
print(f"β Error launching application: {e}")
|
| 1277 |
+
|
| 1278 |
+
# Provide helpful error information
|
| 1279 |
+
if "environment" in str(e).lower():
|
| 1280 |
+
print("\nπ‘ Environment troubleshooting:")
|
| 1281 |
+
print("1. Check if .env file exists with MISTRAL_API_KEY")
|
| 1282 |
+
print("2. Verify your API key is valid")
|
| 1283 |
+
print("3. For Hugging Face Spaces, check Repository secrets")
|
| 1284 |
+
else:
|
| 1285 |
+
print("\nπ‘ Try checking:")
|
| 1286 |
+
print("1. All dependencies are installed: pip install -r requirements.txt")
|
| 1287 |
+
print("2. Port 7860 is available")
|
| 1288 |
+
print("3. Check the logs above for specific error details")
|
| 1289 |
+
|
| 1290 |
+
raise
|
| 1291 |
+
|
| 1292 |
+
|
| 1293 |
+
if __name__ == "__main__":
|
| 1294 |
+
# Setup logging configuration
|
| 1295 |
+
log_level = os.getenv("LOG_LEVEL", "INFO")
|
| 1296 |
+
logging.basicConfig(
|
| 1297 |
+
level=getattr(logging, log_level.upper()),
|
| 1298 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 1299 |
+
handlers=[
|
| 1300 |
+
logging.FileHandler('glycoai.log'),
|
| 1301 |
+
logging.StreamHandler()
|
| 1302 |
+
]
|
| 1303 |
+
)
|
| 1304 |
+
|
| 1305 |
+
# Run the main application
|
| 1306 |
+
main()
|
dexcom_sandbox_oauth.py
ADDED
|
@@ -0,0 +1,790 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Dexcom Sandbox OAuth Integration
|
| 4 |
+
Complete implementation for Dexcom Sandbox authentication with user selection
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import requests
|
| 9 |
+
import urllib.parse
|
| 10 |
+
import json
|
| 11 |
+
import secrets
|
| 12 |
+
import webbrowser
|
| 13 |
+
import logging
|
| 14 |
+
from datetime import datetime, timedelta
|
| 15 |
+
from typing import Dict, List, Optional, Tuple, Any
|
| 16 |
+
from dataclasses import dataclass
|
| 17 |
+
|
| 18 |
+
# Setup logging
|
| 19 |
+
logging.basicConfig(level=logging.INFO)
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
# Load environment variables
|
| 23 |
+
from dotenv import load_dotenv
|
| 24 |
+
load_dotenv()
|
| 25 |
+
|
| 26 |
+
# Dexcom Sandbox Configuration
|
| 27 |
+
CLIENT_ID = os.getenv("DEXCOM_CLIENT_ID", "mLElKHKRwRDVUrAOPBzktFGY7qkTc7Zm")
|
| 28 |
+
CLIENT_SECRET = os.getenv("DEXCOM_CLIENT_SECRET", "HmFpgyVweuwKrQpf")
|
| 29 |
+
REDIRECT_URI = "http://localhost:7860/callback"
|
| 30 |
+
|
| 31 |
+
# Dexcom Sandbox Users (selection-based, no passwords)
|
| 32 |
+
SANDBOX_USERS = {
|
| 33 |
+
"user1": "SandboxUser1",
|
| 34 |
+
"user2": "SandboxUser2",
|
| 35 |
+
"user3": "SandboxUser3",
|
| 36 |
+
"user4": "SandboxUser4",
|
| 37 |
+
"user5": "SandboxUser5",
|
| 38 |
+
"user6": "SandboxUser6 (Dexcom G6)",
|
| 39 |
+
"user7": "SandboxUser7 (Dexcom G7)",
|
| 40 |
+
"user8": "SandboxUser8"
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
# Dexcom API Endpoints - Following Official Documentation
|
| 44 |
+
# Based on https://developer.dexcom.com/docs/dexcomv2/endpoint-overview/ and v3
|
| 45 |
+
|
| 46 |
+
# OAuth endpoints are shared between v2 and v3 (always v2 OAuth)
|
| 47 |
+
OAUTH_ENDPOINTS = {
|
| 48 |
+
"login": "https://api.dexcom.com/v2/oauth2/login", # Production OAuth login
|
| 49 |
+
"token": "https://api.dexcom.com/v2/oauth2/token", # Production OAuth token
|
| 50 |
+
"sandbox_login": "https://developer.dexcom.com/sandbox-login" # Special sandbox login
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
# API Base URLs per official documentation
|
| 54 |
+
API_BASE_URLS = {
|
| 55 |
+
"production": "https://api.dexcom.com",
|
| 56 |
+
"sandbox": "https://sandbox-api.dexcom.com"
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
# Recommended configuration (Sandbox login + Production OAuth + Sandbox API)
|
| 60 |
+
DEFAULT_ENDPOINTS = {
|
| 61 |
+
"login": "https://developer.dexcom.com/sandbox-login", # Special sandbox login
|
| 62 |
+
"token": "https://api.dexcom.com/v2/oauth2/token", # Production OAuth (works for sandbox too)
|
| 63 |
+
"api_v2": "https://sandbox-api.dexcom.com/v2", # Sandbox API v2
|
| 64 |
+
"api_v3": "https://sandbox-api.dexcom.com/v3" # Sandbox API v3
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
# Alternative configurations for troubleshooting
|
| 68 |
+
ENDPOINT_CONFIGURATIONS = [
|
| 69 |
+
{
|
| 70 |
+
"name": "Sandbox Login + Production OAuth + Sandbox API (Recommended)",
|
| 71 |
+
"login": "https://developer.dexcom.com/sandbox-login",
|
| 72 |
+
"token": "https://api.dexcom.com/v2/oauth2/token",
|
| 73 |
+
"api_v2": "https://sandbox-api.dexcom.com/v2",
|
| 74 |
+
"api_v3": "https://sandbox-api.dexcom.com/v3"
|
| 75 |
+
},
|
| 76 |
+
{
|
| 77 |
+
"name": "All Production OAuth + Production API",
|
| 78 |
+
"login": "https://api.dexcom.com/v2/oauth2/login",
|
| 79 |
+
"token": "https://api.dexcom.com/v2/oauth2/token",
|
| 80 |
+
"api_v2": "https://api.dexcom.com/v2",
|
| 81 |
+
"api_v3": "https://api.dexcom.com/v3"
|
| 82 |
+
},
|
| 83 |
+
{
|
| 84 |
+
"name": "All Sandbox (May not work)",
|
| 85 |
+
"login": "https://sandbox-api.dexcom.com/v2/oauth2/login",
|
| 86 |
+
"token": "https://sandbox-api.dexcom.com/v2/oauth2/token",
|
| 87 |
+
"api_v2": "https://sandbox-api.dexcom.com/v2",
|
| 88 |
+
"api_v3": "https://sandbox-api.dexcom.com/v3"
|
| 89 |
+
}
|
| 90 |
+
]
|
| 91 |
+
|
| 92 |
+
@dataclass
|
| 93 |
+
class DexcomSandboxUser:
|
| 94 |
+
"""Profile for Dexcom Sandbox User"""
|
| 95 |
+
name: str = "Dexcom Sandbox User"
|
| 96 |
+
age: int = 35
|
| 97 |
+
device_type: str = "Dexcom G6 (Sandbox)"
|
| 98 |
+
username: str = "sandbox_user"
|
| 99 |
+
password: str = "selection_based"
|
| 100 |
+
description: str = "Dexcom Sandbox User with OAuth authentication"
|
| 101 |
+
diabetes_type: str = "Type 1"
|
| 102 |
+
years_with_diabetes: int = 8
|
| 103 |
+
typical_glucose_pattern: str = "sandbox_data"
|
| 104 |
+
auth_type: str = "dexcom_sandbox"
|
| 105 |
+
|
| 106 |
+
class DexcomSandboxOAuth:
|
| 107 |
+
"""
|
| 108 |
+
Dexcom Sandbox OAuth implementation with user selection
|
| 109 |
+
|
| 110 |
+
Flow:
|
| 111 |
+
1. Generate auth URL β developer.dexcom.com/sandbox-login
|
| 112 |
+
2. User selects sandbox user from dropdown (no password)
|
| 113 |
+
3. Get authorization code from callback
|
| 114 |
+
4. Exchange code for token β sandbox-api.dexcom.com/v2/oauth2/token
|
| 115 |
+
5. Use token for API calls β sandbox-api.dexcom.com/v2/...
|
| 116 |
+
"""
|
| 117 |
+
|
| 118 |
+
def __init__(self, api_version: str = "v3"):
|
| 119 |
+
self.client_id = CLIENT_ID
|
| 120 |
+
self.client_secret = CLIENT_SECRET
|
| 121 |
+
self.redirect_uri = REDIRECT_URI
|
| 122 |
+
self.api_version = api_version # "v2" or "v3"
|
| 123 |
+
|
| 124 |
+
# OAuth state
|
| 125 |
+
self.access_token = None
|
| 126 |
+
self.refresh_token = None
|
| 127 |
+
self.token_expires_at = None
|
| 128 |
+
self.state = None
|
| 129 |
+
|
| 130 |
+
# Use recommended configuration (sandbox login + production OAuth + sandbox API)
|
| 131 |
+
self.working_endpoints = DEFAULT_ENDPOINTS.copy()
|
| 132 |
+
|
| 133 |
+
logger.info(f"β
Dexcom Sandbox OAuth initialized (API {api_version})")
|
| 134 |
+
logger.info(f" Client ID: {self.client_id[:8]}...")
|
| 135 |
+
logger.info(f" Redirect URI: {self.redirect_uri}")
|
| 136 |
+
|
| 137 |
+
if api_version == "v3":
|
| 138 |
+
logger.info(" Using API v3 for G6, G7, ONE, ONE+ device support")
|
| 139 |
+
else:
|
| 140 |
+
logger.info(" Using API v2 for G5, G6 device support")
|
| 141 |
+
|
| 142 |
+
def get_api_base_url(self) -> str:
|
| 143 |
+
"""Get the correct API base URL for the current version"""
|
| 144 |
+
if self.api_version == "v3":
|
| 145 |
+
return self.working_endpoints["api_v3"]
|
| 146 |
+
else:
|
| 147 |
+
return self.working_endpoints["api_v2"]
|
| 148 |
+
|
| 149 |
+
def generate_auth_url(self) -> str:
|
| 150 |
+
"""Generate OAuth authorization URL for Dexcom Sandbox"""
|
| 151 |
+
# Generate secure state parameter
|
| 152 |
+
self.state = secrets.token_urlsafe(32)
|
| 153 |
+
|
| 154 |
+
# OAuth parameters
|
| 155 |
+
params = {
|
| 156 |
+
'client_id': self.client_id,
|
| 157 |
+
'redirect_uri': self.redirect_uri,
|
| 158 |
+
'response_type': 'code',
|
| 159 |
+
'scope': 'offline_access',
|
| 160 |
+
'state': self.state
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
# Build auth URL using current working endpoints
|
| 164 |
+
query_string = urllib.parse.urlencode(params)
|
| 165 |
+
auth_url = f"{self.working_endpoints['login']}?{query_string}"
|
| 166 |
+
|
| 167 |
+
logger.info(f"π Generated auth URL: {auth_url}")
|
| 168 |
+
return auth_url
|
| 169 |
+
|
| 170 |
+
def start_oauth_flow(self) -> Dict[str, Any]:
|
| 171 |
+
"""Start the Dexcom Sandbox OAuth flow"""
|
| 172 |
+
try:
|
| 173 |
+
auth_url = self.generate_auth_url()
|
| 174 |
+
|
| 175 |
+
# Try to open browser automatically
|
| 176 |
+
try:
|
| 177 |
+
webbrowser.open(auth_url)
|
| 178 |
+
browser_opened = True
|
| 179 |
+
logger.info("β
Browser opened automatically")
|
| 180 |
+
except Exception as e:
|
| 181 |
+
browser_opened = False
|
| 182 |
+
logger.warning(f"β οΈ Could not open browser: {e}")
|
| 183 |
+
|
| 184 |
+
return {
|
| 185 |
+
"success": True,
|
| 186 |
+
"auth_url": auth_url,
|
| 187 |
+
"browser_opened": browser_opened,
|
| 188 |
+
"state": self.state,
|
| 189 |
+
"instructions": self._get_oauth_instructions(auth_url, browser_opened)
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
except Exception as e:
|
| 193 |
+
logger.error(f"Failed to start OAuth flow: {e}")
|
| 194 |
+
return {
|
| 195 |
+
"success": False,
|
| 196 |
+
"error": f"Failed to start OAuth: {str(e)}"
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
def _get_oauth_instructions(self, auth_url: str, browser_opened: bool) -> str:
|
| 200 |
+
"""Generate user-friendly OAuth instructions"""
|
| 201 |
+
browser_status = "β
Browser opened automatically" if browser_opened else "β οΈ Please open URL manually"
|
| 202 |
+
|
| 203 |
+
return f"""
|
| 204 |
+
π **Dexcom Sandbox OAuth Started**
|
| 205 |
+
|
| 206 |
+
{browser_status}
|
| 207 |
+
|
| 208 |
+
**π Step-by-Step Instructions:**
|
| 209 |
+
1. π Browser should open to: {auth_url}
|
| 210 |
+
2. π₯ **Select a Sandbox User** from the dropdown:
|
| 211 |
+
β’ **SandboxUser6** - Dexcom G6 device (recommended)
|
| 212 |
+
β’ **SandboxUser7** - Dexcom G7 device
|
| 213 |
+
β’ **SandboxUser1-8** - Various test scenarios
|
| 214 |
+
3. β
Click "Authorize" to grant access
|
| 215 |
+
4. β **You will get a 404 error - THIS IS EXPECTED!**
|
| 216 |
+
5. π **Copy the COMPLETE URL** from your browser's address bar
|
| 217 |
+
6. π₯ Paste the URL below and click "Complete OAuth"
|
| 218 |
+
|
| 219 |
+
**π± Example callback URL:**
|
| 220 |
+
`http://localhost:7860/callback?code=ABC123XYZ&state=your_state_here`
|
| 221 |
+
|
| 222 |
+
**π― Important:**
|
| 223 |
+
- No password needed - just select a user and authorize
|
| 224 |
+
- Copy the **entire URL** (not just the code part)
|
| 225 |
+
- SandboxUser6 = Dexcom G6 device data (most common)
|
| 226 |
+
"""
|
| 227 |
+
|
| 228 |
+
def complete_oauth(self, callback_url: str) -> Dict[str, Any]:
|
| 229 |
+
"""Complete OAuth by processing callback URL and exchanging code for token"""
|
| 230 |
+
if not callback_url or not callback_url.strip():
|
| 231 |
+
return {
|
| 232 |
+
"success": False,
|
| 233 |
+
"error": "Please provide the callback URL from your browser"
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
try:
|
| 237 |
+
# Extract authorization code from callback URL
|
| 238 |
+
auth_code = self._extract_auth_code(callback_url)
|
| 239 |
+
|
| 240 |
+
if not auth_code:
|
| 241 |
+
return {
|
| 242 |
+
"success": False,
|
| 243 |
+
"error": "Could not extract authorization code from URL"
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
# Validate state parameter
|
| 247 |
+
callback_state = self._extract_state(callback_url)
|
| 248 |
+
if callback_state != self.state:
|
| 249 |
+
logger.warning(f"State mismatch: expected {self.state}, got {callback_state}")
|
| 250 |
+
# Continue anyway for sandbox testing
|
| 251 |
+
|
| 252 |
+
# Exchange code for tokens
|
| 253 |
+
token_result = self._exchange_code_for_tokens(auth_code)
|
| 254 |
+
|
| 255 |
+
if token_result["success"]:
|
| 256 |
+
logger.info("β
Dexcom Sandbox OAuth completed successfully")
|
| 257 |
+
return {
|
| 258 |
+
"success": True,
|
| 259 |
+
"message": "β
Dexcom Sandbox authentication successful",
|
| 260 |
+
"access_token": self.access_token,
|
| 261 |
+
"token_expires_at": self.token_expires_at,
|
| 262 |
+
"user_profile": DexcomSandboxUser()
|
| 263 |
+
}
|
| 264 |
+
else:
|
| 265 |
+
return token_result
|
| 266 |
+
|
| 267 |
+
except Exception as e:
|
| 268 |
+
logger.error(f"OAuth completion failed: {e}")
|
| 269 |
+
return {
|
| 270 |
+
"success": False,
|
| 271 |
+
"error": f"OAuth completion failed: {str(e)}"
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
def _extract_auth_code(self, callback_url: str) -> Optional[str]:
|
| 275 |
+
"""Extract authorization code from callback URL"""
|
| 276 |
+
try:
|
| 277 |
+
parsed_url = urllib.parse.urlparse(callback_url)
|
| 278 |
+
query_params = urllib.parse.parse_qs(parsed_url.query)
|
| 279 |
+
|
| 280 |
+
if 'code' in query_params:
|
| 281 |
+
code = query_params['code'][0]
|
| 282 |
+
logger.info(f"β
Extracted auth code: {code[:15]}...")
|
| 283 |
+
return code
|
| 284 |
+
else:
|
| 285 |
+
logger.error("No 'code' parameter found in callback URL")
|
| 286 |
+
return None
|
| 287 |
+
|
| 288 |
+
except Exception as e:
|
| 289 |
+
logger.error(f"Error extracting auth code: {e}")
|
| 290 |
+
return None
|
| 291 |
+
|
| 292 |
+
def _extract_state(self, callback_url: str) -> Optional[str]:
|
| 293 |
+
"""Extract state parameter from callback URL"""
|
| 294 |
+
try:
|
| 295 |
+
parsed_url = urllib.parse.urlparse(callback_url)
|
| 296 |
+
query_params = urllib.parse.parse_qs(parsed_url.query)
|
| 297 |
+
|
| 298 |
+
if 'state' in query_params:
|
| 299 |
+
return query_params['state'][0]
|
| 300 |
+
else:
|
| 301 |
+
return None
|
| 302 |
+
|
| 303 |
+
except Exception as e:
|
| 304 |
+
logger.error(f"Error extracting state: {e}")
|
| 305 |
+
return None
|
| 306 |
+
|
| 307 |
+
def _exchange_code_for_tokens(self, auth_code: str) -> Dict[str, Any]:
|
| 308 |
+
"""Exchange authorization code for access token following Dexcom API v3 guidelines"""
|
| 309 |
+
|
| 310 |
+
# Try sandbox configuration first, then alternatives
|
| 311 |
+
configurations_to_try = [self.working_endpoints] + ENDPOINT_CONFIGURATIONS
|
| 312 |
+
|
| 313 |
+
for i, endpoint_config in enumerate(configurations_to_try):
|
| 314 |
+
endpoint_name = endpoint_config.get('name', f'Sandbox Config {i+1}')
|
| 315 |
+
token_url = endpoint_config["token"]
|
| 316 |
+
|
| 317 |
+
logger.info(f"π Attempting token exchange #{i+1}: {endpoint_name}")
|
| 318 |
+
logger.info(f" Token URL: {token_url}")
|
| 319 |
+
|
| 320 |
+
# Token exchange data per Dexcom OAuth2 spec
|
| 321 |
+
data = {
|
| 322 |
+
'client_id': self.client_id,
|
| 323 |
+
'client_secret': self.client_secret,
|
| 324 |
+
'code': auth_code,
|
| 325 |
+
'grant_type': 'authorization_code',
|
| 326 |
+
'redirect_uri': self.redirect_uri
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
# Headers per Dexcom API guidelines
|
| 330 |
+
headers = {
|
| 331 |
+
'Content-Type': 'application/x-www-form-urlencoded',
|
| 332 |
+
'Accept': 'application/json',
|
| 333 |
+
'User-Agent': 'GlycoAI-DexcomSandbox/1.0',
|
| 334 |
+
'Cache-Control': 'no-cache'
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
try:
|
| 338 |
+
logger.info(f"π€ Auth code: {auth_code[:15]}...")
|
| 339 |
+
|
| 340 |
+
response = requests.post(token_url, data=data, headers=headers, timeout=30)
|
| 341 |
+
|
| 342 |
+
logger.info(f"π¨ Response status: {response.status_code}")
|
| 343 |
+
logger.info(f"π¨ Response headers: {dict(response.headers)}")
|
| 344 |
+
|
| 345 |
+
if response.status_code == 200:
|
| 346 |
+
try:
|
| 347 |
+
token_data = response.json()
|
| 348 |
+
except json.JSONDecodeError:
|
| 349 |
+
logger.error(f"β Invalid JSON response: {response.text}")
|
| 350 |
+
continue
|
| 351 |
+
|
| 352 |
+
# Store tokens
|
| 353 |
+
self.access_token = token_data.get('access_token')
|
| 354 |
+
self.refresh_token = token_data.get('refresh_token')
|
| 355 |
+
|
| 356 |
+
if not self.access_token:
|
| 357 |
+
logger.error(f"β No access_token in response: {token_data}")
|
| 358 |
+
continue
|
| 359 |
+
|
| 360 |
+
# Calculate expiration
|
| 361 |
+
expires_in = token_data.get('expires_in', 3600) # Default 1 hour
|
| 362 |
+
self.token_expires_at = datetime.now() + timedelta(seconds=expires_in)
|
| 363 |
+
|
| 364 |
+
logger.info(f"β
Token exchange successful with {endpoint_name}!")
|
| 365 |
+
logger.info(f" Access token: {self.access_token[:25]}...")
|
| 366 |
+
logger.info(f" Token type: {token_data.get('token_type', 'Bearer')}")
|
| 367 |
+
logger.info(f" Expires in: {expires_in} seconds ({expires_in/3600:.1f} hours)")
|
| 368 |
+
logger.info(f" Scope: {token_data.get('scope', 'offline_access')}")
|
| 369 |
+
|
| 370 |
+
# Update working endpoints to use the successful configuration
|
| 371 |
+
self.working_endpoints = endpoint_config.copy()
|
| 372 |
+
|
| 373 |
+
return {
|
| 374 |
+
"success": True,
|
| 375 |
+
"access_token": self.access_token,
|
| 376 |
+
"refresh_token": self.refresh_token,
|
| 377 |
+
"expires_in": expires_in,
|
| 378 |
+
"working_endpoint": endpoint_config,
|
| 379 |
+
"token_type": token_data.get('token_type', 'Bearer'),
|
| 380 |
+
"scope": token_data.get('scope', 'offline_access')
|
| 381 |
+
}
|
| 382 |
+
else:
|
| 383 |
+
# Log detailed error information
|
| 384 |
+
error_text = response.text
|
| 385 |
+
try:
|
| 386 |
+
error_data = response.json()
|
| 387 |
+
error_message = error_data.get('error_description',
|
| 388 |
+
error_data.get('error', 'Unknown error'))
|
| 389 |
+
error_code = error_data.get('error', 'unknown_error')
|
| 390 |
+
except:
|
| 391 |
+
error_message = error_text
|
| 392 |
+
error_code = f"http_{response.status_code}"
|
| 393 |
+
|
| 394 |
+
logger.warning(f"β {endpoint_name} failed: {response.status_code} - {error_message}")
|
| 395 |
+
|
| 396 |
+
# Specific error handling based on Dexcom API behavior
|
| 397 |
+
if response.status_code == 404:
|
| 398 |
+
logger.info(f" β Token endpoint not found, trying next configuration...")
|
| 399 |
+
continue
|
| 400 |
+
elif response.status_code == 400 and 'invalid_grant' in error_code:
|
| 401 |
+
logger.info(f" β Invalid/expired authorization code, trying next configuration...")
|
| 402 |
+
continue
|
| 403 |
+
elif response.status_code == 401 and 'invalid_client' in error_code:
|
| 404 |
+
logger.info(f" β Invalid client credentials, trying next configuration...")
|
| 405 |
+
continue
|
| 406 |
+
else:
|
| 407 |
+
logger.info(f" β HTTP {response.status_code} error, trying next configuration...")
|
| 408 |
+
continue
|
| 409 |
+
|
| 410 |
+
except requests.exceptions.Timeout:
|
| 411 |
+
logger.warning(f"β±οΈ {endpoint_name} timed out, trying next configuration...")
|
| 412 |
+
continue
|
| 413 |
+
except requests.exceptions.RequestException as e:
|
| 414 |
+
logger.warning(f"π {endpoint_name} network error: {e}, trying next configuration...")
|
| 415 |
+
continue
|
| 416 |
+
except Exception as e:
|
| 417 |
+
logger.warning(f"β {endpoint_name} unexpected error: {e}, trying next configuration...")
|
| 418 |
+
continue
|
| 419 |
+
|
| 420 |
+
# All endpoints failed
|
| 421 |
+
logger.error("β All token endpoint configurations failed")
|
| 422 |
+
return {
|
| 423 |
+
"success": False,
|
| 424 |
+
"error": "All token endpoints failed. This may be due to invalid authorization code, expired code, or Dexcom API issues.",
|
| 425 |
+
"suggestion": "Please try getting a fresh authorization code (they expire in 60 seconds) or check your Dexcom developer credentials.",
|
| 426 |
+
"endpoints_tried": configurations_to_try,
|
| 427 |
+
"troubleshooting": [
|
| 428 |
+
"1. Make sure you copied the COMPLETE callback URL",
|
| 429 |
+
"2. Authorization codes expire in 60 seconds - get a fresh one",
|
| 430 |
+
"3. Verify your CLIENT_ID and CLIENT_SECRET are correct",
|
| 431 |
+
"4. Check if your app is properly registered in Dexcom developer portal",
|
| 432 |
+
"5. Ensure redirect URI matches exactly: http://localhost:7860/callback"
|
| 433 |
+
]
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
def _ensure_valid_token(self):
|
| 437 |
+
"""Ensure we have a valid access token"""
|
| 438 |
+
if not self.access_token:
|
| 439 |
+
raise Exception("No access token available. Please authenticate first.")
|
| 440 |
+
|
| 441 |
+
if self.token_expires_at and datetime.now() >= self.token_expires_at - timedelta(minutes=5):
|
| 442 |
+
logger.info("Token expiring soon, need to refresh...")
|
| 443 |
+
if self.refresh_token:
|
| 444 |
+
if not self._refresh_token():
|
| 445 |
+
raise Exception("Token expired and refresh failed")
|
| 446 |
+
else:
|
| 447 |
+
raise Exception("Token expired and no refresh token available")
|
| 448 |
+
|
| 449 |
+
def _refresh_token(self) -> bool:
|
| 450 |
+
"""Refresh the access token using refresh token"""
|
| 451 |
+
if not self.refresh_token:
|
| 452 |
+
return False
|
| 453 |
+
|
| 454 |
+
token_url = self.working_endpoints["token"]
|
| 455 |
+
|
| 456 |
+
data = {
|
| 457 |
+
'client_id': self.client_id,
|
| 458 |
+
'client_secret': self.client_secret,
|
| 459 |
+
'refresh_token': self.refresh_token,
|
| 460 |
+
'grant_type': 'refresh_token'
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
headers = {
|
| 464 |
+
'Content-Type': 'application/x-www-form-urlencoded',
|
| 465 |
+
'Accept': 'application/json'
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
try:
|
| 469 |
+
response = requests.post(token_url, data=data, headers=headers)
|
| 470 |
+
|
| 471 |
+
if response.status_code == 200:
|
| 472 |
+
token_data = response.json()
|
| 473 |
+
|
| 474 |
+
self.access_token = token_data.get('access_token')
|
| 475 |
+
if 'refresh_token' in token_data:
|
| 476 |
+
self.refresh_token = token_data.get('refresh_token')
|
| 477 |
+
|
| 478 |
+
expires_in = token_data.get('expires_in', 7200)
|
| 479 |
+
self.token_expires_at = datetime.now() + timedelta(seconds=expires_in)
|
| 480 |
+
|
| 481 |
+
logger.info("β
Token refreshed successfully")
|
| 482 |
+
return True
|
| 483 |
+
else:
|
| 484 |
+
logger.error(f"Token refresh failed: {response.status_code}")
|
| 485 |
+
return False
|
| 486 |
+
|
| 487 |
+
except Exception as e:
|
| 488 |
+
logger.error(f"Token refresh error: {e}")
|
| 489 |
+
return False
|
| 490 |
+
|
| 491 |
+
def get_auth_headers(self) -> Dict[str, str]:
|
| 492 |
+
"""Get authorization headers for API calls"""
|
| 493 |
+
self._ensure_valid_token()
|
| 494 |
+
|
| 495 |
+
return {
|
| 496 |
+
'Authorization': f'Bearer {self.access_token}',
|
| 497 |
+
'Accept': 'application/json',
|
| 498 |
+
'User-Agent': 'GlycoAI-DexcomSandbox/1.0'
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
def get_data_range(self) -> Dict[str, Any]:
|
| 502 |
+
"""Get available data range from Dexcom API"""
|
| 503 |
+
api_base = self.get_api_base_url()
|
| 504 |
+
url = f"{api_base}/users/self/dataRange"
|
| 505 |
+
headers = self.get_auth_headers()
|
| 506 |
+
|
| 507 |
+
try:
|
| 508 |
+
response = requests.get(url, headers=headers, timeout=30)
|
| 509 |
+
|
| 510 |
+
if response.status_code == 200:
|
| 511 |
+
data = response.json()
|
| 512 |
+
logger.info(f"β
Data range retrieved from API {self.api_version}: {data}")
|
| 513 |
+
return data
|
| 514 |
+
else:
|
| 515 |
+
logger.error(f"Data range API error: {response.status_code} - {response.text}")
|
| 516 |
+
raise Exception(f"Data range API error: {response.status_code}")
|
| 517 |
+
|
| 518 |
+
except Exception as e:
|
| 519 |
+
logger.error(f"Error getting data range: {e}")
|
| 520 |
+
raise
|
| 521 |
+
|
| 522 |
+
def get_glucose_data(self, start_date: str = None, end_date: str = None) -> List[Dict]:
|
| 523 |
+
"""Get glucose (EGV) data from Dexcom API"""
|
| 524 |
+
api_base = self.get_api_base_url()
|
| 525 |
+
url = f"{api_base}/users/self/egvs"
|
| 526 |
+
headers = self.get_auth_headers()
|
| 527 |
+
|
| 528 |
+
# Per Dexcom documentation: all endpoints except dataRange require startDate and endDate
|
| 529 |
+
params = {}
|
| 530 |
+
if start_date:
|
| 531 |
+
params['startDate'] = start_date
|
| 532 |
+
if end_date:
|
| 533 |
+
params['endDate'] = end_date
|
| 534 |
+
|
| 535 |
+
# Validate date range (max 90 days per Dexcom spec)
|
| 536 |
+
if start_date and end_date:
|
| 537 |
+
from datetime import datetime
|
| 538 |
+
start_dt = datetime.fromisoformat(start_date.replace('Z', '+00:00'))
|
| 539 |
+
end_dt = datetime.fromisoformat(end_date.replace('Z', '+00:00'))
|
| 540 |
+
delta = (end_dt - start_dt).days
|
| 541 |
+
|
| 542 |
+
if delta > 90:
|
| 543 |
+
logger.warning(f"Date range {delta} days exceeds Dexcom 90-day limit")
|
| 544 |
+
# Adjust to last 90 days
|
| 545 |
+
start_dt = end_dt - timedelta(days=90)
|
| 546 |
+
params['startDate'] = start_dt.isoformat() + 'Z'
|
| 547 |
+
logger.info(f"Adjusted to 90-day window: {params['startDate']} to {end_date}")
|
| 548 |
+
|
| 549 |
+
try:
|
| 550 |
+
logger.info(f"π Fetching glucose data from API {self.api_version}")
|
| 551 |
+
logger.info(f" URL: {url}")
|
| 552 |
+
logger.info(f" Params: {params}")
|
| 553 |
+
|
| 554 |
+
response = requests.get(url, headers=headers, params=params, timeout=30)
|
| 555 |
+
|
| 556 |
+
if response.status_code == 200:
|
| 557 |
+
data = response.json()
|
| 558 |
+
egvs = data.get('egvs', [])
|
| 559 |
+
logger.info(f"β
Retrieved {len(egvs)} glucose readings from API {self.api_version}")
|
| 560 |
+
return egvs
|
| 561 |
+
else:
|
| 562 |
+
logger.error(f"EGV API error: {response.status_code} - {response.text}")
|
| 563 |
+
raise Exception(f"EGV API error: {response.status_code}")
|
| 564 |
+
|
| 565 |
+
except Exception as e:
|
| 566 |
+
logger.error(f"Error getting glucose data: {e}")
|
| 567 |
+
raise
|
| 568 |
+
|
| 569 |
+
def get_events_data(self, start_date: str = None, end_date: str = None) -> List[Dict]:
|
| 570 |
+
"""Get events data from Dexcom API"""
|
| 571 |
+
api_base = self.get_api_base_url()
|
| 572 |
+
url = f"{api_base}/users/self/events"
|
| 573 |
+
headers = self.get_auth_headers()
|
| 574 |
+
|
| 575 |
+
params = {}
|
| 576 |
+
if start_date:
|
| 577 |
+
params['startDate'] = start_date
|
| 578 |
+
if end_date:
|
| 579 |
+
params['endDate'] = end_date
|
| 580 |
+
|
| 581 |
+
try:
|
| 582 |
+
response = requests.get(url, headers=headers, params=params, timeout=30)
|
| 583 |
+
|
| 584 |
+
if response.status_code == 200:
|
| 585 |
+
data = response.json()
|
| 586 |
+
events = data.get('events', [])
|
| 587 |
+
logger.info(f"β
Retrieved {len(events)} events from API {self.api_version}")
|
| 588 |
+
return events
|
| 589 |
+
else:
|
| 590 |
+
logger.warning(f"Events API returned {response.status_code}, continuing without events")
|
| 591 |
+
return []
|
| 592 |
+
|
| 593 |
+
except Exception as e:
|
| 594 |
+
logger.warning(f"Error getting events data: {e}, continuing without events")
|
| 595 |
+
return []
|
| 596 |
+
|
| 597 |
+
class DexcomSandboxIntegration:
|
| 598 |
+
"""Integration wrapper for Gradio app with API version selection"""
|
| 599 |
+
|
| 600 |
+
def __init__(self, api_version: str = "v3"):
|
| 601 |
+
self.oauth = DexcomSandboxOAuth(api_version=api_version)
|
| 602 |
+
self.api_version = api_version
|
| 603 |
+
self.authenticated = False
|
| 604 |
+
self.user_profile = None
|
| 605 |
+
self.glucose_data = None
|
| 606 |
+
self.events_data = None
|
| 607 |
+
self.data_loaded_at = None
|
| 608 |
+
|
| 609 |
+
def start_oauth(self) -> Tuple[str, bool, bool]:
|
| 610 |
+
"""Start OAuth flow for Gradio interface"""
|
| 611 |
+
result = self.oauth.start_oauth_flow()
|
| 612 |
+
|
| 613 |
+
if result["success"]:
|
| 614 |
+
return (
|
| 615 |
+
result["instructions"],
|
| 616 |
+
True, # Show callback input
|
| 617 |
+
True # Show complete button
|
| 618 |
+
)
|
| 619 |
+
else:
|
| 620 |
+
return (
|
| 621 |
+
f"β Failed to start OAuth: {result['error']}",
|
| 622 |
+
False,
|
| 623 |
+
False
|
| 624 |
+
)
|
| 625 |
+
|
| 626 |
+
def complete_oauth(self, callback_url: str) -> Tuple[str, bool]:
|
| 627 |
+
"""Complete OAuth flow for Gradio interface"""
|
| 628 |
+
result = self.oauth.complete_oauth(callback_url)
|
| 629 |
+
|
| 630 |
+
if result["success"]:
|
| 631 |
+
self.authenticated = True
|
| 632 |
+
self.user_profile = result["user_profile"]
|
| 633 |
+
|
| 634 |
+
return (
|
| 635 |
+
f"β
Dexcom Sandbox authenticated successfully! Click 'Load Data' to begin.",
|
| 636 |
+
True # Show main interface
|
| 637 |
+
)
|
| 638 |
+
else:
|
| 639 |
+
return (
|
| 640 |
+
f"β OAuth failed: {result['error']}",
|
| 641 |
+
False
|
| 642 |
+
)
|
| 643 |
+
|
| 644 |
+
def load_glucose_data(self, days: int = 14) -> Dict[str, Any]:
|
| 645 |
+
"""Load glucose data for the specified number of days"""
|
| 646 |
+
if not self.authenticated:
|
| 647 |
+
return {
|
| 648 |
+
"success": False,
|
| 649 |
+
"error": "Not authenticated. Please complete OAuth first."
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
try:
|
| 653 |
+
# Calculate date range
|
| 654 |
+
end_time = datetime.now()
|
| 655 |
+
start_time = end_time - timedelta(days=days)
|
| 656 |
+
|
| 657 |
+
# Fetch glucose data
|
| 658 |
+
self.glucose_data = self.oauth.get_glucose_data(
|
| 659 |
+
start_date=start_time.isoformat(),
|
| 660 |
+
end_date=end_time.isoformat()
|
| 661 |
+
)
|
| 662 |
+
|
| 663 |
+
# Fetch events data
|
| 664 |
+
self.events_data = self.oauth.get_events_data(
|
| 665 |
+
start_date=start_time.isoformat(),
|
| 666 |
+
end_date=end_time.isoformat()
|
| 667 |
+
)
|
| 668 |
+
|
| 669 |
+
self.data_loaded_at = datetime.now()
|
| 670 |
+
|
| 671 |
+
return {
|
| 672 |
+
"success": True,
|
| 673 |
+
"glucose_readings": len(self.glucose_data),
|
| 674 |
+
"events": len(self.events_data),
|
| 675 |
+
"date_range": f"{start_time.strftime('%Y-%m-%d')} to {end_time.strftime('%Y-%m-%d')}",
|
| 676 |
+
"user": self.user_profile.name
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
except Exception as e:
|
| 680 |
+
logger.error(f"Failed to load glucose data: {e}")
|
| 681 |
+
return {
|
| 682 |
+
"success": False,
|
| 683 |
+
"error": f"Failed to load data: {str(e)}"
|
| 684 |
+
}
|
| 685 |
+
|
| 686 |
+
def get_user_profile(self) -> Optional[DexcomSandboxUser]:
|
| 687 |
+
"""Get authenticated user profile"""
|
| 688 |
+
return self.user_profile if self.authenticated else None
|
| 689 |
+
|
| 690 |
+
def get_glucose_data_for_ui(self) -> Optional[List[Dict]]:
|
| 691 |
+
"""Get glucose data formatted for UI display"""
|
| 692 |
+
return self.glucose_data if self.authenticated else None
|
| 693 |
+
|
| 694 |
+
def get_status(self) -> Dict[str, Any]:
|
| 695 |
+
"""Get current authentication and data status"""
|
| 696 |
+
return {
|
| 697 |
+
"authenticated": self.authenticated,
|
| 698 |
+
"user": self.user_profile.name if self.user_profile else None,
|
| 699 |
+
"glucose_readings": len(self.glucose_data) if self.glucose_data else 0,
|
| 700 |
+
"events": len(self.events_data) if self.events_data else 0,
|
| 701 |
+
"data_loaded_at": self.data_loaded_at.isoformat() if self.data_loaded_at else None,
|
| 702 |
+
"token_expires_at": self.oauth.token_expires_at.isoformat() if self.oauth.token_expires_at else None
|
| 703 |
+
}
|
| 704 |
+
|
| 705 |
+
def debug_oauth_endpoints():
|
| 706 |
+
"""Debug function to test all OAuth endpoints"""
|
| 707 |
+
print("π DEBUGGING DEXCOM OAUTH ENDPOINTS")
|
| 708 |
+
print("=" * 60)
|
| 709 |
+
|
| 710 |
+
# Test all endpoint configurations
|
| 711 |
+
all_configs = [DEFAULT_ENDPOINTS] + ENDPOINT_CONFIGURATIONS
|
| 712 |
+
|
| 713 |
+
for i, config in enumerate(all_configs):
|
| 714 |
+
name = config.get('name', f'Configuration {i+1}')
|
| 715 |
+
print(f"\nπ§ͺ Testing {name}:")
|
| 716 |
+
print(f" Login: {config['login']}")
|
| 717 |
+
print(f" Token: {config['token']}")
|
| 718 |
+
print(f" API v2: {config['api_v2']}")
|
| 719 |
+
print(f" API v3: {config['api_v3']}")
|
| 720 |
+
|
| 721 |
+
# Test if endpoints are reachable
|
| 722 |
+
for endpoint_type, url in config.items():
|
| 723 |
+
if endpoint_type == 'name':
|
| 724 |
+
continue
|
| 725 |
+
|
| 726 |
+
try:
|
| 727 |
+
# Just check if the endpoint exists (don't send real requests)
|
| 728 |
+
response = requests.head(url, timeout=5)
|
| 729 |
+
status = "β
Reachable" if response.status_code != 404 else "β 404 Not Found"
|
| 730 |
+
print(f" {endpoint_type.upper()}: {status} ({response.status_code})")
|
| 731 |
+
except requests.exceptions.RequestException as e:
|
| 732 |
+
print(f" {endpoint_type.upper()}: β Error - {e}")
|
| 733 |
+
|
| 734 |
+
print(f"\nπ‘ Key Points from Official Documentation:")
|
| 735 |
+
print("1. OAuth endpoints are always v2 (even for v3 API calls)")
|
| 736 |
+
print("2. Sandbox login uses special developer.dexcom.com/sandbox-login")
|
| 737 |
+
print("3. Token exchange uses production OAuth endpoint for sandbox too")
|
| 738 |
+
print("4. API v3 supports G6, G7, ONE, ONE+ devices")
|
| 739 |
+
print("5. API v2 supports G5, G6 devices (legacy)")
|
| 740 |
+
print("6. Time window max 90 days for all endpoints except dataRange")
|
| 741 |
+
|
| 742 |
+
def test_dexcom_sandbox():
|
| 743 |
+
"""Test Dexcom Sandbox OAuth implementation with both API versions"""
|
| 744 |
+
print("π§ͺ Testing Dexcom Sandbox OAuth Implementation")
|
| 745 |
+
print("=" * 60)
|
| 746 |
+
|
| 747 |
+
# Run endpoint debug first
|
| 748 |
+
debug_oauth_endpoints()
|
| 749 |
+
|
| 750 |
+
print(f"\nπ OAuth Flow Test:")
|
| 751 |
+
|
| 752 |
+
# Test both API versions
|
| 753 |
+
for api_version in ["v3", "v2"]:
|
| 754 |
+
print(f"\n--- Testing API {api_version} ---")
|
| 755 |
+
|
| 756 |
+
# Initialize OAuth
|
| 757 |
+
oauth = DexcomSandboxOAuth(api_version=api_version)
|
| 758 |
+
|
| 759 |
+
# Test auth URL generation
|
| 760 |
+
auth_url = oauth.generate_auth_url()
|
| 761 |
+
print(f"β
Auth URL generated: {auth_url}")
|
| 762 |
+
|
| 763 |
+
# Test integration wrapper
|
| 764 |
+
integration = DexcomSandboxIntegration(api_version=api_version)
|
| 765 |
+
|
| 766 |
+
instructions, show_input, show_button = integration.start_oauth()
|
| 767 |
+
print(f"β
OAuth flow started for API {api_version}")
|
| 768 |
+
print(f" Show callback input: {show_input}")
|
| 769 |
+
print(f" Show complete button: {show_button}")
|
| 770 |
+
|
| 771 |
+
print(f"\nπ Next steps for testing:")
|
| 772 |
+
print(f"1. Choose API version (v3 recommended for newer devices)")
|
| 773 |
+
print(f"2. Open auth URL and select sandbox user")
|
| 774 |
+
print(f"3. SandboxUser6 (G6) works with both v2 and v3")
|
| 775 |
+
print(f"4. SandboxUser7 (G7) requires API v3")
|
| 776 |
+
print(f"5. Copy callback URL after 404 error")
|
| 777 |
+
print(f"6. System will automatically try multiple token endpoints")
|
| 778 |
+
|
| 779 |
+
print(f"\nπ₯ Available Sandbox Users:")
|
| 780 |
+
for key, name in SANDBOX_USERS.items():
|
| 781 |
+
print(f" β’ {name}")
|
| 782 |
+
if "G6" in name:
|
| 783 |
+
print(f" β³ π― Works with both API v2 and v3")
|
| 784 |
+
elif "G7" in name:
|
| 785 |
+
print(f" β³ π― Requires API v3")
|
| 786 |
+
|
| 787 |
+
return oauth, integration
|
| 788 |
+
|
| 789 |
+
if __name__ == "__main__":
|
| 790 |
+
test_dexcom_sandbox()
|
unified_data_manager.py
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Unified Data Manager for GlycoAI - MODIFIED VERSION
|
| 3 |
+
Sarah now has unstable glucose values for demonstration
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import logging
|
| 7 |
+
from typing import Dict, Any, Optional, Tuple
|
| 8 |
+
import pandas as pd
|
| 9 |
+
from datetime import datetime, timedelta
|
| 10 |
+
from dataclasses import asdict
|
| 11 |
+
import numpy as np
|
| 12 |
+
import random
|
| 13 |
+
|
| 14 |
+
from apifunctions import (
|
| 15 |
+
DexcomAPI,
|
| 16 |
+
GlucoseAnalyzer,
|
| 17 |
+
DEMO_USERS,
|
| 18 |
+
DemoUser
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
logger = logging.getLogger(__name__)
|
| 22 |
+
|
| 23 |
+
class UnifiedDataManager:
|
| 24 |
+
"""
|
| 25 |
+
MODIFIED: Unified data manager with Sarah having unstable glucose patterns
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
def __init__(self):
|
| 29 |
+
self.dexcom_api = DexcomAPI()
|
| 30 |
+
self.analyzer = GlucoseAnalyzer()
|
| 31 |
+
|
| 32 |
+
logger.info(f"UnifiedDataManager initialized - Sarah will have unstable glucose patterns")
|
| 33 |
+
|
| 34 |
+
# Single source of truth for all data
|
| 35 |
+
self.current_user: Optional[DemoUser] = None
|
| 36 |
+
self.raw_glucose_data: Optional[list] = None
|
| 37 |
+
self.processed_glucose_data: Optional[pd.DataFrame] = None
|
| 38 |
+
self.calculated_stats: Optional[Dict] = None
|
| 39 |
+
self.identified_patterns: Optional[Dict] = None
|
| 40 |
+
|
| 41 |
+
# Metadata
|
| 42 |
+
self.data_loaded_at: Optional[datetime] = None
|
| 43 |
+
self.data_source: str = "none" # "dexcom_api", "mock", or "none"
|
| 44 |
+
|
| 45 |
+
def load_user_data(self, user_key: str, force_reload: bool = False) -> Dict[str, Any]:
|
| 46 |
+
"""
|
| 47 |
+
MODIFIED: Load glucose data with Sarah having unstable patterns
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
# Check if we already have data for this user and it's recent
|
| 51 |
+
if (not force_reload and
|
| 52 |
+
self.current_user and
|
| 53 |
+
self.current_user == DEMO_USERS.get(user_key) and
|
| 54 |
+
self.data_loaded_at and
|
| 55 |
+
(datetime.now() - self.data_loaded_at).seconds < 300): # 5 minutes cache
|
| 56 |
+
|
| 57 |
+
logger.info(f"Using cached data for {user_key}")
|
| 58 |
+
return self._build_success_response()
|
| 59 |
+
|
| 60 |
+
try:
|
| 61 |
+
if user_key not in DEMO_USERS:
|
| 62 |
+
return {
|
| 63 |
+
"success": False,
|
| 64 |
+
"message": f"β Invalid user key '{user_key}'. Available: {', '.join(DEMO_USERS.keys())}"
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
logger.info(f"Loading data for user: {user_key}")
|
| 68 |
+
|
| 69 |
+
# Set current user
|
| 70 |
+
self.current_user = DEMO_USERS[user_key]
|
| 71 |
+
|
| 72 |
+
# Call API EXACTLY as it was working before
|
| 73 |
+
try:
|
| 74 |
+
logger.info(f"Attempting Dexcom API authentication for {user_key}")
|
| 75 |
+
|
| 76 |
+
# ORIGINAL WORKING METHOD: Use the simulate_demo_login exactly as before
|
| 77 |
+
access_token = self.dexcom_api.simulate_demo_login(user_key)
|
| 78 |
+
logger.info(f"Dexcom authentication result: {bool(access_token)}")
|
| 79 |
+
|
| 80 |
+
if access_token:
|
| 81 |
+
# ORIGINAL WORKING METHOD: Get data with 14-day range
|
| 82 |
+
end_date = datetime.now()
|
| 83 |
+
start_date = end_date - timedelta(days=14)
|
| 84 |
+
|
| 85 |
+
# Call get_egv_data EXACTLY as it was working before
|
| 86 |
+
self.raw_glucose_data = self.dexcom_api.get_egv_data(
|
| 87 |
+
start_date.isoformat(),
|
| 88 |
+
end_date.isoformat()
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
if self.raw_glucose_data and len(self.raw_glucose_data) > 0:
|
| 92 |
+
self.data_source = "dexcom_api"
|
| 93 |
+
logger.info(f"β
Successfully loaded {len(self.raw_glucose_data)} readings from Dexcom API")
|
| 94 |
+
else:
|
| 95 |
+
logger.warning("Dexcom API returned empty data - falling back to mock data")
|
| 96 |
+
raise Exception("Empty data from Dexcom API")
|
| 97 |
+
else:
|
| 98 |
+
logger.warning("Failed to get access token - falling back to mock data")
|
| 99 |
+
raise Exception("Authentication failed")
|
| 100 |
+
|
| 101 |
+
except Exception as api_error:
|
| 102 |
+
logger.warning(f"Dexcom API failed ({str(api_error)}) - using mock data fallback")
|
| 103 |
+
self.raw_glucose_data = self._generate_realistic_mock_data(user_key)
|
| 104 |
+
self.data_source = "mock"
|
| 105 |
+
|
| 106 |
+
# Process the raw data (same processing for everyone)
|
| 107 |
+
self.processed_glucose_data = self.analyzer.process_egv_data(self.raw_glucose_data)
|
| 108 |
+
|
| 109 |
+
if self.processed_glucose_data is None or self.processed_glucose_data.empty:
|
| 110 |
+
return {
|
| 111 |
+
"success": False,
|
| 112 |
+
"message": "β Failed to process glucose data"
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
# Calculate statistics (single source of truth)
|
| 116 |
+
self.calculated_stats = self._calculate_unified_stats()
|
| 117 |
+
|
| 118 |
+
# Identify patterns
|
| 119 |
+
self.identified_patterns = self.analyzer.identify_patterns(self.processed_glucose_data)
|
| 120 |
+
|
| 121 |
+
# Mark when data was loaded
|
| 122 |
+
self.data_loaded_at = datetime.now()
|
| 123 |
+
|
| 124 |
+
logger.info(f"Successfully loaded and processed data for {self.current_user.name}")
|
| 125 |
+
logger.info(f"Data source: {self.data_source}, Readings: {len(self.processed_glucose_data)}")
|
| 126 |
+
logger.info(f"TIR: {self.calculated_stats.get('time_in_range_70_180', 0):.1f}%")
|
| 127 |
+
|
| 128 |
+
return self._build_success_response()
|
| 129 |
+
|
| 130 |
+
except Exception as e:
|
| 131 |
+
logger.error(f"Failed to load user data: {e}")
|
| 132 |
+
return {
|
| 133 |
+
"success": False,
|
| 134 |
+
"message": f"β Failed to load user data: {str(e)}"
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
def get_stats_for_ui(self) -> Dict[str, Any]:
|
| 138 |
+
"""Get statistics formatted for the UI display"""
|
| 139 |
+
if not self.calculated_stats:
|
| 140 |
+
return {}
|
| 141 |
+
|
| 142 |
+
return {
|
| 143 |
+
**self.calculated_stats,
|
| 144 |
+
"data_source": self.data_source,
|
| 145 |
+
"loaded_at": self.data_loaded_at.isoformat() if self.data_loaded_at else None,
|
| 146 |
+
"user_name": self.current_user.name if self.current_user else None
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
def get_context_for_agent(self) -> Dict[str, Any]:
|
| 150 |
+
"""Get context formatted for the AI agent"""
|
| 151 |
+
if not self.current_user or not self.calculated_stats:
|
| 152 |
+
return {"error": "No user data loaded"}
|
| 153 |
+
|
| 154 |
+
# Build agent context with the SAME data as UI
|
| 155 |
+
context = {
|
| 156 |
+
"user": {
|
| 157 |
+
"name": self.current_user.name,
|
| 158 |
+
"age": self.current_user.age,
|
| 159 |
+
"diabetes_type": self.current_user.diabetes_type,
|
| 160 |
+
"device_type": self.current_user.device_type,
|
| 161 |
+
"years_with_diabetes": self.current_user.years_with_diabetes,
|
| 162 |
+
"typical_pattern": getattr(self.current_user, 'typical_glucose_pattern', 'normal')
|
| 163 |
+
},
|
| 164 |
+
"statistics": self._safe_convert_for_json(self.calculated_stats),
|
| 165 |
+
"patterns": self._safe_convert_for_json(self.identified_patterns),
|
| 166 |
+
"data_points": len(self.processed_glucose_data) if self.processed_glucose_data is not None else 0,
|
| 167 |
+
"recent_readings": self._get_recent_readings_for_agent(),
|
| 168 |
+
"data_metadata": {
|
| 169 |
+
"source": self.data_source,
|
| 170 |
+
"loaded_at": self.data_loaded_at.isoformat() if self.data_loaded_at else None,
|
| 171 |
+
"data_age_minutes": int((datetime.now() - self.data_loaded_at).total_seconds() / 60) if self.data_loaded_at else None
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
return context
|
| 176 |
+
|
| 177 |
+
def get_chart_data(self) -> Optional[pd.DataFrame]:
|
| 178 |
+
"""Get processed data for chart display"""
|
| 179 |
+
return self.processed_glucose_data
|
| 180 |
+
|
| 181 |
+
def _calculate_unified_stats(self) -> Dict[str, Any]:
|
| 182 |
+
"""Calculate statistics using a single, consistent method"""
|
| 183 |
+
if self.processed_glucose_data is None or self.processed_glucose_data.empty:
|
| 184 |
+
return {"error": "No data available"}
|
| 185 |
+
|
| 186 |
+
try:
|
| 187 |
+
# Get glucose values
|
| 188 |
+
glucose_values = self.processed_glucose_data['value'].dropna()
|
| 189 |
+
|
| 190 |
+
if len(glucose_values) == 0:
|
| 191 |
+
return {"error": "No valid glucose values"}
|
| 192 |
+
|
| 193 |
+
# Convert to numpy array for consistent calculations
|
| 194 |
+
import numpy as np
|
| 195 |
+
values = np.array(glucose_values.tolist(), dtype=float)
|
| 196 |
+
|
| 197 |
+
# Calculate basic statistics
|
| 198 |
+
avg_glucose = float(np.mean(values))
|
| 199 |
+
min_glucose = float(np.min(values))
|
| 200 |
+
max_glucose = float(np.max(values))
|
| 201 |
+
std_glucose = float(np.std(values))
|
| 202 |
+
total_readings = int(len(values))
|
| 203 |
+
|
| 204 |
+
# Calculate time in ranges - CONSISTENT METHOD
|
| 205 |
+
in_range_mask = (values >= 70) & (values <= 180)
|
| 206 |
+
below_range_mask = values < 70
|
| 207 |
+
above_range_mask = values > 180
|
| 208 |
+
|
| 209 |
+
in_range_count = int(np.sum(in_range_mask))
|
| 210 |
+
below_range_count = int(np.sum(below_range_mask))
|
| 211 |
+
above_range_count = int(np.sum(above_range_mask))
|
| 212 |
+
|
| 213 |
+
# Calculate percentages
|
| 214 |
+
time_in_range = (in_range_count / total_readings) * 100 if total_readings > 0 else 0
|
| 215 |
+
time_below_70 = (below_range_count / total_readings) * 100 if total_readings > 0 else 0
|
| 216 |
+
time_above_180 = (above_range_count / total_readings) * 100 if total_readings > 0 else 0
|
| 217 |
+
|
| 218 |
+
# Calculate additional metrics
|
| 219 |
+
gmi = 3.31 + (0.02392 * avg_glucose) # Glucose Management Indicator
|
| 220 |
+
cv = (std_glucose / avg_glucose) * 100 if avg_glucose > 0 else 0 # Coefficient of Variation
|
| 221 |
+
|
| 222 |
+
stats = {
|
| 223 |
+
"average_glucose": avg_glucose,
|
| 224 |
+
"min_glucose": min_glucose,
|
| 225 |
+
"max_glucose": max_glucose,
|
| 226 |
+
"std_glucose": std_glucose,
|
| 227 |
+
"time_in_range_70_180": time_in_range,
|
| 228 |
+
"time_below_70": time_below_70,
|
| 229 |
+
"time_above_180": time_above_180,
|
| 230 |
+
"total_readings": total_readings,
|
| 231 |
+
"gmi": gmi,
|
| 232 |
+
"cv": cv,
|
| 233 |
+
"in_range_count": in_range_count,
|
| 234 |
+
"below_range_count": below_range_count,
|
| 235 |
+
"above_range_count": above_range_count
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
# Log for debugging
|
| 239 |
+
logger.info(f"Calculated stats - TIR: {time_in_range:.1f}%, Total: {total_readings}, In range: {in_range_count}")
|
| 240 |
+
|
| 241 |
+
return stats
|
| 242 |
+
|
| 243 |
+
except Exception as e:
|
| 244 |
+
logger.error(f"Error calculating unified stats: {e}")
|
| 245 |
+
return {"error": f"Statistics calculation failed: {str(e)}"}
|
| 246 |
+
|
| 247 |
+
def _generate_realistic_mock_data(self, user_key: str) -> list:
|
| 248 |
+
"""Generate realistic mock data with SARAH having UNSTABLE patterns"""
|
| 249 |
+
|
| 250 |
+
# MODIFIED: Sarah now has unstable glucose patterns
|
| 251 |
+
pattern_map = {
|
| 252 |
+
"sarah_g7": "unstable_high_variability", # CHANGED: Sarah now unstable
|
| 253 |
+
"marcus_one": "dawn_phenomenon",
|
| 254 |
+
"jennifer_g6": "normal",
|
| 255 |
+
"robert_receiver": "dawn_phenomenon"
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
user_pattern = pattern_map.get(user_key, "normal")
|
| 259 |
+
|
| 260 |
+
# Generate 14 days of data with specific patterns
|
| 261 |
+
if user_key == "sarah_g7":
|
| 262 |
+
# Generate UNSTABLE data for Sarah
|
| 263 |
+
mock_data = self._generate_unstable_glucose_data()
|
| 264 |
+
logger.info(f"Generated {len(mock_data)} UNSTABLE mock data points for Sarah")
|
| 265 |
+
else:
|
| 266 |
+
# Use normal patterns for other users
|
| 267 |
+
mock_data = self._create_realistic_pattern(days=14, user_type=user_pattern)
|
| 268 |
+
logger.info(f"Generated {len(mock_data)} mock data points for {user_key} with pattern {user_pattern}")
|
| 269 |
+
|
| 270 |
+
return mock_data
|
| 271 |
+
|
| 272 |
+
def _generate_unstable_glucose_data(self) -> list:
|
| 273 |
+
"""Generate highly variable, unstable glucose data for Sarah"""
|
| 274 |
+
readings = []
|
| 275 |
+
now = datetime.now()
|
| 276 |
+
|
| 277 |
+
# Generate 14 days of unstable data (every 5 minutes)
|
| 278 |
+
total_minutes = 14 * 24 * 60
|
| 279 |
+
interval_minutes = 5
|
| 280 |
+
total_readings = total_minutes // interval_minutes
|
| 281 |
+
|
| 282 |
+
logger.info(f"Generating {total_readings} unstable glucose readings for Sarah")
|
| 283 |
+
|
| 284 |
+
for i in range(total_readings):
|
| 285 |
+
timestamp = now - timedelta(minutes=total_minutes - (i * interval_minutes))
|
| 286 |
+
|
| 287 |
+
# Create highly variable glucose patterns
|
| 288 |
+
hour = timestamp.hour
|
| 289 |
+
day_of_week = timestamp.weekday()
|
| 290 |
+
|
| 291 |
+
# Base glucose with high variability
|
| 292 |
+
if hour >= 6 and hour <= 8: # Morning - dawn phenomenon + high variability
|
| 293 |
+
base_glucose = random.uniform(140, 220)
|
| 294 |
+
variability = random.uniform(-40, 60)
|
| 295 |
+
elif hour >= 12 and hour <= 14: # Lunch - post-meal spikes
|
| 296 |
+
base_glucose = random.uniform(120, 280)
|
| 297 |
+
variability = random.uniform(-30, 80)
|
| 298 |
+
elif hour >= 18 and hour <= 20: # Dinner - high spikes
|
| 299 |
+
base_glucose = random.uniform(130, 300)
|
| 300 |
+
variability = random.uniform(-50, 70)
|
| 301 |
+
elif hour >= 22 or hour <= 4: # Night - unpredictable lows and highs
|
| 302 |
+
base_glucose = random.uniform(60, 200)
|
| 303 |
+
variability = random.uniform(-30, 50)
|
| 304 |
+
else: # Other times - still unstable
|
| 305 |
+
base_glucose = random.uniform(80, 220)
|
| 306 |
+
variability = random.uniform(-40, 60)
|
| 307 |
+
|
| 308 |
+
# Add weekend effect (even more unstable)
|
| 309 |
+
if day_of_week >= 5: # Weekend
|
| 310 |
+
base_glucose += random.uniform(-20, 40)
|
| 311 |
+
variability += random.uniform(-20, 30)
|
| 312 |
+
|
| 313 |
+
# Add random noise for high variability
|
| 314 |
+
noise = random.uniform(-25, 25)
|
| 315 |
+
glucose_value = base_glucose + variability + noise
|
| 316 |
+
|
| 317 |
+
# Ensure realistic bounds but allow extreme values
|
| 318 |
+
glucose_value = max(40, min(400, glucose_value))
|
| 319 |
+
|
| 320 |
+
# Add some random severe lows and highs
|
| 321 |
+
if random.random() < 0.05: # 5% chance of severe events
|
| 322 |
+
if random.random() < 0.5:
|
| 323 |
+
glucose_value = random.uniform(45, 65) # Severe low
|
| 324 |
+
else:
|
| 325 |
+
glucose_value = random.uniform(280, 350) # Severe high
|
| 326 |
+
|
| 327 |
+
# Determine trend based on glucose change
|
| 328 |
+
if i > 0:
|
| 329 |
+
prev_glucose = readings[-1]['value']
|
| 330 |
+
glucose_change = glucose_value - prev_glucose
|
| 331 |
+
|
| 332 |
+
if glucose_change > 15:
|
| 333 |
+
trend = "rising_rapidly"
|
| 334 |
+
elif glucose_change > 5:
|
| 335 |
+
trend = "rising"
|
| 336 |
+
elif glucose_change < -15:
|
| 337 |
+
trend = "falling_rapidly"
|
| 338 |
+
elif glucose_change < -5:
|
| 339 |
+
trend = "falling"
|
| 340 |
+
else:
|
| 341 |
+
trend = "flat"
|
| 342 |
+
else:
|
| 343 |
+
trend = "flat"
|
| 344 |
+
|
| 345 |
+
reading = {
|
| 346 |
+
"systemTime": timestamp.isoformat(),
|
| 347 |
+
"displayTime": timestamp.isoformat(),
|
| 348 |
+
"value": round(glucose_value, 1),
|
| 349 |
+
"trend": trend,
|
| 350 |
+
"realtimeValue": round(glucose_value, 1),
|
| 351 |
+
"smoothedValue": round(glucose_value * 0.9 + random.uniform(-5, 5), 1)
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
readings.append(reading)
|
| 355 |
+
|
| 356 |
+
# Log statistics of generated data
|
| 357 |
+
values = [r['value'] for r in readings]
|
| 358 |
+
avg_glucose = np.mean(values)
|
| 359 |
+
std_glucose = np.std(values)
|
| 360 |
+
cv = (std_glucose / avg_glucose) * 100
|
| 361 |
+
|
| 362 |
+
in_range = sum(1 for v in values if 70 <= v <= 180)
|
| 363 |
+
below_range = sum(1 for v in values if v < 70)
|
| 364 |
+
above_range = sum(1 for v in values if v > 180)
|
| 365 |
+
|
| 366 |
+
tir = (in_range / len(values)) * 100
|
| 367 |
+
tbr = (below_range / len(values)) * 100
|
| 368 |
+
tar = (above_range / len(values)) * 100
|
| 369 |
+
|
| 370 |
+
logger.info(f"Sarah's UNSTABLE data generated:")
|
| 371 |
+
logger.info(f" Average: {avg_glucose:.1f} mg/dL")
|
| 372 |
+
logger.info(f" CV: {cv:.1f}% (VERY HIGH)")
|
| 373 |
+
logger.info(f" TIR: {tir:.1f}% (LOW)")
|
| 374 |
+
logger.info(f" TBR: {tbr:.1f}% (HIGH)")
|
| 375 |
+
logger.info(f" TAR: {tar:.1f}% (HIGH)")
|
| 376 |
+
|
| 377 |
+
return readings
|
| 378 |
+
|
| 379 |
+
def _create_realistic_pattern(self, days: int = 14, user_type: str = "normal") -> list:
|
| 380 |
+
"""Create realistic glucose patterns for non-Sarah users"""
|
| 381 |
+
readings = []
|
| 382 |
+
now = datetime.now()
|
| 383 |
+
|
| 384 |
+
# Generate data every 5 minutes
|
| 385 |
+
total_minutes = days * 24 * 60
|
| 386 |
+
interval_minutes = 5
|
| 387 |
+
total_readings = total_minutes // interval_minutes
|
| 388 |
+
|
| 389 |
+
for i in range(total_readings):
|
| 390 |
+
timestamp = now - timedelta(minutes=total_minutes - (i * interval_minutes))
|
| 391 |
+
hour = timestamp.hour
|
| 392 |
+
|
| 393 |
+
# Base patterns for different user types
|
| 394 |
+
if user_type == "dawn_phenomenon":
|
| 395 |
+
if hour >= 6 and hour <= 8: # Dawn phenomenon
|
| 396 |
+
base_glucose = random.uniform(150, 190)
|
| 397 |
+
elif hour >= 12 and hour <= 14: # Post lunch
|
| 398 |
+
base_glucose = random.uniform(140, 180)
|
| 399 |
+
elif hour >= 18 and hour <= 20: # Post dinner
|
| 400 |
+
base_glucose = random.uniform(130, 170)
|
| 401 |
+
else:
|
| 402 |
+
base_glucose = random.uniform(90, 140)
|
| 403 |
+
else: # Normal pattern
|
| 404 |
+
if hour >= 12 and hour <= 14: # Post lunch
|
| 405 |
+
base_glucose = random.uniform(120, 160)
|
| 406 |
+
elif hour >= 18 and hour <= 20: # Post dinner
|
| 407 |
+
base_glucose = random.uniform(110, 150)
|
| 408 |
+
else:
|
| 409 |
+
base_glucose = random.uniform(80, 120)
|
| 410 |
+
|
| 411 |
+
# Add moderate variability
|
| 412 |
+
glucose_value = base_glucose + random.uniform(-15, 15)
|
| 413 |
+
glucose_value = max(70, min(250, glucose_value))
|
| 414 |
+
|
| 415 |
+
reading = {
|
| 416 |
+
"systemTime": timestamp.isoformat(),
|
| 417 |
+
"displayTime": timestamp.isoformat(),
|
| 418 |
+
"value": round(glucose_value, 1),
|
| 419 |
+
"trend": "flat",
|
| 420 |
+
"realtimeValue": round(glucose_value, 1),
|
| 421 |
+
"smoothedValue": round(glucose_value, 1)
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
readings.append(reading)
|
| 425 |
+
|
| 426 |
+
return readings
|
| 427 |
+
|
| 428 |
+
def _get_recent_readings_for_agent(self, count: int = 5) -> list:
|
| 429 |
+
"""Get recent readings formatted for agent context"""
|
| 430 |
+
if self.processed_glucose_data is None or self.processed_glucose_data.empty:
|
| 431 |
+
return []
|
| 432 |
+
|
| 433 |
+
try:
|
| 434 |
+
recent_df = self.processed_glucose_data.tail(count)
|
| 435 |
+
readings = []
|
| 436 |
+
|
| 437 |
+
for _, row in recent_df.iterrows():
|
| 438 |
+
display_time = row.get('displayTime') or row.get('systemTime')
|
| 439 |
+
glucose_value = row.get('value')
|
| 440 |
+
trend_value = row.get('trend', 'flat')
|
| 441 |
+
|
| 442 |
+
if pd.notna(display_time):
|
| 443 |
+
if isinstance(display_time, str):
|
| 444 |
+
time_str = display_time
|
| 445 |
+
else:
|
| 446 |
+
time_str = pd.to_datetime(display_time).isoformat()
|
| 447 |
+
else:
|
| 448 |
+
time_str = datetime.now().isoformat()
|
| 449 |
+
|
| 450 |
+
if pd.notna(glucose_value):
|
| 451 |
+
glucose_clean = self._safe_convert_for_json(glucose_value)
|
| 452 |
+
else:
|
| 453 |
+
glucose_clean = None
|
| 454 |
+
|
| 455 |
+
trend_clean = str(trend_value) if pd.notna(trend_value) else 'flat'
|
| 456 |
+
|
| 457 |
+
readings.append({
|
| 458 |
+
"time": time_str,
|
| 459 |
+
"glucose": glucose_clean,
|
| 460 |
+
"trend": trend_clean
|
| 461 |
+
})
|
| 462 |
+
|
| 463 |
+
return readings
|
| 464 |
+
|
| 465 |
+
except Exception as e:
|
| 466 |
+
logger.error(f"Error getting recent readings: {e}")
|
| 467 |
+
return []
|
| 468 |
+
|
| 469 |
+
def _safe_convert_for_json(self, obj):
|
| 470 |
+
"""Safely convert objects for JSON serialization"""
|
| 471 |
+
import numpy as np
|
| 472 |
+
|
| 473 |
+
if obj is None:
|
| 474 |
+
return None
|
| 475 |
+
elif isinstance(obj, (np.integer, np.int64, np.int32)):
|
| 476 |
+
return int(obj)
|
| 477 |
+
elif isinstance(obj, (np.floating, np.float64, np.float32)):
|
| 478 |
+
if np.isnan(obj):
|
| 479 |
+
return None
|
| 480 |
+
return float(obj)
|
| 481 |
+
elif isinstance(obj, dict):
|
| 482 |
+
return {key: self._safe_convert_for_json(value) for key, value in obj.items()}
|
| 483 |
+
elif isinstance(obj, list):
|
| 484 |
+
return [self._safe_convert_for_json(item) for item in obj]
|
| 485 |
+
elif isinstance(obj, pd.Timestamp):
|
| 486 |
+
return obj.isoformat()
|
| 487 |
+
else:
|
| 488 |
+
return obj
|
| 489 |
+
|
| 490 |
+
def _build_success_response(self) -> Dict[str, Any]:
|
| 491 |
+
"""Build a consistent success response"""
|
| 492 |
+
data_points = len(self.processed_glucose_data) if self.processed_glucose_data is not None else 0
|
| 493 |
+
avg_glucose = self.calculated_stats.get('average_glucose', 0)
|
| 494 |
+
time_in_range = self.calculated_stats.get('time_in_range_70_180', 0)
|
| 495 |
+
|
| 496 |
+
return {
|
| 497 |
+
"success": True,
|
| 498 |
+
"message": f"β
Successfully loaded data for {self.current_user.name}",
|
| 499 |
+
"user": asdict(self.current_user),
|
| 500 |
+
"data_points": data_points,
|
| 501 |
+
"stats": self.calculated_stats,
|
| 502 |
+
"data_source": self.data_source,
|
| 503 |
+
"summary": f"π {data_points} readings | Avg: {avg_glucose:.1f} mg/dL | TIR: {time_in_range:.1f}% | Source: {self.data_source}"
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
def validate_data_consistency(self) -> Dict[str, Any]:
|
| 507 |
+
"""Validate that all components are using consistent data"""
|
| 508 |
+
if not self.calculated_stats:
|
| 509 |
+
return {"valid": False, "message": "No data loaded"}
|
| 510 |
+
|
| 511 |
+
validation = {
|
| 512 |
+
"valid": True,
|
| 513 |
+
"data_source": self.data_source,
|
| 514 |
+
"data_age_minutes": int((datetime.now() - self.data_loaded_at).total_seconds() / 60) if self.data_loaded_at else None,
|
| 515 |
+
"total_readings": self.calculated_stats.get('total_readings', 0),
|
| 516 |
+
"time_in_range": self.calculated_stats.get('time_in_range_70_180', 0),
|
| 517 |
+
"average_glucose": self.calculated_stats.get('average_glucose', 0),
|
| 518 |
+
"user": self.current_user.name if self.current_user else None
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
logger.info(f"Data consistency check: {validation}")
|
| 522 |
+
|
| 523 |
+
return validation
|
| 524 |
+
|
| 525 |
+
# ADDITIONAL: Debug function to test the API connection as it was working before
|
| 526 |
+
def test_original_api_method():
|
| 527 |
+
"""Test the API exactly as it was working before unified data manager"""
|
| 528 |
+
from apifunctions import DexcomAPI, DEMO_USERS
|
| 529 |
+
|
| 530 |
+
print("π Testing API exactly as it was working before...")
|
| 531 |
+
|
| 532 |
+
api = DexcomAPI()
|
| 533 |
+
|
| 534 |
+
# Test with sarah_g7 as it was working before
|
| 535 |
+
user_key = "sarah_g7"
|
| 536 |
+
user = DEMO_USERS[user_key]
|
| 537 |
+
|
| 538 |
+
print(f"Testing with {user.name} ({user.username}) - NOW WITH UNSTABLE GLUCOSE")
|
| 539 |
+
|
| 540 |
+
try:
|
| 541 |
+
# Call simulate_demo_login exactly as before
|
| 542 |
+
access_token = api.simulate_demo_login(user_key)
|
| 543 |
+
print(f"β
Authentication: {bool(access_token)}")
|
| 544 |
+
|
| 545 |
+
if access_token:
|
| 546 |
+
# Call get_egv_data exactly as before
|
| 547 |
+
end_date = datetime.now()
|
| 548 |
+
start_date = end_date - timedelta(days=14)
|
| 549 |
+
|
| 550 |
+
egv_data = api.get_egv_data(
|
| 551 |
+
start_date.isoformat(),
|
| 552 |
+
end_date.isoformat()
|
| 553 |
+
)
|
| 554 |
+
|
| 555 |
+
print(f"β
EGV Data: {len(egv_data)} readings")
|
| 556 |
+
|
| 557 |
+
if egv_data:
|
| 558 |
+
print(f"β
SUCCESS! API is working as before (with Sarah's unstable patterns)")
|
| 559 |
+
sample = egv_data[0] if egv_data else {}
|
| 560 |
+
print(f"Sample reading: {sample}")
|
| 561 |
+
return True
|
| 562 |
+
else:
|
| 563 |
+
print("β οΈ API authenticated but returned no data")
|
| 564 |
+
return False
|
| 565 |
+
else:
|
| 566 |
+
print("β Authentication failed")
|
| 567 |
+
return False
|
| 568 |
+
|
| 569 |
+
except Exception as e:
|
| 570 |
+
print(f"β Error: {e}")
|
| 571 |
+
return False
|
| 572 |
+
|
| 573 |
+
if __name__ == "__main__":
|
| 574 |
+
# Test the original API method
|
| 575 |
+
test_original_api_method()
|