Skip to content

Commit a5319af

Browse files
feat: use WaitJobRaw for process_tool (#635)
1 parent ebf78b2 commit a5319af

File tree

4 files changed

+88
-15
lines changed

4 files changed

+88
-15
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-langchain"
3-
version = "0.7.14"
3+
version = "0.7.15"
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"

src/uipath_langchain/agent/tools/process_tool.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Process tool creation for UiPath process execution."""
22

3+
import json
34
from typing import Any
45

56
from langchain.tools import BaseTool
@@ -8,7 +9,8 @@
89
from uipath.agent.models.agent import AgentProcessToolResourceConfig, AgentToolType
910
from uipath.eval.mocks import mockable
1011
from uipath.platform import UiPath
11-
from uipath.platform.common import WaitJob
12+
from uipath.platform.common import WaitJobRaw
13+
from uipath.platform.orchestrator import JobState
1214

1315
from uipath_langchain.agent.react.job_attachments import get_job_attachments
1416
from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model
@@ -75,9 +77,22 @@ async def start_job():
7577
)
7678
_bts_context[bts_key] = str(job.key)
7779

78-
return WaitJob(job=job, process_folder_key=job.folder_key)
80+
return WaitJobRaw(job=job, process_folder_key=job.folder_key)
7981

80-
return await start_job()
82+
job = await start_job()
83+
84+
if (job.state or "").lower() == JobState.FAULTED:
85+
error_info = str(job.info or "Unknown error")
86+
return f"{error_info}"
87+
88+
client = UiPath()
89+
output_str = await client.jobs.extract_output_async(job)
90+
if output_str:
91+
try:
92+
return json.loads(output_str)
93+
except (json.JSONDecodeError, TypeError):
94+
return output_str
95+
return output_str
8196

8297
return await invoke_process(**kwargs)
8398

tests/agent/tools/test_process_tool.py

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,15 @@ async def test_invoke_calls_processes_invoke_async(
124124
mock_job.key = "job-key-123"
125125
mock_job.folder_key = "folder-key-123"
126126

127+
mock_resumed_job = MagicMock(spec=Job)
128+
mock_resumed_job.state = "successful"
129+
127130
mock_client = MagicMock()
128131
mock_client.processes.invoke_async = AsyncMock(return_value=mock_job)
132+
mock_client.jobs.extract_output_async = AsyncMock(return_value=None)
129133
mock_uipath_class.return_value = mock_client
130134

131-
mock_interrupt.return_value = {"output": "result"}
135+
mock_interrupt.return_value = mock_resumed_job
132136

133137
tool = create_process_tool(process_resource)
134138
await tool.ainvoke({})
@@ -148,16 +152,20 @@ async def test_invoke_calls_processes_invoke_async(
148152
async def test_invoke_interrupts_with_wait_job(
149153
self, mock_uipath_class, mock_interrupt, process_resource
150154
):
151-
"""Test that after invoking, the tool interrupts with WaitJob."""
155+
"""Test that after invoking, the tool interrupts with WaitJobRaw."""
152156
mock_job = MagicMock(spec=Job)
153157
mock_job.key = "job-key-456"
154158
mock_job.folder_key = "folder-key-456"
155159

160+
mock_resumed_job = MagicMock(spec=Job)
161+
mock_resumed_job.state = "successful"
162+
156163
mock_client = MagicMock()
157164
mock_client.processes.invoke_async = AsyncMock(return_value=mock_job)
165+
mock_client.jobs.extract_output_async = AsyncMock(return_value=None)
158166
mock_uipath_class.return_value = mock_client
159167

160-
mock_interrupt.return_value = {"output": "done"}
168+
mock_interrupt.return_value = mock_resumed_job
161169

162170
tool = create_process_tool(process_resource)
163171
await tool.ainvoke({})
@@ -179,11 +187,15 @@ async def test_invoke_passes_input_arguments(
179187
mock_job.key = "job-key"
180188
mock_job.folder_key = "folder-key"
181189

190+
mock_resumed_job = MagicMock(spec=Job)
191+
mock_resumed_job.state = "successful"
192+
182193
mock_client = MagicMock()
183194
mock_client.processes.invoke_async = AsyncMock(return_value=mock_job)
195+
mock_client.jobs.extract_output_async = AsyncMock(return_value=None)
184196
mock_uipath_class.return_value = mock_client
185197

186-
mock_interrupt.return_value = {"result": "processed"}
198+
mock_interrupt.return_value = mock_resumed_job
187199

188200
tool = create_process_tool(process_resource_with_inputs)
189201
await tool.ainvoke({"name": "test-data", "count": 42})
@@ -196,25 +208,59 @@ async def test_invoke_passes_input_arguments(
196208
@pytest.mark.asyncio
197209
@patch("uipath_langchain.agent.tools.durable_interrupt.decorator.interrupt")
198210
@patch("uipath_langchain.agent.tools.process_tool.UiPath")
199-
async def test_invoke_returns_interrupt_value(
211+
async def test_invoke_returns_output_from_extract(
200212
self, mock_uipath_class, mock_interrupt, process_resource
201213
):
202-
"""Test that the tool returns the value from interrupt()."""
214+
"""Test that the tool returns the extracted job output on success."""
203215
mock_job = MagicMock(spec=Job)
204216
mock_job.key = "job-key"
205217
mock_job.folder_key = "folder-key"
206218

219+
mock_resumed_job = MagicMock(spec=Job)
220+
mock_resumed_job.state = "successful"
221+
207222
mock_client = MagicMock()
208223
mock_client.processes.invoke_async = AsyncMock(return_value=mock_job)
224+
mock_client.jobs.extract_output_async = AsyncMock(
225+
return_value='{"output_arg": "value123"}'
226+
)
209227
mock_uipath_class.return_value = mock_client
210228

211-
mock_interrupt.return_value = {"output_arg": "value123"}
229+
mock_interrupt.return_value = mock_resumed_job
212230

213231
tool = create_process_tool(process_resource)
214232
result = await tool.ainvoke({})
215233

216234
assert result == {"output_arg": "value123"}
217235

236+
@pytest.mark.asyncio
237+
@patch("uipath_langchain.agent.tools.durable_interrupt.decorator.interrupt")
238+
@patch("uipath_langchain.agent.tools.process_tool.UiPath")
239+
async def test_invoke_returns_error_message_on_faulted_job(
240+
self, mock_uipath_class, mock_interrupt, process_resource
241+
):
242+
"""Test that the tool returns an error message string when the job is faulted."""
243+
mock_job = MagicMock(spec=Job)
244+
mock_job.key = "job-key"
245+
mock_job.folder_key = "folder-key"
246+
247+
mock_resumed_job = MagicMock(spec=Job)
248+
mock_resumed_job.state = "faulted"
249+
mock_resumed_job.job_error = None
250+
mock_resumed_job.info = "Something went wrong in the workflow"
251+
252+
mock_client = MagicMock()
253+
mock_client.processes.invoke_async = AsyncMock(return_value=mock_job)
254+
mock_uipath_class.return_value = mock_client
255+
256+
mock_interrupt.return_value = mock_resumed_job
257+
258+
tool = create_process_tool(process_resource)
259+
result = await tool.ainvoke({})
260+
261+
assert isinstance(result, str)
262+
assert "Something went wrong in the workflow" in result
263+
218264

219265
class TestProcessToolSpanContext:
220266
"""Test that _span_context is properly wired for tracing."""
@@ -230,11 +276,15 @@ async def test_span_context_parent_span_id_passed_to_invoke(
230276
mock_job.key = "job-key"
231277
mock_job.folder_key = "folder-key"
232278

279+
mock_resumed_job = MagicMock(spec=Job)
280+
mock_resumed_job.state = "successful"
281+
233282
mock_client = MagicMock()
234283
mock_client.processes.invoke_async = AsyncMock(return_value=mock_job)
284+
mock_client.jobs.extract_output_async = AsyncMock(return_value=None)
235285
mock_uipath_class.return_value = mock_client
236286

237-
mock_interrupt.return_value = {}
287+
mock_interrupt.return_value = mock_resumed_job
238288

239289
tool = create_process_tool(process_resource)
240290
assert tool.metadata is not None
@@ -258,11 +308,15 @@ async def test_span_context_consumed_after_invoke(
258308
mock_job.key = "job-key"
259309
mock_job.folder_key = "folder-key"
260310

311+
mock_resumed_job = MagicMock(spec=Job)
312+
mock_resumed_job.state = "successful"
313+
261314
mock_client = MagicMock()
262315
mock_client.processes.invoke_async = AsyncMock(return_value=mock_job)
316+
mock_client.jobs.extract_output_async = AsyncMock(return_value=None)
263317
mock_uipath_class.return_value = mock_client
264318

265-
mock_interrupt.return_value = {}
319+
mock_interrupt.return_value = mock_resumed_job
266320

267321
tool = create_process_tool(process_resource)
268322
assert tool.metadata is not None
@@ -284,11 +338,15 @@ async def test_span_context_defaults_to_none_when_empty(
284338
mock_job.key = "job-key"
285339
mock_job.folder_key = "folder-key"
286340

341+
mock_resumed_job = MagicMock(spec=Job)
342+
mock_resumed_job.state = "successful"
343+
287344
mock_client = MagicMock()
288345
mock_client.processes.invoke_async = AsyncMock(return_value=mock_job)
346+
mock_client.jobs.extract_output_async = AsyncMock(return_value=None)
289347
mock_uipath_class.return_value = mock_client
290348

291-
mock_interrupt.return_value = {}
349+
mock_interrupt.return_value = mock_resumed_job
292350

293351
tool = create_process_tool(process_resource)
294352
# Don't set any parent_span_id

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)