Skip to content
Merged
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
8 changes: 8 additions & 0 deletions src/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
AuthenticationConfiguration,
InferenceConfiguration,
DatabaseConfiguration,
ConversationCache,
)


Expand Down Expand Up @@ -124,6 +125,13 @@ def inference(self) -> InferenceConfiguration:
raise LogicError("logic error: configuration is not loaded")
return self._configuration.inference

@property
def conversation_cache(self) -> ConversationCache:
"""Return conversation cache configuration."""
if self._configuration is None:
raise LogicError("logic error: configuration is not loaded")
return self._configuration.conversation_cache

@property
def database_configuration(self) -> DatabaseConfiguration:
"""Return database configuration."""
Expand Down
5 changes: 5 additions & 0 deletions src/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,8 @@
POSTGRES_DEFAULT_SSL_MODE = "prefer"
# See: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-GSSENCMODE
POSTGRES_DEFAULT_GSS_ENCMODE = "prefer"

# cache constants
CACHE_TYPE_MEMORY = "memory"
CACHE_TYPE_SQLITE = "sqlite"
CACHE_TYPE_POSTGRES = "postgres"
48 changes: 48 additions & 0 deletions src/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ class SQLiteDatabaseConfiguration(ConfigurationBase):
db_path: str


class InMemoryCacheConfig(ConfigurationBase):
"""In-memory cache configuration."""

max_entries: PositiveInt


class PostgreSQLDatabaseConfiguration(ConfigurationBase):
"""PostgreSQL database configuration."""

Expand Down Expand Up @@ -480,6 +486,47 @@ def check_default_model_and_provider(self) -> Self:
return self


class ConversationCache(ConfigurationBase):
"""Conversation cache configuration."""

type: Literal["memory", "sqlite", "postgres"] | None = None
memory: Optional[InMemoryCacheConfig] = None
sqlite: Optional[SQLiteDatabaseConfiguration] = None
postgres: Optional[PostgreSQLDatabaseConfiguration] = None

@model_validator(mode="after")
def check_cache_configuration(self) -> Self:
"""Check conversation cache configuration."""
# if any backend config is provided, type must be explicitly selected
if self.type is None:
if any([self.memory, self.sqlite, self.postgres]):
raise ValueError(
"Conversation cache type must be set when backend configuration is provided"
)
# no type selected + no configuration is expected and fully supported
return self
match self.type:
case constants.CACHE_TYPE_MEMORY:
if self.memory is None:
raise ValueError("Memory cache is selected, but not configured")
# no other DBs configuration allowed
if any([self.sqlite, self.postgres]):
raise ValueError("Only memory cache config must be provided")
case constants.CACHE_TYPE_SQLITE:
if self.sqlite is None:
raise ValueError("SQLite cache is selected, but not configured")
# no other DBs configuration allowed
if any([self.memory, self.postgres]):
raise ValueError("Only SQLite cache config must be provided")
case constants.CACHE_TYPE_POSTGRES:
if self.postgres is None:
raise ValueError("PostgreSQL cache is selected, but not configured")
# no other DBs configuration allowed
if any([self.memory, self.sqlite]):
raise ValueError("Only PostgreSQL cache config must be provided")
return self


class Configuration(ConfigurationBase):
"""Global service configuration."""

Expand All @@ -495,6 +542,7 @@ class Configuration(ConfigurationBase):
authorization: Optional[AuthorizationConfiguration] = None
customization: Optional[Customization] = None
inference: InferenceConfiguration = Field(default_factory=InferenceConfiguration)
conversation_cache: ConversationCache = Field(default_factory=ConversationCache)

def dump(self, filename: str = "configuration.json") -> None:
"""Dump actual configuration into JSON file."""
Expand Down
200 changes: 200 additions & 0 deletions tests/unit/models/config/test_conversation_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
"""Unit tests for ConversationCache model."""

from pathlib import Path

import pytest

from pydantic import ValidationError

import constants
from models.config import (
ConversationCache,
InMemoryCacheConfig,
SQLiteDatabaseConfiguration,
PostgreSQLDatabaseConfiguration,
)


def test_conversation_cache_no_type_specified() -> None:
"""Check the test for type as optional attribute."""
c = ConversationCache()
assert c.type is None


def test_conversation_cache_unknown_type() -> None:
"""Check the test for cache type."""
with pytest.raises(
ValidationError, match="Input should be 'memory', 'sqlite' or 'postgres'"
):
_ = ConversationCache(type="foo")


def test_conversation_cache_correct_type_but_not_configured(subtests) -> None:
"""Check the test for cache type."""
with subtests.test(msg="Memory cache"):
with pytest.raises(
ValidationError, match="Memory cache is selected, but not configured"
):
_ = ConversationCache(type=constants.CACHE_TYPE_MEMORY)

with subtests.test(msg="SQLite cache"):
with pytest.raises(
ValidationError, match="SQLite cache is selected, but not configured"
):
_ = ConversationCache(type=constants.CACHE_TYPE_SQLITE)

with subtests.test(msg="SQLite cache"):
with pytest.raises(
ValidationError, match="PostgreSQL cache is selected, but not configured"
):
_ = ConversationCache(type=constants.CACHE_TYPE_POSTGRES)


def test_conversation_cache_no_type_but_configured(subtests) -> None:
"""Check the test for cache type."""
m = "Conversation cache type must be set when backend configuration is provided"

with subtests.test(msg="Memory cache"):
with pytest.raises(ValidationError, match=m):
_ = ConversationCache(memory=InMemoryCacheConfig(max_entries=100))

