Skip to content

Commit cc8284c

Browse files
kunaluipathclaude
andauthored
feat(agent): add tool-output and V2 escalation recipient types [ACTN-10480] (#1667)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent b2b3608 commit cc8284c

9 files changed

Lines changed: 497 additions & 8 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.67"
3+
version = "0.1.68"
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/action_center/_tasks_service.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,34 @@ async def _assign_task_spec(
305305
}
306306
]
307307
}
308+
elif task_recipient.type == TaskRecipientType.WORKLOAD:
309+
# This branch covers BOTH agent-side Workload criteria (single
310+
# group, distributed by workload) AND agent-side CustomAssignees
311+
# criteria (explicit email list — already resolved into
312+
# `task_recipient.values` upstream). Both submit to the Action
313+
# Center API as a "Workload" assignment; the difference is whether
314+
# `values` carries one group or N emails.
315+
request_spec.json = {
316+
"taskAssignments": [
317+
{
318+
"taskId": task_key,
319+
"assignmentCriteria": "Workload",
320+
"assigneeNamesOrEmails": task_recipient.values
321+
or [recipient_value],
322+
}
323+
]
324+
}
325+
elif task_recipient.type == TaskRecipientType.ROUND_ROBIN:
326+
request_spec.json = {
327+
"taskAssignments": [
328+
{
329+
"taskId": task_key,
330+
"assignmentCriteria": "RoundRobin",
331+
"assigneeNamesOrEmails": task_recipient.values
332+
or [recipient_value],
333+
}
334+
]
335+
}
308336
else:
309337
request_spec.json = {
310338
"taskAssignments": [

packages/uipath-platform/src/uipath/platform/action_center/tasks.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,35 @@ class TaskRecipientType(str, enum.Enum):
2222
GROUP_ID = "GroupId"
2323
EMAIL = "UserEmail"
2424
GROUP_NAME = "GroupName"
25+
WORKLOAD = "Workload"
26+
ROUND_ROBIN = "RoundRobin"
2527

2628

2729
class TaskRecipient(BaseModel):
28-
"""Model representing a task recipient."""
30+
"""Model representing a task recipient.
31+
32+
`value` is the single identifier (group name, group id, user id, email, …).
33+
`values` is the multi-assignee form used by Workload-with-custom-emails
34+
assignments; when set it takes precedence over `value` for the
35+
`assigneeNamesOrEmails` payload.
36+
37+
Note: there is no CustomAssignees member here on purpose. The agent-side
38+
CustomAssignees criteria (AgentEscalationRecipientType.CUSTOM_ASSIGNEES,
39+
type 11) is resolved to a Workload assignment with the explicit email list
40+
in `values` before reaching this layer, so the Action Center
41+
AssignTasks API only ever sees the existing literal types.
42+
"""
2943

3044
type: Literal[
3145
TaskRecipientType.USER_ID,
3246
TaskRecipientType.GROUP_ID,
3347
TaskRecipientType.EMAIL,
3448
TaskRecipientType.GROUP_NAME,
49+
TaskRecipientType.WORKLOAD,
50+
TaskRecipientType.ROUND_ROBIN,
3551
] = Field(..., alias="type")
3652
value: str = Field(..., alias="value")
53+
values: Optional[List[str]] = Field(default=None, alias="values")
3754
display_name: Optional[str] = Field(default=None, alias="displayName")
3855

3956

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

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from uipath.platform import UiPathApiConfig, UiPathExecutionContext
88
from uipath.platform.action_center import Task
99
from uipath.platform.action_center._tasks_service import TasksService
10+
from uipath.platform.action_center.tasks import TaskRecipient, TaskRecipientType
1011
from uipath.platform.common.constants import HEADER_USER_AGENT
1112

1213

@@ -186,6 +187,167 @@ def test_create_with_assignee(
186187
assert action.title == "Test Action"
187188

188189

190+
def _mock_app_lookup_and_create(
191+
httpx_mock: HTTPXMock,
192+
base_url: str,
193+
org: str,
194+
tenant: str,
195+
monkeypatch: pytest.MonkeyPatch,
196+
) -> None:
197+
"""Common httpx mock setup for app lookup + task creation + assign."""
198+
monkeypatch.setenv("UIPATH_TENANT_ID", "test-tenant-id")
199+
httpx_mock.add_response(
200+
url=f"{base_url}{org}/apps_/default/api/v1/default/deployed-action-apps-schemas?search=test-app&filterByDeploymentTitle=true",
201+
status_code=200,
202+
json={
203+
"deployed": [
204+
{
205+
"systemName": "test-app",
206+
"deploymentTitle": "test-app",
207+
"actionSchema": {
208+
"key": "test-key",
209+
"inputs": [],
210+
"outputs": [],
211+
"inOuts": [],
212+
"outcomes": [],
213+
},
214+
"deploymentFolder": {
215+
"fullyQualifiedName": "test-folder-path",
216+
"key": "test-folder-key",
217+
},
218+
}
219+
]
220+
},
221+
)
222+
httpx_mock.add_response(
223+
url=f"{base_url}{org}{tenant}/orchestrator_/tasks/AppTasks/CreateAppTask",
224+
status_code=200,
225+
json={"id": 1, "title": "Test Action"},
226+
)
227+
httpx_mock.add_response(
228+
url=f"{base_url}{org}{tenant}/orchestrator_/odata/Tasks/UiPath.Server.Configuration.OData.AssignTasks",
229+
status_code=200,
230+
json={},
231+
)
232+
233+
234+
def _assign_request_payload(httpx_mock: HTTPXMock) -> dict[str, Any]:
235+
"""Return the parsed JSON body of the last AssignTasks request captured by the mock."""
236+
assign_request = next(
237+
req
238+
for req in reversed(httpx_mock.get_requests())
239+
if "AssignTasks" in str(req.url)
240+
)
241+
return json.loads(assign_request.content)
242+
243+
244+
class TestAssignTaskSpec:
245+
"""Tests for the task-assignment payload built by `_assign_task_spec`."""
246+
247+
def test_assign_workload_recipient_uses_workload_criteria_with_group(
248+
self,
249+
httpx_mock: HTTPXMock,
250+
service: TasksService,
251+
base_url: str,
252+
org: str,
253+
tenant: str,
254+
monkeypatch: pytest.MonkeyPatch,
255+
) -> None:
256+
_mock_app_lookup_and_create(httpx_mock, base_url, org, tenant, monkeypatch)
257+
258+
service.create(
259+
title="Test Action",
260+
app_name="test-app",
261+
data={"x": 1},
262+
recipient=TaskRecipient(
263+
type=TaskRecipientType.WORKLOAD,
264+
value="Support Team",
265+
displayName="Support Team",
266+
),
267+
)
268+
269+
payload = _assign_request_payload(httpx_mock)
270+
assert payload == {
271+
"taskAssignments": [
272+
{
273+
"taskId": 1,
274+
"assignmentCriteria": "Workload",
275+
"assigneeNamesOrEmails": ["Support Team"],
276+
}
277+
]
278+
}
279+
280+
def test_assign_round_robin_recipient_uses_round_robin_criteria(
281+
self,
282+
httpx_mock: HTTPXMock,
283+
service: TasksService,
284+
base_url: str,
285+
org: str,
286+
tenant: str,
287+
monkeypatch: pytest.MonkeyPatch,
288+
) -> None:
289+
_mock_app_lookup_and_create(httpx_mock, base_url, org, tenant, monkeypatch)
290+
291+
service.create(
292+
title="Test Action",
293+
app_name="test-app",
294+
data={"x": 1},
295+
recipient=TaskRecipient(
296+
type=TaskRecipientType.ROUND_ROBIN,
297+
value="Support Team",
298+
displayName="Support Team",
299+
),
300+
)
301+
302+
payload = _assign_request_payload(httpx_mock)
303+
assert payload == {
304+
"taskAssignments": [
305+
{
306+
"taskId": 1,
307+
"assignmentCriteria": "RoundRobin",
308+
"assigneeNamesOrEmails": ["Support Team"],
309+
}
310+
]
311+
}
312+
313+
def test_assign_workload_with_multiple_emails_uses_values_list(
314+
self,
315+
httpx_mock: HTTPXMock,
316+
service: TasksService,
317+
base_url: str,
318+
org: str,
319+
tenant: str,
320+
monkeypatch: pytest.MonkeyPatch,
321+
) -> None:
322+
"""Custom-assignees path: Workload criteria with a list of emails."""
323+
_mock_app_lookup_and_create(httpx_mock, base_url, org, tenant, monkeypatch)
324+
325+
service.create(
326+
title="Test Action",
327+
app_name="test-app",
328+
data={"x": 1},
329+
recipient=TaskRecipient(
330+
type=TaskRecipientType.WORKLOAD,
331+
value="alice@example.com",
332+
values=["alice@example.com", "bob@example.com"],
333+
),
334+
)
335+
336+
payload = _assign_request_payload(httpx_mock)
337+
assert payload == {
338+
"taskAssignments": [
339+
{
340+
"taskId": 1,
341+
"assignmentCriteria": "Workload",
342+
"assigneeNamesOrEmails": [
343+
"alice@example.com",
344+
"bob@example.com",
345+
],
346+
}
347+
]
348+
}
349+
350+
189351
def _make_deployed_app(
190352
name: str,
191353
folder_path: str,

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.11.0"
3+
version = "2.11.1"
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.17, <0.6.0",
99
"uipath-runtime>=0.11.0, <0.12.0",
10-
"uipath-platform>=0.1.67, <0.2.0",
10+
"uipath-platform>=0.1.68, <0.2.0",
1111
"click>=8.3.1",
1212
"httpx>=0.28.1",
1313
"pyjwt>=2.10.1",

0 commit comments

Comments
 (0)