Skip to content
Draft
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 packages/uipath/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath"
version = "2.14.43"
version = "2.14.44"
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
27 changes: 26 additions & 1 deletion packages/uipath/src/uipath/agent/models/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import logging
from enum import Enum
from typing import (
Annotated,
Expand All @@ -24,6 +25,7 @@
Discriminator,
Field,
Tag,
field_validator,
model_validator,
)

Expand All @@ -41,6 +43,8 @@
BuiltInValidatorGuardrail,
)

logger = logging.getLogger(__name__)

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


Expand Down Expand Up @@ -1009,6 +1013,25 @@ class AgentConversationalAgentToolProperties(BaseResourceProperties):
cached_agent_card: Optional[Dict[str, Any]] = Field(None, alias="cachedAgentCard")


class AgentConversationalAgentToolSettings(AgentToolSettings):
"""Conversational agent tool settings model."""

# Absent (None) means "single": the tool keeps one ongoing conversation.
conversation_mode: Literal["single", "multiple"] | None = Field(
None, alias="conversationMode"
)
run_in_background: bool | None = Field(None, alias="runInBackground")

@field_validator("conversation_mode", mode="before")
@classmethod
def _ignore_unknown_conversation_mode(cls, value: Any) -> Any:
"""Read a mode this version does not know as absent, like an unknown tool type."""
if isinstance(value, str) and value not in ("single", "multiple"):
logger.warning("Ignoring unknown conversationMode %r", value)
return None
return value


class AgentConversationalAgentToolResourceConfig(BaseAgentToolResourceConfig):
"""Conversational agent tool resource configuration model.

Expand All @@ -1019,7 +1042,9 @@ class AgentConversationalAgentToolResourceConfig(BaseAgentToolResourceConfig):
type: Literal[AgentToolType.CONVERSATIONAL_AGENT]
output_schema: Dict[str, Any] = Field(EMPTY_SCHEMA, alias="outputSchema")
properties: AgentConversationalAgentToolProperties
settings: AgentToolSettings = Field(default_factory=AgentToolSettings)
settings: AgentConversationalAgentToolSettings = Field(
default_factory=AgentConversationalAgentToolSettings
)


class AgentIxpExtractionToolProperties(BaseResourceProperties):
Expand Down
134 changes: 134 additions & 0 deletions packages/uipath/tests/agent/models/test_agent.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import logging
from typing import Any

import pytest
Expand All @@ -15,6 +16,7 @@
AgentContextSettings,
AgentContextType,
AgentConversationalAgentToolResourceConfig,
AgentConversationalAgentToolSettings,
AgentCustomGuardrail,
AgentDefinition,
AgentEscalationChannel,
Expand Down Expand Up @@ -5121,3 +5123,135 @@ def test_round_trip_by_alias_keeps_type(self):
assert isinstance(
reparsed.resources[0], AgentConversationalAgentToolResourceConfig
)


class TestConversationalAgentToolSettings:
"""Settings of the conversational agent tool: conversation mode, background."""

@staticmethod
def _parse_settings(settings: dict[str, Any]):
record = {**_CONVERSATIONAL_AGENT_TOOL, "settings": settings}
(resource,) = AgentDefinition.model_validate(
_conversational_agent_json(record)
).resources
assert isinstance(resource, AgentConversationalAgentToolResourceConfig)
return resource.settings

@pytest.mark.parametrize("mode", ["single", "multiple"])
def test_conversation_mode_parses(self, mode: str):
settings = self._parse_settings({"conversationMode": mode})

assert isinstance(settings, AgentConversationalAgentToolSettings)
assert settings.conversation_mode == mode

def test_conversation_mode_absent_is_none(self):
settings = self._parse_settings({})

assert settings.conversation_mode is None
assert settings.run_in_background is None

def test_conversation_mode_null_is_none(self):
settings = self._parse_settings({"conversationMode": None})

assert settings.conversation_mode is None

@pytest.mark.parametrize("unknown", ["Single", "both", "", "parallel"])
def test_unknown_conversation_mode_becomes_none_with_a_warning(
self, unknown: str, caplog: pytest.LogCaptureFixture
):
with caplog.at_level(logging.WARNING, logger="uipath.agent.models.agent"):
settings = AgentConversationalAgentToolSettings.model_validate(
{"conversationMode": unknown}
)

assert settings.conversation_mode is None
assert [r.levelno for r in caplog.records] == [logging.WARNING]
assert repr(unknown) in caplog.records[0].getMessage()

@pytest.mark.parametrize("mode", ["single", "multiple", None])
def test_known_or_null_conversation_mode_does_not_warn(
self, mode: str | None, caplog: pytest.LogCaptureFixture
):
with caplog.at_level(logging.WARNING, logger="uipath.agent.models.agent"):
AgentConversationalAgentToolSettings.model_validate(
{"conversationMode": mode}
)

assert caplog.records == []

@pytest.mark.parametrize("bad", [1, True, ["single"], {"mode": "single"}])
def test_non_string_conversation_mode_is_still_rejected(self, bad: Any):
with pytest.raises(ValidationError):
AgentConversationalAgentToolSettings.model_validate(
{"conversationMode": bad}
)

def test_unknown_conversation_mode_does_not_fail_the_agent_definition(self):
record = {
**_CONVERSATIONAL_AGENT_TOOL,
"settings": {
"conversationMode": "bogus",
"runInBackground": True,
"timeout": 30,
},
}

(resource,) = AgentDefinition.model_validate(
_conversational_agent_json(record)
).resources

assert isinstance(resource, AgentConversationalAgentToolResourceConfig)
assert resource.settings.conversation_mode is None
assert resource.settings.run_in_background is True
assert resource.settings.timeout == 30

@pytest.mark.parametrize("value", [True, False])
def test_run_in_background_parses(self, value: bool):
settings = self._parse_settings({"runInBackground": value})

assert settings.run_in_background is value

def test_both_settings_together_keep_base_settings_and_extras(self):
settings = self._parse_settings(
{
"conversationMode": "multiple",
"runInBackground": True,
"timeout": 30,
"maxAttempts": 2,
"futureSetting": "x",
}
)

assert settings.conversation_mode == "multiple"
assert settings.run_in_background is True
assert settings.timeout == 30
assert settings.max_attempts == 2
assert (settings.model_extra or {})["futureSetting"] == "x"

def test_record_without_settings_key_still_parses(self):
record = {
k: v for k, v in _CONVERSATIONAL_AGENT_TOOL.items() if k != "settings"
}

(resource,) = AgentDefinition.model_validate(
_conversational_agent_json(record)
).resources

assert isinstance(resource, AgentConversationalAgentToolResourceConfig)
assert resource.settings.conversation_mode is None

def test_round_trip_by_alias_keeps_conversation_mode(self):
record = {
**_CONVERSATIONAL_AGENT_TOOL,
"settings": {"conversationMode": "multiple", "runInBackground": False},
}
config = AgentDefinition.model_validate(_conversational_agent_json(record))

dumped = config.model_dump(by_alias=True, mode="json")
(resource,) = dumped["resources"]
assert resource["settings"]["conversationMode"] == "multiple"
assert resource["settings"]["runInBackground"] is False

(reparsed,) = AgentDefinition.model_validate(dumped).resources
assert isinstance(reparsed, AgentConversationalAgentToolResourceConfig)
assert reparsed.settings.conversation_mode == "multiple"
2 changes: 1 addition & 1 deletion packages/uipath/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading