auto-coder 0.1.397__py3-none-any.whl → 0.1.398__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of auto-coder might be problematic. Click here for more details.
- {auto_coder-0.1.397.dist-info → auto_coder-0.1.398.dist-info}/METADATA +2 -2
- {auto_coder-0.1.397.dist-info → auto_coder-0.1.398.dist-info}/RECORD +30 -11
- autocoder/auto_coder_rag.py +1 -0
- autocoder/chat_auto_coder.py +3 -0
- autocoder/common/conversations/__init__.py +84 -39
- autocoder/common/conversations/backup/__init__.py +14 -0
- autocoder/common/conversations/backup/backup_manager.py +564 -0
- autocoder/common/conversations/backup/restore_manager.py +546 -0
- autocoder/common/conversations/cache/__init__.py +16 -0
- autocoder/common/conversations/cache/base_cache.py +89 -0
- autocoder/common/conversations/cache/cache_manager.py +368 -0
- autocoder/common/conversations/cache/memory_cache.py +224 -0
- autocoder/common/conversations/config.py +195 -0
- autocoder/common/conversations/exceptions.py +72 -0
- autocoder/common/conversations/file_locker.py +145 -0
- autocoder/common/conversations/manager.py +917 -0
- autocoder/common/conversations/models.py +154 -0
- autocoder/common/conversations/search/__init__.py +15 -0
- autocoder/common/conversations/search/filter_manager.py +431 -0
- autocoder/common/conversations/search/text_searcher.py +366 -0
- autocoder/common/conversations/storage/__init__.py +16 -0
- autocoder/common/conversations/storage/base_storage.py +82 -0
- autocoder/common/conversations/storage/file_storage.py +267 -0
- autocoder/common/conversations/storage/index_manager.py +317 -0
- autocoder/rags.py +73 -23
- autocoder/version.py +1 -1
- {auto_coder-0.1.397.dist-info → auto_coder-0.1.398.dist-info}/LICENSE +0 -0
- {auto_coder-0.1.397.dist-info → auto_coder-0.1.398.dist-info}/WHEEL +0 -0
- {auto_coder-0.1.397.dist-info → auto_coder-0.1.398.dist-info}/entry_points.txt +0 -0
- {auto_coder-0.1.397.dist-info → auto_coder-0.1.398.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,917 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main conversation manager that integrates all subsystems.
|
|
3
|
+
|
|
4
|
+
This module provides the PersistConversationManager class, which serves as
|
|
5
|
+
the primary interface for conversation and message management, integrating
|
|
6
|
+
storage, caching, search, and filtering capabilities.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import time
|
|
11
|
+
import uuid
|
|
12
|
+
import contextlib
|
|
13
|
+
from typing import List, Dict, Any, Optional, Union, Generator, Tuple
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from .config import ConversationManagerConfig
|
|
17
|
+
from .exceptions import (
|
|
18
|
+
ConversationManagerError,
|
|
19
|
+
ConversationNotFoundError,
|
|
20
|
+
MessageNotFoundError,
|
|
21
|
+
ConcurrencyError,
|
|
22
|
+
DataIntegrityError
|
|
23
|
+
)
|
|
24
|
+
from .models import Conversation, ConversationMessage
|
|
25
|
+
from .file_locker import FileLocker
|
|
26
|
+
from .storage import FileStorage, IndexManager
|
|
27
|
+
from .cache import CacheManager
|
|
28
|
+
from .search import TextSearcher, FilterManager
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PersistConversationManager:
|
|
32
|
+
"""
|
|
33
|
+
Main conversation manager integrating all subsystems.
|
|
34
|
+
|
|
35
|
+
This class provides a unified interface for conversation and message management,
|
|
36
|
+
with integrated storage, caching, search, and filtering capabilities.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, config: Optional[ConversationManagerConfig] = None):
|
|
40
|
+
"""
|
|
41
|
+
Initialize the conversation manager.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
config: Configuration object for the manager
|
|
45
|
+
"""
|
|
46
|
+
self.config = config or ConversationManagerConfig()
|
|
47
|
+
|
|
48
|
+
# Initialize components
|
|
49
|
+
self._init_storage()
|
|
50
|
+
self._init_cache()
|
|
51
|
+
self._init_search()
|
|
52
|
+
self._init_locks()
|
|
53
|
+
|
|
54
|
+
# Statistics tracking
|
|
55
|
+
self._stats = {
|
|
56
|
+
'conversations_created': 0,
|
|
57
|
+
'conversations_loaded': 0,
|
|
58
|
+
'messages_added': 0,
|
|
59
|
+
'cache_hits': 0,
|
|
60
|
+
'cache_misses': 0
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
def _init_storage(self):
|
|
64
|
+
"""Initialize storage components."""
|
|
65
|
+
# Ensure storage directory exists
|
|
66
|
+
storage_path = Path(self.config.storage_path)
|
|
67
|
+
storage_path.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
|
|
69
|
+
# Initialize storage backend
|
|
70
|
+
self.storage = FileStorage(str(storage_path / "conversations"))
|
|
71
|
+
|
|
72
|
+
# Initialize index manager
|
|
73
|
+
self.index_manager = IndexManager(str(storage_path / "index"))
|
|
74
|
+
|
|
75
|
+
def _init_cache(self):
|
|
76
|
+
"""Initialize cache system."""
|
|
77
|
+
from .cache import MemoryCache
|
|
78
|
+
|
|
79
|
+
# Use simple dictionary-based caching for conversations and messages
|
|
80
|
+
self.conversation_cache = MemoryCache(
|
|
81
|
+
max_size=self.config.max_cache_size,
|
|
82
|
+
default_ttl=self.config.cache_ttl
|
|
83
|
+
)
|
|
84
|
+
self.message_cache = MemoryCache(
|
|
85
|
+
max_size=self.config.max_cache_size * 10, # More messages than conversations
|
|
86
|
+
default_ttl=self.config.cache_ttl
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def _init_search(self):
|
|
90
|
+
"""Initialize search and filtering systems."""
|
|
91
|
+
self.text_searcher = TextSearcher()
|
|
92
|
+
self.filter_manager = FilterManager()
|
|
93
|
+
|
|
94
|
+
def _init_locks(self):
|
|
95
|
+
"""Initialize locking system."""
|
|
96
|
+
lock_dir = Path(self.config.storage_path) / "locks"
|
|
97
|
+
lock_dir.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
self._lock_dir = str(lock_dir)
|
|
99
|
+
|
|
100
|
+
def _get_conversation_lock_file(self, conversation_id: str) -> str:
|
|
101
|
+
"""Get lock file path for a conversation."""
|
|
102
|
+
return os.path.join(self._lock_dir, f"{conversation_id}.lock")
|
|
103
|
+
|
|
104
|
+
@contextlib.contextmanager
|
|
105
|
+
def _conversation_lock(self, conversation_id: str, exclusive: bool = True) -> Generator[None, None, None]:
|
|
106
|
+
"""
|
|
107
|
+
Acquire lock for conversation operations.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
conversation_id: ID of the conversation to lock
|
|
111
|
+
exclusive: Whether to acquire exclusive (write) lock
|
|
112
|
+
"""
|
|
113
|
+
lock_file = self._get_conversation_lock_file(conversation_id)
|
|
114
|
+
locker = FileLocker(lock_file, timeout=self.config.lock_timeout)
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
if exclusive:
|
|
118
|
+
with locker.acquire_write_lock():
|
|
119
|
+
yield
|
|
120
|
+
else:
|
|
121
|
+
with locker.acquire_read_lock():
|
|
122
|
+
yield
|
|
123
|
+
except (ConversationNotFoundError, MessageNotFoundError):
|
|
124
|
+
# Re-raise these exceptions as-is (don't wrap in ConcurrencyError)
|
|
125
|
+
raise
|
|
126
|
+
except Exception as e:
|
|
127
|
+
raise ConcurrencyError(f"Failed to acquire lock for conversation {conversation_id}: {e}")
|
|
128
|
+
|
|
129
|
+
# Conversation Management Methods
|
|
130
|
+
|
|
131
|
+
def create_conversation(
|
|
132
|
+
self,
|
|
133
|
+
name: str,
|
|
134
|
+
description: Optional[str] = None,
|
|
135
|
+
initial_messages: Optional[List[Dict[str, Any]]] = None,
|
|
136
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
137
|
+
) -> str:
|
|
138
|
+
"""
|
|
139
|
+
Create a new conversation.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
name: Name of the conversation
|
|
143
|
+
description: Optional description
|
|
144
|
+
initial_messages: Optional list of initial messages
|
|
145
|
+
metadata: Optional metadata dictionary
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
ID of the created conversation
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
ConversationManagerError: If conversation creation fails
|
|
152
|
+
"""
|
|
153
|
+
try:
|
|
154
|
+
# Create conversation object
|
|
155
|
+
conversation = Conversation(
|
|
156
|
+
name=name,
|
|
157
|
+
description=description,
|
|
158
|
+
metadata=metadata or {}
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Add initial messages if provided
|
|
162
|
+
if initial_messages:
|
|
163
|
+
for msg_data in initial_messages:
|
|
164
|
+
message = ConversationMessage(
|
|
165
|
+
role=msg_data['role'],
|
|
166
|
+
content=msg_data['content'],
|
|
167
|
+
metadata=msg_data.get('metadata', {})
|
|
168
|
+
)
|
|
169
|
+
conversation.add_message(message)
|
|
170
|
+
|
|
171
|
+
# Save conversation with locking
|
|
172
|
+
with self._conversation_lock(conversation.conversation_id):
|
|
173
|
+
# Save to storage
|
|
174
|
+
self.storage.save_conversation(conversation.to_dict())
|
|
175
|
+
|
|
176
|
+
# Update index
|
|
177
|
+
self.index_manager.add_conversation(conversation.to_dict())
|
|
178
|
+
|
|
179
|
+
# Cache the conversation
|
|
180
|
+
self.conversation_cache.set(conversation.conversation_id, conversation.to_dict())
|
|
181
|
+
|
|
182
|
+
# Update statistics
|
|
183
|
+
self._stats['conversations_created'] += 1
|
|
184
|
+
|
|
185
|
+
return conversation.conversation_id
|
|
186
|
+
|
|
187
|
+
except Exception as e:
|
|
188
|
+
raise ConversationManagerError(f"Failed to create conversation: {e}")
|
|
189
|
+
|
|
190
|
+
def get_conversation(self, conversation_id: str) -> Optional[Dict[str, Any]]:
|
|
191
|
+
"""
|
|
192
|
+
Get a conversation by ID.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
conversation_id: ID of the conversation
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Conversation data or None if not found
|
|
199
|
+
"""
|
|
200
|
+
try:
|
|
201
|
+
# Try cache first
|
|
202
|
+
cached_conversation = self.conversation_cache.get(conversation_id)
|
|
203
|
+
if cached_conversation:
|
|
204
|
+
self._stats['cache_hits'] += 1
|
|
205
|
+
return cached_conversation
|
|
206
|
+
|
|
207
|
+
self._stats['cache_misses'] += 1
|
|
208
|
+
|
|
209
|
+
# Load from storage with read lock
|
|
210
|
+
with self._conversation_lock(conversation_id, exclusive=False):
|
|
211
|
+
conversation_data = self.storage.load_conversation(conversation_id)
|
|
212
|
+
|
|
213
|
+
if conversation_data:
|
|
214
|
+
# Cache the loaded conversation
|
|
215
|
+
self.conversation_cache.set(conversation_id, conversation_data)
|
|
216
|
+
self._stats['conversations_loaded'] += 1
|
|
217
|
+
return conversation_data
|
|
218
|
+
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
except Exception as e:
|
|
222
|
+
raise ConversationManagerError(f"Failed to get conversation {conversation_id}: {e}")
|
|
223
|
+
|
|
224
|
+
def update_conversation(
|
|
225
|
+
self,
|
|
226
|
+
conversation_id: str,
|
|
227
|
+
name: Optional[str] = None,
|
|
228
|
+
description: Optional[str] = None,
|
|
229
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
230
|
+
) -> bool:
|
|
231
|
+
"""
|
|
232
|
+
Update conversation metadata.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
conversation_id: ID of the conversation to update
|
|
236
|
+
name: New name (optional)
|
|
237
|
+
description: New description (optional)
|
|
238
|
+
metadata: New metadata (optional)
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
True if update was successful
|
|
242
|
+
|
|
243
|
+
Raises:
|
|
244
|
+
ConversationNotFoundError: If conversation doesn't exist
|
|
245
|
+
"""
|
|
246
|
+
try:
|
|
247
|
+
with self._conversation_lock(conversation_id):
|
|
248
|
+
# Load current conversation
|
|
249
|
+
conversation_data = self.storage.load_conversation(conversation_id)
|
|
250
|
+
if not conversation_data:
|
|
251
|
+
raise ConversationNotFoundError(conversation_id)
|
|
252
|
+
|
|
253
|
+
# Create conversation object and update fields
|
|
254
|
+
conversation = Conversation.from_dict(conversation_data)
|
|
255
|
+
|
|
256
|
+
if name is not None:
|
|
257
|
+
conversation.name = name
|
|
258
|
+
if description is not None:
|
|
259
|
+
conversation.description = description
|
|
260
|
+
if metadata is not None:
|
|
261
|
+
conversation.metadata.update(metadata)
|
|
262
|
+
|
|
263
|
+
conversation.updated_at = time.time()
|
|
264
|
+
|
|
265
|
+
# Save updated conversation
|
|
266
|
+
updated_data = conversation.to_dict()
|
|
267
|
+
self.storage.save_conversation(updated_data)
|
|
268
|
+
|
|
269
|
+
# Update index
|
|
270
|
+
self.index_manager.update_conversation(updated_data)
|
|
271
|
+
|
|
272
|
+
# Update cache
|
|
273
|
+
self.conversation_cache.set(conversation_id, updated_data)
|
|
274
|
+
|
|
275
|
+
return True
|
|
276
|
+
|
|
277
|
+
except ConversationNotFoundError:
|
|
278
|
+
raise
|
|
279
|
+
except Exception as e:
|
|
280
|
+
raise ConversationManagerError(f"Failed to update conversation {conversation_id}: {e}")
|
|
281
|
+
|
|
282
|
+
def delete_conversation(self, conversation_id: str) -> bool:
|
|
283
|
+
"""
|
|
284
|
+
Delete a conversation.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
conversation_id: ID of the conversation to delete
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
True if deletion was successful
|
|
291
|
+
"""
|
|
292
|
+
try:
|
|
293
|
+
with self._conversation_lock(conversation_id):
|
|
294
|
+
# Check if conversation exists
|
|
295
|
+
if not self.storage.conversation_exists(conversation_id):
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
# Delete from storage
|
|
299
|
+
self.storage.delete_conversation(conversation_id)
|
|
300
|
+
|
|
301
|
+
# Remove from index
|
|
302
|
+
self.index_manager.remove_conversation(conversation_id)
|
|
303
|
+
|
|
304
|
+
# Remove from cache
|
|
305
|
+
self.conversation_cache.delete(conversation_id)
|
|
306
|
+
|
|
307
|
+
return True
|
|
308
|
+
|
|
309
|
+
except Exception as e:
|
|
310
|
+
raise ConversationManagerError(f"Failed to delete conversation {conversation_id}: {e}")
|
|
311
|
+
|
|
312
|
+
def list_conversations(
|
|
313
|
+
self,
|
|
314
|
+
limit: Optional[int] = None,
|
|
315
|
+
offset: int = 0,
|
|
316
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
317
|
+
sort_by: str = 'updated_at',
|
|
318
|
+
sort_desc: bool = True
|
|
319
|
+
) -> List[Dict[str, Any]]:
|
|
320
|
+
"""
|
|
321
|
+
List conversations with optional filtering and sorting.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
limit: Maximum number of conversations to return
|
|
325
|
+
offset: Number of conversations to skip
|
|
326
|
+
filters: Optional filter criteria
|
|
327
|
+
sort_by: Field to sort by
|
|
328
|
+
sort_desc: Whether to sort in descending order
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
List of conversation data
|
|
332
|
+
"""
|
|
333
|
+
try:
|
|
334
|
+
# Get conversations from index
|
|
335
|
+
conversations = self.index_manager.list_conversations()
|
|
336
|
+
|
|
337
|
+
# Apply filters if provided
|
|
338
|
+
if filters:
|
|
339
|
+
conversations = self.filter_manager.apply_filters(conversations, filters)
|
|
340
|
+
|
|
341
|
+
# Sort conversations
|
|
342
|
+
conversations = self.index_manager.sort_conversations(
|
|
343
|
+
conversations, sort_by, sort_desc
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Apply pagination
|
|
347
|
+
end_idx = offset + limit if limit else None
|
|
348
|
+
return conversations[offset:end_idx]
|
|
349
|
+
|
|
350
|
+
except Exception as e:
|
|
351
|
+
raise ConversationManagerError(f"Failed to list conversations: {e}")
|
|
352
|
+
|
|
353
|
+
# Message Management Methods
|
|
354
|
+
|
|
355
|
+
def append_message(
|
|
356
|
+
self,
|
|
357
|
+
conversation_id: str,
|
|
358
|
+
role: str,
|
|
359
|
+
content: Union[str, Dict[str, Any], List[Any]],
|
|
360
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
361
|
+
) -> str:
|
|
362
|
+
"""
|
|
363
|
+
Append a message to a conversation.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
conversation_id: ID of the conversation
|
|
367
|
+
role: Role of the message sender
|
|
368
|
+
content: Message content
|
|
369
|
+
metadata: Optional message metadata
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
ID of the added message
|
|
373
|
+
|
|
374
|
+
Raises:
|
|
375
|
+
ConversationNotFoundError: If conversation doesn't exist
|
|
376
|
+
"""
|
|
377
|
+
try:
|
|
378
|
+
# Create message object
|
|
379
|
+
message = ConversationMessage(
|
|
380
|
+
role=role,
|
|
381
|
+
content=content,
|
|
382
|
+
metadata=metadata or {}
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
with self._conversation_lock(conversation_id):
|
|
386
|
+
# Load conversation
|
|
387
|
+
conversation_data = self.storage.load_conversation(conversation_id)
|
|
388
|
+
if not conversation_data:
|
|
389
|
+
raise ConversationNotFoundError(conversation_id)
|
|
390
|
+
|
|
391
|
+
# Add message to conversation
|
|
392
|
+
conversation = Conversation.from_dict(conversation_data)
|
|
393
|
+
conversation.add_message(message)
|
|
394
|
+
|
|
395
|
+
# Save updated conversation
|
|
396
|
+
updated_data = conversation.to_dict()
|
|
397
|
+
self.storage.save_conversation(updated_data)
|
|
398
|
+
|
|
399
|
+
# Update index
|
|
400
|
+
self.index_manager.update_conversation(updated_data)
|
|
401
|
+
|
|
402
|
+
# Update cache
|
|
403
|
+
self.conversation_cache.set(conversation_id, updated_data)
|
|
404
|
+
self.message_cache.set(f"{conversation_id}:{message.message_id}", message.to_dict())
|
|
405
|
+
|
|
406
|
+
# Update statistics
|
|
407
|
+
self._stats['messages_added'] += 1
|
|
408
|
+
|
|
409
|
+
return message.message_id
|
|
410
|
+
|
|
411
|
+
except ConversationNotFoundError:
|
|
412
|
+
raise
|
|
413
|
+
except Exception as e:
|
|
414
|
+
raise ConversationManagerError(f"Failed to append message to conversation {conversation_id}: {e}")
|
|
415
|
+
|
|
416
|
+
def append_messages(
|
|
417
|
+
self,
|
|
418
|
+
conversation_id: str,
|
|
419
|
+
messages: List[Dict[str, Any]]
|
|
420
|
+
) -> List[str]:
|
|
421
|
+
"""
|
|
422
|
+
Append multiple messages to a conversation.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
conversation_id: ID of the conversation
|
|
426
|
+
messages: List of message data dictionaries
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
List of message IDs
|
|
430
|
+
"""
|
|
431
|
+
message_ids = []
|
|
432
|
+
|
|
433
|
+
for msg_data in messages:
|
|
434
|
+
message_id = self.append_message(
|
|
435
|
+
conversation_id,
|
|
436
|
+
msg_data['role'],
|
|
437
|
+
msg_data['content'],
|
|
438
|
+
msg_data.get('metadata')
|
|
439
|
+
)
|
|
440
|
+
message_ids.append(message_id)
|
|
441
|
+
|
|
442
|
+
return message_ids
|
|
443
|
+
|
|
444
|
+
def get_messages(
|
|
445
|
+
self,
|
|
446
|
+
conversation_id: str,
|
|
447
|
+
limit: Optional[int] = None,
|
|
448
|
+
offset: int = 0,
|
|
449
|
+
message_ids: Optional[List[str]] = None
|
|
450
|
+
) -> List[Dict[str, Any]]:
|
|
451
|
+
"""
|
|
452
|
+
Get messages from a conversation.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
conversation_id: ID of the conversation
|
|
456
|
+
limit: Maximum number of messages to return
|
|
457
|
+
offset: Number of messages to skip
|
|
458
|
+
message_ids: Optional list of specific message IDs
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
List of message data
|
|
462
|
+
"""
|
|
463
|
+
try:
|
|
464
|
+
# Get conversation
|
|
465
|
+
conversation_data = self.get_conversation(conversation_id)
|
|
466
|
+
if not conversation_data:
|
|
467
|
+
raise ConversationNotFoundError(conversation_id)
|
|
468
|
+
|
|
469
|
+
messages = conversation_data.get('messages', [])
|
|
470
|
+
|
|
471
|
+
# Filter by message IDs if provided
|
|
472
|
+
if message_ids:
|
|
473
|
+
id_set = set(message_ids)
|
|
474
|
+
messages = [msg for msg in messages if msg.get('message_id') in id_set]
|
|
475
|
+
|
|
476
|
+
# Apply pagination
|
|
477
|
+
end_idx = offset + limit if limit else None
|
|
478
|
+
return messages[offset:end_idx]
|
|
479
|
+
|
|
480
|
+
except ConversationNotFoundError:
|
|
481
|
+
raise
|
|
482
|
+
except Exception as e:
|
|
483
|
+
raise ConversationManagerError(f"Failed to get messages from conversation {conversation_id}: {e}")
|
|
484
|
+
|
|
485
|
+
def get_message(
|
|
486
|
+
self,
|
|
487
|
+
conversation_id: str,
|
|
488
|
+
message_id: str
|
|
489
|
+
) -> Optional[Dict[str, Any]]:
|
|
490
|
+
"""
|
|
491
|
+
Get a specific message.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
conversation_id: ID of the conversation
|
|
495
|
+
message_id: ID of the message
|
|
496
|
+
|
|
497
|
+
Returns:
|
|
498
|
+
Message data or None if not found
|
|
499
|
+
"""
|
|
500
|
+
try:
|
|
501
|
+
# Try cache first
|
|
502
|
+
cached_message = self.message_cache.get(f"{conversation_id}:{message_id}")
|
|
503
|
+
if cached_message:
|
|
504
|
+
return cached_message
|
|
505
|
+
|
|
506
|
+
# Get from conversation
|
|
507
|
+
conversation_data = self.get_conversation(conversation_id)
|
|
508
|
+
if not conversation_data:
|
|
509
|
+
return None
|
|
510
|
+
|
|
511
|
+
# Find message
|
|
512
|
+
for message in conversation_data.get('messages', []):
|
|
513
|
+
if message.get('message_id') == message_id:
|
|
514
|
+
# Cache the message
|
|
515
|
+
self.message_cache.set(f"{conversation_id}:{message_id}", message)
|
|
516
|
+
return message
|
|
517
|
+
|
|
518
|
+
return None
|
|
519
|
+
|
|
520
|
+
except Exception as e:
|
|
521
|
+
raise ConversationManagerError(f"Failed to get message {message_id}: {e}")
|
|
522
|
+
|
|
523
|
+
def update_message(
|
|
524
|
+
self,
|
|
525
|
+
conversation_id: str,
|
|
526
|
+
message_id: str,
|
|
527
|
+
content: Optional[Union[str, Dict[str, Any], List[Any]]] = None,
|
|
528
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
529
|
+
) -> bool:
|
|
530
|
+
"""
|
|
531
|
+
Update a message.
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
conversation_id: ID of the conversation
|
|
535
|
+
message_id: ID of the message to update
|
|
536
|
+
content: New content (optional)
|
|
537
|
+
metadata: New metadata (optional)
|
|
538
|
+
|
|
539
|
+
Returns:
|
|
540
|
+
True if update was successful
|
|
541
|
+
"""
|
|
542
|
+
try:
|
|
543
|
+
with self._conversation_lock(conversation_id):
|
|
544
|
+
# Load conversation
|
|
545
|
+
conversation_data = self.storage.load_conversation(conversation_id)
|
|
546
|
+
if not conversation_data:
|
|
547
|
+
raise ConversationNotFoundError(conversation_id)
|
|
548
|
+
|
|
549
|
+
conversation = Conversation.from_dict(conversation_data)
|
|
550
|
+
|
|
551
|
+
# Find and update message
|
|
552
|
+
for i, message_data in enumerate(conversation.messages):
|
|
553
|
+
msg = ConversationMessage.from_dict(message_data)
|
|
554
|
+
if msg.message_id == message_id:
|
|
555
|
+
# Update message fields
|
|
556
|
+
if content is not None:
|
|
557
|
+
msg.content = content
|
|
558
|
+
if metadata is not None:
|
|
559
|
+
msg.metadata.update(metadata)
|
|
560
|
+
|
|
561
|
+
# Update timestamp
|
|
562
|
+
msg.timestamp = time.time()
|
|
563
|
+
|
|
564
|
+
# Replace in conversation
|
|
565
|
+
conversation.messages[i] = msg.to_dict()
|
|
566
|
+
conversation.updated_at = time.time()
|
|
567
|
+
|
|
568
|
+
# Save updated conversation
|
|
569
|
+
updated_data = conversation.to_dict()
|
|
570
|
+
self.storage.save_conversation(updated_data)
|
|
571
|
+
|
|
572
|
+
# Update index
|
|
573
|
+
self.index_manager.update_conversation(updated_data)
|
|
574
|
+
|
|
575
|
+
# Update caches
|
|
576
|
+
self.conversation_cache.set(conversation_id, updated_data)
|
|
577
|
+
self.message_cache.set(f"{conversation_id}:{message_id}", msg.to_dict())
|
|
578
|
+
|
|
579
|
+
return True
|
|
580
|
+
|
|
581
|
+
raise MessageNotFoundError(message_id)
|
|
582
|
+
|
|
583
|
+
except (ConversationNotFoundError, MessageNotFoundError):
|
|
584
|
+
raise
|
|
585
|
+
except Exception as e:
|
|
586
|
+
raise ConversationManagerError(f"Failed to update message {message_id}: {e}")
|
|
587
|
+
|
|
588
|
+
def delete_message(
|
|
589
|
+
self,
|
|
590
|
+
conversation_id: str,
|
|
591
|
+
message_id: str
|
|
592
|
+
) -> bool:
|
|
593
|
+
"""
|
|
594
|
+
Delete a message from a conversation.
|
|
595
|
+
|
|
596
|
+
Args:
|
|
597
|
+
conversation_id: ID of the conversation
|
|
598
|
+
message_id: ID of the message to delete
|
|
599
|
+
|
|
600
|
+
Returns:
|
|
601
|
+
True if deletion was successful
|
|
602
|
+
"""
|
|
603
|
+
try:
|
|
604
|
+
with self._conversation_lock(conversation_id):
|
|
605
|
+
# Load conversation
|
|
606
|
+
conversation_data = self.storage.load_conversation(conversation_id)
|
|
607
|
+
if not conversation_data:
|
|
608
|
+
raise ConversationNotFoundError(conversation_id)
|
|
609
|
+
|
|
610
|
+
conversation = Conversation.from_dict(conversation_data)
|
|
611
|
+
|
|
612
|
+
# Find and remove message
|
|
613
|
+
original_count = len(conversation.messages)
|
|
614
|
+
conversation.messages = [
|
|
615
|
+
msg for msg in conversation.messages
|
|
616
|
+
if msg.get('message_id') != message_id
|
|
617
|
+
]
|
|
618
|
+
|
|
619
|
+
if len(conversation.messages) == original_count:
|
|
620
|
+
raise MessageNotFoundError(message_id)
|
|
621
|
+
|
|
622
|
+
conversation.updated_at = time.time()
|
|
623
|
+
|
|
624
|
+
# Save updated conversation
|
|
625
|
+
updated_data = conversation.to_dict()
|
|
626
|
+
self.storage.save_conversation(updated_data)
|
|
627
|
+
|
|
628
|
+
# Update index
|
|
629
|
+
self.index_manager.update_conversation(updated_data)
|
|
630
|
+
|
|
631
|
+
# Update caches
|
|
632
|
+
self.conversation_cache.set(conversation_id, updated_data)
|
|
633
|
+
self.message_cache.delete(f"{conversation_id}:{message_id}")
|
|
634
|
+
|
|
635
|
+
return True
|
|
636
|
+
|
|
637
|
+
except (ConversationNotFoundError, MessageNotFoundError):
|
|
638
|
+
raise
|
|
639
|
+
except Exception as e:
|
|
640
|
+
raise ConversationManagerError(f"Failed to delete message {message_id}: {e}")
|
|
641
|
+
|
|
642
|
+
def delete_message_pair(
|
|
643
|
+
self,
|
|
644
|
+
conversation_id: str,
|
|
645
|
+
user_message_id: str
|
|
646
|
+
) -> bool:
|
|
647
|
+
"""
|
|
648
|
+
Delete a user message and its corresponding assistant reply.
|
|
649
|
+
|
|
650
|
+
Args:
|
|
651
|
+
conversation_id: ID of the conversation
|
|
652
|
+
user_message_id: ID of the user message
|
|
653
|
+
|
|
654
|
+
Returns:
|
|
655
|
+
True if deletion was successful
|
|
656
|
+
"""
|
|
657
|
+
try:
|
|
658
|
+
with self._conversation_lock(conversation_id):
|
|
659
|
+
# Load conversation
|
|
660
|
+
conversation_data = self.storage.load_conversation(conversation_id)
|
|
661
|
+
if not conversation_data:
|
|
662
|
+
raise ConversationNotFoundError(conversation_id)
|
|
663
|
+
|
|
664
|
+
conversation = Conversation.from_dict(conversation_data)
|
|
665
|
+
|
|
666
|
+
# Find user message and next assistant message
|
|
667
|
+
messages_to_remove = []
|
|
668
|
+
for i, message in enumerate(conversation.messages):
|
|
669
|
+
if message.get('message_id') == user_message_id:
|
|
670
|
+
messages_to_remove.append(i)
|
|
671
|
+
# Check if next message is assistant reply
|
|
672
|
+
if (i + 1 < len(conversation.messages) and
|
|
673
|
+
conversation.messages[i + 1].get('role') == 'assistant'):
|
|
674
|
+
messages_to_remove.append(i + 1)
|
|
675
|
+
break
|
|
676
|
+
|
|
677
|
+
if not messages_to_remove:
|
|
678
|
+
raise MessageNotFoundError(user_message_id)
|
|
679
|
+
|
|
680
|
+
# Remove messages (in reverse order to maintain indices)
|
|
681
|
+
assistant_message_id = None
|
|
682
|
+
for idx in reversed(messages_to_remove):
|
|
683
|
+
if idx == messages_to_remove[-1] and len(messages_to_remove) > 1:
|
|
684
|
+
assistant_message_id = conversation.messages[idx].get('message_id')
|
|
685
|
+
del conversation.messages[idx]
|
|
686
|
+
|
|
687
|
+
conversation.updated_at = time.time()
|
|
688
|
+
|
|
689
|
+
# Save updated conversation
|
|
690
|
+
updated_data = conversation.to_dict()
|
|
691
|
+
self.storage.save_conversation(updated_data)
|
|
692
|
+
|
|
693
|
+
# Update index
|
|
694
|
+
self.index_manager.update_conversation(updated_data)
|
|
695
|
+
|
|
696
|
+
# Update caches
|
|
697
|
+
self.conversation_cache.set(conversation_id, updated_data)
|
|
698
|
+
self.message_cache.delete(f"{conversation_id}:{user_message_id}")
|
|
699
|
+
if assistant_message_id:
|
|
700
|
+
self.message_cache.delete(f"{conversation_id}:{assistant_message_id}")
|
|
701
|
+
|
|
702
|
+
return True
|
|
703
|
+
|
|
704
|
+
except (ConversationNotFoundError, MessageNotFoundError):
|
|
705
|
+
raise
|
|
706
|
+
except Exception as e:
|
|
707
|
+
raise ConversationManagerError(f"Failed to delete message pair {user_message_id}: {e}")
|
|
708
|
+
|
|
709
|
+
# Search and Filter Methods
|
|
710
|
+
|
|
711
|
+
def search_conversations(
|
|
712
|
+
self,
|
|
713
|
+
query: str,
|
|
714
|
+
search_in_messages: bool = True,
|
|
715
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
716
|
+
max_results: Optional[int] = None,
|
|
717
|
+
min_score: float = 0.0
|
|
718
|
+
) -> List[Dict[str, Any]]:
|
|
719
|
+
"""
|
|
720
|
+
Search conversations.
|
|
721
|
+
|
|
722
|
+
Args:
|
|
723
|
+
query: Search query
|
|
724
|
+
search_in_messages: Whether to search in message content
|
|
725
|
+
filters: Optional filter criteria
|
|
726
|
+
max_results: Maximum number of results
|
|
727
|
+
min_score: Minimum relevance score
|
|
728
|
+
|
|
729
|
+
Returns:
|
|
730
|
+
List of matching conversations with scores
|
|
731
|
+
"""
|
|
732
|
+
try:
|
|
733
|
+
# Get all conversations
|
|
734
|
+
conversations = self.index_manager.list_conversations()
|
|
735
|
+
|
|
736
|
+
# Apply filters first if provided
|
|
737
|
+
if filters:
|
|
738
|
+
conversations = self.filter_manager.apply_filters(conversations, filters)
|
|
739
|
+
|
|
740
|
+
# If search_in_messages is True, load full conversation data
|
|
741
|
+
if search_in_messages:
|
|
742
|
+
full_conversations = []
|
|
743
|
+
for conv in conversations:
|
|
744
|
+
conv_id = conv.get('conversation_id')
|
|
745
|
+
if conv_id:
|
|
746
|
+
full_conv = self.get_conversation(conv_id)
|
|
747
|
+
if full_conv:
|
|
748
|
+
full_conversations.append(full_conv)
|
|
749
|
+
conversations = full_conversations
|
|
750
|
+
|
|
751
|
+
# Perform text search
|
|
752
|
+
results = self.text_searcher.search_conversations(
|
|
753
|
+
query, conversations, max_results, min_score
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
return [{'conversation': conv, 'score': score} for conv, score in results]
|
|
757
|
+
|
|
758
|
+
except Exception as e:
|
|
759
|
+
raise ConversationManagerError(f"Failed to search conversations: {e}")
|
|
760
|
+
|
|
761
|
+
def search_messages(
|
|
762
|
+
self,
|
|
763
|
+
conversation_id: str,
|
|
764
|
+
query: str,
|
|
765
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
766
|
+
max_results: Optional[int] = None,
|
|
767
|
+
min_score: float = 0.0
|
|
768
|
+
) -> List[Dict[str, Any]]:
|
|
769
|
+
"""
|
|
770
|
+
Search messages in a conversation.
|
|
771
|
+
|
|
772
|
+
Args:
|
|
773
|
+
conversation_id: ID of the conversation
|
|
774
|
+
query: Search query
|
|
775
|
+
filters: Optional filter criteria
|
|
776
|
+
max_results: Maximum number of results
|
|
777
|
+
min_score: Minimum relevance score
|
|
778
|
+
|
|
779
|
+
Returns:
|
|
780
|
+
List of matching messages with scores
|
|
781
|
+
"""
|
|
782
|
+
try:
|
|
783
|
+
# Get conversation messages
|
|
784
|
+
messages = self.get_messages(conversation_id)
|
|
785
|
+
|
|
786
|
+
# Apply filters if provided
|
|
787
|
+
if filters:
|
|
788
|
+
messages = self.filter_manager.apply_filters(messages, filters)
|
|
789
|
+
|
|
790
|
+
# Perform text search
|
|
791
|
+
results = self.text_searcher.search_messages(
|
|
792
|
+
query, messages, max_results, min_score
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
return [{'message': msg, 'score': score} for msg, score in results]
|
|
796
|
+
|
|
797
|
+
except Exception as e:
|
|
798
|
+
raise ConversationManagerError(f"Failed to search messages: {e}")
|
|
799
|
+
|
|
800
|
+
# Utility and Management Methods
|
|
801
|
+
|
|
802
|
+
def get_statistics(self) -> Dict[str, Any]:
|
|
803
|
+
"""
|
|
804
|
+
Get manager statistics.
|
|
805
|
+
|
|
806
|
+
Returns:
|
|
807
|
+
Dictionary with statistics
|
|
808
|
+
"""
|
|
809
|
+
cache_stats = {
|
|
810
|
+
'conversation_cache_size': self.conversation_cache.size(),
|
|
811
|
+
'message_cache_size': self.message_cache.size()
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return {
|
|
815
|
+
**self._stats,
|
|
816
|
+
'cache_stats': cache_stats,
|
|
817
|
+
'total_conversations': len(self.index_manager.list_conversations()),
|
|
818
|
+
'storage_path': self.config.storage_path
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
def health_check(self) -> Dict[str, Any]:
|
|
822
|
+
"""
|
|
823
|
+
Perform health check of all components.
|
|
824
|
+
|
|
825
|
+
Returns:
|
|
826
|
+
Health status dictionary
|
|
827
|
+
"""
|
|
828
|
+
health_status = {
|
|
829
|
+
'status': 'healthy',
|
|
830
|
+
'storage': True,
|
|
831
|
+
'cache': True,
|
|
832
|
+
'index': True,
|
|
833
|
+
'search': True,
|
|
834
|
+
'issues': []
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
try:
|
|
838
|
+
# Check storage
|
|
839
|
+
if not os.path.exists(self.config.storage_path):
|
|
840
|
+
health_status['storage'] = False
|
|
841
|
+
health_status['issues'].append('Storage directory not accessible')
|
|
842
|
+
|
|
843
|
+
# Check cache
|
|
844
|
+
conv_cache_size = self.conversation_cache.size()
|
|
845
|
+
if conv_cache_size > self.config.max_cache_size:
|
|
846
|
+
health_status['issues'].append('Conversation cache size exceeds limit')
|
|
847
|
+
|
|
848
|
+
# Check index consistency
|
|
849
|
+
try:
|
|
850
|
+
conversations = self.index_manager.list_conversations()
|
|
851
|
+
health_status['total_conversations'] = len(conversations)
|
|
852
|
+
except Exception as e:
|
|
853
|
+
health_status['index'] = False
|
|
854
|
+
health_status['issues'].append(f'Index error: {e}')
|
|
855
|
+
|
|
856
|
+
# Determine overall status
|
|
857
|
+
if not all([health_status['storage'], health_status['cache'],
|
|
858
|
+
health_status['index'], health_status['search']]):
|
|
859
|
+
health_status['status'] = 'degraded'
|
|
860
|
+
|
|
861
|
+
if health_status['issues']:
|
|
862
|
+
health_status['status'] = 'warning' if health_status['status'] == 'healthy' else health_status['status']
|
|
863
|
+
|
|
864
|
+
except Exception as e:
|
|
865
|
+
health_status['status'] = 'unhealthy'
|
|
866
|
+
health_status['issues'].append(f'Health check failed: {e}')
|
|
867
|
+
|
|
868
|
+
return health_status
|
|
869
|
+
|
|
870
|
+
@contextlib.contextmanager
|
|
871
|
+
def transaction(self, conversation_id: str) -> Generator[None, None, None]:
|
|
872
|
+
"""
|
|
873
|
+
Transaction context manager for atomic operations.
|
|
874
|
+
|
|
875
|
+
Args:
|
|
876
|
+
conversation_id: ID of the conversation for the transaction
|
|
877
|
+
"""
|
|
878
|
+
with self._conversation_lock(conversation_id):
|
|
879
|
+
try:
|
|
880
|
+
yield
|
|
881
|
+
except Exception:
|
|
882
|
+
# In a full implementation, we would rollback changes here
|
|
883
|
+
# For now, we just re-raise the exception
|
|
884
|
+
raise
|
|
885
|
+
|
|
886
|
+
def clear_cache(self):
|
|
887
|
+
"""Clear all caches."""
|
|
888
|
+
self.conversation_cache.clear()
|
|
889
|
+
self.message_cache.clear()
|
|
890
|
+
|
|
891
|
+
def rebuild_index(self):
|
|
892
|
+
"""Rebuild the conversation index from storage."""
|
|
893
|
+
try:
|
|
894
|
+
# Clear existing index
|
|
895
|
+
self.index_manager._index.clear()
|
|
896
|
+
|
|
897
|
+
# Load all conversations from storage and rebuild index
|
|
898
|
+
conversation_ids = self.storage.list_conversations()
|
|
899
|
+
|
|
900
|
+
for conv_id in conversation_ids:
|
|
901
|
+
conversation_data = self.storage.load_conversation(conv_id)
|
|
902
|
+
if conversation_data:
|
|
903
|
+
self.index_manager.add_conversation(conversation_data)
|
|
904
|
+
|
|
905
|
+
except Exception as e:
|
|
906
|
+
raise ConversationManagerError(f"Failed to rebuild index: {e}")
|
|
907
|
+
|
|
908
|
+
def close(self):
|
|
909
|
+
"""Clean up resources."""
|
|
910
|
+
# Clear caches
|
|
911
|
+
self.clear_cache()
|
|
912
|
+
|
|
913
|
+
# Save any pending index changes
|
|
914
|
+
try:
|
|
915
|
+
self.index_manager._save_index()
|
|
916
|
+
except Exception:
|
|
917
|
+
pass # Ignore errors during cleanup
|