diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index b07b09ce4..bccd2b9df 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -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" diff --git a/packages/uipath/src/uipath/agent/models/agent.py b/packages/uipath/src/uipath/agent/models/agent.py index 4f0ce3691..f9c444ca1 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -2,6 +2,7 @@ from __future__ import annotations +import logging from enum import Enum from typing import ( Annotated, @@ -24,6 +25,7 @@ Discriminator, Field, Tag, + field_validator, model_validator, ) @@ -41,6 +43,8 @@ BuiltInValidatorGuardrail, ) +logger = logging.getLogger(__name__) + EMPTY_SCHEMA = {"type": "object", "properties": {}} @@ -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. @@ -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): diff --git a/packages/uipath/tests/agent/models/test_agent.py b/packages/uipath/tests/agent/models/test_agent.py index c65f406e9..7c7f08108 100644 --- a/packages/uipath/tests/agent/models/test_agent.py +++ b/packages/uipath/tests/agent/models/test_agent.py @@ -1,4 +1,5 @@ import json +import logging from typing import Any import pytest @@ -15,6 +16,7 @@ AgentContextSettings, AgentContextType, AgentConversationalAgentToolResourceConfig, + AgentConversationalAgentToolSettings, AgentCustomGuardrail, AgentDefinition, AgentEscalationChannel, @@ -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" diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 3e9313fca..e6ae1076e 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2599,7 +2599,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.14.43" +version = "2.14.44" source = { editable = "." } dependencies = [ { name = "applicationinsights" },