Skip to content

Commit a9c3d81

Browse files
committed
feat: add ixp interrupt models
1 parent 390289d commit a9c3d81

File tree

6 files changed

+182
-27
lines changed

6 files changed

+182
-27
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.2.41"
3+
version = "2.2.42"
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/common/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
from .interrupt_models import (
1414
CreateBatchTransform,
1515
CreateDeepRag,
16+
CreateDocumentExtraction,
1617
CreateEscalation,
1718
CreateTask,
1819
InvokeProcess,
1920
WaitBatchTransform,
2021
WaitDeepRag,
22+
WaitDocumentExtraction,
2123
WaitEscalation,
2224
WaitJob,
2325
WaitTask,
@@ -44,4 +46,6 @@
4446
"WaitDeepRag",
4547
"CreateBatchTransform",
4648
"WaitBatchTransform",
49+
"CreateDocumentExtraction",
50+
"WaitDocumentExtraction",
4751
]

src/uipath/platform/common/interrupt_models.py

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""Models for interrupt operations in UiPath platform."""
22

3-
from typing import Annotated, Any, Dict, Optional
3+
from typing import Annotated, Any
44

5-
from pydantic import BaseModel, Field
5+
from pydantic import BaseModel, ConfigDict, Field, model_validator
66

77
from ..action_center import Task
88
from ..context_grounding import (
@@ -11,36 +11,37 @@
1111
CitationMode,
1212
DeepRagCreationResponse,
1313
)
14+
from ..documents import FileContent, StartExtractionResponse
1415
from ..orchestrator import Job
1516

1617

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

2021
name: str
21-
process_folder_path: Optional[str] = None
22-
process_folder_key: Optional[str] = None
23-
input_arguments: Optional[Dict[str, Any]]
22+
process_folder_path: str | None = None
23+
process_folder_key: str | None = None
24+
input_arguments: dict[str, Any] | None
2425

2526

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

2930
job: Job
30-
process_folder_path: Optional[str] = None
31-
process_folder_key: Optional[str] = None
31+
process_folder_path: str | None = None
32+
process_folder_key: str | None = None
3233

3334

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

3738
title: str
38-
data: Optional[Dict[str, Any]] = None
39-
assignee: Optional[str] = ""
40-
app_name: Optional[str] = None
41-
app_folder_path: Optional[str] = None
42-
app_folder_key: Optional[str] = None
43-
app_key: Optional[str] = None
39+
data: dict[str, Any] | None = None
40+
assignee: str | None = ""
41+
app_name: str | None = None
42+
app_folder_path: str | None = None
43+
app_folder_key: str | None = None
44+
app_key: str | None = None
4445

4546

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

5556
action: Task
56-
app_folder_path: Optional[str] = None
57-
app_folder_key: Optional[str] = None
57+
app_folder_path: str | None = None
58+
app_folder_key: str | None = None
5859

5960

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

8182
deep_rag: DeepRagCreationResponse
82-
index_folder_path: Optional[str] = None
83-
index_folder_key: Optional[str] = None
83+
index_folder_path: str | None = None
84+
index_folder_key: str | None = None
8485

8586

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

105106
batch_transform: BatchTransformCreationResponse
106-
index_folder_path: Optional[str] = None
107-
index_folder_key: Optional[str] = None
107+
index_folder_path: str | None = None
108+
index_folder_key: str | None = None
109+
110+
111+
class CreateDocumentExtraction(BaseModel):
112+
"""Model representing a document extraction task creation."""
113+
114+
project_name: str
115+
tag: str
116+
file: FileContent | None = None
117+
file_path: str | None = None
118+
119+
model_config = ConfigDict(
120+
arbitrary_types_allowed=True,
121+
)
122+
123+
@model_validator(mode="after")
124+
def validate_exactly_one_file_source(self) -> "CreateDocumentExtraction":
125+
"""Validate that exactly one of file or file_path is provided."""
126+
if (self.file is None) == (self.file_path is None):
127+
raise ValueError(
128+
"Exactly one of 'file' or 'file_path' must be provided, not both or neither"
129+
)
130+
return self
131+
132+
133+
class WaitDocumentExtraction(BaseModel):
134+
"""Model representing a wait document extraction task creation."""
135+
136+
extraction: StartExtractionResponse

src/uipath/platform/resume_triggers/_protocol.py

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,22 @@
2424
from uipath.platform.common import (
2525
CreateBatchTransform,
2626
CreateDeepRag,
27+
CreateDocumentExtraction,
2728
CreateEscalation,
2829
CreateTask,
2930
InvokeProcess,
3031
WaitBatchTransform,
3132
WaitDeepRag,
33+
WaitDocumentExtraction,
3234
WaitEscalation,
3335
WaitJob,
3436
WaitTask,
3537
)
3638
from uipath.platform.context_grounding import DeepRagStatus
37-
from uipath.platform.errors import BatchTransformNotCompleteException
39+
from uipath.platform.errors import (
40+
BatchTransformNotCompleteException,
41+
ExtractionNotCompleteException,
42+
)
3843
from uipath.platform.orchestrator.job import JobState
3944
from uipath.platform.resume_triggers._enums import PropertyName, TriggerMarker
4045

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

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

252+
case UiPathResumeTriggerType.IXP_EXTRACTION:
253+
if trigger.item_key:
254+
project_id = self._extract_field("project_id", trigger.payload)
255+
tag = self._extract_field("tag", trigger.payload)
256+
257+
assert project_id is not None
258+
assert tag is not None
259+
260+
try:
261+
extraction_response = (
262+
await uipath.documents.retrieve_ixp_extraction_result_async(
263+
project_id, tag, trigger.item_key
264+
)
265+
)
266+
except ExtractionNotCompleteException as e:
267+
raise UiPathPendingTriggerError(
268+
ErrorCategory.SYSTEM,
269+
f"{e.message}",
270+
) from e
271+
272+
return extraction_response.model_dump()
273+
247274
case UiPathResumeTriggerType.API:
248275
if trigger.api_resume and trigger.api_resume.inbox_id:
249276
try:
@@ -331,7 +358,10 @@ async def create_trigger(self, suspend_value: Any) -> UiPathResumeTrigger:
331358
await self._handle_batch_rag_job_trigger(
332359
suspend_value, resume_trigger, uipath
333360
)
334-
361+
case UiPathResumeTriggerType.IXP_EXTRACTION:
362+
await self._handle_ixp_extraction_trigger(
363+
suspend_value, resume_trigger, uipath
364+
)
335365
case _:
336366
raise UiPathFaultedTriggerError(
337367
ErrorCategory.SYSTEM,
@@ -363,6 +393,8 @@ def _determine_trigger_type(self, value: Any) -> UiPathResumeTriggerType:
363393
return UiPathResumeTriggerType.DEEP_RAG
364394
if isinstance(value, (CreateBatchTransform, WaitBatchTransform)):
365395
return UiPathResumeTriggerType.BATCH_RAG
396+
if isinstance(value, (CreateDocumentExtraction, WaitDocumentExtraction)):
397+
return UiPathResumeTriggerType.IXP_EXTRACTION
366398
# default to API trigger
367399
return UiPathResumeTriggerType.API
368400

@@ -385,6 +417,8 @@ def _determine_trigger_name(self, value: Any) -> UiPathResumeTriggerName:
385417
return UiPathResumeTriggerName.DEEP_RAG
386418
if isinstance(value, (CreateBatchTransform, WaitBatchTransform)):
387419
return UiPathResumeTriggerName.BATCH_RAG
420+
if isinstance(value, (CreateDocumentExtraction, WaitDocumentExtraction)):
421+
return UiPathResumeTriggerName.EXTRACTION
388422
# default to API trigger
389423
return UiPathResumeTriggerName.API
390424

@@ -420,10 +454,10 @@ async def _handle_task_trigger(
420454
async def _handle_deep_rag_job_trigger(
421455
self, value: Any, resume_trigger: UiPathResumeTrigger, uipath: UiPath
422456
) -> None:
423-
"""Handle job-type resume triggers.
457+
"""Handle Deep RAG resume triggers.
424458
425459
Args:
426-
value: The suspend value (InvokeProcess or WaitJob)
460+
value: The suspend value (CreateDeepRag or WaitDeepRag)
427461
resume_trigger: The resume trigger to populate
428462
uipath: The UiPath client instance
429463
"""
@@ -448,10 +482,10 @@ async def _handle_deep_rag_job_trigger(
448482
async def _handle_batch_rag_job_trigger(
449483
self, value: Any, resume_trigger: UiPathResumeTrigger, uipath: UiPath
450484
) -> None:
451-
"""Handle job-type resume triggers.
485+
"""Handle batch transform resume triggers.
452486
453487
Args:
454-
value: The suspend value (InvokeProcess or WaitJob)
488+
value: The suspend value (CreateBatchTransform or WaitBatchTransform)
455489
resume_trigger: The resume trigger to populate
456490
uipath: The UiPath client instance
457491
"""
@@ -474,6 +508,38 @@ async def _handle_batch_rag_job_trigger(
474508
raise Exception("Failed to start batch transform")
475509
resume_trigger.item_key = batch_transform.id
476510

511+
async def _handle_ixp_extraction_trigger(
512+
self, value: Any, resume_trigger: UiPathResumeTrigger, uipath: UiPath
513+
) -> None:
514+
"""Handle IXP Extraction resume triggers.
515+
516+
Args:
517+
value: The suspend value (CreateDocumentExtraction or WaitDocumentExtraction)
518+
resume_trigger: The resume trigger to populate
519+
uipath: The UiPath client instance
520+
"""
521+
resume_trigger.folder_path = resume_trigger.folder_key = None
522+
523+
if isinstance(value, WaitDocumentExtraction):
524+
resume_trigger.item_key = value.extraction.operation_id
525+
elif isinstance(value, CreateDocumentExtraction):
526+
document_extraction = await uipath.documents.start_ixp_extraction_async(
527+
project_name=value.project_name,
528+
tag=value.tag,
529+
file=value.file,
530+
file_path=value.file_path,
531+
)
532+
if not document_extraction:
533+
raise Exception("Failed to start document extraction")
534+
resume_trigger.item_key = document_extraction.operation_id
535+
536+
# add project_id and tag to the payload dict (needed when reading the trigger)
537+
assert isinstance(resume_trigger.payload, dict)
538+
resume_trigger.payload.setdefault(
539+
"project_id", document_extraction.project_id
540+
)
541+
resume_trigger.payload.setdefault("tag", document_extraction.tag)
542+
477543
async def _handle_job_trigger(
478544
self, value: Any, resume_trigger: UiPathResumeTrigger, uipath: UiPath
479545
) -> None:

tests/cli/test_hitl.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from uipath.platform.common import (
1818
CreateBatchTransform,
1919
CreateDeepRag,
20+
CreateDocumentExtraction,
2021
CreateTask,
2122
InvokeProcess,
2223
WaitBatchTransform,
@@ -776,3 +777,58 @@ async def test_create_resume_trigger_wait_batch_transform(
776777
assert resume_trigger is not None
777778
assert resume_trigger.trigger_type == UiPathResumeTriggerType.BATCH_RAG
778779
assert resume_trigger.item_key == batch_transform_id
780+
781+
782+
class TestDocumentExtractionModels:
783+
"""Tests for document extraction models."""
784+
785+
def test_create_document_extraction_with_file(self) -> None:
786+
"""Test CreateDocumentExtraction with file provided."""
787+
file_content = b"test content"
788+
extraction = CreateDocumentExtraction(
789+
project_name="test_project",
790+
tag="test_tag",
791+
file=file_content,
792+
)
793+
794+
assert extraction.project_name == "test_project"
795+
assert extraction.tag == "test_tag"
796+
assert extraction.file == file_content
797+
assert extraction.file_path is None
798+
799+
def test_create_document_extraction_with_file_path(self) -> None:
800+
"""Test CreateDocumentExtraction with file_path provided."""
801+
extraction = CreateDocumentExtraction(
802+
project_name="test_project",
803+
tag="test_tag",
804+
file_path="/path/to/file.pdf",
805+
)
806+
807+
assert extraction.project_name == "test_project"
808+
assert extraction.tag == "test_tag"
809+
assert extraction.file is None
810+
assert extraction.file_path == "/path/to/file.pdf"
811+
812+
def test_create_document_extraction_with_both_raises_error(self) -> None:
813+
"""Test CreateDocumentExtraction with both file and file_path raises ValueError."""
814+
file_content = b"test content"
815+
816+
with pytest.raises(ValueError) as exc_info:
817+
CreateDocumentExtraction(
818+
project_name="test_project",
819+
tag="test_tag",
820+
file=file_content,
821+
file_path="/path/to/file.pdf",
822+
)
823+
824+
assert "not both or neither" in str(exc_info.value)
825+
826+
def test_create_document_extraction_with_neither_raises_error(self) -> None:
827+
"""Test CreateDocumentExtraction with neither file nor file_path raises ValueError."""
828+
with pytest.raises(ValueError) as exc_info:
829+
CreateDocumentExtraction(
830+
project_name="test_project",
831+
tag="test_tag",
832+
)
833+
834+
assert "not both or neither" in str(exc_info.value)

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)