File size: 15,203 Bytes
143d94c
 
8f09ca9
 
143d94c
 
 
 
 
ad66eac
143d94c
 
 
ad66eac
143d94c
 
 
78e1ca4
143d94c
ad66eac
 
143d94c
 
 
 
 
 
 
 
78e1ca4
 
 
 
143d94c
 
8f09ca9
 
 
 
143d94c
 
78e1ca4
143d94c
 
 
 
 
 
 
 
 
 
 
 
 
 
8f09ca9
 
78e1ca4
143d94c
 
cb898e1
8f09ca9
0091076
ad66eac
 
 
78e1ca4
143d94c
 
 
ad66eac
143d94c
 
 
 
ad66eac
 
 
 
143d94c
78e1ca4
ad66eac
78e1ca4
ad66eac
 
78e1ca4
143d94c
78e1ca4
 
 
cb898e1
78e1ca4
143d94c
cb898e1
78e1ca4
143d94c
20d0a16
78e1ca4
 
0091076
ad66eac
cb898e1
 
143d94c
cb898e1
143d94c
cb898e1
 
143d94c
 
cb898e1
 
 
 
ad66eac
 
 
 
cb898e1
 
 
ad66eac
cb898e1
 
 
 
 
 
 
143d94c
ad66eac
143d94c
 
78e1ca4
143d94c
 
 
 
 
78e1ca4
143d94c
 
 
 
 
 
 
 
78e1ca4
 
143d94c
78e1ca4
143d94c
 
ad66eac
143d94c
78e1ca4
 
143d94c
 
 
 
 
 
 
ad66eac
143d94c
ad66eac
 
143d94c
 
ad66eac
143d94c
 
 
 
 
ad66eac
 
143d94c
 
 
 
 
cb898e1
 
143d94c
ad66eac
 
 
 
8f09ca9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143d94c
8f09ca9
 
 
 
143d94c
8f09ca9
 
 
 
 
 
 
 
143d94c
 
8f09ca9
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
#!/usr/bin/env python3
"""
Оновлений MAI-DX Gradio Interface з повним логуванням розмов агентів 
(ВЕРСІЯ З ВИПРАВЛЕНИМ СТАТУС-БАРОМ І СТРУКТУРОЮ)
"""
import os
import sys
import json
import time
import uuid
import pandas as pd
import gradio as gr
from datetime import datetime
from typing import Dict, List, Tuple, Optional, Any
from dataclasses import dataclass, asdict
import warnings

# Налаштування середовища
os.environ.update({
    "SWARMS_VERBOSITY": "ERROR", "RICH_TRACEBACK": "0", 
    "SWARMS_SHOW_PANEL": "true", "SWARMS_AUTO_PRINT": "true"
})
warnings.filterwarnings("ignore")

from dotenv import load_dotenv
load_dotenv()

try:
    from mai_dx_wrapper import SafeMaiDxOrchestrator as MaiDxOrchestrator, MAI_DX_AVAILABLE
    from enhanced_mai_dx_logger import MAIDxConversationLogger, DiagnosisSession, AgentConversation, EnhancedOutputCapture
except ImportError as e:
    MAI_DX_AVAILABLE = False
    IMPORT_ERROR = str(e)

