diff --git a/pyproject.toml b/pyproject.toml index 85a80aa33..a6729cff7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.1.84" +version = "2.1.85" 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.10" diff --git a/src/uipath/_cli/_runtime/_hitl.py b/src/uipath/_cli/_runtime/_hitl.py index 623677557..3818111d6 100644 --- a/src/uipath/_cli/_runtime/_hitl.py +++ b/src/uipath/_cli/_runtime/_hitl.py @@ -62,6 +62,14 @@ async def read(cls, resume_trigger: UiPathResumeTrigger) -> Optional[str]: default_escalation = Escalation() match resume_trigger.trigger_type: case UiPathResumeTriggerType.ACTION: + include_metadata = False + if resume_trigger.payload: + try: + payload_data = json.loads(resume_trigger.payload) + include_metadata = payload_data.get("include_metadata", False) + except (json.JSONDecodeError, AttributeError): + include_metadata = False + if resume_trigger.item_key: action = await uipath.actions.retrieve_async( resume_trigger.item_key, @@ -69,6 +77,9 @@ async def read(cls, resume_trigger: UiPathResumeTrigger) -> Optional[str]: app_folder_path=resume_trigger.folder_path, ) + if include_metadata: + return action + if default_escalation.enabled: return default_escalation.extract_response_value(action.data) diff --git a/src/uipath/models/interrupt_models.py b/src/uipath/models/interrupt_models.py index 8ca4b030d..c9dd8165e 100644 --- a/src/uipath/models/interrupt_models.py +++ b/src/uipath/models/interrupt_models.py @@ -28,9 +28,11 @@ class CreateAction(BaseModel): app_folder_key: Optional[str] = None app_key: Optional[str] = None app_version: Optional[int] = 1 + include_metadata: bool = False class WaitAction(BaseModel): action: Action app_folder_path: Optional[str] = None app_folder_key: Optional[str] = None + include_metadata: bool = False diff --git a/tests/cli/test_hitl.py b/tests/cli/test_hitl.py index 31c9744ad..e0ccd8c70 100644 --- a/tests/cli/test_hitl.py +++ b/tests/cli/test_hitl.py @@ -1,3 +1,4 @@ +import json import uuid from unittest.mock import AsyncMock, patch @@ -312,3 +313,57 @@ async def test_create_resume_trigger_api( assert resume_trigger.api_resume is not None assert isinstance(resume_trigger.api_resume.inbox_id, str) assert resume_trigger.api_resume.request == api_input + + @pytest.mark.anyio + async def test_interrupt_with_include_metadata_true_returns_full_action( + self, + setup_test_env: None, + ) -> None: + """Test that interrupt() with include_metadata=True returns the whole action object.""" + action_key = "test-action-key" + + # Create an action with include_metadata=True + create_action = CreateAction( + title="Test Action", + app_name="TestApp", + app_folder_path="/test/path", + data={"input": "test-input"}, + include_metadata=True, + ) + + # Mock the action that would be retrieved after user interaction + mock_action = Action( + key=action_key, + action="Reject", + data={"input": "test-input"}, + status=2, + title="Test Action", + id=12345, + ) + + with ( + patch( + "uipath._services.actions_service.ActionsService.create_async" + ) as mock_create, + patch( + "uipath._services.actions_service.ActionsService.retrieve_async" + ) as mock_retrieve, + ): + mock_create.return_value = Action(key=action_key) + mock_retrieve.return_value = mock_action + + # Simulate interrupt() + processor = HitlProcessor(create_action) + resume_trigger = await processor.create_resume_trigger() + if isinstance(resume_trigger.payload, dict): + resume_trigger.payload = json.dumps(resume_trigger.payload) + + action_data = await HitlReader.read(resume_trigger) + + # verify we got the whole action object + assert isinstance(action_data, Action) + assert action_data.action == "Reject", "Should contain 'action' field" + assert action_data.data == {"input": "test-input"}, ( + "Should contain correct 'data' field" + ) + assert action_data != mock_action.data