From c085890bd42ab44754c997e24c93c46bec5e5597 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Wed, 20 Aug 2025 08:54:59 +0200 Subject: [PATCH 1/4] Implementation of Llama Stack version check on startup --- src/constants.py | 4 ++++ src/lightspeed_stack.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/src/constants.py b/src/constants.py index 595c69249..4a2a4b862 100644 --- a/src/constants.py +++ b/src/constants.py @@ -1,5 +1,9 @@ """Constants used in business logic.""" +# Minimal and maximal supported Llama Stack version +MINIMAL_SUPPORTED_LLAMA_STACK_VERSION = "0.2.17" +MAXIMAL_SUPPORTED_LLAMA_STACK_VERSION = "0.2.17" + UNABLE_TO_PROCESS_RESPONSE = "Unable to process this request" # Supported attachment types diff --git a/src/lightspeed_stack.py b/src/lightspeed_stack.py index cf47c2f91..192b3c7a1 100644 --- a/src/lightspeed_stack.py +++ b/src/lightspeed_stack.py @@ -12,6 +12,7 @@ from runners.uvicorn import start_uvicorn from configuration import configuration from client import AsyncLlamaStackClientHolder +from utils.llama_stack_version import check_llama_stack_version FORMAT = "%(message)s" logging.basicConfig( @@ -66,6 +67,8 @@ def main() -> None: asyncio.run( AsyncLlamaStackClientHolder().load(configuration.configuration.llama_stack) ) + client = AsyncLlamaStackClientHolder().get_client() + asyncio.run(check_llama_stack_version(client)) if args.dump_configuration: configuration.configuration.dump() From 0d253e2a4f53342bf700189c1a123134ca6dae40 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Wed, 20 Aug 2025 08:55:41 +0200 Subject: [PATCH 2/4] Added new unit tests --- src/utils/llama_stack_version.py | 51 ++++++++++ tests/unit/utils/test_llama_stack_version.py | 101 +++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 src/utils/llama_stack_version.py create mode 100644 tests/unit/utils/test_llama_stack_version.py diff --git a/src/utils/llama_stack_version.py b/src/utils/llama_stack_version.py new file mode 100644 index 000000000..691b9064a --- /dev/null +++ b/src/utils/llama_stack_version.py @@ -0,0 +1,51 @@ +"""Check if the Llama Stack version is supported by the LCS.""" + +import logging + +from semver import Version + +from llama_stack_client._client import AsyncLlamaStackClient + + +from constants import ( + MINIMAL_SUPPORTED_LLAMA_STACK_VERSION, + MAXIMAL_SUPPORTED_LLAMA_STACK_VERSION, +) + +logger = logging.getLogger("utils.llama_stack_version") + + +class InvalidLlamaStackVersionException(Exception): + """Llama Stack version is not valid.""" + + +async def check_llama_stack_version( + client: AsyncLlamaStackClient, +) -> None: + """Check if the Llama Stack version is supported by the LCS.""" + version_info = await client.inspect.version() + compare_versions( + version_info.version, + MINIMAL_SUPPORTED_LLAMA_STACK_VERSION, + MAXIMAL_SUPPORTED_LLAMA_STACK_VERSION, + ) + + +def compare_versions(version_info: str, minimal: str, maximal: str) -> None: + """Compare current Llama Stack version with minimal and maximal allowed versions.""" + current_version = Version.parse(version_info) + minimal_version = Version.parse(minimal) + maximal_version = Version.parse(maximal) + logger.debug("Current version: %s", current_version) + logger.debug("Minimal version: %s", minimal_version) + logger.debug("Maximal version: %s", maximal_version) + + if current_version < minimal_version: + raise InvalidLlamaStackVersionException( + f"Llama Stack version >= {minimal_version} is required, but {current_version} is used" + ) + if current_version > maximal_version: + raise InvalidLlamaStackVersionException( + f"Llama Stack version <= {maximal_version} is required, but {current_version} is used" + ) + logger.info("Correct Llama Stack version : %s", current_version) diff --git a/tests/unit/utils/test_llama_stack_version.py b/tests/unit/utils/test_llama_stack_version.py new file mode 100644 index 000000000..a2ca92806 --- /dev/null +++ b/tests/unit/utils/test_llama_stack_version.py @@ -0,0 +1,101 @@ +"""Unit tests for utility function to check Llama Stack version.""" + +import pytest +from semver import Version + +from llama_stack_client.types import VersionInfo + +from utils.llama_stack_version import ( + check_llama_stack_version, + InvalidLlamaStackVersionException, +) + +from constants import ( + MINIMAL_SUPPORTED_LLAMA_STACK_VERSION, + MAXIMAL_SUPPORTED_LLAMA_STACK_VERSION, +) + + +@pytest.mark.asyncio +async def test_check_llama_stack_version_minimal_supported_version(mocker): + """Test the check_llama_stack_version function.""" + + # mock the Llama Stack client + mock_client = mocker.AsyncMock() + mock_client.inspect.version.return_value = VersionInfo( + version=MINIMAL_SUPPORTED_LLAMA_STACK_VERSION + ) + + # test if the version is checked + await check_llama_stack_version(mock_client) + + +@pytest.mark.asyncio +async def test_check_llama_stack_version_maximal_supported_version(mocker): + """Test the check_llama_stack_version function.""" + + # mock the Llama Stack client + mock_client = mocker.AsyncMock() + mock_client.inspect.version.return_value = VersionInfo( + version=MAXIMAL_SUPPORTED_LLAMA_STACK_VERSION + ) + + # test if the version is checked + await check_llama_stack_version(mock_client) + + +@pytest.mark.asyncio +async def test_check_llama_stack_version_too_small_version(mocker): + """Test the check_llama_stack_version function.""" + + # mock the Llama Stack client + mock_client = mocker.AsyncMock() + + # that is surely out of range + mock_client.inspect.version.return_value = VersionInfo(version="0.0.0") + + expected_exception_msg = ( + f"Llama Stack version >= {MINIMAL_SUPPORTED_LLAMA_STACK_VERSION} " + + "is required, but 0.0.0 is used" + ) + # test if the version is checked + with pytest.raises(InvalidLlamaStackVersionException, match=expected_exception_msg): + await check_llama_stack_version(mock_client) + + +async def _check_version_must_fail(mock_client, bigger_version): + mock_client.inspect.version.return_value = VersionInfo(version=str(bigger_version)) + + expected_exception_msg = ( + f"Llama Stack version <= {MAXIMAL_SUPPORTED_LLAMA_STACK_VERSION} is required, " + + f"but {bigger_version} is used" + ) + # test if the version is checked + with pytest.raises(InvalidLlamaStackVersionException, match=expected_exception_msg): + await check_llama_stack_version(mock_client) + + +@pytest.mark.asyncio +async def test_check_llama_stack_version_too_big_version(mocker, subtests): + """Test the check_llama_stack_version function.""" + + # mock the Llama Stack client + mock_client = mocker.AsyncMock() + + max_version = Version.parse(MAXIMAL_SUPPORTED_LLAMA_STACK_VERSION) + + with subtests.test(msg="Increased patch number"): + bigger_version = max_version.bump_patch() + await _check_version_must_fail(mock_client, bigger_version) + + with subtests.test(msg="Increased minor number"): + bigger_version = max_version.bump_minor() + await _check_version_must_fail(mock_client, bigger_version) + + with subtests.test(msg="Increased major number"): + bigger_version = max_version.bump_major() + await _check_version_must_fail(mock_client, bigger_version) + + with subtests.test(msg="Increased all numbers"): + bigger_version = max_version.bump_major().bump_minor().bump_patch() + await _check_version_must_fail(mock_client, bigger_version) From de4b13ccf9f3d3254de4dadcdc529b9e74a9519a Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Wed, 20 Aug 2025 08:55:50 +0200 Subject: [PATCH 3/4] New dependencies --- pyproject.toml | 2 ++ uv.lock | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index c4c99c83f..28b64298e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ dependencies = [ "email-validator>=2.2.0", "openai==1.99.9", "sqlalchemy>=2.0.42", + "semver<4.0.0", ] @@ -91,6 +92,7 @@ dev = [ "build>=1.2.2.post1", "twine>=6.1.0", "openapi-to-md>=0.1.0b2", + "pytest-subtests>=0.14.2", ] llslibdev = [ # To check llama-stack API provider dependecies: diff --git a/uv.lock b/uv.lock index 6f1062062..4e9067087 100644 --- a/uv.lock +++ b/uv.lock @@ -1255,6 +1255,7 @@ dependencies = [ { name = "openai" }, { name = "prometheus-client" }, { name = "rich" }, + { name = "semver" }, { name = "sqlalchemy" }, { name = "starlette" }, { name = "uvicorn" }, @@ -1279,6 +1280,7 @@ dev = [ { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "pytest-mock" }, + { name = "pytest-subtests" }, { name = "ruff" }, { name = "twine" }, { name = "types-cachetools" }, @@ -1330,6 +1332,7 @@ requires-dist = [ { name = "openai", specifier = "==1.99.9" }, { name = "prometheus-client", specifier = ">=0.22.1" }, { name = "rich", specifier = ">=14.0.0" }, + { name = "semver", specifier = "<4.0.0" }, { name = "sqlalchemy", specifier = ">=2.0.42" }, { name = "starlette", specifier = ">=0.47.1" }, { name = "uvicorn", specifier = ">=0.34.3" }, @@ -1354,6 +1357,7 @@ dev = [ { name = "pytest-asyncio", specifier = ">=1.0.0" }, { name = "pytest-cov", specifier = ">=5.0.0" }, { name = "pytest-mock", specifier = ">=3.14.0" }, + { name = "pytest-subtests", specifier = ">=0.14.2" }, { name = "ruff", specifier = ">=0.11.13" }, { name = "twine", specifier = ">=6.1.0" }, { name = "types-cachetools", specifier = ">=6.1.0.20250717" }, @@ -2683,6 +2687,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" }, ] +[[package]] +name = "pytest-subtests" +version = "0.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/30/6ec8dfc678ddfd1c294212bbd7088c52d3f7fbf3f05e6d8a440c13b9741a/pytest_subtests-0.14.2.tar.gz", hash = "sha256:7154a8665fd528ee70a76d00216a44d139dc3c9c83521a0f779f7b0ad4f800de", size = 18083, upload-time = "2025-06-13T10:50:01.636Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/d4/9bf12e59fb882b0cf4f993871e1adbee094802224c429b00861acee1a169/pytest_subtests-0.14.2-py3-none-any.whl", hash = "sha256:8da0787c994ab372a13a0ad7d390533ad2e4385cac167b3ac501258c885d0b66", size = 9115, upload-time = "2025-06-13T10:50:00.543Z" }, +] + [[package]] name = "pythainlp" version = "5.1.2" @@ -3120,6 +3137,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221, upload-time = "2022-08-13T16:22:44.457Z" }, ] +[[package]] +name = "semver" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730, upload-time = "2025-01-24T13:19:27.617Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" }, +] + [[package]] name = "setuptools" version = "80.9.0" From c285340bd9d5ee09f2170560c41ea9410657a05f Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Wed, 20 Aug 2025 08:56:04 +0200 Subject: [PATCH 4/4] Updated documentation accordingly --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 147200c13..942213c72 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ The service includes comprehensive user data collection capabilities for various * [Llama Stack project and configuration](#llama-stack-project-and-configuration) * [Check connection to Llama Stack](#check-connection-to-llama-stack) * [Llama Stack as client library](#llama-stack-as-client-library) + * [Llama Stack version check](#llama-stack-version-check) * [User data collection](#user-data-collection) * [System prompt](#system-prompt) * [Safety Shields](#safety-shields) @@ -243,6 +244,12 @@ user_data_collection: transcripts_storage: "/tmp/data/transcripts" ``` +## Llama Stack version check + +During Lightspeed Core Stack service startup, the Llama Stack version is retrieved. The version is tested against two constants `MINIMAL_SUPPORTED_LLAMA_STACK_VERSION` and `MAXIMAL_SUPPORTED_LLAMA_STACK_VERSION` which are defined in `src/constants.py`. If the actual Llama Stack version is outside the range defined by these two constants, the service won't start and administrator will be informed about this problem. + + + ## User data collection The Lightspeed Core Stack includes comprehensive user data collection capabilities to gather various types of user interaction data for analysis and improvement. This includes feedback, conversation transcripts, and other user interaction data.