class UpdatedMAIDXInterface:
    """
    Клас, що інкапсулює логіку та побудову Gradio інтерфейсу.
    Це запобігає дублюванню обробників подій.
    """
    def __init__(self):
        self.conversation_logger = MAIDxConversationLogger("mai_dx_logs")
        self.sessions_history = []
        self.sample_cases = {
            "🫀 Кардіологічний (Гострий MI)": {
                "info": "Пацієнт: 58-річний чоловік, менеджер, гіпертонія в анамнезі\nСкарги: Гострий роздираючий біль у грудях 3 години, іррадіація в ліву руку\nОгляд: Блідий, пітливий, АТ 160/90, ЧСС 95\nЕКГ: ST-підйоми у відведеннях II, III, aVF (нижня стінка)\nТропонін I: 8.5 нг/мл (норма <0.04)\nАнамнез: Куріння 30 років, дислипідемія, сімейний анамнез ІХС",
                "expected": "Acute inferior wall myocardial infarction (STEMI)"
            },
            "🧠 Неврологічний (Гострий інсульт)": {
                "info": "Пацієнтка: 67-річна жінка, раптові неврологічні симптоми\nПрезентація: Раптова слабкість правої сторони 2 години тому\nОгляд: Свідома, дезорієнтована у часі, правостороннє опущення обличчя\nНеврологія: Правостороння геміплегія, афазія, девіація очей вліво\nКТ голови: Гострого крововиливу немає, рання ішемія у лівій МСА\nNIHSS: 15 балів",
                "expected": "Acute ischemic stroke in the left middle cerebral artery territory"
            },
            "🦠 Інфекційний (Сепсис)": {
                "info": "Пацієнт: 45-річний чоловік з прогресуючою лихоманкою\nСкарги: Висока температура 39.5°C, озноб, загальна слабкість 3 дні\nОгляд: Гарячий, тахікардія 120/хв, гіпотензія 85/50\nЛабораторно: Лейкоцити 18000, С-реактивний білок 180, прокальцитонін 5.2\nПосів крові: Pending, lactate 4.2 ммоль/л\nАнамнез: Нещодавня стоматологічна процедура",
                "expected": "Sepsis with a possible odontogenic source"
            }
        }
        # Створюємо інтерфейс один раз при ініціалізації
        self.demo = self.build_interface()

    def diagnose_with_full_logging(
        self, case_name: str, patient_info: str, mode: str, budget: int, max_iterations: int,
        model_name: str, expected_diagnosis: str = "",
        progress=gr.Progress(track_tqdm=True)
    ):
        if not MAI_DX_AVAILABLE: return self._format_error(f"❌ MAI-DX недоступний: {IMPORT_ERROR}")
        if not patient_info.strip(): return self._format_error("❌ Введіть інформацію про пацієнта")
        
        session = self.conversation_logger.create_session(case_name, patient_info, mode, budget)
        
        try:
            progress(0.2, desc="🤖 Створення AI-панелі...")
            orchestrator = MaiDxOrchestrator(model_name, max_iterations, budget, mode)
            
            progress(0.3, desc="🔍 Запуск діагностики...")
            start_time = time.time()
            
            with EnhancedOutputCapture() as capture:
                result = orchestrator.run(patient_info, patient_info, expected_diagnosis or "Unknown")
            raw_output = capture.get_value()

            duration = time.time() - start_time
            progress(0.8, desc="📊 Обробка результатів...")
            
            session = self.conversation_logger.finalize_and_save_session(session, result, raw_output, duration)
            self.sessions_history.append(session)
            
            progress(1.0, desc="✅ Готово!")
            
            main_result_str = self._format_main_result(session)
            detailed_analysis_str = self._format_detailed_analysis(session)
            recommendations_str = self._generate_enhanced_recommendations(result, expected_diagnosis)
            agent_report_str = self._create_agent_activity_report(session.conversations)
            structured_conv_str = self._format_structured_conversations(session.conversations)
            
            return main_result_str, detailed_analysis_str, recommendations_str, agent_report_str, structured_conv_str

        except Exception as e:
            import traceback
            error_msg = f"❌ Критична помилка: {traceback.format_exc()}"
            return self._format_error(error_msg)

    def _format_structured_conversations(self, conversations: List[AgentConversation]) -> str:
        if not conversations: return "### 🗣️ Розмови агентів\n\n*Розмови не знайдено в логах. Можливо, діагноз було поставлено миттєво.*"
        output = "### 🗣️ Розмови агентів\n\n"
        for conv in conversations:
            output += f"#### 🔄 Раунд {conv.round_number}\n"
            for msg in conv.messages:
                output += f"<details><summary><strong>{msg.agent_name}</strong> <code>[{msg.message_type}]</code></summary>\n\n"
                output += f"```\n{msg.content.strip()}\n```\n\n</details>\n"
        return output
    
    def _create_agent_activity_report(self, conversations: List[AgentConversation]) -> str:
        if not conversations:
            return "### 🤖 Активність агентів\n\n*Дані відсутні.*"
        
        agent_activity = {}
        for conv in conversations:
            for msg in conv.messages:
                agent_activity[msg.agent_name] = agent_activity.get(msg.agent_name, 0) + 1
        
        if not agent_activity:
            return "### 🤖 Активність агентів\n\n*Повідомлення від агентів не знайдено.*"

        report_md = "### 🤖 Активність агентів\n\n"
        sorted_activity = sorted(agent_activity.items(), key=lambda item: item[1], reverse=True)
        
        for agent, count in sorted_activity:
            report_md += f"- **{agent}**: {count} повідомлень\n"
            
        return report_md
    
    def _format_main_result(self, session: DiagnosisSession) -> str:
        efficiency = ((session.budget - session.cost) / session.budget * 100) if session.budget > 0 else 0
        confidence_emoji = "🎯" if session.confidence >= 4.0 else "👍" if session.confidence >= 3.0 else "⚠️"
        
        return f"""
## 🏥 Результати MAI-DX Діагностики
### 📋 Основна інформація
- **🗂️ Випадок**: {session.case_name}
- **🆔 ID сесії**: {session.case_id}
- **⏰ Час**: {session.timestamp}  
- **🔧 Режим**: {session.mode}
### {confidence_emoji} Діагностичний висновок
**{session.diagnosis}**
### 📊 Показники якості
- **Точність**: {session.confidence:.1f}/5.0 ⭐
- **Статус**: {session.status}
- **Ітерації**: {session.iterations} циклів
### 💰 Економічні показники  
- **Витрачено**: ${session.cost:,.2f}
- **Бюджет**: ${session.budget:,}
- **Ефективність**: {efficiency:.1f}% бюджету збережено
- **Швидкість**: {session.duration:.1f} секунд
"""
    
    def _format_detailed_analysis(self, session: DiagnosisSession) -> str:
        return f"""
## 🔬 Детальний клінічний аналіз
### 💭 Медичне обґрунтування
{session.reasoning}
### 📂 Файли логів
- JSON: `mai_dx_logs/{session.case_id}.json`
- Raw text: `mai_dx_logs/{session.case_id}_raw.txt`
- HTML звіт: `mai_dx_logs/{session.case_id}_conversation.html`
"""
    
    def _generate_enhanced_recommendations(self, result: Any, expected: str) -> str:
        comparison = ""
        if expected:
            is_match = expected.lower() in result.final_diagnosis.lower()
            comparison = f"""
### 🎯 Порівняння з очікуваним
**Очікувався**: {expected}
**Отримано**: {result.final_diagnosis}
**Збіг**: {'✅ Так' if is_match else '❌ Ні'}
"""
        return f"""
## 💡 Клінічні рекомендації
- 🔍 Верифікувати діагноз з лікарем-спеціалістом.
- 📊 Переглянути детальний лог розмов агентів.
{comparison}
### ⚠️ Важливе застереження
🔴 **Цей діагноз згенеровано ШІ і НЕ замінює професійну медичну консультацію.**
"""
    
    def _format_error(self, msg: str) -> Tuple[str, str, str, str, str]:
        return (msg, "", "", "", "")
    
    def load_sample_case(self, key: str) -> Tuple[str, str, str]:
        case = self.sample_cases.get(key, {})
        return case.get("info", ""), key, case.get("expected", "")

    def build_interface(self):
        """
        Метод, що будує інтерфейс Gradio. Викликається один раз.
        """
        with gr.Blocks(title="🏥 MAI-DX Enhanced Platform", theme=gr.themes.Soft(primary_hue="blue")) as demo:
            gr.HTML("""<div style='background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 2rem; border-radius: 15px; margin-bottom: 2rem; text-align: center;'><h1>🏥 MAI-DX Enhanced Research Platform</h1></div>""")
            with gr.Tabs():
                with gr.Tab("🩺 Діагностика"):
                    with gr.Row():
                        with gr.Column(scale=1):
                            gr.Markdown("### 📝 Клінічний випадок")
                            case_name = gr.Textbox(label="🏷️ Назва випадку", placeholder="Наприклад: Кардіологічний випадок №1")
                            sample_selector = gr.Dropdown(choices=list(self.sample_cases.keys()), label="🎯 Готові випадки", interactive=True)
                            patient_info = gr.Textbox(label="👤 Інформація про пацієнта", lines=12)
                            expected_diagnosis = gr.Textbox(label="🎯 Очікуваний діагноз (англ.)", lines=2)
                        with gr.Column(scale=1):
                            gr.Markdown("### ⚙️ Налаштування")
                            mode = gr.Radio(["instant", "question_only", "budgeted", "no_budget", "ensemble"], label="🔧 Режим", value="budgeted")
                            budget = gr.Slider(500, 10000, 3000, step=500, label="💵 Бюджет ($)")
                            max_iterations = gr.Slider(1, 15, 8, step=1, label="🔄 Макс. ітерацій")
                            model_name = gr.Dropdown(["gemini/gemini-2.5-flash", "gpt-4o", "claude-3-5-sonnet", "gpt-4", "gpt-4-turbo"], label="🤖 LLM Модель", value="gemini/gemini-2.5-flash")
                            diagnose_btn = gr.Button("🚀 Запустити діагностику", variant="primary", size="lg")
                    
                    gr.Markdown("---")
                    with gr.Row():
                        with gr.Column(scale=2):
                            main_result = gr.Markdown()
                            detailed_analysis = gr.Markdown()
                        with gr.Column(scale=1):
                            recommendations = gr.Markdown()
                    
                    gr.Markdown("---")
                    with gr.Row():
                        with gr.Column(scale=1):
                            agent_activity_report = gr.Markdown(label="🤖 Активність агентів")
                        with gr.Column(scale=2):
                            structured_conversations = gr.Markdown(label="🗣️ Структуровані розмови")
                    
                    # Прив'язуємо обробники подій до методів класу
                    sample_selector.change(self.load_sample_case, inputs=sample_selector, outputs=[patient_info, case_name, expected_diagnosis])
                    
                    diagnose_btn.click(
                        self.diagnose_with_full_logging,
                        inputs=[case_name, patient_info, mode, budget, max_iterations, model_name, expected_diagnosis],
                        outputs=[main_result, detailed_analysis, recommendations, agent_activity_report, structured_conversations],
                        show_progress="full"
                    )
                
                with gr.Tab("📊 Аналіз логів"):
                    gr.Markdown("### 🔍 Аналіз збережених діагностичних сесій")

        return demo

    def launch(self, *args, **kwargs):
        """Метод для запуску Gradio-додатка."""
        self.demo.launch(*args, **kwargs)

# Ця функція тепер є обгорткою для створення екземпляра класу
def create_updated_gradio_interface():
    app = UpdatedMAIDXInterface()
    return app.demo

if __name__ == "__main__":
    # Якщо файл запускається напряму, створюємо екземпляр і запускаємо
    app = UpdatedMAIDXInterface()
    app.launch(server_name="0.0.0.0", server_port=7860, share=False)