File size: 11,113 Bytes
85e8660
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
"""

Chat Manager Module

Handles conversation history and chat state management

"""

import logging
import json
from typing import List, Dict, Any, Optional
from datetime import datetime
from dataclasses import dataclass, asdict
from pathlib import Path

@dataclass
class ChatMessage:
    """Represents a single chat message"""
    role: str  # 'user' or 'assistant'
    content: str
    timestamp: str
    metadata: Optional[Dict[str, Any]] = None

class ChatManager:
    """Manages chat history and conversation state"""
    
    def __init__(self, max_history: int = 100):
        """

        Initialize chat manager

        

        Args:

            max_history: Maximum number of messages to keep in history

        """
        self.max_history = max_history
        self.messages: List[ChatMessage] = []
        
        # Configure logging
        logging.basicConfig(level=logging.INFO)
        self.logger = logging.getLogger(__name__)
    
    def add_message(self, role: str, content: str, metadata: Optional[Dict[str, Any]] = None):
        """

        Add a new message to the chat history

        

        Args:

            role: Message role ('user' or 'assistant')

            content: Message content

            metadata: Optional metadata

        """
        try:
            # Create message
            message = ChatMessage(
                role=role,
                content=content,
                timestamp=datetime.now().isoformat(),
                metadata=metadata or {}
            )
            
            # Add to history
            self.messages.append(message)
            
            # Trim history if needed
            if len(self.messages) > self.max_history:
                self.messages = self.messages[-self.max_history:]
            
            self.logger.info(f"Added {role} message to chat history")
            
        except Exception as e:
            self.logger.error(f"Error adding message: {e}")
            raise
    
    def get_messages(self, limit: Optional[int] = None) -> List[ChatMessage]:
        """

        Get chat messages

        

        Args:

            limit: Maximum number of messages to return

            

        Returns:

            List of chat messages

        """
        if limit is None:
            return self.messages.copy()
        
        return self.messages[-limit:]
    
    def get_conversation_summary(self) -> str:
        """

        Get a summary of the conversation

        

        Returns:

            Conversation summary

        """
        if not self.messages:
            return "No conversation history"
        
        user_messages = [msg for msg in self.messages if msg.role == "user"]
        assistant_messages = [msg for msg in self.messages if msg.role == "assistant"]
        
        summary = f"Conversation Summary:\n"
        summary += f"- Total messages: {len(self.messages)}\n"
        summary += f"- User messages: {len(user_messages)}\n"
        summary += f"- Assistant messages: {len(assistant_messages)}\n"
        summary += f"- Started: {self.messages[0].timestamp}\n"
        summary += f"- Last activity: {self.messages[-1].timestamp}\n"
        
        return summary
    
    def clear_history(self):
        """Clear all chat history"""
        self.messages.clear()
        self.logger.info("Chat history cleared")
    
    def remove_message(self, index: int):
        """

        Remove a specific message by index

        

        Args:

            index: Index of message to remove

        """
        try:
            if 0 <= index < len(self.messages):
                removed = self.messages.pop(index)
                self.logger.info(f"Removed message at index {index}")
                return removed
            else:
                raise IndexError("Message index out of range")
        except Exception as e:
            self.logger.error(f"Error removing message: {e}")
            raise
    
    def get_context_for_rag(self, max_messages: int = 5) -> str:
        """

        Get recent conversation context for RAG

        

        Args:

            max_messages: Maximum number of recent messages to include

            

        Returns:

            Formatted conversation context

        """
        if not self.messages:
            return ""
        
        # Get recent messages
        recent_messages = self.messages[-max_messages:]
        
        # Format context
        context_parts = []
        for msg in recent_messages:
            role_label = "User" if msg.role == "user" else "Assistant"
            context_parts.append(f"{role_label}: {msg.content}")
        
        return "\n".join(context_parts)
    
    def save_conversation(self, filepath: str):
        """

        Save conversation to file

        

        Args:

            filepath: Path to save the conversation

        """
        try:
            # Convert messages to dictionaries
            messages_dict = [asdict(msg) for msg in self.messages]
            
            # Create conversation data
            conversation_data = {
                "metadata": {
                    "created_at": datetime.now().isoformat(),
                    "total_messages": len(self.messages),
                    "max_history": self.max_history
                },
                "messages": messages_dict
            }
            
            # Save to file
            with open(filepath, 'w', encoding='utf-8') as f:
                json.dump(conversation_data, f, indent=2, ensure_ascii=False)
            
            self.logger.info(f"Conversation saved to {filepath}")
            
        except Exception as e:
            self.logger.error(f"Error saving conversation: {e}")
            raise
    
    def load_conversation(self, filepath: str):
        """

        Load conversation from file

        

        Args:

            filepath: Path to load the conversation from

        """
        try:
            # Load from file
            with open(filepath, 'r', encoding='utf-8') as f:
                conversation_data = json.load(f)
            
            # Clear current history
            self.messages.clear()
            
            # Load messages
            for msg_data in conversation_data.get("messages", []):
                message = ChatMessage(
                    role=msg_data["role"],
                    content=msg_data["content"],
                    timestamp=msg_data["timestamp"],
                    metadata=msg_data.get("metadata")
                )
                self.messages.append(message)
            
            self.logger.info(f"Conversation loaded from {filepath}")
            
        except Exception as e:
            self.logger.error(f"Error loading conversation: {e}")
            raise
    
    def export_conversation_text(self, filepath: str):
        """

        Export conversation as plain text

        

        Args:

            filepath: Path to save the text file

        """
        try:
            with open(filepath, 'w', encoding='utf-8') as f:
                f.write("PDFPal Conversation Export\n")
                f.write("=" * 50 + "\n\n")
                
                for msg in self.messages:
                    role_label = "User" if msg.role == "user" else "Assistant"
                    timestamp = datetime.fromisoformat(msg.timestamp).strftime("%Y-%m-%d %H:%M:%S")
                    f.write(f"[{timestamp}] {role_label}:\n{msg.content}\n\n")
            
            self.logger.info(f"Conversation exported to {filepath}")
            
        except Exception as e:
            self.logger.error(f"Error exporting conversation: {e}")
            raise
    
    def get_statistics(self) -> Dict[str, Any]:
        """

        Get chat statistics

        

        Returns:

            Dictionary with chat statistics

        """
        if not self.messages:
            return {
                "total_messages": 0,
                "user_messages": 0,
                "assistant_messages": 0,
                "avg_message_length": 0,
                "conversation_duration": "0:00:00"
            }
        
        user_messages = [msg for msg in self.messages if msg.role == "user"]
        assistant_messages = [msg for msg in self.messages if msg.role == "assistant"]
        
        # Calculate average message length
        total_length = sum(len(msg.content) for msg in self.messages)
        avg_length = total_length / len(self.messages) if self.messages else 0
        
        # Calculate conversation duration
        start_time = datetime.fromisoformat(self.messages[0].timestamp)
        end_time = datetime.fromisoformat(self.messages[-1].timestamp)
        duration = end_time - start_time
        
        return {
            "total_messages": len(self.messages),
            "user_messages": len(user_messages),
            "assistant_messages": len(assistant_messages),
            "avg_message_length": round(avg_length, 2),
            "conversation_duration": str(duration),
            "start_time": self.messages[0].timestamp,
            "end_time": self.messages[-1].timestamp
        }
    
    def search_messages(self, query: str, case_sensitive: bool = False) -> List[ChatMessage]:
        """

        Search messages by content

        

        Args:

            query: Search query

            case_sensitive: Whether search should be case sensitive

            

        Returns:

            List of matching messages

        """
        matching_messages = []
        
        for msg in self.messages:
            content = msg.content
            if not case_sensitive:
                content = content.lower()
                query = query.lower()
            
            if query in content:
                matching_messages.append(msg)
        
        return matching_messages 

    def get_gradio_chat_history(self) -> list:
        """

        Return chat history as a list of [user, assistant] pairs for Gradio Chatbot.

        Each pair is [user_message, assistant_message].

        Handles cases where the conversation may start or end with a user message without a reply.

        """
        pairs = []
        current_pair = []
        for msg in self.messages:
            if msg.role == "user":
                if current_pair:  # If previous pair is incomplete, append it
                    pairs.append(current_pair)
                current_pair = [msg.content, None]
            elif msg.role == "assistant":
                if current_pair:
                    current_pair[1] = msg.content
                    pairs.append(current_pair)
                    current_pair = []
        if current_pair:  # Handle any trailing user message
            pairs.append(current_pair)
        return pairs