auto-coder 0.1.397__py3-none-any.whl → 0.1.399__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 (85) hide show
  1. auto_coder-0.1.399.dist-info/METADATA +396 -0
  2. {auto_coder-0.1.397.dist-info → auto_coder-0.1.399.dist-info}/RECORD +81 -28
  3. {auto_coder-0.1.397.dist-info → auto_coder-0.1.399.dist-info}/WHEEL +1 -1
  4. {auto_coder-0.1.397.dist-info → auto_coder-0.1.399.dist-info}/entry_points.txt +2 -0
  5. autocoder/agent/base_agentic/base_agent.py +2 -2
  6. autocoder/agent/base_agentic/tools/replace_in_file_tool_resolver.py +1 -1
  7. autocoder/agent/entry_command_agent/__init__.py +29 -0
  8. autocoder/agent/entry_command_agent/auto_tool.py +61 -0
  9. autocoder/agent/entry_command_agent/chat.py +475 -0
  10. autocoder/agent/entry_command_agent/designer.py +53 -0
  11. autocoder/agent/entry_command_agent/generate_command.py +50 -0
  12. autocoder/agent/entry_command_agent/project_reader.py +58 -0
  13. autocoder/agent/entry_command_agent/voice2text.py +71 -0
  14. autocoder/auto_coder.py +23 -548
  15. autocoder/auto_coder_rag.py +1 -0
  16. autocoder/auto_coder_runner.py +510 -8
  17. autocoder/chat/rules_command.py +1 -1
  18. autocoder/chat_auto_coder.py +8 -0
  19. autocoder/common/ac_style_command_parser/__init__.py +15 -0
  20. autocoder/common/ac_style_command_parser/example.py +7 -0
  21. autocoder/{command_parser.py → common/ac_style_command_parser/parser.py} +1 -33
  22. autocoder/common/ac_style_command_parser/test_parser.py +516 -0
  23. autocoder/common/command_completer_v2.py +1 -1
  24. autocoder/common/command_file_manager/examples.py +22 -8
  25. autocoder/common/command_file_manager/manager.py +37 -6
  26. autocoder/common/conversations/__init__.py +84 -39
  27. autocoder/common/conversations/backup/__init__.py +14 -0
  28. autocoder/common/conversations/backup/backup_manager.py +564 -0
  29. autocoder/common/conversations/backup/restore_manager.py +546 -0
  30. autocoder/common/conversations/cache/__init__.py +16 -0
  31. autocoder/common/conversations/cache/base_cache.py +89 -0
  32. autocoder/common/conversations/cache/cache_manager.py +368 -0
  33. autocoder/common/conversations/cache/memory_cache.py +224 -0
  34. autocoder/common/conversations/config.py +195 -0
  35. autocoder/common/conversations/exceptions.py +72 -0
  36. autocoder/common/conversations/file_locker.py +145 -0
  37. autocoder/common/conversations/get_conversation_manager.py +143 -0
  38. autocoder/common/conversations/manager.py +1028 -0
  39. autocoder/common/conversations/models.py +154 -0
  40. autocoder/common/conversations/search/__init__.py +15 -0
  41. autocoder/common/conversations/search/filter_manager.py +431 -0
  42. autocoder/common/conversations/search/text_searcher.py +366 -0
  43. autocoder/common/conversations/storage/__init__.py +16 -0
  44. autocoder/common/conversations/storage/base_storage.py +82 -0
  45. autocoder/common/conversations/storage/file_storage.py +267 -0
  46. autocoder/common/conversations/storage/index_manager.py +406 -0
  47. autocoder/common/v2/agent/agentic_edit.py +131 -18
  48. autocoder/common/v2/agent/agentic_edit_types.py +10 -0
  49. autocoder/common/v2/code_auto_generate_editblock.py +10 -2
  50. autocoder/dispacher/__init__.py +10 -0
  51. autocoder/rags.py +73 -50
  52. autocoder/run_context.py +1 -0
  53. autocoder/sdk/__init__.py +188 -0
  54. autocoder/sdk/cli/__init__.py +15 -0
  55. autocoder/sdk/cli/__main__.py +26 -0
  56. autocoder/sdk/cli/completion_wrapper.py +38 -0
  57. autocoder/sdk/cli/formatters.py +211 -0
  58. autocoder/sdk/cli/handlers.py +174 -0
  59. autocoder/sdk/cli/install_completion.py +301 -0
  60. autocoder/sdk/cli/main.py +284 -0
  61. autocoder/sdk/cli/options.py +72 -0
  62. autocoder/sdk/constants.py +102 -0
  63. autocoder/sdk/core/__init__.py +20 -0
  64. autocoder/sdk/core/auto_coder_core.py +867 -0
  65. autocoder/sdk/core/bridge.py +497 -0
  66. autocoder/sdk/example.py +0 -0
  67. autocoder/sdk/exceptions.py +72 -0
  68. autocoder/sdk/models/__init__.py +19 -0
  69. autocoder/sdk/models/messages.py +209 -0
  70. autocoder/sdk/models/options.py +194 -0
  71. autocoder/sdk/models/responses.py +311 -0
  72. autocoder/sdk/session/__init__.py +32 -0
  73. autocoder/sdk/session/session.py +106 -0
  74. autocoder/sdk/session/session_manager.py +56 -0
  75. autocoder/sdk/utils/__init__.py +24 -0
  76. autocoder/sdk/utils/formatters.py +216 -0
  77. autocoder/sdk/utils/io_utils.py +302 -0
  78. autocoder/sdk/utils/validators.py +287 -0
  79. autocoder/version.py +2 -1
  80. auto_coder-0.1.397.dist-info/METADATA +0 -111
  81. autocoder/common/conversations/compatibility.py +0 -303
  82. autocoder/common/conversations/conversation_manager.py +0 -502
  83. autocoder/common/conversations/example.py +0 -152
  84. {auto_coder-0.1.397.dist-info → auto_coder-0.1.399.dist-info/licenses}/LICENSE +0 -0
  85. {auto_coder-0.1.397.dist-info → auto_coder-0.1.399.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1028 @@
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
+ # Convert sort_desc boolean to sort_order string
335
+ sort_order = 'desc' if sort_desc else 'asc'
336
+
337
+ # Get conversations from index with sorting and pagination
338
+ conversations = self.index_manager.list_conversations(
339
+ limit=limit,
340
+ offset=offset,
341
+ sort_by=sort_by,
342
+ sort_order=sort_order
343
+ )
344
+
345
+ # Apply filters if provided
346
+ if filters:
347
+ conversations = self.filter_manager.apply_filters(conversations, filters)
348
+
349
+ return conversations
350
+
351
+ except Exception as e:
352
+ raise ConversationManagerError(f"Failed to list conversations: {e}")
353
+
354
+ # Message Management Methods
355
+
356
+ def append_message(
357
+ self,
358
+ conversation_id: str,
359
+ role: str,
360
+ content: Union[str, Dict[str, Any], List[Any]],
361
+ metadata: Optional[Dict[str, Any]] = None
362
+ ) -> str:
363
+ """
364
+ Append a message to a conversation.
365
+
366
+ Args:
367
+ conversation_id: ID of the conversation
368
+ role: Role of the message sender
369
+ content: Message content
370
+ metadata: Optional message metadata
371
+
372
+ Returns:
373
+ ID of the added message
374
+
375
+ Raises:
376
+ ConversationNotFoundError: If conversation doesn't exist
377
+ """
378
+ try:
379
+ # Create message object
380
+ message = ConversationMessage(
381
+ role=role,
382
+ content=content,
383
+ metadata=metadata or {}
384
+ )
385
+
386
+ with self._conversation_lock(conversation_id):
387
+ # Load conversation
388
+ conversation_data = self.storage.load_conversation(conversation_id)
389
+ if not conversation_data:
390
+ raise ConversationNotFoundError(conversation_id)
391
+
392
+ # Add message to conversation
393
+ conversation = Conversation.from_dict(conversation_data)
394
+ conversation.add_message(message)
395
+
396
+ # Save updated conversation
397
+ updated_data = conversation.to_dict()
398
+ self.storage.save_conversation(updated_data)
399
+
400
+ # Update index
401
+ self.index_manager.update_conversation(updated_data)
402
+
403
+ # Update cache
404
+ self.conversation_cache.set(conversation_id, updated_data)
405
+ self.message_cache.set(f"{conversation_id}:{message.message_id}", message.to_dict())
406
+
407
+ # Update statistics
408
+ self._stats['messages_added'] += 1
409
+
410
+ return message.message_id
411
+
412
+ except ConversationNotFoundError:
413
+ raise
414
+ except Exception as e:
415
+ raise ConversationManagerError(f"Failed to append message to conversation {conversation_id}: {e}")
416
+
417
+ def append_messages(
418
+ self,
419
+ conversation_id: str,
420
+ messages: List[Dict[str, Any]]
421
+ ) -> List[str]:
422
+ """
423
+ Append multiple messages to a conversation.
424
+
425
+ Args:
426
+ conversation_id: ID of the conversation
427
+ messages: List of message data dictionaries
428
+
429
+ Returns:
430
+ List of message IDs
431
+ """
432
+ message_ids = []
433
+
434
+ for msg_data in messages:
435
+ message_id = self.append_message(
436
+ conversation_id,
437
+ msg_data['role'],
438
+ msg_data['content'],
439
+ msg_data.get('metadata')
440
+ )
441
+ message_ids.append(message_id)
442
+
443
+ return message_ids
444
+
445
+ def get_messages(
446
+ self,
447
+ conversation_id: str,
448
+ limit: Optional[int] = None,
449
+ offset: int = 0,
450
+ message_ids: Optional[List[str]] = None
451
+ ) -> List[Dict[str, Any]]:
452
+ """
453
+ Get messages from a conversation.
454
+
455
+ Args:
456
+ conversation_id: ID of the conversation
457
+ limit: Maximum number of messages to return
458
+ offset: Number of messages to skip
459
+ message_ids: Optional list of specific message IDs
460
+
461
+ Returns:
462
+ List of message data
463
+ """
464
+ try:
465
+ # Get conversation
466
+ conversation_data = self.get_conversation(conversation_id)
467
+ if not conversation_data:
468
+ raise ConversationNotFoundError(conversation_id)
469
+
470
+ messages = conversation_data.get('messages', [])
471
+
472
+ # Filter by message IDs if provided
473
+ if message_ids:
474
+ id_set = set(message_ids)
475
+ messages = [msg for msg in messages if msg.get('message_id') in id_set]
476
+
477
+ # Apply pagination
478
+ end_idx = offset + limit if limit else None
479
+ return messages[offset:end_idx]
480
+
481
+ except ConversationNotFoundError:
482
+ raise
483
+ except Exception as e:
484
+ raise ConversationManagerError(f"Failed to get messages from conversation {conversation_id}: {e}")
485
+
486
+ def get_message(
487
+ self,
488
+ conversation_id: str,
489
+ message_id: str
490
+ ) -> Optional[Dict[str, Any]]:
491
+ """
492
+ Get a specific message.
493
+
494
+ Args:
495
+ conversation_id: ID of the conversation
496
+ message_id: ID of the message
497
+
498
+ Returns:
499
+ Message data or None if not found
500
+ """
501
+ try:
502
+ # Try cache first
503
+ cached_message = self.message_cache.get(f"{conversation_id}:{message_id}")
504
+ if cached_message:
505
+ return cached_message
506
+
507
+ # Get from conversation
508
+ conversation_data = self.get_conversation(conversation_id)
509
+ if not conversation_data:
510
+ return None
511
+
512
+ # Find message
513
+ for message in conversation_data.get('messages', []):
514
+ if message.get('message_id') == message_id:
515
+ # Cache the message
516
+ self.message_cache.set(f"{conversation_id}:{message_id}", message)
517
+ return message
518
+
519
+ return None
520
+
521
+ except Exception as e:
522
+ raise ConversationManagerError(f"Failed to get message {message_id}: {e}")
523
+
524
+ def update_message(
525
+ self,
526
+ conversation_id: str,
527
+ message_id: str,
528
+ content: Optional[Union[str, Dict[str, Any], List[Any]]] = None,
529
+ metadata: Optional[Dict[str, Any]] = None
530
+ ) -> bool:
531
+ """
532
+ Update a message.
533
+
534
+ Args:
535
+ conversation_id: ID of the conversation
536
+ message_id: ID of the message to update
537
+ content: New content (optional)
538
+ metadata: New metadata (optional)
539
+
540
+ Returns:
541
+ True if update was successful
542
+ """
543
+ try:
544
+ with self._conversation_lock(conversation_id):
545
+ # Load conversation
546
+ conversation_data = self.storage.load_conversation(conversation_id)
547
+ if not conversation_data:
548
+ raise ConversationNotFoundError(conversation_id)
549
+
550
+ conversation = Conversation.from_dict(conversation_data)
551
+
552
+ # Find and update message
553
+ for i, message_data in enumerate(conversation.messages):
554
+ msg = ConversationMessage.from_dict(message_data)
555
+ if msg.message_id == message_id:
556
+ # Update message fields
557
+ if content is not None:
558
+ msg.content = content
559
+ if metadata is not None:
560
+ msg.metadata.update(metadata)
561
+
562
+ # Update timestamp
563
+ msg.timestamp = time.time()
564
+
565
+ # Replace in conversation
566
+ conversation.messages[i] = msg.to_dict()
567
+ conversation.updated_at = time.time()
568
+
569
+ # Save updated conversation
570
+ updated_data = conversation.to_dict()
571
+ self.storage.save_conversation(updated_data)
572
+
573
+ # Update index
574
+ self.index_manager.update_conversation(updated_data)
575
+
576
+ # Update caches
577
+ self.conversation_cache.set(conversation_id, updated_data)
578
+ self.message_cache.set(f"{conversation_id}:{message_id}", msg.to_dict())
579
+
580
+ return True
581
+
582
+ raise MessageNotFoundError(message_id)
583
+
584
+ except (ConversationNotFoundError, MessageNotFoundError):
585
+ raise
586
+ except Exception as e:
587
+ raise ConversationManagerError(f"Failed to update message {message_id}: {e}")
588
+
589
+ def delete_message(
590
+ self,
591
+ conversation_id: str,
592
+ message_id: str
593
+ ) -> bool:
594
+ """
595
+ Delete a message from a conversation.
596
+
597
+ Args:
598
+ conversation_id: ID of the conversation
599
+ message_id: ID of the message to delete
600
+
601
+ Returns:
602
+ True if deletion was successful
603
+ """
604
+ try:
605
+ with self._conversation_lock(conversation_id):
606
+ # Load conversation
607
+ conversation_data = self.storage.load_conversation(conversation_id)
608
+ if not conversation_data:
609
+ raise ConversationNotFoundError(conversation_id)
610
+
611
+ conversation = Conversation.from_dict(conversation_data)
612
+
613
+ # Find and remove message
614
+ original_count = len(conversation.messages)
615
+ conversation.messages = [
616
+ msg for msg in conversation.messages
617
+ if msg.get('message_id') != message_id
618
+ ]
619
+
620
+ if len(conversation.messages) == original_count:
621
+ raise MessageNotFoundError(message_id)
622
+
623
+ conversation.updated_at = time.time()
624
+
625
+ # Save updated conversation
626
+ updated_data = conversation.to_dict()
627
+ self.storage.save_conversation(updated_data)
628
+
629
+ # Update index
630
+ self.index_manager.update_conversation(updated_data)
631
+
632
+ # Update caches
633
+ self.conversation_cache.set(conversation_id, updated_data)
634
+ self.message_cache.delete(f"{conversation_id}:{message_id}")
635
+
636
+ return True
637
+
638
+ except (ConversationNotFoundError, MessageNotFoundError):
639
+ raise
640
+ except Exception as e:
641
+ raise ConversationManagerError(f"Failed to delete message {message_id}: {e}")
642
+
643
+ def delete_message_pair(
644
+ self,
645
+ conversation_id: str,
646
+ user_message_id: str
647
+ ) -> bool:
648
+ """
649
+ Delete a user message and its corresponding assistant reply.
650
+
651
+ Args:
652
+ conversation_id: ID of the conversation
653
+ user_message_id: ID of the user message
654
+
655
+ Returns:
656
+ True if deletion was successful
657
+ """
658
+ try:
659
+ with self._conversation_lock(conversation_id):
660
+ # Load conversation
661
+ conversation_data = self.storage.load_conversation(conversation_id)
662
+ if not conversation_data:
663
+ raise ConversationNotFoundError(conversation_id)
664
+
665
+ conversation = Conversation.from_dict(conversation_data)
666
+
667
+ # Find user message and next assistant message
668
+ messages_to_remove = []
669
+ for i, message in enumerate(conversation.messages):
670
+ if message.get('message_id') == user_message_id:
671
+ messages_to_remove.append(i)
672
+ # Check if next message is assistant reply
673
+ if (i + 1 < len(conversation.messages) and
674
+ conversation.messages[i + 1].get('role') == 'assistant'):
675
+ messages_to_remove.append(i + 1)
676
+ break
677
+
678
+ if not messages_to_remove:
679
+ raise MessageNotFoundError(user_message_id)
680
+
681
+ # Remove messages (in reverse order to maintain indices)
682
+ assistant_message_id = None
683
+ for idx in reversed(messages_to_remove):
684
+ if idx == messages_to_remove[-1] and len(messages_to_remove) > 1:
685
+ assistant_message_id = conversation.messages[idx].get('message_id')
686
+ del conversation.messages[idx]
687
+
688
+ conversation.updated_at = time.time()
689
+
690
+ # Save updated conversation
691
+ updated_data = conversation.to_dict()
692
+ self.storage.save_conversation(updated_data)
693
+
694
+ # Update index
695
+ self.index_manager.update_conversation(updated_data)
696
+
697
+ # Update caches
698
+ self.conversation_cache.set(conversation_id, updated_data)
699
+ self.message_cache.delete(f"{conversation_id}:{user_message_id}")
700
+ if assistant_message_id:
701
+ self.message_cache.delete(f"{conversation_id}:{assistant_message_id}")
702
+
703
+ return True
704
+
705
+ except (ConversationNotFoundError, MessageNotFoundError):
706
+ raise
707
+ except Exception as e:
708
+ raise ConversationManagerError(f"Failed to delete message pair {user_message_id}: {e}")
709
+
710
+ # Search and Filter Methods
711
+
712
+ def search_conversations(
713
+ self,
714
+ query: str,
715
+ search_in_messages: bool = True,
716
+ filters: Optional[Dict[str, Any]] = None,
717
+ max_results: Optional[int] = None,
718
+ min_score: float = 0.0
719
+ ) -> List[Dict[str, Any]]:
720
+ """
721
+ Search conversations.
722
+
723
+ Args:
724
+ query: Search query
725
+ search_in_messages: Whether to search in message content
726
+ filters: Optional filter criteria
727
+ max_results: Maximum number of results
728
+ min_score: Minimum relevance score
729
+
730
+ Returns:
731
+ List of matching conversations with scores
732
+ """
733
+ try:
734
+ # Get all conversations
735
+ conversations = self.index_manager.list_conversations()
736
+
737
+ # Apply filters first if provided
738
+ if filters:
739
+ conversations = self.filter_manager.apply_filters(conversations, filters)
740
+
741
+ # If search_in_messages is True, load full conversation data
742
+ if search_in_messages:
743
+ full_conversations = []
744
+ for conv in conversations:
745
+ conv_id = conv.get('conversation_id')
746
+ if conv_id:
747
+ full_conv = self.get_conversation(conv_id)
748
+ if full_conv:
749
+ full_conversations.append(full_conv)
750
+ conversations = full_conversations
751
+
752
+ # Perform text search
753
+ results = self.text_searcher.search_conversations(
754
+ query, conversations, max_results, min_score
755
+ )
756
+
757
+ return [{'conversation': conv, 'score': score} for conv, score in results]
758
+
759
+ except Exception as e:
760
+ raise ConversationManagerError(f"Failed to search conversations: {e}")
761
+
762
+ def search_messages(
763
+ self,
764
+ conversation_id: str,
765
+ query: str,
766
+ filters: Optional[Dict[str, Any]] = None,
767
+ max_results: Optional[int] = None,
768
+ min_score: float = 0.0
769
+ ) -> List[Dict[str, Any]]:
770
+ """
771
+ Search messages in a conversation.
772
+
773
+ Args:
774
+ conversation_id: ID of the conversation
775
+ query: Search query
776
+ filters: Optional filter criteria
777
+ max_results: Maximum number of results
778
+ min_score: Minimum relevance score
779
+
780
+ Returns:
781
+ List of matching messages with scores
782
+ """
783
+ try:
784
+ # Get conversation messages
785
+ messages = self.get_messages(conversation_id)
786
+
787
+ # Apply filters if provided
788
+ if filters:
789
+ messages = self.filter_manager.apply_filters(messages, filters)
790
+
791
+ # Perform text search
792
+ results = self.text_searcher.search_messages(
793
+ query, messages, max_results, min_score
794
+ )
795
+
796
+ return [{'message': msg, 'score': score} for msg, score in results]
797
+
798
+ except Exception as e:
799
+ raise ConversationManagerError(f"Failed to search messages: {e}")
800
+
801
+ # Utility and Management Methods
802
+
803
+ def get_statistics(self) -> Dict[str, Any]:
804
+ """
805
+ Get manager statistics.
806
+
807
+ Returns:
808
+ Dictionary with statistics
809
+ """
810
+ cache_stats = {
811
+ 'conversation_cache_size': self.conversation_cache.size(),
812
+ 'message_cache_size': self.message_cache.size()
813
+ }
814
+
815
+ return {
816
+ **self._stats,
817
+ 'cache_stats': cache_stats,
818
+ 'total_conversations': len(self.index_manager.list_conversations()),
819
+ 'current_conversation_id': self.get_current_conversation_id(),
820
+ 'storage_path': self.config.storage_path
821
+ }
822
+
823
+ def health_check(self) -> Dict[str, Any]:
824
+ """
825
+ Perform health check of all components.
826
+
827
+ Returns:
828
+ Health status dictionary
829
+ """
830
+ health_status = {
831
+ 'status': 'healthy',
832
+ 'storage': True,
833
+ 'cache': True,
834
+ 'index': True,
835
+ 'search': True,
836
+ 'issues': []
837
+ }
838
+
839
+ try:
840
+ # Check storage
841
+ if not os.path.exists(self.config.storage_path):
842
+ health_status['storage'] = False
843
+ health_status['issues'].append('Storage directory not accessible')
844
+
845
+ # Check cache
846
+ conv_cache_size = self.conversation_cache.size()
847
+ if conv_cache_size > self.config.max_cache_size:
848
+ health_status['issues'].append('Conversation cache size exceeds limit')
849
+
850
+ # Check index consistency
851
+ try:
852
+ conversations = self.index_manager.list_conversations()
853
+ health_status['total_conversations'] = len(conversations)
854
+ except Exception as e:
855
+ health_status['index'] = False
856
+ health_status['issues'].append(f'Index error: {e}')
857
+
858
+ # Determine overall status
859
+ if not all([health_status['storage'], health_status['cache'],
860
+ health_status['index'], health_status['search']]):
861
+ health_status['status'] = 'degraded'
862
+
863
+ if health_status['issues']:
864
+ health_status['status'] = 'warning' if health_status['status'] == 'healthy' else health_status['status']
865
+
866
+ except Exception as e:
867
+ health_status['status'] = 'unhealthy'
868
+ health_status['issues'].append(f'Health check failed: {e}')
869
+
870
+ return health_status
871
+
872
+ @contextlib.contextmanager
873
+ def transaction(self, conversation_id: str) -> Generator[None, None, None]:
874
+ """
875
+ Transaction context manager for atomic operations.
876
+
877
+ Args:
878
+ conversation_id: ID of the conversation for the transaction
879
+ """
880
+ with self._conversation_lock(conversation_id):
881
+ try:
882
+ yield
883
+ except Exception:
884
+ # In a full implementation, we would rollback changes here
885
+ # For now, we just re-raise the exception
886
+ raise
887
+
888
+ def clear_cache(self):
889
+ """Clear all caches."""
890
+ self.conversation_cache.clear()
891
+ self.message_cache.clear()
892
+
893
+ def rebuild_index(self):
894
+ """Rebuild the conversation index from storage."""
895
+ try:
896
+ # Clear existing index
897
+ self.index_manager._index.clear()
898
+
899
+ # Load all conversations from storage and rebuild index
900
+ conversation_ids = self.storage.list_conversations()
901
+
902
+ for conv_id in conversation_ids:
903
+ conversation_data = self.storage.load_conversation(conv_id)
904
+ if conversation_data:
905
+ self.index_manager.add_conversation(conversation_data)
906
+
907
+ except Exception as e:
908
+ raise ConversationManagerError(f"Failed to rebuild index: {e}")
909
+
910
+ def close(self):
911
+ """Clean up resources."""
912
+ # Clear caches
913
+ self.clear_cache()
914
+
915
+ # Save any pending index changes
916
+ try:
917
+ self.index_manager._save_index()
918
+ except Exception:
919
+ pass # Ignore errors during cleanup
920
+
921
+ # Current Conversation Management Methods
922
+
923
+ def set_current_conversation(self, conversation_id: str) -> bool:
924
+ """
925
+ 设置当前对话。
926
+
927
+ Args:
928
+ conversation_id: 要设置为当前对话的ID
929
+
930
+ Returns:
931
+ True if setting was successful
932
+
933
+ Raises:
934
+ ConversationNotFoundError: 如果对话不存在
935
+ """
936
+ try:
937
+ # 验证对话是否存在
938
+ conversation_data = self.get_conversation(conversation_id)
939
+ if not conversation_data:
940
+ raise ConversationNotFoundError(conversation_id)
941
+
942
+ # 设置当前对话
943
+ success = self.index_manager.set_current_conversation(conversation_id)
944
+ if not success:
945
+ raise ConversationManagerError(f"Failed to set current conversation: {conversation_id}")
946
+
947
+ return True
948
+
949
+ except ConversationNotFoundError:
950
+ raise
951
+ except Exception as e:
952
+ raise ConversationManagerError(f"Failed to set current conversation {conversation_id}: {e}")
953
+
954
+ def get_current_conversation_id(self) -> Optional[str]:
955
+ """
956
+ 获取当前对话ID。
957
+
958
+ Returns:
959
+ 当前对话ID,如果未设置返回None
960
+ """
961
+ try:
962
+ return self.index_manager.get_current_conversation_id()
963
+ except Exception as e:
964
+ raise ConversationManagerError(f"Failed to get current conversation ID: {e}")
965
+
966
+ def get_current_conversation(self) -> Optional[Dict[str, Any]]:
967
+ """
968
+ 获取当前对话的完整数据。
969
+
970
+ Returns:
971
+ 当前对话的数据字典,如果未设置或对话不存在返回None
972
+ """
973
+ try:
974
+ current_id = self.get_current_conversation_id()
975
+ if not current_id:
976
+ return None
977
+
978
+ return self.get_conversation(current_id)
979
+
980
+ except Exception as e:
981
+ raise ConversationManagerError(f"Failed to get current conversation: {e}")
982
+
983
+ def clear_current_conversation(self) -> bool:
984
+ """
985
+ 清除当前对话设置。
986
+
987
+ Returns:
988
+ True if clearing was successful
989
+ """
990
+ try:
991
+ success = self.index_manager.clear_current_conversation()
992
+ if not success:
993
+ raise ConversationManagerError("Failed to clear current conversation")
994
+
995
+ return True
996
+
997
+ except Exception as e:
998
+ raise ConversationManagerError(f"Failed to clear current conversation: {e}")
999
+
1000
+ def append_message_to_current(
1001
+ self,
1002
+ role: str,
1003
+ content: Union[str, Dict[str, Any], List[Any]],
1004
+ metadata: Optional[Dict[str, Any]] = None
1005
+ ) -> str:
1006
+ """
1007
+ 向当前对话添加消息。
1008
+
1009
+ Args:
1010
+ role: 消息角色
1011
+ content: 消息内容
1012
+ metadata: 可选的消息元数据
1013
+
1014
+ Returns:
1015
+ 消息ID
1016
+
1017
+ Raises:
1018
+ ConversationManagerError: 如果没有设置当前对话或添加失败
1019
+ """
1020
+ try:
1021
+ current_id = self.get_current_conversation_id()
1022
+ if not current_id:
1023
+ raise ConversationManagerError("No current conversation set")
1024
+
1025
+ return self.append_message(current_id, role, content, metadata)
1026
+
1027
+ except Exception as e:
1028
+ raise ConversationManagerError(f"Failed to append message to current conversation: {e}")