From 43cc81cd88ea72c8df1e79e8b6754aafbd8c480c Mon Sep 17 00:00:00 2001 From: Sakshar Thakkar Date: Tue, 30 Dec 2025 16:38:24 -0800 Subject: [PATCH 1/8] feat: add observability metadata to tools and guardrails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add metadata to tools and guardrails for observability span detection. Changes: - escalation_tool.py: Add metadata (tool_type, display_name, channel_type, assignee) - process_tool.py: Add metadata (tool_type, display_name, folder_path) - guardrail_nodes.py: Wrap in RunnableCallable with metadata 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../agent/guardrails/guardrail_nodes.py | 32 +++++++++++++++---- .../agent/tools/escalation_tool.py | 6 ++++ .../agent/tools/process_tool.py | 5 +++ 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/uipath_langchain/agent/guardrails/guardrail_nodes.py b/src/uipath_langchain/agent/guardrails/guardrail_nodes.py index afc95c60..03700592 100644 --- a/src/uipath_langchain/agent/guardrails/guardrail_nodes.py +++ b/src/uipath_langchain/agent/guardrails/guardrail_nodes.py @@ -3,6 +3,7 @@ import re from typing import Any, Callable +from langgraph.utils.runnable import RunnableCallable from langgraph.types import Command from uipath.core.guardrails import ( DeterministicGuardrail, @@ -118,10 +119,11 @@ def _create_guardrail_node( | None = None, output_data_extractor: Callable[[AgentGuardrailsGraphState], dict[str, Any]] | None = None, -) -> tuple[str, Callable[[AgentGuardrailsGraphState], Any]]: + tool_name: str | None = None, +) -> tuple[str, RunnableCallable]: """Private factory for guardrail evaluation nodes. - Returns a node that evaluates the guardrail and routes via Command: + Returns a node wrapped in RunnableCallable with observability metadata: - goto success_node on validation pass - goto failure_node on validation fail """ @@ -177,7 +179,22 @@ async def node( ) raise - return node_name, node + # Wrap in RunnableCallable with observability metadata + wrapped_node = RunnableCallable( + func=None, + afunc=node, + name=node_name, + metadata={ + "guardrail_type": "evaluation", + "guardrail_name": guardrail.name, + "guardrail_description": getattr(guardrail, "description", None), + "guardrail_scope": scope.value, + "guardrail_stage": execution_stage.value, + "tool_name": tool_name, + }, + ) + + return node_name, wrapped_node def create_llm_guardrail_node( @@ -185,7 +202,7 @@ def create_llm_guardrail_node( execution_stage: ExecutionStage, success_node: str, failure_node: str, -) -> tuple[str, Callable[[AgentGuardrailsGraphState], Any]]: +) -> tuple[str, RunnableCallable]: def _payload_generator(state: AgentGuardrailsGraphState) -> str: if not state.messages: return "" @@ -210,7 +227,7 @@ def create_agent_init_guardrail_node( execution_stage: ExecutionStage, success_node: str, failure_node: str, -) -> tuple[str, Callable[[AgentGuardrailsGraphState], Any]]: +) -> tuple[str, RunnableCallable]: def _payload_generator(state: AgentGuardrailsGraphState) -> str: if not state.messages: return "" @@ -231,7 +248,7 @@ def create_agent_terminate_guardrail_node( execution_stage: ExecutionStage, success_node: str, failure_node: str, -) -> tuple[str, Callable[[AgentGuardrailsGraphState], Any]]: +) -> tuple[str, RunnableCallable]: def _payload_generator(state: AgentGuardrailsGraphState) -> str: return str(state.agent_result) @@ -251,7 +268,7 @@ def create_tool_guardrail_node( success_node: str, failure_node: str, tool_name: str, -) -> tuple[str, Callable[[AgentGuardrailsGraphState], Any]]: +) -> tuple[str, RunnableCallable]: """Create a guardrail node for TOOL scope guardrails. Args: @@ -310,4 +327,5 @@ def _output_data_extractor(state: AgentGuardrailsGraphState) -> dict[str, Any]: failure_node, _input_data_extractor, _output_data_extractor, + tool_name, ) diff --git a/src/uipath_langchain/agent/tools/escalation_tool.py b/src/uipath_langchain/agent/tools/escalation_tool.py index 9f4798c2..9774787b 100644 --- a/src/uipath_langchain/agent/tools/escalation_tool.py +++ b/src/uipath_langchain/agent/tools/escalation_tool.py @@ -108,6 +108,12 @@ async def escalation_tool_fn( description=resource.description, args_schema=input_model, coroutine=escalation_tool_fn, + metadata={ + "tool_type": "escalation", + "display_name": channel.properties.app_name, + "channel_type": "actionCenter", + "assignee": assignee, + }, ) return tool diff --git a/src/uipath_langchain/agent/tools/process_tool.py b/src/uipath_langchain/agent/tools/process_tool.py index 818d19d8..621282fd 100644 --- a/src/uipath_langchain/agent/tools/process_tool.py +++ b/src/uipath_langchain/agent/tools/process_tool.py @@ -46,6 +46,11 @@ async def process_tool_fn(**kwargs: Any): args_schema=input_model, coroutine=process_tool_fn, output_type=output_model, + metadata={ + "tool_type": "process", + "display_name": process_name, + "folder_path": folder_path, + }, ) return tool From e6179e5a1c6bb6c9567ae3f487df8fb0c78f5d63 Mon Sep 17 00:00:00 2001 From: Sakshar Thakkar Date: Tue, 30 Dec 2025 16:44:17 -0800 Subject: [PATCH 2/8] fix: use internal import for RunnableCallable to satisfy mypy --- src/uipath_langchain/agent/guardrails/guardrail_nodes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uipath_langchain/agent/guardrails/guardrail_nodes.py b/src/uipath_langchain/agent/guardrails/guardrail_nodes.py index 03700592..a01d5baa 100644 --- a/src/uipath_langchain/agent/guardrails/guardrail_nodes.py +++ b/src/uipath_langchain/agent/guardrails/guardrail_nodes.py @@ -3,7 +3,7 @@ import re from typing import Any, Callable -from langgraph.utils.runnable import RunnableCallable +from langgraph._internal._runnable import RunnableCallable from langgraph.types import Command from uipath.core.guardrails import ( DeterministicGuardrail, From 56ff83359847610bb9d01e3c1a164d953645298e Mon Sep 17 00:00:00 2001 From: Sakshar Thakkar Date: Tue, 30 Dec 2025 16:51:06 -0800 Subject: [PATCH 3/8] fix: use function attribute for guardrail metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RunnableCallable passes metadata as kwargs which breaks node invocation. Use __metadata__ attribute on function instead, which callbacks can read. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../agent/guardrails/guardrail_nodes.py | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/src/uipath_langchain/agent/guardrails/guardrail_nodes.py b/src/uipath_langchain/agent/guardrails/guardrail_nodes.py index a01d5baa..8c573ee4 100644 --- a/src/uipath_langchain/agent/guardrails/guardrail_nodes.py +++ b/src/uipath_langchain/agent/guardrails/guardrail_nodes.py @@ -3,7 +3,6 @@ import re from typing import Any, Callable -from langgraph._internal._runnable import RunnableCallable from langgraph.types import Command from uipath.core.guardrails import ( DeterministicGuardrail, @@ -120,10 +119,10 @@ def _create_guardrail_node( output_data_extractor: Callable[[AgentGuardrailsGraphState], dict[str, Any]] | None = None, tool_name: str | None = None, -) -> tuple[str, RunnableCallable]: +) -> tuple[str, Callable[[AgentGuardrailsGraphState], Any]]: """Private factory for guardrail evaluation nodes. - Returns a node wrapped in RunnableCallable with observability metadata: + Returns a node with observability metadata attached as __metadata__ attribute: - goto success_node on validation pass - goto failure_node on validation fail """ @@ -179,22 +178,17 @@ async def node( ) raise - # Wrap in RunnableCallable with observability metadata - wrapped_node = RunnableCallable( - func=None, - afunc=node, - name=node_name, - metadata={ - "guardrail_type": "evaluation", - "guardrail_name": guardrail.name, - "guardrail_description": getattr(guardrail, "description", None), - "guardrail_scope": scope.value, - "guardrail_stage": execution_stage.value, - "tool_name": tool_name, - }, - ) + # Attach observability metadata as function attribute + node.__metadata__ = { # type: ignore[attr-defined] + "guardrail_type": "evaluation", + "guardrail_name": guardrail.name, + "guardrail_description": getattr(guardrail, "description", None), + "guardrail_scope": scope.value, + "guardrail_stage": execution_stage.value, + "tool_name": tool_name, + } - return node_name, wrapped_node + return node_name, node def create_llm_guardrail_node( @@ -202,7 +196,7 @@ def create_llm_guardrail_node( execution_stage: ExecutionStage, success_node: str, failure_node: str, -) -> tuple[str, RunnableCallable]: +) -> tuple[str, Callable[[AgentGuardrailsGraphState], Any]]: def _payload_generator(state: AgentGuardrailsGraphState) -> str: if not state.messages: return "" @@ -227,7 +221,7 @@ def create_agent_init_guardrail_node( execution_stage: ExecutionStage, success_node: str, failure_node: str, -) -> tuple[str, RunnableCallable]: +) -> tuple[str, Callable[[AgentGuardrailsGraphState], Any]]: def _payload_generator(state: AgentGuardrailsGraphState) -> str: if not state.messages: return "" @@ -248,7 +242,7 @@ def create_agent_terminate_guardrail_node( execution_stage: ExecutionStage, success_node: str, failure_node: str, -) -> tuple[str, RunnableCallable]: +) -> tuple[str, Callable[[AgentGuardrailsGraphState], Any]]: def _payload_generator(state: AgentGuardrailsGraphState) -> str: return str(state.agent_result) @@ -268,7 +262,7 @@ def create_tool_guardrail_node( success_node: str, failure_node: str, tool_name: str, -) -> tuple[str, RunnableCallable]: +) -> tuple[str, Callable[[AgentGuardrailsGraphState], Any]]: """Create a guardrail node for TOOL scope guardrails. Args: From eaeb1fd112d6f03ed1b4bb2a16a4d8a014b45017 Mon Sep 17 00:00:00 2001 From: Sakshar Thakkar Date: Tue, 30 Dec 2025 16:56:07 -0800 Subject: [PATCH 4/8] fix: use channel.type instead of hardcoded actionCenter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/uipath_langchain/agent/tools/escalation_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uipath_langchain/agent/tools/escalation_tool.py b/src/uipath_langchain/agent/tools/escalation_tool.py index 9774787b..95e38c39 100644 --- a/src/uipath_langchain/agent/tools/escalation_tool.py +++ b/src/uipath_langchain/agent/tools/escalation_tool.py @@ -111,7 +111,7 @@ async def escalation_tool_fn( metadata={ "tool_type": "escalation", "display_name": channel.properties.app_name, - "channel_type": "actionCenter", + "channel_type": channel.type, "assignee": assignee, }, ) From 150c8bcfdb13245b831c911aab4e203eda33b370 Mon Sep 17 00:00:00 2001 From: Sakshar Thakkar Date: Tue, 30 Dec 2025 17:07:44 -0800 Subject: [PATCH 5/8] refactor: remove guardrail_type from metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Can be inferred from guardrail_name presence. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/uipath_langchain/agent/guardrails/guardrail_nodes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/uipath_langchain/agent/guardrails/guardrail_nodes.py b/src/uipath_langchain/agent/guardrails/guardrail_nodes.py index 8c573ee4..dc843d07 100644 --- a/src/uipath_langchain/agent/guardrails/guardrail_nodes.py +++ b/src/uipath_langchain/agent/guardrails/guardrail_nodes.py @@ -180,7 +180,6 @@ async def node( # Attach observability metadata as function attribute node.__metadata__ = { # type: ignore[attr-defined] - "guardrail_type": "evaluation", "guardrail_name": guardrail.name, "guardrail_description": getattr(guardrail, "description", None), "guardrail_scope": scope.value, From 84af6fdf8eb1d77b12f067d249102f91fca1ebb4 Mon Sep 17 00:00:00 2001 From: Sakshar Thakkar Date: Tue, 30 Dec 2025 17:16:11 -0800 Subject: [PATCH 6/8] test: add metadata unit tests for observability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add test_process_tool.py for process tool metadata - Add test_escalation_tool.py for escalation tool metadata - Add TestGuardrailNodeMetadata to guardrail node tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../agent/guardrails/test_guardrail_nodes.py | 69 ++++++++++++ tests/agent/tools/test_escalation_tool.py | 106 ++++++++++++++++++ tests/agent/tools/test_process_tool.py | 54 +++++++++ 3 files changed, 229 insertions(+) create mode 100644 tests/agent/tools/test_escalation_tool.py create mode 100644 tests/agent/tools/test_process_tool.py diff --git a/tests/agent/guardrails/test_guardrail_nodes.py b/tests/agent/guardrails/test_guardrail_nodes.py index 6be36d81..60f54989 100644 --- a/tests/agent/guardrails/test_guardrail_nodes.py +++ b/tests/agent/guardrails/test_guardrail_nodes.py @@ -509,3 +509,72 @@ async def test_unsupported_guardrail_type_raises_error(self): assert "MagicMock" in error_message assert "DeterministicGuardrail" in error_message assert "BuiltInValidatorGuardrail" in error_message + + +class TestGuardrailNodeMetadata: + """Tests for guardrail node __metadata__ attribute for observability.""" + + def test_llm_guardrail_node_has_metadata(self): + """Test that LLM guardrail node has __metadata__ attribute.""" + guardrail = MagicMock(spec=BuiltInValidatorGuardrail) + guardrail.name = "TestGuardrail" + guardrail.description = "Test description" + + _, node = create_llm_guardrail_node( + guardrail=guardrail, + execution_stage=ExecutionStage.PRE_EXECUTION, + success_node="ok", + failure_node="nope", + ) + + assert hasattr(node, "__metadata__") + assert isinstance(node.__metadata__, dict) + + def test_llm_guardrail_node_metadata_fields(self): + """Test that LLM guardrail node has correct metadata fields.""" + guardrail = MagicMock(spec=BuiltInValidatorGuardrail) + guardrail.name = "TestGuardrail" + guardrail.description = "Test description" + + _, node = create_llm_guardrail_node( + guardrail=guardrail, + execution_stage=ExecutionStage.PRE_EXECUTION, + success_node="ok", + failure_node="nope", + ) + + assert node.__metadata__["guardrail_name"] == "TestGuardrail" + assert node.__metadata__["guardrail_scope"] == "Llm" + assert node.__metadata__["guardrail_stage"] == "preExecution" + assert node.__metadata__["tool_name"] is None + + def test_tool_guardrail_node_has_tool_name(self): + """Test that TOOL scope guardrail has tool_name in metadata.""" + guardrail = MagicMock(spec=BuiltInValidatorGuardrail) + guardrail.name = "TestGuardrail" + + _, node = create_tool_guardrail_node( + guardrail=guardrail, + execution_stage=ExecutionStage.PRE_EXECUTION, + success_node="ok", + failure_node="nope", + tool_name="my_tool", + ) + + assert node.__metadata__["guardrail_scope"] == "Tool" + assert node.__metadata__["tool_name"] == "my_tool" + + def test_agent_init_guardrail_node_metadata(self): + """Test that AGENT init guardrail has correct scope in metadata.""" + guardrail = MagicMock(spec=BuiltInValidatorGuardrail) + guardrail.name = "TestGuardrail" + + _, node = create_agent_init_guardrail_node( + guardrail=guardrail, + execution_stage=ExecutionStage.POST_EXECUTION, + success_node="ok", + failure_node="nope", + ) + + assert node.__metadata__["guardrail_scope"] == "Agent" + assert node.__metadata__["guardrail_stage"] == "postExecution" diff --git a/tests/agent/tools/test_escalation_tool.py b/tests/agent/tools/test_escalation_tool.py new file mode 100644 index 00000000..dcb79332 --- /dev/null +++ b/tests/agent/tools/test_escalation_tool.py @@ -0,0 +1,106 @@ +"""Tests for escalation_tool.py metadata.""" + +import pytest +from uipath.agent.models.agent import ( + AgentEscalationChannel, + AgentEscalationChannelProperties, + AgentEscalationRecipientType, + AgentEscalationResourceConfig, + StandardRecipient, +) + +from uipath_langchain.agent.tools.escalation_tool import create_escalation_tool + + +class TestEscalationToolMetadata: + """Test that escalation tool has correct metadata for observability.""" + + @pytest.fixture + def escalation_resource(self): + """Create a minimal escalation tool resource config.""" + return AgentEscalationResourceConfig( + name="approval", + description="Request approval", + channels=[ + AgentEscalationChannel( + name="action_center", + type="actionCenter", + description="Action Center channel", + input_schema={"type": "object", "properties": {}}, + output_schema={"type": "object", "properties": {}}, + properties=AgentEscalationChannelProperties( + app_name="ApprovalApp", + app_version=1, + resource_key="test-key", + ), + recipients=[ + StandardRecipient( + type=AgentEscalationRecipientType.USER_EMAIL, + value="user@example.com", + ) + ], + ) + ], + ) + + @pytest.fixture + def escalation_resource_no_recipient(self): + """Create escalation resource without recipients.""" + return AgentEscalationResourceConfig( + name="approval", + description="Request approval", + channels=[ + AgentEscalationChannel( + name="action_center", + type="actionCenter", + description="Action Center channel", + input_schema={"type": "object", "properties": {}}, + output_schema={"type": "object", "properties": {}}, + properties=AgentEscalationChannelProperties( + app_name="ApprovalApp", + app_version=1, + resource_key="test-key", + ), + recipients=[], + ) + ], + ) + + def test_escalation_tool_has_metadata(self, escalation_resource): + """Test that escalation tool has metadata dict.""" + tool = create_escalation_tool(escalation_resource) + + assert tool.metadata is not None + assert isinstance(tool.metadata, dict) + + def test_escalation_tool_metadata_has_tool_type(self, escalation_resource): + """Test that metadata contains tool_type for span detection.""" + tool = create_escalation_tool(escalation_resource) + + assert tool.metadata["tool_type"] == "escalation" + + def test_escalation_tool_metadata_has_display_name(self, escalation_resource): + """Test that metadata contains display_name from app_name.""" + tool = create_escalation_tool(escalation_resource) + + assert tool.metadata["display_name"] == "ApprovalApp" + + def test_escalation_tool_metadata_has_channel_type(self, escalation_resource): + """Test that metadata contains channel_type for span attributes.""" + tool = create_escalation_tool(escalation_resource) + + assert tool.metadata["channel_type"] == "actionCenter" + + def test_escalation_tool_metadata_has_assignee(self, escalation_resource): + """Test that metadata contains assignee when recipient is USER_EMAIL.""" + tool = create_escalation_tool(escalation_resource) + + assert tool.metadata["assignee"] == "user@example.com" + + def test_escalation_tool_metadata_assignee_none_when_no_recipients( + self, escalation_resource_no_recipient + ): + """Test that assignee is None when no recipients configured.""" + tool = create_escalation_tool(escalation_resource_no_recipient) + + assert tool.metadata["assignee"] is None diff --git a/tests/agent/tools/test_process_tool.py b/tests/agent/tools/test_process_tool.py new file mode 100644 index 00000000..4783c28a --- /dev/null +++ b/tests/agent/tools/test_process_tool.py @@ -0,0 +1,54 @@ +"""Tests for process_tool.py metadata.""" + +import pytest +from uipath.agent.models.agent import ( + AgentProcessToolProperties, + AgentProcessToolResourceConfig, + AgentToolType, +) + +from uipath_langchain.agent.tools.process_tool import create_process_tool + + +class TestProcessToolMetadata: + """Test that process tool has correct metadata for observability.""" + + @pytest.fixture + def process_resource(self): + """Create a minimal process tool resource config.""" + return AgentProcessToolResourceConfig( + type=AgentToolType.PROCESS, + name="test_process", + description="Test process description", + input_schema={"type": "object", "properties": {}}, + output_schema={"type": "object", "properties": {}}, + properties=AgentProcessToolProperties( + process_name="MyProcess", + folder_path="/Shared/MyFolder", + ), + ) + + def test_process_tool_has_metadata(self, process_resource): + """Test that process tool has metadata dict.""" + tool = create_process_tool(process_resource) + + assert tool.metadata is not None + assert isinstance(tool.metadata, dict) + + def test_process_tool_metadata_has_tool_type(self, process_resource): + """Test that metadata contains tool_type for span detection.""" + tool = create_process_tool(process_resource) + + assert tool.metadata["tool_type"] == "process" + + def test_process_tool_metadata_has_display_name(self, process_resource): + """Test that metadata contains display_name from process_name.""" + tool = create_process_tool(process_resource) + + assert tool.metadata["display_name"] == "MyProcess" + + def test_process_tool_metadata_has_folder_path(self, process_resource): + """Test that metadata contains folder_path for span attributes.""" + tool = create_process_tool(process_resource) + + assert tool.metadata["folder_path"] == "/Shared/MyFolder" From 6d591df9bb36caec11e5cedae27aee98b66e5452 Mon Sep 17 00:00:00 2001 From: Sakshar Thakkar Date: Tue, 30 Dec 2025 17:45:50 -0800 Subject: [PATCH 7/8] fix: resolve mypy errors in metadata tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use getattr() for __metadata__ access (avoids type:ignore) - Add assert metadata is not None before indexing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../agent/guardrails/test_guardrail_nodes.py | 22 ++++++++++++------- tests/agent/tools/test_escalation_tool.py | 10 ++++----- tests/agent/tools/test_process_tool.py | 6 ++--- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/tests/agent/guardrails/test_guardrail_nodes.py b/tests/agent/guardrails/test_guardrail_nodes.py index 60f54989..a2400d2e 100644 --- a/tests/agent/guardrails/test_guardrail_nodes.py +++ b/tests/agent/guardrails/test_guardrail_nodes.py @@ -543,10 +543,12 @@ def test_llm_guardrail_node_metadata_fields(self): failure_node="nope", ) - assert node.__metadata__["guardrail_name"] == "TestGuardrail" - assert node.__metadata__["guardrail_scope"] == "Llm" - assert node.__metadata__["guardrail_stage"] == "preExecution" - assert node.__metadata__["tool_name"] is None + metadata = getattr(node, "__metadata__", None) + assert metadata is not None + assert metadata["guardrail_name"] == "TestGuardrail" + assert metadata["guardrail_scope"] == "Llm" + assert metadata["guardrail_stage"] == "preExecution" + assert metadata["tool_name"] is None def test_tool_guardrail_node_has_tool_name(self): """Test that TOOL scope guardrail has tool_name in metadata.""" @@ -561,8 +563,10 @@ def test_tool_guardrail_node_has_tool_name(self): tool_name="my_tool", ) - assert node.__metadata__["guardrail_scope"] == "Tool" - assert node.__metadata__["tool_name"] == "my_tool" + metadata = getattr(node, "__metadata__", None) + assert metadata is not None + assert metadata["guardrail_scope"] == "Tool" + assert metadata["tool_name"] == "my_tool" def test_agent_init_guardrail_node_metadata(self): """Test that AGENT init guardrail has correct scope in metadata.""" @@ -576,5 +580,7 @@ def test_agent_init_guardrail_node_metadata(self): failure_node="nope", ) - assert node.__metadata__["guardrail_scope"] == "Agent" - assert node.__metadata__["guardrail_stage"] == "postExecution" + metadata = getattr(node, "__metadata__", None) + assert metadata is not None + assert metadata["guardrail_scope"] == "Agent" + assert metadata["guardrail_stage"] == "postExecution" diff --git a/tests/agent/tools/test_escalation_tool.py b/tests/agent/tools/test_escalation_tool.py index dcb79332..bd18fc14 100644 --- a/tests/agent/tools/test_escalation_tool.py +++ b/tests/agent/tools/test_escalation_tool.py @@ -76,25 +76,25 @@ def test_escalation_tool_has_metadata(self, escalation_resource): def test_escalation_tool_metadata_has_tool_type(self, escalation_resource): """Test that metadata contains tool_type for span detection.""" tool = create_escalation_tool(escalation_resource) - + assert tool.metadata is not None assert tool.metadata["tool_type"] == "escalation" def test_escalation_tool_metadata_has_display_name(self, escalation_resource): """Test that metadata contains display_name from app_name.""" tool = create_escalation_tool(escalation_resource) - + assert tool.metadata is not None assert tool.metadata["display_name"] == "ApprovalApp" def test_escalation_tool_metadata_has_channel_type(self, escalation_resource): """Test that metadata contains channel_type for span attributes.""" tool = create_escalation_tool(escalation_resource) - + assert tool.metadata is not None assert tool.metadata["channel_type"] == "actionCenter" def test_escalation_tool_metadata_has_assignee(self, escalation_resource): """Test that metadata contains assignee when recipient is USER_EMAIL.""" tool = create_escalation_tool(escalation_resource) - + assert tool.metadata is not None assert tool.metadata["assignee"] == "user@example.com" def test_escalation_tool_metadata_assignee_none_when_no_recipients( @@ -102,5 +102,5 @@ def test_escalation_tool_metadata_assignee_none_when_no_recipients( ): """Test that assignee is None when no recipients configured.""" tool = create_escalation_tool(escalation_resource_no_recipient) - + assert tool.metadata is not None assert tool.metadata["assignee"] is None diff --git a/tests/agent/tools/test_process_tool.py b/tests/agent/tools/test_process_tool.py index 4783c28a..fd786b9e 100644 --- a/tests/agent/tools/test_process_tool.py +++ b/tests/agent/tools/test_process_tool.py @@ -38,17 +38,17 @@ def test_process_tool_has_metadata(self, process_resource): def test_process_tool_metadata_has_tool_type(self, process_resource): """Test that metadata contains tool_type for span detection.""" tool = create_process_tool(process_resource) - + assert tool.metadata is not None assert tool.metadata["tool_type"] == "process" def test_process_tool_metadata_has_display_name(self, process_resource): """Test that metadata contains display_name from process_name.""" tool = create_process_tool(process_resource) - + assert tool.metadata is not None assert tool.metadata["display_name"] == "MyProcess" def test_process_tool_metadata_has_folder_path(self, process_resource): """Test that metadata contains folder_path for span attributes.""" tool = create_process_tool(process_resource) - + assert tool.metadata is not None assert tool.metadata["folder_path"] == "/Shared/MyFolder" From 5574928020e0fa593970f849b2e64824e9c561bc Mon Sep 17 00:00:00 2001 From: Sakshar Thakkar Date: Tue, 30 Dec 2025 18:10:17 -0800 Subject: [PATCH 8/8] chore: bump version to 0.2.4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8aa8fceb..e5865141 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-langchain" -version = "0.2.3" +version = "0.2.4" description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/uv.lock b/uv.lock index 426c9c22..da64875c 100644 --- a/uv.lock +++ b/uv.lock @@ -3282,7 +3282,7 @@ wheels = [ [[package]] name = "uipath-langchain" -version = "0.2.3" +version = "0.2.4" source = { editable = "." } dependencies = [ { name = "aiosqlite" },