Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
405 changes: 405 additions & 0 deletions Improvement.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/agents/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def __init__(self, max_retries: int = 3, retry_delay: float = 1.0, agent_name: s
@abstractmethod
def _build_graph(self):
"""Build the LangGraph workflow for this agent."""
pass
raise NotImplementedError("Subclasses must implement _build_graph")

async def _retry_structured_output(self, llm, output_model, prompt, **kwargs) -> T:
"""
Expand Down Expand Up @@ -110,4 +110,4 @@ async def _execute_with_timeout(self, coro, timeout: float = 30.0):
@abstractmethod
async def execute(self, **kwargs) -> AgentResult:
"""Execute the agent with given parameters."""
pass
raise NotImplementedError("Subclasses must implement execute")
25 changes: 25 additions & 0 deletions src/core/config/cache_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""
Cache configuration.

Defines configurable settings for caching strategy including TTL, size limits,
and cache behavior.
"""

from dataclasses import dataclass


@dataclass
class CacheConfig:
"""Cache configuration."""

# Global cache settings (used by recommendations API and other module-level caches)
global_maxsize: int = 1024
global_ttl: int = 3600 # 1 hour in seconds

# Default cache settings for new AsyncCache instances
default_maxsize: int = 100
default_ttl: int = 3600 # 1 hour in seconds

# Cache behavior settings
enable_cache: bool = True # Master switch to disable all caching
enable_metrics: bool = False # Track cache hit/miss rates (future feature)
11 changes: 11 additions & 0 deletions src/core/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from dotenv import load_dotenv

from src.core.config.cache_config import CacheConfig
from src.core.config.cors_config import CORSConfig
from src.core.config.github_config import GitHubConfig
from src.core.config.langsmith_config import LangSmithConfig
Expand Down Expand Up @@ -107,6 +108,16 @@ def __init__(self):
file_path=os.getenv("LOG_FILE_PATH"),
)

# Cache configuration
self.cache = CacheConfig(
global_maxsize=int(os.getenv("CACHE_GLOBAL_MAXSIZE", "1024")),
global_ttl=int(os.getenv("CACHE_GLOBAL_TTL", "3600")),
default_maxsize=int(os.getenv("CACHE_DEFAULT_MAXSIZE", "100")),
default_ttl=int(os.getenv("CACHE_DEFAULT_TTL", "3600")),
enable_cache=os.getenv("CACHE_ENABLE", "true").lower() == "true",
enable_metrics=os.getenv("CACHE_ENABLE_METRICS", "false").lower() == "true",
)

# Development settings
self.debug = os.getenv("DEBUG", "false").lower() == "true"
self.environment = os.getenv("ENVIRONMENT", "development")
Expand Down
5 changes: 4 additions & 1 deletion src/core/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Shared utilities for retry, caching, logging, metrics, and timeout handling.
Shared utilities for retry, caching, logging, metrics, timeout handling, and violation tracking.

This module provides reusable utilities that can be used across the codebase
to avoid code duplication and ensure consistent behavior.
Expand All @@ -10,6 +10,7 @@
from src.core.utils.metrics import track_metrics
from src.core.utils.retry import retry_with_backoff
from src.core.utils.timeout import execute_with_timeout
from src.core.utils.violation_tracker import ViolationTracker, get_violation_tracker

__all__ = [
"AsyncCache",
Expand All @@ -18,4 +19,6 @@
"track_metrics",
"retry_with_backoff",
"execute_with_timeout",
"ViolationTracker",
"get_violation_tracker",
]
128 changes: 112 additions & 16 deletions src/core/utils/caching.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
"""
Caching utilities for async operations.

Provides async-friendly caching with TTL support and decorators
Provides async-friendly caching with TTL (Time To Live) support and decorators
for caching function results.

Caching Strategy
----------------
This module implements a caching strategy with the following features:

1. **TTL (Time To Live)**: Automatic expiration of cached entries after a configurable time period
2. **Eviction Policy**: Uses LRU (Least Recently Used) eviction when cache reaches max size
3. **Configuration**: Configurable via environment variables and CacheConfig
4. **Async Support**: Designed for async operations with proper async/await support

Configuration
-------------
Cache behavior can be configured via environment variables:
- CACHE_GLOBAL_MAXSIZE: Maximum number of entries in global cache (default: 1024)
- CACHE_GLOBAL_TTL: Global cache TTL in seconds (default: 3600)
- CACHE_DEFAULT_MAXSIZE: Default max size for new cache instances (default: 100)
- CACHE_DEFAULT_TTL: Default TTL for new cache instances (default: 3600)
- CACHE_ENABLE: Master switch to enable/disable caching (default: true)

"""

import logging
Expand All @@ -15,6 +34,19 @@

logger = logging.getLogger(__name__)

# Lazy import to avoid circular dependency
_config: Any = None


def _get_config():
"""Lazy load config to avoid circular dependencies."""
global _config
if _config is None:
from src.core.config.settings import config

_config = config
return _config


