Spaces:
Sleeping
Sleeping
Видалено застарілі файли, включаючи модулі для логування, інтерфейси Gradio та приклади використання. Оновлено документацію для відображення нових змін у структурі проекту.
Browse files- README.md +1 -1
- agent_conversation_logger.py +0 -254
- enhanced_mai_dx_logger.py +516 -0
- lmai_dx_interface_log.py +0 -601
- mai_dx_interface.py +0 -855
- mai_dx_logs/case_20250710_173828_5d4912.json +0 -27
- mai_dx_logs/case_20250710_174912_2b0a2d.json +0 -27
- mai_dx_wrapper.py +138 -0
- production_mai_dx.py +0 -254
- run_mai_dx_fixed.py +77 -0
- example.py → test/example.py +0 -0
- example_corrected.py → test/example_corrected.py +0 -0
- simple_test.py → test/simple_test.py +0 -0
- updated_mai_dx_interface.py +730 -0
- utils/analyze_logs.py +295 -0
- utils/test_logging.py +186 -0
- utils/view_conversation.py +249 -0
README.md
CHANGED
@@ -5,7 +5,7 @@ colorFrom: indigo
|
|
5 |
colorTo: green
|
6 |
sdk: gradio
|
7 |
sdk_version: 5.36.2
|
8 |
-
app_file:
|
9 |
pinned: false
|
10 |
short_description: MAI-DX Enhanced Research Platform
|
11 |
---
|
|
|
5 |
colorTo: green
|
6 |
sdk: gradio
|
7 |
sdk_version: 5.36.2
|
8 |
+
app_file: run_mai_dx_fixed.py
|
9 |
pinned: false
|
10 |
short_description: MAI-DX Enhanced Research Platform
|
11 |
---
|
agent_conversation_logger.py
DELETED
@@ -1,254 +0,0 @@
|
|
1 |
-
#!/usr/bin/env python3
|
2 |
-
"""
|
3 |
-
Модуль для логування та управління діагностичними сесіями MAI-DX.
|
4 |
-
|
5 |
-
Цей модуль забезпечує:
|
6 |
-
- Створення та відстеження окремих діагностичних сесій.
|
7 |
-
- Запис детальних повідомлень від кожного ШІ-агента.
|
8 |
-
- Збереження повної інформації про сесію у форматі JSON.
|
9 |
-
- Надання функцій для аналітики: перелік сесій, експорт у CSV та генерація звітів.
|
10 |
-
"""
|
11 |
-
import os
|
12 |
-
import json
|
13 |
-
import csv
|
14 |
-
from datetime import datetime
|
15 |
-
from typing import List, Dict, Any, Optional
|
16 |
-
from dataclasses import dataclass, asdict, field
|
17 |
-
import threading
|
18 |
-
import uuid
|
19 |
-
|
20 |
-
@dataclass
|
21 |
-
class DiagnosisSession:
|
22 |
-
"""
|
23 |
-
Датаклас для зберігання повної інформації про одну діагностичну сесію.
|
24 |
-
"""
|
25 |
-
case_id: str
|
26 |
-
timestamp: str
|
27 |
-
case_name: str
|
28 |
-
patient_info: str
|
29 |
-
mode: str
|
30 |
-
budget: int
|
31 |
-
diagnosis: str = "N/A"
|
32 |
-
confidence: float = 0.0
|
33 |
-
cost: float = 0.0
|
34 |
-
iterations: int = 0
|
35 |
-
duration: float = 0.0
|
36 |
-
status: str = "In Progress"
|
37 |
-
reasoning: str = "N/A"
|
38 |
-
messages: List[Dict[str, Any]] = field(default_factory=list)
|
39 |
-
|
40 |
-
class AgentConversationLogger:
|
41 |
-
"""
|
42 |
-
Клас для управління логуванням діагностичних сесій.
|
43 |
-
Зберігає кожну сесію в окремому JSON-файлі для легкого доступу та аналізу.
|
44 |
-
"""
|
45 |
-
|
46 |
-
def __init__(self, log_dir: str = "mai_dx_logs"):
|
47 |
-
"""
|
48 |
-
Ініціалізує логгер, створюючи директорію для логів, якщо її не існує.
|
49 |
-
|
50 |
-
Args:
|
51 |
-
log_dir (str): Директорія для збереження файлів логів.
|
52 |
-
"""
|
53 |
-
self.log_dir = log_dir
|
54 |
-
os.makedirs(self.log_dir, exist_ok=True)
|
55 |
-
# Використовуємо локальне сховище потоку для ізоляції сесій
|
56 |
-
self.thread_local = threading.local()
|
57 |
-
print(f"✅ Справжній AgentConversationLogger ініціалізовано. Логи зберігаються в '{self.log_dir}'.")
|
58 |
-
|
59 |
-
def start_conversation(self, case_name: str, patient_info: str, mode: str) -> str:
|
60 |
-
"""
|
61 |
-
Розпочинає нову діагностичну сесію.
|
62 |
-
|
63 |
-
Args:
|
64 |
-
case_name (str): Назва клінічного випадку.
|
65 |
-
patient_info (str): Вхідна інформація про пацієнта.
|
66 |
-
mode (str): Режим діагностики.
|
67 |
-
|
68 |
-
Returns:
|
69 |
-
str: Унікальний ідентифікатор створеної сесії (case_id).
|
70 |
-
"""
|
71 |
-
case_id = f"case_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:6]}"
|
72 |
-
session = DiagnosisSession(
|
73 |
-
case_id=case_id,
|
74 |
-
timestamp=datetime.now().isoformat(),
|
75 |
-
case_name=case_name,
|
76 |
-
patient_info=patient_info,
|
77 |
-
mode=mode,
|
78 |
-
budget=0 # Буде встановлено пізніше, якщо потрібно
|
79 |
-
)
|
80 |
-
self.thread_local.session = session
|
81 |
-
return case_id
|
82 |
-
|
83 |
-
def log_agent_message(self, agent_name: str, action: str, content: str, reasoning: str, confidence: float, cost: float, iteration: int):
|
84 |
-
"""
|
85 |
-
Логує повідомлення від одного з ШІ-агентів.
|
86 |
-
"""
|
87 |
-
if not hasattr(self.thread_local, 'session'):
|
88 |
-
print("⚠️ Попередження: Спроба логування без активної сесії.")
|
89 |
-
return
|
90 |
-
|
91 |
-
message = {
|
92 |
-
"timestamp": datetime.now().isoformat(),
|
93 |
-
"agent": agent_name,
|
94 |
-
"action": action,
|
95 |
-
"content": content,
|
96 |
-
"reasoning": reasoning,
|
97 |
-
"confidence": confidence,
|
98 |
-
"cost": cost,
|
99 |
-
"iteration": iteration,
|
100 |
-
}
|
101 |
-
self.thread_local.session.messages.append(message)
|
102 |
-
|
103 |
-
def end_conversation(self, final_diagnosis: str, confidence: float, cost: float, **kwargs) -> str:
|
104 |
-
"""
|
105 |
-
Завершує сесію, оновлює фінальні дані та зберігає її у файл.
|
106 |
-
|
107 |
-
Returns:
|
108 |
-
str: Ідентифікатор збереженої сесії.
|
109 |
-
"""
|
110 |
-
if not hasattr(self.thread_local, 'session'):
|
111 |
-
print("⚠️ Попередження: Спроба завершити неіснуючу сесію.")
|
112 |
-
return "no_active_session"
|
113 |
-
|
114 |
-
session = self.thread_local.session
|
115 |
-
|
116 |
-
# Оновлення фінальних даних сесії
|
117 |
-
session.diagnosis = final_diagnosis
|
118 |
-
session.confidence = confidence
|
119 |
-
session.cost = cost
|
120 |
-
session.status = "✅ Успішно" if confidence >= 3.0 else "⚠️ Потребує перегляду"
|
121 |
-
session.reasoning = kwargs.get('reasoning', 'N/A')
|
122 |
-
session.iterations = kwargs.get('iterations', len(session.messages))
|
123 |
-
|
124 |
-
start_time = datetime.fromisoformat(session.timestamp)
|
125 |
-
session.duration = (datetime.now() - start_time).total_seconds()
|
126 |
-
|
127 |
-
# Збереження сесії у файл
|
128 |
-
file_path = os.path.join(self.log_dir, f"{session.case_id}.json")
|
129 |
-
with open(file_path, 'w', encoding='utf-8') as f:
|
130 |
-
json.dump(asdict(session), f, ensure_ascii=False, indent=4)
|
131 |
-
|
132 |
-
del self.thread_local.session # Очищення сесії для поточного потоку
|
133 |
-
return session.case_id
|
134 |
-
|
135 |
-
def list_conversations(self) -> List[Dict[str, Any]]:
|
136 |
-
"""
|
137 |
-
Зчитує всі збережені сесії з директорії логів.
|
138 |
-
|
139 |
-
Returns:
|
140 |
-
List[Dict[str, Any]]: Список словників, де кожен словник - це одна сесія.
|
141 |
-
"""
|
142 |
-
all_sessions = []
|
143 |
-
for filename in sorted(os.listdir(self.log_dir), reverse=True):
|
144 |
-
if filename.endswith(".json"):
|
145 |
-
try:
|
146 |
-
with open(os.path.join(self.log_dir, filename), 'r', encoding='utf-8') as f:
|
147 |
-
data = json.load(f)
|
148 |
-
# Видаляємо детальні повідомлення для короткого списку
|
149 |
-
data.pop('messages', None)
|
150 |
-
all_sessions.append(data)
|
151 |
-
except (json.JSONDecodeError, IOError) as e:
|
152 |
-
print(f"Помилка читання файлу логу {filename}: {e}")
|
153 |
-
return all_sessions
|
154 |
-
|
155 |
-
def export_analytics_csv(self) -> str:
|
156 |
-
"""
|
157 |
-
Експортує зведену аналітику по всіх сесіях у CSV файл.
|
158 |
-
|
159 |
-
Returns:
|
160 |
-
str: Назва створеного CSV файлу.
|
161 |
-
"""
|
162 |
-
sessions = self.list_conversations()
|
163 |
-
if not sessions:
|
164 |
-
return "Немає даних для експорту."
|
165 |
-
|
166 |
-
filename = f"analytics_report_{datetime.now().strftime('%Y%m%d_%H%M')}.csv"
|
167 |
-
file_path = os.path.join(self.log_dir, filename)
|
168 |
-
|
169 |
-
# Визначаємо поля (колонки) на основі ключів першої сесії
|
170 |
-
fieldnames = sessions[0].keys()
|
171 |
-
|
172 |
-
with open(file_path, 'w', newline='', encoding='utf-8') as csvfile:
|
173 |
-
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
174 |
-
writer.writeheader()
|
175 |
-
writer.writerows(sessions)
|
176 |
-
|
177 |
-
return file_path
|
178 |
-
|
179 |
-
def export_conversation_report(self, case_id: str, format: str = 'html') -> str:
|
180 |
-
"""
|
181 |
-
Створює детальний звіт для конкретного випадку.
|
182 |
-
|
183 |
-
Args:
|
184 |
-
case_id (str): Ідентифікатор сесії для звіту.
|
185 |
-
format (str): Формат звіту ('html' або 'json').
|
186 |
-
|
187 |
-
Returns:
|
188 |
-
str: Назва створеного файлу звіту.
|
189 |
-
"""
|
190 |
-
file_path = os.path.join(self.log_dir, f"{case_id}.json")
|
191 |
-
try:
|
192 |
-
with open(file_path, 'r', encoding='utf-8') as f:
|
193 |
-
data = json.load(f)
|
194 |
-
except FileNotFoundError:
|
195 |
-
return f"Помилка: Файл для випадку {case_id} не знайдено."
|
196 |
-
|
197 |
-
if format == 'json':
|
198 |
-
return file_path
|
199 |
-
|
200 |
-
# Генерація HTML-звіту
|
201 |
-
report_filename = f"report_{case_id}.html"
|
202 |
-
report_path = os.path.join(self.log_dir, report_filename)
|
203 |
-
|
204 |
-
html = f"""
|
205 |
-
<html>
|
206 |
-
<head>
|
207 |
-
<title>Звіт по випадку: {data['case_name']}</title>
|
208 |
-
<style>
|
209 |
-
body {{ font-family: sans-serif; line-height: 1.6; padding: 20px; }}
|
210 |
-
h1, h2 {{ color: #333; }}
|
211 |
-
table {{ width: 100%; border-collapse: collapse; margin-top: 20px; }}
|
212 |
-
th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
|
213 |
-
th {{ background-color: #f2f2f2; }}
|
214 |
-
.summary {{ background-color: #eef; padding: 15px; border-radius: 8px; }}
|
215 |
-
</style>
|
216 |
-
</head>
|
217 |
-
<body>
|
218 |
-
<h1>Клінічний звіт по випадку: {data['case_name']}</h1>
|
219 |
-
<p><strong>ID:</strong> {data['case_id']}</p>
|
220 |
-
<p><strong>Час:</strong> {data['timestamp']}</p>
|
221 |
-
|
222 |
-
<div class="summary">
|
223 |
-
<h2>Підсумки діагностики</h2>
|
224 |
-
<p><strong>Фінальний діагноз:</strong> {data['diagnosis']}</p>
|
225 |
-
<p><strong>Впевненість:</strong> {data['confidence']}/5.0</p>
|
226 |
-
<p><strong>Вартість:</strong> ${data['cost']}</p>
|
227 |
-
<p><strong>Тривалість:</strong> {data['duration']:.2f} сек.</p>
|
228 |
-
</div>
|
229 |
-
|
230 |
-
<h2>Детальний лог взаємодії агентів</h2>
|
231 |
-
<table>
|
232 |
-
<tr>
|
233 |
-
<th>Час</th><th>Агент</th><th>Дія</th><th>Результат/Контент</th>
|
234 |
-
</tr>
|
235 |
-
"""
|
236 |
-
for msg in data.get('messages', []):
|
237 |
-
html += f"""
|
238 |
-
<tr>
|
239 |
-
<td>{datetime.fromisoformat(msg['timestamp']).strftime('%H:%M:%S')}</td>
|
240 |
-
<td>{msg['agent']}</td>
|
241 |
-
<td>{msg['action']}</td>
|
242 |
-
<td>{msg['content']}</td>
|
243 |
-
</tr>
|
244 |
-
"""
|
245 |
-
html += """
|
246 |
-
</table>
|
247 |
-
</body>
|
248 |
-
</html>
|
249 |
-
"""
|
250 |
-
|
251 |
-
with open(report_path, 'w', encoding='utf-8') as f:
|
252 |
-
f.write(html)
|
253 |
-
|
254 |
-
return report_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
enhanced_mai_dx_logger.py
ADDED
@@ -0,0 +1,516 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""
|
3 |
+
Покращений модуль для повного логування діагностичних сесій MAI-DX
|
4 |
+
з детальним захопленням розмов між агентами.
|
5 |
+
"""
|
6 |
+
import os
|
7 |
+
import io
|
8 |
+
import sys
|
9 |
+
import json
|
10 |
+
import time
|
11 |
+
import uuid
|
12 |
+
import re
|
13 |
+
import logging
|
14 |
+
from datetime import datetime
|
15 |
+
from typing import List, Dict, Any, Optional, Callable
|
16 |
+
from dataclasses import dataclass, asdict, field
|
17 |
+
from contextlib import contextmanager, redirect_stdout, redirect_stderr
|
18 |
+
import threading
|
19 |
+
from queue import Queue
|
20 |
+
|
21 |
+
@dataclass
|
22 |
+
class AgentMessage:
|
23 |
+
"""Повідомлення від одного агента"""
|
24 |
+
timestamp: str
|
25 |
+
agent_name: str
|
26 |
+
message_type: str # 'input', 'reasoning', 'output', 'decision'
|
27 |
+
content: str
|
28 |
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
29 |
+
|
30 |
+
@dataclass
|
31 |
+
class AgentConversation:
|
32 |
+
"""Повна розмова між агентами для одного раунду"""
|
33 |
+
round_number: int
|
34 |
+
start_time: str
|
35 |
+
end_time: Optional[str] = None
|
36 |
+
messages: List[AgentMessage] = field(default_factory=list)
|
37 |
+
decision: Optional[str] = None
|
38 |
+
tests_ordered: List[str] = field(default_factory=list)
|
39 |
+
questions_asked: List[str] = field(default_factory=list)
|
40 |
+
cost_incurred: float = 0.0
|
41 |
+
|
42 |
+
@dataclass
|
43 |
+
class DiagnosisSession:
|
44 |
+
"""Повна діагностична сесія з усіма розмовами агентів"""
|
45 |
+
case_id: str
|
46 |
+
timestamp: str
|
47 |
+
case_name: str
|
48 |
+
patient_info: str
|
49 |
+
mode: str
|
50 |
+
budget: int = 0
|
51 |
+
diagnosis: str = "N/A"
|
52 |
+
confidence: float = 0.0
|
53 |
+
cost: float = 0.0
|
54 |
+
iterations: int = 0
|
55 |
+
duration: float = 0.0
|
56 |
+
status: str = "In Progress"
|
57 |
+
reasoning: str = "N/A"
|
58 |
+
conversations: List[AgentConversation] = field(default_factory=list)
|
59 |
+
raw_output: str = "" # Повний сирий вивід
|
60 |
+
|
61 |
+
class EnhancedOutputCapture:
|
62 |
+
"""Клас для захоплення всіх видів виводу з різних джерел"""
|
63 |
+
def __init__(self):
|
64 |
+
self.captured_output = io.StringIO()
|
65 |
+
self.original_stdout = sys.stdout
|
66 |
+
self.original_stderr = sys.stderr
|
67 |
+
self.original_write = None
|
68 |
+
self.log_queue = Queue()
|
69 |
+
self.capture_thread = None
|
70 |
+
self.is_capturing = False
|
71 |
+
self.log_handler = None
|
72 |
+
|
73 |
+
def start_capture(self):
|
74 |
+
"""Розпочати захоплення виводу"""
|
75 |
+
self.is_capturing = True
|
76 |
+
sys.stdout = self
|
77 |
+
sys.stderr = self
|
78 |
+
|
79 |
+
# Перехоплення логів
|
80 |
+
self._setup_logging_capture()
|
81 |
+
|
82 |
+
def stop_capture(self):
|
83 |
+
"""Зупинити захоплення виводу"""
|
84 |
+
self.is_capturing = False
|
85 |
+
sys.stdout = self.original_stdout
|
86 |
+
sys.stderr = self.original_stderr
|
87 |
+
|
88 |
+
# Видаляємо log handler
|
89 |
+
if self.log_handler:
|
90 |
+
logging.root.removeHandler(self.log_handler)
|
91 |
+
|
92 |
+
captured_text = self.captured_output.getvalue()
|
93 |
+
# Очищаємо ANSI escape коди
|
94 |
+
return self._strip_ansi_codes(captured_text)
|
95 |
+
|
96 |
+
def write(self, text):
|
97 |
+
"""Перехоплення всіх записів"""
|
98 |
+
if self.is_capturing:
|
99 |
+
self.captured_output.write(text)
|
100 |
+
self.original_stdout.write(text) # Дублюємо на консоль
|
101 |
+
|
102 |
+
def flush(self):
|
103 |
+
"""Flush для сумісності"""
|
104 |
+
if hasattr(self.captured_output, 'flush'):
|
105 |
+
self.captured_output.flush()
|
106 |
+
self.original_stdout.flush()
|
107 |
+
|
108 |
+
def _setup_logging_capture(self):
|
109 |
+
"""Налаштування перехоплення логів"""
|
110 |
+
class QueueHandler(logging.Handler):
|
111 |
+
def __init__(self, queue):
|
112 |
+
super().__init__()
|
113 |
+
self.queue = queue
|
114 |
+
|
115 |
+
def emit(self, record):
|
116 |
+
self.queue.put(self.format(record))
|
117 |
+
|
118 |
+
# Додаємо handler до root logger
|
119 |
+
self.log_handler = QueueHandler(self.log_queue)
|
120 |
+
logging.root.addHandler(self.log_handler)
|
121 |
+
|
122 |
+
@staticmethod
|
123 |
+
def _strip_ansi_codes(text):
|
124 |
+
"""Видалити ANSI escape коди з тексту"""
|
125 |
+
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
126 |
+
return ansi_escape.sub('', text)
|
127 |
+
|
128 |
+
class MAIDxConversationLogger:
|
129 |
+
"""Покращений логгер для MAI-DX з повним захопленням розмов"""
|
130 |
+
|
131 |
+
def __init__(self, log_dir: str = "mai_dx_logs"):
|
132 |
+
self.log_dir = log_dir
|
133 |
+
os.makedirs(self.log_dir, exist_ok=True)
|
134 |
+
self.sessions: Dict[str, DiagnosisSession] = {}
|
135 |
+
self.output_capture = EnhancedOutputCapture()
|
136 |
+
|
137 |
+
def start_session(self, case_name: str, patient_info: str, mode: str, budget: int) -> str:
|
138 |
+
"""Розпочати нову діагностичну сесію"""
|
139 |
+
case_id = f"case_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:6]}"
|
140 |
+
session = DiagnosisSession(
|
141 |
+
case_id=case_id,
|
142 |
+
timestamp=datetime.now().isoformat(),
|
143 |
+
case_name=case_name,
|
144 |
+
patient_info=patient_info,
|
145 |
+
mode=mode,
|
146 |
+
budget=budget
|
147 |
+
)
|
148 |
+
self.sessions[case_id] = session
|
149 |
+
return case_id
|
150 |
+
|
151 |
+
def capture_orchestrator_output(self, case_id: str, orchestrator_func: Callable, *args, **kwargs):
|
152 |
+
"""Захопити весь вивід від orchestrator.run()"""
|
153 |
+
if case_id not in self.sessions:
|
154 |
+
raise ValueError(f"Сесія {case_id} не існує")
|
155 |
+
|
156 |
+
session = self.sessions[case_id]
|
157 |
+
|
158 |
+
# Розпочинаємо захоплення
|
159 |
+
self.output_capture.start_capture()
|
160 |
+
|
161 |
+
try:
|
162 |
+
# Виконуємо функцію orchestrator
|
163 |
+
result = orchestrator_func(*args, **kwargs)
|
164 |
+
|
165 |
+
# Зупиняємо захоплення
|
166 |
+
captured_text = self.output_capture.stop_capture()
|
167 |
+
session.raw_output = captured_text
|
168 |
+
|
169 |
+
# Парсимо захоплений текст
|
170 |
+
self._parse_captured_output(case_id, captured_text)
|
171 |
+
|
172 |
+
return result
|
173 |
+
|
174 |
+
except Exception as e:
|
175 |
+
# Завжди зупиняємо захоплення
|
176 |
+
self.output_capture.stop_capture()
|
177 |
+
raise e
|
178 |
+
|
179 |
+
def _parse_captured_output(self, case_id: str, captured_text: str):
|
180 |
+
"""Детальний парсинг захопленого тексту"""
|
181 |
+
session = self.sessions[case_id]
|
182 |
+
lines = captured_text.split('\n')
|
183 |
+
|
184 |
+
current_round = None
|
185 |
+
current_agent = None
|
186 |
+
buffer = []
|
187 |
+
in_agent_output = False
|
188 |
+
round_number = 0
|
189 |
+
|
190 |
+
for i, line in enumerate(lines):
|
191 |
+
# Пропускаємо порожні рядки та помилки Rich
|
192 |
+
if not line.strip() or "rich.errors.NotRenderableError" in line or "Exception in thread" in line:
|
193 |
+
continue
|
194 |
+
|
195 |
+
# Початок нового раунду
|
196 |
+
if "Starting Diagnostic Loop" in line:
|
197 |
+
if current_round and buffer:
|
198 |
+
# Зберігаємо попередній раунд
|
199 |
+
self._save_buffered_conversation(session, current_round, buffer)
|
200 |
+
buffer = []
|
201 |
+
|
202 |
+
# Витягуємо номер раунду
|
203 |
+
round_match = re.search(r'Starting Diagnostic Loop (\d+)/\d+', line)
|
204 |
+
if round_match:
|
205 |
+
round_number = int(round_match.group(1))
|
206 |
+
else:
|
207 |
+
round_number += 1
|
208 |
+
|
209 |
+
current_round = AgentConversation(
|
210 |
+
round_number=round_number,
|
211 |
+
start_time=datetime.now().isoformat()
|
212 |
+
)
|
213 |
+
session.conversations.append(current_round)
|
214 |
+
continue
|
215 |
+
|
216 |
+
# Визначаємо початок виводу агента за рамкою
|
217 |
+
if "Agent Name" in line and ("╭" in line or "┌" in line):
|
218 |
+
# Витягуємо ім'я агента
|
219 |
+
agent_match = re.search(r'Agent Name (\w+\.?\s*\w+)', line)
|
220 |
+
if agent_match:
|
221 |
+
if buffer and current_agent and current_round:
|
222 |
+
# Зберігаємо повідомлення попереднього агента
|
223 |
+
self._add_agent_message(current_round, current_agent, '\n'.join(buffer))
|
224 |
+
buffer = []
|
225 |
+
current_agent = agent_match.group(1).strip()
|
226 |
+
in_agent_output = True
|
227 |
+
continue
|
228 |
+
|
229 |
+
# Кінець виводу агента
|
230 |
+
if in_agent_output and ("╰" in line or "└" in line):
|
231 |
+
in_agent_output = False
|
232 |
+
if buffer and current_agent and current_round:
|
233 |
+
self._add_agent_message(current_round, current_agent, '\n'.join(buffer))
|
234 |
+
buffer = []
|
235 |
+
current_agent = None
|
236 |
+
continue
|
237 |
+
|
238 |
+
# Збираємо вміст всередині рамки
|
239 |
+
if in_agent_output and current_agent:
|
240 |
+
# Очищаємо лінію від символів рамки
|
241 |
+
clean_line = re.sub(r'^[│|]\s*', '', line)
|
242 |
+
clean_line = re.sub(r'\s*[│|]\s*', '', clean_line)
|
243 |
+
if clean_line.strip():
|
244 |
+
buffer.append(clean_line.strip())
|
245 |
+
continue
|
246 |
+
|
247 |
+
# Логи від INFO рівня
|
248 |
+
if " | INFO " in line:
|
249 |
+
# Парсимо INFO логи
|
250 |
+
info_match = re.search(r'mai_dx\.main:(\w+):.*? - (.+)', line)
|
251 |
+
if info_match and current_round:
|
252 |
+
action = info_match.group(1)
|
253 |
+
content = info_match.group(2)
|
254 |
+
|
255 |
+
# Визначаємо агента з контексту
|
256 |
+
agent_name = self._extract_agent_from_log(content)
|
257 |
+
if agent_name:
|
258 |
+
self._add_agent_message(
|
259 |
+
current_round,
|
260 |
+
agent_name,
|
261 |
+
content,
|
262 |
+
message_type="log"
|
263 |
+
)
|
264 |
+
|
265 |
+
# Спеціальні маркери
|
266 |
+
if "Panel decision:" in content:
|
267 |
+
decision_match = re.search(r'Panel decision: (\w+) -> (.+)', content)
|
268 |
+
if decision_match:
|
269 |
+
current_round.decision = f"{decision_match.group(1)}: {decision_match.group(2)}"
|
270 |
+
|
271 |
+
if "Current cost:" in content:
|
272 |
+
cost_match = re.search(r'Current cost: \$(\d+(?:\.\d+)?)', content)
|
273 |
+
if cost_match:
|
274 |
+
current_round.cost_incurred = float(cost_match.group(1))
|
275 |
+
|
276 |
+
if "tests ordered:" in content.lower():
|
277 |
+
# Можна додати парсинг тестів
|
278 |
+
pass
|
279 |
+
|
280 |
+
# Зберігаємо останній буфер
|
281 |
+
if buffer and current_round and current_agent:
|
282 |
+
self._add_agent_message(current_round, current_agent, '\n'.join(buffer))
|
283 |
+
|
284 |
+
# Завершуємо останній раунд
|
285 |
+
if current_round:
|
286 |
+
current_round.end_time = datetime.now().isoformat()
|
287 |
+
|
288 |
+
def _extract_agent_from_log(self, log_content: str) -> Optional[str]:
|
289 |
+
"""Витягти ім'я агента з лог повідомлення"""
|
290 |
+
agent_patterns = {
|
291 |
+
'Dr. Hypothesis': ['Dr. Hypothesis', 'differential diagnosis'],
|
292 |
+
'Dr. Test-Chooser': ['Dr. Test-Chooser', 'selecting optimal tests'],
|
293 |
+
'Dr. Challenger': ['Dr. Challenger', 'challenging assumptions'],
|
294 |
+
'Dr. Stewardship': ['Dr. Stewardship', 'cost-effectiveness'],
|
295 |
+
'Dr. Checklist': ['Dr. Checklist', 'quality control'],
|
296 |
+
'Consensus Coordinator': ['Consensus Coordinator', 'synthesizing panel'],
|
297 |
+
'Judge': ['Judge', 'evaluation'],
|
298 |
+
'Gatekeeper': ['Gatekeeper']
|
299 |
+
}
|
300 |
+
|
301 |
+
for agent, patterns in agent_patterns.items():
|
302 |
+
for pattern in patterns:
|
303 |
+
if pattern in log_content:
|
304 |
+
return agent
|
305 |
+
return None
|
306 |
+
|
307 |
+
def _extract_agent_name(self, line: str) -> Optional[str]:
|
308 |
+
"""Витягти ім'я агента з рядка"""
|
309 |
+
agents = [
|
310 |
+
"Dr. Hypothesis", "Dr. Test-Chooser", "Dr. Challenger",
|
311 |
+
"Dr. Stewardship", "Dr. Checklist", "Consensus Coordinator",
|
312 |
+
"Gatekeeper", "Judge"
|
313 |
+
]
|
314 |
+
|
315 |
+
for agent in agents:
|
316 |
+
if agent in line:
|
317 |
+
return agent
|
318 |
+
return None
|
319 |
+
|
320 |
+
def _extract_round_number(self, line: str) -> int:
|
321 |
+
"""Витягти номер раунду"""
|
322 |
+
import re
|
323 |
+
match = re.search(r'(\d+)', line)
|
324 |
+
return int(match.group(1)) if match else 0
|
325 |
+
|
326 |
+
def _extract_tests(self, line: str) -> List[str]:
|
327 |
+
"""Витягти список тестів"""
|
328 |
+
# Тут потрібна більш складна логіка залежно від формату
|
329 |
+
return []
|
330 |
+
|
331 |
+
def _extract_questions(self, line: str) -> List[str]:
|
332 |
+
"""Витягти список питань"""
|
333 |
+
# Тут потрібна більш складна логіка залежно від формату
|
334 |
+
return []
|
335 |
+
|
336 |
+
def _extract_cost(self, line: str) -> Optional[float]:
|
337 |
+
"""Витягти вартість"""
|
338 |
+
import re
|
339 |
+
match = re.search(r'\$?(\d+(?:\.\d+)?)', line)
|
340 |
+
return float(match.group(1)) if match else None
|
341 |
+
|
342 |
+
def _add_agent_message(self, conversation: AgentConversation, agent_name: str, content: str, message_type: Optional[str] = None):
|
343 |
+
"""Додати повідомлення агента до розмови"""
|
344 |
+
if not conversation or not content.strip():
|
345 |
+
return
|
346 |
+
|
347 |
+
if not message_type:
|
348 |
+
message_type = self._determine_message_type(agent_name, content)
|
349 |
+
|
350 |
+
message = AgentMessage(
|
351 |
+
timestamp=datetime.now().isoformat(),
|
352 |
+
agent_name=agent_name,
|
353 |
+
message_type=message_type,
|
354 |
+
content=content.strip(),
|
355 |
+
metadata={}
|
356 |
+
)
|
357 |
+
|
358 |
+
conversation.messages.append(message)
|
359 |
+
|
360 |
+
def _determine_message_type(self, agent_name: str, content: str) -> str:
|
361 |
+
"""Визначити тип повідомлення"""
|
362 |
+
content_lower = content.lower()
|
363 |
+
|
364 |
+
if "analyzing" in content_lower or "considering" in content_lower:
|
365 |
+
return "reasoning"
|
366 |
+
elif "recommend" in content_lower or "suggest" in content_lower:
|
367 |
+
return "decision"
|
368 |
+
elif "?" in content:
|
369 |
+
return "input"
|
370 |
+
else:
|
371 |
+
return "output"
|
372 |
+
|
373 |
+
def _save_buffered_conversation(self, session: DiagnosisSession, conversation: AgentConversation, buffer: List[str]):
|
374 |
+
"""Зберегти буферизовану розмову"""
|
375 |
+
if conversation and buffer:
|
376 |
+
conversation.end_time = datetime.now().isoformat()
|
377 |
+
# Можна додати додаткову обробку
|
378 |
+
|
379 |
+
def end_session(self, case_id: str, **kwargs) -> str:
|
380 |
+
"""Завершити сесію та зберегти всі дані"""
|
381 |
+
if case_id not in self.sessions:
|
382 |
+
return "no_active_session"
|
383 |
+
|
384 |
+
session = self.sessions[case_id]
|
385 |
+
|
386 |
+
# Оновлюємо фінальні дані
|
387 |
+
session.diagnosis = kwargs.get('final_diagnosis', session.diagnosis)
|
388 |
+
session.confidence = kwargs.get('confidence', session.confidence)
|
389 |
+
session.cost = kwargs.get('cost', session.cost)
|
390 |
+
session.reasoning = kwargs.get('reasoning', session.reasoning)
|
391 |
+
session.iterations = len(session.conversations)
|
392 |
+
session.status = "✅ Успішно" if session.confidence >= 3.0 else "⚠️ Потребує перегляду"
|
393 |
+
|
394 |
+
start_time = datetime.fromisoformat(session.timestamp)
|
395 |
+
session.duration = (datetime.now() - start_time).total_seconds()
|
396 |
+
|
397 |
+
# Зберігаємо у файл
|
398 |
+
self._save_session_to_file(session)
|
399 |
+
|
400 |
+
del self.sessions[case_id]
|
401 |
+
return case_id
|
402 |
+
|
403 |
+
def _save_session_to_file(self, session: DiagnosisSession):
|
404 |
+
"""Зберегти сесію у JSON файл з повною структурою"""
|
405 |
+
file_path = os.path.join(self.log_dir, f"{session.case_id}.json")
|
406 |
+
|
407 |
+
# Конвертуємо в словник з усіма вкладеними даними
|
408 |
+
session_dict = asdict(session)
|
409 |
+
|
410 |
+
with open(file_path, 'w', encoding='utf-8') as f:
|
411 |
+
json.dump(session_dict, f, ensure_ascii=False, indent=2)
|
412 |
+
|
413 |
+
# Додатково зберігаємо сирий вивід
|
414 |
+
raw_output_path = os.path.join(self.log_dir, f"{session.case_id}_raw.txt")
|
415 |
+
with open(raw_output_path, 'w', encoding='utf-8') as f:
|
416 |
+
f.write(session.raw_output)
|
417 |
+
|
418 |
+
def get_session_conversations(self, case_id: str) -> List[AgentConversation]:
|
419 |
+
"""Отримати всі розмови для сесії"""
|
420 |
+
file_path = os.path.join(self.log_dir, f"{case_id}.json")
|
421 |
+
|
422 |
+
if not os.path.exists(file_path):
|
423 |
+
# Спробувати з активних сесій
|
424 |
+
if case_id in self.sessions:
|
425 |
+
return self.sessions[case_id].conversations
|
426 |
+
return []
|
427 |
+
|
428 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
429 |
+
data = json.load(f)
|
430 |
+
|
431 |
+
# Реконструюємо об'єкти з JSON
|
432 |
+
conversations = []
|
433 |
+
for conv_data in data.get('conversations', []):
|
434 |
+
messages = [AgentMessage(**msg) for msg in conv_data.get('messages', [])]
|
435 |
+
conv = AgentConversation(
|
436 |
+
round_number=conv_data['round_number'],
|
437 |
+
start_time=conv_data['start_time'],
|
438 |
+
end_time=conv_data.get('end_time'),
|
439 |
+
messages=messages,
|
440 |
+
decision=conv_data.get('decision'),
|
441 |
+
tests_ordered=conv_data.get('tests_ordered', []),
|
442 |
+
questions_asked=conv_data.get('questions_asked', []),
|
443 |
+
cost_incurred=conv_data.get('cost_incurred', 0.0)
|
444 |
+
)
|
445 |
+
conversations.append(conv)
|
446 |
+
|
447 |
+
return conversations
|
448 |
+
|
449 |
+
def export_conversation_html(self, case_id: str) -> str:
|
450 |
+
"""Експортувати розмови у читабельний HTML формат"""
|
451 |
+
conversations = self.get_session_conversations(case_id)
|
452 |
+
|
453 |
+
html_content = f"""
|
454 |
+
<!DOCTYPE html>
|
455 |
+
<html>
|
456 |
+
<head>
|
457 |
+
<meta charset="UTF-8">
|
458 |
+
<title>MAI-DX Conversation Log - {case_id}</title>
|
459 |
+
<style>
|
460 |
+
body {{ font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }}
|
461 |
+
.round {{ background: white; padding: 20px; margin: 20px 0; border-radius: 10px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }}
|
462 |
+
.agent-message {{ margin: 10px 0; padding: 10px; border-left: 4px solid #007bff; background: #f8f9fa; }}
|
463 |
+
.agent-name {{ font-weight: bold; color: #007bff; }}
|
464 |
+
.message-type {{ font-size: 0.9em; color: #666; }}
|
465 |
+
.decision {{ background: #d4edda; border-color: #28a745; }}
|
466 |
+
.tests {{ background: #fff3cd; border-color: #ffc107; }}
|
467 |
+
.cost {{ color: #dc3545; font-weight: bold; }}
|
468 |
+
.raw-output {{ background: #2d3748; color: #e2e8f0; padding: 15px; border-radius: 5px; font-family: 'Courier New', monospace; white-space: pre-wrap; }}
|
469 |
+
</style>
|
470 |
+
</head>
|
471 |
+
<body>
|
472 |
+
<h1>MAI-DX Diagnostic Session: {case_id}</h1>
|
473 |
+
"""
|
474 |
+
|
475 |
+
for conv in conversations:
|
476 |
+
html_content += f"""
|
477 |
+
<div class="round">
|
478 |
+
<h2>Round {conv.round_number}</h2>
|
479 |
+
<p><strong>Time:</strong> {conv.start_time} - {conv.end_time or 'In Progress'}</p>
|
480 |
+
"""
|
481 |
+
|
482 |
+
for msg in conv.messages:
|
483 |
+
css_class = "agent-message"
|
484 |
+
if msg.message_type == "decision":
|
485 |
+
css_class += " decision"
|
486 |
+
|
487 |
+
html_content += f"""
|
488 |
+
<div class="{css_class}">
|
489 |
+
<div class="agent-name">{msg.agent_name}</div>
|
490 |
+
<div class="message-type">[{msg.message_type}]</div>
|
491 |
+
<div class="content">{msg.content}</div>
|
492 |
+
</div>
|
493 |
+
"""
|
494 |
+
|
495 |
+
if conv.decision:
|
496 |
+
html_content += f'<div class="decision"><strong>Decision:</strong> {conv.decision}</div>'
|
497 |
+
|
498 |
+
if conv.tests_ordered:
|
499 |
+
html_content += f'<div class="tests"><strong>Tests:</strong> {", ".join(conv.tests_ordered)}</div>'
|
500 |
+
|
501 |
+
if conv.cost_incurred > 0:
|
502 |
+
html_content += f'<div class="cost">Cost: ${conv.cost_incurred:.2f}</div>'
|
503 |
+
|
504 |
+
html_content += "</div>"
|
505 |
+
|
506 |
+
html_content += """
|
507 |
+
</body>
|
508 |
+
</html>
|
509 |
+
"""
|
510 |
+
|
511 |
+
# Зберігаємо HTML файл
|
512 |
+
html_path = os.path.join(self.log_dir, f"{case_id}_conversation.html")
|
513 |
+
with open(html_path, 'w', encoding='utf-8') as f:
|
514 |
+
f.write(html_content)
|
515 |
+
|
516 |
+
return html_path
|
lmai_dx_interface_log.py
DELETED
@@ -1,601 +0,0 @@
|
|
1 |
-
#!/usr/bin/env python3
|
2 |
-
"""
|
3 |
-
Виправлений MAI-DX Gradio Interface з сумісністю для нових версій Gradio
|
4 |
-
"""
|
5 |
-
import os
|
6 |
-
import sys
|
7 |
-
import json
|
8 |
-
import time
|
9 |
-
import pandas as pd
|
10 |
-
import gradio as gr
|
11 |
-
from datetime import datetime, timedelta
|
12 |
-
from typing import Dict, List, Tuple, Optional
|
13 |
-
from dataclasses import dataclass, asdict
|
14 |
-
import warnings
|
15 |
-
import threading
|
16 |
-
import queue
|
17 |
-
import io
|
18 |
-
import re # Імпортуємо re для парсингу
|
19 |
-
from contextlib import redirect_stdout, redirect_stderr
|
20 |
-
|
21 |
-
# Налаштування середовища
|
22 |
-
os.environ.update({
|
23 |
-
"SWARMS_VERBOSITY": "ERROR",
|
24 |
-
"RICH_TRACEBACK": "0",
|
25 |
-
"SWARMS_SHOW_PANEL": "false",
|
26 |
-
"SWARMS_AUTO_PRINT": "false"
|
27 |
-
})
|
28 |
-
warnings.filterwarnings("ignore")
|
29 |
-
|
30 |
-
from dotenv import load_dotenv
|
31 |
-
load_dotenv()
|
32 |
-
|
33 |
-
# Патч Rich formatter
|
34 |
-
def patch_rich_formatter():
|
35 |
-
try:
|
36 |
-
import swarms.utils.formatter
|
37 |
-
def dummy_print_panel(*args, **kwargs):
|
38 |
-
pass
|
39 |
-
if hasattr(swarms.utils.formatter, 'Formatter'):
|
40 |
-
swarms.utils.formatter.Formatter._print_panel = dummy_print_panel
|
41 |
-
return True
|
42 |
-
except:
|
43 |
-
return False
|
44 |
-
|
45 |
-
patch_rich_formatter()
|
46 |
-
|
47 |
-
# Імпорт MAI-DX
|
48 |
-
try:
|
49 |
-
from mai_dx import MaiDxOrchestrator
|
50 |
-
MAI_DX_AVAILABLE = True
|
51 |
-
except ImportError as e:
|
52 |
-
MAI_DX_AVAILABLE = False
|
53 |
-
IMPORT_ERROR = str(e)
|
54 |
-
|
55 |
-
# Перевірка доступності Plotly
|
56 |
-
try:
|
57 |
-
import plotly.graph_objects as go
|
58 |
-
import plotly.express as px
|
59 |
-
PLOTLY_AVAILABLE = True
|
60 |
-
except ImportError:
|
61 |
-
PLOTLY_AVAILABLE = False
|
62 |
-
print("⚠️ Plotly не встановлено, візуалізації будуть недоступні")
|
63 |
-
|
64 |
-
# Імпорт логгера (створюємо заглушку якщо немає)
|
65 |
-
try:
|
66 |
-
from agent_conversation_logger import AgentConversationLogger, DiagnosisSession
|
67 |
-
LOGGER_AVAILABLE = True
|
68 |
-
except ImportError as e:
|
69 |
-
LOGGER_AVAILABLE = False
|
70 |
-
print(f"⚠️ Не вдалося завантажити логгер, буде використана заглушка: {e}")
|
71 |
-
|
72 |
-
@dataclass
|
73 |
-
class DiagnosisSession:
|
74 |
-
case_id: str
|
75 |
-
timestamp: str
|
76 |
-
case_name: str
|
77 |
-
patient_info: str
|
78 |
-
mode: str
|
79 |
-
budget: int
|
80 |
-
diagnosis: str
|
81 |
-
confidence: float
|
82 |
-
cost: float
|
83 |
-
iterations: int
|
84 |
-
duration: float
|
85 |
-
status: str
|
86 |
-
reasoning: str
|
87 |
-
|
88 |
-
class AgentConversationLogger:
|
89 |
-
def __init__(self, *args, **kwargs):
|
90 |
-
self.conversations = []
|
91 |
-
def start_conversation(self, *args):
|
92 |
-
return f"mock_{len(self.conversations)}"
|
93 |
-
def log_agent_message(self, *args, **kwargs):
|
94 |
-
pass
|
95 |
-
def end_conversation(self, *args, **kwargs):
|
96 |
-
return f"saved_{len(self.conversations)}"
|
97 |
-
def list_conversations(self):
|
98 |
-
return self.conversations
|
99 |
-
def export_conversation_report(self, case_id, format):
|
100 |
-
return f"report_{case_id}.{format}"
|
101 |
-
def export_analytics_csv(self):
|
102 |
-
return "analytics.csv"
|
103 |
-
|
104 |
-
class RealTimeMetrics:
|
105 |
-
"""Метрики в реальному часі"""
|
106 |
-
|
107 |
-
def __init__(self):
|
108 |
-
self.reset()
|
109 |
-
|
110 |
-
def reset(self):
|
111 |
-
self.start_time = time.time()
|
112 |
-
self.agents_activity = {
|
113 |
-
'Dr. Hypothesis': 0,
|
114 |
-
'Dr. Test-Chooser': 0,
|
115 |
-
'Dr. Challenger': 0,
|
116 |
-
'Dr. Stewardship': 0,
|
117 |
-
'Dr. Checklist': 0,
|
118 |
-
'Consensus Coordinator': 0,
|
119 |
-
'Gatekeeper': 0,
|
120 |
-
'Judge': 0
|
121 |
-
}
|
122 |
-
self.cost_progression = []
|
123 |
-
self.confidence_progression = []
|
124 |
-
self.decision_timeline = []
|
125 |
-
|
126 |
-
def update_agent_activity(self, agent_name: str):
|
127 |
-
if agent_name in self.agents_activity:
|
128 |
-
self.agents_activity[agent_name] += 1
|
129 |
-
|
130 |
-
def add_cost_point(self, cost: float):
|
131 |
-
self.cost_progression.append({
|
132 |
-
'time': time.time() - self.start_time,
|
133 |
-
'cost': cost
|
134 |
-
})
|
135 |
-
|
136 |
-
def add_confidence_point(self, confidence: float):
|
137 |
-
self.confidence_progression.append({
|
138 |
-
'time': time.time() - self.start_time,
|
139 |
-
'confidence': confidence
|
140 |
-
})
|
141 |
-
|
142 |
-
class EnhancedMAIDXInterface:
|
143 |
-
"""Покращений інтерфейс з сумісністю для Gradio"""
|
144 |
-
|
145 |
-
def __init__(self):
|
146 |
-
self.sessions_history = []
|
147 |
-
self.conversation_logger = AgentConversationLogger("mai_dx_logs")
|
148 |
-
self.current_metrics = RealTimeMetrics()
|
149 |
-
|
150 |
-
# Розширені тестові кейси (з виправленими очікуваними діагнозами)
|
151 |
-
self.sample_cases = {
|
152 |
-
"🫀 Кардіологічний (Гострий MI)": {
|
153 |
-
"info": """Пацієнт: 58-річний чоловік, менеджер, гіпертонія в анамнезі
|
154 |
-
Скарги: Гострий роздираючий біль у грудях 3 години, іррадіація в ліву руку
|
155 |
-
Огляд: Блідий, пітливий, АТ 160/90, ЧСС 95
|
156 |
-
ЕКГ: ST-підйоми у відведеннях II, III, aVF (нижня стінка)
|
157 |
-
Тропонін I: 8.5 нг/мл (норма <0.04)
|
158 |
-
Анамнез: Куріння 30 років, дислипідемія, сімейний анамнез ІХС""",
|
159 |
-
"expected": "Acute inferior wall myocardial infarction (STEMI)"
|
160 |
-
},
|
161 |
-
|
162 |
-
"🧠 Неврологічний (Гострий інсульт)": {
|
163 |
-
"info": """Пацієнтка: 67-річна жінка, раптові неврологічні симптоми
|
164 |
-
Презентація: Раптова слабкість правої сторони 2 години тому
|
165 |
-
Огляд: Свідома, дезорієнтована у часі, правостороннє опущення обличчя
|
166 |
-
Неврологія: Правостороння геміплегія, афазія, девіація очей вліво
|
167 |
-
КТ голови: Гострого крововиливу немає, рання ішемія у лівій МСА
|
168 |
-
NIHSS: 15 балів""",
|
169 |
-
"expected": "Acute ischemic stroke in the left middle cerebral artery territory"
|
170 |
-
},
|
171 |
-
|
172 |
-
"🦠 Інфекційний (Сепсис)": {
|
173 |
-
"info": """Пацієнт: 45-річний чоловік з прогресуючою лихоманкою
|
174 |
-
Скарги: Висока температура 39.5°C, озноб, загальна слабкість 3 дні
|
175 |
-
Огляд: Гарячий, тахікардія 120/хв, гіпотензія 85/50
|
176 |
-
Лабораторно: Лейкоцити 18000, С-реактивний білок 180, прокальцитонін 5.2
|
177 |
-
Посів крові: Pending, lactate 4.2 ммоль/л
|
178 |
-
Анамнез: Нещодавна стоматологічна процедура""",
|
179 |
-
"expected": "Sepsis with a possible odontogenic source"
|
180 |
-
}
|
181 |
-
}
|
182 |
-
|
183 |
-
def diagnose_with_enhanced_tracking(
|
184 |
-
self,
|
185 |
-
case_name: str,
|
186 |
-
patient_info: str,
|
187 |
-
mode: str,
|
188 |
-
budget: int,
|
189 |
-
max_iterations: int,
|
190 |
-
model_name: str,
|
191 |
-
expected_diagnosis: str = "",
|
192 |
-
enable_logging: bool = True,
|
193 |
-
progress=gr.Progress()
|
194 |
-
) -> Tuple[str, str, str, Optional[object], Optional[object], str]:
|
195 |
-
"""Діагностика з покращеним відстеженням - ВИПРАВЛЕНА ВЕРСІЯ"""
|
196 |
-
|
197 |
-
if not MAI_DX_AVAILABLE:
|
198 |
-
return self._format_error(f"❌ MAI-DX недоступний: {IMPORT_ERROR}")
|
199 |
-
|
200 |
-
if not patient_info.strip():
|
201 |
-
return self._format_error("❌ Введіть інформацію про пацієнта")
|
202 |
-
|
203 |
-
self.current_metrics.reset()
|
204 |
-
conversation_log = ""
|
205 |
-
case_id = None
|
206 |
-
|
207 |
-
try:
|
208 |
-
progress(0.1, desc="🚀 Ініціалізація системи...")
|
209 |
-
|
210 |
-
if enable_logging:
|
211 |
-
case_id = self.conversation_logger.start_conversation(
|
212 |
-
case_name or f"Case_{datetime.now().strftime('%H%M%S')}",
|
213 |
-
patient_info,
|
214 |
-
mode
|
215 |
-
)
|
216 |
-
conversation_log += f"📝 Розпочато логування: {case_id}\n\n"
|
217 |
-
|
218 |
-
progress(0.2, desc="🤖 Створення AI-панелі лікарів...")
|
219 |
-
|
220 |
-
orchestrator = MaiDxOrchestrator(
|
221 |
-
model_name=model_name,
|
222 |
-
max_iterations=max_iterations,
|
223 |
-
initial_budget=budget,
|
224 |
-
mode=mode if mode != "ensemble" else "budgeted"
|
225 |
-
)
|
226 |
-
|
227 |
-
progress(0.3, desc="🔍 Запуск діагностичного процесу...")
|
228 |
-
start_time = time.time()
|
229 |
-
|
230 |
-
# Запуск діагностики з перехопленням виводу
|
231 |
-
with io.StringIO() as captured_output, redirect_stdout(captured_output):
|
232 |
-
try:
|
233 |
-
result = orchestrator.run(
|
234 |
-
initial_case_info=patient_info,
|
235 |
-
full_case_details=patient_info,
|
236 |
-
ground_truth_diagnosis=expected_diagnosis or "Unknown"
|
237 |
-
)
|
238 |
-
except Exception as e:
|
239 |
-
# Спроба обробити помилку і отримати частковий результат
|
240 |
-
print(f"\nПОМИЛКА ПІД ЧАС ВИКОНАННЯ: {e}")
|
241 |
-
result = getattr(orchestrator, 'result', None)
|
242 |
-
if not result:
|
243 |
-
raise e from e
|
244 |
-
finally:
|
245 |
-
captured_text = captured_output.getvalue()
|
246 |
-
|
247 |
-
duration = time.time() - start_time
|
248 |
-
progress(0.9, desc="📊 Обробка результатів...")
|
249 |
-
|
250 |
-
if enable_logging and captured_text:
|
251 |
-
conversation_log += "🤖 Захоплені логи агентів:\n" + "=" * 60 + "\n"
|
252 |
-
conversation_log += captured_text + "\n" + "=" * 60 + "\n\n"
|
253 |
-
# ВИПРАВЛЕНО: Парсинг логів для метрик та повідомлень
|
254 |
-
self._parse_and_log_agent_activity(captured_text)
|
255 |
-
|
256 |
-
if enable_logging:
|
257 |
-
# Логування фінальної оцінки
|
258 |
-
self.conversation_logger.log_agent_message(
|
259 |
-
agent_name="Judge",
|
260 |
-
action="final_evaluation",
|
261 |
-
content=result.final_diagnosis,
|
262 |
-
reasoning=getattr(result, 'accuracy_reasoning', 'Фінальна оцінка'),
|
263 |
-
confidence=result.accuracy_score / 5.0, # Нормалізація до 0-1
|
264 |
-
cost=result.total_cost,
|
265 |
-
iteration=getattr(result, 'iterations', max_iterations)
|
266 |
-
)
|
267 |
-
|
268 |
-
# ВИПРАВЛЕНО: Завершення сесії з усіма даними
|
269 |
-
saved_case_id = self.conversation_logger.end_conversation(
|
270 |
-
final_diagnosis=result.final_diagnosis,
|
271 |
-
confidence=result.accuracy_score,
|
272 |
-
cost=result.total_cost,
|
273 |
-
budget=budget,
|
274 |
-
reasoning=getattr(result, 'accuracy_reasoning', 'N/A'),
|
275 |
-
iterations=getattr(result, 'iterations', max_iterations)
|
276 |
-
)
|
277 |
-
conversation_log += f"💾 Сесію збережено як: {saved_case_id}\n"
|
278 |
-
|
279 |
-
# Створення об'єкту сесії для відображення
|
280 |
-
session = DiagnosisSession(
|
281 |
-
case_id=case_id,
|
282 |
-
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
283 |
-
case_name=case_name or f"Case_{len(self.sessions_history) + 1}",
|
284 |
-
patient_info=patient_info[:200] + "..." if len(patient_info) > 200 else patient_info,
|
285 |
-
mode=mode,
|
286 |
-
budget=budget,
|
287 |
-
diagnosis=result.final_diagnosis,
|
288 |
-
confidence=result.accuracy_score,
|
289 |
-
cost=result.total_cost,
|
290 |
-
iterations=getattr(result, 'iterations', max_iterations),
|
291 |
-
duration=duration,
|
292 |
-
status="✅ Успішно" if result.accuracy_score >= 3.0 else "⚠️ Потребує перегляду",
|
293 |
-
reasoning=getattr(result, 'accuracy_reasoning', 'Недоступно')[:300] + "..."
|
294 |
-
)
|
295 |
-
self.sessions_history.append(session)
|
296 |
-
|
297 |
-
progress(1.0, desc="✅ Готово!")
|
298 |
-
|
299 |
-
metrics_plot = self._create_metrics_visualization_safe()
|
300 |
-
agent_plot = self._create_agent_activity_chart_safe()
|
301 |
-
|
302 |
-
return (
|
303 |
-
self._format_main_result(session, result),
|
304 |
-
self._format_detailed_analysis(session, result),
|
305 |
-
self._generate_enhanced_recommendations(result, expected_diagnosis),
|
306 |
-
metrics_plot,
|
307 |
-
agent_plot,
|
308 |
-
conversation_log
|
309 |
-
)
|
310 |
-
|
311 |
-
except Exception as e:
|
312 |
-
error_msg = f"❌ Помилка діагностики: {str(e)}"
|
313 |
-
if case_id:
|
314 |
-
error_msg += f"\n🗂️ Case ID: {case_id}"
|
315 |
-
return self._format_error(error_msg)
|
316 |
-
|
317 |
-
def _create_metrics_visualization_safe(self):
|
318 |
-
try:
|
319 |
-
if not PLOTLY_AVAILABLE: return None
|
320 |
-
fig = go.Figure()
|
321 |
-
if not self.current_metrics.cost_progression:
|
322 |
-
return None # Немає даних - не показуємо графік
|
323 |
-
|
324 |
-
times = [p['time'] for p in self.current_metrics.cost_progression]
|
325 |
-
costs = [p['cost'] for p in self.current_metrics.cost_progression]
|
326 |
-
|
327 |
-
fig.add_trace(go.Scatter(x=times, y=costs, mode='lines+markers', name='Вартість', line=dict(color='#1f77b4', width=3)))
|
328 |
-
fig.update_layout(title='📈 Прогресія вартості діагностики', xaxis_title='Час (секунди)', yaxis_title='Вартість ($)', template='plotly_white', height=400)
|
329 |
-
return fig
|
330 |
-
except Exception as e:
|
331 |
-
print(f"Помилка візуалізації метрик: {e}")
|
332 |
-
return None
|
333 |
-
|
334 |
-
def _create_agent_activity_chart_safe(self):
|
335 |
-
try:
|
336 |
-
if not PLOTLY_AVAILABLE: return None
|
337 |
-
|
338 |
-
activity_data = {k: v for k, v in self.current_metrics.agents_activity.items() if v > 0}
|
339 |
-
if not activity_data: return None
|
340 |
-
|
341 |
-
agents = list(activity_data.keys())
|
342 |
-
activities = list(activity_data.values())
|
343 |
-
|
344 |
-
colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FECA57', '#FF9FF3', '#54A0FF', '#5F27CD']
|
345 |
-
|
346 |
-
fig = go.Figure(data=[go.Bar(x=agents, y=activities, marker_color=colors, text=activities, textposition='auto')])
|
347 |
-
fig.update_layout(title='🤖 Активність ШІ-агентів', xaxis_title='Агенти', yaxis_title='Кількість взаємодій', template='plotly_white', height=400, showlegend=False)
|
348 |
-
fig.update_xaxes(tickangle=-45)
|
349 |
-
return fig
|
350 |
-
except Exception as e:
|
351 |
-
print(f"Помилка графіку агентів: {e}")
|
352 |
-
return None
|
353 |
-
|
354 |
-
def _parse_and_log_agent_activity(self, captured_text: str):
|
355 |
-
"""
|
356 |
-
Парсить перехоплений текстовий лог для оновлення метрик та логування повідомлень.
|
357 |
-
"""
|
358 |
-
lines = captured_text.split('\n')
|
359 |
-
|
360 |
-
agent_keywords = {
|
361 |
-
"Dr. Hypothesis": "Dr. Hypothesis", "Dr. Test-Chooser": "Dr. Test-Chooser",
|
362 |
-
"Dr. Challenger": "Dr. Challenger", "Dr. Stewardship": "Dr. Stewardship",
|
363 |
-
"Dr. Checklist": "Dr. Checklist", "Consensus Coordinator": "Consensus Coordinator"
|
364 |
-
}
|
365 |
-
|
366 |
-
for line in lines:
|
367 |
-
line = line.strip()
|
368 |
-
if not line: continue
|
369 |
-
|
370 |
-
# 1. Логування активності агентів
|
371 |
-
for keyword, agent_name in agent_keywords.items():
|
372 |
-
if keyword in line and "analyzing" in line:
|
373 |
-
self.current_metrics.update_agent_activity(agent_name)
|
374 |
-
self.conversation_logger.log_agent_message(
|
375 |
-
agent_name=agent_name, action="analysis_start", content=line,
|
376 |
-
reasoning="Parsed from orchestrator output", confidence=0.0, cost=0.0, iteration=0
|
377 |
-
)
|
378 |
-
break
|
379 |
-
|
380 |
-
# 2. Логування рішень панелі
|
381 |
-
if "Panel decision:" in line:
|
382 |
-
self.current_metrics.update_agent_activity("Consensus Coordinator")
|
383 |
-
self.conversation_logger.log_agent_message(
|
384 |
-
agent_name="Consensus Coordinator", action="panel_decision", content=line.split("Panel decision:")[-1].strip(),
|
385 |
-
reasoning="Parsed from orchestrator output", confidence=0.0, cost=0.0, iteration=0
|
386 |
-
)
|
387 |
-
|
388 |
-
# 3. Парсинг вартості
|
389 |
-
cost_match = re.search(r'Current cost: \$(\d+(?:\.\d+)?)', line)
|
390 |
-
if cost_match:
|
391 |
-
current_cost = float(cost_match.group(1))
|
392 |
-
self.current_metrics.add_cost_point(current_cost)
|
393 |
-
|
394 |
-
def _format_main_result(self, session, result):
|
395 |
-
"""Форматування основного результату"""
|
396 |
-
confidence_emoji = "🎯" if session.confidence >= 4.0 else "👍" if session.confidence >= 3.0 else "⚠️"
|
397 |
-
efficiency = ((session.budget - session.cost) / session.budget * 100) if session.budget > 0 else 0
|
398 |
-
|
399 |
-
return f"""
|
400 |
-
## 🏥 Результати MAI-DX Діагностики
|
401 |
-
### 📋 Основна інформація
|
402 |
-
- **🗂️ Випадок**: {session.case_name}
|
403 |
-
- **⏰ Час**: {session.timestamp}
|
404 |
-
- **🔧 Режим**: {session.mode}
|
405 |
-
### {confidence_emoji} Діагностичний висновок
|
406 |
-
**{session.diagnosis}**
|
407 |
-
### 📊 Показники якості
|
408 |
-
- **Точність**: {session.confidence:.1f}/5.0 ⭐
|
409 |
-
- **Статус**: {session.status}
|
410 |
-
- **Ітерації**: {session.iterations}
|
411 |
-
### 💰 Економічні показники
|
412 |
-
- **Витрачено**: ${session.cost:,.2f}
|
413 |
-
- **Бюджет**: ${session.budget:,}
|
414 |
-
- **Ефективність**: {efficiency:.1f}% бюджету збережено
|
415 |
-
- **Швидкість**: {session.duration:.1f} сек
|
416 |
-
### 🎖️ Загальна оцінка
|
417 |
-
{self._get_overall_rating(session)}
|
418 |
-
"""
|
419 |
-
|
420 |
-
def _get_overall_rating(self, session):
|
421 |
-
"""Загальна оцінка ефективності"""
|
422 |
-
efficiency = ((session.budget - session.cost) / session.budget * 100) if session.budget > 0 else 0
|
423 |
-
if session.confidence >= 4.0 and efficiency >= 50: return "🏆 **ВІДМІННО**"
|
424 |
-
if session.confidence >= 3.0 and efficiency >= 20: return "🥈 **ДОБРЕ**"
|
425 |
-
if session.confidence >= 2.0: return "🥉 **ЗАДОВІЛЬНО**"
|
426 |
-
return "❌ **ПОТРЕБУЄ ПЕРЕГЛЯДУ**"
|
427 |
-
|
428 |
-
def _format_detailed_analysis(self, session, result):
|
429 |
-
"""Детальний аналіз"""
|
430 |
-
return f"""
|
431 |
-
## 🔬 Детальний клінічний аналіз
|
432 |
-
### 💭 Медичне обґрунтування
|
433 |
-
{session.reasoning}
|
434 |
-
### 📈 Аналіз ефективності
|
435 |
-
- **💸 Економічна ефективність**: {((session.budget - session.cost) / session.budget * 100) if session.budget > 0 else 0:.1f}%
|
436 |
-
- **🎯 Клінічна точність**: {(session.confidence / 5.0 * 100):.1f}%
|
437 |
-
### 🤖 Процес прийняття рішень
|
438 |
-
Система використала **{session.iterations} ітерацій** для консенсусу між **8 ШІ-агентами**.
|
439 |
-
"""
|
440 |
-
|
441 |
-
def _generate_enhanced_recommendations(self, result, expected_diagnosis):
|
442 |
-
"""Покращені рекомендації"""
|
443 |
-
comparison = ""
|
444 |
-
if expected_diagnosis:
|
445 |
-
is_match = expected_diagnosis.lower() in result.final_diagnosis.lower() or \
|
446 |
-
result.final_diagnosis.lower() in expected_diagnosis.lower()
|
447 |
-
comparison = f"""
|
448 |
-
### 🎯 Порівняння з очікуваним
|
449 |
-
**Очікувався**: {expected_diagnosis}
|
450 |
-
**Отримано**: {result.final_diagnosis}
|
451 |
-
**Збіг**: {'✅ Так' if is_match else '❌ Ні'}
|
452 |
-
"""
|
453 |
-
return f"""
|
454 |
-
## 💡 Клінічні рекомендації
|
455 |
-
### 🏥 Негайні дії
|
456 |
-
- 🔍 Верифікація діагнозу з лікарем
|
457 |
-
- 📋 Розгляд додаткових досліджень
|
458 |
-
### 🔬 Дослідницький потенціал
|
459 |
-
- 📊 Аналіз логів для вивчення роботи агентів
|
460 |
-
- 📈 Валідація на додаткових випадках
|
461 |
-
{comparison}
|
462 |
-
### ⚠️ Важливе застереження
|
463 |
-
🔴 **Цей діагноз згенеровано ШІ і НЕ замінює професійну медичну консультацію.**
|
464 |
-
"""
|
465 |
-
|
466 |
-
def _format_error(self, error_msg):
|
467 |
-
return (error_msg, "", "", None, None, "")
|
468 |
-
|
469 |
-
def load_sample_case(self, sample_key):
|
470 |
-
if sample_key and sample_key in self.sample_cases:
|
471 |
-
case_data = self.sample_cases[sample_key]
|
472 |
-
return case_data["info"], sample_key, case_data["expected"]
|
473 |
-
return "", "", ""
|
474 |
-
|
475 |
-
def get_enhanced_analytics(self):
|
476 |
-
try:
|
477 |
-
conversations = self.conversation_logger.list_conversations()
|
478 |
-
if not conversations:
|
479 |
-
return pd.DataFrame(), "📊 Немає даних для аналізу", []
|
480 |
-
|
481 |
-
df = pd.DataFrame(conversations)
|
482 |
-
df = df.reindex(columns=['case_id', 'timestamp', 'case_name', 'mode', 'diagnosis', 'confidence', 'cost', 'budget', 'duration', 'status'])
|
483 |
-
|
484 |
-
total_cases = len(df)
|
485 |
-
avg_accuracy = df['confidence'].mean()
|
486 |
-
avg_cost = df['cost'].mean()
|
487 |
-
|
488 |
-
summary = f"""
|
489 |
-
### 🎯 Загальна статистика
|
490 |
-
- **Всього випадків**: {total_cases}
|
491 |
-
- **Середня точність**: {avg_accuracy:.2f}/5.0 ⭐
|
492 |
-
- **Середня вартість**: ${avg_cost:,.2f}
|
493 |
-
"""
|
494 |
-
|
495 |
-
case_choices = [(f"{row['case_name']} ({row.get('case_id', 'N/A')})", row.get('case_id')) for _, row in df.iterrows()]
|
496 |
-
return df, summary, gr.Dropdown(choices=case_choices)
|
497 |
-
except Exception as e:
|
498 |
-
return pd.DataFrame(), f"❌ Помилка аналітики: {e}", []
|
499 |
-
|
500 |
-
def create_enhanced_gradio_interface():
|
501 |
-
interface = EnhancedMAIDXInterface()
|
502 |
-
custom_css = """.gradio-container {font-family: 'Inter', sans-serif;} .main-header {background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 2rem; border-radius: 15px; margin-bottom: 2rem; text-align: center;} .conversation-log {font-family: 'Fira Code', monospace; background: #2d3748; color: #e2e8f0; border-radius: 8px; padding: 1rem; max-height: 500px; overflow-y: auto; white-space: pre-wrap;}"""
|
503 |
-
|
504 |
-
with gr.Blocks(title="🏥 MAI-DX Enhanced Research Platform", theme=gr.themes.Soft(primary_hue="blue"), css=custom_css) as demo:
|
505 |
-
gr.HTML("""<div class="main-header"><h1>🏥 MAI-DX Enhanced Research Platform</h1><p>🤖 Дослідницька платформа для ШІ-діагностики з детальним логуванням</p></div>""")
|
506 |
-
|
507 |
-
with gr.Tabs():
|
508 |
-
with gr.Tab("🩺 Діагностика", elem_id="diagnosis-tab"):
|
509 |
-
with gr.Row():
|
510 |
-
with gr.Column(scale=1):
|
511 |
-
gr.Markdown("### 📝 Клінічний випадок")
|
512 |
-
case_name = gr.Textbox(label="🏷️ Назва випадку", placeholder="Наприклад: Кардіологічний випадок №1")
|
513 |
-
sample_selector = gr.Dropdown(choices=list(interface.sample_cases.keys()), label="🎯 Готові тестові випадки", interactive=True)
|
514 |
-
patient_info = gr.Textbox(label="👤 Інформація про пацієнта", lines=10, placeholder="Введіть опис клінічного випадку...")
|
515 |
-
expected_diagnosis = gr.Textbox(label="🎯 Очікуваний діагноз (для порівняння)", lines=2)
|
516 |
-
|
517 |
-
with gr.Column(scale=1):
|
518 |
-
gr.Markdown("### ⚙️ Налаштування")
|
519 |
-
mode = gr.Radio(choices=["instant", "question_only", "budgeted", "no_budget", "ensemble"], label="🔧 Режим", value="budgeted", interactive=True)
|
520 |
-
budget = gr.Slider(minimum=500, maximum=10000, step=500, value=3000, label="💵 Бюджет ($)", interactive=True)
|
521 |
-
max_iterations = gr.Slider(minimum=1, maximum=15, step=1, value=8, label="🔄 Макс. ітерацій", interactive=True)
|
522 |
-
model_name = gr.Dropdown(choices=["gemini/gemini-1.5-flash", "gpt-4", "gpt-4-turbo"], label="🤖 LLM Модель", value="gemini/gemini-1.5-flash", interactive=True)
|
523 |
-
enable_logging = gr.Checkbox(label="📝 Детальне логування", value=True, interactive=True)
|
524 |
-
diagnose_btn = gr.Button("🚀 Запустити діагностику", variant="primary", size="lg")
|
525 |
-
|
526 |
-
sample_selector.change(interface.load_sample_case, inputs=[sample_selector], outputs=[patient_info, case_name, expected_diagnosis])
|
527 |
-
|
528 |
-
gr.Markdown("--- \n ## 📊 Результати діагностики")
|
529 |
-
with gr.Row():
|
530 |
-
with gr.Column(scale=2):
|
531 |
-
main_result = gr.Markdown(label="🎯 Основний результат")
|
532 |
-
detailed_analysis = gr.Markdown(label="🔬 Детальний аналіз")
|
533 |
-
with gr.Column(scale=1):
|
534 |
-
recommendations = gr.Markdown(label="💡 Рекомендації")
|
535 |
-
|
536 |
-
with gr.Row():
|
537 |
-
metrics_plot = gr.Plot(label="📈 Метрики діагностики")
|
538 |
-
agent_activity_plot = gr.Plot(label="🤖 Активність агентів")
|
539 |
-
|
540 |
-
conversation_logs = gr.Textbox(label="💬 Логи взаємодії ШІ-агентів", lines=12, elem_classes=["conversation-log"], interactive=False, show_copy_button=True)
|
541 |
-
|
542 |
-
diagnose_btn.click(interface.diagnose_with_enhanced_tracking,
|
543 |
-
inputs=[case_name, patient_info, mode, budget, max_iterations, model_name, expected_diagnosis, enable_logging],
|
544 |
-
outputs=[main_result, detailed_analysis, recommendations, metrics_plot, agent_activity_plot, conversation_logs])
|
545 |
-
|
546 |
-
with gr.Tab("📊 Аналітика", elem_id="analytics-tab"):
|
547 |
-
gr.Markdown("### 🔍 Аналіз збережених діагностичних сесій")
|
548 |
-
with gr.Row():
|
549 |
-
refresh_analytics_btn = gr.Button("🔄 Оновити")
|
550 |
-
export_data_btn = gr.Button("📤 Експортувати CSV")
|
551 |
-
analytics_table = gr.Dataframe(label="📋 Історія сесій", interactive=False, wrap=True)
|
552 |
-
analytics_summary = gr.Markdown(label="📊 Статистичний звіт")
|
553 |
-
|
554 |
-
gr.Markdown("### 🔬 Детальний звіт по випадку")
|
555 |
-
with gr.Row():
|
556 |
-
case_selector = gr.Dropdown(label="🗂️ Оберіть випадок", choices=[], interactive=True)
|
557 |
-
analyze_case_btn = gr.Button("📋 Створити звіт (HTML)")
|
558 |
-
analysis_status = gr.Markdown()
|
559 |
-
|
560 |
-
def refresh_analytics():
|
561 |
-
df, summary, dd = interface.get_enhanced_analytics()
|
562 |
-
return df, summary, dd
|
563 |
-
|
564 |
-
refresh_analytics_btn.click(refresh_analytics, outputs=[analytics_table, analytics_summary, case_selector])
|
565 |
-
export_data_btn.click(lambda: interface.conversation_logger.export_analytics_csv(), outputs=[analysis_status])
|
566 |
-
analyze_case_btn.click(lambda case_id: f"✅ Звіт створено: {interface.conversation_logger.export_conversation_report(case_id, 'html')}" if case_id else "⚠️ Оберіть випадок", inputs=[case_selector], outputs=[analysis_status])
|
567 |
-
demo.load(refresh_analytics, outputs=[analytics_table, analytics_summary, case_selector])
|
568 |
-
|
569 |
-
with gr.Tab("📚 Документація", elem_id="docs-tab"):
|
570 |
-
gr.Markdown(f"""
|
571 |
-
### 🔧 Статус системи
|
572 |
-
- **MAI-DX доступність**: {'✅' if MAI_DX_AVAILABLE else '❌'}
|
573 |
-
- **Logger доступність**: {'✅' if LOGGER_AVAILABLE else '⚠️ Заглушка'}
|
574 |
-
- **Plotly візуалізації**: {'✅' if PLOTLY_AVAILABLE else '❌'}
|
575 |
-
- **Gradio версія**: {gr.__version__}
|
576 |
-
{'**Помилка MAI-DX**: ' + IMPORT_ERROR if not MAI_DX_AVAILABLE else ''}
|
577 |
-
### 📖 Короткий гайд
|
578 |
-
1. Оберіть готовий випадок або введіть свій.
|
579 |
-
2. Налаштуйте режим, бюджет та модель.
|
580 |
-
3. Натис��іть "Запустити діагностику".
|
581 |
-
4. Аналізуйте результати та логи взаємодії агентів.
|
582 |
-
5. Переходьте на вкладку "Аналітика" для перегляду історії.
|
583 |
-
""")
|
584 |
-
|
585 |
-
return demo
|
586 |
-
|
587 |
-
if __name__ == "__main__":
|
588 |
-
print("🚀 Запуск виправленого MAI-DX Enhanced Research Platform...")
|
589 |
-
print(f"🤖 MAI-DX доступність: {'✅' if MAI_DX_AVAILABLE else '❌'}")
|
590 |
-
print(f"📊 Plotly візуалізації: {'✅' if PLOTLY_AVAILABLE else '❌'}")
|
591 |
-
print(f"📝 Logger: {'✅' if LOGGER_AVAILABLE else '⚠️ заглушка'}")
|
592 |
-
print("🔍 Логи зберігаються у: mai_dx_logs/")
|
593 |
-
|
594 |
-
demo = create_enhanced_gradio_interface()
|
595 |
-
|
596 |
-
demo.launch(
|
597 |
-
server_name="0.0.0.0",
|
598 |
-
server_port=7860,
|
599 |
-
share=True,
|
600 |
-
debug=False
|
601 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mai_dx_interface.py
DELETED
@@ -1,855 +0,0 @@
|
|
1 |
-
#!/usr/bin/env python3
|
2 |
-
"""
|
3 |
-
MAI-DX Gradio Research Interface
|
4 |
-
Зручний веб-інтерфейс для дослідження медичної діагностики з ШІ
|
5 |
-
"""
|
6 |
-
import os
|
7 |
-
import sys
|
8 |
-
import json
|
9 |
-
import time
|
10 |
-
import pandas as pd
|
11 |
-
import gradio as gr
|
12 |
-
from datetime import datetime
|
13 |
-
from typing import Dict, List, Tuple, Optional
|
14 |
-
from dataclasses import dataclass, asdict
|
15 |
-
import warnings
|
16 |
-
|
17 |
-
# Налаштування для запобігання помилок
|
18 |
-
os.environ["SWARMS_VERBOSITY"] = "ERROR"
|
19 |
-
os.environ["RICH_TRACEBACK"] = "0"
|
20 |
-
os.environ["SWARMS_SHOW_PANEL"] = "false"
|
21 |
-
os.environ["SWARMS_AUTO_PRINT"] = "false"
|
22 |
-
warnings.filterwarnings("ignore")
|
23 |
-
|
24 |
-
# Завантаження змінних середовища
|
25 |
-
from dotenv import load_dotenv
|
26 |
-
load_dotenv()
|
27 |
-
|
28 |
-
# Патч Rich formatter
|
29 |
-
def patch_rich_formatter():
|
30 |
-
try:
|
31 |
-
import swarms.utils.formatter
|
32 |
-
def dummy_print_panel(*args, **kwargs):
|
33 |
-
pass
|
34 |
-
if hasattr(swarms.utils.formatter, 'Formatter'):
|
35 |
-
swarms.utils.formatter.Formatter._print_panel = dummy_print_panel
|
36 |
-
return True
|
37 |
-
except:
|
38 |
-
return False
|
39 |
-
|
40 |
-
patch_rich_formatter()
|
41 |
-
|
42 |
-
# Імпорт MAI-DX
|
43 |
-
try:
|
44 |
-
from mai_dx import MaiDxOrchestrator
|
45 |
-
MAI_DX_AVAILABLE = True
|
46 |
-
except ImportError as e:
|
47 |
-
MAI_DX_AVAILABLE = False
|
48 |
-
IMPORT_ERROR = str(e)
|
49 |
-
|
50 |
-
@dataclass
|
51 |
-
class DiagnosisSession:
|
52 |
-
"""Структура для зберігання сесії діагностики"""
|
53 |
-
timestamp: str
|
54 |
-
case_name: str
|
55 |
-
patient_info: str
|
56 |
-
mode: str
|
57 |
-
budget: int
|
58 |
-
diagnosis: str
|
59 |
-
confidence: float
|
60 |
-
cost: float
|
61 |
-
iterations: int
|
62 |
-
duration: float
|
63 |
-
status: str
|
64 |
-
reasoning: str = ""
|
65 |
-
|
66 |
-
class MAIDXGradioInterface:
|
67 |
-
"""Gradio інтерфейс для MAI-DX"""
|
68 |
-
|
69 |
-
def __init__(self):
|
70 |
-
self.sessions_history = []
|
71 |
-
self.current_session = None
|
72 |
-
|
73 |
-
# Перевірка доступності MAI-DX
|
74 |
-
if not MAI_DX_AVAILABLE:
|
75 |
-
print(f"❌ MAI-DX не доступний: {IMPORT_ERROR}")
|
76 |
-
print("📋 Інструкції установки:")
|
77 |
-
print(" pip install mai-dx python-dotenv")
|
78 |
-
print(" Створіть .env файл з API ключами")
|
79 |
-
|
80 |
-
# Предвизначені тестові кейси
|
81 |
-
self.sample_cases = {
|
82 |
-
"Кардіологічний (Інфаркт)": """
|
83 |
-
Пацієнт: 58-річний чоловік, менеджер
|
84 |
-
Скарги: Гострий біль у грудях протягом 3 годин
|
85 |
-
|
86 |
-
Анамнез:
|
87 |
-
- Біль почався під час підйому по сходах
|
88 |
-
- Сильний, здавлюючий, іррадіює в ліву руку та щелепу
|
89 |
-
- Супроводжується потовиділенням, нудотою, задишкою
|
90 |
-
- Не зменшується у спокої
|
91 |
-
|
92 |
-
Фактори ризику:
|
93 |
-
- Цукровий діабет 2 типу
|
94 |
-
- Артеріальна гіпертензія
|
95 |
-
- Куріння 2 пачки на день протягом 30 років
|
96 |
-
- Сімейний анамнез: батько помер від ІМ у 52 роки
|
97 |
-
|
98 |
-
Огляд:
|
99 |
-
- Дискомфорт, потовиділення
|
100 |
-
- АТ 180/110, ЧСС 105, ЧД 24
|
101 |
-
- Серце: S4 галоп, шумів немає
|
102 |
-
- Легені: ясні з обох сторін
|
103 |
-
|
104 |
-
ЕКГ: ST-підйоми у відведеннях II, III, aVF
|
105 |
-
Тропонін I: 8.5 нг/мл (норма <0.04)
|
106 |
-
""",
|
107 |
-
|
108 |
-
"Неврологічний (Інсульт)": """
|
109 |
-
Пацієнтка: 67-річна жінка з раптовими неврологічними симптомами
|
110 |
-
|
111 |
-
Презентація:
|
112 |
-
- Раптова слабкість правої сторони 2 години тому
|
113 |
-
- Утруднення мови (невиразна мова)
|
114 |
-
- Опущення правої сторони обличчя
|
115 |
-
- Без втрати свідомості
|
116 |
-
|
117 |
-
Анамнез:
|
118 |
-
- Фібриляція передсердь (не приймає антикоагулянти)
|
119 |
-
- Артеріальна гіпертензія
|
120 |
-
- Попередня ТІА 6 місяців тому
|
121 |
-
|
122 |
-
Огляд:
|
123 |
-
- У свідомості, але плутається
|
124 |
-
- Правостороннє опущення обличчя
|
125 |
-
- Дрейф правої руки
|
126 |
-
- Дизартрія
|
127 |
-
- NIHSS балів: 8
|
128 |
-
|
129 |
-
КТ голови: Гострого крововиливу немає
|
130 |
-
Час від початку: 2 години 15 хвилин
|
131 |
-
""",
|
132 |
-
|
133 |
-
"Педіатричний (Отит)": """
|
134 |
-
Пацієнт: 3-річний хлопчик, привели батьки через лихоманку
|
135 |
-
|
136 |
-
Анамнез:
|
137 |
-
- Лихоманка до 39.5°C протягом 2 днів
|
138 |
-
- Зменшення апетиту та активності
|
139 |
-
- Тягне за праве вухо
|
140 |
-
- Плаче більше звичайного, особли��о лежачи
|
141 |
-
- Без кашлю, нежиті або блювання
|
142 |
-
|
143 |
-
Анамнез життя:
|
144 |
-
- Доношена вагітність, нормальний розвиток
|
145 |
-
- Щеплення за календарем
|
146 |
-
- Алергії невідомі
|
147 |
-
|
148 |
-
Огляд:
|
149 |
-
- Температура: 39.2°C, ЧСС: 130, ЧД: 28
|
150 |
-
- Загалом дратівливий, але заспокоюється
|
151 |
-
- ЛОР: права барабанна перетинка еритематозна і випнута
|
152 |
-
- Шия: м'яка, лімфаденопатії немає
|
153 |
-
- Груди: ясні з обох сторін
|
154 |
-
- Живіт: м'який, безболісний
|
155 |
-
""",
|
156 |
-
|
157 |
-
"Ендокринологічний (Гіпертиреоз)": """
|
158 |
-
Пацієнтка: 45-річна жінка з прогресуючою втомою
|
159 |
-
|
160 |
-
Скарги: "Постійно втомлююся і худну, хоча їм"
|
161 |
-
|
162 |
-
Анамнез захворювання:
|
163 |
-
- 6-місячна історія прогресуючої втоми
|
164 |
-
- Ненавмисне схуднення на 7 кг
|
165 |
-
- Непереносимість спеки та надмірне потовиділення
|
166 |
-
- Серцебиття та тривожність
|
167 |
-
- Порушення сну
|
168 |
-
- Тремор рук
|
169 |
-
|
170 |
-
Огляд систем:
|
171 |
-
- Часті випорожнення (3-4 рази на день)
|
172 |
-
- Випадіння волосся
|
173 |
-
- Порушення менструального циклу
|
174 |
-
|
175 |
-
Огляд:
|
176 |
-
- АТ 150/85, ЧСС 110, ЧД 16, Т 37.1°C
|
177 |
-
- Худорлява, тривожна жінка
|
178 |
-
- Очі: легкий екзофтальм, симптом запізнення повік
|
179 |
-
- Щитовидна залоза: дифузно збільшена
|
180 |
-
- Серце: тахікардія, шумів немає
|
181 |
-
- Тремор витягнутих рук
|
182 |
-
- Шкіра: тепла та волога
|
183 |
-
"""
|
184 |
-
}
|
185 |
-
|
186 |
-
def check_api_keys(self) -> Tuple[str, str]:
|
187 |
-
"""Перевірка наявності API ключів"""
|
188 |
-
required_keys = ["OPENAI_API_KEY", "GEMINI_API_KEY", "ANTHROPIC_API_KEY"]
|
189 |
-
missing_keys = []
|
190 |
-
available_keys = []
|
191 |
-
|
192 |
-
for key in required_keys:
|
193 |
-
if os.getenv(key):
|
194 |
-
available_keys.append(key)
|
195 |
-
else:
|
196 |
-
missing_keys.append(key)
|
197 |
-
|
198 |
-
if available_keys:
|
199 |
-
status = f"✅ Доступні ключі: {', '.join(available_keys)}"
|
200 |
-
color = "green"
|
201 |
-
else:
|
202 |
-
status = f"❌ Відсутні всі API ключі: {', '.join(missing_keys)}"
|
203 |
-
color = "red"
|
204 |
-
|
205 |
-
if missing_keys:
|
206 |
-
status += f"\n⚠️ Відсутні: {', '.join(missing_keys)}"
|
207 |
-
|
208 |
-
return status, color
|
209 |
-
|
210 |
-
def diagnose_case(
|
211 |
-
self,
|
212 |
-
case_name: str,
|
213 |
-
patient_info: str,
|
214 |
-
mode: str,
|
215 |
-
budget: int,
|
216 |
-
max_iterations: int,
|
217 |
-
model_name: str,
|
218 |
-
expected_diagnosis: str = "",
|
219 |
-
progress=gr.Progress()
|
220 |
-
) -> Tuple[str, str, str, pd.DataFrame]:
|
221 |
-
"""Основна функція діагностики"""
|
222 |
-
|
223 |
-
if not MAI_DX_AVAILABLE:
|
224 |
-
return (
|
225 |
-
f"❌ MAI-DX не доступний: {IMPORT_ERROR}",
|
226 |
-
"",
|
227 |
-
"",
|
228 |
-
pd.DataFrame()
|
229 |
-
)
|
230 |
-
|
231 |
-
if not patient_info.strip():
|
232 |
-
return (
|
233 |
-
"❌ Будь ласка, введіть інформацію про пацієнта",
|
234 |
-
"",
|
235 |
-
"",
|
236 |
-
pd.DataFrame()
|
237 |
-
)
|
238 |
-
|
239 |
-
# Початок діагностики
|
240 |
-
progress(0.1, desc="Ініціалізація MAI-DX...")
|
241 |
-
|
242 |
-
start_time = time.time()
|
243 |
-
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
244 |
-
|
245 |
-
try:
|
246 |
-
# Створення оркестратора
|
247 |
-
progress(0.2, desc="Створення діагностичного оркестратора...")
|
248 |
-
|
249 |
-
orchestrator = MaiDxOrchestrator(
|
250 |
-
model_name=model_name,
|
251 |
-
max_iterations=max_iterations,
|
252 |
-
initial_budget=budget,
|
253 |
-
mode=mode if mode != "ensemble" else "budgeted" # Ensemble використовує базовий режим
|
254 |
-
)
|
255 |
-
|
256 |
-
progress(0.3, desc="Початок аналізу медичного випадку...")
|
257 |
-
|
258 |
-
# Запуск діагностики
|
259 |
-
if mode == "ensemble":
|
260 |
-
# Для ensemble режиму використовуємо run_ensemble якщо доступний
|
261 |
-
progress(0.4, desc="Запуск ensemble діагностики (кілька прогонів)...")
|
262 |
-
try:
|
263 |
-
result = orchestrator.run_ensemble(
|
264 |
-
initial_case_info=patient_info,
|
265 |
-
full_case_details=patient_info,
|
266 |
-
ground_truth_diagnosis=expected_diagnosis or "Unknown",
|
267 |
-
num_runs=2 # Зменшено для швидкості в інтерфейсі
|
268 |
-
)
|
269 |
-
except AttributeError:
|
270 |
-
# Якщо run_ensemble недоступний, використовуємо звичайний run
|
271 |
-
progress(0.4, desc="Ensemble недоступний, використовуємо стандартний режим...")
|
272 |
-
result = orchestrator.run(
|
273 |
-
initial_case_info=patient_info,
|
274 |
-
full_case_details=patient_info,
|
275 |
-
ground_truth_diagnosis=expected_diagnosis or "Unknown"
|
276 |
-
)
|
277 |
-
else:
|
278 |
-
result = orchestrator.run(
|
279 |
-
initial_case_info=patient_info,
|
280 |
-
full_case_details=patient_info,
|
281 |
-
ground_truth_diagnosis=expected_diagnosis or "Unknown"
|
282 |
-
)
|
283 |
-
|
284 |
-
progress(0.8, desc="Обробка результатів...")
|
285 |
-
|
286 |
-
duration = time.time() - start_time
|
287 |
-
|
288 |
-
# Створення сесії
|
289 |
-
session = DiagnosisSession(
|
290 |
-
timestamp=timestamp,
|
291 |
-
case_name=case_name or f"Case_{len(self.sessions_history) + 1}",
|
292 |
-
patient_info=patient_info[:200] + "..." if len(patient_info) > 200 else patient_info,
|
293 |
-
mode=mode,
|
294 |
-
budget=budget,
|
295 |
-
diagnosis=result.final_diagnosis,
|
296 |
-
confidence=result.accuracy_score,
|
297 |
-
cost=result.total_cost,
|
298 |
-
iterations=result.iterations,
|
299 |
-
duration=duration,
|
300 |
-
status="Успішно" if result.accuracy_score >= 2.0 else "Потребує перегляду",
|
301 |
-
reasoning=getattr(result, 'accuracy_reasoning', 'Недоступно')[:300] + "..."
|
302 |
-
)
|
303 |
-
|
304 |
-
self.sessions_history.append(session)
|
305 |
-
self.current_session = session
|
306 |
-
|
307 |
-
progress(1.0, desc="Готово!")
|
308 |
-
|
309 |
-
# Форматування результатів
|
310 |
-
main_result = self._format_main_result(session, result)
|
311 |
-
detailed_analysis = self._format_detailed_analysis(session, result)
|
312 |
-
recommendations = self._generate_recommendations(result)
|
313 |
-
history_df = self._get_history_dataframe()
|
314 |
-
|
315 |
-
return main_result, detailed_analysis, recommendations, history_df
|
316 |
-
|
317 |
-
except Exception as e:
|
318 |
-
error_msg = f"❌ Помилка діагностики: {str(e)}"
|
319 |
-
return error_msg, "", "", pd.DataFrame()
|
320 |
-
|
321 |
-
def _format_main_result(self, session: DiagnosisSession, result) -> str:
|
322 |
-
"""Форматування основного результату"""
|
323 |
-
|
324 |
-
# Визначення рівня довіри
|
325 |
-
if session.confidence >= 4.0:
|
326 |
-
confidence_level = "🎉 ВИСОКИЙ"
|
327 |
-
confidence_color = "green"
|
328 |
-
elif session.confidence >= 3.0:
|
329 |
-
confidence_level = "👍 СЕРЕДНІЙ"
|
330 |
-
confidence_color = "orange"
|
331 |
-
elif session.confidence >= 2.0:
|
332 |
-
confidence_level = "⚠️ НИЗЬКИЙ"
|
333 |
-
confidence_color = "orange"
|
334 |
-
else:
|
335 |
-
confidence_level = "❌ ДУЖЕ НИЗЬКИЙ"
|
336 |
-
confidence_color = "red"
|
337 |
-
|
338 |
-
main_result = f"""
|
339 |
-
# 🏥 Результати MAI-DX Діагностики
|
340 |
-
|
341 |
-
## 📋 Основна інформація
|
342 |
-
- **Випадок**: {session.case_name}
|
343 |
-
- **Час**: {session.timestamp}
|
344 |
-
- **Режим**: {session.mode}
|
345 |
-
|
346 |
-
## 🎯 Діагноз
|
347 |
-
**{session.diagnosis}**
|
348 |
-
|
349 |
-
## 📊 Оцінка якості
|
350 |
-
- **Рівень довіри**: {confidence_level}
|
351 |
-
- **Бал точності**: {session.confidence}/5.0
|
352 |
-
- **Статус**: {session.status}
|
353 |
-
|
354 |
-
## 💰 Ресурси
|
355 |
-
- **Загальна вартість**: ${session.cost:,.2f}
|
356 |
-
- **Бюджет**: ${session.budget:,}
|
357 |
-
- **Ітерації**: {session.iterations}
|
358 |
-
- **Тривалість**: {session.duration:.1f} сек
|
359 |
-
"""
|
360 |
-
|
361 |
-
return main_result
|
362 |
-
|
363 |
-
def _format_detailed_analysis(self, session: DiagnosisSession, result) -> str:
|
364 |
-
"""Детальний аналіз результатів"""
|
365 |
-
|
366 |
-
analysis = f"""
|
367 |
-
# 🔬 Детальний Аналіз
|
368 |
-
|
369 |
-
## 💭 Медичне обґрунтування
|
370 |
-
{session.reasoning}
|
371 |
-
|
372 |
-
## 📈 Показники ефективності
|
373 |
-
- **Швидкість діагностики**: {session.duration:.1f} секунд
|
374 |
-
- **Економічна ефективність**: {((session.budget - session.cost) / session.budget * 100):.1f}% бюджету залишилось
|
375 |
-
- **Ітеративна ефективність**: {session.iterations} циклів аналізу
|
376 |
-
|
377 |
-
## 🔄 Про��ес діагностики
|
378 |
-
1. **Початковий аналіз** пацієнтських даних
|
379 |
-
2. **Диференційна діагностика** з {session.iterations} ітераціями
|
380 |
-
3. **Консенсус панелі** 8 ШІ-лікарів
|
381 |
-
4. **Фінальна оцінка** та верифікація
|
382 |
-
|
383 |
-
## 📊 Порівняння з попередніми випадками
|
384 |
-
"""
|
385 |
-
|
386 |
-
if len(self.sessions_history) > 1:
|
387 |
-
avg_confidence = sum(s.confidence for s in self.sessions_history[:-1]) / len(self.sessions_history[:-1])
|
388 |
-
avg_cost = sum(s.cost for s in self.sessions_history[:-1]) / len(self.sessions_history[:-1])
|
389 |
-
|
390 |
-
analysis += f"""
|
391 |
-
- **Середня точність** попередніх випадків: {avg_confidence:.1f}/5.0
|
392 |
-
- **Середня вартість** попередніх випадків: ${avg_cost:,.2f}
|
393 |
-
- **Поточний випадок**: {"Вище середнього" if session.confidence > avg_confidence else "Нижче середнього"} за точністю
|
394 |
-
"""
|
395 |
-
else:
|
396 |
-
analysis += "\n- Це ваш перший діагностичний випадок"
|
397 |
-
|
398 |
-
return analysis
|
399 |
-
|
400 |
-
def _generate_recommendations(self, result) -> str:
|
401 |
-
"""Генерація рекомендацій"""
|
402 |
-
|
403 |
-
recommendations = """
|
404 |
-
# 💡 Рекомендації
|
405 |
-
|
406 |
-
## 🏥 Клінічні рекомендації
|
407 |
-
"""
|
408 |
-
|
409 |
-
diagnosis = result.final_diagnosis.lower()
|
410 |
-
|
411 |
-
# Базові рекомендації залежно від діагнозу
|
412 |
-
if any(word in diagnosis for word in ["emergency", "acute", "urgent", "infarction", "stroke"]):
|
413 |
-
recommendations += """
|
414 |
-
- 🚨 **НЕВІДКЛАДНА МЕДИЧНА ДОПОМОГА**
|
415 |
-
- Негайна госпіталізація
|
416 |
-
- Моніторинг життєво важливих функцій
|
417 |
-
- Консультація спеціаліста протягом години
|
418 |
-
"""
|
419 |
-
elif any(word in diagnosis for word in ["infection", "bacterial", "viral"]):
|
420 |
-
recommendations += """
|
421 |
-
- 💊 Розгляньте антибактеріальну терапію
|
422 |
-
- 🌡️ Моніторинг температури тіла
|
423 |
-
- 🔬 Додаткові лабораторні дослідження
|
424 |
-
- 📅 Контрольний огляд через 48-72 години
|
425 |
-
"""
|
426 |
-
elif any(word in diagnosis for word in ["chronic", "management", "control"]):
|
427 |
-
recommendations += """
|
428 |
-
- 📋 Розробка довготривалого плану лікування
|
429 |
-
- 🗓️ Регулярні контрольні огляди
|
430 |
-
- 👨⚕️ Консультація профільних спеціалістів
|
431 |
-
- 📚 Навчання пацієнта самоконтролю
|
432 |
-
"""
|
433 |
-
else:
|
434 |
-
recommendations += """
|
435 |
-
- 🔍 Подальше спостереження та моніторинг
|
436 |
-
- 📋 Розгляньте додаткові діагностичні тести
|
437 |
-
- 👨⚕️ Консультація з лікарем для верифікації
|
438 |
-
- 📝 Документування симптомів та їх динаміки
|
439 |
-
"""
|
440 |
-
|
441 |
-
recommendations += f"""
|
442 |
-
|
443 |
-
## 🔬 Дослідницькі рекомендації
|
444 |
-
- **Валідація ШІ-діагнозу**: Порівняйте з експертною оцінкою
|
445 |
-
- **Документування**: Збережіть результат для дослідження
|
446 |
-
- **Аналіз точності**: Оцініть відповідність реальному діагнозу
|
447 |
-
- **Поліпшення**: Використайте результат для налаштування параметрів
|
448 |
-
|
449 |
-
## ⚠️ Важливі застереження
|
450 |
-
- Цей діагноз згенеровано ШІ для дослідницьких цілей
|
451 |
-
- НЕ замінює професійну медичну консультацію
|
452 |
-
- Завжди консультуйтеся з кваліфікованими медичними працівниками
|
453 |
-
- Використовуйте лише як допоміжний інструмент для навчання
|
454 |
-
|
455 |
-
## 📈 Поліпшення точності
|
456 |
-
- Додайте більше клінічних деталей до опису випадку
|
457 |
-
- Використайте режим "no_budget" для складних випадків
|
458 |
-
- Розгляньте ensemble діагностику для критичних випадків
|
459 |
-
"""
|
460 |
-
|
461 |
-
return recommendations
|
462 |
-
|
463 |
-
def _get_history_dataframe(self) -> pd.DataFrame:
|
464 |
-
"""Отримання історії у вигляді DataFrame"""
|
465 |
-
|
466 |
-
if not self.sessions_history:
|
467 |
-
return pd.DataFrame()
|
468 |
-
|
469 |
-
data = []
|
470 |
-
for session in self.sessions_history:
|
471 |
-
data.append({
|
472 |
-
"Час": session.timestamp,
|
473 |
-
"Випадок": session.case_name,
|
474 |
-
"Режим": session.mode,
|
475 |
-
"Діагноз": session.diagnosis[:50] + "..." if len(session.diagnosis) > 50 else session.diagnosis,
|
476 |
-
"Точність": f"{session.confidence:.1f}/5.0",
|
477 |
-
"Вартість": f"${session.cost:.2f}",
|
478 |
-
"Ітерації": session.iterations,
|
479 |
-
"Тривалість": f"{session.duration:.1f}с",
|
480 |
-
"Статус": session.status
|
481 |
-
})
|
482 |
-
|
483 |
-
return pd.DataFrame(data)
|
484 |
-
|
485 |
-
def export_results(self) -> str:
|
486 |
-
"""Експорт результатів дослідження"""
|
487 |
-
|
488 |
-
if not self.sessions_history:
|
489 |
-
return "Немає даних для експорту"
|
490 |
-
|
491 |
-
# Підготовка даних для експорту
|
492 |
-
export_data = {
|
493 |
-
"metadata": {
|
494 |
-
"export_date": datetime.now().isoformat(),
|
495 |
-
"total_sessions": len(self.sessions_history),
|
496 |
-
"software_version": "MAI-DX Gradio Interface v1.0"
|
497 |
-
},
|
498 |
-
"sessions": [asdict(session) for session in self.sessions_history],
|
499 |
-
"statistics": self._calculate_statistics()
|
500 |
-
}
|
501 |
-
|
502 |
-
# Збереження у файл
|
503 |
-
filename = f"mai_dx_research_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
504 |
-
|
505 |
-
try:
|
506 |
-
with open(filename, 'w', encoding='utf-8') as f:
|
507 |
-
json.dump(export_data, f, indent=2, ensure_ascii=False)
|
508 |
-
|
509 |
-
return f"✅ Дані експортовано у файл: {filename}"
|
510 |
-
except Exception as e:
|
511 |
-
return f"❌ Помилка експорту: {e}"
|
512 |
-
|
513 |
-
def _calculate_statistics(self) -> Dict:
|
514 |
-
"""Розрахунок статистики досліджень"""
|
515 |
-
|
516 |
-
if not self.sessions_history:
|
517 |
-
return {}
|
518 |
-
|
519 |
-
sessions = self.sessions_history
|
520 |
-
|
521 |
-
stats = {
|
522 |
-
"total_cases": len(sessions),
|
523 |
-
"average_confidence": sum(s.confidence for s in sessions) / len(sessions),
|
524 |
-
"average_cost": sum(s.cost for s in sessions) / len(sessions),
|
525 |
-
"average_duration": sum(s.duration for s in sessions) / len(sessions),
|
526 |
-
"mode_distribution": {},
|
527 |
-
"confidence_distribution": {
|
528 |
-
"high": len([s for s in sessions if s.confidence >= 4.0]),
|
529 |
-
"medium": len([s for s in sessions if 2.0 <= s.confidence < 4.0]),
|
530 |
-
"low": len([s for s in sessions if s.confidence < 2.0])
|
531 |
-
},
|
532 |
-
"success_rate": len([s for s in sessions if s.confidence >= 2.0]) / len(sessions)
|
533 |
-
}
|
534 |
-
|
535 |
-
# Розподіл по режимах
|
536 |
-
for session in sessions:
|
537 |
-
mode = session.mode
|
538 |
-
stats["mode_distribution"][mode] = stats["mode_distribution"].get(mode, 0) + 1
|
539 |
-
|
540 |
-
return stats
|
541 |
-
|
542 |
-
def create_gradio_interface():
|
543 |
-
"""Створення Gradio інтерфейсу"""
|
544 |
-
|
545 |
-
interface = MAIDXGradioInterface()
|
546 |
-
|
547 |
-
with gr.Blocks(
|
548 |
-
title="MAI-DX Research Interface",
|
549 |
-
theme=gr.themes.Soft(),
|
550 |
-
css="""
|
551 |
-
.main-header { text-align: center; color: #2c3e50; }
|
552 |
-
.status-good { color: green; font-weight: bold; }
|
553 |
-
.status-bad { color: red; font-weight: bold; }
|
554 |
-
.case-input { font-family: monospace; }
|
555 |
-
"""
|
556 |
-
) as demo:
|
557 |
-
|
558 |
-
# Заголовок
|
559 |
-
gr.Markdown("""
|
560 |
-
# 🏥 MAI-DX Research Interface
|
561 |
-
## Дослідницький інтерфейс для медичної діагностики з ШІ
|
562 |
-
|
563 |
-
Цей інтерфейс дозволяє проводити дослідження діагностичних можливостей MAI-DX системи з 8 ШІ-агентами лікарів.
|
564 |
-
""", elem_classes=["main-header"])
|
565 |
-
|
566 |
-
# Статус системи
|
567 |
-
with gr.Row():
|
568 |
-
status_display = gr.Markdown()
|
569 |
-
|
570 |
-
# Оновлення статусу при завантаженні
|
571 |
-
def update_status():
|
572 |
-
status, color = interface.check_api_keys()
|
573 |
-
return f"**Статус системи**: {status}"
|
574 |
-
|
575 |
-
demo.load(update_status, outputs=[status_display])
|
576 |
-
|
577 |
-
# Основний інтерфейс
|
578 |
-
with gr.Tabs():
|
579 |
-
|
580 |
-
# Вкладка діагностики
|
581 |
-
with gr.Tab("🩺 Діагностика"):
|
582 |
-
with gr.Row():
|
583 |
-
with gr.Column(scale=1):
|
584 |
-
gr.Markdown("### 📝 Введення медичного випадку")
|
585 |
-
|
586 |
-
case_name = gr.Textbox(
|
587 |
-
label="Назва випадку",
|
588 |
-
placeholder="Наприклад: Кардіологічний випадок №1",
|
589 |
-
value=""
|
590 |
-
)
|
591 |
-
|
592 |
-
# Dropdown з прикладами
|
593 |
-
sample_selector = gr.Dropdown(
|
594 |
-
choices=list(interface.sample_cases.keys()),
|
595 |
-
label="Приклади випадків",
|
596 |
-
value=None
|
597 |
-
)
|
598 |
-
|
599 |
-
patient_info = gr.Textbox(
|
600 |
-
label="Інформація про пацієнта",
|
601 |
-
lines=15,
|
602 |
-
placeholder="Введіть детальний опис медичного випадку...",
|
603 |
-
elem_classes=["case-input"]
|
604 |
-
)
|
605 |
-
|
606 |
-
# Завантаження прикладу
|
607 |
-
def load_sample(sample_name):
|
608 |
-
if sample_name:
|
609 |
-
return interface.sample_cases[sample_name], sample_name
|
610 |
-
return "", ""
|
611 |
-
|
612 |
-
sample_selector.change(
|
613 |
-
load_sample,
|
614 |
-
inputs=[sample_selector],
|
615 |
-
outputs=[patient_info, case_name]
|
616 |
-
)
|
617 |
-
|
618 |
-
expected_diagnosis = gr.Textbox(
|
619 |
-
label="Очікуваний діагноз (опціонально)",
|
620 |
-
placeholder="Для оцінки точності...",
|
621 |
-
value=""
|
622 |
-
)
|
623 |
-
|
624 |
-
with gr.Column(scale=1):
|
625 |
-
gr.Markdown("### ⚙️ Налаштування діагностики")
|
626 |
-
|
627 |
-
mode = gr.Radio(
|
628 |
-
choices=[
|
629 |
-
("instant", "Миттєвий (найшвидший)"),
|
630 |
-
("question_only", "Тільки питання (швидко)"),
|
631 |
-
("budgeted", "З бюджетом (збалансовано)"),
|
632 |
-
("no_budget", "Без обмежень (повний аналіз)"),
|
633 |
-
("ensemble", "Консенсус (найточніший)")
|
634 |
-
],
|
635 |
-
label="Режим діагностики",
|
636 |
-
value="budgeted"
|
637 |
-
)
|
638 |
-
|
639 |
-
budget = gr.Slider(
|
640 |
-
minimum=500,
|
641 |
-
maximum=10000,
|
642 |
-
step=500,
|
643 |
-
value=3000,
|
644 |
-
label="Бюджет ($)"
|
645 |
-
)
|
646 |
-
|
647 |
-
max_iterations = gr.Slider(
|
648 |
-
minimum=1,
|
649 |
-
maximum=10,
|
650 |
-
step=1,
|
651 |
-
value=5,
|
652 |
-
label="Максимум ітерацій"
|
653 |
-
)
|
654 |
-
|
655 |
-
model_name = gr.Dropdown(
|
656 |
-
choices=[
|
657 |
-
"gemini/gemini-2.5-flash",
|
658 |
-
"gpt-4",
|
659 |
-
"gpt-3.5-turbo",
|
660 |
-
"claude-3-5-sonnet",
|
661 |
-
"grok-beta",
|
662 |
-
"deepseek-chat",
|
663 |
-
"llama-3.1-405b"
|
664 |
-
],
|
665 |
-
label="LLM Модель",
|
666 |
-
value="gemini/gemini-2.5-flash"
|
667 |
-
)
|
668 |
-
|
669 |
-
diagnose_btn = gr.Button(
|
670 |
-
"🚀 Запустити діагностику",
|
671 |
-
variant="primary",
|
672 |
-
size="lg"
|
673 |
-
)
|
674 |
-
|
675 |
-
# Результати
|
676 |
-
gr.Markdown("### 📊 Результати діагностики")
|
677 |
-
|
678 |
-
with gr.Row():
|
679 |
-
with gr.Column():
|
680 |
-
main_result = gr.Markdown(label="Основний результат")
|
681 |
-
|
682 |
-
with gr.Row():
|
683 |
-
with gr.Column():
|
684 |
-
detailed_analysis = gr.Markdown(label="Детальний аналіз")
|
685 |
-
|
686 |
-
with gr.Column():
|
687 |
-
recommendations = gr.Markdown(label="Рекомендації")
|
688 |
-
|
689 |
-
# Запуск діагностики
|
690 |
-
diagnose_btn.click(
|
691 |
-
interface.diagnose_case,
|
692 |
-
inputs=[
|
693 |
-
case_name, patient_info, mode, budget,
|
694 |
-
max_iterations, model_name, expected_diagnosis
|
695 |
-
],
|
696 |
-
outputs=[main_result, detailed_analysis, recommendations, gr.State()]
|
697 |
-
)
|
698 |
-
|
699 |
-
# Вкладка історії
|
700 |
-
with gr.Tab("📈 Історія та Статистика"):
|
701 |
-
gr.Markdown("### 📋 Історія діагностичних сесій")
|
702 |
-
|
703 |
-
with gr.Row():
|
704 |
-
refresh_btn = gr.Button("🔄 Оновити", variant="secondary")
|
705 |
-
export_btn = gr.Button("💾 Експортувати результати", variant="primary")
|
706 |
-
|
707 |
-
history_table = gr.Dataframe(
|
708 |
-
label="Історія діагнозів",
|
709 |
-
interactive=False
|
710 |
-
)
|
711 |
-
|
712 |
-
export_status = gr.Markdown()
|
713 |
-
|
714 |
-
# Функції для оновлення
|
715 |
-
def refresh_history():
|
716 |
-
return interface._get_history_dataframe()
|
717 |
-
|
718 |
-
def export_data():
|
719 |
-
return interface.export_results()
|
720 |
-
|
721 |
-
refresh_btn.click(refresh_history, outputs=[history_table])
|
722 |
-
export_btn.click(export_data, outputs=[export_status])
|
723 |
-
|
724 |
-
# Статистика
|
725 |
-
with gr.Row():
|
726 |
-
with gr.Column():
|
727 |
-
gr.Markdown("### 📊 Статистика досліджень")
|
728 |
-
stats_display = gr.Markdown()
|
729 |
-
|
730 |
-
def update_stats():
|
731 |
-
if not interface.sessions_history:
|
732 |
-
return "Поки що немає даних для статистики"
|
733 |
-
|
734 |
-
stats = interface._calculate_statistics()
|
735 |
-
|
736 |
-
return f"""
|
737 |
-
**Загальна статистика:**
|
738 |
-
- Всього випадків: {stats['total_cases']}
|
739 |
-
- Середня точність: {stats['average_confidence']:.2f}/5.0
|
740 |
-
- Середня вартість: ${stats['average_cost']:.2f}
|
741 |
-
- Середня тривалість: {stats['average_duration']:.1f}с
|
742 |
-
- Рівень успішності: {stats['success_rate']:.1%}
|
743 |
-
|
744 |
-
**Розподіл по режимах:**
|
745 |
-
{chr(10).join([f"- {mode}: {count}" for mode, count in stats['mode_distribution'].items()])}
|
746 |
-
|
747 |
-
**Розподіл по точності:**
|
748 |
-
- Висока (≥4.0): {stats['confidence_distribution']['high']}
|
749 |
-
- Середня (2.0-3.9): {stats['confidence_distribution']['medium']}
|
750 |
-
- Низька (<2.0): {stats['confidence_distribution']['low']}
|
751 |
-
"""
|
752 |
-
|
753 |
-
refresh_stats_btn = gr.Button("🔄 Оновити статистику")
|
754 |
-
refresh_stats_btn.click(update_stats, outputs=[stats_display])
|
755 |
-
|
756 |
-
# Вкладка налаштувань
|
757 |
-
with gr.Tab("⚙️ Налаштування"):
|
758 |
-
gr.Markdown("### 🔧 Конфігурація системи")
|
759 |
-
|
760 |
-
with gr.Row():
|
761 |
-
with gr.Column():
|
762 |
-
gr.Markdown("""
|
763 |
-
#### 📋 Інструкції з установки
|
764 |
-
|
765 |
-
**Для pip (ваша конфігурація):**
|
766 |
-
```bash
|
767 |
-
# 1. Встановлення залежностей
|
768 |
-
pip install mai-dx python-dotenv gradio pandas
|
769 |
-
|
770 |
-
# 2. Створення .env файлу
|
771 |
-
echo "OPENAI_API_KEY=your-key-here" > .env
|
772 |
-
echo "GEMINI_API_KEY=your-key-here" >> .env
|
773 |
-
echo "ANTHROPIC_API_KEY=your-key-here" >> .env
|
774 |
-
|
775 |
-
# 3. Запуск інтерфейсу
|
776 |
-
python mai_dx_gradio_interface.py
|
777 |
-
```
|
778 |
-
|
779 |
-
#### 🔑 Необхідні API ключі:
|
780 |
-
- **OpenAI**: для GPT моделей
|
781 |
-
- **Google AI**: для Gemini моделей
|
782 |
-
- **Anthropic**: для Claude моделей
|
783 |
-
|
784 |
-
Отримайте ключі на відповідних платформах та додайте їх у .env файл.
|
785 |
-
""")
|
786 |
-
|
787 |
-
with gr.Column():
|
788 |
-
gr.Markdown("""
|
789 |
-
#### 🛠️ Troubleshooting
|
790 |
-
|
791 |
-
**Часті проблеми:**
|
792 |
-
|
793 |
-
1. **MAI-DX не імпортується**
|
794 |
-
- Встановіть: `pip install mai-dx`
|
795 |
-
- Перевірте віртуальне середовище
|
796 |
-
|
797 |
-
2. **API ключі не працюють**
|
798 |
-
- Перевірте .env файл
|
799 |
-
- Переконайтеся що файл у тій же папці
|
800 |
-
|
801 |
-
3. **Rich console помилки**
|
802 |
-
- Це нормально, можна ігнорувати
|
803 |
-
- Система працює попри помилки
|
804 |
-
|
805 |
-
4. **Повільна робота**
|
806 |
-
- Використовуйте ��видші моделі
|
807 |
-
- Зменшіть кількість ітерацій
|
808 |
-
- Оберіть режим "question_only"
|
809 |
-
|
810 |
-
#### 📞 Підтримка:
|
811 |
-
- GitHub: [MAI-DX Repository](https://github.com/The-Swarm-Corporation/Open-MAI-Dx-Orchestrator)
|
812 |
-
- Документація: включена у репозиторій
|
813 |
-
""")
|
814 |
-
|
815 |
-
# Тестування підключення
|
816 |
-
with gr.Row():
|
817 |
-
test_connection_btn = gr.Button("🔍 Тест підключення", variant="secondary")
|
818 |
-
connection_status = gr.Markdown()
|
819 |
-
|
820 |
-
def test_system():
|
821 |
-
if not MAI_DX_AVAILABLE:
|
822 |
-
return f"❌ MAI-DX недоступний: {IMPORT_ERROR}"
|
823 |
-
|
824 |
-
api_status, _ = interface.check_api_keys()
|
825 |
-
|
826 |
-
return f"""
|
827 |
-
**Результати тестування:**
|
828 |
-
|
829 |
-
📦 **MAI-DX статус**: ✅ Доступний
|
830 |
-
🔑 **API ключі**: {api_status}
|
831 |
-
🐍 **Python**: {sys.version.split()[0]}
|
832 |
-
📍 **Робоча директорія**: {os.getcwd()}
|
833 |
-
|
834 |
-
**Готовність до роботи**: {"✅ Готово" if MAI_DX_AVAILABLE else "❌ Потребує налаштування"}
|
835 |
-
"""
|
836 |
-
|
837 |
-
test_connection_btn.click(test_system, outputs=[connection_status])
|
838 |
-
|
839 |
-
return demo
|
840 |
-
|
841 |
-
if __name__ == "__main__":
|
842 |
-
# Створення та запуск інтерфейсу
|
843 |
-
demo = create_gradio_interface()
|
844 |
-
|
845 |
-
print("🚀 Запуск MAI-DX Research Interface...")
|
846 |
-
print("📱 Інтерфейс буде доступний у браузері")
|
847 |
-
print("🔧 Переконайтеся що .env файл з API ключами у поточній директорії")
|
848 |
-
|
849 |
-
demo.launch(
|
850 |
-
server_name="0.0.0.0", # Доступ з мережі
|
851 |
-
server_port=7860, # Порт
|
852 |
-
share=False, # Встановіть True для публічного доступу
|
853 |
-
debug=True, # Режим налагодження
|
854 |
-
show_error=True # Показувати помилки
|
855 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mai_dx_logs/case_20250710_173828_5d4912.json
DELETED
@@ -1,27 +0,0 @@
|
|
1 |
-
{
|
2 |
-
"case_id": "case_20250710_173828_5d4912",
|
3 |
-
"timestamp": "2025-07-10T17:38:28.722692",
|
4 |
-
"case_name": "🫀 Кардіологічний (Гострий MI)",
|
5 |
-
"patient_info": "Пацієнт: 58-річний чоловік, менеджер, гіпертонія в анамнезі\nСкарги: Гострий роздираючий біль у грудях 3 години, іррадіація в ліву руку\nОгляд: Блідий, пітливий, АТ 160/90, ЧСС 95\nЕКГ: ST-підйоми у відведеннях II, III, aVF (нижня стінка)\nТропонін I: 8.5 нг/мл (норма <0.04)\nАнамнез: Куріння 30 років, дислипідемія, сімейний анамнез ІХС",
|
6 |
-
"mode": "⚡ Миттєвий (найшвидший, базовий аналіз)",
|
7 |
-
"budget": 0,
|
8 |
-
"diagnosis": "ST-elevation Myocardial Infarction (STEMI)",
|
9 |
-
"confidence": 1.0,
|
10 |
-
"cost": 300,
|
11 |
-
"iterations": 1,
|
12 |
-
"duration": 97.490005,
|
13 |
-
"status": "⚠️ Потребує перегляду",
|
14 |
-
"reasoning": "N/A",
|
15 |
-
"messages": [
|
16 |
-
{
|
17 |
-
"timestamp": "2025-07-10T17:40:06.212655",
|
18 |
-
"agent": "Judge",
|
19 |
-
"action": "final_evaluation",
|
20 |
-
"content": "ST-elevation Myocardial Infarction (STEMI)",
|
21 |
-
"reasoning": "[detailed reasoning for the score]",
|
22 |
-
"confidence": 0.2,
|
23 |
-
"cost": 300,
|
24 |
-
"iteration": 8
|
25 |
-
}
|
26 |
-
]
|
27 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mai_dx_logs/case_20250710_174912_2b0a2d.json
DELETED
@@ -1,27 +0,0 @@
|
|
1 |
-
{
|
2 |
-
"case_id": "case_20250710_174912_2b0a2d",
|
3 |
-
"timestamp": "2025-07-10T17:49:12.547474",
|
4 |
-
"case_name": "🫀 Кардіологічний (Гострий MI)",
|
5 |
-
"patient_info": "Пацієнт: 58-річний чоловік, менеджер, гіпертонія в анамнезі\nСкарги: Гострий роздираючий біль у грудях 3 години, іррадіація в ліву руку\nОгляд: Блідий, пітливий, АТ 160/90, ЧСС 95\nЕКГ: ST-підйоми у відведеннях II, III, aVF (нижня стінка)\nТропонін I: 8.5 нг/мл (норма <0.04)\nАнамнез: Куріння 30 років, дислипідемія, сімейний анамнез ІХС",
|
6 |
-
"mode": "budgeted",
|
7 |
-
"budget": 0,
|
8 |
-
"diagnosis": "ST-Elevation Myocardial Infarction (STEMI)",
|
9 |
-
"confidence": 5.0,
|
10 |
-
"cost": 300,
|
11 |
-
"iterations": 1,
|
12 |
-
"duration": 95.609545,
|
13 |
-
"status": "✅ Успішно",
|
14 |
-
"reasoning": "N/A",
|
15 |
-
"messages": [
|
16 |
-
{
|
17 |
-
"timestamp": "2025-07-10T17:50:48.156978",
|
18 |
-
"agent": "Judge",
|
19 |
-
"action": "final_evaluation",
|
20 |
-
"content": "ST-Elevation Myocardial Infarction (STEMI)",
|
21 |
-
"reasoning": "[detailed reasoning for the score]",
|
22 |
-
"confidence": 1.0,
|
23 |
-
"cost": 300,
|
24 |
-
"iteration": 8
|
25 |
-
}
|
26 |
-
]
|
27 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mai_dx_wrapper.py
ADDED
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""
|
3 |
+
Обгортка для MAI-DX що виправляє проблеми з Rich форматуванням
|
4 |
+
"""
|
5 |
+
import os
|
6 |
+
import sys
|
7 |
+
import warnings
|
8 |
+
|
9 |
+
# Налаштування середовища
|
10 |
+
os.environ.update({
|
11 |
+
"SWARMS_VERBOSITY": "ERROR",
|
12 |
+
"RICH_TRACEBACK": "0",
|
13 |
+
"SWARMS_SHOW_PANEL": "true",
|
14 |
+
"SWARMS_AUTO_PRINT": "true",
|
15 |
+
"PYTHONWARNINGS": "ignore"
|
16 |
+
})
|
17 |
+
|
18 |
+
def patch_rich_and_swarms():
|
19 |
+
"""Патчимо Rich та Swarms для уникнення помилок виводу"""
|
20 |
+
try:
|
21 |
+
# Патчимо Rich Console
|
22 |
+
import rich.console
|
23 |
+
|
24 |
+
original_print = rich.console.Console.print
|
25 |
+
|
26 |
+
def safe_print(self, *args, **kwargs):
|
27 |
+
"""Безпечний print що ігнорує None значення"""
|
28 |
+
try:
|
29 |
+
# Фільтруємо None значення
|
30 |
+
filtered_args = [arg for arg in args if arg is not None]
|
31 |
+
if filtered_args:
|
32 |
+
return original_print(self, *filtered_args, **kwargs)
|
33 |
+
except Exception:
|
34 |
+
# Ігноруємо всі помилки виводу
|
35 |
+
pass
|
36 |
+
|
37 |
+
rich.console.Console.print = safe_print
|
38 |
+
|
39 |
+
# Патчимо Swarms formatter
|
40 |
+
try:
|
41 |
+
import swarms.utils.formatter as formatter
|
42 |
+
|
43 |
+
# # Заміняємо _print_panel на заглушку
|
44 |
+
# def dummy_print_panel(*args, **kwargs):
|
45 |
+
# pass
|
46 |
+
|
47 |
+
# if hasattr(formatter, 'Formatter'):
|
48 |
+
# formatter.Formatter._print_panel = dummy_print_panel
|
49 |
+
|
50 |
+
# Патчимо print_panel_token_usage
|
51 |
+
def safe_print_panel_token_usage(self, *args, **kwargs):
|
52 |
+
try:
|
53 |
+
# Просто виводимо базову інформацію без форматування
|
54 |
+
if args and hasattr(args[0], '__dict__'):
|
55 |
+
data = args[0].__dict__ if hasattr(args[0], '__dict__') else str(args[0])
|
56 |
+
print(f"[Token Usage] {data}")
|
57 |
+
except:
|
58 |
+
pass
|
59 |
+
|
60 |
+
if hasattr(formatter, 'print_panel_token_usage'):
|
61 |
+
formatter.print_panel_token_usage = safe_print_panel_token_usage
|
62 |
+
|
63 |
+
except ImportError:
|
64 |
+
pass
|
65 |
+
|
66 |
+
# Патчимо threading для уникнення помилок в потоках
|
67 |
+
import threading
|
68 |
+
original_thread_init = threading.Thread.__init__
|
69 |
+
|
70 |
+
def safe_thread_init(self, *args, **kwargs):
|
71 |
+
"""Обгортаємо target функцію в try/except"""
|
72 |
+
original_target = kwargs.get('target')
|
73 |
+
|
74 |
+
if original_target:
|
75 |
+
def safe_target(*args, **kwargs):
|
76 |
+
try:
|
77 |
+
return original_target(*args, **kwargs)
|
78 |
+
except Exception:
|
79 |
+
# Ігноруємо помилки в потоках
|
80 |
+
pass
|
81 |
+
|
82 |
+
kwargs['target'] = safe_target
|
83 |
+
|
84 |
+
return original_thread_init(self, *args, **kwargs)
|
85 |
+
|
86 |
+
threading.Thread.__init__ = safe_thread_init
|
87 |
+
|
88 |
+
except Exception as e:
|
89 |
+
print(f"Warning: Could not patch Rich/Swarms: {e}")
|
90 |
+
|
91 |
+
# Застосовуємо патчі перед імпортом MAI-DX
|
92 |
+
patch_rich_and_swarms()
|
93 |
+
|
94 |
+
# Тепер можна безпечно імпортувати MAI-DX
|
95 |
+
try:
|
96 |
+
from mai_dx import MaiDxOrchestrator
|
97 |
+
MAI_DX_AVAILABLE = True
|
98 |
+
except ImportError as e:
|
99 |
+
MAI_DX_AVAILABLE = False
|
100 |
+
print(f"MAI-DX not available: {e}")
|
101 |
+
|
102 |
+
class SafeMaiDxOrchestrator:
|
103 |
+
"""Безпечна обгортка для MaiDxOrchestrator"""
|
104 |
+
|
105 |
+
def __init__(self, *args, **kwargs):
|
106 |
+
if not MAI_DX_AVAILABLE:
|
107 |
+
raise ImportError("MAI-DX is not available")
|
108 |
+
|
109 |
+
# Додаткові патчі безпосередньо перед створенням
|
110 |
+
patch_rich_and_swarms()
|
111 |
+
|
112 |
+
# Створюємо оригінальний orchestrator
|
113 |
+
self.orchestrator = MaiDxOrchestrator(*args, **kwargs)
|
114 |
+
|
115 |
+
def run(self, *args, **kwargs):
|
116 |
+
"""Виконати діагностику з додатковим захистом"""
|
117 |
+
# Переконуємося що патчі активні
|
118 |
+
patch_rich_and_swarms()
|
119 |
+
|
120 |
+
# Тимчасово змінюємо stderr щоб приховати помилки Rich
|
121 |
+
import io
|
122 |
+
original_stderr = sys.stderr
|
123 |
+
sys.stderr = io.StringIO()
|
124 |
+
|
125 |
+
try:
|
126 |
+
# Виконуємо оригінальний run
|
127 |
+
result = self.orchestrator.run(*args, **kwargs)
|
128 |
+
return result
|
129 |
+
finally:
|
130 |
+
# Відновлю��мо stderr
|
131 |
+
sys.stderr = original_stderr
|
132 |
+
|
133 |
+
def __getattr__(self, name):
|
134 |
+
"""Проксі для всіх інших методів"""
|
135 |
+
return getattr(self.orchestrator, name)
|
136 |
+
|
137 |
+
# Експортуємо класи
|
138 |
+
__all__ = ['SafeMaiDxOrchestrator', 'MAI_DX_AVAILABLE', 'patch_rich_and_swarms']
|
production_mai_dx.py
DELETED
@@ -1,254 +0,0 @@
|
|
1 |
-
#!/usr/bin/env python3
|
2 |
-
"""
|
3 |
-
Production-ready MAI-DX Orchestrator
|
4 |
-
Оптимізовано для реального використання
|
5 |
-
"""
|
6 |
-
import os
|
7 |
-
import sys
|
8 |
-
import warnings
|
9 |
-
from dotenv import load_dotenv
|
10 |
-
|
11 |
-
# Завантажуємо змінні середовища
|
12 |
-
load_dotenv()
|
13 |
-
|
14 |
-
# Виробничі налаштування для стабільності
|
15 |
-
os.environ["SWARMS_VERBOSITY"] = "SILENT"
|
16 |
-
os.environ["RICH_TRACEBACK"] = "0"
|
17 |
-
os.environ["SWARMS_SHOW_PANEL"] = "false"
|
18 |
-
os.environ["SWARMS_AUTO_PRINT"] = "false"
|
19 |
-
os.environ["LOGURU_LEVEL"] = "WARNING" # Менше спаму в логах
|
20 |
-
warnings.filterwarnings("ignore")
|
21 |
-
|
22 |
-
# Rich formatter патч (довелося ефективність!)
|
23 |
-
def patch_rich_formatter():
|
24 |
-
try:
|
25 |
-
import swarms.utils.formatter
|
26 |
-
def dummy_print_panel(*args, **kwargs):
|
27 |
-
pass
|
28 |
-
if hasattr(swarms.utils.formatter, 'Formatter'):
|
29 |
-
swarms.utils.formatter.Formatter._print_panel = dummy_print_panel
|
30 |
-
return True
|
31 |
-
except:
|
32 |
-
return False
|
33 |
-
|
34 |
-
patch_rich_formatter()
|
35 |
-
|
36 |
-
from mai_dx import MaiDxOrchestrator
|
37 |
-
from loguru import logger
|
38 |
-
|
39 |
-
class ProductionMAIDX:
|
40 |
-
"""Production-wrapper для MAI-DX з поліпшеннями"""
|
41 |
-
|
42 |
-
def __init__(self, model="gemini/gemini-2.5-flash"):
|
43 |
-
self.model = model
|
44 |
-
self.available_modes = ["question_only", "budgeted", "no_budget"]
|
45 |
-
|
46 |
-
def create_optimized_orchestrator(self, mode="question_only", budget=2000, max_iterations=5):
|
47 |
-
"""Створює оптимізований оркестратор для production"""
|
48 |
-
|
49 |
-
# Рекомендовані налаштування для кожного режиму
|
50 |
-
configs = {
|
51 |
-
"question_only": {"budget": 1000, "iterations": 3},
|
52 |
-
"budgeted": {"budget": 3000, "iterations": 5},
|
53 |
-
"no_budget": {"budget": 10000, "iterations": 7}
|
54 |
-
}
|
55 |
-
|
56 |
-
config = configs.get(mode, {"budget": budget, "iterations": max_iterations})
|
57 |
-
|
58 |
-
try:
|
59 |
-
orchestrator = MaiDxOrchestrator(
|
60 |
-
model_name=self.model,
|
61 |
-
max_iterations=config["iterations"],
|
62 |
-
initial_budget=config["budget"],
|
63 |
-
mode=mode
|
64 |
-
)
|
65 |
-
|
66 |
-
print(f"✅ MAI-DX готовий: {mode} режим, бюджет ${config['budget']}")
|
67 |
-
return orchestrator
|
68 |
-
|
69 |
-
except Exception as e:
|
70 |
-
print(f"❌ Помилка створення: {e}")
|
71 |
-
return None
|
72 |
-
|
73 |
-
def diagnose_case(self, case_description, expected_diagnosis=None, mode="question_only"):
|
74 |
-
"""Основний метод для діагностики"""
|
75 |
-
|
76 |
-
print(f"\n🩺 Запуск MAI-DX діагностики...")
|
77 |
-
print(f"📋 Режим: {mode}")
|
78 |
-
|
79 |
-
# Створюємо оркестратор
|
80 |
-
orchestrator = self.create_optimized_orchestrator(mode)
|
81 |
-
if not orchestrator:
|
82 |
-
return None
|
83 |
-
|
84 |
-
try:
|
85 |
-
# Якщо немає expected_diagnosis, використовуємо загальний
|
86 |
-
ground_truth = expected_diagnosis or "Unknown diagnosis"
|
87 |
-
|
88 |
-
print("🔄 Аналіз випадку... (це може зайняти 2-5 хвилин)")
|
89 |
-
|
90 |
-
# Запускаємо діагностику
|
91 |
-
result = orchestrator.run(
|
92 |
-
initial_case_info=case_description,
|
93 |
-
full_case_details=case_description,
|
94 |
-
ground_truth_diagnosis=ground_truth
|
95 |
-
)
|
96 |
-
|
97 |
-
return self._format_result(result, mode)
|
98 |
-
|
99 |
-
except Exception as e:
|
100 |
-
print(f"❌ Помилка діагностики: {e}")
|
101 |
-
return None
|
102 |
-
|
103 |
-
def _format_result(self, result, mode):
|
104 |
-
"""Форматуємо результат для зручного відображення"""
|
105 |
-
|
106 |
-
# Оцінюємо якість результату
|
107 |
-
if result.accuracy_score >= 4.0:
|
108 |
-
quality = "🎉 ВІДМІННО"
|
109 |
-
elif result.accuracy_score >= 3.0:
|
110 |
-
quality = "👍 ДОБРЕ"
|
111 |
-
elif result.accuracy_score >= 2.0:
|
112 |
-
quality = "⚠️ ЗАДОВІЛЬНО"
|
113 |
-
elif "not reached" in result.final_diagnosis.lower():
|
114 |
-
quality = "🔄 ПОТРІБНО БІЛЬШЕ ЧАСУ"
|
115 |
-
else:
|
116 |
-
quality = "🔍 ПОТРЕБУЄ ПЕРЕВІРКИ"
|
117 |
-
|
118 |
-
formatted = {
|
119 |
-
"diagnosis": result.final_diagnosis,
|
120 |
-
"confidence": result.accuracy_score,
|
121 |
-
"quality": quality,
|
122 |
-
"cost": result.total_cost,
|
123 |
-
"iterations": result.iterations,
|
124 |
-
"mode": mode,
|
125 |
-
"reasoning": getattr(result, 'accuracy_reasoning', 'Недоступно')[:200] + "..."
|
126 |
-
}
|
127 |
-
|
128 |
-
return formatted
|
129 |
-
|
130 |
-
def batch_diagnose(self, cases, mode="question_only"):
|
131 |
-
"""Діагностика кількох випадків"""
|
132 |
-
results = []
|
133 |
-
|
134 |
-
for i, case in enumerate(cases, 1):
|
135 |
-
print(f"\n📋 Випадок {i}/{len(cases)}")
|
136 |
-
|
137 |
-
case_text = case.get("description", "")
|
138 |
-
expected = case.get("expected", None)
|
139 |
-
|
140 |
-
result = self.diagnose_case(case_text, expected, mode)
|
141 |
-
if result:
|
142 |
-
results.append({"case_id": i, **result})
|
143 |
-
|
144 |
-
return results
|
145 |
-
|
146 |
-
def demo_production_usage():
|
147 |
-
"""Демонстрація production використання"""
|
148 |
-
|
149 |
-
print("=" * 70)
|
150 |
-
print("🏥 MAI-DX PRODUCTION DEMO")
|
151 |
-
print("=" * 70)
|
152 |
-
|
153 |
-
# Створюємо production instance
|
154 |
-
mai_dx = ProductionMAIDX()
|
155 |
-
|
156 |
-
# Тестові випадки
|
157 |
-
test_cases = [
|
158 |
-
{
|
159 |
-
"description": "35-year-old man with sudden severe chest pain radiating to left arm, sweating, nausea. Pain started 2 hours ago during exercise.",
|
160 |
-
"expected": "Myocardial infarction"
|
161 |
-
},
|
162 |
-
{
|
163 |
-
"description": "22-year-old woman with severe headache, neck stiffness, fever 39°C, photophobia. Symptoms started this morning.",
|
164 |
-
"expected": "Meningitis"
|
165 |
-
},
|
166 |
-
{
|
167 |
-
"description": "45-year-old smoker with persistent cough for 3 months, weight loss 10kg, bloody sputum occasionally.",
|
168 |
-
"expected": "Lung cancer"
|
169 |
-
}
|
170 |
-
]
|
171 |
-
|
172 |
-
print(f"📋 Тестування {len(test_cases)} випадків...")
|
173 |
-
|
174 |
-
# Запускаємо діагностику
|
175 |
-
results = mai_dx.batch_diagnose(test_cases, mode="question_only")
|
176 |
-
|
177 |
-
# Виводимо результати
|
178 |
-
print(f"\n{'='*70}")
|
179 |
-
print("📊 РЕЗУЛЬТАТИ ТЕСТУВАННЯ")
|
180 |
-
print("="*70)
|
181 |
-
|
182 |
-
for result in results:
|
183 |
-
print(f"\n🔍 Випадок {result['case_id']}:")
|
184 |
-
print(f" Діагноз: {result['diagnosis']}")
|
185 |
-
print(f" Якість: {result['quality']}")
|
186 |
-
print(f" Оцінка: {result['confidence']}/5.0")
|
187 |
-
print(f" Вартість: ${result['cost']}")
|
188 |
-
print(f" Ітерації: {result['iterations']}")
|
189 |
-
|
190 |
-
# Статистика
|
191 |
-
successful = sum(1 for r in results if r['confidence'] >= 2.0)
|
192 |
-
print(f"\n📈 Статистика: {successful}/{len(results)} успішних діагнозів")
|
193 |
-
|
194 |
-
if successful >= len(results) * 0.7:
|
195 |
-
print("🎉 MAI-DX готовий для production!")
|
196 |
-
else:
|
197 |
-
print("⚠️ Потрібне додаткове налаштування")
|
198 |
-
|
199 |
-
def simple_diagnostic_session():
|
200 |
-
"""Простий інтерактивний сеанс діагностики"""
|
201 |
-
|
202 |
-
print("\n🩺 Простий сеанс діагностики")
|
203 |
-
print("-" * 40)
|
204 |
-
|
205 |
-
# Простий випадок для демонстрації
|
206 |
-
case = """
|
207 |
-
29-year-old woman with sore throat and peritonsillar swelling and bleeding.
|
208 |
-
Symptoms did not abate with antimicrobial therapy.
|
209 |
-
|
210 |
-
History: Onset 7 weeks ago, worsening right-sided pain.
|
211 |
-
Physical Exam: Right peritonsillar mass, displaces uvula.
|
212 |
-
Biopsy: Round-cell neoplasm, positive for desmin and MyoD1.
|
213 |
-
"""
|
214 |
-
|
215 |
-
expected = "Embryonal rhabdomyosarcoma of the pharynx"
|
216 |
-
|
217 |
-
mai_dx = ProductionMAIDX()
|
218 |
-
result = mai_dx.diagnose_case(case, expected, "question_only")
|
219 |
-
|
220 |
-
if result:
|
221 |
-
print(f"\n🎯 Діагноз: {result['diagnosis']}")
|
222 |
-
print(f"⭐ Якість: {result['quality']}")
|
223 |
-
print(f"💰 Вартість: ${result['cost']}")
|
224 |
-
|
225 |
-
# Аналіз близькості діагнозу
|
226 |
-
if "rhabdomyosarcoma" in result['diagnosis'].lower():
|
227 |
-
print("✅ Правильний тип пухлини ідентифіковано!")
|
228 |
-
|
229 |
-
return True
|
230 |
-
else:
|
231 |
-
print("❌ Діагностика не вдалася")
|
232 |
-
return False
|
233 |
-
|
234 |
-
if __name__ == "__main__":
|
235 |
-
try:
|
236 |
-
# Запускаємо простий тест
|
237 |
-
simple_diagnostic_session()
|
238 |
-
|
239 |
-
# Опціонально - повний demo
|
240 |
-
print("\n" + "="*50)
|
241 |
-
print("Запустити повний production demo? (y/n): ", end="")
|
242 |
-
|
243 |
-
# Для автоматичного запуску в тестах - пропускаємо input
|
244 |
-
try:
|
245 |
-
choice = input().lower()
|
246 |
-
if choice == 'y':
|
247 |
-
demo_production_usage()
|
248 |
-
except:
|
249 |
-
print("Пропускаємо інтерактивний demo")
|
250 |
-
|
251 |
-
except KeyboardInterrupt:
|
252 |
-
print("\n⏹️ Зупинено користувачем")
|
253 |
-
except Exception as e:
|
254 |
-
print(f"\n💥 Помилка: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
run_mai_dx_fixed.py
ADDED
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""
|
3 |
+
Головний файл запуску MAI-DX з виправленнями та повним логуванням
|
4 |
+
Використовуйте цей файл замість app.py
|
5 |
+
"""
|
6 |
+
|
7 |
+
import os
|
8 |
+
import sys
|
9 |
+
import warnings
|
10 |
+
|
11 |
+
# Встановлюємо змінні середовища ДО всіх імпортів
|
12 |
+
os.environ.update({
|
13 |
+
"SWARMS_VERBOSITY": "ERROR",
|
14 |
+
"RICH_TRACEBACK": "0",
|
15 |
+
"SWARMS_SHOW_PANEL": "true",
|
16 |
+
"SWARMS_AUTO_PRINT": "true",
|
17 |
+
"PYTHONWARNINGS": "ignore",
|
18 |
+
"GRADIO_ANALYTICS_ENABLED": "false"
|
19 |
+
})
|
20 |
+
|
21 |
+
# Ігноруємо всі попередження
|
22 |
+
warnings.filterwarnings("ignore")
|
23 |
+
|
24 |
+
# Перевіряємо що всі необхідні файли присутні
|
25 |
+
required_files = [
|
26 |
+
"enhanced_mai_dx_logger.py",
|
27 |
+
"mai_dx_wrapper.py",
|
28 |
+
"updated_mai_dx_interface.py"
|
29 |
+
]
|
30 |
+
|
31 |
+
for file in required_files:
|
32 |
+
if not os.path.exists(file):
|
33 |
+
print(f"❌ Помилка: Файл {file} не знайдено!")
|
34 |
+
print("Переконайтеся що всі файли з артефактів збережені в робочій директорії.")
|
35 |
+
sys.exit(1)
|
36 |
+
|
37 |
+
# Імпортуємо інтерфейс
|
38 |
+
try:
|
39 |
+
from updated_mai_dx_interface import create_updated_gradio_interface
|
40 |
+
except ImportError as e:
|
41 |
+
print(f"❌ Помилка імпорту інтерфейсу: {e}")
|
42 |
+
print("Перевірте що всі залежності встановлені:")
|
43 |
+
print("pip install gradio pandas plotly python-dotenv")
|
44 |
+
sys.exit(1)
|
45 |
+
|
46 |
+
def main():
|
47 |
+
"""Головна функція запуску"""
|
48 |
+
print("🚀 Запуск MAI-DX Enhanced Platform with Full Agent Conversation Logging...")
|
49 |
+
print("📁 Логи будуть збережені в директорії: mai_dx_logs/")
|
50 |
+
print("=" * 60)
|
51 |
+
|
52 |
+
# Створюємо директорію для логів якщо її немає
|
53 |
+
os.makedirs("mai_dx_logs", exist_ok=True)
|
54 |
+
|
55 |
+
try:
|
56 |
+
# Створюємо та запускаємо інтерфейс
|
57 |
+
demo = create_updated_gradio_interface()
|
58 |
+
|
59 |
+
# Запускаємо з налаштуваннями
|
60 |
+
demo.launch(
|
61 |
+
server_name="0.0.0.0",
|
62 |
+
server_port=7860,
|
63 |
+
share=False,
|
64 |
+
debug=False,
|
65 |
+
show_error=True,
|
66 |
+
quiet=False
|
67 |
+
)
|
68 |
+
|
69 |
+
except KeyboardInterrupt:
|
70 |
+
print("\n⏹️ Зупинка сервера...")
|
71 |
+
except Exception as e:
|
72 |
+
print(f"\n❌ Критична помилка: {e}")
|
73 |
+
import traceback
|
74 |
+
traceback.print_exc()
|
75 |
+
|
76 |
+
if __name__ == "__main__":
|
77 |
+
main()
|
example.py → test/example.py
RENAMED
File without changes
|
example_corrected.py → test/example_corrected.py
RENAMED
File without changes
|
simple_test.py → test/simple_test.py
RENAMED
File without changes
|
updated_mai_dx_interface.py
ADDED
@@ -0,0 +1,730 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""
|
3 |
+
Оновлений MAI-DX Gradio Interface з повним логуванням розмов агентів
|
4 |
+
"""
|
5 |
+
import os
|
6 |
+
import sys
|
7 |
+
import json
|
8 |
+
import time
|
9 |
+
import pandas as pd
|
10 |
+
import gradio as gr
|
11 |
+
from datetime import datetime
|
12 |
+
from typing import Dict, List, Tuple, Optional
|
13 |
+
from dataclasses import dataclass, asdict
|
14 |
+
import warnings
|
15 |
+
|
16 |
+
# Налаштування середовища
|
17 |
+
os.environ.update({
|
18 |
+
"SWARMS_VERBOSITY": "ERROR",
|
19 |
+
"RICH_TRACEBACK": "0",
|
20 |
+
"SWARMS_SHOW_PANEL": "true",
|
21 |
+
"SWARMS_AUTO_PRINT": "true"
|
22 |
+
})
|
23 |
+
warnings.filterwarnings("ignore")
|
24 |
+
|
25 |
+
from dotenv import load_dotenv
|
26 |
+
load_dotenv()
|
27 |
+
|
28 |
+
# Імпорт MAI-DX через безпечну обгортку
|
29 |
+
try:
|
30 |
+
from mai_dx_wrapper import SafeMaiDxOrchestrator as MaiDxOrchestrator, MAI_DX_AVAILABLE
|
31 |
+
except ImportError:
|
32 |
+
try:
|
33 |
+
from mai_dx import MaiDxOrchestrator
|
34 |
+
MAI_DX_AVAILABLE = True
|
35 |
+
except ImportError as e:
|
36 |
+
MAI_DX_AVAILABLE = False
|
37 |
+
IMPORT_ERROR = str(e)
|
38 |
+
|
39 |
+
# Імпорт покращеного логгера
|
40 |
+
from enhanced_mai_dx_logger import MAIDxConversationLogger, DiagnosisSession
|
41 |
+
|
42 |
+
# Перевірка доступності Plotly
|
43 |
+
try:
|
44 |
+
import plotly.graph_objects as go
|
45 |
+
import plotly.express as px
|
46 |
+
PLOTLY_AVAILABLE = True
|
47 |
+
except ImportError:
|
48 |
+
PLOTLY_AVAILABLE = False
|
49 |
+
print("⚠️ Plotly не встановлено, візуалізації будуть недоступні")
|
50 |
+
|
51 |
+
class RealTimeMetrics:
|
52 |
+
"""Метрики в реальному часі"""
|
53 |
+
def __init__(self):
|
54 |
+
self.reset()
|
55 |
+
|
56 |
+
def reset(self):
|
57 |
+
self.start_time = time.time()
|
58 |
+
self.agents_activity = {
|
59 |
+
'Dr. Hypothesis': 0, 'Dr. Test-Chooser': 0, 'Dr. Challenger': 0,
|
60 |
+
'Dr. Stewardship': 0, 'Dr. Checklist': 0, 'Consensus Coordinator': 0,
|
61 |
+
'Gatekeeper': 0, 'Judge': 0
|
62 |
+
}
|
63 |
+
self.cost_progression = []
|
64 |
+
self.conversation_rounds = []
|
65 |
+
|
66 |
+
def update_agent_activity(self, agent_name: str):
|
67 |
+
if agent_name in self.agents_activity:
|
68 |
+
self.agents_activity[agent_name] += 1
|
69 |
+
|
70 |
+
def add_cost_point(self, cost: float):
|
71 |
+
self.cost_progression.append({'time': time.time() - self.start_time, 'cost': cost})
|
72 |
+
|
73 |
+
def add_conversation_round(self, round_data: dict):
|
74 |
+
self.conversation_rounds.append(round_data)
|
75 |
+
|
76 |
+
class UpdatedMAIDXInterface:
|
77 |
+
"""Оновлений інтерфейс з повним логуванням розмов"""
|
78 |
+
|
79 |
+
def __init__(self):
|
80 |
+
self.sessions_history = []
|
81 |
+
self.conversation_logger = MAIDxConversationLogger("mai_dx_logs")
|
82 |
+
self.current_metrics = RealTimeMetrics()
|
83 |
+
|
84 |
+
self.sample_cases = {
|
85 |
+
"🫀 Кардіологічний (Гострий MI)": {
|
86 |
+
"info": "Пацієнт: 58-річний чоловік, менеджер, гіпертонія в анамнезі\nСкарги: Гострий роздираючий біль у грудях 3 години, іррадіація в ліву руку\nОгляд: Блідий, пітливий, АТ 160/90, ЧСС 95\nЕКГ: ST-підйоми у відведеннях II, III, aVF (нижня стінка)\nТропонін I: 8.5 нг/мл (норма <0.04)\nАнамнез: Куріння 30 років, дислипідемія, сімейний анамнез ІХС",
|
87 |
+
"expected": "Acute inferior wall myocardial infarction (STEMI)"
|
88 |
+
},
|
89 |
+
"🧠 Неврологічний (Гострий інсульт)": {
|
90 |
+
"info": "Пацієнтка: 67-річна жінка, раптові неврологічні симптоми\nПрезентація: Раптова слабкість правої сторони 2 години тому\nОгляд: Свідома, дезорієнтована у часі, правостороннє опущення обличчя\nНеврологія: Правостороння геміплегія, афазія, девіація очей вліво\nКТ голови: Гострого крововиливу немає, рання ішемія у лівій МСА\nNIHSS: 15 балів",
|
91 |
+
"expected": "Acute ischemic stroke in the left middle cerebral artery territory"
|
92 |
+
},
|
93 |
+
"🦠 Інфекційний (Сепсис)": {
|
94 |
+
"info": "Пацієнт: 45-річний чоловік з прогресуючою лихоманкою\nСкарги: Висока температура 39.5°C, озноб, загальна слабкість 3 дні\nОгляд: Гарячий, тахікардія 120/хв, гіпотензія 85/50\nЛабораторно: Лейкоцити 18000, С-реактивний білок 180, прокальцитонін 5.2\nПосів крові: Pending, lactate 4.2 ммоль/л\nАнамнез: Нещодавня стоматологічна процедура",
|
95 |
+
"expected": "Sepsis with a possible odontogenic source"
|
96 |
+
}
|
97 |
+
}
|
98 |
+
|
99 |
+
def diagnose_with_full_logging(
|
100 |
+
self, case_name: str, patient_info: str, mode: str, budget: int, max_iterations: int,
|
101 |
+
model_name: str, expected_diagnosis: str = "", enable_logging: bool = True,
|
102 |
+
progress=gr.Progress()
|
103 |
+
) -> Tuple[str, str, str, Optional[object], Optional[object], str, str]:
|
104 |
+
|
105 |
+
if not MAI_DX_AVAILABLE:
|
106 |
+
return self._format_error(f"❌ MAI-DX недоступний: {IMPORT_ERROR}")
|
107 |
+
if not patient_info.strip():
|
108 |
+
return self._format_error("❌ Введіть інформацію про пацієнта")
|
109 |
+
|
110 |
+
self.current_metrics.reset()
|
111 |
+
conversation_log = ""
|
112 |
+
structured_conversations = ""
|
113 |
+
case_id = None
|
114 |
+
|
115 |
+
# Тимчасово відключаємо Rich форматування
|
116 |
+
original_show_panel = os.environ.get("SWARMS_SHOW_PANEL", "false")
|
117 |
+
original_auto_print = os.environ.get("SWARMS_AUTO_PRINT", "false")
|
118 |
+
os.environ["SWARMS_SHOW_PANEL"] = "false"
|
119 |
+
os.environ["SWARMS_AUTO_PRINT"] = "false"
|
120 |
+
|
121 |
+
try:
|
122 |
+
progress(0.1, desc="🚀 Ініціалізація...")
|
123 |
+
|
124 |
+
# Створюємо сесію
|
125 |
+
if enable_logging:
|
126 |
+
case_id = self.conversation_logger.start_session(
|
127 |
+
case_name=case_name,
|
128 |
+
patient_info=patient_info,
|
129 |
+
mode=mode,
|
130 |
+
budget=budget
|
131 |
+
)
|
132 |
+
conversation_log += f"📝 Розпочато логування: {case_id}\n\n"
|
133 |
+
|
134 |
+
progress(0.2, desc="🤖 Створення AI-панелі...")
|
135 |
+
|
136 |
+
# Створюємо orchestrator
|
137 |
+
orchestrator = MaiDxOrchestrator(
|
138 |
+
model_name=model_name,
|
139 |
+
max_iterations=max_iterations,
|
140 |
+
initial_budget=budget,
|
141 |
+
mode=mode
|
142 |
+
)
|
143 |
+
|
144 |
+
progress(0.3, desc="🔍 Запуск діагностики...")
|
145 |
+
start_time = time.time()
|
146 |
+
|
147 |
+
# Виконуємо діагностику з повним захопленням
|
148 |
+
if enable_logging and case_id:
|
149 |
+
result = self.conversation_logger.capture_orchestrator_output(
|
150 |
+
case_id=case_id,
|
151 |
+
orchestrator_func=orchestrator.run,
|
152 |
+
initial_case_info=patient_info,
|
153 |
+
full_case_details=patient_info,
|
154 |
+
ground_truth_diagnosis=expected_diagnosis or "Unknown"
|
155 |
+
)
|
156 |
+
|
157 |
+
# Отримуємо захоплений вивід
|
158 |
+
session = self.conversation_logger.sessions.get(case_id)
|
159 |
+
if session:
|
160 |
+
conversation_log += "🤖 Повний лог виконання:\n" + "="*60 + "\n"
|
161 |
+
conversation_log += session.raw_output + "\n" + "="*60 + "\n\n"
|
162 |
+
|
163 |
+
# Оновлюємо метрики з розмов
|
164 |
+
for conv in session.conversations:
|
165 |
+
self.current_metrics.add_conversation_round({
|
166 |
+
'round': conv.round_number,
|
167 |
+
'agents': len(set(msg.agent_name for msg in conv.messages)),
|
168 |
+
'messages': len(conv.messages),
|
169 |
+
'cost': conv.cost_incurred
|
170 |
+
})
|
171 |
+
|
172 |
+
# Оновлюємо активність агентів
|
173 |
+
for msg in conv.messages:
|
174 |
+
self.current_metrics.update_agent_activity(msg.agent_name)
|
175 |
+
|
176 |
+
# Додаємо точки вартості
|
177 |
+
if conv.cost_incurred > 0:
|
178 |
+
elapsed_time = (datetime.fromisoformat(conv.end_time or conv.start_time) -
|
179 |
+
datetime.fromisoformat(session.timestamp)).total_seconds()
|
180 |
+
self.current_metrics.add_cost_point(conv.cost_incurred)
|
181 |
+
else:
|
182 |
+
# Виконуємо без логування
|
183 |
+
result = orchestrator.run(
|
184 |
+
initial_case_info=patient_info,
|
185 |
+
full_case_details=patient_info,
|
186 |
+
ground_truth_diagnosis=expected_diagnosis or "Unknown"
|
187 |
+
)
|
188 |
+
|
189 |
+
duration = time.time() - start_time
|
190 |
+
progress(0.9, desc="📊 Обробка результатів...")
|
191 |
+
|
192 |
+
# Завершуємо сесію
|
193 |
+
if enable_logging and case_id:
|
194 |
+
saved_case_id = self.conversation_logger.end_session(
|
195 |
+
case_id=case_id,
|
196 |
+
final_diagnosis=result.final_diagnosis,
|
197 |
+
confidence=result.accuracy_score,
|
198 |
+
cost=result.total_cost,
|
199 |
+
reasoning=getattr(result, 'accuracy_reasoning', 'N/A')
|
200 |
+
)
|
201 |
+
conversation_log += f"💾 Сесію збережено як: {saved_case_id}\n"
|
202 |
+
|
203 |
+
# Генеруємо HTML звіт
|
204 |
+
html_path = self.conversation_logger.export_conversation_html(case_id)
|
205 |
+
conversation_log += f"📄 HTML звіт: {html_path}\n"
|
206 |
+
|
207 |
+
# Форматуємо структуровані розмови
|
208 |
+
structured_conversations = self._format_structured_conversations(case_id)
|
209 |
+
|
210 |
+
# Створюємо об'єкт сесії для історії
|
211 |
+
session = DiagnosisSession(
|
212 |
+
case_id=case_id or "no_logging",
|
213 |
+
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
214 |
+
case_name=case_name,
|
215 |
+
patient_info=patient_info,
|
216 |
+
mode=mode,
|
217 |
+
budget=budget,
|
218 |
+
diagnosis=result.final_diagnosis,
|
219 |
+
confidence=result.accuracy_score,
|
220 |
+
cost=result.total_cost,
|
221 |
+
iterations=getattr(result, 'iterations', 0),
|
222 |
+
duration=duration,
|
223 |
+
status="✅ Успішно" if result.accuracy_score >= 3.0 else "⚠️ Потребує перегляду",
|
224 |
+
reasoning=getattr(result, 'accuracy_reasoning', 'N/A')
|
225 |
+
)
|
226 |
+
self.sessions_history.append(session)
|
227 |
+
|
228 |
+
progress(1.0, desc="✅ Готово!")
|
229 |
+
|
230 |
+
# Створюємо візуалізації
|
231 |
+
metrics_plot = self._create_enhanced_metrics_visualization()
|
232 |
+
agent_plot = self._create_conversation_flow_chart()
|
233 |
+
|
234 |
+
return (
|
235 |
+
self._format_main_result(session),
|
236 |
+
self._format_detailed_analysis(session),
|
237 |
+
self._generate_enhanced_recommendations(result, expected_diagnosis),
|
238 |
+
metrics_plot,
|
239 |
+
agent_plot,
|
240 |
+
conversation_log,
|
241 |
+
structured_conversations
|
242 |
+
)
|
243 |
+
|
244 |
+
except Exception as e:
|
245 |
+
error_msg = f"❌ Помилка діагностики: {str(e)}"
|
246 |
+
if case_id:
|
247 |
+
error_msg += f"\n🗂️ Case ID: {case_id}"
|
248 |
+
return self._format_error(error_msg)
|
249 |
+
|
250 |
+
finally:
|
251 |
+
# Відновлюємо оригінальні налаштування
|
252 |
+
os.environ["SWARMS_SHOW_PANEL"] = original_show_panel
|
253 |
+
os.environ["SWARMS_AUTO_PRINT"] = original_auto_print
|
254 |
+
|
255 |
+
def _format_structured_conversations(self, case_id: str) -> str:
|
256 |
+
"""Форматує структуровані розмови у читабельний вигляд"""
|
257 |
+
conversations = self.conversation_logger.get_session_conversations(case_id)
|
258 |
+
|
259 |
+
if not conversations:
|
260 |
+
return "📭 Розмови не знайдено"
|
261 |
+
|
262 |
+
output = "## 💬 Структуровані розмови агентів\n\n"
|
263 |
+
|
264 |
+
for conv in conversations:
|
265 |
+
output += f"### 🔄 Раунд {conv.round_number}\n"
|
266 |
+
output += f"**Час:** {conv.start_time} - {conv.end_time or 'В процесі'}\n\n"
|
267 |
+
|
268 |
+
for msg in conv.messages:
|
269 |
+
emoji = {
|
270 |
+
'reasoning': '🤔',
|
271 |
+
'decision': '💡',
|
272 |
+
'input': '❓',
|
273 |
+
'output': '📊'
|
274 |
+
}.get(msg.message_type, '💬')
|
275 |
+
|
276 |
+
output += f"{emoji} **{msg.agent_name}** [{msg.message_type}]\n"
|
277 |
+
output += f"```\n{msg.content[:500]}{'...' if len(msg.content) > 500 else ''}\n```\n\n"
|
278 |
+
|
279 |
+
if conv.decision:
|
280 |
+
output += f"**🎯 Рішення:** {conv.decision}\n"
|
281 |
+
|
282 |
+
if conv.tests_ordered:
|
283 |
+
output += f"**🧪 Замовлені тести:** {', '.join(conv.tests_ordered)}\n"
|
284 |
+
|
285 |
+
if conv.questions_asked:
|
286 |
+
output += f"**❓ Поставлені питання:** {', '.join(conv.questions_asked[:3])}...\n"
|
287 |
+
|
288 |
+
if conv.cost_incurred > 0:
|
289 |
+
output += f"**💰 Вартість раунду:** ${conv.cost_incurred:.2f}\n"
|
290 |
+
|
291 |
+
output += "\n---\n\n"
|
292 |
+
|
293 |
+
return output
|
294 |
+
|
295 |
+
def _create_enhanced_metrics_visualization(self):
|
296 |
+
"""Створює покращену візуалізацію метрик"""
|
297 |
+
if not PLOTLY_AVAILABLE:
|
298 |
+
return None
|
299 |
+
|
300 |
+
if not self.current_metrics.conversation_rounds:
|
301 |
+
return None
|
302 |
+
|
303 |
+
try:
|
304 |
+
# Створюємо subplot з двома графіками
|
305 |
+
from plotly.subplots import make_subplots
|
306 |
+
|
307 |
+
fig = make_subplots(
|
308 |
+
rows=2, cols=1,
|
309 |
+
subplot_titles=('Прогресія вартості', 'Активність по раундах'),
|
310 |
+
vertical_spacing=0.15
|
311 |
+
)
|
312 |
+
|
313 |
+
# Графік вартості
|
314 |
+
if self.current_metrics.cost_progression:
|
315 |
+
fig.add_trace(
|
316 |
+
go.Scatter(
|
317 |
+
x=[p['time'] for p in self.current_metrics.cost_progression],
|
318 |
+
y=[p['cost'] for p in self.current_metrics.cost_progression],
|
319 |
+
mode='lines+markers',
|
320 |
+
name='Вартість',
|
321 |
+
line=dict(color='#3498db', width=3)
|
322 |
+
),
|
323 |
+
row=1, col=1
|
324 |
+
)
|
325 |
+
|
326 |
+
# Графік активності по раундах
|
327 |
+
rounds_data = self.current_metrics.conversation_rounds
|
328 |
+
if rounds_data:
|
329 |
+
fig.add_trace(
|
330 |
+
go.Bar(
|
331 |
+
x=[f"Раунд {r['round']}" for r in rounds_data],
|
332 |
+
y=[r['messages'] for r in rounds_data],
|
333 |
+
name='Кількість повідомлень',
|
334 |
+
marker_color='#2ecc71'
|
335 |
+
),
|
336 |
+
row=2, col=1
|
337 |
+
)
|
338 |
+
|
339 |
+
fig.update_layout(
|
340 |
+
height=600,
|
341 |
+
showlegend=True,
|
342 |
+
template='plotly_white'
|
343 |
+
)
|
344 |
+
|
345 |
+
return fig
|
346 |
+
|
347 |
+
except Exception as e:
|
348 |
+
print(f"Помилка візуалізації: {e}")
|
349 |
+
return None
|
350 |
+
|
351 |
+
def _create_conversation_flow_chart(self):
|
352 |
+
"""Створює діаграму потоку розмов між агентами"""
|
353 |
+
if not PLOTLY_AVAILABLE:
|
354 |
+
return None
|
355 |
+
|
356 |
+
activity_data = {k: v for k, v in self.current_metrics.agents_activity.items() if v > 0}
|
357 |
+
|
358 |
+
if not activity_data:
|
359 |
+
return None
|
360 |
+
|
361 |
+
try:
|
362 |
+
# Кольорова схема для різних агентів
|
363 |
+
colors = {
|
364 |
+
'Dr. Hypothesis': '#3498db',
|
365 |
+
'Dr. Test-Chooser': '#e74c3c',
|
366 |
+
'Dr. Challenger': '#f39c12',
|
367 |
+
'Dr. Stewardship': '#27ae60',
|
368 |
+
'Dr. Checklist': '#9b59b6',
|
369 |
+
'Consensus Coordinator': '#34495e',
|
370 |
+
'Gatekeeper': '#16a085',
|
371 |
+
'Judge': '#c0392b'
|
372 |
+
}
|
373 |
+
|
374 |
+
agent_colors = [colors.get(agent, '#95a5a6') for agent in activity_data.keys()]
|
375 |
+
|
376 |
+
fig = go.Figure(go.Bar(
|
377 |
+
x=list(activity_data.keys()),
|
378 |
+
y=list(activity_data.values()),
|
379 |
+
text=list(activity_data.values()),
|
380 |
+
textposition='auto',
|
381 |
+
marker_color=agent_colors
|
382 |
+
))
|
383 |
+
|
384 |
+
fig.update_layout(
|
385 |
+
title='🤖 Активність агентів у діагностичному процесі',
|
386 |
+
xaxis_title='Агенти',
|
387 |
+
yaxis_title='Кількість взаємодій',
|
388 |
+
template='plotly_white',
|
389 |
+
height=400,
|
390 |
+
margin=dict(t=60, b=100)
|
391 |
+
)
|
392 |
+
|
393 |
+
fig.update_xaxes(tickangle=-45)
|
394 |
+
|
395 |
+
return fig
|
396 |
+
|
397 |
+
except Exception as e:
|
398 |
+
print(f"Помилка графіку агентів: {e}")
|
399 |
+
return None
|
400 |
+
|
401 |
+
def _format_main_result(self, session):
|
402 |
+
efficiency = ((session.budget - session.cost) / session.budget * 100) if session.budget > 0 else 0
|
403 |
+
confidence_emoji = "🎯" if session.confidence >= 4.0 else "👍" if session.confidence >= 3.0 else "⚠️"
|
404 |
+
|
405 |
+
return f"""
|
406 |
+
## 🏥 Результати MAI-DX Діагностики
|
407 |
+
|
408 |
+
### 📋 Основна інформація
|
409 |
+
- **🗂️ Випадок**: {session.case_name}
|
410 |
+
- **🆔 ID сесії**: {session.case_id}
|
411 |
+
- **⏰ Час**: {session.timestamp}
|
412 |
+
- **🔧 Режим**: {session.mode}
|
413 |
+
|
414 |
+
### {confidence_emoji} Діагностичний висновок
|
415 |
+
**{session.diagnosis}**
|
416 |
+
|
417 |
+
### 📊 Показники якості
|
418 |
+
- **Точність**: {session.confidence:.1f}/5.0 ⭐
|
419 |
+
- **Статус**: {session.status}
|
420 |
+
- **Ітерації**: {session.iterations} циклів
|
421 |
+
- **Кількість агентів**: {len([a for a in self.current_metrics.agents_activity.values() if a > 0])}
|
422 |
+
|
423 |
+
### 💰 Економічні показники
|
424 |
+
- **Витрачено**: ${session.cost:,.2f}
|
425 |
+
- **Бюджет**: ${session.budget:,}
|
426 |
+
- **Ефективність**: {efficiency:.1f}% бюджету збережено
|
427 |
+
- **Швидкість**: {session.duration:.1f} секунд
|
428 |
+
"""
|
429 |
+
|
430 |
+
def _format_detailed_analysis(self, session):
|
431 |
+
return f"""
|
432 |
+
## 🔬 Детальний клінічний аналіз
|
433 |
+
|
434 |
+
### 💭 Мед��чне обґрунтування
|
435 |
+
{session.reasoning}
|
436 |
+
|
437 |
+
### 📂 Файли логів
|
438 |
+
- JSON: `mai_dx_logs/{session.case_id}.json`
|
439 |
+
- Raw text: `mai_dx_logs/{session.case_id}_raw.txt`
|
440 |
+
- HTML звіт: `mai_dx_logs/{session.case_id}_conversation.html`
|
441 |
+
"""
|
442 |
+
|
443 |
+
def _generate_enhanced_recommendations(self, result, expected_diagnosis):
|
444 |
+
comparison = ""
|
445 |
+
if expected_diagnosis:
|
446 |
+
is_match = expected_diagnosis.lower() in result.final_diagnosis.lower()
|
447 |
+
comparison = f"""
|
448 |
+
### 🎯 Порівняння з очікуваним
|
449 |
+
**Очікувався**: {expected_diagnosis}
|
450 |
+
**Отримано**: {result.final_diagnosis}
|
451 |
+
**Збіг**: {'✅ Так' if is_match else '❌ Ні'}
|
452 |
+
"""
|
453 |
+
|
454 |
+
return f"""
|
455 |
+
## 💡 Клінічні рекомендації
|
456 |
+
|
457 |
+
### 🏥 Негайні дії
|
458 |
+
- 🔍 Верифікувати діагноз з лікарем-спеціалістом
|
459 |
+
- 📋 Розглянути необхідність додаткових досліджень
|
460 |
+
- 📊 Переглянути детальний лог розмов агентів
|
461 |
+
|
462 |
+
{comparison}
|
463 |
+
|
464 |
+
### ⚠️ Важливе застереження
|
465 |
+
🔴 **Цей діагноз згенеровано ШІ і НЕ замінює професійну медичну консультацію.**
|
466 |
+
"""
|
467 |
+
|
468 |
+
def _format_error(self, error_msg):
|
469 |
+
return (error_msg, "", "", None, None, "", "")
|
470 |
+
|
471 |
+
def load_sample_case(self, sample_key):
|
472 |
+
case_data = self.sample_cases.get(sample_key, {})
|
473 |
+
return case_data.get("info", ""), sample_key, case_data.get("expected", "")
|
474 |
+
|
475 |
+
def get_session_details(self, case_id: str):
|
476 |
+
"""Отримати детальну інформацію про сесію"""
|
477 |
+
conversations = self.conversation_logger.get_session_conversations(case_id)
|
478 |
+
|
479 |
+
if not conversations:
|
480 |
+
return "Сесія не знайдена"
|
481 |
+
|
482 |
+
details = f"## Детальний аналіз сесії {case_id}\n\n"
|
483 |
+
details += f"**Кількість раундів**: {len(conversations)}\n"
|
484 |
+
|
485 |
+
total_messages = sum(len(conv.messages) for conv in conversations)
|
486 |
+
details += f"**Всього повідомлень**: {total_messages}\n\n"
|
487 |
+
|
488 |
+
# Статистика по агентах
|
489 |
+
agent_stats = {}
|
490 |
+
for conv in conversations:
|
491 |
+
for msg in conv.messages:
|
492 |
+
agent_stats[msg.agent_name] = agent_stats.get(msg.agent_name, 0) + 1
|
493 |
+
|
494 |
+
details += "### Статистика по агентах:\n"
|
495 |
+
for agent, count in sorted(agent_stats.items(), key=lambda x: x[1], reverse=True):
|
496 |
+
details += f"- **{agent}**: {count} повідомлень\n"
|
497 |
+
|
498 |
+
return details
|
499 |
+
|
500 |
+
def create_updated_gradio_interface():
|
501 |
+
interface = UpdatedMAIDXInterface()
|
502 |
+
|
503 |
+
custom_css = """
|
504 |
+
.gradio-container {
|
505 |
+
font-family: 'Inter', sans-serif;
|
506 |
+
}
|
507 |
+
.main-header {
|
508 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
509 |
+
color: white;
|
510 |
+
padding: 2rem;
|
511 |
+
border-radius: 15px;
|
512 |
+
margin-bottom: 2rem;
|
513 |
+
text-align: center;
|
514 |
+
}
|
515 |
+
.conversation-log {
|
516 |
+
font-family: 'Fira Code', monospace;
|
517 |
+
background: #2d3748;
|
518 |
+
color: #e2e8f0;
|
519 |
+
border-radius: 8px;
|
520 |
+
padding: 1rem;
|
521 |
+
max-height: 600px;
|
522 |
+
overflow-y: auto;
|
523 |
+
white-space: pre-wrap;
|
524 |
+
}
|
525 |
+
.structured-conv {
|
526 |
+
background: #f7fafc;
|
527 |
+
border: 1px solid #e2e8f0;
|
528 |
+
border-radius: 8px;
|
529 |
+
padding: 1rem;
|
530 |
+
max-height: 800px;
|
531 |
+
overflow-y: auto;
|
532 |
+
}
|
533 |
+
"""
|
534 |
+
|
535 |
+
with gr.Blocks(
|
536 |
+
title="🏥 MAI-DX Enhanced Platform with Full Logging",
|
537 |
+
theme=gr.themes.Soft(primary_hue="blue"),
|
538 |
+
css=custom_css
|
539 |
+
) as demo:
|
540 |
+
|
541 |
+
gr.HTML("""
|
542 |
+
<div class='main-header'>
|
543 |
+
<h1>🏥 MAI-DX Enhanced Research Platform</h1>
|
544 |
+
<p>🤖 Платформа для ШІ-діагностики з повним логуванням розмов агентів</p>
|
545 |
+
</div>
|
546 |
+
""")
|
547 |
+
|
548 |
+
with gr.Tabs():
|
549 |
+
with gr.Tab("🩺 Діагностика", elem_id="diagnosis-tab"):
|
550 |
+
with gr.Row():
|
551 |
+
with gr.Column(scale=1):
|
552 |
+
gr.Markdown("### 📝 Клінічний випадок")
|
553 |
+
case_name = gr.Textbox(
|
554 |
+
label="🏷️ Назва випадку",
|
555 |
+
placeholder="Наприклад: Кардіологічний випадок №1"
|
556 |
+
)
|
557 |
+
|
558 |
+
sample_selector = gr.Dropdown(
|
559 |
+
choices=list(interface.sample_cases.keys()),
|
560 |
+
label="🎯 Готові тестові випадки",
|
561 |
+
interactive=True
|
562 |
+
)
|
563 |
+
|
564 |
+
patient_info = gr.Textbox(
|
565 |
+
label="👤 Інформація про пацієнта",
|
566 |
+
lines=12,
|
567 |
+
placeholder="Введіть опис клінічного випадку..."
|
568 |
+
)
|
569 |
+
|
570 |
+
expected_diagnosis = gr.Textbox(
|
571 |
+
label="🎯 Очікуваний діагноз (англ.)",
|
572 |
+
lines=2
|
573 |
+
)
|
574 |
+
|
575 |
+
with gr.Column(scale=1):
|
576 |
+
gr.Markdown("### ⚙️ Налаштування")
|
577 |
+
|
578 |
+
mode = gr.Radio(
|
579 |
+
choices=["instant", "question_only", "budgeted", "no_budget", "ensemble"],
|
580 |
+
label="🔧 Режим",
|
581 |
+
value="budgeted",
|
582 |
+
interactive=True
|
583 |
+
)
|
584 |
+
|
585 |
+
budget = gr.Slider(
|
586 |
+
minimum=500,
|
587 |
+
maximum=10000,
|
588 |
+
step=500,
|
589 |
+
value=3000,
|
590 |
+
label="💵 Бюджет ($)",
|
591 |
+
interactive=True
|
592 |
+
)
|
593 |
+
|
594 |
+
max_iterations = gr.Slider(
|
595 |
+
minimum=1,
|
596 |
+
maximum=15,
|
597 |
+
step=1,
|
598 |
+
value=8,
|
599 |
+
label="🔄 Макс. ітерацій",
|
600 |
+
interactive=True
|
601 |
+
)
|
602 |
+
|
603 |
+
model_name = gr.Dropdown(
|
604 |
+
choices=[
|
605 |
+
"gemini/gemini-2.5-flash",
|
606 |
+
"gpt-4",
|
607 |
+
"gpt-4-turbo",
|
608 |
+
"claude-3-5-sonnet",
|
609 |
+
"gpt-4o"
|
610 |
+
],
|
611 |
+
label="🤖 LLM Модель",
|
612 |
+
value="gemini/gemini-2.5-flash",
|
613 |
+
interactive=True
|
614 |
+
)
|
615 |
+
|
616 |
+
enable_logging = gr.Checkbox(
|
617 |
+
label="📝 Повне логування розмов",
|
618 |
+
value=True,
|
619 |
+
interactive=True
|
620 |
+
)
|
621 |
+
|
622 |
+
diagnose_btn = gr.Button(
|
623 |
+
"🚀 Запустити діагностику",
|
624 |
+
variant="primary",
|
625 |
+
size="lg"
|
626 |
+
)
|
627 |
+
|
628 |
+
# Зв'язуємо вибір прикладу
|
629 |
+
sample_selector.change(
|
630 |
+
interface.load_sample_case,
|
631 |
+
inputs=sample_selector,
|
632 |
+
outputs=[patient_info, case_name, expected_diagnosis]
|
633 |
+
)
|
634 |
+
|
635 |
+
gr.Markdown("---\n## 📊 Результати діагностики")
|
636 |
+
|
637 |
+
with gr.Row():
|
638 |
+
with gr.Column(scale=2):
|
639 |
+
main_result = gr.Markdown()
|
640 |
+
detailed_analysis = gr.Markdown()
|
641 |
+
|
642 |
+
with gr.Column(scale=1):
|
643 |
+
recommendations = gr.Markdown()
|
644 |
+
|
645 |
+
# Візуалізації
|
646 |
+
if PLOTLY_AVAILABLE:
|
647 |
+
with gr.Row():
|
648 |
+
metrics_plot = gr.Plot(label="📈 Метрики процесу")
|
649 |
+
agent_activity_plot = gr.Plot(label="🤖 Активність агентів")
|
650 |
+
else:
|
651 |
+
metrics_plot = gr.Plot(visible=False)
|
652 |
+
agent_activity_plot = gr.Plot(visible=False)
|
653 |
+
|
654 |
+
# Логи
|
655 |
+
with gr.Row():
|
656 |
+
with gr.Column():
|
657 |
+
gr.Markdown("### 💬 Сирий вивід системи")
|
658 |
+
conversation_logs = gr.Textbox(
|
659 |
+
label="Raw logs",
|
660 |
+
lines=15,
|
661 |
+
elem_classes=["conversation-log"],
|
662 |
+
interactive=False,
|
663 |
+
show_copy_button=True
|
664 |
+
)
|
665 |
+
|
666 |
+
with gr.Column():
|
667 |
+
gr.Markdown("### 🗣️ Структуровані розмови агентів")
|
668 |
+
structured_conversations = gr.Markdown(
|
669 |
+
elem_classes=["structured-conv"]
|
670 |
+
)
|
671 |
+
|
672 |
+
# Підключаємо функцію діагностики
|
673 |
+
diagnose_btn.click(
|
674 |
+
interface.diagnose_with_full_logging,
|
675 |
+
inputs=[
|
676 |
+
case_name, patient_info, mode, budget,
|
677 |
+
max_iterations, model_name, expected_diagnosis,
|
678 |
+
enable_logging
|
679 |
+
],
|
680 |
+
outputs=[
|
681 |
+
main_result, detailed_analysis, recommendations,
|
682 |
+
metrics_plot, agent_activity_plot,
|
683 |
+
conversation_logs, structured_conversations
|
684 |
+
]
|
685 |
+
)
|
686 |
+
|
687 |
+
with gr.Tab("📊 Аналіз логів", elem_id="logs-tab"):
|
688 |
+
gr.Markdown("### 🔍 Аналіз збережених діагностичних сесій")
|
689 |
+
|
690 |
+
with gr.Row():
|
691 |
+
case_id_input = gr.Textbox(
|
692 |
+
label="🆔 ID сесії",
|
693 |
+
placeholder="Введіть ID сесії для аналізу"
|
694 |
+
)
|
695 |
+
|
696 |
+
analyze_btn = gr.Button("📋 Аналізувати")
|
697 |
+
|
698 |
+
session_details = gr.Markdown()
|
699 |
+
|
700 |
+
# Функція аналізу
|
701 |
+
analyze_btn.click(
|
702 |
+
interface.get_session_details,
|
703 |
+
inputs=[case_id_input],
|
704 |
+
outputs=[session_details]
|
705 |
+
)
|
706 |
+
|
707 |
+
gr.Markdown("### 📁 Експорт даних")
|
708 |
+
|
709 |
+
with gr.Row():
|
710 |
+
export_format = gr.Radio(
|
711 |
+
choices=["JSON", "HTML", "CSV"],
|
712 |
+
label="Формат експорту",
|
713 |
+
value="HTML"
|
714 |
+
)
|
715 |
+
|
716 |
+
export_btn = gr.Button("📤 Експортувати")
|
717 |
+
|
718 |
+
export_status = gr.Markdown()
|
719 |
+
|
720 |
+
return demo
|
721 |
+
|
722 |
+
if __name__ == "__main__":
|
723 |
+
print("🚀 Запуск MAI-DX Enhanced Platform with Full Agent Conversation Logging...")
|
724 |
+
demo = create_updated_gradio_interface()
|
725 |
+
demo.launch(
|
726 |
+
server_name="0.0.0.0",
|
727 |
+
server_port=7860,
|
728 |
+
share=True,
|
729 |
+
debug=False
|
730 |
+
)
|
utils/analyze_logs.py
ADDED
@@ -0,0 +1,295 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""
|
3 |
+
Скрипт для аналізу збережених логів MAI-DX
|
4 |
+
"""
|
5 |
+
|
6 |
+
import os
|
7 |
+
import json
|
8 |
+
import pandas as pd
|
9 |
+
from datetime import datetime
|
10 |
+
from collections import defaultdict
|
11 |
+
import argparse
|
12 |
+
|
13 |
+
def load_all_sessions(log_dir="mai_dx_logs"):
|
14 |
+
"""Завантажити всі сесії з директорії логів"""
|
15 |
+
sessions = []
|
16 |
+
|
17 |
+
if not os.path.exists(log_dir):
|
18 |
+
print(f"❌ Директорія {log_dir} не існує")
|
19 |
+
return sessions
|
20 |
+
|
21 |
+
for filename in os.listdir(log_dir):
|
22 |
+
if filename.endswith('.json') and not filename.endswith('_analytics.json'):
|
23 |
+
filepath = os.path.join(log_dir, filename)
|
24 |
+
try:
|
25 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
26 |
+
session = json.load(f)
|
27 |
+
sessions.append(session)
|
28 |
+
except Exception as e:
|
29 |
+
print(f"⚠️ Помилка читання {filename}: {e}")
|
30 |
+
|
31 |
+
return sessions
|
32 |
+
|
33 |
+
def analyze_session(session):
|
34 |
+
"""Детальний аналіз однієї сесії"""
|
35 |
+
print(f"\n{'='*60}")
|
36 |
+
print(f"📋 Аналіз сесії: {session['case_id']}")
|
37 |
+
print(f"{'='*60}")
|
38 |
+
|
39 |
+
print(f"\n📊 Основна інформація:")
|
40 |
+
print(f" - Назва випадку: {session['case_name']}")
|
41 |
+
print(f" - Час: {session['timestamp']}")
|
42 |
+
print(f" - Режим: {session['mode']}")
|
43 |
+
print(f" - Статус: {session['status']}")
|
44 |
+
|
45 |
+
print(f"\n💊 Діагноз:")
|
46 |
+
print(f" - Фінальний діагноз: {session['diagnosis']}")
|
47 |
+
print(f" - Впевненість: {session['confidence']}/5.0")
|
48 |
+
print(f" - Обґрунтування: {session['reasoning'][:100]}...")
|
49 |
+
|
50 |
+
print(f"\n💰 Економіка:")
|
51 |
+
print(f" - Витрачено: ${session['cost']}")
|
52 |
+
print(f" - Бюджет: ${session['budget']}")
|
53 |
+
print(f" - Ефективність: {((session['budget'] - session['cost']) / session['budget'] * 100):.1f}%")
|
54 |
+
|
55 |
+
print(f"\n⏱️ Продуктивність:")
|
56 |
+
print(f" - Тривалість: {session['duration']:.1f} секунд")
|
57 |
+
print(f" - Ітерацій: {session['iterations']}")
|
58 |
+
|
59 |
+
# Аналіз розмов
|
60 |
+
conversations = session.get('conversations', [])
|
61 |
+
if conversations:
|
62 |
+
print(f"\n💬 Аналіз розмов:")
|
63 |
+
print(f" - Всього раундів: {len(conversations)}")
|
64 |
+
|
65 |
+
# Підрахунок активності агентів
|
66 |
+
agent_activity = defaultdict(int)
|
67 |
+
message_types = defaultdict(int)
|
68 |
+
total_messages = 0
|
69 |
+
|
70 |
+
for conv in conversations:
|
71 |
+
for msg in conv.get('messages', []):
|
72 |
+
agent_activity[msg['agent_name']] += 1
|
73 |
+
message_types[msg['message_type']] += 1
|
74 |
+
total_messages += 1
|
75 |
+
|
76 |
+
print(f" - Всього повідомлень: {total_messages}")
|
77 |
+
|
78 |
+
print(f"\n 🤖 Активність агентів:")
|
79 |
+
for agent, count in sorted(agent_activity.items(), key=lambda x: x[1], reverse=True):
|
80 |
+
print(f" - {agent}: {count} повідомлень ({count/total_messages*100:.1f}%)")
|
81 |
+
|
82 |
+
print(f"\n 📝 Типи повідомлень:")
|
83 |
+
for msg_type, count in sorted(message_types.items(), key=lambda x: x[1], reverse=True):
|
84 |
+
print(f" - {msg_type}: {count} ({count/total_messages*100:.1f}%)")
|
85 |
+
|
86 |
+
# Деталі раундів
|
87 |
+
print(f"\n 🔄 Деталі раундів:")
|
88 |
+
for i, conv in enumerate(conversations):
|
89 |
+
msgs = len(conv.get('messages', []))
|
90 |
+
cost = conv.get('cost_incurred', 0)
|
91 |
+
decision = conv.get('decision', 'N/A')
|
92 |
+
print(f" Раунд {i+1}: {msgs} повідомлень, ${cost:.2f}, Рішення: {decision}")
|
93 |
+
|
94 |
+
def generate_summary_report(sessions, output_file="mai_dx_summary_report.html"):
|
95 |
+
"""Генерація зведеного HTML звіту по всіх сесіях"""
|
96 |
+
if not sessions:
|
97 |
+
print("❌ Немає сесій для аналізу")
|
98 |
+
return
|
99 |
+
|
100 |
+
html_content = """
|
101 |
+
<!DOCTYPE html>
|
102 |
+
<html>
|
103 |
+
<head>
|
104 |
+
<meta charset="UTF-8">
|
105 |
+
<title>MAI-DX Summary Report</title>
|
106 |
+
<style>
|
107 |
+
body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
|
108 |
+
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
109 |
+
h1 { color: #333; text-align: center; }
|
110 |
+
.summary { background: #e8f4f8; padding: 20px; border-radius: 8px; margin: 20px 0; }
|
111 |
+
.metric { display: inline-block; margin: 10px 20px; }
|
112 |
+
.metric-value { font-size: 24px; font-weight: bold; color: #007bff; }
|
113 |
+
.metric-label { color: #666; }
|
114 |
+
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
|
115 |
+
th { background: #007bff; color: white; padding: 12px; text-align: left; }
|
116 |
+
td { padding: 10px; border-bottom: 1px solid #ddd; }
|
117 |
+
tr:hover { background: #f5f5f5; }
|
118 |
+
.success { color: #28a745; }
|
119 |
+
.warning { color: #ffc107; }
|
120 |
+
.danger { color: #dc3545; }
|
121 |
+
</style>
|
122 |
+
</head>
|
123 |
+
<body>
|
124 |
+
<div class="container">
|
125 |
+
<h1>🏥 MAI-DX Summary Report</h1>
|
126 |
+
"""
|
127 |
+
|
128 |
+
# Загальна статистика
|
129 |
+
total_sessions = len(sessions)
|
130 |
+
avg_accuracy = sum(s['confidence'] for s in sessions) / total_sessions
|
131 |
+
avg_cost = sum(s['cost'] for s in sessions) / total_sessions
|
132 |
+
avg_duration = sum(s['duration'] for s in sessions) / total_sessions
|
133 |
+
success_rate = sum(1 for s in sessions if s['confidence'] >= 3.0) / total_sessions * 100
|
134 |
+
|
135 |
+
html_content += f"""
|
136 |
+
<div class="summary">
|
137 |
+
<h2>📊 Загальна статистика</h2>
|
138 |
+
<div class="metric">
|
139 |
+
<div class="metric-value">{total_sessions}</div>
|
140 |
+
<div class="metric-label">Всього сесій</div>
|
141 |
+
</div>
|
142 |
+
<div class="metric">
|
143 |
+
<div class="metric-value">{avg_accuracy:.2f}/5.0</div>
|
144 |
+
<div class="metric-label">Середня точність</div>
|
145 |
+
</div>
|
146 |
+
<div class="metric">
|
147 |
+
<div class="metric-value">${avg_cost:.2f}</div>
|
148 |
+
<div class="metric-label">Середня вартість</div>
|
149 |
+
</div>
|
150 |
+
<div class="metric">
|
151 |
+
<div class="metric-value">{avg_duration:.1f}с</div>
|
152 |
+
<div class="metric-label">Середній час</div>
|
153 |
+
</div>
|
154 |
+
<div class="metric">
|
155 |
+
<div class="metric-value">{success_rate:.1f}%</div>
|
156 |
+
<div class="metric-label">Успішність</div>
|
157 |
+
</div>
|
158 |
+
</div>
|
159 |
+
|
160 |
+
<h2>📋 Детальна таблиця сесій</h2>
|
161 |
+
<table>
|
162 |
+
<thead>
|
163 |
+
<tr>
|
164 |
+
<th>ID сесії</th>
|
165 |
+
<th>Час</th>
|
166 |
+
<th>Випадок</th>
|
167 |
+
<th>Діагноз</th>
|
168 |
+
<th>Точність</th>
|
169 |
+
<th>Вартість</th>
|
170 |
+
<th>Час</th>
|
171 |
+
<th>Статус</th>
|
172 |
+
</tr>
|
173 |
+
</thead>
|
174 |
+
<tbody>
|
175 |
+
"""
|
176 |
+
|
177 |
+
# Сортуємо сесії за часом
|
178 |
+
sorted_sessions = sorted(sessions, key=lambda x: x['timestamp'], reverse=True)
|
179 |
+
|
180 |
+
for session in sorted_sessions:
|
181 |
+
status_class = 'success' if session['confidence'] >= 3.0 else 'warning'
|
182 |
+
html_content += f"""
|
183 |
+
<tr>
|
184 |
+
<td>{session['case_id']}</td>
|
185 |
+
<td>{session['timestamp'][:19]}</td>
|
186 |
+
<td>{session['case_name']}</td>
|
187 |
+
<td>{session['diagnosis']}</td>
|
188 |
+
<td class="{status_class}">{session['confidence']:.1f}/5.0</td>
|
189 |
+
<td>${session['cost']:.2f}</td>
|
190 |
+
<td>{session['duration']:.1f}с</td>
|
191 |
+
<td>{session['status']}</td>
|
192 |
+
</tr>
|
193 |
+
"""
|
194 |
+
|
195 |
+
html_content += """
|
196 |
+
</tbody>
|
197 |
+
</table>
|
198 |
+
|
199 |
+
<p style="text-align: center; color: #666; margin-top: 30px;">
|
200 |
+
Згенеровано: """ + datetime.now().strftime("%Y-%m-%d %H:%M:%S") + """
|
201 |
+
</p>
|
202 |
+
</div>
|
203 |
+
</body>
|
204 |
+
</html>
|
205 |
+
"""
|
206 |
+
|
207 |
+
with open(output_file, 'w', encoding='utf-8') as f:
|
208 |
+
f.write(html_content)
|
209 |
+
|
210 |
+
print(f"\n✅ Зведений звіт збережено: {output_file}")
|
211 |
+
|
212 |
+
def export_to_csv(sessions, output_file="mai_dx_sessions.csv"):
|
213 |
+
"""Експорт сесій в CSV"""
|
214 |
+
if not sessions:
|
215 |
+
print("❌ Немає сесій для експорту")
|
216 |
+
return
|
217 |
+
|
218 |
+
# Створюємо DataFrame
|
219 |
+
data = []
|
220 |
+
for session in sessions:
|
221 |
+
# Підраховуємо агентів та повідомлення
|
222 |
+
total_messages = 0
|
223 |
+
unique_agents = set()
|
224 |
+
|
225 |
+
for conv in session.get('conversations', []):
|
226 |
+
for msg in conv.get('messages', []):
|
227 |
+
total_messages += 1
|
228 |
+
unique_agents.add(msg['agent_name'])
|
229 |
+
|
230 |
+
data.append({
|
231 |
+
'case_id': session['case_id'],
|
232 |
+
'timestamp': session['timestamp'],
|
233 |
+
'case_name': session['case_name'],
|
234 |
+
'mode': session['mode'],
|
235 |
+
'diagnosis': session['diagnosis'],
|
236 |
+
'confidence': session['confidence'],
|
237 |
+
'cost': session['cost'],
|
238 |
+
'budget': session['budget'],
|
239 |
+
'duration': session['duration'],
|
240 |
+
'iterations': session['iterations'],
|
241 |
+
'status': session['status'],
|
242 |
+
'total_messages': total_messages,
|
243 |
+
'unique_agents': len(unique_agents)
|
244 |
+
})
|
245 |
+
|
246 |
+
df = pd.DataFrame(data)
|
247 |
+
df.to_csv(output_file, index=False, encoding='utf-8')
|
248 |
+
|
249 |
+
print(f"✅ Дані експортовано в CSV: {output_file}")
|
250 |
+
print(f" Всього записів: {len(df)}")
|
251 |
+
|
252 |
+
def main():
|
253 |
+
parser = argparse.ArgumentParser(description='Аналіз логів MAI-DX')
|
254 |
+
parser.add_argument('--dir', default='mai_dx_logs', help='Директорія з логами')
|
255 |
+
parser.add_argument('--case', help='ID конкретної сесії для детального аналізу')
|
256 |
+
parser.add_argument('--export-csv', action='store_true', help='Експортувати в CSV')
|
257 |
+
parser.add_argument('--summary', action='store_true', help='Створити зведений HTML звіт')
|
258 |
+
|
259 |
+
args = parser.parse_args()
|
260 |
+
|
261 |
+
print("🔬 Аналіз логів MAI-DX")
|
262 |
+
print("=" * 60)
|
263 |
+
|
264 |
+
# Завантажуємо сесії
|
265 |
+
sessions = load_all_sessions(args.dir)
|
266 |
+
print(f"📁 Знайдено {len(sessions)} сесій в {args.dir}")
|
267 |
+
|
268 |
+
if args.case:
|
269 |
+
# Аналіз конкретної сесії
|
270 |
+
session = next((s for s in sessions if s['case_id'] == args.case), None)
|
271 |
+
if session:
|
272 |
+
analyze_session(session)
|
273 |
+
else:
|
274 |
+
print(f"❌ Сесія {args.case} не знайдена")
|
275 |
+
elif args.export_csv:
|
276 |
+
# Експорт в CSV
|
277 |
+
export_to_csv(sessions)
|
278 |
+
elif args.summary:
|
279 |
+
# Створення зведеного звіту
|
280 |
+
generate_summary_report(sessions)
|
281 |
+
else:
|
282 |
+
# Загальна статистика
|
283 |
+
if sessions:
|
284 |
+
print(f"\n📊 Загальна статистика:")
|
285 |
+
print(f" - Середня точність: {sum(s['confidence'] for s in sessions) / len(sessions):.2f}/5.0")
|
286 |
+
print(f" - Середня вартість: ${sum(s['cost'] for s in sessions) / len(sessions):.2f}")
|
287 |
+
print(f" - Середній час: {sum(s['duration'] for s in sessions) / len(sessions):.1f} секунд")
|
288 |
+
|
289 |
+
print(f"\n💡 Для детального аналізу використовуйте:")
|
290 |
+
print(f" python {__file__} --case <case_id>")
|
291 |
+
print(f" python {__file__} --export-csv")
|
292 |
+
print(f" python {__file__} --summary")
|
293 |
+
|
294 |
+
if __name__ == "__main__":
|
295 |
+
main()
|
utils/test_logging.py
ADDED
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""
|
3 |
+
Тестовий скрипт для перевірки що логування працює коректно
|
4 |
+
"""
|
5 |
+
|
6 |
+
import sys
|
7 |
+
import os
|
8 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
9 |
+
import json
|
10 |
+
from enhanced_mai_dx_logger import MAIDxConversationLogger
|
11 |
+
|
12 |
+
def test_basic_logging():
|
13 |
+
"""Тест базового логування"""
|
14 |
+
print("🧪 Тест 1: Базове логування")
|
15 |
+
|
16 |
+
logger = MAIDxConversationLogger("test_logs")
|
17 |
+
|
18 |
+
# Створюємо тестову сесію
|
19 |
+
case_id = logger.start_session(
|
20 |
+
case_name="Тестовий випадок",
|
21 |
+
patient_info="Тестова інформація",
|
22 |
+
mode="test",
|
23 |
+
budget=1000
|
24 |
+
)
|
25 |
+
|
26 |
+
print(f"✅ Створено сесію: {case_id}")
|
27 |
+
|
28 |
+
# Симулюємо захоплення виводу
|
29 |
+
def mock_orchestrator_run(*args, **kwargs):
|
30 |
+
print("Starting Diagnostic Loop 1/3")
|
31 |
+
print("2025-07-11 10:00:00 | INFO | mai_dx.main:_run_panel_deliberation:1304 - 🧠 Dr. Hypothesis analyzing differential diagnosis...")
|
32 |
+
print("╭─── Agent Name Dr. Hypothesis [Max Loops: 1] ───╮")
|
33 |
+
print("│ Analyzing patient symptoms... │")
|
34 |
+
print("│ High probability of sepsis │")
|
35 |
+
print("╰────────────────────────────────────────────────╯")
|
36 |
+
print("2025-07-11 10:00:05 | INFO | mai_dx.main:run:1689 - ⚕️ Panel decision: DIAGNOSE -> Sepsis")
|
37 |
+
|
38 |
+
# Симулюємо результат
|
39 |
+
class MockResult:
|
40 |
+
final_diagnosis = "Sepsis"
|
41 |
+
accuracy_score = 4.0
|
42 |
+
total_cost = 500.0
|
43 |
+
|
44 |
+
return MockResult()
|
45 |
+
|
46 |
+
# Виконуємо з захопленням
|
47 |
+
result = logger.capture_orchestrator_output(
|
48 |
+
case_id=case_id,
|
49 |
+
orchestrator_func=mock_orchestrator_run
|
50 |
+
)
|
51 |
+
|
52 |
+
print(f"✅ Діагноз: {result.final_diagnosis}")
|
53 |
+
|
54 |
+
# Завершуємо сесію
|
55 |
+
saved_id = logger.end_session(
|
56 |
+
case_id=case_id,
|
57 |
+
final_diagnosis=result.final_diagnosis,
|
58 |
+
confidence=result.accuracy_score,
|
59 |
+
cost=result.total_cost,
|
60 |
+
reasoning="Test reasoning"
|
61 |
+
)
|
62 |
+
|
63 |
+
print(f"✅ Збережено: {saved_id}")
|
64 |
+
|
65 |
+
# Перевіряємо що файли створені
|
66 |
+
json_file = f"test_logs/{case_id}.json"
|
67 |
+
if os.path.exists(json_file):
|
68 |
+
with open(json_file, 'r') as f:
|
69 |
+
data = json.load(f)
|
70 |
+
print(f"✅ JSON файл містить {len(data['conversations'])} розмов")
|
71 |
+
|
72 |
+
if data['conversations']:
|
73 |
+
conv = data['conversations'][0]
|
74 |
+
print(f" - Раунд {conv['round_number']}: {len(conv['messages'])} повідомлень")
|
75 |
+
else:
|
76 |
+
print(f"❌ JSON файл не створено!")
|
77 |
+
|
78 |
+
print("-" * 60)
|
79 |
+
|
80 |
+
def test_conversation_parsing():
|
81 |
+
"""Тест парсингу розмов"""
|
82 |
+
print("🧪 Тест 2: Парсинг складних розмов")
|
83 |
+
|
84 |
+
logger = MAIDxConversationLogger("test_logs")
|
85 |
+
|
86 |
+
case_id = logger.start_session(
|
87 |
+
case_name="Складний випадок",
|
88 |
+
patient_info="Множинні симптоми",
|
89 |
+
mode="budgeted",
|
90 |
+
budget=5000
|
91 |
+
)
|
92 |
+
|
93 |
+
# Симулюємо складніший вивід
|
94 |
+
def complex_orchestrator_run(*args, **kwargs):
|
95 |
+
output = """
|
96 |
+
2025-07-11 10:00:00 | INFO | mai_dx.main:run:1679 - --- Starting Diagnostic Loop 1/3 ---
|
97 |
+
2025-07-11 10:00:00 | INFO | mai_dx.main:run:1682 - Current cost: $300 | Remaining budget: $4,700
|
98 |
+
2025-07-11 10:00:01 | INFO | mai_dx.main:_run_panel_deliberation:1304 - 🧠 Dr. Hypothesis analyzing differential diagnosis...
|
99 |
+
╭─────────────────────────────── Agent Name Dr. Hypothesis [Max Loops: 1 ] ───────────────────────────────╮
|
100 |
+
│ Structured Output - Attempting Function Call Execution [10:00:02] │
|
101 |
+
│ │
|
102 |
+
│ Top differential diagnoses: │
|
103 |
+
│ 1. Acute Myocardial Infarction (85%) │
|
104 |
+
│ 2. Pulmonary Embolism (10%) │
|
105 |
+
│ 3. Aortic Dissection (5%) │
|
106 |
+
╰──────────────────────────────────────────────────────────────��──────────────────────────────────────────╯
|
107 |
+
|
108 |
+
2025-07-11 10:00:05 | INFO | mai_dx.main:_run_panel_deliberation:1320 - 🔬 Dr. Test-Chooser selecting optimal tests...
|
109 |
+
╭────────────────────────────── Agent Name Dr. Test-Chooser [Max Loops: 1 ] ──────────────────────────────╮
|
110 |
+
│ Recommending tests: │
|
111 |
+
│ - ECG (immediate) │
|
112 |
+
│ - Troponin levels │
|
113 |
+
│ - Chest X-ray │
|
114 |
+
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
115 |
+
|
116 |
+
2025-07-11 10:00:10 | INFO | mai_dx.main:run:1689 - ⚕️ Panel decision: TEST -> ECG, Troponin, CXR
|
117 |
+
2025-07-11 10:00:10 | INFO | mai_dx.main:run:1692 - 💭 Medical reasoning: High suspicion for cardiac event
|
118 |
+
"""
|
119 |
+
print(output)
|
120 |
+
|
121 |
+
class MockResult:
|
122 |
+
final_diagnosis = "Acute Myocardial Infarction"
|
123 |
+
accuracy_score = 4.5
|
124 |
+
total_cost = 850.0
|
125 |
+
|
126 |
+
return MockResult()
|
127 |
+
|
128 |
+
result = logger.capture_orchestrator_output(
|
129 |
+
case_id=case_id,
|
130 |
+
orchestrator_func=complex_orchestrator_run
|
131 |
+
)
|
132 |
+
|
133 |
+
# Генеруємо HTML
|
134 |
+
html_path = logger.export_conversation_html(case_id)
|
135 |
+
print(f"✅ HTML звіт: {html_path}")
|
136 |
+
|
137 |
+
# Завершуємо
|
138 |
+
logger.end_session(
|
139 |
+
case_id=case_id,
|
140 |
+
final_diagnosis=result.final_diagnosis,
|
141 |
+
confidence=result.accuracy_score,
|
142 |
+
cost=result.total_cost
|
143 |
+
)
|
144 |
+
|
145 |
+
print("-" * 60)
|
146 |
+
|
147 |
+
def test_error_handling():
|
148 |
+
"""Тест обробки помилок"""
|
149 |
+
print("🧪 Тест 3: Обробка помилок")
|
150 |
+
|
151 |
+
logger = MAIDxConversationLogger("test_logs")
|
152 |
+
|
153 |
+
# Спроба завершити неіснуючу сесію
|
154 |
+
result = logger.end_session("non_existent_id")
|
155 |
+
print(f"✅ Обробка неіснуючої сесії: {result}")
|
156 |
+
|
157 |
+
# Спроба отримати розмови неіснуючої сесії
|
158 |
+
conversations = logger.get_session_conversations("non_existent_id")
|
159 |
+
print(f"✅ Розмови неіснуючої сесії: {len(conversations)} елементів")
|
160 |
+
|
161 |
+
print("-" * 60)
|
162 |
+
|
163 |
+
def main():
|
164 |
+
"""Запуск всіх тестів"""
|
165 |
+
print("=" * 60)
|
166 |
+
print("🔬 Тестування системи логування MAI-DX")
|
167 |
+
print("=" * 60)
|
168 |
+
|
169 |
+
# Створюємо тестову директорію
|
170 |
+
os.makedirs("test_logs", exist_ok=True)
|
171 |
+
|
172 |
+
try:
|
173 |
+
test_basic_logging()
|
174 |
+
test_conversation_parsing()
|
175 |
+
test_error_handling()
|
176 |
+
|
177 |
+
print("\n✅ Всі тести пройдено успішно!")
|
178 |
+
print("📁 Перевірте директорію 'test_logs' для результатів")
|
179 |
+
|
180 |
+
except Exception as e:
|
181 |
+
print(f"\n❌ Помилка під час тестування: {e}")
|
182 |
+
import traceback
|
183 |
+
traceback.print_exc()
|
184 |
+
|
185 |
+
if __name__ == "__main__":
|
186 |
+
main()
|
utils/view_conversation.py
ADDED
@@ -0,0 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""
|
3 |
+
Скрипт для швидкого перегляду розмов MAI-DX сесії в терміналі
|
4 |
+
"""
|
5 |
+
|
6 |
+
import os
|
7 |
+
import json
|
8 |
+
import sys
|
9 |
+
import argparse
|
10 |
+
from datetime import datetime
|
11 |
+
from rich.console import Console
|
12 |
+
from rich.panel import Panel
|
13 |
+
from rich.table import Table
|
14 |
+
from rich.syntax import Syntax
|
15 |
+
from rich.markdown import Markdown
|
16 |
+
|
17 |
+
# Спробуємо імпортувати Rich, якщо немає - використаємо простий вивід
|
18 |
+
try:
|
19 |
+
from rich.console import Console
|
20 |
+
from rich.panel import Panel
|
21 |
+
from rich.table import Table
|
22 |
+
from rich.syntax import Syntax
|
23 |
+
from rich.markdown import Markdown
|
24 |
+
RICH_AVAILABLE = True
|
25 |
+
console = Console()
|
26 |
+
except ImportError:
|
27 |
+
RICH_AVAILABLE = False
|
28 |
+
print("⚠️ Rich не встановлено. Для кращого відображення: pip install rich")
|
29 |
+
|
30 |
+
def load_session(case_id, log_dir="mai_dx_logs"):
|
31 |
+
"""Завантажити конкретну сесію"""
|
32 |
+
json_path = os.path.join(log_dir, f"{case_id}.json")
|
33 |
+
|
34 |
+
if not os.path.exists(json_path):
|
35 |
+
# Спробуємо знайти файл з частковим збігом
|
36 |
+
for filename in os.listdir(log_dir):
|
37 |
+
if case_id in filename and filename.endswith('.json'):
|
38 |
+
json_path = os.path.join(log_dir, filename)
|
39 |
+
break
|
40 |
+
else:
|
41 |
+
return None
|
42 |
+
|
43 |
+
try:
|
44 |
+
with open(json_path, 'r', encoding='utf-8') as f:
|
45 |
+
return json.load(f)
|
46 |
+
except Exception as e:
|
47 |
+
print(f"❌ Помилка читання файлу: {e}")
|
48 |
+
return None
|
49 |
+
|
50 |
+
def display_session_rich(session):
|
51 |
+
"""Відображення сесії з Rich форматуванням"""
|
52 |
+
# Заголовок
|
53 |
+
console.print(Panel.fit(
|
54 |
+
f"[bold blue]MAI-DX Diagnostic Session[/bold blue]\n"
|
55 |
+
f"[dim]{session['case_id']}[/dim]",
|
56 |
+
border_style="blue"
|
57 |
+
))
|
58 |
+
|
59 |
+
# Основна інформація
|
60 |
+
info_table = Table(show_header=False, box=None, padding=(0, 2))
|
61 |
+
info_table.add_column(style="bold cyan")
|
62 |
+
info_table.add_column()
|
63 |
+
|
64 |
+
info_table.add_row("📋 Випадок:", session['case_name'])
|
65 |
+
info_table.add_row("🕐 Час:", session['timestamp'])
|
66 |
+
info_table.add_row("🔧 Режим:", session['mode'])
|
67 |
+
info_table.add_row("💊 Діагноз:", f"[bold yellow]{session['diagnosis']}[/bold yellow]")
|
68 |
+
info_table.add_row("📊 Точність:", f"{session['confidence']}/5.0 {'✅' if session['confidence'] >= 3.0 else '⚠️'}")
|
69 |
+
info_table.add_row("💰 Вартість:", f"${session['cost']} (бюджет: ${session['budget']})")
|
70 |
+
info_table.add_row("⏱️ Тривалість:", f"{session['duration']:.1f} секунд")
|
71 |
+
|
72 |
+
console.print(info_table)
|
73 |
+
console.print()
|
74 |
+
|
75 |
+
# Розмови
|
76 |
+
conversations = session.get('conversations', [])
|
77 |
+
if not conversations:
|
78 |
+
console.print("[yellow]⚠️ Розмови не знайдено[/yellow]")
|
79 |
+
return
|
80 |
+
|
81 |
+
console.print(f"[bold]💬 Розмови ({len(conversations)} раундів):[/bold]\n")
|
82 |
+
|
83 |
+
for i, conv in enumerate(conversations):
|
84 |
+
# Заголовок раунду
|
85 |
+
console.print(Panel(
|
86 |
+
f"[bold]Раунд {conv['round_number']}[/bold]\n"
|
87 |
+
f"Час: {conv.get('start_time', 'N/A')} - {conv.get('end_time', 'N/A')}\n"
|
88 |
+
f"Повідомлень: {len(conv.get('messages', []))}"
|
89 |
+
+ (f"\nРішення: [yellow]{conv['decision']}[/yellow]" if conv.get('decision') else "")
|
90 |
+
+ (f"\nВартість: ${conv['cost_incurred']}" if conv.get('cost_incurred') else ""),
|
91 |
+
title=f"[cyan]Раунд {i+1}[/cyan]",
|
92 |
+
border_style="cyan"
|
93 |
+
))
|
94 |
+
|
95 |
+
# Повідомлення
|
96 |
+
for msg in conv.get('messages', []):
|
97 |
+
agent_color = {
|
98 |
+
'Dr. Hypothesis': 'blue',
|
99 |
+
'Dr. Test-Chooser': 'green',
|
100 |
+
'Dr. Challenger': 'red',
|
101 |
+
'Dr. Stewardship': 'yellow',
|
102 |
+
'Dr. Checklist': 'magenta',
|
103 |
+
'Consensus Coordinator': 'cyan',
|
104 |
+
'Judge': 'white',
|
105 |
+
'Gatekeeper': 'bright_black'
|
106 |
+
}.get(msg['agent_name'], 'white')
|
107 |
+
|
108 |
+
msg_type_icon = {
|
109 |
+
'reasoning': '🤔',
|
110 |
+
'decision': '💡',
|
111 |
+
'input': '❓',
|
112 |
+
'output': '📊',
|
113 |
+
'log': '📝'
|
114 |
+
}.get(msg['message_type'], '💬')
|
115 |
+
|
116 |
+
console.print(
|
117 |
+
f"\n[bold {agent_color}]{msg_type_icon} {msg['agent_name']}[/bold {agent_color}] "
|
118 |
+
f"[dim]({msg['message_type']})[/dim]"
|
119 |
+
)
|
120 |
+
|
121 |
+
# Обмежуємо довжину контенту для кращого відображення
|
122 |
+
content = msg['content']
|
123 |
+
if len(content) > 500:
|
124 |
+
content = content[:500] + "..."
|
125 |
+
|
126 |
+
console.print(Panel(
|
127 |
+
content,
|
128 |
+
border_style=agent_color,
|
129 |
+
padding=(0, 1)
|
130 |
+
))
|
131 |
+
|
132 |
+
def display_session_simple(session):
|
133 |
+
"""Відображення сесії без Rich"""
|
134 |
+
print("="*60)
|
135 |
+
print(f"MAI-DX Diagnostic Session: {session['case_id']}")
|
136 |
+
print("="*60)
|
137 |
+
|
138 |
+
print(f"\n📋 Випадок: {session['case_name']}")
|
139 |
+
print(f"🕐 Час: {session['timestamp']}")
|
140 |
+
print(f"💊 Діагноз: {session['diagnosis']}")
|
141 |
+
print(f"📊 Точність: {session['confidence']}/5.0")
|
142 |
+
print(f"💰 Вартість: ${session['cost']} (бюджет: ${session['budget']})")
|
143 |
+
|
144 |
+
conversations = session.get('conversations', [])
|
145 |
+
if not conversations:
|
146 |
+
print("\n⚠️ Розмови не знайдено")
|
147 |
+
return
|
148 |
+
|
149 |
+
print(f"\n💬 Розмови ({len(conversations)} раундів):")
|
150 |
+
|
151 |
+
for i, conv in enumerate(conversations):
|
152 |
+
print(f"\n{'='*40}")
|
153 |
+
print(f"Раунд {conv['round_number']}")
|
154 |
+
print(f"{'='*40}")
|
155 |
+
|
156 |
+
if conv.get('decision'):
|
157 |
+
print(f"Рішення: {conv['decision']}")
|
158 |
+
|
159 |
+
for msg in conv.get('messages', []):
|
160 |
+
print(f"\n[{msg['message_type']}] {msg['agent_name']}:")
|
161 |
+
content = msg['content']
|
162 |
+
if len(content) > 300:
|
163 |
+
content = content[:300] + "..."
|
164 |
+
print(content)
|
165 |
+
print("-"*40)
|
166 |
+
|
167 |
+
def export_markdown(session, output_file=None):
|
168 |
+
"""Експорт розмов в Markdown"""
|
169 |
+
if not output_file:
|
170 |
+
output_file = f"{session['case_id']}_conversation.md"
|
171 |
+
|
172 |
+
md_content = f"""# MAI-DX Diagnostic Session: {session['case_id']}
|
173 |
+
|
174 |
+
## 📋 Інформація про сесію
|
175 |
+
|
176 |
+
- **Випадок**: {session['case_name']}
|
177 |
+
- **Час**: {session['timestamp']}
|
178 |
+
- **Режим**: {session['mode']}
|
179 |
+
- **Діагноз**: **{session['diagnosis']}**
|
180 |
+
- **Точність**: {session['confidence']}/5.0
|
181 |
+
- **Вартість**: ${session['cost']} (бюджет: ${session['budget']})
|
182 |
+
- **Тривалість**: {session['duration']:.1f} секунд
|
183 |
+
|
184 |
+
## 💬 Розмови
|
185 |
+
|
186 |
+
"""
|
187 |
+
|
188 |
+
for i, conv in enumerate(session.get('conversations', [])):
|
189 |
+
md_content += f"\n### Раунд {conv['round_number']}\n\n"
|
190 |
+
|
191 |
+
if conv.get('decision'):
|
192 |
+
md_content += f"**Рішення**: {conv['decision']}\n\n"
|
193 |
+
|
194 |
+
for msg in conv.get('messages', []):
|
195 |
+
md_content += f"\n#### {msg['agent_name']} [{msg['message_type']}]\n\n"
|
196 |
+
md_content += f"{msg['content']}\n\n"
|
197 |
+
md_content += "---\n"
|
198 |
+
|
199 |
+
with open(output_file, 'w', encoding='utf-8') as f:
|
200 |
+
f.write(md_content)
|
201 |
+
|
202 |
+
print(f"\n✅ Експортовано в Markdown: {output_file}")
|
203 |
+
|
204 |
+
def main():
|
205 |
+
parser = argparse.ArgumentParser(description='Перегляд розмов MAI-DX сесії')
|
206 |
+
parser.add_argument('case_id', help='ID сесії або його частина')
|
207 |
+
parser.add_argument('--dir', default='mai_dx_logs', help='Директорія з логами')
|
208 |
+
parser.add_argument('--simple', action='store_true', help='Простий вивід без Rich')
|
209 |
+
parser.add_argument('--export-md', action='store_true', help='Експортувати в Markdown')
|
210 |
+
parser.add_argument('--raw', action='store_true', help='Показати сирий текст')
|
211 |
+
|
212 |
+
args = parser.parse_args()
|
213 |
+
|
214 |
+
# Завантажуємо сесію
|
215 |
+
session = load_session(args.case_id, args.dir)
|
216 |
+
|
217 |
+
if not session:
|
218 |
+
print(f"❌ Сесія '{args.case_id}' не знайдена в {args.dir}")
|
219 |
+
|
220 |
+
# Показуємо доступні сесії
|
221 |
+
print("\n📁 Доступні сесії:")
|
222 |
+
for filename in sorted(os.listdir(args.dir)):
|
223 |
+
if filename.endswith('.json') and not filename.endswith('_analytics.json'):
|
224 |
+
print(f" - {filename[:-5]}")
|
225 |
+
sys.exit(1)
|
226 |
+
|
227 |
+
# Показуємо сирий текст якщо потрібно
|
228 |
+
if args.raw:
|
229 |
+
raw_file = os.path.join(args.dir, f"{session['case_id']}_raw.txt")
|
230 |
+
if os.path.exists(raw_file):
|
231 |
+
with open(raw_file, 'r', encoding='utf-8') as f:
|
232 |
+
print(f.read())
|
233 |
+
else:
|
234 |
+
print("❌ Файл з сирим текстом не знайдено")
|
235 |
+
return
|
236 |
+
|
237 |
+
# Експортуємо в Markdown якщо потрібно
|
238 |
+
if args.export_md:
|
239 |
+
export_markdown(session)
|
240 |
+
return
|
241 |
+
|
242 |
+
# Відображаємо сесію
|
243 |
+
if args.simple or not RICH_AVAILABLE:
|
244 |
+
display_session_simple(session)
|
245 |
+
else:
|
246 |
+
display_session_rich(session)
|
247 |
+
|
248 |
+
if __name__ == "__main__":
|
249 |
+
main()
|