From 8ded75770074dd5977c928b156c8d0d2d2c421f7 Mon Sep 17 00:00:00 2001 From: Shannon Suhendra Date: Wed, 27 May 2026 17:21:27 -0400 Subject: [PATCH 1/3] add get case state built in tool --- .../internal_tools/get_case_state_tool.py | 100 ++++++++++++ .../internal_tools/internal_tool_factory.py | 2 + .../test_get_case_state_tool.py | 151 ++++++++++++++++++ 3 files changed, 253 insertions(+) create mode 100644 src/uipath_langchain/agent/tools/internal_tools/get_case_state_tool.py create mode 100644 tests/agent/tools/internal_tools/test_get_case_state_tool.py diff --git a/src/uipath_langchain/agent/tools/internal_tools/get_case_state_tool.py b/src/uipath_langchain/agent/tools/internal_tools/get_case_state_tool.py new file mode 100644 index 000000000..512710987 --- /dev/null +++ b/src/uipath_langchain/agent/tools/internal_tools/get_case_state_tool.py @@ -0,0 +1,100 @@ +"""Internal tool that fetches the current state of a PIMs case instance.""" + +from typing import Any + +from langchain_core.language_models import BaseChatModel +from langchain_core.tools import StructuredTool +from uipath.agent.models.agent import AgentInternalToolResourceConfig +from uipath.eval.mocks import mockable +from uipath.platform import UiPath +from uipath.platform.errors import EnrichedException +from uipath.runtime.errors import UiPathErrorCategory + +from uipath_langchain.agent.exceptions import raise_for_enriched +from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model +from uipath_langchain.agent.tools.structured_tool_with_argument_properties import ( + StructuredToolWithArgumentProperties, +) +from uipath_langchain.agent.tools.utils import sanitize_tool_name + +PIMS_INSTANCE_PATH = "pims_/api/v1/instances/{instance_id}" + +_GET_CASE_STATE_ERRORS: dict[ + tuple[int, str | None], tuple[str, UiPathErrorCategory] +] = { + (404, None): ( + "Case instance not found for tool '{tool}': {message}", + UiPathErrorCategory.USER, + ), + (401, None): ( + "Unauthorized when fetching case state for tool '{tool}': {message}", + UiPathErrorCategory.SYSTEM, + ), + (403, None): ( + "Forbidden when fetching case state for tool '{tool}': {message}", + UiPathErrorCategory.USER, + ), +} + + +def create_get_case_state_tool( + resource: AgentInternalToolResourceConfig, llm: BaseChatModel +) -> StructuredTool: + """Create the GetCaseState internal tool. + + Calls the PIMs instances endpoint with the agent's UiPath credentials and + folder context. The folder key is taken from the optional ``folderKey`` + argument when provided, otherwise from the runtime's ``UIPATH_FOLDER_KEY`` + environment variable via :class:`uipath.platform.common.ApiClient`. + """ + tool_name = sanitize_tool_name(resource.name) + input_model = create_model(resource.input_schema) + output_model = create_model(resource.output_schema) + + @mockable( + name=resource.name, + description=resource.description, + input_schema=input_model.model_json_schema(), + output_schema=output_model.model_json_schema(), + ) + async def tool_fn(**kwargs: Any) -> Any: + instance_id = kwargs.get("instanceId") + if not instance_id: + raise ValueError("Argument 'instanceId' is required") + + folder_key_override = kwargs.get("folderKey") + + client = UiPath() + url = PIMS_INSTANCE_PATH.format(instance_id=instance_id) + request_kwargs: dict[str, Any] = {} + if folder_key_override: + request_kwargs["headers"] = {"x-uipath-folderkey": folder_key_override} + else: + request_kwargs["include_folder_headers"] = True + + try: + response = await client.api_client.request_async( + "GET", url, **request_kwargs + ) + except EnrichedException as e: + raise_for_enriched( + e, _GET_CASE_STATE_ERRORS, title=tool_name, tool=tool_name + ) + raise + + return response.json() + + return StructuredToolWithArgumentProperties( + name=tool_name, + description=resource.description, + args_schema=input_model, + coroutine=tool_fn, + output_type=output_model, + argument_properties=resource.argument_properties, + metadata={ + "tool_type": resource.type.lower(), + "display_name": tool_name, + "args_schema": input_model, + "output_schema": output_model, + }, + ) diff --git a/src/uipath_langchain/agent/tools/internal_tools/internal_tool_factory.py b/src/uipath_langchain/agent/tools/internal_tools/internal_tool_factory.py index 5a8410675..f27634588 100644 --- a/src/uipath_langchain/agent/tools/internal_tools/internal_tool_factory.py +++ b/src/uipath_langchain/agent/tools/internal_tools/internal_tool_factory.py @@ -29,6 +29,7 @@ from .analyze_files_tool import create_analyze_file_tool from .batch_transform_tool import create_batch_transform_tool from .deeprag_tool import create_deeprag_tool +from .get_case_state_tool import create_get_case_state_tool _INTERNAL_TOOL_HANDLERS: dict[ AgentInternalToolType, @@ -37,6 +38,7 @@ AgentInternalToolType.ANALYZE_FILES: create_analyze_file_tool, AgentInternalToolType.DEEP_RAG: create_deeprag_tool, AgentInternalToolType.BATCH_TRANSFORM: create_batch_transform_tool, + AgentInternalToolType.GET_CASE_STATE: create_get_case_state_tool, } diff --git a/tests/agent/tools/internal_tools/test_get_case_state_tool.py b/tests/agent/tools/internal_tools/test_get_case_state_tool.py new file mode 100644 index 000000000..edfa292c5 --- /dev/null +++ b/tests/agent/tools/internal_tools/test_get_case_state_tool.py @@ -0,0 +1,151 @@ +"""Tests for get_case_state_tool.py module.""" + +from unittest.mock import AsyncMock, Mock, patch + +import pytest +from uipath.agent.models.agent import ( + AgentInternalGetCaseStateToolProperties, + AgentInternalToolResourceConfig, + AgentInternalToolType, +) + +from uipath_langchain.agent.exceptions import AgentRuntimeError +from uipath_langchain.agent.tools.internal_tools.get_case_state_tool import ( + PIMS_INSTANCE_PATH, + create_get_case_state_tool, +) + + +class TestCreateGetCaseStateTool: + """Test cases for create_get_case_state_tool function.""" + + @pytest.fixture + def mock_llm(self): + return AsyncMock() + + @pytest.fixture + def resource_config(self): + input_schema = { + "type": "object", + "properties": { + "instanceId": {"type": "string"}, + "folderKey": {"type": "string"}, + }, + "required": ["instanceId"], + } + output_schema = { + "type": "object", + "properties": {"state": {"type": "string"}}, + "required": ["state"], + } + properties = AgentInternalGetCaseStateToolProperties( + tool_type=AgentInternalToolType.GET_CASE_STATE + ) + return AgentInternalToolResourceConfig( + name="get_case_state", + description="Fetch the current state of a case by instance id.", + input_schema=input_schema, + output_schema=output_schema, + properties=properties, + ) + + @staticmethod + def _mock_uipath_response(mock_uipath_class, payload): + response = Mock() + response.json.return_value = payload + mock_uipath = Mock() + mock_uipath.api_client.request_async = AsyncMock(return_value=response) + mock_uipath_class.return_value = mock_uipath + return mock_uipath + + @patch( + "uipath_langchain.agent.tools.internal_tools.get_case_state_tool.mockable", + lambda **kwargs: lambda f: f, + ) + @patch("uipath_langchain.agent.tools.internal_tools.get_case_state_tool.UiPath") + async def test_explicit_folder_key_sends_header( + self, mock_uipath_class, resource_config, mock_llm + ): + mock_uipath = self._mock_uipath_response( + mock_uipath_class, {"state": "Resolved"} + ) + + tool = create_get_case_state_tool(resource_config, mock_llm) + assert tool.coroutine is not None + result = await tool.coroutine(instanceId="abc-123", folderKey="folder-xyz") + + assert result == {"state": "Resolved"} + mock_uipath.api_client.request_async.assert_awaited_once() + args, kwargs = mock_uipath.api_client.request_async.call_args + assert args == ("GET", PIMS_INSTANCE_PATH.format(instance_id="abc-123")) + assert kwargs == {"headers": {"x-uipath-folderkey": "folder-xyz"}} + + @patch( + "uipath_langchain.agent.tools.internal_tools.get_case_state_tool.mockable", + lambda **kwargs: lambda f: f, + ) + @patch("uipath_langchain.agent.tools.internal_tools.get_case_state_tool.UiPath") + async def test_falls_back_to_runtime_folder_context( + self, mock_uipath_class, resource_config, mock_llm + ): + mock_uipath = self._mock_uipath_response( + mock_uipath_class, {"state": "InProgress"} + ) + + tool = create_get_case_state_tool(resource_config, mock_llm) + assert tool.coroutine is not None + result = await tool.coroutine(instanceId="abc-123") + + assert result == {"state": "InProgress"} + args, kwargs = mock_uipath.api_client.request_async.call_args + assert args == ("GET", PIMS_INSTANCE_PATH.format(instance_id="abc-123")) + assert kwargs == {"include_folder_headers": True} + + @patch( + "uipath_langchain.agent.tools.internal_tools.get_case_state_tool.mockable", + lambda **kwargs: lambda f: f, + ) + @patch("uipath_langchain.agent.tools.internal_tools.get_case_state_tool.UiPath") + async def test_missing_instance_id_raises( + self, mock_uipath_class, resource_config, mock_llm + ): + self._mock_uipath_response(mock_uipath_class, {}) + + tool = create_get_case_state_tool(resource_config, mock_llm) + assert tool.coroutine is not None + with pytest.raises(ValueError, match="instanceId"): + await tool.coroutine(folderKey="folder-xyz") + + @patch( + "uipath_langchain.agent.tools.internal_tools.get_case_state_tool.mockable", + lambda **kwargs: lambda f: f, + ) + @patch("uipath_langchain.agent.tools.internal_tools.get_case_state_tool.UiPath") + async def test_404_maps_to_agent_runtime_error( + self, + mock_uipath_class, + resource_config, + mock_llm, + make_enriched_exception, + ): + mock_uipath = Mock() + mock_uipath.api_client.request_async = AsyncMock( + side_effect=make_enriched_exception(404, body="instance not found") + ) + mock_uipath_class.return_value = mock_uipath + + tool = create_get_case_state_tool(resource_config, mock_llm) + assert tool.coroutine is not None + with pytest.raises(AgentRuntimeError): + await tool.coroutine(instanceId="missing-id") + + def test_factory_registered_in_handlers(self): + from uipath_langchain.agent.tools.internal_tools.internal_tool_factory import ( + _INTERNAL_TOOL_HANDLERS, + ) + + assert AgentInternalToolType.GET_CASE_STATE in _INTERNAL_TOOL_HANDLERS + assert ( + _INTERNAL_TOOL_HANDLERS[AgentInternalToolType.GET_CASE_STATE] + is create_get_case_state_tool + ) From 516a3b383a3a1bd9c52e0b3bc5c2a8b0330fb8d0 Mon Sep 17 00:00:00 2001 From: "diana.grecu" Date: Tue, 2 Jun 2026 12:39:35 +0300 Subject: [PATCH 2/3] feat(agent/tools): add case plan, case entity, and execution trace tools --- .../internal_tools/get_case_entity_tool.py | 104 +++++++++++ .../internal_tools/get_case_plan_tool.py | 103 +++++++++++ .../get_execution_trace_tool.py | 108 ++++++++++++ .../internal_tools/internal_tool_factory.py | 6 + .../test_get_case_entity_tool.py | 150 ++++++++++++++++ .../internal_tools/test_get_case_plan_tool.py | 158 +++++++++++++++++ .../test_get_execution_trace_tool.py | 166 ++++++++++++++++++ 7 files changed, 795 insertions(+) create mode 100644 src/uipath_langchain/agent/tools/internal_tools/get_case_entity_tool.py create mode 100644 src/uipath_langchain/agent/tools/internal_tools/get_case_plan_tool.py create mode 100644 src/uipath_langchain/agent/tools/internal_tools/get_execution_trace_tool.py create mode 100644 tests/agent/tools/internal_tools/test_get_case_entity_tool.py create mode 100644 tests/agent/tools/internal_tools/test_get_case_plan_tool.py create mode 100644 tests/agent/tools/internal_tools/test_get_execution_trace_tool.py diff --git a/src/uipath_langchain/agent/tools/internal_tools/get_case_entity_tool.py b/src/uipath_langchain/agent/tools/internal_tools/get_case_entity_tool.py new file mode 100644 index 000000000..37496fde1 --- /dev/null +++ b/src/uipath_langchain/agent/tools/internal_tools/get_case_entity_tool.py @@ -0,0 +1,104 @@ +"""Internal tool that fetches the case entity (business object, documents, comments) for a PIMs case.""" + +from typing import Any + +from langchain_core.language_models import BaseChatModel +from langchain_core.tools import StructuredTool +from uipath.agent.models.agent import AgentInternalToolResourceConfig +from uipath.eval.mocks import mockable +from uipath.platform import UiPath +from uipath.platform.errors import EnrichedException +from uipath.runtime.errors import UiPathErrorCategory + +from uipath_langchain.agent.exceptions import raise_for_enriched +from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model +from uipath_langchain.agent.tools.structured_tool_with_argument_properties import ( + StructuredToolWithArgumentProperties, +) +from uipath_langchain.agent.tools.utils import sanitize_tool_name + +PIMS_CASE_ENTITY_PATH = "pims_/api/v1/cases/{instance_id}/case-json" + +_GET_CASE_ENTITY_ERRORS: dict[ + tuple[int, str | None], tuple[str, UiPathErrorCategory] +] = { + (404, None): ( + "Case not found for tool '{tool}': {message}", + UiPathErrorCategory.USER, + ), + (401, None): ( + "Unauthorized when fetching case entity for tool '{tool}': {message}", + UiPathErrorCategory.SYSTEM, + ), + (403, None): ( + "Forbidden when fetching case entity for tool '{tool}': {message}", + UiPathErrorCategory.USER, + ), +} + + +def create_get_case_entity_tool( + resource: AgentInternalToolResourceConfig, llm: BaseChatModel +) -> StructuredTool: + """Create the GetCaseEntity internal tool. + + Calls the PIMs ``case-json`` endpoint with the agent's UiPath credentials + and folder context. Returns the case entity payload — the business object, + linked documents, and comments — used by the agent to ground reasoning in + case facts. + + The folder key is taken from the optional ``folderKey`` argument when + provided, otherwise from the runtime's ``UIPATH_FOLDER_KEY`` environment + variable via :class:`uipath.platform.common.ApiClient`. + """ + tool_name = sanitize_tool_name(resource.name) + input_model = create_model(resource.input_schema) + output_model = create_model(resource.output_schema) + + @mockable( + name=resource.name, + description=resource.description, + input_schema=input_model.model_json_schema(), + output_schema=output_model.model_json_schema(), + ) + async def tool_fn(**kwargs: Any) -> Any: + instance_id = kwargs.get("instanceId") + if not instance_id: + raise ValueError("Argument 'instanceId' is required") + + folder_key_override = kwargs.get("folderKey") + + client = UiPath() + url = PIMS_CASE_ENTITY_PATH.format(instance_id=instance_id) + request_kwargs: dict[str, Any] = {} + if folder_key_override: + request_kwargs["headers"] = {"x-uipath-folderkey": folder_key_override} + else: + request_kwargs["include_folder_headers"] = True + + try: + response = await client.api_client.request_async( + "GET", url, **request_kwargs + ) + except EnrichedException as e: + raise_for_enriched( + e, _GET_CASE_ENTITY_ERRORS, title=tool_name, tool=tool_name + ) + raise + + return response.json() + + return StructuredToolWithArgumentProperties( + name=tool_name, + description=resource.description, + args_schema=input_model, + coroutine=tool_fn, + output_type=output_model, + argument_properties=resource.argument_properties, + metadata={ + "tool_type": resource.type.lower(), + "display_name": tool_name, + "args_schema": input_model, + "output_schema": output_model, + }, + ) diff --git a/src/uipath_langchain/agent/tools/internal_tools/get_case_plan_tool.py b/src/uipath_langchain/agent/tools/internal_tools/get_case_plan_tool.py new file mode 100644 index 000000000..ec1fd9043 --- /dev/null +++ b/src/uipath_langchain/agent/tools/internal_tools/get_case_plan_tool.py @@ -0,0 +1,103 @@ +"""Internal tool that fetches the case plan (stages, tasks, rules) for a PIMs case.""" + +from typing import Any + +from langchain_core.language_models import BaseChatModel +from langchain_core.tools import StructuredTool +from uipath.agent.models.agent import AgentInternalToolResourceConfig +from uipath.eval.mocks import mockable +from uipath.platform import UiPath +from uipath.platform.errors import EnrichedException +from uipath.runtime.errors import UiPathErrorCategory + +from uipath_langchain.agent.exceptions import raise_for_enriched +from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model +from uipath_langchain.agent.tools.structured_tool_with_argument_properties import ( + StructuredToolWithArgumentProperties, +) +from uipath_langchain.agent.tools.utils import sanitize_tool_name + +PIMS_CASE_PLAN_PATH = "pims_/api/v1/cases/{instance_id}/case-rules" + +_GET_CASE_PLAN_ERRORS: dict[ + tuple[int, str | None], tuple[str, UiPathErrorCategory] +] = { + (404, None): ( + "Case not found for tool '{tool}': {message}", + UiPathErrorCategory.USER, + ), + (401, None): ( + "Unauthorized when fetching case plan for tool '{tool}': {message}", + UiPathErrorCategory.SYSTEM, + ), + (403, None): ( + "Forbidden when fetching case plan for tool '{tool}': {message}", + UiPathErrorCategory.USER, + ), +} + + +def create_get_case_plan_tool( + resource: AgentInternalToolResourceConfig, llm: BaseChatModel +) -> StructuredTool: + """Create the GetCasePlan internal tool. + + Calls the PIMs ``case-rules`` endpoint with the agent's UiPath credentials + and folder context. Returns the full case plan: every stage and task with + its entry/exit rules, ``isRequired``, and ``shouldRunOnlyOnce``. + + The folder key is taken from the optional ``folderKey`` argument when + provided, otherwise from the runtime's ``UIPATH_FOLDER_KEY`` environment + variable via :class:`uipath.platform.common.ApiClient`. + """ + tool_name = sanitize_tool_name(resource.name) + input_model = create_model(resource.input_schema) + output_model = create_model(resource.output_schema) + + @mockable( + name=resource.name, + description=resource.description, + input_schema=input_model.model_json_schema(), + output_schema=output_model.model_json_schema(), + ) + async def tool_fn(**kwargs: Any) -> Any: + instance_id = kwargs.get("instanceId") + if not instance_id: + raise ValueError("Argument 'instanceId' is required") + + folder_key_override = kwargs.get("folderKey") + + client = UiPath() + url = PIMS_CASE_PLAN_PATH.format(instance_id=instance_id) + request_kwargs: dict[str, Any] = {} + if folder_key_override: + request_kwargs["headers"] = {"x-uipath-folderkey": folder_key_override} + else: + request_kwargs["include_folder_headers"] = True + + try: + response = await client.api_client.request_async( + "GET", url, **request_kwargs + ) + except EnrichedException as e: + raise_for_enriched( + e, _GET_CASE_PLAN_ERRORS, title=tool_name, tool=tool_name + ) + raise + + return response.json() + + return StructuredToolWithArgumentProperties( + name=tool_name, + description=resource.description, + args_schema=input_model, + coroutine=tool_fn, + output_type=output_model, + argument_properties=resource.argument_properties, + metadata={ + "tool_type": resource.type.lower(), + "display_name": tool_name, + "args_schema": input_model, + "output_schema": output_model, + }, + ) diff --git a/src/uipath_langchain/agent/tools/internal_tools/get_execution_trace_tool.py b/src/uipath_langchain/agent/tools/internal_tools/get_execution_trace_tool.py new file mode 100644 index 000000000..994e8d0cd --- /dev/null +++ b/src/uipath_langchain/agent/tools/internal_tools/get_execution_trace_tool.py @@ -0,0 +1,108 @@ +"""Internal tool that fetches the execution trace (audit trail) for a PIMs case.""" + +from typing import Any + +from langchain_core.language_models import BaseChatModel +from langchain_core.tools import StructuredTool +from uipath.agent.models.agent import AgentInternalToolResourceConfig +from uipath.eval.mocks import mockable +from uipath.platform import UiPath +from uipath.platform.errors import EnrichedException +from uipath.runtime.errors import UiPathErrorCategory + +from uipath_langchain.agent.exceptions import raise_for_enriched +from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model +from uipath_langchain.agent.tools.structured_tool_with_argument_properties import ( + StructuredToolWithArgumentProperties, +) +from uipath_langchain.agent.tools.utils import sanitize_tool_name + +PIMS_EXECUTION_TRACE_PATH = ( + "pims_/api/v2/element-executions/case-instances/{instance_id}" +) + +_GET_EXECUTION_TRACE_ERRORS: dict[ + tuple[int, str | None], tuple[str, UiPathErrorCategory] +] = { + (404, None): ( + "Case not found for tool '{tool}': {message}", + UiPathErrorCategory.USER, + ), + (401, None): ( + "Unauthorized when fetching execution trace for tool '{tool}': {message}", + UiPathErrorCategory.SYSTEM, + ), + (403, None): ( + "Forbidden when fetching execution trace for tool '{tool}': {message}", + UiPathErrorCategory.USER, + ), +} + + +def create_get_execution_trace_tool( + resource: AgentInternalToolResourceConfig, llm: BaseChatModel +) -> StructuredTool: + """Create the GetExecutionTrace internal tool. + + Calls the PIMs ``element-executions/case-instances`` (v2) endpoint with the + agent's UiPath credentials and folder context. Returns the audit trail of + the case instance — which rules fired, which agent invocations occurred, + what was decided, and in what order — used by the agent to detect patterns, + avoid duplicate actions, and reason about why the case is in its current + state. + + The folder key is taken from the optional ``folderKey`` argument when + provided, otherwise from the runtime's ``UIPATH_FOLDER_KEY`` environment + variable via :class:`uipath.platform.common.ApiClient`. + """ + tool_name = sanitize_tool_name(resource.name) + input_model = create_model(resource.input_schema) + output_model = create_model(resource.output_schema) + + @mockable( + name=resource.name, + description=resource.description, + input_schema=input_model.model_json_schema(), + output_schema=output_model.model_json_schema(), + ) + async def tool_fn(**kwargs: Any) -> Any: + instance_id = kwargs.get("instanceId") + if not instance_id: + raise ValueError("Argument 'instanceId' is required") + + folder_key_override = kwargs.get("folderKey") + + client = UiPath() + url = PIMS_EXECUTION_TRACE_PATH.format(instance_id=instance_id) + request_kwargs: dict[str, Any] = {} + if folder_key_override: + request_kwargs["headers"] = {"x-uipath-folderkey": folder_key_override} + else: + request_kwargs["include_folder_headers"] = True + + try: + response = await client.api_client.request_async( + "GET", url, **request_kwargs + ) + except EnrichedException as e: + raise_for_enriched( + e, _GET_EXECUTION_TRACE_ERRORS, title=tool_name, tool=tool_name + ) + raise + + return response.json() + + return StructuredToolWithArgumentProperties( + name=tool_name, + description=resource.description, + args_schema=input_model, + coroutine=tool_fn, + output_type=output_model, + argument_properties=resource.argument_properties, + metadata={ + "tool_type": resource.type.lower(), + "display_name": tool_name, + "args_schema": input_model, + "output_schema": output_model, + }, + ) diff --git a/src/uipath_langchain/agent/tools/internal_tools/internal_tool_factory.py b/src/uipath_langchain/agent/tools/internal_tools/internal_tool_factory.py index f27634588..9b0a905a1 100644 --- a/src/uipath_langchain/agent/tools/internal_tools/internal_tool_factory.py +++ b/src/uipath_langchain/agent/tools/internal_tools/internal_tool_factory.py @@ -29,7 +29,10 @@ from .analyze_files_tool import create_analyze_file_tool from .batch_transform_tool import create_batch_transform_tool from .deeprag_tool import create_deeprag_tool +from .get_case_entity_tool import create_get_case_entity_tool +from .get_case_plan_tool import create_get_case_plan_tool from .get_case_state_tool import create_get_case_state_tool +from .get_execution_trace_tool import create_get_execution_trace_tool _INTERNAL_TOOL_HANDLERS: dict[ AgentInternalToolType, @@ -39,6 +42,9 @@ AgentInternalToolType.DEEP_RAG: create_deeprag_tool, AgentInternalToolType.BATCH_TRANSFORM: create_batch_transform_tool, AgentInternalToolType.GET_CASE_STATE: create_get_case_state_tool, + AgentInternalToolType.GET_CASE_PLAN: create_get_case_plan_tool, + AgentInternalToolType.GET_CASE_ENTITY: create_get_case_entity_tool, + AgentInternalToolType.GET_EXECUTION_TRACE: create_get_execution_trace_tool, } diff --git a/tests/agent/tools/internal_tools/test_get_case_entity_tool.py b/tests/agent/tools/internal_tools/test_get_case_entity_tool.py new file mode 100644 index 000000000..4ca16c2f6 --- /dev/null +++ b/tests/agent/tools/internal_tools/test_get_case_entity_tool.py @@ -0,0 +1,150 @@ +"""Tests for get_case_entity_tool.py module.""" + +from unittest.mock import AsyncMock, Mock, patch + +import pytest +from uipath.agent.models.agent import ( + AgentInternalGetCaseEntityToolProperties, + AgentInternalToolResourceConfig, + AgentInternalToolType, +) + +from uipath_langchain.agent.exceptions import AgentRuntimeError +from uipath_langchain.agent.tools.internal_tools.get_case_entity_tool import ( + PIMS_CASE_ENTITY_PATH, + create_get_case_entity_tool, +) + + +class TestCreateGetCaseEntityTool: + """Test cases for create_get_case_entity_tool function.""" + + @pytest.fixture + def mock_llm(self): + return AsyncMock() + + @pytest.fixture + def resource_config(self): + input_schema = { + "type": "object", + "properties": { + "instanceId": {"type": "string"}, + "folderKey": {"type": "string"}, + }, + "required": ["instanceId"], + } + output_schema = { + "type": "object", + "properties": {"entity": {"type": "object"}}, + } + properties = AgentInternalGetCaseEntityToolProperties( + tool_type=AgentInternalToolType.GET_CASE_ENTITY + ) + return AgentInternalToolResourceConfig( + name="get_case_entity", + description="Fetch the case entity (business object, documents, comments) by instance id.", + input_schema=input_schema, + output_schema=output_schema, + properties=properties, + ) + + @staticmethod + def _mock_uipath_response(mock_uipath_class, payload): + response = Mock() + response.json.return_value = payload + mock_uipath = Mock() + mock_uipath.api_client.request_async = AsyncMock(return_value=response) + mock_uipath_class.return_value = mock_uipath + return mock_uipath + + @patch( + "uipath_langchain.agent.tools.internal_tools.get_case_entity_tool.mockable", + lambda **kwargs: lambda f: f, + ) + @patch("uipath_langchain.agent.tools.internal_tools.get_case_entity_tool.UiPath") + async def test_explicit_folder_key_sends_header( + self, mock_uipath_class, resource_config, mock_llm + ): + mock_uipath = self._mock_uipath_response( + mock_uipath_class, {"caseSummary": "test"} + ) + + tool = create_get_case_entity_tool(resource_config, mock_llm) + assert tool.coroutine is not None + result = await tool.coroutine(instanceId="abc-123", folderKey="folder-xyz") + + assert result == {"caseSummary": "test"} + mock_uipath.api_client.request_async.assert_awaited_once() + args, kwargs = mock_uipath.api_client.request_async.call_args + assert args == ("GET", PIMS_CASE_ENTITY_PATH.format(instance_id="abc-123")) + assert kwargs == {"headers": {"x-uipath-folderkey": "folder-xyz"}} + + @patch( + "uipath_langchain.agent.tools.internal_tools.get_case_entity_tool.mockable", + lambda **kwargs: lambda f: f, + ) + @patch("uipath_langchain.agent.tools.internal_tools.get_case_entity_tool.UiPath") + async def test_falls_back_to_runtime_folder_context( + self, mock_uipath_class, resource_config, mock_llm + ): + mock_uipath = self._mock_uipath_response( + mock_uipath_class, {"caseSummary": "fallback"} + ) + + tool = create_get_case_entity_tool(resource_config, mock_llm) + assert tool.coroutine is not None + result = await tool.coroutine(instanceId="abc-123") + + assert result == {"caseSummary": "fallback"} + args, kwargs = mock_uipath.api_client.request_async.call_args + assert args == ("GET", PIMS_CASE_ENTITY_PATH.format(instance_id="abc-123")) + assert kwargs == {"include_folder_headers": True} + + @patch( + "uipath_langchain.agent.tools.internal_tools.get_case_entity_tool.mockable", + lambda **kwargs: lambda f: f, + ) + @patch("uipath_langchain.agent.tools.internal_tools.get_case_entity_tool.UiPath") + async def test_missing_instance_id_raises( + self, mock_uipath_class, resource_config, mock_llm + ): + self._mock_uipath_response(mock_uipath_class, {}) + + tool = create_get_case_entity_tool(resource_config, mock_llm) + assert tool.coroutine is not None + with pytest.raises(ValueError, match="instanceId"): + await tool.coroutine(folderKey="folder-xyz") + + @patch( + "uipath_langchain.agent.tools.internal_tools.get_case_entity_tool.mockable", + lambda **kwargs: lambda f: f, + ) + @patch("uipath_langchain.agent.tools.internal_tools.get_case_entity_tool.UiPath") + async def test_404_maps_to_agent_runtime_error( + self, + mock_uipath_class, + resource_config, + mock_llm, + make_enriched_exception, + ): + mock_uipath = Mock() + mock_uipath.api_client.request_async = AsyncMock( + side_effect=make_enriched_exception(404, body="case not found") + ) + mock_uipath_class.return_value = mock_uipath + + tool = create_get_case_entity_tool(resource_config, mock_llm) + assert tool.coroutine is not None + with pytest.raises(AgentRuntimeError): + await tool.coroutine(instanceId="missing-id") + + def test_factory_registered_in_handlers(self): + from uipath_langchain.agent.tools.internal_tools.internal_tool_factory import ( + _INTERNAL_TOOL_HANDLERS, + ) + + assert AgentInternalToolType.GET_CASE_ENTITY in _INTERNAL_TOOL_HANDLERS + assert ( + _INTERNAL_TOOL_HANDLERS[AgentInternalToolType.GET_CASE_ENTITY] + is create_get_case_entity_tool + ) diff --git a/tests/agent/tools/internal_tools/test_get_case_plan_tool.py b/tests/agent/tools/internal_tools/test_get_case_plan_tool.py new file mode 100644 index 000000000..758bc165f --- /dev/null +++ b/tests/agent/tools/internal_tools/test_get_case_plan_tool.py @@ -0,0 +1,158 @@ +"""Tests for get_case_plan_tool.py module.""" + +from unittest.mock import AsyncMock, Mock, patch + +import pytest +from uipath.agent.models.agent import ( + AgentInternalGetCasePlanToolProperties, + AgentInternalToolResourceConfig, + AgentInternalToolType, +) + +from uipath_langchain.agent.exceptions import AgentRuntimeError +from uipath_langchain.agent.tools.internal_tools.get_case_plan_tool import ( + PIMS_CASE_PLAN_PATH, + create_get_case_plan_tool, +) + + +class TestCreateGetCasePlanTool: + """Test cases for create_get_case_plan_tool function.""" + + @pytest.fixture + def mock_llm(self): + return AsyncMock() + + @pytest.fixture + def resource_config(self): + input_schema = { + "type": "object", + "properties": { + "instanceId": {"type": "string"}, + "folderKey": {"type": "string"}, + }, + "required": ["instanceId"], + } + output_schema = { + "type": "object", + "properties": { + "case": {"type": "object"}, + "stages": {"type": "array"}, + }, + } + properties = AgentInternalGetCasePlanToolProperties( + tool_type=AgentInternalToolType.GET_CASE_PLAN + ) + return AgentInternalToolResourceConfig( + name="get_case_plan", + description="Fetch the case plan (stages, tasks, rules) by instance id.", + input_schema=input_schema, + output_schema=output_schema, + properties=properties, + ) + + @staticmethod + def _mock_uipath_response(mock_uipath_class, payload): + response = Mock() + response.json.return_value = payload + mock_uipath = Mock() + mock_uipath.api_client.request_async = AsyncMock(return_value=response) + mock_uipath_class.return_value = mock_uipath + return mock_uipath + + @patch( + "uipath_langchain.agent.tools.internal_tools.get_case_plan_tool.mockable", + lambda **kwargs: lambda f: f, + ) + @patch("uipath_langchain.agent.tools.internal_tools.get_case_plan_tool.UiPath") + async def test_explicit_folder_key_sends_header( + self, mock_uipath_class, resource_config, mock_llm + ): + mock_uipath = self._mock_uipath_response( + mock_uipath_class, + {"case": {"exitConditions": []}, "stages": []}, + ) + + tool = create_get_case_plan_tool(resource_config, mock_llm) + assert tool.coroutine is not None + result = await tool.coroutine(instanceId="abc-123", folderKey="folder-xyz") + + assert result == {"case": {"exitConditions": []}, "stages": []} + mock_uipath.api_client.request_async.assert_awaited_once() + args, kwargs = mock_uipath.api_client.request_async.call_args + assert args == ("GET", PIMS_CASE_PLAN_PATH.format(instance_id="abc-123")) + assert kwargs == {"headers": {"x-uipath-folderkey": "folder-xyz"}} + + @patch( + "uipath_langchain.agent.tools.internal_tools.get_case_plan_tool.mockable", + lambda **kwargs: lambda f: f, + ) + @patch("uipath_langchain.agent.tools.internal_tools.get_case_plan_tool.UiPath") + async def test_falls_back_to_runtime_folder_context( + self, mock_uipath_class, resource_config, mock_llm + ): + mock_uipath = self._mock_uipath_response( + mock_uipath_class, + {"case": {"exitConditions": []}, "stages": [{"name": "stage-1"}]}, + ) + + tool = create_get_case_plan_tool(resource_config, mock_llm) + assert tool.coroutine is not None + result = await tool.coroutine(instanceId="abc-123") + + assert result == { + "case": {"exitConditions": []}, + "stages": [{"name": "stage-1"}], + } + args, kwargs = mock_uipath.api_client.request_async.call_args + assert args == ("GET", PIMS_CASE_PLAN_PATH.format(instance_id="abc-123")) + assert kwargs == {"include_folder_headers": True} + + @patch( + "uipath_langchain.agent.tools.internal_tools.get_case_plan_tool.mockable", + lambda **kwargs: lambda f: f, + ) + @patch("uipath_langchain.agent.tools.internal_tools.get_case_plan_tool.UiPath") + async def test_missing_instance_id_raises( + self, mock_uipath_class, resource_config, mock_llm + ): + self._mock_uipath_response(mock_uipath_class, {}) + + tool = create_get_case_plan_tool(resource_config, mock_llm) + assert tool.coroutine is not None + with pytest.raises(ValueError, match="instanceId"): + await tool.coroutine(folderKey="folder-xyz") + + @patch( + "uipath_langchain.agent.tools.internal_tools.get_case_plan_tool.mockable", + lambda **kwargs: lambda f: f, + ) + @patch("uipath_langchain.agent.tools.internal_tools.get_case_plan_tool.UiPath") + async def test_404_maps_to_agent_runtime_error( + self, + mock_uipath_class, + resource_config, + mock_llm, + make_enriched_exception, + ): + mock_uipath = Mock() + mock_uipath.api_client.request_async = AsyncMock( + side_effect=make_enriched_exception(404, body="case not found") + ) + mock_uipath_class.return_value = mock_uipath + + tool = create_get_case_plan_tool(resource_config, mock_llm) + assert tool.coroutine is not None + with pytest.raises(AgentRuntimeError): + await tool.coroutine(instanceId="missing-id") + + def test_factory_registered_in_handlers(self): + from uipath_langchain.agent.tools.internal_tools.internal_tool_factory import ( + _INTERNAL_TOOL_HANDLERS, + ) + + assert AgentInternalToolType.GET_CASE_PLAN in _INTERNAL_TOOL_HANDLERS + assert ( + _INTERNAL_TOOL_HANDLERS[AgentInternalToolType.GET_CASE_PLAN] + is create_get_case_plan_tool + ) diff --git a/tests/agent/tools/internal_tools/test_get_execution_trace_tool.py b/tests/agent/tools/internal_tools/test_get_execution_trace_tool.py new file mode 100644 index 000000000..fb641f523 --- /dev/null +++ b/tests/agent/tools/internal_tools/test_get_execution_trace_tool.py @@ -0,0 +1,166 @@ +"""Tests for get_execution_trace_tool.py module.""" + +from unittest.mock import AsyncMock, Mock, patch + +import pytest +from uipath.agent.models.agent import ( + AgentInternalGetExecutionTraceToolProperties, + AgentInternalToolResourceConfig, + AgentInternalToolType, +) + +from uipath_langchain.agent.exceptions import AgentRuntimeError +from uipath_langchain.agent.tools.internal_tools.get_execution_trace_tool import ( + PIMS_EXECUTION_TRACE_PATH, + create_get_execution_trace_tool, +) + + +class TestCreateGetExecutionTraceTool: + """Test cases for create_get_execution_trace_tool function.""" + + @pytest.fixture + def mock_llm(self): + return AsyncMock() + + @pytest.fixture + def resource_config(self): + input_schema = { + "type": "object", + "properties": { + "instanceId": {"type": "string"}, + "folderKey": {"type": "string"}, + }, + "required": ["instanceId"], + } + output_schema = { + "type": "object", + "properties": { + "elementExecutions": {"type": "array"}, + "traceId": {"type": "string"}, + }, + } + properties = AgentInternalGetExecutionTraceToolProperties( + tool_type=AgentInternalToolType.GET_EXECUTION_TRACE + ) + return AgentInternalToolResourceConfig( + name="get_execution_trace", + description="Fetch the audit trail (element executions) of a case instance by id.", + input_schema=input_schema, + output_schema=output_schema, + properties=properties, + ) + + @staticmethod + def _mock_uipath_response(mock_uipath_class, payload): + response = Mock() + response.json.return_value = payload + mock_uipath = Mock() + mock_uipath.api_client.request_async = AsyncMock(return_value=response) + mock_uipath_class.return_value = mock_uipath + return mock_uipath + + @patch( + "uipath_langchain.agent.tools.internal_tools.get_execution_trace_tool.mockable", + lambda **kwargs: lambda f: f, + ) + @patch( + "uipath_langchain.agent.tools.internal_tools.get_execution_trace_tool.UiPath" + ) + async def test_explicit_folder_key_sends_header( + self, mock_uipath_class, resource_config, mock_llm + ): + mock_uipath = self._mock_uipath_response( + mock_uipath_class, + {"elementExecutions": [], "traceId": "trace-1"}, + ) + + tool = create_get_execution_trace_tool(resource_config, mock_llm) + assert tool.coroutine is not None + result = await tool.coroutine(instanceId="abc-123", folderKey="folder-xyz") + + assert result == {"elementExecutions": [], "traceId": "trace-1"} + mock_uipath.api_client.request_async.assert_awaited_once() + args, kwargs = mock_uipath.api_client.request_async.call_args + assert args == ("GET", PIMS_EXECUTION_TRACE_PATH.format(instance_id="abc-123")) + assert kwargs == {"headers": {"x-uipath-folderkey": "folder-xyz"}} + + @patch( + "uipath_langchain.agent.tools.internal_tools.get_execution_trace_tool.mockable", + lambda **kwargs: lambda f: f, + ) + @patch( + "uipath_langchain.agent.tools.internal_tools.get_execution_trace_tool.UiPath" + ) + async def test_falls_back_to_runtime_folder_context( + self, mock_uipath_class, resource_config, mock_llm + ): + mock_uipath = self._mock_uipath_response( + mock_uipath_class, + {"elementExecutions": [{"elementId": "e1"}], "traceId": "trace-2"}, + ) + + tool = create_get_execution_trace_tool(resource_config, mock_llm) + assert tool.coroutine is not None + result = await tool.coroutine(instanceId="abc-123") + + assert result == { + "elementExecutions": [{"elementId": "e1"}], + "traceId": "trace-2", + } + args, kwargs = mock_uipath.api_client.request_async.call_args + assert args == ("GET", PIMS_EXECUTION_TRACE_PATH.format(instance_id="abc-123")) + assert kwargs == {"include_folder_headers": True} + + @patch( + "uipath_langchain.agent.tools.internal_tools.get_execution_trace_tool.mockable", + lambda **kwargs: lambda f: f, + ) + @patch( + "uipath_langchain.agent.tools.internal_tools.get_execution_trace_tool.UiPath" + ) + async def test_missing_instance_id_raises( + self, mock_uipath_class, resource_config, mock_llm + ): + self._mock_uipath_response(mock_uipath_class, {}) + + tool = create_get_execution_trace_tool(resource_config, mock_llm) + assert tool.coroutine is not None + with pytest.raises(ValueError, match="instanceId"): + await tool.coroutine(folderKey="folder-xyz") + + @patch( + "uipath_langchain.agent.tools.internal_tools.get_execution_trace_tool.mockable", + lambda **kwargs: lambda f: f, + ) + @patch( + "uipath_langchain.agent.tools.internal_tools.get_execution_trace_tool.UiPath" + ) + async def test_404_maps_to_agent_runtime_error( + self, + mock_uipath_class, + resource_config, + mock_llm, + make_enriched_exception, + ): + mock_uipath = Mock() + mock_uipath.api_client.request_async = AsyncMock( + side_effect=make_enriched_exception(404, body="case not found") + ) + mock_uipath_class.return_value = mock_uipath + + tool = create_get_execution_trace_tool(resource_config, mock_llm) + assert tool.coroutine is not None + with pytest.raises(AgentRuntimeError): + await tool.coroutine(instanceId="missing-id") + + def test_factory_registered_in_handlers(self): + from uipath_langchain.agent.tools.internal_tools.internal_tool_factory import ( + _INTERNAL_TOOL_HANDLERS, + ) + + assert AgentInternalToolType.GET_EXECUTION_TRACE in _INTERNAL_TOOL_HANDLERS + assert ( + _INTERNAL_TOOL_HANDLERS[AgentInternalToolType.GET_EXECUTION_TRACE] + is create_get_execution_trace_tool + ) From 657bb90a13456b13db12b4082bf51f41b7e06da8 Mon Sep 17 00:00:00 2001 From: "diana.grecu" Date: Tue, 2 Jun 2026 14:19:56 +0300 Subject: [PATCH 3/3] refactor(agent/tools): extract shared PIMs read tool factory --- .../internal_tools/_pims_read_tool_factory.py | 136 ++++++++++++++++++ .../internal_tools/get_case_entity_tool.py | 97 ++----------- .../internal_tools/get_case_plan_tool.py | 95 ++---------- .../internal_tools/get_case_state_tool.py | 91 ++---------- .../get_execution_trace_tool.py | 97 ++----------- .../test_get_case_entity_tool.py | 16 +-- .../internal_tools/test_get_case_plan_tool.py | 16 +-- .../test_get_case_state_tool.py | 16 +-- .../test_get_execution_trace_tool.py | 16 +-- 9 files changed, 204 insertions(+), 376 deletions(-) create mode 100644 src/uipath_langchain/agent/tools/internal_tools/_pims_read_tool_factory.py diff --git a/src/uipath_langchain/agent/tools/internal_tools/_pims_read_tool_factory.py b/src/uipath_langchain/agent/tools/internal_tools/_pims_read_tool_factory.py new file mode 100644 index 000000000..bc374812b --- /dev/null +++ b/src/uipath_langchain/agent/tools/internal_tools/_pims_read_tool_factory.py @@ -0,0 +1,136 @@ +"""Shared factory for PIMs read tools. + +All read-only PIMs tools (case state, case plan, case entity, execution trace) +share the same shape: ``GET`` a PIMs endpoint scoped by ``instanceId``, accept +an optional ``folderKey`` override, return the parsed JSON body, and map known +HTTP errors to friendly :class:`AgentRuntimeError` instances. Only the path +template and the human-readable subject (e.g. "case plan") differ per tool. + +This module provides that shared builder. Each per-tool file collapses to a +path constant plus a thin call to :func:`build_pims_read_tool`. +""" + +from typing import Any + +from langchain_core.tools import StructuredTool +from uipath.agent.models.agent import AgentInternalToolResourceConfig +from uipath.eval.mocks import mockable +from uipath.platform import UiPath +from uipath.platform.errors import EnrichedException +from uipath.runtime.errors import UiPathErrorCategory + +from uipath_langchain.agent.exceptions import raise_for_enriched +from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model +from uipath_langchain.agent.tools.structured_tool_with_argument_properties import ( + StructuredToolWithArgumentProperties, +) +from uipath_langchain.agent.tools.utils import sanitize_tool_name + + +def _build_error_map( + subject: str, +) -> dict[tuple[int, str | None], tuple[str, UiPathErrorCategory]]: + """Build the standard PIMs-read error map for the given subject. + + ``subject`` is the noun used in 401/403 messages (e.g. "case plan", + "execution trace"). The 404 message stays generic since the missing thing + is always the case instance itself. + """ + return { + (404, None): ( + "Case instance not found for tool '{tool}': {message}", + UiPathErrorCategory.USER, + ), + (401, None): ( + f"Unauthorized when fetching {subject} for tool '{{tool}}': {{message}}", + UiPathErrorCategory.SYSTEM, + ), + (403, None): ( + f"Forbidden when fetching {subject} for tool '{{tool}}': {{message}}", + UiPathErrorCategory.USER, + ), + } + + +def build_pims_read_tool( + resource: AgentInternalToolResourceConfig, + *, + path_template: str, + subject: str, +) -> StructuredTool: + """Build a read-only PIMs GET tool. + + The tool takes an LLM-supplied ``instanceId`` and an optional ``folderKey`` + override, calls the PIMs endpoint at ``path_template`` (with the + ``{instance_id}`` placeholder filled in), and returns the parsed JSON body. + Known 4xx errors map to friendly :class:`AgentRuntimeError` instances. + + Args: + resource: The tool resource configuration (name, description, input + and output schemas, argument properties). + path_template: The PIMs path with an ``{instance_id}`` placeholder + (e.g. ``"pims_/api/v1/cases/{instance_id}/case-rules"``). + subject: The noun used in 401/403 error messages + (e.g. ``"case plan"``, ``"execution trace"``). + + Returns: + A :class:`StructuredTool` ready to register in the internal-tool + factory map. + + Notes: + The folder key is taken from the optional ``folderKey`` argument when + provided, otherwise from the runtime's ``UIPATH_FOLDER_KEY`` env var + via :class:`uipath.platform.common.ApiClient`. + """ + tool_name = sanitize_tool_name(resource.name) + input_model = create_model(resource.input_schema) + output_model = create_model(resource.output_schema) + error_map = _build_error_map(subject) + + @mockable( + name=resource.name, + description=resource.description, + input_schema=input_model.model_json_schema(), + output_schema=output_model.model_json_schema(), + ) + async def tool_fn(**kwargs: Any) -> Any: + instance_id = kwargs.get("instanceId") + if not instance_id: + raise ValueError("Argument 'instanceId' is required") + + folder_key_override = kwargs.get("folderKey") + + client = UiPath() + url = path_template.format(instance_id=instance_id) + request_kwargs: dict[str, Any] = {} + if folder_key_override: + request_kwargs["headers"] = {"x-uipath-folderkey": folder_key_override} + else: + request_kwargs["include_folder_headers"] = True + + try: + response = await client.api_client.request_async( + "GET", url, **request_kwargs + ) + except EnrichedException as e: + raise_for_enriched( + e, error_map, title=tool_name, tool=tool_name + ) + raise + + return response.json() + + return StructuredToolWithArgumentProperties( + name=tool_name, + description=resource.description, + args_schema=input_model, + coroutine=tool_fn, + output_type=output_model, + argument_properties=resource.argument_properties, + metadata={ + "tool_type": resource.type.lower(), + "display_name": tool_name, + "args_schema": input_model, + "output_schema": output_model, + }, + ) diff --git a/src/uipath_langchain/agent/tools/internal_tools/get_case_entity_tool.py b/src/uipath_langchain/agent/tools/internal_tools/get_case_entity_tool.py index 37496fde1..d326b244e 100644 --- a/src/uipath_langchain/agent/tools/internal_tools/get_case_entity_tool.py +++ b/src/uipath_langchain/agent/tools/internal_tools/get_case_entity_tool.py @@ -1,104 +1,29 @@ """Internal tool that fetches the case entity (business object, documents, comments) for a PIMs case.""" -from typing import Any - from langchain_core.language_models import BaseChatModel from langchain_core.tools import StructuredTool from uipath.agent.models.agent import AgentInternalToolResourceConfig -from uipath.eval.mocks import mockable -from uipath.platform import UiPath -from uipath.platform.errors import EnrichedException -from uipath.runtime.errors import UiPathErrorCategory -from uipath_langchain.agent.exceptions import raise_for_enriched -from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model -from uipath_langchain.agent.tools.structured_tool_with_argument_properties import ( - StructuredToolWithArgumentProperties, -) -from uipath_langchain.agent.tools.utils import sanitize_tool_name +from ._pims_read_tool_factory import build_pims_read_tool PIMS_CASE_ENTITY_PATH = "pims_/api/v1/cases/{instance_id}/case-json" -_GET_CASE_ENTITY_ERRORS: dict[ - tuple[int, str | None], tuple[str, UiPathErrorCategory] -] = { - (404, None): ( - "Case not found for tool '{tool}': {message}", - UiPathErrorCategory.USER, - ), - (401, None): ( - "Unauthorized when fetching case entity for tool '{tool}': {message}", - UiPathErrorCategory.SYSTEM, - ), - (403, None): ( - "Forbidden when fetching case entity for tool '{tool}': {message}", - UiPathErrorCategory.USER, - ), -} - def create_get_case_entity_tool( resource: AgentInternalToolResourceConfig, llm: BaseChatModel ) -> StructuredTool: """Create the GetCaseEntity internal tool. - Calls the PIMs ``case-json`` endpoint with the agent's UiPath credentials - and folder context. Returns the case entity payload — the business object, - linked documents, and comments — used by the agent to ground reasoning in - case facts. + Returns the case-json payload from the PIMs ``case-json`` endpoint. Shared + HTTP/auth/folder/error logic lives in :func:`build_pims_read_tool`. - The folder key is taken from the optional ``folderKey`` argument when - provided, otherwise from the runtime's ``UIPATH_FOLDER_KEY`` environment - variable via :class:`uipath.platform.common.ApiClient`. + Note: as of testing on alpha, ``case-json`` returns the case **definition** + (id, version, metadata, stages) rather than the business object / linked + documents / comments. The "entity" naming is provisional pending the CM + team confirming the right surface. """ - tool_name = sanitize_tool_name(resource.name) - input_model = create_model(resource.input_schema) - output_model = create_model(resource.output_schema) - - @mockable( - name=resource.name, - description=resource.description, - input_schema=input_model.model_json_schema(), - output_schema=output_model.model_json_schema(), - ) - async def tool_fn(**kwargs: Any) -> Any: - instance_id = kwargs.get("instanceId") - if not instance_id: - raise ValueError("Argument 'instanceId' is required") - - folder_key_override = kwargs.get("folderKey") - - client = UiPath() - url = PIMS_CASE_ENTITY_PATH.format(instance_id=instance_id) - request_kwargs: dict[str, Any] = {} - if folder_key_override: - request_kwargs["headers"] = {"x-uipath-folderkey": folder_key_override} - else: - request_kwargs["include_folder_headers"] = True - - try: - response = await client.api_client.request_async( - "GET", url, **request_kwargs - ) - except EnrichedException as e: - raise_for_enriched( - e, _GET_CASE_ENTITY_ERRORS, title=tool_name, tool=tool_name - ) - raise - - return response.json() - - return StructuredToolWithArgumentProperties( - name=tool_name, - description=resource.description, - args_schema=input_model, - coroutine=tool_fn, - output_type=output_model, - argument_properties=resource.argument_properties, - metadata={ - "tool_type": resource.type.lower(), - "display_name": tool_name, - "args_schema": input_model, - "output_schema": output_model, - }, + return build_pims_read_tool( + resource, + path_template=PIMS_CASE_ENTITY_PATH, + subject="case entity", ) diff --git a/src/uipath_langchain/agent/tools/internal_tools/get_case_plan_tool.py b/src/uipath_langchain/agent/tools/internal_tools/get_case_plan_tool.py index ec1fd9043..a6e52d68d 100644 --- a/src/uipath_langchain/agent/tools/internal_tools/get_case_plan_tool.py +++ b/src/uipath_langchain/agent/tools/internal_tools/get_case_plan_tool.py @@ -1,103 +1,26 @@ """Internal tool that fetches the case plan (stages, tasks, rules) for a PIMs case.""" -from typing import Any - from langchain_core.language_models import BaseChatModel from langchain_core.tools import StructuredTool from uipath.agent.models.agent import AgentInternalToolResourceConfig -from uipath.eval.mocks import mockable -from uipath.platform import UiPath -from uipath.platform.errors import EnrichedException -from uipath.runtime.errors import UiPathErrorCategory -from uipath_langchain.agent.exceptions import raise_for_enriched -from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model -from uipath_langchain.agent.tools.structured_tool_with_argument_properties import ( - StructuredToolWithArgumentProperties, -) -from uipath_langchain.agent.tools.utils import sanitize_tool_name +from ._pims_read_tool_factory import build_pims_read_tool PIMS_CASE_PLAN_PATH = "pims_/api/v1/cases/{instance_id}/case-rules" -_GET_CASE_PLAN_ERRORS: dict[ - tuple[int, str | None], tuple[str, UiPathErrorCategory] -] = { - (404, None): ( - "Case not found for tool '{tool}': {message}", - UiPathErrorCategory.USER, - ), - (401, None): ( - "Unauthorized when fetching case plan for tool '{tool}': {message}", - UiPathErrorCategory.SYSTEM, - ), - (403, None): ( - "Forbidden when fetching case plan for tool '{tool}': {message}", - UiPathErrorCategory.USER, - ), -} - def create_get_case_plan_tool( resource: AgentInternalToolResourceConfig, llm: BaseChatModel ) -> StructuredTool: """Create the GetCasePlan internal tool. - Calls the PIMs ``case-rules`` endpoint with the agent's UiPath credentials - and folder context. Returns the full case plan: every stage and task with - its entry/exit rules, ``isRequired``, and ``shouldRunOnlyOnce``. - - The folder key is taken from the optional ``folderKey`` argument when - provided, otherwise from the runtime's ``UIPATH_FOLDER_KEY`` environment - variable via :class:`uipath.platform.common.ApiClient`. + Returns the full case plan from the PIMs ``case-rules`` endpoint: every + stage and task with entry/exit rules, ``isRequired``, and + ``shouldRunOnlyOnce``. Shared HTTP/auth/folder/error logic lives in + :func:`build_pims_read_tool`. """ - tool_name = sanitize_tool_name(resource.name) - input_model = create_model(resource.input_schema) - output_model = create_model(resource.output_schema) - - @mockable( - name=resource.name, - description=resource.description, - input_schema=input_model.model_json_schema(), - output_schema=output_model.model_json_schema(), - ) - async def tool_fn(**kwargs: Any) -> Any: - instance_id = kwargs.get("instanceId") - if not instance_id: - raise ValueError("Argument 'instanceId' is required") - - folder_key_override = kwargs.get("folderKey") - - client = UiPath() - url = PIMS_CASE_PLAN_PATH.format(instance_id=instance_id) - request_kwargs: dict[str, Any] = {} - if folder_key_override: - request_kwargs["headers"] = {"x-uipath-folderkey": folder_key_override} - else: - request_kwargs["include_folder_headers"] = True - - try: - response = await client.api_client.request_async( - "GET", url, **request_kwargs - ) - except EnrichedException as e: - raise_for_enriched( - e, _GET_CASE_PLAN_ERRORS, title=tool_name, tool=tool_name - ) - raise - - return response.json() - - return StructuredToolWithArgumentProperties( - name=tool_name, - description=resource.description, - args_schema=input_model, - coroutine=tool_fn, - output_type=output_model, - argument_properties=resource.argument_properties, - metadata={ - "tool_type": resource.type.lower(), - "display_name": tool_name, - "args_schema": input_model, - "output_schema": output_model, - }, + return build_pims_read_tool( + resource, + path_template=PIMS_CASE_PLAN_PATH, + subject="case plan", ) diff --git a/src/uipath_langchain/agent/tools/internal_tools/get_case_state_tool.py b/src/uipath_langchain/agent/tools/internal_tools/get_case_state_tool.py index 512710987..c6c19cd59 100644 --- a/src/uipath_langchain/agent/tools/internal_tools/get_case_state_tool.py +++ b/src/uipath_langchain/agent/tools/internal_tools/get_case_state_tool.py @@ -1,100 +1,25 @@ """Internal tool that fetches the current state of a PIMs case instance.""" -from typing import Any - from langchain_core.language_models import BaseChatModel from langchain_core.tools import StructuredTool from uipath.agent.models.agent import AgentInternalToolResourceConfig -from uipath.eval.mocks import mockable -from uipath.platform import UiPath -from uipath.platform.errors import EnrichedException -from uipath.runtime.errors import UiPathErrorCategory -from uipath_langchain.agent.exceptions import raise_for_enriched -from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model -from uipath_langchain.agent.tools.structured_tool_with_argument_properties import ( - StructuredToolWithArgumentProperties, -) -from uipath_langchain.agent.tools.utils import sanitize_tool_name +from ._pims_read_tool_factory import build_pims_read_tool PIMS_INSTANCE_PATH = "pims_/api/v1/instances/{instance_id}" -_GET_CASE_STATE_ERRORS: dict[ - tuple[int, str | None], tuple[str, UiPathErrorCategory] -] = { - (404, None): ( - "Case instance not found for tool '{tool}': {message}", - UiPathErrorCategory.USER, - ), - (401, None): ( - "Unauthorized when fetching case state for tool '{tool}': {message}", - UiPathErrorCategory.SYSTEM, - ), - (403, None): ( - "Forbidden when fetching case state for tool '{tool}': {message}", - UiPathErrorCategory.USER, - ), -} - def create_get_case_state_tool( resource: AgentInternalToolResourceConfig, llm: BaseChatModel ) -> StructuredTool: """Create the GetCaseState internal tool. - Calls the PIMs instances endpoint with the agent's UiPath credentials and - folder context. The folder key is taken from the optional ``folderKey`` - argument when provided, otherwise from the runtime's ``UIPATH_FOLDER_KEY`` - environment variable via :class:`uipath.platform.common.ApiClient`. + Returns the case instance state from the PIMs ``/v1/instances/{id}`` + endpoint. Shared HTTP/auth/folder/error logic lives in + :func:`build_pims_read_tool`. """ - tool_name = sanitize_tool_name(resource.name) - input_model = create_model(resource.input_schema) - output_model = create_model(resource.output_schema) - - @mockable( - name=resource.name, - description=resource.description, - input_schema=input_model.model_json_schema(), - output_schema=output_model.model_json_schema(), - ) - async def tool_fn(**kwargs: Any) -> Any: - instance_id = kwargs.get("instanceId") - if not instance_id: - raise ValueError("Argument 'instanceId' is required") - - folder_key_override = kwargs.get("folderKey") - - client = UiPath() - url = PIMS_INSTANCE_PATH.format(instance_id=instance_id) - request_kwargs: dict[str, Any] = {} - if folder_key_override: - request_kwargs["headers"] = {"x-uipath-folderkey": folder_key_override} - else: - request_kwargs["include_folder_headers"] = True - - try: - response = await client.api_client.request_async( - "GET", url, **request_kwargs - ) - except EnrichedException as e: - raise_for_enriched( - e, _GET_CASE_STATE_ERRORS, title=tool_name, tool=tool_name - ) - raise - - return response.json() - - return StructuredToolWithArgumentProperties( - name=tool_name, - description=resource.description, - args_schema=input_model, - coroutine=tool_fn, - output_type=output_model, - argument_properties=resource.argument_properties, - metadata={ - "tool_type": resource.type.lower(), - "display_name": tool_name, - "args_schema": input_model, - "output_schema": output_model, - }, + return build_pims_read_tool( + resource, + path_template=PIMS_INSTANCE_PATH, + subject="case state", ) diff --git a/src/uipath_langchain/agent/tools/internal_tools/get_execution_trace_tool.py b/src/uipath_langchain/agent/tools/internal_tools/get_execution_trace_tool.py index 994e8d0cd..22fa27c5a 100644 --- a/src/uipath_langchain/agent/tools/internal_tools/get_execution_trace_tool.py +++ b/src/uipath_langchain/agent/tools/internal_tools/get_execution_trace_tool.py @@ -1,108 +1,27 @@ """Internal tool that fetches the execution trace (audit trail) for a PIMs case.""" -from typing import Any - from langchain_core.language_models import BaseChatModel from langchain_core.tools import StructuredTool from uipath.agent.models.agent import AgentInternalToolResourceConfig -from uipath.eval.mocks import mockable -from uipath.platform import UiPath -from uipath.platform.errors import EnrichedException -from uipath.runtime.errors import UiPathErrorCategory -from uipath_langchain.agent.exceptions import raise_for_enriched -from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model -from uipath_langchain.agent.tools.structured_tool_with_argument_properties import ( - StructuredToolWithArgumentProperties, -) -from uipath_langchain.agent.tools.utils import sanitize_tool_name +from ._pims_read_tool_factory import build_pims_read_tool PIMS_EXECUTION_TRACE_PATH = ( "pims_/api/v2/element-executions/case-instances/{instance_id}" ) -_GET_EXECUTION_TRACE_ERRORS: dict[ - tuple[int, str | None], tuple[str, UiPathErrorCategory] -] = { - (404, None): ( - "Case not found for tool '{tool}': {message}", - UiPathErrorCategory.USER, - ), - (401, None): ( - "Unauthorized when fetching execution trace for tool '{tool}': {message}", - UiPathErrorCategory.SYSTEM, - ), - (403, None): ( - "Forbidden when fetching execution trace for tool '{tool}': {message}", - UiPathErrorCategory.USER, - ), -} - def create_get_execution_trace_tool( resource: AgentInternalToolResourceConfig, llm: BaseChatModel ) -> StructuredTool: """Create the GetExecutionTrace internal tool. - Calls the PIMs ``element-executions/case-instances`` (v2) endpoint with the - agent's UiPath credentials and folder context. Returns the audit trail of - the case instance — which rules fired, which agent invocations occurred, - what was decided, and in what order — used by the agent to detect patterns, - avoid duplicate actions, and reason about why the case is in its current - state. - - The folder key is taken from the optional ``folderKey`` argument when - provided, otherwise from the runtime's ``UIPATH_FOLDER_KEY`` environment - variable via :class:`uipath.platform.common.ApiClient`. + Returns the audit trail (``elementExecutions[]``, ``traceId``, etc.) from + the PIMs ``v2/element-executions/case-instances`` endpoint. Shared + HTTP/auth/folder/error logic lives in :func:`build_pims_read_tool`. """ - tool_name = sanitize_tool_name(resource.name) - input_model = create_model(resource.input_schema) - output_model = create_model(resource.output_schema) - - @mockable( - name=resource.name, - description=resource.description, - input_schema=input_model.model_json_schema(), - output_schema=output_model.model_json_schema(), - ) - async def tool_fn(**kwargs: Any) -> Any: - instance_id = kwargs.get("instanceId") - if not instance_id: - raise ValueError("Argument 'instanceId' is required") - - folder_key_override = kwargs.get("folderKey") - - client = UiPath() - url = PIMS_EXECUTION_TRACE_PATH.format(instance_id=instance_id) - request_kwargs: dict[str, Any] = {} - if folder_key_override: - request_kwargs["headers"] = {"x-uipath-folderkey": folder_key_override} - else: - request_kwargs["include_folder_headers"] = True - - try: - response = await client.api_client.request_async( - "GET", url, **request_kwargs - ) - except EnrichedException as e: - raise_for_enriched( - e, _GET_EXECUTION_TRACE_ERRORS, title=tool_name, tool=tool_name - ) - raise - - return response.json() - - return StructuredToolWithArgumentProperties( - name=tool_name, - description=resource.description, - args_schema=input_model, - coroutine=tool_fn, - output_type=output_model, - argument_properties=resource.argument_properties, - metadata={ - "tool_type": resource.type.lower(), - "display_name": tool_name, - "args_schema": input_model, - "output_schema": output_model, - }, + return build_pims_read_tool( + resource, + path_template=PIMS_EXECUTION_TRACE_PATH, + subject="execution trace", ) diff --git a/tests/agent/tools/internal_tools/test_get_case_entity_tool.py b/tests/agent/tools/internal_tools/test_get_case_entity_tool.py index 4ca16c2f6..fc8020ff4 100644 --- a/tests/agent/tools/internal_tools/test_get_case_entity_tool.py +++ b/tests/agent/tools/internal_tools/test_get_case_entity_tool.py @@ -58,10 +58,10 @@ def _mock_uipath_response(mock_uipath_class, payload): return mock_uipath @patch( - "uipath_langchain.agent.tools.internal_tools.get_case_entity_tool.mockable", + "uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.mockable", lambda **kwargs: lambda f: f, ) - @patch("uipath_langchain.agent.tools.internal_tools.get_case_entity_tool.UiPath") + @patch("uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.UiPath") async def test_explicit_folder_key_sends_header( self, mock_uipath_class, resource_config, mock_llm ): @@ -80,10 +80,10 @@ async def test_explicit_folder_key_sends_header( assert kwargs == {"headers": {"x-uipath-folderkey": "folder-xyz"}} @patch( - "uipath_langchain.agent.tools.internal_tools.get_case_entity_tool.mockable", + "uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.mockable", lambda **kwargs: lambda f: f, ) - @patch("uipath_langchain.agent.tools.internal_tools.get_case_entity_tool.UiPath") + @patch("uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.UiPath") async def test_falls_back_to_runtime_folder_context( self, mock_uipath_class, resource_config, mock_llm ): @@ -101,10 +101,10 @@ async def test_falls_back_to_runtime_folder_context( assert kwargs == {"include_folder_headers": True} @patch( - "uipath_langchain.agent.tools.internal_tools.get_case_entity_tool.mockable", + "uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.mockable", lambda **kwargs: lambda f: f, ) - @patch("uipath_langchain.agent.tools.internal_tools.get_case_entity_tool.UiPath") + @patch("uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.UiPath") async def test_missing_instance_id_raises( self, mock_uipath_class, resource_config, mock_llm ): @@ -116,10 +116,10 @@ async def test_missing_instance_id_raises( await tool.coroutine(folderKey="folder-xyz") @patch( - "uipath_langchain.agent.tools.internal_tools.get_case_entity_tool.mockable", + "uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.mockable", lambda **kwargs: lambda f: f, ) - @patch("uipath_langchain.agent.tools.internal_tools.get_case_entity_tool.UiPath") + @patch("uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.UiPath") async def test_404_maps_to_agent_runtime_error( self, mock_uipath_class, diff --git a/tests/agent/tools/internal_tools/test_get_case_plan_tool.py b/tests/agent/tools/internal_tools/test_get_case_plan_tool.py index 758bc165f..91b73dab7 100644 --- a/tests/agent/tools/internal_tools/test_get_case_plan_tool.py +++ b/tests/agent/tools/internal_tools/test_get_case_plan_tool.py @@ -61,10 +61,10 @@ def _mock_uipath_response(mock_uipath_class, payload): return mock_uipath @patch( - "uipath_langchain.agent.tools.internal_tools.get_case_plan_tool.mockable", + "uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.mockable", lambda **kwargs: lambda f: f, ) - @patch("uipath_langchain.agent.tools.internal_tools.get_case_plan_tool.UiPath") + @patch("uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.UiPath") async def test_explicit_folder_key_sends_header( self, mock_uipath_class, resource_config, mock_llm ): @@ -84,10 +84,10 @@ async def test_explicit_folder_key_sends_header( assert kwargs == {"headers": {"x-uipath-folderkey": "folder-xyz"}} @patch( - "uipath_langchain.agent.tools.internal_tools.get_case_plan_tool.mockable", + "uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.mockable", lambda **kwargs: lambda f: f, ) - @patch("uipath_langchain.agent.tools.internal_tools.get_case_plan_tool.UiPath") + @patch("uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.UiPath") async def test_falls_back_to_runtime_folder_context( self, mock_uipath_class, resource_config, mock_llm ): @@ -109,10 +109,10 @@ async def test_falls_back_to_runtime_folder_context( assert kwargs == {"include_folder_headers": True} @patch( - "uipath_langchain.agent.tools.internal_tools.get_case_plan_tool.mockable", + "uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.mockable", lambda **kwargs: lambda f: f, ) - @patch("uipath_langchain.agent.tools.internal_tools.get_case_plan_tool.UiPath") + @patch("uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.UiPath") async def test_missing_instance_id_raises( self, mock_uipath_class, resource_config, mock_llm ): @@ -124,10 +124,10 @@ async def test_missing_instance_id_raises( await tool.coroutine(folderKey="folder-xyz") @patch( - "uipath_langchain.agent.tools.internal_tools.get_case_plan_tool.mockable", + "uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.mockable", lambda **kwargs: lambda f: f, ) - @patch("uipath_langchain.agent.tools.internal_tools.get_case_plan_tool.UiPath") + @patch("uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.UiPath") async def test_404_maps_to_agent_runtime_error( self, mock_uipath_class, diff --git a/tests/agent/tools/internal_tools/test_get_case_state_tool.py b/tests/agent/tools/internal_tools/test_get_case_state_tool.py index edfa292c5..f984b2210 100644 --- a/tests/agent/tools/internal_tools/test_get_case_state_tool.py +++ b/tests/agent/tools/internal_tools/test_get_case_state_tool.py @@ -59,10 +59,10 @@ def _mock_uipath_response(mock_uipath_class, payload): return mock_uipath @patch( - "uipath_langchain.agent.tools.internal_tools.get_case_state_tool.mockable", + "uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.mockable", lambda **kwargs: lambda f: f, ) - @patch("uipath_langchain.agent.tools.internal_tools.get_case_state_tool.UiPath") + @patch("uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.UiPath") async def test_explicit_folder_key_sends_header( self, mock_uipath_class, resource_config, mock_llm ): @@ -81,10 +81,10 @@ async def test_explicit_folder_key_sends_header( assert kwargs == {"headers": {"x-uipath-folderkey": "folder-xyz"}} @patch( - "uipath_langchain.agent.tools.internal_tools.get_case_state_tool.mockable", + "uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.mockable", lambda **kwargs: lambda f: f, ) - @patch("uipath_langchain.agent.tools.internal_tools.get_case_state_tool.UiPath") + @patch("uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.UiPath") async def test_falls_back_to_runtime_folder_context( self, mock_uipath_class, resource_config, mock_llm ): @@ -102,10 +102,10 @@ async def test_falls_back_to_runtime_folder_context( assert kwargs == {"include_folder_headers": True} @patch( - "uipath_langchain.agent.tools.internal_tools.get_case_state_tool.mockable", + "uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.mockable", lambda **kwargs: lambda f: f, ) - @patch("uipath_langchain.agent.tools.internal_tools.get_case_state_tool.UiPath") + @patch("uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.UiPath") async def test_missing_instance_id_raises( self, mock_uipath_class, resource_config, mock_llm ): @@ -117,10 +117,10 @@ async def test_missing_instance_id_raises( await tool.coroutine(folderKey="folder-xyz") @patch( - "uipath_langchain.agent.tools.internal_tools.get_case_state_tool.mockable", + "uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.mockable", lambda **kwargs: lambda f: f, ) - @patch("uipath_langchain.agent.tools.internal_tools.get_case_state_tool.UiPath") + @patch("uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.UiPath") async def test_404_maps_to_agent_runtime_error( self, mock_uipath_class, diff --git a/tests/agent/tools/internal_tools/test_get_execution_trace_tool.py b/tests/agent/tools/internal_tools/test_get_execution_trace_tool.py index fb641f523..ef40177bd 100644 --- a/tests/agent/tools/internal_tools/test_get_execution_trace_tool.py +++ b/tests/agent/tools/internal_tools/test_get_execution_trace_tool.py @@ -61,11 +61,11 @@ def _mock_uipath_response(mock_uipath_class, payload): return mock_uipath @patch( - "uipath_langchain.agent.tools.internal_tools.get_execution_trace_tool.mockable", + "uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.mockable", lambda **kwargs: lambda f: f, ) @patch( - "uipath_langchain.agent.tools.internal_tools.get_execution_trace_tool.UiPath" + "uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.UiPath" ) async def test_explicit_folder_key_sends_header( self, mock_uipath_class, resource_config, mock_llm @@ -86,11 +86,11 @@ async def test_explicit_folder_key_sends_header( assert kwargs == {"headers": {"x-uipath-folderkey": "folder-xyz"}} @patch( - "uipath_langchain.agent.tools.internal_tools.get_execution_trace_tool.mockable", + "uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.mockable", lambda **kwargs: lambda f: f, ) @patch( - "uipath_langchain.agent.tools.internal_tools.get_execution_trace_tool.UiPath" + "uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.UiPath" ) async def test_falls_back_to_runtime_folder_context( self, mock_uipath_class, resource_config, mock_llm @@ -113,11 +113,11 @@ async def test_falls_back_to_runtime_folder_context( assert kwargs == {"include_folder_headers": True} @patch( - "uipath_langchain.agent.tools.internal_tools.get_execution_trace_tool.mockable", + "uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.mockable", lambda **kwargs: lambda f: f, ) @patch( - "uipath_langchain.agent.tools.internal_tools.get_execution_trace_tool.UiPath" + "uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.UiPath" ) async def test_missing_instance_id_raises( self, mock_uipath_class, resource_config, mock_llm @@ -130,11 +130,11 @@ async def test_missing_instance_id_raises( await tool.coroutine(folderKey="folder-xyz") @patch( - "uipath_langchain.agent.tools.internal_tools.get_execution_trace_tool.mockable", + "uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.mockable", lambda **kwargs: lambda f: f, ) @patch( - "uipath_langchain.agent.tools.internal_tools.get_execution_trace_tool.UiPath" + "uipath_langchain.agent.tools.internal_tools._pims_read_tool_factory.UiPath" ) async def test_404_maps_to_agent_runtime_error( self,