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()
|