Skip to content

Commit d0bb685

Browse files
feat(agent/tools): cover Flow process tool type (#869)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8c3011a commit d0bb685

4 files changed

Lines changed: 138 additions & 8 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.11.3"
3+
version = "0.11.4"
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.10.68, <2.11.0",
8+
"uipath>=2.10.69, <2.11.0",
99
"uipath-core>=0.5.15, <0.6.0",
1010
"uipath-platform>=0.1.45, <0.2.0",
1111
"uipath-runtime>=0.10.0, <0.11.0",

tests/agent/tools/test_process_tool.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,22 @@ def process_resource():
3131
)
3232

3333

34+
@pytest.fixture
35+
def flow_resource():
36+
"""Create a process tool resource config of type Flow (Maestro Flow release)."""
37+
return AgentProcessToolResourceConfig(
38+
type=AgentToolType.FLOW,
39+
name="test_flow",
40+
description="Test flow description",
41+
input_schema={"type": "object", "properties": {}},
42+
output_schema={"type": "object", "properties": {}},
43+
properties=AgentProcessToolProperties(
44+
process_name="MyFlow",
45+
folder_path="/Shared/Flows",
46+
),
47+
)
48+
49+
3450
@pytest.fixture
3551
def process_resource_with_inputs():
3652
"""Create a process tool resource config with input arguments."""
@@ -453,3 +469,84 @@ async def test_run_as_me_default_sends_none(
453469

454470
call_kwargs = mock_client.processes.invoke_async.call_args[1]
455471
assert call_kwargs["run_as_me"] is None
472+
473+
474+
class TestProcessToolFlowType:
475+
"""Test that a Flow-type resource flows through create_process_tool correctly."""
476+
477+
def test_flow_tool_metadata_has_flow_tool_type(self, flow_resource):
478+
"""A Flow resource produces a tool with metadata.tool_type == 'flow'."""
479+
tool = create_process_tool(flow_resource)
480+
assert tool.metadata is not None
481+
assert tool.metadata["tool_type"] == "flow"
482+
483+
def test_flow_tool_metadata_has_display_name(self, flow_resource):
484+
"""Flow tool metadata exposes the process_name as display_name."""
485+
tool = create_process_tool(flow_resource)
486+
assert tool.metadata is not None
487+
assert tool.metadata["display_name"] == "MyFlow"
488+
489+
@pytest.mark.asyncio
490+
@patch.dict(os.environ, {"UIPATH_FOLDER_PATH": "/Shared/Flows"})
491+
@patch("uipath_langchain._utils.durable_interrupt.decorator.interrupt")
492+
@patch("uipath_langchain.agent.tools.process_tool.UiPath")
493+
async def test_flow_tool_invokes_processes_invoke_async(
494+
self, mock_uipath_class, mock_interrupt, flow_resource
495+
):
496+
"""Flow tool invokes client.processes.invoke_async (same path as Process)."""
497+
mock_job = MagicMock(spec=Job)
498+
mock_job.key = "flow-job-key"
499+
mock_job.folder_key = "flow-folder-key"
500+
501+
mock_resumed_job = MagicMock(spec=Job)
502+
mock_resumed_job.state = "successful"
503+
504+
mock_client = MagicMock()
505+
mock_client.processes.invoke_async = AsyncMock(return_value=mock_job)
506+
mock_client.jobs.extract_output_async = AsyncMock(return_value=None)
507+
mock_uipath_class.return_value = mock_client
508+
509+
mock_interrupt.return_value = mock_resumed_job
510+
511+
tool = create_process_tool(flow_resource)
512+
await tool.ainvoke({})
513+
514+
mock_client.processes.invoke_async.assert_called_once_with(
515+
name="MyFlow",
516+
input_arguments={},
517+
folder_path="/Shared/Flows",
518+
attachments=[],
519+
parent_span_id=None,
520+
parent_operation_id=None,
521+
run_as_me=None,
522+
)
523+
524+
@pytest.mark.asyncio
525+
@patch("uipath_langchain._utils.durable_interrupt.decorator.interrupt")
526+
@patch("uipath_langchain.agent.tools.process_tool.UiPath")
527+
async def test_flow_tool_uses_non_agent_bts_key(
528+
self, mock_uipath_class, mock_interrupt, flow_resource
529+
):
530+
"""Flow tool stores the BTS tracking key under 'wait_for_job_key' (not agent variant)."""
531+
mock_job = MagicMock(spec=Job)
532+
mock_job.key = "flow-job-key"
533+
mock_job.folder_key = "flow-folder-key"
534+
535+
mock_resumed_job = MagicMock(spec=Job)
536+
mock_resumed_job.state = "successful"
537+
538+
mock_client = MagicMock()
539+
mock_client.processes.invoke_async = AsyncMock(return_value=mock_job)
540+
mock_client.jobs.extract_output_async = AsyncMock(return_value=None)
541+
mock_uipath_class.return_value = mock_client
542+
543+
mock_interrupt.return_value = mock_resumed_job
544+
545+
tool = create_process_tool(flow_resource)
546+
assert tool.metadata is not None
547+
548+
await tool.ainvoke({})
549+
550+
bts_context = tool.metadata["_bts_context"]
551+
assert bts_context.get("wait_for_job_key") == "flow-job-key"
552+
assert "wait_for_agent_job_key" not in bts_context

tests/agent/tools/test_tool_factory.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,22 @@ def process_resource() -> AgentProcessToolResourceConfig:
6868
)
6969

7070

71+
@pytest.fixture
72+
def flow_resource() -> AgentProcessToolResourceConfig:
73+
"""Create a process tool resource config of type Flow."""
74+
return AgentProcessToolResourceConfig(
75+
type=AgentToolType.FLOW,
76+
name="test_flow",
77+
description="Test flow description",
78+
input_schema=EMPTY_SCHEMA,
79+
output_schema=EMPTY_SCHEMA,
80+
properties=AgentProcessToolProperties(
81+
process_name="MyFlow",
82+
folder_path="/Shared/Flows",
83+
),
84+
)
85+
86+
7187
@pytest.fixture
7288
def context_resource() -> AgentContextResourceConfig:
7389
"""Create a context tool resource config."""
@@ -270,6 +286,7 @@ async def test_only_enabled_tools_returned(
270286
"resource_fixture",
271287
[
272288
"process_resource",
289+
"flow_resource",
273290
"context_resource",
274291
"escalation_resource",
275292
"integration_resource",
@@ -288,3 +305,19 @@ async def test_resource_produces_base_uipath_tool(
288305
mock_llm = AsyncMock(spec=BaseChatModel)
289306
tool = await _build_tool_for_resource(resource, mock_llm)
290307
assert_tool_is_base_uipath(tool)
308+
309+
async def test_flow_resource_routes_through_process_tool_path(
310+
self, flow_resource, mock_uipath_sdk
311+
):
312+
"""A Flow-type resource is dispatched via the process_tool factory path."""
313+
mock_llm = AsyncMock(spec=BaseChatModel)
314+
with patch(
315+
"uipath_langchain.agent.tools.tool_factory.create_process_tool"
316+
) as mock_create_process_tool:
317+
mock_create_process_tool.return_value = MagicMock(
318+
spec=BaseUiPathStructuredTool
319+
)
320+
tool = await _build_tool_for_resource(flow_resource, mock_llm)
321+
322+
mock_create_process_tool.assert_called_once_with(flow_resource, run_as_me=False)
323+
assert tool is not None

uv.lock

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

0 commit comments

Comments
 (0)