Skip to content

Commit 7f5871d

Browse files
committed
feat: context grounding add create index and ingest data
1 parent 222c29f commit 7f5871d

6 files changed

Lines changed: 205 additions & 14 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from datetime import datetime
2+
from typing import List, Optional
3+
4+
from pydantic import BaseModel, ConfigDict, Field
5+
6+
7+
class DataSource(BaseModel):
8+
model_config = ConfigDict(
9+
validate_by_name=True,
10+
validate_by_alias=True,
11+
use_enum_values=True,
12+
arbitrary_types_allowed=True,
13+
extra="allow",
14+
json_encoders={datetime: lambda v: v.isoformat() if v else None},
15+
)
16+
id: Optional[str] = Field(default=None, alias="id")
17+
folder: Optional[str] = Field(default=None, alias="folder")
18+
19+
class EcsIndex(BaseModel):
20+
model_config = ConfigDict(
21+
validate_by_name=True,
22+
validate_by_alias=True,
23+
use_enum_values=True,
24+
arbitrary_types_allowed=True,
25+
extra="allow",
26+
json_encoders={datetime: lambda v: v.isoformat() if v else None},
27+
)
28+
id: Optional[str] = Field(default=None, alias="id")
29+
name: Optional[str] = Field(default=None, alias="name")
30+
description: Optional[str] = Field(default=None, alias="description")
31+
memory_usage: Optional[int] = Field(default=None, alias="memoryUsage")
32+
disk_usage: Optional[int] = Field(default=None, alias="diskUsage")
33+
data_source: Optional[DataSource] = Field(default=None, alias="dataSource")
34+
pre_processing: Optional[dict] = Field(default=None, alias="preProcessing")
35+
fields: Optional[List[Field]] = Field(default=None, alias="fields")
36+
last_ingestion_status: Optional[str] = Field(default=None, alias="lastIngestionStatus")
37+
last_ingested: Optional[datetime] = Field(default=None, alias="lastIngested")
38+
last_queried: Optional[datetime] = Field(default=None, alias="lastQueried")
39+
folder_key: Optional[str] = Field(default=None, alias="folderKey")

sdk/core/uipath_sdk/_services/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from .llm_gateway_service import UiPathLlmChatService, UiPathOpenAIService
99
from .processes_service import ProcessesService
1010
from .queues_service import QueuesService
11+
from .folder_service import FolderService
1112

1213
__all__ = [
1314
"ActionsService",
@@ -21,4 +22,5 @@
2122
"JobsService",
2223
"UiPathOpenAIService",
2324
"UiPathLlmChatService",
25+
"FolderService",
2426
]

sdk/core/uipath_sdk/_services/context_grounding_service.py

Lines changed: 114 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import json
2-
from typing import Any, Dict, List
2+
from typing import Any, Dict, List, Optional
3+
from urllib.request import Request
34

45
from pydantic import TypeAdapter
56

67
from .._config import Config
78
from .._execution_context import ExecutionContext
89
from .._folder_context import FolderContext
910
from .._models.context_grounding import ContextGroundingQueryResponse
11+
from .._models.ecs_index import EcsIndex
1012
from .._utils import Endpoint, RequestSpec
13+
from .._utils.constants import ORCHESTRATOR_STORAGE_BUCKET_DATA_SOURCE, ENV_FOLDER_PATH, HEADER_FOLDER_KEY
1114
from ._base_service import BaseService
12-
13-
15+
from .folder_service import FolderService
1416
class ContextGroundingService(FolderContext, BaseService):
1517
"""Service for managing semantic automation contexts in UiPath.
1618
@@ -24,10 +26,11 @@ class ContextGroundingService(FolderContext, BaseService):
2426
context.
2527
"""
2628

27-
def __init__(self, config: Config, execution_context: ExecutionContext) -> None:
29+
def __init__(self, config: Config, execution_context: ExecutionContext, folders_service: FolderService) -> None:
30+
self._folders_service = folders_service
2831
super().__init__(config=config, execution_context=execution_context)
2932

30-
def retrieve(self, name: str) -> Any:
33+
def retrieve(self, name: str) -> Optional[EcsIndex]:
3134
"""Retrieve context grounding index information by its name.
3235
3336
This method fetches details about a specific context index, which can be
@@ -38,17 +41,18 @@ def retrieve(self, name: str) -> Any:
3841
name (str): The name of the context index to retrieve.
3942
4043
Returns:
41-
Any: The index information, including its configuration and metadata.
44+
Optional[EcsIndex]: The index information, including its configuration and metadata if found, otherwise None.
4245
"""
4346
spec = self._retrieve_spec(name)
4447

