Skip to content

Commit 4872e94

Browse files
authored
feat: add support for system indexes [ECS-1791] (#818)
1 parent 87852f5 commit 4872e94

6 files changed

Lines changed: 233 additions & 8 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ requires-python = ">=3.11"
77
dependencies = [
88
"uipath>=2.10.57, <2.11.0",
99
"uipath-core>=0.5.13, <0.6.0",
10-
"uipath-platform>=0.1.36, <0.2.0",
10+
"uipath-platform>=0.1.43, <0.2.0",
1111
"uipath-runtime>=0.10.0, <0.11.0",
1212
"langgraph>=1.1.8, <2.0.0",
1313
"langchain-core>=1.2.11, <2.0.0",

src/uipath_langchain/agent/tools/context_tool.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,13 +248,16 @@ async def context_tool_fn(
248248
static_folder_path_prefix or _resolved_arg_folder_prefix
249249
)
250250

251+
debug_run = UiPathConfig.is_studio_project
252+
251253
retriever = ContextGroundingRetriever(
252254
index_name=resource.index_name,
253255
folder_path=get_execution_folder_path(),
254256
number_of_results=result_count,
255257
threshold=threshold,
256258
scope_folder=resolved_folder_path_prefix,
257259
scope_extension=file_extension,
260+
include_system_indexes=debug_run,
258261
)
259262

260263
actual_query = prompt or query

src/uipath_langchain/retrievers/context_grounding_retriever.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import logging
2+
13
from langchain_core.callbacks import (
24
AsyncCallbackManagerForRetrieverRun,
35
CallbackManagerForRetrieverRun,
@@ -7,6 +9,8 @@
79
from uipath.platform import UiPath
810
from uipath.platform.context_grounding import SearchMode, UnifiedSearchScope
911

12+
logger = logging.getLogger(__name__)
13+
1014

1115
class ContextGroundingRetriever(BaseRetriever):
1216
index_name: str
@@ -17,6 +21,7 @@ class ContextGroundingRetriever(BaseRetriever):
1721
threshold: float = 0.0
1822
scope_folder: str | None = None
1923
scope_extension: str | None = None
24+
include_system_indexes: bool = False
2025

2126
def _build_scope(self) -> UnifiedSearchScope | None:
2227
if self.scope_folder or self.scope_extension:
@@ -32,6 +37,11 @@ def _get_relevant_documents(
3237
"""Sync implementation calls context_grounding unified_search API."""
3338

3439
sdk = self.uipath_sdk if self.uipath_sdk is not None else UiPath()
40+
if self.include_system_indexes:
41+
logger.debug(
42+
"Searching index '%s' with system-index fallback enabled",
43+
self.index_name,
44+
)
3545
result = sdk.context_grounding.unified_search(
3646
self.index_name,
3747
query,
@@ -43,6 +53,7 @@ def _get_relevant_documents(
4353
scope=self._build_scope(),
4454
folder_path=self.folder_path,
4555
folder_key=self.folder_key,
56+
include_system_indexes=self.include_system_indexes,
4657
)
4758

4859
values = result.semantic_results.values if result.semantic_results else []
@@ -72,6 +83,11 @@ async def _aget_relevant_documents(
7283
"""Async implementation calls context_grounding unified_search_async API."""
7384

7485
sdk = self.uipath_sdk if self.uipath_sdk is not None else UiPath()
86+
if self.include_system_indexes:
87+
logger.debug(
88+
"Searching index '%s' with system-index fallback enabled",
89+
self.index_name,
90+
)
7591
result = await sdk.context_grounding.unified_search_async(
7692
self.index_name,
7793
query,
@@ -83,6 +99,7 @@ async def _aget_relevant_documents(
8399
scope=self._build_scope(),
84100
folder_path=self.folder_path,
85101
folder_key=self.folder_key,
102+
include_system_indexes=self.include_system_indexes,
86103
)
87104

88105
values = result.semantic_results.values if result.semantic_results else []

tests/agent/tools/test_context_tool.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,48 @@ async def test_semantic_search_uses_execution_folder_path(self, semantic_config)
525525
call_kwargs = mock_retriever_class.call_args[1]
526526
assert call_kwargs["folder_path"] == "/Shared/TestFolder"
527527

528+
@pytest.mark.asyncio
529+
async def test_semantic_search_enables_system_index_fallback_in_studio_project(
530+
self,
531+
semantic_config,
532+
monkeypatch: pytest.MonkeyPatch,
533+
) -> None:
534+
monkeypatch.setenv("UIPATH_PROJECT_ID", "some-project-id")
535+
with patch(
536+
"uipath_langchain.agent.tools.context_tool.ContextGroundingRetriever"
537+
) as mock_retriever_class:
538+
mock_retriever = AsyncMock()
539+
mock_retriever.ainvoke.return_value = []
540+
mock_retriever_class.return_value = mock_retriever
541+
542+
tool = handle_semantic_search("semantic_tool", semantic_config)
543+
assert tool.coroutine is not None
544+
await tool.coroutine(query="test query")
545+
546+
call_kwargs = mock_retriever_class.call_args.kwargs
547+
assert call_kwargs["include_system_indexes"] is True
548+
549+
@pytest.mark.asyncio
550+
async def test_semantic_search_disables_system_index_fallback_outside_studio_project(
551+
self,
552+
semantic_config,
553+
monkeypatch: pytest.MonkeyPatch,
554+
) -> None:
555+
monkeypatch.delenv("UIPATH_PROJECT_ID", raising=False)
556+
with patch(
557+
"uipath_langchain.agent.tools.context_tool.ContextGroundingRetriever"
558+
) as mock_retriever_class:
559+
mock_retriever = AsyncMock()
560+
mock_retriever.ainvoke.return_value = []
561+
mock_retriever_class.return_value = mock_retriever
562+
563+
tool = handle_semantic_search("semantic_tool", semantic_config)
564+
assert tool.coroutine is not None
565+
await tool.coroutine(query="test query")
566+
567+
call_kwargs = mock_retriever_class.call_args.kwargs
568+
assert call_kwargs["include_system_indexes"] is False
569+
528570

529571
class TestHandleBatchTransform:
530572
"""Test cases for handle_batch_transform function."""
@@ -1095,3 +1137,87 @@ async def test_non_400_enriched_exception_propagates(
10951137

10961138
with pytest.raises(EnrichedException):
10971139
await tool.coroutine(query="test query")
1140+
1141+
1142+
class TestSemanticSearchSystemIndexFallbackIntegration:
1143+
"""End-to-end mocked test that exercises the full SDK chain.
1144+
1145+
Verifies that when running as a Studio project (debug run), the agent's
1146+
semantic-search tool resolves the index via the system-indexes
1147+
endpoint after the across-folders listing returns empty, and
1148+
successfully runs the unified search against the resolved id.
1149+
"""
1150+
1151+
@pytest.mark.asyncio
1152+
async def test_resolves_system_index_and_runs_unified_search(
1153+
self,
1154+
httpx_mock,
1155+
monkeypatch: pytest.MonkeyPatch,
1156+
) -> None:
1157+
monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com/org/tenant")
1158+
monkeypatch.setenv("UIPATH_ACCESS_TOKEN", "test-token")
1159+
monkeypatch.setenv("UIPATH_ORGANIZATION_ID", "org-id")
1160+
monkeypatch.setenv("UIPATH_TENANT_ID", "tenant-id")
1161+
monkeypatch.setenv("UIPATH_TRACING_ENABLED", "False")
1162+
monkeypatch.setenv("UIPATH_PROJECT_ID", "studio-project-id")
1163+
monkeypatch.delenv("UIPATH_FOLDER_PATH", raising=False)
1164+
monkeypatch.delenv("UIPATH_FOLDER_KEY", raising=False)
1165+
1166+
base = "https://cloud.uipath.com/org/tenant"
1167+
httpx_mock.add_response(
1168+
url=f"{base}/ecs_/v2/indexes/allacrossfolders?$expand=dataSource&$filter=Name eq 'system-template-index'",
1169+
status_code=200,
1170+
json={"value": []},
1171+
)
1172+
httpx_mock.add_response(
1173+
url=f"{base}/ecs_/v2/indexes/allsystemindexes?$expand=dataSource&$filter=Name eq 'system-template-index'",
1174+
status_code=200,
1175+
json={
1176+
"value": [
1177+
{
1178+
"id": "sys-1",
1179+
"name": "system-template-index",
1180+
"lastIngestionStatus": "Completed",
1181+
}
1182+
]
1183+
},
1184+
)
1185+
httpx_mock.add_response(
1186+
url=f"{base}/ecs_/v1.2/search/sys-1",
1187+
status_code=200,
1188+
json={
1189+
"semanticResults": {
1190+
"metadata": {"operation_id": "op-1", "strategy": "semantic"},
1191+
"values": [
1192+
{
1193+
"id": "doc-1",
1194+
"source": "src",
1195+
"page_number": 1,
1196+
"content": "hello world",
1197+
"score": 0.9,
1198+
}
1199+
],
1200+
}
1201+
},
1202+
)
1203+
1204+
resource = _make_context_resource(
1205+
name="semantic_tool",
1206+
description="Semantic search tool",
1207+
index_name="system-template-index",
1208+
retrieval_mode=AgentContextRetrievalMode.SEMANTIC,
1209+
query_variant="dynamic",
1210+
)
1211+
1212+
tool = handle_semantic_search("semantic_tool", resource)
1213+
assert tool.coroutine is not None
1214+
result = await tool.coroutine(query="hi")
1215+
1216+
assert "documents" in result
1217+
assert len(result["documents"]) == 1
1218+
assert result["documents"][0]["page_content"] == "hello world"
1219+
1220+
urls = [str(r.url) for r in httpx_mock.get_requests()]
1221+
assert any("/v2/indexes/allacrossfolders" in u for u in urls)
1222+
assert any("/v2/indexes/allsystemindexes" in u for u in urls)
1223+
assert any("/v1.2/search/sys-1" in u for u in urls)
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Tests for ContextGroundingRetriever's include_system_indexes plumbing."""
2+
3+
from unittest.mock import AsyncMock, MagicMock
4+
5+
import pytest
6+
from uipath.platform import UiPath
7+
8+
from uipath_langchain.retrievers import ContextGroundingRetriever
9+
10+
11+
def _make_unified_search_result() -> MagicMock:
12+
result = MagicMock()
13+
result.semantic_results.values = []
14+
result.semantic_results.metadata.operation_id = "op-1"
15+
return result
16+
17+
18+
def _make_sdk_mock() -> MagicMock:
19+
return MagicMock(spec=UiPath)
20+
21+
22+
def test_retriever_forwards_include_system_indexes_when_true() -> None:
23+
sdk = _make_sdk_mock()
24+
sdk.context_grounding.unified_search.return_value = _make_unified_search_result()
25+
26+
retriever = ContextGroundingRetriever(
27+
index_name="my-index",
28+
uipath_sdk=sdk,
29+
include_system_indexes=True,
30+
)
31+
retriever.invoke("hello")
32+
33+
sdk.context_grounding.unified_search.assert_called_once()
34+
kwargs = sdk.context_grounding.unified_search.call_args.kwargs
35+
assert kwargs["include_system_indexes"] is True
36+
37+
38+
def test_retriever_defaults_include_system_indexes_to_false() -> None:
39+
sdk = _make_sdk_mock()
40+
sdk.context_grounding.unified_search.return_value = _make_unified_search_result()
41+
42+
retriever = ContextGroundingRetriever(index_name="my-index", uipath_sdk=sdk)
43+
retriever.invoke("hello")
44+
45+
kwargs = sdk.context_grounding.unified_search.call_args.kwargs
46+
assert kwargs["include_system_indexes"] is False
47+
48+
49+
@pytest.mark.asyncio
50+
async def test_retriever_async_forwards_include_system_indexes_when_true() -> None:
51+
sdk = _make_sdk_mock()
52+
sdk.context_grounding.unified_search_async = AsyncMock(
53+
return_value=_make_unified_search_result()
54+
)
55+
56+
retriever = ContextGroundingRetriever(
57+
index_name="my-index",
58+
uipath_sdk=sdk,
59+
include_system_indexes=True,
60+
)
61+
await retriever.ainvoke("hello")
62+
63+
sdk.context_grounding.unified_search_async.assert_awaited_once()
64+
kwargs = sdk.context_grounding.unified_search_async.call_args.kwargs
65+
assert kwargs["include_system_indexes"] is True
66+
67+
68+
@pytest.mark.asyncio
69+
async def test_retriever_async_defaults_include_system_indexes_to_false() -> None:
70+
sdk = _make_sdk_mock()
71+
sdk.context_grounding.unified_search_async = AsyncMock(
72+
return_value=_make_unified_search_result()
73+
)
74+
75+
retriever = ContextGroundingRetriever(index_name="my-index", uipath_sdk=sdk)
76+
await retriever.ainvoke("hello")
77+
78+
kwargs = sdk.context_grounding.unified_search_async.call_args.kwargs
79+
assert kwargs["include_system_indexes"] is False

uv.lock

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)