class AsyncCache:
"""
Expand Down Expand Up @@ -50,6 +82,10 @@ def get(self, key: str) -> Any | None:

Returns:
Cached value or None if not found or expired

Note:
Expired entries are removed lazily (on access) to avoid
background cleanup overhead.
"""
if key not in self._cache:
return None
Expand All @@ -72,9 +108,13 @@ def set(self, key: str, value: Any) -> None:
Args:
key: Cache key
value: Value to cache

Note:
Uses LRU (Least Recently Used) eviction policy when cache is full.
The oldest entry (by timestamp) is removed to make room.
"""
if len(self._cache) >= self.maxsize:
# Remove oldest entry
# Remove oldest entry (LRU eviction)
oldest_key = min(
self._cache.keys(),
key=lambda k: self._cache[k].get("timestamp", 0),
Expand Down Expand Up @@ -115,31 +155,78 @@ def size(self) -> int:
return len(self._cache)


# Simple module-level cache used by recommendations API
_GLOBAL_CACHE = AsyncCache(maxsize=1024, ttl=3600)
# Global module-level cache used by recommendations API and other shared operations
# Initialized lazily with config values to avoid circular dependencies
_GLOBAL_CACHE: AsyncCache | None = None


def _get_global_cache() -> AsyncCache:
"""
Get or initialize the global cache with config values.

Returns:
Global AsyncCache instance configured from settings
"""
global _GLOBAL_CACHE
if _GLOBAL_CACHE is None:
config = _get_config()
_GLOBAL_CACHE = AsyncCache(
maxsize=config.cache.global_maxsize,
ttl=config.cache.global_ttl,
)
return _GLOBAL_CACHE


async def get_cache(key: str) -> Any | None:
"""
Async helper to fetch from the module-level cache.

Args:
key: Cache key to retrieve

Returns:
Cached value or None if not found, expired, or caching disabled

Note:
Respects CACHE_ENABLE setting - returns None if caching is disabled.
"""
return _GLOBAL_CACHE.get(key)
config = _get_config()
if not config.cache.enable_cache:
return None
return _get_global_cache().get(key)


async def set_cache(key: str, value: Any, ttl: int | None = None) -> None:
"""
Async helper to store into the module-level cache.

Args:
key: Cache key
value: Value to cache
ttl: Optional TTL override (applies to entire cache, not just this entry)

Note:
Respects CACHE_ENABLE setting - no-op if caching is disabled.
If ttl is provided, it updates the cache's TTL for all entries.
Individual entry TTL is not supported; all entries share the cache TTL.
"""
if ttl and ttl != _GLOBAL_CACHE.ttl:
_GLOBAL_CACHE.ttl = ttl
_GLOBAL_CACHE.set(key, value)
config = _get_config()
if not config.cache.enable_cache:
return

cache = _get_global_cache()
if ttl and ttl != cache.ttl:
# Update cache TTL (affects all entries)
cache.ttl = ttl
logger.debug(f"Updated global cache TTL to {ttl}s")
cache.set(key, value)


def cached_async(
cache: AsyncCache | TTLCache | None = None,
key_func: Callable[..., str] | None = None,
ttl: int | None = None,
maxsize: int = 100,
maxsize: int | None = None,
):
"""
Decorator for caching async function results.
Expand All @@ -148,7 +235,7 @@ def cached_async(
cache: Cache instance to use (creates new AsyncCache if None)
key_func: Function to generate cache key from function arguments
ttl: Time to live in seconds (only used if cache is None)
maxsize: Maximum cache size (only used if cache is None)
maxsize: Maximum cache size (only used if cache is None, defaults to config)

Returns:
Decorated async function with caching
Expand All @@ -157,17 +244,26 @@ def cached_async(
@cached_async(ttl=3600, key_func=lambda repo, *args: f"repo:{repo}")
async def fetch_repo_data(repo: str):
return await api_call(repo)

Note:
Respects CACHE_ENABLE setting - bypasses cache if disabled.
Uses config defaults for ttl and maxsize if not provided.
"""
if cache is None:
if ttl:
cache = AsyncCache(maxsize=maxsize, ttl=ttl)
else:
# Use TTLCache as fallback
cache = TTLCache(maxsize=maxsize, ttl=ttl or 3600)
config = _get_config()
# Use provided values or fall back to config defaults
cache_ttl = ttl if ttl is not None else config.cache.default_ttl
cache_maxsize = maxsize if maxsize is not None else config.cache.default_maxsize
cache = AsyncCache(maxsize=cache_maxsize, ttl=cache_ttl)

def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
@wraps(func)
async def wrapper(*args: Any, **kwargs: Any) -> Any:
config = _get_config()
# Bypass cache if disabled
if not config.cache.enable_cache:
return await func(*args, **kwargs)

# Generate cache key
if key_func:
cache_key = key_func(*args, **kwargs)
Expand Down Expand Up @@ -201,4 +297,4 @@ async def wrapper(*args: Any, **kwargs: Any) -> Any:

return wrapper

return decorator
return decorator
Loading
Loading