Skip to content

Commit 13ddb2f

Browse files
committed
feat: add uipath context-grounding CLI commands
Adds a new command group for working with Context Grounding (ECS) indexes directly from the terminal — the same semantic search agents use at runtime. Commands -------- uipath context-grounding list --folder-path PATH uipath context-grounding retrieve --index NAME --folder-path PATH uipath context-grounding search QUERY --index NAME --folder-path PATH uipath context-grounding ingest --index NAME --folder-path PATH uipath context-grounding delete --index NAME --folder-path PATH All commands accept --folder-path / --folder-key, --format [json|table|csv], and -o FILE, consistent with `uipath buckets` and `uipath assets`. search also accepts --limit (min 1, default 10) and --threshold (optional). Service changes (uipath-platform) ---------------------------------- - Add list() / list_async() to ContextGroundingService (GET /ecs_/v2/indexes without $filter; retrieve() uses the same endpoint with Name eq filter) - Replace bare list[T] annotations in the service with List[T] from typing to fix a class-scope shadowing issue: naming a method `list` causes Python to resolve the built-in as the method when evaluating later annotations - list and search table output projects to key columns; full objects for JSON/CSV CLI changes (uipath) -------------------- - Register `context-grounding` in _LAZY_COMMANDS with hyphen->underscore fix for getattr (context-grounding -> context_grounding) - --index named option on all commands that take an index name - --limit (min 1, default 10) and --threshold (optional float) for search - ingest and delete guard against index.id=None to avoid a silent SDK no-op - ingest fast-fails via index.in_progress_ingestion() before the HTTP call - Error handling mirrors cli_buckets.py: HTTPStatusError 404 and SDK bare Exception route through handle_not_found_error() - _handle_retrieve_error() typed -> NoReturn for correct control-flow inference - All exceptions from delete_index()/ingest_data() surface as ClickException Files ----- packages/uipath-platform/.../context_grounding/_context_grounding_service.py packages/uipath-platform/tests/services/test_context_grounding_service.py packages/uipath/src/uipath/_cli/__init__.py packages/uipath/src/uipath/_cli/services/__init__.py packages/uipath/src/uipath/_cli/services/cli_context_grounding.py (new) packages/uipath/tests/cli/contract/test_sdk_cli_alignment.py packages/uipath/tests/cli/integration/test_context_grounding_commands.py (new) Verified against alpha.uipath.com / goldenagents / DefaultTenant.
1 parent b9bcaab commit 13ddb2f

11 files changed

