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..27fb0724b 100644 --- a/tests/agent/tools/test_process_tool.py +++ b/tests/agent/tools/test_process_tool.py @@ -181,8 +181,45 @@ 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 is not None + 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 +672,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 +755,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