Skip to content

Commit c3ebb21

Browse files
feat: replace call to assets service with async version
1 parent 30441b7 commit c3ebb21

3 files changed

Lines changed: 66 additions & 43 deletions

File tree

src/uipath_langchain/agent/tools/escalation_tool.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,24 @@ class EscalationAction(str, Enum):
3232
END = "end"
3333

3434

35-
def resolve_recipient_value(recipient: AgentEscalationRecipient) -> str | None:
35+
async def resolve_recipient_value(recipient: AgentEscalationRecipient) -> str | None:
3636
"""Resolve recipient value based on recipient type."""
3737
if isinstance(recipient, AssetRecipient):
38-
return resolve_asset(recipient.asset_name, recipient.folder_path)
38+
return await resolve_asset(recipient.asset_name, recipient.folder_path)
3939

4040
if isinstance(recipient, StandardRecipient):
4141
return recipient.value
4242

4343
return None
4444

4545

46-
def resolve_asset(asset_name: str, folder_path: str) -> str | None:
46+
async def resolve_asset(asset_name: str, folder_path: str) -> str | None:
4747
"""Retrieve asset value."""
4848
try:
4949
client = UiPath()
50-
result = client.assets.retrieve(name=asset_name, folder_path=folder_path)
50+
result = await client.assets.retrieve_async(
51+
name=asset_name, folder_path=folder_path
52+
)
5153

5254
if not result or not result.value:
5355
raise ValueError(f"Asset '{asset_name}' has no value configured.")
@@ -63,7 +65,9 @@ class StructuredToolWithWrapper(StructuredTool, ToolWrapperMixin):
6365
pass
6466

6567

66-
def create_escalation_tool(resource: AgentEscalationResourceConfig) -> StructuredTool:
68+
async def create_escalation_tool(
69+
resource: AgentEscalationResourceConfig,
70+
) -> StructuredTool:
6771
"""Uses interrupt() for Action Center human-in-the-loop."""
6872

6973
tool_name: str = f"escalate_{sanitize_tool_name(resource.name)}"
@@ -73,7 +77,9 @@ def create_escalation_tool(resource: AgentEscalationResourceConfig) -> Structure
7377
output_model: Any = create_model(channel.output_schema)
7478

7579
assignee: str | None = (
76-
resolve_recipient_value(channel.recipients[0]) if channel.recipients else None
80+
await resolve_recipient_value(channel.recipients[0])
81+
if channel.recipients
82+
else None
7783
)
7884

