Skip to content

Commit ebf78b2

Browse files
fix: support static arguments for integration tools (#639)
1 parent ef7d2e4 commit ebf78b2

File tree

7 files changed

+1095
-283
lines changed

7 files changed

+1095
-283
lines changed

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
[project]
22
name = "uipath-langchain"
3-
version = "0.7.13"
3+
version = "0.7.14"
44
description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"
77
dependencies = [
88
"uipath>=2.10.0, <2.11.0",
99
"uipath-core>=0.5.2, <0.6.0",
10-
"uipath-platform>=0.0.4, <0.1.0",
10+
"uipath-platform>=0.0.8, <0.1.0",
1111
"uipath-runtime>=0.9.1, <0.10.0",
1212
"langgraph>=1.0.0, <2.0.0",
1313
"langchain-core>=1.2.11, <2.0.0",

src/uipath_langchain/agent/tools/integration_tool.py

Lines changed: 160 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
"""Process tool creation for UiPath process execution."""
22

33
import copy
4-
from typing import Any
4+
import re
5+
from typing import Any, cast
56

67
from langchain.tools import BaseTool
78
from langchain_core.messages import ToolCall
89
from langchain_core.tools import StructuredTool
9-
from uipath.agent.models.agent import AgentIntegrationToolResourceConfig
10+
from uipath.agent.models.agent import (
11+
AgentIntegrationToolParameter,
12+
AgentIntegrationToolResourceConfig,
13+
AgentToolArgumentArgumentProperties,
14+
AgentToolArgumentProperties,
15+
AgentToolStaticArgumentProperties,
16+
)
1017
from uipath.eval.mocks import mockable
1118
from uipath.platform import UiPath
1219
from uipath.platform.connections import ActivityMetadata, ActivityParameterLocationInfo
@@ -15,18 +22,151 @@
1522
from uipath_langchain.agent.exceptions import AgentStartupError, AgentStartupErrorCode
1623
from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model
1724
from uipath_langchain.agent.react.types import AgentGraphState
18-
from uipath_langchain.agent.tools.static_args import handle_static_args
25+
from uipath_langchain.agent.tools.static_args import (
26+
ArgumentPropertiesMixin,
27+
handle_static_args,
28+
)
1929
from uipath_langchain.agent.tools.tool_node import (
20-
ToolWrapperMixin,
2130
ToolWrapperReturnType,
2231
)
2332

24-
from .structured_tool_with_output_type import StructuredToolWithOutputType
33+
from .schema_editing import strip_matching_enums
34+
from .structured_tool_with_argument_properties import (
35+
StructuredToolWithArgumentProperties,
36+
)
2537
from .utils import sanitize_dict_for_serialization, sanitize_tool_name
2638

2739

28-
class StructuredToolWithWrapper(StructuredToolWithOutputType, ToolWrapperMixin):
29-
pass
40+
def convert_integration_parameters_to_argument_properties(
41+
parameters: list[AgentIntegrationToolParameter],
42+
) -> dict[str, AgentToolArgumentProperties]:
43+
"""Convert integration tool parameters to argument_properties format.
44+
45+
Converts parameters with fieldVariant 'static' or 'argument' to the
46+
corresponding AgentToolArgumentProperties type. Parameters without
47+
a recognized fieldVariant are skipped.
48+
49+
Args:
50+
parameters: List of integration tool parameters to convert.
51+
52+
Returns:
53+
Dictionary mapping JSONPath keys to argument properties.
54+
55+
Raises:
56+
AgentStartupError: If an argument variant parameter has a malformed template.
57+
"""
58+
result: dict[str, AgentToolArgumentProperties] = {}
59+
60+
for param in parameters:
61+
if param.field_variant == "static":
62+
key = _is_param_name_to_jsonpath(param.name)
63+
result[key] = AgentToolStaticArgumentProperties(
64+
is_sensitive=False,
65+
value=param.value,
66+
)
67+
elif param.field_variant == "argument":
68+
value_str = str(param.value) if param.value is not None else ""
69+
match = _TEMPLATE_PATTERN.match(value_str)
70+
if not match:
71+
raise AgentStartupError(
72+
code=AgentStartupErrorCode.INVALID_TOOL_CONFIG,
73+
title="Malformed integration tool argument",
74+
detail=f"Argument parameter '{param.name}' has malformed template: "
75+
f"'{param.value}'. Expected format: '{{{{argName}}}}'",
76+
category=UiPathErrorCategory.USER,
77+
)
78+
arg_name = match.group(1)
79+
key = _is_param_name_to_jsonpath(param.name)
80+
result[key] = AgentToolArgumentArgumentProperties(
81+
is_sensitive=False,
82+
argument_path=arg_name,
83+
)
84+
85+
return result
86+
87+
88+
_TEMPLATE_PATTERN = re.compile(r"^\{\{(.+?)\}\}$")
89+
90+
91+
def _param_name_to_segments(param_name: str) -> list[str]:
92+
"""Parse an Integration Service dot-notation parameter name into path segments.
93+
94+
Splits by '.' and expands '[*]' suffixes into a separate '*' wildcard segment.
95+
96+
Examples:
97+
"channel" -> ["channel"]
98+
"attachment.title" -> ["attachment", "title"]
99+
"attachments[*].actions[*].confirm.text"
100+
-> ["attachments", "*", "actions", "*", "confirm", "text"]
101+
"""
102+
segments: list[str] = []
103+
for part in param_name.split("."):
104+
if part.endswith("[*]"):
105+
segments.append(part[:-3])
106+
segments.append("*")
107+
else:
108+
segments.append(part)
109+
return segments
110+
111+
112+
def _escape_jsonpath_property(name: str) -> str:
113+
"""Escape a property name for bracket-notation JSONPath.
114+
115+
Per the JSONPath bracket-notation spec, escapes are applied in order:
116+
1. Backslashes first: \\\\ -> \\\\\\\\
117+
2. Single quotes second: ' -> \\\\'
118+
"""
119+
return name.replace("\\", "\\\\").replace("'", "\\'")
120+
121+
122+
def _is_param_name_to_jsonpath(param_name: str) -> str:
123+
"""Convert an IS dot-notation parameter name to bracket-notation JSONPath.
124+
125+
Examples:
126+
"channel" -> "$['channel']"
127+
"attachment.title" -> "$['attachment']['title']"
128+
"attachments[*].text" -> "$['attachments'][*]['text']"
129+
"""
130+
segments = _param_name_to_segments(param_name)
131+
parts: list[str] = []
132+
for seg in segments:
133+
if seg == "*":
134+
parts.append("[*]")
135+
else:
136+
parts.append(f"['{_escape_jsonpath_property(seg)}']")
137+
return "$" + "".join(parts)
138+
139+
140+
def strip_template_enums_from_schema(
141+
schema: dict[str, Any],
142+
parameters: list[AgentIntegrationToolParameter],
143+
) -> dict[str, Any]:
144+
"""Remove {{template}} enum values only from argument-variant parameter fields.
145+
146+
For each parameter with fieldVariant 'argument', navigates the schema to the
147+
corresponding field (supporting nested objects, arrays, and $ref resolution)
148+
and strips enum values matching the {{...}} pattern.
149+
150+
The function deep-copies the schema so the original is never mutated.
151+
152+
Args:
153+
schema: A JSON-schema-style dictionary (the tool's inputSchema).
154+
parameters: List of integration tool parameters from resource.properties.
155+
156+
Returns:
157+
A cleaned copy of the schema with template enum values removed
158+
only from argument-variant fields.
159+
"""
160+
schema = copy.deepcopy(schema)
161+
162+
for param in parameters:
163+
if param.field_variant != "argument":
164+
continue
165+
166+
segments = _param_name_to_segments(param.name)
167+
strip_matching_enums(schema, segments, _TEMPLATE_PATTERN)
168+
169+
return schema
30170

31171

32172
def remove_asterisk_from_properties(fields: dict[str, Any]) -> dict[str, Any]:
@@ -155,14 +295,21 @@ def create_integration_tool(
155295

156296
activity_metadata = convert_to_activity_metadata(resource)
157297

158-
input_model = create_model(resource.input_schema)
298+
cleaned_input_schema = strip_template_enums_from_schema(
299+
resource.input_schema, resource.properties.parameters
300+
)
301+
input_model = create_model(cleaned_input_schema)
159302
# note: IS tools output schemas were recently added and are most likely not present in all resources
160303
output_model: Any = (
161304
create_model(remove_asterisk_from_properties(resource.output_schema))
162305
if resource.output_schema
163306
else create_model({"type": "object", "properties": {}})
164307
)
165308

309+
argument_properties = convert_integration_parameters_to_argument_properties(
310+
resource.properties.parameters
311+
)
312+
166313
sdk = UiPath()
167314

168315
@mockable(
@@ -189,10 +336,12 @@ async def integration_tool_wrapper(
189336
call: ToolCall,
190337
state: AgentGraphState,
191338
) -> ToolWrapperReturnType:
192-
call["args"] = handle_static_args(resource, state, call["args"])
339+
call["args"] = handle_static_args(
340+
cast(ArgumentPropertiesMixin, tool), state, call["args"]
341+
)
193342
return await tool.ainvoke(call)
194343

195-
tool = StructuredToolWithWrapper(
344+
tool = StructuredToolWithArgumentProperties(
196345
name=tool_name,
197346
description=resource.description,
198347
args_schema=input_model,
@@ -204,6 +353,7 @@ async def integration_tool_wrapper(
204353
"connector_key": resource.properties.connection.id,
205354
"connector_name": resource.properties.connection.name,
206355
},
356+
argument_properties=argument_properties,
207357
)
208358
tool.set_tool_wrappers(awrapper=integration_tool_wrapper)
209359

src/uipath_langchain/agent/tools/schema_editing.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import copy
44
import json
5+
import re
56
from typing import Any
67

78
from jsonpath_ng import ( # type: ignore[import-untyped]
@@ -108,6 +109,37 @@ def _navigate_schema_inlining_refs(
108109
return current
109110

110111

112+
def strip_matching_enums(
113+
schema: dict[str, Any],
114+
path_segments: list[str],
115+
pattern: re.Pattern[str],
116+
) -> None:
117+
"""Navigate to a field in schema and remove enum values matching the pattern.
118+
119+
If the path doesn't exist in the schema, this is a no-op.
120+
After removing matching values, if the enum becomes empty, the enum key is deleted.
121+
122+
Args:
123+
schema: The root JSON schema (modified in place).
124+
path_segments: Path segments to navigate to the field.
125+
pattern: Compiled regex pattern to match enum values against.
126+
"""
127+
try:
128+
field_schema = _navigate_schema_inlining_refs(schema, path_segments)
129+
except SchemaModificationError:
130+
return
131+
132+
enum = field_schema.get("enum")
133+
if enum is None:
134+
return
135+
136+
cleaned = [v for v in enum if not (isinstance(v, str) and pattern.match(v))]
137+
if not cleaned:
138+
del field_schema["enum"]
139+
else:
140+
field_schema["enum"] = cleaned
141+
142+
111143
def _apply_sensitive_schema_modification(
112144
schema: dict[str, Any],
113145
path_parts: list[str],

src/uipath_langchain/agent/tools/static_args.py

Lines changed: 9 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
from langchain_core.tools import StructuredTool
99
from pydantic import BaseModel
1010
from uipath.agent.models.agent import (
11-
AgentIntegrationToolParameter,
12-
AgentIntegrationToolResourceConfig,
1311
AgentToolArgumentArgumentProperties,
1412
AgentToolArgumentProperties,
1513
AgentToolStaticArgumentProperties,
@@ -137,24 +135,20 @@ def apply_static_argument_properties_to_schema(
137135

138136

139137
def resolve_static_args(
140-
resource: BaseAgentResourceConfig,
138+
resource: BaseAgentResourceConfig | ArgumentPropertiesMixin,
141139
agent_input: dict[str, Any],
142140
) -> dict[str, Any]:
143141
"""Resolves static arguments for a given resource with a given input.
144142
145143
Args:
146-
resource: The agent resource configuration.
147-
input: The input arguments passed to the agent.
144+
resource: The agent resource configuration or tool with argument_properties.
145+
agent_input: The input arguments passed to the agent.
148146
149147
Returns:
150148
A dictionary of expanded arguments to be used in the tool call.
151149
"""
152150

153-
if isinstance(resource, AgentIntegrationToolResourceConfig):
154-
return resolve_integration_static_args(
155-
resource.properties.parameters, agent_input
156-
)
157-
elif hasattr(resource, "argument_properties"):
151+
if hasattr(resource, "argument_properties"):
158152
# TODO: MCP tools don't inherit from BaseAgentResourceConfig; will need to handle separately
159153
static_arguments = _resolve_argument_properties_to_static_arguments(
160154
resource.argument_properties, agent_input
@@ -167,47 +161,6 @@ def resolve_static_args(
167161
return {} # to be implemented for other resource types in the future
168162

169163

170-
def resolve_integration_static_args(
171-
parameters: list[AgentIntegrationToolParameter],
172-
agent_input: dict[str, Any],
173-
) -> dict[str, Any]:
174-
"""Resolves static arguments for an integration tool resource.
175-
176-
Args:
177-
resource: The AgentIntegrationToolResourceConfig instance.
178-
input: The input arguments passed to the agent.
179-
180-
Returns:
181-
A dictionary of expanded static arguments for the integration tool.
182-
"""
183-
184-
static_args: dict[str, Any] = {}
185-
for param in parameters:
186-
value = None
187-
188-
# static parameter, use the defined static value
189-
if param.field_variant == "static":
190-
value = param.value
191-
# argument parameter, extract value from agent input
192-
elif param.field_variant == "argument":
193-
if (
194-
not isinstance(param.value, str)
195-
or not param.value.startswith("{{")
196-
or not param.value.endswith("}}")
197-
):
198-
raise ValueError(
199-
f"Parameter value must be in the format '{{argument_name}}' when field_variant is 'argument', got {param.value}"
200-
)
201-
arg_name = param.value[2:-2].strip()
202-
# currently only support top-level arguments
203-
value = agent_input.get(arg_name)
204-
205-
if value is not None:
206-
static_args[param.name] = value
207-
208-
return static_args
209-
210-
211164
def apply_static_args(
212165
static_args: dict[str, Any],
213166
kwargs: dict[str, Any],
@@ -244,12 +197,14 @@ def apply_static_args(
244197

245198

246199
def handle_static_args(
247-
resource: BaseAgentResourceConfig, state: BaseModel, input_args: dict[str, Any]
200+
resource: BaseAgentResourceConfig | ArgumentPropertiesMixin,
201+
state: BaseModel,
202+
input_args: dict[str, Any],
248203
) -> dict[str, Any]:
249204
"""Resolves and applies static arguments for a tool call.
250205
Args:
251-
resource: The agent resource configuration.
252-
runtime: The tool runtime providing the current state.
206+
resource: The agent resource configuration or tool with argument_properties.
207+
state: The current agent state.
253208
input_args: The original input arguments to the tool.
254209
Returns:
255210
A dictionary of input arguments with static arguments applied.

0 commit comments

Comments
 (0)