Skip to content

Commit 846b412

Browse files
committed
feat: add support for system indexes [ECS-1791]
1 parent 79756aa commit 846b412

8 files changed

Lines changed: 705 additions & 22 deletions

File tree

packages/uipath-platform/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-platform"
3-
version = "0.1.39"
3+
version = "0.1.40"
44
description = "HTTP client library for programmatic access to UiPath Platform"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

packages/uipath-platform/src/uipath/platform/context_grounding/_context_grounding_service.py

Lines changed: 157 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from ..errors import (
2020
BatchTransformFailedException,
2121
BatchTransformNotCompleteException,
22+
ContextGroundingIndexNotFoundError,
2223
IngestionInProgressException,
2324
UnsupportedDataSourceException,
2425
)
@@ -256,37 +257,107 @@ async def retrieve_across_folders_async(
256257
ContextGroundingIndex.model_validate(item) for item in response["value"]
257258
]
258259

260+
@traced(name="contextgrounding_retrieve_system_indexes", run_type="uipath")
261+
def retrieve_system_indexes(
262+
self,
263+
name: Optional[str] = None,
264+
) -> List[ContextGroundingIndex]:
265+
"""Retrieve tenant-wide system context grounding indexes.
266+
267+
System indexes are shared across all folders and tenants and do not
268+
require a folder key. They are typically used by bundled StudioWeb
269+
templates that reference shared indexes.
270+
271+
Args:
272+
name (Optional[str]): Optional name filter. If provided, only indexes
273+
matching this name will be returned.
274+
275+
Returns:
276+
List[ContextGroundingIndex]: A list of system indexes.
277+
"""
278+
spec = self._retrieve_system_indexes_spec(name=name)
279+
280+
response = self.request(
281+
spec.method,
282+
spec.endpoint,
283+
params=spec.params,
284+
).json()
285+
286+
return [
287+
ContextGroundingIndex.model_validate(item) for item in response["value"]
288+
]
289+
290+
@traced(name="contextgrounding_retrieve_system_indexes", run_type="uipath")
291+
async def retrieve_system_indexes_async(
292+
self,
293+
name: Optional[str] = None,
294+
) -> List[ContextGroundingIndex]:
295+
"""Asynchronously retrieve tenant-wide system context grounding indexes.
296+
297+
System indexes are shared across all folders and tenants and do not
298+
require a folder key. They are typically used by bundled StudioWeb
299+
templates that reference shared indexes.
300+
301+
Args:
302+
name (Optional[str]): Optional name filter. If provided, only indexes
303+
matching this name will be returned.
304+
305+
Returns:
306+
List[ContextGroundingIndex]: A list of system indexes.
307+
"""
308+
spec = self._retrieve_system_indexes_spec(name=name)
309+
310+
response = (
311+
await self.request_async(
312+
spec.method,
313+
spec.endpoint,
314+
params=spec.params,
315+
)
316+
).json()
317+
318+
return [
319+
ContextGroundingIndex.model_validate(item) for item in response["value"]
320+
]
321+
259322
@resource_override(resource_type="index")
260323
@traced(name="contextgrounding_retrieve", run_type="uipath")
261324
def retrieve(
262325
self,
263326
name: str,
264327
folder_key: Optional[str] = None,
265328
folder_path: Optional[str] = None,
329+
include_system_indexes: bool = False,
266330
) -> ContextGroundingIndex:
267331
"""Retrieve context grounding index information by its name.
268332
269333
If no folder_key or folder_path is provided and no folder context is
270-
configured, falls back to searching across all folders.
334+
configured, falls back to searching across all folders. When
335+
``include_system_indexes`` is True, an additional fallback against
336+
system indexes is attempted before raising not-found.
271337
272338
Args:
273339
name (str): The name of the context index to retrieve.
274340
folder_key (Optional[str]): The key of the folder where the index resides.
275341
folder_path (Optional[str]): The path of the folder where the index resides.
342+
include_system_indexes (bool): If True, fall back to system indexes
343+
when the index is not found in the per-folder or across-folders listings.
344+
Defaults to False.
276345
277346
Returns:
278347
ContextGroundingIndex: The index information, including its configuration and metadata if found.
279348
280349
Raises:
281-
Exception: If no index with the given name is found.
350+
ContextGroundingIndexNotFoundError: If no index with the given name is found.
282351
"""
283352
resolved_folder_key = self._resolve_folder_key(folder_key, folder_path)
284353
if not resolved_folder_key:
285354
indexes = self.retrieve_across_folders(name=name)
286355
try:
287356
return next(index for index in indexes if index.name == name)
288-
except StopIteration as e:
289-
raise Exception("ContextGroundingIndex not found") from e
357+
except StopIteration:
358+
if include_system_indexes:
359+
return self._retrieve_from_system_indexes(name)
360+
raise ContextGroundingIndexNotFoundError(name) from None
290361

