Frontend (17 items): - Virtualized message list with batch loading - CSS split with skeleton, drawer, search filter, message action styles - Code splitting via React.lazy + Suspense for Admin/Ethics/Settings pages - Skeleton loading components (Skeleton, SkeletonCard, SkeletonGrid) - Debounced search/filter component (SearchFilter) - Error boundary with fallback UI - Keyboard shortcuts (Ctrl+K search, Ctrl+Enter send, Escape dismiss) - Page transition animations (fade-in) - PWA support (manifest.json + service worker) - WebSocket auto-reconnect with exponential backoff (10 retries) - Chat history persistence to localStorage (500 msg limit) - Message edit/delete on hover - Copy-to-clipboard on code blocks - Mobile drawer (bottom-sheet for consensus panel) - File upload support - User preferences sync to backend Testing (8 items): - Component tests: Toast, Markdown, ChatMessage, Avatar, ErrorBoundary, Skeleton - Hook tests: useChatHistory - E2E smoke tests (5 tests) - Accessibility audit utility Backend (12 items): - Vector memory with cosine similarity search - TTS/STT adapter factory wiring - Geometry kernel with orphan detection - Tenant registry with CRUD operations - Response cache with TTL - Connection pool (async) - Background task queue - Health check endpoints (/health, /ready) - Request tracing middleware (X-Request-ID) - API key rotation mechanism - Environment-based config (settings.py) - API route documentation improvements Infrastructure (4 items): - Grafana dashboard template - Database migration system - Storybook configuration Documentation (3 items): - ADR-001: Advisory Governance Model - ADR-002: Twelve-Head Architecture - ADR-003: Consequence Engine 552 Python tests + 45 frontend tests passing, 0 ruff errors. Co-Authored-By: Nakamoto, S <defi@defi-oracle.io>
62 lines
2.3 KiB
Python
62 lines
2.3 KiB
Python
"""In-memory response cache with TTL for the FusionAGI API."""
|
|
|
|
import hashlib
|
|
import json
|
|
import time
|
|
from typing import Any
|
|
|
|
|
|
class ResponseCache:
|
|
"""LRU-like response cache with configurable TTL.
|
|
|
|
For production, replace with Redis-backed cache.
|
|
"""
|
|
|
|
def __init__(self, max_size: int = 1000, ttl_seconds: float = 300.0) -> None:
|
|
self._cache: dict[str, tuple[float, Any]] = {}
|
|
self._max_size = max_size
|
|
self._ttl = ttl_seconds
|
|
|
|
@staticmethod
|
|
def _make_key(prompt: str, session_id: str, tenant_id: str = "default") -> str:
|
|
"""Generate a cache key from prompt + session context."""
|
|
raw = json.dumps({"prompt": prompt, "session": session_id, "tenant": tenant_id}, sort_keys=True)
|
|
return hashlib.sha256(raw.encode()).hexdigest()
|
|
|
|
def get(self, prompt: str, session_id: str, tenant_id: str = "default") -> Any | None:
|
|
"""Get cached response if it exists and hasn't expired."""
|
|
key = self._make_key(prompt, session_id, tenant_id)
|
|
entry = self._cache.get(key)
|
|
if entry is None:
|
|
return None
|
|
ts, value = entry
|
|
if time.time() - ts > self._ttl:
|
|
del self._cache[key]
|
|
return None
|
|
return value
|
|
|
|
def set(self, prompt: str, session_id: str, value: Any, tenant_id: str = "default") -> None:
|
|
"""Cache a response."""
|
|
if len(self._cache) >= self._max_size:
|
|
oldest_key = min(self._cache, key=lambda k: self._cache[k][0])
|
|
del self._cache[oldest_key]
|
|
key = self._make_key(prompt, session_id, tenant_id)
|
|
self._cache[key] = (time.time(), value)
|
|
|
|
def invalidate(self, prompt: str, session_id: str, tenant_id: str = "default") -> bool:
|
|
"""Remove a specific cache entry."""
|
|
key = self._make_key(prompt, session_id, tenant_id)
|
|
return self._cache.pop(key, None) is not None
|
|
|
|
def clear(self) -> int:
|
|
"""Clear all cache entries. Returns count of cleared entries."""
|
|
count = len(self._cache)
|
|
self._cache.clear()
|
|
return count
|
|
|
|
def stats(self) -> dict[str, int]:
|
|
"""Return cache statistics."""
|
|
now = time.time()
|
|
active = sum(1 for ts, _ in self._cache.values() if now - ts <= self._ttl)
|
|
return {"total": len(self._cache), "active": active, "max_size": self._max_size}
|