Skip to content

Conversation

@tisnik
Copy link
Contributor

@tisnik tisnik commented Sep 21, 2025

Description

LCORE-298: Conversation cache factory

Type of change

  • Refactor
  • New feature
  • Bug fix
  • CVE fix
  • Optimization
  • Documentation Update
  • Configuration Update
  • Bump-up service version
  • Bump-up dependent library
  • Bump-up library or tool used for development (does not change the final image)
  • CI configuration change
  • Konflux configuration change
  • Unit tests improvement
  • Integration tests improvement
  • End to end tests improvement

Related Tickets & Documents

  • Related Issue #LCORE-298

Summary by CodeRabbit

  • Refactor

    • Renamed the public configuration entity and its accessor to ConversationCacheConfiguration and conversation_cache_configuration; behavior and validation unchanged.
  • New Features

    • Introduced a cache abstraction, a cache factory, a cache-entry model, and a dedicated cache error type to support pluggable conversation caching.
  • Tests

    • Updated unit tests to use the renamed configuration and accessor.
  • Documentation

    • Updated diagrams and docs to reflect the new names and caching concepts.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 21, 2025

Walkthrough

Adds a new Cache abstraction and factory, renames ConversationCache → ConversationCacheConfiguration, updates Configuration and AppConfig accessors (adds a lazy conversation_cache that returns a Cache instance via CacheFactory and renames the configuration accessor to conversation_cache_configuration), and updates docs and tests accordingly.

Changes

