From dcceeaa3b553f410e0a281e6d90f204398c58ce6 Mon Sep 17 00:00:00 2001 From: cotovanu-cristian Date: Mon, 8 Jun 2026 17:17:16 +0300 Subject: [PATCH 1/3] feat(agent): add QuickForm escalation channel models Introduce a discriminated-union hierarchy for escalation channels (BaseEscalationChannelProperties, AgentActionCenterEscalationChannel, AgentQuickFormEscalationChannel) so QuickForm escalations carry their FormLib schema instead of an Action Center app reference. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/uipath/pyproject.toml | 2 +- .../uipath/src/uipath/agent/models/agent.py | 104 ++++++++++++------ .../uipath/tests/agent/models/test_agent.py | 60 +++++++++- packages/uipath/uv.lock | 2 +- 4 files changed, 131 insertions(+), 37 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index c4da7e297..2e490ade3 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.78" +version = "2.10.79" 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 5aa959e8f..33aa169e0 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -142,6 +142,14 @@ class AgentEscalationRecipientType(str, CaseInsensitiveEnum): ARGUMENT_GROUP_NAME = "ArgumentGroupName" +class AgentEscalationChannelType(str, CaseInsensitiveEnum): + """Agent escalation channel type enumeration.""" + + ACTION_CENTER = "actionCenter" + ACTION_CENTER_QUICK_FORM = "actionCenterQuickForm" + UNKNOWN = "unknown" # fallback branch discriminator + + class AgentContextRetrievalMode(str, CaseInsensitiveEnum): """Agent context retrieval mode enumeration.""" @@ -691,13 +699,9 @@ def _resolve_task_title(v: Any) -> Any: return v -class AgentEscalationChannelProperties(BaseResourceProperties): - """Agent escalation channel properties model.""" +class BaseEscalationChannelProperties(BaseResourceProperties): + """Fields shared by every escalation channel's properties.""" - app_name: str | None = Field(default=None, alias="appName") - app_version: int = Field(..., alias="appVersion") - folder_name: Optional[str] = Field(None, alias="folderName") - resource_key: str | None = Field(default=None, alias="resourceKey") is_actionable_message_enabled: Optional[bool] = Field( None, alias="isActionableMessageEnabled" ) @@ -706,12 +710,31 @@ class AgentEscalationChannelProperties(BaseResourceProperties): ) -class AgentEscalationChannel(BaseCfg): - """Agent escalation channel model.""" +class AgentEscalationChannelProperties(BaseEscalationChannelProperties): + """Action Center app-task channel properties (channel type ``actionCenter``).""" + + app_name: str | None = Field(default=None, alias="appName") + app_version: int = Field(..., alias="appVersion") + folder_name: Optional[str] = Field(None, alias="folderName") + resource_key: str | None = Field(default=None, alias="resourceKey") + + +class AgentQuickFormChannelProperties(BaseEscalationChannelProperties): + """Quick Form channel properties (channel type ``actionCenterQuickForm``).""" + + schema: Dict[str, Any] = Field(...) # type: ignore[assignment] + + @property + def schema_id(self) -> str | None: + """Return the schema id nested inside schema.""" + return self.schema.get("schemaId") + + +class BaseAgentEscalationChannel(BaseCfg): + """Fields shared by every escalation channel variant.""" id: Optional[str] = Field(None, alias="id") name: str = Field(..., alias="name") - type: str = Field(alias="type") description: str = Field(..., alias="description") input_schema: Dict[str, Any] = Field(..., alias="inputSchema") output_schema: Dict[str, Any] = Field(EMPTY_SCHEMA, alias="outputSchema") @@ -719,16 +742,12 @@ class AgentEscalationChannel(BaseCfg): {}, alias="argumentProperties" ) outcome_mapping: Optional[Dict[str, str]] = Field(None, alias="outcomeMapping") - properties: AgentEscalationChannelProperties = Field(..., alias="properties") recipients: List[AgentEscalationRecipient] = Field(..., alias="recipients") task_title: Optional[Union[str, TaskTitle]] = Field( default="Escalation Task", alias="taskTitle" ) priority: Optional[str] = None labels: List[str] = Field(default_factory=list) - # schema_body avoids shadowing pydantic.BaseModel.schema(); JSON alias stays "schema". - schema_id: Optional[str] = Field(None, alias="schemaId") - schema_body: Optional[Dict[str, Any]] = Field(None, alias="schema") @model_validator(mode="before") @classmethod @@ -737,6 +756,46 @@ def _apply_task_title_resolution(cls, v: Any) -> Any: return _resolve_task_title(v) +class AgentActionCenterEscalationChannel(BaseAgentEscalationChannel): + """Action Center app-task escalation channel.""" + + type: Literal[AgentEscalationChannelType.ACTION_CENTER] = Field( + default=AgentEscalationChannelType.ACTION_CENTER, alias="type" + ) + properties: AgentEscalationChannelProperties = Field(..., alias="properties") + + +class AgentQuickFormEscalationChannel(BaseAgentEscalationChannel): + """Quick Form escalation channel; FormLib schema lives in ``properties.schema``.""" + + type: Literal[AgentEscalationChannelType.ACTION_CENTER_QUICK_FORM] = Field( + default=AgentEscalationChannelType.ACTION_CENTER_QUICK_FORM, alias="type" + ) + properties: AgentQuickFormChannelProperties = Field(..., alias="properties") + + +class AgentUnknownEscalationChannel(BaseAgentEscalationChannel): + """Fallback for unknown or future escalation channel types.""" + + type: Literal[AgentEscalationChannelType.UNKNOWN] = Field( + default=AgentEscalationChannelType.UNKNOWN, alias="type" + ) + properties: BaseEscalationChannelProperties = Field( + default_factory=BaseEscalationChannelProperties, alias="properties" + ) + + +AgentEscalationChannel = Annotated[ + Union[ + AgentActionCenterEscalationChannel, + AgentQuickFormEscalationChannel, + AgentUnknownEscalationChannel, + ], + Field(discriminator="type"), + _case_insensitive_enum_validator("type", AgentEscalationChannelType), +] + + class AgentEscalationResourceConfig(BaseAgentResourceConfig): """Agent escalation resource configuration model.""" @@ -772,23 +831,6 @@ class AgentIxpVsEscalationResourceConfig(BaseAgentResourceConfig): ) -class AgentQuickFormEscalationResourceConfig(BaseAgentResourceConfig): - """Quick Form Agent escalation resource configuration model (escalationType=2). - - Quick Form escalations render a schema-first HITL task in Action Center via FormLib. - The schema (and its key) live on the channel (see AgentEscalationChannel.schema_id / - schema) and are sent inline to Orchestrator's GenericTasks/CreateTask endpoint. - """ - - id: Optional[str] = Field(None, alias="id") - resource_type: Literal[AgentResourceType.ESCALATION] = Field( - alias="$resourceType", default=AgentResourceType.ESCALATION, frozen=True - ) - channels: List[AgentEscalationChannel] = Field(alias="channels") - is_agent_memory_enabled: bool = Field(default=False, alias="isAgentMemoryEnabled") - escalation_type: Literal[2] = Field(default=2, alias="escalationType") - - class BaseAgentToolResourceConfig(BaseAgentResourceConfig): """Base agent tool resource configuration model.""" @@ -994,11 +1036,11 @@ class AgentUnknownToolResourceConfig(BaseAgentToolResourceConfig): Field(discriminator="type"), ] + EscalationResourceConfig = Annotated[ Union[ Annotated[AgentEscalationResourceConfig, Tag(0)], Annotated[AgentIxpVsEscalationResourceConfig, Tag(1)], - Annotated[AgentQuickFormEscalationResourceConfig, Tag(2)], ], Discriminator(lambda v: v.get("escalation_type") or v.get("escalationType") or 0), ] diff --git a/packages/uipath/tests/agent/models/test_agent.py b/packages/uipath/tests/agent/models/test_agent.py index fcfefa946..9991711d1 100644 --- a/packages/uipath/tests/agent/models/test_agent.py +++ b/packages/uipath/tests/agent/models/test_agent.py @@ -5,6 +5,7 @@ from uipath.agent.models.agent import ( AgentA2aResourceConfig, + AgentActionCenterEscalationChannel, AgentBooleanOperator, AgentBooleanRule, AgentBuiltInValidatorGuardrail, @@ -14,7 +15,6 @@ AgentContextType, AgentCustomGuardrail, AgentDefinition, - AgentEscalationChannel, AgentEscalationRecipient, AgentEscalationRecipientType, AgentEscalationResourceConfig, @@ -36,6 +36,7 @@ AgentNumberOperator, AgentNumberRule, AgentProcessToolResourceConfig, + AgentQuickFormChannelProperties, AgentResourceType, AgentToolArgumentPropertiesVariant, AgentToolType, @@ -2607,11 +2608,62 @@ def test_agent_with_ixp_vs_escalation(self): assert len(channel.recipients) == 0 # Validate channel properties + assert isinstance(channel, AgentActionCenterEscalationChannel) assert channel.properties.app_name is None assert channel.properties.app_version == 1 assert channel.properties.folder_name is None assert channel.properties.resource_key is None + def test_quick_form_channel_properties_derive_schema_id_from_body(self): + """schema_id reads the schemaId nested inside the schema body.""" + + props = AgentQuickFormChannelProperties.model_validate( + { + "schema": { + "schemaId": "e74ebb74-80ba-47b9-a370-532a1ba4c41e", + "fields": [], + "outcomes": [], + }, + } + ) + assert props.schema_id == "e74ebb74-80ba-47b9-a370-532a1ba4c41e" + + def test_quick_form_channel_properties_schema_id_none_when_absent(self): + """schema_id is None when the schema body carries no schemaId.""" + + props = AgentQuickFormChannelProperties.model_validate( + {"schema": {"fields": [], "outcomes": []}} + ) + assert props.schema_id is None + + def test_quick_form_channel_properties_require_schema(self): + with pytest.raises(ValidationError): + AgentQuickFormChannelProperties.model_validate( + {"isActionableMessageEnabled": False} + ) + + def test_quick_form_channel_requires_schema(self): + """A quick-form channel without a schema fails to parse.""" + with pytest.raises(ValidationError): + TypeAdapter(AgentEscalationResourceConfig).validate_python( + { + "$resourceType": "escalation", + "name": "Escalation", + "description": "", + "channels": [ + { + "name": "c", + "description": "", + "inputSchema": {"type": "object", "properties": {}}, + "type": "actionCenterQuickForm", + "recipients": [], + "properties": {"isActionableMessageEnabled": False}, + } + ], + "isAgentMemoryEnabled": False, + } + ) + def test_task_title_text_builder_type(self): """Test TextBuilderTaskTitle with tokens.""" from uipath.agent.models.agent import ( @@ -2664,7 +2716,7 @@ def test_escalation_channel_uses_task_title_v2_when_present(self): }, } - channel = AgentEscalationChannel(**channel_data) # type: ignore[arg-type] + channel = AgentActionCenterEscalationChannel(**channel_data) # type: ignore[arg-type] assert isinstance(channel.task_title, dict) or hasattr( channel.task_title, "tokens" @@ -2688,7 +2740,7 @@ def test_escalation_channel_uses_legacy_task_title(self): "taskTitle": "Legacy Task Title", } - channel = AgentEscalationChannel(**channel_data) # type: ignore[arg-type] + channel = AgentActionCenterEscalationChannel(**channel_data) # type: ignore[arg-type] assert channel.task_title == "Legacy Task Title" @@ -2709,7 +2761,7 @@ def test_escalation_channel_defaults_to_escalation_task(self): "recipients": [], } - channel = AgentEscalationChannel(**channel_data) # type: ignore[arg-type] + channel = AgentActionCenterEscalationChannel(**channel_data) # type: ignore[arg-type] assert channel.task_title == "Escalation Task" diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 87b11124e..8acee32f9 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.78" +version = "2.10.79" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From 3ac8debce488a157ea695a86c0e2d99f2708de65 Mon Sep 17 00:00:00 2001 From: cotovanu-cristian Date: Mon, 8 Jun 2026 19:26:07 +0300 Subject: [PATCH 2/3] fix(agent): drop unknown escalation channel fallback; rename schema field Address PR review: - Remove AgentUnknownEscalationChannel and the UNKNOWN channel-type enum member. The runtime cannot execute an unrecognized escalation channel, so an unknown type is now rejected at parse time rather than silently routed to a fallback (Field(discriminator="type") raises on unknown values). - Rename AgentQuickFormChannelProperties.schema -> form_schema (alias "schema") to stop shadowing BaseModel.schema(); drops the type: ignore. - Lock the intended break with a test asserting unknown channel types fail. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../uipath/src/uipath/agent/models/agent.py | 21 ++++-------------- .../uipath/tests/agent/models/test_agent.py | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/uipath/src/uipath/agent/models/agent.py b/packages/uipath/src/uipath/agent/models/agent.py index 33aa169e0..2d280a66b 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -147,7 +147,6 @@ class AgentEscalationChannelType(str, CaseInsensitiveEnum): ACTION_CENTER = "actionCenter" ACTION_CENTER_QUICK_FORM = "actionCenterQuickForm" - UNKNOWN = "unknown" # fallback branch discriminator class AgentContextRetrievalMode(str, CaseInsensitiveEnum): @@ -722,12 +721,12 @@ class AgentEscalationChannelProperties(BaseEscalationChannelProperties): class AgentQuickFormChannelProperties(BaseEscalationChannelProperties): """Quick Form channel properties (channel type ``actionCenterQuickForm``).""" - schema: Dict[str, Any] = Field(...) # type: ignore[assignment] + form_schema: Dict[str, Any] = Field(..., alias="schema") @property def schema_id(self) -> str | None: - """Return the schema id nested inside schema.""" - return self.schema.get("schemaId") + """Return the schema id nested inside the form schema body.""" + return self.form_schema.get("schemaId") class BaseAgentEscalationChannel(BaseCfg): @@ -766,7 +765,7 @@ class AgentActionCenterEscalationChannel(BaseAgentEscalationChannel): class AgentQuickFormEscalationChannel(BaseAgentEscalationChannel): - """Quick Form escalation channel; FormLib schema lives in ``properties.schema``.""" + """Quick Form escalation channel; FormLib schema lives in ``properties.form_schema``.""" type: Literal[AgentEscalationChannelType.ACTION_CENTER_QUICK_FORM] = Field( default=AgentEscalationChannelType.ACTION_CENTER_QUICK_FORM, alias="type" @@ -774,22 +773,10 @@ class AgentQuickFormEscalationChannel(BaseAgentEscalationChannel): properties: AgentQuickFormChannelProperties = Field(..., alias="properties") -class AgentUnknownEscalationChannel(BaseAgentEscalationChannel): - """Fallback for unknown or future escalation channel types.""" - - type: Literal[AgentEscalationChannelType.UNKNOWN] = Field( - default=AgentEscalationChannelType.UNKNOWN, alias="type" - ) - properties: BaseEscalationChannelProperties = Field( - default_factory=BaseEscalationChannelProperties, alias="properties" - ) - - AgentEscalationChannel = Annotated[ Union[ AgentActionCenterEscalationChannel, AgentQuickFormEscalationChannel, - AgentUnknownEscalationChannel, ], Field(discriminator="type"), _case_insensitive_enum_validator("type", AgentEscalationChannelType), diff --git a/packages/uipath/tests/agent/models/test_agent.py b/packages/uipath/tests/agent/models/test_agent.py index 9991711d1..fc8802cf7 100644 --- a/packages/uipath/tests/agent/models/test_agent.py +++ b/packages/uipath/tests/agent/models/test_agent.py @@ -2664,6 +2664,28 @@ def test_quick_form_channel_requires_schema(self): } ) + def test_unknown_escalation_channel_type_is_rejected(self): + """An unrecognized channel type fails to parse; the runtime cannot handle it.""" + with pytest.raises(ValidationError): + TypeAdapter(AgentEscalationResourceConfig).validate_python( + { + "$resourceType": "escalation", + "name": "Escalation", + "description": "", + "channels": [ + { + "name": "c", + "description": "", + "inputSchema": {"type": "object", "properties": {}}, + "type": "someFutureChannel", + "recipients": [], + "properties": {}, + } + ], + "isAgentMemoryEnabled": False, + } + ) + def test_task_title_text_builder_type(self): """Test TextBuilderTaskTitle with tokens.""" from uipath.agent.models.agent import ( From 2420c21949875ec5a5b1a5dd1dbf89c1b6b9073c Mon Sep 17 00:00:00 2001 From: cotovanu-cristian Date: Mon, 8 Jun 2026 19:53:54 +0300 Subject: [PATCH 3/3] fix(agent): keep AgentEscalationChannel instantiable for backward compat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The channel refactor had renamed the concrete Action Center channel to AgentActionCenterEscalationChannel and rebound AgentEscalationChannel to an Annotated[Union[...]]. A typing.Union is not instantiable, so every consumer that builds AgentEscalationChannel(...) directly broke — e.g. uipath-langchain main's escalation test fixtures fail with "TypeError: Cannot instantiate typing.Union" under the cross-repo test. Restore AgentEscalationChannel as the concrete Action Center channel and move the discriminated union to EscalationChannel (matching the existing EscalationResourceConfig union-naming convention) for the channels field. Runtime parsing, QuickForm routing, and unknown-type rejection are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/uipath/src/uipath/agent/models/agent.py | 12 ++++++------ packages/uipath/tests/agent/models/test_agent.py | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/uipath/src/uipath/agent/models/agent.py b/packages/uipath/src/uipath/agent/models/agent.py index 2d280a66b..385a37e7c 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -755,8 +755,8 @@ def _apply_task_title_resolution(cls, v: Any) -> Any: return _resolve_task_title(v) -class AgentActionCenterEscalationChannel(BaseAgentEscalationChannel): - """Action Center app-task escalation channel.""" +class AgentEscalationChannel(BaseAgentEscalationChannel): + """Action Center app-task escalation channel (channel type ``actionCenter``).""" type: Literal[AgentEscalationChannelType.ACTION_CENTER] = Field( default=AgentEscalationChannelType.ACTION_CENTER, alias="type" @@ -773,9 +773,9 @@ class AgentQuickFormEscalationChannel(BaseAgentEscalationChannel): properties: AgentQuickFormChannelProperties = Field(..., alias="properties") -AgentEscalationChannel = Annotated[ +EscalationChannel = Annotated[ Union[ - AgentActionCenterEscalationChannel, + AgentEscalationChannel, AgentQuickFormEscalationChannel, ], Field(discriminator="type"), @@ -790,7 +790,7 @@ class AgentEscalationResourceConfig(BaseAgentResourceConfig): resource_type: Literal[AgentResourceType.ESCALATION] = Field( alias="$resourceType", default=AgentResourceType.ESCALATION, frozen=True ) - channels: List[AgentEscalationChannel] = Field(alias="channels") + channels: List[EscalationChannel] = Field(alias="channels") is_agent_memory_enabled: bool = Field(default=False, alias="isAgentMemoryEnabled") escalation_type: Literal[0] = Field(default=0, alias="escalationType") @@ -810,7 +810,7 @@ class AgentIxpVsEscalationResourceConfig(BaseAgentResourceConfig): resource_type: Literal[AgentResourceType.ESCALATION] = Field( alias="$resourceType", default=AgentResourceType.ESCALATION, frozen=True ) - channels: List[AgentEscalationChannel] = Field(alias="channels") + channels: List[EscalationChannel] = Field(alias="channels") is_agent_memory_enabled: bool = Field(default=False, alias="isAgentMemoryEnabled") escalation_type: Literal[1] = Field(default=1, alias="escalationType") vs_escalation_properties: AgentIxpVsEscalationProperties = Field( diff --git a/packages/uipath/tests/agent/models/test_agent.py b/packages/uipath/tests/agent/models/test_agent.py index fc8802cf7..c324d4e7e 100644 --- a/packages/uipath/tests/agent/models/test_agent.py +++ b/packages/uipath/tests/agent/models/test_agent.py @@ -5,7 +5,6 @@ from uipath.agent.models.agent import ( AgentA2aResourceConfig, - AgentActionCenterEscalationChannel, AgentBooleanOperator, AgentBooleanRule, AgentBuiltInValidatorGuardrail, @@ -15,6 +14,7 @@ AgentContextType, AgentCustomGuardrail, AgentDefinition, + AgentEscalationChannel, AgentEscalationRecipient, AgentEscalationRecipientType, AgentEscalationResourceConfig, @@ -2608,7 +2608,7 @@ def test_agent_with_ixp_vs_escalation(self): assert len(channel.recipients) == 0 # Validate channel properties - assert isinstance(channel, AgentActionCenterEscalationChannel) + assert isinstance(channel, AgentEscalationChannel) assert channel.properties.app_name is None assert channel.properties.app_version == 1 assert channel.properties.folder_name is None @@ -2738,7 +2738,7 @@ def test_escalation_channel_uses_task_title_v2_when_present(self): }, } - channel = AgentActionCenterEscalationChannel(**channel_data) # type: ignore[arg-type] + channel = AgentEscalationChannel(**channel_data) # type: ignore[arg-type] assert isinstance(channel.task_title, dict) or hasattr( channel.task_title, "tokens" @@ -2762,7 +2762,7 @@ def test_escalation_channel_uses_legacy_task_title(self): "taskTitle": "Legacy Task Title", } - channel = AgentActionCenterEscalationChannel(**channel_data) # type: ignore[arg-type] + channel = AgentEscalationChannel(**channel_data) # type: ignore[arg-type] assert channel.task_title == "Legacy Task Title" @@ -2783,7 +2783,7 @@ def test_escalation_channel_defaults_to_escalation_task(self): "recipients": [], } - channel = AgentActionCenterEscalationChannel(**channel_data) # type: ignore[arg-type] + channel = AgentEscalationChannel(**channel_data) # type: ignore[arg-type] assert channel.task_title == "Escalation Task"