From c42b88788a1c6849445506b19842e93f8e633760 Mon Sep 17 00:00:00 2001 From: "diana.grecu" Date: Sat, 24 Jan 2026 21:43:52 +0200 Subject: [PATCH 01/11] feat: initial taskTitleV2 support # Conflicts: # src/uipath_langchain/agent/tools/escalation_tool.py --- .../agent/tools/escalation_tool.py | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/uipath_langchain/agent/tools/escalation_tool.py b/src/uipath_langchain/agent/tools/escalation_tool.py index 2b456a81..932e8e20 100644 --- a/src/uipath_langchain/agent/tools/escalation_tool.py +++ b/src/uipath_langchain/agent/tools/escalation_tool.py @@ -12,8 +12,9 @@ AgentEscalationRecipientType, AgentEscalationResourceConfig, AssetRecipient, - StandardRecipient, + StandardRecipient, TaskTitleType, ) +from uipath.agent.utils.text_tokens import build_string_from_tokens from uipath.eval.mocks import mockable from uipath.platform import UiPath from uipath.platform.action_center.tasks import TaskRecipient, TaskRecipientType @@ -21,7 +22,6 @@ from uipath.runtime.errors import UiPathErrorCode from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model -from uipath_langchain.agent.react.types import AgentGraphState from uipath_langchain.agent.tools.static_args import ( handle_static_args, ) @@ -29,9 +29,10 @@ StructuredToolWithArgumentProperties, ) +from ..react.types import AgentGraphState from ..exceptions import AgentTerminationException +from .utils import sanitize_tool_name, sanitize_dict_for_serialization from .tool_node import ToolWrapperReturnType -from .utils import sanitize_tool_name class EscalationAction(str, Enum): @@ -100,7 +101,18 @@ def create_escalation_tool( example_calls=channel.properties.example_calls, ) async def escalation_tool_fn(**kwargs: Any) -> dict[str, Any]: - task_title = channel.task_title or "Escalation Task" + #task_title = channel.task_title or "Escalation Task" + if channel.task_title_v2: + if channel.task_title_v2.type == TaskTitleType.TEXT_BUILDER: + task_title = tool.metadata["taskTitleV2"] + else: + raise NotImplementedError( + f"TaskTitle type '{channel.task_title_v2.type}' not implemented" + ) + elif channel.task_title: + task_title = channel.task_title # Legacy fallback + else: + task_title = "Escalation Task" # Default recipient: TaskRecipient | None = ( await resolve_recipient_value(channel.recipients[0]) @@ -150,6 +162,8 @@ async def escalation_wrapper( call: ToolCall, state: AgentGraphState, ) -> ToolWrapperReturnType: + tool.metadata["taskTitleV2"] = build_string_from_tokens(channel.task_title_v2.tokens, sanitize_dict_for_serialization( + dict(state))) call["args"] = handle_static_args(resource, state, call["args"]) result = await tool.ainvoke(call["args"]) From ff300297df54cb1f56d0b9d8faed8fcd553ae92e Mon Sep 17 00:00:00 2001 From: "diana.grecu" Date: Mon, 26 Jan 2026 12:38:59 +0200 Subject: [PATCH 02/11] feat: remove taskTitleV2 from code --- .../agent/tools/escalation_tool.py | 37 ++++--- tests/agent/tools/test_escalation_tool.py | 100 ++++++++++++++++++ 2 files changed, 121 insertions(+), 16 deletions(-) diff --git a/src/uipath_langchain/agent/tools/escalation_tool.py b/src/uipath_langchain/agent/tools/escalation_tool.py index 932e8e20..9553be63 100644 --- a/src/uipath_langchain/agent/tools/escalation_tool.py +++ b/src/uipath_langchain/agent/tools/escalation_tool.py @@ -12,7 +12,8 @@ AgentEscalationRecipientType, AgentEscalationResourceConfig, AssetRecipient, - StandardRecipient, TaskTitleType, + StandardRecipient, + TextBuilderTaskTitle, ) from uipath.agent.utils.text_tokens import build_string_from_tokens from uipath.eval.mocks import mockable @@ -29,10 +30,10 @@ StructuredToolWithArgumentProperties, ) -from ..react.types import AgentGraphState from ..exceptions import AgentTerminationException -from .utils import sanitize_tool_name, sanitize_dict_for_serialization from .tool_node import ToolWrapperReturnType +from ..react.types import AgentGraphState +from .utils import sanitize_dict_for_serialization, sanitize_tool_name class EscalationAction(str, Enum): @@ -101,18 +102,17 @@ def create_escalation_tool( example_calls=channel.properties.example_calls, ) async def escalation_tool_fn(**kwargs: Any) -> dict[str, Any]: - #task_title = channel.task_title or "Escalation Task" - if channel.task_title_v2: - if channel.task_title_v2.type == TaskTitleType.TEXT_BUILDER: - task_title = tool.metadata["taskTitleV2"] - else: - raise NotImplementedError( - f"TaskTitle type '{channel.task_title_v2.type}' not implemented" - ) - elif channel.task_title: - task_title = channel.task_title # Legacy fallback + # Get task title: use built string from tokens if TEXT_BUILDER, otherwise use directly + if isinstance(channel.task_title, TextBuilderTaskTitle): + if tool.metadata is None: + raise RuntimeError("Tool metadata is required for TEXT_BUILDER task titles") + task_title = tool.metadata["taskTitle"] + elif isinstance(channel.task_title, str): + task_title = channel.task_title else: - task_title = "Escalation Task" # Default + raise NotImplementedError( + f"TaskTitle type '{type(channel.task_title).__name__}' not implemented" + ) recipient: TaskRecipient | None = ( await resolve_recipient_value(channel.recipients[0]) @@ -162,8 +162,13 @@ async def escalation_wrapper( call: ToolCall, state: AgentGraphState, ) -> ToolWrapperReturnType: - tool.metadata["taskTitleV2"] = build_string_from_tokens(channel.task_title_v2.tokens, sanitize_dict_for_serialization( - dict(state))) + # Build task title from tokens if it's a TEXT_BUILDER type + if isinstance(channel.task_title, TextBuilderTaskTitle): + if tool.metadata is None: + raise RuntimeError("Tool metadata is required for TEXT_BUILDER task titles") + tool.metadata["taskTitle"] = build_string_from_tokens( + channel.task_title.tokens, sanitize_dict_for_serialization(dict(state)) + ) call["args"] = handle_static_args(resource, state, call["args"]) result = await tool.ainvoke(call["args"]) diff --git a/tests/agent/tools/test_escalation_tool.py b/tests/agent/tools/test_escalation_tool.py index 90d39908..b46ff976 100644 --- a/tests/agent/tools/test_escalation_tool.py +++ b/tests/agent/tools/test_escalation_tool.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest +from langchain_core.messages import ToolCall from uipath.agent.models.agent import ( AgentEscalationChannel, AgentEscalationChannelProperties, @@ -10,6 +11,9 @@ AgentEscalationResourceConfig, AssetRecipient, StandardRecipient, + TextBuilderTaskTitle, + TextToken, + TextTokenType, ) from uipath.platform.action_center.tasks import TaskRecipient, TaskRecipientType @@ -308,3 +312,99 @@ async def test_escalation_tool_metadata_recipient_none_when_no_recipients( assert tool.metadata is not None assert tool.metadata["recipient"] is None + + @pytest.mark.asyncio + @patch("uipath_langchain.agent.tools.escalation_tool.interrupt") + async def test_escalation_tool_with_string_task_title(self, mock_interrupt): + """Test escalation tool with legacy string task title.""" + mock_result = MagicMock() + mock_result.action = None + mock_result.data = {} + mock_interrupt.return_value = mock_result + + # Create resource with string task title + resource = AgentEscalationResourceConfig( + name="approval", + description="Request approval", + channels=[ + AgentEscalationChannel( + name="action_center", + type="actionCenter", + description="Action Center channel", + input_schema={"type": "object", "properties": {}}, + output_schema={"type": "object", "properties": {}}, + properties=AgentEscalationChannelProperties( + app_name="ApprovalApp", + app_version=1, + resource_key="test-key", + ), + recipients=[], + task_title="Static Task Title", + ) + ], + ) + + tool = await create_escalation_tool(resource) + + # Invoke the tool + await tool.ainvoke({}) + + # Verify interrupt was called with the static title + call_args = mock_interrupt.call_args[0][0] + assert call_args.title == "Static Task Title" + + @pytest.mark.asyncio + @patch("uipath_langchain.agent.tools.escalation_tool.interrupt") + async def test_escalation_tool_with_text_builder_task_title(self, mock_interrupt): + """Test escalation tool with TEXT_BUILDER task title builds from tokens.""" + mock_result = MagicMock() + mock_result.action = None + mock_result.data = {} + mock_interrupt.return_value = mock_result + + # Create resource with TEXT_BUILDER task title containing variable token + resource = AgentEscalationResourceConfig( + name="approval", + description="Request approval", + channels=[ + AgentEscalationChannel( + name="action_center", + type="actionCenter", + description="Action Center channel", + input_schema={"type": "object", "properties": {}}, + output_schema={"type": "object", "properties": {}}, + properties=AgentEscalationChannelProperties( + app_name="ApprovalApp", + app_version=1, + resource_key="test-key", + ), + recipients=[], + task_title=TextBuilderTaskTitle( + type="textBuilder", + tokens=[ + TextToken( + type=TextTokenType.SIMPLE_TEXT, + raw_string="Approve request for ", + ), + TextToken( + type=TextTokenType.VARIABLE, raw_string="input.userName" + ), + ], + ), + ) + ], + ) + + tool = await create_escalation_tool(resource) + + # Create mock state with variables for token interpolation + state = {"userName": "John Doe", "messages": []} + call = ToolCall(args={}, id="test-call", name=tool.name) + + # Invoke through the wrapper to test full flow + await tool.awrapper(tool, call, state) # type: ignore[attr-defined] + + # Verify interrupt was called with the correctly built task title + assert mock_interrupt.called + call_args = mock_interrupt.call_args[0][0] + assert call_args.title == "Approve request for John Doe" From cbfe01521b0fb90348bd3e66b0d11a564bce16d8 Mon Sep 17 00:00:00 2001 From: "diana.grecu" Date: Mon, 26 Jan 2026 15:58:36 +0200 Subject: [PATCH 03/11] fix: linting errors --- .../agent/tools/escalation_tool.py | 8 +- tests/agent/tools/test_escalation_tool.py | 84 +++++++++---------- 2 files changed, 44 insertions(+), 48 deletions(-) diff --git a/src/uipath_langchain/agent/tools/escalation_tool.py b/src/uipath_langchain/agent/tools/escalation_tool.py index 9553be63..eeb9bc76 100644 --- a/src/uipath_langchain/agent/tools/escalation_tool.py +++ b/src/uipath_langchain/agent/tools/escalation_tool.py @@ -105,7 +105,9 @@ async def escalation_tool_fn(**kwargs: Any) -> dict[str, Any]: # Get task title: use built string from tokens if TEXT_BUILDER, otherwise use directly if isinstance(channel.task_title, TextBuilderTaskTitle): if tool.metadata is None: - raise RuntimeError("Tool metadata is required for TEXT_BUILDER task titles") + raise RuntimeError( + "Tool metadata is required for TEXT_BUILDER task titles" + ) task_title = tool.metadata["taskTitle"] elif isinstance(channel.task_title, str): task_title = channel.task_title @@ -165,7 +167,9 @@ async def escalation_wrapper( # Build task title from tokens if it's a TEXT_BUILDER type if isinstance(channel.task_title, TextBuilderTaskTitle): if tool.metadata is None: - raise RuntimeError("Tool metadata is required for TEXT_BUILDER task titles") + raise RuntimeError( + "Tool metadata is required for TEXT_BUILDER task titles" + ) tool.metadata["taskTitle"] = build_string_from_tokens( channel.task_title.tokens, sanitize_dict_for_serialization(dict(state)) ) diff --git a/tests/agent/tools/test_escalation_tool.py b/tests/agent/tools/test_escalation_tool.py index b46ff976..96879f4d 100644 --- a/tests/agent/tools/test_escalation_tool.py +++ b/tests/agent/tools/test_escalation_tool.py @@ -11,9 +11,6 @@ AgentEscalationResourceConfig, AssetRecipient, StandardRecipient, - TextBuilderTaskTitle, - TextToken, - TextTokenType, ) from uipath.platform.action_center.tasks import TaskRecipient, TaskRecipientType @@ -323,25 +320,25 @@ async def test_escalation_tool_with_string_task_title(self, mock_interrupt): mock_interrupt.return_value = mock_result # Create resource with string task title + channel_dict = { + "name": "action_center", + "type": "actionCenter", + "description": "Action Center channel", + "inputSchema": {"type": "object", "properties": {}}, + "outputSchema": {"type": "object", "properties": {}}, + "properties": { + "appName": "ApprovalApp", + "appVersion": 1, + "resourceKey": "test-key", + }, + "recipients": [], + "taskTitle": "Static Task Title", + } + resource = AgentEscalationResourceConfig( name="approval", description="Request approval", - channels=[ - AgentEscalationChannel( - name="action_center", - type="actionCenter", - description="Action Center channel", - input_schema={"type": "object", "properties": {}}, - output_schema={"type": "object", "properties": {}}, - properties=AgentEscalationChannelProperties( - app_name="ApprovalApp", - app_version=1, - resource_key="test-key", - ), - recipients=[], - task_title="Static Task Title", - ) - ], + channels=[AgentEscalationChannel(**channel_dict)], ) tool = await create_escalation_tool(resource) @@ -363,36 +360,31 @@ async def test_escalation_tool_with_text_builder_task_title(self, mock_interrupt mock_interrupt.return_value = mock_result # Create resource with TEXT_BUILDER task title containing variable token + channel_dict = { + "name": "action_center", + "type": "actionCenter", + "description": "Action Center channel", + "inputSchema": {"type": "object", "properties": {}}, + "outputSchema": {"type": "object", "properties": {}}, + "properties": { + "appName": "ApprovalApp", + "appVersion": 1, + "resourceKey": "test-key", + }, + "recipients": [], + "taskTitle": { + "type": "textBuilder", + "tokens": [ + {"type": "simpleText", "rawString": "Approve request for "}, + {"type": "variable", "rawString": "input.userName"}, + ], + }, + } + resource = AgentEscalationResourceConfig( name="approval", description="Request approval", - channels=[ - AgentEscalationChannel( - name="action_center", - type="actionCenter", - description="Action Center channel", - input_schema={"type": "object", "properties": {}}, - output_schema={"type": "object", "properties": {}}, - properties=AgentEscalationChannelProperties( - app_name="ApprovalApp", - app_version=1, - resource_key="test-key", - ), - recipients=[], - task_title=TextBuilderTaskTitle( - type="textBuilder", - tokens=[ - TextToken( - type=TextTokenType.SIMPLE_TEXT, - raw_string="Approve request for ", - ), - TextToken( - type=TextTokenType.VARIABLE, raw_string="input.userName" - ), - ], - ), - ) - ], + channels=[AgentEscalationChannel(**channel_dict)], ) tool = await create_escalation_tool(resource) From 0d9037d0bb4223786a84c6998114f7ba9dbf1533 Mon Sep 17 00:00:00 2001 From: "diana.grecu" Date: Mon, 26 Jan 2026 16:02:47 +0200 Subject: [PATCH 04/11] fix: update metadata field name --- src/uipath_langchain/agent/tools/escalation_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uipath_langchain/agent/tools/escalation_tool.py b/src/uipath_langchain/agent/tools/escalation_tool.py index eeb9bc76..2b7abeb5 100644 --- a/src/uipath_langchain/agent/tools/escalation_tool.py +++ b/src/uipath_langchain/agent/tools/escalation_tool.py @@ -202,7 +202,7 @@ async def escalation_wrapper( "tool_type": "escalation", "display_name": channel.properties.app_name, "channel_type": channel.type, - "assignee": None, + "recipient": None, }, ) tool.set_tool_wrappers(awrapper=escalation_wrapper) From 059f1caf5cdd6010ea339333288c7228915b8e15 Mon Sep 17 00:00:00 2001 From: "diana.grecu" Date: Tue, 27 Jan 2026 18:16:05 +0200 Subject: [PATCH 05/11] fix: linting error --- src/uipath_langchain/agent/tools/escalation_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uipath_langchain/agent/tools/escalation_tool.py b/src/uipath_langchain/agent/tools/escalation_tool.py index 2b7abeb5..740cff8c 100644 --- a/src/uipath_langchain/agent/tools/escalation_tool.py +++ b/src/uipath_langchain/agent/tools/escalation_tool.py @@ -31,8 +31,8 @@ ) from ..exceptions import AgentTerminationException -from .tool_node import ToolWrapperReturnType from ..react.types import AgentGraphState +from .tool_node import ToolWrapperReturnType from .utils import sanitize_dict_for_serialization, sanitize_tool_name From d8199a90298efaa012741f72c589991996034291 Mon Sep 17 00:00:00 2001 From: "diana.grecu" Date: Wed, 28 Jan 2026 14:46:35 +0200 Subject: [PATCH 06/11] fix: address comments --- .../agent/tools/escalation_tool.py | 31 ++++++------------- tests/agent/tools/test_escalation_tool.py | 20 ++++++++---- 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/src/uipath_langchain/agent/tools/escalation_tool.py b/src/uipath_langchain/agent/tools/escalation_tool.py index 740cff8c..fbcfbc92 100644 --- a/src/uipath_langchain/agent/tools/escalation_tool.py +++ b/src/uipath_langchain/agent/tools/escalation_tool.py @@ -102,29 +102,16 @@ def create_escalation_tool( example_calls=channel.properties.example_calls, ) async def escalation_tool_fn(**kwargs: Any) -> dict[str, Any]: - # Get task title: use built string from tokens if TEXT_BUILDER, otherwise use directly - if isinstance(channel.task_title, TextBuilderTaskTitle): - if tool.metadata is None: - raise RuntimeError( - "Tool metadata is required for TEXT_BUILDER task titles" - ) - task_title = tool.metadata["taskTitle"] - elif isinstance(channel.task_title, str): - task_title = channel.task_title - else: - raise NotImplementedError( - f"TaskTitle type '{type(channel.task_title).__name__}' not implemented" - ) - recipient: TaskRecipient | None = ( await resolve_recipient_value(channel.recipients[0]) if channel.recipients else None ) - # Recipient requires runtime resolution, store in metadata after resolving if tool.metadata is not None: + # Recipient requires runtime resolution, store in metadata after resolving tool.metadata["recipient"] = recipient + task_title = tool.metadata["task_title"] result = interrupt( CreateEscalation( @@ -133,7 +120,6 @@ async def escalation_tool_fn(**kwargs: Any) -> dict[str, Any]: recipient=recipient, app_name=channel.properties.app_name, app_folder_path=channel.properties.folder_name, - app_version=channel.properties.app_version, priority=channel.priority, labels=channel.labels, is_actionable_message_enabled=channel.properties.is_actionable_message_enabled, @@ -164,15 +150,16 @@ async def escalation_wrapper( call: ToolCall, state: AgentGraphState, ) -> ToolWrapperReturnType: - # Build task title from tokens if it's a TEXT_BUILDER type + if tool.metadata is None: + raise RuntimeError("Tool metadata is required for task_title resolution") + if isinstance(channel.task_title, TextBuilderTaskTitle): - if tool.metadata is None: - raise RuntimeError( - "Tool metadata is required for TEXT_BUILDER task titles" - ) - tool.metadata["taskTitle"] = build_string_from_tokens( + tool.metadata["task_title"] = build_string_from_tokens( channel.task_title.tokens, sanitize_dict_for_serialization(dict(state)) ) + elif isinstance(channel.task_title, str): + tool.metadata["task_title"] = channel.task_title + call["args"] = handle_static_args(resource, state, call["args"]) result = await tool.ainvoke(call["args"]) diff --git a/tests/agent/tools/test_escalation_tool.py b/tests/agent/tools/test_escalation_tool.py index 96879f4d..009965b9 100644 --- a/tests/agent/tools/test_escalation_tool.py +++ b/tests/agent/tools/test_escalation_tool.py @@ -282,8 +282,11 @@ async def test_escalation_tool_metadata_has_recipient( tool = create_escalation_tool(escalation_resource) - # Invoke the tool to trigger assignee resolution - await tool.ainvoke({}) + # Create mock state and call to invoke through wrapper + call = ToolCall(args={}, id="test-call", name=tool.name) + + # Invoke through the wrapper to test full flow + await tool.awrapper(tool, call, {}) # type: ignore[attr-defined] assert tool.metadata is not None assert tool.metadata["recipient"] == TaskRecipient( @@ -304,8 +307,11 @@ async def test_escalation_tool_metadata_recipient_none_when_no_recipients( tool = create_escalation_tool(escalation_resource_no_recipient) - # Invoke the tool to trigger assignee resolution - await tool.ainvoke({}) + # Create mock state and call to invoke through wrapper + call = ToolCall(args={}, id="test-call", name=tool.name) + + # Invoke through the wrapper to test full flow + await tool.awrapper(tool, call, {}) # type: ignore[attr-defined] assert tool.metadata is not None assert tool.metadata["recipient"] is None @@ -343,8 +349,10 @@ async def test_escalation_tool_with_string_task_title(self, mock_interrupt): tool = await create_escalation_tool(resource) - # Invoke the tool - await tool.ainvoke({}) + call = ToolCall(args={}, id="test-call", name=tool.name) + + # Invoke through the wrapper to test full flow + await tool.awrapper(tool, call, {}) # type: ignore[attr-defined] # Verify interrupt was called with the static title call_args = mock_interrupt.call_args[0][0] From dd37647c90b1038171ee2a01fc653369a4653291 Mon Sep 17 00:00:00 2001 From: "diana.grecu" Date: Wed, 28 Jan 2026 19:55:16 +0200 Subject: [PATCH 07/11] fix: task title should default to Escalation Task if empty --- .../agent/tools/escalation_tool.py | 2 +- tests/agent/tools/test_escalation_tool.py | 44 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/uipath_langchain/agent/tools/escalation_tool.py b/src/uipath_langchain/agent/tools/escalation_tool.py index fbcfbc92..295de454 100644 --- a/src/uipath_langchain/agent/tools/escalation_tool.py +++ b/src/uipath_langchain/agent/tools/escalation_tool.py @@ -111,7 +111,7 @@ async def escalation_tool_fn(**kwargs: Any) -> dict[str, Any]: if tool.metadata is not None: # Recipient requires runtime resolution, store in metadata after resolving tool.metadata["recipient"] = recipient - task_title = tool.metadata["task_title"] + task_title = tool.metadata.get("task_title") or "Escalation Task" result = interrupt( CreateEscalation( diff --git a/tests/agent/tools/test_escalation_tool.py b/tests/agent/tools/test_escalation_tool.py index 009965b9..dd2c1fe3 100644 --- a/tests/agent/tools/test_escalation_tool.py +++ b/tests/agent/tools/test_escalation_tool.py @@ -408,3 +408,47 @@ async def test_escalation_tool_with_text_builder_task_title(self, mock_interrupt assert mock_interrupt.called call_args = mock_interrupt.call_args[0][0] assert call_args.title == "Approve request for John Doe" + + @pytest.mark.asyncio + @patch("uipath_langchain.agent.tools.escalation_tool.interrupt") + async def test_escalation_tool_with_empty_task_title_defaults_to_escalation_task( + self, mock_interrupt + ): + """Test escalation tool defaults to 'Escalation Task' when task title is empty.""" + mock_result = MagicMock() + mock_result.action = None + mock_result.data = {} + mock_interrupt.return_value = mock_result + + # Create resource with empty string task title + channel_dict = { + "name": "action_center", + "type": "actionCenter", + "description": "Action Center channel", + "inputSchema": {"type": "object", "properties": {}}, + "outputSchema": {"type": "object", "properties": {}}, + "properties": { + "appName": "ApprovalApp", + "appVersion": 1, + "resourceKey": "test-key", + }, + "recipients": [], + "taskTitle": "", + } + + resource = AgentEscalationResourceConfig( + name="approval", + description="Request approval", + channels=[AgentEscalationChannel(**channel_dict)], + ) + + tool = await create_escalation_tool(resource) + + call = ToolCall(args={}, id="test-call", name=tool.name) + + # Invoke through the wrapper to test full flow + await tool.awrapper(tool, call, {}) # type: ignore[attr-defined] + + # Verify interrupt was called with the default title + call_args = mock_interrupt.call_args[0][0] + assert call_args.title == "Escalation Task" From 755143047c89b869d049fa50169a689fc1736a44 Mon Sep 17 00:00:00 2001 From: "diana.grecu" Date: Thu, 29 Jan 2026 10:02:41 +0200 Subject: [PATCH 08/11] fix: reformat task title initialization --- src/uipath_langchain/agent/tools/escalation_tool.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/uipath_langchain/agent/tools/escalation_tool.py b/src/uipath_langchain/agent/tools/escalation_tool.py index 295de454..6030f3eb 100644 --- a/src/uipath_langchain/agent/tools/escalation_tool.py +++ b/src/uipath_langchain/agent/tools/escalation_tool.py @@ -108,10 +108,11 @@ async def escalation_tool_fn(**kwargs: Any) -> dict[str, Any]: else None ) + task_title = "Escalation Task" if tool.metadata is not None: # Recipient requires runtime resolution, store in metadata after resolving tool.metadata["recipient"] = recipient - task_title = tool.metadata.get("task_title") or "Escalation Task" + task_title = tool.metadata.get("task_title") or task_title result = interrupt( CreateEscalation( From b542ac6c1ee254035d07c26184c490c4c8fb8a85 Mon Sep 17 00:00:00 2001 From: "diana.grecu" Date: Thu, 29 Jan 2026 14:53:54 +0200 Subject: [PATCH 09/11] feat: small refactoring # Conflicts: # src/uipath_langchain/agent/tools/escalation_tool.py --- .../agent/tools/escalation_tool.py | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/uipath_langchain/agent/tools/escalation_tool.py b/src/uipath_langchain/agent/tools/escalation_tool.py index 6030f3eb..6ad81d11 100644 --- a/src/uipath_langchain/agent/tools/escalation_tool.py +++ b/src/uipath_langchain/agent/tools/escalation_tool.py @@ -13,6 +13,7 @@ AgentEscalationResourceConfig, AssetRecipient, StandardRecipient, + TaskTitle, TextBuilderTaskTitle, ) from uipath.agent.utils.text_tokens import build_string_from_tokens @@ -83,6 +84,19 @@ async def resolve_asset(asset_name: str, folder_path: str) -> str | None: ) from e +def _resolve_task_title( + task_title: TaskTitle | str | None, agent_input: dict[str, Any] +) -> str: + """Resolve task title based on channel configuration.""" + if isinstance(task_title, TextBuilderTaskTitle): + return build_string_from_tokens(task_title.tokens, agent_input) + + if isinstance(task_title, str): + return task_title + + return "Escalation Task" + + def create_escalation_tool( resource: AgentEscalationResourceConfig, ) -> StructuredTool: @@ -154,12 +168,9 @@ async def escalation_wrapper( if tool.metadata is None: raise RuntimeError("Tool metadata is required for task_title resolution") - if isinstance(channel.task_title, TextBuilderTaskTitle): - tool.metadata["task_title"] = build_string_from_tokens( - channel.task_title.tokens, sanitize_dict_for_serialization(dict(state)) - ) - elif isinstance(channel.task_title, str): - tool.metadata["task_title"] = channel.task_title + tool.metadata["task_title"] = _resolve_task_title( + channel.task_title, sanitize_dict_for_serialization(dict(state)) + ) call["args"] = handle_static_args(resource, state, call["args"]) result = await tool.ainvoke(call["args"]) From 930799440c14a2a1a16d718ac1b3ef28ee3f90c2 Mon Sep 17 00:00:00 2001 From: "diana.grecu" Date: Thu, 29 Jan 2026 14:58:58 +0200 Subject: [PATCH 10/11] chore: update version --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d3b9decc..edbbbe45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-langchain" -version = "0.5.13" +version = "0.5.14" description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/uv.lock b/uv.lock index 3271afd0..7a723927 100644 --- a/uv.lock +++ b/uv.lock @@ -3297,7 +3297,7 @@ wheels = [ [[package]] name = "uipath-langchain" -version = "0.5.13" +version = "0.5.14" source = { editable = "." } dependencies = [ { name = "httpx" }, From 870ec354f05704f647d9bdad809eec97fc7f87b5 Mon Sep 17 00:00:00 2001 From: "diana.grecu" Date: Thu, 29 Jan 2026 15:01:52 +0200 Subject: [PATCH 11/11] fix: failing tests after rebase --- tests/agent/tools/test_escalation_tool.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/agent/tools/test_escalation_tool.py b/tests/agent/tools/test_escalation_tool.py index dd2c1fe3..624f42f1 100644 --- a/tests/agent/tools/test_escalation_tool.py +++ b/tests/agent/tools/test_escalation_tool.py @@ -347,7 +347,7 @@ async def test_escalation_tool_with_string_task_title(self, mock_interrupt): channels=[AgentEscalationChannel(**channel_dict)], ) - tool = await create_escalation_tool(resource) + tool = create_escalation_tool(resource) call = ToolCall(args={}, id="test-call", name=tool.name) @@ -395,7 +395,7 @@ async def test_escalation_tool_with_text_builder_task_title(self, mock_interrupt channels=[AgentEscalationChannel(**channel_dict)], ) - tool = await create_escalation_tool(resource) + tool = create_escalation_tool(resource) # Create mock state with variables for token interpolation state = {"userName": "John Doe", "messages": []} @@ -442,7 +442,7 @@ async def test_escalation_tool_with_empty_task_title_defaults_to_escalation_task channels=[AgentEscalationChannel(**channel_dict)], ) - tool = await create_escalation_tool(resource) + tool = create_escalation_tool(resource) call = ToolCall(args={}, id="test-call", name=tool.name)