Valentina9502 commited on
Commit
cdda4ac
Β·
verified Β·
1 Parent(s): a7abcf6

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +1306 -0
  2. dexcom_sandbox_oauth.py +790 -0
  3. 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()