DocUA commited on
Commit
0091076
·
1 Parent(s): 20d0a16

Оновлено логіку захоплення та обробки виводу в MAIDxConversationLogger, покращено структуру класів для сесій діагностики. Внесено зміни в інтерфейс для коректного завершення сесій та збереження результатів. Виправлено помилки в обробці логів та оновлено документацію для відображення нових функцій.

Browse files
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 # '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
@@ -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 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,
@@ -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 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: 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 or "rich.errors.NotRenderableError" in line or "Exception in thread" in line:
198
- continue
199
 
200
- # 1. Перевірка на початок нового раунду
201
  if "Starting Diagnostic Loop" in line:
202
- finalize_round(current_round) # Завершуємо попередній раунд
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
- # 3. Перевірка на кінець блоку агента
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
- # 4. Збір вмісту всередині блоку агента
235
- elif in_agent_output and current_agent:
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
- # 5. Обробка звичайних логів INFO (якщо ми не всередині блоку агента)
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 _extract_agent_from_log(self, log_content: str) -> Optional[str]:
265
- """Витягти ім'я агента з лог повідомлення"""
266
- agent_patterns = {
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 "unknown_function"
335
-
336
- # Надійний regex для витягнення JSON, що працює з багаторядковим текстом
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
- json_str = args_match.group(1)
350
- args_data = json.loads(json_str)
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
- timestamp=datetime.now().isoformat(),
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(session_dict, f, ensure_ascii=False, indent=2)
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 get_session_conversations(self, case_id: str) -> List[AgentConversation]:
452
- """Отримати всі розмови для сесії"""
453
- file_path = os.path.join(self.log_dir, f"{case_id}.json")
454
-
455
- if not os.path.exists(file_path):
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
- css_class = "agent-message"
517
- if msg.message_type == "decision":
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": "true",
16
- "SWARMS_AUTO_PRINT": "true",
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=False,
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": "true",
21
- "SWARMS_AUTO_PRINT": "true"
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, AgentConversation
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
- ) -> 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():
@@ -118,17 +117,20 @@ class UpdatedMAIDXInterface:
118
 
119
  # Створюємо сесію логування, якщо увімкнено
120
  if enable_logging:
121
- case_id = self.conversation_logger.start_session(
 
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
- # Створюємо orchestrator
 
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() # Друкуємо повний traceback у консоль для налагодження
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, conversations: List[AgentConversation]) -> str:
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
- if conv.start_time and conv.end_time:
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}** `[{msg.message_type}]`\n"
269
- # Використовуємо Markdown-блоки для кращого відображення
270
- if msg.message_type == 'function_call':
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-2.5-flash",
602
  "gpt-4",
603
  "gpt-4-turbo",
604
  "claude-3-5-sonnet",
605
  "gpt-4o"
606
  ],
607
  label="🤖 LLM Модель",
608
- value="gemini/gemini-2.5-flash",
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