From 9f00148eafbc4cbc0631edd6a0503568334a54f6 Mon Sep 17 00:00:00 2001 From: cotovanu-cristian Date: Mon, 9 Feb 2026 09:35:11 +0200 Subject: [PATCH 1/8] feat: add legacy agents packages support --- src/uipath/agent/models/_legacy.py | 87 +++++++++ src/uipath/agent/models/agent.py | 2 + tests/agent/models/test_legacy.py | 295 +++++++++++++++++++++++++++++ 3 files changed, 384 insertions(+) create mode 100644 src/uipath/agent/models/_legacy.py create mode 100644 tests/agent/models/test_legacy.py diff --git a/src/uipath/agent/models/_legacy.py b/src/uipath/agent/models/_legacy.py new file mode 100644 index 000000000..ae72e699f --- /dev/null +++ b/src/uipath/agent/models/_legacy.py @@ -0,0 +1,87 @@ +"""Legacy backwards compatibility for flat AgentDefinition formats. + +Converts legacy flat fields (systemPrompt, userPrompt, tools, contexts, +escalations) into the unified format (messages, resources, +features) before Pydantic validation runs. +""" + +from __future__ import annotations + +from typing import Any, Dict + + +def normalize_legacy_format(data: Dict[str, Any]) -> Dict[str, Any]: + """Normalize a legacy flat agent definition into the unified format. + + Mutates and returns *data*. Safe to call on already-modern payloads + (existing ``messages`` / ``resources`` / ``features`` are never overwritten). + """ + _normalize_messages(data) + _normalize_legacy_resources(data) + _cleanup_legacy_fields(data) + return data + + +def _normalize_messages(data: Dict[str, Any]) -> None: + messages = data.get("messages") + if messages: + return + + system_prompt = data.get("systemPrompt") + user_prompt = data.get("userPrompt") + + if system_prompt is None and user_prompt is None: + return + + built: list[Dict[str, Any]] = [] + + if system_prompt is not None: + if isinstance(system_prompt, dict): + built.append({"role": "system", **system_prompt}) + else: + built.append({"role": "system", "content": str(system_prompt)}) + + if user_prompt is not None: + if isinstance(user_prompt, dict): + built.append({"role": "user", **user_prompt}) + else: + built.append({"role": "user", "content": str(user_prompt)}) + + data["messages"] = built + + +def _normalize_legacy_resources(data: Dict[str, Any]) -> None: + resources = data.get("resources") + if resources: + return + + built: list[Dict[str, Any]] = [] + + for item in data.get("tools") or []: + if isinstance(item, dict): + item.setdefault("$resourceType", "tool") + item.setdefault("isEnabled", True) + built.append(item) + + for item in data.get("contexts") or []: + if isinstance(item, dict): + item.setdefault("$resourceType", "context") + built.append(item) + + for item in data.get("escalations") or []: + if isinstance(item, dict): + item.setdefault("$resourceType", "escalation") + built.append(item) + + if built: + data["resources"] = built + + +_LEGACY_KEYS = frozenset( + ["systemPrompt", "userPrompt", "tools", "contexts", "escalations"] +) + + +def _cleanup_legacy_fields(data: Dict[str, Any]) -> None: + for key in _LEGACY_KEYS: + data.pop(key, None) diff --git a/src/uipath/agent/models/agent.py b/src/uipath/agent/models/agent.py index c88f710fe..4110c227b 100644 --- a/src/uipath/agent/models/agent.py +++ b/src/uipath/agent/models/agent.py @@ -20,6 +20,7 @@ UniversalRule, ) +from uipath.agent.models._legacy import normalize_legacy_format from uipath.platform.connections import Connection from uipath.platform.guardrails import ( BuiltInValidatorGuardrail, @@ -1206,6 +1207,7 @@ def _normalize_resources(v: Dict[str, Any]) -> None: def _normalize_all(cls, v: Any) -> Any: if not isinstance(v, dict): return v + normalize_legacy_format(v) cls._normalize_guardrails(v) cls._normalize_resources(v) return v diff --git a/tests/agent/models/test_legacy.py b/tests/agent/models/test_legacy.py new file mode 100644 index 000000000..0f8450453 --- /dev/null +++ b/tests/agent/models/test_legacy.py @@ -0,0 +1,295 @@ +"""Tests for legacy AgentDefinition backwards compatibility.""" + +import copy + +from pydantic import TypeAdapter + +from uipath.agent.models._legacy import normalize_legacy_format +from uipath.agent.models.agent import ( + AgentDefinition, + AgentResourceType, +) +from uipath.agent.models.evals import AgentEvalsDefinition + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +_MINIMAL_SETTINGS = { + "engine": "basic-v1", + "model": "gpt-4o", + "maxTokens": 4096, + "temperature": 0, +} + +_MINIMAL_SCHEMAS = { + "inputSchema": {"type": "object", "properties": {}}, + "outputSchema": {"type": "object", "properties": {}}, +} + + +def _base(**overrides): + """Return a minimal modern-format dict, with overrides merged in.""" + d = { + "version": "1.0.0", + "settings": _MINIMAL_SETTINGS, + **_MINIMAL_SCHEMAS, + } + d.update(overrides) + return d + + +# =================================================================== +# Unit tests — exercise normalize_legacy_format on plain dicts +# =================================================================== + + +class TestNormalizeLegacyFormatMessages: + def test_modern_format_passes_through_unchanged(self): + data = _base( + messages=[ + {"role": "system", "content": "hello"}, + {"role": "user", "content": "world"}, + ], + resources=[], + ) + original = copy.deepcopy(data) + normalize_legacy_format(data) + assert data["messages"] == original["messages"] + assert data["resources"] == original["resources"] + + def test_system_and_user_prompt_strings(self): + data = _base(systemPrompt="sys", userPrompt="usr") + normalize_legacy_format(data) + assert len(data["messages"]) == 2 + assert data["messages"][0] == {"role": "system", "content": "sys"} + assert data["messages"][1] == {"role": "user", "content": "usr"} + assert "systemPrompt" not in data + assert "userPrompt" not in data + + def test_system_prompt_only(self): + data = _base(systemPrompt="sys only") + normalize_legacy_format(data) + assert len(data["messages"]) == 1 + assert data["messages"][0]["role"] == "system" + assert data["messages"][0]["content"] == "sys only" + + def test_user_prompt_only(self): + data = _base(userPrompt="usr only") + normalize_legacy_format(data) + assert len(data["messages"]) == 1 + assert data["messages"][0]["role"] == "user" + assert data["messages"][0]["content"] == "usr only" + + def test_prompts_as_dicts(self): + data = _base( + systemPrompt={"content": "sys dict"}, + userPrompt={"content": "usr dict"}, + ) + normalize_legacy_format(data) + assert data["messages"][0] == {"role": "system", "content": "sys dict"} + assert data["messages"][1] == {"role": "user", "content": "usr dict"} + + def test_existing_messages_not_overwritten(self): + data = _base( + messages=[{"role": "system", "content": "keep me"}], + systemPrompt="should be ignored", + userPrompt="should also be ignored", + ) + normalize_legacy_format(data) + assert len(data["messages"]) == 1 + assert data["messages"][0]["content"] == "keep me" + # legacy fields still cleaned up + assert "systemPrompt" not in data + assert "userPrompt" not in data + + +class TestNormalizeLegacyFormatResources: + def test_tools_become_tool_resources(self): + tool = {"name": "t1", "description": "d1", "type": "Process", "inputSchema": {}, "outputSchema": {}, "properties": {}, "settings": {}} + data = _base(tools=[tool]) + normalize_legacy_format(data) + assert len(data["resources"]) == 1 + assert data["resources"][0]["$resourceType"] == "tool" + assert data["resources"][0]["isEnabled"] is True + assert "tools" not in data + + def test_contexts_become_context_resources(self): + ctx = {"name": "c1", "description": "d1", "folderPath": "f", "indexName": "i", "settings": {"resultCount": 3, "retrievalMode": "Semantic"}} + data = _base(contexts=[ctx]) + normalize_legacy_format(data) + assert len(data["resources"]) == 1 + assert data["resources"][0]["$resourceType"] == "context" + assert "contexts" not in data + + def test_escalations_become_escalation_resources(self): + esc = {"name": "e1", "description": "d1", "channels": [], "escalationType": 0} + data = _base(escalations=[esc]) + normalize_legacy_format(data) + assert len(data["resources"]) == 1 + assert data["resources"][0]["$resourceType"] == "escalation" + assert "escalations" not in data + + def test_mixed_legacy_resources_maintain_order(self): + tool = {"name": "t", "description": "d"} + ctx = {"name": "c", "description": "d"} + esc = {"name": "e", "description": "d"} + data = _base(tools=[tool], contexts=[ctx], escalations=[esc]) + normalize_legacy_format(data) + types = [r["$resourceType"] for r in data["resources"]] + assert types == ["tool", "context", "escalation"] + + def test_existing_resources_not_overwritten(self): + data = _base( + resources=[{"$resourceType": "tool", "name": "keep"}], + tools=[{"name": "ignored", "description": "d"}], + ) + normalize_legacy_format(data) + assert len(data["resources"]) == 1 + assert data["resources"][0]["name"] == "keep" + + def test_setdefault_does_not_overwrite_existing_resource_type(self): + tool = {"name": "t", "description": "d", "$resourceType": "custom"} + data = _base(tools=[tool]) + normalize_legacy_format(data) + assert data["resources"][0]["$resourceType"] == "custom" + + +class TestNormalizeLegacyFormatCleanup: + def test_legacy_fields_removed(self): + data = _base( + systemPrompt="s", + userPrompt="u", + tools=[], + contexts=[], + escalations=[], + ) + normalize_legacy_format(data) + for key in ["systemPrompt", "userPrompt", "tools", "contexts", "escalations"]: + assert key not in data + + +class TestNormalizeLegacyFormatIdempotent: + def test_double_application_is_stable(self): + data = _base( + systemPrompt="sys", + userPrompt="usr", + tools=[{"name": "t", "description": "d"}], + ) + normalize_legacy_format(data) + snapshot = copy.deepcopy(data) + normalize_legacy_format(data) + assert data == snapshot + + +# =================================================================== +# Integration tests — full AgentDefinition parsing +# =================================================================== + + +class TestLegacyAgentDefinitionIntegration: + def _legacy_payload(self, **extra): + """Build a complete legacy payload that should parse into AgentDefinition.""" + payload = { + "version": "1.0.0", + "settings": _MINIMAL_SETTINGS, + **_MINIMAL_SCHEMAS, + "systemPrompt": "You are an assistant.", + "userPrompt": "Help with {{task}}.", + } + payload.update(extra) + return payload + + def test_basic_legacy_payload_parses(self): + data = self._legacy_payload() + agent = TypeAdapter(AgentDefinition).validate_python(data) + assert len(agent.messages) == 2 + assert agent.messages[0].role == "system" + assert agent.messages[0].content == "You are an assistant." + assert agent.messages[1].role == "user" + assert agent.messages[1].content == "Help with {{task}}." + + def test_legacy_payload_with_all_resource_types(self): + data = self._legacy_payload( + tools=[ + { + "name": "MyTool", + "description": "A process tool", + "type": "Process", + "inputSchema": {"type": "object", "properties": {}}, + "outputSchema": {"type": "object", "properties": {}}, + "properties": {"folderPath": "f", "processName": "p"}, + "settings": {"maxAttempts": 0, "retryDelay": 0, "timeout": 0}, + } + ], + contexts=[ + { + "name": "MyContext", + "description": "A context", + "folderPath": "f", + "indexName": "idx", + "settings": { + "resultCount": 5, + "retrievalMode": "Semantic", + "threshold": 0, + }, + } + ], + escalations=[ + { + "name": "MyEscalation", + "description": "An escalation", + "escalationType": 0, + "channels": [ + { + "name": "ch", + "type": "ActionCenter", + "description": "chan desc", + "inputSchema": {"type": "object", "properties": {}}, + "outputSchema": {"type": "object", "properties": {}}, + "properties": {"appVersion": 1}, + "recipients": [{"type": "UserId", "value": "uid"}], + } + ], + } + ], + ) + + agent = TypeAdapter(AgentDefinition).validate_python(data) + + assert len(agent.messages) == 2 + assert len(agent.resources) == 3 + + types = [r.resource_type for r in agent.resources] + assert AgentResourceType.TOOL in types + assert AgentResourceType.CONTEXT in types + assert AgentResourceType.ESCALATION in types + + def test_legacy_format_through_evals_definition(self): + data = { + "version": "1.0.0", + "settings": _MINIMAL_SETTINGS, + **_MINIMAL_SCHEMAS, + "systemPrompt": "Eval agent prompt", + "userPrompt": "Eval user prompt", + "tools": [ + { + "name": "EvalTool", + "description": "Tool for eval", + "type": "Process", + "inputSchema": {"type": "object", "properties": {}}, + "outputSchema": {"type": "object", "properties": {}}, + "properties": {"folderPath": "f", "processName": "p"}, + "settings": {"maxAttempts": 0, "retryDelay": 0, "timeout": 0}, + } + ], + } + + agent = TypeAdapter(AgentEvalsDefinition).validate_python(data) + + assert isinstance(agent, AgentEvalsDefinition) + assert len(agent.messages) == 2 + assert agent.messages[0].content == "Eval agent prompt" + assert len(agent.resources) == 1 + assert agent.resources[0].resource_type == AgentResourceType.TOOL From 546d3bb515d8b2a50a879e50297556cae2aa5157 Mon Sep 17 00:00:00 2001 From: Andrei Ancuta Date: Wed, 11 Feb 2026 15:45:12 +0200 Subject: [PATCH 2/8] feat: refactor --- src/uipath/agent/models/_legacy.py | 28 +++++++------ tests/agent/models/test_legacy.py | 66 ++++++++++++++++-------------- 2 files changed, 52 insertions(+), 42 deletions(-) diff --git a/src/uipath/agent/models/_legacy.py b/src/uipath/agent/models/_legacy.py index ae72e699f..3d82968f8 100644 --- a/src/uipath/agent/models/_legacy.py +++ b/src/uipath/agent/models/_legacy.py @@ -1,20 +1,18 @@ """Legacy backwards compatibility for flat AgentDefinition formats. -Converts legacy flat fields (systemPrompt, userPrompt, tools, contexts, -escalations) into the unified format (messages, resources, -features) before Pydantic validation runs. +Converts legacy flat fields into the unified format before Pydantic validation runs. """ from __future__ import annotations -from typing import Any, Dict +from typing import Any -def normalize_legacy_format(data: Dict[str, Any]) -> Dict[str, Any]: +def normalize_legacy_format(data: dict[str, Any]) -> dict[str, Any]: """Normalize a legacy flat agent definition into the unified format. Mutates and returns *data*. Safe to call on already-modern payloads - (existing ``messages`` / ``resources`` / ``features`` are never overwritten). + (existing messages and resources are never overwritten). """ _normalize_messages(data) _normalize_legacy_resources(data) @@ -22,7 +20,7 @@ def normalize_legacy_format(data: Dict[str, Any]) -> Dict[str, Any]: return data -def _normalize_messages(data: Dict[str, Any]) -> None: +def _normalize_messages(data: dict[str, Any]) -> None: messages = data.get("messages") if messages: return @@ -33,7 +31,7 @@ def _normalize_messages(data: Dict[str, Any]) -> None: if system_prompt is None and user_prompt is None: return - built: list[Dict[str, Any]] = [] + built: list[dict[str, Any]] = [] if system_prompt is not None: if isinstance(system_prompt, dict): @@ -50,12 +48,12 @@ def _normalize_messages(data: Dict[str, Any]) -> None: data["messages"] = built -def _normalize_legacy_resources(data: Dict[str, Any]) -> None: +def _normalize_legacy_resources(data: dict[str, Any]) -> None: resources = data.get("resources") if resources: return - built: list[Dict[str, Any]] = [] + built: list[dict[str, Any]] = [] for item in data.get("tools") or []: if isinstance(item, dict): @@ -78,10 +76,16 @@ def _normalize_legacy_resources(data: Dict[str, Any]) -> None: _LEGACY_KEYS = frozenset( - ["systemPrompt", "userPrompt", "tools", "contexts", "escalations"] + [ + "systemPrompt", + "userPrompt", + "tools", + "contexts", + "escalations", + ] ) -def _cleanup_legacy_fields(data: Dict[str, Any]) -> None: +def _cleanup_legacy_fields(data: dict[str, Any]) -> None: for key in _LEGACY_KEYS: data.pop(key, None) diff --git a/tests/agent/models/test_legacy.py b/tests/agent/models/test_legacy.py index 0f8450453..998501071 100644 --- a/tests/agent/models/test_legacy.py +++ b/tests/agent/models/test_legacy.py @@ -11,11 +11,6 @@ ) from uipath.agent.models.evals import AgentEvalsDefinition - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - _MINIMAL_SETTINGS = { "engine": "basic-v1", "model": "gpt-4o", @@ -40,11 +35,6 @@ def _base(**overrides): return d -# =================================================================== -# Unit tests — exercise normalize_legacy_format on plain dicts -# =================================================================== - - class TestNormalizeLegacyFormatMessages: def test_modern_format_passes_through_unchanged(self): data = _base( @@ -69,14 +59,14 @@ def test_system_and_user_prompt_strings(self): assert "userPrompt" not in data def test_system_prompt_only(self): - data = _base(systemPrompt="sys only") + data = _base(systemPrompt={"content": "sys only"}) normalize_legacy_format(data) assert len(data["messages"]) == 1 assert data["messages"][0]["role"] == "system" assert data["messages"][0]["content"] == "sys only" def test_user_prompt_only(self): - data = _base(userPrompt="usr only") + data = _base(userPrompt={"content": "usr only"}) normalize_legacy_format(data) assert len(data["messages"]) == 1 assert data["messages"][0]["role"] == "user" @@ -94,8 +84,8 @@ def test_prompts_as_dicts(self): def test_existing_messages_not_overwritten(self): data = _base( messages=[{"role": "system", "content": "keep me"}], - systemPrompt="should be ignored", - userPrompt="should also be ignored", + systemPrompt={"content": "should be ignored"}, + userPrompt={"content": "should also be ignored"}, ) normalize_legacy_format(data) assert len(data["messages"]) == 1 @@ -107,7 +97,15 @@ def test_existing_messages_not_overwritten(self): class TestNormalizeLegacyFormatResources: def test_tools_become_tool_resources(self): - tool = {"name": "t1", "description": "d1", "type": "Process", "inputSchema": {}, "outputSchema": {}, "properties": {}, "settings": {}} + tool = { + "name": "t1", + "description": "d1", + "type": "Process", + "inputSchema": {}, + "outputSchema": {}, + "properties": {}, + "settings": {}, + } data = _base(tools=[tool]) normalize_legacy_format(data) assert len(data["resources"]) == 1 @@ -116,7 +114,13 @@ def test_tools_become_tool_resources(self): assert "tools" not in data def test_contexts_become_context_resources(self): - ctx = {"name": "c1", "description": "d1", "folderPath": "f", "indexName": "i", "settings": {"resultCount": 3, "retrievalMode": "Semantic"}} + ctx = { + "name": "c1", + "description": "d1", + "folderPath": "f", + "indexName": "i", + "settings": {"resultCount": 3, "retrievalMode": "Semantic"}, + } data = _base(contexts=[ctx]) normalize_legacy_format(data) assert len(data["resources"]) == 1 @@ -159,22 +163,29 @@ def test_setdefault_does_not_overwrite_existing_resource_type(self): class TestNormalizeLegacyFormatCleanup: def test_legacy_fields_removed(self): data = _base( - systemPrompt="s", - userPrompt="u", + systemPrompt={"content": "s"}, + userPrompt={"content": "u"}, tools=[], contexts=[], escalations=[], ) normalize_legacy_format(data) - for key in ["systemPrompt", "userPrompt", "tools", "contexts", "escalations"]: + legacy_fields = [ + "systemPrompt", + "userPrompt", + "tools", + "contexts", + "escalations", + ] + for key in legacy_fields: assert key not in data class TestNormalizeLegacyFormatIdempotent: def test_double_application_is_stable(self): data = _base( - systemPrompt="sys", - userPrompt="usr", + systemPrompt={"content": "sys"}, + userPrompt={"content": "usr"}, tools=[{"name": "t", "description": "d"}], ) normalize_legacy_format(data) @@ -183,11 +194,6 @@ def test_double_application_is_stable(self): assert data == snapshot -# =================================================================== -# Integration tests — full AgentDefinition parsing -# =================================================================== - - class TestLegacyAgentDefinitionIntegration: def _legacy_payload(self, **extra): """Build a complete legacy payload that should parse into AgentDefinition.""" @@ -195,8 +201,8 @@ def _legacy_payload(self, **extra): "version": "1.0.0", "settings": _MINIMAL_SETTINGS, **_MINIMAL_SCHEMAS, - "systemPrompt": "You are an assistant.", - "userPrompt": "Help with {{task}}.", + "systemPrompt": {"content": "You are an assistant."}, + "userPrompt": {"content": "Help with {{task}}."}, } payload.update(extra) return payload @@ -271,8 +277,8 @@ def test_legacy_format_through_evals_definition(self): "version": "1.0.0", "settings": _MINIMAL_SETTINGS, **_MINIMAL_SCHEMAS, - "systemPrompt": "Eval agent prompt", - "userPrompt": "Eval user prompt", + "systemPrompt": {"content": "Eval agent prompt"}, + "userPrompt": {"content": "Eval user prompt"}, "tools": [ { "name": "EvalTool", From 8d9c9db0f4392eea735c5face7ac1a69fdfc8474 Mon Sep 17 00:00:00 2001 From: Andrei Ancuta Date: Wed, 11 Feb 2026 18:01:23 +0200 Subject: [PATCH 3/8] feat: handle missing escalationType discriminator --- src/uipath/agent/models/agent.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/uipath/agent/models/agent.py b/src/uipath/agent/models/agent.py index 4110c227b..eaa7c525c 100644 --- a/src/uipath/agent/models/agent.py +++ b/src/uipath/agent/models/agent.py @@ -9,7 +9,9 @@ BaseModel, BeforeValidator, ConfigDict, + Discriminator, Field, + Tag, field_validator, model_validator, ) @@ -767,10 +769,10 @@ class AgentUnknownToolResourceConfig(BaseAgentToolResourceConfig): EscalationResourceConfig = Annotated[ Union[ - AgentEscalationResourceConfig, - AgentIxpVsEscalationResourceConfig, + Annotated[AgentEscalationResourceConfig, Tag(0)], + Annotated[AgentIxpVsEscalationResourceConfig, Tag(1)], ], - Field(discriminator="escalation_type"), + Discriminator(lambda v: v.get("escalation_type") or v.get("escalationType") or 0), ] AgentResourceConfig = Annotated[ From 820166bef7096d5877b7ca423b3b1e361f9a8501 Mon Sep 17 00:00:00 2001 From: Andrei Ancuta Date: Wed, 11 Feb 2026 18:21:49 +0200 Subject: [PATCH 4/8] feat: match recipientType case-insensitively --- src/uipath/agent/models/agent.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/uipath/agent/models/agent.py b/src/uipath/agent/models/agent.py index eaa7c525c..4773133ae 100644 --- a/src/uipath/agent/models/agent.py +++ b/src/uipath/agent/models/agent.py @@ -2,7 +2,7 @@ from __future__ import annotations -from enum import Enum +from enum import Enum, StrEnum from typing import Annotated, Any, Dict, List, Literal, Optional, Union from pydantic import ( @@ -38,6 +38,14 @@ def _decapitalize_first_letter(s: str) -> str: return s[0].lower() + s[1:] +def _match_enum_case_insensitive(enum: type[StrEnum], value: str) -> str: + """Find the corresponding enum value, ignoring case.""" + for enum_value in enum: + if enum_value.value.lower() == value.lower(): + return enum_value.value + return value + + class AgentResourceType(str, Enum): """Agent resource type enumeration.""" @@ -69,7 +77,7 @@ class AgentInternalToolType(str, Enum): BATCH_TRANSFORM = "batch-transform" -class AgentEscalationRecipientType(str, Enum): +class AgentEscalationRecipientType(StrEnum): """Agent escalation recipient type enumeration.""" USER_ID = "UserId" @@ -390,6 +398,10 @@ def _normalize_recipient_type(recipient: Any) -> Any: 6: AgentEscalationRecipientType.ASSET_GROUP_NAME, } recipient["type"] = type_mapping.get(recipient_type, str(recipient_type)) + elif isinstance(recipient_type, str): + recipient["type"] = _match_enum_case_insensitive( + AgentEscalationRecipientType, recipient_type + ) return recipient From 1ddf31a42bcc9c4d9e74e22912fd22f8e05f42d6 Mon Sep 17 00:00:00 2001 From: Andrei Ancuta Date: Wed, 11 Feb 2026 18:22:03 +0200 Subject: [PATCH 5/8] feat: handle missing output_schema in tools --- src/uipath/agent/models/agent.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/uipath/agent/models/agent.py b/src/uipath/agent/models/agent.py index 4773133ae..1e9d0a81a 100644 --- a/src/uipath/agent/models/agent.py +++ b/src/uipath/agent/models/agent.py @@ -28,6 +28,8 @@ BuiltInValidatorGuardrail, ) +EMPTY_SCHEMA = {"type": "object", "properties": {}} + def _decapitalize_first_letter(s: str) -> str: """Convert first letter to lowercase (e.g., 'SimpleText' -> 'simpleText').""" @@ -532,7 +534,7 @@ class AgentEscalationChannel(BaseCfg): type: str = Field(alias="type") description: str = Field(..., alias="description") input_schema: Dict[str, Any] = Field(..., alias="inputSchema") - output_schema: Dict[str, Any] = Field(..., alias="outputSchema") + output_schema: Dict[str, Any] = Field(EMPTY_SCHEMA, alias="outputSchema") argument_properties: Dict[str, AgentToolArgumentProperties] = Field( {}, alias="argumentProperties" ) @@ -612,7 +614,7 @@ class AgentProcessToolResourceConfig(BaseAgentToolResourceConfig): AgentToolType.API, AgentToolType.PROCESS_ORCHESTRATION, ] - output_schema: Dict[str, Any] = Field(..., alias="outputSchema") + output_schema: Dict[str, Any] = Field(EMPTY_SCHEMA, alias="outputSchema") properties: AgentProcessToolProperties settings: AgentToolSettings = Field(default_factory=AgentToolSettings) arguments: Dict[str, Any] = Field(default_factory=dict) @@ -632,7 +634,7 @@ class AgentIxpExtractionResourceConfig(BaseAgentToolResourceConfig): """Agent ixp extraction tool resource configuration model.""" type: Literal[AgentToolType.IXP] = AgentToolType.IXP - output_schema: dict[str, Any] = Field(..., alias="outputSchema") + output_schema: dict[str, Any] = Field(EMPTY_SCHEMA, alias="outputSchema") settings: AgentToolSettings = Field(default_factory=AgentToolSettings) properties: AgentIxpExtractionToolProperties @@ -755,7 +757,7 @@ class AgentInternalToolResourceConfig(BaseAgentToolResourceConfig): properties: AgentInternalToolProperties settings: Optional[AgentToolSettings] = Field(None) arguments: Optional[Dict[str, Any]] = Field(default_factory=dict) - output_schema: Dict[str, Any] = Field(..., alias="outputSchema") + output_schema: Dict[str, Any] = Field(EMPTY_SCHEMA, alias="outputSchema") argument_properties: Dict[str, AgentToolArgumentProperties] = Field( {}, alias="argumentProperties" ) From 181cce20a2554dad6b943c08f43b4702bf73a6eb Mon Sep 17 00:00:00 2001 From: Andrei Ancuta Date: Thu, 12 Feb 2026 19:05:44 +0200 Subject: [PATCH 6/8] feat: handle StaticGroupName recipientType --- src/uipath/agent/models/agent.py | 58 ++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/src/uipath/agent/models/agent.py b/src/uipath/agent/models/agent.py index 1e9d0a81a..0a61347a3 100644 --- a/src/uipath/agent/models/agent.py +++ b/src/uipath/agent/models/agent.py @@ -3,7 +3,17 @@ from __future__ import annotations from enum import Enum, StrEnum -from typing import Annotated, Any, Dict, List, Literal, Optional, Union +from typing import ( + Annotated, + Any, + Dict, + List, + Literal, + Mapping, + Optional, + TypeVar, + Union, +) from pydantic import ( BaseModel, @@ -40,12 +50,15 @@ def _decapitalize_first_letter(s: str) -> str: return s[0].lower() + s[1:] -def _match_enum_case_insensitive(enum: type[StrEnum], value: str) -> str: +EnumT = TypeVar("EnumT", bound=StrEnum) + + +def _match_enum_case_insensitive(enum: type[EnumT], value: str) -> EnumT | None: """Find the corresponding enum value, ignoring case.""" for enum_value in enum: if enum_value.value.lower() == value.lower(): - return enum_value.value - return value + return enum_value + return None class AgentResourceType(str, Enum): @@ -384,26 +397,35 @@ class AgentMcpResourceConfig(BaseAgentResourceConfig): available_tools: List[AgentMcpTool] = Field(..., alias="availableTools") +_RECIPIENT_TYPE_NORMALIZED_MAP: Mapping[int | str, AgentEscalationRecipientType] = { + 1: AgentEscalationRecipientType.USER_ID, + 2: AgentEscalationRecipientType.GROUP_ID, + 3: AgentEscalationRecipientType.USER_EMAIL, + 4: AgentEscalationRecipientType.ASSET_USER_EMAIL, + 5: AgentEscalationRecipientType.GROUP_NAME, + "staticgroupname": AgentEscalationRecipientType.GROUP_NAME, + 6: AgentEscalationRecipientType.ASSET_GROUP_NAME, +} + + def _normalize_recipient_type(recipient: Any) -> Any: - """Normalize recipient type from integer to enum before discrimination.""" + """Normalize recipient type from integer or string to enum before discrimination.""" if not isinstance(recipient, dict): return recipient - recipient_type = recipient.get("type") + + normalized: AgentEscalationRecipientType | None = None if isinstance(recipient_type, int): - type_mapping = { - 1: AgentEscalationRecipientType.USER_ID, - 2: AgentEscalationRecipientType.GROUP_ID, - 3: AgentEscalationRecipientType.USER_EMAIL, - 4: AgentEscalationRecipientType.ASSET_USER_EMAIL, - 5: AgentEscalationRecipientType.GROUP_NAME, - 6: AgentEscalationRecipientType.ASSET_GROUP_NAME, - } - recipient["type"] = type_mapping.get(recipient_type, str(recipient_type)) + normalized = _RECIPIENT_TYPE_NORMALIZED_MAP.get(recipient_type) elif isinstance(recipient_type, str): - recipient["type"] = _match_enum_case_insensitive( - AgentEscalationRecipientType, recipient_type - ) + normalized = _RECIPIENT_TYPE_NORMALIZED_MAP.get(recipient_type.lower()) + if normalized is None: + normalized = _match_enum_case_insensitive( + AgentEscalationRecipientType, recipient_type + ) + + if normalized is not None: + recipient["type"] = normalized.value return recipient From d214c4011428e8bf5653b5e359e0e8ae0503d31d Mon Sep 17 00:00:00 2001 From: Andrei Ancuta Date: Thu, 12 Feb 2026 19:05:53 +0200 Subject: [PATCH 7/8] feat: add tests --- tests/agent/models/test_agent.py | 213 +++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) diff --git a/tests/agent/models/test_agent.py b/tests/agent/models/test_agent.py index 2eaec8115..f836cf4f2 100644 --- a/tests/agent/models/test_agent.py +++ b/tests/agent/models/test_agent.py @@ -1,3 +1,5 @@ +from typing import Any + import pytest from pydantic import TypeAdapter @@ -2757,3 +2759,214 @@ def test_is_conversational_false_by_default(self): ) assert config.is_conversational is False + + +class TestAgentBuilderConfigResources: + """Tests for AgentDefinition resource configuration parsing.""" + + def _agent_dict_with_resources(self, resources: list[Any]) -> dict[str, Any]: + """Helper method that returns an agent dict with default fields and provided resources.""" + return { + "version": "1.0.0", + "id": "test-agent-id", + "name": "Test Agent", + "metadata": {"isConversational": False, "storageVersion": "22.0.0"}, + "messages": [ + { + "role": "System", + "content": "You are a test agent.", + } + ], + "inputSchema": {"type": "object", "properties": {}}, + "outputSchema": {"type": "object", "properties": {}}, + "settings": { + "model": "gpt-4o", + "maxTokens": 4096, + "temperature": 0, + "engine": "basic-v1", + }, + "resources": resources, + } + + def test_escalation_with_static_group_name_recipient_type(self): + """Test that escalation with StaticGroupName recipientType is parsed correctly.""" + resources = [ + { + "$resourceType": "escalation", + "id": "escalation-1", + "channels": [ + { + "name": "Test Channel", + "description": "Test channel description", + "type": "ActionCenter", + "inputSchema": { + "type": "object", + "properties": {"field": {"type": "string"}}, + }, + "outputSchema": {"type": "object", "properties": {}}, + "outcomeMapping": {"Approve": "continue"}, + "properties": { + "appName": "TestApp", + "appVersion": 1, + "folderName": "TestFolder", + "resourceKey": "test-key", + }, + "recipients": [ + { + "value": "TestGroup", + "type": "staticgroupname", + } + ], + "taskTitle": "Test Task", + "priority": "Medium", + } + ], + "isAgentMemoryEnabled": False, + "escalationType": 0, + "name": "Test Escalation", + "description": "Test escalation", + } + ] + + json_data = self._agent_dict_with_resources(resources) + config: AgentDefinition = TypeAdapter(AgentDefinition).validate_python( + json_data + ) + + escalation_resource = config.resources[0] + assert isinstance(escalation_resource, AgentEscalationResourceConfig) + recipient = escalation_resource.channels[0].recipients[0] + assert isinstance(recipient, StandardRecipient) + assert recipient.type == AgentEscalationRecipientType.GROUP_NAME + assert recipient.value == "TestGroup" + + def test_escalation_with_lowercase_userid_recipient_type(self): + """Test that escalation with lowercase userid recipientType is parsed correctly.""" + resources = [ + { + "$resourceType": "escalation", + "id": "escalation-2", + "channels": [ + { + "name": "Test Channel", + "description": "Test channel description", + "type": "ActionCenter", + "inputSchema": { + "type": "object", + "properties": {"field": {"type": "string"}}, + }, + "outputSchema": {"type": "object", "properties": {}}, + "outcomeMapping": {"Approve": "continue"}, + "properties": { + "appName": "TestApp", + "appVersion": 1, + "folderName": "TestFolder", + "resourceKey": "test-key", + }, + "recipients": [ + { + "value": "user-123", + "type": "userid", + } + ], + "taskTitle": "Test Task", + "priority": "Medium", + } + ], + "isAgentMemoryEnabled": False, + "escalationType": 0, + "name": "Test Escalation", + "description": "Test escalation", + } + ] + + json_data = self._agent_dict_with_resources(resources) + config: AgentDefinition = TypeAdapter(AgentDefinition).validate_python( + json_data + ) + + escalation_resource = config.resources[0] + assert isinstance(escalation_resource, AgentEscalationResourceConfig) + recipient = escalation_resource.channels[0].recipients[0] + assert isinstance(recipient, StandardRecipient) + assert recipient.type == AgentEscalationRecipientType.USER_ID + assert recipient.value == "user-123" + + def test_process_tool_missing_output_schema(self): + """Test that process tool without outputSchema is parsed correctly.""" + resources = [ + { + "$resourceType": "tool", + "type": "ProcessOrchestration", + "id": "process-tool-1", + "inputSchema": { + "type": "object", + "properties": {"input": {"type": "string"}}, + }, + "arguments": {}, + "settings": {"timeout": 0, "maxAttempts": 0, "retryDelay": 0}, + "properties": { + "processName": "TestProcess", + "folderPath": "TestFolder", + }, + "name": "Test Process", + "description": "Test process tool", + } + ] + + json_data = self._agent_dict_with_resources(resources) + config: AgentDefinition = TypeAdapter(AgentDefinition).validate_python( + json_data + ) + + tool_resource = config.resources[0] + assert isinstance(tool_resource, AgentProcessToolResourceConfig) + assert tool_resource.output_schema == {"type": "object", "properties": {}} + + def test_escalation_missing_escalation_type_defaults_to_zero(self): + """Test that missing escalationType defaults to 0.""" + resources = [ + { + "$resourceType": "escalation", + "id": "escalation-3", + "channels": [ + { + "name": "Test Channel", + "description": "Test channel description", + "type": "ActionCenter", + "inputSchema": { + "type": "object", + "properties": {"field": {"type": "string"}}, + }, + "outputSchema": {"type": "object", "properties": {}}, + "outcomeMapping": {"Approve": "continue"}, + "properties": { + "appName": "TestApp", + "appVersion": 1, + "folderName": "TestFolder", + "resourceKey": "test-key", + }, + "recipients": [ + { + "value": "user-123", + "type": "UserId", + } + ], + "taskTitle": "Test Task", + "priority": "Medium", + } + ], + "isAgentMemoryEnabled": False, + "name": "Test Escalation", + "description": "Test escalation", + } + ] + + json_data = self._agent_dict_with_resources(resources) + config: AgentDefinition = TypeAdapter(AgentDefinition).validate_python( + json_data + ) + + escalation_resource = config.resources[0] + assert isinstance(escalation_resource, AgentEscalationResourceConfig) + assert escalation_resource.escalation_type == 0 From bd2dea05b616a43e0d78cc6a71ddb4a12cf1a482 Mon Sep 17 00:00:00 2001 From: Andrei Ancuta Date: Mon, 16 Feb 2026 10:41:46 +0200 Subject: [PATCH 8/8] feat: bump version --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ad9dc81ce..5627f4dd4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.8.33" +version = "2.8.34" 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/uv.lock b/uv.lock index 61f75fddb..4b32785ea 100644 --- a/uv.lock +++ b/uv.lock @@ -2531,7 +2531,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.8.33" +version = "2.8.34" source = { editable = "." } dependencies = [ { name = "applicationinsights" },