From 69fd8e525c66fbc09c61391166abe00ed767f965 Mon Sep 17 00:00:00 2001 From: Ion Mincu Date: Fri, 9 Oct 2026 18:35:37 +0300 Subject: [PATCH] feat(agent): conversational agent tool config [PRODEV-1742] - Add AgentToolType.CONVERSATIONAL_AGENT ("ConversationalAgent") and the "conversationalagent" TOOL_MAP entry, so any casing normalizes. A tool type rather than an `a2a` variant: older runtimes skip just the unknown tool instead of rejecting the whole agent. - Add AgentConversationalAgentToolResourceConfig and its properties class (processName optional so a bad record still loads; folderPath and cachedAgentCard optional), as a separate class so langchain's tool_factory cannot route it to the job-start path. Added to the ToolResourceConfig union. - Tests: mixed agent.json parsing, alias round trip, case variants, optional and missing properties, the cross-repo contract fixture, unknown type still maps to Unknown. - Bump uipath to 2.14.37 (pyproject and uv.lock). Co-Authored-By: Claude Opus 5.5 --- packages/uipath/pyproject.toml | 2 +- .../uipath/src/uipath/agent/models/agent.py | 25 ++ .../uipath/tests/agent/models/test_agent.py | 215 ++++++++++++++++++ packages/uipath/uv.lock | 2 +- 4 files changed, 242 insertions(+), 2 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index dc3dc5795..c8ae36006 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.14.36" +version = "2.14.37" 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 a62bc89eb..fab429836 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -115,6 +115,7 @@ class AgentToolType(str, CaseInsensitiveEnum): PROCESS_ORCHESTRATION = "ProcessOrchestration" FLOW = "Flow" FUNCTION = "Function" + CONVERSATIONAL_AGENT = "ConversationalAgent" INTEGRATION = "Integration" INTERNAL = "Internal" IXP = "Ixp" @@ -999,6 +1000,28 @@ class AgentProcessToolResourceConfig(BaseAgentToolResourceConfig): ) +class AgentConversationalAgentToolProperties(BaseResourceProperties): + """Conversational agent tool properties model.""" + + # Optional so a record missing it loads; the runtime reports it at call time. + process_name: Optional[str] = Field(None, alias="processName") + folder_path: Optional[str] = Field(None, alias="folderPath") + cached_agent_card: Optional[Dict[str, Any]] = Field(None, alias="cachedAgentCard") + + +class AgentConversationalAgentToolResourceConfig(BaseAgentToolResourceConfig): + """Conversational agent tool resource configuration model. + + Kept separate from ``AgentProcessToolResourceConfig`` so it is never routed to + the job-start path; the runtime calls it as an A2A agent. + """ + + type: Literal[AgentToolType.CONVERSATIONAL_AGENT] + output_schema: Dict[str, Any] = Field(EMPTY_SCHEMA, alias="outputSchema") + properties: AgentConversationalAgentToolProperties + settings: AgentToolSettings = Field(default_factory=AgentToolSettings) + + class AgentIxpExtractionToolProperties(BaseResourceProperties): """Agent process tool properties model.""" @@ -1292,6 +1315,7 @@ class AgentUnknownToolResourceConfig(BaseAgentToolResourceConfig): ToolResourceConfig = Annotated[ Union[ AgentProcessToolResourceConfig, + AgentConversationalAgentToolResourceConfig, AgentIntegrationToolResourceConfig, AgentInternalToolResourceConfig, AgentIxpExtractionResourceConfig, @@ -1687,6 +1711,7 @@ def _normalize_resources(v: Dict[str, Any]) -> None: "processorchestration": "ProcessOrchestration", "flow": "Flow", "function": "Function", + "conversationalagent": "ConversationalAgent", "integration": "Integration", "internal": "Internal", "ixp": "Ixp", diff --git a/packages/uipath/tests/agent/models/test_agent.py b/packages/uipath/tests/agent/models/test_agent.py index c45bbb1c1..c65f406e9 100644 --- a/packages/uipath/tests/agent/models/test_agent.py +++ b/packages/uipath/tests/agent/models/test_agent.py @@ -1,3 +1,4 @@ +import json from typing import Any import pytest @@ -13,6 +14,7 @@ AgentContextRetrievalMode, AgentContextSettings, AgentContextType, + AgentConversationalAgentToolResourceConfig, AgentCustomGuardrail, AgentDefinition, AgentEscalationChannel, @@ -4906,3 +4908,216 @@ def test_reaches_the_resource_config(self): ) assert resource.settings is not None assert resource.settings.search_during_ingestion is True + + +def _conversational_agent_json(*resources: dict[str, Any]) -> dict[str, Any]: + return { + "version": "1.0.0", + "id": "test-conversational-agent-tool", + "name": "Agent with conversational agent tool", + "metadata": {"isConversational": False, "storageVersion": "50.0.0"}, + "messages": [{"role": "System", "content": "You are an assistant."}], + "inputSchema": {"type": "object", "properties": {}}, + "outputSchema": {"type": "object", "properties": {}}, + "settings": { + "model": "gpt-4o-2024-11-20", + "maxTokens": 16384, + "temperature": 0, + "engine": "basic-v2", + }, + "resources": list(resources), + "features": [], + "guardrails": [], + } + + +_AGENT_TOOL = { + "$resourceType": "tool", + "type": "agent", + "id": "agent-1", + "name": "Child Agent", + "description": "A child agent", + "inputSchema": {"type": "object", "properties": {}}, + "outputSchema": {"type": "object", "properties": {}}, + "settings": {}, + "properties": {"processName": "Child", "folderPath": "Shared"}, + "argumentProperties": {}, +} + +_REMOTE_A2A_RESOURCE = { + "$resourceType": "a2a", + "id": "remote-1", + "name": "Remote Agent", + "description": "A remote A2A agent", + "folderPath": "shared", + "slug": "remote-agent", + "solutionProperties": {"resourceKey": "key-remote"}, +} + +_CONVERSATIONAL_AGENT_TOOL = { + "$resourceType": "tool", + "type": "conversationalAgent", + "id": "conv-1", + "name": "Conversational Agent", + "description": "A conversational agent", + "location": "solution", + "inputSchema": { + "type": "object", + "properties": { + "message": {"type": "string", "description": "The message to send."} + }, + "required": ["message"], + }, + "outputSchema": {"type": "object", "properties": {}}, + "settings": {}, + "properties": { + "processName": "My Conversational Agent", + "folderPath": "Shared/Agents", + "cachedAgentCard": {"name": "Conversational Agent"}, + "requireConversationalConfirmation": True, + }, +} + + +_CONTRACT_FIXTURE = ( + '{"$resourceType":"tool","type":"conversationalAgent","id":"conv-1",' + '"name":"Support agent","description":"Answers support questions",' + '"location":"solution","isEnabled":true,"inputSchema":{"type":"object",' + '"properties":{"message":{"type":"string","description":"The message to send ' + 'to the agent."}},"required":["message"]},"outputSchema":{"type":"object",' + '"properties":{}},"settings":{},"properties":{"processName":"SupportAgent",' + '"folderPath":"Shared/Support","cachedAgentCard":null,' + '"requireConversationalConfirmation":false},"canvasNodeId":"node-1"}' +) + + +class TestConversationalAgentTool: + """Conversational agent resources (tool type 'conversationalAgent').""" + + def test_mixed_agent_json_parses_into_right_classes(self): + config = AgentDefinition.model_validate( + _conversational_agent_json( + _AGENT_TOOL, _REMOTE_A2A_RESOURCE, _CONVERSATIONAL_AGENT_TOOL + ) + ) + + agent_tool, remote, conversational = config.resources + assert type(agent_tool) is AgentProcessToolResourceConfig + assert type(remote) is AgentA2aResourceConfig + assert remote.slug == "remote-agent" + assert type(conversational) is AgentConversationalAgentToolResourceConfig + assert conversational.resource_type == AgentResourceType.TOOL + assert conversational.type == AgentToolType.CONVERSATIONAL_AGENT + assert conversational.name == "Conversational Agent" + assert conversational.properties.process_name == "My Conversational Agent" + assert conversational.properties.folder_path == "Shared/Agents" + assert conversational.properties.cached_agent_card == { + "name": "Conversational Agent" + } + assert conversational.properties.require_conversational_confirmation is True + assert conversational.input_schema["required"] == ["message"] + + @pytest.mark.parametrize( + "type_value", + ["conversationalAgent", "ConversationalAgent", "conversationalagent"], + ) + def test_type_casing_is_normalized(self, type_value: str): + record = {**_CONVERSATIONAL_AGENT_TOOL, "type": type_value} + + (resource,) = AgentDefinition.model_validate( + _conversational_agent_json(record) + ).resources + + assert type(resource) is AgentConversationalAgentToolResourceConfig + assert resource.type == AgentToolType.CONVERSATIONAL_AGENT + + def test_optional_properties_may_be_missing_or_null(self): + record = { + **_CONVERSATIONAL_AGENT_TOOL, + "properties": {"processName": "p", "cachedAgentCard": None}, + } + + (resource,) = AgentDefinition.model_validate( + _conversational_agent_json(record) + ).resources + + assert isinstance(resource, AgentConversationalAgentToolResourceConfig) + assert resource.properties.folder_path is None + assert resource.properties.cached_agent_card is None + + def test_missing_process_name_still_loads_the_agent(self): + record = {**_CONVERSATIONAL_AGENT_TOOL, "properties": {"folderPath": "x"}} + + config = AgentDefinition.model_validate( + _conversational_agent_json(_AGENT_TOOL, record) + ) + + agent_tool, conversational = config.resources + assert type(agent_tool) is AgentProcessToolResourceConfig + assert type(conversational) is AgentConversationalAgentToolResourceConfig + assert conversational.properties.process_name is None + + def test_contract_fixture_parses_with_every_field_preserved(self): + record = json.loads(_CONTRACT_FIXTURE) + + (resource,) = AgentDefinition.model_validate( + _conversational_agent_json(record) + ).resources + + assert type(resource) is AgentConversationalAgentToolResourceConfig + assert resource.resource_type == AgentResourceType.TOOL + assert resource.type == AgentToolType.CONVERSATIONAL_AGENT + assert resource.name == "Support agent" + assert resource.description == "Answers support questions" + assert resource.is_enabled is True + assert resource.input_schema == record["inputSchema"] + assert resource.output_schema == record["outputSchema"] + assert resource.properties.process_name == "SupportAgent" + assert resource.properties.folder_path == "Shared/Support" + assert resource.properties.cached_agent_card is None + assert resource.properties.require_conversational_confirmation is False + extras = resource.model_extra or {} + assert extras["id"] == "conv-1" + assert extras["location"] == "solution" + assert extras["canvasNodeId"] == "node-1" + + def test_legacy_argument_properties_record_still_parses(self): + legacy_properties = {"message": {"variant": "static"}} + record = { + **_CONVERSATIONAL_AGENT_TOOL, + "argumentProperties": legacy_properties, + } + + (resource,) = AgentDefinition.model_validate( + _conversational_agent_json(record) + ).resources + + assert type(resource) is AgentConversationalAgentToolResourceConfig + assert not hasattr(resource, "argument_properties") + assert (resource.model_extra or {})["argumentProperties"] == legacy_properties + + def test_unknown_tool_type_still_goes_to_unknown(self): + record = {**_CONVERSATIONAL_AGENT_TOOL, "type": "futureKind"} + + (resource,) = AgentDefinition.model_validate( + _conversational_agent_json(record) + ).resources + + assert isinstance(resource, AgentUnknownToolResourceConfig) + + def test_round_trip_by_alias_keeps_type(self): + config = AgentDefinition.model_validate( + _conversational_agent_json(_CONVERSATIONAL_AGENT_TOOL) + ) + + dumped = config.model_dump(by_alias=True, mode="json") + (resource,) = dumped["resources"] + assert resource["$resourceType"] == "tool" + assert resource["type"] == "ConversationalAgent" + assert resource["properties"]["processName"] == "My Conversational Agent" + assert resource["properties"]["folderPath"] == "Shared/Agents" + + reparsed = AgentDefinition.model_validate(dumped) + assert isinstance( + reparsed.resources[0], AgentConversationalAgentToolResourceConfig + ) diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 09541f1cc..cede61da3 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2599,7 +2599,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.14.36" +version = "2.14.37" source = { editable = "." } dependencies = [ { name = "applicationinsights" },