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
2 changes: 1 addition & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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\*'. |
Comment thread
are-ces marked this conversation as resolved.


## PostgreSQLDatabaseConfiguration
Expand Down
8 changes: 3 additions & 5 deletions docs/rag_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
6 changes: 3 additions & 3 deletions examples/lightspeed-stack-byok-okp-rag.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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*"
Comment thread
are-ces marked this conversation as resolved.
32 changes: 14 additions & 18 deletions src/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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()
Expand All @@ -78,21 +73,22 @@ 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)
except (OSError, yaml.YAMLError) as e:
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())
Comment on lines 79 to +91

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What does yaml.safe_load from PyYAML return for an empty YAML document?

💡 Result:

For an empty YAML stream/document (e.g., "" or a file containing only whitespace/comments), yaml.safe_load(...) returns None. [1], [2]


🏁 Script executed:

# Find enrichment helper functions
fd "\.py$" src/ | xargs grep -l "def enrich_byok_rag\|def enrich_solr"

Repository: lightspeed-core/lightspeed-stack

Length of output: 108


🏁 Script executed:

# Get the content around the enrichment calls to understand context
rg "def enrich_byok_rag|def enrich_solr" -A 15

Repository: lightspeed-core/lightspeed-stack

Length of output: 2310


🏁 Script executed:

# Check if ls_config is ever checked for None elsewhere in the file
rg "ls_config" src/client.py -B 2 -A 2

Repository: lightspeed-core/lightspeed-stack

Length of output: 924


Guard against non-dict YAML payloads before enrichment.

yaml.safe_load returns None for empty or comment-only YAML; enrichment helpers expect a mapping and will crash with TypeError when accessing dict operations on None.

Proposed fix
         try:
             with open(input_config_path, "r", encoding="utf-8") as f:
                 ls_config = yaml.safe_load(f)
         except (OSError, yaml.YAMLError) as e:
             logger.warning("Failed to read llama-stack config: %s", e)
             return input_config_path
+        if not isinstance(ls_config, dict):
+            logger.warning(
+                "Invalid llama-stack config format in %s: expected mapping at root",
+                input_config_path,
+            )
+            return input_config_path
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client.py` around lines 79 - 91, yaml.safe_load can return None (or
non-dict) which causes enrich_byok_rag and enrich_solr to fail; after loading
ls_config, check if it's a mapping (isinstance(ls_config, dict)), and if not log
a warning and replace it with an empty dict (ls_config = {}) before calling
enrich_byok_rag and enrich_solr so the enrichment helpers always receive a dict;
keep existing logger usage and the same variables (ls_config, configuration,
enrich_byok_rag, enrich_solr).


enriched_path = os.path.join(
tempfile.gettempdir(), "llama_stack_enriched_config.yaml"
Expand Down
3 changes: 3 additions & 0 deletions src/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
38 changes: 22 additions & 16 deletions src/llama_stack_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Comment thread
are-ces marked this conversation as resolved.

logger.info("Enriching Llama Stack config with OKP")

# Add vector_io provider for Solr
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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", {}))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Normalize nullable rag / okp sections before passing them down.

dict.get(key, {}) only uses the fallback when the key is missing. If the YAML explicitly sets rag or okp to null, this call passes None into enrich_solr(), and the first .get() there raises AttributeError.

Proposed fix
-    enrich_solr(ls_config, config.get("rag", {}), config.get("okp", {}))
+    enrich_solr(ls_config, config.get("rag") or {}, config.get("okp") or {})
📝 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
enrich_solr(ls_config, config.get("rag", {}), config.get("okp", {}))
enrich_solr(ls_config, config.get("rag") or {}, config.get("okp") or {})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/llama_stack_configuration.py` at line 560, The call
enrich_solr(ls_config, config.get("rag", {}), config.get("okp", {})) can pass
None if the YAML explicitly sets rag or okp to null; normalize those values
before calling enrich_solr by replacing config.get("rag", {}) and
config.get("okp", {}) with expressions that coerce falsy/None to empty dicts
(e.g., rag = config.get("rag") or {}; okp = config.get("okp") or {}) and then
call enrich_solr(ls_config, rag, okp) so enrich_solr always receives dicts and
avoids AttributeError.


logger.info("Writing Llama Stack configuration into file %s", output_file)

Expand Down
10 changes: 4 additions & 6 deletions src/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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*'.",
)
Comment thread
are-ces marked this conversation as resolved.


Expand Down
10 changes: 5 additions & 5 deletions tests/unit/models/config/test_dump_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
8 changes: 3 additions & 5 deletions tests/unit/models/config/test_rag_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)."""
Expand All @@ -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."""
Expand Down
50 changes: 40 additions & 10 deletions tests/unit/test_llama_stack_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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 = [
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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"]
Expand All @@ -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
Expand All @@ -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"
)
Loading