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.

Files changed (30) hide show
  1. {auto_coder-0.1.397.dist-info → auto_coder-0.1.398.dist-info}/METADATA +2 -2
  2. {auto_coder-0.1.397.dist-info → auto_coder-0.1.398.dist-info}/RECORD +30 -11
  3. autocoder/auto_coder_rag.py +1 -0
  4. autocoder/chat_auto_coder.py +3 -0
  5. autocoder/common/conversations/__init__.py +84 -39
  6. autocoder/common/conversations/backup/__init__.py +14 -0
  7. autocoder/common/conversations/backup/backup_manager.py +564 -0
  8. autocoder/common/conversations/backup/restore_manager.py +546 -0
  9. autocoder/common/conversations/cache/__init__.py +16 -0
  10. autocoder/common/conversations/cache/base_cache.py +89 -0
  11. autocoder/common/conversations/cache/cache_manager.py +368 -0
  12. autocoder/common/conversations/cache/memory_cache.py +224 -0
  13. autocoder/common/conversations/config.py +195 -0
  14. autocoder/common/conversations/exceptions.py +72 -0
  15. autocoder/common/conversations/file_locker.py +145 -0
  16. autocoder/common/conversations/manager.py +917 -0
  17. autocoder/common/conversations/models.py +154 -0
  18. autocoder/common/conversations/search/__init__.py +15 -0
  19. autocoder/common/conversations/search/filter_manager.py +431 -0
  20. autocoder/common/conversations/search/text_searcher.py +366 -0
  21. autocoder/common/conversations/storage/__init__.py +16 -0
  22. autocoder/common/conversations/storage/base_storage.py +82 -0
  23. autocoder/common/conversations/storage/file_storage.py +267 -0
  24. autocoder/common/conversations/storage/index_manager.py +317 -0
  25. autocoder/rags.py +73 -23
  26. autocoder/version.py +1 -1
  27. {auto_coder-0.1.397.dist-info → auto_coder-0.1.398.dist-info}/LICENSE +0 -0
  28. {auto_coder-0.1.397.dist-info → auto_coder-0.1.398.dist-info}/WHEEL +0 -0
  29. {auto_coder-0.1.397.dist-info → auto_coder-0.1.398.dist-info}/entry_points.txt +0 -0
  30. {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