7985
@mockable(

src/uipath_langchain/agent/tools/tool_factory.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ async def _build_tool_for_resource(
4242
return create_context_tool(resource)
4343

4444
elif isinstance(resource, AgentEscalationResourceConfig):
45-
return create_escalation_tool(resource)
45+
return await create_escalation_tool(resource)
4646

4747
elif isinstance(resource, AgentIntegrationToolResourceConfig):
4848
return create_integration_tool(resource)

tests/agent/tools/test_escalation_tool.py

Lines changed: 53 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Tests for escalation_tool.py metadata."""
22

3-
from unittest.mock import MagicMock, patch
3+
from unittest.mock import AsyncMock, MagicMock, patch
44

55
import pytest
66
from uipath.agent.models.agent import (
@@ -22,66 +22,72 @@
2222
class TestResolveAsset:
2323
"""Test the resolve_asset function."""
2424

25+
@pytest.mark.asyncio
2526
@patch("uipath_langchain.agent.tools.escalation_tool.UiPath")
26-
def test_resolve_asset_success(self, mock_uipath_class):
27+
async def test_resolve_asset_success(self, mock_uipath_class):
2728
"""Test successful asset retrieval."""
2829
# Setup mock
2930
mock_client = MagicMock()
3031
mock_uipath_class.return_value = mock_client
3132
mock_result = MagicMock()
3233
mock_result.value = "test@example.com"
33-
mock_client.assets.retrieve.return_value = mock_result
34+
mock_client.assets.retrieve_async = AsyncMock(return_value=mock_result)
3435

3536
# Execute
36-
result = resolve_asset("email_asset", "/Test/Folder")
37+
result = await resolve_asset("email_asset", "/Test/Folder")
3738

3839
# Assert
3940
assert result == "test@example.com"
40-
mock_client.assets.retrieve.assert_called_once_with(
41+
mock_client.assets.retrieve_async.assert_called_once_with(
4142
name="email_asset", folder_path="/Test/Folder"
4243
)
4344

45+
@pytest.mark.asyncio
4446
@patch("uipath_langchain.agent.tools.escalation_tool.UiPath")
45-
def test_resolve_asset_no_value(self, mock_uipath_class):
47+
async def test_resolve_asset_no_value(self, mock_uipath_class):
4648
"""Test asset with no value raises ValueError."""
4749
# Setup mock
4850
mock_client = MagicMock()
4951
mock_uipath_class.return_value = mock_client
5052
mock_result = MagicMock()
5153
mock_result.value = None
52-
mock_client.assets.retrieve.return_value = mock_result
54+
mock_client.assets.retrieve_async = AsyncMock(return_value=mock_result)
5355

5456
# Execute and assert
5557
with pytest.raises(ValueError) as exc_info:
56-
resolve_asset("empty_asset", "/Test/Folder")
58+
await resolve_asset("empty_asset", "/Test/Folder")
5759

5860
assert "Asset 'empty_asset' has no value configured" in str(exc_info.value)
5961

62+
@pytest.mark.asyncio
6063
@patch("uipath_langchain.agent.tools.escalation_tool.UiPath")
61-
def test_resolve_asset_not_found(self, mock_uipath_class):
64+
async def test_resolve_asset_not_found(self, mock_uipath_class):
6265
"""Test asset not found raises ValueError."""
6366
# Setup mock
6467
mock_client = MagicMock()
6568
mock_uipath_class.return_value = mock_client
66-
mock_client.assets.retrieve.return_value = None
69+
mock_client.assets.retrieve_async = AsyncMock(return_value=None)
6770

6871
# Execute and assert
6972
with pytest.raises(ValueError) as exc_info:
70-
resolve_asset("missing_asset", "/Test/Folder")
73+
await resolve_asset("missing_asset", "/Test/Folder")
7174

7275
assert "Asset 'missing_asset' has no value configured" in str(exc_info.value)
7376

77+
@pytest.mark.asyncio
7478
@patch("uipath_langchain.agent.tools.escalation_tool.UiPath")
75-
def test_resolve_asset_retrieval_exception(self, mock_uipath_class):
79+
async def test_resolve_asset_retrieval_exception(self, mock_uipath_class):
7680
"""Test exception during asset retrieval raises ValueError with context."""
7781
# Setup mock
7882
mock_client = MagicMock()
7983
mock_uipath_class.return_value = mock_client
80-
mock_client.assets.retrieve.side_effect = Exception("Connection error")
84+
mock_client.assets.retrieve_async = AsyncMock(
85+
side_effect=Exception("Connection error")
86+
)
8187

8288
# Execute and assert
8389
with pytest.raises(ValueError) as exc_info:
84-
resolve_asset("problem_asset", "/Test/Folder")
90+
await resolve_asset("problem_asset", "/Test/Folder")
8591

8692
assert (
8793
"Failed to resolve asset 'problem_asset' in folder '/Test/Folder'"
@@ -93,8 +99,9 @@ def test_resolve_asset_retrieval_exception(self, mock_uipath_class):
9399
class TestResolveRecipientValue:
94100
"""Test the resolve_recipient_value function."""
95101

102+
@pytest.mark.asyncio
96103
@patch("uipath_langchain.agent.tools.escalation_tool.resolve_asset")
97-
def test_resolve_recipient_asset_user_email(self, mock_resolve_asset):
104+
async def test_resolve_recipient_asset_user_email(self, mock_resolve_asset):
98105
"""Test ASSET_USER_EMAIL type calls resolve_asset."""
99106
mock_resolve_asset.return_value = "resolved@example.com"
100107

@@ -104,13 +111,14 @@ def test_resolve_recipient_asset_user_email(self, mock_resolve_asset):
104111
folder_path="/Test/Folder",
105112
)
106113

107-
result = resolve_recipient_value(recipient)
114+
result = await resolve_recipient_value(recipient)
108115

109116
assert result == "resolved@example.com"
110117
mock_resolve_asset.assert_called_once_with("email_asset", "/Test/Folder")
111118

119+
@pytest.mark.asyncio
112120
@patch("uipath_langchain.agent.tools.escalation_tool.resolve_asset")
113-
def test_resolve_recipient_asset_group_name(self, mock_resolve_asset):
121+
async def test_resolve_recipient_asset_group_name(self, mock_resolve_asset):
114122
"""Test ASSET_GROUP_NAME type calls resolve_asset."""
115123
mock_resolve_asset.return_value = "ResolvedGroup"
116124

@@ -120,24 +128,26 @@ def test_resolve_recipient_asset_group_name(self, mock_resolve_asset):
120128
folder_path="/Test/Folder",
121129
)
122130

123-
result = resolve_recipient_value(recipient)
131+
result = await resolve_recipient_value(recipient)
124132

125133
assert result == "ResolvedGroup"
126134
mock_resolve_asset.assert_called_once_with("group_asset", "/Test/Folder")
127135

128-
def test_resolve_recipient_user_email(self):
136+
@pytest.mark.asyncio
137+
async def test_resolve_recipient_user_email(self):
129138
"""Test USER_EMAIL type returns value directly."""
130139
recipient = StandardRecipient(
131140
type=AgentEscalationRecipientType.USER_EMAIL,
132141
value="direct@example.com",
133142
)
134143

135-
result = resolve_recipient_value(recipient)
144+
result = await resolve_recipient_value(recipient)
136145

137146
assert result == "direct@example.com"
138147

148+
@pytest.mark.asyncio
139149
@patch("uipath_langchain.agent.tools.escalation_tool.resolve_asset")
140-
def test_resolve_recipient_propagates_error_when_asset_resolution_fails(
150+
async def test_resolve_recipient_propagates_error_when_asset_resolution_fails(
141151
self, mock_resolve_asset
142152
):
143153
"""Test AssetRecipient when asset resolution fails."""
@@ -150,18 +160,19 @@ def test_resolve_recipient_propagates_error_when_asset_resolution_fails(
150160
)
151161

152162
with pytest.raises(ValueError) as exc_info:
153-
resolve_recipient_value(recipient)
163+
await resolve_recipient_value(recipient)
154164

155165
assert "Asset not found" in str(exc_info.value)
156166

157-
def test_resolve_recipient_no_value(self):
167+
@pytest.mark.asyncio
168+
async def test_resolve_recipient_no_value(self):
158169
"""Test recipient without value attribute returns None."""
159170
# Create a minimal recipient object without value
160171
recipient = MagicMock()
161172
recipient.type = AgentEscalationRecipientType.USER_EMAIL
162173
del recipient.value # Simulate no value attribute
163174

164-
result = resolve_recipient_value(recipient)
175+
result = await resolve_recipient_value(recipient)
165176

166177
assert result is None
167178

@@ -220,41 +231,47 @@ def escalation_resource_no_recipient(self):
220231
],
221232
)
222233

223-
def test_escalation_tool_has_metadata(self, escalation_resource):
234+
@pytest.mark.asyncio
235+
async def test_escalation_tool_has_metadata(self, escalation_resource):
224236
"""Test that escalation tool has metadata dict."""
225-
tool = create_escalation_tool(escalation_resource)
237+
tool = await create_escalation_tool(escalation_resource)
226238

227239
assert tool.metadata is not None
228240
assert isinstance(tool.metadata, dict)
229241

230-
def test_escalation_tool_metadata_has_tool_type(self, escalation_resource):
242+
@pytest.mark.asyncio
243+
async def test_escalation_tool_metadata_has_tool_type(self, escalation_resource):
231244
"""Test that metadata contains tool_type for span detection."""
232-
tool = create_escalation_tool(escalation_resource)
245+
tool = await create_escalation_tool(escalation_resource)
233246
assert tool.metadata is not None
234247
assert tool.metadata["tool_type"] == "escalation"
235248

236-
def test_escalation_tool_metadata_has_display_name(self, escalation_resource):
249+
@pytest.mark.asyncio
250+
async def test_escalation_tool_metadata_has_display_name(self, escalation_resource):
237251
"""Test that metadata contains display_name from app_name."""
238-
tool = create_escalation_tool(escalation_resource)
252+
tool = await create_escalation_tool(escalation_resource)
239253
assert tool.metadata is not None
240254
assert tool.metadata["display_name"] == "ApprovalApp"
241255

242-
def test_escalation_tool_metadata_has_channel_type(self, escalation_resource):
256+
@pytest.mark.asyncio
257+
async def test_escalation_tool_metadata_has_channel_type(self, escalation_resource):
243258
"""Test that metadata contains channel_type for span attributes."""
244-
tool = create_escalation_tool(escalation_resource)
259+
tool = await create_escalation_tool(escalation_resource)
245260
assert tool.metadata is not None
246261
assert tool.metadata["channel_type"] == "actionCenter"
247262

248-
def test_escalation_tool_metadata_has_assignee(self, escalation_resource):
263+
@pytest.mark.asyncio
264+
async def test_escalation_tool_metadata_has_assignee(self, escalation_resource):
249265
"""Test that metadata contains assignee when recipient is USER_EMAIL."""
250-
tool = create_escalation_tool(escalation_resource)
266+
tool = await create_escalation_tool(escalation_resource)
251267
assert tool.metadata is not None
252268
assert tool.metadata["assignee"] == "user@example.com"
253269

254-
def test_escalation_tool_metadata_assignee_none_when_no_recipients(
270+
@pytest.mark.asyncio
271+
async def test_escalation_tool_metadata_assignee_none_when_no_recipients(
255272
self, escalation_resource_no_recipient
256273
):
257274
"""Test that assignee is None when no recipients configured."""
258-
tool = create_escalation_tool(escalation_resource_no_recipient)
275+
tool = await create_escalation_tool(escalation_resource_no_recipient)
259276
assert tool.metadata is not None
260277
assert tool.metadata["assignee"] is None

0 commit comments

Comments
 (0)