Skip to content
Open
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.2.41"
version = "2.2.42"
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
4 changes: 4 additions & 0 deletions src/uipath/platform/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
from .interrupt_models import (
CreateBatchTransform,
CreateDeepRag,
CreateDocumentExtraction,
CreateEscalation,
CreateTask,
InvokeProcess,
WaitBatchTransform,
WaitDeepRag,
WaitDocumentExtraction,
WaitEscalation,
WaitJob,
WaitTask,
Expand All @@ -44,4 +46,6 @@
"WaitDeepRag",
"CreateBatchTransform",
"WaitBatchTransform",
"CreateDocumentExtraction",
"WaitDocumentExtraction",
]
67 changes: 48 additions & 19 deletions src/uipath/platform/common/interrupt_models.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Models for interrupt operations in UiPath platform."""

from typing import Annotated, Any, Dict, Optional
from typing import Annotated, Any

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

from ..action_center import Task
from ..context_grounding import (
Expand All @@ -11,36 +11,37 @@
CitationMode,
DeepRagCreationResponse,
)
from ..documents import FileContent, StartExtractionResponse
from ..orchestrator import Job


class InvokeProcess(BaseModel):
"""Model representing a process invocation."""

name: str
process_folder_path: Optional[str] = None
process_folder_key: Optional[str] = None
input_arguments: Optional[Dict[str, Any]]
process_folder_path: str | None = None
process_folder_key: str | None = None
input_arguments: dict[str, Any] | None


class WaitJob(BaseModel):
"""Model representing a wait job operation."""

job: Job
process_folder_path: Optional[str] = None
process_folder_key: Optional[str] = None
process_folder_path: str | None = None
process_folder_key: str | None = None


class CreateTask(BaseModel):
"""Model representing an action creation."""

title: str
data: Optional[Dict[str, Any]] = None
assignee: Optional[str] = ""
app_name: Optional[str] = None
app_folder_path: Optional[str] = None
app_folder_key: Optional[str] = None
app_key: Optional[str] = None
data: dict[str, Any] | None = None
assignee: str | None = ""
app_name: str | None = None
app_folder_path: str | None = None
app_folder_key: str | None = None
app_key: str | None = None


class CreateEscalation(CreateTask):
Expand All @@ -53,8 +54,8 @@ class WaitTask(BaseModel):
"""Model representing a wait action operation."""

action: Task
app_folder_path: Optional[str] = None
app_folder_key: Optional[str] = None
app_folder_path: str | None = None
app_folder_key: str | None = None


class WaitEscalation(WaitTask):
Expand All @@ -79,8 +80,8 @@ class WaitDeepRag(BaseModel):
"""Model representing a wait Deep RAG task."""

deep_rag: DeepRagCreationResponse
index_folder_path: Optional[str] = None
index_folder_key: Optional[str] = None
index_folder_path: str | None = None
index_folder_key: str | None = None


class CreateBatchTransform(BaseModel):
Expand All @@ -103,5 +104,33 @@ class WaitBatchTransform(BaseModel):
"""Model representing a wait Batch Transform task."""

batch_transform: BatchTransformCreationResponse
index_folder_path: Optional[str] = None
index_folder_key: Optional[str] = None
index_folder_path: str | None = None
index_folder_key: str | None = None


class CreateDocumentExtraction(BaseModel):
"""Model representing a document extraction task creation."""

project_name: str
tag: str
file: FileContent | None = None
file_path: str | None = None

model_config = ConfigDict(
arbitrary_types_allowed=True,
)

@model_validator(mode="after")
def validate_exactly_one_file_source(self) -> "CreateDocumentExtraction":
"""Validate that exactly one of file or file_path is provided."""
if (self.file is None) == (self.file_path is None):
raise ValueError(
"Exactly one of 'file' or 'file_path' must be provided, not both or neither"
)
return self


class WaitDocumentExtraction(BaseModel):
"""Model representing a wait document extraction task creation."""

extraction: StartExtractionResponse
78 changes: 72 additions & 6 deletions src/uipath/platform/resume_triggers/_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,22 @@
from uipath.platform.common import (
CreateBatchTransform,
CreateDeepRag,
CreateDocumentExtraction,
CreateEscalation,
CreateTask,
InvokeProcess,
WaitBatchTransform,
WaitDeepRag,
WaitDocumentExtraction,
WaitEscalation,
WaitJob,
WaitTask,
)
from uipath.platform.context_grounding import DeepRagStatus
from uipath.platform.errors import BatchTransformNotCompleteException
from uipath.platform.errors import (
BatchTransformNotCompleteException,
ExtractionNotCompleteException,
)
from uipath.platform.orchestrator.job import JobState
from uipath.platform.resume_triggers._enums import PropertyName, TriggerMarker

Expand Down Expand Up @@ -244,6 +249,28 @@ async def read_trigger(self, trigger: UiPathResumeTrigger) -> Any | None:

return f"Batch transform completed. Modified file available at {os.path.abspath(destination_path)}"

case UiPathResumeTriggerType.IXP_EXTRACTION:
if trigger.item_key:
project_id = self._extract_field("project_id", trigger.payload)
tag = self._extract_field("tag", trigger.payload)

assert project_id is not None
assert tag is not None

try:
extraction_response = (
await uipath.documents.retrieve_ixp_extraction_result_async(
project_id, tag, trigger.item_key
)
)
except ExtractionNotCompleteException as e:
raise UiPathPendingTriggerError(
ErrorCategory.SYSTEM,
f"{e.message}",
) from e

return extraction_response.model_dump()

case UiPathResumeTriggerType.API:
if trigger.api_resume and trigger.api_resume.inbox_id:
try:
Expand Down Expand Up @@ -331,7 +358,10 @@ async def create_trigger(self, suspend_value: Any) -> UiPathResumeTrigger:
await self._handle_batch_rag_job_trigger(
suspend_value, resume_trigger, uipath
)

case UiPathResumeTriggerType.IXP_EXTRACTION:
await self._handle_ixp_extraction_trigger(
suspend_value, resume_trigger, uipath
)
case _:
raise UiPathFaultedTriggerError(
ErrorCategory.SYSTEM,
Expand Down Expand Up @@ -363,6 +393,8 @@ def _determine_trigger_type(self, value: Any) -> UiPathResumeTriggerType:
return UiPathResumeTriggerType.DEEP_RAG
if isinstance(value, (CreateBatchTransform, WaitBatchTransform)):
return UiPathResumeTriggerType.BATCH_RAG
if isinstance(value, (CreateDocumentExtraction, WaitDocumentExtraction)):
return UiPathResumeTriggerType.IXP_EXTRACTION
# default to API trigger
return UiPathResumeTriggerType.API

Expand All @@ -385,6 +417,8 @@ def _determine_trigger_name(self, value: Any) -> UiPathResumeTriggerName:
return UiPathResumeTriggerName.DEEP_RAG
if isinstance(value, (CreateBatchTransform, WaitBatchTransform)):
return UiPathResumeTriggerName.BATCH_RAG
if isinstance(value, (CreateDocumentExtraction, WaitDocumentExtraction)):
return UiPathResumeTriggerName.EXTRACTION
# default to API trigger
return UiPathResumeTriggerName.API

Expand Down Expand Up @@ -420,10 +454,10 @@ async def _handle_task_trigger(
async def _handle_deep_rag_job_trigger(
self, value: Any, resume_trigger: UiPathResumeTrigger, uipath: UiPath
) -> None:
"""Handle job-type resume triggers.
"""Handle Deep RAG resume triggers.

Args:
value: The suspend value (InvokeProcess or WaitJob)
value: The suspend value (CreateDeepRag or WaitDeepRag)
resume_trigger: The resume trigger to populate
uipath: The UiPath client instance
"""
Expand All @@ -448,10 +482,10 @@ async def _handle_deep_rag_job_trigger(
async def _handle_batch_rag_job_trigger(
self, value: Any, resume_trigger: UiPathResumeTrigger, uipath: UiPath
) -> None:
"""Handle job-type resume triggers.
"""Handle batch transform resume triggers.

Args:
value: The suspend value (InvokeProcess or WaitJob)
value: The suspend value (CreateBatchTransform or WaitBatchTransform)
resume_trigger: The resume trigger to populate
uipath: The UiPath client instance
"""
Expand All @@ -474,6 +508,38 @@ async def _handle_batch_rag_job_trigger(
raise Exception("Failed to start batch transform")
resume_trigger.item_key = batch_transform.id

async def _handle_ixp_extraction_trigger(
self, value: Any, resume_trigger: UiPathResumeTrigger, uipath: UiPath
) -> None:
"""Handle IXP Extraction resume triggers.

Args:
value: The suspend value (CreateDocumentExtraction or WaitDocumentExtraction)
resume_trigger: The resume trigger to populate
uipath: The UiPath client instance
"""
resume_trigger.folder_path = resume_trigger.folder_key = None

if isinstance(value, WaitDocumentExtraction):
resume_trigger.item_key = value.extraction.operation_id
elif isinstance(value, CreateDocumentExtraction):
document_extraction = await uipath.documents.start_ixp_extraction_async(
project_name=value.project_name,
tag=value.tag,
file=value.file,
file_path=value.file_path,
)
if not document_extraction:
raise Exception("Failed to start document extraction")
resume_trigger.item_key = document_extraction.operation_id

# add project_id and tag to the payload dict (needed when reading the trigger)
assert isinstance(resume_trigger.payload, dict)
resume_trigger.payload.setdefault(
"project_id", document_extraction.project_id
)
resume_trigger.payload.setdefault("tag", document_extraction.tag)

async def _handle_job_trigger(
self, value: Any, resume_trigger: UiPathResumeTrigger, uipath: UiPath
) -> None:
Expand Down
56 changes: 56 additions & 0 deletions tests/cli/test_hitl.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from uipath.platform.common import (
CreateBatchTransform,
CreateDeepRag,
CreateDocumentExtraction,
CreateTask,
InvokeProcess,
WaitBatchTransform,
Expand Down Expand Up @@ -776,3 +777,58 @@ async def test_create_resume_trigger_wait_batch_transform(
assert resume_trigger is not None
assert resume_trigger.trigger_type == UiPathResumeTriggerType.BATCH_RAG
assert resume_trigger.item_key == batch_transform_id


class TestDocumentExtractionModels:
"""Tests for document extraction models."""

def test_create_document_extraction_with_file(self) -> None:
"""Test CreateDocumentExtraction with file provided."""
file_content = b"test content"
extraction = CreateDocumentExtraction(
project_name="test_project",
tag="test_tag",
file=file_content,
)

assert extraction.project_name == "test_project"
assert extraction.tag == "test_tag"
assert extraction.file == file_content
assert extraction.file_path is None

def test_create_document_extraction_with_file_path(self) -> None:
"""Test CreateDocumentExtraction with file_path provided."""
extraction = CreateDocumentExtraction(
project_name="test_project",
tag="test_tag",
file_path="/path/to/file.pdf",
)

assert extraction.project_name == "test_project"
assert extraction.tag == "test_tag"
assert extraction.file is None
assert extraction.file_path == "/path/to/file.pdf"

def test_create_document_extraction_with_both_raises_error(self) -> None:
"""Test CreateDocumentExtraction with both file and file_path raises ValueError."""
file_content = b"test content"

with pytest.raises(ValueError) as exc_info:
CreateDocumentExtraction(
project_name="test_project",
tag="test_tag",
file=file_content,
file_path="/path/to/file.pdf",
)

assert "not both or neither" in str(exc_info.value)

def test_create_document_extraction_with_neither_raises_error(self) -> None:
"""Test CreateDocumentExtraction with neither file nor file_path raises ValueError."""
with pytest.raises(ValueError) as exc_info:
CreateDocumentExtraction(
project_name="test_project",
tag="test_tag",
)

assert "not both or neither" in str(exc_info.value)
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.

Loading