diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index a96a8eded..c52b917eb 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.41" +version = "0.1.42" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/context_grounding/_context_grounding_service.py b/packages/uipath-platform/src/uipath/platform/context_grounding/_context_grounding_service.py index 706394aca..1c2eabd85 100644 --- a/packages/uipath-platform/src/uipath/platform/context_grounding/_context_grounding_service.py +++ b/packages/uipath-platform/src/uipath/platform/context_grounding/_context_grounding_service.py @@ -19,6 +19,7 @@ from ..errors import ( BatchTransformFailedException, BatchTransformNotCompleteException, + ContextGroundingIndexNotFoundError, IngestionInProgressException, UnsupportedDataSourceException, ) @@ -256,6 +257,42 @@ async def retrieve_across_folders_async( ContextGroundingIndex.model_validate(item) for item in response["value"] ] + @traced(name="contextgrounding_retrieve_system_indexes", run_type="uipath") + def _retrieve_system_indexes( + self, + name: Optional[str] = None, + ) -> List[ContextGroundingIndex]: + spec = self._retrieve_system_indexes_spec(name=name) + + response = self.request( + spec.method, + spec.endpoint, + params=spec.params, + ).json() + + return [ + ContextGroundingIndex.model_validate(item) for item in response["value"] + ] + + @traced(name="contextgrounding_retrieve_system_indexes", run_type="uipath") + async def _retrieve_system_indexes_async( + self, + name: Optional[str] = None, + ) -> List[ContextGroundingIndex]: + spec = self._retrieve_system_indexes_spec(name=name) + + response = ( + await self.request_async( + spec.method, + spec.endpoint, + params=spec.params, + ) + ).json() + + return [ + ContextGroundingIndex.model_validate(item) for item in response["value"] + ] + @resource_override(resource_type="index") @traced(name="contextgrounding_retrieve", run_type="uipath") def retrieve( @@ -263,30 +300,38 @@ def retrieve( name: str, folder_key: Optional[str] = None, folder_path: Optional[str] = None, + include_system_indexes: bool = False, ) -> ContextGroundingIndex: """Retrieve context grounding index information by its name. If no folder_key or folder_path is provided and no folder context is - configured, falls back to searching across all folders. + configured, falls back to searching across all folders. When + ``include_system_indexes`` is True, an additional fallback against + system indexes is attempted before raising not-found. Args: name (str): The name of the context index to retrieve. folder_key (Optional[str]): The key of the folder where the index resides. folder_path (Optional[str]): The path of the folder where the index resides. + include_system_indexes (bool): If True, fall back to system indexes + when the index is not found in the per-folder or across-folders listings. + Defaults to False. Returns: ContextGroundingIndex: The index information, including its configuration and metadata if found. Raises: - Exception: If no index with the given name is found. + ContextGroundingIndexNotFoundError: If no index with the given name is found. """ resolved_folder_key = self._resolve_folder_key(folder_key, folder_path) if not resolved_folder_key: indexes = self.retrieve_across_folders(name=name) try: return next(index for index in indexes if index.name == name) - except StopIteration as e: - raise Exception("ContextGroundingIndex not found") from e + except StopIteration: + if include_system_indexes: + return self._retrieve_from_system_indexes(name) + raise ContextGroundingIndexNotFoundError(name) from None spec = self._retrieve_spec( name, @@ -305,8 +350,10 @@ def retrieve( for item in response["value"] if item["name"] == name ) - except StopIteration as e: - raise Exception("ContextGroundingIndex not found") from e + except StopIteration: + if include_system_indexes: + return self._retrieve_from_system_indexes(name) + raise ContextGroundingIndexNotFoundError(name) from None @resource_override(resource_type="index") @traced(name="contextgrounding_retrieve", run_type="uipath") @@ -315,30 +362,38 @@ async def retrieve_async( name: str, folder_key: Optional[str] = None, folder_path: Optional[str] = None, + include_system_indexes: bool = False, ) -> ContextGroundingIndex: """Asynchronously retrieve context grounding index information by its name. If no folder_key or folder_path is provided and no folder context is - configured, falls back to searching across all folders. + configured, falls back to searching across all folders. When + ``include_system_indexes`` is True, an additional fallback against + system indexes is attempted before raising not-found. Args: name (str): The name of the context index to retrieve. folder_key (Optional[str]): The key of the folder where the index resides. folder_path (Optional[str]): The path of the folder where the index resides. + include_system_indexes (bool): If True, fall back to system indexes when + the index is not found in the per-folder or across-folders listings. + Defaults to False. Returns: ContextGroundingIndex: The index information, including its configuration and metadata if found. Raises: - Exception: If no index with the given name is found. + ContextGroundingIndexNotFoundError: If no index with the given name is found. """ resolved_folder_key = self._resolve_folder_key(folder_key, folder_path) if not resolved_folder_key: indexes = await self.retrieve_across_folders_async(name=name) try: return next(index for index in indexes if index.name == name) - except StopIteration as e: - raise Exception("ContextGroundingIndex not found") from e + except StopIteration: + if include_system_indexes: + return await self._retrieve_from_system_indexes_async(name) + raise ContextGroundingIndexNotFoundError(name) from None spec = self._retrieve_spec( name, @@ -359,8 +414,26 @@ async def retrieve_async( for item in response["value"] if item["name"] == name ) - except StopIteration as e: - raise Exception("ContextGroundingIndex not found") from e + except StopIteration: + if include_system_indexes: + return await self._retrieve_from_system_indexes_async(name) + raise ContextGroundingIndexNotFoundError(name) from None + + def _retrieve_from_system_indexes(self, name: str) -> ContextGroundingIndex: + indexes = self._retrieve_system_indexes(name=name) + try: + return next(index for index in indexes if index.name == name) + except StopIteration: + raise ContextGroundingIndexNotFoundError(name) from None + + async def _retrieve_from_system_indexes_async( + self, name: str + ) -> ContextGroundingIndex: + indexes = await self._retrieve_system_indexes_async(name=name) + try: + return next(index for index in indexes if index.name == name) + except StopIteration: + raise ContextGroundingIndexNotFoundError(name) from None @traced(name="contextgrounding_list", run_type="uipath") def list( @@ -1489,6 +1562,7 @@ def unified_search( scope: Optional[UnifiedSearchScope] = None, folder_key: Optional[str] = None, folder_path: Optional[str] = None, + include_system_indexes: bool = False, ) -> UnifiedQueryResult: """Perform a unified search on a context grounding index. @@ -1504,11 +1578,19 @@ def unified_search( scope (Optional[UnifiedSearchScope]): Optional search scope (folder, extension). folder_key (Optional[str]): The key of the folder where the index resides. folder_path (Optional[str]): The path of the folder where the index resides. + include_system_indexes (bool): If True, fall back to tenant-wide + system indexes when the index is not found in folder or + across-folders listings. Defaults to False. Returns: UnifiedQueryResult: The unified search result containing semantic and/or tabular results. """ - index = self.retrieve(name, folder_key=folder_key, folder_path=folder_path) + index = self.retrieve( + name, + folder_key=folder_key, + folder_path=folder_path, + include_system_indexes=include_system_indexes, + ) folder_key = folder_key or index.folder_key @@ -1544,6 +1626,7 @@ async def unified_search_async( scope: Optional[UnifiedSearchScope] = None, folder_key: Optional[str] = None, folder_path: Optional[str] = None, + include_system_indexes: bool = False, ) -> UnifiedQueryResult: """Asynchronously perform a unified search on a context grounding index. @@ -1559,12 +1642,18 @@ async def unified_search_async( scope (Optional[UnifiedSearchScope]): Optional search scope (folder, extension). folder_key (Optional[str]): The key of the folder where the index resides. folder_path (Optional[str]): The path of the folder where the index resides. + include_system_indexes (bool): If True, fall back to tenant-wide + system indexes when the index is not found in folder or + across-folders listings. Defaults to False. Returns: UnifiedQueryResult: The unified search result containing semantic and/or tabular results. """ index = await self.retrieve_async( - name, folder_key=folder_key, folder_path=folder_path + name, + folder_key=folder_key, + folder_path=folder_path, + include_system_indexes=include_system_indexes, ) if index and index.in_progress_ingestion(): raise IngestionInProgressException(index_name=name) @@ -1911,6 +2000,16 @@ def _ingest_spec( }, ) + @staticmethod + def _odata_name_filter(name: str) -> str: + """Build an OData ``Name eq ''`` filter with single quotes escaped. + + OData string literals escape ``'`` by doubling it. URL encoding of the + resulting filter is handled by the HTTP client when params are passed + as a dict. + """ + return "Name eq '{}'".format(name.replace("'", "''")) + def _retrieve_across_folders_spec( self, name: Optional[str] = None, @@ -1919,7 +2018,7 @@ def _retrieve_across_folders_spec( "$expand": "dataSource", } if name: - params["$filter"] = f"Name eq '{name}'" + params["$filter"] = self._odata_name_filter(name) return RequestSpec( method="GET", @@ -1927,6 +2026,22 @@ def _retrieve_across_folders_spec( params=params, ) + def _retrieve_system_indexes_spec( + self, + name: Optional[str] = None, + ) -> RequestSpec: + params: Dict[str, str] = { + "$expand": "dataSource", + } + if name: + params["$filter"] = self._odata_name_filter(name) + + return RequestSpec( + method="GET", + endpoint=Endpoint("/ecs_/v2/indexes/allsystemindexes"), + params=params, + ) + def _list_spec( self, folder_key: Optional[str] = None, @@ -1954,7 +2069,7 @@ def _retrieve_spec( method="GET", endpoint=Endpoint("/ecs_/v2/indexes"), params={ - "$filter": f"Name eq '{name}'", + "$filter": self._odata_name_filter(name), "$expand": "dataSource", }, headers={ diff --git a/packages/uipath-platform/src/uipath/platform/errors/__init__.py b/packages/uipath-platform/src/uipath/platform/errors/__init__.py index 58afd93f7..97f7e6d98 100644 --- a/packages/uipath-platform/src/uipath/platform/errors/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/errors/__init__.py @@ -8,6 +8,7 @@ - FolderNotFoundException: Raised when a folder cannot be found - UnsupportedDataSourceException: Raised when an operation is attempted on an unsupported data source type - IngestionInProgressException: Raised when a search is attempted on an index during ingestion +- ContextGroundingIndexNotFoundError: Raised when a context grounding index cannot be resolved by name - BatchTransformFailedException: Raised when a batch transform has failed - BatchTransformNotCompleteException: Raised when attempting to get results from an incomplete batch transform - OperationNotCompleteException: Raised when attempting to get results from an incomplete operation @@ -18,6 +19,9 @@ from ._base_url_missing_error import BaseUrlMissingError from ._batch_transform_failed_exception import BatchTransformFailedException from ._batch_transform_not_complete_exception import BatchTransformNotCompleteException +from ._context_grounding_index_not_found_exception import ( + ContextGroundingIndexNotFoundError, +) from ._enriched_exception import EnrichedException, ExtractedErrorInfo from ._folder_not_found_exception import FolderNotFoundException from ._ingestion_in_progress_exception import IngestionInProgressException @@ -30,6 +34,7 @@ "BaseUrlMissingError", "BatchTransformFailedException", "BatchTransformNotCompleteException", + "ContextGroundingIndexNotFoundError", "EnrichedException", "ExtractedErrorInfo", "FolderNotFoundException", diff --git a/packages/uipath-platform/src/uipath/platform/errors/_context_grounding_index_not_found_exception.py b/packages/uipath-platform/src/uipath/platform/errors/_context_grounding_index_not_found_exception.py new file mode 100644 index 000000000..653be92e8 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/errors/_context_grounding_index_not_found_exception.py @@ -0,0 +1,13 @@ +from typing import Optional + + +class ContextGroundingIndexNotFoundError(Exception): + """Raised when a context grounding index cannot be resolved by name.""" + + def __init__(self, index_name: Optional[str] = None): + self.index_name = index_name + if index_name: + self.message = f"ContextGroundingIndex '{index_name}' not found" + else: + self.message = "ContextGroundingIndex not found" + super().__init__(self.message) diff --git a/packages/uipath-platform/tests/services/test_context_grounding_service.py b/packages/uipath-platform/tests/services/test_context_grounding_service.py index 94626b8a1..3fef9f47a 100644 --- a/packages/uipath-platform/tests/services/test_context_grounding_service.py +++ b/packages/uipath-platform/tests/services/test_context_grounding_service.py @@ -31,6 +31,7 @@ from uipath.platform.context_grounding._context_grounding_service import ( ContextGroundingService, ) +from uipath.platform.errors import ContextGroundingIndexNotFoundError from uipath.platform.orchestrator._buckets_service import BucketsService from uipath.platform.orchestrator._folder_service import FolderService @@ -761,6 +762,529 @@ async def test_retrieve_async_falls_back_to_across_folders_when_no_folder_contex assert len(sent_requests) == 1 assert "/ecs_/v2/indexes/allacrossfolders" in str(sent_requests[0].url) + def test_retrieve_system_indexes( + self, + httpx_mock: HTTPXMock, + service: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allsystemindexes?$expand=dataSource", + status_code=200, + json={ + "value": [ + { + "id": "sys-index-1", + "name": "system-template-index", + "lastIngestionStatus": "Completed", + }, + { + "id": "sys-index-2", + "name": "system-other-index", + "lastIngestionStatus": "Completed", + }, + ] + }, + ) + + indexes = service._retrieve_system_indexes() + + assert isinstance(indexes, list) + assert len(indexes) == 2 + assert isinstance(indexes[0], ContextGroundingIndex) + assert indexes[0].id == "sys-index-1" + assert indexes[0].name == "system-template-index" + + sent_requests = httpx_mock.get_requests() + assert sent_requests[0].method == "GET" + assert "/ecs_/v2/indexes/allsystemindexes" in str(sent_requests[0].url) + assert "x-uipath-folderkey" not in sent_requests[0].headers + + assert HEADER_USER_AGENT in sent_requests[0].headers + assert ( + sent_requests[0].headers[HEADER_USER_AGENT] + == f"UiPath.Python.Sdk/UiPath.Python.Sdk.Activities.ContextGroundingService._retrieve_system_indexes/{version}" + ) + + def test_retrieve_system_indexes_with_name_filter( + self, + httpx_mock: HTTPXMock, + service: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allsystemindexes?$expand=dataSource&$filter=Name eq 'system-template-index'", + status_code=200, + json={ + "value": [ + { + "id": "sys-index-1", + "name": "system-template-index", + "lastIngestionStatus": "Completed", + }, + ] + }, + ) + + indexes = service._retrieve_system_indexes(name="system-template-index") + + assert len(indexes) == 1 + assert indexes[0].name == "system-template-index" + + sent_requests = httpx_mock.get_requests() + assert "allsystemindexes" in str(sent_requests[0].url) + assert "x-uipath-folderkey" not in sent_requests[0].headers + + @pytest.mark.anyio + async def test_retrieve_system_indexes_async( + self, + httpx_mock: HTTPXMock, + service: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allsystemindexes?$expand=dataSource", + status_code=200, + json={ + "value": [ + { + "id": "sys-index-1", + "name": "system-template-index", + "lastIngestionStatus": "Completed", + } + ] + }, + ) + + indexes = await service._retrieve_system_indexes_async() + + assert len(indexes) == 1 + assert indexes[0].id == "sys-index-1" + + sent_requests = httpx_mock.get_requests() + assert sent_requests[0].method == "GET" + assert "/ecs_/v2/indexes/allsystemindexes" in str(sent_requests[0].url) + assert "x-uipath-folderkey" not in sent_requests[0].headers + + assert ( + sent_requests[0].headers[HEADER_USER_AGENT] + == f"UiPath.Python.Sdk/UiPath.Python.Sdk.Activities.ContextGroundingService._retrieve_system_indexes_async/{version}" + ) + + def test_retrieve_system_indexes_escapes_single_quote_in_name( + self, + httpx_mock: HTTPXMock, + service: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allsystemindexes?$expand=dataSource&$filter=Name eq 'O''Brien'", + status_code=200, + json={ + "value": [ + { + "id": "sys-1", + "name": "O'Brien", + "lastIngestionStatus": "Completed", + } + ] + }, + ) + + indexes = service._retrieve_system_indexes(name="O'Brien") + + assert len(indexes) == 1 + assert indexes[0].name == "O'Brien" + + def test_retrieve_across_folders_escapes_single_quote_in_name( + self, + httpx_mock: HTTPXMock, + service: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allacrossfolders?$expand=dataSource&$filter=Name eq 'O''Brien'", + status_code=200, + json={ + "value": [ + { + "id": "idx-1", + "name": "O'Brien", + "lastIngestionStatus": "Completed", + } + ] + }, + ) + + indexes = service.retrieve_across_folders(name="O'Brien") + + assert len(indexes) == 1 + assert indexes[0].name == "O'Brien" + + def test_retrieve_system_indexes_empty( + self, + httpx_mock: HTTPXMock, + service: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allsystemindexes?$expand=dataSource", + status_code=200, + json={"value": []}, + ) + + indexes = service._retrieve_system_indexes() + + assert indexes == [] + + def test_retrieve_raises_typed_not_found_when_across_folders_empty( + self, + httpx_mock: HTTPXMock, + service_no_folder: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allacrossfolders?$expand=dataSource&$filter=Name eq 'missing-index'", + status_code=200, + json={"value": []}, + ) + + with pytest.raises(ContextGroundingIndexNotFoundError) as exc_info: + service_no_folder.retrieve(name="missing-index") + + assert exc_info.value.index_name == "missing-index" + + @pytest.mark.anyio + async def test_retrieve_async_raises_typed_not_found_when_across_folders_empty( + self, + httpx_mock: HTTPXMock, + service_no_folder: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allacrossfolders?$expand=dataSource&$filter=Name eq 'missing-index'", + status_code=200, + json={"value": []}, + ) + + with pytest.raises(ContextGroundingIndexNotFoundError): + await service_no_folder.retrieve_async(name="missing-index") + + def test_retrieve_falls_back_to_system_indexes_when_flag_true( + self, + httpx_mock: HTTPXMock, + service_no_folder: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allacrossfolders?$expand=dataSource&$filter=Name eq 'system-template-index'", + status_code=200, + json={"value": []}, + ) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/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", + } + ] + }, + ) + + index = service_no_folder.retrieve( + name="system-template-index", include_system_indexes=True + ) + + assert index.id == "sys-1" + assert index.name == "system-template-index" + + sent_requests = httpx_mock.get_requests() + assert len(sent_requests) == 2 + assert "/ecs_/v2/indexes/allacrossfolders" in str(sent_requests[0].url) + assert "/ecs_/v2/indexes/allsystemindexes" in str(sent_requests[1].url) + + def test_retrieve_does_not_fall_back_to_system_indexes_when_flag_false( + self, + httpx_mock: HTTPXMock, + service_no_folder: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allacrossfolders?$expand=dataSource&$filter=Name eq 'missing-index'", + status_code=200, + json={"value": []}, + ) + + with pytest.raises(ContextGroundingIndexNotFoundError): + service_no_folder.retrieve(name="missing-index") + + sent_requests = httpx_mock.get_requests() + assert len(sent_requests) == 1 + assert "/ecs_/v2/indexes/allacrossfolders" in str(sent_requests[0].url) + + def test_retrieve_skips_system_indexes_when_across_folders_resolves( + self, + httpx_mock: HTTPXMock, + service_no_folder: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allacrossfolders?$expand=dataSource&$filter=Name eq 'tenant-index'", + status_code=200, + json={ + "value": [ + { + "id": "tenant-1", + "name": "tenant-index", + "lastIngestionStatus": "Completed", + "folderKey": "folder-x", + } + ] + }, + ) + + index = service_no_folder.retrieve( + name="tenant-index", include_system_indexes=True + ) + + assert index.id == "tenant-1" + + sent_requests = httpx_mock.get_requests() + assert len(sent_requests) == 1 + assert "/ecs_/v2/indexes/allacrossfolders" in str(sent_requests[0].url) + + def test_retrieve_falls_back_to_system_indexes_after_folder_lookup_misses( + self, + httpx_mock: HTTPXMock, + service: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/api/FoldersNavigation/GetFoldersForCurrentUser?searchText=test-folder-path&skip=0&take=20", + status_code=200, + json={ + "PageItems": [ + { + "Key": "test-folder-key", + "FullyQualifiedName": "test-folder-path", + } + ] + }, + ) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes?$filter=Name eq 'system-template-index'&$expand=dataSource", + status_code=200, + json={"value": []}, + ) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/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", + } + ] + }, + ) + + index = service.retrieve( + name="system-template-index", + folder_path="test-folder-path", + include_system_indexes=True, + ) + + assert index.id == "sys-1" + + @pytest.mark.anyio + async def test_retrieve_async_falls_back_to_system_indexes_when_flag_true( + self, + httpx_mock: HTTPXMock, + service_no_folder: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allacrossfolders?$expand=dataSource&$filter=Name eq 'system-template-index'", + status_code=200, + json={"value": []}, + ) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/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", + } + ] + }, + ) + + index = await service_no_folder.retrieve_async( + name="system-template-index", include_system_indexes=True + ) + + assert index.id == "sys-1" + + def test_retrieve_with_flag_raises_when_system_indexes_also_empty( + self, + httpx_mock: HTTPXMock, + service_no_folder: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allacrossfolders?$expand=dataSource&$filter=Name eq 'missing-index'", + status_code=200, + json={"value": []}, + ) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allsystemindexes?$expand=dataSource&$filter=Name eq 'missing-index'", + status_code=200, + json={"value": []}, + ) + + with pytest.raises(ContextGroundingIndexNotFoundError): + service_no_folder.retrieve( + name="missing-index", include_system_indexes=True + ) + + def test_unified_search_forwards_include_system_indexes( + self, + httpx_mock: HTTPXMock, + service_no_folder: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allacrossfolders?$expand=dataSource&$filter=Name eq 'system-template-index'", + status_code=200, + json={"value": []}, + ) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/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_url}{org}{tenant}/ecs_/v1.2/search/sys-1", + status_code=200, + json={ + "semanticResults": { + "values": [], + "metadata": {"operation_id": "op-1", "strategy": "semantic"}, + } + }, + ) + + result = service_no_folder.unified_search( + name="system-template-index", + query="hello", + include_system_indexes=True, + ) + + assert isinstance(result, UnifiedQueryResult) + + sent_requests = httpx_mock.get_requests() + assert any("allsystemindexes" in str(r.url) for r in sent_requests) + assert any("search/sys-1" in str(r.url) for r in sent_requests) + + @pytest.mark.anyio + async def test_unified_search_async_forwards_include_system_indexes( + self, + httpx_mock: HTTPXMock, + service_no_folder: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allacrossfolders?$expand=dataSource&$filter=Name eq 'system-template-index'", + status_code=200, + json={"value": []}, + ) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/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_url}{org}{tenant}/ecs_/v1.2/search/sys-1", + status_code=200, + json={ + "semanticResults": { + "values": [], + "metadata": {"operation_id": "op-1", "strategy": "semantic"}, + } + }, + ) + + result = await service_no_folder.unified_search_async( + name="system-template-index", + query="hello", + include_system_indexes=True, + ) + + assert isinstance(result, UnifiedQueryResult) + + sent_requests = httpx_mock.get_requests() + assert any("allsystemindexes" in str(r.url) for r in sent_requests) + assert any("search/sys-1" in str(r.url) for r in sent_requests) + def test_search_uses_index_folder_key_when_no_folder_context( self, httpx_mock: HTTPXMock, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index b8f7a0163..8695a33f1 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.41" +version = "0.1.42" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 7e98f563b..406c919af 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.11" dependencies = [ "uipath-core>=0.5.8, <0.6.0", "uipath-runtime>=0.10.1, <0.11.0", - "uipath-platform>=0.1.41, <0.2.0", + "uipath-platform>=0.1.42, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", diff --git a/packages/uipath/tests/cli/contract/test_sdk_cli_alignment.py b/packages/uipath/tests/cli/contract/test_sdk_cli_alignment.py index 52699d42e..a690727d4 100644 --- a/packages/uipath/tests/cli/contract/test_sdk_cli_alignment.py +++ b/packages/uipath/tests/cli/contract/test_sdk_cli_alignment.py @@ -192,9 +192,13 @@ def assert_cli_sdk_alignment( # Used when SDK has optional params that CLI doesn't expose SDK_EXCLUSIONS = { "context-grounding_list": set(), - "context-grounding_retrieve": set(), + "context-grounding_retrieve": {"include_system_indexes"}, "context-grounding_create": {"source", "embeddings_enabled", "is_encrypted"}, - "context-grounding_search": {"scope", "number_of_results"}, + "context-grounding_search": { + "scope", + "number_of_results", + "include_system_indexes", + }, "context-grounding_ingest": set(), "context-grounding_delete": set(), "context-grounding_deep-rag_start": set(), diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 0525ffb1c..d59970eea 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.41" +version = "0.1.42" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" },