devsquad 3.6.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.
- devsquad-3.6.0.dist-info/METADATA +944 -0
- devsquad-3.6.0.dist-info/RECORD +95 -0
- devsquad-3.6.0.dist-info/WHEEL +5 -0
- devsquad-3.6.0.dist-info/entry_points.txt +2 -0
- devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
- devsquad-3.6.0.dist-info/top_level.txt +2 -0
- scripts/__init__.py +0 -0
- scripts/ai_semantic_matcher.py +512 -0
- scripts/alert_manager.py +505 -0
- scripts/api/__init__.py +43 -0
- scripts/api/models.py +386 -0
- scripts/api/routes/__init__.py +20 -0
- scripts/api/routes/dispatch.py +348 -0
- scripts/api/routes/lifecycle.py +330 -0
- scripts/api/routes/metrics_gates.py +347 -0
- scripts/api_server.py +318 -0
- scripts/auth.py +451 -0
- scripts/cli/__init__.py +1 -0
- scripts/cli/cli_visual.py +642 -0
- scripts/cli.py +1094 -0
- scripts/collaboration/__init__.py +212 -0
- scripts/collaboration/_version.py +1 -0
- scripts/collaboration/agent_briefing.py +656 -0
- scripts/collaboration/ai_semantic_matcher.py +260 -0
- scripts/collaboration/anchor_checker.py +281 -0
- scripts/collaboration/anti_rationalization.py +470 -0
- scripts/collaboration/async_integration_example.py +255 -0
- scripts/collaboration/batch_scheduler.py +149 -0
- scripts/collaboration/checkpoint_manager.py +561 -0
- scripts/collaboration/ci_feedback_adapter.py +351 -0
- scripts/collaboration/code_map_generator.py +247 -0
- scripts/collaboration/concern_pack_loader.py +352 -0
- scripts/collaboration/confidence_score.py +496 -0
- scripts/collaboration/config_loader.py +188 -0
- scripts/collaboration/consensus.py +244 -0
- scripts/collaboration/context_compressor.py +533 -0
- scripts/collaboration/coordinator.py +668 -0
- scripts/collaboration/dispatcher.py +1636 -0
- scripts/collaboration/dual_layer_context.py +128 -0
- scripts/collaboration/enhanced_worker.py +539 -0
- scripts/collaboration/feature_usage_tracker.py +206 -0
- scripts/collaboration/five_axis_consensus.py +334 -0
- scripts/collaboration/input_validator.py +401 -0
- scripts/collaboration/integration_example.py +287 -0
- scripts/collaboration/intent_workflow_mapper.py +350 -0
- scripts/collaboration/language_parsers.py +269 -0
- scripts/collaboration/lifecycle_protocol.py +1446 -0
- scripts/collaboration/llm_backend.py +453 -0
- scripts/collaboration/llm_cache.py +448 -0
- scripts/collaboration/llm_cache_async.py +347 -0
- scripts/collaboration/llm_retry.py +387 -0
- scripts/collaboration/llm_retry_async.py +389 -0
- scripts/collaboration/mce_adapter.py +597 -0
- scripts/collaboration/memory_bridge.py +1607 -0
- scripts/collaboration/models.py +537 -0
- scripts/collaboration/null_providers.py +297 -0
- scripts/collaboration/operation_classifier.py +289 -0
- scripts/collaboration/output_slicer.py +225 -0
- scripts/collaboration/performance_monitor.py +462 -0
- scripts/collaboration/permission_guard.py +865 -0
- scripts/collaboration/prompt_assembler.py +756 -0
- scripts/collaboration/prompt_variant_generator.py +483 -0
- scripts/collaboration/protocols.py +267 -0
- scripts/collaboration/report_formatter.py +352 -0
- scripts/collaboration/retrospective.py +279 -0
- scripts/collaboration/role_matcher.py +92 -0
- scripts/collaboration/role_template_market.py +352 -0
- scripts/collaboration/rule_collector.py +678 -0
- scripts/collaboration/scratchpad.py +346 -0
- scripts/collaboration/skill_registry.py +151 -0
- scripts/collaboration/skillifier.py +878 -0
- scripts/collaboration/standardized_role_template.py +317 -0
- scripts/collaboration/task_completion_checker.py +237 -0
- scripts/collaboration/test_quality_guard.py +695 -0
- scripts/collaboration/unified_gate_engine.py +598 -0
- scripts/collaboration/usage_tracker.py +309 -0
- scripts/collaboration/user_friendly_error.py +176 -0
- scripts/collaboration/verification_gate.py +312 -0
- scripts/collaboration/warmup_manager.py +635 -0
- scripts/collaboration/worker.py +513 -0
- scripts/collaboration/workflow_engine.py +684 -0
- scripts/dashboard.py +1088 -0
- scripts/generate_benchmark_report.py +786 -0
- scripts/history_manager.py +604 -0
- scripts/mcp_server.py +289 -0
- skills/__init__.py +32 -0
- skills/dispatch/handler.py +52 -0
- skills/intent/handler.py +59 -0
- skills/registry.py +67 -0
- skills/retrospective/__init__.py +0 -0
- skills/retrospective/handler.py +125 -0
- skills/review/handler.py +356 -0
- skills/security/handler.py +454 -0
- skills/test/__init__.py +0 -0
- skills/test/handler.py +78 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Async LLM Cache Module
|
|
5
|
+
|
|
6
|
+
Provides asynchronous caching for LLM API responses to reduce costs and improve performance.
|
|
7
|
+
Compatible with asyncio-based applications.
|
|
8
|
+
|
|
9
|
+
Features:
|
|
10
|
+
- Async memory + disk dual-layer caching
|
|
11
|
+
- TTL-based expiration
|
|
12
|
+
- LRU eviction policy
|
|
13
|
+
- Thread-safe operations
|
|
14
|
+
- Statistics tracking
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
from scripts.collaboration import get_async_llm_cache
|
|
18
|
+
|
|
19
|
+
cache = get_async_llm_cache()
|
|
20
|
+
|
|
21
|
+
# Try to get from cache
|
|
22
|
+
response = await cache.get(prompt, backend="openai", model="gpt-4")
|
|
23
|
+
if not response:
|
|
24
|
+
# Call API
|
|
25
|
+
response = await your_async_api_call(prompt)
|
|
26
|
+
# Save to cache
|
|
27
|
+
await cache.set(prompt, response, backend="openai", model="gpt-4")
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import asyncio
|
|
31
|
+
import hashlib
|
|
32
|
+
import json
|
|
33
|
+
import logging
|
|
34
|
+
import os
|
|
35
|
+
import time
|
|
36
|
+
from dataclasses import dataclass, asdict
|
|
37
|
+
from datetime import datetime, timedelta
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from typing import Optional, Dict, List, Tuple
|
|
40
|
+
from collections import OrderedDict
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class CacheEntry:
|
|
47
|
+
"""Cache entry with metadata"""
|
|
48
|
+
prompt: str
|
|
49
|
+
response: str
|
|
50
|
+
backend: str
|
|
51
|
+
model: str
|
|
52
|
+
timestamp: float
|
|
53
|
+
ttl_seconds: int
|
|
54
|
+
hit_count: int = 0
|
|
55
|
+
|
|
56
|
+
def is_expired(self) -> bool:
|
|
57
|
+
"""Check if entry has expired"""
|
|
58
|
+
age = time.time() - self.timestamp
|
|
59
|
+
return age > self.ttl_seconds
|
|
60
|
+
|
|
61
|
+
def age_hours(self) -> float:
|
|
62
|
+
"""Get age in hours"""
|
|
63
|
+
return (time.time() - self.timestamp) / 3600
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class AsyncLLMCache:
|
|
67
|
+
"""
|
|
68
|
+
Async LLM response cache with memory and disk persistence.
|
|
69
|
+
|
|
70
|
+
Thread-safe and asyncio-compatible implementation.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
cache_dir: str = "data/llm_cache",
|
|
76
|
+
ttl_seconds: int = 86400, # 24 hours
|
|
77
|
+
max_memory_entries: int = 1000
|
|
78
|
+
):
|
|
79
|
+
"""
|
|
80
|
+
Initialize async cache.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
cache_dir: Directory for disk cache
|
|
84
|
+
ttl_seconds: Time-to-live for cache entries (default: 24 hours)
|
|
85
|
+
max_memory_entries: Maximum entries in memory cache
|
|
86
|
+
"""
|
|
87
|
+
self.cache_dir = Path(cache_dir)
|
|
88
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
|
|
90
|
+
self.ttl_seconds = ttl_seconds
|
|
91
|
+
self.max_memory_entries = max_memory_entries
|
|
92
|
+
|
|
93
|
+
# Memory cache (LRU)
|
|
94
|
+
self._memory_cache: OrderedDict[str, CacheEntry] = OrderedDict()
|
|
95
|
+
|
|
96
|
+
# Statistics
|
|
97
|
+
self._stats = {
|
|
98
|
+
"hits": 0,
|
|
99
|
+
"misses": 0,
|
|
100
|
+
"sets": 0,
|
|
101
|
+
"evictions": 0
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Lock for thread safety
|
|
105
|
+
self._lock = asyncio.Lock()
|
|
106
|
+
|
|
107
|
+
logger.info(f"AsyncLLMCache initialized: dir={cache_dir}, ttl={ttl_seconds}s, max_memory={max_memory_entries}")
|
|
108
|
+
|
|
109
|
+
def _generate_cache_key(self, prompt: str, backend: str, model: str) -> str:
|
|
110
|
+
"""Generate cache key from prompt, backend, and model"""
|
|
111
|
+
content = f"{backend}:{model}:{prompt}"
|
|
112
|
+
return hashlib.sha256(content.encode()).hexdigest()
|
|
113
|
+
|
|
114
|
+
def _get_disk_path(self, cache_key: str) -> Path:
|
|
115
|
+
"""Get disk cache file path"""
|
|
116
|
+
# Use first 2 chars for subdirectory to avoid too many files in one dir
|
|
117
|
+
subdir = cache_key[:2]
|
|
118
|
+
return self.cache_dir / subdir / f"{cache_key}.json"
|
|
119
|
+
|
|
120
|
+
async def get(
|
|
121
|
+
self,
|
|
122
|
+
prompt: str,
|
|
123
|
+
backend: str,
|
|
124
|
+
model: str
|
|
125
|
+
) -> Optional[str]:
|
|
126
|
+
"""
|
|
127
|
+
Get cached response asynchronously.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
prompt: Input prompt
|
|
131
|
+
backend: LLM backend (e.g., "openai", "anthropic")
|
|
132
|
+
model: Model name (e.g., "gpt-4", "claude-3")
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Cached response or None if not found/expired
|
|
136
|
+
"""
|
|
137
|
+
cache_key = self._generate_cache_key(prompt, backend, model)
|
|
138
|
+
|
|
139
|
+
async with self._lock:
|
|
140
|
+
# Try memory cache first
|
|
141
|
+
if cache_key in self._memory_cache:
|
|
142
|
+
entry = self._memory_cache[cache_key]
|
|
143
|
+
|
|
144
|
+
if not entry.is_expired():
|
|
145
|
+
# Move to end (LRU)
|
|
146
|
+
self._memory_cache.move_to_end(cache_key)
|
|
147
|
+
entry.hit_count += 1
|
|
148
|
+
self._stats["hits"] += 1
|
|
149
|
+
logger.debug(f"Memory cache hit: {cache_key[:8]}... (age: {entry.age_hours():.1f}h)")
|
|
150
|
+
return entry.response
|
|
151
|
+
else:
|
|
152
|
+
# Expired, remove from memory
|
|
153
|
+
del self._memory_cache[cache_key]
|
|
154
|
+
logger.debug(f"Memory cache expired: {cache_key[:8]}...")
|
|
155
|
+
|
|
156
|
+
# Try disk cache
|
|
157
|
+
disk_path = self._get_disk_path(cache_key)
|
|
158
|
+
if disk_path.exists():
|
|
159
|
+
try:
|
|
160
|
+
# Read from disk asynchronously
|
|
161
|
+
loop = asyncio.get_event_loop()
|
|
162
|
+
data = await loop.run_in_executor(None, disk_path.read_text)
|
|
163
|
+
entry_dict = json.loads(data)
|
|
164
|
+
entry = CacheEntry(**entry_dict)
|
|
165
|
+
|
|
166
|
+
if not entry.is_expired():
|
|
167
|
+
# Load into memory cache
|
|
168
|
+
self._memory_cache[cache_key] = entry
|
|
169
|
+
self._memory_cache.move_to_end(cache_key)
|
|
170
|
+
entry.hit_count += 1
|
|
171
|
+
self._stats["hits"] += 1
|
|
172
|
+
|
|
173
|
+
# Evict if memory cache is full
|
|
174
|
+
await self._evict_if_needed()
|
|
175
|
+
|
|
176
|
+
logger.debug(f"Disk cache hit: {cache_key[:8]}... (age: {entry.age_hours():.1f}h)")
|
|
177
|
+
return entry.response
|
|
178
|
+
else:
|
|
179
|
+
# Expired, delete from disk
|
|
180
|
+
await loop.run_in_executor(None, disk_path.unlink)
|
|
181
|
+
logger.debug(f"Disk cache expired: {cache_key[:8]}...")
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.warning(f"Error reading disk cache: {e}")
|
|
184
|
+
|
|
185
|
+
# Cache miss
|
|
186
|
+
self._stats["misses"] += 1
|
|
187
|
+
logger.debug(f"Cache miss: {cache_key[:8]}...")
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
async def set(
|
|
191
|
+
self,
|
|
192
|
+
prompt: str,
|
|
193
|
+
response: str,
|
|
194
|
+
backend: str,
|
|
195
|
+
model: str,
|
|
196
|
+
ttl_seconds: Optional[int] = None
|
|
197
|
+
):
|
|
198
|
+
"""
|
|
199
|
+
Set cache entry asynchronously.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
prompt: Input prompt
|
|
203
|
+
response: LLM response
|
|
204
|
+
backend: LLM backend
|
|
205
|
+
model: Model name
|
|
206
|
+
ttl_seconds: Custom TTL (optional, uses default if not provided)
|
|
207
|
+
"""
|
|
208
|
+
cache_key = self._generate_cache_key(prompt, backend, model)
|
|
209
|
+
ttl = ttl_seconds or self.ttl_seconds
|
|
210
|
+
|
|
211
|
+
entry = CacheEntry(
|
|
212
|
+
prompt=prompt,
|
|
213
|
+
response=response,
|
|
214
|
+
backend=backend,
|
|
215
|
+
model=model,
|
|
216
|
+
timestamp=time.time(),
|
|
217
|
+
ttl_seconds=ttl,
|
|
218
|
+
hit_count=0
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
async with self._lock:
|
|
222
|
+
# Add to memory cache
|
|
223
|
+
self._memory_cache[cache_key] = entry
|
|
224
|
+
self._memory_cache.move_to_end(cache_key)
|
|
225
|
+
self._stats["sets"] += 1
|
|
226
|
+
|
|
227
|
+
# Evict if needed
|
|
228
|
+
await self._evict_if_needed()
|
|
229
|
+
|
|
230
|
+
# Save to disk asynchronously
|
|
231
|
+
disk_path = self._get_disk_path(cache_key)
|
|
232
|
+
disk_path.parent.mkdir(parents=True, exist_ok=True)
|
|
233
|
+
|
|
234
|
+
loop = asyncio.get_event_loop()
|
|
235
|
+
await loop.run_in_executor(
|
|
236
|
+
None,
|
|
237
|
+
disk_path.write_text,
|
|
238
|
+
json.dumps(asdict(entry), indent=2)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
logger.debug(f"Cache set: {cache_key[:8]}... (ttl: {ttl}s)")
|
|
242
|
+
|
|
243
|
+
async def _evict_if_needed(self):
|
|
244
|
+
"""Evict oldest entries if memory cache is full"""
|
|
245
|
+
while len(self._memory_cache) > self.max_memory_entries:
|
|
246
|
+
# Remove oldest (first) entry
|
|
247
|
+
oldest_key, _ = self._memory_cache.popitem(last=False)
|
|
248
|
+
self._stats["evictions"] += 1
|
|
249
|
+
logger.debug(f"Evicted from memory: {oldest_key[:8]}...")
|
|
250
|
+
|
|
251
|
+
async def clear(self, backend: Optional[str] = None):
|
|
252
|
+
"""
|
|
253
|
+
Clear cache asynchronously.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
backend: If provided, only clear entries for this backend
|
|
257
|
+
"""
|
|
258
|
+
async with self._lock:
|
|
259
|
+
if backend:
|
|
260
|
+
# Clear specific backend
|
|
261
|
+
keys_to_remove = [
|
|
262
|
+
k for k, v in self._memory_cache.items()
|
|
263
|
+
if v.backend == backend
|
|
264
|
+
]
|
|
265
|
+
for key in keys_to_remove:
|
|
266
|
+
del self._memory_cache[key]
|
|
267
|
+
logger.info(f"Cleared cache for backend: {backend}")
|
|
268
|
+
else:
|
|
269
|
+
# Clear all
|
|
270
|
+
self._memory_cache.clear()
|
|
271
|
+
logger.info("Cleared all cache")
|
|
272
|
+
|
|
273
|
+
def get_stats(self) -> Dict:
|
|
274
|
+
"""Get cache statistics"""
|
|
275
|
+
total_requests = self._stats["hits"] + self._stats["misses"]
|
|
276
|
+
hit_rate = self._stats["hits"] / total_requests if total_requests > 0 else 0.0
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
"hits": self._stats["hits"],
|
|
280
|
+
"misses": self._stats["misses"],
|
|
281
|
+
"sets": self._stats["sets"],
|
|
282
|
+
"evictions": self._stats["evictions"],
|
|
283
|
+
"hit_rate": hit_rate,
|
|
284
|
+
"memory_entries": len(self._memory_cache),
|
|
285
|
+
"max_memory_entries": self.max_memory_entries
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async def export_stats_report(self) -> str:
|
|
289
|
+
"""Export statistics as markdown report"""
|
|
290
|
+
stats = self.get_stats()
|
|
291
|
+
|
|
292
|
+
report = "# Async LLM Cache Statistics\n\n"
|
|
293
|
+
report += f"**Hit Rate**: {stats['hit_rate']:.1%}\n\n"
|
|
294
|
+
report += "| Metric | Value |\n"
|
|
295
|
+
report += "|--------|-------|\n"
|
|
296
|
+
report += f"| Cache Hits | {stats['hits']} |\n"
|
|
297
|
+
report += f"| Cache Misses | {stats['misses']} |\n"
|
|
298
|
+
report += f"| Cache Sets | {stats['sets']} |\n"
|
|
299
|
+
report += f"| Evictions | {stats['evictions']} |\n"
|
|
300
|
+
report += f"| Memory Entries | {stats['memory_entries']} / {stats['max_memory_entries']} |\n"
|
|
301
|
+
|
|
302
|
+
return report
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# Global async cache instance
|
|
306
|
+
_global_async_cache: Optional[AsyncLLMCache] = None
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def get_async_llm_cache() -> AsyncLLMCache:
|
|
310
|
+
"""Get global async LLM cache instance (singleton)"""
|
|
311
|
+
global _global_async_cache
|
|
312
|
+
if _global_async_cache is None:
|
|
313
|
+
_global_async_cache = AsyncLLMCache()
|
|
314
|
+
return _global_async_cache
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def reset_async_cache():
|
|
318
|
+
"""Reset global async cache instance"""
|
|
319
|
+
global _global_async_cache
|
|
320
|
+
_global_async_cache = None
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
if __name__ == "__main__":
|
|
324
|
+
# Example usage
|
|
325
|
+
async def main():
|
|
326
|
+
cache = get_async_llm_cache()
|
|
327
|
+
|
|
328
|
+
# Set cache
|
|
329
|
+
await cache.set(
|
|
330
|
+
prompt="What is Python?",
|
|
331
|
+
response="Python is a programming language.",
|
|
332
|
+
backend="openai",
|
|
333
|
+
model="gpt-4"
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Get from cache
|
|
337
|
+
response = await cache.get(
|
|
338
|
+
prompt="What is Python?",
|
|
339
|
+
backend="openai",
|
|
340
|
+
model="gpt-4"
|
|
341
|
+
)
|
|
342
|
+
print(f"Cached response: {response}")
|
|
343
|
+
|
|
344
|
+
# Print stats
|
|
345
|
+
print(await cache.export_stats_report())
|
|
346
|
+
|
|
347
|
+
asyncio.run(main())
|