ai-coding-assistant 0.5.0__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.
- ai_coding_assistant-0.5.0.dist-info/METADATA +226 -0
- ai_coding_assistant-0.5.0.dist-info/RECORD +89 -0
- ai_coding_assistant-0.5.0.dist-info/WHEEL +4 -0
- ai_coding_assistant-0.5.0.dist-info/entry_points.txt +3 -0
- ai_coding_assistant-0.5.0.dist-info/licenses/LICENSE +21 -0
- coding_assistant/__init__.py +3 -0
- coding_assistant/__main__.py +19 -0
- coding_assistant/cli/__init__.py +1 -0
- coding_assistant/cli/app.py +158 -0
- coding_assistant/cli/commands/__init__.py +19 -0
- coding_assistant/cli/commands/ask.py +178 -0
- coding_assistant/cli/commands/config.py +438 -0
- coding_assistant/cli/commands/diagram.py +267 -0
- coding_assistant/cli/commands/document.py +410 -0
- coding_assistant/cli/commands/explain.py +192 -0
- coding_assistant/cli/commands/fix.py +249 -0
- coding_assistant/cli/commands/index.py +162 -0
- coding_assistant/cli/commands/refactor.py +245 -0
- coding_assistant/cli/commands/search.py +182 -0
- coding_assistant/cli/commands/serve_docs.py +128 -0
- coding_assistant/cli/repl.py +381 -0
- coding_assistant/cli/theme.py +90 -0
- coding_assistant/codebase/__init__.py +1 -0
- coding_assistant/codebase/crawler.py +93 -0
- coding_assistant/codebase/parser.py +266 -0
- coding_assistant/config/__init__.py +25 -0
- coding_assistant/config/config_manager.py +615 -0
- coding_assistant/config/settings.py +82 -0
- coding_assistant/context/__init__.py +19 -0
- coding_assistant/context/chunker.py +443 -0
- coding_assistant/context/enhanced_retriever.py +322 -0
- coding_assistant/context/hybrid_search.py +311 -0
- coding_assistant/context/ranker.py +355 -0
- coding_assistant/context/retriever.py +119 -0
- coding_assistant/context/window.py +362 -0
- coding_assistant/documentation/__init__.py +23 -0
- coding_assistant/documentation/agents/__init__.py +27 -0
- coding_assistant/documentation/agents/coordinator.py +510 -0
- coding_assistant/documentation/agents/module_documenter.py +111 -0
- coding_assistant/documentation/agents/synthesizer.py +139 -0
- coding_assistant/documentation/agents/task_delegator.py +100 -0
- coding_assistant/documentation/decomposition/__init__.py +21 -0
- coding_assistant/documentation/decomposition/context_preserver.py +477 -0
- coding_assistant/documentation/decomposition/module_detector.py +302 -0
- coding_assistant/documentation/decomposition/partitioner.py +621 -0
- coding_assistant/documentation/generators/__init__.py +14 -0
- coding_assistant/documentation/generators/dataflow_generator.py +440 -0
- coding_assistant/documentation/generators/diagram_generator.py +511 -0
- coding_assistant/documentation/graph/__init__.py +13 -0
- coding_assistant/documentation/graph/dependency_builder.py +468 -0
- coding_assistant/documentation/graph/module_analyzer.py +475 -0
- coding_assistant/documentation/writers/__init__.py +11 -0
- coding_assistant/documentation/writers/markdown_writer.py +322 -0
- coding_assistant/embeddings/__init__.py +0 -0
- coding_assistant/embeddings/generator.py +89 -0
- coding_assistant/embeddings/store.py +187 -0
- coding_assistant/exceptions/__init__.py +50 -0
- coding_assistant/exceptions/base.py +110 -0
- coding_assistant/exceptions/llm.py +249 -0
- coding_assistant/exceptions/recovery.py +263 -0
- coding_assistant/exceptions/storage.py +213 -0
- coding_assistant/exceptions/validation.py +230 -0
- coding_assistant/llm/__init__.py +1 -0
- coding_assistant/llm/client.py +277 -0
- coding_assistant/llm/gemini_client.py +181 -0
- coding_assistant/llm/groq_client.py +160 -0
- coding_assistant/llm/prompts.py +98 -0
- coding_assistant/llm/together_client.py +160 -0
- coding_assistant/operations/__init__.py +13 -0
- coding_assistant/operations/differ.py +369 -0
- coding_assistant/operations/generator.py +347 -0
- coding_assistant/operations/linter.py +430 -0
- coding_assistant/operations/validator.py +406 -0
- coding_assistant/storage/__init__.py +9 -0
- coding_assistant/storage/database.py +363 -0
- coding_assistant/storage/session.py +231 -0
- coding_assistant/utils/__init__.py +31 -0
- coding_assistant/utils/cache.py +477 -0
- coding_assistant/utils/hardware.py +132 -0
- coding_assistant/utils/keystore.py +206 -0
- coding_assistant/utils/logger.py +32 -0
- coding_assistant/utils/progress.py +311 -0
- coding_assistant/validation/__init__.py +13 -0
- coding_assistant/validation/files.py +305 -0
- coding_assistant/validation/inputs.py +335 -0
- coding_assistant/validation/params.py +280 -0
- coding_assistant/validation/sanitizers.py +243 -0
- coding_assistant/vcs/__init__.py +5 -0
- coding_assistant/vcs/git.py +269 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
"""Caching system for embeddings, responses, and search results."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import pickle
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Optional, Dict, List
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from dataclasses import dataclass, asdict
|
|
10
|
+
import threading
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class CacheEntry:
|
|
15
|
+
"""Cache entry with metadata."""
|
|
16
|
+
key: str
|
|
17
|
+
value: Any
|
|
18
|
+
created_at: datetime
|
|
19
|
+
expires_at: Optional[datetime]
|
|
20
|
+
hits: int = 0
|
|
21
|
+
size_bytes: int = 0
|
|
22
|
+
|
|
23
|
+
def is_expired(self) -> bool:
|
|
24
|
+
"""Check if entry is expired."""
|
|
25
|
+
if self.expires_at is None:
|
|
26
|
+
return False
|
|
27
|
+
return datetime.now() > self.expires_at
|
|
28
|
+
|
|
29
|
+
def to_dict(self) -> Dict:
|
|
30
|
+
"""Convert to dictionary for serialization."""
|
|
31
|
+
return {
|
|
32
|
+
'key': self.key,
|
|
33
|
+
'created_at': self.created_at.isoformat(),
|
|
34
|
+
'expires_at': self.expires_at.isoformat() if self.expires_at else None,
|
|
35
|
+
'hits': self.hits,
|
|
36
|
+
'size_bytes': self.size_bytes,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Cache:
|
|
41
|
+
"""
|
|
42
|
+
LRU cache with TTL support and persistence.
|
|
43
|
+
|
|
44
|
+
Features:
|
|
45
|
+
- In-memory caching with disk persistence
|
|
46
|
+
- TTL (Time To Live) support
|
|
47
|
+
- LRU eviction policy
|
|
48
|
+
- Thread-safe operations
|
|
49
|
+
- Cache statistics
|
|
50
|
+
- File hash-based invalidation
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
cache_dir: Path,
|
|
56
|
+
max_size_mb: int = 500,
|
|
57
|
+
default_ttl: int = 3600 # 1 hour
|
|
58
|
+
):
|
|
59
|
+
"""
|
|
60
|
+
Initialize cache.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
cache_dir: Directory to store cache files
|
|
64
|
+
max_size_mb: Maximum cache size in MB
|
|
65
|
+
default_ttl: Default TTL in seconds
|
|
66
|
+
"""
|
|
67
|
+
self.cache_dir = Path(cache_dir)
|
|
68
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
|
|
70
|
+
self.max_size_bytes = max_size_mb * 1024 * 1024
|
|
71
|
+
self.default_ttl = default_ttl
|
|
72
|
+
|
|
73
|
+
# In-memory cache for fast access
|
|
74
|
+
self._memory_cache: Dict[str, CacheEntry] = {}
|
|
75
|
+
self._lock = threading.Lock()
|
|
76
|
+
|
|
77
|
+
# Statistics
|
|
78
|
+
self.stats = {
|
|
79
|
+
'hits': 0,
|
|
80
|
+
'misses': 0,
|
|
81
|
+
'evictions': 0,
|
|
82
|
+
'size_bytes': 0
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
def get(self, key: str) -> Optional[Any]:
|
|
86
|
+
"""
|
|
87
|
+
Get cached value.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
key: Cache key
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Cached value or None if not found/expired
|
|
94
|
+
"""
|
|
95
|
+
with self._lock:
|
|
96
|
+
# Check memory cache first
|
|
97
|
+
if key in self._memory_cache:
|
|
98
|
+
entry = self._memory_cache[key]
|
|
99
|
+
|
|
100
|
+
if entry.is_expired():
|
|
101
|
+
# Remove expired entry
|
|
102
|
+
self._remove_entry(key)
|
|
103
|
+
self.stats['misses'] += 1
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
# Update stats
|
|
107
|
+
entry.hits += 1
|
|
108
|
+
self.stats['hits'] += 1
|
|
109
|
+
return entry.value
|
|
110
|
+
|
|
111
|
+
# Check disk cache
|
|
112
|
+
cache_file = self._get_cache_file(key)
|
|
113
|
+
if cache_file.exists():
|
|
114
|
+
try:
|
|
115
|
+
entry = self._load_from_disk(key)
|
|
116
|
+
if entry and not entry.is_expired():
|
|
117
|
+
# Load into memory
|
|
118
|
+
self._memory_cache[key] = entry
|
|
119
|
+
self.stats['hits'] += 1
|
|
120
|
+
return entry.value
|
|
121
|
+
else:
|
|
122
|
+
# Remove expired file
|
|
123
|
+
cache_file.unlink()
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
self.stats['misses'] += 1
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
def set(
|
|
131
|
+
self,
|
|
132
|
+
key: str,
|
|
133
|
+
value: Any,
|
|
134
|
+
ttl: Optional[int] = None
|
|
135
|
+
) -> bool:
|
|
136
|
+
"""
|
|
137
|
+
Set cached value with TTL.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
key: Cache key
|
|
141
|
+
value: Value to cache
|
|
142
|
+
ttl: Time to live in seconds (None = use default)
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
True if cached successfully
|
|
146
|
+
"""
|
|
147
|
+
with self._lock:
|
|
148
|
+
ttl = ttl if ttl is not None else self.default_ttl
|
|
149
|
+
|
|
150
|
+
# Create cache entry
|
|
151
|
+
expires_at = datetime.now() + timedelta(seconds=ttl) if ttl > 0 else None
|
|
152
|
+
|
|
153
|
+
# Serialize value to get size
|
|
154
|
+
try:
|
|
155
|
+
serialized = pickle.dumps(value)
|
|
156
|
+
size_bytes = len(serialized)
|
|
157
|
+
except Exception:
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
entry = CacheEntry(
|
|
161
|
+
key=key,
|
|
162
|
+
value=value,
|
|
163
|
+
created_at=datetime.now(),
|
|
164
|
+
expires_at=expires_at,
|
|
165
|
+
size_bytes=size_bytes
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Check if we need to evict
|
|
169
|
+
while self._should_evict(size_bytes):
|
|
170
|
+
self._evict_lru()
|
|
171
|
+
|
|
172
|
+
# Store in memory
|
|
173
|
+
self._memory_cache[key] = entry
|
|
174
|
+
self.stats['size_bytes'] += size_bytes
|
|
175
|
+
|
|
176
|
+
# Persist to disk
|
|
177
|
+
try:
|
|
178
|
+
self._save_to_disk(key, entry, serialized)
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
def delete(self, key: str) -> bool:
|
|
185
|
+
"""
|
|
186
|
+
Delete cached value.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
key: Cache key
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
True if deleted
|
|
193
|
+
"""
|
|
194
|
+
with self._lock:
|
|
195
|
+
return self._remove_entry(key)
|
|
196
|
+
|
|
197
|
+
def invalidate_pattern(self, pattern: str) -> int:
|
|
198
|
+
"""
|
|
199
|
+
Invalidate cache entries matching pattern.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
pattern: Glob pattern (e.g., "embeddings:*")
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Number of entries invalidated
|
|
206
|
+
"""
|
|
207
|
+
count = 0
|
|
208
|
+
|
|
209
|
+
with self._lock:
|
|
210
|
+
# Invalidate memory cache
|
|
211
|
+
keys_to_remove = [
|
|
212
|
+
k for k in self._memory_cache.keys()
|
|
213
|
+
if self._matches_pattern(k, pattern)
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
for key in keys_to_remove:
|
|
217
|
+
if self._remove_entry(key):
|
|
218
|
+
count += 1
|
|
219
|
+
|
|
220
|
+
# Invalidate disk cache
|
|
221
|
+
for cache_file in self.cache_dir.glob("*"):
|
|
222
|
+
try:
|
|
223
|
+
meta_file = cache_file.with_suffix('.meta')
|
|
224
|
+
if meta_file.exists():
|
|
225
|
+
with open(meta_file, 'r') as f:
|
|
226
|
+
meta = json.load(f)
|
|
227
|
+
if self._matches_pattern(meta['key'], pattern):
|
|
228
|
+
cache_file.unlink()
|
|
229
|
+
meta_file.unlink()
|
|
230
|
+
count += 1
|
|
231
|
+
except Exception:
|
|
232
|
+
pass
|
|
233
|
+
|
|
234
|
+
return count
|
|
235
|
+
|
|
236
|
+
def clear(self) -> None:
|
|
237
|
+
"""Clear all cache entries."""
|
|
238
|
+
with self._lock:
|
|
239
|
+
self._memory_cache.clear()
|
|
240
|
+
self.stats['size_bytes'] = 0
|
|
241
|
+
self.stats['evictions'] = 0
|
|
242
|
+
|
|
243
|
+
# Clear disk cache
|
|
244
|
+
for cache_file in self.cache_dir.glob("*"):
|
|
245
|
+
try:
|
|
246
|
+
cache_file.unlink()
|
|
247
|
+
except Exception:
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
251
|
+
"""
|
|
252
|
+
Get cache statistics.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Dictionary with cache stats
|
|
256
|
+
"""
|
|
257
|
+
with self._lock:
|
|
258
|
+
total = self.stats['hits'] + self.stats['misses']
|
|
259
|
+
hit_rate = (self.stats['hits'] / total * 100) if total > 0 else 0
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
'hits': self.stats['hits'],
|
|
263
|
+
'misses': self.stats['misses'],
|
|
264
|
+
'hit_rate': f"{hit_rate:.1f}%",
|
|
265
|
+
'entries': len(self._memory_cache),
|
|
266
|
+
'size_mb': self.stats['size_bytes'] / (1024 * 1024),
|
|
267
|
+
'max_size_mb': self.max_size_bytes / (1024 * 1024),
|
|
268
|
+
'evictions': self.stats['evictions'],
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
def _should_evict(self, new_size: int) -> bool:
|
|
272
|
+
"""Check if we need to evict entries."""
|
|
273
|
+
return self.stats['size_bytes'] + new_size > self.max_size_bytes
|
|
274
|
+
|
|
275
|
+
def _evict_lru(self) -> None:
|
|
276
|
+
"""Evict least recently used entry."""
|
|
277
|
+
if not self._memory_cache:
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
# Find LRU entry (lowest hits, oldest)
|
|
281
|
+
lru_key = min(
|
|
282
|
+
self._memory_cache.keys(),
|
|
283
|
+
key=lambda k: (
|
|
284
|
+
self._memory_cache[k].hits,
|
|
285
|
+
self._memory_cache[k].created_at
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
self._remove_entry(lru_key)
|
|
290
|
+
self.stats['evictions'] += 1
|
|
291
|
+
|
|
292
|
+
def _remove_entry(self, key: str) -> bool:
|
|
293
|
+
"""Remove entry from cache."""
|
|
294
|
+
if key in self._memory_cache:
|
|
295
|
+
entry = self._memory_cache.pop(key)
|
|
296
|
+
self.stats['size_bytes'] -= entry.size_bytes
|
|
297
|
+
|
|
298
|
+
# Remove from disk
|
|
299
|
+
cache_file = self._get_cache_file(key)
|
|
300
|
+
meta_file = cache_file.with_suffix('.meta')
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
if cache_file.exists():
|
|
304
|
+
cache_file.unlink()
|
|
305
|
+
if meta_file.exists():
|
|
306
|
+
meta_file.unlink()
|
|
307
|
+
except Exception:
|
|
308
|
+
pass
|
|
309
|
+
|
|
310
|
+
return True
|
|
311
|
+
|
|
312
|
+
return False
|
|
313
|
+
|
|
314
|
+
def _get_cache_file(self, key: str) -> Path:
|
|
315
|
+
"""Get cache file path for key."""
|
|
316
|
+
key_hash = hashlib.sha256(key.encode()).hexdigest()
|
|
317
|
+
return self.cache_dir / f"{key_hash}.cache"
|
|
318
|
+
|
|
319
|
+
def _save_to_disk(self, key: str, entry: CacheEntry, serialized: bytes) -> None:
|
|
320
|
+
"""Save cache entry to disk."""
|
|
321
|
+
cache_file = self._get_cache_file(key)
|
|
322
|
+
meta_file = cache_file.with_suffix('.meta')
|
|
323
|
+
|
|
324
|
+
# Save data
|
|
325
|
+
with open(cache_file, 'wb') as f:
|
|
326
|
+
f.write(serialized)
|
|
327
|
+
|
|
328
|
+
# Save metadata
|
|
329
|
+
with open(meta_file, 'w') as f:
|
|
330
|
+
json.dump(entry.to_dict(), f)
|
|
331
|
+
|
|
332
|
+
def _load_from_disk(self, key: str) -> Optional[CacheEntry]:
|
|
333
|
+
"""Load cache entry from disk."""
|
|
334
|
+
cache_file = self._get_cache_file(key)
|
|
335
|
+
meta_file = cache_file.with_suffix('.meta')
|
|
336
|
+
|
|
337
|
+
if not cache_file.exists() or not meta_file.exists():
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
# Load metadata
|
|
342
|
+
with open(meta_file, 'r') as f:
|
|
343
|
+
meta = json.load(f)
|
|
344
|
+
|
|
345
|
+
# Load data
|
|
346
|
+
with open(cache_file, 'rb') as f:
|
|
347
|
+
value = pickle.load(f)
|
|
348
|
+
|
|
349
|
+
# Reconstruct entry
|
|
350
|
+
entry = CacheEntry(
|
|
351
|
+
key=key,
|
|
352
|
+
value=value,
|
|
353
|
+
created_at=datetime.fromisoformat(meta['created_at']),
|
|
354
|
+
expires_at=datetime.fromisoformat(meta['expires_at']) if meta['expires_at'] else None,
|
|
355
|
+
hits=meta['hits'],
|
|
356
|
+
size_bytes=meta['size_bytes']
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
return entry
|
|
360
|
+
|
|
361
|
+
except Exception:
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
def _matches_pattern(self, key: str, pattern: str) -> bool:
|
|
365
|
+
"""Check if key matches glob pattern."""
|
|
366
|
+
import fnmatch
|
|
367
|
+
return fnmatch.fnmatch(key, pattern)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class EmbeddingCache(Cache):
|
|
371
|
+
"""Specialized cache for code embeddings."""
|
|
372
|
+
|
|
373
|
+
def __init__(self, cache_dir: Path):
|
|
374
|
+
super().__init__(
|
|
375
|
+
cache_dir=cache_dir / "embeddings",
|
|
376
|
+
max_size_mb=1000, # 1GB for embeddings
|
|
377
|
+
default_ttl=0 # No expiration for embeddings
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
def get_embedding(self, file_path: str, file_hash: str) -> Optional[List[float]]:
|
|
381
|
+
"""
|
|
382
|
+
Get cached embedding for file.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
file_path: Path to file
|
|
386
|
+
file_hash: Hash of file content
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
Embedding vector or None
|
|
390
|
+
"""
|
|
391
|
+
key = f"{file_path}:{file_hash}"
|
|
392
|
+
return self.get(key)
|
|
393
|
+
|
|
394
|
+
def set_embedding(self, file_path: str, file_hash: str, embedding: List[float]) -> bool:
|
|
395
|
+
"""
|
|
396
|
+
Cache embedding for file.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
file_path: Path to file
|
|
400
|
+
file_hash: Hash of file content
|
|
401
|
+
embedding: Embedding vector
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
True if cached successfully
|
|
405
|
+
"""
|
|
406
|
+
key = f"{file_path}:{file_hash}"
|
|
407
|
+
return self.set(key, embedding, ttl=0) # No expiration
|
|
408
|
+
|
|
409
|
+
def invalidate_file(self, file_path: str) -> int:
|
|
410
|
+
"""Invalidate cache for specific file."""
|
|
411
|
+
return self.invalidate_pattern(f"{file_path}:*")
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class ResponseCache(Cache):
|
|
415
|
+
"""Specialized cache for LLM responses."""
|
|
416
|
+
|
|
417
|
+
def __init__(self, cache_dir: Path):
|
|
418
|
+
super().__init__(
|
|
419
|
+
cache_dir=cache_dir / "responses",
|
|
420
|
+
max_size_mb=200, # 200MB for responses
|
|
421
|
+
default_ttl=1800 # 30 minutes
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
def get_response(self, query: str, context_hash: str) -> Optional[str]:
|
|
425
|
+
"""
|
|
426
|
+
Get cached response.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
query: User query
|
|
430
|
+
context_hash: Hash of context chunks
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
Cached response or None
|
|
434
|
+
"""
|
|
435
|
+
key = self._make_key(query, context_hash)
|
|
436
|
+
return self.get(key)
|
|
437
|
+
|
|
438
|
+
def set_response(self, query: str, context_hash: str, response: str, ttl: int = 1800) -> bool:
|
|
439
|
+
"""
|
|
440
|
+
Cache response.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
query: User query
|
|
444
|
+
context_hash: Hash of context chunks
|
|
445
|
+
response: LLM response
|
|
446
|
+
ttl: Time to live in seconds
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
True if cached successfully
|
|
450
|
+
"""
|
|
451
|
+
key = self._make_key(query, context_hash)
|
|
452
|
+
return self.set(key, response, ttl=ttl)
|
|
453
|
+
|
|
454
|
+
def _make_key(self, query: str, context_hash: str) -> str:
|
|
455
|
+
"""Create cache key from query and context."""
|
|
456
|
+
query_hash = hashlib.md5(query.encode()).hexdigest()[:16]
|
|
457
|
+
return f"response:{query_hash}:{context_hash}"
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def compute_context_hash(chunks: List[Dict]) -> str:
|
|
461
|
+
"""
|
|
462
|
+
Compute hash of context chunks for cache keying.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
chunks: List of context chunks
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
Hash string
|
|
469
|
+
"""
|
|
470
|
+
# Create deterministic string from chunks
|
|
471
|
+
chunk_strings = []
|
|
472
|
+
for chunk in chunks:
|
|
473
|
+
chunk_str = f"{chunk.get('file', '')}:{chunk.get('start_line', 0)}"
|
|
474
|
+
chunk_strings.append(chunk_str)
|
|
475
|
+
|
|
476
|
+
combined = "|".join(sorted(chunk_strings))
|
|
477
|
+
return hashlib.md5(combined.encode()).hexdigest()[:16]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Hardware detection for LLM provider recommendations."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import psutil
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class HardwareInfo:
|
|
10
|
+
"""Hardware information and recommendations."""
|
|
11
|
+
|
|
12
|
+
total_ram_gb: float
|
|
13
|
+
available_ram_gb: float
|
|
14
|
+
recommended_provider: str
|
|
15
|
+
recommended_model: str
|
|
16
|
+
can_run_local: bool
|
|
17
|
+
|
|
18
|
+
def to_dict(self):
|
|
19
|
+
"""Convert to dictionary."""
|
|
20
|
+
return {
|
|
21
|
+
'total_ram_gb': self.total_ram_gb,
|
|
22
|
+
'available_ram_gb': self.available_ram_gb,
|
|
23
|
+
'recommended_provider': self.recommended_provider,
|
|
24
|
+
'recommended_model': self.recommended_model,
|
|
25
|
+
'can_run_local': self.can_run_local
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class HardwareDetector:
|
|
30
|
+
"""
|
|
31
|
+
Hardware detection for optimal LLM provider selection.
|
|
32
|
+
|
|
33
|
+
Detects available RAM and recommends the best provider based on hardware capabilities.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
# RAM thresholds in GB
|
|
37
|
+
MIN_RAM_FOR_7B = 8.0
|
|
38
|
+
MIN_RAM_FOR_32B = 32.0
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def get_hardware_info() -> HardwareInfo:
|
|
42
|
+
"""
|
|
43
|
+
Get hardware information and provider recommendations.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
HardwareInfo with system specs and recommendations
|
|
47
|
+
"""
|
|
48
|
+
# Get memory info
|
|
49
|
+
mem = psutil.virtual_memory()
|
|
50
|
+
total_ram_gb = mem.total / (1024 ** 3)
|
|
51
|
+
available_ram_gb = mem.available / (1024 ** 3)
|
|
52
|
+
|
|
53
|
+
# Determine recommendations based on available RAM
|
|
54
|
+
if total_ram_gb >= HardwareDetector.MIN_RAM_FOR_32B:
|
|
55
|
+
recommended_provider = "ollama"
|
|
56
|
+
recommended_model = "qwen2.5-coder:32b"
|
|
57
|
+
can_run_local = True
|
|
58
|
+
elif total_ram_gb >= HardwareDetector.MIN_RAM_FOR_7B:
|
|
59
|
+
recommended_provider = "ollama"
|
|
60
|
+
recommended_model = "qwen2.5-coder:7b"
|
|
61
|
+
can_run_local = True
|
|
62
|
+
else:
|
|
63
|
+
recommended_provider = "groq"
|
|
64
|
+
recommended_model = "llama-3.3-70b-versatile"
|
|
65
|
+
can_run_local = False
|
|
66
|
+
|
|
67
|
+
return HardwareInfo(
|
|
68
|
+
total_ram_gb=total_ram_gb,
|
|
69
|
+
available_ram_gb=available_ram_gb,
|
|
70
|
+
recommended_provider=recommended_provider,
|
|
71
|
+
recommended_model=recommended_model,
|
|
72
|
+
can_run_local=can_run_local
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def format_recommendation(info: HardwareInfo) -> str:
|
|
77
|
+
"""
|
|
78
|
+
Format hardware info as user-friendly message.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
info: Hardware information
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Formatted recommendation message
|
|
85
|
+
"""
|
|
86
|
+
message = f"💻 System RAM: {info.total_ram_gb:.1f}GB total, {info.available_ram_gb:.1f}GB available\n\n"
|
|
87
|
+
|
|
88
|
+
if info.can_run_local:
|
|
89
|
+
message += f"✓ Your system can run local models!\n"
|
|
90
|
+
message += f" Recommended: {info.recommended_provider} with {info.recommended_model}\n\n"
|
|
91
|
+
message += "To get started:\n"
|
|
92
|
+
message += f" 1. Install Ollama: curl -fsSL https://ollama.com/install.sh | sh\n"
|
|
93
|
+
message += f" 2. Pull model: ollama pull {info.recommended_model}\n"
|
|
94
|
+
message += f" 3. Start using: assistant ask \"your question\"\n\n"
|
|
95
|
+
message += "Alternative: Use cloud providers (Groq free tier, Together AI free trial)\n"
|
|
96
|
+
message += " Run: assistant config set-api-key groq <your-key>"
|
|
97
|
+
else:
|
|
98
|
+
message += f"⚠️ Limited RAM detected ({info.total_ram_gb:.1f}GB)\n"
|
|
99
|
+
message += f" Local models need {HardwareDetector.MIN_RAM_FOR_7B}GB+ RAM\n\n"
|
|
100
|
+
message += f"✓ Recommended: Use cloud provider ({info.recommended_provider})\n"
|
|
101
|
+
message += f" Model: {info.recommended_model}\n\n"
|
|
102
|
+
message += "To get started with Groq (FREE tier):\n"
|
|
103
|
+
message += " 1. Get API key: https://console.groq.com\n"
|
|
104
|
+
message += " 2. Set key: assistant config set-api-key groq <your-key>\n"
|
|
105
|
+
message += " 3. Start using: assistant ask \"your question\"\n\n"
|
|
106
|
+
message += "Alternative: Together AI ($25 free trial)\n"
|
|
107
|
+
message += " 1. Get API key: https://api.together.xyz\n"
|
|
108
|
+
message += " 2. Set key: assistant config set-api-key together <your-key>"
|
|
109
|
+
|
|
110
|
+
return message
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def get_recommended_provider() -> str:
|
|
114
|
+
"""
|
|
115
|
+
Get recommended provider name based on hardware.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Provider name (ollama, groq, or together)
|
|
119
|
+
"""
|
|
120
|
+
info = HardwareDetector.get_hardware_info()
|
|
121
|
+
return info.recommended_provider
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def can_run_local_models() -> bool:
|
|
125
|
+
"""
|
|
126
|
+
Check if system can run local LLM models.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
True if system has sufficient RAM for local models
|
|
130
|
+
"""
|
|
131
|
+
info = HardwareDetector.get_hardware_info()
|
|
132
|
+
return info.can_run_local
|