From 0dd9e2e62a27ca1dacb269dba1f5c7efa66f507b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 27 Feb 2026 15:06:21 +0000 Subject: [PATCH 1/2] refactor: address PR review comments for integration tools static args 1. Unify regex: reuse compiled _TEMPLATE_PATTERN (with capturing group) instead of inline re.fullmatch for template matching consistency. 2. Extract enum-stripping to schema_editing.py: add strip_matching_enums() that reuses _navigate_schema_inlining_refs, removing duplicate _resolve_schema_ref and _navigate_schema_to_field from integration_tool.py. 3. Remove integration-specific handling from static_args: delete resolve_integration_static_args() and the AgentIntegrationToolResourceConfig branch in resolve_static_args(), since integration tools now use argument_properties uniformly via StructuredToolWithArgumentProperties. https://claude.ai/code/session_01NJvRhU5bJ3GweCdNhhgidg --- .../agent/tools/integration_tool.py | 69 +---- .../agent/tools/schema_editing.py | 32 +++ .../agent/tools/static_args.py | 62 +---- tests/agent/tools/test_integration_tool.py | 3 +- tests/agent/tools/test_static_args.py | 235 +----------------- 5 files changed, 56 insertions(+), 345 deletions(-) diff --git a/src/uipath_langchain/agent/tools/integration_tool.py b/src/uipath_langchain/agent/tools/integration_tool.py index 05024e3a1..ac86bb6fc 100644 --- a/src/uipath_langchain/agent/tools/integration_tool.py +++ b/src/uipath_langchain/agent/tools/integration_tool.py @@ -27,6 +27,7 @@ ToolWrapperReturnType, ) +from .schema_editing import strip_matching_enums from .structured_tool_with_argument_properties import ( StructuredToolWithArgumentProperties, ) @@ -62,7 +63,7 @@ def convert_integration_parameters_to_argument_properties( ) elif param.field_variant == "argument": value_str = str(param.value) if param.value is not None else "" - match = re.fullmatch(r"\{\{(.+?)\}\}", value_str) + match = _TEMPLATE_PATTERN.match(value_str) if not match: raise AgentStartupError( code=AgentStartupErrorCode.INVALID_TOOL_CONFIG, @@ -81,7 +82,7 @@ def convert_integration_parameters_to_argument_properties( return result -_TEMPLATE_PATTERN = re.compile(r"^\{\{.*\}\}$") +_TEMPLATE_PATTERN = re.compile(r"^\{\{(.+?)\}\}$") def _param_name_to_segments(param_name: str) -> list[str]: @@ -133,52 +134,6 @@ def _is_param_name_to_jsonpath(param_name: str) -> str: return "$" + "".join(parts) -def _resolve_schema_ref( - root_schema: dict[str, Any], node: dict[str, Any] -) -> dict[str, Any]: - """Resolve a $ref pointer in a JSON schema node. - - Returns the referenced definition if $ref is present, - otherwise returns the node unchanged. - """ - ref = node.get("$ref") - if ref is None: - return node - parts = ref.lstrip("#/").split("/") - current = root_schema - for part in parts: - current = current[part] - return current - - -def _navigate_schema_to_field( - root_schema: dict[str, Any], segments: list[str] -) -> dict[str, Any] | None: - """Navigate a JSON schema to a leaf field using parsed path segments. - - Args: - root_schema: The root schema (needed for $ref resolution). - segments: Path segments from _parse_is_param_name. - - Returns: - The schema dict of the leaf field, or None if the path doesn't exist. - """ - current = root_schema - for seg in segments: - current = _resolve_schema_ref(root_schema, current) - if seg == "*": - items = current.get("items") - if items is None: - return None - current = items - else: - props = current.get("properties") - if props is None or seg not in props: - return None - current = props[seg] - return _resolve_schema_ref(root_schema, current) - - def strip_template_enums_from_schema( schema: dict[str, Any], parameters: list[AgentIntegrationToolParameter], @@ -206,21 +161,7 @@ def strip_template_enums_from_schema( continue segments = _param_name_to_segments(param.name) - field_schema = _navigate_schema_to_field(schema, segments) - if field_schema is None: - continue - - enum = field_schema.get("enum") - if enum is None: - continue - - cleaned = [ - v for v in enum if not (isinstance(v, str) and _TEMPLATE_PATTERN.match(v)) - ] - if not cleaned: - del field_schema["enum"] - else: - field_schema["enum"] = cleaned + strip_matching_enums(schema, segments, _TEMPLATE_PATTERN) return schema @@ -392,7 +333,7 @@ async def integration_tool_wrapper( call: ToolCall, state: AgentGraphState, ) -> ToolWrapperReturnType: - call["args"] = handle_static_args(resource, state, call["args"]) + call["args"] = handle_static_args(tool, state, call["args"]) return await tool.ainvoke(call) tool = StructuredToolWithArgumentProperties( diff --git a/src/uipath_langchain/agent/tools/schema_editing.py b/src/uipath_langchain/agent/tools/schema_editing.py index f171ced4c..88b198978 100644 --- a/src/uipath_langchain/agent/tools/schema_editing.py +++ b/src/uipath_langchain/agent/tools/schema_editing.py @@ -2,6 +2,7 @@ import copy import json +import re from typing import Any from jsonpath_ng import ( # type: ignore[import-untyped] @@ -108,6 +109,37 @@ def _navigate_schema_inlining_refs( return current +def strip_matching_enums( + schema: dict[str, Any], + path_segments: list[str], + pattern: re.Pattern[str], +) -> None: + """Navigate to a field in schema and remove enum values matching the pattern. + + If the path doesn't exist in the schema, this is a no-op. + After removing matching values, if the enum becomes empty, the enum key is deleted. + + Args: + schema: The root JSON schema (modified in place). + path_segments: Path segments to navigate to the field. + pattern: Compiled regex pattern to match enum values against. + """ + try: + field_schema = _navigate_schema_inlining_refs(schema, path_segments) + except SchemaModificationError: + return + + enum = field_schema.get("enum") + if enum is None: + return + + cleaned = [v for v in enum if not (isinstance(v, str) and pattern.match(v))] + if not cleaned: + del field_schema["enum"] + else: + field_schema["enum"] = cleaned + + def _apply_sensitive_schema_modification( schema: dict[str, Any], path_parts: list[str], diff --git a/src/uipath_langchain/agent/tools/static_args.py b/src/uipath_langchain/agent/tools/static_args.py index 923ad52ef..7aa71cc04 100644 --- a/src/uipath_langchain/agent/tools/static_args.py +++ b/src/uipath_langchain/agent/tools/static_args.py @@ -8,13 +8,10 @@ from langchain_core.tools import StructuredTool from pydantic import BaseModel from uipath.agent.models.agent import ( - AgentIntegrationToolParameter, - AgentIntegrationToolResourceConfig, AgentToolArgumentArgumentProperties, AgentToolArgumentProperties, AgentToolStaticArgumentProperties, AgentToolTextBuilderArgumentProperties, - BaseAgentResourceConfig, ) from uipath.agent.utils.text_tokens import build_string_from_tokens @@ -137,24 +134,20 @@ def apply_static_argument_properties_to_schema( def resolve_static_args( - resource: BaseAgentResourceConfig, + resource: Any, agent_input: dict[str, Any], ) -> dict[str, Any]: """Resolves static arguments for a given resource with a given input. Args: - resource: The agent resource configuration. - input: The input arguments passed to the agent. + resource: The agent resource configuration or tool with argument_properties. + agent_input: The input arguments passed to the agent. Returns: A dictionary of expanded arguments to be used in the tool call. """ - if isinstance(resource, AgentIntegrationToolResourceConfig): - return resolve_integration_static_args( - resource.properties.parameters, agent_input - ) - elif hasattr(resource, "argument_properties"): + if hasattr(resource, "argument_properties"): # TODO: MCP tools don't inherit from BaseAgentResourceConfig; will need to handle separately static_arguments = _resolve_argument_properties_to_static_arguments( resource.argument_properties, agent_input @@ -167,47 +160,6 @@ def resolve_static_args( return {} # to be implemented for other resource types in the future -def resolve_integration_static_args( - parameters: list[AgentIntegrationToolParameter], - agent_input: dict[str, Any], -) -> dict[str, Any]: - """Resolves static arguments for an integration tool resource. - - Args: - resource: The AgentIntegrationToolResourceConfig instance. - input: The input arguments passed to the agent. - - Returns: - A dictionary of expanded static arguments for the integration tool. - """ - - static_args: dict[str, Any] = {} - for param in parameters: - value = None - - # static parameter, use the defined static value - if param.field_variant == "static": - value = param.value - # argument parameter, extract value from agent input - elif param.field_variant == "argument": - if ( - not isinstance(param.value, str) - or not param.value.startswith("{{") - or not param.value.endswith("}}") - ): - raise ValueError( - f"Parameter value must be in the format '{{argument_name}}' when field_variant is 'argument', got {param.value}" - ) - arg_name = param.value[2:-2].strip() - # currently only support top-level arguments - value = agent_input.get(arg_name) - - if value is not None: - static_args[param.name] = value - - return static_args - - def apply_static_args( static_args: dict[str, Any], kwargs: dict[str, Any], @@ -244,12 +196,12 @@ def apply_static_args( def handle_static_args( - resource: BaseAgentResourceConfig, state: BaseModel, input_args: dict[str, Any] + resource: Any, state: BaseModel, input_args: dict[str, Any] ) -> dict[str, Any]: """Resolves and applies static arguments for a tool call. Args: - resource: The agent resource configuration. - runtime: The tool runtime providing the current state. + resource: The agent resource configuration or tool with argument_properties. + state: The current agent state. input_args: The original input arguments to the tool. Returns: A dictionary of input arguments with static arguments applied. diff --git a/tests/agent/tools/test_integration_tool.py b/tests/agent/tools/test_integration_tool.py index dce652fd8..c5f3f1bd9 100644 --- a/tests/agent/tools/test_integration_tool.py +++ b/tests/agent/tools/test_integration_tool.py @@ -783,7 +783,8 @@ def test_handles_ref_resolution(self): result = strip_template_enums_from_schema(schema, parameters) - config_props = result["definitions"]["Config"]["properties"] + # The $ref is inlined and modified on the inlined copy + config_props = result["properties"]["config"]["properties"] assert config_props["mode"]["enum"] == ["auto"] def test_skips_argument_param_when_schema_path_not_found(self): diff --git a/tests/agent/tools/test_static_args.py b/tests/agent/tools/test_static_args.py index bd3f7e9ab..368e45b29 100644 --- a/tests/agent/tools/test_static_args.py +++ b/tests/agent/tools/test_static_args.py @@ -6,19 +6,14 @@ import pytest from pydantic import BaseModel, Field from uipath.agent.models.agent import ( - AgentIntegrationToolParameter, - AgentIntegrationToolProperties, - AgentIntegrationToolResourceConfig, AgentToolArgumentProperties, AgentToolStaticArgumentProperties, BaseAgentResourceConfig, ) -from uipath.platform.connections import Connection from uipath_langchain.agent.tools.static_args import ( apply_static_args, apply_static_argument_properties_to_schema, - resolve_integration_static_args, resolve_static_args, ) from uipath_langchain.agent.tools.structured_tool_with_argument_properties import ( @@ -29,64 +24,19 @@ class TestResolveStaticArgs: """Test cases for resolve_static_args function.""" - @pytest.fixture - def connection(self): - """Common connection object used by tests.""" - return Connection( - id="test-connection-id", name="Test Connection", element_instance_id=12345 - ) - - @pytest.fixture - def integration_properties_factory(self, connection): - """Factory for creating integration tool properties.""" - - def _create_properties(parameters=None): - return AgentIntegrationToolProperties( - method="POST", - tool_path="/api/test", - object_name="test_object", - tool_display_name="Test Tool", - tool_description="Test tool description", - connection=connection, - parameters=parameters or [], - ) - - return _create_properties + def test_resolve_static_args_with_argument_properties(self): + """Test resolve_static_args with an object that has argument_properties.""" - @pytest.fixture - def integration_resource_factory(self, integration_properties_factory): - """Factory for creating integration resource config.""" - - def _create_resource(parameters=None): - properties = integration_properties_factory(parameters) - return AgentIntegrationToolResourceConfig( - name="test_tool", - description="Test tool", - properties=properties, - input_schema={}, - ) - - return _create_resource - - def test_resolve_static_args_with_integration_resource( - self, integration_resource_factory - ): - """Test resolve_static_args with AgentIntegrationToolResourceConfig.""" - parameters = [ - AgentIntegrationToolParameter( - name="static_param", - type="string", - field_variant="static", - field_location="body", - value="static_value", - ) - ] - resource = integration_resource_factory(parameters) - agent_input = {"input_arg": "input_value"} + class ResourceWithProps: + argument_properties = { + "$['host']": AgentToolStaticArgumentProperties( + is_sensitive=False, value="api.example.com" + ), + } - result = resolve_static_args(resource, agent_input) + result = resolve_static_args(ResourceWithProps(), {"unused": "input"}) - assert result == {"static_param": "static_value"} + assert result == {"$['host']": "api.example.com"} def test_resolve_static_args_with_unknown_resource_type(self): """Test resolve_static_args with unknown resource type returns empty dict.""" @@ -98,171 +48,6 @@ def test_resolve_static_args_with_unknown_resource_type(self): assert result == {} -class TestResolveIntegrationStaticArgs: - """Test cases for resolve_integration_static_args function.""" - - def test_resolve_with_static_values(self): - """Test resolving parameters with static values.""" - parameters = [ - AgentIntegrationToolParameter( - name="connection_id", - type="string", - field_variant="static", - field_location="body", - value="12345", - ), - AgentIntegrationToolParameter( - name="timeout", - type="integer", - field_variant="static", - field_location="body", - value=30, - ), - AgentIntegrationToolParameter( - name="config", - type="object", - field_variant="static", - field_location="body", - value={"enabled": True, "retries": 3}, - ), - ] - agent_input = {"user_input": "test"} - - result = resolve_integration_static_args(parameters, agent_input) - - expected = { - "connection_id": "12345", - "timeout": 30, - "config": {"enabled": True, "retries": 3}, - } - assert result == expected - - def test_resolve_with_input_arg_values(self): - """Test resolving parameters with input argument values.""" - parameters = [ - AgentIntegrationToolParameter( - name="user_id", - type="string", - field_variant="argument", - field_location="body", - value="{{userId}}", - ), - AgentIntegrationToolParameter( - name="query", - type="string", - field_variant="argument", - field_location="body", - value="{{searchQuery}}", - ), - ] - agent_input = { - "userId": "user123", - "searchQuery": "test search", - "unused_arg": "not_used", - } - - result = resolve_integration_static_args(parameters, agent_input) - - expected = {"user_id": "user123", "query": "test search"} - assert result == expected - - def test_resolve_with_mixed_static_and_argument_values(self): - """Test resolving parameters with both static and argument values.""" - parameters = [ - AgentIntegrationToolParameter( - name="api_key", - type="string", - field_variant="static", - field_location="body", - value="secret_key", - ), - AgentIntegrationToolParameter( - name="user_id", - type="string", - field_variant="argument", - field_location="body", - value="{{userId}}", - ), - AgentIntegrationToolParameter( - name="version", - type="string", - field_variant="static", - field_location="body", - value="v1", - ), - ] - agent_input = {"userId": "user456"} - - result = resolve_integration_static_args(parameters, agent_input) - - expected = {"api_key": "secret_key", "user_id": "user456", "version": "v1"} - assert result == expected - - def test_resolve_skips_none_values(self): - """Test that None values are skipped in the result.""" - parameters = [ - AgentIntegrationToolParameter( - name="existing_param", - type="string", - field_variant="argument", - field_location="body", - value="{{existingArg}}", - ), - AgentIntegrationToolParameter( - name="missing_param", - type="string", - field_variant="argument", - field_location="body", - value="{{missingArg}}", - ), - ] - agent_input = {"existingArg": "exists"} - - result = resolve_integration_static_args(parameters, agent_input) - - assert result == {"existing_param": "exists"} - assert "missing_param" not in result - - def test_resolve_with_invalid_argument_format_raises_error(self): - """Test that invalid argument format raises ValueError.""" - parameters = [ - AgentIntegrationToolParameter( - name="invalid_param", - type="string", - field_variant="argument", - field_location="body", - value="invalid_format", - ) - ] - - with pytest.raises(ValueError, match="Parameter value must be in the format"): - resolve_integration_static_args(parameters, {}) - - def test_resolve_with_malformed_argument_braces(self): - """Test various malformed argument brace patterns.""" - test_cases = [ - "{missing_closing", - "missing_opening}", - "{{missing_closing}", - "{missing_opening}}", - "no_braces_at_all", - ] - - for invalid_value in test_cases: - parameters = [ - AgentIntegrationToolParameter( - name="test_param", - type="string", - field_variant="argument", - field_location="body", - value=invalid_value, - ) - ] - - with pytest.raises(ValueError): - resolve_integration_static_args(parameters, {}) - - class TestApplyStaticArgs: """Test cases for apply_static_args function.""" From 8f72d7bb5f0a2575407669277950c211c952b168 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 27 Feb 2026 16:24:26 +0000 Subject: [PATCH 2/2] test: add missing test coverage for deleted resolve_integration_static_args Covers gaps from removing resolve_integration_static_args: - Static values of different types (string, int, object) through resolve_static_args - AgentToolArgumentArgumentProperties runtime resolution from agent_input - Mixed static and argument properties resolved together - Missing argument key in agent_input is skipped - Parametrized malformed brace patterns (5 patterns) for argument template validation https://claude.ai/code/session_01NJvRhU5bJ3GweCdNhhgidg --- tests/agent/tools/test_integration_tool.py | 25 ++++++ tests/agent/tools/test_static_args.py | 99 ++++++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/tests/agent/tools/test_integration_tool.py b/tests/agent/tools/test_integration_tool.py index c5f3f1bd9..cc1fa0fe5 100644 --- a/tests/agent/tools/test_integration_tool.py +++ b/tests/agent/tools/test_integration_tool.py @@ -461,6 +461,31 @@ def test_argument_parameter_with_invalid_template_raises(self): with pytest.raises(AgentStartupError): convert_integration_parameters_to_argument_properties(params) + @pytest.mark.parametrize( + "invalid_value", + [ + "{missing_closing", + "missing_opening}", + "{{missing_closing}", + "{missing_opening}}", + "no_braces_at_all", + ], + ) + def test_argument_parameter_with_malformed_braces_raises(self, invalid_value): + """Various malformed brace patterns raise AgentStartupError.""" + params = [ + AgentIntegrationToolParameter( + name="test_param", + type="string", + value=invalid_value, + field_location="body", + field_variant="argument", + ), + ] + + with pytest.raises(AgentStartupError): + convert_integration_parameters_to_argument_properties(params) + class TestStripTemplateEnumsFromSchema: """Test cases for strip_template_enums_from_schema function.""" diff --git a/tests/agent/tools/test_static_args.py b/tests/agent/tools/test_static_args.py index 368e45b29..8f0ea10f5 100644 --- a/tests/agent/tools/test_static_args.py +++ b/tests/agent/tools/test_static_args.py @@ -6,6 +6,7 @@ import pytest from pydantic import BaseModel, Field from uipath.agent.models.agent import ( + AgentToolArgumentArgumentProperties, AgentToolArgumentProperties, AgentToolStaticArgumentProperties, BaseAgentResourceConfig, @@ -38,6 +39,104 @@ class ResourceWithProps: assert result == {"$['host']": "api.example.com"} + def test_resolve_static_args_with_static_values_of_different_types(self): + """Test resolve_static_args resolves string, integer, and object static values.""" + + class ResourceWithProps: + argument_properties = { + "$['connection_id']": AgentToolStaticArgumentProperties( + is_sensitive=False, value="12345" + ), + "$['timeout']": AgentToolStaticArgumentProperties( + is_sensitive=False, value=30 + ), + "$['config']": AgentToolStaticArgumentProperties( + is_sensitive=False, value={"enabled": True, "retries": 3} + ), + } + + result = resolve_static_args(ResourceWithProps(), {"unused": "input"}) + + assert result == { + "$['connection_id']": "12345", + "$['timeout']": 30, + "$['config']": {"enabled": True, "retries": 3}, + } + + def test_resolve_static_args_with_argument_properties_extracts_from_agent_input( + self, + ): + """Test resolve_static_args resolves AgentToolArgumentArgumentProperties from agent_input.""" + + class ResourceWithProps: + argument_properties = { + "$['user_id']": AgentToolArgumentArgumentProperties( + is_sensitive=False, argument_path="userId" + ), + "$['query']": AgentToolArgumentArgumentProperties( + is_sensitive=False, argument_path="searchQuery" + ), + } + + agent_input = { + "userId": "user123", + "searchQuery": "test search", + "unused_arg": "not_used", + } + + result = resolve_static_args(ResourceWithProps(), agent_input) + + assert result == { + "$['user_id']": "user123", + "$['query']": "test search", + } + + def test_resolve_static_args_with_mixed_static_and_argument_properties(self): + """Test resolve_static_args with both static and argument properties.""" + + class ResourceWithProps: + argument_properties = { + "$['api_key']": AgentToolStaticArgumentProperties( + is_sensitive=False, value="secret_key" + ), + "$['user_id']": AgentToolArgumentArgumentProperties( + is_sensitive=False, argument_path="userId" + ), + "$['version']": AgentToolStaticArgumentProperties( + is_sensitive=False, value="v1" + ), + } + + agent_input = {"userId": "user456"} + + result = resolve_static_args(ResourceWithProps(), agent_input) + + assert result == { + "$['api_key']": "secret_key", + "$['user_id']": "user456", + "$['version']": "v1", + } + + def test_resolve_static_args_skips_missing_argument_values(self): + """Test that argument properties referencing missing agent_input keys are skipped.""" + + class ResourceWithProps: + argument_properties = { + "$['existing_param']": AgentToolArgumentArgumentProperties( + is_sensitive=False, argument_path="existingArg" + ), + "$['missing_param']": AgentToolArgumentArgumentProperties( + is_sensitive=False, argument_path="missingArg" + ), + } + + agent_input = {"existingArg": "exists"} + + result = resolve_static_args(ResourceWithProps(), agent_input) + + assert result == {"$['existing_param']": "exists"} + assert "$['missing_param']" not in result + def test_resolve_static_args_with_unknown_resource_type(self): """Test resolve_static_args with unknown resource type returns empty dict.""" mock_resource = MagicMock(spec=BaseAgentResourceConfig)