Skip to content
Closed
Show file tree
Hide file tree
Changes from 8 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
406 changes: 406 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")
26 changes: 26 additions & 0 deletions src/core/config/cache_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""
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",
]
105 changes: 91 additions & 14 deletions src/core/utils/caching.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

Provides async-friendly caching with TTL support and decorators
for caching function results.

"""

import logging
Expand All @@ -15,6 +16,18 @@

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 +63,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 +89,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 +136,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 +216,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 +225,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
Loading
Loading