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,368 @@
1
+ """
2
+ Cache manager for conversation and message caching.
3
+
4
+ This module provides a high-level interface for managing caches of
5
+ conversations and messages, with support for cache warming, invalidation,
6
+ and statistics reporting.
7
+ """
8
+
9
+ import logging
10
+ from typing import Optional, List, Dict, Any, Callable
11
+
12
+ from .base_cache import BaseCache
13
+ from .memory_cache import MemoryCache
14
+ from ..models import Conversation, ConversationMessage
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class CacheManager:
20
+ """High-level cache manager for conversations and messages."""
21
+
22
+ def __init__(
23
+ self,
24
+ conversation_cache: Optional[BaseCache] = None,
25
+ message_cache: Optional[BaseCache] = None
26
+ ):
27
+ """
28
+ Initialize cache manager.
29
+
30
+ Args:
31
+ conversation_cache: Cache instance for conversations
32
+ message_cache: Cache instance for messages
33
+ """
34
+ self.conversation_cache = conversation_cache or MemoryCache(
35
+ max_size=100, default_ttl=600.0 # 10 minutes default
36
+ )
37
+ self.message_cache = message_cache or MemoryCache(
38
+ max_size=500, default_ttl=300.0 # 5 minutes default
39
+ )
40
+
41
+ # Ensure caches implement required interface
42
+ self._validate_cache_interface(self.conversation_cache)
43
+ self._validate_cache_interface(self.message_cache)
44
+
45
+ def _validate_cache_interface(self, cache: BaseCache) -> None:
46
+ """Validate that cache implements required interface."""
47
+ required_methods = ['get', 'set', 'delete', 'clear', 'exists', 'size', 'keys']
48
+ for method in required_methods:
49
+ if not hasattr(cache, method) or not callable(getattr(cache, method)):
50
+ raise TypeError(f"Cache must implement {method} method")
51
+
52
+ def _get_conversation_key(self, conversation_id: str) -> str:
53
+ """Generate cache key for conversation."""
54
+ return f"conv:{conversation_id}"
55
+
56
+ def _get_messages_key(self, conversation_id: str) -> str:
57
+ """Generate cache key for conversation messages."""
58
+ return f"msgs:{conversation_id}"
59
+
60
+ def cache_conversation(
61
+ self,
62
+ conversation: Conversation,
63
+ ttl: Optional[float] = None
64
+ ) -> None:
65
+ """
66
+ Cache a conversation.
67
+
68
+ Args:
69
+ conversation: The conversation to cache
70
+ ttl: Time to live in seconds, None for default
71
+ """
72
+ try:
73
+ key = self._get_conversation_key(conversation.conversation_id)
74
+ self.conversation_cache.set(key, conversation, ttl=ttl)
75
+ logger.debug(f"Cached conversation {conversation.conversation_id}")
76
+ except Exception as e:
77
+ logger.error(f"Failed to cache conversation {conversation.conversation_id}: {e}")
78
+
79
+ def get_conversation(self, conversation_id: str) -> Optional[Conversation]:
80
+ """
81
+ Get a conversation from cache.
82
+
83
+ Args:
84
+ conversation_id: The conversation ID
85
+
86
+ Returns:
87
+ The cached conversation or None if not found
88
+ """
89
+ try:
90
+ key = self._get_conversation_key(conversation_id)
91
+ conversation = self.conversation_cache.get(key)
92
+ if conversation:
93
+ logger.debug(f"Cache hit for conversation {conversation_id}")
94
+ else:
95
+ logger.debug(f"Cache miss for conversation {conversation_id}")
96
+ return conversation
97
+ except Exception as e:
98
+ logger.error(f"Failed to get conversation {conversation_id} from cache: {e}")
99
+ return None
100
+
101
+ def cache_messages(
102
+ self,
103
+ conversation_id: str,
104
+ messages: List[ConversationMessage],
105
+ ttl: Optional[float] = None
106
+ ) -> None:
107
+ """
108
+ Cache messages for a conversation.
109
+
110
+ Args:
111
+ conversation_id: The conversation ID
112
+ messages: List of messages to cache
113
+ ttl: Time to live in seconds, None for default
114
+ """
115
+ try:
116
+ key = self._get_messages_key(conversation_id)
117
+ self.message_cache.set(key, messages, ttl=ttl)
118
+ logger.debug(f"Cached {len(messages)} messages for conversation {conversation_id}")
119
+ except Exception as e:
120
+ logger.error(f"Failed to cache messages for conversation {conversation_id}: {e}")
121
+
122
+ def get_messages(self, conversation_id: str) -> Optional[List[ConversationMessage]]:
123
+ """
124
+ Get messages from cache.
125
+
126
+ Args:
127
+ conversation_id: The conversation ID
128
+
129
+ Returns:
130
+ List of cached messages or None if not found
131
+ """
132
+ try:
133
+ key = self._get_messages_key(conversation_id)
134
+ messages = self.message_cache.get(key)
135
+ if messages:
136
+ logger.debug(f"Cache hit for messages of conversation {conversation_id}")
137
+ else:
138
+ logger.debug(f"Cache miss for messages of conversation {conversation_id}")
139
+ return messages
140
+ except Exception as e:
141
+ logger.error(f"Failed to get messages for conversation {conversation_id} from cache: {e}")
142
+ return None
143
+
144
+ def invalidate_conversation(self, conversation_id: str) -> bool:
145
+ """
146
+ Invalidate cached conversation.
147
+
148
+ Args:
149
+ conversation_id: The conversation ID
150
+
151
+ Returns:
152
+ True if conversation was cached and removed, False otherwise
153
+ """
154
+ try:
155
+ key = self._get_conversation_key(conversation_id)
156
+ result = self.conversation_cache.delete(key)
157
+ if result:
158
+ logger.debug(f"Invalidated conversation {conversation_id}")
159
+ return result
160
+ except Exception as e:
161
+ logger.error(f"Failed to invalidate conversation {conversation_id}: {e}")
162
+ return False
163
+
164
+ def invalidate_messages(self, conversation_id: str) -> bool:
165
+ """
166
+ Invalidate cached messages.
167
+
168
+ Args:
169
+ conversation_id: The conversation ID
170
+
171
+ Returns:
172
+ True if messages were cached and removed, False otherwise
173
+ """
174
+ try:
175
+ key = self._get_messages_key(conversation_id)
176
+ result = self.message_cache.delete(key)
177
+ if result:
178
+ logger.debug(f"Invalidated messages for conversation {conversation_id}")
179
+ return result
180
+ except Exception as e:
181
+ logger.error(f"Failed to invalidate messages for conversation {conversation_id}: {e}")
182
+ return False
183
+
184
+ def invalidate_all(self, conversation_id: str) -> Dict[str, bool]:
185
+ """
186
+ Invalidate all cached data for a conversation.
187
+
188
+ Args:
189
+ conversation_id: The conversation ID
190
+
191
+ Returns:
192
+ Dictionary with invalidation results
193
+ """
194
+ return {
195
+ "conversation": self.invalidate_conversation(conversation_id),
196
+ "messages": self.invalidate_messages(conversation_id)
197
+ }
198
+
199
+ def warm_conversation_cache(
200
+ self,
201
+ data_loader: Callable[[], List[Conversation]]
202
+ ) -> int:
203
+ """
204
+ Warm conversation cache with data.
205
+
206
+ Args:
207
+ data_loader: Function that returns conversations to cache
208
+
209
+ Returns:
210
+ Number of conversations cached
211
+ """
212
+ try:
213
+ conversations = data_loader()
214
+ count = 0
215
+
216
+ for conversation in conversations:
217
+ self.cache_conversation(conversation)
218
+ count += 1
219
+
220
+ logger.info(f"Warmed conversation cache with {count} conversations")
221
+ return count
222
+
223
+ except Exception as e:
224
+ logger.error(f"Failed to warm conversation cache: {e}")
225
+ return 0
226
+
227
+ def cache_conversations(
228
+ self,
229
+ conversations: List[Conversation],
230
+ ttl: Optional[float] = None
231
+ ) -> int:
232
+ """
233
+ Cache multiple conversations.
234
+
235
+ Args:
236
+ conversations: List of conversations to cache
237
+ ttl: Time to live in seconds, None for default
238
+
239
+ Returns:
240
+ Number of conversations successfully cached
241
+ """
242
+ count = 0
243
+ for conversation in conversations:
244
+ try:
245
+ self.cache_conversation(conversation, ttl=ttl)
246
+ count += 1
247
+ except Exception as e:
248
+ logger.error(f"Failed to cache conversation {conversation.conversation_id}: {e}")
249
+
250
+ return count
251
+
252
+ def invalidate_conversations(self, conversation_ids: List[str]) -> Dict[str, bool]:
253
+ """
254
+ Invalidate multiple conversations.
255
+
256
+ Args:
257
+ conversation_ids: List of conversation IDs to invalidate
258
+
259
+ Returns:
260
+ Dictionary mapping conversation IDs to invalidation results
261
+ """
262
+ results = {}
263
+ for conversation_id in conversation_ids:
264
+ results[conversation_id] = self.invalidate_conversation(conversation_id)
265
+
266
+ return results
267
+
268
+ def clear_all_caches(self) -> None:
269
+ """Clear all caches."""
270
+ try:
271
+ self.conversation_cache.clear()
272
+ self.message_cache.clear()
273
+ logger.info("Cleared all caches")
274
+ except Exception as e:
275
+ logger.error(f"Failed to clear caches: {e}")
276
+
277
+ def get_cache_statistics(self) -> Dict[str, Any]:
278
+ """
279
+ Get statistics for all caches.
280
+
281
+ Returns:
282
+ Dictionary with cache statistics
283
+ """
284
+ try:
285
+ stats = {
286
+ "conversation_cache": {
287
+ "size": self.conversation_cache.size(),
288
+ "max_size": getattr(self.conversation_cache, 'max_size', 'unknown')
289
+ },
290
+ "message_cache": {
291
+ "size": self.message_cache.size(),
292
+ "max_size": getattr(self.message_cache, 'max_size', 'unknown')
293
+ }
294
+ }
295
+
296
+ # Add detailed stats if available
297
+ if hasattr(self.conversation_cache, 'get_statistics'):
298
+ stats["conversation_cache"].update(
299
+ self.conversation_cache.get_statistics()
300
+ )
301
+
302
+ if hasattr(self.message_cache, 'get_statistics'):
303
+ stats["message_cache"].update(
304
+ self.message_cache.get_statistics()
305
+ )
306
+
307
+ return stats
308
+
309
+ except Exception as e:
310
+ logger.error(f"Failed to get cache statistics: {e}")
311
+ return {
312
+ "conversation_cache": {"size": 0, "max_size": "unknown"},
313
+ "message_cache": {"size": 0, "max_size": "unknown"},
314
+ "error": str(e)
315
+ }
316
+
317
+ def is_conversation_cached(self, conversation_id: str) -> bool:
318
+ """
319
+ Check if a conversation is cached.
320
+
321
+ Args:
322
+ conversation_id: The conversation ID
323
+
324
+ Returns:
325
+ True if conversation is cached, False otherwise
326
+ """
327
+ try:
328
+ key = self._get_conversation_key(conversation_id)
329
+ return self.conversation_cache.exists(key)
330
+ except Exception as e:
331
+ logger.error(f"Failed to check if conversation {conversation_id} is cached: {e}")
332
+ return False
333
+
334
+ def is_messages_cached(self, conversation_id: str) -> bool:
335
+ """
336
+ Check if messages are cached.
337
+
338
+ Args:
339
+ conversation_id: The conversation ID
340
+
341
+ Returns:
342
+ True if messages are cached, False otherwise
343
+ """
344
+ try:
345
+ key = self._get_messages_key(conversation_id)
346
+ return self.message_cache.exists(key)
347
+ except Exception as e:
348
+ logger.error(f"Failed to check if messages for conversation {conversation_id} are cached: {e}")
349
+ return False
350
+
351
+ def get_cached_conversation_ids(self) -> List[str]:
352
+ """
353
+ Get all cached conversation IDs.
354
+
355
+ Returns:
356
+ List of conversation IDs currently cached
357
+ """
358
+ try:
359
+ keys = self.conversation_cache.keys()
360
+ # Extract conversation IDs from cache keys
361
+ conversation_ids = []
362
+ for key in keys:
363
+ if key.startswith("conv:"):
364
+ conversation_ids.append(key[5:]) # Remove "conv:" prefix
365
+ return conversation_ids
366
+ except Exception as e:
367
+ logger.error(f"Failed to get cached conversation IDs: {e}")
368
+ return []
@@ -0,0 +1,224 @@
1
+ """
2
+ Memory-based cache implementation with LRU eviction and TTL support.
3
+
4
+ This module provides a thread-safe in-memory cache with configurable
5
+ size limits, TTL expiration, and LRU eviction policies.
6
+ """
7
+
8
+ import time
9
+ import threading
10
+ from collections import OrderedDict
11
+ from typing import Optional, Any, List, Dict
12
+ from dataclasses import dataclass
13
+
14
+ from .base_cache import BaseCache
15
+
16
+ # Sentinel object to distinguish between "no ttl provided" and "ttl=None"
17
+ _TTL_NOT_PROVIDED = object()
18
+
19
+ @dataclass
20
+ class CacheEntry:
21
+ """Cache entry with value and expiration time."""
22
+ value: Any
23
+ expires_at: Optional[float] = None
24
+
25
+ def is_expired(self) -> bool:
26
+ """Check if this entry has expired."""
27
+ if self.expires_at is None:
28
+ return False
29
+ return time.time() > self.expires_at
30
+
31
+
32
+ class MemoryCache(BaseCache):
33
+ """Thread-safe in-memory cache with LRU eviction and TTL support."""
34
+
35
+ def __init__(self, max_size: int = 100, default_ttl: float = 300.0):
36
+ """
37
+ Initialize memory cache.
38
+
39
+ Args:
40
+ max_size: Maximum number of items to cache
41
+ default_ttl: Default TTL in seconds for cached items
42
+ """
43
+ if max_size <= 0:
44
+ raise ValueError("max_size must be positive")
45
+
46
+ self.max_size = max_size
47
+ self.default_ttl = default_ttl
48
+ self._cache: OrderedDict[str, CacheEntry] = OrderedDict()
49
+ self._lock = threading.RLock()
50
+
51
+ # Optional statistics
52
+ self.hit_count = 0
53
+ self.miss_count = 0
54
+
55
+ def get(self, key: str) -> Optional[Any]:
56
+ """
57
+ Get a value from the cache.
58
+
59
+ Args:
60
+ key: The cache key
61
+
62
+ Returns:
63
+ The cached value or None if not found/expired
64
+ """
65
+ with self._lock:
66
+ entry = self._cache.get(key)
67
+
68
+ if entry is None:
69
+ self.miss_count += 1
70
+ return None
71
+
72
+ if entry.is_expired():
73
+ # Remove expired entry
74
+ del self._cache[key]
75
+ self.miss_count += 1
76
+ return None
77
+
78
+ # Move to end (most recently used)
79
+ self._cache.move_to_end(key)
80
+ self.hit_count += 1
81
+ return entry.value
82
+
83
+ def set(self, key: str, value: Any, ttl: Optional[float] = _TTL_NOT_PROVIDED) -> None:
84
+ """
85
+ Set a value in the cache.
86
+
87
+ Args:
88
+ key: The cache key
89
+ value: The value to cache
90
+ ttl: Time to live in seconds, None for no expiration
91
+ """
92
+ with self._lock:
93
+ # Calculate expiration time
94
+ expires_at = None
95
+
96
+ if ttl is _TTL_NOT_PROVIDED:
97
+ # No ttl provided, use default_ttl behavior
98
+ if self.default_ttl > 0:
99
+ expires_at = time.time() + self.default_ttl
100
+ elif ttl is None:
101
+ # Explicit None means permanent caching
102
+ expires_at = None
103
+ elif ttl > 0:
104
+ # Positive ttl value
105
+ expires_at = time.time() + ttl
106
+ # ttl <= 0 means permanent caching (expires_at stays None)
107
+
108
+ # Create cache entry
109
+ entry = CacheEntry(value=value, expires_at=expires_at)
110
+
111
+ # If key already exists, update it
112
+ if key in self._cache:
113
+ self._cache[key] = entry
114
+ self._cache.move_to_end(key)
115
+ else:
116
+ # Add new entry
117
+ self._cache[key] = entry
118
+
119
+ # Evict oldest items if over capacity
120
+ while len(self._cache) > self.max_size:
121
+ oldest_key = next(iter(self._cache))
122
+ del self._cache[oldest_key]
123
+
124
+ def delete(self, key: str) -> bool:
125
+ """
126
+ Delete a value from the cache.
127
+
128
+ Args:
129
+ key: The cache key
130
+
131
+ Returns:
132
+ True if the key was deleted, False if it didn't exist
133
+ """
134
+ with self._lock:
135
+ if key in self._cache:
136
+ del self._cache[key]
137
+ return True
138
+ return False
139
+
140
+ def clear(self) -> None:
141
+ """Clear all items from the cache."""
142
+ with self._lock:
143
+ self._cache.clear()
144
+
145
+ def exists(self, key: str) -> bool:
146
+ """
147
+ Check if a key exists in the cache.
148
+
149
+ Args:
150
+ key: The cache key
151
+
152
+ Returns:
153
+ True if the key exists and is not expired, False otherwise
154
+ """
155
+ with self._lock:
156
+ entry = self._cache.get(key)
157
+
158
+ if entry is None:
159
+ return False
160
+
161
+ if entry.is_expired():
162
+ # Remove expired entry
163
+ del self._cache[key]
164
+ return False
165
+
166
+ return True
167
+
168
+ def size(self) -> int:
169
+ """
170
+ Get the current number of items in the cache.
171
+
172
+ Returns:
173
+ The number of items currently in the cache
174
+ """
175
+ with self._lock:
176
+ # Clean up expired items
177
+ self._cleanup_expired()
178
+ return len(self._cache)
179
+
180
+ def keys(self) -> List[str]:
181
+ """
182
+ Get all keys currently in the cache.
183
+
184
+ Returns:
185
+ List of keys currently in the cache
186
+ """
187
+ with self._lock:
188
+ # Clean up expired items
189
+ self._cleanup_expired()
190
+ return list(self._cache.keys())
191
+
192
+ def _cleanup_expired(self) -> None:
193
+ """Remove all expired entries from the cache."""
194
+ current_time = time.time()
195
+ expired_keys = []
196
+
197
+ for key, entry in self._cache.items():
198
+ if entry.expires_at is not None and current_time > entry.expires_at:
199
+ expired_keys.append(key)
200
+
201
+ for key in expired_keys:
202
+ del self._cache[key]
203
+
204
+ def get_statistics(self) -> Dict[str, Any]:
205
+ """
206
+ Get cache statistics.
207
+
208
+ Returns:
209
+ Dictionary with cache statistics
210
+ """
211
+ with self._lock:
212
+ self._cleanup_expired()
213
+
214
+ total_requests = self.hit_count + self.miss_count
215
+ hit_rate = self.hit_count / total_requests if total_requests > 0 else 0.0
216
+
217
+ return {
218
+ "size": len(self._cache),
219
+ "max_size": self.max_size,
220
+ "hit_count": self.hit_count,
221
+ "miss_count": self.miss_count,
222
+ "hit_rate": hit_rate,
223
+ "default_ttl": self.default_ttl
224
+ }