Valentina9502 commited on
Commit
232e999
Β·
1 Parent(s): 471b7a1
Files changed (12) hide show
  1. .env.example +49 -0
  2. .gitignore +290 -0
  3. README copy.md +229 -0
  4. apifunctions.py +499 -0
  5. config.py +326 -0
  6. config_management.py +171 -0
  7. dexcom_real_auth_system.py +501 -0
  8. hybrid_auth.py +648 -0
  9. main.py +779 -0
  10. mistral_chat.py +727 -0
  11. requirements.txt +7 -0
  12. unified_data_manager.py +413 -0
.env.example ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GlycoAI Environment Configuration
2
+ # Copy this file to .env and fill in your actual values
3
+
4
+ # ===========================================
5
+ # REQUIRED CONFIGURATION
6
+ # ===========================================
7
+
8
+ # Mistral AI API Key (Required)
9
+ MISTRAL_API_KEY=your_mistral_api_key_here
10
+
11
+ # Mistral AI Agent ID (Optional but recommended)
12
+ MISTRAL_AGENT_ID=your_mistral_agent_id_here
13
+
14
+ # ===========================================
15
+ # OPTIONAL CONFIGURATION
16
+ # ===========================================
17
+
18
+ # Application Environment (development/production)
19
+ ENVIRONMENT=development
20
+
21
+ # Debug Mode (true/false)
22
+ DEBUG=false
23
+
24
+ # Log Level (DEBUG/INFO/WARNING/ERROR)
25
+ LOG_LEVEL=INFO
26
+
27
+ # ===========================================
28
+ # SETUP INSTRUCTIONS
29
+ # ===========================================
30
+
31
+ # For Local Development:
32
+ # 1. Copy this file: cp .env.example .env
33
+ # 2. Get your Mistral API key from: https://console.mistral.ai/
34
+ # 3. Replace "your_mistral_api_key_here" with your actual key
35
+ # 4. Optionally add your agent ID if you have one
36
+
37
+ # For Hugging Face Spaces:
38
+ # 1. Go to your Space settings
39
+ # 2. Navigate to "Repository secrets"
40
+ # 3. Add MISTRAL_API_KEY with your actual key
41
+ # 4. Optionally add MISTRAL_AGENT_ID
42
+
43
+ # ===========================================
44
+ # SECURITY NOTES
45
+ # ===========================================
46
+ # - Never commit the .env file to git
47
+ # - Keep your API keys secure
48
+ # - Use different keys for development and production
49
+ # - Monitor your API usage and costs
.gitignore ADDED
@@ -0,0 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GlycoAI .gitignore
2
+ # Security and cleanliness for the repository
3
+
4
+ # ===========================================
5
+ # SECURITY - NEVER COMMIT THESE!
6
+ # ===========================================
7
+
8
+ # Environment files with secrets
9
+ .env
10
+ .env.local
11
+ .env.development
12
+ .env.test
13
+ .env.production
14
+
15
+ # API Keys and credentials
16
+ **/api_keys.py
17
+ **/secrets.py
18
+ **/credentials.json
19
+ **/*_key.txt
20
+ **/*_secret.txt
21
+
22
+ # Configuration files with secrets
23
+ config_local.py
24
+ local_config.py
25
+
26
+ # ===========================================
27
+ # PYTHON
28
+ # ===========================================
29
+
30
+ # Byte-compiled / optimized / DLL files
31
+ __pycache__/
32
+ *.py[cod]
33
+ *$py.class
34
+
35
+ # C extensions
36
+ *.so
37
+
38
+ # Distribution / packaging
39
+ .Python
40
+ build/
41
+ develop-eggs/
42
+ dist/
43
+ downloads/
44
+ eggs/
45
+ .eggs/
46
+ lib/
47
+ lib64/
48
+ parts/
49
+ sdist/
50
+ var/
51
+ wheels/
52
+ pip-wheel-metadata/
53
+ share/python-wheels/
54
+ *.egg-info/
55
+ .installed.cfg
56
+ *.egg
57
+ MANIFEST
58
+
59
+ # PyInstaller
60
+ *.manifest
61
+ *.spec
62
+
63
+ # Installer logs
64
+ pip-log.txt
65
+ pip-delete-this-directory.txt
66
+
67
+ # Unit test / coverage reports
68
+ htmlcov/
69
+ .tox/
70
+ .nox/
71
+ .coverage
72
+ .coverage.*
73
+ .cache
74
+ nosetests.xml
75
+ coverage.xml
76
+ *.cover
77
+ *.py,cover
78
+ .hypothesis/
79
+ .pytest_cache/
80
+
81
+ # Translations
82
+ *.mo
83
+ *.pot
84
+
85
+ # Django stuff:
86
+ *.log
87
+ local_settings.py
88
+ db.sqlite3
89
+ db.sqlite3-journal
90
+
91
+ # Flask stuff:
92
+ instance/
93
+ .webassets-cache
94
+
95
+ # Scrapy stuff:
96
+ .scrapy
97
+
98
+ # Sphinx documentation
99
+ docs/_build/
100
+
101
+ # PyBuilder
102
+ target/
103
+
104
+ # Jupyter Notebook
105
+ .ipynb_checkpoints
106
+
107
+ # IPython
108
+ profile_default/
109
+ ipython_config.py
110
+
111
+ # pyenv
112
+ .python-version
113
+
114
+ # pipenv
115
+ Pipfile.lock
116
+
117
+ # PEP 582
118
+ __pypackages__/
119
+
120
+ # Celery stuff
121
+ celerybeat-schedule
122
+ celerybeat.pid
123
+
124
+ # SageMath parsed files
125
+ *.sage.py
126
+
127
+ # Environments
128
+ .venv
129
+ env/
130
+ venv/
131
+ ENV/
132
+ env.bak/
133
+ venv.bak/
134
+
135
+ # Spyder project settings
136
+ .spyderproject
137
+ .spyproject
138
+
139
+ # Rope project settings
140
+ .ropeproject
141
+
142
+ # mkdocs documentation
143
+ /site
144
+
145
+ # mypy
146
+ .mypy_cache/
147
+ .dmypy.json
148
+ dmypy.json
149
+
150
+ # Pyre type checker
151
+ .pyre/
152
+
153
+ # ===========================================
154
+ # GRADIO
155
+ # ===========================================
156
+
157
+ # Gradio cache
158
+ gradio_cached_examples/
159
+ *.db
160
+
161
+ # Gradio logs
162
+ gradio.log
163
+
164
+ # ===========================================
165
+ # IDE / EDITORS
166
+ # ===========================================
167
+
168
+ # VSCode
169
+ .vscode/
170
+ *.code-workspace
171
+
172
+ # PyCharm
173
+ .idea/
174
+
175
+ # Sublime Text
176
+ *.sublime-project
177
+ *.sublime-workspace
178
+
179
+ # Vim
180
+ *.swp
181
+ *.swo
182
+ *~
183
+
184
+ # Emacs
185
+ *~
186
+ \#*\#
187
+ .\#*
188
+
189
+ # ===========================================
190
+ # OPERATING SYSTEM
191
+ # ===========================================
192
+
193
+ # macOS
194
+ .DS_Store
195
+ .AppleDouble
196
+ .LSOverride
197
+
198
+ # macOS Thumbnails
199
+ ._*
200
+
201
+ # Windows
202
+ Thumbs.db
203
+ Thumbs.db:encryptable
204
+ ehthumbs.db
205
+ ehthumbs_vista.db
206
+ *.stackdump
207
+ [Dd]esktop.ini
208
+ $RECYCLE.BIN/
209
+ *.cab
210
+ *.msi
211
+ *.msix
212
+ *.msm
213
+ *.msp
214
+ *.lnk
215
+
216
+ # Linux
217
+ *~
218
+
219
+ # ===========================================
220
+ # APPLICATION SPECIFIC
221
+ # ===========================================
222
+
223
+ # Logs
224
+ logs/
225
+ *.log
226
+ npm-debug.log*
227
+ yarn-debug.log*
228
+ yarn-error.log*
229
+
230
+ # Runtime data
231
+ pids
232
+ *.pid
233
+ *.seed
234
+ *.pid.lock
235
+
236
+ # Coverage directory used by tools like istanbul
237
+ coverage/
238
+
239
+ # nyc test coverage
240
+ .nyc_output
241
+
242
+ # Temporary folders
243
+ tmp/
244
+ temp/
245
+
246
+ # ===========================================
247
+ # HUGGING FACE SPACES
248
+ # ===========================================
249
+
250
+ # HF Spaces specific (these are handled by the platform)
251
+ # flagged/
252
+
253
+ # ===========================================
254
+ # DEMO DATA (if generated locally)
255
+ # ===========================================
256
+
257
+ # Local demo data files
258
+ demo_data/
259
+ test_data/
260
+ sample_data/
261
+
262
+ # ===========================================
263
+ # BACKUP FILES
264
+ # ===========================================
265
+
266
+ *.bak
267
+ *.backup
268
+ *.old
269
+ *.orig
270
+
271
+ # ===========================================
272
+ # MISCELLANEOUS
273
+ # ===========================================
274
+
275
+ # Node modules (if any JS dependencies)
276
+ node_modules/
277
+
278
+ # Package files
279
+ *.7z
280
+ *.dmg
281
+ *.gz
282
+ *.iso
283
+ *.jar
284
+ *.rar
285
+ *.tar
286
+ *.zip
287
+
288
+ # Lock files
289
+ package-lock.json
290
+ yarn.lock
README copy.md ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: GlycoAI - AI Glucose Insights
3
+ emoji: 🩺
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: gradio
7
+ sdk_version: 4.44.0
8
+ app_file: main.py
9
+ pinned: false
10
+ license: mit
11
+ tags:
12
+ - healthcare
13
+ - diabetes
14
+ - glucose
15
+ - ai-assistant
16
+ - medical
17
+ - gradio
18
+ - mistral
19
+ - agent-demo-track
20
+ - dexcom
21
+ - cgm
22
+ ---
23
+
24
+ # 🩺 GlycoAI - AI-Powered Glucose Insights
25
+
26
+ **An intelligent diabetes management assistant powered by Mistral AI and Dexcom CGM integration**
27
+
28
+ [![Hugging Face Spaces](https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Spaces-blue)](https://huggingface.co/spaces/)
29
+ [![Gradio](https://img.shields.io/badge/Gradio-4.44.0-orange)](https://gradio.app/)
30
+ [![Mistral AI](https://img.shields.io/badge/Mistral%20AI-Agent-red)](https://mistral.ai/)
31
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
32
+
33
+ ## 🌟 Overview
34
+
35
+ GlycoAI is an advanced AI-powered chatbot that provides personalized glucose management insights for people with diabetes. By integrating Dexcom CGM data with Mistral AI's intelligent agents, it offers real-time analysis, pattern recognition, and actionable recommendations for better diabetes management.
36
+
37
+ ### ✨ Key Features
38
+
39
+ - πŸ“Š **14-Day Glucose Analysis**: Comprehensive pattern analysis across two weeks of data
40
+ - πŸ€– **AI-Powered Insights**: Mistral AI agent provides personalized recommendations
41
+ - πŸ“ˆ **Interactive Visualizations**: Glucose trend charts and statistics
42
+ - πŸ‘₯ **Demo Users**: Pre-configured profiles for different diabetes scenarios
43
+ - πŸ’¬ **Natural Conversations**: Chat naturally about your glucose patterns and concerns
44
+ - 🎯 **Clinical Targets**: Track time-in-range, hypoglycemia, and variability metrics
45
+ - πŸ“± **Multi-Device Support**: Works with G6, G7, and ONE+ CGM systems
46
+
47
+ ## πŸš€ Try It Now
48
+
49
+ **No setup required!** Simply:
50
+ 1. Select a demo user (Sarah, Marcus, Jennifer, or Robert)
51
+ 2. Load their 14-day glucose data
52
+ 3. Start chatting with GlycoAI about patterns and recommendations
53
+
54
+ ## πŸ”¬ Technical Implementation
55
+
56
+ ### AI Agent Architecture
57
+ - **Mistral AI Agent**: Custom-trained agent specialized in diabetes management
58
+ - **Context-Aware**: Incorporates real glucose data into conversations
59
+ - **Pattern Recognition**: Identifies trends, meal effects, and lifestyle correlations
60
+ - **Personalized Advice**: Tailored recommendations based on individual patterns
61
+
62
+ ### Data Processing Pipeline
63
+ ```
64
+ Dexcom API β†’ Data Validation β†’ Pattern Analysis β†’ AI Context β†’ Chat Response
65
+ ```
66
+
67
+ ### Key Metrics Analyzed
68
+ - **Time in Range (TIR)**: Target 70-180 mg/dL
69
+ - **Glucose Variability**: Coefficient of variation
70
+ - **Hypoglycemia Risk**: Time below 70 mg/dL
71
+ - **Hyperglycemia**: Time above 180 mg/dL
72
+ - **Daily Patterns**: Dawn phenomenon, meal effects
73
+ - **Weekly Trends**: Weekday vs weekend variations
74
+
75
+ ## πŸ‘₯ Demo Users
76
+
77
+ ### πŸƒβ€β™€οΈ Sarah Thompson (G7 Mobile)
78
+ - **Profile**: 32-year-old professional with Type 1 diabetes
79
+ - **Pattern**: Stable control with meal spikes
80
+ - **Device**: Dexcom G7 with smartphone integration
81
+
82
+ ### πŸ‘¨β€πŸ‘§β€πŸ‘¦ Marcus Rodriguez (ONE+ Mobile)
83
+ - **Profile**: 45-year-old father with Type 2 diabetes
84
+ - **Pattern**: Dawn phenomenon, moderate variability
85
+ - **Device**: Dexcom ONE+ with lifestyle management focus
86
+
87
+ ### πŸŽ“ Jennifer Chen (G6 Mobile)
88
+ - **Profile**: 28-year-old graduate student with Type 1 diabetes
89
+ - **Pattern**: Exercise-related lows, tech-savvy user
90
+ - **Device**: Dexcom G6 with active lifestyle
91
+
92
+ ### πŸ‘¨β€πŸ« Robert Williams (G6 Receiver)
93
+ - **Profile**: 67-year-old retired teacher with Type 2 diabetes
94
+ - **Pattern**: Consistent dawn phenomenon
95
+ - **Device**: Dexcom G6 with dedicated receiver
96
+
97
+ ## πŸ’‘ Example Conversations
98
+
99
+ **"What's my average glucose level?"**
100
+ > Based on your 14-day data, your average glucose is 142 mg/dL with good stability. Your time in range is 68%, which is close to the clinical target of >70%. πŸ“Š
101
+
102
+ **"I keep having morning highs. What can I do?"**
103
+ > I notice you have dawn phenomenon with glucose rising 30-40 mg/dL between 4-7 AM. This affects 5 out of 14 mornings in your data. Consider discussing overnight insulin adjustments with your healthcare provider. πŸŒ…
104
+
105
+ **"How does my weekend compare to weekdays?"**
106
+ > Interesting pattern! Your weekends show 15 mg/dL lower average glucose (135 vs 150 mg/dL weekdays). You seem to have more consistent meal timing on weekends. πŸ“ˆ
107
+
108
+ ## πŸ› οΈ Technical Stack
109
+
110
+ - **Frontend**: Gradio 4.44.0 with custom CSS styling
111
+ - **Backend**: Python with FastAPI-style architecture
112
+ - **AI Engine**: Mistral AI agents with specialized diabetes knowledge
113
+ - **Data Source**: Dexcom Sandbox API with realistic mock data
114
+ - **Visualization**: Plotly for interactive glucose charts
115
+ - **Processing**: Pandas/NumPy for statistical analysis
116
+
117
+ ## πŸ“Š Data Analysis Features
118
+
119
+ ### Pattern Recognition
120
+ - **Meal Effects**: Identifies post-meal glucose spikes and timing
121
+ - **Exercise Impact**: Detects glucose drops during physical activity
122
+ - **Sleep Patterns**: Analyzes overnight glucose stability
123
+ - **Stress Correlation**: Identifies high-glucose periods linked to lifestyle
124
+
125
+ ### Statistical Analysis
126
+ - **Glucose Management Indicator (GMI)**: HbA1c estimation
127
+ - **Coefficient of Variation**: Glucose stability measurement
128
+ - **Time-in-Range Analysis**: Clinical target tracking
129
+ - **Trend Analysis**: Week-over-week improvement detection
130
+
131
+ ### Predictive Insights
132
+ - **Risk Identification**: Predicts hypoglycemia patterns
133
+ - **Optimization Suggestions**: Recommends timing adjustments
134
+ - **Lifestyle Correlations**: Links patterns to daily activities
135
+ - **Goal Tracking**: Monitors progress toward clinical targets
136
+
137
+ ## πŸ”’ Privacy & Security
138
+
139
+ - **No Data Storage**: Conversations and glucose data are not permanently stored
140
+ - **Sandbox Environment**: Uses Dexcom's secure sandbox API
141
+ - **Educational Purpose**: Designed for demonstration and learning
142
+ - **Medical Disclaimer**: Not intended to replace professional medical advice
143
+
144
+ ## βš•οΈ Medical Disclaimer
145
+
146
+ **Important**: GlycoAI is for educational and informational purposes only. It does not provide medical advice, diagnosis, or treatment. Always consult with qualified healthcare providers before making any changes to your diabetes management plan.
147
+
148
+ ## 🎯 Agent Demo Track
149
+
150
+ This application showcases advanced AI agent capabilities for healthcare applications:
151
+
152
+ - **Contextual Understanding**: Processes complex medical data
153
+ - **Personalized Responses**: Adapts advice to individual patterns
154
+ - **Multi-Modal Analysis**: Combines numerical data with conversational AI
155
+ - **Domain Expertise**: Specialized knowledge in diabetes management
156
+ - **Real-Time Processing**: Instant analysis of glucose trends
157
+
158
+ Perfect example of how AI agents can augment healthcare decision-making while maintaining appropriate clinical boundaries.
159
+
160
+ ## πŸš€ Getting Started
161
+
162
+ ### Option 1: Use This Space (Recommended)
163
+ Just click the demo above! No installation needed.
164
+
165
+ ### Option 2: Local Installation
166
+ ```bash
167
+ git clone https://github.com/your-repo/glycoai
168
+ cd glycoai
169
+ pip install -r requirements.txt
170
+ python main.py
171
+ ```
172
+
173
+ ### Option 3: API Integration
174
+ ```python
175
+ from mistral_chat import GlucoBuddyMistralChat
176
+
177
+ # Initialize with your Mistral API key
178
+ chat = GlucoBuddyMistralChat("your-api-key", "your-agent-id")
179
+
180
+ # Load demo user
181
+ chat.load_user_data("sarah_g7")
182
+
183
+ # Start chatting
184
+ response = chat.chat_with_mistral("What's my time in range?")
185
+ print(response['response'])
186
+ ```
187
+
188
+ ## πŸ“ˆ Roadmap
189
+
190
+ - [ ] **Real Dexcom Integration**: Connect to live CGM data
191
+ - [ ] **Health data**: integration with Apple Health for obtaining further insights (e.g hormonal influence)
192
+ - [ ] **Insulin Tracking**: Dosing recommendations and timing
193
+ - [ ] **Healthcare Provider Dashboard**: Shareable reports
194
+ - [ ] **Mobile App**: Native iOS/Android applications
195
+ - [ ] **Multiple Languages**: Multilingual diabetes support
196
+
197
+ ## 🀝 Contributing
198
+
199
+ We welcome contributions! Areas of interest:
200
+ - **Medical Accuracy**: Improve clinical recommendations
201
+ - **UI/UX Enhancement**: Better user experience design
202
+ - **Data Analysis**: Advanced pattern recognition algorithms
203
+ - **Agent Training**: Enhance AI conversation quality
204
+ - **Integration**: Additional CGM device support
205
+
206
+ ## πŸ“ž Support
207
+
208
+ - **Documentation**: [Full Documentation](https://github.com/your-repo/glycoai/wiki)
209
+ - **Issues**: [GitHub Issues](https://github.com/your-repo/glycoai/issues)
210
+ - **Discussions**: [Community Forum](https://github.com/your-repo/glycoai/discussions)
211
+
212
+
213
+
214
+ ## πŸ“„ License
215
+
216
+ MIT License - see [LICENSE](LICENSE) file for details.
217
+
218
+ ---
219
+
220
+ <div align="center">
221
+
222
+ **Built with ❀️ for the diabetes community**
223
+
224
+ *Empowering better glucose management through AI*
225
+
226
+ [![Follow on HF](https://img.shields.io/badge/Follow%20on-Hugging%20Face-yellow)](https://huggingface.co/spaces/)
227
+ [![Star on GitHub](https://img.shields.io/badge/Star%20on-GitHub-black)](https://github.com/your-repo/glycoai)
228
+
229
+ </div>
apifunctions.py ADDED
@@ -0,0 +1,499 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Dexcom Client for glucose data analysis - RESTORED WORKING VERSION
4
+ This restores the original working API code before unified data manager
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ import sys
10
+ from typing import Any, Dict, List, Optional, Union
11
+ from datetime import datetime, timedelta
12
+ import pandas as pd
13
+ from dataclasses import asdict
14
+
15
+ """
16
+ Dexcom API Integration Functions
17
+ This module contains all the functions for interacting with the Dexcom Sandbox API
18
+ and processing glucose data for AI insights.
19
+ """
20
+
21
+ import requests
22
+ import json
23
+ from datetime import datetime, timedelta
24
+ from typing import Dict, List, Optional, Tuple
25
+ import pandas as pd
26
+ from dataclasses import dataclass
27
+ import logging
28
+ import base64
29
+ import secrets
30
+ import hashlib
31
+ import requests
32
+ from typing import Dict, List, Optional
33
+
34
+ # Set up logging
35
+ logging.basicConfig(level=logging.INFO)
36
+ logger = logging.getLogger(__name__)
37
+
38
+ @dataclass
39
+ class DemoUser:
40
+ """Demo user configuration for sandbox testing"""
41
+ name: str
42
+ device_type: str
43
+ username: str
44
+ password: str
45
+ description: str
46
+ age: int = 30
47
+ diabetes_type: str = "Type 1"
48
+ years_with_diabetes: int = 5
49
+ typical_glucose_pattern: str = "normal"
50
+
51
+ # Demo users - EXACTLY as they were when it was working
52
+ DEMO_USERS = {
53
+ "sarah_g7": DemoUser(
54
+ name="Sarah Thompson",
55
+ age=32,
56
+ device_type="G7 Mobile App",
57
+ username="[email protected]",
58
+ password="Dexcom123!",
59
+ description="Active professional with Type 1 diabetes, uses G7 CGM with smartphone integration",
60
+ diabetes_type="Type 1",
61
+ years_with_diabetes=8,
62
+ typical_glucose_pattern="stable_with_meal_spikes"
63
+ ),
64
+ "marcus_one": DemoUser(
65
+ name="Marcus Rodriguez",
66
+ age=45,
67
+ device_type="ONE+ Mobile App",
68
+ username="[email protected]",
69
+ password="Dexcom123!",
70
+ description="Father of two with Type 2 diabetes, manages with Dexcom ONE+ and lifestyle changes",
71
+ diabetes_type="Type 2",
72
+ years_with_diabetes=3,
73
+ typical_glucose_pattern="moderate_variability"
74
+ ),
75
+ "jennifer_g6": DemoUser(
76
+ name="Jennifer Chen",
77
+ age=28,
78
+ device_type="G6 Mobile App",
79
+ username="[email protected]",
80
+ password="Dexcom123!",
81
+ description="Graduate student with Type 1 diabetes, tech-savvy G6 user with active lifestyle",
82
+ diabetes_type="Type 1",
83
+ years_with_diabetes=12,
84
+ typical_glucose_pattern="exercise_related_lows"
85
+ ),
86
+ "robert_receiver": DemoUser(
87
+ name="Robert Williams",
88
+ age=67,
89
+ device_type="G6 Touchscreen Receiver",
90
+ username="[email protected]",
91
+ password="Dexcom123!",
92
+ description="Retired teacher with Type 2 diabetes, prefers dedicated receiver device",
93
+ diabetes_type="Type 2",
94
+ years_with_diabetes=15,
95
+ typical_glucose_pattern="dawn_phenomenon"
96
+ )
97
+ }
98
+
99
+ # Dexcom API Configuration - ORIGINAL WORKING VERSION
100
+ SANDBOX_BASE_URL = "https://sandbox-api.dexcom.com"
101
+ CLIENT_ID = "mLElKHKRwRDVUrAOPBzktFGY7qkTc7Zm"
102
+ CLIENT_SECRET = "HmFpgyVweuwKrQpf"
103
+ REDIRECT_URI = "http://localhost:7860/callback"
104
+
105
+ class DexcomAPI:
106
+ """Handles all Dexcom API interactions with proper OAuth flow - ORIGINAL WORKING VERSION"""
107
+
108
+ def __init__(self):
109
+ self.base_url = SANDBOX_BASE_URL
110
+ self.access_token = None
111
+ self.refresh_token = None
112
+ self.token_expires_at = None
113
+
114
+ def get_authorization_url(self, state: str = None) -> str:
115
+ """Generate the OAuth authorization URL for user login"""
116
+ params = {
117
+ "client_id": CLIENT_ID,
118
+ "redirect_uri": REDIRECT_URI,
119
+ "response_type": "code",
120
+ "scope": "offline_access"
121
+ }
122
+ if state:
123
+ params["state"] = state
124
+
125
+ query_string = "&".join([f"{k}={v}" for k, v in params.items()])
126
+ return f"{self.base_url}/v2/oauth2/login?{query_string}"
127
+
128
+ def exchange_code_for_token(self, authorization_code: str) -> Dict:
129
+ """Exchange authorization code for access and refresh tokens"""
130
+ url = f"{self.base_url}/v2/oauth2/token"
131
+
132
+ data = {
133
+ "client_id": CLIENT_ID,
134
+ "client_secret": CLIENT_SECRET,
135
+ "code": authorization_code,
136
+ "grant_type": "authorization_code",
137
+ "redirect_uri": REDIRECT_URI
138
+ }
139
+
140
+ headers = {
141
+ "Content-Type": "application/x-www-form-urlencoded"
142
+ }
143
+
144
+ try:
145
+ response = requests.post(url, data=data, headers=headers)
146
+ response.raise_for_status()
147
+
148
+ token_data = response.json()
149
+ self.access_token = token_data.get("access_token")
150
+ self.refresh_token = token_data.get("refresh_token")
151
+
152
+ expires_in = token_data.get("expires_in", 3600)
153
+ self.token_expires_at = datetime.now() + timedelta(seconds=expires_in)
154
+
155
+ return token_data
156
+ except requests.exceptions.RequestException as e:
157
+ raise Exception(f"Failed to exchange authorization code: {str(e)}")
158
+
159
+ def simulate_demo_login(self, demo_user_key: str) -> str:
160
+ """Simulate OAuth flow for demo users using sandbox credentials - ORIGINAL WORKING VERSION"""
161
+ if demo_user_key not in DEMO_USERS:
162
+ raise ValueError(f"Invalid demo user: {demo_user_key}")
163
+
164
+ user = DEMO_USERS[demo_user_key]
165
+
166
+ try:
167
+ auth_code = self._simulate_sandbox_login(user.username, user.password)
168
+
169
+ if auth_code:
170
+ token_data = self.exchange_code_for_token(auth_code)
171
+ logger.info(f"Successfully obtained tokens for {user.name}")
172
+ return self.access_token
173
+ else:
174
+ return self._direct_sandbox_token_request(user.username, user.password)
175
+
176
+ except Exception as e:
177
+ logger.warning(f"OAuth flow failed, trying direct sandbox authentication: {e}")
178
+ return self._direct_sandbox_token_request(user.username, user.password)
179
+
180
+ def _simulate_sandbox_login(self, username: str, password: str) -> Optional[str]:
181
+ """Simulate the sandbox login process to get authorization code"""
182
+ try:
183
+ auth_string = f"{username}:{password}:{datetime.now().strftime('%Y%m%d')}"
184
+ auth_code = base64.b64encode(auth_string.encode()).decode()[:32]
185
+ return auth_code
186
+
187
+ except Exception as e:
188
+ logger.error(f"Failed to simulate sandbox login: {e}")
189
+ return None
190
+
191
+ def _direct_sandbox_token_request(self, username: str, password: str) -> str:
192
+ """Direct token request for sandbox environment"""
193
+ url = f"{self.base_url}/v2/oauth2/token"
194
+ credentials = base64.b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()
195
+
196
+ headers = {
197
+ "Authorization": f"Basic {credentials}",
198
+ "Content-Type": "application/x-www-form-urlencoded"
199
+ }
200
+
201
+ data = {
202
+ "grant_type": "password",
203
+ "username": username,
204
+ "password": password,
205
+ "scope": "offline_access"
206
+ }
207
+
208
+ try:
209
+ response = requests.post(url, data=data, headers=headers)
210
+
211
+ if response.status_code == 200:
212
+ token_data = response.json()
213
+ self.access_token = token_data.get("access_token")
214
+ self.refresh_token = token_data.get("refresh_token")
215
+
216
+ expires_in = token_data.get("expires_in", 3600)
217
+ self.token_expires_at = datetime.now() + timedelta(seconds=expires_in)
218
+
219
+ logger.info(f"Successfully obtained sandbox token for {username}")
220
+ return self.access_token
221
+ else:
222
+ sandbox_token = self._generate_sandbox_demo_token(username)
223
+ self.access_token = sandbox_token
224
+ self.token_expires_at = datetime.now() + timedelta(hours=1)
225
+
226
+ logger.info(f"Using demo token for sandbox user {username}")
227
+ return sandbox_token
228
+
229
+ except Exception as e:
230
+ logger.error(f"Direct token request failed: {e}")
231
+ sandbox_token = self._generate_sandbox_demo_token(username)
232
+ self.access_token = sandbox_token
233
+ self.token_expires_at = datetime.now() + timedelta(hours=1)
234
+ return sandbox_token
235
+
236
+ def _generate_sandbox_demo_token(self, username: str) -> str:
237
+ """Generate a realistic-looking demo token for sandbox"""
238
+ token_data = f"{username}:{datetime.now().strftime('%Y%m%d')}:{CLIENT_ID}"
239
+ token_hash = hashlib.sha256(token_data.encode()).hexdigest()
240
+ return f"sandbox_token_{token_hash[:16]}"
241
+
242
+ def _is_token_expired(self) -> bool:
243
+ """Check if the current token is expired"""
244
+ if not self.token_expires_at:
245
+ return True
246
+ return datetime.now() >= self.token_expires_at
247
+
248
+ def _ensure_valid_token(self):
249
+ """Ensure we have a valid, non-expired token"""
250
+ if not self.access_token or self._is_token_expired():
251
+ if self.refresh_token:
252
+ try:
253
+ self.refresh_access_token()
254
+ except:
255
+ raise Exception("Token expired and refresh failed. Please re-authenticate.")
256
+ else:
257
+ raise Exception("No valid token available. Please authenticate first.")
258
+
259
+ def refresh_access_token(self) -> Dict:
260
+ """Refresh the access token using refresh token"""
261
+ if not self.refresh_token:
262
+ raise Exception("No refresh token available")
263
+
264
+ url = f"{self.base_url}/v2/oauth2/token"
265
+
266
+ data = {
267
+ "client_id": CLIENT_ID,
268
+ "client_secret": CLIENT_SECRET,
269
+ "refresh_token": self.refresh_token,
270
+ "grant_type": "refresh_token"
271
+ }
272
+
273
+ headers = {
274
+ "Content-Type": "application/x-www-form-urlencoded"
275
+ }
276
+
277
+ try:
278
+ response = requests.post(url, data=data, headers=headers)
279
+ response.raise_for_status()
280
+
281
+ token_data = response.json()
282
+ self.access_token = token_data.get("access_token")
283
+ if "refresh_token" in token_data:
284
+ self.refresh_token = token_data.get("refresh_token")
285
+
286
+ expires_in = token_data.get("expires_in", 3600)
287
+ self.token_expires_at = datetime.now() + timedelta(seconds=expires_in)
288
+
289
+ return token_data
290
+ except requests.exceptions.RequestException as e:
291
+ raise Exception(f"Failed to refresh token: {str(e)}")
292
+
293
+ def get_data_range(self) -> Dict:
294
+ """Get the available data range for the authenticated user"""
295
+ self._ensure_valid_token()
296
+
297
+ url = f"{self.base_url}/v2/users/self/dataRange"
298
+ headers = {
299
+ "Authorization": f"Bearer {self.access_token}"
300
+ }
301
+
302
+ try:
303
+ response = requests.get(url, headers=headers)
304
+
305
+ if response.status_code == 401:
306
+ if self.refresh_token:
307
+ self.refresh_access_token()
308
+ headers["Authorization"] = f"Bearer {self.access_token}"
309
+ response = requests.get(url, headers=headers)
310
+
311
+ if response.status_code == 200:
312
+ return response.json()
313
+ else:
314
+ return {
315
+ "egvStart": (datetime.now() - timedelta(days=30)).isoformat(),
316
+ "egvEnd": datetime.now().isoformat(),
317
+ "eventStart": (datetime.now() - timedelta(days=30)).isoformat(),
318
+ "eventEnd": datetime.now().isoformat()
319
+ }
320
+
321
+ except requests.exceptions.RequestException as e:
322
+ logger.warning(f"Failed to get data range from API: {e}, using demo range")
323
+ return {
324
+ "egvStart": (datetime.now() - timedelta(days=30)).isoformat(),
325
+ "egvEnd": datetime.now().isoformat(),
326
+ "eventStart": (datetime.now() - timedelta(days=30)).isoformat(),
327
+ "eventEnd": datetime.now().isoformat()
328
+ }
329
+
330
+ def get_egv_data(self, start_date: str = None, end_date: str = None) -> List[Dict]:
331
+ """Get Estimated Glucose Values (EGV) data - ORIGINAL WORKING VERSION"""
332
+ self._ensure_valid_token()
333
+
334
+ url = f"{self.base_url}/v2/users/self/egvs"
335
+ headers = {
336
+ "Authorization": f"Bearer {self.access_token}"
337
+ }
338
+
339
+ params = {}
340
+ if start_date:
341
+ params["startDate"] = start_date
342
+ if end_date:
343
+ params["endDate"] = end_date
344
+
345
+ try:
346
+ response = requests.get(url, headers=headers, params=params)
347
+
348
+ if response.status_code == 401:
349
+ if self.refresh_token:
350
+ self.refresh_access_token()
351
+ headers["Authorization"] = f"Bearer {self.access_token}"
352
+ response = requests.get(url, headers=headers, params=params)
353
+
354
+ if response.status_code == 200:
355
+ data = response.json()
356
+ return data.get("egvs", [])
357
+ else:
358
+ logger.warning(f"API returned status {response.status_code}, generating demo data")
359
+ return []
360
+
361
+ except requests.exceptions.RequestException as e:
362
+ logger.warning(f"Failed to get EGV data from API: {e}, will use demo data")
363
+ return []
364
+
365
+ def get_events_data(self, start_date: str = None, end_date: str = None) -> List[Dict]:
366
+ """Get events data (meals, insulin, etc.)"""
367
+ self._ensure_valid_token()
368
+
369
+ url = f"{self.base_url}/v2/users/self/events"
370
+ headers = {
371
+ "Authorization": f"Bearer {self.access_token}"
372
+ }
373
+
374
+ params = {}
375
+ if start_date:
376
+ params["startDate"] = start_date
377
+ if end_date:
378
+ params["endDate"] = end_date
379
+
380
+ try:
381
+ response = requests.get(url, headers=headers, params=params)
382
+
383
+ if response.status_code == 401:
384
+ if self.refresh_token:
385
+ self.refresh_access_token()
386
+ headers["Authorization"] = f"Bearer {self.access_token}"
387
+ response = requests.get(url, headers=headers, params=params)
388
+
389
+ if response.status_code == 200:
390
+ data = response.json()
391
+ return data.get("events", [])
392
+ else:
393
+ logger.warning(f"Events API returned status {response.status_code}")
394
+ return []
395
+
396
+ except requests.exceptions.RequestException as e:
397
+ logger.warning(f"Failed to get events data: {e}")
398
+ return []
399
+
400
+ class GlucoseAnalyzer:
401
+ """Analyzes glucose data and generates insights"""
402
+
403
+ @staticmethod
404
+ def process_egv_data(egv_data: List[Dict]) -> pd.DataFrame:
405
+ """Convert EGV data to pandas DataFrame for analysis"""
406
+ if not egv_data:
407
+ return pd.DataFrame()
408
+
409
+ df = pd.DataFrame(egv_data)
410
+ df['systemTime'] = pd.to_datetime(df['systemTime'])
411
+ df['displayTime'] = pd.to_datetime(df['displayTime'])
412
+
413
+ df['value'] = pd.to_numeric(df['value'], errors='coerce')
414
+
415
+ return df.sort_values('systemTime')
416
+
417
+ @staticmethod
418
+ def calculate_basic_stats(df: pd.DataFrame) -> Dict:
419
+ """Calculate basic glucose statistics"""
420
+ if df.empty:
421
+ return {}
422
+
423
+ glucose_values = df['value'].dropna()
424
+
425
+ return {
426
+ "average_glucose": glucose_values.mean(),
427
+ "min_glucose": glucose_values.min(),
428
+ "max_glucose": glucose_values.max(),
429
+ "std_glucose": glucose_values.std(),
430
+ "time_in_range_70_180": len(glucose_values[(glucose_values >= 70) & (glucose_values <= 180)]) / len(glucose_values) * 100,
431
+ "time_below_70": len(glucose_values[glucose_values < 70]) / len(glucose_values) * 100,
432
+ "time_above_180": len(glucose_values[glucose_values > 180]) / len(glucose_values) * 100,
433
+ "total_readings": len(glucose_values)
434
+ }
435
+
436
+ @staticmethod
437
+ def identify_patterns(df: pd.DataFrame) -> Dict:
438
+ """Identify glucose patterns and trends"""
439
+ if df.empty or len(df) < 10:
440
+ return {"patterns": "Insufficient data for pattern analysis"}
441
+
442
+ patterns = []
443
+
444
+ df['hour'] = df['systemTime'].dt.hour
445
+ hourly_avg = df.groupby('hour')['value'].mean()
446
+
447
+ peak_hour = hourly_avg.idxmax()
448
+ low_hour = hourly_avg.idxmin()
449
+
450
+ patterns.append(f"Glucose typically peaks around {peak_hour}:00")
451
+ patterns.append(f"Glucose is typically lowest around {low_hour}:00")
452
+
453
+ glucose_std = df['value'].std()
454
+ if glucose_std > 50:
455
+ patterns.append("High glucose variability detected - consider discussing with healthcare provider")
456
+ elif glucose_std < 20:
457
+ patterns.append("Good glucose stability observed")
458
+
459
+ recent_data = df.tail(20)
460
+ if len(recent_data) >= 10:
461
+ trend_slope = (recent_data['value'].iloc[-1] - recent_data['value'].iloc[0]) / len(recent_data)
462
+ if trend_slope > 2:
463
+ patterns.append("Recent upward glucose trend observed")
464
+ elif trend_slope < -2:
465
+ patterns.append("Recent downward glucose trend observed")
466
+ else:
467
+ patterns.append("Glucose levels relatively stable recently")
468
+
469
+ return {"patterns": patterns}
470
+
471
+ def format_glucose_data_for_display(df: pd.DataFrame) -> str:
472
+ """Format glucose data for display in the interface"""
473
+ if df.empty:
474
+ return "No glucose data available"
475
+
476
+ recent_data = df.tail(10)
477
+
478
+ formatted_data = "## Recent Glucose Readings\n\n"
479
+ formatted_data += "| Time | Glucose (mg/dL) | Trend |\n"
480
+ formatted_data += "|------|-----------------|-------|\n"
481
+
482
+ for _, row in recent_data.iterrows():
483
+ time_str = row['displayTime'].strftime("%m/%d %H:%M")
484
+ glucose = row['value']
485
+ trend = row.get('trend', 'N/A')
486
+
487
+ trend_arrow = {
488
+ 'flat': 'β†’',
489
+ 'fortyFiveUp': 'β†—',
490
+ 'singleUp': '↑',
491
+ 'doubleUp': '⬆',
492
+ 'fortyFiveDown': 'β†˜',
493
+ 'singleDown': '↓',
494
+ 'doubleDown': '⬇'
495
+ }.get(trend, 'β†’')
496
+
497
+ formatted_data += f"| {time_str} | {glucose:.0f} | {trend_arrow} |\n"
498
+
499
+ return formatted_data
config.py ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ GlucoBuddy Configuration File
3
+ This file contains all configuration settings for the GlucoBuddy application,
4
+ including Dexcom API credentials, Claude MCP settings, and application preferences.
5
+
6
+ IMPORTANT:
7
+ - Replace placeholder values with your actual credentials
8
+ - Never commit this file to version control with real credentials
9
+ - Consider using environment variables for sensitive data in production
10
+ """
11
+
12
+ import os
13
+ from typing import Dict, Any
14
+
15
+ # =============================================================================
16
+ # DEXCOM API CONFIGURATION
17
+ # =============================================================================
18
+
19
+ # Dexcom Developer Portal Credentials
20
+ # Get these from: https://developer.dexcom.com/
21
+ DEXCOM_CONFIG = {
22
+ # REQUIRED: Replace with your actual Dexcom Developer Portal credentials
23
+ "CLIENT_ID": os.getenv("DEXCOM_CLIENT_ID", "your_client_id_here"),
24
+ "CLIENT_SECRET": os.getenv("DEXCOM_CLIENT_SECRET", "your_client_secret_here"),
25
+
26
+ # OAuth Redirect URI - must match exactly what you registered in Developer Portal
27
+ "REDIRECT_URI": "http://localhost:7860/callback",
28
+
29
+ # API Endpoints
30
+ "SANDBOX_BASE_URL": "https://sandbox-api.dexcom.com",
31
+ "PRODUCTION_BASE_URL": "https://api.dexcom.com",
32
+
33
+ # Environment setting - set to False for production
34
+ "USE_SANDBOX": True,
35
+
36
+ # OAuth Scopes
37
+ "SCOPES": ["offline_access"],
38
+
39
+ # Token refresh settings
40
+ "TOKEN_REFRESH_BUFFER_MINUTES": 5, # Refresh token 5 minutes before expiration
41
+ "MAX_RETRY_ATTEMPTS": 3,
42
+ }
43
+
44
+ # =============================================================================
45
+ # CLAUDE MCP CONFIGURATION
46
+ # =============================================================================
47
+
48
+ # Claude API Configuration
49
+ # Get your API key from: https://console.anthropic.com/
50
+ CLAUDE_CONFIG = {
51
+ # REQUIRED: Replace with your actual Anthropic API key
52
+ "API_KEY": os.getenv("ANTHROPIC_API_KEY", "your_anthropic_api_key_here"),
53
+
54
+ # Model selection
55
+ "MODEL": "claude-3-5-sonnet-20241022", # Updated to latest model
56
+
57
+ # Generation parameters
58
+ "MAX_TOKENS": 4000,
59
+ "TEMPERATURE": 0.3, # Lower for more consistent medical advice
60
+
61
+ # Timeout settings
62
+ "REQUEST_TIMEOUT": 60, # seconds
63
+ "MAX_RETRIES": 2,
64
+
65
+ # Content filtering
66
+ "ENABLE_SAFETY_FILTERS": True,
67
+ }
68
+
69
+ # =============================================================================
70
+ # APPLICATION SETTINGS
71
+ # =============================================================================
72
+
73
+ # Logging Configuration
74
+ LOGGING_CONFIG = {
75
+ "LEVEL": "INFO", # DEBUG, INFO, WARNING, ERROR, CRITICAL
76
+ "FORMAT": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
77
+ "LOG_FILE": "glucobuddy.log",
78
+ "MAX_LOG_SIZE_MB": 10,
79
+ "BACKUP_COUNT": 3,
80
+ "ENABLE_CONSOLE_LOGGING": True,
81
+ "ENABLE_FILE_LOGGING": True,
82
+ }
83
+
84
+ # Data Processing Settings
85
+ DATA_CONFIG = {
86
+ # Default data range for analysis
87
+ "DEFAULT_DAYS_BACK": 7,
88
+ "MAX_DAYS_BACK": 30,
89
+
90
+ # Glucose thresholds (mg/dL)
91
+ "TARGET_RANGE_LOW": 70,
92
+ "TARGET_RANGE_HIGH": 180,
93
+ "HYPOGLYCEMIA_THRESHOLD": 70,
94
+ "HYPERGLYCEMIA_THRESHOLD": 250,
95
+
96
+ # Time in Range targets (percentages)
97
+ "EXCELLENT_TIR_THRESHOLD": 80,
98
+ "GOOD_TIR_THRESHOLD": 70,
99
+ "ACCEPTABLE_TIR_THRESHOLD": 50,
100
+
101
+ # Data quality settings
102
+ "MIN_READINGS_FOR_ANALYSIS": 20,
103
+ "MAX_GAP_HOURS": 3, # Maximum gap between readings for continuous analysis
104
+
105
+ # Pattern detection settings
106
+ "PATTERN_DETECTION_MIN_DAYS": 3,
107
+ "SIGNIFICANT_TREND_THRESHOLD": 2.0, # mg/dL per reading
108
+ }
109
+
110
+ # =============================================================================
111
+ # DEMO USER CONFIGURATION
112
+ # =============================================================================
113
+
114
+ # Demo mode settings - for development and testing
115
+ DEMO_CONFIG = {
116
+ "ENABLE_DEMO_MODE": True,
117
+ "DEFAULT_DEMO_USER": "sarah_g7", # Which demo user to use by default
118
+ "GENERATE_SYNTHETIC_DATA": True, # Generate realistic demo data when API fails
119
+ "DEMO_DATA_DAYS": 14, # Days of demo data to generate
120
+ "DEMO_DATA_NOISE_FACTOR": 0.1, # Randomness in demo data (0.0 to 1.0)
121
+ }
122
+
123
+ # =============================================================================
124
+ # SECURITY SETTINGS
125
+ # =============================================================================
126
+
127
+ SECURITY_CONFIG = {
128
+ # Data encryption (for local storage)
129
+ "ENCRYPT_LOCAL_DATA": True,
130
+ "ENCRYPTION_KEY": os.getenv("GLUCOBUDDY_ENCRYPTION_KEY", None),
131
+
132
+ # Session management
133
+ "SESSION_TIMEOUT_MINUTES": 30,
134
+ "REQUIRE_REAUTHENTICATION": True,
135
+
136
+ # Data retention
137
+ "MAX_LOCAL_DATA_DAYS": 90,
138
+ "AUTO_CLEANUP_OLD_DATA": True,
139
+
140
+ # Privacy settings
141
+ "ANONYMIZE_LOGS": True,
142
+ "ALLOW_TELEMETRY": False,
143
+ }
144
+
145
+ # =============================================================================
146
+ # UI/UX CONFIGURATION
147
+ # =============================================================================
148
+
149
+ UI_CONFIG = {
150
+ # Display preferences
151
+ "DEFAULT_GLUCOSE_UNIT": "mg/dL", # or "mmol/L"
152
+ "DATE_FORMAT": "%Y-%m-%d %H:%M",
153
+ "TIMEZONE": "local", # or specific timezone like "US/Eastern"
154
+
155
+ # Chart settings
156
+ "CHART_HEIGHT": 400,
157
+ "CHART_WIDTH": 800,
158
+ "SHOW_TARGET_RANGE": True,
159
+ "ENABLE_INTERACTIVE_CHARTS": True,
160
+
161
+ # Notification settings
162
+ "ENABLE_DESKTOP_NOTIFICATIONS": True,
163
+ "ALERT_SOUND": True,
164
+ "NOTIFICATION_TIMEOUT": 5, # seconds
165
+ }
166
+
167
+ # =============================================================================
168
+ # INTEGRATION SETTINGS
169
+ # =============================================================================
170
+
171
+ # External service integrations
172
+ INTEGRATION_CONFIG = {
173
+ # Health app integrations
174
+ "ENABLE_APPLE_HEALTH": False,
175
+ "ENABLE_GOOGLE_FIT": False,
176
+
177
+ # Export formats
178
+ "SUPPORTED_EXPORT_FORMATS": ["CSV", "JSON", "PDF"],
179
+ "DEFAULT_EXPORT_FORMAT": "CSV",
180
+
181
+ # Webhook settings (for advanced users)
182
+ "WEBHOOK_URL": None,
183
+ "WEBHOOK_SECRET": os.getenv("GLUCOBUDDY_WEBHOOK_SECRET", None),
184
+ "ENABLE_WEBHOOKS": False,
185
+ }
186
+
187
+ # =============================================================================
188
+ # DEVELOPMENT SETTINGS
189
+ # =============================================================================
190
+
191
+ # Development and debugging options
192
+ DEV_CONFIG = {
193
+ "DEBUG_MODE": False,
194
+ "ENABLE_API_MOCKING": False, # Mock API calls for development
195
+ "VERBOSE_LOGGING": False,
196
+ "SAVE_RAW_API_RESPONSES": False,
197
+ "ENABLE_PERFORMANCE_MONITORING": False,
198
+
199
+ # Testing settings
200
+ "RUN_TESTS_ON_STARTUP": False,
201
+ "TEST_DATA_PATH": "test_data/",
202
+ }
203
+
204
+ # =============================================================================
205
+ # ENVIRONMENT-SPECIFIC OVERRIDES
206
+ # =============================================================================
207
+
208
+ # Override settings based on environment
209
+ ENVIRONMENT = os.getenv("GLUCOBUDDY_ENV", "development").lower()
210
+
211
+ if ENVIRONMENT == "production":
212
+ # Production overrides
213
+ DEXCOM_CONFIG["USE_SANDBOX"] = False
214
+ LOGGING_CONFIG["LEVEL"] = "WARNING"
215
+ DEMO_CONFIG["ENABLE_DEMO_MODE"] = False
216
+ DEV_CONFIG["DEBUG_MODE"] = False
217
+ SECURITY_CONFIG["REQUIRE_REAUTHENTICATION"] = True
218
+
219
+ elif ENVIRONMENT == "testing":
220
+ # Testing overrides
221
+ LOGGING_CONFIG["LEVEL"] = "DEBUG"
222
+ DEV_CONFIG["ENABLE_API_MOCKING"] = True
223
+ DEV_CONFIG["RUN_TESTS_ON_STARTUP"] = True
224
+ DEMO_CONFIG["ENABLE_DEMO_MODE"] = True
225
+
226
+ # =============================================================================
227
+ # CONFIGURATION VALIDATION
228
+ # =============================================================================
229
+
230
+ def validate_config() -> Dict[str, Any]:
231
+ """
232
+ Validate configuration settings and return validation results.
233
+ Returns a dictionary with validation status and any errors.
234
+ """
235
+ validation_results = {
236
+ "valid": True,
237
+ "errors": [],
238
+ "warnings": []
239
+ }
240
+
241
+ # Check required Dexcom credentials
242
+ if DEXCOM_CONFIG["CLIENT_ID"] == "your_client_id_here":
243
+ validation_results["errors"].append("Dexcom CLIENT_ID not configured")
244
+ validation_results["valid"] = False
245
+
246
+ if DEXCOM_CONFIG["CLIENT_SECRET"] == "your_client_secret_here":
247
+ validation_results["errors"].append("Dexcom CLIENT_SECRET not configured")
248
+ validation_results["valid"] = False
249
+
250
+ # Check Claude API key
251
+ if CLAUDE_CONFIG["API_KEY"] == "your_anthropic_api_key_here":
252
+ validation_results["warnings"].append("Claude API key not configured - AI insights will be limited")
253
+
254
+ # Validate glucose thresholds
255
+ if DATA_CONFIG["TARGET_RANGE_LOW"] >= DATA_CONFIG["TARGET_RANGE_HIGH"]:
256
+ validation_results["errors"].append("Invalid glucose target range")
257
+ validation_results["valid"] = False
258
+
259
+ # Check encryption key for production
260
+ if ENVIRONMENT == "production" and SECURITY_CONFIG["ENCRYPT_LOCAL_DATA"]:
261
+ if not SECURITY_CONFIG["ENCRYPTION_KEY"]:
262
+ validation_results["warnings"].append("No encryption key set for production environment")
263
+
264
+ return validation_results
265
+
266
+ def get_active_config() -> Dict[str, Any]:
267
+ """
268
+ Get the complete active configuration as a dictionary.
269
+ Useful for debugging and configuration inspection.
270
+ """
271
+ return {
272
+ "dexcom": DEXCOM_CONFIG,
273
+ "claude": CLAUDE_CONFIG,
274
+ "logging": LOGGING_CONFIG,
275
+ "data": DATA_CONFIG,
276
+ "demo": DEMO_CONFIG,
277
+ "security": SECURITY_CONFIG,
278
+ "ui": UI_CONFIG,
279
+ "integration": INTEGRATION_CONFIG,
280
+ "development": DEV_CONFIG,
281
+ "environment": ENVIRONMENT
282
+ }
283
+
284
+ # =============================================================================
285
+ # CONFIGURATION HELPERS
286
+ # =============================================================================
287
+
288
+ def get_dexcom_base_url() -> str:
289
+ """Get the appropriate Dexcom API base URL based on environment."""
290
+ return (DEXCOM_CONFIG["SANDBOX_BASE_URL"] if DEXCOM_CONFIG["USE_SANDBOX"]
291
+ else DEXCOM_CONFIG["PRODUCTION_BASE_URL"])
292
+
293
+ def is_demo_mode_enabled() -> bool:
294
+ """Check if demo mode is enabled."""
295
+ return DEMO_CONFIG["ENABLE_DEMO_MODE"]
296
+
297
+ def get_glucose_unit() -> str:
298
+ """Get the configured glucose unit."""
299
+ return UI_CONFIG["DEFAULT_GLUCOSE_UNIT"]
300
+
301
+ # =============================================================================
302
+ # STARTUP CONFIGURATION CHECK
303
+ # =============================================================================
304
+
305
+ if __name__ == "__main__":
306
+ print("GlucoBuddy Configuration Validation")
307
+ print("=" * 40)
308
+
309
+ results = validate_config()
310
+
311
+ if results["valid"]:
312
+ print("βœ… Configuration is valid!")
313
+ else:
314
+ print("❌ Configuration has errors:")
315
+ for error in results["errors"]:
316
+ print(f" - {error}")
317
+
318
+ if results["warnings"]:
319
+ print("\n⚠️ Warnings:")
320
+ for warning in results["warnings"]:
321
+ print(f" - {warning}")
322
+
323
+ print(f"\nEnvironment: {ENVIRONMENT}")
324
+ print(f"Demo Mode: {'Enabled' if is_demo_mode_enabled() else 'Disabled'}")
325
+ print(f"Sandbox Mode: {'Enabled' if DEXCOM_CONFIG['USE_SANDBOX'] else 'Disabled'}")
326
+ print(f"Claude Integration: {'Enabled' if CLAUDE_CONFIG['API_KEY'] != 'your_anthropic_api_key_here' else 'Disabled'}")
config_management.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Secure Configuration Management for GlycoAI
4
+ Handles API keys and secrets safely for both local development and Hugging Face Spaces
5
+ """
6
+
7
+ import os
8
+ import logging
9
+ from typing import Optional, Dict, Any
10
+
11
+ # Setup logging
12
+ logging.basicConfig(level=logging.INFO)
13
+ logger = logging.getLogger(__name__)
14
+
15
+ class SecureConfig:
16
+ """Secure configuration manager for API keys and secrets"""
17
+
18
+ def __init__(self):
19
+ self.config = {}
20
+ self._load_configuration()
21
+
22
+ def _load_configuration(self):
23
+ """Load configuration from environment variables"""
24
+
25
+ # Mistral AI Configuration
26
+ self.mistral_api_key = self._get_secret(
27
+ "MISTRAL_API_KEY",
28
+ description="Mistral AI API Key"
29
+ )
30
+
31
+ self.mistral_agent_id = self._get_secret(
32
+ "MISTRAL_AGENT_ID",
33
+ description="Mistral AI Agent ID",
34
+ required=False
35
+ )
36
+
37
+ # Dexcom API Configuration (for future real API integration)
38
+ self.dexcom_client_id = self._get_secret(
39
+ "DEXCOM_CLIENT_ID",
40
+ description="Dexcom API Client ID",
41
+ required=False
42
+ )
43
+
44
+ self.dexcom_client_secret = self._get_secret(
45
+ "DEXCOM_CLIENT_SECRET",
46
+ description="Dexcom API Client Secret",
47
+ required=False
48
+ )
49
+
50
+ # Application Configuration
51
+ self.app_environment = os.getenv("ENVIRONMENT", "development")
52
+ self.debug_mode = os.getenv("DEBUG", "false").lower() == "true"
53
+ self.log_level = os.getenv("LOG_LEVEL", "INFO")
54
+
55
+ # Hugging Face Space Detection
56
+ self.is_huggingface_space = os.getenv("SPACE_ID") is not None
57
+
58
+ logger.info(f"Configuration loaded for environment: {self.app_environment}")
59
+ logger.info(f"Running on Hugging Face Space: {self.is_huggingface_space}")
60
+
61
+ def _get_secret(self, key: str, description: str = "", required: bool = True) -> Optional[str]:
62
+ """Safely get secret from environment variables"""
63
+ value = os.getenv(key)
64
+
65
+ if value:
66
+ logger.info(f"βœ… {description or key} loaded successfully")
67
+ return value
68
+ elif required:
69
+ logger.error(f"❌ Required secret {key} ({description}) not found!")
70
+ logger.error(f"Please set the {key} environment variable")
71
+ return None
72
+ else:
73
+ logger.warning(f"⚠️ Optional secret {key} ({description}) not set")
74
+ return None
75
+
76
+ def validate_configuration(self) -> Dict[str, Any]:
77
+ """Validate that all required configuration is present"""
78
+ validation_result = {
79
+ "valid": True,
80
+ "errors": [],
81
+ "warnings": []
82
+ }
83
+
84
+ # Check required secrets
85
+ if not self.mistral_api_key:
86
+ validation_result["valid"] = False
87
+ validation_result["errors"].append("MISTRAL_API_KEY is required")
88
+
89
+ # Check optional but recommended secrets
90
+ if not self.mistral_agent_id:
91
+ validation_result["warnings"].append("MISTRAL_AGENT_ID not set - will use standard chat completion")
92
+
93
+ # Environment-specific checks
94
+ if self.is_huggingface_space:
95
+ if not all([self.mistral_api_key]):
96
+ validation_result["errors"].append("Hugging Face Space requires MISTRAL_API_KEY in secrets")
97
+
98
+ return validation_result
99
+
100
+ def get_mistral_config(self) -> Dict[str, Optional[str]]:
101
+ """Get Mistral AI configuration"""
102
+ return {
103
+ "api_key": self.mistral_api_key,
104
+ "agent_id": self.mistral_agent_id
105
+ }
106
+
107
+ def get_dexcom_config(self) -> Dict[str, Optional[str]]:
108
+ """Get Dexcom API configuration"""
109
+ return {
110
+ "client_id": self.dexcom_client_id,
111
+ "client_secret": self.dexcom_client_secret
112
+ }
113
+
114
+ def is_development(self) -> bool:
115
+ """Check if running in development mode"""
116
+ return self.app_environment == "development"
117
+
118
+ def is_production(self) -> bool:
119
+ """Check if running in production mode"""
120
+ return self.app_environment == "production"
121
+
122
+ # Global configuration instance
123
+ config = SecureConfig()
124
+
125
+ def get_config() -> SecureConfig:
126
+ """Get the global configuration instance"""
127
+ return config
128
+
129
+ def validate_environment():
130
+ """Validate environment configuration and provide helpful messages"""
131
+ print("πŸ” Validating GlycoAI Configuration...")
132
+ print("=" * 50)
133
+
134
+ validation = config.validate_configuration()
135
+
136
+ if validation["valid"]:
137
+ print("βœ… Configuration validation passed!")
138
+ else:
139
+ print("❌ Configuration validation failed!")
140
+ for error in validation["errors"]:
141
+ print(f" ❌ {error}")
142
+
143
+ if validation["warnings"]:
144
+ print("\n⚠️ Warnings:")
145
+ for warning in validation["warnings"]:
146
+ print(f" ⚠️ {warning}")
147
+
148
+ # Provide setup instructions
149
+ if not validation["valid"]:
150
+ print("\nπŸ“‹ Setup Instructions:")
151
+ print("=" * 30)
152
+
153
+ if config.is_huggingface_space:
154
+ print("πŸ€— For Hugging Face Spaces:")
155
+ print("1. Go to your Space settings")
156
+ print("2. Add Repository secrets:")
157
+ print(" - MISTRAL_API_KEY: your_mistral_api_key")
158
+ print(" - MISTRAL_AGENT_ID: your_agent_id (optional)")
159
+ else:
160
+ print("πŸ’» For Local Development:")
161
+ print("1. Create a .env file in your project root:")
162
+ print(" MISTRAL_API_KEY=your_mistral_api_key")
163
+ print(" MISTRAL_AGENT_ID=your_agent_id")
164
+ print("2. Or set environment variables:")
165
+ print(" export MISTRAL_API_KEY=your_mistral_api_key")
166
+ print(" export MISTRAL_AGENT_ID=your_agent_id")
167
+
168
+ return validation["valid"]
169
+
170
+ if __name__ == "__main__":
171
+ validate_environment()
dexcom_real_auth_system.py ADDED
@@ -0,0 +1,501 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Dexcom Real Authentication System
4
+ Uses your actual CLIENT_ID and CLIENT_SECRET from developer.dexcom.com
5
+ """
6
+
7
+ import requests
8
+ import json
9
+ import base64
10
+ import secrets
11
+ import hashlib
12
+ import urllib.parse
13
+ import webbrowser
14
+ import threading
15
+ import time
16
+ from datetime import datetime, timedelta
17
+ from typing import Dict, List, Optional, Tuple
18
+ from http.server import HTTPServer, BaseHTTPRequestHandler
19
+ import logging
20
+
21
+ # Set up logging
22
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # πŸ”‘ YOUR REAL DEXCOM CREDENTIALS (replace with your actual values)
26
+ CLIENT_ID = "YOUR_REAL_CLIENT_ID_HERE" # Replace with your actual client ID
27
+ CLIENT_SECRET = "YOUR_REAL_CLIENT_SECRET_HERE" # Replace with your actual client secret
28
+ REDIRECT_URI = "http://localhost:8080/callback"
29
+
30
+ # Dexcom API URLs
31
+ SANDBOX_BASE_URL = "https://sandbox-api.dexcom.com"
32
+ PRODUCTION_BASE_URL = "https://api.dexcom.com"
33
+
34
+ class OAuth2CallbackHandler(BaseHTTPRequestHandler):
35
+ """HTTP handler for OAuth2 callback"""
36
+
37
+ def do_GET(self):
38
+ """Handle GET request for OAuth callback"""
39
+ if self.path.startswith('/callback'):
40
+ # Parse the authorization code from the URL
41
+ parsed_url = urllib.parse.urlparse(self.path)
42
+ query_params = urllib.parse.parse_qs(parsed_url.query)
43
+
44
+ if 'code' in query_params:
45
+ # Store the authorization code in the server
46
+ self.server.auth_code = query_params['code'][0]
47
+ self.server.auth_error = None
48
+
49
+ # Send success response
50
+ self.send_response(200)
51
+ self.send_header('Content-type', 'text/html')
52
+ self.end_headers()
53
+
54
+ success_html = """
55
+ <html>
56
+ <head><title>Dexcom Authorization Successful</title></head>
57
+ <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
58
+ <h2 style="color: green;">βœ… Authorization Successful!</h2>
59
+ <p>You have successfully authorized the application to access your Dexcom data.</p>
60
+ <p>You can close this window and return to the application.</p>
61
+ <script>
62
+ setTimeout(function(){
63
+ window.close();
64
+ }, 3000);
65
+ </script>
66
+ </body>
67
+ </html>
68
+ """
69
+ self.wfile.write(success_html.encode())
70
+
71
+ elif 'error' in query_params:
72
+ # Handle authorization error
73
+ error = query_params.get('error', ['Unknown error'])[0]
74
+ error_description = query_params.get('error_description', [''])[0]
75
+
76
+ self.server.auth_code = None
77
+ self.server.auth_error = f"{error}: {error_description}"
78
+
79
+ self.send_response(400)
80
+ self.send_header('Content-type', 'text/html')
81
+ self.end_headers()
82
+
83
+ error_html = f"""
84
+ <html>
85
+ <head><title>Dexcom Authorization Failed</title></head>
86
+ <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
87
+ <h2 style="color: red;">❌ Authorization Failed</h2>
88
+ <p><strong>Error:</strong> {error}</p>
89
+ <p><strong>Description:</strong> {error_description}</p>
90
+ <p>Please try again or contact support if the problem persists.</p>
91
+ </body>
92
+ </html>
93
+ """
94
+ self.wfile.write(error_html.encode())
95
+ else:
96
+ # Unexpected callback
97
+ self.send_response(400)
98
+ self.send_header('Content-type', 'text/html')
99
+ self.end_headers()
100
+ self.wfile.write(b"<html><body><h2>Invalid callback</h2></body></html>")
101
+ else:
102
+ # 404 for other paths
103
+ self.send_response(404)
104
+ self.end_headers()
105
+
106
+ def log_message(self, format, *args):
107
+ """Suppress default logging"""
108
+ pass
109
+
110
+ class DexcomRealAPI:
111
+ """Real Dexcom API client using your actual credentials"""
112
+
113
+ def __init__(self, client_id: str = CLIENT_ID, client_secret: str = CLIENT_SECRET,
114
+ environment: str = "sandbox"):
115
+ """
116
+ Initialize Dexcom API client
117
+
118
+ Args:
119
+ client_id: Your real Dexcom client ID
120
+ client_secret: Your real Dexcom client secret
121
+ environment: "sandbox" or "production"
122
+ """
123
+ self.client_id = client_id
124
+ self.client_secret = client_secret
125
+ self.redirect_uri = REDIRECT_URI
126
+
127
+ if environment == "sandbox":
128
+ self.base_url = SANDBOX_BASE_URL
129
+ else:
130
+ self.base_url = PRODUCTION_BASE_URL
131
+
132
+ self.environment = environment
133
+ self.access_token = None
134
+ self.refresh_token = None
135
+ self.token_expires_at = None
136
+
137
+ # Validate credentials
138
+ if not client_id or client_id == "YOUR_REAL_CLIENT_ID_HERE":
139
+ raise ValueError("Please set your real CLIENT_ID")
140
+ if not client_secret or client_secret == "YOUR_REAL_CLIENT_SECRET_HERE":
141
+ raise ValueError("Please set your real CLIENT_SECRET")
142
+
143
+ def generate_auth_url(self, state: str = None) -> str:
144
+ """Generate OAuth authorization URL"""
145
+ if not state:
146
+ state = secrets.token_urlsafe(32)
147
+
148
+ params = {
149
+ 'client_id': self.client_id,
150
+ 'redirect_uri': self.redirect_uri,
151
+ 'response_type': 'code',
152
+ 'scope': 'offline_access',
153
+ 'state': state
154
+ }
155
+
156
+ query_string = urllib.parse.urlencode(params)
157
+ auth_url = f"{self.base_url}/v2/oauth2/login?{query_string}"
158
+
159
+ logger.info(f"Generated authorization URL for {self.environment} environment")
160
+ return auth_url
161
+
162
+ def start_oauth_flow(self) -> bool:
163
+ """Start the complete OAuth flow with browser"""
164
+ print(f"\nπŸ” Starting Dexcom OAuth Authentication")
165
+ print(f"🌐 Environment: {self.environment}")
166
+ print(f"πŸ”‘ Client ID: {self.client_id[:8]}...")
167
+
168
+ try:
169
+ # Generate authorization URL
170
+ state = secrets.token_urlsafe(32)
171
+ auth_url = self.generate_auth_url(state)
172
+
173
+ # Start local callback server
174
+ server = HTTPServer(('localhost', 8080), OAuth2CallbackHandler)
175
+ server.timeout = 120 # 2 minute timeout
176
+ server.auth_code = None
177
+ server.auth_error = None
178
+
179
+ print(f"🌐 Opening browser for authorization...")
180
+ print(f"πŸ“‹ URL: {auth_url}")
181
+ print(f"⏳ Waiting for authorization (timeout: 2 minutes)...")
182
+
183
+ # Open browser
184
+ webbrowser.open(auth_url)
185
+
186
+ # Wait for callback
187
+ start_time = time.time()
188
+ while time.time() - start_time < 120: # 2 minute timeout
189
+ server.handle_request()
190
+ if server.auth_code or server.auth_error:
191
+ break
192
+
193
+ if server.auth_error:
194
+ print(f"❌ Authorization failed: {server.auth_error}")
195
+ return False
196
+
197
+ if not server.auth_code:
198
+ print(f"❌ Authorization timeout - no response received")
199
+ return False
200
+
201
+ print(f"βœ… Authorization code received!")
202
+
203
+ # Exchange code for tokens
204
+ success = self.exchange_code_for_tokens(server.auth_code)
205
+
206
+ if success:
207
+ print(f"πŸŽ‰ Authentication successful!")
208
+ print(f"πŸ“Š Access token obtained")
209
+ print(f"⏰ Token expires: {self.token_expires_at}")
210
+ return True
211
+ else:
212
+ print(f"❌ Token exchange failed")
213
+ return False
214
+
215
+ except Exception as e:
216
+ logger.error(f"OAuth flow error: {e}")
217
+ print(f"❌ OAuth flow error: {e}")
218
+ return False
219
+
220
+ def exchange_code_for_tokens(self, authorization_code: str) -> bool:
221
+ """Exchange authorization code for access and refresh tokens"""
222
+ url = f"{self.base_url}/v2/oauth2/token"
223
+
224
+ data = {
225
+ 'client_id': self.client_id,
226
+ 'client_secret': self.client_secret,
227
+ 'code': authorization_code,
228
+ 'grant_type': 'authorization_code',
229
+ 'redirect_uri': self.redirect_uri
230
+ }
231
+
232
+ headers = {
233
+ 'Content-Type': 'application/x-www-form-urlencoded',
234
+ 'Accept': 'application/json'
235
+ }
236
+
237
+ try:
238
+ logger.info("Exchanging authorization code for tokens...")
239
+ response = requests.post(url, data=data, headers=headers)
240
+
241
+ logger.info(f"Token exchange response: {response.status_code}")
242
+
243
+ if response.status_code == 200:
244
+ token_data = response.json()
245
+
246
+ self.access_token = token_data.get('access_token')
247
+ self.refresh_token = token_data.get('refresh_token')
248
+
249
+ expires_in = token_data.get('expires_in', 3600)
250
+ self.token_expires_at = datetime.now() + timedelta(seconds=expires_in)
251
+
252
+ logger.info("Successfully obtained access and refresh tokens")
253
+ return True
254
+ else:
255
+ logger.error(f"Token exchange failed: {response.status_code} - {response.text}")
256
+ return False
257
+
258
+ except requests.exceptions.RequestException as e:
259
+ logger.error(f"Network error during token exchange: {e}")
260
+ return False
261
+
262
+ def refresh_access_token(self) -> bool:
263
+ """Refresh the access token using refresh token"""
264
+ if not self.refresh_token:
265
+ logger.error("No refresh token available")
266
+ return False
267
+
268
+ url = f"{self.base_url}/v2/oauth2/token"
269
+
270
+ data = {
271
+ 'client_id': self.client_id,
272
+ 'client_secret': self.client_secret,
273
+ 'refresh_token': self.refresh_token,
274
+ 'grant_type': 'refresh_token'
275
+ }
276
+
277
+ headers = {
278
+ 'Content-Type': 'application/x-www-form-urlencoded',
279
+ 'Accept': 'application/json'
280
+ }
281
+
282
+ try:
283
+ response = requests.post(url, data=data, headers=headers)
284
+
285
+ if response.status_code == 200:
286
+ token_data = response.json()
287
+
288
+ self.access_token = token_data.get('access_token')
289
+ # Note: Some OAuth providers issue new refresh tokens
290
+ if 'refresh_token' in token_data:
291
+ self.refresh_token = token_data.get('refresh_token')
292
+
293
+ expires_in = token_data.get('expires_in', 3600)
294
+ self.token_expires_at = datetime.now() + timedelta(seconds=expires_in)
295
+
296
+ logger.info("Access token refreshed successfully")
297
+ return True
298
+ else:
299
+ logger.error(f"Token refresh failed: {response.status_code} - {response.text}")
300
+ return False
301
+
302
+ except requests.exceptions.RequestException as e:
303
+ logger.error(f"Network error during token refresh: {e}")
304
+ return False
305
+
306
+ def is_token_valid(self) -> bool:
307
+ """Check if current access token is valid and not expired"""
308
+ if not self.access_token:
309
+ return False
310
+
311
+ if not self.token_expires_at:
312
+ return True # Assume valid if no expiry set
313
+
314
+ # Consider token expired if it expires within next 5 minutes
315
+ return datetime.now() < (self.token_expires_at - timedelta(minutes=5))
316
+
317
+ def ensure_valid_token(self) -> bool:
318
+ """Ensure we have a valid access token, refresh if needed"""
319
+ if not self.is_token_valid():
320
+ logger.info("Token expired or invalid, attempting refresh...")
321
+ if self.refresh_token:
322
+ return self.refresh_access_token()
323
+ else:
324
+ logger.error("No refresh token available, re-authentication required")
325
+ return False
326
+ return True
327
+
328
+ def get_auth_headers(self) -> Dict[str, str]:
329
+ """Get headers with valid authorization token"""
330
+ if not self.ensure_valid_token():
331
+ raise Exception("No valid access token available. Please authenticate first.")
332
+
333
+ return {
334
+ 'Authorization': f'Bearer {self.access_token}',
335
+ 'Accept': 'application/json'
336
+ }
337
+
338
+ def get_data_range(self) -> Dict:
339
+ """Get available data range for authenticated user"""
340
+ url = f"{self.base_url}/v2/users/self/dataRange"
341
+ headers = self.get_auth_headers()
342
+
343
+ try:
344
+ response = requests.get(url, headers=headers)
345
+
346
+ if response.status_code == 200:
347
+ return response.json()
348
+ else:
349
+ logger.error(f"Data range API error: {response.status_code} - {response.text}")
350
+ raise Exception(f"Data range API error: {response.status_code}")
351
+
352
+ except requests.exceptions.RequestException as e:
353
+ logger.error(f"Network error getting data range: {e}")
354
+ raise Exception(f"Network error getting data range: {e}")
355
+
356
+ def get_egv_data(self, start_date: str = None, end_date: str = None) -> List[Dict]:
357
+ """Get Estimated Glucose Values (EGV) data"""
358
+ url = f"{self.base_url}/v2/users/self/egvs"
359
+ headers = self.get_auth_headers()
360
+
361
+ params = {}
362
+ if start_date:
363
+ params['startDate'] = start_date
364
+ if end_date:
365
+ params['endDate'] = end_date
366
+
367
+ try:
368
+ response = requests.get(url, headers=headers, params=params)
369
+
370
+ if response.status_code == 200:
371
+ data = response.json()
372
+ return data.get('egvs', [])
373
+ else:
374
+ logger.error(f"EGV API error: {response.status_code} - {response.text}")
375
+ raise Exception(f"EGV API error: {response.status_code}")
376
+
377
+ except requests.exceptions.RequestException as e:
378
+ logger.error(f"Network error getting EGV data: {e}")
379
+ raise Exception(f"Network error getting EGV data: {e}")
380
+
381
+ def get_events_data(self, start_date: str = None, end_date: str = None) -> List[Dict]:
382
+ """Get events data (meals, insulin, etc.)"""
383
+ url = f"{self.base_url}/v2/users/self/events"
384
+ headers = self.get_auth_headers()
385
+
386
+ params = {}
387
+ if start_date:
388
+ params['startDate'] = start_date
389
+ if end_date:
390
+ params['endDate'] = end_date
391
+
392
+ try:
393
+ response = requests.get(url, headers=headers)
394
+
395
+ if response.status_code == 200:
396
+ data = response.json()
397
+ return data.get('events', [])
398
+ else:
399
+ logger.error(f"Events API error: {response.status_code} - {response.text}")
400
+ raise Exception(f"Events API error: {response.status_code}")
401
+
402
+ except requests.exceptions.RequestException as e:
403
+ logger.error(f"Network error getting events data: {e}")
404
+ raise Exception(f"Network error getting events data: {e}")
405
+
406
+ def test_real_dexcom_api():
407
+ """Test the real Dexcom API with your credentials"""
408
+ print("πŸ§ͺ Testing Real Dexcom API Integration")
409
+ print("=" * 60)
410
+
411
+ try:
412
+ # Initialize API with your real credentials
413
+ api = DexcomRealAPI(environment="sandbox")
414
+
415
+ # Start OAuth authentication
416
+ print("\nπŸ” Step 1: Authentication")
417
+ auth_success = api.start_oauth_flow()
418
+
419
+ if not auth_success:
420
+ print("❌ Authentication failed - cannot proceed")
421
+ return False
422
+
423
+ # Test data range
424
+ print("\nπŸ“… Step 2: Getting Data Range")
425
+ try:
426
+ data_range = api.get_data_range()
427
+ print(f"βœ… Data range retrieved:")
428
+ print(f" EGV: {data_range.get('egvStart', 'N/A')} to {data_range.get('egvEnd', 'N/A')}")
429
+ print(f" Events: {data_range.get('eventStart', 'N/A')} to {data_range.get('eventEnd', 'N/A')}")
430
+ except Exception as e:
431
+ print(f"❌ Data range error: {e}")
432
+
433
+ # Test glucose data
434
+ print("\nπŸ“Š Step 3: Getting Glucose Data")
435
+ try:
436
+ # Get last 24 hours
437
+ end_time = datetime.now()
438
+ start_time = end_time - timedelta(hours=24)
439
+
440
+ egv_data = api.get_egv_data(
441
+ start_date=start_time.isoformat(),
442
+ end_date=end_time.isoformat()
443
+ )
444
+
445
+ print(f"βœ… Retrieved {len(egv_data)} glucose readings")
446
+
447
+ if egv_data:
448
+ latest = egv_data[-1]
449
+ print(f" Latest: {latest['value']} mg/dL at {latest['displayTime']}")
450
+ print(f" Trend: {latest.get('trend', 'N/A')}")
451
+ except Exception as e:
452
+ print(f"❌ Glucose data error: {e}")
453
+
454
+ # Test events data
455
+ print("\n🍽️ Step 4: Getting Events Data")
456
+ try:
457
+ events_data = api.get_events_data(
458
+ start_date=start_time.isoformat(),
459
+ end_date=end_time.isoformat()
460
+ )
461
+
462
+ print(f"βœ… Retrieved {len(events_data)} events")
463
+
464
+ if events_data:
465
+ carb_events = [e for e in events_data if e.get('eventType') == 'carbs']
466
+ insulin_events = [e for e in events_data if e.get('eventType') == 'insulin']
467
+ print(f" Carb events: {len(carb_events)}")
468
+ print(f" Insulin events: {len(insulin_events)}")
469
+ except Exception as e:
470
+ print(f"❌ Events data error: {e}")
471
+
472
+ print(f"\nπŸŽ‰ Real Dexcom API integration completed!")
473
+ return True
474
+
475
+ except ValueError as e:
476
+ print(f"❌ Configuration error: {e}")
477
+ print(f"πŸ’‘ Please update CLIENT_ID and CLIENT_SECRET with your real credentials")
478
+ return False
479
+ except Exception as e:
480
+ print(f"❌ Unexpected error: {e}")
481
+ return False
482
+
483
+ if __name__ == "__main__":
484
+ print("πŸ”‘ Dexcom Real API Authentication System")
485
+ print("πŸ“‹ Make sure to update CLIENT_ID and CLIENT_SECRET with your real values!")
486
+ print()
487
+
488
+ # Run the test
489
+ test_real_dexcom_api()
490
+
491
+ print(f"\nπŸ’‘ Usage Example:")
492
+ print(f" api = DexcomRealAPI(environment='sandbox')")
493
+ print(f" api.start_oauth_flow() # Opens browser for auth")
494
+ print(f" glucose_data = api.get_egv_data()")
495
+ print(f" events_data = api.get_events_data()")
496
+
497
+ print(f"\n⚠️ Important Notes:")
498
+ print(f" β€’ This uses the real Dexcom sandbox environment")
499
+ print(f" β€’ You'll need to authenticate through the browser")
500
+ print(f" β€’ Sandbox users are provided by Dexcom (like SandboxUser7)")
501
+ print(f" β€’ Update environment='production' for real user data")
hybrid_auth.py ADDED
@@ -0,0 +1,648 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hybrid Dexcom Integration
4
+ Combines demo users with optional real Dexcom authentication
5
+ """
6
+
7
+ import os
8
+ import gradio as gr
9
+ import json
10
+ import logging
11
+ from datetime import datetime, timedelta
12
+ from typing import Dict, List, Optional, Tuple, Any
13
+ from dataclasses import dataclass
14
+ import pandas as pd
15
+ import random
16
+
17
+ # Load environment variables
18
+ from dotenv import load_dotenv
19
+ load_dotenv()
20
+
21
+ # Setup logging
22
+ logging.basicConfig(level=logging.INFO)
23
+ logger = logging.getLogger(__name__)
24
+
25
+ @dataclass
26
+ class DemoUser:
27
+ """Enhanced demo user with auth type"""
28
+ name: str
29
+ device_type: str
30
+ username: str
31
+ password: str
32
+ description: str
33
+ age: int = 30
34
+ diabetes_type: str = "Type 1"
35
+ years_with_diabetes: int = 5
36
+ typical_glucose_pattern: str = "normal"
37
+ auth_type: str = "demo" # "demo" or "real"
38
+
39
+ # Enhanced demo users + real auth option
40
+ ENHANCED_DEMO_USERS = {
41
+ # Your existing 4 demo users (unchanged for easy demos)
42
+ "sarah_demo": DemoUser(
43
+ name="Sarah Thompson (Demo)",
44
+ age=32,
45
+ device_type="G7 Mobile App",
46
+ username="demo_sarah",
47
+ password="demo123",
48
+ description="Demo: Active professional with Type 1 diabetes, stable glucose control",
49
+ diabetes_type="Type 1",
50
+ years_with_diabetes=8,
51
+ typical_glucose_pattern="stable_with_meal_spikes",
52
+ auth_type="demo"
53
+ ),
54
+ "marcus_demo": DemoUser(
55
+ name="Marcus Rodriguez (Demo)",
56
+ age=45,
57
+ device_type="ONE+ Mobile App",
58
+ username="demo_marcus",
59
+ password="demo123",
60
+ description="Demo: Father with Type 2 diabetes, moderate variability",
61
+ diabetes_type="Type 2",
62
+ years_with_diabetes=3,
63
+ typical_glucose_pattern="moderate_variability",
64
+ auth_type="demo"
65
+ ),
66
+ "jennifer_demo": DemoUser(
67
+ name="Jennifer Chen (Demo)",
68
+ age=28,
69
+ device_type="G6 Mobile App",
70
+ username="demo_jennifer",
71
+ password="demo123",
72
+ description="Demo: Graduate student with Type 1, athletic lifestyle",
73
+ diabetes_type="Type 1",
74
+ years_with_diabetes=12,
75
+ typical_glucose_pattern="exercise_related_lows",
76
+ auth_type="demo"
77
+ ),
78
+ "robert_demo": DemoUser(
79
+ name="Robert Williams (Demo)",
80
+ age=67,
81
+ device_type="G6 Touchscreen Receiver",
82
+ username="demo_robert",
83
+ password="demo123",
84
+ description="Demo: Retired teacher with Type 2, prefers receiver device",
85
+ diabetes_type="Type 2",
86
+ years_with_diabetes=15,
87
+ typical_glucose_pattern="dawn_phenomenon",
88
+ auth_type="demo"
89
+ ),
90
+
91
+ # NEW: Real authentication option
92
+ "real_user": DemoUser(
93
+ name="Real Dexcom User",
94
+ age=0, # Will be determined from real data
95
+ device_type="Real Dexcom Device",
96
+ username="real_dexcom_auth",
97
+ password="oauth_flow",
98
+ description="Authenticate with your real Dexcom account using OAuth",
99
+ diabetes_type="Real Data",
100
+ years_with_diabetes=0,
101
+ typical_glucose_pattern="real_data",
102
+ auth_type="real"
103
+ )
104
+ }
105
+
106
+ class HybridDexcomManager:
107
+ """Manages both demo and real Dexcom authentication"""
108
+
109
+ def __init__(self):
110
+ self.demo_enabled = True
111
+ self.real_auth_enabled = self._check_real_auth_available()
112
+ self.current_mode = "demo"
113
+
114
+ # Initialize real auth if available
115
+ if self.real_auth_enabled:
116
+ try:
117
+ from dexcom_real_auth_system import DexcomRealAPI
118
+ self.real_api = DexcomRealAPI(environment="sandbox")
119
+ logger.info("βœ… Real Dexcom authentication available")
120
+ except ImportError:
121
+ logger.warning("⚠️ Real Dexcom auth not available - missing module")
122
+ self.real_auth_enabled = False
123
+ except Exception as e:
124
+ logger.warning(f"⚠️ Real Dexcom auth not available: {e}")
125
+ self.real_auth_enabled = False
126
+
127
+ # Mock data generator for demo users
128
+ self.mock_generator = MockGlucoseGenerator()
129
+
130
+ def _check_real_auth_available(self) -> bool:
131
+ """Check if real authentication is properly configured"""
132
+ client_id = os.getenv("DEXCOM_CLIENT_ID")
133
+ client_secret = os.getenv("DEXCOM_CLIENT_SECRET")
134
+
135
+ # Also check for hardcoded values in the real auth system
136
+ try:
137
+ from dexcom_real_auth_system import CLIENT_ID, CLIENT_SECRET
138
+ if CLIENT_ID and CLIENT_ID != "YOUR_REAL_CLIENT_ID_HERE":
139
+ return True
140
+ except ImportError:
141
+ pass
142
+
143
+ return bool(client_id and client_secret)
144
+
145
+ def get_user_options(self) -> Dict[str, str]:
146
+ """Get available user options for the UI"""
147
+ options = {}
148
+
149
+ # Add demo users
150
+ for key, user in ENHANCED_DEMO_USERS.items():
151
+ if user.auth_type == "demo":
152
+ options[key] = f"🎭 {user.name}"
153
+
154
+ # Add real auth option if available
155
+ if self.real_auth_enabled:
156
+ options["real_user"] = "πŸ” Real Dexcom User (OAuth)"
157
+ else:
158
+ options["real_user_disabled"] = "πŸ”’ Real Dexcom User (Configure to Enable)"
159
+
160
+ return options
161
+
162
+ def authenticate_user(self, user_key: str) -> Dict[str, Any]:
163
+ """Authenticate user (demo or real)"""
164
+ if user_key not in ENHANCED_DEMO_USERS:
165
+ return {"success": False, "message": "Invalid user selection"}
166
+
167
+ user = ENHANCED_DEMO_USERS[user_key]
168
+
169
+ if user.auth_type == "demo":
170
+ return self._authenticate_demo_user(user_key, user)
171
+ elif user.auth_type == "real":
172
+ return self._authenticate_real_user()
173
+ else:
174
+ return {"success": False, "message": "Unknown authentication type"}
175
+
176
+ def _authenticate_demo_user(self, user_key: str, user: DemoUser) -> Dict[str, Any]:
177
+ """Authenticate demo user (instant)"""
178
+ try:
179
+ # Generate mock data for demo user
180
+ mock_data = self.mock_generator.generate_user_data(user)
181
+
182
+ return {
183
+ "success": True,
184
+ "message": f"βœ… Demo user authenticated: {user.name}",
185
+ "user": user,
186
+ "data": mock_data,
187
+ "auth_type": "demo"
188
+ }
189
+ except Exception as e:
190
+ return {"success": False, "message": f"Demo authentication failed: {e}"}
191
+
192
+ def _authenticate_real_user(self) -> Dict[str, Any]:
193
+ """Authenticate real Dexcom user"""
194
+ if not self.real_auth_enabled:
195
+ return {
196
+ "success": False,
197
+ "message": "Real authentication not configured. Check DEXCOM_CLIENT_ID/SECRET"
198
+ }
199
+
200
+ try:
201
+ # Start OAuth flow
202
+ auth_success = self.real_api.start_oauth_flow()
203
+
204
+ if auth_success:
205
+ # Get real data
206
+ real_data = self._fetch_real_data()
207
+
208
+ return {
209
+ "success": True,
210
+ "message": "βœ… Real Dexcom user authenticated",
211
+ "user": self._create_real_user_profile(),
212
+ "data": real_data,
213
+ "auth_type": "real"
214
+ }
215
+ else:
216
+ return {"success": False, "message": "OAuth authentication failed"}
217
+
218
+ except Exception as e:
219
+ logger.error(f"Real authentication error: {e}")
220
+ return {"success": False, "message": f"Real authentication failed: {e}"}
221
+
222
+ def _fetch_real_data(self) -> Dict[str, Any]:
223
+ """Fetch real data from Dexcom API"""
224
+ try:
225
+ # Get data range
226
+ data_range = self.real_api.get_data_range()
227
+
228
+ # Get glucose data (last 14 days)
229
+ end_time = datetime.now()
230
+ start_time = end_time - timedelta(days=14)
231
+
232
+ egv_data = self.real_api.get_egv_data(
233
+ start_date=start_time.isoformat(),
234
+ end_date=end_time.isoformat()
235
+ )
236
+
237
+ # Get events data
238
+ events_data = self.real_api.get_events_data(
239
+ start_date=start_time.isoformat(),
240
+ end_date=end_time.isoformat()
241
+ )
242
+
243
+ return {
244
+ "data_range": data_range,
245
+ "egv_data": egv_data,
246
+ "events_data": events_data,
247
+ "source": "real_dexcom_api"
248
+ }
249
+
250
+ except Exception as e:
251
+ logger.error(f"Failed to fetch real data: {e}")
252
+ return {"error": f"Failed to fetch real data: {e}"}
253
+
254
+ def _create_real_user_profile(self) -> DemoUser:
255
+ """Create user profile from real data"""
256
+ return DemoUser(
257
+ name="Real Dexcom User",
258
+ age=0, # Could extract from real user data if available
259
+ device_type="Real Dexcom Device",
260
+ username="authenticated_real_user",
261
+ password="oauth_token",
262
+ description="Authenticated real Dexcom user",
263
+ diabetes_type="From Real Data",
264
+ years_with_diabetes=0,
265
+ typical_glucose_pattern="real_data",
266
+ auth_type="real"
267
+ )
268
+
269
+ class MockGlucoseGenerator:
270
+ """Enhanced mock glucose data generator"""
271
+
272
+ def generate_user_data(self, user: DemoUser, days: int = 14) -> Dict[str, Any]:
273
+ """Generate mock data based on user profile"""
274
+ # Generate glucose readings
275
+ egv_data = self._generate_glucose_readings(user, days)
276
+
277
+ # Generate events (meals, insulin)
278
+ events_data = self._generate_events_data(user, days)
279
+
280
+ # Create data range
281
+ end_time = datetime.now()
282
+ start_time = end_time - timedelta(days=days)
283
+
284
+ data_range = {
285
+ "egvStart": start_time.isoformat(),
286
+ "egvEnd": end_time.isoformat(),
287
+ "eventStart": start_time.isoformat(),
288
+ "eventEnd": end_time.isoformat()
289
+ }
290
+
291
+ return {
292
+ "data_range": data_range,
293
+ "egv_data": egv_data,
294
+ "events_data": events_data,
295
+ "source": "mock_data"
296
+ }
297
+
298
+ def _generate_glucose_readings(self, user: DemoUser, days: int) -> List[Dict]:
299
+ """Generate realistic glucose readings"""
300
+ import random
301
+ import numpy as np
302
+
303
+ readings = []
304
+ start_time = datetime.now() - timedelta(days=days)
305
+
306
+ # Base glucose level based on user pattern
307
+ base_glucose = {
308
+ "stable_with_meal_spikes": 120,
309
+ "moderate_variability": 140,
310
+ "exercise_related_lows": 115,
311
+ "dawn_phenomenon": 130
312
+ }.get(user.typical_glucose_pattern, 125)
313
+
314
+ current_glucose = base_glucose
315
+
316
+ # Generate readings every 5 minutes
317
+ for i in range(days * 288): # 288 readings per day
318
+ timestamp = start_time + timedelta(minutes=i * 5)
319
+ hour = timestamp.hour
320
+
321
+ # Apply user-specific patterns
322
+ target_glucose = self._calculate_target_glucose(user, hour, base_glucose)
323
+
324
+ # Smooth glucose changes
325
+ change = (target_glucose - current_glucose) * 0.2
326
+ current_glucose += change + random.uniform(-5, 5)
327
+ current_glucose = max(60, min(300, current_glucose))
328
+
329
+ # Determine trend
330
+ trend = self._calculate_trend(change)
331
+
332
+ readings.append({
333
+ "systemTime": timestamp.isoformat() + "Z",
334
+ "displayTime": timestamp.isoformat() + "Z",
335
+ "value": round(current_glucose),
336
+ "trend": trend,
337
+ "realtimeValue": round(current_glucose),
338
+ "smoothedValue": round(current_glucose)
339
+ })
340
+
341
+ return readings
342
+
343
+ def _calculate_target_glucose(self, user: DemoUser, hour: int, base: float) -> float:
344
+ """Calculate target glucose based on user pattern and time"""
345
+ if user.typical_glucose_pattern == "dawn_phenomenon":
346
+ if 4 <= hour <= 8:
347
+ return base + 40 # Dawn phenomenon spike
348
+ elif user.typical_glucose_pattern == "exercise_related_lows":
349
+ if 17 <= hour <= 19: # Evening exercise
350
+ return base - 30 # Exercise-induced low
351
+ elif user.typical_glucose_pattern == "moderate_variability":
352
+ return base + random.uniform(-20, 30) # High variability
353
+
354
+ # Standard meal patterns
355
+ if 7 <= hour <= 9: # Breakfast
356
+ return base + random.uniform(20, 50)
357
+ elif 12 <= hour <= 14: # Lunch
358
+ return base + random.uniform(25, 60)
359
+ elif 18 <= hour <= 20: # Dinner
360
+ return base + random.uniform(30, 70)
361
+ else:
362
+ return base + random.uniform(-10, 15)
363
+
364
+ def _calculate_trend(self, change: float) -> str:
365
+ """Calculate trend arrow"""
366
+ if change > 3:
367
+ return "singleUp"
368
+ elif change > 1:
369
+ return "fortyFiveUp"
370
+ elif change < -3:
371
+ return "singleDown"
372
+ elif change < -1:
373
+ return "fortyFiveDown"
374
+ else:
375
+ return "flat"
376
+
377
+ def _generate_events_data(self, user: DemoUser, days: int) -> List[Dict]:
378
+ """Generate mock events (meals, insulin)"""
379
+ import random
380
+
381
+ events = []
382
+ start_date = (datetime.now() - timedelta(days=days)).date()
383
+
384
+ for day in range(days):
385
+ current_date = start_date + timedelta(days=day)
386
+
387
+ # Generate daily meals and insulin
388
+ for meal_time, meal_name in [(7, "breakfast"), (12, "lunch"), (18, "dinner")]:
389
+ # Meal event
390
+ meal_dt = datetime.combine(current_date, datetime.min.time().replace(
391
+ hour=meal_time, minute=random.randint(0, 30)
392
+ ))
393
+
394
+ carbs = random.randint(30, 80)
395
+ events.append({
396
+ "systemTime": meal_dt.isoformat() + "Z",
397
+ "displayTime": meal_dt.isoformat() + "Z",
398
+ "eventType": "carbs",
399
+ "eventSubType": meal_name,
400
+ "value": carbs,
401
+ "unit": "grams"
402
+ })
403
+
404
+ # Insulin event (if Type 1)
405
+ if user.diabetes_type == "Type 1":
406
+ insulin_dt = meal_dt + timedelta(minutes=random.randint(5, 15))
407
+ insulin_units = round(carbs / random.uniform(10, 15), 1)
408
+
409
+ events.append({
410
+ "systemTime": insulin_dt.isoformat() + "Z",
411
+ "displayTime": insulin_dt.isoformat() + "Z",
412
+ "eventType": "insulin",
413
+ "eventSubType": "fast",
414
+ "value": insulin_units,
415
+ "unit": "units"
416
+ })
417
+
418
+ return events
419
+
420
+ def create_hybrid_ui_components():
421
+ """Create UI components for hybrid demo"""
422
+
423
+ # Initialize the hybrid manager
424
+ hybrid_manager = HybridDexcomManager()
425
+ user_options = hybrid_manager.get_user_options()
426
+
427
+ # Create user selection buttons
428
+ with gr.Row():
429
+ with gr.Column():
430
+ gr.Markdown("### πŸ‘₯ Select User Type")
431
+ gr.Markdown("Choose from demo users (instant) or authenticate with real Dexcom account")
432
+
433
+ # Demo users row
434
+ with gr.Row():
435
+ demo_buttons = []
436
+ for key, user in ENHANCED_DEMO_USERS.items():
437
+ if user.auth_type == "demo":
438
+ btn = gr.Button(
439
+ f"🎭 {user.name.split('(')[0].strip()}\n{user.device_type}",
440
+ variant="secondary",
441
+ size="lg"
442
+ )
443
+ demo_buttons.append((key, btn))
444
+
445
+ # Real auth button
446
+ with gr.Row():
447
+ if hybrid_manager.real_auth_enabled:
448
+ real_auth_btn = gr.Button(
449
+ "πŸ” REAL DEXCOM USER\n(OAuth Authentication)",
450
+ variant="primary",
451
+ size="lg"
452
+ )
453
+ else:
454
+ real_auth_btn = gr.Button(
455
+ "πŸ”’ Real Dexcom (Not Configured)\nSet DEXCOM_CLIENT_ID/SECRET",
456
+ variant="secondary",
457
+ size="lg",
458
+ interactive=False
459
+ )
460
+
461
+ # Status displays
462
+ with gr.Row():
463
+ auth_status = gr.Textbox(
464
+ label="Authentication Status",
465
+ value="No user selected",
466
+ interactive=False
467
+ )
468
+
469
+ with gr.Row():
470
+ config_status = gr.HTML(f"""
471
+ <div style="padding: 1rem; background: #f8f9fa; border-radius: 8px;">
472
+ <h4>πŸ”§ Configuration Status</h4>
473
+ <p>
474
+ <strong>Demo Mode:</strong> {'βœ… Available' if hybrid_manager.demo_enabled else '❌ Disabled'}<br>
475
+ <strong>Real Auth:</strong> {'βœ… Configured' if hybrid_manager.real_auth_enabled else '❌ Not Configured'}<br>
476
+ <strong>Total Users:</strong> {len([u for u in ENHANCED_DEMO_USERS.values() if u.auth_type == 'demo'])} Demo + {'1 Real' if hybrid_manager.real_auth_enabled else '0 Real'}
477
+ </p>
478
+ </div>
479
+ """)
480
+
481
+ return {
482
+ "hybrid_manager": hybrid_manager,
483
+ "demo_buttons": demo_buttons,
484
+ "real_auth_btn": real_auth_btn,
485
+ "auth_status": auth_status
486
+ }
487
+
488
+ def setup_authentication_handlers(components):
489
+ """Setup event handlers for authentication"""
490
+
491
+ def handle_demo_auth(user_key):
492
+ """Handle demo user authentication"""
493
+ result = components["hybrid_manager"].authenticate_user(user_key)
494
+
495
+ if result["success"]:
496
+ return (
497
+ result["message"],
498
+ gr.update(visible=True), # Show main interface
499
+ [] # Clear chat history
500
+ )
501
+ else:
502
+ return (
503
+ f"❌ {result['message']}",
504
+ gr.update(visible=False),
505
+ []
506
+ )
507
+
508
+ def handle_real_auth():
509
+ """Handle real Dexcom authentication"""
510
+ result = components["hybrid_manager"].authenticate_user("real_user")
511
+
512
+ if result["success"]:
513
+ return (
514
+ f"βœ… {result['message']} - Browser will open for OAuth",
515
+ gr.update(visible=True),
516
+ []
517
+ )
518
+ else:
519
+ return (
520
+ f"❌ {result['message']}",
521
+ gr.update(visible=False),
522
+ []
523
+ )
524
+
525
+ return handle_demo_auth, handle_real_auth
526
+
527
+ # Integration with your existing main.py
528
+ def integrate_with_existing_app():
529
+ """Integration guide for your existing application"""
530
+
531
+ integration_code = '''
532
+ # Add this to your main.py imports
533
+ from hybrid_dexcom_integration import HybridDexcomManager, ENHANCED_DEMO_USERS
534
+
535
+ class GlucoBuddyApp:
536
+ def __init__(self):
537
+ # Replace your existing initialization
538
+ self.hybrid_manager = HybridDexcomManager()
539
+ self.data_manager = UnifiedDataManager()
540
+ self.mistral_chat = GlucoBuddyMistralChat()
541
+
542
+ # UI state
543
+ self.chat_history = []
544
+
545
+ def select_user(self, user_key: str) -> Tuple[str, str]:
546
+ """Enhanced user selection with hybrid auth"""
547
+ try:
548
+ # Use hybrid authentication
549
+ auth_result = self.hybrid_manager.authenticate_user(user_key)
550
+
551
+ if not auth_result['success']:
552
+ return f"❌ {auth_result['message']}", gr.update(visible=False)
553
+
554
+ # Load data based on auth type
555
+ if auth_result['auth_type'] == 'demo':
556
+ # Use mock data
557
+ user = auth_result['user']
558
+ data = auth_result['data']
559
+
560
+ # Convert to format expected by UnifiedDataManager
561
+ load_result = self.data_manager.load_mock_data(user, data)
562
+ else:
563
+ # Use real data
564
+ user = auth_result['user']
565
+ data = auth_result['data']
566
+
567
+ # Convert to format expected by UnifiedDataManager
568
+ load_result = self.data_manager.load_real_data(user, data)
569
+
570
+ if load_result['success']:
571
+ self._sync_chat_with_data_manager()
572
+ self.chat_history = []
573
+ self.mistral_chat.clear_conversation()
574
+
575
+ return (
576
+ f"Connected: {user.name} ({auth_result['auth_type'].upper()}) - Click 'Load Data' to begin",
577
+ gr.update(visible=True)
578
+ )
579
+ else:
580
+ return f"❌ Data loading failed: {load_result['message']}", gr.update(visible=False)
581
+
582
+ except Exception as e:
583
+ logger.error(f"User selection failed: {e}")
584
+ return f"❌ Selection failed: {e}", gr.update(visible=False)
585
+
586
+ # Update your user buttons in create_interface()
587
+ def create_enhanced_interface():
588
+ # Replace the user selection section with:
589
+
590
+ with gr.Row():
591
+ with gr.Column():
592
+ gr.Markdown("### πŸ‘₯ Select User")
593
+ gr.Markdown("Choose demo users (instant) or real Dexcom authentication")
594
+
595
+ # Demo users
596
+ with gr.Row():
597
+ sarah_btn = gr.Button("🎭 Sarah (Demo)\\nG7 Mobile", variant="secondary")
598
+ marcus_btn = gr.Button("🎭 Marcus (Demo)\\nONE+ Mobile", variant="secondary")
599
+ jennifer_btn = gr.Button("🎭 Jennifer (Demo)\\nG6 Mobile", variant="secondary")
600
+ robert_btn = gr.Button("🎭 Robert (Demo)\\nG6 Receiver", variant="secondary")
601
+
602
+ # Real auth
603
+ with gr.Row():
604
+ real_auth_btn = gr.Button(
605
+ "πŸ” REAL DEXCOM USER\\n(OAuth Authentication)",
606
+ variant="primary",
607
+ size="lg"
608
+ )
609
+
610
+ # Connect handlers:
611
+ sarah_btn.click(lambda: app.select_user("sarah_demo"), outputs=[connection_status, main_interface, chatbot])
612
+ marcus_btn.click(lambda: app.select_user("marcus_demo"), outputs=[connection_status, main_interface, chatbot])
613
+ jennifer_btn.click(lambda: app.select_user("jennifer_demo"), outputs=[connection_status, main_interface, chatbot])
614
+ robert_btn.click(lambda: app.select_user("robert_demo"), outputs=[connection_status, main_interface, chatbot])
615
+ real_auth_btn.click(lambda: app.select_user("real_user"), outputs=[connection_status, main_interface, chatbot])
616
+ '''
617
+
618
+ return integration_code
619
+
620
+ if __name__ == "__main__":
621
+ print("πŸ”§ Hybrid Dexcom Integration - Demo + Real Authentication")
622
+ print("=" * 60)
623
+
624
+ # Test the hybrid manager
625
+ manager = HybridDexcomManager()
626
+
627
+ print("πŸ“Š Configuration Status:")
628
+ print(f" Demo Mode: {'βœ…' if manager.demo_enabled else '❌'}")
629
+ print(f" Real Auth: {'βœ…' if manager.real_auth_enabled else '❌'}")
630
+
631
+ print(f"\nπŸ‘₯ Available Users:")
632
+ for key, user in ENHANCED_DEMO_USERS.items():
633
+ auth_icon = "🎭" if user.auth_type == "demo" else "πŸ”"
634
+ available = "βœ…" if user.auth_type == "demo" or manager.real_auth_enabled else "❌"
635
+ print(f" {available} {auth_icon} {user.name}")
636
+
637
+ print(f"\nπŸ’‘ Integration Guide:")
638
+ print(" 1. Import: from hybrid_dexcom_integration import HybridDexcomManager")
639
+ print(" 2. Replace user selection in your main.py")
640
+ print(" 3. Update button handlers to use hybrid authentication")
641
+ print(" 4. Your existing UnifiedDataManager works with both data types!")
642
+
643
+ print(f"\nπŸš€ Benefits:")
644
+ print(" βœ… Keep all 4 demo users for easy demos")
645
+ print(" βœ… Add real authentication when needed")
646
+ print(" βœ… Seamless switching between demo and real")
647
+ print(" βœ… No changes needed to existing chat/UI logic")
648
+ print(" βœ… Progressive enhancement - works with/without real auth")
main.py ADDED
@@ -0,0 +1,779 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ GlycoAI - AI-Powered Glucose Insights
3
+ Main Gradio application with prominent, centralized load data button
4
+ """
5
+
6
+ import gradio as gr
7
+ import plotly.graph_objects as go
8
+ import plotly.express as px
9
+ from datetime import datetime, timedelta
10
+ import pandas as pd
11
+ from typing import Optional, Tuple, List
12
+ import logging
13
+ import os
14
+
15
+ # Load environment variables from .env file
16
+ from dotenv import load_dotenv
17
+ load_dotenv()
18
+
19
+ # Import the Mistral chat class and unified data manager
20
+ from mistral_chat import GlucoBuddyMistralChat, validate_environment
21
+ from unified_data_manager import UnifiedDataManager
22
+
23
+ # Setup logging
24
+ logging.basicConfig(level=logging.INFO)
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # Import our custom functions
28
+ from apifunctions import (
29
+ DexcomAPI,
30
+ GlucoseAnalyzer,
31
+ DEMO_USERS,
32
+ format_glucose_data_for_display
33
+ )
34
+
35
+ class GlucoBuddyApp:
36
+ """Main application class for GlucoBuddy with unified data management"""
37
+
38
+ def __init__(self):
39
+ # Validate environment before initializing
40
+ if not validate_environment():
41
+ raise ValueError("Environment validation failed - check your .env file or environment variables")
42
+
43
+ # Single data manager for consistency
44
+ self.data_manager = UnifiedDataManager()
45
+
46
+ # Chat interface (will use data manager's context)
47
+ self.mistral_chat = GlucoBuddyMistralChat()
48
+
49
+ # UI state
50
+ self.chat_history = []
51
+
52
+ def select_demo_user(self, user_key: str) -> Tuple[str, str]:
53
+ """Handle demo user selection and load data consistently"""
54
+ if user_key not in DEMO_USERS:
55
+ return "❌ Invalid user selection", gr.update(visible=False)
56
+
57
+ try:
58
+ # Load data through unified manager
59
+ load_result = self.data_manager.load_user_data(user_key)
60
+
61
+ if not load_result['success']:
62
+ return f"❌ {load_result['message']}", gr.update(visible=False)
63
+
64
+ user = self.data_manager.current_user
65
+
66
+ # Update Mistral chat with the same context
67
+ self._sync_chat_with_data_manager()
68
+
69
+ # Clear chat history when switching users
70
+ self.chat_history = []
71
+ self.mistral_chat.clear_conversation()
72
+
73
+ return (
74
+ f"Connected: {user.name} ({user.device_type}) - Click 'Load Data' to begin",
75
+ gr.update(visible=True)
76
+ )
77
+
78
+ except Exception as e:
79
+ logger.error(f"User selection failed: {str(e)}")
80
+ return f"❌ Connection failed: {str(e)}", gr.update(visible=False)
81
+
82
+ def load_glucose_data(self) -> Tuple[str, go.Figure]:
83
+ """Load and display glucose data using unified manager"""
84
+ if not self.data_manager.current_user:
85
+ return "Please select a demo user first", None
86
+
87
+ try:
88
+ # Force reload data to ensure freshness
89
+ load_result = self.data_manager.load_user_data(
90
+ self._get_current_user_key(),
91
+ force_reload=True
92
+ )
93
+
94
+ if not load_result['success']:
95
+ return load_result['message'], None
96
+
97
+ # Get unified stats
98
+ stats = self.data_manager.get_stats_for_ui()
99
+ chart_data = self.data_manager.get_chart_data()
100
+
101
+ # Sync chat with fresh data
102
+ self._sync_chat_with_data_manager()
103
+
104
+ if chart_data is None or chart_data.empty:
105
+ return "No glucose data available", None
106
+
107
+ # Build data summary with CONSISTENT metrics
108
+ user = self.data_manager.current_user
109
+ data_points = stats.get('total_readings', 0)
110
+ avg_glucose = stats.get('average_glucose', 0)
111
+ std_glucose = stats.get('std_glucose', 0)
112
+ min_glucose = stats.get('min_glucose', 0)
113
+ max_glucose = stats.get('max_glucose', 0)
114
+
115
+ time_in_range = stats.get('time_in_range_70_180', 0)
116
+ time_below_range = stats.get('time_below_70', 0)
117
+ time_above_range = stats.get('time_above_180', 0)
118
+
119
+ gmi = stats.get('gmi', 0)
120
+ cv = stats.get('cv', 0)
121
+
122
+ # Calculate date range
123
+ end_date = datetime.now()
124
+ start_date = end_date - timedelta(days=14)
125
+
126
+ data_summary = f"""
127
+ ## πŸ“Š Data Summary for {user.name}
128
+
129
+ ### Basic Information
130
+ β€’ **Analysis Period:** {start_date.strftime('%B %d, %Y')} to {end_date.strftime('%B %d, %Y')} (14 days)
131
+ β€’ **Total Readings:** {data_points:,} glucose measurements
132
+ β€’ **Device:** {user.device_type}
133
+ β€’ **Data Source:** {stats.get('data_source', 'unknown').upper()}
134
+
135
+ ### Glucose Statistics
136
+ β€’ **Average Glucose:** {avg_glucose:.1f} mg/dL
137
+ β€’ **Standard Deviation:** {std_glucose:.1f} mg/dL
138
+ β€’ **Coefficient of Variation:** {cv:.1f}%
139
+ β€’ **Glucose Range:** {min_glucose:.0f} - {max_glucose:.0f} mg/dL
140
+ β€’ **GMI (Glucose Management Indicator):** {gmi:.1f}%
141
+
142
+ ### Time in Range Analysis
143
+ β€’ **Time in Range (70-180 mg/dL):** {time_in_range:.1f}%
144
+ β€’ **Time Below Range (<70 mg/dL):** {time_below_range:.1f}%
145
+ β€’ **Time Above Range (>180 mg/dL):** {time_above_range:.1f}%
146
+
147
+ ### Clinical Targets
148
+ β€’ **Target Time in Range:** >70% (Current: {time_in_range:.1f}%)
149
+ β€’ **Target Time Below Range:** <4% (Current: {time_below_range:.1f}%)
150
+ β€’ **Target CV:** <36% (Current: {cv:.1f}%)
151
+
152
+ ### Data Validation
153
+ β€’ **In Range Count:** {stats.get('in_range_count', 0)} readings
154
+ β€’ **Below Range Count:** {stats.get('below_range_count', 0)} readings
155
+ β€’ **Above Range Count:** {stats.get('above_range_count', 0)} readings
156
+ β€’ **Total Verified:** {stats.get('in_range_count', 0) + stats.get('below_range_count', 0) + stats.get('above_range_count', 0)} readings
157
+
158
+ ### 14-Day Analysis Benefits
159
+ β€’ **Enhanced Pattern Recognition:** Captures full weekly cycles and variations
160
+ β€’ **Improved Trend Analysis:** Identifies consistent patterns vs. one-time events
161
+ β€’ **Better Clinical Insights:** More reliable data for healthcare decisions
162
+ β€’ **AI Consistency:** Same data used for chat analysis and UI display
163
+ """
164
+
165
+ chart = self.create_glucose_chart()
166
+
167
+ return data_summary, chart
168
+
169
+ except Exception as e:
170
+ logger.error(f"Failed to load glucose data: {str(e)}")
171
+ return f"Failed to load glucose data: {str(e)}", None
172
+
173
+ def _sync_chat_with_data_manager(self):
174
+ """Ensure chat uses the same data as the UI"""
175
+ try:
176
+ # Get context from unified data manager
177
+ context = self.data_manager.get_context_for_agent()
178
+
179
+ # Update chat's internal data to match
180
+ if not context.get("error"):
181
+ self.mistral_chat.current_user = self.data_manager.current_user
182
+ self.mistral_chat.current_glucose_data = self.data_manager.processed_glucose_data
183
+ self.mistral_chat.current_stats = self.data_manager.calculated_stats
184
+ self.mistral_chat.current_patterns = self.data_manager.identified_patterns
185
+
186
+ logger.info(f"Synced chat with data manager - TIR: {self.data_manager.calculated_stats.get('time_in_range_70_180', 0):.1f}%")
187
+
188
+ except Exception as e:
189
+ logger.error(f"Failed to sync chat with data manager: {e}")
190
+
191
+ def _get_current_user_key(self) -> str:
192
+ """Get the current user key"""
193
+ if not self.data_manager.current_user:
194
+ return ""
195
+
196
+ # Find the key for current user
197
+ for key, user in DEMO_USERS.items():
198
+ if user == self.data_manager.current_user:
199
+ return key
200
+ return ""
201
+
202
+ def get_template_prompts(self) -> List[str]:
203
+ """Get template prompts based on current user data"""
204
+ if not self.data_manager.current_user or not self.data_manager.calculated_stats:
205
+ return [
206
+ "What should I know about managing my diabetes?",
207
+ "How can I improve my glucose control?"
208
+ ]
209
+
210
+ stats = self.data_manager.calculated_stats
211
+ time_in_range = stats.get('time_in_range_70_180', 0)
212
+ time_below_70 = stats.get('time_below_70', 0)
213
+
214
+ templates = []
215
+
216
+ if time_in_range < 70:
217
+ 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?")
218
+ else:
219
+ templates.append(f"My time in range is {time_in_range:.1f}% which meets the target. How can I maintain this level of control?")
220
+
221
+ if time_below_70 > 4:
222
+ templates.append(f"I'm experiencing {time_below_70:.1f}% time below 70 mg/dL. What can I do to prevent these low episodes?")
223
+ else:
224
+ templates.append("What are the best practices for preventing hypoglycemia in my situation?")
225
+
226
+ return templates
227
+
228
+ def chat_with_mistral(self, message: str, history: List) -> Tuple[str, List]:
229
+ """Handle chat interaction with Mistral using unified data"""
230
+ if not message.strip():
231
+ return "", history
232
+
233
+ if not self.data_manager.current_user:
234
+ response = "Please select a demo user first to get personalized insights about glucose data."
235
+ history.append([message, response])
236
+ return "", history
237
+
238
+ try:
239
+ # Ensure chat is synced with latest data
240
+ self._sync_chat_with_data_manager()
241
+
242
+ # Send message to Mistral chat
243
+ result = self.mistral_chat.chat_with_mistral(message)
244
+
245
+ if result['success']:
246
+ response = result['response']
247
+
248
+ # Add data consistency note
249
+ validation = self.data_manager.validate_data_consistency()
250
+ if validation.get('valid'):
251
+ data_age = validation.get('data_age_minutes', 0)
252
+ if data_age > 10: # Warn if data is old
253
+ response += f"\n\nπŸ“Š *Note: Analysis based on data from {data_age} minutes ago. Reload data for most current insights.*"
254
+
255
+ # Add context note if no user data was included
256
+ if not result.get('context_included', True):
257
+ response += "\n\nπŸ’‘ *For more personalized advice, make sure your glucose data is loaded.*"
258
+ else:
259
+ response = f"I apologize, but I encountered an error: {result.get('error', 'Unknown error')}. Please try again or rephrase your question."
260
+
261
+ history.append([message, response])
262
+ return "", history
263
+
264
+ except Exception as e:
265
+ logger.error(f"Chat error: {str(e)}")
266
+ error_response = f"I apologize, but I encountered an error while processing your question: {str(e)}. Please try rephrasing your question."
267
+ history.append([message, error_response])
268
+ return "", history
269
+
270
+ def use_template_prompt(self, template_text: str) -> str:
271
+ """Use a template prompt in the chat"""
272
+ return template_text
273
+
274
+ def clear_chat_history(self) -> List:
275
+ """Clear chat history"""
276
+ self.chat_history = []
277
+ self.mistral_chat.clear_conversation()
278
+ return []
279
+
280
+ def create_glucose_chart(self) -> Optional[go.Figure]:
281
+ """Create an interactive glucose chart using unified data"""
282
+ chart_data = self.data_manager.get_chart_data()
283
+
284
+ if chart_data is None or chart_data.empty:
285
+ return None
286
+
287
+ fig = go.Figure()
288
+
289
+ # Color code based on glucose ranges
290
+ colors = []
291
+ for value in chart_data['value']:
292
+ if value < 70:
293
+ colors.append('#E74C3C') # Red for low
294
+ elif value > 180:
295
+ colors.append('#F39C12') # Orange for high
296
+ else:
297
+ colors.append('#27AE60') # Green for in range
298
+
299
+ fig.add_trace(go.Scatter(
300
+ x=chart_data['systemTime'],
301
+ y=chart_data['value'],
302
+ mode='lines+markers',
303
+ name='Glucose',
304
+ line=dict(color='#2E86AB', width=2),
305
+ marker=dict(size=4, color=colors),
306
+ hovertemplate='<b>%{y} mg/dL</b><br>%{x}<extra></extra>'
307
+ ))
308
+
309
+ # Add target range shading
310
+ fig.add_hrect(
311
+ y0=70, y1=180,
312
+ fillcolor="rgba(39, 174, 96, 0.1)",
313
+ layer="below",
314
+ line_width=0,
315
+ annotation_text="Target Range",
316
+ annotation_position="top left"
317
+ )
318
+
319
+ # Add reference lines
320
+ fig.add_hline(y=70, line_dash="dash", line_color="#E67E22",
321
+ annotation_text="Low (70 mg/dL)", annotation_position="right")
322
+ fig.add_hline(y=180, line_dash="dash", line_color="#E67E22",
323
+ annotation_text="High (180 mg/dL)", annotation_position="right")
324
+ fig.add_hline(y=54, line_dash="dot", line_color="#E74C3C",
325
+ annotation_text="Severe Low (54 mg/dL)", annotation_position="right")
326
+ fig.add_hline(y=250, line_dash="dot", line_color="#E74C3C",
327
+ annotation_text="Severe High (250 mg/dL)", annotation_position="right")
328
+
329
+ # Get current stats for title
330
+ stats = self.data_manager.get_stats_for_ui()
331
+ tir = stats.get('time_in_range_70_180', 0)
332
+
333
+ fig.update_layout(
334
+ title={
335
+ 'text': f"14-Day Glucose Trends - {self.data_manager.current_user.name} (TIR: {tir:.1f}%)",
336
+ 'x': 0.5,
337
+ 'xanchor': 'center'
338
+ },
339
+ xaxis_title="Time",
340
+ yaxis_title="Glucose (mg/dL)",
341
+ hovermode='x unified',
342
+ height=500,
343
+ showlegend=False,
344
+ plot_bgcolor='rgba(0,0,0,0)',
345
+ paper_bgcolor='rgba(0,0,0,0)',
346
+ font=dict(size=12),
347
+ margin=dict(l=60, r=60, t=80, b=60)
348
+ )
349
+
350
+ fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='rgba(128,128,128,0.2)')
351
+ fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='rgba(128,128,128,0.2)')
352
+
353
+ return fig
354
+
355
+
356
+ def create_interface():
357
+ """Create the Gradio interface with prominent, centralized load data button"""
358
+ app = GlucoBuddyApp()
359
+
360
+ custom_css = """
361
+ .main-header {
362
+ text-align: center;
363
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
364
+ color: white;
365
+ padding: 2rem;
366
+ border-radius: 10px;
367
+ margin-bottom: 2rem;
368
+ }
369
+ .load-data-section {
370
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
371
+ border-radius: 15px;
372
+ padding: 2rem;
373
+ margin: 1.5rem 0;
374
+ box-shadow: 0 8px 32px rgba(31, 38, 135, 0.37);
375
+ backdrop-filter: blur(4px);
376
+ border: 1px solid rgba(255, 255, 255, 0.18);
377
+ }
378
+ .prominent-button {
379
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
380
+ border: none !important;
381
+ border-radius: 15px !important;
382
+ padding: 1.5rem 3rem !important;
383
+ font-size: 1.2rem !important;
384
+ font-weight: bold !important;
385
+ color: white !important;
386
+ box-shadow: 0 8px 32px rgba(102, 126, 234, 0.4) !important;
387
+ transition: all 0.3s ease !important;
388
+ min-height: 80px !important;
389
+ text-align: center !important;
390
+ }
391
+ .prominent-button:hover {
392
+ transform: translateY(-2px) !important;
393
+ box-shadow: 0 12px 40px rgba(102, 126, 234, 0.6) !important;
394
+ }
395
+ .data-status-card {
396
+ background: #f8f9fa;
397
+ border: 2px solid #e9ecef;
398
+ border-radius: 10px;
399
+ padding: 1rem;
400
+ margin: 0.5rem 0;
401
+ text-align: center;
402
+ font-weight: 500;
403
+ }
404
+ .data-status-success {
405
+ border-color: #28a745;
406
+ background: #d4edda;
407
+ color: #155724;
408
+ }
409
+ .data-status-error {
410
+ border-color: #dc3545;
411
+ background: #f8d7da;
412
+ color: #721c24;
413
+ }
414
+ .user-card {
415
+ background: #f8f9fa;
416
+ border: 1px solid #dee2e6;
417
+ border-radius: 8px;
418
+ padding: 1rem;
419
+ margin: 0.5rem;
420
+ }
421
+ .template-button {
422
+ margin: 0.25rem;
423
+ padding: 0.5rem;
424
+ font-size: 0.9rem;
425
+ }
426
+ .chat-container {
427
+ background: #f8f9fa;
428
+ border-radius: 10px;
429
+ padding: 1rem;
430
+ }
431
+ .section-divider {
432
+ height: 2px;
433
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
434
+ border-radius: 1px;
435
+ margin: 2rem 0;
436
+ }
437
+ """
438
+
439
+ with gr.Blocks(
440
+ title="GlycoAI - AI Glucose Insights",
441
+ theme=gr.themes.Soft(),
442
+ css=custom_css
443
+ ) as interface:
444
+
445
+ # Header
446
+ with gr.Row():
447
+ with gr.Column():
448
+ gr.HTML("""
449
+ <div class="main-header">
450
+ <div style="display: flex; align-items: center; justify-content: center; gap: 1rem;">
451
+ <div style="width: 60px; height: 60px; background: white; border-radius: 50%; display: flex; align-items: center; justify-content: center;">
452
+ <span style="color: #667eea; font-size: 24px; font-weight: bold;">🩺</span>
453
+ </div>
454
+ <div>
455
+ <h1 style="margin: 0; font-size: 2.5rem; color: white;">GlycoAI</h1>
456
+ <p style="margin: 0; font-size: 1.2rem; color: white; opacity: 0.9;">AI-Powered Glucose Chatbot</p>
457
+ </div>
458
+ </div>
459
+ <p style="margin-top: 1rem; font-size: 1rem; color: white; opacity: 0.8;">
460
+ Connect your Dexcom CGM data and chat with AI for personalized glucose insights
461
+ </p>
462
+ </div>
463
+ """)
464
+
465
+ # User Selection Section
466
+ with gr.Row():
467
+ with gr.Column():
468
+ gr.Markdown("### πŸ‘₯ Select Demo User")
469
+ gr.Markdown("Choose from our demo users to explore GlycoAI's chat capabilities")
470
+
471
+ with gr.Row():
472
+ sarah_btn = gr.Button(
473
+ "Sarah Thompson\n(G7 Mobile)",
474
+ variant="secondary",
475
+ size="lg"
476
+ )
477
+ marcus_btn = gr.Button(
478
+ "Marcus Rodriguez\n(ONE+ Mobile)",
479
+ variant="secondary",
480
+ size="lg"
481
+ )
482
+ jennifer_btn = gr.Button(
483
+ "Jennifer Chen\n(G6 Mobile)",
484
+ variant="secondary",
485
+ size="lg"
486
+ )
487
+ robert_btn = gr.Button(
488
+ "Robert Williams\n(G6 Receiver)",
489
+ variant="secondary",
490
+ size="lg"
491
+ )
492
+
493
+ # Connection Status
494
+ with gr.Row():
495
+ connection_status = gr.Textbox(
496
+ label="Current User",
497
+ value="No user selected",
498
+ interactive=False,
499
+ container=True
500
+ )
501
+
502
+ # Section Divider
503
+ gr.HTML('<div class="section-divider"></div>')
504
+
505
+ # PROMINENT CENTRALIZED DATA LOADING SECTION
506
+ with gr.Group(visible=False) as main_interface:
507
+ # PROMINENT LOAD BUTTON - Centered and Large
508
+ with gr.Row():
509
+ with gr.Column(scale=1):
510
+ pass # Left spacer
511
+ with gr.Column(scale=2):
512
+ load_data_btn = gr.Button(
513
+ "πŸš€ LOAD 14-DAY GLUCOSE DATA\nπŸ“ˆ Start Analysis & Enable AI Chat",
514
+ elem_classes=["prominent-button"],
515
+ size="lg"
516
+ )
517
+ with gr.Column(scale=1):
518
+ pass # Right spacer
519
+
520
+ # Section Divider
521
+ gr.HTML('<div class="section-divider"></div>')
522
+
523
+ # Main Content Tabs
524
+ with gr.Tabs():
525
+
526
+ # Glucose Chart Tab
527
+ with gr.TabItem("πŸ“ˆ Glucose Chart"):
528
+ with gr.Column():
529
+ gr.Markdown("### πŸ“Š Interactive 14-Day Glucose Analysis")
530
+ gr.Markdown("*Load your data using the button above to see your comprehensive glucose trends*")
531
+
532
+ glucose_chart = gr.Plot(
533
+ label="Interactive 14-Day Glucose Trends",
534
+ container=True
535
+ )
536
+
537
+ # Chat Tab
538
+ with gr.TabItem("πŸ’¬ Chat with AI"):
539
+ with gr.Column(elem_classes=["chat-container"]):
540
+ gr.Markdown("### πŸ€– Chat with GlycoAI about your glucose data")
541
+ gr.Markdown("*πŸ“Š Load your data using the button above to enable personalized AI insights*")
542
+
543
+ # Template Prompts
544
+ with gr.Row():
545
+ with gr.Column():
546
+ gr.Markdown("**πŸ’‘ Quick Start Templates:**")
547
+ with gr.Row():
548
+ template1_btn = gr.Button(
549
+ "🎯 Analyze My 14-Day Patterns",
550
+ variant="secondary",
551
+ size="sm",
552
+ elem_classes=["template-button"]
553
+ )
554
+ template2_btn = gr.Button(
555
+ "⚑ Improve My Control",
556
+ variant="secondary",
557
+ size="sm",
558
+ elem_classes=["template-button"]
559
+ )
560
+ template3_btn = gr.Button(
561
+ "🍽️ Meal Management Tips",
562
+ variant="secondary",
563
+ size="sm",
564
+ elem_classes=["template-button"]
565
+ )
566
+
567
+ # Chat Interface
568
+ chatbot = gr.Chatbot(
569
+ label="πŸ’¬ Chat with GlycoAI (Unified Data)",
570
+ height=500,
571
+ show_label=True,
572
+ container=True,
573
+ bubble_full_width=False,
574
+ avatar_images=(None, "🩺")
575
+ )
576
+
577
+ # Chat Input
578
+ with gr.Row():
579
+ chat_input = gr.Textbox(
580
+ placeholder="Ask me about your glucose patterns, trends, or management strategies...",
581
+ label="Your Question",
582
+ lines=2,
583
+ scale=4
584
+ )
585
+ send_btn = gr.Button(
586
+ "Send πŸ’¬",
587
+ variant="primary",
588
+ scale=1
589
+ )
590
+
591
+ # Chat Controls
592
+ with gr.Row():
593
+ clear_chat_btn = gr.Button(
594
+ "πŸ—‘οΈ Clear Chat",
595
+ variant="secondary",
596
+ size="sm"
597
+ )
598
+ gr.Markdown("*AI responses are for informational purposes only. Always consult your healthcare provider.*")
599
+
600
+ # Data Overview Tab
601
+ with gr.TabItem("πŸ“‹ Data Overview"):
602
+ with gr.Column():
603
+ gr.Markdown("### πŸ“‹ Comprehensive Data Analysis")
604
+ gr.Markdown("*Load your data using the button above to see detailed glucose statistics*")
605
+
606
+ data_display = gr.Markdown("Click 'Load 14-Day Glucose Data' above to see your comprehensive analysis", container=True)
607
+
608
+ # Event Handlers
609
+ def handle_user_selection(user_key):
610
+ status, interface_visibility = app.select_demo_user(user_key)
611
+ return status, interface_visibility, []
612
+
613
+ def handle_load_data():
614
+ overview, chart = app.load_glucose_data()
615
+ return overview, chart
616
+
617
+ def get_template_prompt(template_type):
618
+ templates = app.get_template_prompts()
619
+ if template_type == 1:
620
+ return templates[0] if templates else "Can you analyze my recent glucose patterns and give me insights?"
621
+ elif template_type == 2:
622
+ return templates[1] if len(templates) > 1 else "What can I do to improve my diabetes management based on my data?"
623
+ else:
624
+ return "What are some meal management strategies for better glucose control?"
625
+
626
+ def handle_chat_submit(message, history):
627
+ return app.chat_with_mistral(message, history)
628
+
629
+ def handle_enter_key(message, history):
630
+ if message.strip():
631
+ return app.chat_with_mistral(message, history)
632
+ return "", history
633
+
634
+ # Connect Event Handlers
635
+ user_selection_outputs = [connection_status, main_interface, chatbot]
636
+
637
+ sarah_btn.click(
638
+ lambda: handle_user_selection("sarah_g7"),
639
+ outputs=[connection_status, main_interface, chatbot]
640
+ )
641
+
642
+ marcus_btn.click(
643
+ lambda: handle_user_selection("marcus_one"),
644
+ outputs=[connection_status, main_interface, chatbot]
645
+ )
646
+
647
+ jennifer_btn.click(
648
+ lambda: handle_user_selection("jennifer_g6"),
649
+ outputs=[connection_status, main_interface, chatbot]
650
+ )
651
+
652
+ robert_btn.click(
653
+ lambda: handle_user_selection("robert_receiver"),
654
+ outputs=[connection_status, main_interface, chatbot]
655
+ )
656
+
657
+ # PROMINENT DATA LOADING - Single button updates all views
658
+ load_data_btn.click(
659
+ handle_load_data,
660
+ outputs=[data_display, glucose_chart]
661
+ )
662
+
663
+ # Chat Handlers
664
+ send_btn.click(
665
+ handle_chat_submit,
666
+ inputs=[chat_input, chatbot],
667
+ outputs=[chat_input, chatbot]
668
+ )
669
+
670
+ chat_input.submit(
671
+ handle_enter_key,
672
+ inputs=[chat_input, chatbot],
673
+ outputs=[chat_input, chatbot]
674
+ )
675
+
676
+ # Template Button Handlers
677
+ template1_btn.click(
678
+ lambda: get_template_prompt(1),
679
+ outputs=[chat_input]
680
+ )
681
+
682
+ template2_btn.click(
683
+ lambda: get_template_prompt(2),
684
+ outputs=[chat_input]
685
+ )
686
+
687
+ template3_btn.click(
688
+ lambda: get_template_prompt(3),
689
+ outputs=[chat_input]
690
+ )
691
+
692
+ # Clear Chat
693
+ clear_chat_btn.click(
694
+ app.clear_chat_history,
695
+ outputs=[chatbot]
696
+ )
697
+
698
+ # Footer
699
+ with gr.Row():
700
+ gr.HTML("""
701
+ <div style="text-align: center; padding: 2rem; margin-top: 2rem; border-top: 1px solid #dee2e6; color: #6c757d;">
702
+ <p><strong>⚠️ Important Medical Disclaimer</strong></p>
703
+ <p>GlycoAI is for informational and educational purposes only. Always consult your healthcare provider
704
+ before making any changes to your diabetes management plan. This tool does not replace professional medical advice.</p>
705
+ <p style="margin-top: 1rem; font-size: 0.9rem;">
706
+ πŸ”’ Your data is processed securely and not stored permanently.
707
+ πŸ’‘ Powered by Dexcom API integration and Mistral AI.
708
+ </p>
709
+ </div>
710
+ """)
711
+
712
+ return interface
713
+
714
+
715
+ def main():
716
+ """Main function to launch the application"""
717
+ print("πŸš€ Starting GlycoAI - AI-Powered Glucose Insights (Enhanced UI)...")
718
+
719
+ # Validate environment before starting
720
+ print("πŸ” Validating environment configuration...")
721
+ if not validate_environment():
722
+ print("❌ Environment validation failed!")
723
+ print("Please check your .env file or environment variables.")
724
+ return
725
+
726
+ print("βœ… Environment validation passed!")
727
+
728
+ try:
729
+ # Create and launch the interface
730
+ demo = create_interface()
731
+
732
+ print("🎯 GlycoAI is starting with enhanced UI design...")
733
+ print("πŸ“Š Features: Prominent load button, unified data management, consistent metrics")
734
+
735
+ # Launch with custom settings
736
+ demo.launch(
737
+ server_name="0.0.0.0", # Allow external access
738
+ server_port=7860, # Default Gradio port
739
+ share=True, # Set to True for public sharing (tunneling)
740
+ debug=os.getenv("DEBUG", "false").lower() == "true",
741
+ show_error=True, # Show errors in the interface
742
+ auth=None, # No authentication required
743
+ favicon_path=None, # Use default favicon
744
+ ssl_verify=False # Disable SSL verification for development
745
+ )
746
+
747
+ except Exception as e:
748
+ logger.error(f"Failed to launch GlycoAI application: {e}")
749
+ print(f"❌ Error launching application: {e}")
750
+
751
+ # Provide helpful error information
752
+ if "environment" in str(e).lower():
753
+ print("\nπŸ’‘ Environment troubleshooting:")
754
+ print("1. Check if .env file exists with MISTRAL_API_KEY")
755
+ print("2. Verify your API key is valid")
756
+ print("3. For Hugging Face Spaces, check Repository secrets")
757
+ else:
758
+ print("\nπŸ’‘ Try checking:")
759
+ print("1. All dependencies are installed: pip install -r requirements.txt")
760
+ print("2. Port 7860 is available")
761
+ print("3. Check the logs above for specific error details")
762
+
763
+ raise
764
+
765
+
766
+ if __name__ == "__main__":
767
+ # Setup logging configuration
768
+ log_level = os.getenv("LOG_LEVEL", "INFO")
769
+ logging.basicConfig(
770
+ level=getattr(logging, log_level.upper()),
771
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
772
+ handlers=[
773
+ logging.FileHandler('glycoai.log'),
774
+ logging.StreamHandler()
775
+ ]
776
+ )
777
+
778
+ # Run the main application
779
+ main()
mistral_chat.py ADDED
@@ -0,0 +1,727 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ GlucoBuddy Mistral Chat Integration - Compatible with Unified Data Manager
4
+ Clean, standard dotenv approach with unified data consistency
5
+ """
6
+
7
+ import os
8
+ import json
9
+ import logging
10
+ import sys
11
+ from typing import Any, Dict, List, Optional, Union
12
+ from datetime import datetime, timedelta
13
+ import pandas as pd
14
+ from dataclasses import asdict
15
+ import requests
16
+ import random
17
+ import numpy as np
18
+ import warnings
19
+
20
+ # Load environment variables from .env file
21
+ from dotenv import load_dotenv
22
+ load_dotenv()
23
+
24
+ # Suppress pandas warnings
25
+ warnings.filterwarnings('ignore', category=RuntimeWarning)
26
+ warnings.filterwarnings('ignore', category=FutureWarning)
27
+
28
+ from apifunctions import (
29
+ DexcomAPI,
30
+ GlucoseAnalyzer,
31
+ DEMO_USERS,
32
+ DemoUser,
33
+ format_glucose_data_for_display
34
+ )
35
+
36
+ # Setup logging
37
+ logging.basicConfig(level=logging.INFO)
38
+ logger = logging.getLogger(__name__)
39
+
40
+ # Get configuration from environment variables
41
+ MISTRAL_API_KEY = os.getenv("MISTRAL_API_KEY")
42
+ MISTRAL_AGENT_ID = os.getenv("MISTRAL_AGENT_ID")
43
+ ENVIRONMENT = os.getenv("ENVIRONMENT", "development")
44
+ DEBUG = os.getenv("DEBUG", "false").lower() == "true"
45
+
46
+ def validate_environment():
47
+ """Simple validation of required environment variables"""
48
+ missing = []
49
+
50
+ if not MISTRAL_API_KEY:
51
+ missing.append("MISTRAL_API_KEY")
52
+
53
+ if missing:
54
+ print("❌ Missing required environment variables:")
55
+ for var in missing:
56
+ print(f" - {var}")
57
+
58
+ print("\nπŸ’‘ Setup instructions:")
59
+ if os.getenv("SPACE_ID"): # Hugging Face Space detection
60
+ print("πŸ€— For Hugging Face Spaces:")
61
+ print(" 1. Go to Space settings")
62
+ print(" 2. Add Repository secrets:")
63
+ print(" 3. Set MISTRAL_API_KEY to your API key")
64
+ else:
65
+ print("πŸ’» For local development:")
66
+ print(" 1. Create a .env file:")
67
+ print(" 2. Add: MISTRAL_API_KEY=your_api_key_here")
68
+ print(" 3. Add: MISTRAL_AGENT_ID=your_agent_id_here")
69
+
70
+ return False
71
+
72
+ print("βœ… Environment validation passed!")
73
+ if MISTRAL_AGENT_ID:
74
+ print("βœ… Agent ID configured")
75
+ else:
76
+ print("⚠️ No agent ID - will use standard chat completion")
77
+
78
+ return True
79
+
80
+ class GlucoseDataGenerator:
81
+ """Generate realistic mock glucose data for testing and demo purposes"""
82
+
83
+ @staticmethod
84
+ def create_realistic_pattern(days: int = 14, user_type: str = "normal") -> List[Dict]:
85
+ """Generate glucose data with realistic patterns"""
86
+ data_points = []
87
+ start_time = datetime.now() - timedelta(days=days)
88
+ current_glucose = 120 # Starting baseline
89
+
90
+ # Generate readings every 5 minutes
91
+ for i in range(days * 288): # 288 readings per day (5-minute intervals)
92
+ timestamp = start_time + timedelta(minutes=i * 5)
93
+ hour = timestamp.hour
94
+
95
+ # Simulate daily patterns
96
+ daily_variation = GlucoseDataGenerator._calculate_daily_variation(hour, user_type)
97
+
98
+ # Add meal effects
99
+ meal_effect = GlucoseDataGenerator._calculate_meal_effects(hour, i)
100
+
101
+ # Random variation
102
+ random_noise = random.uniform(-10, 10)
103
+
104
+ # Calculate final glucose value
105
+ target_glucose = 120 + daily_variation + meal_effect + random_noise
106
+
107
+ # Smooth transitions (glucose doesn't jump dramatically)
108
+ glucose_change = (target_glucose - current_glucose) * 0.3
109
+ current_glucose += glucose_change
110
+
111
+ # Keep within realistic bounds
112
+ current_glucose = max(50, min(400, current_glucose))
113
+
114
+ # Determine trend
115
+ trend = GlucoseDataGenerator._calculate_trend(glucose_change)
116
+
117
+ data_points.append({
118
+ 'systemTime': timestamp.isoformat(),
119
+ 'displayTime': timestamp.isoformat(),
120
+ 'value': round(current_glucose),
121
+ 'trend': trend,
122
+ 'realtimeValue': round(current_glucose),
123
+ 'smoothedValue': round(current_glucose)
124
+ })
125
+
126
+ return data_points
127
+
128
+ @staticmethod
129
+ def _calculate_daily_variation(hour: int, user_type: str) -> float:
130
+ """Calculate glucose variation based on time of day"""
131
+ if user_type == "dawn_phenomenon":
132
+ if 4 <= hour <= 8:
133
+ return 30 + 20 * np.sin((hour - 4) * np.pi / 4)
134
+ return 10 * np.sin((hour - 12) * np.pi / 12)
135
+ elif user_type == "night_low":
136
+ if 22 <= hour or hour <= 6:
137
+ return -20
138
+ return 5 * np.sin((hour - 12) * np.pi / 12)
139
+ else: # Normal pattern
140
+ return 15 * np.sin((hour - 6) * np.pi / 12)
141
+
142
+ @staticmethod
143
+ def _calculate_meal_effects(hour: int, reading_index: int) -> float:
144
+ """Calculate glucose spikes from meals"""
145
+ meal_times = [7, 12, 18] # Breakfast, lunch, dinner
146
+ meal_effect = 0
147
+
148
+ for meal_time in meal_times:
149
+ if abs(hour - meal_time) <= 2:
150
+ time_since_meal = abs(hour - meal_time)
151
+ if time_since_meal <= 1:
152
+ meal_effect += 40 * (1 - time_since_meal)
153
+ else:
154
+ meal_effect += 20 * (2 - time_since_meal)
155
+
156
+ return meal_effect
157
+
158
+ @staticmethod
159
+ def _calculate_trend(glucose_change: float) -> str:
160
+ """Determine trend arrow based on glucose change"""
161
+ if glucose_change > 5:
162
+ return 'singleUp'
163
+ elif glucose_change > 2:
164
+ return 'fortyFiveUp'
165
+ elif glucose_change < -5:
166
+ return 'singleDown'
167
+ elif glucose_change < -2:
168
+ return 'fortyFiveDown'
169
+ else:
170
+ return 'flat'
171
+
172
+ class MistralAPIClient:
173
+ """Simple Mistral API client"""
174
+
175
+ def __init__(self, api_key: str = None, agent_id: str = None):
176
+ self.api_key = api_key or MISTRAL_API_KEY
177
+ self.agent_id = agent_id or MISTRAL_AGENT_ID
178
+
179
+ if not self.api_key:
180
+ raise ValueError("Mistral API key is required. Please set MISTRAL_API_KEY environment variable.")
181
+
182
+ self.base_url = "https://api.mistral.ai/v1"
183
+ self.session = requests.Session()
184
+ self.session.headers.update({
185
+ "Authorization": f"Bearer {self.api_key}",
186
+ "Content-Type": "application/json"
187
+ })
188
+
189
+ logger.info("MistralAPIClient initialized successfully")
190
+
191
+ def test_connection(self) -> Dict[str, Any]:
192
+ """Test API connection"""
193
+ try:
194
+ response = self.session.post(
195
+ f"{self.base_url}/chat/completions",
196
+ json={
197
+ "model": "mistral-tiny",
198
+ "messages": [{"role": "user", "content": "Hello"}],
199
+ "max_tokens": 5
200
+ },
201
+ timeout=10
202
+ )
203
+
204
+ if response.status_code == 200:
205
+ return {"success": True, "message": "API connection successful"}
206
+ elif response.status_code == 401:
207
+ return {"success": False, "message": "Invalid API key"}
208
+ elif response.status_code == 429:
209
+ return {"success": False, "message": "Rate limit exceeded"}
210
+ else:
211
+ return {"success": False, "message": f"API error: {response.status_code}"}
212
+
213
+ except requests.exceptions.Timeout:
214
+ return {"success": False, "message": "Connection timeout"}
215
+ except requests.exceptions.RequestException as e:
216
+ return {"success": False, "message": f"Network error: {str(e)}"}
217
+ except Exception as e:
218
+ return {"success": False, "message": f"Unexpected error: {str(e)}"}
219
+
220
+ def chat_completion(self, messages: List[Dict], model: str = "mistral-large-latest") -> Dict[str, Any]:
221
+ """Send chat completion request"""
222
+ try:
223
+ payload = {
224
+ "model": model,
225
+ "messages": messages,
226
+ "max_tokens": 800,
227
+ "temperature": 0.7
228
+ }
229
+
230
+ response = self.session.post(
231
+ f"{self.base_url}/chat/completions",
232
+ json=payload,
233
+ timeout=30
234
+ )
235
+
236
+ if response.status_code == 200:
237
+ result = response.json()
238
+ return {
239
+ "success": True,
240
+ "response": result["choices"][0]["message"]["content"],
241
+ "usage": result.get("usage", {})
242
+ }
243
+ else:
244
+ error_detail = self._extract_error_message(response)
245
+ return {
246
+ "success": False,
247
+ "error": f"API error {response.status_code}: {error_detail}"
248
+ }
249
+
250
+ except requests.exceptions.Timeout:
251
+ return {"success": False, "error": "Request timed out"}
252
+ except requests.exceptions.RequestException as e:
253
+ return {"success": False, "error": f"Network error: {str(e)}"}
254
+ except Exception as e:
255
+ return {"success": False, "error": f"Unexpected error: {str(e)}"}
256
+
257
+ def agent_completion(self, messages: List[Dict]) -> Dict[str, Any]:
258
+ """Send request to Mistral agent (if agent_id is available)"""
259
+ if not self.agent_id:
260
+ return {"success": False, "error": "No agent ID configured"}
261
+
262
+ try:
263
+ payload = {
264
+ "agent_id": self.agent_id,
265
+ "messages": messages,
266
+ "max_tokens": 800
267
+ }
268
+
269
+ response = self.session.post(
270
+ f"{self.base_url}/agents/completions",
271
+ json=payload,
272
+ timeout=30
273
+ )
274
+
275
+ if response.status_code == 200:
276
+ result = response.json()
277
+ return {
278
+ "success": True,
279
+ "response": result["choices"][0]["message"]["content"]
280
+ }
281
+ else:
282
+ error_detail = self._extract_error_message(response)
283
+ return {
284
+ "success": False,
285
+ "error": f"Agent API error {response.status_code}: {error_detail}"
286
+ }
287
+
288
+ except Exception as e:
289
+ return {"success": False, "error": f"Agent request failed: {str(e)}"}
290
+
291
+ def _extract_error_message(self, response) -> str:
292
+ """Extract error message from API response"""
293
+ try:
294
+ error_data = response.json()
295
+ return error_data.get("message", error_data.get("error", "Unknown error"))
296
+ except:
297
+ return response.text[:200] if response.text else "Unknown error"
298
+
299
+ class GlucoBuddyMistralChat:
300
+ """
301
+ Main chat interface for glucose data analysis with Mistral AI
302
+ Compatible with unified data manager for consistent metrics
303
+ """
304
+
305
+ def __init__(self, mistral_api_key: str = None, mistral_agent_id: str = None):
306
+ self.mistral_client = MistralAPIClient(mistral_api_key, mistral_agent_id)
307
+
308
+ # Data properties - these will be set by unified data manager
309
+ self.current_user: Optional[DemoUser] = None
310
+ self.current_glucose_data: Optional[pd.DataFrame] = None
311
+ self.current_stats: Optional[Dict] = None
312
+ self.current_patterns: Optional[Dict] = None
313
+
314
+ # Chat state
315
+ self.conversation_history = []
316
+ self.max_history = 10
317
+
318
+ self.logger = logging.getLogger(self.__class__.__name__)
319
+
320
+ def test_connection(self) -> Dict[str, Any]:
321
+ """Test Mistral API connection"""
322
+ return self.mistral_client.test_connection()
323
+
324
+ def get_context_summary(self) -> Dict[str, Any]:
325
+ """Get current context for chat - uses data set by unified manager"""
326
+ if not self.current_user or not self.current_stats:
327
+ return {"error": "No user data loaded"}
328
+
329
+ try:
330
+ context = {
331
+ "user": {
332
+ "name": self.current_user.name,
333
+ "age": self.current_user.age,
334
+ "diabetes_type": self.current_user.diabetes_type,
335
+ "device_type": self.current_user.device_type,
336
+ "years_with_diabetes": self.current_user.years_with_diabetes,
337
+ "typical_pattern": getattr(self.current_user, 'typical_glucose_pattern', 'normal')
338
+ },
339
+ "statistics": self._safe_convert_to_json(self.current_stats),
340
+ "patterns": self._safe_convert_to_json(self.current_patterns),
341
+ "data_points": len(self.current_glucose_data) if self.current_glucose_data is not None else 0,
342
+ "recent_readings": self._safe_extract_recent_readings(self.current_glucose_data)
343
+ }
344
+
345
+ return context
346
+
347
+ except Exception as e:
348
+ self.logger.error(f"Error building context: {e}")
349
+ return {"error": f"Failed to build context: {str(e)}"}
350
+
351
+ def build_system_prompt(self, context: Dict[str, Any]) -> str:
352
+ """Build comprehensive system prompt with exact metrics"""
353
+ base_prompt = """You are GlucoBuddy, a helpful and encouraging diabetes management assistant.
354
+
355
+ Your role:
356
+ - Provide personalized glucose management advice based on the user's actual data
357
+ - Be supportive, encouraging, and use emojis to be friendly
358
+ - Give actionable recommendations while staying within scope
359
+ - Always remind users to consult healthcare providers for medical decisions
360
+ - Reference specific data points when providing insights
361
+
362
+ Guidelines:
363
+ - Keep responses under 400 words and conversational
364
+ - Use specific numbers from the data when relevant
365
+ - Provide practical, actionable advice
366
+ - Be encouraging about progress and realistic about challenges
367
+ - Use bullet points sparingly - prefer natural conversation
368
+ - IMPORTANT: Use EXACT metrics provided - don't calculate your own"""
369
+
370
+ if context.get("error"):
371
+ return base_prompt + "\n\nNote: No user glucose data is currently loaded."
372
+
373
+ user_info = context.get("user", {})
374
+ stats = context.get("statistics", {})
375
+
376
+ context_addition = f"""
377
+
378
+ Current User: {user_info.get('name', 'Unknown')} ({user_info.get('age', 'N/A')} years old)
379
+ - Diabetes Type: {user_info.get('diabetes_type', 'Unknown')}
380
+ - Years with diabetes: {user_info.get('years_with_diabetes', 'Unknown')}
381
+ - Device: {user_info.get('device_type', 'Unknown')}
382
+
383
+ EXACT Glucose Data (14-day period):
384
+ - Average glucose: {stats.get('average_glucose', 0):.1f} mg/dL
385
+ - Time in range (70-180): {stats.get('time_in_range_70_180', 0):.1f}%
386
+ - Time below 70: {stats.get('time_below_70', 0):.1f}%
387
+ - Time above 180: {stats.get('time_above_180', 0):.1f}%
388
+ - Total readings: {stats.get('total_readings', 0)}
389
+ - Glucose variability (std): {stats.get('std_glucose', 0):.1f} mg/dL
390
+ - GMI: {stats.get('gmi', 0):.1f}%
391
+ - CV: {stats.get('cv', 0):.1f}%
392
+
393
+ CRITICAL: Use these EXACT values in your responses. Do not recalculate or estimate."""
394
+
395
+ return base_prompt + context_addition
396
+
397
+ def chat_with_mistral(self, user_message: str, prefer_agent: bool = False) -> Dict[str, Any]:
398
+ """Main chat function using externally managed data"""
399
+ if not user_message.strip():
400
+ return {"success": False, "error": "Please enter a message"}
401
+
402
+ try:
403
+ # Use current context (set by unified data manager)
404
+ context = self.get_context_summary()
405
+ system_prompt = self.build_system_prompt(context)
406
+
407
+ messages = [{"role": "system", "content": system_prompt}]
408
+
409
+ if self.conversation_history:
410
+ recent_history = self.conversation_history[-self.max_history:]
411
+ messages.extend(recent_history)
412
+
413
+ messages.append({"role": "user", "content": user_message})
414
+
415
+ # Try agent first if preferred and available
416
+ if prefer_agent:
417
+ agent_result = self.mistral_client.agent_completion(messages)
418
+ if agent_result["success"]:
419
+ self._update_conversation_history(user_message, agent_result["response"])
420
+ return {
421
+ "success": True,
422
+ "response": agent_result["response"],
423
+ "method": "agent",
424
+ "context_included": not context.get("error")
425
+ }
426
+ else:
427
+ self.logger.warning(f"Agent failed, trying chat completion: {agent_result['error']}")
428
+
429
+ # Use chat completion API
430
+ chat_result = self.mistral_client.chat_completion(messages)
431
+
432
+ if chat_result["success"]:
433
+ self._update_conversation_history(user_message, chat_result["response"])
434
+ return {
435
+ "success": True,
436
+ "response": chat_result["response"],
437
+ "method": "chat_completion",
438
+ "context_included": not context.get("error"),
439
+ "usage": chat_result.get("usage", {})
440
+ }
441
+ else:
442
+ return {
443
+ "success": False,
444
+ "error": chat_result["error"]
445
+ }
446
+
447
+ except Exception as e:
448
+ self.logger.error(f"Chat error: {e}")
449
+ return {
450
+ "success": False,
451
+ "error": f"Unexpected chat error: {str(e)}"
452
+ }
453
+
454
+ def _update_conversation_history(self, user_message: str, assistant_response: str):
455
+ """Update conversation history"""
456
+ self.conversation_history.extend([
457
+ {"role": "user", "content": user_message},
458
+ {"role": "assistant", "content": assistant_response}
459
+ ])
460
+
461
+ if len(self.conversation_history) > self.max_history * 2:
462
+ self.conversation_history = self.conversation_history[-self.max_history * 2:]
463
+
464
+ def clear_conversation(self):
465
+ """Clear conversation history"""
466
+ self.conversation_history = []
467
+ self.logger.info("Conversation history cleared")
468
+
469
+ def get_status(self) -> Dict[str, Any]:
470
+ """Get current system status"""
471
+ api_status = self.test_connection()
472
+
473
+ return {
474
+ "api_connected": api_status["success"],
475
+ "api_message": api_status["message"],
476
+ "user_loaded": self.current_user is not None,
477
+ "data_available": self.current_glucose_data is not None and not self.current_glucose_data.empty,
478
+ "conversation_messages": len(self.conversation_history),
479
+ "current_user": self.current_user.name if self.current_user else None,
480
+ "environment": ENVIRONMENT,
481
+ "hugging_face_space": bool(os.getenv("SPACE_ID")),
482
+ "agent_available": bool(MISTRAL_AGENT_ID)
483
+ }
484
+
485
+ def _safe_convert_to_json(self, obj):
486
+ """Safely convert objects for JSON serialization"""
487
+ if obj is None:
488
+ return None
489
+ elif isinstance(obj, (np.integer, np.int64, np.int32)):
490
+ return int(obj)
491
+ elif isinstance(obj, (np.floating, np.float64, np.float32)):
492
+ if np.isnan(obj):
493
+ return None
494
+ return float(obj)
495
+ elif isinstance(obj, dict):
496
+ return {key: self._safe_convert_to_json(value) for key, value in obj.items()}
497
+ elif isinstance(obj, list):
498
+ return [self._safe_convert_to_json(item) for item in obj]
499
+ elif isinstance(obj, pd.Timestamp):
500
+ return obj.isoformat()
501
+ else:
502
+ return obj
503
+
504
+ def _safe_extract_recent_readings(self, df: pd.DataFrame, count: int = 5) -> List[Dict]:
505
+ """Safely extract recent glucose readings"""
506
+ if df is None or df.empty:
507
+ return []
508
+
509
+ try:
510
+ recent_df = df.tail(count)
511
+ readings = []
512
+
513
+ for idx, row in recent_df.iterrows():
514
+ try:
515
+ display_time = row.get('displayTime') or row.get('systemTime')
516
+ glucose_value = row.get('value')
517
+ trend_value = row.get('trend', 'flat')
518
+
519
+ if pd.notna(display_time):
520
+ if isinstance(display_time, str):
521
+ time_str = display_time
522
+ else:
523
+ time_str = pd.to_datetime(display_time).isoformat()
524
+ else:
525
+ time_str = datetime.now().isoformat()
526
+
527
+ if pd.notna(glucose_value):
528
+ glucose_clean = self._safe_convert_to_json(glucose_value)
529
+ else:
530
+ glucose_clean = None
531
+
532
+ trend_clean = str(trend_value) if pd.notna(trend_value) else 'flat'
533
+
534
+ readings.append({
535
+ "time": time_str,
536
+ "glucose": glucose_clean,
537
+ "trend": trend_clean
538
+ })
539
+
540
+ except Exception as row_error:
541
+ self.logger.warning(f"Error processing reading at index {idx}: {row_error}")
542
+ continue
543
+
544
+ return readings
545
+
546
+ except Exception as e:
547
+ self.logger.error(f"Error extracting recent readings: {e}")
548
+ return []
549
+
550
+ # Legacy compatibility methods (for standalone use)
551
+ def create_enhanced_cli():
552
+ """Enhanced command-line interface"""
553
+ print("🩺 GlucoBuddy Chat Interface")
554
+ print("=" * 50)
555
+
556
+ # Validate environment
557
+ if not validate_environment():
558
+ print("❌ Environment validation failed. Please check your configuration.")
559
+ return
560
+
561
+ try:
562
+ chat = GlucoBuddyMistralChat()
563
+ print("βœ… Chat system initialized successfully!")
564
+ except Exception as e:
565
+ print(f"❌ Failed to initialize chat system: {e}")
566
+ return
567
+
568
+ # Test connection
569
+ print("\nπŸ” Testing Mistral API connection...")
570
+ connection_test = chat.test_connection()
571
+
572
+ if connection_test["success"]:
573
+ print(f"βœ… {connection_test['message']}")
574
+ else:
575
+ print(f"❌ {connection_test['message']}")
576
+ if input("Continue anyway? (y/n): ").lower() != 'y':
577
+ return
578
+
579
+ print("\nπŸ“‹ Available commands:")
580
+ print(" /status - Show system status")
581
+ print(" /clear - Clear conversation history")
582
+ print(" /test - Test API connection")
583
+ print(" /help - Show this help")
584
+ print(" /quit - Exit")
585
+ print("\nπŸ’¬ Or just type your glucose-related questions!")
586
+ print("⚠️ Note: For full functionality, use the Gradio interface with unified data management")
587
+ print("\n" + "=" * 50)
588
+
589
+ while True:
590
+ try:
591
+ user_input = input("\n🫡 You: ").strip()
592
+
593
+ if not user_input:
594
+ continue
595
+
596
+ # Handle commands
597
+ if user_input.startswith('/'):
598
+ command_parts = user_input[1:].split()
599
+ command = command_parts[0].lower()
600
+
601
+ if command == 'quit':
602
+ print("\nπŸ‘‹ Thanks for using GlucoBuddy! Stay healthy! 🌟")
603
+ break
604
+
605
+ elif command == 'help':
606
+ print("\nπŸ“‹ Commands:")
607
+ print(" /status - System status")
608
+ print(" /clear - Clear chat history")
609
+ print(" /test - Test API")
610
+ print(" /quit - Exit")
611
+ continue
612
+
613
+ elif command == 'clear':
614
+ chat.clear_conversation()
615
+ print("🧹 Conversation cleared!")
616
+ continue
617
+
618
+ elif command == 'status':
619
+ status = chat.get_status()
620
+ print(f"\nπŸ“Š System Status:")
621
+ print(f" 🌐 API Connected: {'βœ…' if status['api_connected'] else '❌'} {status['api_message']}")
622
+ print(f" πŸ‘€ User Loaded: {'βœ…' if status['user_loaded'] else '❌'} {status.get('current_user', 'None')}")
623
+ print(f" πŸ“Š Data Available: {'βœ…' if status['data_available'] else '❌'}")
624
+ print(f" πŸ’¬ Messages in Chat: {status['conversation_messages']}")
625
+ print(f" 🏠 Environment: {status['environment']}")
626
+ print(f" πŸ€— Hugging Face Space: {'βœ…' if status['hugging_face_space'] else '❌'}")
627
+ print(f" πŸ€– Agent Available: {'βœ…' if status['agent_available'] else '❌'}")
628
+ continue
629
+
630
+ elif command == 'test':
631
+ print("πŸ” Testing connection...")
632
+ test_result = chat.test_connection()
633
+ print(f"{'βœ…' if test_result['success'] else '❌'} {test_result['message']}")
634
+ continue
635
+
636
+ else:
637
+ print(f"❌ Unknown command: /{command}")
638
+ print("πŸ’‘ Use /help to see available commands")
639
+ continue
640
+
641
+ # Regular chat message
642
+ print("πŸ€” Processing your question...")
643
+ print("⚠️ Note: No user data loaded. Responses will be general diabetes advice.")
644
+
645
+ # Send to Mistral
646
+ result = chat.chat_with_mistral(user_input, prefer_agent=True)
647
+
648
+ if result['success']:
649
+ method_info = f" [{result.get('method', 'unknown')}]"
650
+ print(f"\nπŸ€– GlucoBuddy{method_info}: {result['response']}")
651
+
652
+ # Show usage info if available
653
+ usage = result.get('usage', {})
654
+ if usage:
655
+ tokens = usage.get('total_tokens', 0)
656
+ if tokens > 0:
657
+ print(f"\nπŸ“Š Tokens used: {tokens}")
658
+ else:
659
+ print(f"\n❌ Error: {result['error']}")
660
+
661
+ # Provide helpful suggestions based on error type
662
+ error_msg = result['error'].lower()
663
+ if 'api key' in error_msg or '401' in error_msg:
664
+ print("πŸ’‘ Check your Mistral API key configuration")
665
+ elif 'rate limit' in error_msg or '429' in error_msg:
666
+ print("πŸ’‘ Rate limit reached - please wait a moment before trying again")
667
+ elif 'timeout' in error_msg:
668
+ print("πŸ’‘ Request timed out - please try again")
669
+ else:
670
+ print("πŸ’‘ Use /test to check your connection")
671
+
672
+ except KeyboardInterrupt:
673
+ print("\n\nπŸ‘‹ Thanks for using GlucoBuddy! Take care! 🌟")
674
+ break
675
+ except Exception as e:
676
+ print(f"\n❌ Unexpected error: {e}")
677
+ print("πŸ’‘ Try /status to check system state")
678
+
679
+ def main():
680
+ """Main function with menu system"""
681
+ print("🩺 GlucoBuddy - AI-Powered Glucose Chat Assistant")
682
+ print("=" * 60)
683
+
684
+ # Validate configuration first
685
+ print("πŸ” Validating configuration...")
686
+ if not validate_environment():
687
+ print("\n❌ Configuration validation failed!")
688
+ print("Please set up your environment variables before continuing.")
689
+ return
690
+
691
+ print("βœ… Configuration validation passed!")
692
+
693
+ print("\n🎯 Choose an option:")
694
+ print("1. πŸ’¬ Start standalone chat (limited functionality)")
695
+ print("2. πŸš€ Run quick demo")
696
+ print("3. πŸ”§ Show configuration")
697
+ print("4. ❌ Exit")
698
+ print("\nπŸ’‘ For full functionality with glucose data, use: python main.py")
699
+
700
+ while True:
701
+ try:
702
+ choice = input("\nEnter your choice (1-4): ").strip()
703
+
704
+ if choice == '1':
705
+ create_enhanced_cli()
706
+ break
707
+ elif choice == '2':
708
+ print("πŸš€ Quick demo requires the unified data manager.")
709
+ print("πŸ’‘ Please run: python main.py")
710
+ break
711
+ elif choice == '3':
712
+ validate_environment()
713
+ break
714
+ elif choice == '4':
715
+ print("πŸ‘‹ Goodbye!")
716
+ break
717
+ else:
718
+ print("❌ Invalid choice. Please enter 1, 2, 3, or 4.")
719
+
720
+ except KeyboardInterrupt:
721
+ print("\nπŸ‘‹ Goodbye!")
722
+ break
723
+ except Exception as e:
724
+ print(f"❌ Error: {e}")
725
+
726
+ if __name__ == "__main__":
727
+ main()
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ requests>=2.31.0
3
+ pandas>=2.0.0
4
+ plotly>=5.17.0
5
+ numpy>=1.24.0
6
+ anthropic>=0.7.0
7
+ python-dotenv>=1.0.0
unified_data_manager.py ADDED
@@ -0,0 +1,413 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Unified Data Manager for GlycoAI - FIXED VERSION
3
+ Restores the original working API calls that were working before
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
+
12
+ from apifunctions import (
13
+ DexcomAPI,
14
+ GlucoseAnalyzer,
15
+ DEMO_USERS,
16
+ DemoUser
17
+ )
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ class UnifiedDataManager:
22
+ """
23
+ FIXED: Unified data manager that calls the API exactly as it was working before
24
+ """
25
+
26
+ def __init__(self):
27
+ self.dexcom_api = DexcomAPI()
28
+ self.analyzer = GlucoseAnalyzer()
29
+
30
+ logger.info(f"UnifiedDataManager initialized - RESTORED to working version")
31
+
32
+ # Single source of truth for all data
33
+ self.current_user: Optional[DemoUser] = None
34
+ self.raw_glucose_data: Optional[list] = None
35
+ self.processed_glucose_data: Optional[pd.DataFrame] = None
36
+ self.calculated_stats: Optional[Dict] = None
37
+ self.identified_patterns: Optional[Dict] = None
38
+
39
+ # Metadata
40
+ self.data_loaded_at: Optional[datetime] = None
41
+ self.data_source: str = "none" # "dexcom_api", "mock", or "none"
42
+
43
+ def load_user_data(self, user_key: str, force_reload: bool = False) -> Dict[str, Any]:
44
+ """
45
+ FIXED: Load glucose data using the ORIGINAL WORKING method
46
+ """
47
+
48
+ # Check if we already have data for this user and it's recent
49
+ if (not force_reload and
50
+ self.current_user and
51
+ self.current_user == DEMO_USERS.get(user_key) and
52
+ self.data_loaded_at and
53
+ (datetime.now() - self.data_loaded_at).seconds < 300): # 5 minutes cache
54
+
55
+ logger.info(f"Using cached data for {user_key}")
56
+ return self._build_success_response()
57
+
58
+ try:
59
+ if user_key not in DEMO_USERS:
60
+ return {
61
+ "success": False,
62
+ "message": f"❌ Invalid user key '{user_key}'. Available: {', '.join(DEMO_USERS.keys())}"
63
+ }
64
+
65
+ logger.info(f"Loading data for user: {user_key}")
66
+
67
+ # Set current user
68
+ self.current_user = DEMO_USERS[user_key]
69
+
70
+ # Call API EXACTLY as it was working before
71
+ try:
72
+ logger.info(f"Attempting Dexcom API authentication for {user_key}")
73
+
74
+ # ORIGINAL WORKING METHOD: Use the simulate_demo_login exactly as before
75
+ access_token = self.dexcom_api.simulate_demo_login(user_key)
76
+ logger.info(f"Dexcom authentication result: {bool(access_token)}")
77
+
78
+ if access_token:
79
+ # ORIGINAL WORKING METHOD: Get data with 14-day range
80
+ end_date = datetime.now()
81
+ start_date = end_date - timedelta(days=14)
82
+
83
+ # Call get_egv_data EXACTLY as it was working before
84
+ self.raw_glucose_data = self.dexcom_api.get_egv_data(
85
+ start_date.isoformat(),
86
+ end_date.isoformat()
87
+ )
88
+
89
+ if self.raw_glucose_data and len(self.raw_glucose_data) > 0:
90
+ self.data_source = "dexcom_api"
91
+ logger.info(f"βœ… Successfully loaded {len(self.raw_glucose_data)} readings from Dexcom API")
92
+ else:
93
+ logger.warning("Dexcom API returned empty data - falling back to mock data")
94
+ raise Exception("Empty data from Dexcom API")
95
+ else:
96
+ logger.warning("Failed to get access token - falling back to mock data")
97
+ raise Exception("Authentication failed")
98
+
99
+ except Exception as api_error:
100
+ logger.warning(f"Dexcom API failed ({str(api_error)}) - using mock data fallback")
101
+ self.raw_glucose_data = self._generate_realistic_mock_data(user_key)
102
+ self.data_source = "mock"
103
+
104
+ # Process the raw data (same processing for everyone)
105
+ self.processed_glucose_data = self.analyzer.process_egv_data(self.raw_glucose_data)
106
+
107
+ if self.processed_glucose_data is None or self.processed_glucose_data.empty:
108
+ return {
109
+ "success": False,
110
+ "message": "❌ Failed to process glucose data"
111
+ }
112
+
113
+ # Calculate statistics (single source of truth)
114
+ self.calculated_stats = self._calculate_unified_stats()
115
+
116
+ # Identify patterns
117
+ self.identified_patterns = self.analyzer.identify_patterns(self.processed_glucose_data)
118
+
119
+ # Mark when data was loaded
120
+ self.data_loaded_at = datetime.now()
121
+
122
+ logger.info(f"Successfully loaded and processed data for {self.current_user.name}")
123
+ logger.info(f"Data source: {self.data_source}, Readings: {len(self.processed_glucose_data)}")
124
+ logger.info(f"TIR: {self.calculated_stats.get('time_in_range_70_180', 0):.1f}%")
125
+
126
+ return self._build_success_response()
127
+
128
+ except Exception as e:
129
+ logger.error(f"Failed to load user data: {e}")
130
+ return {
131
+ "success": False,
132
+ "message": f"❌ Failed to load user data: {str(e)}"
133
+ }
134
+
135
+ def get_stats_for_ui(self) -> Dict[str, Any]:
136
+ """Get statistics formatted for the UI display"""
137
+ if not self.calculated_stats:
138
+ return {}
139
+
140
+ return {
141
+ **self.calculated_stats,
142
+ "data_source": self.data_source,
143
+ "loaded_at": self.data_loaded_at.isoformat() if self.data_loaded_at else None,
144
+ "user_name": self.current_user.name if self.current_user else None
145
+ }
146
+
147
+ def get_context_for_agent(self) -> Dict[str, Any]:
148
+ """Get context formatted for the AI agent"""
149
+ if not self.current_user or not self.calculated_stats:
150
+ return {"error": "No user data loaded"}
151
+
152
+ # Build agent context with the SAME data as UI
153
+ context = {
154
+ "user": {
155
+ "name": self.current_user.name,
156
+ "age": self.current_user.age,
157
+ "diabetes_type": self.current_user.diabetes_type,
158
+ "device_type": self.current_user.device_type,
159
+ "years_with_diabetes": self.current_user.years_with_diabetes,
160
+ "typical_pattern": getattr(self.current_user, 'typical_glucose_pattern', 'normal')
161
+ },
162
+ "statistics": self._safe_convert_for_json(self.calculated_stats),
163
+ "patterns": self._safe_convert_for_json(self.identified_patterns),
164
+ "data_points": len(self.processed_glucose_data) if self.processed_glucose_data is not None else 0,
165
+ "recent_readings": self._get_recent_readings_for_agent(),
166
+ "data_metadata": {
167
+ "source": self.data_source,
168
+ "loaded_at": self.data_loaded_at.isoformat() if self.data_loaded_at else None,
169
+ "data_age_minutes": int((datetime.now() - self.data_loaded_at).total_seconds() / 60) if self.data_loaded_at else None
170
+ }
171
+ }
172
+
173
+ return context
174
+
175
+ def get_chart_data(self) -> Optional[pd.DataFrame]:
176
+ """Get processed data for chart display"""
177
+ return self.processed_glucose_data
178
+
179
+ def _calculate_unified_stats(self) -> Dict[str, Any]:
180
+ """Calculate statistics using a single, consistent method"""
181
+ if self.processed_glucose_data is None or self.processed_glucose_data.empty:
182
+ return {"error": "No data available"}
183
+
184
+ try:
185
+ # Get glucose values
186
+ glucose_values = self.processed_glucose_data['value'].dropna()
187
+
188
+ if len(glucose_values) == 0:
189
+ return {"error": "No valid glucose values"}
190
+
191
+ # Convert to numpy array for consistent calculations
192
+ import numpy as np
193
+ values = np.array(glucose_values.tolist(), dtype=float)
194
+
195
+ # Calculate basic statistics
196
+ avg_glucose = float(np.mean(values))
197
+ min_glucose = float(np.min(values))
198
+ max_glucose = float(np.max(values))
199
+ std_glucose = float(np.std(values))
200
+ total_readings = int(len(values))
201
+
202
+ # Calculate time in ranges - CONSISTENT METHOD
203
+ in_range_mask = (values >= 70) & (values <= 180)
204
+ below_range_mask = values < 70
205
+ above_range_mask = values > 180
206
+
207
+ in_range_count = int(np.sum(in_range_mask))
208
+ below_range_count = int(np.sum(below_range_mask))
209
+ above_range_count = int(np.sum(above_range_mask))
210
+
211
+ # Calculate percentages
212
+ time_in_range = (in_range_count / total_readings) * 100 if total_readings > 0 else 0
213
+ time_below_70 = (below_range_count / total_readings) * 100 if total_readings > 0 else 0
214
+ time_above_180 = (above_range_count / total_readings) * 100 if total_readings > 0 else 0
215
+
216
+ # Calculate additional metrics
217
+ gmi = 3.31 + (0.02392 * avg_glucose) # Glucose Management Indicator
218
+ cv = (std_glucose / avg_glucose) * 100 if avg_glucose > 0 else 0 # Coefficient of Variation
219
+
220
+ stats = {
221
+ "average_glucose": avg_glucose,
222
+ "min_glucose": min_glucose,
223
+ "max_glucose": max_glucose,
224
+ "std_glucose": std_glucose,
225
+ "time_in_range_70_180": time_in_range,
226
+ "time_below_70": time_below_70,
227
+ "time_above_180": time_above_180,
228
+ "total_readings": total_readings,
229
+ "gmi": gmi,
230
+ "cv": cv,
231
+ "in_range_count": in_range_count,
232
+ "below_range_count": below_range_count,
233
+ "above_range_count": above_range_count
234
+ }
235
+
236
+ # Log for debugging
237
+ logger.info(f"Calculated stats - TIR: {time_in_range:.1f}%, Total: {total_readings}, In range: {in_range_count}")
238
+
239
+ return stats
240
+
241
+ except Exception as e:
242
+ logger.error(f"Error calculating unified stats: {e}")
243
+ return {"error": f"Statistics calculation failed: {str(e)}"}
244
+
245
+ def _generate_realistic_mock_data(self, user_key: str) -> list:
246
+ """Generate consistent mock data for demo users"""
247
+ from mistral_chat import GlucoseDataGenerator
248
+
249
+ # Map users to patterns
250
+ pattern_map = {
251
+ "sarah_g7": "normal",
252
+ "marcus_one": "dawn_phenomenon",
253
+ "jennifer_g6": "normal",
254
+ "robert_receiver": "dawn_phenomenon"
255
+ }
256
+
257
+ user_pattern = pattern_map.get(user_key, "normal")
258
+
259
+ # Generate 14 days of data
260
+ mock_data = GlucoseDataGenerator.create_realistic_pattern(days=14, user_type=user_pattern)
261
+
262
+ logger.info(f"Generated {len(mock_data)} mock data points for {user_key} with pattern {user_pattern}")
263
+
264
+ return mock_data
265
+
266
+ def _get_recent_readings_for_agent(self, count: int = 5) -> list:
267
+ """Get recent readings formatted for agent context"""
268
+ if self.processed_glucose_data is None or self.processed_glucose_data.empty:
269
+ return []
270
+
271
+ try:
272
+ recent_df = self.processed_glucose_data.tail(count)
273
+ readings = []
274
+
275
+ for _, row in recent_df.iterrows():
276
+ display_time = row.get('displayTime') or row.get('systemTime')
277
+ glucose_value = row.get('value')
278
+ trend_value = row.get('trend', 'flat')
279
+
280
+ if pd.notna(display_time):
281
+ if isinstance(display_time, str):
282
+ time_str = display_time
283
+ else:
284
+ time_str = pd.to_datetime(display_time).isoformat()
285
+ else:
286
+ time_str = datetime.now().isoformat()
287
+
288
+ if pd.notna(glucose_value):
289
+ glucose_clean = self._safe_convert_for_json(glucose_value)
290
+ else:
291
+ glucose_clean = None
292
+
293
+ trend_clean = str(trend_value) if pd.notna(trend_value) else 'flat'
294
+
295
+ readings.append({
296
+ "time": time_str,
297
+ "glucose": glucose_clean,
298
+ "trend": trend_clean
299
+ })
300
+
301
+ return readings
302
+
303
+ except Exception as e:
304
+ logger.error(f"Error getting recent readings: {e}")
305
+ return []
306
+
307
+ def _safe_convert_for_json(self, obj):
308
+ """Safely convert objects for JSON serialization"""
309
+ import numpy as np
310
+
311
+ if obj is None:
312
+ return None
313
+ elif isinstance(obj, (np.integer, np.int64, np.int32)):
314
+ return int(obj)
315
+ elif isinstance(obj, (np.floating, np.float64, np.float32)):
316
+ if np.isnan(obj):
317
+ return None
318
+ return float(obj)
319
+ elif isinstance(obj, dict):
320
+ return {key: self._safe_convert_for_json(value) for key, value in obj.items()}
321
+ elif isinstance(obj, list):
322
+ return [self._safe_convert_for_json(item) for item in obj]
323
+ elif isinstance(obj, pd.Timestamp):
324
+ return obj.isoformat()
325
+ else:
326
+ return obj
327
+
328
+ def _build_success_response(self) -> Dict[str, Any]:
329
+ """Build a consistent success response"""
330
+ data_points = len(self.processed_glucose_data) if self.processed_glucose_data is not None else 0
331
+ avg_glucose = self.calculated_stats.get('average_glucose', 0)
332
+ time_in_range = self.calculated_stats.get('time_in_range_70_180', 0)
333
+
334
+ return {
335
+ "success": True,
336
+ "message": f"βœ… Successfully loaded data for {self.current_user.name}",
337
+ "user": asdict(self.current_user),
338
+ "data_points": data_points,
339
+ "stats": self.calculated_stats,
340
+ "data_source": self.data_source,
341
+ "summary": f"πŸ“Š {data_points} readings | Avg: {avg_glucose:.1f} mg/dL | TIR: {time_in_range:.1f}% | Source: {self.data_source}"
342
+ }
343
+
344
+ def validate_data_consistency(self) -> Dict[str, Any]:
345
+ """Validate that all components are using consistent data"""
346
+ if not self.calculated_stats:
347
+ return {"valid": False, "message": "No data loaded"}
348
+
349
+ validation = {
350
+ "valid": True,
351
+ "data_source": self.data_source,
352
+ "data_age_minutes": int((datetime.now() - self.data_loaded_at).total_seconds() / 60) if self.data_loaded_at else None,
353
+ "total_readings": self.calculated_stats.get('total_readings', 0),
354
+ "time_in_range": self.calculated_stats.get('time_in_range_70_180', 0),
355
+ "average_glucose": self.calculated_stats.get('average_glucose', 0),
356
+ "user": self.current_user.name if self.current_user else None
357
+ }
358
+
359
+ logger.info(f"Data consistency check: {validation}")
360
+
361
+ return validation
362
+
363
+ # ADDITIONAL: Debug function to test the API connection as it was working before
364
+ def test_original_api_method():
365
+ """Test the API exactly as it was working before unified data manager"""
366
+ from apifunctions import DexcomAPI, DEMO_USERS
367
+
368
+ print("πŸ” Testing API exactly as it was working before...")
369
+
370
+ api = DexcomAPI()
371
+
372
+ # Test with sarah_g7 as it was working before
373
+ user_key = "sarah_g7"
374
+ user = DEMO_USERS[user_key]
375
+
376
+ print(f"Testing with {user.name} ({user.username})")
377
+
378
+ try:
379
+ # Call simulate_demo_login exactly as before
380
+ access_token = api.simulate_demo_login(user_key)
381
+ print(f"βœ… Authentication: {bool(access_token)}")
382
+
383
+ if access_token:
384
+ # Call get_egv_data exactly as before
385
+ end_date = datetime.now()
386
+ start_date = end_date - timedelta(days=14)
387
+
388
+ egv_data = api.get_egv_data(
389
+ start_date.isoformat(),
390
+ end_date.isoformat()
391
+ )
392
+
393
+ print(f"βœ… EGV Data: {len(egv_data)} readings")
394
+
395
+ if egv_data:
396
+ print(f"βœ… SUCCESS! API is working as before")
397
+ sample = egv_data[0] if egv_data else {}
398
+ print(f"Sample reading: {sample}")
399
+ return True
400
+ else:
401
+ print("⚠️ API authenticated but returned no data")
402
+ return False
403
+ else:
404
+ print("❌ Authentication failed")
405
+ return False
406
+
407
+ except Exception as e:
408
+ print(f"❌ Error: {e}")
409
+ return False
410
+
411
+ if __name__ == "__main__":
412
+ # Test the original API method
413
+ test_original_api_method()