Spaces:
Sleeping
Sleeping
Commit
Β·
232e999
1
Parent(s):
471b7a1
Code
Browse files- .env.example +49 -0
- .gitignore +290 -0
- README copy.md +229 -0
- apifunctions.py +499 -0
- config.py +326 -0
- config_management.py +171 -0
- dexcom_real_auth_system.py +501 -0
- hybrid_auth.py +648 -0
- main.py +779 -0
- mistral_chat.py +727 -0
- requirements.txt +7 -0
- 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 |
+
[](https://huggingface.co/spaces/)
|
29 |
+
[](https://gradio.app/)
|
30 |
+
[](https://mistral.ai/)
|
31 |
+
[](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 |
+
[](https://huggingface.co/spaces/)
|
227 |
+
[](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()
|