Spaces:
Sleeping
Sleeping
Оновлено логіку захоплення та обробки виводу в MAIDxConversationLogger, покращено структуру класів для сесій діагностики. Внесено зміни в інтерфейс для коректного завершення сесій та збереження результатів. Виправлено помилки в обробці логів та оновлено документацію для відображення нових функцій.
Browse files- enhanced_mai_dx_logger.py +61 -402
- run_mai_dx_fixed.py +3 -3
- updated_mai_dx_interface.py +23 -37
enhanced_mai_dx_logger.py
CHANGED
@@ -14,34 +14,26 @@ 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
|
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
|
@@ -56,88 +48,43 @@ class DiagnosisSession:
|
|
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
|
74 |
-
"""Розпочати захоплення виводу"""
|
75 |
-
self.is_capturing = True
|
76 |
sys.stdout = self
|
77 |
-
|
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 |
-
|
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
|
109 |
-
|
110 |
-
|
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
|
138 |
-
"""Розпочати нову діагностичну сесію"""
|
139 |
case_id = f"case_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:6]}"
|
140 |
-
|
141 |
case_id=case_id,
|
142 |
timestamp=datetime.now().isoformat(),
|
143 |
case_name=case_name,
|
@@ -145,405 +92,117 @@ class MAIDxConversationLogger:
|
|
145 |
mode=mode,
|
146 |
budget=budget
|
147 |
)
|
148 |
-
self.sessions[case_id] = session
|
149 |
-
return case_id
|
150 |
|
151 |
-
def
|
152 |
-
|
153 |
-
|
154 |
-
raise ValueError(f"Сесія {case_id} не існує")
|
155 |
-
|
156 |
-
session = self.sessions[case_id]
|
157 |
|
158 |
-
|
159 |
-
|
|
|
|
|
|
|
|
|
|
|
160 |
|
161 |
-
|
162 |
-
|
163 |
-
|
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: Optional[AgentConversation] = None
|
185 |
current_agent: Optional[str] = None
|
186 |
buffer: List[str] = []
|
187 |
in_agent_output = False
|
188 |
round_number = 0
|
189 |
|
190 |
-
def finalize_round(round_obj: Optional[AgentConversation]):
|
191 |
-
"""Завершити поточний раунд, якщо він існує."""
|
192 |
-
if round_obj and not round_obj.end_time:
|
193 |
-
round_obj.end_time = datetime.now().isoformat()
|
194 |
-
|
195 |
for line in lines:
|
196 |
stripped_line = line.strip()
|
197 |
-
if not stripped_line
|
198 |
-
continue
|
199 |
|
200 |
-
# 1. Перевірка на початок нового раунду
|
201 |
if "Starting Diagnostic Loop" in line:
|
202 |
-
|
203 |
-
|
204 |
round_match = re.search(r'Starting Diagnostic Loop (\d+)/\d+', line)
|
205 |
round_number = int(round_match.group(1)) if round_match else round_number + 1
|
206 |
-
|
207 |
-
current_round = AgentConversation(
|
208 |
-
round_number=round_number,
|
209 |
-
start_time=datetime.now().isoformat()
|
210 |
-
)
|
211 |
session.conversations.append(current_round)
|
212 |
continue
|
213 |
|
214 |
-
# 2. Перевірка на початок блоку агента
|
215 |
if "Agent Name" in line and ("╭" in line or "┌" in line):
|
216 |
agent_match = re.search(r'Agent Name (.*?) \[', line)
|
217 |
if agent_match:
|
218 |
current_agent = agent_match.group(1).strip()
|
219 |
in_agent_output = True
|
220 |
-
buffer = []
|
221 |
continue
|
222 |
|
223 |
-
|
224 |
-
elif in_agent_output and ("╰" in line or "└" in line):
|
225 |
if buffer and current_agent and current_round:
|
226 |
self._add_agent_message(current_round, current_agent, '\n'.join(buffer))
|
227 |
-
|
228 |
-
# Скидаємо стан
|
229 |
-
in_agent_output = False
|
230 |
-
current_agent = None
|
231 |
-
buffer = []
|
232 |
continue
|
233 |
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
clean_line = re.sub(r'^[│|]\s*', '', line)
|
238 |
-
clean_line = re.sub(r'\s*[│|]\s*$', '', clean_line) # Видаляємо символ і в кінці
|
239 |
-
if clean_line: # Додаємо лише непусті рядки
|
240 |
-
buffer.append(clean_line)
|
241 |
continue
|
242 |
|
243 |
-
|
244 |
-
elif " | INFO " in line and current_round:
|
245 |
info_match = re.search(r'mai_dx\.main:([^:]+):.*? - (.*)', line)
|
246 |
if info_match:
|
247 |
-
action = info_match.group(1)
|
248 |
content = info_match.group(2).strip()
|
249 |
-
|
250 |
-
# Обробка ключових INFO-повідомлень
|
251 |
if "Panel decision:" in content:
|
252 |
decision_match = re.search(r'Panel decision: (\w+) -> (.*)', content)
|
253 |
-
if decision_match:
|
254 |
-
current_round.decision = f"{decision_match.group(1)}: {decision_match.group(2)}"
|
255 |
-
|
256 |
elif "Current cost:" in content:
|
257 |
cost_match = re.search(r'Current cost: \$(\d+(?:\.\d+)?)', content)
|
258 |
-
if cost_match:
|
259 |
-
current_round.cost_incurred = float(cost_match.group(1))
|
260 |
|
261 |
-
|
262 |
-
finalize_round(current_round)
|
263 |
|
264 |
-
def
|
265 |
-
|
266 |
-
|
267 |
-
'Dr. Hypothesis': ['Dr. Hypothesis', 'differential diagnosis'],
|
268 |
-
'Dr. Test-Chooser': ['Dr. Test-Chooser', 'selecting optimal tests'],
|
269 |
-
'Dr. Challenger': ['Dr. Challenger', 'challenging assumptions'],
|
270 |
-
'Dr. Stewardship': ['Dr. Stewardship', 'cost-effectiveness'],
|
271 |
-
'Dr. Checklist': ['Dr. Checklist', 'quality control'],
|
272 |
-
'Consensus Coordinator': ['Consensus Coordinator', 'synthesizing panel'],
|
273 |
-
'Judge': ['Judge', 'evaluation'],
|
274 |
-
'Gatekeeper': ['Gatekeeper']
|
275 |
-
}
|
276 |
-
|
277 |
-
for agent, patterns in agent_patterns.items():
|
278 |
-
for pattern in patterns:
|
279 |
-
if pattern in log_content:
|
280 |
-
return agent
|
281 |
-
return None
|
282 |
-
|
283 |
-
def _extract_agent_name(self, line: str) -> Optional[str]:
|
284 |
-
"""Витягти ім'я агента з рядка"""
|
285 |
-
agents = [
|
286 |
-
"Dr. Hypothesis", "Dr. Test-Chooser", "Dr. Challenger",
|
287 |
-
"Dr. Stewardship", "Dr. Checklist", "Consensus Coordinator",
|
288 |
-
"Gatekeeper", "Judge"
|
289 |
-
]
|
290 |
-
|
291 |
-
for agent in agents:
|
292 |
-
if agent in line:
|
293 |
-
return agent
|
294 |
-
return None
|
295 |
-
|
296 |
-
def _extract_round_number(self, line: str) -> int:
|
297 |
-
"""Витягти номер раунду"""
|
298 |
-
import re
|
299 |
-
match = re.search(r'(\d+)', line)
|
300 |
-
return int(match.group(1)) if match else 0
|
301 |
-
|
302 |
-
def _extract_tests(self, line: str) -> List[str]:
|
303 |
-
"""Витягти список тестів"""
|
304 |
-
# Тут потрібна більш складна логіка залежно від формату
|
305 |
-
return []
|
306 |
-
|
307 |
-
def _extract_questions(self, line: str) -> List[str]:
|
308 |
-
"""Витягти список питань"""
|
309 |
-
# Тут потрібна більш складна логіка залежно від формату
|
310 |
-
return []
|
311 |
-
|
312 |
-
def _extract_cost(self, line: str) -> Optional[float]:
|
313 |
-
"""Витягти вартість"""
|
314 |
-
import re
|
315 |
-
match = re.search(r'\$?(\d+(?:\.\d+)?)', line)
|
316 |
-
return float(match.group(1)) if match else None
|
317 |
-
|
318 |
-
def _add_agent_message(self, conversation: AgentConversation, agent_name: str, content: str, message_type: Optional[str] = None):
|
319 |
-
"""Додати повідомлення агента до розмови з надійним парсингом JSON."""
|
320 |
-
import re
|
321 |
-
import json
|
322 |
-
|
323 |
-
if not conversation or not content.strip():
|
324 |
-
return
|
325 |
|
326 |
-
formatted_content = ""
|
327 |
-
original_content = content
|
328 |
-
|
329 |
-
# Перевіряємо, чи це структурований вивід з викликом функції
|
330 |
if "Structured Output - Attempting Function Call Execution" in content:
|
331 |
message_type = "function_call"
|
332 |
-
|
333 |
func_name_match = re.search(r"name:\s*(\w+)", content)
|
334 |
-
func_name = func_name_match.group(1) if func_name_match else "
|
335 |
-
|
336 |
-
|
337 |
-
# і стандартним модулем 're'. Він шукає блок, що починається з "arguments: {"
|
338 |
-
# і закінчується на "}" перед "name:".
|
339 |
-
args_match = re.search(r"arguments:\s*(\{.*?\})\s*,\s*\"name\":", content, re.DOTALL)
|
340 |
-
# Альтернативний варіант, якщо "name" не знайдено
|
341 |
-
if not args_match:
|
342 |
-
args_match = re.search(r"arguments:\s*(\{.*\})", content, re.DOTALL)
|
343 |
-
|
344 |
-
|
345 |
-
formatted_content = f"🤖 **Дія:** Виклик функції `{func_name}`\n\n"
|
346 |
-
|
347 |
if args_match:
|
348 |
try:
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
if func_name in ['update_differential_diagnosis', 'UpdateDifferentialDiagnosisDifferentialDiagnoses']:
|
353 |
-
formatted_content += f"**Резюме:** {args_data.get('summary', 'N/A')}\n"
|
354 |
-
if 'differential_diagnoses' in args_data:
|
355 |
-
formatted_content += "**Основний діагноз:**\n"
|
356 |
-
for diag in args_data.get('differential_diagnoses', []):
|
357 |
-
prob = diag.get('probability', 0) * 100
|
358 |
-
formatted_content += f"- {diag.get('diagnosis')} ({prob:.0f}%)\n"
|
359 |
-
else: # Обробка іншого формату
|
360 |
-
prob = args_data.get('probability', 0) * 100
|
361 |
-
formatted_content += f"- {args_data.get('diagnosis')} ({prob:.0f}%)\n"
|
362 |
-
|
363 |
-
|
364 |
-
elif func_name == 'make_consensus_decision':
|
365 |
-
formatted_content += f"**Рішення:** {args_data.get('content', 'N/A')}\n"
|
366 |
-
formatted_content += f"**Обґрунтування:** {args_data.get('reasoning', 'N/A')}"
|
367 |
-
|
368 |
-
else:
|
369 |
-
formatted_content += "**Аргументи:**\n"
|
370 |
-
formatted_content += f"```json\n{json.dumps(args_data, indent=2, ensure_ascii=False)}\n```"
|
371 |
-
|
372 |
except json.JSONDecodeError:
|
373 |
-
formatted_content += "Помилка парсингу JSON
|
374 |
else:
|
375 |
-
formatted_content += "
|
376 |
-
|
377 |
-
else:
|
378 |
-
# Це звичайне текстове повідомлення
|
379 |
-
formatted_content = content.strip()
|
380 |
-
if not message_type:
|
381 |
-
message_type = self._determine_message_type(agent_name, formatted_content)
|
382 |
-
|
383 |
message = AgentMessage(
|
384 |
-
|
385 |
-
agent_name=agent_name,
|
386 |
-
message_type=message_type,
|
387 |
-
content=formatted_content.strip(),
|
388 |
-
metadata={'raw_content': original_content}
|
389 |
)
|
390 |
-
|
391 |
conversation.messages.append(message)
|
392 |
-
|
393 |
-
def _determine_message_type(self, agent_name: str, content: str) -> str:
|
394 |
-
"""Визначити тип повідомлення"""
|
395 |
-
content_lower = content.lower()
|
396 |
-
|
397 |
-
if "analyzing" in content_lower or "considering" in content_lower:
|
398 |
-
return "reasoning"
|
399 |
-
elif "recommend" in content_lower or "suggest" in content_lower:
|
400 |
-
return "decision"
|
401 |
-
elif "?" in content:
|
402 |
-
return "input"
|
403 |
-
else:
|
404 |
-
return "output"
|
405 |
-
|
406 |
-
def _save_buffered_conversation(self, session: DiagnosisSession, conversation: AgentConversation, buffer: List[str]):
|
407 |
-
"""Зберегти буферизовану розмову"""
|
408 |
-
if conversation and buffer:
|
409 |
-
conversation.end_time = datetime.now().isoformat()
|
410 |
-
# Можна додати додаткову обробку
|
411 |
-
|
412 |
-
# У файлі enhanced_mai_dx_logger.py
|
413 |
-
def end_session(self, case_id: str, **kwargs) -> Optional[DiagnosisSession]:
|
414 |
-
if case_id not in self.sessions:
|
415 |
-
return None
|
416 |
-
|
417 |
-
session = self.sessions[case_id]
|
418 |
-
|
419 |
-
# Оновлюємо фінальні дані
|
420 |
-
session.diagnosis = kwargs.get('final_diagnosis', session.diagnosis)
|
421 |
-
session.confidence = kwargs.get('confidence', session.confidence)
|
422 |
-
session.cost = kwargs.get('cost', session.cost)
|
423 |
-
session.reasoning = kwargs.get('reasoning', session.reasoning)
|
424 |
-
session.iterations = len(session.conversations)
|
425 |
-
session.status = "✅ Успішно" if session.confidence >= 3.0 else "⚠️ Потребує перегляду"
|
426 |
-
|
427 |
-
start_time = datetime.fromisoformat(session.timestamp)
|
428 |
-
session.duration = (datetime.now() - start_time).total_seconds()
|
429 |
-
|
430 |
-
# Зберігаємо у файл
|
431 |
-
self._save_session_to_file(session)
|
432 |
-
|
433 |
-
del self.sessions[case_id]
|
434 |
-
return session
|
435 |
|
436 |
def _save_session_to_file(self, session: DiagnosisSession):
|
437 |
-
"""Зберегти сесію у JSON файл з повною структурою"""
|
438 |
file_path = os.path.join(self.log_dir, f"{session.case_id}.json")
|
439 |
-
|
440 |
-
# Конвертуємо в словник з усіма вкладеними даними
|
441 |
-
session_dict = asdict(session)
|
442 |
-
|
443 |
with open(file_path, 'w', encoding='utf-8') as f:
|
444 |
-
json.dump(
|
445 |
|
446 |
-
# Додатково зберігаємо сирий вивід
|
447 |
raw_output_path = os.path.join(self.log_dir, f"{session.case_id}_raw.txt")
|
448 |
with open(raw_output_path, 'w', encoding='utf-8') as f:
|
449 |
f.write(session.raw_output)
|
450 |
|
451 |
-
def
|
452 |
-
|
453 |
-
|
454 |
-
|
455 |
-
|
456 |
-
|
457 |
-
if case_id in self.sessions:
|
458 |
-
return self.sessions[case_id].conversations
|
459 |
-
return []
|
460 |
-
|
461 |
-
with open(file_path, 'r', encoding='utf-8') as f:
|
462 |
-
data = json.load(f)
|
463 |
-
|
464 |
-
# Реконструюємо об'єкти з JSON
|
465 |
-
conversations = []
|
466 |
-
for conv_data in data.get('conversations', []):
|
467 |
-
messages = [AgentMessage(**msg) for msg in conv_data.get('messages', [])]
|
468 |
-
conv = AgentConversation(
|
469 |
-
round_number=conv_data['round_number'],
|
470 |
-
start_time=conv_data['start_time'],
|
471 |
-
end_time=conv_data.get('end_time'),
|
472 |
-
messages=messages,
|
473 |
-
decision=conv_data.get('decision'),
|
474 |
-
tests_ordered=conv_data.get('tests_ordered', []),
|
475 |
-
questions_asked=conv_data.get('questions_asked', []),
|
476 |
-
cost_incurred=conv_data.get('cost_incurred', 0.0)
|
477 |
-
)
|
478 |
-
conversations.append(conv)
|
479 |
-
|
480 |
-
return conversations
|
481 |
-
|
482 |
-
def export_conversation_html(self, case_id: str) -> str:
|
483 |
-
"""Експортувати розмови у читабельний HTML формат"""
|
484 |
-
conversations = self.get_session_conversations(case_id)
|
485 |
-
|
486 |
-
html_content = f"""
|
487 |
-
<!DOCTYPE html>
|
488 |
-
<html>
|
489 |
-
<head>
|
490 |
-
<meta charset="UTF-8">
|
491 |
-
<title>MAI-DX Conversation Log - {case_id}</title>
|
492 |
-
<style>
|
493 |
-
body {{ font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }}
|
494 |
-
.round {{ background: white; padding: 20px; margin: 20px 0; border-radius: 10px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }}
|
495 |
-
.agent-message {{ margin: 10px 0; padding: 10px; border-left: 4px solid #007bff; background: #f8f9fa; }}
|
496 |
-
.agent-name {{ font-weight: bold; color: #007bff; }}
|
497 |
-
.message-type {{ font-size: 0.9em; color: #666; }}
|
498 |
-
.decision {{ background: #d4edda; border-color: #28a745; }}
|
499 |
-
.tests {{ background: #fff3cd; border-color: #ffc107; }}
|
500 |
-
.cost {{ color: #dc3545; font-weight: bold; }}
|
501 |
-
.raw-output {{ background: #2d3748; color: #e2e8f0; padding: 15px; border-radius: 5px; font-family: 'Courier New', monospace; white-space: pre-wrap; }}
|
502 |
-
</style>
|
503 |
-
</head>
|
504 |
-
<body>
|
505 |
-
<h1>MAI-DX Diagnostic Session: {case_id}</h1>
|
506 |
-
"""
|
507 |
-
|
508 |
-
for conv in conversations:
|
509 |
-
html_content += f"""
|
510 |
-
<div class="round">
|
511 |
-
<h2>Round {conv.round_number}</h2>
|
512 |
-
<p><strong>Time:</strong> {conv.start_time} - {conv.end_time or 'In Progress'}</p>
|
513 |
-
"""
|
514 |
-
|
515 |
for msg in conv.messages:
|
516 |
-
|
517 |
-
|
518 |
-
css_class += " decision"
|
519 |
-
|
520 |
-
html_content += f"""
|
521 |
-
<div class="{css_class}">
|
522 |
-
<div class="agent-name">{msg.agent_name}</div>
|
523 |
-
<div class="message-type">[{msg.message_type}]</div>
|
524 |
-
<div class="content">{msg.content}</div>
|
525 |
-
</div>
|
526 |
-
"""
|
527 |
-
|
528 |
-
if conv.decision:
|
529 |
-
html_content += f'<div class="decision"><strong>Decision:</strong> {conv.decision}</div>'
|
530 |
-
|
531 |
-
if conv.tests_ordered:
|
532 |
-
html_content += f'<div class="tests"><strong>Tests:</strong> {", ".join(conv.tests_ordered)}</div>'
|
533 |
-
|
534 |
-
if conv.cost_incurred > 0:
|
535 |
-
html_content += f'<div class="cost">Cost: ${conv.cost_incurred:.2f}</div>'
|
536 |
-
|
537 |
-
html_content += "</div>"
|
538 |
-
|
539 |
-
html_content += """
|
540 |
-
</body>
|
541 |
-
</html>
|
542 |
-
"""
|
543 |
-
|
544 |
-
# Зберігаємо HTML файл
|
545 |
-
html_path = os.path.join(self.log_dir, f"{case_id}_conversation.html")
|
546 |
with open(html_path, 'w', encoding='utf-8') as f:
|
547 |
f.write(html_content)
|
548 |
-
|
549 |
return html_path
|
|
|
14 |
from datetime import datetime
|
15 |
from typing import List, Dict, Any, Optional, Callable
|
16 |
from dataclasses import dataclass, asdict, field
|
|
|
|
|
|
|
17 |
|
18 |
@dataclass
|
19 |
class AgentMessage:
|
|
|
20 |
timestamp: str
|
21 |
agent_name: str
|
22 |
+
message_type: str
|
23 |
content: str
|
24 |
metadata: Dict[str, Any] = field(default_factory=dict)
|
25 |
|
26 |
@dataclass
|
27 |
class AgentConversation:
|
|
|
28 |
round_number: int
|
29 |
start_time: str
|
30 |
end_time: Optional[str] = None
|
31 |
messages: List[AgentMessage] = field(default_factory=list)
|
32 |
decision: Optional[str] = None
|
|
|
|
|
33 |
cost_incurred: float = 0.0
|
34 |
|
35 |
@dataclass
|
36 |
class DiagnosisSession:
|
|
|
37 |
case_id: str
|
38 |
timestamp: str
|
39 |
case_name: str
|
|
|
48 |
status: str = "In Progress"
|
49 |
reasoning: str = "N/A"
|
50 |
conversations: List[AgentConversation] = field(default_factory=list)
|
51 |
+
raw_output: str = ""
|
52 |
|
53 |
class EnhancedOutputCapture:
|
|
|
54 |
def __init__(self):
|
55 |
self.captured_output = io.StringIO()
|
56 |
self.original_stdout = sys.stdout
|
|
|
|
|
|
|
|
|
|
|
|
|
57 |
|
58 |
+
def __enter__(self):
|
|
|
|
|
59 |
sys.stdout = self
|
60 |
+
return self
|
61 |
|
62 |
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
|
|
|
|
|
|
|
|
|
63 |
sys.stdout = self.original_stdout
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
|
65 |
def write(self, text):
|
66 |
+
self.captured_output.write(text)
|
67 |
+
self.original_stdout.write(text)
|
|
|
|
|
68 |
|
69 |
def flush(self):
|
|
|
|
|
|
|
70 |
self.original_stdout.flush()
|
71 |
|
72 |
+
def get_value(self):
|
73 |
+
return self._strip_ansi_codes(self.captured_output.getvalue())
|
74 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
75 |
@staticmethod
|
76 |
def _strip_ansi_codes(text):
|
|
|
77 |
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
78 |
return ansi_escape.sub('', text)
|
79 |
|
80 |
class MAIDxConversationLogger:
|
|
|
|
|
81 |
def __init__(self, log_dir: str = "mai_dx_logs"):
|
82 |
self.log_dir = log_dir
|
83 |
os.makedirs(self.log_dir, exist_ok=True)
|
|
|
|
|
84 |
|
85 |
+
def create_session(self, case_name: str, patient_info: str, mode: str, budget: int) -> DiagnosisSession:
|
|
|
86 |
case_id = f"case_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:6]}"
|
87 |
+
return DiagnosisSession(
|
88 |
case_id=case_id,
|
89 |
timestamp=datetime.now().isoformat(),
|
90 |
case_name=case_name,
|
|
|
92 |
mode=mode,
|
93 |
budget=budget
|
94 |
)
|
|
|
|
|
95 |
|
96 |
+
def finalize_and_save_session(self, session: DiagnosisSession, result: Any, raw_output: str, duration: float) -> DiagnosisSession:
|
97 |
+
session.raw_output = raw_output
|
98 |
+
self._parse_captured_output(session, raw_output)
|
|
|
|
|
|
|
99 |
|
100 |
+
session.diagnosis = getattr(result, 'final_diagnosis', 'N/A')
|
101 |
+
session.confidence = getattr(result, 'accuracy_score', 0.0)
|
102 |
+
session.cost = getattr(result, 'total_cost', 0.0)
|
103 |
+
session.reasoning = getattr(result, 'accuracy_reasoning', 'N/A')
|
104 |
+
session.iterations = len(session.conversations)
|
105 |
+
session.status = "✅ Успішно" if session.confidence >= 3.0 else "⚠️ Потребує перегляду"
|
106 |
+
session.duration = duration
|
107 |
|
108 |
+
self._save_session_to_file(session)
|
109 |
+
self.export_conversation_html(session)
|
110 |
+
return session
|
111 |
+
|
112 |
+
def _parse_captured_output(self, session: DiagnosisSession, captured_text: str):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
113 |
lines = captured_text.split('\n')
|
|
|
114 |
current_round: Optional[AgentConversation] = None
|
115 |
current_agent: Optional[str] = None
|
116 |
buffer: List[str] = []
|
117 |
in_agent_output = False
|
118 |
round_number = 0
|
119 |
|
|
|
|
|
|
|
|
|
|
|
120 |
for line in lines:
|
121 |
stripped_line = line.strip()
|
122 |
+
if not stripped_line: continue
|
|
|
123 |
|
|
|
124 |
if "Starting Diagnostic Loop" in line:
|
125 |
+
if current_round: current_round.end_time = datetime.now().isoformat()
|
|
|
126 |
round_match = re.search(r'Starting Diagnostic Loop (\d+)/\d+', line)
|
127 |
round_number = int(round_match.group(1)) if round_match else round_number + 1
|
128 |
+
current_round = AgentConversation(round_number=round_number, start_time=datetime.now().isoformat())
|
|
|
|
|
|
|
|
|
129 |
session.conversations.append(current_round)
|
130 |
continue
|
131 |
|
|
|
132 |
if "Agent Name" in line and ("╭" in line or "┌" in line):
|
133 |
agent_match = re.search(r'Agent Name (.*?) \[', line)
|
134 |
if agent_match:
|
135 |
current_agent = agent_match.group(1).strip()
|
136 |
in_agent_output = True
|
137 |
+
buffer = []
|
138 |
continue
|
139 |
|
140 |
+
if in_agent_output and ("╰" in line or "└" in line):
|
|
|
141 |
if buffer and current_agent and current_round:
|
142 |
self._add_agent_message(current_round, current_agent, '\n'.join(buffer))
|
143 |
+
in_agent_output, current_agent, buffer = False, None, []
|
|
|
|
|
|
|
|
|
144 |
continue
|
145 |
|
146 |
+
if in_agent_output:
|
147 |
+
clean_line = re.sub(r'^[│|]\s*|\s*[│|]\s*$', '', line)
|
148 |
+
if clean_line: buffer.append(clean_line)
|
|
|
|
|
|
|
|
|
149 |
continue
|
150 |
|
151 |
+
if " | INFO " in line and current_round:
|
|
|
152 |
info_match = re.search(r'mai_dx\.main:([^:]+):.*? - (.*)', line)
|
153 |
if info_match:
|
|
|
154 |
content = info_match.group(2).strip()
|
|
|
|
|
155 |
if "Panel decision:" in content:
|
156 |
decision_match = re.search(r'Panel decision: (\w+) -> (.*)', content)
|
157 |
+
if decision_match: current_round.decision = f"{decision_match.group(1)}: {decision_match.group(2)}"
|
|
|
|
|
158 |
elif "Current cost:" in content:
|
159 |
cost_match = re.search(r'Current cost: \$(\d+(?:\.\d+)?)', content)
|
160 |
+
if cost_match: current_round.cost_incurred = float(cost_match.group(1))
|
|
|
161 |
|
162 |
+
if current_round: current_round.end_time = datetime.now().isoformat()
|
|
|
163 |
|
164 |
+
def _add_agent_message(self, conversation: AgentConversation, agent_name: str, content: str):
|
165 |
+
message_type = "output"
|
166 |
+
formatted_content = content.strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
167 |
|
|
|
|
|
|
|
|
|
168 |
if "Structured Output - Attempting Function Call Execution" in content:
|
169 |
message_type = "function_call"
|
|
|
170 |
func_name_match = re.search(r"name:\s*(\w+)", content)
|
171 |
+
func_name = func_name_match.group(1) if func_name_match else "unknown"
|
172 |
+
args_match = re.search(r"arguments:\s*(\{.*\})", content, re.DOTALL)
|
173 |
+
formatted_content = f"🤖 **Дія:** `{func_name}`\n\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
174 |
if args_match:
|
175 |
try:
|
176 |
+
args_data = json.loads(args_match.group(1))
|
177 |
+
formatted_content += f"```json\n{json.dumps(args_data, indent=2, ensure_ascii=False)}\n```"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
178 |
except json.JSONDecodeError:
|
179 |
+
formatted_content += "Помилка парсингу JSON."
|
180 |
else:
|
181 |
+
formatted_content += "Аргументи не знайдено."
|
182 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
183 |
message = AgentMessage(
|
184 |
+
datetime.now().isoformat(), agent_name, message_type, formatted_content, {'raw_content': content}
|
|
|
|
|
|
|
|
|
185 |
)
|
|
|
186 |
conversation.messages.append(message)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
187 |
|
188 |
def _save_session_to_file(self, session: DiagnosisSession):
|
|
|
189 |
file_path = os.path.join(self.log_dir, f"{session.case_id}.json")
|
|
|
|
|
|
|
|
|
190 |
with open(file_path, 'w', encoding='utf-8') as f:
|
191 |
+
json.dump(asdict(session), f, ensure_ascii=False, indent=2)
|
192 |
|
|
|
193 |
raw_output_path = os.path.join(self.log_dir, f"{session.case_id}_raw.txt")
|
194 |
with open(raw_output_path, 'w', encoding='utf-8') as f:
|
195 |
f.write(session.raw_output)
|
196 |
|
197 |
+
def export_conversation_html(self, session: DiagnosisSession) -> str:
|
198 |
+
html_path = os.path.join(self.log_dir, f"{session.case_id}_conversation.html")
|
199 |
+
html_content = f"<html><head><title>Log - {session.case_id}</title></head><body>"
|
200 |
+
html_content += f"<h1>Session: {session.case_id}</h1>"
|
201 |
+
for conv in session.conversations:
|
202 |
+
html_content += f"<h2>Round {conv.round_number}</h2>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
203 |
for msg in conv.messages:
|
204 |
+
html_content += f"<div><b>{msg.agent_name}</b> [{msg.message_type}]:<pre>{msg.content}</pre></div>"
|
205 |
+
html_content += "</body></html>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
206 |
with open(html_path, 'w', encoding='utf-8') as f:
|
207 |
f.write(html_content)
|
|
|
208 |
return html_path
|
run_mai_dx_fixed.py
CHANGED
@@ -12,8 +12,8 @@ import warnings
|
|
12 |
os.environ.update({
|
13 |
"SWARMS_VERBOSITY": "ERROR",
|
14 |
"RICH_TRACEBACK": "0",
|
15 |
-
"SWARMS_SHOW_PANEL": "
|
16 |
-
"SWARMS_AUTO_PRINT": "
|
17 |
"PYTHONWARNINGS": "ignore",
|
18 |
"GRADIO_ANALYTICS_ENABLED": "false"
|
19 |
})
|
@@ -60,7 +60,7 @@ def main():
|
|
60 |
demo.launch(
|
61 |
server_name="0.0.0.0",
|
62 |
server_port=7860,
|
63 |
-
share=
|
64 |
debug=False,
|
65 |
show_error=True,
|
66 |
quiet=False
|
|
|
12 |
os.environ.update({
|
13 |
"SWARMS_VERBOSITY": "ERROR",
|
14 |
"RICH_TRACEBACK": "0",
|
15 |
+
"SWARMS_SHOW_PANEL": "false",
|
16 |
+
"SWARMS_AUTO_PRINT": "false",
|
17 |
"PYTHONWARNINGS": "ignore",
|
18 |
"GRADIO_ANALYTICS_ENABLED": "false"
|
19 |
})
|
|
|
60 |
demo.launch(
|
61 |
server_name="0.0.0.0",
|
62 |
server_port=7860,
|
63 |
+
share=True,
|
64 |
debug=False,
|
65 |
show_error=True,
|
66 |
quiet=False
|
updated_mai_dx_interface.py
CHANGED
@@ -17,8 +17,8 @@ import warnings
|
|
17 |
os.environ.update({
|
18 |
"SWARMS_VERBOSITY": "ERROR",
|
19 |
"RICH_TRACEBACK": "0",
|
20 |
-
"SWARMS_SHOW_PANEL": "
|
21 |
-
"SWARMS_AUTO_PRINT": "
|
22 |
})
|
23 |
warnings.filterwarnings("ignore")
|
24 |
|
@@ -37,7 +37,7 @@ except ImportError:
|
|
37 |
IMPORT_ERROR = str(e)
|
38 |
|
39 |
# Імпорт покращеного логгера
|
40 |
-
from enhanced_mai_dx_logger import MAIDxConversationLogger, DiagnosisSession
|
41 |
|
42 |
# Перевірка доступності Plotly
|
43 |
try:
|
@@ -100,8 +100,7 @@ class UpdatedMAIDXInterface:
|
|
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 |
-
)
|
104 |
-
|
105 |
if not MAI_DX_AVAILABLE:
|
106 |
return self._format_error(f"❌ MAI-DX недоступний: {IMPORT_ERROR}")
|
107 |
if not patient_info.strip():
|
@@ -118,17 +117,20 @@ class UpdatedMAIDXInterface:
|
|
118 |
|
119 |
# Створюємо сесію логування, якщо увімкнено
|
120 |
if enable_logging:
|
121 |
-
|
|
|
122 |
case_name=case_name,
|
123 |
patient_info=patient_info,
|
124 |
mode=mode,
|
125 |
budget=budget
|
126 |
)
|
|
|
127 |
conversation_log += f"📝 Розпочато логування сесії: {case_id}\n\n"
|
128 |
|
129 |
progress(0.2, desc="🤖 Створення AI-панелі...")
|
130 |
|
131 |
-
#
|
|
|
132 |
orchestrator = MaiDxOrchestrator(
|
133 |
model_name=model_name,
|
134 |
max_iterations=max_iterations,
|
@@ -149,7 +151,6 @@ class UpdatedMAIDXInterface:
|
|
149 |
ground_truth_diagnosis=expected_diagnosis or "Unknown"
|
150 |
)
|
151 |
else:
|
152 |
-
# Виконуємо без логування
|
153 |
result = orchestrator.run(
|
154 |
initial_case_info=patient_info,
|
155 |
full_case_details=patient_info,
|
@@ -159,8 +160,8 @@ class UpdatedMAIDXInterface:
|
|
159 |
duration = time.time() - start_time
|
160 |
progress(0.9, desc="📊 Обробка результатів...")
|
161 |
|
162 |
-
# Завершуємо сесію і отримуємо фінальний, заповнений об'єкт сесії
|
163 |
if enable_logging and case_id:
|
|
|
164 |
session = self.conversation_logger.end_session(
|
165 |
case_id=case_id,
|
166 |
final_diagnosis=result.final_diagnosis,
|
@@ -169,9 +170,7 @@ class UpdatedMAIDXInterface:
|
|
169 |
reasoning=getattr(result, 'accuracy_reasoning', 'N/A')
|
170 |
)
|
171 |
|
172 |
-
|
173 |
-
# створюємо об'єкт сесії вручну для відображення
|
174 |
-
if not session:
|
175 |
session = DiagnosisSession(
|
176 |
case_id=case_id or "no_logging",
|
177 |
timestamp=datetime.now().isoformat(),
|
@@ -188,25 +187,18 @@ class UpdatedMAIDXInterface:
|
|
188 |
reasoning=getattr(result, 'accuracy_reasoning', 'N/A')
|
189 |
)
|
190 |
|
191 |
-
# Тепер, коли у нас є гарантовано існуючий об'єкт session,
|
192 |
-
# ми можемо заповнити решту даних для виводу
|
193 |
if session:
|
194 |
self.sessions_history.append(session)
|
195 |
-
|
196 |
-
# Додаємо сирий вивід у лог інтерфейсу
|
197 |
conversation_log += "🤖 Повний сирий вивід системи:\n" + "="*60 + "\n"
|
198 |
conversation_log += session.raw_output + "\n" + "="*60 + "\n\n"
|
199 |
conversation_log += f"💾 Сесію збережено як: {session.case_id}\n"
|
200 |
|
201 |
-
# Генеруємо HTML звіт і додаємо посилання
|
202 |
if enable_logging:
|
203 |
html_path = self.conversation_logger.export_conversation_html(session.case_id)
|
204 |
conversation_log += f"📄 HTML звіт: {html_path}\n"
|
205 |
|
206 |
-
# Форматуємо структуровані розмови, передаючи список напряму
|
207 |
structured_conversations = self._format_structured_conversations(session.conversations)
|
208 |
|
209 |
-
# Оновлюємо метрики з розмов
|
210 |
for conv in session.conversations:
|
211 |
self.current_metrics.add_conversation_round({
|
212 |
'round': conv.round_number,
|
@@ -221,11 +213,9 @@ class UpdatedMAIDXInterface:
|
|
221 |
|
222 |
progress(1.0, desc="✅ Готово!")
|
223 |
|
224 |
-
# Створюємо візуалізації
|
225 |
metrics_plot = self._create_enhanced_metrics_visualization()
|
226 |
agent_plot = self._create_conversation_flow_chart()
|
227 |
|
228 |
-
# Передаємо ПРАВИЛЬНИЙ об'єкт сесії у форматери
|
229 |
return (
|
230 |
self._format_main_result(session),
|
231 |
self._format_detailed_analysis(session),
|
@@ -238,14 +228,16 @@ class UpdatedMAIDXInterface:
|
|
238 |
|
239 |
except Exception as e:
|
240 |
import traceback
|
241 |
-
traceback.print_exc()
|
242 |
error_msg = f"❌ Критична помилка діагностики: {str(e)}"
|
243 |
if case_id:
|
244 |
error_msg += f"\n🗂️ ID сесії: {case_id}"
|
245 |
-
return self._format_error(error_msg)
|
246 |
-
|
247 |
-
def _format_structured_conversations(self,
|
248 |
"""Форматує структуровані розмови у читабельний вигляд"""
|
|
|
|
|
249 |
if not conversations:
|
250 |
return "📭 Розмови не знайдено"
|
251 |
|
@@ -253,25 +245,19 @@ class UpdatedMAIDXInterface:
|
|
253 |
|
254 |
for conv in conversations:
|
255 |
output += f"### 🔄 Раунд {conv.round_number}\n"
|
256 |
-
|
257 |
-
output += f"**Час:** {conv.start_time} - {conv.end_time}\n\n"
|
258 |
|
259 |
for msg in conv.messages:
|
260 |
emoji = {
|
261 |
-
'function_call': '🤖',
|
262 |
'reasoning': '🤔',
|
263 |
'decision': '💡',
|
264 |
'input': '❓',
|
265 |
'output': '📊'
|
266 |
}.get(msg.message_type, '💬')
|
267 |
|
268 |
-
output += f"{emoji} **{msg.agent_name}**
|
269 |
-
|
270 |
-
|
271 |
-
output += f"{msg.content}\n\n"
|
272 |
-
else:
|
273 |
-
output += f"```\n{msg.content}\n```\n\n"
|
274 |
-
|
275 |
if conv.decision:
|
276 |
output += f"**🎯 Рішення:** {conv.decision}\n"
|
277 |
|
@@ -598,14 +584,14 @@ def create_updated_gradio_interface():
|
|
598 |
|
599 |
model_name = gr.Dropdown(
|
600 |
choices=[
|
601 |
-
"gemini/gemini-
|
602 |
"gpt-4",
|
603 |
"gpt-4-turbo",
|
604 |
"claude-3-5-sonnet",
|
605 |
"gpt-4o"
|
606 |
],
|
607 |
label="🤖 LLM Модель",
|
608 |
-
value="gemini/gemini-
|
609 |
interactive=True
|
610 |
)
|
611 |
|
|
|
17 |
os.environ.update({
|
18 |
"SWARMS_VERBOSITY": "ERROR",
|
19 |
"RICH_TRACEBACK": "0",
|
20 |
+
"SWARMS_SHOW_PANEL": "false",
|
21 |
+
"SWARMS_AUTO_PRINT": "false"
|
22 |
})
|
23 |
warnings.filterwarnings("ignore")
|
24 |
|
|
|
37 |
IMPORT_ERROR = str(e)
|
38 |
|
39 |
# Імпорт покращеного логгера
|
40 |
+
from enhanced_mai_dx_logger import MAIDxConversationLogger, DiagnosisSession
|
41 |
|
42 |
# Перевірка доступності Plotly
|
43 |
try:
|
|
|
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 |
+
):
|
|
|
104 |
if not MAI_DX_AVAILABLE:
|
105 |
return self._format_error(f"❌ MAI-DX недоступний: {IMPORT_ERROR}")
|
106 |
if not patient_info.strip():
|
|
|
117 |
|
118 |
# Створюємо сесію логування, якщо увімкнено
|
119 |
if enable_logging:
|
120 |
+
# ВИПРАВЛЕННЯ ТУТ: Використовуємо create_session
|
121 |
+
session = self.conversation_logger.create_session(
|
122 |
case_name=case_name,
|
123 |
patient_info=patient_info,
|
124 |
mode=mode,
|
125 |
budget=budget
|
126 |
)
|
127 |
+
case_id = session.case_id
|
128 |
conversation_log += f"📝 Розпочато логування сесії: {case_id}\n\n"
|
129 |
|
130 |
progress(0.2, desc="🤖 Створення AI-панелі...")
|
131 |
|
132 |
+
# ... решта коду методу залишається без змін ...
|
133 |
+
|
134 |
orchestrator = MaiDxOrchestrator(
|
135 |
model_name=model_name,
|
136 |
max_iterations=max_iterations,
|
|
|
151 |
ground_truth_diagnosis=expected_diagnosis or "Unknown"
|
152 |
)
|
153 |
else:
|
|
|
154 |
result = orchestrator.run(
|
155 |
initial_case_info=patient_info,
|
156 |
full_case_details=patient_info,
|
|
|
160 |
duration = time.time() - start_time
|
161 |
progress(0.9, desc="📊 Обробка результатів...")
|
162 |
|
|
|
163 |
if enable_logging and case_id:
|
164 |
+
# ВАЖЛИВО: Отримуємо об'єкт сесії з пам'яті логера, а не створюємо новий
|
165 |
session = self.conversation_logger.end_session(
|
166 |
case_id=case_id,
|
167 |
final_diagnosis=result.final_diagnosis,
|
|
|
170 |
reasoning=getattr(result, 'accuracy_reasoning', 'N/A')
|
171 |
)
|
172 |
|
173 |
+
if not session: # Резервний варіант, якщо логування було вимкнене
|
|
|
|
|
174 |
session = DiagnosisSession(
|
175 |
case_id=case_id or "no_logging",
|
176 |
timestamp=datetime.now().isoformat(),
|
|
|
187 |
reasoning=getattr(result, 'accuracy_reasoning', 'N/A')
|
188 |
)
|
189 |
|
|
|
|
|
190 |
if session:
|
191 |
self.sessions_history.append(session)
|
|
|
|
|
192 |
conversation_log += "🤖 Повний сирий вивід системи:\n" + "="*60 + "\n"
|
193 |
conversation_log += session.raw_output + "\n" + "="*60 + "\n\n"
|
194 |
conversation_log += f"💾 Сесію збережено як: {session.case_id}\n"
|
195 |
|
|
|
196 |
if enable_logging:
|
197 |
html_path = self.conversation_logger.export_conversation_html(session.case_id)
|
198 |
conversation_log += f"📄 HTML звіт: {html_path}\n"
|
199 |
|
|
|
200 |
structured_conversations = self._format_structured_conversations(session.conversations)
|
201 |
|
|
|
202 |
for conv in session.conversations:
|
203 |
self.current_metrics.add_conversation_round({
|
204 |
'round': conv.round_number,
|
|
|
213 |
|
214 |
progress(1.0, desc="✅ Готово!")
|
215 |
|
|
|
216 |
metrics_plot = self._create_enhanced_metrics_visualization()
|
217 |
agent_plot = self._create_conversation_flow_chart()
|
218 |
|
|
|
219 |
return (
|
220 |
self._format_main_result(session),
|
221 |
self._format_detailed_analysis(session),
|
|
|
228 |
|
229 |
except Exception as e:
|
230 |
import traceback
|
231 |
+
traceback.print_exc()
|
232 |
error_msg = f"❌ Критична помилка діагностики: {str(e)}"
|
233 |
if case_id:
|
234 |
error_msg += f"\n🗂️ ID сесії: {case_id}"
|
235 |
+
return self._format_error(error_msg)
|
236 |
+
|
237 |
+
def _format_structured_conversations(self, case_id: str) -> str:
|
238 |
"""Форматує структуровані розмови у читабельний вигляд"""
|
239 |
+
conversations = self.conversation_logger.get_session_conversations(case_id)
|
240 |
+
|
241 |
if not conversations:
|
242 |
return "📭 Розмови не знайдено"
|
243 |
|
|
|
245 |
|
246 |
for conv in conversations:
|
247 |
output += f"### 🔄 Раунд {conv.round_number}\n"
|
248 |
+
output += f"**Час:** {conv.start_time} - {conv.end_time or 'В процесі'}\n\n"
|
|
|
249 |
|
250 |
for msg in conv.messages:
|
251 |
emoji = {
|
|
|
252 |
'reasoning': '🤔',
|
253 |
'decision': '💡',
|
254 |
'input': '❓',
|
255 |
'output': '📊'
|
256 |
}.get(msg.message_type, '💬')
|
257 |
|
258 |
+
output += f"{emoji} **{msg.agent_name}** [{msg.message_type}]\n"
|
259 |
+
output += f"```\n{msg.content[:500]}{'...' if len(msg.content) > 500 else ''}\n```\n\n"
|
260 |
+
|
|
|
|
|
|
|
|
|
261 |
if conv.decision:
|
262 |
output += f"**🎯 Рішення:** {conv.decision}\n"
|
263 |
|
|
|
584 |
|
585 |
model_name = gr.Dropdown(
|
586 |
choices=[
|
587 |
+
"gemini/gemini-1.5-flash",
|
588 |
"gpt-4",
|
589 |
"gpt-4-turbo",
|
590 |
"claude-3-5-sonnet",
|
591 |
"gpt-4o"
|
592 |
],
|
593 |
label="🤖 LLM Модель",
|
594 |
+
value="gemini/gemini-1.5-flash",
|
595 |
interactive=True
|
596 |
)
|
597 |
|