Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[project]
name = "uipath-langchain"
version = "0.10.10"
version = "0.10.11"
description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
dependencies = [
"uipath>=2.10.57, <2.11.0",
"uipath-core>=0.5.13, <0.6.0",
"uipath-platform>=0.1.36, <0.2.0",
"uipath-platform>=0.1.42, <0.2.0",
"uipath-runtime>=0.10.0, <0.11.0",
"langgraph>=1.1.8, <2.0.0",
"langchain-core>=1.2.11, <2.0.0",
Expand Down
3 changes: 3 additions & 0 deletions src/uipath_langchain/agent/tools/context_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,13 +248,16 @@ async def context_tool_fn(
static_folder_path_prefix or _resolved_arg_folder_prefix
)

debug_run = UiPathConfig.is_studio_project

retriever = ContextGroundingRetriever(
index_name=resource.index_name,
folder_path=get_execution_folder_path(),
number_of_results=result_count,
threshold=threshold,
scope_folder=resolved_folder_path_prefix,
scope_extension=file_extension,
include_system_indexes=debug_run,
)

actual_query = prompt or query
Expand Down
17 changes: 17 additions & 0 deletions src/uipath_langchain/retrievers/context_grounding_retriever.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import logging

from langchain_core.callbacks import (
AsyncCallbackManagerForRetrieverRun,
CallbackManagerForRetrieverRun,
Expand All @@ -7,6 +9,8 @@
from uipath.platform import UiPath
from uipath.platform.context_grounding import SearchMode, UnifiedSearchScope

logger = logging.getLogger(__name__)


class ContextGroundingRetriever(BaseRetriever):
index_name: str
Expand All @@ -17,6 +21,7 @@ class ContextGroundingRetriever(BaseRetriever):
threshold: float = 0.0
scope_folder: str | None = None
scope_extension: str | None = None
include_system_indexes: bool = False

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

sdk = self.uipath_sdk if self.uipath_sdk is not None else UiPath()
if self.include_system_indexes:
logger.debug(
"Searching index '%s' with system-index fallback enabled",
self.index_name,
)
result = sdk.context_grounding.unified_search(
self.index_name,
query,
Expand All @@ -43,6 +53,7 @@ def _get_relevant_documents(
scope=self._build_scope(),
folder_path=self.folder_path,
folder_key=self.folder_key,
include_system_indexes=self.include_system_indexes,
)

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

sdk = self.uipath_sdk if self.uipath_sdk is not None else UiPath()
if self.include_system_indexes:
logger.debug(
"Searching index '%s' with system-index fallback enabled",
self.index_name,
)
result = await sdk.context_grounding.unified_search_async(
self.index_name,
query,
Expand All @@ -83,6 +99,7 @@ async def _aget_relevant_documents(
scope=self._build_scope(),
folder_path=self.folder_path,
folder_key=self.folder_key,
include_system_indexes=self.include_system_indexes,
)

values = result.semantic_results.values if result.semantic_results else []
Expand Down
126 changes: 126 additions & 0 deletions tests/agent/tools/test_context_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,48 @@ async def test_semantic_search_uses_execution_folder_path(self, semantic_config)
call_kwargs = mock_retriever_class.call_args[1]
assert call_kwargs["folder_path"] == "/Shared/TestFolder"

@pytest.mark.asyncio
async def test_semantic_search_enables_system_index_fallback_when_not_studio_project(
self,
semantic_config,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("UIPATH_PROJECT_ID", raising=False)
with patch(
"uipath_langchain.agent.tools.context_tool.ContextGroundingRetriever"
) as mock_retriever_class:
mock_retriever = AsyncMock()
mock_retriever.ainvoke.return_value = []
mock_retriever_class.return_value = mock_retriever

tool = handle_semantic_search("semantic_tool", semantic_config)
assert tool.coroutine is not None
await tool.coroutine(query="test query")

call_kwargs = mock_retriever_class.call_args.kwargs
assert call_kwargs["include_system_indexes"] is True

@pytest.mark.asyncio
async def test_semantic_search_disables_system_index_fallback_when_studio_project(
self,
semantic_config,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("UIPATH_PROJECT_ID", "some-project-id")
with patch(
"uipath_langchain.agent.tools.context_tool.ContextGroundingRetriever"
) as mock_retriever_class:
mock_retriever = AsyncMock()
mock_retriever.ainvoke.return_value = []
mock_retriever_class.return_value = mock_retriever

tool = handle_semantic_search("semantic_tool", semantic_config)
assert tool.coroutine is not None
await tool.coroutine(query="test query")

call_kwargs = mock_retriever_class.call_args.kwargs
assert call_kwargs["include_system_indexes"] is False


class TestHandleBatchTransform:
"""Test cases for handle_batch_transform function."""
Expand Down Expand Up @@ -1095,3 +1137,87 @@ async def test_non_400_enriched_exception_propagates(

with pytest.raises(EnrichedException):
await tool.coroutine(query="test query")


class TestSemanticSearchSystemIndexFallbackIntegration:
"""End-to-end mocked test that exercises the full SDK chain.

Verifies that when not running as a Studio project, the agent's
semantic-search tool resolves the index via the system-indexes
endpoint after the across-folders listing returns empty, and
successfully runs the unified search against the resolved id.
"""

@pytest.mark.asyncio
async def test_resolves_system_index_and_runs_unified_search(
self,
httpx_mock,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com/org/tenant")
monkeypatch.setenv("UIPATH_ACCESS_TOKEN", "test-token")
monkeypatch.setenv("UIPATH_ORGANIZATION_ID", "org-id")
monkeypatch.setenv("UIPATH_TENANT_ID", "tenant-id")
monkeypatch.setenv("UIPATH_TRACING_ENABLED", "False")
monkeypatch.delenv("UIPATH_PROJECT_ID", raising=False)
monkeypatch.delenv("UIPATH_FOLDER_PATH", raising=False)
monkeypatch.delenv("UIPATH_FOLDER_KEY", raising=False)

base = "https://cloud.uipath.com/org/tenant"
httpx_mock.add_response(
url=f"{base}/ecs_/v2/indexes/allacrossfolders?$expand=dataSource&$filter=Name eq 'system-template-index'",
status_code=200,
json={"value": []},
)
httpx_mock.add_response(
url=f"{base}/ecs_/v2/indexes/allsystemindexes?$expand=dataSource&$filter=Name eq 'system-template-index'",
status_code=200,
json={
"value": [
{
"id": "sys-1",
"name": "system-template-index",
"lastIngestionStatus": "Completed",
}
]
},
)
httpx_mock.add_response(
url=f"{base}/ecs_/v1.2/search/sys-1",
status_code=200,
json={
"semanticResults": {
"metadata": {"operation_id": "op-1", "strategy": "semantic"},
"values": [
{
"id": "doc-1",
"source": "src",
"page_number": 1,
"content": "hello world",
"score": 0.9,
}
],
}
},
)

resource = _make_context_resource(
name="semantic_tool",
description="Semantic search tool",
index_name="system-template-index",
retrieval_mode=AgentContextRetrievalMode.SEMANTIC,
query_variant="dynamic",
)

tool = handle_semantic_search("semantic_tool", resource)
assert tool.coroutine is not None
result = await tool.coroutine(query="hi")

assert "documents" in result
assert len(result["documents"]) == 1
assert result["documents"][0]["page_content"] == "hello world"

urls = [str(r.url) for r in httpx_mock.get_requests()]
assert any("/v2/indexes/allacrossfolders" in u for u in urls)
assert any("/v2/indexes/allsystemindexes" in u for u in urls)
assert any("/v1.2/search/sys-1" in u for u in urls)
79 changes: 79 additions & 0 deletions tests/retrievers/test_context_grounding_retriever.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Tests for ContextGroundingRetriever's include_system_indexes plumbing."""

from unittest.mock import AsyncMock, MagicMock

import pytest
from uipath.platform import UiPath

from uipath_langchain.retrievers import ContextGroundingRetriever


def _make_unified_search_result() -> MagicMock:
result = MagicMock()
result.semantic_results.values = []
result.semantic_results.metadata.operation_id = "op-1"
return result


def _make_sdk_mock() -> MagicMock:
return MagicMock(spec=UiPath)


def test_retriever_forwards_include_system_indexes_when_true() -> None:
sdk = _make_sdk_mock()
sdk.context_grounding.unified_search.return_value = _make_unified_search_result()

retriever = ContextGroundingRetriever(
index_name="my-index",
uipath_sdk=sdk,
include_system_indexes=True,
)
retriever.invoke("hello")

sdk.context_grounding.unified_search.assert_called_once()
kwargs = sdk.context_grounding.unified_search.call_args.kwargs
assert kwargs["include_system_indexes"] is True


def test_retriever_defaults_include_system_indexes_to_false() -> None:
sdk = _make_sdk_mock()
sdk.context_grounding.unified_search.return_value = _make_unified_search_result()

retriever = ContextGroundingRetriever(index_name="my-index", uipath_sdk=sdk)
retriever.invoke("hello")

kwargs = sdk.context_grounding.unified_search.call_args.kwargs
assert kwargs["include_system_indexes"] is False


@pytest.mark.asyncio
async def test_retriever_async_forwards_include_system_indexes_when_true() -> None:
sdk = _make_sdk_mock()
sdk.context_grounding.unified_search_async = AsyncMock(
return_value=_make_unified_search_result()
)

retriever = ContextGroundingRetriever(
index_name="my-index",
uipath_sdk=sdk,
include_system_indexes=True,
)
await retriever.ainvoke("hello")

sdk.context_grounding.unified_search_async.assert_awaited_once()
kwargs = sdk.context_grounding.unified_search_async.call_args.kwargs
assert kwargs["include_system_indexes"] is True


@pytest.mark.asyncio
async def test_retriever_async_defaults_include_system_indexes_to_false() -> None:
sdk = _make_sdk_mock()
sdk.context_grounding.unified_search_async = AsyncMock(
return_value=_make_unified_search_result()
)

retriever = ContextGroundingRetriever(index_name="my-index", uipath_sdk=sdk)
await retriever.ainvoke("hello")

kwargs = sdk.context_grounding.unified_search_async.call_args.kwargs
assert kwargs["include_system_indexes"] is False
10 changes: 5 additions & 5 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading