Skip to content

Commit 4e037c7

Browse files
Feat/add guardrails task recipient (#448)
1 parent 5a35650 commit 4e037c7

6 files changed

Lines changed: 73 additions & 32 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
[project]
22
name = "uipath-langchain"
3-
version = "0.4.21"
3+
version = "0.4.22"
44
description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"
77
dependencies = [
8-
"uipath>=2.5.22,<2.6.0",
8+
"uipath>=2.5.23,<2.6.0",
99
"uipath-runtime>=0.5.1,<0.6.0",
1010
"langgraph>=1.0.0, <2.0.0",
1111
"langchain-core>=1.2.5, <2.0.0",

src/uipath_langchain/agent/guardrails/actions/escalate_action.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ async def _node(
7878
state: AgentGuardrailsGraphState,
7979
) -> Dict[str, Any] | Command[Any]:
8080
# Resolve recipient value (handles both StandardRecipient and AssetRecipient)
81-
assignee = await resolve_recipient_value(self.recipient)
81+
task_recipient = await resolve_recipient_value(self.recipient)
8282

8383
# Validate message count based on execution stage
8484
_validate_message_count(state, execution_stage)
@@ -140,7 +140,7 @@ async def _node(
140140
app_folder_path=self.app_folder_path,
141141
title="Agents Guardrail Task",
142142
data=data,
143-
assignee=assignee,
143+
recipient=task_recipient,
144144
)
145145
)
146146

src/uipath_langchain/agent/tools/escalation_tool.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
from uipath.agent.models.agent import (
1010
AgentEscalationChannel,
1111
AgentEscalationRecipient,
12+
AgentEscalationRecipientType,
1213
AgentEscalationResourceConfig,
1314
AssetRecipient,
1415
StandardRecipient,
1516
)
1617
from uipath.eval.mocks import mockable
1718
from uipath.platform import UiPath
19+
from uipath.platform.action_center.tasks import TaskRecipient, TaskRecipientType
1820
from uipath.platform.common import CreateEscalation
1921
from uipath.runtime.errors import UiPathErrorCode
2022

@@ -32,13 +34,24 @@ class EscalationAction(str, Enum):
3234
END = "end"
3335

3436

35-
async def resolve_recipient_value(recipient: AgentEscalationRecipient) -> str | None:
37+
async def resolve_recipient_value(
38+
recipient: AgentEscalationRecipient,
39+
) -> TaskRecipient | None:
3640
"""Resolve recipient value based on recipient type."""
3741
if isinstance(recipient, AssetRecipient):
38-
return await resolve_asset(recipient.asset_name, recipient.folder_path)
42+
value = await resolve_asset(recipient.asset_name, recipient.folder_path)
43+
type = None
44+
if recipient.type == AgentEscalationRecipientType.ASSET_USER_EMAIL:
45+
type = TaskRecipientType.EMAIL
46+
elif recipient.type == AgentEscalationRecipientType.ASSET_GROUP_NAME:
47+
type = TaskRecipientType.GROUP_NAME
48+
return TaskRecipient(value=value, type=type)
3949

4050
if isinstance(recipient, StandardRecipient):
41-
return recipient.value
51+
type = TaskRecipientType(recipient.type)
52+
if recipient.type == AgentEscalationRecipientType.USER_EMAIL:
53+
type = TaskRecipientType.EMAIL
54+
return TaskRecipient(value=recipient.value, type=type)
4255

4356
return None
4457

@@ -86,21 +99,21 @@ async def create_escalation_tool(
8699
async def escalation_tool_fn(**kwargs: Any) -> dict[str, Any]:
87100
task_title = channel.task_title or "Escalation Task"
88101

89-
assignee: str | None = (
102+
recipient: TaskRecipient | None = (
90103
await resolve_recipient_value(channel.recipients[0])
91104
if channel.recipients
92105
else None
93106
)
94107

95-
# Assignee requires runtime resolution, store in metadata after resolving
108+
# Recipient requires runtime resolution, store in metadata after resolving
96109
if tool.metadata is not None:
97-
tool.metadata["assignee"] = assignee
110+
tool.metadata["recipient"] = recipient
98111

99112
result = interrupt(
100113
CreateEscalation(
101114
title=task_title,
102115
data=kwargs,
103-
assignee=assignee,
116+
recipient=recipient,
104117
app_name=channel.properties.app_name,
105118
app_folder_path=channel.properties.folder_name,
106119
app_version=channel.properties.app_version,

tests/agent/guardrails/actions/test_escalate_action.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
AssetRecipient,
1414
StandardRecipient,
1515
)
16+
from uipath.platform.action_center.tasks import TaskRecipient, TaskRecipientType
1617
from uipath.platform.guardrails import GuardrailScope
1718
from uipath.runtime.errors import UiPathErrorCode
1819

@@ -184,7 +185,10 @@ async def test_node_interrupts_with_correct_message_data(
184185
assert call_args.app_name == "TestApp"
185186
assert call_args.app_folder_path == "TestFolder"
186187
assert call_args.title == "Agents Guardrail Task"
187-
assert call_args.assignee == "test@example.com"
188+
assert call_args.assignee == ""
189+
assert call_args.recipient == TaskRecipient(
190+
value="test@example.com", type=TaskRecipientType.EMAIL
191+
)
188192
assert call_args.data["GuardrailName"] == "Test Guardrail"
189193
assert call_args.data["GuardrailDescription"] == "Test description"
190194
assert call_args.data["ExecutionStage"] == expected_stage
@@ -248,7 +252,10 @@ async def test_node_post_agent_interrupts_with_correct_agent_result_data(
248252
assert call_args.app_name == "TestApp"
249253
assert call_args.app_folder_path == "TestFolder"
250254
assert call_args.title == "Agents Guardrail Task"
251-
assert call_args.assignee == "test@example.com"
255+
assert call_args.assignee == ""
256+
assert call_args.recipient == TaskRecipient(
257+
value="test@example.com", type=TaskRecipientType.EMAIL
258+
)
252259
assert call_args.data["GuardrailName"] == "Test Guardrail"
253260
assert call_args.data["GuardrailDescription"] == "Test description"
254261
assert call_args.data["ExecutionStage"] == "PostExecution"
@@ -1524,10 +1531,22 @@ async def test_validate_message_count_empty_messages_raises_exception(self):
15241531
@pytest.mark.parametrize(
15251532
"recipient,expected_value",
15261533
[
1527-
(STANDARD_USER_EMAIL_RECIPIENT, "user@example.com"),
1528-
(STANDARD_GROUP_NAME_RECIPIENT, "AdminGroup"),
1529-
(ASSET_USER_EMAIL_RECIPIENT, "user@example.com"),
1530-
(ASSET_GROUP_NAME_RECIPIENT, "AdminGroup"),
1534+
(
1535+
STANDARD_USER_EMAIL_RECIPIENT,
1536+
TaskRecipient(value="user@example.com", type=TaskRecipientType.EMAIL),
1537+
),
1538+
(
1539+
STANDARD_GROUP_NAME_RECIPIENT,
1540+
TaskRecipient(value="AdminGroup", type=TaskRecipientType.GROUP_NAME),
1541+
),
1542+
(
1543+
ASSET_USER_EMAIL_RECIPIENT,
1544+
TaskRecipient(value="user@example.com", type=TaskRecipientType.EMAIL),
1545+
),
1546+
(
1547+
ASSET_GROUP_NAME_RECIPIENT,
1548+
TaskRecipient(value="AdminGroup", type=TaskRecipientType.GROUP_NAME),
1549+
),
15311550
],
15321551
)
15331552
@patch(
@@ -1575,7 +1594,7 @@ async def test_node_resolves_recipient_correctly(
15751594
# Verify interrupt was called with the resolved assignee
15761595
assert mock_interrupt.called
15771596
call_args = mock_interrupt.call_args[0][0]
1578-
assert call_args.assignee == expected_value
1597+
assert call_args.recipient == expected_value
15791598

15801599
@pytest.mark.asyncio
15811600
@patch(

tests/agent/tools/test_escalation_tool.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
AssetRecipient,
1212
StandardRecipient,
1313
)
14+
from uipath.platform.action_center.tasks import TaskRecipient, TaskRecipientType
1415

1516
from uipath_langchain.agent.tools.escalation_tool import (
1617
create_escalation_tool,
@@ -113,7 +114,9 @@ async def test_resolve_recipient_asset_user_email(self, mock_resolve_asset):
113114

114115
result = await resolve_recipient_value(recipient)
115116

116-
assert result == "resolved@example.com"
117+
assert result == TaskRecipient(
118+
value="resolved@example.com", type=TaskRecipientType.EMAIL
119+
)
117120
mock_resolve_asset.assert_called_once_with("email_asset", "/Test/Folder")
118121

119122
@pytest.mark.asyncio
@@ -130,7 +133,9 @@ async def test_resolve_recipient_asset_group_name(self, mock_resolve_asset):
130133

131134
result = await resolve_recipient_value(recipient)
132135

133-
assert result == "ResolvedGroup"
136+
assert result == TaskRecipient(
137+
value="ResolvedGroup", type=TaskRecipientType.GROUP_NAME
138+
)
134139
mock_resolve_asset.assert_called_once_with("group_asset", "/Test/Folder")
135140

136141
@pytest.mark.asyncio
@@ -143,7 +148,9 @@ async def test_resolve_recipient_user_email(self):
143148

144149
result = await resolve_recipient_value(recipient)
145150

146-
assert result == "direct@example.com"
151+
assert result == TaskRecipient(
152+
value="direct@example.com", type=TaskRecipientType.EMAIL
153+
)
147154

148155
@pytest.mark.asyncio
149156
@patch("uipath_langchain.agent.tools.escalation_tool.resolve_asset")
@@ -262,10 +269,10 @@ async def test_escalation_tool_metadata_has_channel_type(self, escalation_resour
262269

263270
@pytest.mark.asyncio
264271
@patch("uipath_langchain.agent.tools.escalation_tool.interrupt")
265-
async def test_escalation_tool_metadata_has_assignee(
272+
async def test_escalation_tool_metadata_has_recipient(
266273
self, mock_interrupt, escalation_resource
267274
):
268-
"""Test that metadata contains assignee when recipient is USER_EMAIL."""
275+
"""Test that metadata contains recipient when recipient is USER_EMAIL."""
269276
# Mock interrupt to return a result
270277
mock_result = MagicMock()
271278
mock_result.action = None
@@ -278,14 +285,16 @@ async def test_escalation_tool_metadata_has_assignee(
278285
await tool.ainvoke({})
279286

280287
assert tool.metadata is not None
281-
assert tool.metadata["assignee"] == "user@example.com"
288+
assert tool.metadata["recipient"] == TaskRecipient(
289+
value="user@example.com", type=TaskRecipientType.EMAIL
290+
)
282291

283292
@pytest.mark.asyncio
284293
@patch("uipath_langchain.agent.tools.escalation_tool.interrupt")
285-
async def test_escalation_tool_metadata_assignee_none_when_no_recipients(
294+
async def test_escalation_tool_metadata_recipient_none_when_no_recipients(
286295
self, mock_interrupt, escalation_resource_no_recipient
287296
):
288-
"""Test that assignee is None when no recipients configured."""
297+
"""Test that recipient is None when no recipients configured."""
289298
# Mock interrupt to return a result
290299
mock_result = MagicMock()
291300
mock_result.action = None
@@ -298,4 +307,4 @@ async def test_escalation_tool_metadata_assignee_none_when_no_recipients(
298307
await tool.ainvoke({})
299308

300309
assert tool.metadata is not None
301-
assert tool.metadata["assignee"] is None
310+
assert tool.metadata["recipient"] is None

uv.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)