with subtests.test(msg="SQLite cache"):
with pytest.raises(ValidationError, match=m):
_ = ConversationCache(sqlite=SQLiteDatabaseConfiguration(db_path="path"))

with subtests.test(msg="PostgreSQL cache"):
d = PostgreSQLDatabaseConfiguration(
db="db",
user="user",
password="password",
port=1234,
ca_cert_path=Path("tests/configuration/server.crt"),
)
with pytest.raises(ValidationError, match=m):
_ = ConversationCache(postgres=d)


def test_conversation_cache_multiple_configurations(subtests) -> None:
"""Test how multiple configurations are handled."""
d = PostgreSQLDatabaseConfiguration(
db="db",
user="user",
password="password",
port=1234,
ca_cert_path=Path("tests/configuration/server.crt"),
)

with subtests.test(msg="Memory cache"):
with pytest.raises(
ValidationError, match="Only memory cache config must be provided"
):
_ = ConversationCache(
type=constants.CACHE_TYPE_MEMORY,
memory=InMemoryCacheConfig(max_entries=100),
sqlite=SQLiteDatabaseConfiguration(db_path="path"),
postgres=d,
)

with subtests.test(msg="SQLite cache"):
with pytest.raises(
ValidationError, match="Only SQLite cache config must be provided"
):
_ = ConversationCache(
type=constants.CACHE_TYPE_SQLITE,
memory=InMemoryCacheConfig(max_entries=100),
sqlite=SQLiteDatabaseConfiguration(db_path="path"),
postgres=d,
)

with subtests.test(msg="PostgreSQL cache"):
with pytest.raises(
ValidationError, match="Only PostgreSQL cache config must be provided"
):
_ = ConversationCache(
type=constants.CACHE_TYPE_POSTGRES,
memory=InMemoryCacheConfig(max_entries=100),
sqlite=SQLiteDatabaseConfiguration(db_path="path"),
postgres=d,
)


def test_conversation_type_memory() -> None:
"""Test the memory conversation cache configuration."""
c = ConversationCache(
type=constants.CACHE_TYPE_MEMORY, memory=InMemoryCacheConfig(max_entries=100)
)
assert c.type == constants.CACHE_TYPE_MEMORY
assert c.memory is not None
assert c.sqlite is None
assert c.postgres is None
assert c.memory.max_entries == 100


def test_conversation_type_memory_wrong_config() -> None:
"""Test the memory conversation cache configuration."""
with pytest.raises(ValidationError, match="Field required"):
_ = ConversationCache(
type=constants.CACHE_TYPE_MEMORY,
memory=InMemoryCacheConfig(),
)

with pytest.raises(ValidationError, match="Input should be greater than 0"):
_ = ConversationCache(
type=constants.CACHE_TYPE_MEMORY,
memory=InMemoryCacheConfig(max_entries=-100),
)


def test_conversation_type_sqlite() -> None:
"""Test the SQLite conversation cache configuration."""
c = ConversationCache(
type=constants.CACHE_TYPE_SQLITE,
sqlite=SQLiteDatabaseConfiguration(db_path="path"),
)
assert c.type == constants.CACHE_TYPE_SQLITE
assert c.memory is None
assert c.sqlite is not None
assert c.postgres is None
assert c.sqlite.db_path == "path"


def test_conversation_type_sqlite_wrong_config() -> None:
"""Test the SQLite conversation cache configuration."""
with pytest.raises(ValidationError, match="Field required"):
_ = ConversationCache(
type=constants.CACHE_TYPE_SQLITE,
memory=SQLiteDatabaseConfiguration(),
)


def test_conversation_type_postgres() -> None:
"""Test the PostgreSQL conversation cache configuration."""
d = PostgreSQLDatabaseConfiguration(
db="db",
user="user",
password="password",
port=1234,
ca_cert_path=Path("tests/configuration/server.crt"),
)

c = ConversationCache(
type=constants.CACHE_TYPE_POSTGRES,
postgres=d,
)
assert c.type == constants.CACHE_TYPE_POSTGRES
assert c.memory is None
assert c.sqlite is None
assert c.postgres is not None
assert c.postgres.host == "localhost"
assert c.postgres.port == 1234
assert c.postgres.db == "db"
assert c.postgres.user == "user"


def test_conversation_type_postgres_wrong_config() -> None:
"""Test the SQLite conversation cache configuration."""
with pytest.raises(ValidationError, match="Field required"):
_ = ConversationCache(
type=constants.CACHE_TYPE_POSTGRES,
postgres=PostgreSQLDatabaseConfiguration(),
)
6 changes: 6 additions & 0 deletions tests/unit/models/config/test_dump_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@ def test_dump_configuration(tmp_path) -> None:
},
},
"authorization": None,
"conversation_cache": {
"memory": None,
"postgres": None,
"sqlite": None,
"type": None,
},
}


Expand Down
7 changes: 7 additions & 0 deletions tests/unit/test_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ def test_default_configuration() -> None:
# try to read property
_ = cfg.database_configuration # pylint: disable=pointless-statement

with pytest.raises(Exception, match="logic error: configuration is not loaded"):
# try to read property
_ = cfg.conversation_cache # pylint: disable=pointless-statement


def test_configuration_is_singleton() -> None:
"""Test that configuration is singleton."""
Expand Down Expand Up @@ -144,6 +148,9 @@ def test_init_from_dict() -> None:
# check inference configuration
assert cfg.inference is not None

# check conversation cache
assert cfg.conversation_cache is not None


def test_init_from_dict_with_mcp_servers() -> None:
"""Test initialization with MCP servers configuration."""
Expand Down
Loading