45-
return self.request(
48+
response = self.request(
4649
spec.method,
4750
spec.endpoint,
4851
params=spec.params,
4952
).json()
53+
return next((EcsIndex.model_validate(item) for item in response["value"] if item["name"] == name), None)
5054

51-
async def retrieve_async(self, name: str) -> Any:
55+
async def retrieve_async(self, name: str) -> Optional[EcsIndex]:
5256
"""Retrieve asynchronously context grounding index information by its name.
5357
5458
This method fetches details about a specific context index, which can be
@@ -59,18 +63,17 @@ async def retrieve_async(self, name: str) -> Any:
5963
name (str): The name of the context index to retrieve.
6064
6165
Returns:
62-
Any: The index information, including its configuration and metadata.
66+
Optional[EcsIndex]: The index information, including its configuration and metadata if found, otherwise None.
6367
6468
"""
6569
spec = self._retrieve_spec(name)
6670

67-
response = await self.request_async(
71+
response = (await self.request_async(
6872
spec.method,
6973
spec.endpoint,
7074
params=spec.params,
71-
)
72-
73-
return response.json()
75+
)).json()
76+
return next((EcsIndex.model_validate(item) for item in response["value"] if item["name"] == name), None)
7477

7578
def retrieve_by_id(self, id: str) -> Any:
7679
"""Retrieve context grounding index information by its ID.
@@ -183,19 +186,117 @@ async def search_async(
183186
response.json()
184187
)
185188

189+
def get_or_create_index(self,
190+
name: str,
191+
*,
192+
description: Optional[str],
193+
storage_bucket_name: Optional[str],
194+
file_name_glob: Optional[str],
195+
storage_bucket_folder_path: Optional[str],
196+
) -> EcsIndex:
197+
spec = self._create_or_get_spec(name, description, storage_bucket_name, file_name_glob, storage_bucket_folder_path)
198+
index = self.retrieve(name=name)
199+
if index:
200+
return index
201+
202+
response = self.request(
203+
spec.method,
204+
spec.endpoint,
205+
content=spec.content,
206+
headers=spec.headers,
207+
).json()
208+
return EcsIndex.model_validate(response)
209+
210+
async def get_or_create_index_async(self,
211+
name: str,
212+
*,
213+
description: Optional[str],
214+
storage_bucket_name: Optional[str],
215+
file_name_glob: Optional[str],
216+
storage_bucket_folder_path: Optional[str],
217+
) -> EcsIndex:
218+
spec = self._create_or_get_spec(name, description, storage_bucket_name, file_name_glob, storage_bucket_folder_path)
219+
index = await self.retrieve_async(name=name)
220+
if index:
221+
return index
222+
223+
response = (await self.request_async(
224+
spec.method,
225+
spec.endpoint,
226+
content=spec.content,
227+
headers=spec.headers,
228+
)).json()
229+
return EcsIndex.model_validate(response)
230+
231+
def ingest_data(self, index: EcsIndex) -> None:
232+
spec = self._ingest_spec(index.id)
233+
self.request(
234+
spec.method,
235+
spec.endpoint,
236+
headers=spec.headers,
237+
)
238+
239+
async def ingest_data(self, index: EcsIndex) -> None:
240+
spec = self._ingest_spec(index.id)
241+
await self.request_async(
242+
spec.method,
243+
spec.endpoint,
244+
headers=spec.headers,
245+
)
246+
186247
@property
187248
def custom_headers(self) -> Dict[str, str]:
188249
if self.folder_headers["x-uipath-folderkey"] is None:
189250
raise ValueError("Folder key is not set (UIPATH_FOLDER_KEY)")
190251

191252
return self.folder_headers
192253

254+
def _ingest_spec(self, key: str) -> RequestSpec:
255+
if not self._folder_path:
256+
raise ValueError(f"Folder path is not set ({ENV_FOLDER_PATH})")
257+
folder_key = self._folder_key if self._folder_key else self._folders_service.retrieve_key_by_folder_path(self._folder_path)
258+
259+
return RequestSpec(
260+
method="POST",
261+
endpoint=Endpoint(f"/ecs_/v2/indexes/{key}/ingest"),
262+
headers={HEADER_FOLDER_KEY: folder_key}
263+
)
264+
193265
def _retrieve_spec(self, name: str) -> RequestSpec:
194266
return RequestSpec(
195267
method="GET",
196268
endpoint=Endpoint("/ecs_/v2/indexes"),
197269
params={"$filter": f"Name eq '{name}'"},
198270
)
271+
def _create_or_get_spec(self,
272+
name: str,
273+
description: Optional[str],
274+
storage_bucket_name: Optional[str],
275+
file_name_glob: Optional[str],
276+
storage_bucket_folder_path: Optional[str],
277+
) -> RequestSpec:
278+
if not self._folder_path:
279+
raise ValueError(f"Folder path is not set ({ENV_FOLDER_PATH})")
280+
storage_bucket_folder_path = storage_bucket_folder_path if storage_bucket_folder_path else self._folder_path
281+
folder_key = self._folder_key if self._folder_key else self._folders_service.retrieve_key_by_folder_path(self._folder_path)
282+
return RequestSpec(
283+
method="POST",
284+
endpoint=Endpoint("/ecs_/v2/indexes/create"),
285+
content=json.dumps(
286+
{
287+
"name": name,
288+
"description": description,
289+
"dataSource":
290+
{
291+
"@odata.type": ORCHESTRATOR_STORAGE_BUCKET_DATA_SOURCE,
292+
"folder": storage_bucket_folder_path,
293+
"bucketName": storage_bucket_name,
294+
"fileNameGlob": file_name_glob if file_name_glob is not None else "*",
295+
}
296+
}
297+
),
298+
headers={HEADER_FOLDER_KEY: folder_key},
299+
)
199300

200301
def _retrieve_by_id_spec(self, id: str) -> RequestSpec:
201302
return RequestSpec(
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from typing import Optional
2+
3+
from .._config import Config
4+
from .._execution_context import ExecutionContext
5+
from .._utils import Endpoint, RequestSpec, header_folder
6+
from ._base_service import BaseService
7+
8+
9+
def _retrieve_spec(folder_path: str) -> RequestSpec:
10+
folder_name = folder_path.split("/")[-1]
11+
return RequestSpec(
12+
method="GET",
13+
endpoint=Endpoint(f"orchestrator_/api/FoldersNavigation/GetFoldersForCurrentUser?searchText={folder_name}")
14+
)
15+
16+
17+
class FolderService(BaseService):
18+
"""Service for managing UiPath Folders.
19+
20+
A folder represents a single area for data organization
21+
and access control - it is created when you need to categorize, manage, and enforce authorization rules for a group
22+
of UiPath resources (i.e. processes, assets, connections, storage buckets etc.) or other folders
23+
"""
24+
25+
def __init__(self, config: Config, execution_context: ExecutionContext) -> None:
26+
super().__init__(config=config, execution_context=execution_context)
27+
28+
29+
def retrieve_key_by_folder_path(self, folder_path: str) -> Optional[str]:
30+
spec = _retrieve_spec(folder_path)
31+
response = self.request(
32+
spec.method,
33+
url=spec.endpoint
34+
).json()
35+
36+
return next((item['Key'] for item in response['PageItems'] if item['FullyQualifiedName'] == folder_path), None)

sdk/core/uipath_sdk/_uipath_sdk.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
JobsService,
1616
ProcessesService,
1717
QueuesService,
18+
FolderService
1819
)
1920
from ._utils import setup_logging
2021
from ._utils.constants import (
@@ -45,6 +46,7 @@ def __init__(
4546
base_url=base_url_value, # type: ignore
4647
secret=secret_value, # type: ignore
4748
)
49+
self._folders_service = None
4850

4951
setup_logging(debug)
5052
self._execution_context = ExecutionContext()
@@ -75,7 +77,9 @@ def connections(self) -> ConnectionsService:
7577

7678
@property
7779
def context_grounding(self) -> ContextGroundingService:
78-
return ContextGroundingService(self._config, self._execution_context)
80+
if not self._folders_service:
81+
self._folders_service = FolderService(self._config, self._execution_context)
82+
return ContextGroundingService(self._config, self._execution_context, self._folders_service)
7983

8084
@property
8185
def queues(self) -> QueuesService:
@@ -84,3 +88,9 @@ def queues(self) -> QueuesService:
8488
@property
8589
def jobs(self) -> JobsService:
8690
return JobsService(self._config, self._execution_context)
91+
92+
@property
93+
def folders(self) -> FolderService:
94+
if not self._folders_service:
95+
self._folders_service = FolderService(self._config, self._execution_context)
96+
return self._folders_service

sdk/core/uipath_sdk/_utils/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,6 @@
1818

1919
# Entrypoint for plugins
2020
ENTRYPOINT = "uipath_sdk.connectors"
21+
22+
# Data sources
23+
ORCHESTRATOR_STORAGE_BUCKET_DATA_SOURCE="#UiPath.Vdbs.Domain.Api.V20Models.StorageBucketDataSourceRequest"

0 commit comments

Comments
 (0)