From 3079480c4af84c562c866fd536b5390e6c31ea11 Mon Sep 17 00:00:00 2001 From: Petru Date: Wed, 19 Aug 2026 14:15:46 +0300 Subject: [PATCH 1/2] feat: forward the configured entry point from a process tool [PC-4935] Reads entry_point_path off the tool's stored properties and passes it to invoke_async, so a process tool runs the entry point the user selected rather than whichever one the release happens to default to. Also surfaced in tool metadata, following the shape #1038 established for folder_key. The read is a getattr rather than plain attribute access, because the attribute is genuinely optional at runtime. BaseResourceProperties sets extra="allow", so against a uipath release predating the declared field the value exists only when the stored JSON carried it, and plain access raises AttributeError for every tool that has no selection -- which today is all of them. Dependency floors deliberately NOT raised in this commit. uipath 2.14.6 and uipath-platform 0.2.20 carry the parameter but are not on PyPI yet, so raising the floors now makes the project unresolvable. They must be raised before this merges: without the raise, an older uipath absorbs entry_point_path into invoke_async's **kwargs and drops it before the payload, so the selection is silently ignored and the job still reports success. Co-Authored-By: Claude Opus 5 (1M context) --- .../agent/tools/process_tool.py | 7 ++++ tests/agent/tools/test_process_tool.py | 38 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/uipath_langchain/agent/tools/process_tool.py b/src/uipath_langchain/agent/tools/process_tool.py index 49e5866b0..1488540d7 100644 --- a/src/uipath_langchain/agent/tools/process_tool.py +++ b/src/uipath_langchain/agent/tools/process_tool.py @@ -60,6 +60,11 @@ def create_process_tool( # invoke_async accepts only one of folder_path/folder_key, so resolve one. folder_path = get_execution_folder_path() or resource.properties.folder_path folder_key = get_execution_folder_key() if not folder_path else None + # getattr because the attribute is genuinely optional at runtime: BaseResourceProperties sets + # extra="allow", so against a uipath release predating the declared field the value is present + # only when the stored JSON carried it. None is also the meaningful value -- it is how + # Orchestrator is told to use the release's configured entry point. + entry_point_path = getattr(resource.properties, "entry_point_path", None) input_model: Any = create_model(resource.input_schema) output_model: Any = create_output_model(resource.output_schema, resource.name) @@ -95,6 +100,7 @@ async def start_job(): parent_span_id=parent_span_id, parent_operation_id=parent_operation_id, run_as_me=True if run_as_me else None, + entry_point_path=entry_point_path, ) except EnrichedException as e: raise_for_enriched( @@ -145,6 +151,7 @@ async def start_job(): "display_name": process_name, "folder_path": folder_path, "folder_key": folder_key, + "entry_point_path": entry_point_path, "args_schema": input_model, "output_schema": output_model, "_span_context": _span_context, diff --git a/tests/agent/tools/test_process_tool.py b/tests/agent/tools/test_process_tool.py index 23dc69d1a..a767f4449 100644 --- a/tests/agent/tools/test_process_tool.py +++ b/tests/agent/tools/test_process_tool.py @@ -181,8 +181,44 @@ async def test_invoke_calls_processes_invoke_async( parent_span_id=None, parent_operation_id=None, run_as_me=None, + entry_point_path=None, ) + @pytest.mark.asyncio + @patch("uipath_langchain._utils.durable_interrupt.decorator.interrupt") + @patch("uipath_langchain.agent.tools.process_tool.UiPath") + async def test_invoke_forwards_the_configured_entry_point( + self, mock_uipath_class, mock_interrupt, process_resource + ): + """A stored entry point reaches StartJobs; without it Orchestrator runs the release default.""" + process_resource.properties.entry_point_path = "Workflows/Main.xaml" + + mock_job = MagicMock(spec=Job) + mock_job.key = "job-key-123" + mock_job.folder_key = "folder-key-123" + + mock_client = MagicMock() + mock_client.processes.invoke_async = AsyncMock(return_value=mock_job) + mock_client.jobs.extract_output_async = AsyncMock(return_value=None) + mock_uipath_class.return_value = mock_client + + resumed = MagicMock(spec=Job) + resumed.state = "successful" + mock_interrupt.return_value = resumed + + tool = create_process_tool(process_resource) + await tool.ainvoke({}) + + _, kwargs = mock_client.processes.invoke_async.call_args + assert kwargs["entry_point_path"] == "Workflows/Main.xaml" + + def test_metadata_carries_the_entry_point(self, process_resource): + process_resource.properties.entry_point_path = "Workflows/Main.xaml" + + tool = create_process_tool(process_resource) + + assert tool.metadata["entry_point_path"] == "Workflows/Main.xaml" + @pytest.mark.asyncio @patch("uipath_langchain._utils.durable_interrupt.decorator.interrupt") @patch("uipath_langchain.agent.tools.process_tool.UiPath") @@ -635,6 +671,7 @@ async def test_flow_tool_invokes_processes_invoke_async( parent_span_id=None, parent_operation_id=None, run_as_me=None, + entry_point_path=None, ) @pytest.mark.asyncio @@ -717,6 +754,7 @@ async def test_function_tool_invokes_processes_invoke_async( parent_span_id=None, parent_operation_id=None, run_as_me=None, + entry_point_path=None, ) @pytest.mark.asyncio From 8ef8435eea91be7200c1dd40e11a42322cc959c7 Mon Sep 17 00:00:00 2001 From: Petru Date: Wed, 19 Aug 2026 14:20:07 +0300 Subject: [PATCH 2/2] test: guard the optional metadata dict in the entry-point test [PC-4935] CI runs mypy as part of lint; I had only run ruff locally. tool.metadata is typed dict[str, Any] | None, so indexing it needs the same `is not None` assertion every other metadata test in this file already makes. Co-Authored-By: Claude Opus 5 (1M context) --- tests/agent/tools/test_process_tool.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/agent/tools/test_process_tool.py b/tests/agent/tools/test_process_tool.py index a767f4449..27fb0724b 100644 --- a/tests/agent/tools/test_process_tool.py +++ b/tests/agent/tools/test_process_tool.py @@ -217,6 +217,7 @@ def test_metadata_carries_the_entry_point(self, process_resource): tool = create_process_tool(process_resource) + assert tool.metadata is not None assert tool.metadata["entry_point_path"] == "Workflows/Main.xaml" @pytest.mark.asyncio