Add files via upload
initial commit
This commit is contained in:
parent
8d3d5ff628
commit
b33bb415dd
24 changed files with 4840 additions and 0 deletions
179
semantic_llm_cache/backends/memory.py
Normal file
179
semantic_llm_cache/backends/memory.py
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
"""In-memory storage backend."""
|
||||
|
||||
import sys
|
||||
from typing import Any, Optional
|
||||
|
||||
from semantic_llm_cache.backends.base import BaseBackend
|
||||
from semantic_llm_cache.config import CacheEntry
|
||||
from semantic_llm_cache.exceptions import CacheBackendError
|
||||
|
||||
|
||||
class MemoryBackend(BaseBackend):
|
||||
"""In-memory cache storage with LRU eviction.
|
||||
|
||||
All operations are in-memory dict access — no I/O — so async methods
|
||||
run directly in the event loop without thread offloading.
|
||||
"""
|
||||
|
||||
def __init__(self, max_size: Optional[int] = None) -> None:
|
||||
"""Initialize memory backend.
|
||||
|
||||
Args:
|
||||
max_size: Maximum number of entries to store (LRU eviction when reached)
|
||||
"""
|
||||
super().__init__()
|
||||
self._cache: dict[str, CacheEntry] = {}
|
||||
self._access_order: dict[str, float] = {}
|
||||
self._max_size = max_size
|
||||
self._access_counter: float = 0.0
|
||||
|
||||
def _evict_if_needed(self) -> None:
|
||||
"""Evict oldest entry if at capacity."""
|
||||
if self._max_size is None or len(self._cache) < self._max_size:
|
||||
return
|
||||
|
||||
if self._access_order:
|
||||
lru_key = min(self._access_order, key=lambda k: self._access_order.get(k, 0))
|
||||
del self._cache[lru_key]
|
||||
del self._access_order[lru_key]
|
||||
|
||||
def _update_access_time(self, key: str) -> None:
|
||||
"""Update access time for LRU tracking."""
|
||||
self._access_counter += 1
|
||||
self._access_order[key] = self._access_counter
|
||||
|
||||
async def get(self, key: str) -> Optional[CacheEntry]:
|
||||
"""Retrieve cache entry by key.
|
||||
|
||||
Args:
|
||||
key: Cache key to retrieve
|
||||
|
||||
Returns:
|
||||
CacheEntry if found and not expired, None otherwise
|
||||
"""
|
||||
try:
|
||||
entry = self._cache.get(key)
|
||||
if entry is None:
|
||||
self._increment_misses()
|
||||
return None
|
||||
|
||||
if self._check_expired(entry):
|
||||
await self.delete(key)
|
||||
self._increment_misses()
|
||||
return None
|
||||
|
||||
self._increment_hits()
|
||||
self._update_access_time(key)
|
||||
entry.hit_count += 1
|
||||
return entry
|
||||
except Exception as e:
|
||||
raise CacheBackendError(f"Failed to get entry: {e}") from e
|
||||
|
||||
async def set(self, key: str, entry: CacheEntry) -> None:
|
||||
"""Store cache entry.
|
||||
|
||||
Args:
|
||||
key: Cache key to store under
|
||||
entry: CacheEntry to store
|
||||
"""
|
||||
try:
|
||||
self._evict_if_needed()
|
||||
self._cache[key] = entry
|
||||
self._update_access_time(key)
|
||||
except Exception as e:
|
||||
raise CacheBackendError(f"Failed to set entry: {e}") from e
|
||||
|
||||
async def delete(self, key: str) -> bool:
|
||||
"""Delete cache entry.
|
||||
|
||||
Args:
|
||||
key: Cache key to delete
|
||||
|
||||
Returns:
|
||||
True if entry was deleted, False if not found
|
||||
"""
|
||||
try:
|
||||
if key in self._cache:
|
||||
del self._cache[key]
|
||||
self._access_order.pop(key, None)
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
raise CacheBackendError(f"Failed to delete entry: {e}") from e
|
||||
|
||||
async def clear(self) -> None:
|
||||
"""Clear all cache entries."""
|
||||
try:
|
||||
self._cache.clear()
|
||||
self._access_order.clear()
|
||||
except Exception as e:
|
||||
raise CacheBackendError(f"Failed to clear cache: {e}") from e
|
||||
|
||||
async def iterate(
|
||||
self, namespace: Optional[str] = None
|
||||
) -> list[tuple[str, CacheEntry]]:
|
||||
"""Iterate over cache entries, optionally filtered by namespace.
|
||||
|
||||
Args:
|
||||
namespace: Optional namespace filter
|
||||
|
||||
Returns:
|
||||
List of (key, entry) tuples
|
||||
"""
|
||||
try:
|
||||
if namespace is None:
|
||||
return list(self._cache.items())
|
||||
|
||||
return [
|
||||
(k, v)
|
||||
for k, v in self._cache.items()
|
||||
if v.namespace == namespace and not self._check_expired(v)
|
||||
]
|
||||
except Exception as e:
|
||||
raise CacheBackendError(f"Failed to iterate entries: {e}") from e
|
||||
|
||||
async def find_similar(
|
||||
self,
|
||||
embedding: list[float],
|
||||
threshold: float,
|
||||
namespace: Optional[str] = None,
|
||||
) -> Optional[tuple[str, CacheEntry, float]]:
|
||||
"""Find semantically similar cached entry.
|
||||
|
||||
Args:
|
||||
embedding: Query embedding vector
|
||||
threshold: Minimum similarity score (0-1)
|
||||
namespace: Optional namespace filter
|
||||
|
||||
Returns:
|
||||
(key, entry, similarity) tuple if found above threshold, None otherwise
|
||||
"""
|
||||
try:
|
||||
candidates = [
|
||||
(k, v)
|
||||
for k, v in self._cache.items()
|
||||
if v.embedding is not None
|
||||
and not self._check_expired(v)
|
||||
and (namespace is None or v.namespace == namespace)
|
||||
]
|
||||
return self._find_best_match(candidates, embedding, threshold)
|
||||
except Exception as e:
|
||||
raise CacheBackendError(f"Failed to find similar entry: {e}") from e
|
||||
|
||||
async def get_stats(self) -> dict[str, Any]:
|
||||
"""Get backend statistics.
|
||||
|
||||
Returns:
|
||||
Dictionary with size, memory usage, hits, misses
|
||||
"""
|
||||
base_stats = await super().get_stats()
|
||||
memory_usage = sys.getsizeof(self._cache) + sum(
|
||||
sys.getsizeof(k) + sys.getsizeof(v) for k, v in self._cache.items()
|
||||
)
|
||||
|
||||
return {
|
||||
**base_stats,
|
||||
"size": len(self._cache),
|
||||
"memory_bytes": memory_usage,
|
||||
"max_size": self._max_size,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue