Skip to content

Commit 0448199

Browse files
committed
LCORE-1853: Add relevance cutoff score to inline BYOK RAG
1 parent 7a44438 commit 0448199

11 files changed

Lines changed: 369 additions & 9 deletions

File tree

docs/byok_guide.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ Both modes rely on:
7979

8080
Inline RAG additionally supports:
8181
- **Score Multiplier**: Optional weight applied per BYOK vector store when mixing multiple sources. Allows custom prioritization of content.
82+
- **Relevance cutoff score** (`relevance_cutoff_score` in `byok_rag`): Minimum raw similarity score for a chunk to be returned from that BYOK vector store. Chunks below the threshold are dropped before results are merged and ranked with other sources. Configure per knowledge source (each `byok_rag` entry has its own value). The default when omitted is `0.3` (see `DEFAULT_BYOK_RAG_RELEVANCE_CUTOFF_SCORE` in `src/constants.py`). This value is passed to Llama Stack as the vector search `score_threshold` for that store.
8283

8384
> [!NOTE]
8485
> OKP and BYOK scores are not directly comparable (different scoring systems), so
@@ -89,6 +90,13 @@ Inline RAG additionally supports:
8990
> chunks (BYOK + OKP) delivered to the LLM. Tool RAG is controlled independently
9091
> by `TOOL_RAG_MAX_CHUNKS`.
9192
93+
94+
> [!NOTE]
95+
> `relevance_cutoff_score` applies to Inline RAG only. When the model uses Tool RAG (`file_search`),
96+
> Lightspeed Stack does not send this setting; retrieval uses Llama Stack’s default ranking for that path.
97+
> Use Inline RAG if you need per-store cutoff behavior from configuration.
98+
99+
92100
---
93101

