| | """ |
| | Caching module for Silver Table Assistant. |
| | Provides in-memory caching for performance optimization. |
| | """ |
| |
|
| | import time |
| | import hashlib |
| | import json |
| | from typing import Any, Optional, Dict, Callable |
| | from functools import wraps |
| | import asyncio |
| |
|
| |
|
| | class Cache: |
| | """Simple in-memory cache with TTL support.""" |
| | |
| | def __init__(self, default_ttl: int = 3600): |
| | self._cache: Dict[str, Dict[str, Any]] = {} |
| | self.default_ttl = default_ttl |
| | |
| | def _generate_key(self, prefix: str, *args, **kwargs) -> str: |
| | """Generate a cache key from function arguments.""" |
| | |
| | key_data = { |
| | "args": args, |
| | "kwargs": kwargs |
| | } |
| | key_string = json.dumps(key_data, sort_keys=True, default=str) |
| | key_hash = hashlib.md5(key_string.encode()).hexdigest() |
| | return f"{prefix}:{key_hash}" |
| | |
| | def get(self, key: str) -> Optional[Any]: |
| | """Get value from cache if not expired.""" |
| | if key in self._cache: |
| | cache_entry = self._cache[key] |
| | if time.time() < cache_entry["expires_at"]: |
| | return cache_entry["value"] |
| | else: |
| | |
| | del self._cache[key] |
| | return None |
| | |
| | def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None: |
| | """Set value in cache with TTL.""" |
| | ttl = ttl or self.default_ttl |
| | self._cache[key] = { |
| | "value": value, |
| | "expires_at": time.time() + ttl |
| | } |
| | |
| | def delete(self, key: str) -> None: |
| | """Delete key from cache.""" |
| | if key in self._cache: |
| | del self._cache[key] |
| | |
| | def clear(self) -> None: |
| | """Clear all cache entries.""" |
| | self._cache.clear() |
| | |
| | def cleanup_expired(self) -> int: |
| | """Remove all expired entries and return count removed.""" |
| | current_time = time.time() |
| | expired_keys = [ |
| | key for key, entry in self._cache.items() |
| | if current_time >= entry["expires_at"] |
| | ] |
| | |
| | for key in expired_keys: |
| | del self._cache[key] |
| | |
| | return len(expired_keys) |
| | |
| | def get_stats(self) -> Dict[str, Any]: |
| | """Get cache statistics.""" |
| | current_time = time.time() |
| | total_entries = len(self._cache) |
| | expired_entries = sum( |
| | 1 for entry in self._cache.values() |
| | if current_time >= entry["expires_at"] |
| | ) |
| | |
| | return { |
| | "total_entries": total_entries, |
| | "active_entries": total_entries - expired_entries, |
| | "expired_entries": expired_entries, |
| | "cache_hit_potential": "high" if total_entries > 100 else "medium" if total_entries > 10 else "low" |
| | } |
| |
|
| |
|
| | |
| | document_cache = Cache(default_ttl=1800) |
| | nutrition_cache = Cache(default_ttl=3600) |
| | user_context_cache = Cache(default_ttl=900) |
| |
|
| |
|
| | def cache_result(cache_instance: Cache, prefix: str, ttl: Optional[int] = None): |
| | """Decorator to cache function results.""" |
| | |
| | def decorator(func: Callable): |
| | @wraps(func) |
| | async def async_wrapper(*args, **kwargs): |
| | |
| | cache_key = cache_instance._generate_key(prefix, *args, **kwargs) |
| | |
| | |
| | cached_result = cache_instance.get(cache_key) |
| | if cached_result is not None: |
| | return cached_result |
| | |
| | |
| | if asyncio.iscoroutinefunction(func): |
| | result = await func(*args, **kwargs) |
| | else: |
| | result = func(*args, **kwargs) |
| | |
| | cache_instance.set(cache_key, result, ttl) |
| | return result |
| | |
| | @wraps(func) |
| | def sync_wrapper(*args, **kwargs): |
| | |
| | cache_key = cache_instance._generate_key(prefix, *args, **kwargs) |
| | |
| | |
| | cached_result = cache_instance.get(cache_key) |
| | if cached_result is not None: |
| | return cached_result |
| | |
| | |
| | result = func(*args, **kwargs) |
| | cache_instance.set(cache_key, result, ttl) |
| | return result |
| | |
| | |
| | if asyncio.iscoroutinefunction(func): |
| | return async_wrapper |
| | else: |
| | return sync_wrapper |
| | |
| | return decorator |
| |
|
| |
|
| | class NutritionCache: |
| | """Specialized cache for nutrition calculations.""" |
| | |
| | @staticmethod |
| | def get_menu_item_nutrition(menu_item_id: int) -> Optional[Dict[str, Any]]: |
| | """Get cached nutrition data for a menu item.""" |
| | key = f"nutrition:menu_item:{menu_item_id}" |
| | return nutrition_cache.get(key) |
| | |
| | @staticmethod |
| | def set_menu_item_nutrition(menu_item_id: int, nutrition_data: Dict[str, Any], ttl: Optional[int] = None) -> None: |
| | """Cache nutrition data for a menu item.""" |
| | key = f"nutrition:menu_item:{menu_item_id}" |
| | nutrition_cache.set(key, nutrition_data, ttl) |
| | |
| | @staticmethod |
| | def get_user_nutrition_summary(user_id: str, days: int = 7) -> Optional[Dict[str, Any]]: |
| | """Get cached nutrition summary for a user.""" |
| | key = f"nutrition:summary:{user_id}:{days}" |
| | return nutrition_cache.get(key) |
| | |
| | @staticmethod |
| | def set_user_nutrition_summary(user_id: str, days: int, summary_data: Dict[str, Any], ttl: Optional[int] = None) -> None: |
| | """Cache nutrition summary for a user.""" |
| | key = f"nutrition:summary:{user_id}:{days}" |
| | nutrition_cache.set(key, summary_data, ttl) |
| | |
| | @staticmethod |
| | def invalidate_user_nutrition(user_id: str) -> None: |
| | """Invalidate all nutrition cache for a user.""" |
| | |
| | keys_to_remove = [ |
| | key for key in nutrition_cache._cache.keys() |
| | if key.startswith(f"nutrition:summary:{user_id}") |
| | ] |
| | for key in keys_to_remove: |
| | nutrition_cache.delete(key) |
| |
|
| |
|
| | class DocumentCache: |
| | """Specialized cache for document queries.""" |
| | |
| | @staticmethod |
| | def get_relevant_documents(query: str, k: int, score_threshold: Optional[float] = None) -> Optional[list]: |
| | """Get cached document search results.""" |
| | threshold_key = f":{score_threshold}" if score_threshold else "" |
| | key = f"documents:query:{hashlib.md5(query.encode()).hexdigest()}:k{k}{threshold_key}" |
| | return document_cache.get(key) |
| | |
| | @staticmethod |
| | def set_relevant_documents(query: str, k: int, documents: list, score_threshold: Optional[float] = None, ttl: Optional[int] = None) -> None: |
| | """Cache document search results.""" |
| | threshold_key = f":{score_threshold}" if score_threshold else "" |
| | key = f"documents:query:{hashlib.md5(query.encode()).hexdigest()}:k{k}{threshold_key}" |
| | document_cache.set(key, documents, ttl) |
| | |
| | @staticmethod |
| | def invalidate_document_cache() -> None: |
| | """Invalidate all document cache entries.""" |
| | document_cache.clear() |
| |
|
| |
|
| | class UserContextCache: |
| | """Specialized cache for user context data.""" |
| | |
| | @staticmethod |
| | def get_user_context(user_id: str) -> Optional[Dict[str, Any]]: |
| | """Get cached user context.""" |
| | key = f"context:user:{user_id}" |
| | return user_context_cache.get(key) |
| | |
| | @staticmethod |
| | def set_user_context(user_id: str, context_data: Dict[str, Any], ttl: Optional[int] = None) -> None: |
| | """Cache user context data.""" |
| | key = f"context:user:{user_id}" |
| | user_context_cache.set(key, context_data, ttl) |
| | |
| | @staticmethod |
| | def invalidate_user_context(user_id: str) -> None: |
| | """Invalidate user context cache.""" |
| | key = f"context:user:{user_id}" |
| | user_context_cache.delete(key) |
| |
|
| |
|
| | |
| | def get_cache_stats() -> Dict[str, Any]: |
| | """Get statistics for all caches.""" |
| | return { |
| | "document_cache": document_cache.get_stats(), |
| | "nutrition_cache": nutrition_cache.get_stats(), |
| | "user_context_cache": user_context_cache.get_stats() |
| | } |
| |
|
| |
|
| | def cleanup_all_caches() -> Dict[str, int]: |
| | """Clean up expired entries from all caches.""" |
| | return { |
| | "document_cache": document_cache.cleanup_expired(), |
| | "nutrition_cache": nutrition_cache.cleanup_expired(), |
| | "user_context_cache": user_context_cache.cleanup_expired() |
| | } |
| |
|
| |
|
| | def invalidate_user_cache(user_id: str) -> None: |
| | """Invalidate all cache entries for a specific user.""" |
| | NutritionCache.invalidate_user_nutrition(user_id) |
| | UserContextCache.invalidate_user_context(user_id) |