Lines changed: 1594 additions & 32 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.13"
3+
version = "0.1.14"
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: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,62 @@ async def retrieve_async(
361361
except StopIteration as e:
362362
raise Exception("ContextGroundingIndex not found") from e
363363

364+
@traced(name="contextgrounding_list", run_type="uipath")
365+
def list(
366+
self,
367+
folder_key: Optional[str] = None,
368+
folder_path: Optional[str] = None,
369+
) -> List[ContextGroundingIndex]:
370+
"""List all context grounding indexes in a folder.
371+
372+
Args:
373+
folder_key (Optional[str]): The key of the folder to list indexes from.
374+
folder_path (Optional[str]): The path of the folder to list indexes from.
375+
376+
Returns:
377+
List[ContextGroundingIndex]: All indexes in the folder.
378+
"""
379+
folder_key = self._resolve_folder_key(folder_key, folder_path)
380+
response = self.request(
381+
"GET",
382+
Endpoint("/ecs_/v2/indexes"),
383+
params={"$expand": "dataSource"},
384+
headers={**header_folder(folder_key, None)},
385+
).json()
386+
return [
387+
ContextGroundingIndex.model_validate(item)
388+
for item in response.get("value", [])
389+
]
390+
391+
@traced(name="contextgrounding_list", run_type="uipath")
392+
async def list_async(
393+
self,
394+
folder_key: Optional[str] = None,
395+
folder_path: Optional[str] = None,
396+
) -> List[ContextGroundingIndex]:
397+
"""Asynchronously list all context grounding indexes in a folder.
398+
399+
Args:
400+
folder_key (Optional[str]): The key of the folder to list indexes from.
401+
folder_path (Optional[str]): The path of the folder to list indexes from.
402+
403+
Returns:
404+
List[ContextGroundingIndex]: All indexes in the folder.
405+
"""
406+
folder_key = self._resolve_folder_key(folder_key, folder_path)
407+
response = (
408+
await self.request_async(
409+
"GET",
410+
Endpoint("/ecs_/v2/indexes"),
411+
params={"$expand": "dataSource"},
412+
headers={**header_folder(folder_key, None)},
413+
)
414+
).json()
415+
return [
416+
ContextGroundingIndex.model_validate(item)
417+
for item in response.get("value", [])
418+
]
419+
364420
@traced(name="contextgrounding_retrieve_by_id", run_type="uipath")
365421
def retrieve_by_id(
366422
self,
@@ -542,7 +598,7 @@ async def create_index_async(
542598
@resource_override(resource_type="index")
543599
@traced(name="contextgrounding_create_ephemeral_index", run_type="uipath")
544600
def create_ephemeral_index(
545-
self, usage: EphemeralIndexUsage, attachments: list[str]
601+
self, usage: EphemeralIndexUsage, attachments: List[str]
546602
) -> ContextGroundingIndex:
547603
"""Create a new ephemeral context grounding index.
548604
@@ -570,7 +626,7 @@ def create_ephemeral_index(
570626
@resource_override(resource_type="index")
571627
@traced(name="contextgrounding_create_ephemeral_index", run_type="uipath")
572628
async def create_ephemeral_index_async(
573-
self, usage: EphemeralIndexUsage, attachments: list[str]
629+
self, usage: EphemeralIndexUsage, attachments: List[str]
574630
) -> ContextGroundingIndex:
575631
"""Create a new ephemeral context grounding index.
576632
@@ -661,7 +717,7 @@ def start_batch_transform(
661717
self,
662718
name: str,
663719
prompt: Annotated[str, Field(max_length=250000)],
664-
output_columns: list[BatchTransformOutputColumn],
720+
output_columns: List[BatchTransformOutputColumn],
665721
storage_bucket_folder_path_prefix: Annotated[
666722
str | None, Field(max_length=512)
667723
] = None,
@@ -737,7 +793,7 @@ async def start_batch_transform_async(
737793
self,
738794
name: str,
739795
prompt: Annotated[str, Field(max_length=250000)],
740-
output_columns: list[BatchTransformOutputColumn],
796+
output_columns: List[BatchTransformOutputColumn],
741797
storage_bucket_folder_path_prefix: Annotated[
742798
str | None, Field(max_length=512)
743799
] = None,
@@ -813,7 +869,7 @@ async def start_batch_transform_ephemeral(
813869
self,
814870
name: str,
815871
prompt: Annotated[str, Field(max_length=250000)],
816-
output_columns: list[BatchTransformOutputColumn],
872+
output_columns: List[BatchTransformOutputColumn],
817873
storage_bucket_folder_path_prefix: Annotated[
818874
str | None, Field(max_length=512)
819875
] = None,
@@ -859,7 +915,7 @@ async def start_batch_transform_ephemeral_async(
859915
self,
860916
name: str,
861917
prompt: Annotated[str, Field(max_length=250000)],
862-
output_columns: list[BatchTransformOutputColumn],
918+
output_columns: List[BatchTransformOutputColumn],
863919
storage_bucket_folder_path_prefix: Annotated[
864920
str | None, Field(max_length=512)
865921
] = None,
@@ -1741,7 +1797,7 @@ def _create_spec(
17411797
def _create_ephemeral_spec(
17421798
self,
17431799
usage: str,
1744-
attachments: list[str],
1800+
attachments: List[str],
17451801
) -> RequestSpec:
17461802
"""Create request spec for ephemeral index creation.
17471803
@@ -1834,7 +1890,7 @@ def _build_data_source(self, source: SourceConfig) -> Dict[str, Any]:
18341890

18351891
return data_source.model_dump(by_alias=True, exclude_none=True)
18361892

1837-
def _build_ephemeral_data_source(self, attachments: list[str]) -> Dict[str, Any]:
1893+
def _build_ephemeral_data_source(self, attachments: List[str]) -> Dict[str, Any]:
18381894
"""Build data source configuration from typed source config.
18391895
18401896
Args:
@@ -2002,7 +2058,7 @@ def _batch_transform_creation_spec(
20022058
index_id: str,
20032059
name: str,
20042060
enable_web_search_grounding: bool,
2005-
output_columns: list[BatchTransformOutputColumn],
2061+
output_columns: List[BatchTransformOutputColumn],
20062062
storage_bucket_folder_path_prefix: str | None,
20072063
target_file_name: str | None,
20082064
prompt: str,
@@ -2052,7 +2108,7 @@ def _batch_transform_ephemeral_creation_spec(
20522108
index_id: str | None,
20532109
name: str,
20542110
enable_web_search_grounding: bool,
2055-
output_columns: list[BatchTransformOutputColumn],
2111+
output_columns: List[BatchTransformOutputColumn],
20562112
storage_bucket_folder_path_prefix: str | None,
20572113
prompt: str,
20582114
) -> RequestSpec:

packages/uipath-platform/tests/services/test_context_grounding_service.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,153 @@ async def test_retrieve_async(
376376
== f"UiPath.Python.Sdk/UiPath.Python.Sdk.Activities.ContextGroundingService.retrieve_async/{version}"
377377
)
378378

379+
def test_list(
380+
self,
381+
httpx_mock: HTTPXMock,
382+
service: ContextGroundingService,
383+
base_url: str,
384+
org: str,
385+
tenant: str,
386+
version: str,
387+
) -> None:
388+
httpx_mock.add_response(
389+
url=f"{base_url}{org}{tenant}/orchestrator_/api/FoldersNavigation/GetFoldersForCurrentUser?searchText=test-folder-path&skip=0&take=20",
390+
status_code=200,
391+
json={
392+
"PageItems": [
393+
{
394+
"Key": "test-folder-key",
395+
"FullyQualifiedName": "test-folder-path",
396+
}
397+
]
398+
},
399+
)
400+
httpx_mock.add_response(
401+
url=f"{base_url}{org}{tenant}/ecs_/v2/indexes?$expand=dataSource",
402+
status_code=200,
403+
json={
404+
"value": [
405+
{
406+
"id": "index-id-1",
407+
"name": "index-one",
408+
"lastIngestionStatus": "Completed",
409+
},
410+
{
411+
"id": "index-id-2",
412+
"name": "index-two",
413+
"lastIngestionStatus": "Queued",
414+
},
415+
]
416+
},
417+
)
418+
419+
indexes = service.list()
420+
421+
assert isinstance(indexes, list)
422+
assert len(indexes) == 2
423+
assert all(isinstance(i, ContextGroundingIndex) for i in indexes)
424+
assert indexes[0].id == "index-id-1"
425+
assert indexes[0].name == "index-one"
426+
assert indexes[1].id == "index-id-2"
427+
assert indexes[1].name == "index-two"
428+
429+
sent_requests = httpx_mock.get_requests()
430+
assert sent_requests[1].method == "GET"
431+
assert (
432+
sent_requests[1].url
433+
== f"{base_url}{org}{tenant}/ecs_/v2/indexes?%24expand=dataSource"
434+
)
435+
assert HEADER_USER_AGENT in sent_requests[1].headers
436+
assert (
437+
sent_requests[1].headers[HEADER_USER_AGENT]
438+
== f"UiPath.Python.Sdk/UiPath.Python.Sdk.Activities.ContextGroundingService.list/{version}"
439+
)
440+
441+
@pytest.mark.anyio
442+
async def test_list_async(
443+
self,
444+
httpx_mock: HTTPXMock,
445+
service: ContextGroundingService,
446+
base_url: str,
447+
org: str,
448+
tenant: str,
449+
version: str,
450+
) -> None:
451+
httpx_mock.add_response(
452+
url=f"{base_url}{org}{tenant}/orchestrator_/api/FoldersNavigation/GetFoldersForCurrentUser?searchText=test-folder-path&skip=0&take=20",
453+
status_code=200,
454+
json={
455+
"PageItems": [
456+
{
457+
"Key": "test-folder-key",
458+
"FullyQualifiedName": "test-folder-path",
459+
}
460+
]
461+
},
462+
)
463+
httpx_mock.add_response(
464+
url=f"{base_url}{org}{tenant}/ecs_/v2/indexes?$expand=dataSource",
465+
status_code=200,
466+
json={
467+
"value": [
468+
{
469+
"id": "index-id-1",
470+
"name": "index-one",
471+
"lastIngestionStatus": "Completed",
472+
},
473+
]
474+
},
475+
)
476+
477+
indexes = await service.list_async()
478+
479+
assert isinstance(indexes, list)
480+
assert len(indexes) == 1
481+
assert isinstance(indexes[0], ContextGroundingIndex)
482+
assert indexes[0].id == "index-id-1"
483+
484+
sent_requests = httpx_mock.get_requests()
485+
assert sent_requests[1].method == "GET"
486+
assert (
487+
sent_requests[1].url
488+
== f"{base_url}{org}{tenant}/ecs_/v2/indexes?%24expand=dataSource"
489+
)
490+
assert HEADER_USER_AGENT in sent_requests[1].headers
491+
assert (
492+
sent_requests[1].headers[HEADER_USER_AGENT]
493+
== f"UiPath.Python.Sdk/UiPath.Python.Sdk.Activities.ContextGroundingService.list_async/{version}"
494+
)
495+
496+
def test_list_empty(
497+
self,
498+
httpx_mock: HTTPXMock,
499+
service: ContextGroundingService,
500+
base_url: str,
501+
org: str,
502+
tenant: str,
503+
) -> None:
504+
httpx_mock.add_response(
505+
url=f"{base_url}{org}{tenant}/orchestrator_/api/FoldersNavigation/GetFoldersForCurrentUser?searchText=test-folder-path&skip=0&take=20",
506+
status_code=200,
507+
json={
508+
"PageItems": [
509+
{
510+
"Key": "test-folder-key",
511+
"FullyQualifiedName": "test-folder-path",
512+
}
513+
]
514+
},
515+
)
516+
httpx_mock.add_response(
517+
url=f"{base_url}{org}{tenant}/ecs_/v2/indexes?$expand=dataSource",
518+
status_code=200,
519+
json={"value": []},
520+
)
521+
522+
indexes = service.list()
523+
524+
assert indexes == []
525+
379526
def test_retrieve_across_folders(
380527
self,
381528
httpx_mock: HTTPXMock,

packages/uipath-platform/uv.lock

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

packages/uipath/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"
3-
version = "2.10.35"
3+
version = "2.10.36"
44
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

packages/uipath/src/uipath/_cli/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"debug": "cli_debug",
4848
"assets": "services.cli_assets",
4949
"buckets": "services.cli_buckets",
50+
"context-grounding": "services.cli_context_grounding",
5051
}
5152

5253
_RUNTIME_COMMANDS = {"init", "dev", "run", "eval", "debug", "server"}
@@ -78,7 +79,10 @@ def _load_command(name: str):
7879

7980
module_name = _LAZY_COMMANDS[name]
8081
mod = __import__(f"uipath._cli.{module_name}", fromlist=[name])
81-
return getattr(mod, name)
82+
# CLI names may use hyphens (e.g. "context-grounding") but Python
83+
# attribute names use underscores; convert before getattr.
84+
attr_name = name.replace("-", "_")
85+
return getattr(mod, attr_name)
8286

8387

8488
def __getattr__(name: str):

packages/uipath/src/uipath/_cli/services/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99

1010
from .cli_assets import assets
1111
from .cli_buckets import buckets
12+
from .cli_context_grounding import context_grounding
1213

13-
__all__ = ["assets", "buckets", "register_service_commands"]
14+
__all__ = ["assets", "buckets", "context_grounding", "register_service_commands"]
1415

1516

1617
def register_service_commands(cli_group):
@@ -31,7 +32,7 @@ def register_service_commands(cli_group):
3132
Industry Precedent:
3233
AWS CLI, Azure CLI, and gcloud all use explicit registration.
3334
"""
34-
services = [assets, buckets]
35+
services = [assets, buckets, context_grounding]
3536

3637
for service in services:
3738
cli_group.add_command(service)

0 commit comments

Comments
 (0)