Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath"
version = "2.5.22"
version = "2.5.23"
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
112 changes: 103 additions & 9 deletions src/uipath/platform/action_center/_tasks_service.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
import os
import uuid
from typing import Any, Dict, List, Optional, Tuple
Expand All @@ -10,9 +11,15 @@
HEADER_TENANT_ID,
)
from ...tracing import traced
from ..common import BaseService, FolderContext, UiPathApiConfig, UiPathExecutionContext
from ..common import (
BaseService,
FolderContext,
UiPathApiConfig,
UiPathConfig,
UiPathExecutionContext,
)
from .task_schema import TaskSchema
from .tasks import Task
from .tasks import Task, TaskRecipient, TaskRecipientType


def _create_spec(
Expand Down Expand Up @@ -113,13 +120,94 @@ def _retrieve_action_spec(
)


def _assign_task_spec(task_key: str, assignee: str) -> RequestSpec:
return RequestSpec(
async def _assign_task_spec(
self, task_key: str, assignee: str | None, task_recipient: TaskRecipient | None
) -> RequestSpec:
request_spec = RequestSpec(
method="POST",
endpoint=Endpoint(
"/orchestrator_/odata/Tasks/UiPath.Server.Configuration.OData.AssignTasks"
),
json={"taskAssignments": [{"taskId": task_key, "UserNameOrEmail": assignee}]},
)
if task_recipient:
recipient_value = await _resolve_recipient(self, task_recipient)
if (
task_recipient.type == TaskRecipientType.USER_ID
or task_recipient.type == TaskRecipientType.EMAIL
):
request_spec.json = {
"taskAssignments": [
{
"taskId": task_key,
"assignmentCriteria": "SingleUser",
"userNameOrEmail": recipient_value,
}
]
}
else:
request_spec.json = {
"taskAssignments": [
{
"taskId": task_key,
"assignmentCriteria": "AllUsers",
"assigneeNamesOrEmails": [recipient_value],
}
]
}
elif assignee:
request_spec.json = {
"taskAssignments": [{"taskId": task_key, "UserNameOrEmail": assignee}]
}
return request_spec


async def _resolve_recipient(self, task_recipient: TaskRecipient) -> str:
recipient_value = task_recipient.value
if task_recipient.type == TaskRecipientType.USER_ID:
user_spec = _resolve_user(task_recipient.value)
user_response = await self.request_async(
user_spec.method,
user_spec.endpoint,
json=user_spec.json,
content=user_spec.content,
headers=user_spec.headers,
scoped="org",
)
recipient_value = user_response.json().get("email")
if task_recipient.type == TaskRecipientType.GROUP_ID:
group_spec = _resolve_group(task_recipient.value)
group_response = await self.request_async(
group_spec.method,
group_spec.endpoint,
json=group_spec.json,
content=group_spec.content,
headers=group_spec.headers,
scoped="org",
)
recipient_value = group_response.json().get("displayName")
return recipient_value


def _resolve_user(entity_id: str) -> RequestSpec:
org_id = UiPathConfig.organization_id
return RequestSpec(
method="POST",
endpoint=Endpoint(
"/identity_/api/Directory/Resolve/{org_id}".format(org_id=org_id)
),
json={"entityId": entity_id, "entityType": "User"},
)


def _resolve_group(entity_id: str) -> RequestSpec:
org_id = UiPathConfig.organization_id
return RequestSpec(
method="GET",
endpoint=Endpoint(
"/identity_/api/Group/{org_id}/{entity_id}".format(
org_id=org_id, entity_id=entity_id
)
),
)


Expand Down Expand Up @@ -181,6 +269,7 @@ async def create_async(
app_folder_path: Optional[str] = None,
app_folder_key: Optional[str] = None,
assignee: Optional[str] = None,
recipient: Optional[TaskRecipient] = None,
) -> Task:
"""Creates a new action asynchronously.

Expand Down Expand Up @@ -226,8 +315,10 @@ async def create_async(
headers=spec.headers,
)
json_response = response.json()
if assignee:
spec = _assign_task_spec(json_response["id"], assignee)
if assignee or recipient:
spec = await _assign_task_spec(
self, json_response["id"], assignee, recipient
)
await self.request_async(
spec.method, spec.endpoint, json=spec.json, content=spec.content
)
Expand All @@ -249,6 +340,7 @@ def create(
app_folder_path: Optional[str] = None,
app_folder_key: Optional[str] = None,
assignee: Optional[str] = None,
recipient: Optional[TaskRecipient] = None,
) -> Task:
"""Creates a new task synchronously.

Expand Down Expand Up @@ -294,8 +386,10 @@ def create(
headers=spec.headers,
)
json_response = response.json()
if assignee:
spec = _assign_task_spec(json_response["id"], assignee)
if assignee or recipient:
spec = asyncio.run(
_assign_task_spec(self, json_response["id"], assignee, recipient)
)
self.request(
spec.method, spec.endpoint, json=spec.json, content=spec.content
)
Expand Down
23 changes: 22 additions & 1 deletion src/uipath/platform/action_center/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import enum
from datetime import datetime
from typing import Any, Dict, List, Optional, Union
from typing import Any, Dict, List, Literal, Optional, Union

from pydantic import BaseModel, ConfigDict, Field, field_serializer

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


class TaskRecipientType(str, enum.Enum):
"""Task recipient type enumeration."""

USER_ID = "UserId"
GROUP_ID = "GroupId"
EMAIL = "UserEmail"
GROUP_NAME = "GroupName"


class TaskRecipient(BaseModel):
"""Model representing a task recipient."""

type: Literal[
TaskRecipientType.USER_ID,
TaskRecipientType.GROUP_ID,
TaskRecipientType.EMAIL,
TaskRecipientType.GROUP_NAME,
] = Field(..., alias="type")
value: str = Field(..., alias="value")


class Task(BaseModel):
"""Model representing a Task in the UiPath Platform."""

Expand Down
3 changes: 2 additions & 1 deletion src/uipath/platform/common/interrupt_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from pydantic import BaseModel, ConfigDict, Field, model_validator

from ..action_center.tasks import Task
from ..action_center.tasks import Task, TaskRecipient
from ..context_grounding import (
BatchTransformCreationResponse,
BatchTransformOutputColumn,
Expand Down Expand Up @@ -38,6 +38,7 @@ class CreateTask(BaseModel):
title: str
data: dict[str, Any] | None = None
assignee: str | None = ""
recipient: TaskRecipient | None = None
app_name: str | None = None
app_folder_path: str | None = None
app_folder_key: str | None = None
Expand Down
1 change: 1 addition & 0 deletions src/uipath/platform/resume_triggers/_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,7 @@ async def _handle_task_trigger(
app_folder_key=value.app_folder_key if value.app_folder_key else "",
app_key=value.app_key if value.app_key else "",
assignee=value.assignee if value.assignee else "",
recipient=value.recipient if value.recipient else "",
data=value.data,
)
if not action:
Expand Down
1 change: 1 addition & 0 deletions tests/cli/test_hitl.py
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,7 @@ async def test_create_resume_trigger_create_task(
app_folder_key="",
app_key="",
assignee="",
recipient="",
data=create_action.data,
)

Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.