Skip to content

Commit 71cd53c

Browse files
authored
Merge pull request #1850 from asimurka/refator_source_resolution
LCORE-2310: Refactor source resolution utility
2 parents e89b3ad + 3a04812 commit 71cd53c

2 files changed

Lines changed: 78 additions & 125 deletions

File tree

src/utils/responses.py

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -884,18 +884,16 @@ def parse_referenced_documents( # pylint: disable=too-many-locals
884884
id_mapping = rag_id_mapping or {}
885885

886886
for output_item in response.output:
887-
item_type = getattr(output_item, "type", None)
887+
item_type = output_item.type
888888

889889
if item_type == "file_search_call":
890-
results = getattr(output_item, "results", []) or []
890+
item = cast(FileSearchCall, output_item)
891+
results = item.results or []
891892
for result in results:
892-
resolved_source = _resolve_source_for_result(result, vs_ids, id_mapping)
893-
894-
# Handle both object and dict access
895-
if isinstance(result, dict):
896-
attributes = result.get("attributes", {})
897-
else:
898-
attributes = getattr(result, "attributes", {})
893+
attributes = result.attributes
894+
resolved_source = resolve_source_for_result(
895+
attributes, vs_ids, id_mapping
896+
)
899897

900898
# Try to get URL from attributes
901899
# Look for common URL fields in attributes
@@ -1197,8 +1195,8 @@ def build_tool_result_from_mcp_output_item_done(
11971195
)
11981196

11991197

1200-
def _resolve_source_for_result(
1201-
result: Any,
1198+
def resolve_source_for_result(
1199+
attributes: dict[str, Any],
12021200
vector_store_ids: list[str],
12031201
rag_id_mapping: dict[str, str],
12041202
) -> Optional[str]:
@@ -1209,7 +1207,7 @@ def _resolve_source_for_result(
12091207
12101208
Parameters:
12111209
----------
1212-
result: A file search result object with optional attributes.
1210+
attributes: A dictionary of attributes from a file search result.
12131211
vector_store_ids: The vector store IDs used in this query.
12141212
rag_id_mapping: Mapping from vector_db_id to user-facing rag_id.
12151213
@@ -1221,22 +1219,17 @@ def _resolve_source_for_result(
12211219
store_id = vector_store_ids[0]
12221220
return rag_id_mapping.get(store_id, store_id)
12231221

1224-
if len(vector_store_ids) > 1:
1225-
attributes = getattr(result, "attributes", {}) or {}
1226-
1227-
# Primary: read index name embedded directly by rag-content.
1228-
# This value is already the user-facing rag_id, not a vector_db_id,
1229-
# so no mapping is needed.
1230-
attr_source: Optional[str] = attributes.get("source")
1231-
if attr_source:
1232-
return attr_source
1222+
# source is the user-facing source name, no mapping needed
1223+
if source := attributes.get("source"):
1224+
return str(source)
12331225

1234-
# Fallback: if llama-stack ever populates vector_store_id in results,
1235-
# use it with the rag_id_mapping.
1236-
attr_store_id: Optional[str] = attributes.get("vector_store_id")
1237-
if attr_store_id:
1238-
return rag_id_mapping.get(attr_store_id, attr_store_id)
1226+
# Fallback: if llama-stack ever populates vector_store_id in results,
1227+
# use it with the rag_id_mapping.
1228+
if vector_store_id := attributes.get("vector_store_id"):
1229+
vector_store_id = str(vector_store_id)
1230+
return rag_id_mapping.get(vector_store_id, vector_store_id)
12391231

1232+
# ambiguous: multiple vector stores, no source or vector store id
12401233
return None
12411234

12421235

@@ -1279,8 +1272,8 @@ def extract_rag_chunks_from_file_search_item(
12791272

12801273
rag_chunks: list[RAGChunk] = []
12811274
for result in item.results:
1282-
source = _resolve_source_for_result(
1283-
result, vector_store_ids or [], rag_id_mapping or {}
1275+
source = resolve_source_for_result(
1276+
result.attributes, vector_store_ids or [], rag_id_mapping or {}
12841277
)
12851278
attributes = _build_chunk_attributes(result)
12861279
rag_chunk = RAGChunk(

tests/unit/utils/test_responses.py

Lines changed: 57 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@
6161
from utils.responses import (
6262
_build_chunk_attributes,
6363
_merge_tools,
64-
_resolve_source_for_result,
6564
build_mcp_tool_call_from_arguments_done,
6665
build_tool_call_summary,
6766
build_tool_result_from_mcp_output_item_done,
@@ -81,6 +80,7 @@
8180
prepare_responses_params,
8281
prepare_tools,
8382
resolve_client_tool_choice,
83+
resolve_source_for_result,
8484
resolve_tool_choice,
8585
resolve_vector_store_ids,
8686
)
@@ -2250,12 +2250,11 @@ def test_parse_referenced_documents_file_search_call(
22502250
"document_id": "doc_1",
22512251
}
22522252

2253-
mock_result2 = {
2254-
"attributes": {
2255-
"url": "https://example.com/doc2",
2256-
"title": "Document 2",
2257-
"doc_id": "doc_2",
2258-
},
2253+
mock_result2 = mocker.Mock()
2254+
mock_result2.attributes = {
2255+
"url": "https://example.com/doc2",
2256+
"title": "Document 2",
2257+
"doc_id": "doc_2",
22592258
}
22602259

22612260
mock_output_item = mocker.Mock()
@@ -2465,9 +2464,14 @@ def test_build_tool_call_summary_file_search_call(
24652464
mock_result.text = "chunk text"
24662465
mock_result.filename = "doc.pdf"
24672466
mock_result.score = 0.9
2468-
mock_result.attributes = None
2467+
mock_result.attributes = {}
24692468
mock_result.model_dump = mocker.Mock(
2470-
return_value={"text": "chunk text", "filename": "doc.pdf", "score": 0.9}
2469+
return_value={
2470+
"text": "chunk text",
2471+
"filename": "doc.pdf",
2472+
"score": 0.9,
2473+
"attributes": {},
2474+
}
24712475
)
24722476

24732477
mock_item = mocker.Mock(spec=FileSearchCall)
@@ -2620,13 +2624,13 @@ def test_extract_rag_chunks_with_results(self, mocker: MockerFixture) -> None:
26202624
mock_result1.text = "chunk 1"
26212625
mock_result1.filename = "doc1.pdf"
26222626
mock_result1.score = 0.9
2623-
mock_result1.attributes = None
2627+
mock_result1.attributes = {}
26242628

26252629
mock_result2 = mocker.Mock()
26262630
mock_result2.text = "chunk 2"
26272631
mock_result2.filename = "doc2.pdf"
26282632
mock_result2.score = 0.8
2629-
mock_result2.attributes = None
2633+
mock_result2.attributes = {}
26302634

26312635
mock_item = mocker.Mock(spec=FileSearchCall)
26322636
mock_item.results = [mock_result1, mock_result2]
@@ -2778,133 +2782,84 @@ def test_build_mcp_tool_result_none_output(self, mocker: MockerFixture) -> None:
27782782

27792783

27802784
class TestResolveSourceForResult:
2781-
"""Tests for _resolve_source_for_result function."""
2785+
"""Tests for resolve_source_for_result function."""
27822786

2783-
def test_single_store_mapped(self, mocker: MockerFixture) -> None:
2787+
def test_single_store_mapped(self) -> None:
27842788
"""Test resolution with single vector store that has a mapping."""
2785-
mock_result = mocker.Mock()
2786-
mock_result.filename = "file-abc123"
2787-
2788-
source = _resolve_source_for_result(
2789-
mock_result, ["vs-001"], {"vs-001": "ocp-4.18-docs"}
2790-
)
2789+
source = resolve_source_for_result({}, ["vs-001"], {"vs-001": "ocp-4.18-docs"})
27912790
assert source == "ocp-4.18-docs"
27922791

2793-
def test_single_store_unmapped(self, mocker: MockerFixture) -> None:
2792+
def test_single_store_unmapped(self) -> None:
27942793
"""Test resolution with single vector store without mapping returns raw store ID."""
2795-
mock_result = mocker.Mock()
2796-
mock_result.filename = "file-abc123"
2797-
2798-
source = _resolve_source_for_result(mock_result, ["vs-unknown"], {})
2794+
source = resolve_source_for_result({}, ["vs-unknown"], {})
27992795
assert source == "vs-unknown"
28002796

2801-
def test_multiple_stores_with_attribute(self, mocker: MockerFixture) -> None:
2802-
"""Test resolution with multiple stores using result attributes."""
2803-
mock_result = mocker.Mock()
2804-
mock_result.filename = "file-abc123"
2805-
mock_result.attributes = {"vector_store_id": "vs-002"}
2806-
2807-
source = _resolve_source_for_result(
2808-
mock_result,
2797+
def test_multiple_stores_with_attribute(self) -> None:
2798+
"""Test resolution with multiple stores using vector_store_id attribute."""
2799+
source = resolve_source_for_result(
2800+
{"vector_store_id": "vs-002"},
28092801
["vs-001", "vs-002"],
28102802
{"vs-001": "ocp-4.18-docs", "vs-002": "rhel-9-docs"},
28112803
)
28122804
assert source == "rhel-9-docs"
28132805

2814-
def test_multiple_stores_no_attribute(self, mocker: MockerFixture) -> None:
2815-
"""Test resolution with multiple stores and no vector_store_id attribute returns None."""
2816-
mock_result = mocker.Mock()
2817-
mock_result.filename = "file-abc123"
2818-
mock_result.attributes = {}
2819-
2820-
source = _resolve_source_for_result(
2821-
mock_result,
2806+
def test_multiple_stores_no_attribute(self) -> None:
2807+
"""Test resolution with multiple stores and empty attributes returns None."""
2808+
source = resolve_source_for_result(
2809+
{},
28222810
["vs-001", "vs-002"],
28232811
{"vs-001": "ocp-4.18-docs", "vs-002": "rhel-9-docs"},
28242812
)
28252813
assert source is None
28262814

2827-
def test_no_stores(self, mocker: MockerFixture) -> None:
2815+
def test_no_stores(self) -> None:
28282816
"""Test resolution with no vector stores returns None."""
2829-
mock_result = mocker.Mock()
2830-
mock_result.filename = "file-abc123"
2831-
2832-
source = _resolve_source_for_result(mock_result, [], {})
2817+
source = resolve_source_for_result({}, [], {})
28332818
assert source is None
28342819

2835-
def test_multiple_stores_attribute_not_in_mapping(
2836-
self, mocker: MockerFixture
2837-
) -> None:
2820+
def test_multiple_stores_attribute_not_in_mapping(self) -> None:
28382821
"""Test resolution when attribute store ID is not in mapping returns raw store ID."""
2839-
mock_result = mocker.Mock()
2840-
mock_result.filename = "file-abc123"
2841-
mock_result.attributes = {"vector_store_id": "vs-unknown"}
2842-
2843-
source = _resolve_source_for_result(
2844-
mock_result,
2822+
source = resolve_source_for_result(
2823+
{"vector_store_id": "vs-unknown"},
28452824
["vs-001", "vs-002"],
28462825
{"vs-001": "ocp-docs"},
28472826
)
28482827
assert source == "vs-unknown"
28492828

2850-
def test_multiple_stores_source_attribute_fallback(
2851-
self, mocker: MockerFixture
2852-
) -> None:
2829+
def test_multiple_stores_source_attribute_fallback(self) -> None:
28532830
"""Test resolution falls back to source attribute when no vector_store_id."""
2854-
mock_result = mocker.Mock()
2855-
mock_result.filename = "file-abc123"
2856-
mock_result.attributes = {"source": "ocp-documentation"}
2857-
2858-
source = _resolve_source_for_result(
2859-
mock_result,
2831+
source = resolve_source_for_result(
2832+
{"source": "ocp-documentation"},
28602833
["vs-001", "vs-002"],
28612834
{"vs-001": "ocp-4.18-docs"},
28622835
)
28632836
assert source == "ocp-documentation"
28642837

2865-
def test_multiple_stores_source_attribute_ignores_mapping(
2866-
self, mocker: MockerFixture
2867-
) -> None:
2838+
def test_multiple_stores_source_attribute_ignores_mapping(self) -> None:
28682839
"""Test source attribute is returned directly without rag_id_mapping lookup."""
2869-
mock_result = mocker.Mock()
2870-
mock_result.filename = "file-abc123"
2871-
mock_result.attributes = {"source": "custom-index"}
2872-
2873-
source = _resolve_source_for_result(
2874-
mock_result,
2840+
source = resolve_source_for_result(
2841+
{"source": "custom-index"},
28752842
["vs-001", "vs-002"],
28762843
{"custom-index": "should-not-be-used"},
28772844
)
28782845
assert source == "custom-index"
28792846

2880-
def test_multiple_stores_source_preferred_over_vector_store_id(
2881-
self, mocker: MockerFixture
2882-
) -> None:
2847+
def test_multiple_stores_source_preferred_over_vector_store_id(self) -> None:
28832848
"""Test source attribute takes precedence over vector_store_id."""
2884-
mock_result = mocker.Mock()
2885-
mock_result.filename = "file-abc123"
2886-
mock_result.attributes = {
2887-
"vector_store_id": "vs-002",
2888-
"source": "ocp-documentation",
2889-
}
2890-
2891-
source = _resolve_source_for_result(
2892-
mock_result,
2849+
source = resolve_source_for_result(
2850+
{
2851+
"vector_store_id": "vs-002",
2852+
"source": "ocp-documentation",
2853+
},
28932854
["vs-001", "vs-002"],
28942855
{"vs-002": "rhel-9-docs"},
28952856
)
28962857
assert source == "ocp-documentation"
28972858

2898-
def test_multiple_stores_no_vector_store_id_no_source(
2899-
self, mocker: MockerFixture
2900-
) -> None:
2859+
def test_multiple_stores_no_vector_store_id_no_source(self) -> None:
29012860
"""Test resolution returns None when neither vector_store_id nor source present."""
2902-
mock_result = mocker.Mock()
2903-
mock_result.filename = "file-abc123"
2904-
mock_result.attributes = {"title": "some doc"}
2905-
2906-
source = _resolve_source_for_result(
2907-
mock_result,
2861+
source = resolve_source_for_result(
2862+
{"title": "some doc"},
29082863
["vs-001", "vs-002"],
29092864
{"vs-001": "ocp-docs"},
29102865
)
@@ -3001,12 +2956,12 @@ def test_chunks_resolved_single_store(self, mocker: MockerFixture) -> None:
30012956
assert rag_chunks[0].score == 0.95
30022957

30032958
def test_chunks_no_mapping_falls_back(self, mocker: MockerFixture) -> None:
3004-
"""Test RAG chunk source falls back to filename when no mapping."""
2959+
"""Test RAG chunk source is None with empty attributes and no vector stores."""
30052960
mock_result = mocker.Mock()
30062961
mock_result.text = "content"
30072962
mock_result.filename = "file-abc"
30082963
mock_result.score = 0.5
3009-
mock_result.attributes = None
2964+
mock_result.attributes = {}
30102965

30112966
mock_item = mocker.Mock(spec=FileSearchCall)
30122967
mock_item.results = [mock_result]
@@ -3090,9 +3045,14 @@ def test_file_search_without_mapping(self, mocker: MockerFixture) -> None:
30903045
mock_result.text = "chunk text"
30913046
mock_result.filename = "doc.pdf"
30923047
mock_result.score = 0.9
3093-
mock_result.attributes = None
3048+
mock_result.attributes = {}
30943049
mock_result.model_dump = mocker.Mock(
3095-
return_value={"text": "chunk text", "filename": "doc.pdf", "score": 0.9}
3050+
return_value={
3051+
"text": "chunk text",
3052+
"filename": "doc.pdf",
3053+
"score": 0.9,
3054+
"attributes": {},
3055+
}
30963056
)
30973057

30983058
mock_item = mocker.Mock(spec=FileSearchCall)

0 commit comments

Comments
 (0)