291362
spec = self._retrieve_spec(
292363
name,
@@ -305,8 +376,10 @@ def retrieve(
305376
for item in response["value"]
306377
if item["name"] == name
307378
)
308-
except StopIteration as e:
309-
raise Exception("ContextGroundingIndex not found") from e
379+
except StopIteration:
380+
if include_system_indexes:
381+
return self._retrieve_from_system_indexes(name)
382+
raise ContextGroundingIndexNotFoundError(name) from None
310383

311384
@resource_override(resource_type="index")
312385
@traced(name="contextgrounding_retrieve", run_type="uipath")
@@ -315,30 +388,38 @@ async def retrieve_async(
315388
name: str,
316389
folder_key: Optional[str] = None,
317390
folder_path: Optional[str] = None,
391+
include_system_indexes: bool = False,
318392
) -> ContextGroundingIndex:
319393
"""Asynchronously retrieve context grounding index information by its name.
320394
321395
If no folder_key or folder_path is provided and no folder context is
322-
configured, falls back to searching across all folders.
396+
configured, falls back to searching across all folders. When
397+
``include_system_indexes`` is True, an additional fallback against
398+
system indexes is attempted before raising not-found.
323399
324400
Args:
325401
name (str): The name of the context index to retrieve.
326402
folder_key (Optional[str]): The key of the folder where the index resides.
327403
folder_path (Optional[str]): The path of the folder where the index resides.
404+
include_system_indexes (bool): If True, fall back to system indexes when
405+
the index is not found in the per-folder or across-folders listings.
406+
Defaults to False.
328407
329408
Returns:
330409
ContextGroundingIndex: The index information, including its configuration and metadata if found.
331410
332411
Raises:
333-
Exception: If no index with the given name is found.
412+
ContextGroundingIndexNotFoundError: If no index with the given name is found.
334413
"""
335414
resolved_folder_key = self._resolve_folder_key(folder_key, folder_path)
336415
if not resolved_folder_key:
337416
indexes = await self.retrieve_across_folders_async(name=name)
338417
try:
339418
return next(index for index in indexes if index.name == name)
340-
except StopIteration as e:
341-
raise Exception("ContextGroundingIndex not found") from e
419+
except StopIteration:
420+
if include_system_indexes:
421+
return await self._retrieve_from_system_indexes_async(name)
422+
raise ContextGroundingIndexNotFoundError(name) from None
342423

343424
spec = self._retrieve_spec(
344425
name,
@@ -359,8 +440,26 @@ async def retrieve_async(
359440
for item in response["value"]
360441
if item["name"] == name
361442
)
362-
except StopIteration as e:
363-
raise Exception("ContextGroundingIndex not found") from e
443+
except StopIteration:
444+
if include_system_indexes:
445+
return await self._retrieve_from_system_indexes_async(name)
446+
raise ContextGroundingIndexNotFoundError(name) from None
447+
448+
def _retrieve_from_system_indexes(self, name: str) -> ContextGroundingIndex:
449+
indexes = self.retrieve_system_indexes(name=name)
450+
try:
451+
return next(index for index in indexes if index.name == name)
452+
except StopIteration:
453+
raise ContextGroundingIndexNotFoundError(name) from None
454+
455+
async def _retrieve_from_system_indexes_async(
456+
self, name: str
457+
) -> ContextGroundingIndex:
458+
indexes = await self.retrieve_system_indexes_async(name=name)
459+
try:
460+
return next(index for index in indexes if index.name == name)
461+
except StopIteration:
462+
raise ContextGroundingIndexNotFoundError(name) from None
364463

365464
@traced(name="contextgrounding_list", run_type="uipath")
366465
def list(
@@ -1489,6 +1588,7 @@ def unified_search(
14891588
scope: Optional[UnifiedSearchScope] = None,
14901589
folder_key: Optional[str] = None,
14911590
folder_path: Optional[str] = None,
1591+
include_system_indexes: bool = False,
14921592
) -> UnifiedQueryResult:
14931593
"""Perform a unified search on a context grounding index.
14941594
@@ -1504,11 +1604,19 @@ def unified_search(
15041604
scope (Optional[UnifiedSearchScope]): Optional search scope (folder, extension).
15051605
folder_key (Optional[str]): The key of the folder where the index resides.
15061606
folder_path (Optional[str]): The path of the folder where the index resides.
1607+
include_system_indexes (bool): If True, fall back to tenant-wide
1608+
system indexes when the index is not found in folder or
1609+
across-folders listings. Defaults to False.
15071610
15081611
Returns:
15091612
UnifiedQueryResult: The unified search result containing semantic and/or tabular results.
15101613
"""
1511-
index = self.retrieve(name, folder_key=folder_key, folder_path=folder_path)
1614+
index = self.retrieve(
1615+
name,
1616+
folder_key=folder_key,
1617+
folder_path=folder_path,
1618+
include_system_indexes=include_system_indexes,
1619+
)
15121620

15131621
folder_key = folder_key or index.folder_key
15141622

@@ -1544,6 +1652,7 @@ async def unified_search_async(
15441652
scope: Optional[UnifiedSearchScope] = None,
15451653
folder_key: Optional[str] = None,
15461654
folder_path: Optional[str] = None,
1655+
include_system_indexes: bool = False,
15471656
) -> UnifiedQueryResult:
15481657
"""Asynchronously perform a unified search on a context grounding index.
15491658
@@ -1559,12 +1668,18 @@ async def unified_search_async(
15591668
scope (Optional[UnifiedSearchScope]): Optional search scope (folder, extension).
15601669
folder_key (Optional[str]): The key of the folder where the index resides.
15611670
folder_path (Optional[str]): The path of the folder where the index resides.
1671+
include_system_indexes (bool): If True, fall back to tenant-wide
1672+
system indexes when the index is not found in folder or
1673+
across-folders listings. Defaults to False.
15621674
15631675
Returns:
15641676
UnifiedQueryResult: The unified search result containing semantic and/or tabular results.
15651677
"""
15661678
index = await self.retrieve_async(
1567-
name, folder_key=folder_key, folder_path=folder_path
1679+
name,
1680+
folder_key=folder_key,
1681+
folder_path=folder_path,
1682+
include_system_indexes=include_system_indexes,
15681683
)
15691684
if index and index.in_progress_ingestion():
15701685
raise IngestionInProgressException(index_name=name)
@@ -1911,6 +2026,16 @@ def _ingest_spec(
19112026
},
19122027
)
19132028

2029+
@staticmethod
2030+
def _odata_name_filter(name: str) -> str:
2031+
"""Build an OData ``Name eq '<name>'`` filter with single quotes escaped.
2032+
2033+
OData string literals escape ``'`` by doubling it. URL encoding of the
2034+
resulting filter is handled by the HTTP client when params are passed
2035+
as a dict.
2036+
"""
2037+
return "Name eq '{}'".format(name.replace("'", "''"))
2038+
19142039
def _retrieve_across_folders_spec(
19152040
self,
19162041
name: Optional[str] = None,
@@ -1919,14 +2044,30 @@ def _retrieve_across_folders_spec(
19192044
"$expand": "dataSource",
19202045
}
19212046
if name:
1922-
params["$filter"] = f"Name eq '{name}'"
2047+
params["$filter"] = self._odata_name_filter(name)
19232048

19242049
return RequestSpec(
19252050
method="GET",
19262051
endpoint=Endpoint("/ecs_/v2/indexes/allacrossfolders"),
19272052
params=params,
19282053
)
19292054

2055+
def _retrieve_system_indexes_spec(
2056+
self,
2057+
name: Optional[str] = None,
2058+
) -> RequestSpec:
2059+
params: Dict[str, str] = {
2060+
"$expand": "dataSource",
2061+
}
2062+
if name:
2063+
params["$filter"] = self._odata_name_filter(name)
2064+
2065+
return RequestSpec(
2066+
method="GET",
2067+
endpoint=Endpoint("/ecs_/v2/indexes/allsystemindexes"),
2068+
params=params,
2069+
)
2070+
19302071
def _list_spec(
19312072
self,
19322073
folder_key: Optional[str] = None,
@@ -1954,7 +2095,7 @@ def _retrieve_spec(
19542095
method="GET",
19552096
endpoint=Endpoint("/ecs_/v2/indexes"),
19562097
params={
1957-
"$filter": f"Name eq '{name}'",
2098+
"$filter": self._odata_name_filter(name),
19582099
"$expand": "dataSource",
19592100
},
19602101
headers={

packages/uipath-platform/src/uipath/platform/errors/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- FolderNotFoundException: Raised when a folder cannot be found
99
- UnsupportedDataSourceException: Raised when an operation is attempted on an unsupported data source type
1010
- IngestionInProgressException: Raised when a search is attempted on an index during ingestion
11+
- ContextGroundingIndexNotFoundError: Raised when a context grounding index cannot be resolved by name
1112
- BatchTransformFailedException: Raised when a batch transform has failed
1213
- BatchTransformNotCompleteException: Raised when attempting to get results from an incomplete batch transform
1314
- OperationNotCompleteException: Raised when attempting to get results from an incomplete operation
@@ -18,6 +19,9 @@
1819
from ._base_url_missing_error import BaseUrlMissingError
1920
from ._batch_transform_failed_exception import BatchTransformFailedException
2021
from ._batch_transform_not_complete_exception import BatchTransformNotCompleteException
22+
from ._context_grounding_index_not_found_exception import (
23+
ContextGroundingIndexNotFoundError,
24+
)
2125
from ._enriched_exception import EnrichedException, ExtractedErrorInfo
2226
from ._folder_not_found_exception import FolderNotFoundException
2327
from ._ingestion_in_progress_exception import IngestionInProgressException
@@ -30,6 +34,7 @@
3034
"BaseUrlMissingError",
3135
"BatchTransformFailedException",
3236
"BatchTransformNotCompleteException",
37+
"ContextGroundingIndexNotFoundError",
3338
"EnrichedException",
3439
"ExtractedErrorInfo",
3540
"FolderNotFoundException",
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from typing import Optional
2+
3+
4+
class ContextGroundingIndexNotFoundError(Exception):
5+
"""Raised when a context grounding index cannot be resolved by name."""
6+
7+
def __init__(self, index_name: Optional[str] = None):
8+
self.index_name = index_name
9+
if index_name:
10+
self.message = f"ContextGroundingIndex '{index_name}' not found"
11+
else:
12+
self.message = "ContextGroundingIndex not found"
13+
super().__init__(self.message)

0 commit comments

Comments
 (0)