Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
91 changes: 91 additions & 0 deletions src/uipath/agent/models/_legacy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""Legacy backwards compatibility for flat AgentDefinition formats.

Converts legacy flat fields into the unified format before Pydantic validation runs.
"""

from __future__ import annotations

from typing import 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 and resources 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)
82 changes: 61 additions & 21 deletions src/uipath/agent/models/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,26 @@

from __future__ import annotations

from enum import Enum
from typing import Annotated, Any, Dict, List, Literal, Optional, Union
from enum import Enum, StrEnum
from typing import (
Annotated,
Any,
Dict,
List,
Literal,
Mapping,
Optional,
TypeVar,
Union,
)

from pydantic import (
BaseModel,
BeforeValidator,
ConfigDict,
Discriminator,
Field,
Tag,
field_validator,
model_validator,
)
Expand All @@ -20,11 +32,14 @@
UniversalRule,
)

from uipath.agent.models._legacy import normalize_legacy_format
from uipath.platform.connections import Connection
from uipath.platform.guardrails import (
BuiltInValidatorGuardrail,
)

EMPTY_SCHEMA = {"type": "object", "properties": {}}


def _decapitalize_first_letter(s: str) -> str:
"""Convert first letter to lowercase (e.g., 'SimpleText' -> 'simpleText')."""
Expand All @@ -35,6 +50,17 @@ def _decapitalize_first_letter(s: str) -> str:
return s[0].lower() + s[1:]


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
return None


class AgentResourceType(str, Enum):
"""Agent resource type enumeration."""

Expand Down Expand Up @@ -66,7 +92,7 @@ class AgentInternalToolType(str, Enum):
BATCH_TRANSFORM = "batch-transform"


class AgentEscalationRecipientType(str, Enum):
class AgentEscalationRecipientType(StrEnum):
"""Agent escalation recipient type enumeration."""

USER_ID = "UserId"
Expand Down Expand Up @@ -371,22 +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):
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

Expand Down Expand Up @@ -517,7 +556,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"
)
Expand Down Expand Up @@ -597,7 +636,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)
Expand All @@ -617,7 +656,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

Expand Down Expand Up @@ -740,7 +779,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"
)
Expand All @@ -766,10 +805,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[
Expand Down Expand Up @@ -1206,6 +1245,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
Expand Down
Loading