File size: 7,622 Bytes
143d94c
 
 
ac50cc2
143d94c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0091076
143d94c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0091076
143d94c
 
 
 
 
 
0091076
143d94c
0091076
143d94c
0091076
143d94c
 
 
ac50cc2
 
 
 
 
0091076
ac50cc2
143d94c
 
 
 
0091076
 
 
143d94c
 
 
 
 
 
 
 
 
 
0091076
143d94c
0091076
143d94c
 
 
 
 
 
 
 
0091076
 
 
143d94c
0091076
 
 
 
 
 
 
143d94c
0091076
 
 
 
 
143d94c
20d0a16
 
 
143d94c
 
20d0a16
 
 
 
143d94c
ac50cc2
cb898e1
 
 
 
 
 
 
143d94c
 
 
0091076
143d94c
20d0a16
0091076
143d94c
 
0091076
20d0a16
 
0091076
 
cb898e1
143d94c
20d0a16
cb898e1
 
20d0a16
0091076
 
 
20d0a16
cb898e1
20d0a16
cb898e1
 
 
 
 
143d94c
0091076
143d94c
 
 
 
 
 
0091076
143d94c
 
 
 
 
0091076
 
 
 
 
 
143d94c
0091076
 
143d94c
 
 
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
#!/usr/bin/env python3
"""
Покращений модуль для повного логування діагностичних сесій MAI-DX 
з детальним захопленням розмов між агентами. (ВЕРСІЯ З ВИПРАВЛЕНИМ ПАРСЕРОМ І ПОВЕРНЕНИМ ВИВОДОМ)
"""
import os
import io
import sys
import json
import time
import uuid
import re
import logging
from datetime import datetime
from typing import List, Dict, Any, Optional, Callable
from dataclasses import dataclass, asdict, field

@dataclass
class AgentMessage:
    timestamp: str
    agent_name: str
    message_type: str
    content: str
    metadata: Dict[str, Any] = field(default_factory=dict)

@dataclass 
class AgentConversation:
    round_number: int
    start_time: str
    end_time: Optional[str] = None
    messages: List[AgentMessage] = field(default_factory=list)
    decision: Optional[str] = None
    cost_incurred: float = 0.0

@dataclass
class DiagnosisSession:
    case_id: str
    timestamp: str
    case_name: str
    patient_info: str
    mode: str
    budget: int = 0
    diagnosis: str = "N/A"
    confidence: float = 0.0
    cost: float = 0.0
    iterations: int = 0
    duration: float = 0.0
    status: str = "In Progress"
    reasoning: str = "N/A"
    conversations: List[AgentConversation] = field(default_factory=list)
    raw_output: str = ""

class EnhancedOutputCapture:
    def __init__(self):
        self.captured_output = io.StringIO()
        self.original_stdout = sys.stdout
        
    def __enter__(self):
        sys.stdout = self
        return self
        
    def __exit__(self, exc_type, exc_val, exc_tb):
        sys.stdout = self.original_stdout
        
    def write(self, text):
        """
        FIX: Повертаємо вивід в консоль.
        Цей метод тепер і записує вивід у буфер для парсингу,
        і дублює його в оригінальну консоль.
        """
        self.captured_output.write(text)
        self.original_stdout.write(text)
        
    def flush(self):
        self.original_stdout.flush()
        
    def get_value(self):
        return self._strip_ansi_codes(self.captured_output.getvalue())

    @staticmethod
    def _strip_ansi_codes(text):
        ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
        return ansi_escape.sub('', text)

