Coverage for src / osiris_cli / performance.py: 0%
98 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-27 17:41 +0200
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-27 17:41 +0200
1#!/usr/bin/env python3
2"""
3Performance Optimizer for Osiris CLI
4Caches responses, optimizes API calls, and manages resources
5"""
7import hashlib
8import json
9from pathlib import Path
10from typing import Optional, Dict, Any
11from datetime import datetime, timedelta
12from rich.console import Console
14console = Console()
17class ResponseCache:
18 """Cache AI responses to reduce API calls"""
20 def __init__(self, cache_dir: Path = None):
21 if cache_dir is None:
22 cache_dir = Path.home() / ".osiris" / "cache"
24 self.cache_dir = cache_dir
25 self.cache_dir.mkdir(parents=True, exist_ok=True)
26 self.cache_file = self.cache_dir / "response_cache.json"
27 self.cache: Dict[str, Dict[str, Any]] = {}
28 self.max_age_hours = 24 # Cache expires after 24 hours
29 self.max_cache_size = 100 # Keep last 100 responses
30 self.load()
32 def _get_cache_key(self, prompt: str, model: str, provider: str) -> str:
33 """Generate cache key from prompt, model, and provider"""
34 key_str = f"{provider}:{model}:{prompt}"
35 return hashlib.sha256(key_str.encode()).hexdigest()
37 def load(self):
38 """Load cache from disk"""
39 if self.cache_file.exists():
40 try:
41 with open(self.cache_file, 'r') as f:
42 self.cache = json.load(f)
43 except Exception:
44 self.cache = {}
46 def save(self):
47 """Save cache to disk"""
48 try:
49 # Keep only most recent entries
50 if len(self.cache) > self.max_cache_size:
51 # Sort by timestamp and keep newest
52 sorted_items = sorted(
53 self.cache.items(),
54 key=lambda x: x[1].get('timestamp', ''),
55 reverse=True
56 )
57 self.cache = dict(sorted_items[:self.max_cache_size])
59 with open(self.cache_file, 'w') as f:
60 json.dump(self.cache, f, indent=2)
61 except Exception as e:
62 console.print(f"[yellow]Warning: Could not save cache: {e}[/yellow]")
64 def get(self, prompt: str, model: str, provider: str) -> Optional[str]:
65 """Get cached response if available and not expired"""
66 cache_key = self._get_cache_key(prompt, model, provider)
68 if cache_key in self.cache:
69 entry = self.cache[cache_key]
70 timestamp = datetime.fromisoformat(entry['timestamp'])
71 age = datetime.now() - timestamp
73 # Check if cache is still valid
74 if age < timedelta(hours=self.max_age_hours):
75 console.print(f"[dim]Using cached response (age: {age.seconds // 60}m)[/dim]")
76 return entry['response']
77 else:
78 # Remove expired entry
79 del self.cache[cache_key]
81 return None
83 def set(self, prompt: str, model: str, provider: str, response: str):
84 """Cache a response"""
85 cache_key = self._get_cache_key(prompt, model, provider)
87 self.cache[cache_key] = {
88 'prompt': prompt[:100], # Store truncated prompt for reference
89 'model': model,
90 'provider': provider,
91 'response': response,
92 'timestamp': datetime.now().isoformat()
93 }
95 self.save()
97 def clear(self):
98 """Clear all cached responses"""
99 self.cache = {}
100 self.save()
101 console.print("[green]✓[/green] Cache cleared")
103 def get_stats(self) -> Dict[str, Any]:
104 """Get cache statistics"""
105 if not self.cache:
106 return {
107 'entries': 0,
108 'size_kb': 0,
109 'oldest': None,
110 'newest': None
111 }
113 timestamps = [datetime.fromisoformat(e['timestamp']) for e in self.cache.values()]
115 return {
116 'entries': len(self.cache),
117 'size_kb': self.cache_file.stat().st_size / 1024 if self.cache_file.exists() else 0,
118 'oldest': min(timestamps).isoformat() if timestamps else None,
119 'newest': max(timestamps).isoformat() if timestamps else None
120 }
123class PerformanceOptimizer:
124 """Optimize performance across the CLI"""
126 def __init__(self):
127 self.response_cache = ResponseCache()
128 self.stats = {
129 'api_calls': 0,
130 'cache_hits': 0,
131 'cache_misses': 0,
132 'total_tokens': 0,
133 'total_time': 0.0
134 }
136 def should_use_cache(self, prompt: str) -> bool:
137 """Determine if caching should be used for this prompt"""
138 # Don't cache commands
139 if prompt.startswith('/'):
140 return False
142 # Don't cache very short prompts
143 if len(prompt) < 10:
144 return False
146 # Don't cache prompts with time-sensitive keywords
147 time_keywords = ['now', 'today', 'current', 'latest', 'recent']
148 if any(keyword in prompt.lower() for keyword in time_keywords):
149 return False
151 return True
153 def get_cached_response(self, prompt: str, model: str, provider: str) -> Optional[str]:
154 """Try to get cached response"""
155 if not self.should_use_cache(prompt):
156 return None
158 response = self.response_cache.get(prompt, model, provider)
160 if response:
161 self.stats['cache_hits'] += 1
162 else:
163 self.stats['cache_misses'] += 1
165 return response
167 def cache_response(self, prompt: str, model: str, provider: str, response: str):
168 """Cache a response"""
169 if self.should_use_cache(prompt):
170 self.response_cache.set(prompt, model, provider, response)
172 def record_api_call(self, tokens: int, time_seconds: float):
173 """Record API call statistics"""
174 self.stats['api_calls'] += 1
175 self.stats['total_tokens'] += tokens
176 self.stats['total_time'] += time_seconds
178 def get_stats(self) -> Dict[str, Any]:
179 """Get performance statistics"""
180 cache_stats = self.response_cache.get_stats()
182 total_requests = self.stats['cache_hits'] + self.stats['cache_misses']
183 cache_hit_rate = (self.stats['cache_hits'] / total_requests * 100) if total_requests > 0 else 0
185 avg_time = self.stats['total_time'] / self.stats['api_calls'] if self.stats['api_calls'] > 0 else 0
187 return {
188 'api_calls': self.stats['api_calls'],
189 'cache_hits': self.stats['cache_hits'],
190 'cache_misses': self.stats['cache_misses'],
191 'cache_hit_rate': f"{cache_hit_rate:.1f}%",
192 'total_tokens': self.stats['total_tokens'],
193 'avg_response_time': f"{avg_time:.2f}s",
194 'cache_entries': cache_stats['entries'],
195 'cache_size_kb': f"{cache_stats['size_kb']:.1f} KB"
196 }
198 def clear_cache(self):
199 """Clear response cache"""
200 self.response_cache.clear()
203# Global optimizer instance
204performance_optimizer = PerformanceOptimizer()