Skip to content

Commit 5b386a3

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

File tree

6 files changed

+212
-16
lines changed

6 files changed

+212
-16
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from datetime import datetime
2+
from typing import List, Optional, Any
3+
4+
from pydantic import BaseModel, ConfigDict, Field
5+
6+
class FieldModel(BaseModel):
7+
id: Optional[str] = Field(default=None, alias="id")
8+
name: Optional[str] = Field(default=None, alias="name")
9+
description: Optional[str] = Field(default=None, alias="description")
10+
type: Optional[str] = Field(default=None, alias="type")
11+
is_filterable: Optional[bool] = Field(default=None, alias="isFilterable")
12+
searchable_type: Optional[str] = Field(default=None, alias="searchableType")
13+
is_user_defined: Optional[bool] = Field(default=None, alias="isUserDefined")
14+
15+
16+
class DataSource(BaseModel):
17+
model_config = ConfigDict(
18+
validate_by_name=True,
19+
validate_by_alias=True,
20+
use_enum_values=True,
21+
arbitrary_types_allowed=True,
22+
extra="allow",
23+
json_encoders={datetime: lambda v: v.isoformat() if v else None},
24+
)
25+
id: Optional[str] = Field(default=None, alias="id")
26+
folder: Optional[str] = Field(default=None, alias="folder")
27+
28+
class EcsIndex(BaseModel):
29+
model_config = ConfigDict(
30+
validate_by_name=True,
31+
validate_by_alias=True,
32+
use_enum_values=True,
33+
arbitrary_types_allowed=True,
34+
extra="allow",
35+
json_encoders={datetime: lambda v: v.isoformat() if v else None},
36+
)
37+
id: Optional[str] = Field(default=None, alias="id")
38+
name: Optional[str] = Field(default=None, alias="name")
39+
description: Optional[str] = Field(default=None, alias="description")
40+
memory_usage: Optional[int] = Field(default=None, alias="memoryUsage")
41+
disk_usage: Optional[int] = Field(default=None, alias="diskUsage")
42+
data_source: Optional[DataSource] = Field(default=None, alias="dataSource")
43+
pre_processing: Any = Field(default=None, alias="preProcessing")
44+
fields: Optional[List[FieldModel]] = Field(default=None, alias="fields")
45+
last_ingestion_status: Optional[str] = Field(default=None, alias="lastIngestionStatus")
46+
last_ingested: Optional[datetime] = Field(default=None, alias="lastIngested")
47+
last_queried: Optional[datetime] = Field(default=None, alias="lastQueried")
48+
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: 107 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import json
2-
from typing import Any, Dict, List
2+
from typing import Any, Dict, List, Optional
33

44
from pydantic import TypeAdapter
55

66
from .._config import Config
77
from .._execution_context import ExecutionContext
88
from .._folder_context import FolderContext
99
from .._models.context_grounding import ContextGroundingQueryResponse
10+
from .._models.ecs_index import EcsIndex
1011
from .._utils import Endpoint, RequestSpec
12+
from .._utils.constants import ORCHESTRATOR_STORAGE_BUCKET_DATA_SOURCE, HEADER_FOLDER_KEY
1113
from ._base_service import BaseService
12-
13-
14+
from .folder_service import FolderService
1415
class ContextGroundingService(FolderContext, BaseService):
1516
"""Service for managing semantic automation contexts in UiPath.
1617
@@ -24,10 +25,11 @@ class ContextGroundingService(FolderContext, BaseService):
2425
context.
2526
"""
2627

27-
def __init__(self, config: Config, execution_context: ExecutionContext) -> None:
28+
def __init__(self, config: Config, execution_context: ExecutionContext, folders_service: FolderService) -> None:
29+
self._folders_service = folders_service
2830
super().__init__(config=config, execution_context=execution_context)
2931

30-
def retrieve(self, name: str) -> Any:
32+
def retrieve(self, name: str) -> Optional[EcsIndex]:
3133
"""Retrieve context grounding index information by its name.
3234
3335
This method fetches details about a specific context index, which can be
@@ -38,17 +40,18 @@ def retrieve(self, name: str) -> Any:
3840
name (str): The name of the context index to retrieve.
3941
4042
Returns:
41-
Any: The index information, including its configuration and metadata.
43+
Optional[EcsIndex]: The index information, including its configuration and metadata if found, otherwise None.
4244
"""
4345
spec = self._retrieve_spec(name)
4446