class MAIDxConversationLogger:
    def __init__(self, log_dir: str = "mai_dx_logs"):
        self.log_dir = log_dir
        os.makedirs(self.log_dir, exist_ok=True)
        
    def create_session(self, case_name: str, patient_info: str, mode: str, budget: int) -> DiagnosisSession:
        case_id = f"case_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:6]}"
        return DiagnosisSession(
            case_id=case_id,
            timestamp=datetime.now().isoformat(),
            case_name=case_name,
            patient_info=patient_info,
            mode=mode,
            budget=budget
        )
        
    def finalize_and_save_session(self, session: DiagnosisSession, result: Any, raw_output: str, duration: float) -> DiagnosisSession:
        session.raw_output = raw_output
        self._parse_captured_output(session, raw_output)
        
        session.diagnosis = getattr(result, 'final_diagnosis', 'N/A')
        session.confidence = getattr(result, 'accuracy_score', 0.0)
        session.cost = getattr(result, 'total_cost', 0.0)
        session.reasoning = getattr(result, 'accuracy_reasoning', 'N/A')
        session.iterations = len(session.conversations)
        session.status = "✅ Успішно" if session.confidence >= 3.0 else "⚠️ Потребує перегляду"
        session.duration = duration
        
        self._save_session_to_file(session)
        self.export_conversation_html(session)
        return session

    def _parse_captured_output(self, session: DiagnosisSession, captured_text: str):
        lines = captured_text.split('\n')
        current_round: Optional[AgentConversation] = None
        current_agent: Optional[str] = None
        buffer: List[str] = []
        in_agent_output = False
        round_number = 0

        for line in lines:
            stripped_line = line.strip()

            if "Agent Name" in line and ("╭" in line or "┌" in line):
                if not current_round or in_agent_output:
                    round_number += 1
                    if current_round:
                        current_round.end_time = datetime.now().isoformat()
                    current_round = AgentConversation(round_number=round_number, start_time=datetime.now().isoformat())
                    session.conversations.append(current_round)

                agent_match = re.search(r'Agent Name (.*?) (?:\[|\s)', line)
                if agent_match:
                    current_agent = agent_match.group(1).strip()
                    in_agent_output = True
                    buffer = []
                continue

            if in_agent_output and ("╰" in line or "└" in line):
                if buffer and current_agent and current_round:
                    self._add_agent_message(current_round, current_agent, '\n'.join(buffer))
                in_agent_output, current_agent, buffer = False, None, []
                continue

            if in_agent_output:
                clean_line = re.sub(r'^[│|]\s*|\s*[│|]\s*$', '', line)
                buffer.append(clean_line)
                continue
        
        if current_round:
            current_round.end_time = datetime.now().isoformat()

    def _add_agent_message(self, conversation: AgentConversation, agent_name: str, content: str):
        message_type = "output"
        formatted_content = content.strip()

        if "Structured Output - Attempting Function Call Execution" in formatted_content:
            message_type = "function_call"
        elif "Score:" in formatted_content and "Justification:" in formatted_content:
            message_type = "judgement"
        elif "No tests have been proposed" in formatted_content:
            message_type = "status_update"

        message = AgentMessage(
            datetime.now().isoformat(), agent_name, message_type, formatted_content, {'raw_content': content}
        )
        conversation.messages.append(message)
        
    def _save_session_to_file(self, session: DiagnosisSession):
        file_path = os.path.join(self.log_dir, f"{session.case_id}.json")
        with open(file_path, 'w', encoding='utf-8') as f:
            json.dump(asdict(session), f, ensure_ascii=False, indent=2)
            
        raw_output_path = os.path.join(self.log_dir, f"{session.case_id}_raw.txt")
        with open(raw_output_path, 'w', encoding='utf-8') as f:
            f.write(session.raw_output)
            
    def export_conversation_html(self, session: DiagnosisSession) -> str:
        html_path = os.path.join(self.log_dir, f"{session.case_id}_conversation.html")
        html_content = f"<html><head><title>Log - {session.case_id}</title></head><body>"
        html_content += f"<h1>Session: {session.case_id}</h1>"
        for conv in session.conversations:
            html_content += f"<h2>Round {conv.round_number}</h2>"
            for msg in conv.messages:
                html_content += f"<div><b>{msg.agent_name}</b> [{msg.message_type}]:<pre>{msg.content}</pre></div>"
        html_content += "</body></html>"
        with open(html_path, 'w', encoding='utf-8') as f:
            f.write(html_content)
        return html_path