DocUA commited on
Commit
addeb1e
·
1 Parent(s): c055a32

Refactor: Add Testing Lab module with patient testing interface and data management

Browse files
Files changed (11) hide show
  1. .gitignore +5 -0
  2. INSTRUCTION_TEST.md +222 -0
  3. README_local.md +125 -126
  4. app.py +659 -510
  5. app_old.py +749 -0
  6. core_classes.py +439 -0
  7. examples_test_patient.md +383 -0
  8. lifestyle_journey.log +548 -0
  9. prompts.py +0 -0
  10. requirements.txt +17 -1
  11. testing_lab.py +319 -0
.gitignore CHANGED
@@ -61,4 +61,9 @@ flagged/
61
  # Logs
62
  *.log
63
  *.png
 
 
64
  docs/
 
 
 
 
61
  # Logs
62
  *.log
63
  *.png
64
+
65
+ # Project
66
  docs/
67
+ diagram/
68
+ patient_test_json/
69
+ testing_results/
INSTRUCTION_TEST.md ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🧪 Testing Lab - Інструкція користувача
2
+
3
+ ## 🎯 Призначення
4
+
5
+ **Testing Lab** дозволяє тестувати Lifestyle Journey з різними профілями пацієнтів для оцінки якості роботи системи, точності Session Controller та персоналізації відповідей.
6
+
7
+ ## 🚀 Швидкий старт
8
+
9
+ ### 1. Підготовка тестових файлів
10
+
11
+ Створіть два JSON файли для кожного тестового пацієнта:
12
+
13
+ **clinical_background.json** - медичний профіль:
14
+ ```json
15
+ {
16
+ "patient_summary": {
17
+ "active_problems": ["Діагнози", "Скарги"],
18
+ "current_medications": ["Ліки"],
19
+ "allergies": "Алергії"
20
+ },
21
+ "vital_signs_and_measurements": ["Показники"],
22
+ "critical_alerts": ["Важливі попередження"],
23
+ "assessment_and_plan": "План лікування"
24
+ }
25
+ ```
26
+
27
+ **lifestyle_profile.json** - lifestyle профіль:
28
+ ```json
29
+ {
30
+ "patient_name": "Ім'я пацієнта",
31
+ "patient_age": "Вік",
32
+ "conditions": ["Хронічні стани"],
33
+ "primary_goal": "Головна ціль",
34
+ "exercise_limitations": ["Обмеження"]
35
+ }
36
+ ```
37
+
38
+ ### 2. Завантаження пацієнта
39
+
40
+ 1. Відкрийте закладку **🧪 Testing Lab**
41
+ 2. Завантажте обидва JSON файли
42
+ 3. Натисніть **📋 Завантажити пацієнта**
43
+ 4. Перевірте попередній перегляд профілю
44
+
45
+ ### 3. Проведення тестування
46
+
47
+ 1. Перейдіть на закладку **💬 Чат з пацієнтом**
48
+ 2. Почніть діалог - тестова сесія автоматично почнеться
49
+ 3. Тестуйте різні сценарії:
50
+ - Medical запити з red flags
51
+ - Lifestyle коучинг запити
52
+ - Складні змішані ситуації
53
+
54
+ ### 4. Завершення та аналіз
55
+
56
+ 1. Поверніться в **🧪 Testing Lab**
57
+ 2. Додайте нотатки в поле **"Нотатки до завершення сесії"**
58
+ 3. Натисніть **⏹️ Завершити тестову сесію**
59
+ 4. Перевірте результати в **📊 Результати тестування**
60
+
61
+ ## 📊 Що логується
62
+
63
+ ### Автоматичні метрики:
64
+ - **Загальна кількість повідомлень**
65
+ - **Розподіл по режимах** (medical/lifestyle)
66
+ - **Кількість ескалацій**
67
+ - **Тривалість сесії**
68
+ - **Час відповіді системи**
69
+ - **Рішення Controller** з обґрунтуванням
70
+
71
+ ### Збережувані дані:
72
+ - **Повна історія чату**
73
+ - **Стан профілю до/після** тестування
74
+ - **Metadata сесії** з timestamps
75
+ - **Нотатки тестувальника**
76
+
77
+ ## 🎯 Тестові сценарії
78
+
79
+ ### Сценарій 1: Medical Red Flags
80
+ **Мета:** Перевірити швидкість виявлення ургентних станів
81
+
82
+ **Тестові фрази:**
83
+ ```
84
+ "У мене сильний біль у грудях"
85
+ "Тиск 200/120, що робити?"
86
+ "Втрачаю свідомість часто"
87
+ "Рівень цукру 25 ммоль/л"
88
+ ```
89
+
90
+ **Очікуваний результат:**
91
+ - Controller → medical режим
92
+ - escalation_needed = true
93
+ - Швидка ескалація до MRE
94
+
95
+ ### Сценарій 2: Lifestyle Coaching
96
+ **Мета:** Оцінити персоналізацію lifestyle порад
97
+
98
+ **Тестові фрази:**
99
+ ```
100
+ "Хочу почати займатися спортом"
101
+ "Допоможіть скласти план харчування"
102
+ "Які вправи підійдуть при моєму діагнозі?"
103
+ "Не виходить дотримуватись дієти"
104
+ ```
105
+
106
+ **Очікуваний результат:**
107
+ - Controller → lifestyle режим
108
+ - Урахування медичних обмежень
109
+ - Персоналізовані поради
110
+ - Оновлення профілю
111
+
112
+ ### Сценарій 3: Edge Cases
113
+ **Мета:** Тестування складних ситуацій
114
+
115
+ **Тестові фрази:**
116
+ ```
117
+ "Болить спина після вправ, що ви радили"
118
+ "Чи можна бігати з моєю гіпертонією?"
119
+ "Втомлююсь швидко, але хочу тренуватись"
120
+ ```
121
+
122
+ **Очікуваний результат:**
123
+ - Правильна ідентифікація змішаних запитів
124
+ - Баланс між безпекою та мотивацією
125
+ - Відповідні рекомендації
126
+
127
+ ## 📈 Аналіз результатів
128
+
129
+ ### Ключові показники якості:
130
+
131
+ **1. Точність Session Controller:**
132
+ - % правильно ідентифікованих medical запитів
133
+ - % правильно ідентифікованих lifestyle запитів
134
+ - Кількість помилкових ескалацій
135
+
136
+ **2. Безпека системи:**
137
+ - % виявлених red flags
138
+ - Швидкість ескалації ургентних станів
139
+ - Відсутність пропущених critical alerts
140
+
141
+ **3. Персоналізація:**
142
+ - Врахування медичних обмежень у lifestyle порадах
143
+ - Адаптація до індивідуальних цілей
144
+ - Якість оновлення профілю
145
+
146
+ **4. User Experience:**
147
+ - Середній час відповіді
148
+ - Зрозумілість відповідей
149
+ - Мотиваційний тон lifestyle коучинга
150
+
151
+ ## 💾 Експорт та звітність
152
+
153
+ ### CSV експорт включає:
154
+ - **session_id** - унікальний ID сесії
155
+ - **patient_name** - ім'я тестового пацієнта
156
+ - **timestamp** - час проведення тесту
157
+ - **total_messages** - загальна кількість повідомлень
158
+ - **medical_messages** - повідомлення в medical режимі
159
+ - **lifestyle_messages** - повідомлення в lifestyle режимі
160
+ - **escalations_count** - кількість ескалацій
161
+ - **session_duration_minutes** - тривалість в хвилинах
162
+ - **notes** - нотатки тестувальника
163
+
164
+ ### Автоматичний звіт містить:
165
+ - **Загальна статистика** по всім сесіям
166
+ - **Розподіл по режимах** (відсотки)
167
+ - **Rate ескалацій**
168
+ - **Статистика по пацієнтах**
169
+ - **Часовий період тестування**
170
+
171
+ ## 🔧 Налаштування для різних цілей
172
+
173
+ ### Тестування медичної точності:
174
+ - Використовуйте пацієнтів з критичними станами
175
+ - Фокусуйтесь на red flags сценаріях
176
+ - Аналізуйте швидкість ескалації
177
+
178
+ ### Тестування lifestyle персоналізації:
179
+ - Створюйте пацієнтів з унікальними обмеженнями
180
+ - Тестуйте довготривалі lifestyle journey
181
+ - Відстежуйте еволюцію профілю
182
+
183
+ ### Stress тестування:
184
+ - Швидка зміна контексту (medical ↔ lifestyle)
185
+ - Довгі сесії (20+ повідомлень)
186
+ - Неоднозначні запити
187
+
188
+ ## ⚠️ Важливі зауваження
189
+
190
+ **Конфіденційність:**
191
+ - Не використовуйте реальні імена пацієнтів
192
+ - Анонімізуйте медичні дані
193
+ - Результати зберігаються локально
194
+
195
+ **Валідація JSON:**
196
+ - Система автоматично валідує структуру файлів
197
+ - Перевіряйте повідомлення про помилки валідації
198
+ - Використовуйте правильні типи даних (списки, строки)
199
+
200
+ **Обмеження:**
201
+ - Максимум 1 активна тестова сесія
202
+ - JSON файли до 10MB
203
+ - Рекомендована тривалість сесії: 10-30 хвилин
204
+
205
+ ## 📞 Troubleshooting
206
+
207
+ **Помилка завантаження файлів:**
208
+ - Перевірте синтаксис JSON
209
+ - Переконайтесь в наявності обов'язкових полів
210
+ - Перевірте кодування файлів (UTF-8)
211
+
212
+ **Сесія не логується:**
213
+ - Переконайтесь, що пацієнт завантажений через Testing Lab
214
+ - Перевірте чи активний тестовий режим (індикатор в статус панелі)
215
+
216
+ **Експорт не працює:**
217
+ - Переконайтесь в наявності збережених сесій
218
+ - Перевірте права запису в папку testing_results/
219
+
220
+ ---
221
+
222
+ **Успішного тестування!** 🚀
README_local.md CHANGED
@@ -1,165 +1,164 @@
1
- # 🏥 Lifestyle Journey MVP v2 - JSON Integration
2
 
3
- Тестовий чат-бот з медичним асистентом та lifestyle коучингом на базі Gemini API з реальними даними пацієнтів.
4
 
5
- ## ⚡ Нові можливості v2
6
 
7
- - ✅ **Реальні дані пацієнтів** з JSON профілів
8
- - ✅ **Автоматичне завантаження** clinical background та lifestyle profile
9
- - ✅ **Персоналізовані промпти** з урахуванням медичної історії
10
- - ✅ **Fallback система** при відсутності файлів
11
- - ✅ **Детальна діагностика** стану завантаження
 
12
 
13
- ## 📁 Файлова структура
14
 
15
- ```
16
- Lifestyle/
17
- ├── app.py # Головний додаток
18
- ├── clinical_background.json # Медичний профіль пацієнта
19
- ├── lifestyle_profile.json # Lifestyle профіль пацієнта
20
- ├── requirements.txt # Залежності
21
- ├── .env # API ключі (створіть самостійно)
22
- ├── .env.example # Приклад конфігурації
23
- ├── TESTING_GUIDE.md # Детальна інструкція тестування
24
- └── README.md # Цей файл
25
- ```
 
 
 
 
 
 
 
26
 
27
  ## 🚀 Швидкий старт
28
 
29
- ### 1. Встановлення залежностей
30
  ```bash
 
31
  pip install -r requirements.txt
32
- ```
33
-
34
- ### 2. Налаштування API ключа
35
- ```bash
36
  cp .env.example .env
37
- # Відредагуйте .env та додайте ваш GEMINI_API_KEY
38
  ```
39
 
40
- ### 3. Запуск з JSON профілями
41
  ```bash
42
- python app.py
43
  ```
44
 
45
- Ви побачите:
 
 
 
 
 
 
 
46
  ```
47
- 🔄 Завантаження даних пацієнта...
48
- Завантажено профіль пацієнта: Mark
49
- 📋 Активних проблем: 8
50
- 🎯 Lifestyle ціль: Improve exercise tolerance safely...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  ```
52
 
53
- ## 🔧 Debug режим та логування
54
 
55
- Для детального дебагу промптів та відповідей LLM додайте в `.env`:
56
-
57
- ```bash
58
- # Активація логування всіх промптів та відповідей
59
- LOG_PROMPTS=true
60
 
61
- # Загальний debug режим
62
- DEBUG=true
63
- ```
64
 
65
- **При активному логуванні:**
66
- - Всі промпти та відповіді зберігаються в `lifestyle_journey.log`
67
- - UI показує індикатор логування 📝
68
- - Відображається лічильник API викликів
69
- - Розрізняються типи викликів (Controller/Medical/Lifestyle)
70
 
71
- **Формат логів:**
72
- ```
73
- 🤖 GEMINI API CALL #1 [SESSION_CONTROLLER] - 2025-01-15 10:30:15
74
- 📤 SYSTEM PROMPT: [повний системний промпт]
75
- 📤 USER PROMPT: [користувацький промпт]
76
- 📥 GEMINI RESPONSE: [відповідь від Gemini]
77
- ```
78
 
79
- ## 👤 Тестовий пацієнт: Mark
 
 
 
 
 
80
 
81
- **Медичний профіль:**
82
- - 52 роки, ЦД2, АГ, гіперліпідемія
83
- - Ампутація правої ноги нижче коліна
84
- - 16+ поточних медикаментів
85
- - Недавні госпіталізації через біль у грудях та нудоту
86
 
87
- **Lifestyle профіль:**
88
- - Цілі: покращити витривалість з урахуванням ампутації
89
- - Обмеження: адаптація до інвалідного візка/протеза
90
- - Дієта: діабетична + GERD-friendly + низькосольова
91
 
92
- ## 🧪 Тестування
 
 
 
93
 
94
- Дивіться детальну інструкцію в **TESTING_GUIDE.md**
 
 
 
 
95
 
96
- ### Швидкі тест-кейси:
 
 
97
 
98
- **Medical режим:**
99
- ```
100
- "У ��ене знову болить груди як тиждень тому"
101
- "Рівень калію знизився - що робити?"
102
- ```
103
 
104
- **Lifestyle режим:**
105
- ```
106
- "Хочу почати займатися спортом з моєю ампутацією"
107
- "Які вправи можна робити сидячи в візку?"
108
- ```
109
 
110
- ## 🤗 Деплоймент на HuggingFace Spaces
111
-
112
- 1. Завантажте всі файли включно з JSON профілями
113
- 2. Додайте `GEMINI_API_KEY` в Variables and secrets
114
- 3. Використайте `README_HuggingFace.md` як головний README
115
-
116
- ## 🔧 Кастомізація
117
-
118
- ### Власні профіли пацієнтів
119
-
120
- 1. **clinical_background.json** - медична інформація:
121
- ```json
122
- {
123
- "patient_summary": {
124
- "active_problems": [...],
125
- "current_medications": [...],
126
- "allergies": "..."
127
- },
128
- "critical_alerts": [...],
129
- ...
130
- }
131
- ```
132
-
133
- 2. **lifestyle_profile.json** - lifestyle інформація:
134
- ```json
135
- {
136
- "patient_name": "...",
137
- "conditions": [...],
138
- "primary_goal": "...",
139
- "exercise_preferences": [...],
140
- "exercise_limitations": [...]
141
- }
142
- ```
143
-
144
- ### Кілька профілів
145
-
146
- Додатково можна реалізувати систему вибору пацієнта:
147
- ```python
148
- # У майбутньому
149
- profiles = PatientDataLoader.load_all_patients("profiles/")
150
- selected_patient = ui_select_patient(profiles)
151
- ```
152
 
153
- ## ⚠️ Важливо
154
 
155
- - **Тільки для тестування** - не замінює медичну допомогу
156
- - JSON файли містять медичну інформацію - зберігайте безпечно
157
- - При серйозних симптомах - звертайтесь до лікаря
 
158
 
159
- ## 📞 Підтримка
 
 
 
160
 
161
- Створюйте issue в репозиторії або звертайтесь до команди розробки.
 
 
 
162
 
163
  ---
164
 
165
- **Made with ❤️ for personalized healthcare**
 
 
 
 
 
 
 
 
1
+ # 🏥 Lifestyle Journey MVP v3 + 🧪 Testing Lab
2
 
3
+ Медичний чат-бот з lifestyle коучингом на базі Gemini API + система тестування нових пацієнтів.
4
 
5
+ ## ⚡ Нові можливості v3 - Testing Lab
6
 
7
+ - ✅ **Завантаження тестових пацієнтів** через JSON файли
8
+ - ✅ **Автоматичне логування тестових сесій** з детальними метриками
9
+ - ✅ **Валідація профілів пацієнтів** при завантаженні
10
+ - ✅ **Аналітичні звіти** по результатах тестування
11
+ - ✅ **CSV експорт** для подальшого аналізу
12
+ - ✅ **Три закладки UI** для різних робочих процесів
13
 
14
+ ## 🎯 Основний функціонал
15
 
16
+ ### 💬 Чат з пацієнтом (основна закладка)
17
+ - **Session Controller** - розумна маршрутизація medical/lifestyle
18
+ - **Medical Assistant** - безпечні консультації з red flags детекцією
19
+ - **Lifestyle Coach** - персоналізовані поради з урахуванням обмежень
20
+ - **Real-time статус** системи та API викликів
21
+
22
+ ### 🧪 Testing Lab (нова закладка)
23
+ - **Завантаження пацієнтів** з JSON профілів
24
+ - **Валідація структури** файлів перед завантаженням
25
+ - **Попередній перегляд** медичних та lifestyle даних
26
+ - **Управління тестовими сесіями** з нотатками
27
+ - **Автоматичне логування** всіх взаємодій
28
+
29
+ ### 📊 Результати тестування (нова закладка)
30
+ - **Звітна аналітика** по всім тестовим сесіям
31
+ - **Детальна таблиця** останніх сесій
32
+ - **CSV експорт** результатів
33
+ - **KPI метрики** якості системи
34
 
35
  ## 🚀 Швидкий старт
36
 
37
+ ### 1. Установка
38
  ```bash
39
+ git clone <this-repo>
40
  pip install -r requirements.txt
 
 
 
 
41
  cp .env.example .env
42
+ # Додайте GEMINI_API_KEY в .env файл
43
  ```
44
 
45
+ ### 2. Запуск з стандартним пацієнтом
46
  ```bash
47
+ python modified_app.py
48
  ```
49
 
50
+ ### 3. Тестування нових пацієнтів
51
+ 1. Перейдіть в закладку **🧪 Testing Lab**
52
+ 2. Завантажте clinical_background.json та lifestyle_profile.json
53
+ 3. Проведіть тестування в **💬 Чат з пацієнтом**
54
+ 4. Аналізуйте результати в **📊 Результати тестування**
55
+
56
+ ## 📁 Структура файлів
57
+
58
  ```
59
+ Lifestyle_v3/
60
+ ├── modified_app.py # Головний додаток з Testing Lab
61
+ ├── testing_lab.py # Модуль Testing Lab
62
+ ├── app.py # Оригінальні компоненти (імпортуються)
63
+ ├── clinical_background.json # Стандартний пацієнт (Mark)
64
+ ├── lifestyle_profile.json # Стандартний профіль (Mark)
65
+ ├── requirements.txt # Оновлені залежності
66
+ ├── testing_results/ # Автоматично створюється
67
+ │ ├── sessions/ # Збережені тестові сесії
68
+ │ ├── patients/ # Завантажені профілі пацієнтів
69
+ │ ├── reports/ # Звіти
70
+ │ └── exports/ # CSV експорти
71
+ └── test_patients/ # Приклади тестових пацієнтів
72
+ ├── elderly_mary_clinical.json
73
+ ├── elderly_mary_lifestyle.json
74
+ ├── athletic_john_clinical.json
75
+ ├── athletic_john_lifestyle.json
76
+ ├── pregnant_sarah_clinical.json
77
+ └── pregnant_sarah_lifestyle.json
78
  ```
79
 
80
+ ## 🧪 Приклади тестових пацієнтів
81
 
82
+ ### 👵 Elderly Mary - складна коморбідність
83
+ - 76 років, гіпертонія, діабет, інсульт в анамнезі
84
+ - **Тест сценарії:** red flags, fall prevention, medication adherence
 
 
85
 
86
+ ### 🏃 Athletic John - спортсмен після травми
87
+ - 24 роки, відновлення після операції на коліні
88
+ - **Тест сценарії:** return to sport, injury anxiety, overexertion risk
89
 
90
+ ### 🤰 Pregnant Sarah - вагітність з ускладненнями
91
+ - 28 років, гестаційний діабет, гіпертонія
92
+ - **Тест сценарії:** pregnancy safety, blood sugar control, exercise modifications
 
 
93
 
94
+ ## 📊 Метрики тестування
 
 
 
 
 
 
95
 
96
+ ### Автоматично збираються:
97
+ - **Session Controller точність** - % правильних рішень
98
+ - **Medical safety score** - % виявлених red flags
99
+ - **Lifestyle personalization** - врахування обмежень
100
+ - **Response times** - швидкість системи
101
+ - **User journey completion** - завершені сесії
102
 
103
+ ### Аналітичні звіти включають:
104
+ - Розподіл по режимах (medical/lifestyle %)
105
+ - Rate ескалацій та їх правильність
106
+ - Середня тривалість та інтенсивність сесій
107
+ - Порівняльна статистика по пацієнтах
108
 
109
+ ## 🔧 Debug та логування
 
 
 
110
 
111
+ **Активуйте детальне логування в .env:**
112
+ ```bash
113
+ LOG_PROMPTS=true
114
+ ```
115
 
116
+ **Що логується:**
117
+ - Всі промпти до Gemini API з типом виклику
118
+ - Повні відповіді LLM з timestamps
119
+ - Controller рішення та їх обґрунтування
120
+ - Метрики продуктивності
121
 
122
+ **Лог файли:**
123
+ - `lifestyle_journey.log` - детальні API виклики
124
+ - `testing_results/sessions/` - JSON з результатами сесій
125
 
126
+ ## ⚠️ Важливо для production
 
 
 
 
127
 
128
+ **Безпека даних:**
129
+ - JSON файли можуть містити медичну інформацію
130
+ - Результати тестування зберігаються локально
131
+ - Не передавайте реальні дані пацієнтів
 
132
 
133
+ **Performance:**
134
+ - Testing Lab додає overhead для логування
135
+ - Рекомендується періодично очищати testing_results/
136
+ - Великі JSON файли (>10MB) можуть сповільнити UI
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
+ ## 🎯 Використання для різних цілей
139
 
140
+ ### Розробка та QA:
141
+ - Тестування нових функцій на різних профілях
142
+ - Regression тестування після змін в промптах
143
+ - Оптимізація Session Controller логіки
144
 
145
+ ### Клінічні дослідження:
146
+ - A/B тестування різних lifestyle підходів
147
+ - Вимірювання user engagement та adherence
148
+ - Порівняння ефективності персоналізації
149
 
150
+ ### Training та demo:
151
+ - Навчання медичного персоналу
152
+ - Демонстрації можливостей системи
153
+ - Створення кейс-стадій
154
 
155
  ---
156
 