45-
return self.request(
47+
response = self.request(
4648
spec.method,
4749
spec.endpoint,
4850
params=spec.params,
4951
).json()
52+
return next((EcsIndex.model_validate(item) for item in response["value"] if item["name"] == name), None)
5053

51-
async def retrieve_async(self, name: str) -> Any:
54+
async def retrieve_async(self, name: str) -> Optional[EcsIndex]:
5255
"""Retrieve asynchronously context grounding index information by its name.
5356
5457
This method fetches details about a specific context index, which can be
@@ -59,18 +62,17 @@ async def retrieve_async(self, name: str) -> Any:
5962
name (str): The name of the context index to retrieve.
6063
6164
Returns:
62-
Any: The index information, including its configuration and metadata.
65+
Optional[EcsIndex]: The index information, including its configuration and metadata if found, otherwise None.
6366
6467
"""
6568
spec = self._retrieve_spec(name)
6669

67-
response = await self.request_async(
70+
response = (await self.request_async(
6871
spec.method,
6972
spec.endpoint,
7073
params=spec.params,
71-
)
72-
73-
return response.json()
74+
)).json()
75+
return next((EcsIndex.model_validate(item) for item in response["value"] if item["name"] == name), None)
7476

7577
def retrieve_by_id(self, id: str) -> Any:
7678
"""Retrieve context grounding index information by its ID.
@@ -183,19 +185,109 @@ async def search_async(
183185
response.json()
184186
)
185187

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

191252
return self.folder_headers
192253

254+
def _ingest_spec(self, key: str) -> RequestSpec:
255+
return RequestSpec(
256+
method="POST",
257+
endpoint=Endpoint(f"/ecs_/v2/indexes/{key}/ingest")
258+
)
259+
193260
def _retrieve_spec(self, name: str) -> RequestSpec:
194261
return RequestSpec(
195262
method="GET",
196263
endpoint=Endpoint("/ecs_/v2/indexes"),
197264
params={"$filter": f"Name eq '{name}'"},
198265
)
266+
def _create_spec(self,
267+
name: str,
268+
description: Optional[str],
269+
storage_bucket_name: Optional[str],
270+
file_name_glob: Optional[str],
271+
storage_bucket_folder_path: Optional[str],
272+
) -> RequestSpec:
273+
storage_bucket_folder_path = storage_bucket_folder_path if storage_bucket_folder_path else self._folder_path
274+
return RequestSpec(
275+
method="POST",
276+
endpoint=Endpoint("/ecs_/v2/indexes/create"),
277+
content=json.dumps(
278+
{
279+
"name": name,
280+
"description": description,
281+
"dataSource":
282+
{
283+
"@odata.type": ORCHESTRATOR_STORAGE_BUCKET_DATA_SOURCE,
284+
"folder": storage_bucket_folder_path,
285+
"bucketName": storage_bucket_name,
286+
"fileNameGlob": file_name_glob if file_name_glob is not None else "*",
287+
}
288+
}
289+
)
290+
)
199291

200292
def _retrieve_by_id_spec(self, id: str) -> RequestSpec:
201293
return RequestSpec(
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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"),
14+
params =
15+
{
16+
"searchText": folder_name,
17+
}
18+
)
19+
20+
21+
class FolderService(BaseService):
22+
"""Service for managing UiPath Folders.
23+
24+
A folder represents a single area for data organization
25+
and access control - it is created when you need to categorize, manage, and enforce authorization rules for a group
26+
of UiPath resources (i.e. processes, assets, connections, storage buckets etc.) or other folders
27+
"""
28+
29+
def __init__(self, config: Config, execution_context: ExecutionContext) -> None:
30+
super().__init__(config=config, execution_context=execution_context)
31+
32+
33+
def retrieve_key_by_folder_path(self, folder_path: str) -> Optional[str]:
34+
spec = _retrieve_spec(folder_path)
35+
response = self.request(
36+
spec.method,
37+
url=spec.endpoint,
38+
params=spec.params,
39+
).json()
40+
41+
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)