Skip to content

Commit 1dfd4b6

Browse files
feat: add task recipient for guardrails (#1161)
Co-authored-by: iliescucristian <[email protected]>
1 parent f225043 commit 1dfd4b6

File tree

7 files changed

+131
-13
lines changed

7 files changed

+131
-13
lines changed

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.5.22"
3+
version = "2.5.23"
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"

src/uipath/platform/action_center/_tasks_service.py

Lines changed: 103 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
import os
23
import uuid
34
from typing import Any, Dict, List, Optional, Tuple
@@ -10,9 +11,15 @@
1011
HEADER_TENANT_ID,
1112
)
1213
from ...tracing import traced
13-
from ..common import BaseService, FolderContext, UiPathApiConfig, UiPathExecutionContext
14+
from ..common import (
15+
BaseService,
16+
FolderContext,
17+
UiPathApiConfig,
18+
UiPathConfig,
19+
UiPathExecutionContext,
20+
)
1421
from .task_schema import TaskSchema
15-
from .tasks import Task
22+
from .tasks import Task, TaskRecipient, TaskRecipientType
1623

1724

1825
def _create_spec(
@@ -113,13 +120,94 @@ def _retrieve_action_spec(
113120
)
114121

115122

116-
def _assign_task_spec(task_key: str, assignee: str) -> RequestSpec:
117-
return RequestSpec(
123+
async def _assign_task_spec(
124+
self, task_key: str, assignee: str | None, task_recipient: TaskRecipient | None
125+
) -> RequestSpec:
126+
request_spec = RequestSpec(
118127
method="POST",
119128
endpoint=Endpoint(
120129
"/orchestrator_/odata/Tasks/UiPath.Server.Configuration.OData.AssignTasks"
121130
),
122-
json={"taskAssignments": [{"taskId": task_key, "UserNameOrEmail": assignee}]},
131+
)
132+
if task_recipient:
133+
recipient_value = await _resolve_recipient(self, task_recipient)
134+
if (
135+
task_recipient.type == TaskRecipientType.USER_ID
136+
or task_recipient.type == TaskRecipientType.EMAIL
137+
):
138+
request_spec.json = {
139+
"taskAssignments": [
140+
{
141+
"taskId": task_key,
142+
"assignmentCriteria": "SingleUser",
143+
"userNameOrEmail": recipient_value,
144+
}
145+
]
146+
}
147+
else:
148+
request_spec.json = {
149+
"taskAssignments": [
150+
{
151+
"taskId": task_key,
152+
"assignmentCriteria": "AllUsers",
153+
"assigneeNamesOrEmails": [recipient_value],
154+
}
155+
]
156+
}
157+
elif assignee:
158+
request_spec.json = {
159+
"taskAssignments": [{"taskId": task_key, "UserNameOrEmail": assignee}]
160+
}
161+
return request_spec
162+
163+
164+
async def _resolve_recipient(self, task_recipient: TaskRecipient) -> str:
165+
recipient_value = task_recipient.value
166+
if task_recipient.type == TaskRecipientType.USER_ID:
167+
user_spec = _resolve_user(task_recipient.value)
168+
user_response = await self.request_async(
169+
user_spec.method,
170+
user_spec.endpoint,
171+
json=user_spec.json,
172+
content=user_spec.content,
173+
headers=user_spec.headers,
174+
scoped="org",
175+
)
176+
recipient_value = user_response.json().get("email")
177+
if task_recipient.type == TaskRecipientType.GROUP_ID:
178+
group_spec = _resolve_group(task_recipient.value)
179+
group_response = await self.request_async(
180+
group_spec.method,
181+
group_spec.endpoint,
182+
json=group_spec.json,
183+
content=group_spec.content,
184+
headers=group_spec.headers,
185+
scoped="org",
186+
)
187+
recipient_value = group_response.json().get("displayName")
188+
return recipient_value
189+
190+
191+
def _resolve_user(entity_id: str) -> RequestSpec:
192+
org_id = UiPathConfig.organization_id
193+
return RequestSpec(
194+
method="POST",
195+
endpoint=Endpoint(
196+
"/identity_/api/Directory/Resolve/{org_id}".format(org_id=org_id)
197+
),
198+
json={"entityId": entity_id, "entityType": "User"},
199+
)
200+
201+
202+
def _resolve_group(entity_id: str) -> RequestSpec:
203+
org_id = UiPathConfig.organization_id
204+
return RequestSpec(
205+
method="GET",
206+
endpoint=Endpoint(
207+
"/identity_/api/Group/{org_id}/{entity_id}".format(
208+
org_id=org_id, entity_id=entity_id
209+
)
210+
),
123211
)
124212

125213

@@ -181,6 +269,7 @@ async def create_async(
181269
app_folder_path: Optional[str] = None,
182270
app_folder_key: Optional[str] = None,
183271
assignee: Optional[str] = None,
272+
recipient: Optional[TaskRecipient] = None,
184273
) -> Task:
185274
"""Creates a new action asynchronously.
186275
@@ -226,8 +315,10 @@ async def create_async(
226315
headers=spec.headers,
227316
)
228317
json_response = response.json()
229-
if assignee:
230-
spec = _assign_task_spec(json_response["id"], assignee)
318+
if assignee or recipient:
319+
spec = await _assign_task_spec(
320+
self, json_response["id"], assignee, recipient
321+
)
231322
await self.request_async(
232323
spec.method, spec.endpoint, json=spec.json, content=spec.content
233324
)
@@ -249,6 +340,7 @@ def create(
249340
app_folder_path: Optional[str] = None,
250341
app_folder_key: Optional[str] = None,
251342
assignee: Optional[str] = None,
343+
recipient: Optional[TaskRecipient] = None,
252344
) -> Task:
253345
"""Creates a new task synchronously.
254346
@@ -294,8 +386,10 @@ def create(
294386
headers=spec.headers,
295387
)
296388
json_response = response.json()
297-
if assignee:
298-
spec = _assign_task_spec(json_response["id"], assignee)
389+
if assignee or recipient:
390+
spec = asyncio.run(
391+
_assign_task_spec(self, json_response["id"], assignee, recipient)
392+
)
299393
self.request(
300394
spec.method, spec.endpoint, json=spec.json, content=spec.content
301395
)

src/uipath/platform/action_center/tasks.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import enum
44
from datetime import datetime
5-
from typing import Any, Dict, List, Optional, Union
5+
from typing import Any, Dict, List, Literal, Optional, Union
66

77
from pydantic import BaseModel, ConfigDict, Field, field_serializer
88

@@ -15,6 +15,27 @@ class TaskStatus(enum.IntEnum):
1515
COMPLETED = 2
1616

1717

18+
class TaskRecipientType(str, enum.Enum):
19+
"""Task recipient type enumeration."""
20+
21+
USER_ID = "UserId"
22+
GROUP_ID = "GroupId"
23+
EMAIL = "UserEmail"
24+
GROUP_NAME = "GroupName"
25+
26+
27+
class TaskRecipient(BaseModel):
28+
"""Model representing a task recipient."""
29+
30+
type: Literal[
31+
TaskRecipientType.USER_ID,
32+
TaskRecipientType.GROUP_ID,
33+
TaskRecipientType.EMAIL,
34+
TaskRecipientType.GROUP_NAME,
35+
] = Field(..., alias="type")
36+
value: str = Field(..., alias="value")
37+
38+
1839
class Task(BaseModel):
1940
"""Model representing a Task in the UiPath Platform."""
2041

src/uipath/platform/common/interrupt_models.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from pydantic import BaseModel, ConfigDict, Field, model_validator
66

7-
from ..action_center.tasks import Task
7+
from ..action_center.tasks import Task, TaskRecipient
88
from ..context_grounding import (
99
BatchTransformCreationResponse,
1010
BatchTransformOutputColumn,
@@ -38,6 +38,7 @@ class CreateTask(BaseModel):
3838
title: str
3939
data: dict[str, Any] | None = None
4040
assignee: str | None = ""
41+
recipient: TaskRecipient | None = None
4142
app_name: str | None = None
4243
app_folder_path: str | None = None
4344
app_folder_key: str | None = None

src/uipath/platform/resume_triggers/_protocol.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,7 @@ async def _handle_task_trigger(
444444
app_folder_key=value.app_folder_key if value.app_folder_key else "",
445445
app_key=value.app_key if value.app_key else "",
446446
assignee=value.assignee if value.assignee else "",
447+
recipient=value.recipient if value.recipient else "",
447448
data=value.data,
448449
)
449450
if not action:

tests/cli/test_hitl.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,7 @@ async def test_create_resume_trigger_create_task(
600600
app_folder_key="",
601601
app_key="",
602602
assignee="",
603+
recipient="",
603604
data=create_action.data,
604605
)
605606

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.

0 commit comments

Comments
 (0)