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
10 changes: 10 additions & 0 deletions src/utils/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,16 @@ def _resolve_source_for_result(

if len(vector_store_ids) > 1:
attributes = getattr(result, "attributes", {}) or {}

# Primary: read index name embedded directly by rag-content.
# This value is already the user-facing rag_id, not a vector_db_id,
# so no mapping is needed.
attr_source: Optional[str] = attributes.get("source")
if attr_source:
Comment on lines 844 to +850
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Handle dict-backed results before reading source.

parse_referenced_documents() already supports both object and dict-shaped results, but this branch only uses getattr. When result is a dict, Line 849 never sees the embedded source, so multi-store source resolution falls back to None for referenced documents.

Suggested fix
-        attributes = getattr(result, "attributes", {}) or {}
+        raw_attributes = (
+            result.get("attributes", {})
+            if isinstance(result, Mapping)
+            else getattr(result, "attributes", {})
+        )
+        attributes = raw_attributes if isinstance(raw_attributes, Mapping) else {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/responses.py` around lines 844 - 850, The branch in
parse_referenced_documents() uses getattr(result, "attributes", {}) which misses
dict-shaped results; change attribute extraction to handle both objects and
dicts (e.g., if isinstance(result, dict): attributes = result.get("attributes",
{}) else: attributes = getattr(result, "attributes", {}) or {}), and when
resolving attr_source use attributes.get("source") with a fallback to
result.get("source") for dict-backed results so dict results can expose the
embedded "source" the same way object results do.

return attr_source

# Fallback: if llama-stack ever populates vector_store_id in results,
# use it with the rag_id_mapping.
attr_store_id: Optional[str] = attributes.get("vector_store_id")
if attr_store_id:
return rag_id_mapping.get(attr_store_id, attr_store_id)
Expand Down
63 changes: 63 additions & 0 deletions tests/unit/utils/test_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -2044,6 +2044,69 @@ def test_multiple_stores_attribute_not_in_mapping(
)
assert source == "vs-unknown"

def test_multiple_stores_source_attribute_fallback(
self, mocker: MockerFixture
) -> None:
"""Test resolution falls back to source attribute when no vector_store_id."""
mock_result = mocker.Mock()
mock_result.filename = "file-abc123"
mock_result.attributes = {"source": "ocp-documentation"}

source = _resolve_source_for_result(
mock_result,
["vs-001", "vs-002"],
{"vs-001": "ocp-4.18-docs"},
)
assert source == "ocp-documentation"

def test_multiple_stores_source_attribute_ignores_mapping(
self, mocker: MockerFixture
) -> None:
"""Test source attribute is returned directly without rag_id_mapping lookup."""
mock_result = mocker.Mock()
mock_result.filename = "file-abc123"
mock_result.attributes = {"source": "custom-index"}

source = _resolve_source_for_result(
mock_result,
["vs-001", "vs-002"],
{"custom-index": "should-not-be-used"},
)
assert source == "custom-index"

def test_multiple_stores_source_preferred_over_vector_store_id(
self, mocker: MockerFixture
) -> None:
"""Test source attribute takes precedence over vector_store_id."""
mock_result = mocker.Mock()
mock_result.filename = "file-abc123"
mock_result.attributes = {
"vector_store_id": "vs-002",
"source": "ocp-documentation",
}

source = _resolve_source_for_result(
mock_result,
["vs-001", "vs-002"],
{"vs-002": "rhel-9-docs"},
)
assert source == "ocp-documentation"

def test_multiple_stores_no_vector_store_id_no_source(
self, mocker: MockerFixture
) -> None:
"""Test resolution returns None when neither vector_store_id nor source present."""
mock_result = mocker.Mock()
mock_result.filename = "file-abc123"
mock_result.attributes = {"title": "some doc"}

source = _resolve_source_for_result(
mock_result,
["vs-001", "vs-002"],
{"vs-001": "ocp-docs"},
)
assert source is None


class TestBuildChunkAttributes:
"""Tests for _build_chunk_attributes function."""
Expand Down
Loading