94102
## Prerequisites
@@ -236,8 +244,13 @@ byok_rag:
236244
vector_db_id: vs_d4c8e1f0-92ab-4d3c-a5e7-6b8f0c2d1e3a
237245
db_path: /data/vector_dbs/internal_kb/faiss_store.db
238246
score_multiplier: 1.2 # Boost results from this store
247+
relevance_cutoff_score: 0.3 # Optional: min raw similarity per chunk for this store (Inline RAG only; default 0.3)
239248
```
240249

250+
`relevance_cutoff_score` is interpreted in the same score space as the vector backend for that
251+
store. It is not comparable across different vector stores or OKP; tune each `byok_rag` entry
252+
using retrieval quality on that corpus.
253+
241254
**⚠️ Important**: The `vector_db_id` value must exactly match the ID generated by the rag-content tool during index creation (e.g. `vs_8c94967b-81cc-4028-a294-9cfac6fd9ae2`). This identifier links your configuration to the specific vector database index.
242255

243256
### Step 5: Configure RAG Strategy
@@ -265,10 +278,10 @@ okp:
265278

266279
Both modes can be enabled simultaneously. Choose based on your latency and control preferences:
267280

268-
| Mode | When context is fetched | Tool call needed | score_multiplier |
269-
|------------|-------------------------|------------------|------------------|
270-
| Inline RAG | With every query | No | Yes (BYOK only) |
271-
| Tool RAG | On LLM demand | Yes | No |
281+
| Mode | When context is fetched | Tool call needed | score_multiplier | relevance_cutoff_score |
282+
|------|------------------------|------------------|----------------|------------------------|
283+
| Inline RAG | With every query | No | Yes (BYOK only) | Yes (BYOK only) |
284+
| Tool RAG | On LLM demand | Yes | No | No |
272285

273286
> [!TIP]
274287
> A ready-to-use example combining BYOK and OKP is available at

docs/openapi.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11870,6 +11870,13 @@
1187011870
"description": "Multiplier applied to relevance scores from this vector store. Used to weight results when querying multiple knowledge sources. Values > 1 boost this store's results; values < 1 reduce them.",
1187111871
"default": 1.0
1187211872
},
11873+
"relevance_cutoff_score": {
11874+
"type": "number",
11875+
"exclusiveMinimum": 0.0,
11876+
"title": "Relevance cutoff score",
11877+
"description": "Minimum raw similarity score to consider a result relevant. Results with a similarity score below this threshold are not returned.",
11878+
"default": 0.3
11879+
},
1187311880
"host": {
1187411881
"anyOf": [
1187511882
{

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,10 @@ ignore = ["query.py"]
229229
requires = ["pdm-backend"]
230230
build-backend = "pdm.backend"
231231

232+
[tool.pylint.design]
233+
max-args = 8
234+
max-positional-arguments = 8
235+
232236
[tool.pylint."MESSAGES CONTROL"]
233237
disable = ["R0801"]
234238

src/configuration.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,24 @@ def score_multiplier_mapping(self) -> dict[str, float]:
547547
for brag in self._configuration.byok_rag
548548
}
549549

550+
@property
551+
def relevance_cutoff_mapping(self) -> dict[str, float]:
552+
"""Return mapping from vector_db_id to relevance_cutoff_score from BYOK RAG config.
553+
554+
Returns:
555+
dict[str, float]: Mapping where keys are llama-stack vector_db_ids
556+
and values are relevance cutoff scores from configuration.
557+
558+
Raises:
559+
LogicError: If the configuration has not been loaded.
560+
"""
561+
if self._configuration is None:
562+
raise LogicError("logic error: configuration is not loaded")
563+
return {
564+
brag.vector_db_id: brag.relevance_cutoff_score
565+
for brag in self._configuration.byok_rag
566+
}
567+
550568
@property
551569
def inline_solr_enabled(self) -> bool:
552570
"""Return whether OKP is included in the inline RAG list.

src/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,9 @@
203203
# Score multiplier applied to BYOK chunks after cross-encoder reranking (Solr chunks unchanged)
204204
BYOK_RAG_RERANK_BOOST: Final[float] = 1.2
205205

206+
# Default minimum raw similarity per BYOK store
207+
DEFAULT_BYOK_RAG_RELEVANCE_CUTOFF_SCORE: Final[float] = 0.3
208+
206209
# Solr OKP constants
207210
SOLR_VECTOR_SEARCH_DEFAULT_K: Final[int] = 5
208211
SOLR_VECTOR_SEARCH_DEFAULT_SCORE_THRESHOLD: Final[float] = 0.3

src/models/config.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1876,6 +1876,14 @@ class ByokRag(ConfigurationBase):
18761876
"Values > 1 boost this store's results; values < 1 reduce them.",
18771877
)
18781878

1879+
relevance_cutoff_score: float = Field(
1880+
constants.DEFAULT_BYOK_RAG_RELEVANCE_CUTOFF_SCORE,
1881+
gt=0,
1882+
title="Relevance cutoff score",
1883+
description="Minimum raw similarity score to consider a result relevant. "
1884+
"Results with a similarity score below this threshold are not returned.",
1885+
)
1886+
18791887
host: Optional[str] = Field(
18801888
default=None,
18811889
title="PostgreSQL host",
@@ -1930,7 +1938,6 @@ def validate_rag_type_fields(self) -> Self:
19301938
object.__setattr__(self, field_name, default_value)
19311939
return self
19321940

1933-
19341941
class QuotaLimiterConfiguration(ConfigurationBase):
19351942
"""Configuration for one quota limiter.
19361943

src/utils/vector_search.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ async def _query_store_for_byok_rag(
250250
vector_store_id: str,
251251
query: str,
252252
weight: float,
253+
score_threshold: float,
253254
max_chunks: int = constants.BYOK_RAG_MAX_CHUNKS,
254255
) -> list[dict[str, Any]]:
255256
"""Query a single vector store for BYOK RAG.
@@ -260,6 +261,7 @@ async def _query_store_for_byok_rag(
260261
query: Search query string
261262
weight: Score multiplier to apply
262263
max_chunks: Maximum number of chunks to request from this store.
264+
score_threshold: Minimum raw similarity score (``relevance_cutoff_score``)
263265
264266
Returns:
265267
List of weighted result dictionaries, or empty list on error
@@ -271,6 +273,7 @@ async def _query_store_for_byok_rag(
271273
params={
272274
"max_chunks": max_chunks,
273275
"mode": "vector",
276+
"score_threshold": score_threshold,
274277
},
275278
)
276279
return _extract_byok_rag_chunks(search_response, vector_store_id, weight)
@@ -496,8 +499,9 @@ async def _fetch_byok_rag( # pylint: disable=too-many-locals
496499
return rag_chunks, referenced_documents
497500

498501
try:
499-
# Get score multiplier and rag_id mappings
502+
# Get per-store mappings from configuration
500503
score_multiplier_mapping = configuration.score_multiplier_mapping
504+
relevance_cutoff_mapping = configuration.relevance_cutoff_mapping
501505
rag_id_mapping = configuration.rag_id_mapping
502506

503507
# Query all vector stores in parallel
@@ -508,6 +512,10 @@ async def _fetch_byok_rag( # pylint: disable=too-many-locals
508512
vector_store_id,
509513
query,
510514
score_multiplier_mapping.get(vector_store_id, 1.0),
515+
relevance_cutoff_mapping.get(
516+
vector_store_id,
517+
constants.DEFAULT_BYOK_RAG_RELEVANCE_CUTOFF_SCORE,
518+
),
511519
max_chunks=limit,
512520
)
513521
for vector_store_id in vector_store_ids_to_query

tests/unit/models/config/test_byok_rag.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from pydantic import ValidationError
55

66
from constants import (
7+
DEFAULT_BYOK_RAG_RELEVANCE_CUTOFF_SCORE,
78
DEFAULT_EMBEDDING_DIMENSION,
89
DEFAULT_EMBEDDING_MODEL,
910
DEFAULT_RAG_TYPE,
@@ -35,6 +36,7 @@ def test_byok_rag_configuration_default_values() -> None:
3536
assert byok_rag.vector_db_id == "vector_db_id"
3637
assert byok_rag.db_path == "tests/configuration/rag.txt"
3738
assert byok_rag.score_multiplier == DEFAULT_SCORE_MULTIPLIER
39+
assert byok_rag.relevance_cutoff_score == DEFAULT_BYOK_RAG_RELEVANCE_CUTOFF_SCORE
3840

3941

4042
def test_byok_rag_configuration_nondefault_values() -> None:
@@ -54,6 +56,7 @@ def test_byok_rag_configuration_nondefault_values() -> None:
5456
vector_db_id="vector_db_id",
5557
db_path="tests/configuration/rag.txt",
5658
score_multiplier=1.0,
59+
relevance_cutoff_score=0.72,
5760
)
5861
assert byok_rag is not None
5962
assert byok_rag.rag_id == "rag_id"
@@ -62,6 +65,7 @@ def test_byok_rag_configuration_nondefault_values() -> None:
6265
assert byok_rag.embedding_dimension == 1024
6366
assert byok_rag.vector_db_id == "vector_db_id"
6467
assert byok_rag.db_path == "tests/configuration/rag.txt"
68+
assert byok_rag.relevance_cutoff_score == 0.72
6569

6670

6771
def test_byok_rag_configuration_wrong_dimension() -> None:
@@ -187,7 +191,10 @@ def test_byok_rag_configuration_custom_score_multiplier() -> None:
187191
assert byok_rag.score_multiplier == 2.5
188192

189193

190-
def test_byok_rag_configuration_score_multiplier_must_be_positive() -> None:
194+
@pytest.mark.parametrize("bad_multiplier", [0.0, -0.5])
195+
def test_byok_rag_configuration_score_multiplier_must_be_positive(
196+
bad_multiplier: float,
197+
) -> None:
191198
"""Test that score_multiplier must be greater than 0."""
192199
with pytest.raises(ValidationError, match="greater than 0"):
193200
_ = ByokRag(
@@ -197,7 +204,25 @@ def test_byok_rag_configuration_score_multiplier_must_be_positive() -> None:
197204
embedding_model="embedding_model",
198205
embedding_dimension=1024,
199206
db_path="tests/configuration/rag.txt",
200-
score_multiplier=0.0,
207+
score_multiplier=bad_multiplier,
208+
)
209+
210+
211+
@pytest.mark.parametrize("bad_cutoff", [0.0, -0.5])
212+
def test_byok_rag_configuration_relevance_cutoff_must_be_positive(
213+
bad_cutoff: float,
214+
) -> None:
215+
"""Test that relevance_cutoff_score must be greater than 0."""
216+
with pytest.raises(ValidationError, match="greater than 0"):
217+
_ = ByokRag(
218+
rag_id="rag_id",
219+
rag_type="rag_type",
220+
vector_db_id="vector_db_id",
221+
embedding_model="embedding_model",
222+
embedding_dimension=1024,
223+
db_path="tests/configuration/rag.txt",
224+
score_multiplier=1.0,
225+
relevance_cutoff_score=bad_cutoff,
201226
)
202227

203228

tests/unit/models/config/test_dump_configuration.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1250,6 +1250,7 @@ def test_dump_configuration_byok(tmp_path: Path) -> None:
12501250
"rag_type": "inline::faiss",
12511251
"vector_db_id": "vector_db_id",
12521252
"score_multiplier": 1.0,
1253+
"relevance_cutoff_score": constants.DEFAULT_BYOK_RAG_RELEVANCE_CUTOFF_SCORE,
12531254
"host": None,
12541255
"port": None,
12551256
"db": None,

tests/unit/test_configuration.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,86 @@ def test_score_multiplier_mapping_not_loaded() -> None:
11951195
_ = cfg.score_multiplier_mapping
11961196

11971197

1198+
def test_relevance_cutoff_mapping_empty_when_no_byok(minimal_config: AppConfig) -> None:
1199+
"""Test that relevance_cutoff_mapping returns empty dict when no BYOK RAG configured."""
1200+
assert minimal_config.relevance_cutoff_mapping == {}
1201+
1202+
1203+
def test_relevance_cutoff_mapping_with_byok_defaults(tmp_path: Path) -> None:
1204+
"""Test that relevance_cutoff_mapping uses default cutoff when not specified."""
1205+
db_file = tmp_path / "test.db"
1206+
db_file.touch()
1207+
cfg = AppConfig()
1208+
cfg.init_from_dict(
1209+
{
1210+
"name": "test",
1211+
"service": {"host": "localhost", "port": 8080},
1212+
"llama_stack": {
1213+
"api_key": "k",
1214+
"url": "http://test.com:1234",
1215+
"use_as_library_client": False,
1216+
},
1217+
"user_data_collection": {},
1218+
"authentication": {"module": "noop"},
1219+
"byok_rag": [
1220+
{
1221+
"rag_id": "my-kb",
1222+
"vector_db_id": "vs-001",
1223+
"db_path": str(db_file),
1224+
},
1225+
],
1226+
}
1227+
)
1228+
assert cfg.relevance_cutoff_mapping == {
1229+
"vs-001": constants.DEFAULT_BYOK_RAG_RELEVANCE_CUTOFF_SCORE,
1230+
}
1231+
1232+
1233+
def test_relevance_cutoff_mapping_with_custom_values(tmp_path: Path) -> None:
1234+
"""Test that relevance_cutoff_mapping builds correct mapping with custom values."""
1235+
db_file1 = tmp_path / "test1.db"
1236+
db_file1.touch()
1237+
db_file2 = tmp_path / "test2.db"
1238+
db_file2.touch()
1239+
cfg = AppConfig()
1240+
cfg.init_from_dict(
1241+
{
1242+
"name": "test",
1243+
"service": {"host": "localhost", "port": 8080},
1244+
"llama_stack": {
1245+
"api_key": "k",
1246+
"url": "http://test.com:1234",
1247+
"use_as_library_client": False,
1248+
},
1249+
"user_data_collection": {},
1250+
"authentication": {"module": "noop"},
1251+
"byok_rag": [
1252+
{
1253+
"rag_id": "kb1",
1254+
"vector_db_id": "vs-001",
1255+
"db_path": str(db_file1),
1256+
"relevance_cutoff_score": 0.55,
1257+
},
1258+
{
1259+
"rag_id": "kb2",
1260+
"vector_db_id": "vs-002",
1261+
"db_path": str(db_file2),
1262+
"relevance_cutoff_score": 0.1,
1263+
},
1264+
],
1265+
}
1266+
)
1267+
assert cfg.relevance_cutoff_mapping == {"vs-001": 0.55, "vs-002": 0.1}
1268+
1269+
1270+
def test_relevance_cutoff_mapping_not_loaded() -> None:
1271+
"""Test that relevance_cutoff_mapping raises when config not loaded."""
1272+
cfg = AppConfig()
1273+
cfg._configuration = None
1274+
with pytest.raises(LogicError):
1275+
_ = cfg.relevance_cutoff_mapping
1276+
1277+
11981278
wrong_configurations = [
11991279
{
12001280
"name": "Colin Adams",

0 commit comments

Comments
 (0)