From 0abfa3e6822a1e82e09e231995a4aa10cd43de33 Mon Sep 17 00:00:00 2001 From: Sandeepan-Ghosh-0312 <134266678+Sandeepan-Ghosh-0312@users.noreply.github.com> Date: Tue, 15 Sep 2026 23:59:33 +0530 Subject: [PATCH] fix(action-center): strip solution_folder for inline guardrail escalation project --- packages/uipath/pyproject.toml | 2 +- .../uipath/src/uipath/agent/models/agent.py | 21 +++++- .../uipath/tests/agent/models/test_agent.py | 70 ++++++++++++++++++- packages/uipath/uv.lock | 2 +- 4 files changed, 90 insertions(+), 5 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 05594e827..1b5e14a83 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.14.12" +version = "2.14.13" 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" diff --git a/packages/uipath/src/uipath/agent/models/agent.py b/packages/uipath/src/uipath/agent/models/agent.py index 359d05f2a..bce611d32 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -81,6 +81,23 @@ def normalizer(v: Any) -> Any: return BeforeValidator(normalizer) +# solution_folder placeholder needs to be converted to None so that inline guardrail escalations can be resolved just-in-time +_SOLUTION_LOCAL_FOLDER_PLACEHOLDERS = frozenset({"solution_folder", ".", ""}) + + +def _normalize_solution_local_folder(value: Any) -> Any: + """Map a solution-local folder placeholder onto ``None``.""" + if isinstance(value, str) and value.strip() in _SOLUTION_LOCAL_FOLDER_PLACEHOLDERS: + return None + return value + + +SolutionLocalFolder = Annotated[ + Optional[str], BeforeValidator(_normalize_solution_local_folder) +] +"""A folder field that may carry the folder name or solution-folder placeholder which is parsed as ``None``.""" + + class CaseInsensitiveEnum(Enum): """Base class for case-insensitive enums.""" @@ -830,7 +847,7 @@ class AgentEscalationChannelProperties(BaseEscalationChannelProperties): app_name: str | None = Field(default=None, alias="appName") app_version: int = Field(..., alias="appVersion") - folder_name: Optional[str] = Field(None, alias="folderName") + folder_name: SolutionLocalFolder = Field(None, alias="folderName") resource_key: str | None = Field(default=None, alias="resourceKey") @@ -1216,7 +1233,7 @@ class AgentGuardrailEscalateActionApp(BaseModel): version: int name: str folder_id: Optional[str] = Field(None, alias="folderId") - folder_name: str = Field(alias="folderName") + folder_name: SolutionLocalFolder = Field(None, alias="folderName") app_process_key: Optional[str] = Field(None, alias="appProcessKey") runtime: Optional[str] = None model_config = ConfigDict(populate_by_name=True, extra="allow") diff --git a/packages/uipath/tests/agent/models/test_agent.py b/packages/uipath/tests/agent/models/test_agent.py index ec0a6351f..44ca8813a 100644 --- a/packages/uipath/tests/agent/models/test_agent.py +++ b/packages/uipath/tests/agent/models/test_agent.py @@ -768,7 +768,7 @@ def test_agent_config_loads_guardrails(self): assert escalate_action.action_type == "escalate" assert escalate_action.app.id == "cf4cb73d-7310-49b1-9a9e-e7653dad7f4e" assert escalate_action.app.name == "-Guardrail Form" - assert escalate_action.app.folder_name == "solution_folder" + assert escalate_action.app.folder_name is None assert escalate_action.recipient.type == AgentEscalationRecipientType.USER_ID assert escalate_action.recipient.value == "5f872639-fc71-4a50-b17d-f68eb357b436" assert escalate_action.recipient.display_name == "User Name" @@ -4778,3 +4778,71 @@ def test_native_bag_survives_verbatim_by_alias(self): self._agent_settings(modelSettings=native) ) assert settings.model_dump(by_alias=True)["modelSettings"] == native + + +class TestSolutionLocalFolderNormalization: + """A solution-local resource has no real folder; Studio Web stores a placeholder. + + Forwarding that placeholder as a folder makes Action Center resolve an app in a + folder that does not exist, so it is normalized to None at parse time. + """ + + @staticmethod + def _channel(folder_name: Any) -> dict[str, Any]: + return { + "name": "Channel", + "description": "", + "inputSchema": {"type": "object", "properties": {}}, + "type": "actionCenter", + "recipients": [], + "properties": { + "appName": "SimpleApprovalApp", + "appVersion": 1, + "folderName": folder_name, + }, + } + + @staticmethod + def _guardrail_app(folder_name: Any) -> dict[str, Any]: + return { + "$actionType": "escalate", + "app": { + "id": "812e9239-4cb9-4bd9-9479-bf4f35731e8f", + "version": 1, + "name": "Guardrail Escalation Action App 1", + "folderId": "d8fa328c-cf2e-fae9-2818-da090419aba3", + "folderName": folder_name, + }, + "recipient": {"type": 1, "value": "user-id", "displayName": "user"}, + } + + @pytest.mark.parametrize("placeholder", ["solution_folder", "", ".", " "]) + def test_escalation_channel_placeholder_becomes_none(self, placeholder: str): + channel = TypeAdapter(AgentEscalationChannel).validate_python( + self._channel(placeholder) + ) + assert channel.properties.folder_name is None + + @pytest.mark.parametrize("placeholder", ["solution_folder", "", ".", " "]) + def test_guardrail_escalate_app_placeholder_becomes_none(self, placeholder: str): + action = AgentGuardrailEscalateAction.model_validate( + self._guardrail_app(placeholder) + ) + assert action.app.folder_name is None + + def test_guardrail_escalate_app_folder_name_is_optional(self): + """Definitions written before the placeholder was dropped omit it entirely.""" + payload = self._guardrail_app("solution_folder") + del payload["app"]["folderName"] + action = AgentGuardrailEscalateAction.model_validate(payload) + assert action.app.folder_name is None + + def test_real_folder_survives(self): + channel = TypeAdapter(AgentEscalationChannel).validate_python( + self._channel("Shared/Apps") + ) + action = AgentGuardrailEscalateAction.model_validate( + self._guardrail_app("Shared") + ) + assert channel.properties.folder_name == "Shared/Apps" + assert action.app.folder_name == "Shared" diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 9c2e03144..397757c56 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2599,7 +2599,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.14.12" +version = "2.14.13" source = { editable = "." } dependencies = [ { name = "applicationinsights" },