Cohort / File(s) Summary
Model rename & config update
src/models/config.py
Renames ConversationCacheConversationCacheConfiguration; updates Configuration.conversation_cache type and default_factory.
AppConfig accessor & cache wiring
src/configuration.py
Imports ConversationCacheConfiguration; renames AppConfig property conversation_cacheconversation_cache_configuration (returns config object); adds private _conversation_cache and new public conversation_cache property that lazily builds/returns Cache via CacheFactory. Adds imports for Cache and CacheFactory.
Cache abstraction & factory
src/cache/cache.py, src/cache/cache_factory.py, src/cache/cache_error.py, src/cache/__init__.py
Adds abstract Cache class (interface, key utilities), CacheError exception, CacheFactory with `conversation_cache(config) -> Cache
Cache entry model
src/models/cache_entry.py
Adds CacheEntry(BaseModel) with fields query, response, provider, model.
Tests updated
tests/unit/models/config/test_conversation_cache.py, tests/unit/test_configuration.py
Tests updated to use ConversationCacheConfiguration and to reference AppConfig .conversation_cache_configuration; existing validation and error expectations retained.
Docs / diagram update
docs/config.puml
Updates diagram references and edges to ConversationCacheConfiguration and adjusts Configuration relationship.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Caller
  participant AppConfig
  participant Configuration
  participant CacheFactory
  participant Cache as CacheImpl

  Note over AppConfig,Configuration: configuration accessor (unchanged backing field)
  Caller->>AppConfig: get conversation_cache_configuration
  AppConfig->>Configuration: access conversation_cache
  Configuration-->>AppConfig: ConversationCacheConfiguration
  AppConfig-->>Caller: ConversationCacheConfiguration

  Note over AppConfig,CacheFactory: lazy cache construction path
  Caller->>AppConfig: get conversation_cache
  alt cache cached
    AppConfig-->>Caller: return cached Cache
  else not cached
    AppConfig->>CacheFactory: conversation_cache(ConversationCacheConfiguration)
    CacheFactory-->>AppConfig: Cache | None
    AppConfig->>AppConfig: store in _conversation_cache
    AppConfig-->>Caller: return Cache | None
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

Poem

A rabbit hops through code so neat,
Renamed caches, patterns sweet.
I build a Cache with careful art,
Store keys and entries, play my part.
Hooray — configs tidy, tests all fleet! 🥕🐇

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "LCORE-298: Conversation cache factory" is concise and accurately reflects the primary change in the PR (adding a conversation cache factory and related cache abstractions), making it clear to reviewers and maintainers scanning history. It is specific without being noisy or overly broad.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@tisnik tisnik force-pushed the lcore-298-conversation-cache-factory- branch 2 times, most recently from 0a7a605 to 1d6107c Compare September 21, 2025 11:10
@tisnik tisnik force-pushed the lcore-298-conversation-cache-factory- branch from 1d6107c to 15d0379 Compare September 21, 2025 11:14
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/configuration.py (2)

57-57: Do not log full configuration payload (secrets/PII risk)

This can leak credentials/URLs into logs.

-            logger.info("Loaded configuration: %s", config_dict)
+            logger.info("Loaded configuration from %s", filename)

If you need visibility, add a redaction helper and log only non‑sensitive sections.


133-138: Return type must be Optional to match model

configuration.conversation_cache is Optional[…]; current annotation is incorrect and can mislead callers.

-    def conversation_cache_configuration(self) -> ConversationCacheConfiguration:
-        """Return conversation cache configuration."""
+    def conversation_cache_configuration(self) -> Optional[ConversationCacheConfiguration]:
+        """Return conversation cache configuration if configured, otherwise None."""
         if self._configuration is None:
             raise LogicError("logic error: configuration is not loaded")
         return self._configuration.conversation_cache
🧹 Nitpick comments (5)
src/models/cache_entry.py (1)

6-19: Consider forbidding unknown fields and adding field descriptions

Tighten the model and improve generated schema/docs.

Apply:

-from pydantic import BaseModel
+from pydantic import BaseModel, Field
+from pydantic import ConfigDict
@@
 class CacheEntry(BaseModel):
@@
-    query: str
-    response: str
-    provider: str
-    model: str
+    # Reject unexpected keys
+    model_config = ConfigDict(extra="forbid")
+
+    query: str = Field(..., description="User query text")
+    response: str = Field(..., description="Model response text")
+    provider: str = Field(..., description="Provider identifier (e.g., 'openai')")
+    model: str = Field(..., description="Model identifier/version")
src/cache/cache_factory.py (1)

3-9: Use standard logger per repo guidelines

Align with “logger = logging.getLogger(name)” and drop custom get_logger here for consistency with src/configuration.py.

-import constants
-from models.config import ConversationCacheConfiguration
-from cache.cache import Cache
-from log import get_logger
+import logging
+import constants
+from models.config import ConversationCacheConfiguration
+from cache.cache import Cache
@@
-logger = get_logger("cache.cache_factory")
+logger = logging.getLogger(__name__)
src/cache/cache.py (2)

19-21: Annotate class constant as ClassVar[str]

Matches guideline “type annotations for all class attributes.”

-from abc import ABC, abstractmethod
+from abc import ABC, abstractmethod
+from typing import ClassVar
@@
-    COMPOUND_KEY_SEPARATOR = ":"
+    COMPOUND_KEY_SEPARATOR: ClassVar[str] = ":"

56-58: Fix return contract in docstring

Type is list[CacheEntry], so don’t mention None; prefer empty list when not found.

-        Returns:
-            The value (CacheEntry(s)) associated with the key, or None if not found.
+        Returns:
+            A list of CacheEntry associated with the key (empty list if not found).
src/configuration.py (1)

11-23: Inconsistent logger usage — standardize on one approach

Repo mixes the custom get_logger (src/log.py) and direct logging.getLogger(...) calls. Examples: src/cache/cache_factory.py uses get_logger(...); src/configuration.py uses logging.getLogger(name). Choose one approach and apply repo‑wide. If you keep the helper, stop changing logger state in the helper (remove per‑module setLevel) and centralize logging configuration in the app entrypoint.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d7ce460 and f9ca7ef.

⛔ Files ignored due to path filters (2)
  • docs/config.png is excluded by !**/*.png
  • docs/config.svg is excluded by !**/*.svg
📒 Files selected for processing (7)
  • docs/config.puml (3 hunks)
  • src/cache/__init__.py (1 hunks)
  • src/cache/cache.py (1 hunks)
  • src/cache/cache_error.py (1 hunks)
  • src/cache/cache_factory.py (1 hunks)
  • src/configuration.py (4 hunks)
  • src/models/cache_entry.py (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/cache/init.py
🧰 Additional context used
📓 Path-based instructions (5)
src/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Use absolute imports for internal modules (e.g., from auth import get_auth_dependency)

Files:

  • src/cache/cache_error.py
  • src/models/cache_entry.py
  • src/cache/cache_factory.py
  • src/cache/cache.py
  • src/configuration.py
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: All modules start with descriptive module-level docstrings explaining purpose
Use logger = logging.getLogger(name) for module logging after import logging
Define type aliases at module level for clarity
All functions require docstrings with brief descriptions
Provide complete type annotations for all function parameters and return types
Use typing_extensions.Self in model validators where appropriate
Use modern union syntax (str | int) and Optional[T] or T | None consistently
Function names use snake_case with descriptive, action-oriented prefixes (get_, validate_, check_)
Avoid in-place parameter modification; return new data structures instead of mutating arguments
Use appropriate logging levels: debug, info, warning, error with clear messages
All classes require descriptive docstrings explaining purpose
Class names use PascalCase with conventional suffixes (Configuration, Error/Exception, Resolver, Interface)
Abstract base classes should use abc.ABC and @AbstractMethod for interfaces
Provide complete type annotations for all class attributes
Follow Google Python docstring style for modules, classes, and functions, including Args, Returns, Raises, Attributes sections as needed

Files:

  • src/cache/cache_error.py
  • src/models/cache_entry.py
  • src/cache/cache_factory.py
  • src/cache/cache.py
  • src/configuration.py
src/{models/**/*.py,configuration.py}

📄 CodeRabbit inference engine (CLAUDE.md)

src/{models/**/*.py,configuration.py}: Use @field_validator and @model_validator for custom validation in Pydantic models
Use precise type hints in configuration (e.g., Optional[FilePath], PositiveInt, SecretStr)

Files:

  • src/models/cache_entry.py
  • src/configuration.py
src/models/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/models/**/*.py: Pydantic models: use BaseModel for data models and extend ConfigurationBase for configuration
Use @model_validator and @field_validator for Pydantic model validation

Files:

  • src/models/cache_entry.py
src/{models/config.py,configuration.py}

📄 CodeRabbit inference engine (CLAUDE.md)

src/{models/config.py,configuration.py}: All configuration uses Pydantic models extending ConfigurationBase
Configuration base models must set model_config with extra="forbid" to reject unknown fields

Files:

  • src/configuration.py
🧠 Learnings (3)
📚 Learning: 2025-09-18T16:46:33.353Z
Learnt from: CR
PR: lightspeed-core/lightspeed-stack#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-18T16:46:33.353Z
Learning: Applies to src/{models/config.py,configuration.py} : All configuration uses Pydantic models extending ConfigurationBase

Applied to files:

  • docs/config.puml
📚 Learning: 2025-09-18T16:46:33.353Z
Learnt from: CR
PR: lightspeed-core/lightspeed-stack#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-18T16:46:33.353Z
Learning: Applies to src/models/**/*.py : Pydantic models: use BaseModel for data models and extend ConfigurationBase for configuration

Applied to files:

  • docs/config.puml
📚 Learning: 2025-09-18T16:46:33.353Z
Learnt from: CR
PR: lightspeed-core/lightspeed-stack#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-18T16:46:33.353Z
Learning: Applies to src/{models/config.py,configuration.py} : Configuration base models must set model_config with extra="forbid" to reject unknown fields

Applied to files:

  • docs/config.puml
🧬 Code graph analysis (3)
src/cache/cache_factory.py (4)
src/models/config.py (2)
  • config (138-144)
  • ConversationCacheConfiguration (489-527)
src/cache/cache.py (1)
  • Cache (9-110)
src/log.py (1)
  • get_logger (7-13)
src/configuration.py (1)
  • conversation_cache (147-153)
src/cache/cache.py (2)
src/models/cache_entry.py (1)
  • CacheEntry (6-19)
src/utils/suid.py (1)
  • check_suid (15-28)
src/configuration.py (3)
src/models/config.py (1)
  • ConversationCacheConfiguration (489-527)
src/cache/cache.py (1)
  • Cache (9-110)
src/cache/cache_factory.py (2)
  • CacheFactory (12-35)
  • conversation_cache (16-35)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build-pr
  • GitHub Check: e2e_tests
🔇 Additional comments (4)
src/cache/cache_error.py (1)

1-5: LGTM — dedicated CacheError type

Clear, minimal, and fits the new cache layer.

docs/config.puml (1)

32-32: Diagram updates look correct

References and inheritance updated to ConversationCacheConfiguration and optionality reflected.

Also applies to: 46-52, 160-161

src/configuration.py (1)

132-138: No call‑site breakage detected — rename is safe.
Tests and callers use conversation_cache_configuration for the config object; runtime cache remains conversation_cache (CacheFactory/internal _conversation_cache).

src/cache/cache.py (1)

92-102: Avoid method name shadowing built‑in list() — rename to list_conversations

Rename the abstract method in src/cache/cache.py (lines 92–102) to list_conversations to avoid shadowing Python's built-in and clarify intent. The provided ripgrep output shows only client.agents.session.list calls and no references to Cache.list, so rename before first implementation to prevent API churn.

Comment on lines +15 to +35
@staticmethod
def conversation_cache(config: ConversationCacheConfiguration) -> Cache | None:
"""Create an instance of Cache based on loaded configuration.
Returns:
An instance of `Cache` (either `PostgresCache` or `InMemoryCache`).
"""
logger.info("Creating cache instance of type %s", config.type)
match config.type:
case constants.CACHE_TYPE_MEMORY:
return None
case constants.CACHE_TYPE_SQLITE:
return None
case constants.CACHE_TYPE_POSTGRES:
return None
case _:
raise ValueError(
f"Invalid cache type: {config.type}. "
f"Use '{constants.CACHE_TYPE_POSTGRES}' '{constants.CACHE_TYPE_SQLITE}' or "
f"'{constants.CACHE_TYPE_MEMORY}' options."
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle None/disabled cache config and fix docstring/options text

Factory currently assumes config is non‑None and will crash when configuration.conversation_cache is None or when type is None (a valid state per validator). Also docstring mentions only Postgres/InMemory while SQLite is supported by the type.

 class CacheFactory:
@@
-    def conversation_cache(config: ConversationCacheConfiguration) -> Cache | None:
-        """Create an instance of Cache based on loaded configuration.
-
-        Returns:
-            An instance of `Cache` (either `PostgresCache` or `InMemoryCache`).
-        """
-        logger.info("Creating cache instance of type %s", config.type)
-        match config.type:
+    def conversation_cache(config: ConversationCacheConfiguration | None) -> Cache | None:
+        """Create an instance of Cache based on loaded configuration.
+
+        Returns:
+            A Cache instance, or None when cache is not configured/disabled or not yet implemented.
+        """
+        if config is None or config.type is None:
+            logger.info("Conversation cache disabled or not configured")
+            return None
+
+        logger.info("Creating cache instance of type %s", config.type)
+        match config.type:
             case constants.CACHE_TYPE_MEMORY:
                 return None
             case constants.CACHE_TYPE_SQLITE:
                 return None
             case constants.CACHE_TYPE_POSTGRES:
                 return None
             case _:
                 raise ValueError(
-                    f"Invalid cache type: {config.type}. "
-                    f"Use '{constants.CACHE_TYPE_POSTGRES}' '{constants.CACHE_TYPE_SQLITE}' or "
-                    f"'{constants.CACHE_TYPE_MEMORY}' options."
+                    f"Invalid cache type: {config.type}. "
+                    f"Use '{constants.CACHE_TYPE_POSTGRES}', '{constants.CACHE_TYPE_SQLITE}', or "
+                    f"'{constants.CACHE_TYPE_MEMORY}'."
                 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@staticmethod
def conversation_cache(config: ConversationCacheConfiguration) -> Cache | None:
"""Create an instance of Cache based on loaded configuration.
Returns:
An instance of `Cache` (either `PostgresCache` or `InMemoryCache`).
"""
logger.info("Creating cache instance of type %s", config.type)
match config.type:
case constants.CACHE_TYPE_MEMORY:
return None
case constants.CACHE_TYPE_SQLITE:
return None
case constants.CACHE_TYPE_POSTGRES:
return None
case _:
raise ValueError(
f"Invalid cache type: {config.type}. "
f"Use '{constants.CACHE_TYPE_POSTGRES}' '{constants.CACHE_TYPE_SQLITE}' or "
f"'{constants.CACHE_TYPE_MEMORY}' options."
)
@staticmethod
def conversation_cache(config: ConversationCacheConfiguration | None) -> Cache | None:
"""Create an instance of Cache based on loaded configuration.
Returns:
A Cache instance, or None when cache is not configured/disabled or not yet implemented.
"""
if config is None or config.type is None:
logger.info("Conversation cache disabled or not configured")
return None
logger.info("Creating cache instance of type %s", config.type)
match config.type:
case constants.CACHE_TYPE_MEMORY:
return None
case constants.CACHE_TYPE_SQLITE:
return None
case constants.CACHE_TYPE_POSTGRES:
return None
case _:
raise ValueError(
f"Invalid cache type: {config.type}. "
f"Use '{constants.CACHE_TYPE_POSTGRES}', '{constants.CACHE_TYPE_SQLITE}', or "
f"'{constants.CACHE_TYPE_MEMORY}'."
)
🤖 Prompt for AI Agents
In src/cache/cache_factory.py around lines 15 to 35, the factory assumes a
non-None config and a non-None config.type and the docstring only lists
Postgres/InMemory; change the function to first handle a None/disabled config by
returning None if config is None or config.type is falsy, then use the existing
match on config.type to return the appropriate Cache implementations (or None
for in-memory/disabled), and update the docstring and the thrown ValueError
message to list all supported options including SQLite, Postgres and InMemory so
the documentation and error text match actual supported types.

Comment on lines +146 to +153
@property
def conversation_cache(self) -> Cache | None:
"""Return the conversation cache."""
if self._conversation_cache is None and self._configuration is not None:
self._conversation_cache = CacheFactory.conversation_cache(
self._configuration.conversation_cache
)
return self._conversation_cache
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard for unloaded config and None cache config before calling factory

Avoids passing None to the factory and keeps behavior consistent with other accessors.

 @property
 def conversation_cache(self) -> Cache | None:
     """Return the conversation cache."""
-    if self._conversation_cache is None and self._configuration is not None:
-        self._conversation_cache = CacheFactory.conversation_cache(
-            self._configuration.conversation_cache
-        )
-    return self._conversation_cache
+    if self._configuration is None:
+        raise LogicError("logic error: configuration is not loaded")
+    if self._conversation_cache is None:
+        cfg = self._configuration.conversation_cache
+        if cfg is None or cfg.type is None:
+            self._conversation_cache = None
+        else:
+            self._conversation_cache = CacheFactory.conversation_cache(cfg)
+    return self._conversation_cache
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@property
def conversation_cache(self) -> Cache | None:
"""Return the conversation cache."""
if self._conversation_cache is None and self._configuration is not None:
self._conversation_cache = CacheFactory.conversation_cache(
self._configuration.conversation_cache
)
return self._conversation_cache
@property
def conversation_cache(self) -> Cache | None:
"""Return the conversation cache."""
if self._configuration is None:
raise LogicError("logic error: configuration is not loaded")
if self._conversation_cache is None:
cfg = self._configuration.conversation_cache
if cfg is None or cfg.type is None:
self._conversation_cache = None
else:
self._conversation_cache = CacheFactory.conversation_cache(cfg)
return self._conversation_cache
🤖 Prompt for AI Agents
In src/configuration.py around lines 146 to 153, the property currently calls
CacheFactory.conversation_cache even when self._configuration is None or when
self._configuration.conversation_cache is None; add a guard so the factory is
only called if self._conversation_cache is None AND self._configuration is not
None AND self._configuration.conversation_cache is not None, otherwise leave
self._conversation_cache as None and return it; mirror the same defensive
pattern used by the other accessors to avoid passing None into the factory.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant