Skip to content

Commit 2ddb1da

Browse files
kunaluipathclaude
andcommitted
feat(agent): add tool-output and V2 escalation recipient types
Adds the agent + platform models needed for the V2 HITL escalation recipient flow consumed by the langchain runtime. uipath/agent/models: - New literal recipient classes for Workload (9), RoundRobin (10), and CustomAssignees (11) matching the storage schema v50 surface. - New ToolOutputRecipient class accepting types USER_ID/GROUP_ID/WORKLOAD/ ROUND_ROBIN/CUSTOM_ASSIGNEES with source="toolOutput", toolName, outputPath fields for runtime-resolved assignees. - ToolOutputRecipient listed first in the recipient Union so payloads carrying `source` match before falling through to literal variants. uipath/platform/action_center: - TaskRecipientType gains Workload and RoundRobin members. - TaskRecipient gains an optional `values` list field for the multi-assignee assigneeNamesOrEmails payload. - _tasks_service updated to forward the new fields when present. Backwards-compatible: existing payloads without source/values parse and serialize identically. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 9a5d2bb commit 2ddb1da

9 files changed

Lines changed: 468 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.1.61"
3+
version = "0.1.62"
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: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,28 @@ async def _assign_task_spec(
305305
}
306306
]
307307
}
308+
elif task_recipient.type == TaskRecipientType.WORKLOAD:
309+
request_spec.json = {
310+
"taskAssignments": [
311+
{
312+
"taskId": task_key,
313+
"assignmentCriteria": "Workload",
314+
"assigneeNamesOrEmails": task_recipient.values
315+
or [recipient_value],
316+
}
317+
]
318+
}
319+
elif task_recipient.type == TaskRecipientType.ROUND_ROBIN:
320+
request_spec.json = {
321+
"taskAssignments": [
322+
{
323+
"taskId": task_key,
324+
"assignmentCriteria": "RoundRobin",
325+
"assigneeNamesOrEmails": task_recipient.values
326+
or [recipient_value],
327+
}
328+
]
329+
}
308330
else:
309331
request_spec.json = {
310332
"taskAssignments": [

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,29 @@ 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+
"""
2937

3038
type: Literal[
3139
TaskRecipientType.USER_ID,
3240
TaskRecipientType.GROUP_ID,
3341
TaskRecipientType.EMAIL,
3442
TaskRecipientType.GROUP_NAME,
43+
TaskRecipientType.WORKLOAD,
44+
TaskRecipientType.ROUND_ROBIN,
3545
] = Field(..., alias="type")
3646
value: str = Field(..., alias="value")
47+
values: Optional[List[str]] = Field(default=None, alias="values")
3748
display_name: Optional[str] = Field(default=None, alias="displayName")
3849

3950

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: 2 additions & 2 deletions
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.10.78"
3+
version = "2.10.79"
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.60, <0.2.0",
10+
"uipath-platform>=0.1.62, <0.2.0",
1111
"click>=8.3.1",
1212
"httpx>=0.28.1",
1313
"pyjwt>=2.10.1",

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

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ class AgentEscalationRecipientType(str, CaseInsensitiveEnum):
140140
ASSET_GROUP_NAME = "AssetGroupName"
141141
ARGUMENT_EMAIL = "ArgumentEmail"
142142
ARGUMENT_GROUP_NAME = "ArgumentGroupName"
143+
WORKLOAD = "Workload"
144+
ROUND_ROBIN = "RoundRobin"
145+
CUSTOM_ASSIGNEES = "CustomAssignees"
143146

144147

145148
class AgentContextRetrievalMode(str, CaseInsensitiveEnum):
@@ -540,6 +543,9 @@ class AgentA2aResourceConfig(BaseAgentResourceConfig):
540543
6: AgentEscalationRecipientType.ASSET_GROUP_NAME,
541544
7: AgentEscalationRecipientType.ARGUMENT_EMAIL,
542545
8: AgentEscalationRecipientType.ARGUMENT_GROUP_NAME,
546+
9: AgentEscalationRecipientType.WORKLOAD,
547+
10: AgentEscalationRecipientType.ROUND_ROBIN,
548+
11: AgentEscalationRecipientType.CUSTOM_ASSIGNEES,
543549
}
544550

545551

@@ -619,14 +625,86 @@ class ArgumentGroupNameRecipient(BaseEscalationRecipient):
619625
argument_path: str = Field(..., alias="argumentName")
620626

621627

628+
class WorkloadRecipient(BaseEscalationRecipient):
629+
"""Workload-based group assignment.
630+
631+
The Action Center distributes tasks to the group member with the lightest workload.
632+
"""
633+
634+
type: Literal[AgentEscalationRecipientType.WORKLOAD,] = Field(..., alias="type")
635+
value: str = Field(..., alias="value")
636+
display_name: str = Field(..., alias="displayName")
637+
638+
639+
class RoundRobinRecipient(BaseEscalationRecipient):
640+
"""Round-robin group assignment.
641+
642+
The Action Center cycles through group members in order on each new task.
643+
"""
644+
645+
type: Literal[AgentEscalationRecipientType.ROUND_ROBIN,] = Field(..., alias="type")
646+
value: str = Field(..., alias="value")
647+
display_name: str = Field(..., alias="displayName")
648+
649+
650+
class CustomAssigneesRecipient(BaseEscalationRecipient):
651+
"""Custom multi-user assignment.
652+
653+
A channel can carry multiple instances, one per assignee email. All are passed
654+
to Action Center together using a Workload assignment criteria.
655+
"""
656+
657+
type: Literal[AgentEscalationRecipientType.CUSTOM_ASSIGNEES,] = Field(
658+
..., alias="type"
659+
)
660+
value: str = Field(..., alias="value")
661+
display_name: Optional[str] = Field(default=None, alias="displayName")
662+
663+
664+
class ToolOutputRecipient(BaseEscalationRecipient):
665+
"""Recipient whose value is resolved at runtime from a named tool's output.
666+
667+
Instead of a literal value entered at design time, this binding points at a
668+
field within a named tool's output. The runtime walks the agent's message
669+
history, finds the most recent ToolMessage matching `tool_name`, parses its
670+
content as JSON, and extracts `output_path` (a top-level field for v1).
671+
672+
Only the assignment-criteria recipient types that accept a runtime-computed
673+
value are supported: USER_ID, GROUP_ID, WORKLOAD, ROUND_ROBIN,
674+
CUSTOM_ASSIGNEES. The asset/static/argument types do not participate in
675+
tool-output binding (they have their own design-time resolution rules).
676+
"""
677+
678+
type: Literal[
679+
AgentEscalationRecipientType.USER_ID,
680+
AgentEscalationRecipientType.GROUP_ID,
681+
AgentEscalationRecipientType.WORKLOAD,
682+
AgentEscalationRecipientType.ROUND_ROBIN,
683+
AgentEscalationRecipientType.CUSTOM_ASSIGNEES,
684+
] = Field(..., alias="type")
685+
source: Literal["toolOutput"] = Field(..., alias="source")
686+
tool_name: str = Field(..., alias="toolName")
687+
output_path: str = Field(..., alias="outputPath")
688+
689+
690+
# Note: order matters in this union — ToolOutputRecipient is listed first so payloads
691+
# carrying `source: "toolOutput"` match it before the literal variants get a chance.
692+
# The literal classes don't define a `source` field, so Pydantic's overlap heuristics
693+
# pick the right class via the presence of required fields (value/displayName vs
694+
# source/toolName/outputPath). A `Field(discriminator="type")` cannot be used here
695+
# because multiple classes share the same `type` literals (literal and tool-output
696+
# variants of the same criteria).
622697
AgentEscalationRecipient = Annotated[
623698
Union[
699+
ToolOutputRecipient,
624700
StandardRecipient,
625701
AssetRecipient,
626702
ArgumentEmailRecipient,
627703
ArgumentGroupNameRecipient,
704+
WorkloadRecipient,
705+
RoundRobinRecipient,
706+
CustomAssigneesRecipient,
628707
],
629-
Field(discriminator="type"),
630708
BeforeValidator(_normalize_recipient_type),
631709
]
632710

0 commit comments

Comments
 (0)