Skip to content

Commit fb7ec1a

Browse files
authored
Merge pull request #1248 from max-svistunov/lcore-1376-translate-byok-vector-store-ids
LCORE-1376 RAG: resolve BYOK rag_ids to llama-stack vector_db_ids in query path
2 parents 4380346 + dcfe402 commit fb7ec1a

2 files changed

Lines changed: 156 additions & 1 deletion

File tree

src/utils/responses.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import metrics
3535
from configuration import configuration
3636
from constants import DEFAULT_RAG_TOOL
37+
from models.config import ByokRag
3738
from models.database.conversations import UserConversation
3839
from models.requests import QueryRequest
3940
from models.responses import (
@@ -142,6 +143,11 @@ async def prepare_tools( # pylint: disable=too-many-arguments,too-many-position
142143
except APIStatusError as e:
143144
error_response = InternalServerErrorResponse.generic()
144145
raise HTTPException(**error_response.model_dump()) from e
146+
else:
147+
# Translate customer-facing BYOK rag_ids to llama-stack vector_db_ids
148+
vector_store_ids = resolve_vector_store_ids(
149+
vector_store_ids, configuration.configuration.byok_rag
150+
)
145151

146152
# Add RAG tools if vector stores are available
147153
rag_tools = get_rag_tools(vector_store_ids)
@@ -309,6 +315,28 @@ def extract_vector_store_ids_from_tools(
309315
return vector_store_ids
310316

311317

318+
def resolve_vector_store_ids(
319+
vector_store_ids: list[str], byok_rags: list[ByokRag]
320+
) -> list[str]:
321+
"""Translate customer-facing BYOK rag_ids to llama-stack vector_db_ids.
322+
323+
Each ID is looked up against the BYOK RAG configuration. If a matching
324+
``rag_id`` is found, the corresponding ``vector_db_id`` is returned.
325+
Otherwise the ID is passed through unchanged (assumed to already be a
326+
llama-stack vector store ID).
327+
328+
Parameters:
329+
vector_store_ids: List of IDs from the client request (may be
330+
customer-facing rag_ids or raw llama-stack vector_db_ids).
331+
byok_rags: BYOK RAG configuration entries.
332+
333+
Returns:
334+
List of llama-stack vector_db_ids ready for the Llama Stack API.
335+
"""
336+
rag_id_to_vector_db_id = {brag.rag_id: brag.vector_db_id for brag in byok_rags}
337+
return [rag_id_to_vector_db_id.get(vs_id, vs_id) for vs_id in vector_store_ids]
338+
339+
312340
def get_rag_tools(vector_store_ids: list[str]) -> Optional[list[InputToolFileSearch]]:
313341
"""Convert vector store IDs to tools format for Responses API.
314342

tests/unit/utils/test_responses.py

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from pydantic import AnyUrl
2424
from pytest_mock import MockerFixture
2525

26-
from models.config import ModelContextProtocolServer
26+
from models.config import ByokRag, ModelContextProtocolServer
2727
from models.requests import QueryRequest
2828
from utils.responses import (
2929
build_mcp_tool_call_from_arguments_done,
@@ -41,6 +41,7 @@
4141
parse_referenced_documents,
4242
prepare_responses_params,
4343
prepare_tools,
44+
resolve_vector_store_ids,
4445
_build_chunk_attributes,
4546
_increment_llm_call_metric,
4647
_resolve_source_for_result,
@@ -1000,6 +1001,132 @@ async def test_prepare_tools_no_tools_true(self, mocker: MockerFixture) -> None:
10001001
mock_client.vector_stores.list.assert_not_called()
10011002

10021003

1004+
class TestResolveVectorStoreIds:
1005+
"""Tests for resolve_vector_store_ids function."""
1006+
1007+
@staticmethod
1008+
def _make_byok_rag(rag_id: str, vector_db_id: str) -> ByokRag:
1009+
"""Create a ByokRag instance for testing."""
1010+
return ByokRag(
1011+
rag_id=rag_id,
1012+
vector_db_id=vector_db_id,
1013+
db_path="tests/configuration/rag.txt",
1014+
)
1015+
1016+
def test_translates_customer_facing_ids_to_internal(self) -> None:
1017+
"""Test that customer-facing rag_ids are translated to vector_db_ids."""
1018+
byok_rags = [
1019+
self._make_byok_rag("ocp_docs", "vs-001"),
1020+
self._make_byok_rag("knowledge_base", "vs-002"),
1021+
]
1022+
result = resolve_vector_store_ids(["ocp_docs", "knowledge_base"], byok_rags)
1023+
assert result == ["vs-001", "vs-002"]
1024+
1025+
def test_passes_through_unknown_ids(self) -> None:
1026+
"""Test that IDs not matching any rag_id are passed through unchanged."""
1027+
byok_rags = [self._make_byok_rag("ocp_docs", "vs-001")]
1028+
result = resolve_vector_store_ids(["unknown-id"], byok_rags)
1029+
assert result == ["unknown-id"]
1030+
1031+
def test_mixed_known_and_unknown_ids(self) -> None:
1032+
"""Test mix of customer-facing IDs and raw llama-stack IDs."""
1033+
byok_rags = [self._make_byok_rag("ocp_docs", "vs-001")]
1034+
result = resolve_vector_store_ids(["ocp_docs", "already-internal"], byok_rags)
1035+
assert result == ["vs-001", "already-internal"]
1036+
1037+
def test_empty_vector_store_ids(self) -> None:
1038+
"""Test that empty input returns empty output."""
1039+
byok_rags = [self._make_byok_rag("ocp_docs", "vs-001")]
1040+
result = resolve_vector_store_ids([], byok_rags)
1041+
assert result == []
1042+
1043+
def test_empty_byok_rags(self) -> None:
1044+
"""Test that all IDs pass through when no BYOK RAGs are configured."""
1045+
result = resolve_vector_store_ids(["vs-001", "vs-002"], [])
1046+
assert result == ["vs-001", "vs-002"]
1047+
1048+
def test_preserves_order(self) -> None:
1049+
"""Test that output order matches input order."""
1050+
byok_rags = [
1051+
self._make_byok_rag("b_rag", "vs-b"),
1052+
self._make_byok_rag("a_rag", "vs-a"),
1053+
]
1054+
result = resolve_vector_store_ids(["a_rag", "b_rag"], byok_rags)
1055+
assert result == ["vs-a", "vs-b"]
1056+
1057+
1058+
class TestPrepareToolsTranslatesVectorStoreIds:
1059+
"""Tests that prepare_tools translates BYOK IDs before building RAG tools."""
1060+
1061+
@pytest.mark.asyncio
1062+
async def test_translates_byok_ids_in_prepare_tools(
1063+
self, mocker: MockerFixture
1064+
) -> None:
1065+
"""Test that prepare_tools translates customer-facing IDs to internal IDs."""
1066+
mock_client = mocker.AsyncMock()
1067+
mocker.patch("utils.responses.get_mcp_tools", return_value=None)
1068+
1069+
# Configure BYOK RAG mapping
1070+
mock_byok_rag = mocker.Mock()
1071+
mock_byok_rag.rag_id = "ocp_docs"
1072+
mock_byok_rag.vector_db_id = "vs-001"
1073+
mock_config = mocker.Mock()
1074+
mock_config.configuration.byok_rag = [mock_byok_rag]
1075+
mocker.patch("utils.responses.configuration", mock_config)
1076+
1077+
result = await prepare_tools(mock_client, ["ocp_docs"], False, "token")
1078+
assert result is not None
1079+
assert len(result) == 1
1080+
assert result[0].type == "file_search"
1081+
assert result[0].vector_store_ids == ["vs-001"]
1082+
1083+
@pytest.mark.asyncio
1084+
async def test_passes_through_unknown_ids_in_prepare_tools(
1085+
self, mocker: MockerFixture
1086+
) -> None:
1087+
"""Test that prepare_tools passes through IDs not in BYOK config."""
1088+
mock_client = mocker.AsyncMock()
1089+
mocker.patch("utils.responses.get_mcp_tools", return_value=None)
1090+
1091+
# Configure empty BYOK RAG
1092+
mock_config = mocker.Mock()
1093+
mock_config.configuration.byok_rag = []
1094+
mocker.patch("utils.responses.configuration", mock_config)
1095+
1096+
result = await prepare_tools(mock_client, ["raw-internal-id"], False, "token")
1097+
assert result is not None
1098+
assert result[0].vector_store_ids == ["raw-internal-id"]
1099+
1100+
@pytest.mark.asyncio
1101+
async def test_does_not_translate_when_ids_fetched_from_llama_stack(
1102+
self, mocker: MockerFixture
1103+
) -> None:
1104+
"""Test that IDs fetched from llama-stack (None path) are not translated."""
1105+
mock_client = mocker.AsyncMock()
1106+
mock_vector_store = mocker.Mock()
1107+
mock_vector_store.id = "vs-internal"
1108+
mock_vector_stores = mocker.Mock()
1109+
mock_vector_stores.data = [mock_vector_store]
1110+
mock_client.vector_stores.list = mocker.AsyncMock(
1111+
return_value=mock_vector_stores
1112+
)
1113+
mocker.patch("utils.responses.get_mcp_tools", return_value=None)
1114+
1115+
# Configure BYOK RAG whose rag_id matches the fetched ID so that
1116+
# accidental translation would change the result and fail the assertion
1117+
mock_byok_rag = mocker.Mock()
1118+
mock_byok_rag.rag_id = "vs-internal"
1119+
mock_byok_rag.vector_db_id = "vs-translated"
1120+
mock_config = mocker.Mock()
1121+
mock_config.configuration.byok_rag = [mock_byok_rag]
1122+
mocker.patch("utils.responses.configuration", mock_config)
1123+
1124+
result = await prepare_tools(mock_client, None, False, "token")
1125+
assert result is not None
1126+
# The IDs from llama-stack should be used as-is (no BYOK translation on None path)
1127+
assert result[0].vector_store_ids == ["vs-internal"]
1128+
1129+
10031130
class TestPrepareResponsesParams:
10041131
"""Tests for prepare_responses_params function."""
10051132

0 commit comments

Comments
 (0)