Skip to content

Commit 7620775

Browse files
feat(context-grounding): let agents opt in to searching an ingesting index (#1891)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 5b44f9b commit 7620775

11 files changed

Lines changed: 262 additions & 10 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.2.29"
3+
version = "0.2.30"
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: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1669,6 +1669,7 @@ def unified_search(
16691669
folder_key: Optional[str] = None,
16701670
folder_path: Optional[str] = None,
16711671
include_system_indexes: bool = False,
1672+
search_during_ingestion: bool = False,
16721673
) -> UnifiedQueryResult:
16731674
"""Perform a unified search on a context grounding index.
16741675
@@ -1687,16 +1688,26 @@ def unified_search(
16871688
include_system_indexes (bool): If True, fall back to tenant-wide
16881689
system indexes when the index is not found in folder or
16891690
across-folders listings. Defaults to False.
1691+
search_during_ingestion (bool): If True, query the index even while an
1692+
ingestion is in progress, returning results from the documents
1693+
indexed so far. Defaults to False, which raises
1694+
IngestionInProgressException instead.
16901695
16911696
Returns:
16921697
UnifiedQueryResult: The unified search result containing semantic and/or tabular results.
1698+
1699+
Raises:
1700+
IngestionInProgressException: If the index is still ingesting and
1701+
search_during_ingestion is False.
16931702
"""
16941703
index = self.retrieve(
16951704
name,
16961705
folder_key=folder_key,
16971706
folder_path=folder_path,
16981707
include_system_indexes=include_system_indexes,
16991708
)
1709+
if not search_during_ingestion and index and index.in_progress_ingestion():
1710+
raise IngestionInProgressException(index_name=name)
17001711

17011712
folder_key = folder_key or index.folder_key
17021713

@@ -1733,6 +1744,7 @@ async def unified_search_async(
17331744
folder_key: Optional[str] = None,
17341745
folder_path: Optional[str] = None,
17351746
include_system_indexes: bool = False,
1747+
search_during_ingestion: bool = False,
17361748
) -> UnifiedQueryResult:
17371749
"""Asynchronously perform a unified search on a context grounding index.
17381750
@@ -1751,17 +1763,25 @@ async def unified_search_async(
17511763
include_system_indexes (bool): If True, fall back to tenant-wide
17521764
system indexes when the index is not found in folder or
17531765
across-folders listings. Defaults to False.
1766+
search_during_ingestion (bool): If True, query the index even while an
1767+
ingestion is in progress, returning results from the documents
1768+
indexed so far. Defaults to False, which raises
1769+
IngestionInProgressException instead.
17541770
17551771
Returns:
17561772
UnifiedQueryResult: The unified search result containing semantic and/or tabular results.
1773+
1774+
Raises:
1775+
IngestionInProgressException: If the index is still ingesting and
1776+
search_during_ingestion is False.
17571777
"""
17581778
index = await self.retrieve_async(
17591779
name,
17601780
folder_key=folder_key,
17611781
folder_path=folder_path,
17621782
include_system_indexes=include_system_indexes,
17631783
)
1764-
if index and index.in_progress_ingestion():
1784+
if not search_during_ingestion and index and index.in_progress_ingestion():
17651785
raise IngestionInProgressException(index_name=name)
17661786

17671787
folder_key = folder_key or index.folder_key

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

Lines changed: 168 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@
3535
from uipath.platform.context_grounding._context_grounding_service import (
3636
ContextGroundingService,
3737
)
38-
from uipath.platform.errors import ContextGroundingIndexNotFoundError
38+
from uipath.platform.errors import (
39+
ContextGroundingIndexNotFoundError,
40+
IngestionInProgressException,
41+
)
3942
from uipath.platform.orchestrator._buckets_service import BucketsService
4043
from uipath.platform.orchestrator._folder_service import FolderService
4144

@@ -3839,6 +3842,170 @@ async def test_unified_search_async(
38393842
assert response.semantic_results.metadata is not None
38403843
assert len(response.semantic_results.values) == 1
38413844

3845+
def _mock_index_lookup(
3846+
self,
3847+
httpx_mock: HTTPXMock,
3848+
base_url: str,
3849+
org: str,
3850+
tenant: str,
3851+
ingestion_status: str,
3852+
) -> None:
3853+
"""Mock the folder + index lookup that precedes a unified search."""
3854+
for _ in range(2):
3855+
httpx_mock.add_response(
3856+
url=f"{base_url}{org}{tenant}/orchestrator_/api/FoldersNavigation/GetFoldersForCurrentUser?searchText=test-folder-path&skip=0&take=20",
3857+
status_code=200,
3858+
json={
3859+
"PageItems": [
3860+
{
3861+
"Key": "test-folder-key",
3862+
"FullyQualifiedName": "test-folder-path",
3863+
}
3864+
]
3865+
},
3866+
)
3867+
httpx_mock.add_response(
3868+
url=f"{base_url}{org}{tenant}/ecs_/v2/indexes?$filter=Name eq 'test-index'&$expand=dataSource",
3869+
status_code=200,
3870+
json={
3871+
"value": [
3872+
{
3873+
"id": "test-index-id",
3874+
"name": "test-index",
3875+
"lastIngestionStatus": ingestion_status,
3876+
}
3877+
]
3878+
},
3879+
)
3880+
3881+
@pytest.mark.anyio
3882+
@pytest.mark.httpx_mock(assert_all_responses_were_requested=False)
3883+
@pytest.mark.parametrize("ingestion_status", ["Queued", "InProgress"])
3884+
async def test_unified_search_async_blocks_during_ingestion_by_default(
3885+
self,
3886+
httpx_mock: HTTPXMock,
3887+
service: ContextGroundingService,
3888+
base_url: str,
3889+
org: str,
3890+
tenant: str,
3891+
ingestion_status: str,
3892+
) -> None:
3893+
self._mock_index_lookup(httpx_mock, base_url, org, tenant, ingestion_status)
3894+
3895+
with pytest.raises(IngestionInProgressException):
3896+
await service.unified_search_async(name="test-index", query="test query")
3897+
3898+
@pytest.mark.httpx_mock(assert_all_responses_were_requested=False)
3899+
@pytest.mark.parametrize("ingestion_status", ["Queued", "InProgress"])
3900+
def test_unified_search_blocks_during_ingestion_by_default(
3901+
self,
3902+
httpx_mock: HTTPXMock,
3903+
service: ContextGroundingService,
3904+
base_url: str,
3905+
org: str,
3906+
tenant: str,
3907+
ingestion_status: str,
3908+
) -> None:
3909+
self._mock_index_lookup(httpx_mock, base_url, org, tenant, ingestion_status)
3910+
3911+
with pytest.raises(IngestionInProgressException):
3912+
service.unified_search(name="test-index", query="test query")
3913+
3914+
@pytest.mark.parametrize("ingestion_status", ["Queued", "InProgress"])
3915+
def test_unified_search_opt_in_searches_during_ingestion(
3916+
self,
3917+
httpx_mock: HTTPXMock,
3918+
service: ContextGroundingService,
3919+
base_url: str,
3920+
org: str,
3921+
tenant: str,
3922+
ingestion_status: str,
3923+
) -> None:
3924+
self._mock_index_lookup(httpx_mock, base_url, org, tenant, ingestion_status)
3925+
httpx_mock.add_response(
3926+
url=f"{base_url}{org}{tenant}/ecs_/v1.2/search/test-index-id",
3927+
status_code=200,
3928+
json={
3929+
"semanticResults": {
3930+
"metadata": {
3931+
"operation_id": "test-op",
3932+
"strategy": "test-strategy",
3933+
},
3934+
"values": [
3935+
{
3936+
"id": "result-1",
3937+
"source": "test-source",
3938+
"page_number": 1,
3939+
"content": "Partially ingested content",
3940+
"score": 0.95,
3941+
}
3942+
],
3943+
},
3944+
"explanation": "test explanation",
3945+
},
3946+
)
3947+
3948+
response = service.unified_search(
3949+
name="test-index",
3950+
query="test query",
3951+
search_during_ingestion=True,
3952+
)
3953+
3954+
assert isinstance(response, UnifiedQueryResult)
3955+
assert response.semantic_results is not None
3956+
assert len(response.semantic_results.values) == 1
3957+
assert (
3958+
response.semantic_results.values[0].content == "Partially ingested content"
3959+
)
3960+
3961+
@pytest.mark.anyio
3962+
@pytest.mark.parametrize("ingestion_status", ["Queued", "InProgress"])
3963+
async def test_unified_search_async_opt_in_searches_during_ingestion(
3964+
self,
3965+
httpx_mock: HTTPXMock,
3966+
service: ContextGroundingService,
3967+
base_url: str,
3968+
org: str,
3969+
tenant: str,
3970+
ingestion_status: str,
3971+
) -> None:
3972+
self._mock_index_lookup(httpx_mock, base_url, org, tenant, ingestion_status)
3973+
httpx_mock.add_response(
3974+
url=f"{base_url}{org}{tenant}/ecs_/v1.2/search/test-index-id",
3975+
status_code=200,
3976+
json={
3977+
"semanticResults": {
3978+
"metadata": {
3979+
"operation_id": "test-op",
3980+
"strategy": "test-strategy",
3981+
},
3982+
"values": [
3983+
{
3984+
"id": "result-1",
3985+
"source": "test-source",
3986+
"page_number": 1,
3987+
"content": "Partially ingested content",
3988+
"score": 0.95,
3989+
}
3990+
],
3991+
},
3992+
"explanation": "test explanation",
3993+
},
3994+
)
3995+
3996+
response = await service.unified_search_async(
3997+
name="test-index",
3998+
query="test query",
3999+
search_during_ingestion=True,
4000+
)
4001+
4002+
assert isinstance(response, UnifiedQueryResult)
4003+
assert response.semantic_results is not None
4004+
assert len(response.semantic_results.values) == 1
4005+
assert (
4006+
response.semantic_results.values[0].content == "Partially ingested content"
4007+
)
4008+
38424009
def test_unified_search_with_scope(
38434010
self,
38444011
httpx_mock: HTTPXMock,

‎packages/uipath-platform/uv.lock‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/uipath/pyproject.toml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
[project]
22
name = "uipath"
3-
version = "2.14.18"
3+
version = "2.14.19"
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"
77
dependencies = [
88
"uipath-core>=0.5.30, <0.6.0",
99
"uipath-runtime>=0.13.5, <0.14.0",
10-
"uipath-platform>=0.2.29, <0.3.0",
10+
"uipath-platform>=0.2.30, <0.3.0",
1111
"click>=8.3.3, <9.0.0",
1212
"httpx>=0.28.1",
1313
"pyjwt>=2.13.0, <3.0.0",

‎packages/uipath/src/uipath/_cli/services/cli_context_grounding.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,12 @@ def ingest_index(
464464
default="Semantic",
465465
help="Search mode (default: Semantic)",
466466
)
467+
@click.option(
468+
"--search-during-ingestion",
469+
is_flag=True,
470+
default=False,
471+
help="Search the documents indexed so far even while ingestion is in progress",
472+
)
467473
@common_service_options
468474
@service_command
469475
def search_index(
@@ -473,6 +479,7 @@ def search_index(
473479
limit: int,
474480
threshold: float,
475481
search_mode: str,
482+
search_during_ingestion: bool,
476483
folder_path: Optional[str],
477484
folder_key: Optional[str],
478485
format: Optional[str],
@@ -494,6 +501,7 @@ def search_index(
494501
number_of_results=limit,
495502
threshold=threshold,
496503
search_mode=SearchMode(search_mode),
504+
search_during_ingestion=search_during_ingestion,
497505
folder_path=folder_path,
498506
folder_key=folder_key,
499507
)

‎packages/uipath/src/uipath/_resources/CLI_REFERENCE.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -924,6 +924,7 @@ Options:
924924
- `--limit`: Maximum number of results (default: 10) (default: `10`)
925925
- `--threshold`: Minimum similarity threshold (default: 0.0) (default: `0.0`)
926926
- `--search-mode`: Search mode (default: Semantic) (default: `Semantic`)
927+
- `--search-during-ingestion`: Search the documents indexed so far even while ingestion is in progress
927928
- `--folder-path`: Folder path (e.g., "Shared"). Can also be set via UIPATH_FOLDER_PATH environment variable. (default: `Sentinel.UNSET`)
928929
- `--folder-key`: Folder key (UUID) (default: `Sentinel.UNSET`)
929930
- `--format`: Output format (overrides global) (default: `Sentinel.UNSET`)

‎packages/uipath/src/uipath/_resources/SDK_REFERENCE.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -405,10 +405,10 @@ sdk.context_grounding.start_deep_rag_ephemeral(name: str, prompt: Annotated[str,
405405
sdk.context_grounding.start_deep_rag_ephemeral_async(name: str, prompt: Annotated[str, FieldInfo(annotation=NoneType, required=True, metadata=[MaxLen(max_length=250000)])], glob_pattern: Annotated[str, FieldInfo(annotation=NoneType, required=False, default='*', metadata=[MaxLen(max_length=512)])]="**", citation_mode: <enum 'CitationMode="CitationMode.SKIP", index_id: Optional[Annotated[str, FieldInfo(annotation=NoneType, required=True, metadata=[MaxLen(max_length=512)])]]=None) -> uipath.platform.context_grounding.context_grounding.DeepRagCreationResponse
406406

407407
# Perform a unified search on a context grounding index.
408-
sdk.context_grounding.unified_search(name: str, query: str, search_mode: <enum 'SearchMode="SearchMode.SEMANTIC", number_of_results: int=10, threshold: float=0.0, scope: Optional[uipath.platform.context_grounding.context_grounding.UnifiedSearchScope]=None, folder_key: Optional[str]=None, folder_path: Optional[str]=None, include_system_indexes: bool=False) -> uipath.platform.context_grounding.context_grounding.UnifiedQueryResult
408+
sdk.context_grounding.unified_search(name: str, query: str, search_mode: <enum 'SearchMode="SearchMode.SEMANTIC", number_of_results: int=10, threshold: float=0.0, scope: Optional[uipath.platform.context_grounding.context_grounding.UnifiedSearchScope]=None, folder_key: Optional[str]=None, folder_path: Optional[str]=None, include_system_indexes: bool=False, search_during_ingestion: bool=False) -> uipath.platform.context_grounding.context_grounding.UnifiedQueryResult
409409

410410
# Asynchronously perform a unified search on a context grounding index.
411-
sdk.context_grounding.unified_search_async(name: str, query: str, search_mode: <enum 'SearchMode="SearchMode.SEMANTIC", number_of_results: int=10, threshold: float=0.0, scope: Optional[uipath.platform.context_grounding.context_grounding.UnifiedSearchScope]=None, folder_key: Optional[str]=None, folder_path: Optional[str]=None, include_system_indexes: bool=False) -> uipath.platform.context_grounding.context_grounding.UnifiedQueryResult
411+
sdk.context_grounding.unified_search_async(name: str, query: str, search_mode: <enum 'SearchMode="SearchMode.SEMANTIC", number_of_results: int=10, threshold: float=0.0, scope: Optional[uipath.platform.context_grounding.context_grounding.UnifiedSearchScope]=None, folder_key: Optional[str]=None, folder_path: Optional[str]=None, include_system_indexes: bool=False, search_during_ingestion: bool=False) -> uipath.platform.context_grounding.context_grounding.UnifiedQueryResult
412412

413413
```
414414

‎packages/uipath/src/uipath/agent/models/agent.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,15 @@ class AgentContextSettings(BaseCfg):
428428
output_columns: Optional[List[AgentContextOutputColumn]] = Field(
429429
None, alias="outputColumns"
430430
)
431+
search_during_ingestion: bool = Field(
432+
default=False,
433+
alias="searchDuringIngestion",
434+
description=(
435+
"Allow the agent to query this index while an ingestion is still in "
436+
"progress, returning results from the documents indexed so far. When "
437+
"False, a search against an ingesting index fails instead."
438+
),
439+
)
431440

432441

433442
class AgentContextResourceConfig(BaseAgentResourceConfig):

0 commit comments

Comments
 (0)