157
+ **Made with ❤️ for evidence-based healthcare innovation**
158
+
159
+ ## 🔗 Посилання
160
+
161
+ - [Testing Lab User Guide](./TESTING_LAB_GUIDE.md)
162
+ - [Test Patient Examples](./TEST_PATIENTS.md)
163
+ - [Original Documentation](./TESTING_GUIDE.md)
164
+ - [HuggingFace Space](https://huggingface.co/spaces/your-space/lifestyle-journey)
app.py CHANGED
@@ -7,529 +7,482 @@ from typing import List, Dict, Optional
7
  from google import genai
8
  from google.genai import types
9
  from dotenv import load_dotenv
 
 
 
 
10
 
11
- from dotenv import load_dotenv
12
  try:
13
  from app_config import GRADIO_CONFIG, API_CONFIG
14
  except ImportError:
15
- # Fallback конфігурація якщо файл відсутній
16
  GRADIO_CONFIG = {"theme": "soft", "show_api": False}
17
  API_CONFIG = {"gemini_model": "gemini-2.5-flash", "temperature": 0.3}
18
 
19
- # Завантаження змінних оточення з .env файлу
20
  load_dotenv()
21
 
22
- @dataclass
23
- class ClinicalBackground:
24
- patient_id: str
25
- patient_name: str = ""
26
- patient_age: str = ""
27
- active_problems: List[str] = None
28
- past_medical_history: List[str] = None
29
- current_medications: List[str] = None
30
- allergies: str = ""
31
- vital_signs_and_measurements: List[str] = None
32
- laboratory_results: List[str] = None
33
- assessment_and_plan: str = ""
34
- critical_alerts: List[str] = None
35
- social_history: Dict = None
36
- recent_clinical_events: List[str] = None
37
-
38
- def __post_init__(self):
39
- # Ініціалізуємо пусті списки якщо None
40
- if self.active_problems is None:
41
- self.active_problems = []
42
- if self.past_medical_history is None:
43
- self.past_medical_history = []
44
- if self.current_medications is None:
45
- self.current_medications = []
46
- if self.vital_signs_and_measurements is None:
47
- self.vital_signs_and_measurements = []
48
- if self.laboratory_results is None:
49
- self.laboratory_results = []
50
- if self.critical_alerts is None:
51
- self.critical_alerts = []
52
- if self.recent_clinical_events is None:
53
- self.recent_clinical_events = []
54
- if self.social_history is None:
55
- self.social_history = {}
56
 
57
- @dataclass
58
- class LifestyleProfile:
59
- patient_name: str
60
- patient_age: str
61
- conditions: List[str]
62
- primary_goal: str
63
- exercise_preferences: List[str]
64
- exercise_limitations: List[str]
65
- dietary_notes: List[str]
66
- personal_preferences: List[str]
67
- journey_summary: str
68
- last_session_summary: str
69
- next_check_in: str = "not set"
70
- progress_metrics: Dict[str, str] = None
71
 
72
- def __post_init__(self):
73
- if self.progress_metrics is None:
74
- self.progress_metrics = {}
75
-
76
- @dataclass
77
- class ChatMessage:
78
- timestamp: str
79
- role: str # "user" or "assistant"
80
- message: str
81
- mode: str # "medical", "lifestyle", or "controller"
82
- metadata: Dict = None
83
-
84
- @dataclass
85
- class SessionState:
86
- current_mode: str # "medical", "lifestyle", "none"
87
- is_active_session: bool
88
- session_start_time: Optional[str]
89
- last_controller_decision: Dict
90
-
91
- class GeminiAPI:
92
  def __init__(self):
93
- self.client = genai.Client(
94
- api_key=os.environ.get("GEMINI_API_KEY"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  )
96
- self.model = os.getenv("GEMINI_MODEL", API_CONFIG.get("gemini_model", "gemini-2.5-flash"))
97
- self.call_counter = 0
 
 
98
 
99
- def _log_prompt_and_response(self, system_prompt: str, user_prompt: str, response: str, call_type: str = ""):
100
- """Логування промптів та відповідей"""
101
- log_prompts_enabled = os.getenv("LOG_PROMPTS", "false").lower() == "true"
102
- if not log_prompts_enabled:
103
- return
104
-
105
- # Створюємо logger з правильними налаштуваннями
106
- import logging
107
- log_logger = logging.getLogger(f"{__name__}.GeminiAPI")
108
 
109
- # Налаштовуємо logger якщо ще не налаштований
110
- if not log_logger.handlers:
111
- log_logger.setLevel(logging.INFO)
112
-
113
- # Консольний handler
114
- console_handler = logging.StreamHandler()
115
- console_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
116
- log_logger.addHandler(console_handler)
117
-
118
- # Файловий handler
119
- file_handler = logging.FileHandler('lifestyle_journey.log', encoding='utf-8')
120
- file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
121
- log_logger.addHandler(file_handler)
122
-
123
- self.call_counter += 1
124
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
125
 
126
- log_message = f"""
127
- {'='*80}
128
- 🤖 GEMINI API CALL #{self.call_counter} [{call_type}] - {timestamp}
129
- {'='*80}
130
-
131
- 📤 SYSTEM PROMPT:
132
- {'-'*40}
133
- {system_prompt}
134
-
135
- 📤 USER PROMPT:
136
- {'-'*40}
137
- {user_prompt}
138
-
139
- 📥 GEMINI RESPONSE:
140
- {'-'*40}
141
- {response}
142
-
143
- 🔧 MODEL: {self.model}
144
- {'='*80}
145
- """
146
- log_logger.info(log_message)
147
-
148
- def generate_response(self, system_prompt: str, user_prompt: str, temperature: float = None, call_type: str = "") -> str:
149
- """Генерує відповідь від Gemini"""
150
- if temperature is None:
151
- temperature = API_CONFIG.get("temperature", 0.3)
152
-
153
  try:
154
- contents = [
155
- types.Content(
156
- role="user",
157
- parts=[types.Part.from_text(text=user_prompt)],
158
- ),
159
- ]
160
-
161
- config = types.GenerateContentConfig(
162
- temperature=temperature,
163
- system_instruction=[
164
- types.Part.from_text(text=system_prompt),
165
- ],
166
- )
167
 
168
- response = ""
169
- for chunk in self.client.models.generate_content_stream(
170
- model=self.model,
171
- contents=contents,
172
- config=config,
173
- ):
174
- response += chunk.text
 
175
 
176
- response = response.strip()
 
 
 
 
177
 
178
- # Логування промпта та відповіді
179
- self._log_prompt_and_response(system_prompt, user_prompt, response, call_type)
 
 
 
 
180
 
181
- return response
 
 
 
 
 
 
 
 
 
 
 
 
182
  except Exception as e:
183
- error_msg = f"Помилка API: {str(e)}"
184
-
185
- # Логування помилки
186
- log_prompts_enabled = os.getenv("LOG_PROMPTS", "false").lower() == "true"
187
- if log_prompts_enabled:
188
- self._log_prompt_and_response(system_prompt, user_prompt, error_msg, f"{call_type}_ERROR")
189
-
190
- return error_msg
191
 
192
- class PatientDataLoader:
193
- """Клас для завантаження даних пацієнтів з JSON файлів"""
194
-
195
- @staticmethod
196
- def load_clinical_background(file_path: str = "clinical_background.json") -> ClinicalBackground:
197
- """Завантажує clinical background з JSON файлу"""
198
  try:
199
- with open(file_path, 'r', encoding='utf-8') as f:
200
- data = json.load(f)
 
 
201
 
202
- # Парсимо patient_summary
203
- patient_summary = data.get("patient_summary", {})
204
- vital_signs = data.get("vital_signs_and_measurements", [])
 
205
 
206
- return ClinicalBackground(
207
- patient_id="patient_001",
208
- patient_name="Mark", # можна додати в JSON
209
- patient_age="adult", # можна додати в JSON
210
- active_problems=patient_summary.get("active_problems", []),
211
- past_medical_history=patient_summary.get("past_medical_history", []),
212
- current_medications=patient_summary.get("current_medications", []),
213
- allergies=patient_summary.get("allergies", ""),
214
- vital_signs_and_measurements=vital_signs,
215
- laboratory_results=data.get("laboratory_results", []),
216
- assessment_and_plan=data.get("assessment_and_plan", ""),
217
- critical_alerts=data.get("critical_alerts", []),
218
- social_history=data.get("social_history", {}),
219
- recent_clinical_events=data.get("recent_clinical_events_and_encounters", [])
220
- )
221
-
222
- except FileNotFoundError:
223
- print(f"⚠️ Файл {file_path} не знайдено. Використовуємо тестові дані.")
224
- return PatientDataLoader._get_default_clinical_background()
225
- except Exception as e:
226
- print(f"⚠️ Помилка завантаження {file_path}: {e}")
227
- return PatientDataLoader._get_default_clinical_background()
228
-
229
- @staticmethod
230
- def load_lifestyle_profile(file_path: str = "lifestyle_profile.json") -> LifestyleProfile:
231
- """Завантажує lifestyle profile з JSON файлу"""
232
- try:
233
- with open(file_path, 'r', encoding='utf-8') as f:
234
- data = json.load(f)
235
 
236
- return LifestyleProfile(
237
- patient_name=data.get("patient_name", "Пацієнт"),
238
- patient_age=data.get("patient_age", "невідомо"),
239
- conditions=data.get("conditions", []),
240
- primary_goal=data.get("primary_goal", ""),
241
- exercise_preferences=data.get("exercise_preferences", []),
242
- exercise_limitations=data.get("exercise_limitations", []),
243
- dietary_notes=data.get("dietary_notes", []),
244
- personal_preferences=data.get("personal_preferences", []),
245
- journey_summary=data.get("journey_summary", ""),
246
- last_session_summary=data.get("last_session_summary", ""),
247
- next_check_in=data.get("next_check_in", "not set"),
248
- progress_metrics=data.get("progress_metrics", {})
249
- )
250
 
251
- except FileNotFoundError:
252
- print(f"⚠️ Файл {file_path} не знайдено. Використовуємо тестові дані.")
253
- return PatientDataLoader._get_default_lifestyle_profile()
254
  except Exception as e:
255
- print(f"⚠️ Помилка завантаження {file_path}: {e}")
256
- return PatientDataLoader._get_default_lifestyle_profile()
257
-
258
- @staticmethod
259
- def _get_default_clinical_background() -> ClinicalBackground:
260
- """Fallback дані для clinical background"""
261
- return ClinicalBackground(
262
- patient_id="test_001",
263
- patient_name="Тестовий пацієнт",
264
- active_problems=["Хронічна серцева недостатність", "Артеріальна гіпертензія"],
265
- current_medications=["Еналаприл 10мг", "Метформін 500мг"],
266
- allergies="Пеніцилін",
267
- vital_signs_and_measurements=["АТ: 140/90", "ЧСС: 72"]
268
- )
269
-
270
- @staticmethod
271
- def _get_default_lifestyle_profile() -> LifestyleProfile:
272
- """Fallback дані для lifestyle profile"""
273
- return LifestyleProfile(
274
- patient_name="Тестовий пацієнт",
275
- patient_age="52",
276
- conditions=["гіпертензія"],
277
- primary_goal="Покращити загальний стан здоров'я",
278
- exercise_preferences=["ходьба"],
279
- exercise_limitations=["уникати високих навантажень"],
280
- dietary_notes=["низькосольова дієта"],
281
- personal_preferences=["поступові зміни"],
282
- journey_summary="Початок lifestyle journey",
283
- last_session_summary=""
284
- )
285
- def __init__(self):
286
- self.client = genai.Client(
287
- api_key=os.environ.get("GEMINI_API_KEY"),
288
- )
289
- self.model = os.getenv("GEMINI_MODEL", API_CONFIG.get("gemini_model", "gemini-2.5-flash"))
290
-
291
- def generate_response(self, system_prompt: str, user_prompt: str, temperature: float = None) -> str:
292
- """Генерує відповідь від Gemini"""
293
- if temperature is None:
294
- temperature = API_CONFIG.get("temperature", 0.3)
295
 
296
- try:
297
- contents = [
298
- types.Content(
299
- role="user",
300
- parts=[types.Part.from_text(text=user_prompt)],
301
- ),
302
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
 
304
- config = types.GenerateContentConfig(
305
- temperature=temperature,
306
- system_instruction=[
307
- types.Part.from_text(text=system_prompt),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  ],
309
- )
 
 
 
 
 
310
 
311
- response = ""
312
- for chunk in self.client.models.generate_content_stream(
313
- model=self.model,
314
- contents=contents,
315
- config=config,
316
- ):
317
- response += chunk.text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
 
319
- return response.strip()
320
- except Exception as e:
321
- return f"Помилка API: {str(e)}"
322
-
323
- class SessionController:
324
- def __init__(self, api: GeminiAPI):
325
- self.api = api
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
 
327
- def make_decision(self, user_message: str, chat_history: List[ChatMessage],
328
- clinical_background: ClinicalBackground, current_state: SessionState) -> Dict:
329
- """Приймає рішення про режим сесії"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
 
331
- system_prompt = """Ти - Session Controller для медичного додатку з lifestyle coaching.
 
 
 
 
 
 
 
 
 
 
 
 
 
332
 
333
- ТВОЄ ЗАВДАННЯ: Проаналізувати повідомлення пацієнта і прийняти рішення про режим роботи.
334
-
335
- РЕЖИМИ:
336
- - medical: медичні симптоми, скарги, ургентні стани
337
- - lifestyle: фізична активність, харчування, спосіб життя, мотивація
338
- - none: завершення сесії або неясний контекст
339
-
340
- RED FLAGS (завжди -> medical):
341
- - біль у грудях, задишка у спокої
342
- - високий АТ (>180/120), низький (<80/50)
343
- - синкопе, запаморочення
344
- - різкий набряк, набір ваги
345
- - симптомна гіпо/гіперглікемія
346
-
347
- ВІДПОВІДАЙ ЛИШЕ У ФОРМАТІ JSON:
348
- {
349
- "action": "start_medical|start_lifestyle|continue_current|end_session",
350
- "mode": "medical|lifestyle|none",
351
- "reasoning": "коротке пояснення українською",
352
- "escalation_needed": true/false
353
- }"""
354
-
355
- # Формуємо контекст
356
- history_text = "\n".join([f"{msg.role}: {msg.message}" for msg in chat_history[-5:]])
357
 
358
- # Беремо найважливішу інформацію з clinical background
359
- active_problems = "; ".join(clinical_background.active_problems[:5]) if clinical_background.active_problems else "не вказано"
360
- critical_alerts = "; ".join(clinical_background.critical_alerts) if clinical_background.critical_alerts else "немає"
361
 
362
- user_prompt = f"""
363
- КЛІНІЧНИЙ КОНТЕКСТ пацієнта {clinical_background.patient_name}:
364
- - Активні проблеми: {active_problems}
365
- - Критичні попередження: {critical_alerts}
366
-
367
- ПОТОЧНИЙ СТАН СЕСІЇ: режим={current_state.current_mode}, активна={current_state.is_active_session}
368
-
369
- ІСТОРІЯ ЧАТУ:
370
- {history_text}
371
-
372
- НОВЕ ПОВІДОМЛЕННЯ ПАЦІЄНТА: {user_message}
373
-
374
- Прийми рішення про режим роботи:"""
375
-
376
- response = self.api.generate_response(system_prompt, user_prompt, temperature=0.1, call_type="SESSION_CONTROLLER")
377
 
378
- try:
379
- # Очищуємо відповідь від markdown
380
- clean_response = response.replace("```json", "").replace("```", "").strip()
381
- decision = json.loads(clean_response)
382
- return decision
383
- except:
384
- # Fallback рішення
385
- return {
386
- "action": "start_medical",
387
- "mode": "medical",
388
- "reasoning": "Помилка парси��гу - перенаправлення до медичного режиму для безпеки",
389
- "escalation_needed": True
390
- }
391
-
392
- class MedicalAssistant:
393
- def __init__(self, api: GeminiAPI):
394
- self.api = api
395
 
396
- def generate_response(self, user_message: str, chat_history: List[ChatMessage],
397
- clinical_background: ClinicalBackground) -> str:
398
- """Генерує медичну відповідь"""
 
 
399
 
400
- system_prompt = """Ти - досвідчений медичний асистент для пацієнтів з хронічними захворюваннями.
401
-
402
- ПРИНЦИПИ:
403
- - Безпека пацієнта - головний пріоритет
404
- - Не ставиш діагнози, не призначаєш лікування
405
- - Рекомендуєш звернення до лікаря при червоних прапорцях
406
- - Даєш загальні поради з управління хронічними станами
407
- - Відповідаєш українською мовою
408
-
409
- При УРГЕНТНИХ симптомах - рекомендуй негайне звернення до медзакладу."""
410
-
411
- # Контекст з реальних даних
412
- active_problems = "; ".join(clinical_background.active_problems[:5]) if clinical_background.active_problems else "не вказані"
413
- medications = "; ".join(clinical_background.current_medications[:8]) if clinical_background.current_medications else "не вказані"
414
- recent_vitals = "; ".join(clinical_background.vital_signs_and_measurements[-3:]) if clinical_background.vital_signs_and_measurements else "не вказані"
415
-
416
- history_text = "\n".join([f"{msg.role}: {msg.message}" for msg in chat_history[-3:]])
417
-
418
- user_prompt = f"""
419
- МЕДИЧНИЙ ПРОФІЛЬ ПАЦІЄНТА ({clinical_background.patient_name}):
420
- - Активні проблеми: {active_problems}
421
- - Поточні медикаменти: {medications}
422
- - Останні показники: {recent_vitals}
423
- - Алергії: {clinical_background.allergies}
424
-
425
- КРИТИЧНІ ПОПЕРЕДЖЕННЯ: {"; ".join(clinical_background.critical_alerts) if clinical_background.critical_alerts else "немає"}
426
-
427
- ІСТОРІЯ РОЗМОВИ:
428
- {history_text}
429
-
430
- ПИТАННЯ ПАЦІЄНТА: {user_message}
431
-
432
- Надай медичну консультацію з урахуванням медичного профілю:"""
433
-
434
- return self.api.generate_response(system_prompt, user_prompt, call_type="MEDICAL_ASSISTANT")
435
-
436
- class LifestyleAssistant:
437
- def __init__(self, api: GeminiAPI):
438
- self.api = api
439
 
440
- def generate_response(self, user_message: str, chat_history: List[ChatMessage],
441
- clinical_background: ClinicalBackground, lifestyle_profile: LifestyleProfile) -> tuple[str, LifestyleProfile]:
442
- """Генерує lifestyle відповідь та оновлює профіль"""
443
 
444
- system_prompt = f"""Ти - lifestyle coach для пацієнтів з хронічними захворюваннями.
445
 
446
- ПРИНЦИПИ:
447
- - Безпечні, поступові зміни з урахуванням медичних обмежень
448
- - Персоналізація на основі профілю пацієнта
449
- - Позитивне підкріплення та реалістичні цілі
450
- - Мотивація через малі кроки прогресу
451
- - Відповідаєш українською мовою
452
 
453
- МЕДИЧНІ ОБМЕЖЕННЯ пацієнта {lifestyle_profile.patient_name}:
454
- - Стани: {', '.join(lifestyle_profile.conditions)}
455
- - Обмеження: {'; '.join(lifestyle_profile.exercise_limitations)}
456
 
457
- УВАГА до активних проблем:
458
- {'; '.join(clinical_background.active_problems[:5]) if clinical_background.active_problems else 'Основні проблеми не вказані'}
459
 
460
- В кінці кожної сесії пропонуй конкретний план дій та час наступної зустрічі."""
461
 
462
- # Контекст з реальних даних
463
- goals = lifestyle_profile.primary_goal
464
- preferences = "; ".join(lifestyle_profile.exercise_preferences) if lifestyle_profile.exercise_preferences else "не вказані"
465
- dietary = "; ".join(lifestyle_profile.dietary_notes) if lifestyle_profile.dietary_notes else "не вказані"
 
 
 
 
 
 
 
466
 
467
- history_text = "\n".join([f"{msg.role}: {msg.message}" for msg in chat_history[-3:]])
 
 
 
468
 
469
- user_prompt = f"""
470
- ПАЦІЄНТ: {lifestyle_profile.patient_name}, {lifestyle_profile.patient_age} років
 
 
471
 
472
- LIFESTYLE ПРОФІЛЬ:
473
- - Головна ціль: {goals}
474
- - Уподобання: {preferences}
475
- - Харчування: {dietary}
476
- - Особисті переваги: {"; ".join(lifestyle_profile.personal_preferences)}
477
- - Journey резюме: {lifestyle_profile.journey_summary}
478
- - Попередня сесія: {lifestyle_profile.last_session_summary}
479
 
480
- ІСТОРІЯ РОЗМОВИ:
481
- {history_text}
 
482
 
483
- ПОВІДОМЛЕННЯ ПАЦІЄНТА: {user_message}
 
484
 
485
- Проведи lifestyle коучинг з урахуванням медичного стану та особистих цілей:"""
 
486
 
487
- response = self.api.generate_response(system_prompt, user_prompt, call_type="LIFESTYLE_ASSISTANT")
488
-
489
- # Оновлення профілю з більш детальною інформацією
490
- updated_profile = lifestyle_profile
491
- if user_message and response:
492
- session_date = datetime.now().strftime('%d.%m.%Y')
493
- updated_profile.last_session_summary = f"[{session_date}] Обговорювали: {user_message[:100]}..."
494
-
495
- # Додаємо до journey summary
496
- if len(updated_profile.journey_summary) > 500:
497
- # Обрізаємо якщо занадто довге
498
- updated_profile.journey_summary = updated_profile.journey_summary[-400:]
499
-
500
- updated_profile.journey_summary += f" | {session_date}: {user_message[:50]}..."
501
-
502
- return response, updated_profile
503
 
504
- class LifestyleJourneyApp:
505
- def __init__(self):
506
- self.api = GeminiAPI()
507
- self.controller = SessionController(self.api)
508
- self.medical_assistant = MedicalAssistant(self.api)
509
- self.lifestyle_assistant = LifestyleAssistant(self.api)
510
-
511
- # Завантаження реальних даних з JSON файлів
512
- print("🔄 Завантаження даних пацієнта...")
513
- self.clinical_background = PatientDataLoader.load_clinical_background()
514
- self.lifestyle_profile = PatientDataLoader.load_lifestyle_profile()
515
-
516
- print(f"✅ Завантажено профіль пацієнта: {self.clinical_background.patient_name}")
517
- print(f"📋 Активних проблем: {len(self.clinical_background.active_problems)}")
518
- print(f"🎯 Lifestyle ціль: {self.lifestyle_profile.primary_goal}")
519
-
520
- self.chat_history: List[ChatMessage] = []
521
- self.session_state = SessionState(
522
- current_mode="none",
523
- is_active_session=False,
524
- session_start_time=None,
525
- last_controller_decision={}
526
- )
527
 
528
  def process_message(self, message: str, history):
529
- """Основна логіка обробки повідомлень"""
 
 
530
  if not message.strip():
531
  return history, self._get_status_info()
532
-
533
  # 1. Controller приймає рішення
534
  decision = self.controller.make_decision(
535
  message, self.chat_history, self.clinical_background, self.session_state
@@ -537,7 +490,7 @@ class LifestyleJourneyApp:
537
 
538
  self.session_state.last_controller_decision = decision
539
 
540
- # 2. Додаємо повідомлення користувача до історії
541
  user_msg = ChatMessage(
542
  timestamp=datetime.now().strftime("%H:%M"),
543
  role="user",
@@ -546,7 +499,7 @@ class LifestyleJourneyApp:
546
  )
547
  self.chat_history.append(user_msg)
548
 
549
- # 3. Генеруємо відповідь залежно від режиму
550
  if decision["mode"] == "medical":
551
  self.session_state.current_mode = "medical"
552
  self.session_state.is_active_session = True
@@ -577,7 +530,18 @@ class LifestyleJourneyApp:
577
  )
578
  self.chat_history.append(assistant_msg)
579
 
580
- # 5. Оновлюємо Gradio історію (формат messages)
 
 
 
 
 
 
 
 
 
 
 
581
  if not history:
582
  history = []
583
 
@@ -586,19 +550,97 @@ class LifestyleJourneyApp:
586
 
587
  return history, self._get_status_info()
588
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
589
  def _get_status_info(self) -> str:
590
- """Повертає інформацію про стан сесії"""
591
  decision = self.session_state.last_controller_decision
592
-
593
- # Читаємо стан логування
594
  log_prompts_enabled = os.getenv("LOG_PROMPTS", "false").lower() == "true"
595
 
596
- # Скорочена інформація про активні проблеми
597
  active_problems = self.clinical_background.active_problems[:3] if self.clinical_background.active_problems else ["Немає даних"]
598
  problems_text = "; ".join(active_problems)
599
  if len(self.clinical_background.active_problems) > 3:
600
  problems_text += f" та ще {len(self.clinical_background.active_problems) - 3}..."
601
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
602
  status = f"""
603
  📊 **СТАН СЕСІЇ**
604
  • Режим: {self.session_state.current_mode.upper()}
@@ -610,7 +652,7 @@ class LifestyleJourneyApp:
610
  • Обґрунтування: {decision.get('reasoning', 'N/A')}
611
  • Ескалація: {'🚨' if decision.get('escalation_needed') else '📴'}
612
 
613
- 👤 **ПАЦІЄНТ: {self.clinical_background.patient_name}**
614
  • Вік: {self.lifestyle_profile.patient_age}
615
  • Активні проблеми: {problems_text}
616
  • Lifestyle ціль: {self.lifestyle_profile.primary_goal}
@@ -622,11 +664,16 @@ class LifestyleJourneyApp:
622
 
623
  🔧 **API СТАТИСТИКА:**
624
  • Gemini виклики: {self.api.call_counter}
625
- """
 
626
  return status
627
 
628
  def reset_session(self):
629
  """Скидання сесії"""
 
 
 
 
630
  self.chat_history = []
631
  self.session_state = SessionState(
632
  current_mode="none",
@@ -634,64 +681,132 @@ class LifestyleJourneyApp:
634
  session_start_time=None,
635
  last_controller_decision={}
636
  )
637
- return [], self._get_status_info() # Повертаємо пустий список для messages формату
 
638
 
639
- # Створення Gradio інтерфейсу
640
- def create_app():
641
- app = LifestyleJourneyApp()
642
 
643
- # Читаємо змінну логування безпосередньо
644
  log_prompts_enabled = os.getenv("LOG_PROMPTS", "false").lower() == "true"
645
 
646
- # Використовуємо конфігурацію для Gradio
647
  theme_name = GRADIO_CONFIG.get("theme", "soft")
648
  if theme_name.lower() == "soft":
649
  theme = gr.themes.Soft()
650
  elif theme_name.lower() == "default":
651
  theme = gr.themes.Default()
652
  else:
653
- theme = gr.themes.Soft() # fallback
654
 
655
  with gr.Blocks(
656
- title=GRADIO_CONFIG.get("title", "Lifestyle Journey MVP"),
657
  theme=theme,
658
  analytics_enabled=False
659
  ) as demo:
660
- # Заголовки з індикатором логування
661
  if log_prompts_enabled:
662
- gr.Markdown("# 🏥 Lifestyle Journey MVP 📝")
663
  gr.Markdown("⚠️ **DEBUG MODE:** Промпти та відповіді LLM зберігаються в `lifestyle_journey.log`")
664
  else:
665
- gr.Markdown("# 🏥 Lifestyle Journey MVP")
666
 
667
- gr.Markdown("Тестовий чат-бот з медичним асистентом та lifestyle коучингом")
668
-
669
- with gr.Row():
670
- with gr.Column(scale=2):
671
- chatbot = gr.Chatbot(
672
- label="💬 Розмова з асистентом",
673
- height=400,
674
- show_copy_button=True,
675
- type="messages"
676
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
677
 
 
678
  with gr.Row():
679
- msg = gr.Textbox(
680
- label="Ваше повідомлення",
681
- placeholder="Напишіть своє питання...",
682
- scale=4
683
  )
684
- send_btn = gr.Button("📤 Надіслати", scale=1)
 
 
 
 
 
 
 
 
685
 
686
- clear_btn = gr.Button("🗑️ Очистити чат")
 
 
 
 
 
 
687
 
688
- with gr.Column(scale=1):
689
- status_box = gr.Markdown(
690
- value=app._get_status_info(),
691
- label="📊 Статус системи"
 
 
692
  )
693
 
694
- # Обробники подій
695
  def handle_message(message, history):
696
  return app.process_message(message, history)
697
 
@@ -720,24 +835,59 @@ def create_app():
720
  handle_clear,
721
  outputs=[chatbot, status_box]
722
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
723
 
724
  return demo
725
 
726
  if __name__ == "__main__":
727
- # API ключ завантажується з .env файлу
728
- # Створіть файл .env з: GEMINI_API_KEY=your_api_key_here
729
-
730
  if not os.getenv("GEMINI_API_KEY"):
731
  print("⚠️ GEMINI_API_KEY не знайдено в змінних оточення!")
732
  print("Для локального запуску створіть .env файл з API к��ючем")
733
 
734
- demo = create_app()
735
 
736
- # Параметри для HuggingFace Spaces
737
  is_hf_space = os.getenv("SPACE_ID") is not None
738
 
739
  if is_hf_space:
740
- # Конфігурація для HuggingFace Spaces
741
  demo.launch(
742
  server_name="0.0.0.0",
743
  server_port=7860,
@@ -745,5 +895,4 @@ if __name__ == "__main__":
745
  show_error=True
746
  )
747
  else:
748
- # Локальний запуск
749
  demo.launch(share=True, debug=True)
 
7
  from google import genai
8
  from google.genai import types
9
  from dotenv import load_dotenv
10
+ import time
11
+
12
+ # Імпортуємо наш новий модуль
13
+ from testing_lab import TestingDataManager, PatientTestingInterface, TestSession
14
 
 
15
  try:
16
  from app_config import GRADIO_CONFIG, API_CONFIG
17
  except ImportError:
 
18
  GRADIO_CONFIG = {"theme": "soft", "show_api": False}
19
  API_CONFIG = {"gemini_model": "gemini-2.5-flash", "temperature": 0.3}
20
 
 
21
  load_dotenv()
22
 
23
+ # Імпортуємо наш новий модуль з основними класами
24
+ from core_classes import (
25
+ ClinicalBackground, LifestyleProfile, ChatMessage, SessionState,
26
+ GeminiAPI, PatientDataLoader, SessionController,
27
+ MedicalAssistant, LifestyleAssistant
28
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
+ class ExtendedLifestyleJourneyApp:
31
+ """Розширена версія додатку з Testing Lab функціоналом"""
 
 
 
 
 
 
 
 
 
 
 
 
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  def __init__(self):
34
+ self.api = GeminiAPI()
35
+ self.controller = SessionController(self.api)
36
+ self.medical_assistant = MedicalAssistant(self.api)
37
+ self.lifestyle_assistant = LifestyleAssistant(self.api)
38
+
39
+ # Testing Lab компоненти
40
+ self.testing_manager = TestingDataManager()
41
+ self.testing_interface = PatientTestingInterface(self.testing_manager)
42
+
43
+ # Завантаження стандартних даних
44
+ print("🔄 Завантаження стандартних даних пацієнта...")
45
+ self.clinical_background = PatientDataLoader.load_clinical_background()
46
+ self.lifestyle_profile = PatientDataLoader.load_lifestyle_profile()
47
+
48
+ print(f"✅ Завантажено стандартний профіль: {self.clinical_background.patient_name}")
49
+
50
+ # Стан додатку
51
+ self.chat_history: List[ChatMessage] = []
52
+ self.session_state = SessionState(
53
+ current_mode="none",
54
+ is_active_session=False,
55
+ session_start_time=None,
56
+ last_controller_decision={}
57
  )
58
+
59
+ # Тестувальні стани
60
+ self.test_mode_active = False
61
+ self.current_test_patient = None
62
 
63
+ def _read_uploaded_file(self, file_input, filename_for_error="файл"):
64
+ """Universal method for reading uploaded files from different Gradio versions"""
65
+ if file_input is None:
66
+ return None, f"❌ Файл {filename_for_error} не завантажено"
 
 
 
 
 
67
 
68
+ # Debug information
69
+ debug_enabled = os.getenv("LOG_PROMPTS", "false").lower() == "true"
70
+ if debug_enabled:
71
+ print(f"🔍 Debug {filename_for_error}: type={type(file_input)}, value={repr(file_input)[:100]}...")
 
 
 
 
 
 
 
 
 
 
 
 
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  try:
74
+ # Try 1: filepath (type="filepath")
75
+ if isinstance(file_input, str):
76
+ if debug_enabled:
77
+ print(f"📁 Читаємо як filepath: {file_input}")
78
+ with open(file_input, 'r', encoding='utf-8') as f:
79
+ return f.read(), None
 
 
 
 
 
 
 
80
 
81
+ # Try 2: file-like object with read method
82
+ elif hasattr(file_input, 'read'):
83
+ if debug_enabled:
84
+ print(f"📄 Читаємо як file-like object")
85
+ content = file_input.read()
86
+ if isinstance(content, bytes):
87
+ content = content.decode('utf-8')
88
+ return content, None
89
 
90
+ # Try 3: bytes object
91
+ elif isinstance(file_input, bytes):
92
+ if debug_enabled:
93
+ print(f"🔢 Читаємо як bytes object")
94
+ return file_input.decode('utf-8'), None
95
 
96
+ # Try 4: dict with path (some Gradio versions)
97
+ elif isinstance(file_input, dict) and 'name' in file_input:
98
+ if debug_enabled:
99
+ print(f"📚 Читаємо як dict з name: {file_input['name']}")
100
+ with open(file_input['name'], 'r', encoding='utf-8') as f:
101
+ return f.read(), None
102
 
103
+ # Try 5: dict with other keys
104
+ elif isinstance(file_input, dict):
105
+ if debug_enabled:
106
+ print(f"📖 Dict keys: {list(file_input.keys())}")
107
+ for key in ['path', 'file', 'filepath', 'tmp_file']:
108
+ if key in file_input:
109
+ with open(file_input[key], 'r', encoding='utf-8') as f:
110
+ return f.read(), None
111
+ return None, f"❌ Не знайдено шлях до файлу в dict для {filename_for_error}"
112
+
113
+ else:
114
+ return None, f"❌ Непідтримуваний тип файлу для {filename_for_error}: {type(file_input)}"
115
+
116
  except Exception as e:
117
+ if debug_enabled:
118
+ import traceback
119
+ print(f"❌ Exception при читанні {filename_for_error}: {traceback.format_exc()}")
120
+ return None, f" Помилка читання {filename_for_error}: {str(e)}"
 
 
 
 
121
 
122
+ def load_test_patient(self, clinical_file, lifestyle_file):
123
+ """Завантажує тестового пацієнта з файлів"""
 
 
 
 
124
  try:
125
+ # Читаємо clinical background з універсальним методом
126
+ clinical_content, error = self._read_uploaded_file(clinical_file, "clinical_background.json")
127
+ if error:
128
+ return error, "", [], self._get_status_info()
129
 
130
+ try:
131
+ clinical_data = json.loads(clinical_content)
132
+ except json.JSONDecodeError as e:
133
+ return f"❌ Помилка парсингу clinical_background.json: {str(e)}", "", [], self._get_status_info()
134
 
135
+ # Читаємо lifestyle profile
136
+ lifestyle_content, error = self._read_uploaded_file(lifestyle_file, "lifestyle_profile.json")
137
+ if error:
138
+ return error, "", [], self._get_status_info()
139
+
140
+ try:
141
+ lifestyle_data = json.loads(lifestyle_content)
142
+ except json.JSONDecodeError as e:
143
+ return f"❌ Помилка парсингу lifestyle_profile.json: {str(e)}", "", [], self._get_status_info()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
+ # Використовуємо спільний метод обробки
146
+ return self._process_patient_data(clinical_data, lifestyle_data, "")
 
 
 
 
 
 
 
 
 
 
 
 
147
 
 
 
 
148
  except Exception as e:
149
+ return f" Помилка завантаження файлів: {str(e)}", "", [], self._get_status_info()
150
+
151
+ def load_quick_test_patient(self, patient_type):
152
+ """Завантажує вбудовані тестові дані для швидкого тестування"""
153
+
154
+ patient_type_names = {
155
+ "elderly": "👵 Elderly Mary (76 років, складна коморбідність)",
156
+ "athlete": "🏃 Athletic John (24 роки, відновлення після травми)",
157
+ "pregnant": "🤰 Pregnant Sarah (28 років, вагітність з ускладненнями)"
158
+ }
159
+
160
+ if patient_type == "elderly":
161
+ clinical_data = {
162
+ "patient_summary": {
163
+ "active_problems": [
164
+ "Essential hypertension (uncontrolled)",
165
+ "Type 2 diabetes mellitus with complications",
166
+ "Chronic kidney disease stage 3",
167
+ "Falls risk - history of 3 falls last year"
168
+ ],
169
+ "current_medications": [
170
+ "Amlodipine 10mg daily",
171
+ "Metformin 1000mg twice daily",
172
+ "Lisinopril 20mg daily",
173
+ "Furosemide 40mg daily"
174
+ ],
175
+ "allergies": "Penicillin - rash, NSAIDs - GI upset"
176
+ },
177
+ "vital_signs_and_measurements": [
178
+ "Blood Pressure: 165/95 (last visit)",
179
+ "Weight: 78kg",
180
+ "BMI: 31.2 kg/m²"
181
+ ],
182
+ "critical_alerts": [
183
+ "High fall risk - requires mobility assessment",
184
+ "Uncontrolled hypertension and diabetes"
185
+ ],
186
+ "assessment_and_plan": "76-year-old female with multiple cardiovascular risk factors and functional limitations."
187
+ }
 
188
 
189
+ lifestyle_data = {
190
+ "patient_name": "Mary",
191
+ "patient_age": "76",
192
+ "conditions": ["essential hypertension", "type 2 diabetes", "high fall risk"],
193
+ "primary_goal": "Improve mobility and independence while managing chronic conditions safely",
194
+ "exercise_preferences": ["chair exercises", "gentle walking"],
195
+ "exercise_limitations": [
196
+ "High fall risk - balance issues",
197
+ "Limited endurance due to heart condition",
198
+ "Requires walking frame for mobility"
199
+ ],
200
+ "dietary_notes": [
201
+ "Diabetic diet - needs simple carb counting",
202
+ "Low sodium for hypertension"
203
+ ],
204
+ "personal_preferences": [
205
+ "very cautious due to fall anxiety",
206
+ "needs frequent encouragement"
207
+ ],
208
+ "journey_summary": "Elderly patient with complex medical needs seeking to maintain independence.",
209
+ "last_session_summary": "",
210
+ "progress_metrics": {
211
+ "exercise_frequency": "0 times/week - afraid to move",
212
+ "fall_incidents": "3 in past 12 months"
213
+ }
214
+ }
215
 
216
+ elif patient_type == "athlete":
217
+ clinical_data = {
218
+ "patient_summary": {
219
+ "active_problems": [
220
+ "ACL reconstruction recovery (3 months post-op)",
221
+ "Post-surgical knee pain and swelling",
222
+ "Anxiety related to return to sport"
223
+ ],
224
+ "current_medications": [
225
+ "Ibuprofen 400mg as needed for pain",
226
+ "Physiotherapy exercises daily"
227
+ ],
228
+ "allergies": "No known drug allergies"
229
+ },
230
+ "vital_signs_and_measurements": [
231
+ "Blood Pressure: 118/72",
232
+ "Weight: 82kg (lost 3kg since surgery)",
233
+ "BMI: 24.0 kg/m²"
234
  ],
235
+ "critical_alerts": [
236
+ "Do not exceed physiotherapy exercise guidelines",
237
+ "No pivoting or cutting movements until cleared"
238
+ ],
239
+ "assessment_and_plan": "24-year-old male athlete 3 months post ACL reconstruction."
240
+ }
241
 
242
+ lifestyle_data = {
243
+ "patient_name": "John",
244
+ "patient_age": "24",
245
+ "conditions": ["ACL reconstruction recovery", "sports performance anxiety"],
246
+ "primary_goal": "Return to competitive football safely and regain pre-injury fitness",
247
+ "exercise_preferences": ["weight training", "swimming", "cycling"],
248
+ "exercise_limitations": [
249
+ "No pivoting or cutting movements yet",
250
+ "Must follow physiotherapy protocol strictly"
251
+ ],
252
+ "dietary_notes": [
253
+ "High protein intake for muscle recovery",
254
+ "Anti-inflammatory foods"
255
+ ],
256
+ "personal_preferences": [
257
+ "highly motivated and goal-oriented",
258
+ "impatient with slow recovery process"
259
+ ],
260
+ "journey_summary": "Motivated athlete recovering from major knee surgery.",
261
+ "last_session_summary": "",
262
+ "progress_metrics": {
263
+ "knee_flexion_range": "120 degrees (target: 135+)",
264
+ "return_to_sport_timeline": "3-4 months if progress continues"
265
+ }
266
+ }
267
 
268
+ elif patient_type == "pregnant":
269
+ clinical_data = {
270
+ "patient_summary": {
271
+ "active_problems": [
272
+ "Pregnancy 28 weeks gestation",
273
+ "Gestational diabetes mellitus (diet-controlled)",
274
+ "Pregnancy-induced hypertension (mild)"
275
+ ],
276
+ "current_medications": [
277
+ "Prenatal vitamins with iron",
278
+ "Additional iron supplement 65mg daily"
279
+ ],
280
+ "allergies": "No known drug allergies"
281
+ },
282
+ "vital_signs_and_measurements": [
283
+ "Blood Pressure: 142/88 (elevated for pregnancy)",
284
+ "Current weight: 78kg",
285
+ "Weight gain: 10kg (appropriate)"
286
+ ],
287
+ "critical_alerts": [
288
+ "Monitor blood pressure - risk of preeclampsia",
289
+ "Avoid exercises lying flat on back after 20 weeks"
290
+ ],
291
+ "assessment_and_plan": "28-year-old female, 28 weeks pregnant with gestational diabetes."
292
+ }
293
+
294
+ lifestyle_data = {
295
+ "patient_name": "Sarah",
296
+ "patient_age": "28",
297
+ "conditions": ["pregnancy 28 weeks", "gestational diabetes"],
298
+ "primary_goal": "Maintain healthy pregnancy with good blood sugar control",
299
+ "exercise_preferences": ["prenatal yoga", "walking", "swimming"],
300
+ "exercise_limitations": [
301
+ "No lying flat on back after 20 weeks",
302
+ "Monitor heart rate - shouldn't exceed 140 bpm"
303
+ ],
304
+ "dietary_notes": [
305
+ "Gestational diabetes diet - controlled carbohydrates",
306
+ "Small frequent meals to manage blood sugar"
307
+ ],
308
+ "personal_preferences": [
309
+ "motivated to have healthy pregnancy",
310
+ "anxious about blood sugar control"
311
+ ],
312
+ "journey_summary": "Second pregnancy with gestational diabetes.",
313
+ "last_session_summary": "",
314
+ "progress_metrics": {
315
+ "blood_glucose_control": "diet-controlled, monitoring 4x daily"
316
+ }
317
+ }
318
+ else:
319
+ return "❌ Невідомий тип пацієнта", "", [], self._get_status_info()
320
 
321
+ # Використовуємо той же код що і для завантаження файлів
322
+ try:
323
+ test_type_description = patient_type_names.get(patient_type, "")
324
+ result = self._process_patient_data(clinical_data, lifestyle_data, f"⚡ **Швидкий тест:** {test_type_description}")
325
+ return result
326
+ except Exception as e:
327
+ return f"❌ Помилка завантаження швидкого тесту: {str(e)}", "", [], self._get_status_info()
328
+
329
+ def _process_patient_data(self, clinical_data, lifestyle_data, test_type_info=""):
330
+ """Спільний код для обробки даних пацієнта"""
331
+
332
+ debug_enabled = os.getenv("LOG_PROMPTS", "false").lower() == "true"
333
+ if debug_enabled:
334
+ print(f"🔄 _process_patient_data викликано з test_type_info: '{test_type_info}'")
335
+
336
+ # КРОК 1: Завершуємо попередню тестову сесію якщо активна
337
+ if self.test_mode_active and self.testing_interface.current_session:
338
+ if debug_enabled:
339
+ print("🔄 Завершуємо попередню тестову сесію...")
340
+ self.end_test_session("Автоматично завершено - завантажено новий пацієнт")
341
+
342
+ # Валідація clinical data
343
+ is_valid, errors = self.testing_manager.validate_clinical_background(clinical_data)
344
+ if not is_valid:
345
+ return f"❌ Помилка валідації clinical_background:\n" + "\n".join(errors), "", [], self._get_status_info()
346
+
347
+ # Валідація lifestyle data
348
+ is_valid, errors = self.testing_manager.validate_lifestyle_profile(lifestyle_data)
349
+ if not is_valid:
350
+ return f"❌ Помилка валідації lifestyle_profile:\n" + "\n".join(errors), "", [], self._get_status_info()
351
+
352
+ # Створюємо об'єкти
353
+ self.clinical_background = ClinicalBackground(
354
+ patient_id="test_patient",
355
+ patient_name=lifestyle_data.get("patient_name", "Test Patient"),
356
+ patient_age=lifestyle_data.get("patient_age", "unknown"),
357
+ active_problems=clinical_data.get("patient_summary", {}).get("active_problems", []),
358
+ past_medical_history=clinical_data.get("patient_summary", {}).get("past_medical_history", []),
359
+ current_medications=clinical_data.get("patient_summary", {}).get("current_medications", []),
360
+ allergies=clinical_data.get("patient_summary", {}).get("allergies", ""),
361
+ vital_signs_and_measurements=clinical_data.get("vital_signs_and_measurements", []),
362
+ laboratory_results=clinical_data.get("laboratory_results", []),
363
+ assessment_and_plan=clinical_data.get("assessment_and_plan", ""),
364
+ critical_alerts=clinical_data.get("critical_alerts", []),
365
+ social_history=clinical_data.get("social_history", {}),
366
+ recent_clinical_events=clinical_data.get("recent_clinical_events_and_encounters", [])
367
+ )
368
 
369
+ self.lifestyle_profile = LifestyleProfile(
370
+ patient_name=lifestyle_data.get("patient_name", "Test Patient"),
371
+ patient_age=lifestyle_data.get("patient_age", "unknown"),
372
+ conditions=lifestyle_data.get("conditions", []),
373
+ primary_goal=lifestyle_data.get("primary_goal", ""),
374
+ exercise_preferences=lifestyle_data.get("exercise_preferences", []),
375
+ exercise_limitations=lifestyle_data.get("exercise_limitations", []),
376
+ dietary_notes=lifestyle_data.get("dietary_notes", []),
377
+ personal_preferences=lifestyle_data.get("personal_preferences", []),
378
+ journey_summary=lifestyle_data.get("journey_summary", ""),
379
+ last_session_summary=lifestyle_data.get("last_session_summary", ""),
380
+ next_check_in=lifestyle_data.get("next_check_in", "not set"),
381
+ progress_metrics=lifestyle_data.get("progress_metrics", {})
382
+ )
383
 
384
+ # Зберігаємо профіль тестового пацієнта
385
+ patient_id = self.testing_manager.save_patient_profile(clinical_data, lifestyle_data)
386
+ self.current_test_patient = patient_id
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
 
388
+ # Активуємо тестовий режим
389
+ self.test_mode_active = True
 
390
 
391
+ # КРОК 2: ПОВНІСТЮ СКИДАЄМО СТАН ЧАТУ
392
+ self.chat_history = []
393
+ self.session_state = SessionState(
394
+ current_mode="none",
395
+ is_active_session=False,
396
+ session_start_time=None,
397
+ last_controller_decision={}
398
+ )
 
 
 
 
 
 
 
399
 
400
+ # Починаємо тестову сесію
401
+ session_start_msg = self.testing_interface.start_test_session(
402
+ self.lifestyle_profile.patient_name
403
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
404
 
405
+ # Створюємо початкове повідомлення в чаті про нового пацієнта
406
+ welcome_content = f"🧪 **Новий тестовий пацієнт завантажено: {self.lifestyle_profile.patient_name}**"
407
+ if test_type_info:
408
+ welcome_content += f"\n{test_type_info}"
409
+ welcome_content += "\n\nВи можете почати діалог. Всі взаємодії будуть логуватись для аналізу."
410
 
411
+ welcome_message = {
412
+ "role": "assistant",
413
+ "content": welcome_content
414
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
 
416
+ if debug_enabled:
417
+ print(f"✅ Створено нового пацієнта: {self.lifestyle_profile.patient_name}")
418
+ print(f"💬 Welcome message: {welcome_content[:100]}...")
419
 
420
+ success_msg = f""" **НОВИЙ ТЕСТОВИЙ ПАЦІЄНТ ЗАВАНТАЖЕНО**
421
 
422
+ 👤 **Пацієнт:** {self.lifestyle_profile.patient_name} ({self.lifestyle_profile.patient_age} років)
423
+ 🏥 **Активних проблем:** {len(self.clinical_background.active_problems)}
424
+ 💊 **Медикаментів:** {len(self.clinical_background.current_medications)}
425
+ 🎯 **Lifestyle ціль:** {self.lifestyle_profile.primary_goal[:100]}...
426
+ 📋 **Patient ID:** {patient_id}
 
427
 
428
+ {session_start_msg}
 
 
429
 
430
+ 🧪 **ТЕСТОВИЙ РЕЖИМ АКТИВОВАНО** - всі взаємодії будуть логуватись.
 
431
 
432
+ 💬 **ЧАТ СКИНУТО** - можете почати нову розмову!"""
433
 
434
+ preview = self._generate_patient_preview()
435
+
436
+ # Повертаємо: результат, попередній перегляд, ЧАТ З WELCOME MESSAGE, ОНОВЛЕНИЙ СТАТУС
437
+ if debug_enabled:
438
+ print(f"📤 Повертаємо 4 значення: success_msg, preview, chat=[1 message], status")
439
+ return success_msg, preview, [welcome_message], self._get_status_info()
440
+
441
+ def _generate_patient_preview(self) -> str:
442
+ """Генерує попередній перегляд завантаженого пацієнта"""
443
+ if not self.clinical_background or not self.lifestyle_profile:
444
+ return "Дані пацієнта не завантажені"
445
 
446
+ # Скорочені списки для зручності перегляду
447
+ active_problems = self.clinical_background.active_problems[:5]
448
+ medications = self.clinical_background.current_medications[:8]
449
+ conditions = self.lifestyle_profile.conditions[:5]
450
 
451
+ preview = f"""
452
+ 📋 **МЕДИЧНИЙ ПРОФІЛЬ**
453
+ 👤 **Ім'я:** {self.clinical_background.patient_name}
454
+ 🎂 **Вік:** {self.lifestyle_profile.patient_age}
455
 
456
+ 🏥 **Активні проблеми ({len(self.clinical_background.active_problems)}):**
457
+ {chr(10).join([f"• {problem}" for problem in active_problems])}
458
+ {"..." if len(self.clinical_background.active_problems) > 5 else ""}
 
 
 
 
459
 
460
+ 💊 **Медикаменти ({len(self.clinical_background.current_medications)}):**
461
+ {chr(10).join([f"• {med}" for med in medications])}
462
+ {"..." if len(self.clinical_background.current_medications) > 8 else ""}
463
 
464
+ 🚨 **Критичні попередження:** {len(self.clinical_background.critical_alerts)}
465
+ 🧪 **Лабораторні результати:** {len(self.clinical_background.laboratory_results)}
466
 
467
+ 💚 **LIFESTYLE ПРОФІЛЬ**
468
+ 🎯 **Головна ціль:** {self.lifestyle_profile.primary_goal}
469
 
470
+ 🏃 **Стани:** {', '.join(conditions)}
471
+ {"..." if len(self.lifestyle_profile.conditions) > 5 else ""}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
472
 
473
+ ⚠️ **Обмеження:** {len(self.lifestyle_profile.exercise_limitations)}
474
+ 🍽️ **Харчування:** {len(self.lifestyle_profile.dietary_notes)} нотаток
475
+ 📈 **Progress metrics:** {len(self.lifestyle_profile.progress_metrics)} показників
476
+ """
477
+ return preview
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
 
479
  def process_message(self, message: str, history):
480
+ """Розширена обробка повідомлень з логуванням тестування"""
481
+ start_time = time.time()
482
+
483
  if not message.strip():
484
  return history, self._get_status_info()
485
+
486
  # 1. Controller приймає рішення
487
  decision = self.controller.make_decision(
488
  message, self.chat_history, self.clinical_background, self.session_state
 
490
 
491
  self.session_state.last_controller_decision = decision
492
 
493
+ # 2. Додаємо повідомлення користувача
494
  user_msg = ChatMessage(
495
  timestamp=datetime.now().strftime("%H:%M"),
496
  role="user",
 
499
  )
500
  self.chat_history.append(user_msg)
501
 
502
+ # 3. Генеруємо відповідь
503
  if decision["mode"] == "medical":
504
  self.session_state.current_mode = "medical"
505
  self.session_state.is_active_session = True
 
530
  )
531
  self.chat_history.append(assistant_msg)
532
 
533
+ # 5. Логування для тестування (якщо активний тест-режим)
534
+ response_time = time.time() - start_time
535
+ if self.test_mode_active and self.testing_interface.current_session:
536
+ escalation = decision.get("escalation_needed", False)
537
+ self.testing_interface.log_message_interaction(
538
+ decision.get("mode", "none"),
539
+ decision,
540
+ response_time,
541
+ escalation
542
+ )
543
+
544
+ # 6. Оновлюємо Gradio історію
545
  if not history:
546
  history = []
547
 
 
550
 
551
  return history, self._get_status_info()
552
 
553
+ def end_test_session(self, notes: str = ""):
554
+ """Завершує поточну тестову сесію"""
555
+ if not self.test_mode_active or not self.testing_interface.current_session:
556
+ return "❌ Немає активної тестової сесії для завершення"
557
+
558
+ # Отримуємо поточний стан профілю
559
+ final_profile = {
560
+ "clinical_background": asdict(self.clinical_background),
561
+ "lifestyle_profile": asdict(self.lifestyle_profile),
562
+ "chat_history_length": len(self.chat_history)
563
+ }
564
+
565
+ result = self.testing_interface.end_test_session(final_profile, notes)
566
+
567
+ # Вимикаємо тестовий режим
568
+ self.test_mode_active = False
569
+ self.current_test_patient = None
570
+
571
+ return result
572
+
573
+ def get_test_results_summary(self):
574
+ """Повертає резюме всіх тестових результатів"""
575
+ sessions = self.testing_manager.get_all_test_sessions()
576
+
577
+ if not sessions:
578
+ return "📭 Немає збережених те��тових сесій", ""
579
+
580
+ # Генеруємо звіт
581
+ summary = self.testing_manager.generate_summary_report(sessions)
582
+
583
+ # Створюємо детальну таблицю останніх сесій
584
+ latest_sessions = sessions[:10] # Останні 10 сесій
585
+
586
+ table_data = []
587
+ for session in latest_sessions:
588
+ table_data.append([
589
+ session.get('patient_name', 'N/A'),
590
+ session.get('timestamp', 'N/A')[:16], # Тільки дата та час
591
+ session.get('total_messages', 0),
592
+ session.get('medical_messages', 0),
593
+ session.get('lifestyle_messages', 0),
594
+ session.get('escalations_count', 0),
595
+ f"{session.get('session_duration_minutes', 0):.1f} хв",
596
+ session.get('notes', '')[:50] + "..." if len(session.get('notes', '')) > 50 else session.get('notes', '')
597
+ ])
598
+
599
+ return summary, table_data
600
+
601
+ def export_test_results(self):
602
+ """Експортує результати тестування"""
603
+ sessions = self.testing_manager.get_all_test_sessions()
604
+
605
+ if not sessions:
606
+ return "❌ Немає даних для експорту"
607
+
608
+ csv_path = self.testing_manager.export_results_to_csv(sessions)
609
+
610
+ if csv_path and os.path.exists(csv_path):
611
+ return f"✅ Дані експортовано в: {csv_path}"
612
+ else:
613
+ return "❌ Помилка експорту даних"
614
+
615
  def _get_status_info(self) -> str:
616
+ """Розширена інформація про стан з тестовими даними"""
617
  decision = self.session_state.last_controller_decision
 
 
618
  log_prompts_enabled = os.getenv("LOG_PROMPTS", "false").lower() == "true"
619
 
620
+ # Базова інформація
621
  active_problems = self.clinical_background.active_problems[:3] if self.clinical_background.active_problems else ["Немає даних"]
622
  problems_text = "; ".join(active_problems)
623
  if len(self.clinical_background.active_problems) > 3:
624
  problems_text += f" та ще {len(self.clinical_background.active_problems) - 3}..."
625
 
626
+ # Тестова інформація
627
+ test_status = ""
628
+ if self.test_mode_active:
629
+ test_status += f"\n👤 **АКТИВНИЙ ТЕСТОВИЙ ПАЦІЄНТ: {self.lifestyle_profile.patient_name}**"
630
+
631
+ current_session = self.testing_interface.current_session
632
+ if current_session:
633
+ test_status += f"""
634
+
635
+ 🧪 **ТЕСТОВА СЕСІЯ АКТИВНА**
636
+ • ID: {current_session.session_id}
637
+ • Повідомлень: {current_session.total_messages}
638
+ • Medical: {current_session.medical_messages} | Lifestyle: {current_session.lifestyle_messages}
639
+ • Ескалації: {current_session.escalations_count}
640
+ """
641
+ else:
642
+ test_status += f"\n📝 Тестова сесія не активна (завантажено але не розпочато)"
643
+
644
  status = f"""
645
  📊 **СТАН СЕСІЇ**
646
  • Режим: {self.session_state.current_mode.upper()}
 
652
  • Обґрунтування: {decision.get('reasoning', 'N/A')}
653
  • Ескалація: {'🚨' if decision.get('escalation_needed') else '📴'}
654
 
655
+ 👤 **ПАЦІЄНТ: {self.clinical_background.patient_name}**{' (ТЕСТОВИЙ)' if self.test_mode_active else ''}
656
  • Вік: {self.lifestyle_profile.patient_age}
657
  • Активні проблеми: {problems_text}
658
  • Lifestyle ціль: {self.lifestyle_profile.primary_goal}
 
664
 
665
  🔧 **API СТАТИСТИКА:**
666
  • Gemini виклики: {self.api.call_counter}
667
+ {test_status}"""
668
+
669
  return status
670
 
671
  def reset_session(self):
672
  """Скидання сесії"""
673
+ # Якщо активний тестовий режим, завершуємо сесію
674
+ if self.test_mode_active and self.testing_interface.current_session:
675
+ self.end_test_session("Сесія скинута користувачем")
676
+
677
  self.chat_history = []
678
  self.session_state = SessionState(
679
  current_mode="none",
 
681
  session_start_time=None,
682
  last_controller_decision={}
683
  )
684
+
685
+ return [], self._get_status_info()
686
 
687
+ def create_extended_app():
688
+ """Створення розширеного Gradio додатку з Testing Lab"""
689
+ app = ExtendedLifestyleJourneyApp()
690
 
 
691
  log_prompts_enabled = os.getenv("LOG_PROMPTS", "false").lower() == "true"
692
 
 
693
  theme_name = GRADIO_CONFIG.get("theme", "soft")
694
  if theme_name.lower() == "soft":
695
  theme = gr.themes.Soft()
696
  elif theme_name.lower() == "default":
697
  theme = gr.themes.Default()
698
  else:
699
+ theme = gr.themes.Soft()
700
 
701
  with gr.Blocks(
702
+ title=GRADIO_CONFIG.get("title", "Lifestyle Journey MVP + Testing Lab"),
703
  theme=theme,
704
  analytics_enabled=False
705
  ) as demo:
706
+ # Заголовок
707
  if log_prompts_enabled:
708
+ gr.Markdown("# 🏥 Lifestyle Journey MVP + 🧪 Testing Lab 📝")
709
  gr.Markdown("⚠️ **DEBUG MODE:** Промпти та відповіді LLM зберігаються в `lifestyle_journey.log`")
710
  else:
711
+ gr.Markdown("# 🏥 Lifestyle Journey MVP + 🧪 Testing Lab")
712
 
713
+ gr.Markdown("Медичний чат-бот з lifestyle коучингом та системою тестування нових пацієнтів")
714
+
715
+ # Табби
716
+ with gr.Tabs():
717
+ # Основна закладка чату
718
+ with gr.TabItem("💬 Чат з пацієнтом", id="main_chat"):
719
+ with gr.Row():
720
+ with gr.Column(scale=2):
721
+ chatbot = gr.Chatbot(
722
+ label="💬 Розмова з асистентом",
723
+ height=400,
724
+ show_copy_button=True,
725
+ type="messages"
726
+ )
727
+
728
+ with gr.Row():
729
+ msg = gr.Textbox(
730
+ label="Ваше повідомлення",
731
+ placeholder="Напишіть своє питання...",
732
+ scale=4
733
+ )
734
+ send_btn = gr.Button("📤 Надіслати", scale=1)
735
+
736
+ clear_btn = gr.Button("🗑️ Очистити чат")
737
+
738
+ with gr.Column(scale=1):
739
+ status_box = gr.Markdown(
740
+ value=app._get_status_info(),
741
+ label="📊 Статус системи"
742
+ )
743
+
744
+ # Нова закладка Testing Lab
745
+ with gr.TabItem("🧪 Testing Lab", id="testing_lab"):
746
+ gr.Markdown("## 📁 Завантаження тестового пацієнта")
747
+
748
+ with gr.Row():
749
+ with gr.Column():
750
+ clinical_file = gr.File(
751
+ label="🏥 Clinical Background JSON",
752
+ file_types=[".json"],
753
+ type="filepath"
754
+ )
755
+ lifestyle_file = gr.File(
756
+ label="💚 Lifestyle Profile JSON",
757
+ file_types=[".json"],
758
+ type="filepath"
759
+ )
760
+
761
+ load_patient_btn = gr.Button("📋 Завантажити пацієнта", variant="primary")
762
+
763
+ with gr.Column():
764
+ load_result = gr.Markdown(value="Оберіть файли для завантаження")
765
+
766
+ # Швидкі тестові кнопки
767
+ gr.Markdown("## ⚡ Швидке тестування (вбудовані дані)")
768
+ with gr.Row():
769
+ quick_elderly_btn = gr.Button("👵 Elderly Mary", size="sm")
770
+ quick_athlete_btn = gr.Button("🏃 Athletic John", size="sm")
771
+ quick_pregnant_btn = gr.Button("🤰 Pregnant Sarah", size="sm")
772
+
773
+ gr.Markdown("## 👤 Попередній перегляд пацієнта")
774
+ patient_preview = gr.Markdown(value="Пацієнт не завантажений")
775
 
776
+ gr.Markdown("## 🎯 Управління тестовою сесією")
777
  with gr.Row():
778
+ end_session_notes = gr.Textbox(
779
+ label="Нотатки до завершення сесії",
780
+ placeholder="Опишіть результати тестування...",
781
+ lines=3
782
  )
783
+ with gr.Column():
784
+ end_session_btn = gr.Button("⏹️ Завершити тестову сесію")
785
+ end_session_result = gr.Markdown(value="")
786
+
787
+ # Закладка результатів тестування
788
+ with gr.TabItem("📊 Результати тестування", id="test_results"):
789
+ gr.Markdown("## 📈 Аналіз тестових сесій")
790
+
791
+ refresh_results_btn = gr.Button("🔄 Оновити результати")
792
 
793
+ with gr.Row():
794
+ with gr.Column(scale=2):
795
+ results_summary = gr.Markdown(value="Натисніть 'Оновити результати'")
796
+
797
+ with gr.Column(scale=1):
798
+ export_btn = gr.Button("💾 Експортувати в CSV")
799
+ export_result = gr.Markdown(value="")
800
 
801
+ gr.Markdown("## 📋 Останні тестові сесії")
802
+ results_table = gr.Dataframe(
803
+ headers=["Пацієнт", "Час", "Повідомлень", "Medical", "Lifestyle", "Ескалації", "Тривалість", "Нотатки"],
804
+ datatype=["str", "str", "number", "number", "number", "number", "str", "str"],
805
+ label="Деталі сесій",
806
+ value=[]
807
  )
808
 
809
+ # Обробники подій для основного чату
810
  def handle_message(message, history):
811
  return app.process_message(message, history)
812
 
 
835
  handle_clear,
836
  outputs=[chatbot, status_box]
837
  )
838
+
839
+ # Обробники для Testing Lab
840
+ load_patient_btn.click(
841
+ app.load_test_patient,
842
+ inputs=[clinical_file, lifestyle_file],
843
+ outputs=[load_result, patient_preview, chatbot, status_box]
844
+ )
845
+
846
+ # Швидкі кнопки тестування
847
+ quick_elderly_btn.click(
848
+ lambda: app.load_quick_test_patient("elderly"),
849
+ outputs=[load_result, patient_preview, chatbot, status_box]
850
+ )
851
+
852
+ quick_athlete_btn.click(
853
+ lambda: app.load_quick_test_patient("athlete"),
854
+ outputs=[load_result, patient_preview, chatbot, status_box]
855
+ )
856
+
857
+ quick_pregnant_btn.click(
858
+ lambda: app.load_quick_test_patient("pregnant"),
859
+ outputs=[load_result, patient_preview, chatbot, status_box]
860
+ )
861
+
862
+ end_session_btn.click(
863
+ app.end_test_session,
864
+ inputs=[end_session_notes],
865
+ outputs=[end_session_result]
866
+ )
867
+
868
+ # Обробники для результатів
869
+ refresh_results_btn.click(
870
+ app.get_test_results_summary,
871
+ outputs=[results_summary, results_table]
872
+ )
873
+
874
+ export_btn.click(
875
+ app.export_test_results,
876
+ outputs=[export_result]
877
+ )
878
 
879
  return demo
880
 
881
  if __name__ == "__main__":
 
 
 
882
  if not os.getenv("GEMINI_API_KEY"):
883
  print("⚠️ GEMINI_API_KEY не знайдено в змінних оточення!")
884
  print("Для локального запуску створіть .env файл з API к��ючем")
885
 
886
+ demo = create_extended_app()
887
 
 
888
  is_hf_space = os.getenv("SPACE_ID") is not None
889
 
890
  if is_hf_space:
 
891
  demo.launch(
892
  server_name="0.0.0.0",
893
  server_port=7860,
 
895
  show_error=True
896
  )
897
  else:
 
898
  demo.launch(share=True, debug=True)
app_old.py ADDED
@@ -0,0 +1,749 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import gradio as gr
4
+ from datetime import datetime
5
+ from dataclasses import dataclass, asdict
6
+ from typing import List, Dict, Optional
7
+ from google import genai
8
+ from google.genai import types
9
+ from dotenv import load_dotenv
10
+
11
+ from dotenv import load_dotenv
12
+ try:
13
+ from app_config import GRADIO_CONFIG, API_CONFIG
14
+ except ImportError:
15
+ # Fallback конфігурація якщо файл відсутній
16
+ GRADIO_CONFIG = {"theme": "soft", "show_api": False}
17
+ API_CONFIG = {"gemini_model": "gemini-2.5-flash", "temperature": 0.3}
18
+
19
+ # Завантаження змінних оточення з .env файлу
20
+ load_dotenv()
21
+
22
+ @dataclass
23
+ class ClinicalBackground:
24
+ patient_id: str
25
+ patient_name: str = ""
26
+ patient_age: str = ""
27
+ active_problems: List[str] = None
28
+ past_medical_history: List[str] = None
29
+ current_medications: List[str] = None
30
+ allergies: str = ""
31
+ vital_signs_and_measurements: List[str] = None
32
+ laboratory_results: List[str] = None
33
+ assessment_and_plan: str = ""
34
+ critical_alerts: List[str] = None
35
+ social_history: Dict = None
36
+ recent_clinical_events: List[str] = None
37
+
38
+ def __post_init__(self):
39
+ # Ініціалізуємо пусті списки якщо None
40
+ if self.active_problems is None:
41
+ self.active_problems = []
42
+ if self.past_medical_history is None:
43
+ self.past_medical_history = []
44
+ if self.current_medications is None:
45
+ self.current_medications = []
46
+ if self.vital_signs_and_measurements is None:
47
+ self.vital_signs_and_measurements = []
48
+ if self.laboratory_results is None:
49
+ self.laboratory_results = []
50
+ if self.critical_alerts is None:
51
+ self.critical_alerts = []
52
+ if self.recent_clinical_events is None:
53
+ self.recent_clinical_events = []
54
+ if self.social_history is None:
55
+ self.social_history = {}
56
+
57
+ @dataclass
58
+ class LifestyleProfile:
59
+ patient_name: str
60
+ patient_age: str
61
+ conditions: List[str]
62
+ primary_goal: str
63
+ exercise_preferences: List[str]
64
+ exercise_limitations: List[str]
65
+ dietary_notes: List[str]
66
+ personal_preferences: List[str]
67
+ journey_summary: str
68
+ last_session_summary: str
69
+ next_check_in: str = "not set"
70
+ progress_metrics: Dict[str, str] = None
71
+
72
+ def __post_init__(self):
73
+ if self.progress_metrics is None:
74
+ self.progress_metrics = {}
75
+
76
+ @dataclass
77
+ class ChatMessage:
78
+ timestamp: str
79
+ role: str # "user" or "assistant"
80
+ message: str
81
+ mode: str # "medical", "lifestyle", or "controller"
82
+ metadata: Dict = None
83
+
84
+ @dataclass
85
+ class SessionState:
86
+ current_mode: str # "medical", "lifestyle", "none"
87
+ is_active_session: bool
88
+ session_start_time: Optional[str]
89
+ last_controller_decision: Dict
90
+
91
+ class GeminiAPI:
92
+ def __init__(self):
93
+ self.client = genai.Client(
94
+ api_key=os.environ.get("GEMINI_API_KEY"),
95
+ )
96
+ self.model = os.getenv("GEMINI_MODEL", API_CONFIG.get("gemini_model", "gemini-2.5-flash"))
97
+ self.call_counter = 0
98
+
99
+ def _log_prompt_and_response(self, system_prompt: str, user_prompt: str, response: str, call_type: str = ""):
100
+ """Логування промптів та відповідей"""
101
+ log_prompts_enabled = os.getenv("LOG_PROMPTS", "false").lower() == "true"
102
+ if not log_prompts_enabled:
103
+ return
104
+
105
+ # Створюємо logger з правильними налаштуваннями
106
+ import logging
107
+ log_logger = logging.getLogger(f"{__name__}.GeminiAPI")
108
+
109
+ # Налаштовуємо logger якщо ще не налаштований
110
+ if not log_logger.handlers:
111
+ log_logger.setLevel(logging.INFO)
112
+
113
+ # Консольний handler
114
+ console_handler = logging.StreamHandler()
115
+ console_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
116
+ log_logger.addHandler(console_handler)
117
+
118
+ # Файловий handler
119
+ file_handler = logging.FileHandler('lifestyle_journey.log', encoding='utf-8')
120
+ file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
121
+ log_logger.addHandler(file_handler)
122
+
123
+ self.call_counter += 1
124
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
125
+
126
+ log_message = f"""
127
+ {'='*80}
128
+ 🤖 GEMINI API CALL #{self.call_counter} [{call_type}] - {timestamp}
129
+ {'='*80}
130
+
131
+ 📤 SYSTEM PROMPT:
132
+ {'-'*40}
133
+ {system_prompt}
134
+
135
+ 📤 USER PROMPT:
136
+ {'-'*40}
137
+ {user_prompt}
138
+
139
+ 📥 GEMINI RESPONSE:
140
+ {'-'*40}
141
+ {response}
142
+
143
+ 🔧 MODEL: {self.model}
144
+ {'='*80}
145
+ """
146
+ log_logger.info(log_message)
147
+
148
+ def generate_response(self, system_prompt: str, user_prompt: str, temperature: float = None, call_type: str = "") -> str:
149
+ """Генерує відповідь від Gemini"""
150
+ if temperature is None:
151
+ temperature = API_CONFIG.get("temperature", 0.3)
152
+
153
+ try:
154
+ contents = [
155
+ types.Content(
156
+ role="user",
157
+ parts=[types.Part.from_text(text=user_prompt)],
158
+ ),
159
+ ]
160
+
161
+ config = types.GenerateContentConfig(
162
+ temperature=temperature,
163
+ system_instruction=[
164
+ types.Part.from_text(text=system_prompt),
165
+ ],
166
+ )
167
+
168
+ response = ""
169
+ for chunk in self.client.models.generate_content_stream(
170
+ model=self.model,
171
+ contents=contents,
172
+ config=config,
173
+ ):
174
+ response += chunk.text
175
+
176
+ response = response.strip()
177
+
178
+ # Логування промпта та відповіді
179
+ self._log_prompt_and_response(system_prompt, user_prompt, response, call_type)
180
+
181
+ return response
182
+ except Exception as e:
183
+ error_msg = f"Помилка API: {str(e)}"
184
+
185
+ # Логування помилки
186
+ log_prompts_enabled = os.getenv("LOG_PROMPTS", "false").lower() == "true"
187
+ if log_prompts_enabled:
188
+ self._log_prompt_and_response(system_prompt, user_prompt, error_msg, f"{call_type}_ERROR")
189
+
190
+ return error_msg
191
+
192
+ class PatientDataLoader:
193
+ """Клас для завантаження даних пацієнтів з JSON файлів"""
194
+
195
+ @staticmethod
196
+ def load_clinical_background(file_path: str = "clinical_background.json") -> ClinicalBackground:
197
+ """Завантажує clinical background з JSON файлу"""
198
+ try:
199
+ with open(file_path, 'r', encoding='utf-8') as f:
200
+ data = json.load(f)
201
+
202
+ # Парсимо patient_summary
203
+ patient_summary = data.get("patient_summary", {})
204
+ vital_signs = data.get("vital_signs_and_measurements", [])
205
+
206
+ return ClinicalBackground(
207
+ patient_id="patient_001",
208
+ patient_name="Mark", # можна додати в JSON
209
+ patient_age="adult", # можна додати в JSON
210
+ active_problems=patient_summary.get("active_problems", []),
211
+ past_medical_history=patient_summary.get("past_medical_history", []),
212
+ current_medications=patient_summary.get("current_medications", []),
213
+ allergies=patient_summary.get("allergies", ""),
214
+ vital_signs_and_measurements=vital_signs,
215
+ laboratory_results=data.get("laboratory_results", []),
216
+ assessment_and_plan=data.get("assessment_and_plan", ""),
217
+ critical_alerts=data.get("critical_alerts", []),
218
+ social_history=data.get("social_history", {}),
219
+ recent_clinical_events=data.get("recent_clinical_events_and_encounters", [])
220
+ )
221
+
222
+ except FileNotFoundError:
223
+ print(f"⚠️ Файл {file_path} не знайдено. Використовуємо тестові дані.")
224
+ return PatientDataLoader._get_default_clinical_background()
225
+ except Exception as e:
226
+ print(f"⚠️ Помилка завантаження {file_path}: {e}")
227
+ return PatientDataLoader._get_default_clinical_background()
228
+
229
+ @staticmethod
230
+ def load_lifestyle_profile(file_path: str = "lifestyle_profile.json") -> LifestyleProfile:
231
+ """Завантажує lifestyle profile з JSON файлу"""
232
+ try:
233
+ with open(file_path, 'r', encoding='utf-8') as f:
234
+ data = json.load(f)
235
+
236
+ return LifestyleProfile(
237
+ patient_name=data.get("patient_name", "Пацієнт"),
238
+ patient_age=data.get("patient_age", "невідомо"),
239
+ conditions=data.get("conditions", []),
240
+ primary_goal=data.get("primary_goal", ""),
241
+ exercise_preferences=data.get("exercise_preferences", []),
242
+ exercise_limitations=data.get("exercise_limitations", []),
243
+ dietary_notes=data.get("dietary_notes", []),
244
+ personal_preferences=data.get("personal_preferences", []),
245
+ journey_summary=data.get("journey_summary", ""),
246
+ last_session_summary=data.get("last_session_summary", ""),
247
+ next_check_in=data.get("next_check_in", "not set"),
248
+ progress_metrics=data.get("progress_metrics", {})
249
+ )
250
+
251
+ except FileNotFoundError:
252
+ print(f"⚠️ Файл {file_path} не знайдено. Використовуємо тестові дані.")
253
+ return PatientDataLoader._get_default_lifestyle_profile()
254
+ except Exception as e:
255
+ print(f"⚠️ Помилка завантаження {file_path}: {e}")
256
+ return PatientDataLoader._get_default_lifestyle_profile()
257
+
258
+ @staticmethod
259
+ def _get_default_clinical_background() -> ClinicalBackground:
260
+ """Fallback дані для clinical background"""
261
+ return ClinicalBackground(
262
+ patient_id="test_001",
263
+ patient_name="Тестовий пацієнт",
264
+ active_problems=["Хронічна серцева недостатність", "Артеріальна гіпертензія"],
265
+ current_medications=["Еналаприл 10мг", "Метформін 500мг"],
266
+ allergies="Пеніцилін",
267
+ vital_signs_and_measurements=["АТ: 140/90", "ЧСС: 72"]
268
+ )
269
+
270
+ @staticmethod
271
+ def _get_default_lifestyle_profile() -> LifestyleProfile:
272
+ """Fallback дані для lifestyle profile"""
273
+ return LifestyleProfile(
274
+ patient_name="Тестовий пацієнт",
275
+ patient_age="52",
276
+ conditions=["гіпертензія"],
277
+ primary_goal="Покращити загальний стан здоров'я",
278
+ exercise_preferences=["ходьба"],
279
+ exercise_limitations=["уникати високих навантажень"],
280
+ dietary_notes=["низькосольова дієта"],
281
+ personal_preferences=["поступові зміни"],
282
+ journey_summary="Початок lifestyle journey",
283
+ last_session_summary=""
284
+ )
285
+ def __init__(self):
286
+ self.client = genai.Client(
287
+ api_key=os.environ.get("GEMINI_API_KEY"),
288
+ )
289
+ self.model = os.getenv("GEMINI_MODEL", API_CONFIG.get("gemini_model", "gemini-2.5-flash"))
290
+
291
+ def generate_response(self, system_prompt: str, user_prompt: str, temperature: float = None) -> str:
292
+ """Генерує відповідь від Gemini"""
293
+ if temperature is None:
294
+ temperature = API_CONFIG.get("temperature", 0.3)
295
+
296
+ try:
297
+ contents = [
298
+ types.Content(
299
+ role="user",
300
+ parts=[types.Part.from_text(text=user_prompt)],
301
+ ),
302
+ ]
303
+
304
+ config = types.GenerateContentConfig(
305
+ temperature=temperature,
306
+ system_instruction=[
307
+ types.Part.from_text(text=system_prompt),
308
+ ],
309
+ )
310
+
311
+ response = ""
312
+ for chunk in self.client.models.generate_content_stream(
313
+ model=self.model,
314
+ contents=contents,
315
+ config=config,
316
+ ):
317
+ response += chunk.text
318
+
319
+ return response.strip()
320
+ except Exception as e:
321
+ return f"Помилка API: {str(e)}"
322
+
323
+ class SessionController:
324
+ def __init__(self, api: GeminiAPI):
325
+ self.api = api
326
+
327
+ def make_decision(self, user_message: str, chat_history: List[ChatMessage],
328
+ clinical_background: ClinicalBackground, current_state: SessionState) -> Dict:
329
+ """Приймає рішення про режим сесії"""
330
+
331
+ system_prompt = """Ти - Session Controller для медичного додатку з lifestyle coaching.
332
+
333
+ ТВОЄ ЗАВДАННЯ: Проаналізувати повідомлення пацієнта і прийняти рішення про режим роботи.
334
+
335
+ РЕЖИМИ:
336
+ - medical: медичні симптоми, скарги, ургентні стани
337
+ - lifestyle: фізична активність, харчування, спосіб життя, мотивація
338
+ - none: завершення сесії або неясний контекст
339
+
340
+ RED FLAGS (завжди -> medical):
341
+ - біль у грудях, задишка у спокої
342
+ - високий АТ (>180/120), низький (<80/50)
343
+ - синкопе, запаморочення
344
+ - різкий набряк, набір ваги
345
+ - симптомна гіпо/гіперглікемія
346
+
347
+ ВІДПОВІДАЙ ЛИШЕ У ФОРМАТІ JSON:
348
+ {
349
+ "action": "start_medical|start_lifestyle|continue_current|end_session",
350
+ "mode": "medical|lifestyle|none",
351
+ "reasoning": "коротке пояснення українською",
352
+ "escalation_needed": true/false
353
+ }"""
354
+
355
+ # Формуємо контекст
356
+ history_text = "\n".join([f"{msg.role}: {msg.message}" for msg in chat_history[-5:]])
357
+
358
+ # Беремо найважливішу інформацію з clinical background
359
+ active_problems = "; ".join(clinical_background.active_problems[:5]) if clinical_background.active_problems else "не вказано"
360
+ critical_alerts = "; ".join(clinical_background.critical_alerts) if clinical_background.critical_alerts else "немає"
361
+
362
+ user_prompt = f"""
363
+ КЛІНІЧНИЙ КОНТЕКСТ пацієнта {clinical_background.patient_name}:
364
+ - Активні проблеми: {active_problems}
365
+ - Критичні попередження: {critical_alerts}
366
+
367
+ ПОТОЧНИЙ СТАН СЕСІЇ: режим={current_state.current_mode}, активна={current_state.is_active_session}
368
+
369
+ ІСТОРІЯ ЧАТУ:
370
+ {history_text}
371
+
372
+ НОВЕ ПОВІДОМЛЕННЯ ПАЦІЄНТА: {user_message}
373
+
374
+ Прийми рішення про режим роботи:"""
375
+
376
+ response = self.api.generate_response(system_prompt, user_prompt, temperature=0.1, call_type="SESSION_CONTROLLER")
377
+
378
+ try:
379
+ # Очищуємо відповідь від markdown
380
+ clean_response = response.replace("```json", "").replace("```", "").strip()
381
+ decision = json.loads(clean_response)
382
+ return decision
383
+ except:
384
+ # Fallback рішення
385
+ return {
386
+ "action": "start_medical",
387
+ "mode": "medical",
388
+ "reasoning": "Помилка парсингу - перенаправлення до медичного режиму для безпеки",
389
+ "escalation_needed": True
390
+ }
391
+
392
+ class MedicalAssistant:
393
+ def __init__(self, api: GeminiAPI):
394
+ self.api = api
395
+
396
+ def generate_response(self, user_message: str, chat_history: List[ChatMessage],
397
+ clinical_background: ClinicalBackground) -> str:
398
+ """Генерує медичну відповідь"""
399
+
400
+ system_prompt = """Ти - досвідчений медичний асистент для пацієнтів з хронічними захворюваннями.
401
+
402
+ ПРИНЦИПИ:
403
+ - Безпека пацієнта - головний пріоритет
404
+ - Не ставиш діагнози, не призначаєш лікування
405
+ - Рекомендуєш звернення до лікаря при червоних прапорцях
406
+ - Даєш загальні поради з управління хронічними станами
407
+ - Відповідаєш українською мовою
408
+
409
+ При УРГЕНТНИХ симптомах - рекомендуй негайне звернення до медзакладу."""
410
+
411
+ # Контекст з реальних даних
412
+ active_problems = "; ".join(clinical_background.active_problems[:5]) if clinical_background.active_problems else "не вказані"
413
+ medications = "; ".join(clinical_background.current_medications[:8]) if clinical_background.current_medications else "не вказані"
414
+ recent_vitals = "; ".join(clinical_background.vital_signs_and_measurements[-3:]) if clinical_background.vital_signs_and_measurements else "не вказані"
415
+
416
+ history_text = "\n".join([f"{msg.role}: {msg.message}" for msg in chat_history[-3:]])
417
+
418
+ user_prompt = f"""
419
+ МЕДИЧНИЙ ПРОФІЛЬ ПАЦІЄНТА ({clinical_background.patient_name}):
420
+ - Активні проблеми: {active_problems}
421
+ - Поточні медикаменти: {medications}
422
+ - Останні показники: {recent_vitals}
423
+ - Алергії: {clinical_background.allergies}
424
+
425
+ КРИТИЧНІ ПОПЕРЕДЖЕННЯ: {"; ".join(clinical_background.critical_alerts) if clinical_background.critical_alerts else "немає"}
426
+
427
+ ІСТОРІЯ РОЗМОВИ:
428
+ {history_text}
429
+
430
+ ПИТАННЯ ПАЦІЄНТА: {user_message}
431
+
432
+ Надай медичну консультацію з урахуванням медичного профілю:"""
433
+
434
+ return self.api.generate_response(system_prompt, user_prompt, call_type="MEDICAL_ASSISTANT")
435
+
436
+ class LifestyleAssistant:
437
+ def __init__(self, api: GeminiAPI):
438
+ self.api = api
439
+
440
+ def generate_response(self, user_message: str, chat_history: List[ChatMessage],
441
+ clinical_background: ClinicalBackground, lifestyle_profile: LifestyleProfile) -> tuple[str, LifestyleProfile]:
442
+ """Генерує lifestyle відповідь та оновлює профіль"""
443
+
444
+ system_prompt = f"""Ти - lifestyle coach для пацієнтів з хронічними захворюваннями.
445
+
446
+ ПРИНЦИПИ:
447
+ - Безпечні, поступові зміни з урахуванням медичних обмежень
448
+ - Персоналізація на основі профілю пацієнта
449
+ - Позитивне підкріплення та реалістичні цілі
450
+ - Мотивація через малі кроки прогресу
451
+ - Відповідаєш українською мовою
452
+
453
+ МЕДИЧНІ ОБМЕЖЕННЯ пацієнта {lifestyle_profile.patient_name}:
454
+ - Стани: {', '.join(lifestyle_profile.conditions)}
455
+ - Обмеження: {'; '.join(lifestyle_profile.exercise_limitations)}
456
+
457
+ УВАГА до активних пробл��м:
458
+ {'; '.join(clinical_background.active_problems[:5]) if clinical_background.active_problems else 'Основні проблеми не вказані'}
459
+
460
+ В кінці кожної сесії пропонуй конкретний план дій та час наступної зустрічі."""
461
+
462
+ # Контекст з реальних даних
463
+ goals = lifestyle_profile.primary_goal
464
+ preferences = "; ".join(lifestyle_profile.exercise_preferences) if lifestyle_profile.exercise_preferences else "не вказані"
465
+ dietary = "; ".join(lifestyle_profile.dietary_notes) if lifestyle_profile.dietary_notes else "не вказані"
466
+
467
+ history_text = "\n".join([f"{msg.role}: {msg.message}" for msg in chat_history[-3:]])
468
+
469
+ user_prompt = f"""
470
+ ПАЦІЄНТ: {lifestyle_profile.patient_name}, {lifestyle_profile.patient_age} років
471
+
472
+ LIFESTYLE ПРОФІЛЬ:
473
+ - Головна ціль: {goals}
474
+ - Уподобання: {preferences}
475
+ - Харчування: {dietary}
476
+ - Особисті переваги: {"; ".join(lifestyle_profile.personal_preferences)}
477
+ - Journey резюме: {lifestyle_profile.journey_summary}
478
+ - Попередня сесія: {lifestyle_profile.last_session_summary}
479
+
480
+ ІСТОРІЯ РОЗМОВИ:
481
+ {history_text}
482
+
483
+ ПОВІДОМЛЕННЯ ПАЦІЄНТА: {user_message}
484
+
485
+ Проведи lifestyle коучинг з урахуванням медичного стану та особистих цілей:"""
486
+
487
+ response = self.api.generate_response(system_prompt, user_prompt, call_type="LIFESTYLE_ASSISTANT")
488
+
489
+ # Оновлення профілю з більш детальною інформацією
490
+ updated_profile = lifestyle_profile
491
+ if user_message and response:
492
+ session_date = datetime.now().strftime('%d.%m.%Y')
493
+ updated_profile.last_session_summary = f"[{session_date}] Обговорювали: {user_message[:100]}..."
494
+
495
+ # Додаємо до journey summary
496
+ if len(updated_profile.journey_summary) > 500:
497
+ # Обрізаємо якщо занадто довге
498
+ updated_profile.journey_summary = updated_profile.journey_summary[-400:]
499
+
500
+ updated_profile.journey_summary += f" | {session_date}: {user_message[:50]}..."
501
+
502
+ return response, updated_profile
503
+
504
+ class LifestyleJourneyApp:
505
+ def __init__(self):
506
+ self.api = GeminiAPI()
507
+ self.controller = SessionController(self.api)
508
+ self.medical_assistant = MedicalAssistant(self.api)
509
+ self.lifestyle_assistant = LifestyleAssistant(self.api)
510
+
511
+ # Завантаження реальних даних з JSON файлів
512
+ print("🔄 Завантаження даних пацієнта...")
513
+ self.clinical_background = PatientDataLoader.load_clinical_background()
514
+ self.lifestyle_profile = PatientDataLoader.load_lifestyle_profile()
515
+
516
+ print(f"✅ Завантажено профіль пацієнта: {self.clinical_background.patient_name}")
517
+ print(f"📋 Активних проблем: {len(self.clinical_background.active_problems)}")
518
+ print(f"🎯 Lifestyle ціль: {self.lifestyle_profile.primary_goal}")
519
+
520
+ self.chat_history: List[ChatMessage] = []
521
+ self.session_state = SessionState(
522
+ current_mode="none",
523
+ is_active_session=False,
524
+ session_start_time=None,
525
+ last_controller_decision={}
526
+ )
527
+
528
+ def process_message(self, message: str, history):
529
+ """Основна логіка обробки повідомлень"""
530
+ if not message.strip():
531
+ return history, self._get_status_info()
532
+
533
+ # 1. Controller приймає рішення
534
+ decision = self.controller.make_decision(
535
+ message, self.chat_history, self.clinical_background, self.session_state
536
+ )
537
+
538
+ self.session_state.last_controller_decision = decision
539
+
540
+ # 2. Додаємо повідомлення користувача до історії
541
+ user_msg = ChatMessage(
542
+ timestamp=datetime.now().strftime("%H:%M"),
543
+ role="user",
544
+ message=message,
545
+ mode=decision.get("mode", "unknown")
546
+ )
547
+ self.chat_history.append(user_msg)
548
+
549
+ # 3. Генеруємо відповідь залежно від режиму
550
+ if decision["mode"] == "medical":
551
+ self.session_state.current_mode = "medical"
552
+ self.session_state.is_active_session = True
553
+
554
+ response = self.medical_assistant.generate_response(
555
+ message, self.chat_history, self.clinical_background
556
+ )
557
+
558
+ elif decision["mode"] == "lifestyle":
559
+ self.session_state.current_mode = "lifestyle"
560
+ self.session_state.is_active_session = True
561
+
562
+ response, self.lifestyle_profile = self.lifestyle_assistant.generate_response(
563
+ message, self.chat_history, self.clinical_background, self.lifestyle_profile
564
+ )
565
+
566
+ else:
567
+ self.session_state.current_mode = "none"
568
+ self.session_state.is_active_session = False
569
+ response = "Будь ласка, уточніть ваше питання. Я можу допомогти з медичними питаннями або питаннями способу життя."
570
+
571
+ # 4. Додаємо відповідь асистента
572
+ assistant_msg = ChatMessage(
573
+ timestamp=datetime.now().strftime("%H:%M"),
574
+ role="assistant",
575
+ message=response,
576
+ mode=self.session_state.current_mode
577
+ )
578
+ self.chat_history.append(assistant_msg)
579
+
580
+ # 5. Оновлюємо Gradio історію (формат messages)
581
+ if not history:
582
+ history = []
583
+
584
+ history.append({"role": "user", "content": message})
585
+ history.append({"role": "assistant", "content": response})
586
+
587
+ return history, self._get_status_info()
588
+
589
+ def _get_status_info(self) -> str:
590
+ """Повертає інформацію про стан сесії"""
591
+ decision = self.session_state.last_controller_decision
592
+
593
+ # Читаємо стан логування
594
+ log_prompts_enabled = os.getenv("LOG_PROMPTS", "false").lower() == "true"
595
+
596
+ # Скорочена інформація про активні проблеми
597
+ active_problems = self.clinical_background.active_problems[:3] if self.clinical_background.active_problems else ["Немає даних"]
598
+ problems_text = "; ".join(active_problems)
599
+ if len(self.clinical_background.active_problems) > 3:
600
+ problems_text += f" та ще {len(self.clinical_background.active_problems) - 3}..."
601
+
602
+ status = f"""
603
+ 📊 **СТАН СЕСІЇ**
604
+ • Режим: {self.session_state.current_mode.upper()}
605
+ • Активна: {'✅' if self.session_state.is_active_session else '❌'}
606
+ • Логування: {'📝 АКТИВНЕ' if log_prompts_enabled else '❌ ВИМКНЕНО'}
607
+
608
+ 🧠 **ОСТАННЄ РІШЕННЯ CONTROLLER:**
609
+ • Дія: {decision.get('action', 'N/A')}
610
+ • Обґрунтування: {decision.get('reasoning', 'N/A')}
611
+ • Ескалація: {'🚨' if decision.get('escalation_needed') else '📴'}
612
+
613
+ 👤 **ПАЦІЄНТ: {self.clinical_background.patient_name}**
614
+ • Вік: {self.lifestyle_profile.patient_age}
615
+ • Активні проблеми: {problems_text}
616
+ • Lifestyle ціль: {self.lifestyle_profile.primary_goal}
617
+
618
+ 🏥 **МЕДИЧНИЙ КОНТЕКСТ:**
619
+ • Медикаментів: {len(self.clinical_background.current_medications)}
620
+ • Критичних попереджень: {len(self.clinical_background.critical_alerts)}
621
+ • Останні показники: {len(self.clinical_background.vital_signs_and_measurements)}
622
+
623
+ 🔧 **API СТАТИСТИКА:**
624
+ • Gemini виклики: {self.api.call_counter}
625
+ """
626
+ return status
627
+
628
+ def reset_session(self):
629
+ """Скидання сесії"""
630
+ self.chat_history = []
631
+ self.session_state = SessionState(
632
+ current_mode="none",
633
+ is_active_session=False,
634
+ session_start_time=None,
635
+ last_controller_decision={}
636
+ )
637
+ return [], self._get_status_info() # Повертаємо пустий список для messages формату
638
+
639
+ # Створення Gradio інтерфейсу
640
+ def create_app():
641
+ app = LifestyleJourneyApp()
642
+
643
+ # Читаємо змінну логування безпосередньо
644
+ log_prompts_enabled = os.getenv("LOG_PROMPTS", "false").lower() == "true"
645
+
646
+ # Використовуємо конфігурацію для Gradio
647
+ theme_name = GRADIO_CONFIG.get("theme", "soft")
648
+ if theme_name.lower() == "soft":
649
+ theme = gr.themes.Soft()
650
+ elif theme_name.lower() == "default":
651
+ theme = gr.themes.Default()
652
+ else:
653
+ theme = gr.themes.Soft() # fallback
654
+
655
+ with gr.Blocks(
656
+ title=GRADIO_CONFIG.get("title", "Lifestyle Journey MVP"),
657
+ theme=theme,
658
+ analytics_enabled=False
659
+ ) as demo:
660
+ # Заголовки з індикатором логування
661
+ if log_prompts_enabled:
662
+ gr.Markdown("# 🏥 Lifestyle Journey MVP 📝")
663
+ gr.Markdown("⚠️ **DEBUG MODE:** Промпти та відповіді LLM зберігаються в `lifestyle_journey.log`")
664
+ else:
665
+ gr.Markdown("# 🏥 Lifestyle Journey MVP")
666
+
667
+ gr.Markdown("Тестовий чат-бот з медичним асистентом та lifestyle коучингом")
668
+
669
+ with gr.Row():
670
+ with gr.Column(scale=2):
671
+ chatbot = gr.Chatbot(
672
+ label="💬 Розмова з асистентом",
673
+ height=400,
674
+ show_copy_button=True,
675
+ type="messages"
676
+ )
677
+
678
+ with gr.Row():
679
+ msg = gr.Textbox(
680
+ label="Ваше повідомлення",
681
+ placeholder="Напишіть своє питання...",
682
+ scale=4
683
+ )
684
+ send_btn = gr.Button("📤 Надіслати", scale=1)
685
+
686
+ clear_btn = gr.Button("🗑️ Очистити чат")
687
+
688
+ with gr.Column(scale=1):
689
+ status_box = gr.Markdown(
690
+ value=app._get_status_info(),
691
+ label="📊 Статус системи"
692
+ )
693
+
694
+ # Обробники подій
695
+ def handle_message(message, history):
696
+ return app.process_message(message, history)
697
+
698
+ def handle_clear():
699
+ return app.reset_session()
700
+
701
+ send_btn.click(
702
+ handle_message,
703
+ inputs=[msg, chatbot],
704
+ outputs=[chatbot, status_box]
705
+ ).then(
706
+ lambda: "",
707
+ outputs=[msg]
708
+ )
709
+
710
+ msg.submit(
711
+ handle_message,
712
+ inputs=[msg, chatbot],
713
+ outputs=[chatbot, status_box]
714
+ ).then(
715
+ lambda: "",
716
+ outputs=[msg]
717
+ )
718
+
719
+ clear_btn.click(
720
+ handle_clear,
721
+ outputs=[chatbot, status_box]
722
+ )
723
+
724
+ return demo
725
+
726
+ if __name__ == "__main__":
727
+ # API ключ завантажується з .env файлу
728
+ # Створіть файл .env з: GEMINI_API_KEY=your_api_key_here
729
+
730
+ if not os.getenv("GEMINI_API_KEY"):
731
+ print("⚠️ GEMINI_API_KEY не знайдено в змінних оточення!")
732
+ print("Для локального запуску створіть .env файл з API ключем")
733
+
734
+ demo = create_app()
735
+
736
+ # Параметри для HuggingFace Spaces
737
+ is_hf_space = os.getenv("SPACE_ID") is not None
738
+
739
+ if is_hf_space:
740
+ # Конфігурація для HuggingFace Spaces
741
+ demo.launch(
742
+ server_name="0.0.0.0",
743
+ server_port=7860,
744
+ show_api=False,
745
+ show_error=True
746
+ )
747
+ else:
748
+ # Локальний запуск
749
+ demo.launch(share=True, debug=True)
core_classes.py ADDED
@@ -0,0 +1,439 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # core_classes.py - Основні класи для Lifestyle Journey
2
+
3
+ import os
4
+ import json
5
+ from datetime import datetime
6
+ from dataclasses import dataclass
7
+ from typing import List, Dict, Optional
8
+ from google import genai
9
+ from google.genai import types
10
+
11
+ try:
12
+ from app_config import API_CONFIG
13
+ except ImportError:
14
+ API_CONFIG = {"gemini_model": "gemini-2.5-flash", "temperature": 0.3}
15
+
16
+ @dataclass
17
+ class ClinicalBackground:
18
+ patient_id: str
19
+ patient_name: str = ""
20
+ patient_age: str = ""
21
+ active_problems: List[str] = None
22
+ past_medical_history: List[str] = None
23
+ current_medications: List[str] = None
24
+ allergies: str = ""
25
+ vital_signs_and_measurements: List[str] = None
26
+ laboratory_results: List[str] = None
27
+ assessment_and_plan: str = ""
28
+ critical_alerts: List[str] = None
29
+ social_history: Dict = None
30
+ recent_clinical_events: List[str] = None
31
+
32
+ def __post_init__(self):
33
+ if self.active_problems is None:
34
+ self.active_problems = []
35
+ if self.past_medical_history is None:
36
+ self.past_medical_history = []
37
+ if self.current_medications is None:
38
+ self.current_medications = []
39
+ if self.vital_signs_and_measurements is None:
40
+ self.vital_signs_and_measurements = []
41
+ if self.laboratory_results is None:
42
+ self.laboratory_results = []
43
+ if self.critical_alerts is None:
44
+ self.critical_alerts = []
45
+ if self.recent_clinical_events is None:
46
+ self.recent_clinical_events = []
47
+ if self.social_history is None:
48
+ self.social_history = {}
49
+
50
+ @dataclass
51
+ class LifestyleProfile:
52
+ patient_name: str
53
+ patient_age: str
54
+ conditions: List[str]
55
+ primary_goal: str
56
+ exercise_preferences: List[str]
57
+ exercise_limitations: List[str]
58
+ dietary_notes: List[str]
59
+ personal_preferences: List[str]
60
+ journey_summary: str
61
+ last_session_summary: str
62
+ next_check_in: str = "not set"
63
+ progress_metrics: Dict[str, str] = None
64
+
65
+ def __post_init__(self):
66
+ if self.progress_metrics is None:
67
+ self.progress_metrics = {}
68
+
69
+ @dataclass
70
+ class ChatMessage:
71
+ timestamp: str
72
+ role: str
73
+ message: str
74
+ mode: str
75
+ metadata: Dict = None
76
+
77
+ @dataclass
78
+ class SessionState:
79
+ current_mode: str
80
+ is_active_session: bool
81
+ session_start_time: Optional[str]
82
+ last_controller_decision: Dict
83
+
84
+ class GeminiAPI:
85
+ def __init__(self):
86
+ self.client = genai.Client(
87
+ api_key=os.environ.get("GEMINI_API_KEY"),
88
+ )
89
+ self.model = os.getenv("GEMINI_MODEL", API_CONFIG.get("gemini_model", "gemini-2.5-flash"))
90
+ self.call_counter = 0
91
+
92
+ def _log_prompt_and_response(self, system_prompt: str, user_prompt: str, response: str, call_type: str = ""):
93
+ """Логування промптів та відповідей"""
94
+ log_prompts_enabled = os.getenv("LOG_PROMPTS", "false").lower() == "true"
95
+ if not log_prompts_enabled:
96
+ return
97
+
98
+ import logging
99
+ log_logger = logging.getLogger(f"{__name__}.GeminiAPI")
100
+
101
+ if not log_logger.handlers:
102
+ log_logger.setLevel(logging.INFO)
103
+
104
+ console_handler = logging.StreamHandler()
105
+ console_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
106
+ log_logger.addHandler(console_handler)
107
+
108
+ file_handler = logging.FileHandler('lifestyle_journey.log', encoding='utf-8')
109
+ file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
110
+ log_logger.addHandler(file_handler)
111
+
112
+ self.call_counter += 1
113
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
114
+
115
+ log_message = f"""
116
+ {'='*80}
117
+ 🤖 GEMINI API CALL #{self.call_counter} [{call_type}] - {timestamp}
118
+ {'='*80}
119
+
120
+ 📤 SYSTEM PROMPT:
121
+ {'-'*40}
122
+ {system_prompt}
123
+
124
+ 📤 USER PROMPT:
125
+ {'-'*40}
126
+ {user_prompt}
127
+
128
+ 📥 GEMINI RESPONSE:
129
+ {'-'*40}
130
+ {response}
131
+
132
+ 🔧 MODEL: {self.model}
133
+ {'='*80}
134
+ """
135
+ log_logger.info(log_message)
136
+
137
+ def generate_response(self, system_prompt: str, user_prompt: str, temperature: float = None, call_type: str = "") -> str:
138
+ """Генерує відповідь від Gemini"""
139
+ if temperature is None:
140
+ temperature = API_CONFIG.get("temperature", 0.3)
141
+
142
+ try:
143
+ contents = [
144
+ types.Content(
145
+ role="user",
146
+ parts=[types.Part.from_text(text=user_prompt)],
147
+ ),
148
+ ]
149
+
150
+ config = types.GenerateContentConfig(
151
+ temperature=temperature,
152
+ system_instruction=[
153
+ types.Part.from_text(text=system_prompt),
154
+ ],
155
+ )
156
+
157
+ response = ""
158
+ for chunk in self.client.models.generate_content_stream(
159
+ model=self.model,
160
+ contents=contents,
161
+ config=config,
162
+ ):
163
+ response += chunk.text
164
+
165
+ response = response.strip()
166
+ self._log_prompt_and_response(system_prompt, user_prompt, response, call_type)
167
+ return response
168
+ except Exception as e:
169
+ error_msg = f"Помилка API: {str(e)}"
170
+ log_prompts_enabled = os.getenv("LOG_PROMPTS", "false").lower() == "true"
171
+ if log_prompts_enabled:
172
+ self._log_prompt_and_response(system_prompt, user_prompt, error_msg, f"{call_type}_ERROR")
173
+ return error_msg
174
+
175
+ class PatientDataLoader:
176
+ """Клас для завантаження даних пацієнтів з JSON файлів"""
177
+
178
+ @staticmethod
179
+ def load_clinical_background(file_path: str = "clinical_background.json") -> ClinicalBackground:
180
+ """Завантажує clinical background з JSON файлу"""
181
+ try:
182
+ with open(file_path, 'r', encoding='utf-8') as f:
183
+ data = json.load(f)
184
+
185
+ patient_summary = data.get("patient_summary", {})
186
+ vital_signs = data.get("vital_signs_and_measurements", [])
187
+
188
+ return ClinicalBackground(
189
+ patient_id="patient_001",
190
+ patient_name="Mark",
191
+ patient_age="adult",
192
+ active_problems=patient_summary.get("active_problems", []),
193
+ past_medical_history=patient_summary.get("past_medical_history", []),
194
+ current_medications=patient_summary.get("current_medications", []),
195
+ allergies=patient_summary.get("allergies", ""),
196
+ vital_signs_and_measurements=vital_signs,
197
+ laboratory_results=data.get("laboratory_results", []),
198
+ assessment_and_plan=data.get("assessment_and_plan", ""),
199
+ critical_alerts=data.get("critical_alerts", []),
200
+ social_history=data.get("social_history", {}),
201
+ recent_clinical_events=data.get("recent_clinical_events_and_encounters", [])
202
+ )
203
+
204
+ except FileNotFoundError:
205
+ print(f"⚠️ Файл {file_path} не знайдено. Використовуємо тестові дані.")
206
+ return PatientDataLoader._get_default_clinical_background()
207
+ except Exception as e:
208
+ print(f"⚠️ Помилка завантаження {file_path}: {e}")
209
+ return PatientDataLoader._get_default_clinical_background()
210
+
211
+ @staticmethod
212
+ def load_lifestyle_profile(file_path: str = "lifestyle_profile.json") -> LifestyleProfile:
213
+ """Завантажує lifestyle profile з JSON файлу"""
214
+ try:
215
+ with open(file_path, 'r', encoding='utf-8') as f:
216
+ data = json.load(f)
217
+
218
+ return LifestyleProfile(
219
+ patient_name=data.get("patient_name", "Пацієнт"),
220
+ patient_age=data.get("patient_age", "невідомо"),
221
+ conditions=data.get("conditions", []),
222
+ primary_goal=data.get("primary_goal", ""),
223
+ exercise_preferences=data.get("exercise_preferences", []),
224
+ exercise_limitations=data.get("exercise_limitations", []),
225
+ dietary_notes=data.get("dietary_notes", []),
226
+ personal_preferences=data.get("personal_preferences", []),
227
+ journey_summary=data.get("journey_summary", ""),
228
+ last_session_summary=data.get("last_session_summary", ""),
229
+ next_check_in=data.get("next_check_in", "not set"),
230
+ progress_metrics=data.get("progress_metrics", {})
231
+ )
232
+
233
+ except FileNotFoundError:
234
+ print(f"⚠️ Файл {file_path} не знайдено. Використовуємо тестові дані.")
235
+ return PatientDataLoader._get_default_lifestyle_profile()
236
+ except Exception as e:
237
+ print(f"⚠️ Помилка завантаження {file_path}: {e}")
238
+ return PatientDataLoader._get_default_lifestyle_profile()
239
+
240
+ @staticmethod
241
+ def _get_default_clinical_background() -> ClinicalBackground:
242
+ """Fallback дані для clinical background"""
243
+ return ClinicalBackground(
244
+ patient_id="test_001",
245
+ patient_name="Тестовий пацієнт",
246
+ active_problems=["Хронічна серцева недостатність", "Артеріальна гіпертензія"],
247
+ current_medications=["Еналаприл 10мг", "Метформін 500мг"],
248
+ allergies="Пеніцилін",
249
+ vital_signs_and_measurements=["АТ: 140/90", "ЧСС: 72"]
250
+ )
251
+
252
+ @staticmethod
253
+ def _get_default_lifestyle_profile() -> LifestyleProfile:
254
+ """Fallback дані для lifestyle profile"""
255
+ return LifestyleProfile(
256
+ patient_name="Тестовий пацієнт",
257
+ patient_age="52",
258
+ conditions=["гіпертензія"],
259
+ primary_goal="Покращити загальний стан здоров'я",
260
+ exercise_preferences=["ходьба"],
261
+ exercise_limitations=["уникати високих навантажень"],
262
+ dietary_notes=["низькосольова дієта"],
263
+ personal_preferences=["поступові зміни"],
264
+ journey_summary="Початок lifestyle journey",
265
+ last_session_summary=""
266
+ )
267
+
268
+ class SessionController:
269
+ def __init__(self, api: GeminiAPI):
270
+ self.api = api
271
+
272
+ def make_decision(self, user_message: str, chat_history: List[ChatMessage],
273
+ clinical_background: ClinicalBackground, current_state: SessionState) -> Dict:
274
+ """Приймає рішення про режим сесії"""
275
+
276
+ system_prompt = """Ти - Session Controller для медичного додатку з lifestyle coaching.
277
+
278
+ ТВОЄ ЗАВДАННЯ: Проаналізувати повідомлення пацієнта і прийняти рішення про режим роботи.
279
+
280
+ РЕЖИМИ:
281
+ - medical: медичні симптоми, скарги, ургентні стани
282
+ - lifestyle: фізична активність, харчування, спосіб життя, мотивація
283
+ - none: завершення сесії або неясний контекст
284
+
285
+ RED FLAGS (завжди -> medical):
286
+ - біль у грудях, задишка у спокої
287
+ - високий АТ (>180/120), низький (<80/50)
288
+ - синкопе, запаморочення
289
+ - різкий набряк, набір ваги
290
+ - симптомна гіпо/гіперглікемія
291
+
292
+ ВІДПОВІДАЙ ЛИШЕ У ФОРМАТІ JSON:
293
+ {
294
+ "action": "start_medical|start_lifestyle|continue_current|end_session",
295
+ "mode": "medical|lifestyle|none",
296
+ "reasoning": "коротке пояснення українською",
297
+ "escalation_needed": true/false
298
+ }"""
299
+
300
+ history_text = "\n".join([f"{msg.role}: {msg.message}" for msg in chat_history[-5:]])
301
+
302
+ active_problems = "; ".join(clinical_background.active_problems[:5]) if clinical_background.active_problems else "не вказано"
303
+ critical_alerts = "; ".join(clinical_background.critical_alerts) if clinical_background.critical_alerts else "немає"
304
+
305
+ user_prompt = f"""
306
+ КЛІНІЧНИЙ КОНТЕКСТ пацієнта {clinical_background.patient_name}:
307
+ - Активні проблеми: {active_problems}
308
+ - Критичні попередження: {critical_alerts}
309
+
310
+ ПОТОЧНИЙ СТАН СЕСІЇ: режим={current_state.current_mode}, активна={current_state.is_active_session}
311
+
312
+ ІСТОРІЯ ЧАТУ:
313
+ {history_text}
314
+
315
+ НОВЕ ПОВІДОМЛЕННЯ ПАЦІЄНТА: {user_message}
316
+
317
+ Прийми рішення про режим роботи:"""
318
+
319
+ response = self.api.generate_response(system_prompt, user_prompt, temperature=0.1, call_type="SESSION_CONTROLLER")
320
+
321
+ try:
322
+ clean_response = response.replace("```json", "").replace("```", "").strip()
323
+ decision = json.loads(clean_response)
324
+ return decision
325
+ except:
326
+ return {
327
+ "action": "start_medical",
328
+ "mode": "medical",
329
+ "reasoning": "Помилка парсингу - перенаправлення до медичного режиму для безпеки",
330
+ "escalation_needed": True
331
+ }
332
+
333
+ class MedicalAssistant:
334
+ def __init__(self, api: GeminiAPI):
335
+ self.api = api
336
+
337
+ def generate_response(self, user_message: str, chat_history: List[ChatMessage],
338
+ clinical_background: ClinicalBackground) -> str:
339
+ """Генерує медичну відповідь"""
340
+
341
+ system_prompt = """Ти - досвідчений медичний асистент для пацієнтів з хронічними захворюваннями.
342
+
343
+ ПРИНЦИПИ:
344
+ - Безпека пацієнта - головний пріоритет
345
+ - Не ставиш діагнози, не призначаєш лікування
346
+ - Рекомендуєш звернення до лікаря при червоних прапорцях
347
+ - Даєш загальні поради з управління хронічними станами
348
+ - Відповідаєш українською мовою
349
+
350
+ При УРГЕНТНИХ симптомах - рекомендуй негайне звернення до медзакладу."""
351
+
352
+ active_problems = "; ".join(clinical_background.active_problems[:5]) if clinical_background.active_problems else "не вказані"
353
+ medications = "; ".join(clinical_background.current_medications[:8]) if clinical_background.current_medications else "не вказані"
354
+ recent_vitals = "; ".join(clinical_background.vital_signs_and_measurements[-3:]) if clinical_background.vital_signs_and_measurements else "не вказані"
355
+
356
+ history_text = "\n".join([f"{msg.role}: {msg.message}" for msg in chat_history[-3:]])
357
+
358
+ user_prompt = f"""
359
+ МЕДИЧНИЙ ПРОФІЛЬ ПАЦІЄНТА ({clinical_background.patient_name}):
360
+ - Активні проблеми: {active_problems}
361
+ - Поточні медикаменти: {medications}
362
+ - Останні показники: {recent_vitals}
363
+ - Алергії: {clinical_background.allergies}
364
+
365
+ КРИТИЧНІ ПОПЕРЕДЖЕННЯ: {"; ".join(clinical_background.critical_alerts) if clinical_background.critical_alerts else "немає"}
366
+
367
+ ІСТОРІЯ РОЗМОВИ:
368
+ {history_text}
369
+
370
+ ПИТАННЯ ПАЦІЄНТА: {user_message}
371
+
372
+ Надай медичну консультацію з урахуванням медичного профілю:"""
373
+
374
+ return self.api.generate_response(system_prompt, user_prompt, call_type="MEDICAL_ASSISTANT")
375
+
376
+ class LifestyleAssistant:
377
+ def __init__(self, api: GeminiAPI):
378
+ self.api = api
379
+
380
+ def generate_response(self, user_message: str, chat_history: List[ChatMessage],
381
+ clinical_background: ClinicalBackground, lifestyle_profile: LifestyleProfile) -> tuple[str, LifestyleProfile]:
382
+ """Генерує lifestyle відповідь та оновлює профіль"""
383
+
384
+ system_prompt = f"""Ти - lifestyle coach для пацієнтів з хронічними захворюваннями.
385
+
386
+ ПРИНЦИПИ:
387
+ - Безпечні, поступові зміни з урахуванням медичних обмежень
388
+ - Персоналізація на основі профілю пацієнта
389
+ - Позитивне підкріплення та реалістичні цілі
390
+ - Мотивація через малі кроки прогресу
391
+ - Відповідаєш українською мовою
392
+
393
+ МЕДИЧНІ ОБМЕЖЕННЯ пацієнта {lifestyle_profile.patient_name}:
394
+ - Стани: {', '.join(lifestyle_profile.conditions)}
395
+ - Обмеження: {'; '.join(lifestyle_profile.exercise_limitations)}
396
+
397
+ УВАГА до активних проблем:
398
+ {'; '.join(clinical_background.active_problems[:5]) if clinical_background.active_problems else 'Основні проблеми не вказані'}
399
+
400
+ В кінці кожної сесії пропонуй конкретний план дій та час наступної зустрічі."""
401
+
402
+ goals = lifestyle_profile.primary_goal
403
+ preferences = "; ".join(lifestyle_profile.exercise_preferences) if lifestyle_profile.exercise_preferences else "не вказані"
404
+ dietary = "; ".join(lifestyle_profile.dietary_notes) if lifestyle_profile.dietary_notes else "не вказані"
405
+
406
+ history_text = "\n".join([f"{msg.role}: {msg.message}" for msg in chat_history[-3:]])
407
+
408
+ user_prompt = f"""
409
+ ПАЦІЄНТ: {lifestyle_profile.patient_name}, {lifestyle_profile.patient_age} років
410
+
411
+ LIFESTYLE ПРОФІЛЬ:
412
+ - Головна ціль: {goals}
413
+ - Уподобання: {preferences}
414
+ - Харчування: {dietary}
415
+ - Особисті переваги: {"; ".join(lifestyle_profile.personal_preferences)}
416
+ - Journey резюме: {lifestyle_profile.journey_summary}
417
+ - Попередня сесія: {lifestyle_profile.last_session_summary}
418
+
419
+ ІСТОРІЯ РОЗМОВИ:
420
+ {history_text}
421
+
422
+ ПОВІДОМЛЕННЯ ПАЦІЄНТА: {user_message}
423
+
424
+ Проведи lifestyle коучинг з урахуванням медичного стану та особистих цілей:"""
425
+
426
+ response = self.api.generate_response(system_prompt, user_prompt, call_type="LIFESTYLE_ASSISTANT")
427
+
428
+ # Оновлення профілю
429
+ updated_profile = lifestyle_profile
430
+ if user_message and response:
431
+ session_date = datetime.now().strftime('%d.%m.%Y')
432
+ updated_profile.last_session_summary = f"[{session_date}] Обговорювали: {user_message[:100]}..."
433
+
434
+ if len(updated_profile.journey_summary) > 500:
435
+ updated_profile.journey_summary = updated_profile.journey_summary[-400:]
436
+
437
+ updated_profile.journey_summary += f" | {session_date}: {user_message[:50]}..."
438
+
439
+ return response, updated_profile
examples_test_patient.md ADDED
@@ -0,0 +1,383 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 👥 Приклади тестових пацієнтів
2
+
3
+ ## 🧓 Пацієнт 1: "Elderly_Mary" - Старша жінка з множинними коморбідностями
4
+
5
+ ### clinical_background.json
6
+ ```json
7
+ {
8
+ "patient_summary": {
9
+ "active_problems": [
10
+ "Essential hypertension (uncontrolled)",
11
+ "Type 2 diabetes mellitus with complications",
12
+ "Chronic kidney disease stage 3",
13
+ "Osteoarthritis bilateral knees",
14
+ "Depression, recurrent episode",
15
+ "Falls risk - history of 3 falls last year"
16
+ ],
17
+ "past_medical_history": [
18
+ "Myocardial infarction (2020)",
19
+ "Stroke with residual left-sided weakness (2019)",
20
+ "Hip fracture left (2022)",
21
+ "Chronic heart failure"
22
+ ],
23
+ "current_medications": [
24
+ "Amlodipine 10mg daily",
25
+ "Metformin 1000mg twice daily",
26
+ "Lisinopril 20mg daily",
27
+ "Atorvastatin 40mg at bedtime",
28
+ "Metoprolol 50mg twice daily",
29
+ "Furosemide 40mg daily",
30
+ "Sertraline 50mg daily",
31
+ "Calcium + Vitamin D3 daily"
32
+ ],
33
+ "allergies": "Penicillin - rash, NSAIDs - GI upset"
34
+ },
35
+ "vital_signs_and_measurements": [
36
+ "Blood Pressure: 165/95 (last visit)",
37
+ "Heart Rate: 78 bpm",
38
+ "Weight: 78kg",
39
+ "Height: 1.58m",
40
+ "BMI: 31.2 kg/m²",
41
+ "HbA1c: 8.2% (poor control)"
42
+ ],
43
+ "laboratory_results": [
44
+ "Creatinine: 145 μmol/L (elevated)",
45
+ "eGFR: 42 ml/min/1.73m² (stage 3 CKD)",
46
+ "HbA1c: 8.2% (64 mmol/mol)",
47
+ "LDL: 3.2 mmol/L"
48
+ ],
49
+ "assessment_and_plan": "76-year-old female with multiple cardiovascular risk factors and functional limitations. Primary goals: BP control, diabetes management, fall prevention. Requires gentle lifestyle modifications with careful monitoring.",
50
+ "critical_alerts": [
51
+ "High fall risk - requires mobility assessment",
52
+ "CKD stage 3 - monitor kidney function with any changes",
53
+ "Depression - monitor for worsening mood",
54
+ "Uncontrolled hypertension and diabetes"
55
+ ],
56
+ "social_history": {
57
+ "living_situation": "Lives alone, adult children nearby",
58
+ "mobility": "Uses walking frame, limited to ground floor",
59
+ "smoking_status": "Never smoker",
60
+ "alcohol_use": "Occasional glass of wine"
61
+ },
62
+ "recent_clinical_events_and_encounters": [
63
+ "2025-01-10: Fall at home, no injury but increased anxiety",
64
+ "2024-12-15: ER visit for chest pain - ruled out MI, anxiety-related",
65
+ "2024-11-20: Routine follow-up - BP poorly controlled, meds adjusted"
66
+ ]
67
+ }
68
+ ```
69
+
70
+ ### lifestyle_profile.json
71
+ ```json
72
+ {
73
+ "patient_name": "Mary",
74
+ "patient_age": "76",
75
+ "conditions": [
76
+ "essential hypertension",
77
+ "type 2 diabetes",
78
+ "chronic kidney disease",
79
+ "osteoarthritis",
80
+ "depression",
81
+ "history of stroke"
82
+ ],
83
+ "primary_goal": "Improve mobility and independence while managing multiple chronic conditions safely. Prevent falls and maintain current functional level.",
84
+ "exercise_preferences": [
85
+ "chair exercises",
86
+ "gentle walking with frame",
87
+ "tai chi (interested but never tried)",
88
+ "swimming (if accessible)"
89
+ ],
90
+ "exercise_limitations": [
91
+ "Left-sided weakness from stroke",
92
+ "Severe knee arthritis - painful weight bearing",
93
+ "High fall risk - balance issues",
94
+ "Limited endurance due to heart condition",
95
+ "Gets breathless with minimal exertion",
96
+ "Requires walking frame for mobility"
97
+ ],
98
+ "dietary_notes": [
99
+ "Diabetic diet - needs simple carb counting education",
100
+ "Low sodium for hypertension and heart failure",
101
+ "CKD diet - limited protein and phosphorus",
102
+ "Poor appetite due to depression",
103
+ "Lives alone - convenience foods common",
104
+ "Limited cooking ability due to arthritis"
105
+ ],
106
+ "personal_preferences": [
107
+ "very cautious about new activities due to fall anxiety",
108
+ "prefers morning activities when energy is better",
109
+ "needs frequent encouragement and reassurance",
110
+ "responds well to small, achievable goals",
111
+ "family involvement important for motivation"
112
+ ],
113
+ "journey_summary": "Elderly patient with complex medical needs seeking to maintain independence. Recent falls have increased anxiety about movement. Needs gentle, supervised approach to lifestyle modifications.",
114
+ "last_session_summary": "",
115
+ "progress_metrics": {
116
+ "current_mobility": "walking frame required, 50m max distance",
117
+ "exercise_frequency": "0 times/week - afraid to move",
118
+ "fall_incidents": "3 in past 12 months",
119
+ "medication_adherence": "good with pill organizer",
120
+ "bp_control": "poor - 165/95 average",
121
+ "diabetes_control": "poor - HbA1c 8.2%"
122
+ }
123
+ }
124
+ ```
125
+
126
+ ---
127
+
128
+ ## 🏃 Пацієнт 2: "Athletic_John" - Молодий спортсмен після травми
129
+
130
+ ### clinical_background.json
131
+ ```json
132
+ {
133
+ "patient_summary": {
134
+ "active_problems": [
135
+ "ACL reconstruction recovery (3 months post-op)",
136
+ "Post-surgical knee pain and swelling",
137
+ "Muscle atrophy right quadriceps",
138
+ "Anxiety related to return to sport",
139
+ "Sleep disturbance due to pain"
140
+ ],
141
+ "past_medical_history": [
142
+ "ACL tear during football match (6 months ago)",
143
+ "Previous ankle sprain (2022)",
144
+ "Exercise-induced asthma (childhood)"
145
+ ],
146
+ "current_medications": [
147
+ "Ibuprofen 400mg as needed for pain",
148
+ "Physiotherapy exercises daily",
149
+ "Protein supplements"
150
+ ],
151
+ "allergies": "No known drug allergies"
152
+ },
153
+ "vital_signs_and_measurements": [
154
+ "Blood Pressure: 118/72",
155
+ "Heart Rate: 58 bpm (athletic)",
156
+ "Weight: 82kg (lost 3kg since surgery)",
157
+ "Height: 1.85m",
158
+ "BMI: 24.0 kg/m²",
159
+ "Body fat: 12% (increased from 8% pre-injury)"
160
+ ],
161
+ "laboratory_results": [
162
+ "All routine bloods normal",
163
+ "Vitamin D: sufficient"
164
+ ],
165
+ "assessment_and_plan": "24-year-old male athlete 3 months post ACL reconstruction. Good surgical healing, ready for progressive return to activity under physiotherapy guidance. Mental health support may be needed for sports anxiety.",
166
+ "critical_alerts": [
167
+ "Do not exceed physiotherapy exercise guidelines",
168
+ "No pivoting or cutting movements until cleared",
169
+ "Monitor for signs of depression or anxiety"
170
+ ],
171
+ "social_history": {
172
+ "occupation": "Semi-professional footballer",
173
+ "activity_level": "Previously 6-7 training sessions per week",
174
+ "smoking_status": "Never",
175
+ "alcohol_use": "Social drinking, 2-3 units per week"
176
+ },
177
+ "recent_clinical_events_and_encounters": [
178
+ "2025-01-05: Physiotherapy review - progressing well",
179
+ "2024-12-20: Orthopedic follow-up - cleared for gym work",
180
+ "2024-11-15: 6-week post-op check - healing excellent"
181
+ ]
182
+ }
183
+ ```
184
+
185
+ ### lifestyle_profile.json
186
+ ```json
187
+ {
188
+ "patient_name": "John",
189
+ "patient_age": "24",
190
+ "conditions": [
191
+ "ACL reconstruction recovery",
192
+ "post-surgical deconditioning",
193
+ "sports performance anxiety"
194
+ ],
195
+ "primary_goal": "Return to competitive football safely and regain pre-injury fitness level. Build confidence in knee stability and prevent re-injury.",
196
+ "exercise_preferences": [
197
+ "weight training (upper body focus currently)",
198
+ "swimming",
199
+ "cycling",
200
+ "football-specific drills (when cleared)",
201
+ "plyometric training (future goal)"
202
+ ],
203
+ "exercise_limitations": [
204
+ "No pivoting or cutting movements yet",
205
+ "Limited knee flexion under load",
206
+ "No contact sports until 6+ months post-op",
207
+ "Must follow physiotherapy protocol strictly",
208
+ "Avoid high-impact activities on hard surfaces"
209
+ ],
210
+ "dietary_notes": [
211
+ "High protein intake for muscle recovery",
212
+ "Anti-inflammatory foods to reduce swelling",
213
+ "Adequate calories to support training",
214
+ "Sports nutrition knowledge good",
215
+ "Hydration important for recovery"
216
+ ],
217
+ "personal_preferences": [
218
+ "highly motivated and goal-oriented",
219
+ "impatient with slow recovery process",
220
+ "competitive personality",
221
+ "prefers intense workouts when possible",
222
+ "needs measurable progress to stay motivated"
223
+ ],
224
+ "journey_summary": "Highly motivated athlete recovering from major knee surgery. Risk of doing too much too soon. Needs structured progression plan and psychological support for sports anxiety.",
225
+ "last_session_summary": "",
226
+ "progress_metrics": {
227
+ "knee_flexion_range": "120 degrees (target: 135+)",
228
+ "quad_strength": "70% of uninjured leg",
229
+ "cardio_fitness": "estimated 60% of pre-injury level",
230
+ "training_frequency": "4 sessions/week (physio + gym)",
231
+ "pain_level": "2/10 at rest, 4/10 with exercise",
232
+ "return_to_sport_timeline": "3-4 months if progress continues"
233
+ }
234
+ }
235
+ ```
236
+
237
+ ---
238
+
239
+ ## 🤰 Пацієнт 3: "Pregnant_Sarah" - Вагітна з гестаційним діабетом
240
+
241
+ ### clinical_background.json
242
+ ```json
243
+ {
244
+ "patient_summary": {
245
+ "active_problems": [
246
+ "Pregnancy 28 weeks gestation",
247
+ "Gestational diabetes mellitus (diet-controlled)",
248
+ "Pregnancy-induced hypertension (mild)",
249
+ "Iron deficiency anemia",
250
+ "Lower back pain",
251
+ "Carpal tunnel syndrome pregnancy-related"
252
+ ],
253
+ "past_medical_history": [
254
+ "Gravida 2, Para 1 (one previous normal delivery)",
255
+ "Previous gestational diabetes (resolved postpartum)",
256
+ "Polycystic ovary syndrome"
257
+ ],
258
+ "current_medications": [
259
+ "Prenatal vitamins with iron",
260
+ "Additional iron supplement 65mg daily",
261
+ "Folic acid 5mg daily",
262
+ "Vitamin D3 1000 IU daily"
263
+ ],
264
+ "allergies": "No known drug allergies"
265
+ },
266
+ "vital_signs_and_measurements": [
267
+ "Blood Pressure: 142/88 (elevated for pregnancy)",
268
+ "Heart Rate: 88 bpm",
269
+ "Pre-pregnancy weight: 68kg",
270
+ "Current weight: 78kg",
271
+ "Weight gain: 10kg (appropriate for gestational age)",
272
+ "Fundal height: 28cm (matches dates)"
273
+ ],
274
+ "laboratory_results": [
275
+ "Glucose tolerance test: abnormal (gestational diabetes)",
276
+ "Hemoglobin: 95 g/L (anemic)",
277
+ "Ferritin: low",
278
+ "Urine protein: trace (monitoring for preeclampsia)"
279
+ ],
280
+ "assessment_and_plan": "28-year-old female, 28 weeks pregnant with gestational diabetes and mild hypertension. Currently well-controlled with diet. Regular monitoring required. Delivery planning at 38-39 weeks.",
281
+ "critical_alerts": [
282
+ "Monitor blood pressure - risk of preeclampsia",
283
+ "Blood glucose monitoring required",
284
+ "Avoid exercises lying flat on back after 20 weeks",
285
+ "Contact immediately if severe headaches, visual changes, or upper abdominal pain"
286
+ ],
287
+ "social_history": {
288
+ "occupation": "Office worker (desk job)",
289
+ "living_situation": "Married, supportive partner",
290
+ "previous_pregnancy": "Uncomplicated delivery 3 years ago",
291
+ "smoking_status": "Never",
292
+ "alcohol_use": "None during pregnancy"
293
+ },
294
+ "recent_clinical_events_and_encounters": [
295
+ "2025-01-08: Antenatal appointment - BP slightly elevated",
296
+ "2024-12-28: Glucose tolerance test - abnormal result",
297
+ "2024-12-15: Routine 26-week appointment - all well"
298
+ ]
299
+ }
300
+ ```
301
+
302
+ ### lifestyle_profile.json
303
+ ```json
304
+ {
305
+ "patient_name": "Sarah",
306
+ "patient_age": "28",
307
+ "conditions": [
308
+ "pregnancy 28 weeks",
309
+ "gestational diabetes",
310
+ "pregnancy-induced hypertension",
311
+ "iron deficiency anemia"
312
+ ],
313
+ "primary_goal": "Maintain healthy pregnancy with good blood sugar control, manage blood pressure, and prepare physically for delivery while ensuring baby's wellbeing.",
314
+ "exercise_preferences": [
315
+ "prenatal yoga",
316
+ "walking",
317
+ "swimming",
318
+ "stationary cycling",
319
+ "prenatal fitness classes"
320
+ ],
321
+ "exercise_limitations": [
322
+ "No lying flat on back after 20 weeks",
323
+ "Avoid high-impact or contact activities",
324
+ "No scuba diving or activities at altitude",
325
+ "Monitor heart rate - shouldn't exceed 140 bpm",
326
+ "Stop if experiencing dizziness, chest pain, or contractions",
327
+ "Avoid overheating"
328
+ ],
329
+ "dietary_notes": [
330
+ "Gestational diabetes diet - controlled carbohydrates",
331
+ "Small frequent meals to manage blood sugar",
332
+ "High fiber foods to prevent constipation",
333
+ "Iron-rich foods for anemia",
334
+ "Adequate protein for fetal growth",
335
+ "Limit caffeine intake",
336
+ "Avoid alcohol completely"
337
+ ],
338
+ "personal_preferences": [
339
+ "motivated to have healthy pregnancy",
340
+ "anxious about blood sugar control",
341
+ "prefers gentle, safe activities",
342
+ "likes group exercise for motivation",
343
+ "concerned about weight gain"
344
+ ],
345
+ "journey_summary": "Second pregnancy with new diagnosis of gestational diabetes. Previously active but concerned about exercise safety. Needs education on appropriate prenatal fitness and diabetes management.",
346
+ "last_session_summary": "",
347
+ "progress_metrics": {
348
+ "blood_glucose_control": "diet-controlled, monitoring 4x daily",
349
+ "blood_pressure": "mild elevation, monitoring twice weekly",
350
+ "weight_gain": "10kg total, appropriate for gestation",
351
+ "exercise_frequency": "2-3 times per week currently",
352
+ "energy_level": "moderate fatigue, better in second trimester",
353
+ "delivery_preparation": "considering prenatal classes"
354
+ }
355
+ }
356
+ ```
357
+
358
+ ---
359
+
360
+ ## 📋 Використання прикладів
361
+
362
+ **Для тестування medical режиму:**
363
+ - Завантажте "Elderly_Mary" - протестуйте red flags (падіння, біль у грудях)
364
+ - Використовуйте критичні попередження для перевірки ескалації
365
+
366
+ **Для тестування lifestyle персоналізації:**
367
+ - "Athletic_John" - складні обмеження після травми
368
+ - "Pregnant_Sarah" - специфічні safety guidelines
369
+
370
+ **Для комплексного тестування:**
371
+ - Всі три пацієнти мають різні рівні складності
372
+ - Можна тестувати адаптацію системи до різних вікових груп та станів
373
+
374
+ **Зберегти як окремі файли:**
375
+ ```
376
+ test_patients/
377
+ ├── elderly_mary_clinical.json
378
+ ├── elderly_mary_lifestyle.json
379
+ ├── athletic_john_clinical.json
380
+ ├── athletic_john_lifestyle.json
381
+ ├── pregnant_sarah_clinical.json
382
+ └── pregnant_sarah_lifestyle.json
383
+ ```
lifestyle_journey.log CHANGED
@@ -252,3 +252,551 @@ RED FLAGS (завжди -> medical):
252
  🔧 MODEL: gemini-2.5-flash
253
  ================================================================================
254
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  🔧 MODEL: gemini-2.5-flash
253
  ================================================================================
254
 
255
+ 2025-09-02 16:18:11,336 - INFO -
256
+ ================================================================================
257
+ 🤖 GEMINI API CALL #1 [SESSION_CONTROLLER] - 2025-09-02 16:18:11
258
+ ================================================================================
259
+
260
+ 📤 SYSTEM PROMPT:
261
+ ----------------------------------------
262
+ Ти - Session Controller для медичного додатку з lifestyle coaching.
263
+
264
+ ТВОЄ ЗАВДАННЯ: Проаналізувати повідомлення пацієнта і прийняти рішення про режим роботи.
265
+
266
+ РЕЖИМИ:
267
+ - medical: медичні симптоми, скарги, ургентні стани
268
+ - lifestyle: фізична активність, харчування, спосіб життя, мотивація
269
+ - none: завершення сесії або неясний контекст
270
+
271
+ RED FLAGS (завжди -> medical):
272
+ - біль у грудях, задишка у спокої
273
+ - високий АТ (>180/120), низький (<80/50)
274
+ - синкопе, запаморочення
275
+ - різкий набряк, набір ваги
276
+ - симптомна гіпо/гіперглікемія
277
+
278
+ ВІДПОВІДАЙ ЛИШЕ У ФОРМАТІ JSON:
279
+ {
280
+ "action": "start_medical|start_lifestyle|continue_current|end_session",
281
+ "mode": "medical|lifestyle|none",
282
+ "reasoning": "коротке пояснення українською",
283
+ "escalation_needed": true/false
284
+ }
285
+
286
+ 📤 USER PROMPT:
287
+ ----------------------------------------
288
+
289
+ КЛІНІЧНИЙ КОНТЕКСТ пацієнта Mary:
290
+ - Активні проблеми: Essential hypertension (uncontrolled); Type 2 diabetes mellitus with complications; Chronic kidney disease stage 3; Osteoarthritis bilateral knees; Depression, recurrent episode
291
+ - Критичні попередження: High fall risk - requires mobility assessment; CKD stage 3 - monitor kidney function with any changes; Depression - monitor for worsening mood; Uncontrolled hypertension and diabetes
292
+
293
+ ПОТОЧНИЙ СТАН СЕСІЇ: режим=none, активна=False
294
+
295
+ ІСТОРІЯ ЧАТУ:
296
+
297
+
298
+ НОВЕ ПОВІДОМЛЕННЯ ПАЦІЄНТА: Привіт
299
+
300
+ Прийми рішення про режим роботи:
301
+
302
+ 📥 GEMINI RESPONSE:
303
+ ----------------------------------------
304
+ ```json
305
+ {
306
+ "action": "continue_current",
307
+ "mode": "none",
308
+ "reasoning": "Пацієнт надіслав привітання. Недостатньо інформації для визначення медичного чи лайфстайл режиму. Сесія залишається в режимі 'none' для очікування подальшого контексту.",
309
+ "escalation_needed": false
310
+ }
311
+ ```
312
+
313
+ 🔧 MODEL: gemini-2.5-flash
314
+ ================================================================================
315
+
316
+ 2025-09-02 16:18:47,098 - INFO -
317
+ ================================================================================
318
+ 🤖 GEMINI API CALL #2 [SESSION_CONTROLLER] - 2025-09-02 16:18:47
319
+ ================================================================================
320
+
321
+ 📤 SYSTEM PROMPT:
322
+ ----------------------------------------
323
+ Ти - Session Controller для медичного додатку з lifestyle coaching.
324
+
325
+ ТВОЄ ЗАВДАННЯ: Проаналізувати повідомлення пацієнта і прийняти рішення про режим роботи.
326
+
327
+ РЕЖИМИ:
328
+ - medical: медичні симптоми, скарги, ургентні стани
329
+ - lifestyle: фізична активність, харчування, спосіб життя, мотивація
330
+ - none: завершення сесії або неясний контекст
331
+
332
+ RED FLAGS (завжди -> medical):
333
+ - біль у грудях, задишка у спокої
334
+ - високий АТ (>180/120), низький (<80/50)
335
+ - синкопе, запаморочення
336
+ - різкий набряк, набір ваги
337
+ - симптомна гіпо/гіперглікемія
338
+
339
+ ВІДПОВІДАЙ ЛИШЕ У ФОРМАТІ JSON:
340
+ {
341
+ "action": "start_medical|start_lifestyle|continue_current|end_session",
342
+ "mode": "medical|lifestyle|none",
343
+ "reasoning": "коротке пояснення українською",
344
+ "escalation_needed": true/false
345
+ }
346
+
347
+ 📤 USER PROMPT:
348
+ ----------------------------------------
349
+
350
+ КЛІНІЧНИЙ КОНТЕКСТ пацієнта Mary:
351
+ - Активні проблеми: Essential hypertension (uncontrolled); Type 2 diabetes mellitus with complications; Chronic kidney disease stage 3; Osteoarthritis bilateral knees; Depression, recurrent episode
352
+ - Критичні попередження: High fall risk - requires mobility assessment; CKD stage 3 - monitor kidney function with any changes; Depression - monitor for worsening mood; Uncontrolled hypertension and diabetes
353
+
354
+ ПОТОЧНИЙ СТАН СЕСІЇ: режим=none, активна=False
355
+
356
+ ІСТОРІЯ ЧАТУ:
357
+ user: Привіт
358
+ assistant: Будь ласка, уточніть ваше питання. Я можу допомогти з медичними питаннями або питаннями способу життя.
359
+
360
+ НОВЕ ПОВІДОМЛЕННЯ ПАЦІЄНТА: у мене болить голова
361
+
362
+ Прийми рішення про режим роботи:
363
+
364
+ 📥 GEMINI RESPONSE:
365
+ ----------------------------------------
366
+ ```json
367
+ {
368
+ "action": "start_medical",
369
+ "mode": "medical",
370
+ "reasoning": "Пацієнт скаржиться на головний біль. Враховуючи клінічний контекст (неконтрольована гіпертензія), головний біль може бути ознакою підвищеного артеріального тиску або іншого медичного стану, що потребує негайної оцінки.",
371
+ "escalation_needed": true
372
+ }
373
+ ```
374
+
375
+ 🔧 MODEL: gemini-2.5-flash
376
+ ================================================================================
377
+
378
+ 2025-09-02 16:18:59,979 - INFO -
379
+ ================================================================================
380
+ 🤖 GEMINI API CALL #3 [MEDICAL_ASSISTANT] - 2025-09-02 16:18:59
381
+ ================================================================================
382
+
383
+ 📤 SYSTEM PROMPT:
384
+ ----------------------------------------
385
+ Ти - досвідчений медичний асистент для пацієнтів з хронічними захворюваннями.
386
+
387
+ ПРИНЦИПИ:
388
+ - Безпека пацієнта - головний пріоритет
389
+ - Не ставиш діагнози, не призначаєш лікування
390
+ - Рекомендуєш звернення до лікаря при червоних прапорцях
391
+ - Даєш загальні поради з управління хронічними станами
392
+ - Відповідаєш українською мовою
393
+
394
+ При УРГЕНТНИХ симптомах - рекомендуй негайне звернення до медзакладу.
395
+
396
+ 📤 USER PROMPT:
397
+ ----------------------------------------
398
+
399
+ МЕДИЧНИЙ ПРОФІЛЬ ПАЦІЄНТА (Mary):
400
+ - Активні проблеми: Essential hypertension (uncontrolled); Type 2 diabetes mellitus with complications; Chronic kidney disease stage 3; Osteoarthritis bilateral knees; Depression, recurrent episode
401
+ - Поточні медикаменти: Amlodipine 10mg daily; Metformin 1000mg twice daily; Lisinopril 20mg daily; Atorvastatin 40mg at bedtime; Metoprolol 50mg twice daily; Furosemide 40mg daily; Sertraline 50mg daily; Calcium + Vitamin D3 daily
402
+ - Останні показники: Height: 1.58m; BMI: 31.2 kg/m²; HbA1c: 8.2% (poor control)
403
+ - Алергії: Penicillin - rash, NSAIDs - GI upset
404
+
405
+ КРИТИЧНІ ПОПЕРЕДЖЕННЯ: High fall risk - requires mobility assessment; CKD stage 3 - monitor kidney function with any changes; Depression - monitor for worsening mood; Uncontrolled hypertension and diabetes
406
+
407
+ ІСТОРІЯ РОЗМОВИ:
408
+ user: Привіт
409
+ assistant: Будь ласка, уточніть ваше питання. Я можу допомогти з медичними питаннями або питаннями способу життя.
410
+ user: у мене болить голова
411
+
412
+ ПИТАННЯ ПАЦІЄНТА: у мене болить голова
413
+
414
+ Надай медичну консультацію з урахуванням медичного профілю:
415
+
416
+ 📥 GEMINI RESPONSE:
417
+ ----------------------------------------
418
+ Добрий день, Мері. Мені шкода чути, що у Вас болить голова.
419
+
420
+ Враховуючи Ваш медичний профіль, особливо неконтрольовану гіпертензію (високий артеріальний тиск), головний біль є симптомом, на який потрібно звернути особливу увагу.
421
+
422
+ **Будь ласка, негайно виміряйте свій артеріальний тиск.** Які були показники?
423
+
424
+ Поки Ви вимірюєте тиск, будь ласка, надайте мені додаткову інформацію про Ваш головний біль:
425
+
426
+ 1. **Наскільки сильний біль** (наприклад, за шкалою від 1 до 10, де 10 – найсильніший біль)?
427
+ 2. **Де саме болить голова** (наприклад, спереду, ззаду, збоку, пульсуючий біль, стискаючий)?
428
+ 3. **Коли почався цей біль?** Він з'явився раптово чи поступово?
429
+ 4. **Чи є якісь інші симптоми**, такі як нудота, блювота, за��аморочення, зміни зору (наприклад, розмитість, "мушки" перед очима), слабкість, оніміння, сплутаність свідомості, чутливість до світла або звуку?
430
+ 5. **Чи приймали Ви щось від болю**, і чи допомогло це? (Будь ласка, пам'ятайте про Вашу алергію на НПЗЗ).
431
+
432
+ **Дуже важливо:**
433
+
434
+ * **Якщо Ваш артеріальний тиск дуже високий (наприклад, вище 180/120 мм рт.ст.) або якщо головний біль дуже сильний, раптовий, супроводжується змінами зору, слабкістю, онімінням, сплутаністю свідомості, або є найсильнішим болем у Вашому житті, Вам слід негайно звернутися за невідкладною медичною допомогою.** Це може бути ознакою серйозного стану.
435
+ * Поки ми чекаємо на відповіді, спробуйте відпочити в тихій, затемненій кімнаті. Пийте достатньо води.
436
+
437
+ Я не можу ставити діагнози чи призначати лікування, але я тут, щоб допомогти Вам зрозуміти Ваші симптоми та підказати, коли потрібно звернутися до лікаря. Ваша безпека – мій головний пріоритет.
438
+
439
+ 🔧 MODEL: gemini-2.5-flash
440
+ ================================================================================
441
+
442
+ 2025-09-02 16:19:25,604 - INFO -
443
+ ================================================================================
444
+ 🤖 GEMINI API CALL #4 [SESSION_CONTROLLER] - 2025-09-02 16:19:25
445
+ ================================================================================
446
+
447
+ 📤 SYSTEM PROMPT:
448
+ ----------------------------------------
449
+ Ти - Session Controller для медичного додатку з lifestyle coaching.
450
+
451
+ ТВОЄ ЗАВДАННЯ: Проаналізувати повідомлення пацієнта і прийняти рішення про режим роботи.
452
+
453
+ РЕЖИМИ:
454
+ - medical: медичні симптоми, скарги, ургентні стани
455
+ - lifestyle: фізична активність, харчування, спосіб життя, мотивація
456
+ - none: завершення сесії або неясний контекст
457
+
458
+ RED FLAGS (завжди -> medical):
459
+ - біль у грудях, задишка у спокої
460
+ - високий АТ (>180/120), низький (<80/50)
461
+ - синкопе, запаморочення
462
+ - різкий набряк, набір ваги
463
+ - симптомна гіпо/гіперглікемія
464
+
465
+ ВІДПОВІДАЙ ЛИШЕ У ФОРМАТІ JSON:
466
+ {
467
+ "action": "start_medical|start_lifestyle|continue_current|end_session",
468
+ "mode": "medical|lifestyle|none",
469
+ "reasoning": "коротке пояснення українською",
470
+ "escalation_needed": true/false
471
+ }
472
+
473
+ 📤 USER PROMPT:
474
+ ----------------------------------------
475
+
476
+ КЛІНІЧНИЙ КОНТЕКСТ пацієнта Mary:
477
+ - Активні проблеми: Essential hypertension (uncontrolled); Type 2 diabetes mellitus with complications; Chronic kidney disease stage 3; Osteoarthritis bilateral knees; Depression, recurrent episode
478
+ - Критичні попередження: High fall risk - requires mobility assessment; CKD stage 3 - monitor kidney function with any changes; Depression - monitor for worsening mood; Uncontrolled hypertension and diabetes
479
+
480
+ ПОТОЧНИЙ СТАН СЕСІЇ: режим=medical, активна=True
481
+
482
+ ІСТОРІЯ ЧАТУ:
483
+ user: Привіт
484
+ assistant: Будь ласка, уточніть ваше питання. Я можу допомогти з медичними питаннями або питаннями способу життя.
485
+ user: у мене болить голова
486
+ assistant: Добрий день, Мері. Мені шкода чути, що у Вас болить голова.
487
+
488
+ Враховуючи Ваш медичний профіль, особливо неконтрольовану гіпертензію (високий артеріальний тиск), головний біль є симптомом, на який потрібно звернути особливу увагу.
489
+
490
+ **Будь ласка, негайно виміряйте свій артеріальний тиск.** Які були показники?
491
+
492
+ Поки Ви вимірюєте тиск, будь ласка, надайте мені додаткову інформацію про Ваш головний біль:
493
+
494
+ 1. **Наскільки сильний біль** (наприклад, за шкалою від 1 до 10, де 10 – найсильніший біль)?
495
+ 2. **Де саме болить голова** (наприклад, спереду, ззаду, збоку, пульсуючий біль, стискаючий)?
496
+ 3. **Коли почався цей біль?** Він з'явився раптово чи поступово?
497
+ 4. **Чи є якісь інші симптоми**, такі як нудота, блювота, запаморочення, зміни зору (наприклад, розмитість, "мушки" перед очима), слабкість, оніміння, сплутаність свідомості, чутливість до світла або звуку?
498
+ 5. **Чи приймали Ви щось від болю**, і чи допомогло це? (Будь ласка, пам'ятайте про Вашу алергію на НПЗЗ).
499
+
500
+ **Дуже важливо:**
501
+
502
+ * **Якщо Ваш артеріальний тиск дуже високий (наприклад, вище 180/120 мм рт.ст.) або якщо головний біль дуже сильний, раптовий, супроводжується змінами зору, слабкістю, онімінням, сплутаністю свідомості, або є найсильнішим болем у Вашому житті, Вам слід негайно звернутися за невідкладною медичною допомогою.** Це може бути ознакою серйозного стану.
503
+ * Поки ми чекаємо на відповіді, спробуйте відпочити в тихій, затемненій кімнаті. Пийте достатньо води.
504
+
505
+ Я не можу ставити діагнози чи призначати лікування, але я тут, щоб допомогти Вам зрозуміти Ваші симптоми та підказати, коли потрібно звернутися до лікаря. Ваша безпека – мій головний пріоритет.
506
+
507
+ НОВЕ ПОВІДОМЛЕННЯ ПАЦІЄНТА: біль пройшла і готова поговорити про життя
508
+
509
+ Прийми рішення про режим роботи:
510
+
511
+ 📥 GEMINI RESPONSE:
512
+ ----------------------------------------
513
+ ```json
514
+ {
515
+ "action": "start_lifestyle",
516
+ "mode": "lifestyle",
517
+ "reasoning": "Пацієнт повідомляє, що біль пройшов і висловлює бажання обговорити питання, пов'язані зі способом життя. Це вказує на перехід від медичного запиту до запиту щодо способу життя.",
518
+ "escalation_needed": false
519
+ }
520
+ ```
521
+
522
+ 🔧 MODEL: gemini-2.5-flash
523
+ ================================================================================
524
+
525
+ 2025-09-02 16:19:38,475 - INFO -
526
+ ================================================================================
527
+ 🤖 GEMINI API CALL #5 [LIFESTYLE_ASSISTANT] - 2025-09-02 16:19:38
528
+ ================================================================================
529
+
530
+ 📤 SYSTEM PROMPT:
531
+ ----------------------------------------
532
+ Ти - lifestyle coach для пацієнтів з хронічними захворюваннями.
533
+
534
+ ПРИНЦИПИ:
535
+ - Безпечні, поступові зміни з урахуванням медичних обмежень
536
+ - Персоналізація на основі профілю пацієнта
537
+ - Позитивне підкріплення та реалістичні цілі
538
+ - Мотивація через малі кроки прогресу
539
+ - Відповідаєш українською мовою
540
+
541
+ МЕДИЧНІ ОБМЕЖЕННЯ пацієнта Mary:
542
+ - Стани: essential hypertension, type 2 diabetes, chronic kidney disease, osteoarthritis, depression, history of stroke
543
+ - Обмеження: Left-sided weakness from stroke; Severe knee arthritis - painful weight bearing; High fall risk - balance issues; Limited endurance due to heart condition; Gets breathless with minimal exertion; Requires walking frame for mobility
544
+
545
+ УВАГА до активних проблем:
546
+ Essential hypertension (uncontrolled); Type 2 diabetes mellitus with complications; Chronic kidney disease stage 3; Osteoarthritis bilateral knees; Depression, recurrent episode
547
+
548
+ В кінці кожної сесії пропонуй конкретний план дій та час наступної зустрічі.
549
+
550
+ 📤 USER PROMPT:
551
+ ----------------------------------------
552
+
553
+ ПАЦІЄНТ: Mary, 76 років
554
+
555
+ LIFESTYLE ПРОФІЛЬ:
556
+ - Головна ціль: Improve mobility and independence while managing multiple chronic conditions safely. Prevent falls and maintain current functional level.
557
+ - Уподобання: chair exercises; gentle walking with frame; tai chi (interested but never tried); swimming (if accessible)
558
+ - Харчування: Diabetic diet - needs simple carb counting education; Low sodium for hypertension and heart failure; CKD diet - limited protein and phosphorus; Poor appetite due to depression; Lives alone - convenience foods common; Limited cooking ability due to arthritis
559
+ - Особисті переваги: very cautious about new activities due to fall anxiety; prefers morning activities when energy is better; needs frequent encouragement and reassurance; responds well to small, achievable goals; family involvement important for motivation
560
+ - Journey резюме: Elderly patient with complex medical needs seeking to maintain independence. Recent falls have increased anxiety about movement. Needs gentle, supervised approach to lifestyle modifications.
561
+ - Попередня сесія:
562
+
563
+ ІСТОРІЯ РОЗМОВИ:
564
+ user: у мене болить голова
565
+ assistant: Добрий день, Мері. Мені шкода чути, що у Вас болить голова.
566
+
567
+ Враховуючи Ваш медичний профіль, особливо неконтрольовану гіпертензію (високий артеріальний тиск), головний біль є симптомом, на який потрібно звернути особливу увагу.
568
+
569
+ **Будь ласка, негайно виміряйте свій артеріальний тиск.** Які були показники?
570
+
571
+ Поки Ви вимірюєте тиск, будь ласка, надайте мені додаткову інформацію про Ваш головний біль:
572
+
573
+ 1. **Наскільки сильний біль** (наприклад, за шкалою від 1 до 10, де 10 – найсильніший біль)?
574
+ 2. **Де саме болить голова** (наприклад, спереду, ззаду, збоку, пульсуючий біль, стискаючий)?
575
+ 3. **Коли почався цей біль?** Він з'явився раптово чи поступово?
576
+ 4. **Чи є якісь інші симптоми**, такі як нудота, блювота, запаморочення, зміни зору (наприклад, розмитість, "мушки" перед очима), слабкість, оніміння, сплутаність свідомості, чутливість до світла або звуку?
577
+ 5. **Чи приймали Ви щось від болю**, і чи допомогло це? (Будь ласка, пам'ятайте про Вашу алергію на НПЗЗ).
578
+
579
+ **Дуже важливо:**
580
+
581
+ * **Якщо Ваш артеріальний тиск дуже високий (наприклад, вище 180/120 мм рт.ст.) або якщо головний біль дуже сильний, раптовий, супроводжується змінами зору, слабкістю, онімінням, сплутаністю свідомості, або є найсильнішим болем у Вашому житті, Вам слід негайно звернутися за невідкладною медичною допомогою.** Це може бути ознакою серйозного стану.
582
+ * Поки ми чекаємо на відповіді, спробуйте відпочити в тихій, затемненій кімнаті. Пийте достатньо води.
583
+
584
+ Я не можу ставити діагнози чи призначати лікування, але я тут, щоб допомогти Вам зрозуміти Ваші симптоми та підказати, коли потрібно звернутися до лікаря. Ваша безпека – мій головний пріоритет.
585
+ user: біль пройшла і готова поговорити про життя
586
+
587
+ ПОВІДОМЛЕННЯ ПАЦІЄНТА: біль пройшла і готова поговорити про життя
588
+
589
+ Проведи lifestyle коучинг з урахуванням медичного стану та особистих цілей:
590
+
591
+ 📥 GEMINI RESPONSE:
592
+ ----------------------------------------
593
+ Чудово, Мері! Я дуже рада чути, що головний біль минув і Ви почуваєтеся краще. Це дуже важлива новина. Пам'ятайте, що регулярний моніторинг артеріального тиску є ключовим для Вашого здоров'я, особливо враховуючи Вашу історію.
594
+
595
+ Тепер, коли ми можемо зосередитися на Ваших цілях, давайте поговоримо про те, як ми можемо покращити Вашу мобільність та незалежність, зробити Ваше життя комфортнішим та безпечнішим. Я знаю, що Ви прагнете запобігти падінням і підтримувати свій функціональний рівень, і це дуже реалістичні та важливі цілі.
596
+
597
+ З огляду на Ваші уподобання та медичні обмеження, ми будемо рухатися дуже обережно, крок за кроком.
598
+
599
+ **Давайте почнемо з руху та активності, адже це Ваша головна ціль.**
600
+
601
+ Ви згадували, що Вам подобаються вправи сидячи та легкі прогулянки з ходунками. Це чудові відправні точки! Вправи сидячи ідеально підходять, оскільки вони зменшують навантаження на коліна, мінімізують ризик падінь і дозволяють працювати над силою та гнучкістю, навіть з лівосторонньою слабкістю.
602
+
603
+ **Моя пропозиція для початку:**
604
+
605
+ 1. **Ранкові вправи сидячи:** Оскільки Ви віддаєте перевагу ранковим заняттям, коли у Вас більше енергії, давайте спробуємо включити дуже короткий комплекс вправ сидячи у Ваш ранок.
606
+ * **Що робити:** Сидячи на стільці з міцною спинкою, спробуйте обережно піднімати ноги по черзі, випрямляючи їх перед собою. Почніть з 3-5 повторень для кожної ноги.
607
+ * **Також:** Обережні рухи руками – піднімайте руки в сторони, вперед, робіть кругові рухи плечима. Це допоможе покращити кровообіг та підтримати м'язи.
608
+ * **Тривалість:** Почніть з 5 хвилин. Це дуже мало, але це вже крок! Якщо 5 хвилин забагато, почніть з 2-3 хвилин. Головне – регулярність і слухати своє тіло. Якщо відчуваєте задишку або біль, негайно зупиніться.
609
+ * **Безпека:** Завжди тримайтеся за спинку стільця або використовуйте підлокітники для підтримки.
610
+
611
+ 2. **Прогулянки з ходунками:** Якщо Ви почуваєтеся добре і маєте сили, спробуйте зробити дуже коротку прогулянку по дому з ходунками. Можливо, просто пройтися до кухні та назад, або до вікна. Це допоможе підтримувати Вашу витривалість та баланс. Знову ж таки, слухайте своє тіло і не перевантажуйтесь.
612
+
613
+ **Щодо харчування:**
614
+
615
+ Я розумію, що у Вас багато обмежень і складнощів з приготуванням їжі. Це абсолютно нормально, і ми знайдемо способи, як зробити Ваше харчування більш збалансованим, не ускладнюючи життя.
616
+
617
+ * **Маленький крок:** Давайте зосередимося на тому, щоб кожен прийом їжі містив хоча б один овоч або фрукт. Навіть якщо це заморожені овочі, які легко приготувати, або яблуко. Це додасть вітамінів та клітковини, що важливо для діабету та загального самопочуття.
618
+ * **Гідратація:** Переконайтеся, що Ви п'єте достатньо води протягом дня. Це дуже важливо для нирок та загального стану.
619
+
620
+ **Пам'ятайте, Мері:** Кожен маленький крок – це велика перемога! Ваша безпека та комфорт – мій головний пріоритет. Не бійтеся просити допомоги у родини, вони можуть бути чудовою підтримкою.
621
+
622
+ ---
623
+
624
+ **Ваш план дій до наступної зустрічі:**
625
+
626
+ 1. **Щоранку:** Спробуйте виконувати 5 хвилин вправ сидячи (піднімання ніг, рухи руками) у зручному для Вас темпі.
627
+ 2. **Харчування:** Постарайтеся додати хоча б один овоч або фрукт до кожного основного прийому їжі.
628
+ 3. **Моніторинг:** Продовжуйте регулярно вимірювати артеріальний тиск і записувати показники.
629
+
630
+ Ми можемо зустрітися знову, щоб обговорити Ваш прогрес та скоригувати план. Як щодо того, щоб провести нашу наступну сесію **через 3 дні, у той самий час**? Це дасть Вам достатньо часу, щоб спробувати нові вправи, і ми зможемо обговорити, як Ви почуваєтеся.
631
+
632
+ Що Ви думаєте про цей план, Мері? Чи здається він Вам реалістичним та комфортним?
633
+
634
+ 🔧 MODEL: gemini-2.5-flash
635
+ ================================================================================
636
+
637
+ 2025-09-02 16:21:58,576 - INFO -
638
+ ================================================================================
639
+ 🤖 GEMINI API CALL #6 [SESSION_CONTROLLER_ERROR] - 2025-09-02 16:21:58
640
+ ================================================================================
641
+
642
+ 📤 SYSTEM PROMPT:
643
+ ----------------------------------------
644
+ Ти - Session Controller для медичного додатку з lifestyle coaching.
645
+
646
+ ТВОЄ ЗАВДАННЯ: Проаналізувати повідомлення пацієнта і прийняти рішення про режим роботи.
647
+
648
+ РЕЖИМИ:
649
+ - medical: медичні симптоми, скарги, ургентні стани
650
+ - lifestyle: фізична активність, харчування, спосіб життя, мотивація
651
+ - none: завершення сесії або неясний контекст
652
+
653
+ RED FLAGS (завжди -> medical):
654
+ - біль у грудях, задишка у спокої
655
+ - високий АТ (>180/120), низький (<80/50)
656
+ - синкопе, запаморочення
657
+ - різкий набряк, набір ваги
658
+ - симптомна гіпо/гіперглікемія
659
+
660
+ ВІДПОВІДАЙ ЛИШЕ У ФОРМАТІ JSON:
661
+ {
662
+ "action": "start_medical|start_lifestyle|continue_current|end_session",
663
+ "mode": "medical|lifestyle|none",
664
+ "reasoning": "коротке пояснення українською",
665
+ "escalation_needed": true/false
666
+ }
667
+
668
+ 📤 USER PROMPT:
669
+ ----------------------------------------
670
+
671
+ КЛІНІЧНИЙ КОНТЕКСТ пацієнта Mary:
672
+ - Активні проблеми: Essential hypertension (uncontrolled); Type 2 diabetes mellitus with complications; Chronic kidney disease stage 3; Osteoarthritis bilateral knees; Depression, recurrent episode
673
+ - Критичні попередження: High fall risk - requires mobility assessment; CKD stage 3 - monitor kidney function with any changes; Depression - monitor for worsening mood; Uncontrolled hypertension and diabetes
674
+
675
+ ПОТОЧНИЙ СТАН СЕСІЇ: режим=none, активна=False
676
+
677
+ ІСТОРІЯ ЧАТУ:
678
+
679
+
680
+ НОВЕ ПОВІДОМЛЕННЯ ПАЦІЄНТА: Привіт
681
+
682
+ Прийми рішення про режим роботи:
683
+
684
+ 📥 GEMINI RESPONSE:
685
+ ----------------------------------------
686
+ Помилка API: can only concatenate str (not "NoneType") to str
687
+
688
+ 🔧 MODEL: gemini-2.5-flash
689
+ ================================================================================
690
+
691
+ 2025-09-02 16:22:04,932 - INFO -
692
+ ================================================================================
693
+ 🤖 GEMINI API CALL #7 [MEDICAL_ASSISTANT] - 2025-09-02 16:22:04
694
+ ================================================================================
695
+
696
+ 📤 SYSTEM PROMPT:
697
+ ----------------------------------------
698
+ Ти - досвідчений медичний асистент для пацієнтів з хронічними захворюваннями.
699
+
700
+ ПРИНЦИПИ:
701
+ - Безпека пацієнта - головний пріоритет
702
+ - Не ставиш діагнози, не призначаєш лікування
703
+ - Рекомендуєш звернення до лікаря при червоних прапорцях
704
+ - Даєш загальні поради з управління хронічними станами
705
+ - Відповідаєш українською мовою
706
+
707
+ При УРГЕНТНИХ симптомах - рекомендуй негайне звернення до медзакладу.
708
+
709
+ 📤 USER PROMPT:
710
+ ----------------------------------------
711
+
712
+ МЕДИЧНИЙ ПРОФІЛЬ ПАЦІЄНТА (Mary):
713
+ - Активні проблеми: Essential hypertension (uncontrolled); Type 2 diabetes mellitus with complications; Chronic kidney disease stage 3; Osteoarthritis bilateral knees; Depression, recurrent episode
714
+ - Поточні медикаменти: Amlodipine 10mg daily; Metformin 1000mg twice daily; Lisinopril 20mg daily; Atorvastatin 40mg at bedtime; Metoprolol 50mg twice daily; Furosemide 40mg daily; Sertraline 50mg daily; Calcium + Vitamin D3 daily
715
+ - Останні показники: Height: 1.58m; BMI: 31.2 kg/m²; HbA1c: 8.2% (poor control)
716
+ - Алергії: Penicillin - rash, NSAIDs - GI upset
717
+
718
+ КРИТИЧНІ ПОПЕРЕДЖЕННЯ: High fall risk - requires mobility assessment; CKD stage 3 - monitor kidney function with any changes; Depression - monitor for worsening mood; Uncontrolled hypertension and diabetes
719
+
720
+ ІСТОРІЯ РОЗМОВИ:
721
+ user: Привіт
722
+
723
+ ПИТАННЯ ПАЦІЄНТА: Привіт
724
+
725
+ Надай медичну консультацію з урахуванням медичного профілю:
726
+
727
+ 📥 GEMINI RESPONSE:
728
+ ----------------------------------------
729
+ Привіт, Mary!
730
+
731
+ Я тут, щоб надати Вам підтримку та загальну інформацію щодо управління Вашими хронічними станами. Бачу, що у Вас є кілька важливих проблем зі здоров'ям, таких як гіпертонія (яка наразі не контролюється), цукровий діабет 2 типу (з поганим контролем HbA1c), хронічна хвороба нирок 3 стадії, остеоартрит та депресія. Розумію, що керувати всіма цими станами може бути непросто.
732
+
733
+ Пам'ятайте, що я не можу ставити діагнози, призначати лікування або змінювати Ваші поточні медикаменти. Це завжди має робити Ваш лікар або медична команда. Моя роль – надавати Вам загальні поради та інформацію, а також нагадувати про важливість тісної співпраці з Вашими лікарями.
734
+
735
+ З Вашого профілю видно, що неконтрольований артеріальний тиск та цукровий діабет, а також хронічна хвороба нирок 3 стадії та високий ризик падінь є ключовими моментами, які потребують пильної уваги.
736
+
737
+ Як я можу Вам допомогти сьогодні? Чи є у Вас якісь конкретні питання або теми, які Ви хотіли б обговорити щодо управління Вашими станами? Ми можемо поговорити про загальні стратегії здорового способу життя, важливість дотримання приписів лікаря та моніторингу Вашого стану.
738
+
739
+ 🔧 MODEL: gemini-2.5-flash
740
+ ================================================================================
741
+
742
+ 2025-09-02 16:42:12,741 - INFO -
743
+ ================================================================================
744
+ 🤖 GEMINI API CALL #1 [SESSION_CONTROLLER] - 2025-09-02 16:42:12
745
+ ================================================================================
746
+
747
+ 📤 SYSTEM PROMPT:
748
+ ----------------------------------------
749
+ Ти - Session Controller для медичного додатку з lifestyle coaching.
750
+
751
+ ТВОЄ ЗАВДАННЯ: Проаналізувати повідомлення пацієнта і прийняти рішення про режим роботи.
752
+
753
+ РЕЖИМИ:
754
+ - medical: медичні симптоми, скарги, ургентні стани
755
+ - lifestyle: фізична активність, харчування, спосіб життя, мотивація
756
+ - none: завершення сесії або неясний контекст
757
+
758
+ RED FLAGS (завжди -> medical):
759
+ - біль у грудях, задишка у спокої
760
+ - високий АТ (>180/120), низький (<80/50)
761
+ - синкопе, запаморочення
762
+ - різкий набряк, набір ваги
763
+ - симптомна гіпо/гіперглікемія
764
+
765
+ ВІДПОВІДАЙ ЛИШЕ У ФОРМАТІ JSON:
766
+ {
767
+ "action": "start_medical|start_lifestyle|continue_current|end_session",
768
+ "mode": "medical|lifestyle|none",
769
+ "reasoning": "коротке пояснення українською",
770
+ "escalation_needed": true/false
771
+ }
772
+
773
+ 📤 USER PROMPT:
774
+ ----------------------------------------
775
+
776
+ КЛІНІЧНИЙ КОНТЕКСТ пацієнта Mark:
777
+ - Активні проблеми: Nausea (01/02/2025); Hypokalemia (01/07/2025); Type 2 diabetes mellitus with other diabetic neurological complication (01/07/2025); Right leg stump pain (06/17/2024); Foot ulcerations (10/07/2024)
778
+ - Критичні попередження: Life endangering medical noncompliance (03/06/2023); Low potassium (3.3 mmol/L) - requires monitoring; Recent chest pain episode (12/22/2024) - cardiac workup completed
779
+
780
+ ПОТОЧНИЙ СТАН СЕСІЇ: режим=none, активна=False
781
+
782
+ ІСТОРІЯ ЧАТУ:
783
+
784
+
785
+ НОВЕ ПОВІДОМЛЕННЯ ПАЦІЄНТА: Привіт
786
+
787
+ Прийми рішення про режим роботи:
788
+
789
+ 📥 GEMINI RESPONSE:
790
+ ----------------------------------------
791
+ ```json
792
+ {
793
+ "action": "continue_current",
794
+ "mode": "none",
795
+ "reasoning": "Пацієнт надіслав лише привітання. Контекст не дозволяє визнач��ти режим роботи (medical/lifestyle).",
796
+ "escalation_needed": false
797
+ }
798
+ ```
799
+
800
+ 🔧 MODEL: gemini-2.5-flash
801
+ ================================================================================
802
+
prompts.py ADDED
File without changes
requirements.txt CHANGED
@@ -1,6 +1,22 @@
 
1
  gradio>=5.3.0
2
  python-dotenv>=1.0.0
3
  google-genai>=0.5.0
4
  typing-extensions>=4.5.0
 
 
 
5
  dataclasses; python_version<"3.7"
6
- huggingface-hub>=0.16.0
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core dependencies for Lifestyle Journey MVP
2
  gradio>=5.3.0
3
  python-dotenv>=1.0.0
4
  google-genai>=0.5.0
5
  typing-extensions>=4.5.0
6
+ huggingface-hub>=0.16.0
7
+
8
+ # Python compatibility
9
  dataclasses; python_version<"3.7"
10
+
11
+ # Testing Lab additional dependencies
12
+ pandas>=2.0.0
13
+ numpy>=1.24.0
14
+
15
+ # Optional: for enhanced data analysis (if needed)
16
+ matplotlib>=3.6.0
17
+ seaborn>=0.12.0
18
+
19
+ # Development dependencies (optional)
20
+ pytest>=7.0.0
21
+ black>=23.0.0
22
+ flake8>=6.0.0
testing_lab.py ADDED
@@ -0,0 +1,319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Testing Lab Module - система для тестування нових пацієнтів
3
+ """
4
+
5
+ import json
6
+ import os
7
+ from datetime import datetime
8
+ from typing import Dict, List, Optional, Tuple
9
+ from dataclasses import dataclass, asdict
10
+ import csv
11
+
12
+ @dataclass
13
+ class TestSession:
14
+ """Клас для збереження результатів тестової сесії"""
15
+ session_id: str
16
+ patient_name: str
17
+ timestamp: str
18
+ total_messages: int
19
+ medical_messages: int
20
+ lifestyle_messages: int
21
+ escalations_count: int
22
+ controller_decisions: List[Dict]
23
+ response_times: List[float]
24
+ session_duration_minutes: float
25
+ final_profile_state: Dict
26
+ notes: str = ""
27
+
28
+ @dataclass
29
+ class TestingMetrics:
30
+ """Метрики для аналізу тестування"""
31
+ session_id: str
32
+ accuracy_score: float # % правильних рішень Controller
33
+ response_quality_score: float # суб'єктивна оцінка
34
+ medical_safety_score: float # % правильно виявлених red flags
35
+ lifestyle_personalization_score: float # % врахування обмежень
36
+ user_experience_score: float # загальна оцінка UX
37
+
38
+ class TestingDataManager:
39
+ """Клас для управління тестовими даними та результатами"""
40
+
41
+ def __init__(self):
42
+ self.results_dir = "testing_results"
43
+ self.ensure_results_directory()
44
+
45
+ def ensure_results_directory(self):
46
+ """Створює директорії для збереження результатів"""
47
+ if not os.path.exists(self.results_dir):
48
+ os.makedirs(self.results_dir)
49
+
50
+ # Піддиректорії
51
+ subdirs = ["sessions", "patients", "reports", "exports"]
52
+ for subdir in subdirs:
53
+ path = os.path.join(self.results_dir, subdir)
54
+ if not os.path.exists(path):
55
+ os.makedirs(path)
56
+
57
+ def validate_clinical_background(self, json_data: dict) -> Tuple[bool, List[str]]:
58
+ """Валідує структуру clinical_background.json"""
59
+ errors = []
60
+ required_fields = [
61
+ "patient_summary",
62
+ "vital_signs_and_measurements",
63
+ "assessment_and_plan"
64
+ ]
65
+
66
+ for field in required_fields:
67
+ if field not in json_data:
68
+ errors.append(f"Відсутнє обов'язкове поле: {field}")
69
+
70
+ # Перевірка patient_summary
71
+ if "patient_summary" in json_data:
72
+ patient_summary = json_data["patient_summary"]
73
+ required_sub_fields = ["active_problems", "current_medications"]
74
+
75
+ for field in required_sub_fields:
76
+ if field not in patient_summary:
77
+ errors.append(f"Відсутнє поле в patient_summary: {field}")
78
+
79
+ return len(errors) == 0, errors
80
+
81
+ def validate_lifestyle_profile(self, json_data: dict) -> Tuple[bool, List[str]]:
82
+ """Валідує структуру lifestyle_profile.json"""
83
+ errors = []
84
+ required_fields = [
85
+ "patient_name",
86
+ "patient_age",
87
+ "conditions",
88
+ "primary_goal",
89
+ "exercise_limitations"
90
+ ]
91
+
92
+ for field in required_fields:
93
+ if field not in json_data:
94
+ errors.append(f"Відсутнє обов'язкове поле: {field}")
95
+
96
+ # Перевірка типів даних
97
+ if "conditions" in json_data and not isinstance(json_data["conditions"], list):
98
+ errors.append("Поле 'conditions' має бути списком")
99
+
100
+ if "exercise_limitations" in json_data and not isinstance(json_data["exercise_limitations"], list):
101
+ errors.append("Поле 'exercise_limitations' має бути списком")
102
+
103
+ return len(errors) == 0, errors
104
+
105
+ def save_patient_profile(self, clinical_data: dict, lifestyle_data: dict) -> str:
106
+ """Зберігає профіль пацієнта для тестування"""
107
+ patient_name = lifestyle_data.get("patient_name", "Unknown")
108
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
109
+ patient_id = f"{patient_name}_{timestamp}"
110
+
111
+ # Зберігаємо в окремих файлах
112
+ clinical_path = os.path.join(self.results_dir, "patients", f"{patient_id}_clinical.json")
113
+ lifestyle_path = os.path.join(self.results_dir, "patients", f"{patient_id}_lifestyle.json")
114
+
115
+ with open(clinical_path, 'w', encoding='utf-8') as f:
116
+ json.dump(clinical_data, f, indent=2, ensure_ascii=False)
117
+
118
+ with open(lifestyle_path, 'w', encoding='utf-8') as f:
119
+ json.dump(lifestyle_data, f, indent=2, ensure_ascii=False)
120
+
121
+ return patient_id
122
+
123
+ def save_test_session(self, session: TestSession) -> str:
124
+ """Зберігає результати тестової сесії"""
125
+ filename = f"session_{session.session_id}.json"
126
+ filepath = os.path.join(self.results_dir, "sessions", filename)
127
+
128
+ with open(filepath, 'w', encoding='utf-8') as f:
129
+ json.dump(asdict(session), f, indent=2, ensure_ascii=False)
130
+
131
+ return filepath
132
+
133
+ def save_testing_metrics(self, metrics: TestingMetrics) -> str:
134
+ """Зберігає метрики тестування"""
135
+ filename = f"metrics_{metrics.session_id}.json"
136
+ filepath = os.path.join(self.results_dir, "sessions", filename)
137
+
138
+ with open(filepath, 'w', encoding='utf-8') as f:
139
+ json.dump(asdict(metrics), f, indent=2, ensure_ascii=False)
140
+
141
+ return filepath
142
+
143
+ def get_all_test_sessions(self) -> List[Dict]:
144
+ """Повертає всі збережені тестові сесії"""
145
+ sessions_dir = os.path.join(self.results_dir, "sessions")
146
+ sessions = []
147
+
148
+ for filename in os.listdir(sessions_dir):
149
+ if filename.startswith("session_") and filename.endswith(".json"):
150
+ filepath = os.path.join(sessions_dir, filename)
151
+ try:
152
+ with open(filepath, 'r', encoding='utf-8') as f:
153
+ session_data = json.load(f)
154
+ sessions.append(session_data)
155
+ except Exception as e:
156
+ print(f"Помилка читання сесії {filename}: {e}")
157
+
158
+ return sorted(sessions, key=lambda x: x.get('timestamp', ''), reverse=True)
159
+
160
+ def export_results_to_csv(self, sessions: List[Dict]) -> str:
161
+ """Експортує результати в CSV формат"""
162
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
163
+ filename = f"testing_results_export_{timestamp}.csv"
164
+ filepath = os.path.join(self.results_dir, "exports", filename)
165
+
166
+ if not sessions:
167
+ return ""
168
+
169
+ # Визначаємо поля для CSV
170
+ fieldnames = [
171
+ 'session_id', 'patient_name', 'timestamp', 'total_messages',
172
+ 'medical_messages', 'lifestyle_messages', 'escalations_count',
173
+ 'session_duration_minutes', 'notes'
174
+ ]
175
+
176
+ with open(filepath, 'w', newline='', encoding='utf-8') as csvfile:
177
+ writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
178
+ writer.writeheader()
179
+
180
+ for session in sessions:
181
+ # Фільтруємо тільки потрібні поля
182
+ filtered_session = {key: session.get(key, '') for key in fieldnames}
183
+ writer.writerow(filtered_session)
184
+
185
+ return filepath
186
+
187
+ def generate_summary_report(self, sessions: List[Dict]) -> str:
188
+ """Генерує звітний текст по результатах тестування"""
189
+ if not sessions:
190
+ return "Немає даних для звіту"
191
+
192
+ total_sessions = len(sessions)
193
+ total_messages = sum(session.get('total_messages', 0) for session in sessions)
194
+ total_medical = sum(session.get('medical_messages', 0) for session in sessions)
195
+ total_lifestyle = sum(session.get('lifestyle_messages', 0) for session in sessions)
196
+ total_escalations = sum(session.get('escalations_count', 0) for session in sessions)
197
+
198
+ # Середні показники
199
+ avg_messages_per_session = total_messages / total_sessions if total_sessions > 0 else 0
200
+ avg_duration = sum(session.get('session_duration_minutes', 0) for session in sessions) / total_sessions
201
+
202
+ # Розподіл по режимах
203
+ medical_percentage = (total_medical / total_messages * 100) if total_messages > 0 else 0
204
+ lifestyle_percentage = (total_lifestyle / total_messages * 100) if total_messages > 0 else 0
205
+ escalation_rate = (total_escalations / total_messages * 100) if total_messages > 0 else 0
206
+
207
+ report = f"""
208
+ 📊 ЗВІТ ПО ТЕСТУВАННЮ LIFESTYLE JOURNEY
209
+ {'='*50}
210
+
211
+ 📈 ЗАГАЛЬНА СТАТИСТИКА:
212
+ • Всього тестових сесій: {total_sessions}
213
+ • Загальна кількість повідомлень: {total_messages}
214
+ • Середня тривалість сесії: {avg_duration:.1f} хв
215
+ • Середня кількість повідомлень на сесію: {avg_messages_per_session:.1f}
216
+
217
+ 🔄 РОЗПОДІЛ ПО РЕЖИМАХ:
218
+ • Medical режим: {total_medical} ({medical_percentage:.1f}%)
219
+ • Lifestyle режим: {total_lifestyle} ({lifestyle_percentage:.1f}%)
220
+ • Ескалації: {total_escalations} ({escalation_rate:.1f}%)
221
+
222
+ 👥 ПАЦІЄНТИ В ТЕСТУВАННІ:
223
+ """
224
+
225
+ # Додаємо інформацію про пацієнтів
226
+ patients = {}
227
+ for session in sessions:
228
+ patient_name = session.get('patient_name', 'Unknown')
229
+ if patient_name not in patients:
230
+ patients[patient_name] = {
231
+ 'sessions': 0,
232
+ 'messages': 0,
233
+ 'escalations': 0
234
+ }
235
+ patients[patient_name]['sessions'] += 1
236
+ patients[patient_name]['messages'] += session.get('total_messages', 0)
237
+ patients[patient_name]['escalations'] += session.get('escalations_count', 0)
238
+
239
+ for patient_name, stats in patients.items():
240
+ report += f"• {patient_name}: {stats['sessions']} сесій, {stats['messages']} повідомлень, {stats['escalations']} ескалацій\n"
241
+
242
+ report += f"\n📅 Період тестування: {sessions[-1].get('timestamp', 'N/A')} - {sessions[0].get('timestamp', 'N/A')}"
243
+
244
+ return report
245
+
246
+ class PatientTestingInterface:
247
+ """Інтерфейс для тестування нових пацієнтів"""
248
+
249
+ def __init__(self, testing_manager: TestingDataManager):
250
+ self.testing_manager = testing_manager
251
+ self.current_session: Optional[TestSession] = None
252
+ self.session_start_time: Optional[datetime] = None
253
+
254
+ def start_test_session(self, patient_name: str) -> str:
255
+ """Початок нової тестової сесії"""
256
+ self.session_start_time = datetime.now()
257
+ session_id = f"{patient_name}_{self.session_start_time.strftime('%Y%m%d_%H%M%S')}"
258
+
259
+ self.current_session = TestSession(
260
+ session_id=session_id,
261
+ patient_name=patient_name,
262
+ timestamp=self.session_start_time.isoformat(),
263
+ total_messages=0,
264
+ medical_messages=0,
265
+ lifestyle_messages=0,
266
+ escalations_count=0,
267
+ controller_decisions=[],
268
+ response_times=[],
269
+ session_duration_minutes=0.0,
270
+ final_profile_state={}
271
+ )
272
+
273
+ return f"🧪 Почато тестову сесію: {session_id}"
274
+
275
+ def log_message_interaction(self, mode: str, decision: Dict, response_time: float, escalation: bool):
276
+ """Логує взаємодію в поточній сесії"""
277
+ if not self.current_session:
278
+ return
279
+
280
+ self.current_session.total_messages += 1
281
+
282
+ if mode == "medical":
283
+ self.current_session.medical_messages += 1
284
+ elif mode == "lifestyle":
285
+ self.current_session.lifestyle_messages += 1
286
+
287
+ if escalation:
288
+ self.current_session.escalations_count += 1
289
+
290
+ self.current_session.controller_decisions.append({
291
+ "timestamp": datetime.now().isoformat(),
292
+ "mode": mode,
293
+ "decision": decision,
294
+ "escalation": escalation
295
+ })
296
+
297
+ self.current_session.response_times.append(response_time)
298
+
299
+ def end_test_session(self, final_profile: Dict, notes: str = "") -> str:
300
+ """Завершення тестової сесії"""
301
+ if not self.current_session or not self.session_start_time:
302
+ return "Немає активної сесії для завершення"
303
+
304
+ end_time = datetime.now()
305
+ duration = (end_time - self.session_start_time).total_seconds() / 60
306
+
307
+ self.current_session.session_duration_minutes = duration
308
+ self.current_session.final_profile_state = final_profile
309
+ self.current_session.notes = notes
310
+
311
+ # Зберігаємо сесію
312
+ filepath = self.testing_manager.save_test_session(self.current_session)
313
+ session_id = self.current_session.session_id
314
+
315
+ # Скидаємо поточну сесію
316
+ self.current_session = None
317
+ self.session_start_time = None
318
+
319
+ return f"✅ Сесію завершено та збережено: {session_id}\n📁 Файл: {filepath}"