From cf588c1ca698a250c8d24db72dfe852127003afc Mon Sep 17 00:00:00 2001 From: are-ces <195810094+are-ces@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:49:48 +0100 Subject: [PATCH] Solr enrichment in library mode - The solr enrichment script runs also when running lightspeed-stack in library mode - Hid is_chunk:true filtering attribute from the user --- docs/config.md | 2 +- docs/rag_guide.md | 8 ++- examples/lightspeed-stack-byok-okp-rag.yaml | 6 +-- src/client.py | 32 ++++++------ src/constants.py | 3 ++ src/llama_stack_configuration.py | 38 ++++++++------ src/models/config.py | 10 ++-- .../models/config/test_dump_configuration.py | 10 ++-- .../models/config/test_rag_configuration.py | 8 ++- tests/unit/test_llama_stack_configuration.py | 50 +++++++++++++++---- 10 files changed, 98 insertions(+), 69 deletions(-) diff --git a/docs/config.md b/docs/config.md index 10246df56..e4c9a8f39 100644 --- a/docs/config.md +++ b/docs/config.md @@ -408,7 +408,7 @@ Only relevant when ``"okp"`` is listed in ``rag.inline`` or ``rag.tool``. | Field | Type | Description | |-------|------|-------------| | offline | boolean | When True, use parent_id for OKP chunk source URLs. When False, use reference_url for chunk source URLs. | -| chunk_filter_query | string | OKP filter query applied to every OKP search request. Defaults to 'is_chunk:true' to restrict results to chunk documents. To add extra constraints, extend the expression using boolean syntax, e.g. 'is_chunk:true AND product:*openshift*'. | +| chunk_filter_query | string | Additional OKP filter query applied to every OKP search request. Use Solr boolean syntax, e.g. 'product:\*ansible\* AND product:\*openshift\*'. | ## PostgreSQLDatabaseConfiguration diff --git a/docs/rag_guide.md b/docs/rag_guide.md index a7122b6ea..1d0389fe3 100644 --- a/docs/rag_guide.md +++ b/docs/rag_guide.md @@ -343,15 +343,13 @@ curl -sX POST http://localhost:8080/v1/query \ **Query Filtering:** -To filter the Solr context, set the `chunk_filter_query` field in the `okp` section of +To further filter the OKP context, set the `chunk_filter_query` field in the `okp` section of `lightspeed-stack.yaml`. Filters follow the Solr key:value format and are applied as a static -`fq` parameter on every OKP search request. The default value `"is_chunk:true"` restricts -results to chunk documents. To add extra constraints, extend the expression using Solr boolean -syntax: +`fq` parameter on every OKP search request. ```yaml okp: - chunk_filter_query: "is_chunk:true AND product:*openshift*" + chunk_filter_query: "product:*openshift*" ``` > [!NOTE] diff --git a/examples/lightspeed-stack-byok-okp-rag.yaml b/examples/lightspeed-stack-byok-okp-rag.yaml index 3cd358ff1..4a6b433b8 100644 --- a/examples/lightspeed-stack-byok-okp-rag.yaml +++ b/examples/lightspeed-stack-byok-okp-rag.yaml @@ -66,6 +66,6 @@ rag: # OKP provider settings (only used when 'okp' is listed in rag.inline or rag.tool) okp: offline: true # true = use parent_id for source URLs, false = use reference_url - # Solr fq applied to every OKP search request. Combine with AND for extra constraints: - # chunk_filter_query: "is_chunk:true AND product:*openshift*" - chunk_filter_query: "is_chunk:true" + # Additional Solr filter query applied to every OKP search request. + # Use Solr boolean syntax + # chunk_filter_query: "product:*ansible* AND product:*openshift*" diff --git a/src/client.py b/src/client.py index d7ac27406..e516d84ca 100644 --- a/src/client.py +++ b/src/client.py @@ -11,11 +11,11 @@ from llama_stack_client import APIConnectionError, AsyncLlamaStackClient # type: ignore from configuration import configuration -from llama_stack_configuration import enrich_byok_rag, YamlDumper +from llama_stack_configuration import YamlDumper, enrich_byok_rag, enrich_solr +from log import get_logger from models.config import LlamaStackConfiguration from models.responses import ServiceUnavailableResponse from utils.types import Singleton -from log import get_logger logger = get_logger(__name__) @@ -52,14 +52,9 @@ async def _load_library_client(self, config: LlamaStackConfiguration) -> None: ) logger.info("Using Llama stack as library client") - byok_rag = [b.model_dump() for b in configuration.configuration.byok_rag] - - if byok_rag: # BYOK RAG configured - enrich and store enriched path - self._config_path = self._enrich_library_config( - config.library_client_config_path, byok_rag - ) - else: # No RAG - store original path - self._config_path = config.library_client_config_path + self._config_path = self._enrich_library_config( + config.library_client_config_path + ) client = AsyncLlamaStackAsLibraryClient(self._config_path) await client.initialize() @@ -78,13 +73,8 @@ def _load_service_client(self, config: LlamaStackConfiguration) -> None: base_url=base_url, api_key=api_key, timeout=config.timeout ) - def _enrich_library_config( - self, input_config_path: str, byok_rag: list[dict] - ) -> str: - """Enrich llama-stack config with BYOK RAG settings. - - Only called when BYOK RAG is configured. - """ + def _enrich_library_config(self, input_config_path: str) -> str: + """Enrich llama-stack config with BYOK RAG and OKP Solr settings.""" try: with open(input_config_path, "r", encoding="utf-8") as f: ls_config = yaml.safe_load(f) @@ -92,7 +82,13 @@ def _enrich_library_config( logger.warning("Failed to read llama-stack config: %s", e) return input_config_path - enrich_byok_rag(ls_config, byok_rag) + config = configuration.configuration + + # Enrichment: BYOK RAG + enrich_byok_rag(ls_config, [b.model_dump() for b in config.byok_rag]) + + # Enrichment: Solr - enabled when "okp" appears in either inline or tool list + enrich_solr(ls_config, config.rag.model_dump(), config.okp.model_dump()) enriched_path = os.path.join( tempfile.gettempdir(), "llama_stack_enriched_config.yaml" diff --git a/src/constants.py b/src/constants.py index 20145a812..ffb805c8a 100644 --- a/src/constants.py +++ b/src/constants.py @@ -184,6 +184,9 @@ SOLR_VECTOR_SEARCH_DEFAULT_SCORE_THRESHOLD = 0.3 SOLR_VECTOR_SEARCH_DEFAULT_MODE = "hybrid" +# Internal Solr filter always applied to restrict results to chunk documents +SOLR_CHUNK_FILTER_QUERY = "is_chunk:true" + # SOLR OKP RAG MIMIR_DOC_URL = "https://mimir.corp.redhat.com" diff --git a/src/llama_stack_configuration.py b/src/llama_stack_configuration.py index a0530b326..a0b7cac7a 100644 --- a/src/llama_stack_configuration.py +++ b/src/llama_stack_configuration.py @@ -377,19 +377,36 @@ def enrich_byok_rag(ls_config: dict[str, Any], byok_rag: list[dict[str, Any]]) - # ============================================================================= -def enrich_solr(ls_config: dict[str, Any], solr_config: dict[str, Any]) -> None: +def enrich_solr( # pylint: disable=too-many-locals + ls_config: dict[str, Any], + rag_config: dict[str, Any], + okp_config: dict[str, Any], +) -> None: """Enrich Llama Stack config with Solr settings. Args: ls_config: Llama Stack configuration dict (modified in place) - solr_config: Solr configuration dict. Expected keys: - - enabled (bool): whether Solr enrichment should run + rag_config: RAG configuration dict. Used keys: + - inline (list[str]): inline RAG IDs + - tool (list[str]): tool RAG IDs + okp_config: OKP configuration dict. Used keys: - chunk_filter_query (str): Solr filter query for chunk retrieval """ - if not solr_config or not solr_config.get("enabled"): + inline_ids = rag_config.get("inline") or [] + tool_ids = rag_config.get("tool") or [] + okp_enabled = constants.OKP_RAG_ID in inline_ids or constants.OKP_RAG_ID in tool_ids + + if not okp_enabled: logger.info("OKP is not enabled: skipping") return + user_filter = okp_config.get("chunk_filter_query") + chunk_filter_query = ( + f"{constants.SOLR_CHUNK_FILTER_QUERY} AND {user_filter}" + if user_filter + else constants.SOLR_CHUNK_FILTER_QUERY + ) + logger.info("Enriching Llama Stack config with OKP") # Add vector_io provider for Solr @@ -420,9 +437,6 @@ def enrich_solr(ls_config: dict[str, Any], solr_config: dict[str, Any]) -> None: embedding_dim_env = ( f"${{env.SOLR_EMBEDDING_DIM:={constants.SOLR_DEFAULT_EMBEDDING_DIMENSION}}}" ) - - chunk_filter_query = solr_config.get("chunk_filter_query", "is_chunk:true") - ls_config["providers"]["vector_io"].append( { "provider_id": constants.SOLR_PROVIDER_ID, @@ -543,15 +557,7 @@ def generate_configuration( enrich_byok_rag(ls_config, config.get("byok_rag", [])) # Enrichment: Solr - enabled when "okp" appears in either inline or tool list - rag_config = config.get("rag", {}) - inline_ids = rag_config.get("inline") or [] - tool_ids = rag_config.get("tool") or [] - okp_enabled = constants.OKP_RAG_ID in inline_ids or constants.OKP_RAG_ID in tool_ids - okp_config = config.get("okp", {}) - chunk_filter_query = okp_config.get("chunk_filter_query", "is_chunk:true") - enrich_solr( - ls_config, {"enabled": okp_enabled, "chunk_filter_query": chunk_filter_query} - ) + enrich_solr(ls_config, config.get("rag", {}), config.get("okp", {})) logger.info("Writing Llama Stack configuration into file %s", output_file) diff --git a/src/models/config.py b/src/models/config.py index d7a15a334..c797fa362 100644 --- a/src/models/config.py +++ b/src/models/config.py @@ -1740,13 +1740,11 @@ class OkpConfiguration(ConfigurationBase): "When False, use reference_url for chunk source URLs.", ) - chunk_filter_query: str = Field( - default="is_chunk:true", + chunk_filter_query: Optional[str] = Field( + default=None, title="OKP chunk filter query", - description="OKP filter query applied to every OKP search request. " - "Defaults to 'is_chunk:true' to restrict results to chunk documents. " - "To add extra constraints, extend the expression using boolean syntax, " - "e.g. 'is_chunk:true AND product:*openshift*'.", + description="Additional OKP filter query applied to every OKP search request. " + "Use Solr boolean syntax, e.g. 'product:ansible AND product:*openshift*'.", ) diff --git a/tests/unit/models/config/test_dump_configuration.py b/tests/unit/models/config/test_dump_configuration.py index 56c69ac72..4cf164a43 100644 --- a/tests/unit/models/config/test_dump_configuration.py +++ b/tests/unit/models/config/test_dump_configuration.py @@ -212,7 +212,7 @@ def test_dump_configuration(tmp_path: Path) -> None: }, "okp": { "offline": True, - "chunk_filter_query": "is_chunk:true", + "chunk_filter_query": None, }, "splunk": None, "deployment_environment": "development", @@ -563,7 +563,7 @@ def test_dump_configuration_with_quota_limiters(tmp_path: Path) -> None: }, "okp": { "offline": True, - "chunk_filter_query": "is_chunk:true", + "chunk_filter_query": None, }, "splunk": None, "deployment_environment": "development", @@ -792,7 +792,7 @@ def test_dump_configuration_with_quota_limiters_different_values( }, "okp": { "offline": True, - "chunk_filter_query": "is_chunk:true", + "chunk_filter_query": None, }, "splunk": None, "deployment_environment": "development", @@ -996,7 +996,7 @@ def test_dump_configuration_byok(tmp_path: Path) -> None: }, "okp": { "offline": True, - "chunk_filter_query": "is_chunk:true", + "chunk_filter_query": None, }, "splunk": None, "deployment_environment": "development", @@ -1185,7 +1185,7 @@ def test_dump_configuration_pg_namespace(tmp_path: Path) -> None: }, "okp": { "offline": True, - "chunk_filter_query": "is_chunk:true", + "chunk_filter_query": None, }, "splunk": None, "deployment_environment": "development", diff --git a/tests/unit/models/config/test_rag_configuration.py b/tests/unit/models/config/test_rag_configuration.py index ee13da0aa..bc44ef154 100644 --- a/tests/unit/models/config/test_rag_configuration.py +++ b/tests/unit/models/config/test_rag_configuration.py @@ -73,7 +73,7 @@ def test_default_values(self) -> None: """Test that OkpConfiguration has correct default values.""" config = OkpConfiguration() assert config.offline is True - assert config.chunk_filter_query == "is_chunk:true" + assert config.chunk_filter_query is None def test_offline_false(self) -> None: """Test offline can be set to False (online mode).""" @@ -82,10 +82,8 @@ def test_offline_false(self) -> None: def test_custom_chunk_filter_query(self) -> None: """Test that chunk_filter_query can be customised.""" - config = OkpConfiguration( - chunk_filter_query="is_chunk:true AND product:*openshift*" - ) - assert config.chunk_filter_query == "is_chunk:true AND product:*openshift*" + config = OkpConfiguration(chunk_filter_query="product:*openshift*") + assert config.chunk_filter_query == "product:*openshift*" def test_no_unknown_fields_allowed(self) -> None: """Test that OkpConfiguration rejects unknown fields.""" diff --git a/tests/unit/test_llama_stack_configuration.py b/tests/unit/test_llama_stack_configuration.py index 8d17a96c7..e402ebd88 100644 --- a/tests/unit/test_llama_stack_configuration.py +++ b/tests/unit/test_llama_stack_configuration.py @@ -403,24 +403,27 @@ def test_generate_configuration_with_byok(tmp_path: Path) -> None: # ============================================================================= +_OKP_RAG_CONFIG = {"inline": ["okp"]} + + def test_enrich_solr_skips_when_not_enabled() -> None: - """Test enrich_solr does nothing when Solr is not enabled.""" + """Test enrich_solr does nothing when OKP is not in rag inline or tool lists.""" ls_config: dict[str, Any] = {} - enrich_solr(ls_config, {"enabled": False}) + enrich_solr(ls_config, {"inline": [], "tool": []}, {}) assert not ls_config def test_enrich_solr_skips_when_empty_config() -> None: - """Test enrich_solr does nothing with empty config.""" + """Test enrich_solr does nothing with empty rag config.""" ls_config: dict[str, Any] = {} - enrich_solr(ls_config, {}) + enrich_solr(ls_config, {}, {}) assert not ls_config def test_enrich_solr_adds_vector_io_provider() -> None: """Test enrich_solr adds Solr provider to vector_io section.""" ls_config: dict[str, Any] = {} - enrich_solr(ls_config, {"enabled": True}) + enrich_solr(ls_config, _OKP_RAG_CONFIG, {}) assert "providers" in ls_config assert "vector_io" in ls_config["providers"] @@ -431,7 +434,7 @@ def test_enrich_solr_adds_vector_io_provider() -> None: def test_enrich_solr_adds_vector_store_registration() -> None: """Test enrich_solr registers the Solr vector store.""" ls_config: dict[str, Any] = {} - enrich_solr(ls_config, {"enabled": True}) + enrich_solr(ls_config, _OKP_RAG_CONFIG, {}) assert "registered_resources" in ls_config store_ids = [ @@ -443,7 +446,7 @@ def test_enrich_solr_adds_vector_store_registration() -> None: def test_enrich_solr_adds_embedding_model() -> None: """Test enrich_solr registers the Solr embedding model.""" ls_config: dict[str, Any] = {} - enrich_solr(ls_config, {"enabled": True}) + enrich_solr(ls_config, _OKP_RAG_CONFIG, {}) model_ids = [m["model_id"] for m in ls_config["registered_resources"]["models"]] assert "solr_embedding" in model_ids @@ -454,7 +457,7 @@ def test_enrich_solr_skips_duplicate_provider() -> None: ls_config: dict[str, Any] = { "providers": {"vector_io": [{"provider_id": "okp_solr"}]} } - enrich_solr(ls_config, {"enabled": True}) + enrich_solr(ls_config, _OKP_RAG_CONFIG, {}) provider_ids = [p["provider_id"] for p in ls_config["providers"]["vector_io"]] assert provider_ids.count("okp_solr") == 1 @@ -465,7 +468,7 @@ def test_enrich_solr_skips_duplicate_vector_store() -> None: ls_config: dict[str, Any] = { "registered_resources": {"vector_stores": [{"vector_store_id": "portal-rag"}]} } - enrich_solr(ls_config, {"enabled": True}) + enrich_solr(ls_config, _OKP_RAG_CONFIG, {}) store_ids = [ s["vector_store_id"] for s in ls_config["registered_resources"]["vector_stores"] @@ -482,7 +485,7 @@ def test_enrich_solr_preserves_existing_config() -> None: "models": [{"model_id": "existing_model"}], }, } - enrich_solr(ls_config, {"enabled": True}) + enrich_solr(ls_config, _OKP_RAG_CONFIG, {}) provider_ids = [p["provider_id"] for p in ls_config["providers"]["vector_io"]] assert "existing_provider" in provider_ids @@ -493,3 +496,30 @@ def test_enrich_solr_preserves_existing_config() -> None: ] assert "existing_store" in store_ids assert "portal-rag" in store_ids + + +def test_enrich_solr_default_chunk_filter_query() -> None: + """Test enrich_solr uses the internal chunk filter when no user filter is set.""" + ls_config: dict[str, Any] = {} + enrich_solr(ls_config, _OKP_RAG_CONFIG, {}) + + provider = next( + p for p in ls_config["providers"]["vector_io"] if p["provider_id"] == "okp_solr" + ) + assert ( + provider["config"]["chunk_window_config"]["chunk_filter_query"] + == "is_chunk:true" + ) + + +def test_enrich_solr_user_chunk_filter_query_is_conjoined() -> None: + """Test enrich_solr ANDs the user filter with the internal chunk filter.""" + ls_config: dict[str, Any] = {} + enrich_solr(ls_config, _OKP_RAG_CONFIG, {"chunk_filter_query": "product:ansible"}) + + provider = next( + p for p in ls_config["providers"]["vector_io"] if p["provider_id"] == "okp_solr" + ) + assert provider["config"]["chunk_window_config"]["chunk_filter_query"] == ( + "is_chunk:true AND product:ansible" + )