From 1da4c4cf04c8947d210938b5912a8514678c07e7 Mon Sep 17 00:00:00 2001 From: Sandeepan-Ghosh-0312 Date: Tue, 21 Jul 2026 16:28:14 +0530 Subject: [PATCH 1/2] feat: just-in-time debug escalation apps support --- packages/uipath-platform/pyproject.toml | 2 +- .../platform/action_center/_tasks_service.py | 99 +++++++---- .../uipath/platform/action_center/tasks.py | 7 + .../src/uipath/platform/common/_config.py | 9 + .../src/uipath/platform/orchestrator/job.py | 1 + .../tests/common/test_config_env_vars.py | 36 ++++ .../tests/services/test_actions_service.py | 164 +++++++++++++++++- packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 4 +- .../uipath/src/uipath/agent/models/agent.py | 2 + packages/uipath/uv.lock | 4 +- 11 files changed, 290 insertions(+), 40 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index bbf16c8ca..d973884de 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.2.13" +version = "0.2.14" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py b/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py index dea78f882..c6d4fa981 100644 --- a/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py +++ b/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py @@ -17,7 +17,7 @@ from ..common._folder_context import FolderContext, header_folder from ..common._models import Endpoint, RequestSpec from .task_schema import TaskSchema -from .tasks import Task, TaskRecipient, TaskRecipientType +from .tasks import Task, TaskRecipient, TaskRecipientType, is_low_code_app def _ensure_string_value(value: Any) -> str: @@ -39,6 +39,8 @@ def _create_spec( is_actionable_message_enabled: Optional[bool] = None, actionable_message_metadata: Optional[Dict[str, Any]] = None, source_name: str = "Agent", + app_project_key: Optional[str] = None, + app_type: Optional[str] = None, ) -> RequestSpec: field_list = [] outcome_list = [] @@ -94,7 +96,7 @@ def _create_spec( ) json_payload: Dict[str, Any] = { - "appId": app_key, + "appId": app_key if app_key is not None else f"{uuid.UUID(int=0).hex}", "title": title, "data": data if data is not None else {}, "actionableMessageMetaData": actionable_message_metadata @@ -119,11 +121,14 @@ def _create_spec( ), } + if app_project_key is not None: + json_payload["appType"] = app_type + json_payload["appProjectKey"] = app_project_key + _apply_priority_labels_and_actionable_toggle( json_payload, priority, labels, is_actionable_message_enabled ) _apply_task_source(json_payload, source_name) - return RequestSpec( method="POST", endpoint=Endpoint("/orchestrator_/tasks/AppTasks/CreateAppTask"), @@ -159,7 +164,10 @@ def _apply_priority_labels_and_actionable_toggle( payload["isActionableMessageEnabled"] = is_actionable_message_enabled -def _apply_task_source(payload: Dict[str, Any], source_name: str) -> None: +def _apply_task_source( + payload: Dict[str, Any], + source_name: str, +) -> None: """Populate ``payload["taskSource"]`` when UiPathConfig has project_id + trace_id. Shared between AppTask and QuickForm spec builders — the taskSource block is @@ -167,6 +175,7 @@ def _apply_task_source(payload: Dict[str, Any], source_name: str) -> None: """ project_id = UiPathConfig.project_id trace_id = UiPathConfig.trace_id + solution_id = UiPathConfig.studio_solution_id if not (project_id and trace_id): return payload["taskSource"] = { @@ -178,8 +187,13 @@ def _apply_task_source(payload: Dict[str, Any], source_name: str) -> None: "JobKey": UiPathConfig.job_key, "ProcessKey": UiPathConfig.process_uuid, }, + "jobId": UiPathConfig.job_key, } + if UiPathConfig.is_rooted_to_debug_job: + payload["taskSource"]["isDebug"] = True + payload["taskSource"]["solutionId"] = solution_id + def _normalize_priority(priority: str | None) -> str | None: """Normalize priority string to match API expectations. @@ -459,6 +473,9 @@ async def create_async( is_actionable_message_enabled: Optional[bool] = None, actionable_message_metadata: Optional[Dict[str, Any]] = None, source_name: str = "Agent", + app_project_key: Optional[str] = None, + app_type: Optional[str] = None, + action_schema: Optional[Dict[str, Any]] = None, ) -> Task: """Creates a new action asynchronously. @@ -478,6 +495,9 @@ async def create_async( is_actionable_message_enabled: Optional boolean indicating whether actionable notifications are enabled for this task actionable_message_metadata: Optional metadata for the action source_name: The name of the source that created the task. Defaults to 'Agent'. + app_project_key: Optional project key of the app. Used for JIT (debug) task creation so + Orchestrator can resolve a not-yet-deployed app. + app_type: Optional app type ("Custom" or "Coded"), forwarded for JIT (debug) task creation. Returns: Action: The created action object @@ -485,18 +505,28 @@ async def create_async( Raises: Exception: If neither app_name nor app_key is provided for app-specific actions """ - (key, action_schema) = ( - (app_key, None) - if app_key - else await self._get_app_key_and_schema_async( + key: Optional[str] + schema: Optional[TaskSchema] + if app_project_key and is_low_code_app(app_type) and action_schema is not None: + key = app_key + schema = TaskSchema( + key=action_schema["key"], + in_outs=action_schema["inOuts"], + inputs=action_schema["inputs"], + outputs=action_schema["outputs"], + outcomes=action_schema["outcomes"], + ) + elif app_key: + key, schema = app_key, None + else: + key, schema = await self._get_app_key_and_schema_async( app_name, app_folder_path, app_folder_key ) - ) spec = _create_spec( title=title, data=data, app_key=key, - action_schema=action_schema, + action_schema=schema, app_folder_key=app_folder_key, app_folder_path=app_folder_path, priority=priority, @@ -504,8 +534,9 @@ async def create_async( is_actionable_message_enabled=is_actionable_message_enabled, actionable_message_metadata=actionable_message_metadata, source_name=source_name, + app_project_key=app_project_key, + app_type=app_type, ) - response = await self.request_async( spec.method, spec.endpoint, @@ -545,25 +576,13 @@ def create( is_actionable_message_enabled: Optional[bool] = None, actionable_message_metadata: Optional[Dict[str, Any]] = None, source_name: str = "Agent", + app_project_key: Optional[str] = None, + app_type: Optional[str] = None, + action_schema: Optional[Dict[str, Any]] = None, ) -> Task: """Creates a new task synchronously. - This method creates a new action task in UiPath Orchestrator. The action can be - either app-specific (using app_name or app_key) or a generic action. - - Args: - title: The title of the action - data: Optional dictionary containing input data for the action - app_name: The name of the application (if creating an app-specific action) - app_key: The key of the application (if creating an app-specific action) - app_folder_path: Optional folder path for the action - app_folder_key: Optional folder key for the action - assignee: Optional username or email to assign the task to - priority: Optional priority of the task - labels: Optional list of labels for the task - is_actionable_message_enabled: Optional boolean indicating whether actionable notifications are enabled for this task - actionable_message_metadata: Optional metadata for the action - source_name: The name of the source that created the task. Defaults to 'Agent'. + See :meth:`create_async` for parameter docs. Returns: Action: The created action object @@ -571,16 +590,28 @@ def create( Raises: Exception: If neither app_name nor app_key is provided for app-specific actions """ - (key, action_schema) = ( - (app_key, None) - if app_key - else self._get_app_key_and_schema(app_name, app_folder_path, app_folder_key) - ) + key: Optional[str] + schema: Optional[TaskSchema] + if app_project_key and is_low_code_app(app_type) and action_schema is not None: + key = app_key + schema = TaskSchema( + key=action_schema["key"], + in_outs=action_schema["inOuts"], + inputs=action_schema["inputs"], + outputs=action_schema["outputs"], + outcomes=action_schema["outcomes"], + ) + elif app_key: + key, schema = app_key, None + else: + key, schema = self._get_app_key_and_schema( + app_name, app_folder_path, app_folder_key + ) spec = _create_spec( title=title, data=data, app_key=key, - action_schema=action_schema, + action_schema=schema, app_folder_key=app_folder_key, app_folder_path=app_folder_path, priority=priority, @@ -588,6 +619,8 @@ def create( is_actionable_message_enabled=is_actionable_message_enabled, actionable_message_metadata=actionable_message_metadata, source_name=source_name, + app_project_key=app_project_key, + app_type=app_type, ) response = self.request( diff --git a/packages/uipath-platform/src/uipath/platform/action_center/tasks.py b/packages/uipath-platform/src/uipath/platform/action_center/tasks.py index f1a932cb8..ca9a87816 100644 --- a/packages/uipath-platform/src/uipath/platform/action_center/tasks.py +++ b/packages/uipath-platform/src/uipath/platform/action_center/tasks.py @@ -6,6 +6,13 @@ from pydantic import BaseModel, ConfigDict, Field, field_serializer +APP_TYPE_LOW_CODE = "Custom" + + +def is_low_code_app(app_type: str | None) -> bool: + """Return True when ``app_type`` denotes a low-code (custom) app.""" + return app_type == APP_TYPE_LOW_CODE + class TaskStatus(enum.IntEnum): """Enum representing possible Task status.""" diff --git a/packages/uipath-platform/src/uipath/platform/common/_config.py b/packages/uipath-platform/src/uipath/platform/common/_config.py index 549844db8..03ba02f25 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_config.py +++ b/packages/uipath-platform/src/uipath/platform/common/_config.py @@ -14,6 +14,7 @@ class UiPathApiConfig(BaseModel): class ConfigurationManager: _instance = None studio_solution_id: str | None = None + _is_debug_run_override: bool | None = None def __new__(cls): if cls._instance is None: @@ -194,8 +195,15 @@ def licensing_context(self) -> str | None: @property def is_rooted_to_debug_job(self) -> bool: """Whether this job, which may be a deployed process, is rooted to a debug session (e.g. Maestro solution debug).""" + if self._is_debug_run_override is not None: + return self._is_debug_run_override return self._read_internal_argument("isDebug") is True + @is_rooted_to_debug_job.setter + def is_rooted_to_debug_job(self, value: bool) -> None: + """Override the debug-run status resolved at runtime.""" + self._is_debug_run_override = value + @property def is_tracing_enabled(self) -> bool: from uipath.platform.constants import ENV_TRACING_ENABLED @@ -205,6 +213,7 @@ def is_tracing_enabled(self) -> bool: def reset(self) -> None: """Reset mutable cached state to defaults.""" self.studio_solution_id = None + self._is_debug_run_override = None # Invalidate cached_property by removing from instance __dict__ self.__dict__.pop("_internal_arguments", None) diff --git a/packages/uipath-platform/src/uipath/platform/orchestrator/job.py b/packages/uipath-platform/src/uipath/platform/orchestrator/job.py index 6464405b4..2e960de54 100644 --- a/packages/uipath-platform/src/uipath/platform/orchestrator/job.py +++ b/packages/uipath-platform/src/uipath/platform/orchestrator/job.py @@ -80,4 +80,5 @@ class Job(BaseModel): has_warnings: Optional[bool] = Field(default=None, alias="HasWarnings") job_error: Optional[JobErrorInfo] = Field(default=None, alias="JobError") folder_key: Optional[str] = Field(default=None, alias="FolderKey") + parent_context: Optional[str] = Field(default=None, alias="ParentContext") id: int = Field(alias="Id") diff --git a/packages/uipath-platform/tests/common/test_config_env_vars.py b/packages/uipath-platform/tests/common/test_config_env_vars.py index 1e48ac894..b0a0b5062 100644 --- a/packages/uipath-platform/tests/common/test_config_env_vars.py +++ b/packages/uipath-platform/tests/common/test_config_env_vars.py @@ -190,3 +190,39 @@ def test_has_eval_folder(self, monkeypatch, tmp_path): assert UiPathConfig.has_eval_folder is False (tmp_path / EVALS_FOLDER).mkdir() assert UiPathConfig.has_eval_folder is True + + +class TestIsRootedToDebugJob: + @pytest.fixture(autouse=True) + def _reset_singleton(self): + # is_rooted_to_debug_job mutates the process-wide singleton; reset around + # each test so nothing leaks between tests. + UiPathConfig.reset() + yield + UiPathConfig.reset() + + def test_defaults_to_false_when_unset(self): + assert UiPathConfig.is_rooted_to_debug_job is False + + def test_reads_is_debug_internal_argument(self): + UiPathConfig.__dict__["_internal_arguments"] = {"isDebug": True} + assert UiPathConfig.is_rooted_to_debug_job is True + + def test_setter_override_wins_and_is_true(self): + UiPathConfig.is_rooted_to_debug_job = True + assert UiPathConfig.is_rooted_to_debug_job is True + + def test_setter_override_beats_internal_argument(self): + # Even when the internal argument says True, an explicit False override wins. + UiPathConfig.__dict__["_internal_arguments"] = {"isDebug": True} + UiPathConfig.is_rooted_to_debug_job = False + assert UiPathConfig.is_rooted_to_debug_job is False + + def test_reset_clears_override(self): + UiPathConfig.__dict__["_internal_arguments"] = {"isDebug": True} + UiPathConfig.is_rooted_to_debug_job = False + assert UiPathConfig.is_rooted_to_debug_job is False + UiPathConfig.reset() + # Override cleared, so it falls back to the internal argument again — but + # reset() also drops the cached internal arguments, so it re-reads (None). + assert UiPathConfig.is_rooted_to_debug_job is False diff --git a/packages/uipath-platform/tests/services/test_actions_service.py b/packages/uipath-platform/tests/services/test_actions_service.py index 28180dbbb..c22dc7eca 100644 --- a/packages/uipath-platform/tests/services/test_actions_service.py +++ b/packages/uipath-platform/tests/services/test_actions_service.py @@ -7,7 +7,12 @@ from uipath.platform import UiPathApiConfig, UiPathExecutionContext from uipath.platform.action_center import Task from uipath.platform.action_center._tasks_service import TasksService -from uipath.platform.action_center.tasks import TaskRecipient, TaskRecipientType +from uipath.platform.action_center.tasks import ( + TaskRecipient, + TaskRecipientType, + is_low_code_app, +) +from uipath.platform.common import UiPathConfig from uipath.platform.constants import HEADER_USER_AGENT @@ -22,6 +27,14 @@ def service( return TasksService(config=config, execution_context=execution_context) +@pytest.fixture +def reset_uipath_config(): + """Reset the ``UiPathConfig`` singleton around a test that mutates it.""" + UiPathConfig.reset() + yield UiPathConfig + UiPathConfig.reset() + + class TestTasksService: def test_retrieve( self, @@ -128,6 +141,141 @@ def test_create_with_app_key( assert action.id == 1 assert action.title == "Test Action" + def test_create_jit_custom_app_uses_provided_schema_and_sends_project_key( + self, + httpx_mock: HTTPXMock, + service: TasksService, + base_url: str, + org: str, + tenant: str, + ) -> None: + # JIT: a not-yet-deployed Custom app supplies its action schema and project + # key, so no deployed-app schema lookup happens and the schema is built from + # the supplied action_schema. app type + project key are sent so Orchestrator + # can resolve the app. + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/tasks/AppTasks/CreateAppTask", + status_code=200, + json={"id": 1, "title": "Test Action"}, + ) + + action = service.create( + title="Test Action", + data={"stringInput": "value"}, + app_project_key="proj-key-abc", + app_type="Custom", + action_schema={ + "key": "schema-key", + "inOuts": [], + "inputs": [{"name": "stringInput", "key": "field-1"}], + "outputs": [], + "outcomes": [{"name": "approve", "key": "outcome-1"}], + }, + ) + + assert isinstance(action, Task) + requests = httpx_mock.get_requests() + # No deployed-app schema resolution — the provided action_schema is used. + assert all("deployed-action-apps-schemas" not in str(r.url) for r in requests) + create_request = [r for r in requests if "CreateAppTask" in str(r.url)][0] + body = json.loads(create_request.content) + assert body["appType"] == "Custom" + assert body["appProjectKey"] == "proj-key-abc" + # Field set is derived from the supplied action_schema, not a lookup. + field_names = [ + f["Name"] for f in body["actionableMessageMetaData"]["fieldSet"]["fields"] + ] + assert "stringInput" in field_names + + def test_create_omits_project_key_when_not_provided( + self, + httpx_mock: HTTPXMock, + service: TasksService, + base_url: str, + org: str, + tenant: str, + ) -> None: + # Non-JIT (deployed) path: no app_project_key, so appProjectKey is not sent. + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/tasks/AppTasks/CreateAppTask", + status_code=200, + json={"id": 1, "title": "Test Action"}, + ) + + action = service.create( + title="Test Action", + app_key="test-app-key", + data={"test": "data"}, + ) + + assert isinstance(action, Task) + create_request = [ + r for r in httpx_mock.get_requests() if "CreateAppTask" in str(r.url) + ][0] + body = json.loads(create_request.content) + assert "appProjectKey" not in body + assert "appType" not in body + + def test_create_stamps_isdebug_task_source_from_config( + self, + httpx_mock: HTTPXMock, + service: TasksService, + base_url: str, + org: str, + tenant: str, + monkeypatch: pytest.MonkeyPatch, + reset_uipath_config, + ) -> None: + # isDebug + solutionId on taskSource are driven purely by UiPathConfig, not a + # per-call flag. taskSource requires project_id + trace_id to be present. + monkeypatch.setenv("UIPATH_PROJECT_ID", "proj-1") + monkeypatch.setenv("UIPATH_TRACE_ID", "trace-1") + reset_uipath_config.studio_solution_id = "sol-1" + reset_uipath_config.is_rooted_to_debug_job = True + + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/tasks/AppTasks/CreateAppTask", + status_code=200, + json={"id": 1, "title": "Test Action"}, + ) + + service.create(title="Test Action", app_key="test-app-key", data={}) + + create_request = [ + r for r in httpx_mock.get_requests() if "CreateAppTask" in str(r.url) + ][0] + task_source = json.loads(create_request.content)["taskSource"] + assert task_source["isDebug"] is True + assert task_source["solutionId"] == "sol-1" + + def test_create_no_isdebug_task_source_when_not_debug( + self, + httpx_mock: HTTPXMock, + service: TasksService, + base_url: str, + org: str, + tenant: str, + monkeypatch: pytest.MonkeyPatch, + reset_uipath_config, + ) -> None: + monkeypatch.setenv("UIPATH_PROJECT_ID", "proj-1") + monkeypatch.setenv("UIPATH_TRACE_ID", "trace-1") + # is_rooted_to_debug_job defaults to False (no override, no internal arg). + + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/tasks/AppTasks/CreateAppTask", + status_code=200, + json={"id": 1, "title": "Test Action"}, + ) + + service.create(title="Test Action", app_key="test-app-key", data={}) + + create_request = [ + r for r in httpx_mock.get_requests() if "CreateAppTask" in str(r.url) + ][0] + task_source = json.loads(create_request.content)["taskSource"] + assert "isDebug" not in task_source + def test_create_with_assignee( self, httpx_mock: HTTPXMock, @@ -862,3 +1010,17 @@ async def test_create_quickform_async_with_assignee_triggers_assign_call( await qf_runner_async(assignee="user@example.com") body = _posted_body(httpx_mock, qf_assign_url) assert body["taskAssignments"][0]["UserNameOrEmail"] == "user@example.com" + + +@pytest.mark.parametrize( + "app_type,expected", + [ + ("Custom", True), + ("Coded", False), + (None, False), + ("", False), + ("custom", False), # case-sensitive + ], +) +def test_is_low_code_app(app_type: Any, expected: bool) -> None: + assert is_low_code_app(app_type) is expected diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 295f77671..80451fb6f 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.2.13" +version = "0.2.14" source = { editable = "." } dependencies = [ { name = "anyio" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 9c5cf81ca..869eff75a 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "uipath" -version = "2.13.16" +version = "2.13.17" 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" dependencies = [ "uipath-core>=0.5.30, <0.6.0", "uipath-runtime>=0.12.2, <0.13.0", - "uipath-platform>=0.2.4, <0.3.0", + "uipath-platform>=0.2.14, <0.3.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", diff --git a/packages/uipath/src/uipath/agent/models/agent.py b/packages/uipath/src/uipath/agent/models/agent.py index 86a0de739..0f1179434 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -830,6 +830,8 @@ class AgentEscalationChannelProperties(BaseEscalationChannelProperties): app_name: str | None = Field(default=None, alias="appName") app_version: int = Field(..., alias="appVersion") + app_type: str | None = Field(default=None, alias="appType") + action_schema: Optional[Any] = Field(default=None, alias="actionSchema") folder_name: Optional[str] = Field(None, alias="folderName") resource_key: str | None = Field(default=None, alias="resourceKey") diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 7af888cf9..c7ddd2360 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2598,7 +2598,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.13.16" +version = "2.13.17" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2741,7 +2741,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.2.13" +version = "0.2.14" source = { editable = "../uipath-platform" } dependencies = [ { name = "anyio" }, From 9942649ff784eada2d3b217b705a4669aafdca2f Mon Sep 17 00:00:00 2001 From: Sandeepan-Ghosh-0312 Date: Fri, 31 Jul 2026 03:09:00 +0530 Subject: [PATCH 2/2] feat: jit changes for escalation apps --- .../platform/action_center/_tasks_service.py | 134 ++++--- .../uipath/platform/action_center/tasks.py | 7 - .../src/uipath/platform/common/_config.py | 9 - .../src/uipath/platform/orchestrator/job.py | 1 - .../tests/common/test_config_env_vars.py | 36 -- .../tests/services/test_actions_service.py | 340 +++++++++--------- .../uipath/src/uipath/agent/models/agent.py | 2 - 7 files changed, 254 insertions(+), 275 deletions(-) diff --git a/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py b/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py index c6d4fa981..95d3dbc91 100644 --- a/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py +++ b/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py @@ -3,6 +3,7 @@ import uuid from typing import Any, Dict, List, Optional +from uipath.core.feature_flags import FeatureFlags from uipath.core.tracing import traced from uipath.platform.constants import ( @@ -17,7 +18,9 @@ from ..common._folder_context import FolderContext, header_folder from ..common._models import Endpoint, RequestSpec from .task_schema import TaskSchema -from .tasks import Task, TaskRecipient, TaskRecipientType, is_low_code_app +from .tasks import Task, TaskRecipient, TaskRecipientType + +_JIT_ESCALATION_APPS_FEATURE_FLAG = "EnableJITEscalationApps" def _ensure_string_value(value: Any) -> str: @@ -27,6 +30,24 @@ def _ensure_string_value(value: Any) -> str: return str(value) if value else "" +def _is_jit_debug_app_task(app_name: Optional[str], app_key: Optional[str]) -> bool: + """Return whether this app task must be created just-in-time (JIT). + + During a debug run an app task may target an app that is not deployed yet, + so neither an app key nor an action schema can be resolved from the + deployed-apps endpoint. Such a task is instead created with the app *name* + and folder path, and Action Center resolves the app itself. + + Gated on the ``EnableJITEscalationApps`` feature flag. An explicit + ``app_key`` always wins, since the caller already knows the deployed app. + """ + if FeatureFlags.is_flag_enabled(_JIT_ESCALATION_APPS_FEATURE_FLAG, default=False): + if app_key or not app_name: + return False + return UiPathConfig.is_studio_project + return False + + def _create_spec( data: Optional[Dict[str, Any]], action_schema: Optional[TaskSchema], @@ -39,8 +60,7 @@ def _create_spec( is_actionable_message_enabled: Optional[bool] = None, actionable_message_metadata: Optional[Dict[str, Any]] = None, source_name: str = "Agent", - app_project_key: Optional[str] = None, - app_type: Optional[str] = None, + is_debug: bool = False, ) -> RequestSpec: field_list = [] outcome_list = [] @@ -96,7 +116,7 @@ def _create_spec( ) json_payload: Dict[str, Any] = { - "appId": app_key if app_key is not None else f"{uuid.UUID(int=0).hex}", + "appId": app_key, "title": title, "data": data if data is not None else {}, "actionableMessageMetaData": actionable_message_metadata @@ -121,14 +141,14 @@ def _create_spec( ), } - if app_project_key is not None: - json_payload["appType"] = app_type - json_payload["appProjectKey"] = app_project_key + if is_debug: + json_payload["folderPath"] = app_folder_path _apply_priority_labels_and_actionable_toggle( json_payload, priority, labels, is_actionable_message_enabled ) - _apply_task_source(json_payload, source_name) + _apply_task_source(json_payload, source_name, is_debug=is_debug) + return RequestSpec( method="POST", endpoint=Endpoint("/orchestrator_/tasks/AppTasks/CreateAppTask"), @@ -165,17 +185,16 @@ def _apply_priority_labels_and_actionable_toggle( def _apply_task_source( - payload: Dict[str, Any], - source_name: str, + payload: Dict[str, Any], source_name: str, is_debug: bool = False ) -> None: """Populate ``payload["taskSource"]`` when UiPathConfig has project_id + trace_id. Shared between AppTask and QuickForm spec builders — the taskSource block is - identical for both task types. + identical for both task types. ``is_debug`` marks a JIT task so Action Center + resolves the app from the name and folder path on the payload. """ project_id = UiPathConfig.project_id trace_id = UiPathConfig.trace_id - solution_id = UiPathConfig.studio_solution_id if not (project_id and trace_id): return payload["taskSource"] = { @@ -189,10 +208,8 @@ def _apply_task_source( }, "jobId": UiPathConfig.job_key, } - - if UiPathConfig.is_rooted_to_debug_job: + if is_debug: payload["taskSource"]["isDebug"] = True - payload["taskSource"]["solutionId"] = solution_id def _normalize_priority(priority: str | None) -> str | None: @@ -473,9 +490,6 @@ async def create_async( is_actionable_message_enabled: Optional[bool] = None, actionable_message_metadata: Optional[Dict[str, Any]] = None, source_name: str = "Agent", - app_project_key: Optional[str] = None, - app_type: Optional[str] = None, - action_schema: Optional[Dict[str, Any]] = None, ) -> Task: """Creates a new action asynchronously. @@ -495,9 +509,6 @@ async def create_async( is_actionable_message_enabled: Optional boolean indicating whether actionable notifications are enabled for this task actionable_message_metadata: Optional metadata for the action source_name: The name of the source that created the task. Defaults to 'Agent'. - app_project_key: Optional project key of the app. Used for JIT (debug) task creation so - Orchestrator can resolve a not-yet-deployed app. - app_type: Optional app type ("Custom" or "Coded"), forwarded for JIT (debug) task creation. Returns: Action: The created action object @@ -506,27 +517,23 @@ async def create_async( Exception: If neither app_name nor app_key is provided for app-specific actions """ key: Optional[str] - schema: Optional[TaskSchema] - if app_project_key and is_low_code_app(app_type) and action_schema is not None: - key = app_key - schema = TaskSchema( - key=action_schema["key"], - in_outs=action_schema["inOuts"], - inputs=action_schema["inputs"], - outputs=action_schema["outputs"], - outcomes=action_schema["outcomes"], - ) - elif app_key: - key, schema = app_key, None + action_schema: Optional[TaskSchema] + is_debug = _is_jit_debug_app_task(app_name, app_key) + if is_debug: + key, action_schema = app_name, None else: - key, schema = await self._get_app_key_and_schema_async( - app_name, app_folder_path, app_folder_key + (key, action_schema) = ( + (app_key, None) + if app_key + else await self._get_app_key_and_schema_async( + app_name, app_folder_path, app_folder_key + ) ) spec = _create_spec( title=title, data=data, app_key=key, - action_schema=schema, + action_schema=action_schema, app_folder_key=app_folder_key, app_folder_path=app_folder_path, priority=priority, @@ -534,9 +541,9 @@ async def create_async( is_actionable_message_enabled=is_actionable_message_enabled, actionable_message_metadata=actionable_message_metadata, source_name=source_name, - app_project_key=app_project_key, - app_type=app_type, + is_debug=is_debug, ) + response = await self.request_async( spec.method, spec.endpoint, @@ -576,13 +583,25 @@ def create( is_actionable_message_enabled: Optional[bool] = None, actionable_message_metadata: Optional[Dict[str, Any]] = None, source_name: str = "Agent", - app_project_key: Optional[str] = None, - app_type: Optional[str] = None, - action_schema: Optional[Dict[str, Any]] = None, ) -> Task: """Creates a new task synchronously. - See :meth:`create_async` for parameter docs. + This method creates a new action task in UiPath Orchestrator. The action can be + either app-specific (using app_name or app_key) or a generic action. + + Args: + title: The title of the action + data: Optional dictionary containing input data for the action + app_name: The name of the application (if creating an app-specific action) + app_key: The key of the application (if creating an app-specific action) + app_folder_path: Optional folder path for the action + app_folder_key: Optional folder key for the action + assignee: Optional username or email to assign the task to + priority: Optional priority of the task + labels: Optional list of labels for the task + is_actionable_message_enabled: Optional boolean indicating whether actionable notifications are enabled for this task + actionable_message_metadata: Optional metadata for the action + source_name: The name of the source that created the task. Defaults to 'Agent'. Returns: Action: The created action object @@ -591,27 +610,25 @@ def create( Exception: If neither app_name nor app_key is provided for app-specific actions """ key: Optional[str] - schema: Optional[TaskSchema] - if app_project_key and is_low_code_app(app_type) and action_schema is not None: - key = app_key - schema = TaskSchema( - key=action_schema["key"], - in_outs=action_schema["inOuts"], - inputs=action_schema["inputs"], - outputs=action_schema["outputs"], - outcomes=action_schema["outcomes"], - ) - elif app_key: - key, schema = app_key, None + action_schema: Optional[TaskSchema] + is_debug = _is_jit_debug_app_task(app_name, app_key) + if is_debug: + # The app may not be deployed yet, so there is nothing to resolve: + # send the name and let Action Center resolve the app. + key, action_schema = app_name, None else: - key, schema = self._get_app_key_and_schema( - app_name, app_folder_path, app_folder_key + (key, action_schema) = ( + (app_key, None) + if app_key + else self._get_app_key_and_schema( + app_name, app_folder_path, app_folder_key + ) ) spec = _create_spec( title=title, data=data, app_key=key, - action_schema=schema, + action_schema=action_schema, app_folder_key=app_folder_key, app_folder_path=app_folder_path, priority=priority, @@ -619,8 +636,7 @@ def create( is_actionable_message_enabled=is_actionable_message_enabled, actionable_message_metadata=actionable_message_metadata, source_name=source_name, - app_project_key=app_project_key, - app_type=app_type, + is_debug=is_debug, ) response = self.request( diff --git a/packages/uipath-platform/src/uipath/platform/action_center/tasks.py b/packages/uipath-platform/src/uipath/platform/action_center/tasks.py index ca9a87816..f1a932cb8 100644 --- a/packages/uipath-platform/src/uipath/platform/action_center/tasks.py +++ b/packages/uipath-platform/src/uipath/platform/action_center/tasks.py @@ -6,13 +6,6 @@ from pydantic import BaseModel, ConfigDict, Field, field_serializer -APP_TYPE_LOW_CODE = "Custom" - - -def is_low_code_app(app_type: str | None) -> bool: - """Return True when ``app_type`` denotes a low-code (custom) app.""" - return app_type == APP_TYPE_LOW_CODE - class TaskStatus(enum.IntEnum): """Enum representing possible Task status.""" diff --git a/packages/uipath-platform/src/uipath/platform/common/_config.py b/packages/uipath-platform/src/uipath/platform/common/_config.py index 03ba02f25..549844db8 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_config.py +++ b/packages/uipath-platform/src/uipath/platform/common/_config.py @@ -14,7 +14,6 @@ class UiPathApiConfig(BaseModel): class ConfigurationManager: _instance = None studio_solution_id: str | None = None - _is_debug_run_override: bool | None = None def __new__(cls): if cls._instance is None: @@ -195,15 +194,8 @@ def licensing_context(self) -> str | None: @property def is_rooted_to_debug_job(self) -> bool: """Whether this job, which may be a deployed process, is rooted to a debug session (e.g. Maestro solution debug).""" - if self._is_debug_run_override is not None: - return self._is_debug_run_override return self._read_internal_argument("isDebug") is True - @is_rooted_to_debug_job.setter - def is_rooted_to_debug_job(self, value: bool) -> None: - """Override the debug-run status resolved at runtime.""" - self._is_debug_run_override = value - @property def is_tracing_enabled(self) -> bool: from uipath.platform.constants import ENV_TRACING_ENABLED @@ -213,7 +205,6 @@ def is_tracing_enabled(self) -> bool: def reset(self) -> None: """Reset mutable cached state to defaults.""" self.studio_solution_id = None - self._is_debug_run_override = None # Invalidate cached_property by removing from instance __dict__ self.__dict__.pop("_internal_arguments", None) diff --git a/packages/uipath-platform/src/uipath/platform/orchestrator/job.py b/packages/uipath-platform/src/uipath/platform/orchestrator/job.py index 2e960de54..6464405b4 100644 --- a/packages/uipath-platform/src/uipath/platform/orchestrator/job.py +++ b/packages/uipath-platform/src/uipath/platform/orchestrator/job.py @@ -80,5 +80,4 @@ class Job(BaseModel): has_warnings: Optional[bool] = Field(default=None, alias="HasWarnings") job_error: Optional[JobErrorInfo] = Field(default=None, alias="JobError") folder_key: Optional[str] = Field(default=None, alias="FolderKey") - parent_context: Optional[str] = Field(default=None, alias="ParentContext") id: int = Field(alias="Id") diff --git a/packages/uipath-platform/tests/common/test_config_env_vars.py b/packages/uipath-platform/tests/common/test_config_env_vars.py index b0a0b5062..1e48ac894 100644 --- a/packages/uipath-platform/tests/common/test_config_env_vars.py +++ b/packages/uipath-platform/tests/common/test_config_env_vars.py @@ -190,39 +190,3 @@ def test_has_eval_folder(self, monkeypatch, tmp_path): assert UiPathConfig.has_eval_folder is False (tmp_path / EVALS_FOLDER).mkdir() assert UiPathConfig.has_eval_folder is True - - -class TestIsRootedToDebugJob: - @pytest.fixture(autouse=True) - def _reset_singleton(self): - # is_rooted_to_debug_job mutates the process-wide singleton; reset around - # each test so nothing leaks between tests. - UiPathConfig.reset() - yield - UiPathConfig.reset() - - def test_defaults_to_false_when_unset(self): - assert UiPathConfig.is_rooted_to_debug_job is False - - def test_reads_is_debug_internal_argument(self): - UiPathConfig.__dict__["_internal_arguments"] = {"isDebug": True} - assert UiPathConfig.is_rooted_to_debug_job is True - - def test_setter_override_wins_and_is_true(self): - UiPathConfig.is_rooted_to_debug_job = True - assert UiPathConfig.is_rooted_to_debug_job is True - - def test_setter_override_beats_internal_argument(self): - # Even when the internal argument says True, an explicit False override wins. - UiPathConfig.__dict__["_internal_arguments"] = {"isDebug": True} - UiPathConfig.is_rooted_to_debug_job = False - assert UiPathConfig.is_rooted_to_debug_job is False - - def test_reset_clears_override(self): - UiPathConfig.__dict__["_internal_arguments"] = {"isDebug": True} - UiPathConfig.is_rooted_to_debug_job = False - assert UiPathConfig.is_rooted_to_debug_job is False - UiPathConfig.reset() - # Override cleared, so it falls back to the internal argument again — but - # reset() also drops the cached internal arguments, so it re-reads (None). - assert UiPathConfig.is_rooted_to_debug_job is False diff --git a/packages/uipath-platform/tests/services/test_actions_service.py b/packages/uipath-platform/tests/services/test_actions_service.py index c22dc7eca..b11a40900 100644 --- a/packages/uipath-platform/tests/services/test_actions_service.py +++ b/packages/uipath-platform/tests/services/test_actions_service.py @@ -7,12 +7,7 @@ from uipath.platform import UiPathApiConfig, UiPathExecutionContext from uipath.platform.action_center import Task from uipath.platform.action_center._tasks_service import TasksService -from uipath.platform.action_center.tasks import ( - TaskRecipient, - TaskRecipientType, - is_low_code_app, -) -from uipath.platform.common import UiPathConfig +from uipath.platform.action_center.tasks import TaskRecipient, TaskRecipientType from uipath.platform.constants import HEADER_USER_AGENT @@ -27,14 +22,6 @@ def service( return TasksService(config=config, execution_context=execution_context) -@pytest.fixture -def reset_uipath_config(): - """Reset the ``UiPathConfig`` singleton around a test that mutates it.""" - UiPathConfig.reset() - yield UiPathConfig - UiPathConfig.reset() - - class TestTasksService: def test_retrieve( self, @@ -141,141 +128,6 @@ def test_create_with_app_key( assert action.id == 1 assert action.title == "Test Action" - def test_create_jit_custom_app_uses_provided_schema_and_sends_project_key( - self, - httpx_mock: HTTPXMock, - service: TasksService, - base_url: str, - org: str, - tenant: str, - ) -> None: - # JIT: a not-yet-deployed Custom app supplies its action schema and project - # key, so no deployed-app schema lookup happens and the schema is built from - # the supplied action_schema. app type + project key are sent so Orchestrator - # can resolve the app. - httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/orchestrator_/tasks/AppTasks/CreateAppTask", - status_code=200, - json={"id": 1, "title": "Test Action"}, - ) - - action = service.create( - title="Test Action", - data={"stringInput": "value"}, - app_project_key="proj-key-abc", - app_type="Custom", - action_schema={ - "key": "schema-key", - "inOuts": [], - "inputs": [{"name": "stringInput", "key": "field-1"}], - "outputs": [], - "outcomes": [{"name": "approve", "key": "outcome-1"}], - }, - ) - - assert isinstance(action, Task) - requests = httpx_mock.get_requests() - # No deployed-app schema resolution — the provided action_schema is used. - assert all("deployed-action-apps-schemas" not in str(r.url) for r in requests) - create_request = [r for r in requests if "CreateAppTask" in str(r.url)][0] - body = json.loads(create_request.content) - assert body["appType"] == "Custom" - assert body["appProjectKey"] == "proj-key-abc" - # Field set is derived from the supplied action_schema, not a lookup. - field_names = [ - f["Name"] for f in body["actionableMessageMetaData"]["fieldSet"]["fields"] - ] - assert "stringInput" in field_names - - def test_create_omits_project_key_when_not_provided( - self, - httpx_mock: HTTPXMock, - service: TasksService, - base_url: str, - org: str, - tenant: str, - ) -> None: - # Non-JIT (deployed) path: no app_project_key, so appProjectKey is not sent. - httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/orchestrator_/tasks/AppTasks/CreateAppTask", - status_code=200, - json={"id": 1, "title": "Test Action"}, - ) - - action = service.create( - title="Test Action", - app_key="test-app-key", - data={"test": "data"}, - ) - - assert isinstance(action, Task) - create_request = [ - r for r in httpx_mock.get_requests() if "CreateAppTask" in str(r.url) - ][0] - body = json.loads(create_request.content) - assert "appProjectKey" not in body - assert "appType" not in body - - def test_create_stamps_isdebug_task_source_from_config( - self, - httpx_mock: HTTPXMock, - service: TasksService, - base_url: str, - org: str, - tenant: str, - monkeypatch: pytest.MonkeyPatch, - reset_uipath_config, - ) -> None: - # isDebug + solutionId on taskSource are driven purely by UiPathConfig, not a - # per-call flag. taskSource requires project_id + trace_id to be present. - monkeypatch.setenv("UIPATH_PROJECT_ID", "proj-1") - monkeypatch.setenv("UIPATH_TRACE_ID", "trace-1") - reset_uipath_config.studio_solution_id = "sol-1" - reset_uipath_config.is_rooted_to_debug_job = True - - httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/orchestrator_/tasks/AppTasks/CreateAppTask", - status_code=200, - json={"id": 1, "title": "Test Action"}, - ) - - service.create(title="Test Action", app_key="test-app-key", data={}) - - create_request = [ - r for r in httpx_mock.get_requests() if "CreateAppTask" in str(r.url) - ][0] - task_source = json.loads(create_request.content)["taskSource"] - assert task_source["isDebug"] is True - assert task_source["solutionId"] == "sol-1" - - def test_create_no_isdebug_task_source_when_not_debug( - self, - httpx_mock: HTTPXMock, - service: TasksService, - base_url: str, - org: str, - tenant: str, - monkeypatch: pytest.MonkeyPatch, - reset_uipath_config, - ) -> None: - monkeypatch.setenv("UIPATH_PROJECT_ID", "proj-1") - monkeypatch.setenv("UIPATH_TRACE_ID", "trace-1") - # is_rooted_to_debug_job defaults to False (no override, no internal arg). - - httpx_mock.add_response( - url=f"{base_url}{org}{tenant}/orchestrator_/tasks/AppTasks/CreateAppTask", - status_code=200, - json={"id": 1, "title": "Test Action"}, - ) - - service.create(title="Test Action", app_key="test-app-key", data={}) - - create_request = [ - r for r in httpx_mock.get_requests() if "CreateAppTask" in str(r.url) - ][0] - task_source = json.loads(create_request.content)["taskSource"] - assert "isDebug" not in task_source - def test_create_with_assignee( self, httpx_mock: HTTPXMock, @@ -1012,15 +864,181 @@ async def test_create_quickform_async_with_assignee_triggers_assign_call( assert body["taskAssignments"][0]["UserNameOrEmail"] == "user@example.com" -@pytest.mark.parametrize( - "app_type,expected", - [ - ("Custom", True), - ("Coded", False), - (None, False), - ("", False), - ("custom", False), # case-sensitive - ], -) -def test_is_low_code_app(app_type: Any, expected: bool) -> None: - assert is_low_code_app(app_type) is expected +# --------------------------------------------------------------------------- +# JIT (debug) app task tests +# --------------------------------------------------------------------------- + +_JIT_FLAG_ENV = "UIPATH_FEATURE_EnableJITEscalationApps" +_APP_SCHEMAS_PATH = "deployed-action-apps-schemas" + + +@pytest.fixture +def create_task_url(base_url: str, org: str, tenant: str) -> str: + return f"{base_url}{org}{tenant}/orchestrator_/tasks/AppTasks/CreateAppTask" + + +@pytest.fixture +def jit_debug_env(monkeypatch: pytest.MonkeyPatch) -> None: + """Enable the JIT flag and place the process in a Studio debug run.""" + monkeypatch.setenv(_JIT_FLAG_ENV, "true") + monkeypatch.setenv("UIPATH_PROJECT_ID", "project-1") + monkeypatch.setenv("UIPATH_TRACE_ID", "trace-1") + monkeypatch.setenv("UIPATH_TENANT_ID", "test-tenant-id") + + +def _mock_create_task(httpx_mock: HTTPXMock, create_task_url: str) -> None: + httpx_mock.add_response( + url=create_task_url, status_code=200, json={"id": 1, "title": "Test Action"} + ) + + +def _requested_app_schemas(httpx_mock: HTTPXMock) -> bool: + return any(_APP_SCHEMAS_PATH in str(r.url) for r in httpx_mock.get_requests()) + + +def test_create_jit_sends_app_name_and_folder_path_without_resolving( + httpx_mock: HTTPXMock, + service: TasksService, + create_task_url: str, + jit_debug_env: None, +) -> None: + _mock_create_task(httpx_mock, create_task_url) + + task = service.create( + title="Test Action", + app_name="my-inline-app", + app_folder_path="Shared/Apps", + data={"test": "data"}, + ) + + assert isinstance(task, Task) + body = _posted_body(httpx_mock, create_task_url) + # The app may not be deployed yet: the name is sent in place of a key and no + # deployed-apps lookup happens. + assert body["appId"] == "my-inline-app" + assert body["folderPath"] == "Shared/Apps" + assert body["taskSource"]["isDebug"] is True + assert not _requested_app_schemas(httpx_mock) + + +async def test_create_async_jit_sends_app_name_and_folder_path_without_resolving( + httpx_mock: HTTPXMock, + service: TasksService, + create_task_url: str, + jit_debug_env: None, +) -> None: + _mock_create_task(httpx_mock, create_task_url) + + task = await service.create_async( + title="Test Action", + app_name="my-inline-app", + app_folder_path="Shared/Apps", + ) + + assert isinstance(task, Task) + body = _posted_body(httpx_mock, create_task_url) + assert body["appId"] == "my-inline-app" + assert body["folderPath"] == "Shared/Apps" + assert body["taskSource"]["isDebug"] is True + assert not _requested_app_schemas(httpx_mock) + + +def test_create_jit_carries_no_action_schema( + httpx_mock: HTTPXMock, + service: TasksService, + create_task_url: str, + jit_debug_env: None, +) -> None: + _mock_create_task(httpx_mock, create_task_url) + + service.create( + title="Test Action", + app_name="my-inline-app", + app_folder_path="Shared/Apps", + data={"test": "data"}, + ) + + # Action Center builds the fields from the app it resolves, so nothing is + # derived from a schema here. + body = _posted_body(httpx_mock, create_task_url) + assert body["actionableMessageMetaData"] == {} + assert body["data"] == {"test": "data"} + + +def test_create_skips_jit_when_flag_disabled( + httpx_mock: HTTPXMock, + service: TasksService, + create_task_url: str, + jit_debug_env: None, + monkeypatch: pytest.MonkeyPatch, + base_url: str, + org: str, +) -> None: + monkeypatch.setenv(_JIT_FLAG_ENV, "false") + httpx_mock.add_response( + url=f"{base_url}{org}/apps_/default/api/v1/default/{_APP_SCHEMAS_PATH}?search=my-app&filterByDeploymentTitle=true", + status_code=200, + json={"deployed": [_make_deployed_app("my-app", "Shared/Apps", "folder-key")]}, + ) + _mock_create_task(httpx_mock, create_task_url) + + service.create( + title="Test Action", + app_name="my-app", + app_folder_path="Shared/Apps", + ) + + body = _posted_body(httpx_mock, create_task_url) + assert body["appId"] == "my-app" # resolved systemName, not the JIT passthrough + assert "folderPath" not in body + assert "isDebug" not in body["taskSource"] + assert _requested_app_schemas(httpx_mock) + + +def test_create_skips_jit_when_not_a_studio_project( + httpx_mock: HTTPXMock, + service: TasksService, + create_task_url: str, + jit_debug_env: None, + monkeypatch: pytest.MonkeyPatch, + base_url: str, + org: str, +) -> None: + monkeypatch.delenv("UIPATH_PROJECT_ID") + httpx_mock.add_response( + url=f"{base_url}{org}/apps_/default/api/v1/default/{_APP_SCHEMAS_PATH}?search=my-app&filterByDeploymentTitle=true", + status_code=200, + json={"deployed": [_make_deployed_app("my-app", "Shared/Apps", "folder-key")]}, + ) + _mock_create_task(httpx_mock, create_task_url) + + service.create( + title="Test Action", + app_name="my-app", + app_folder_path="Shared/Apps", + ) + + assert _requested_app_schemas(httpx_mock) + assert "folderPath" not in _posted_body(httpx_mock, create_task_url) + + +def test_create_skips_jit_when_app_key_is_given( + httpx_mock: HTTPXMock, + service: TasksService, + create_task_url: str, + jit_debug_env: None, +) -> None: + _mock_create_task(httpx_mock, create_task_url) + + service.create( + title="Test Action", + app_name="my-app", + app_key="test-app-key", + app_folder_path="Shared/Apps", + ) + + # An explicit key means the caller already knows the deployed app. + body = _posted_body(httpx_mock, create_task_url) + assert body["appId"] == "test-app-key" + assert "folderPath" not in body + assert "isDebug" not in body["taskSource"] diff --git a/packages/uipath/src/uipath/agent/models/agent.py b/packages/uipath/src/uipath/agent/models/agent.py index 0f1179434..86a0de739 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -830,8 +830,6 @@ class AgentEscalationChannelProperties(BaseEscalationChannelProperties): app_name: str | None = Field(default=None, alias="appName") app_version: int = Field(..., alias="appVersion") - app_type: str | None = Field(default=None, alias="appType") - action_schema: Optional[Any] = Field(default=None, alias="actionSchema") folder_name: Optional[str] = Field(None, alias="folderName") resource_key: str | None = Field(default=None, alias="resourceKey")