Skip to content

Commit ada41c9

Browse files
fix: do not add static args not found in the input schema (#884)
1 parent e194eb8 commit ada41c9

4 files changed

Lines changed: 143 additions & 15 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-langchain"
3-
version = "0.11.11"
3+
version = "0.11.12"
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"

src/uipath_langchain/agent/tools/static_args.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -164,23 +164,28 @@ def deduplicate_argument_properties(
164164
def _apply_static_arguments_to_schema(
165165
tool: ToolT,
166166
static_args: dict[str, ToolStaticArgument],
167-
) -> ToolT:
167+
) -> tuple[ToolT, set[str]]:
168168
"""Modify tool schema based on pre-resolved static arguments.
169169
170170
Args:
171171
tool: The tool to modify
172172
static_args: The mapping from JSON paths to static arguments
173+
174+
Returns:
175+
The schema-modified tool and the set of json paths that were applied to
176+
the schema. Paths that cannot be applied are skipped.
173177
"""
174178
if not static_args:
175-
return tool
179+
return tool, set()
176180

177181
if isinstance(tool.args_schema, dict):
178182
modified_json_schema = copy.deepcopy(tool.args_schema)
179183
elif tool.args_schema and issubclass(tool.args_schema, BaseModel):
180184
modified_json_schema = tool.args_schema.model_json_schema()
181185
else:
182-
return tool
186+
return tool, set(static_args)
183187

188+
applied_paths: set[str] = set()
184189
for json_path, static_arg in static_args.items():
185190
try:
186191
apply_static_value_to_schema(
@@ -189,6 +194,7 @@ def _apply_static_arguments_to_schema(
189194
static_arg.display_value,
190195
static_arg.is_sensitive,
191196
)
197+
applied_paths.add(json_path)
192198
except SchemaModificationError as e:
193199
logger.warning(
194200
f"Skipping invalid static argument path '{json_path}' for tool '{tool.name}': {e}"
@@ -197,7 +203,7 @@ def _apply_static_arguments_to_schema(
197203
modified_tool = tool.model_copy(deep=True)
198204
modified_tool.args_schema = create_model(modified_json_schema)
199205

200-
return modified_tool
206+
return modified_tool, applied_paths
201207

202208

203209
@deprecated(
@@ -287,19 +293,27 @@ def initialize(
287293
self._processed_tools = []
288294
self._sanitized_static_values = {}
289295
for tool in tools:
290-
if isinstance(tool, ArgumentPropertiesMixin) and tool.argument_properties:
296+
if (
297+
isinstance(tool, ArgumentPropertiesMixin)
298+
and isinstance(tool, StructuredTool)
299+
and tool.argument_properties
300+
):
291301
static_args = _resolve_argument_properties(
292302
tool.argument_properties, agent_input
293303
)
294-
self._sanitized_static_values[tool.name] = (
295-
sanitize_dict_for_serialization(
296-
{k: sa.value for k, sa in static_args.items()}
297-
)
304+
modified_tool, applied_paths = _apply_static_arguments_to_schema(
305+
tool, static_args
298306
)
299-
self._processed_tools.append(
300-
_apply_static_arguments_to_schema(tool, static_args)
301-
if isinstance(tool, StructuredTool)
302-
else tool
307+
self._processed_tools.append(modified_tool)
308+
# Only thread args that survived schema modification: paths the
309+
# schema rejected would fail the synthesized strict validator.
310+
applied_static_values = {
311+
path: sa.value
312+
for path, sa in static_args.items()
313+
if path in applied_paths
314+
}
315+
self._sanitized_static_values[tool.name] = (
316+
sanitize_dict_for_serialization(applied_static_values)
303317
)
304318
else:
305319
self._processed_tools.append(tool)

tests/agent/tools/test_static_args.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ class InputSchema(BaseModel):
8888
userId: str
8989
searchQuery: str
9090

91+
class ToolArgs(BaseModel):
92+
user_id: str
93+
query: str
94+
9195
tool = _create_tool(
9296
"test_tool",
9397
{
@@ -98,6 +102,7 @@ class InputSchema(BaseModel):
98102
is_sensitive=False, argument_path="searchQuery"
99103
),
100104
},
105+
args_schema=ToolArgs,
101106
)
102107
handler = StaticArgsHandler()
103108
state = InputSchema(userId="user123", searchQuery="test search")
@@ -114,6 +119,10 @@ def test_initialize_with_mixed_static_and_argument_properties(self):
114119
class InputSchema(BaseModel):
115120
userId: str
116121

122+
class ToolArgs(BaseModel):
123+
api_key: str
124+
user_id: str
125+
117126
tool = _create_tool(
118127
"test_tool",
119128
{
@@ -124,6 +133,7 @@ class InputSchema(BaseModel):
124133
is_sensitive=False, argument_path="userId"
125134
),
126135
},
136+
args_schema=ToolArgs,
127137
)
128138
handler = StaticArgsHandler()
129139
handler.initialize([tool], InputSchema(userId="user456"), InputSchema)
@@ -140,6 +150,10 @@ class InputSchema(BaseModel):
140150
existingArg: str
141151
missingArg: str = ""
142152

153+
class ToolArgs(BaseModel):
154+
existing_param: str
155+
missing_param: str = ""
156+
143157
tool = _create_tool(
144158
"test_tool",
145159
{
@@ -150,6 +164,7 @@ class InputSchema(BaseModel):
150164
is_sensitive=False, argument_path="nonExistentField"
151165
),
152166
},
167+
args_schema=ToolArgs,
153168
)
154169
handler = StaticArgsHandler()
155170
handler.initialize([tool], InputSchema(existingArg="exists"), InputSchema)
@@ -159,6 +174,31 @@ class InputSchema(BaseModel):
159174
assert call["args"]["existing_param"] == "exists"
160175
assert "missing_param" not in call["args"]
161176

177+
def test_initialize_with_all_argument_properties_unresolved(self):
178+
"""A tool whose argument properties all resolve to nothing is returned
179+
unchanged and threads no static args into the call."""
180+
181+
class InputSchema(BaseModel):
182+
present: str = ""
183+
184+
tool = _create_tool(
185+
"test_tool",
186+
{
187+
"$['query']": AgentToolArgumentArgumentProperties(
188+
is_sensitive=False, argument_path="absentField"
189+
),
190+
},
191+
)
192+
handler = StaticArgsHandler()
193+
processed_tools = handler.initialize([tool], InputSchema(), InputSchema)
194+
195+
# No static args resolved, so the original tool passes through unmodified.
196+
assert processed_tools[0] is tool
197+
198+
call = _make_tool_call("test_tool", {"host": "h"})
199+
handler.apply_to_response([call])
200+
assert call["args"] == {"host": "h"}
201+
162202
def test_apply_to_response_merges_with_existing_args(self):
163203
"""Test that apply_to_response merges static args with existing tool call args."""
164204
tool = _create_tool(
@@ -250,6 +290,33 @@ def test_initialize_returns_original_tool_when_no_static_args(self):
250290
assert len(processed_tools) == 1
251291
assert processed_tools[0] is tool
252292

293+
def test_initialize_keeps_static_args_when_schema_is_not_a_model(self):
294+
"""A tool without a dict/BaseModel schema can't be schema-filtered, so its
295+
static args pass through unchanged and the tool is returned as-is."""
296+
297+
async def tool_fn(**kwargs: Any) -> str:
298+
return "ok"
299+
300+
tool = StructuredToolWithArgumentProperties(
301+
name="test_tool",
302+
description="A test tool",
303+
args_schema=None,
304+
coroutine=tool_fn,
305+
output_type=None,
306+
argument_properties={
307+
"$['x']": AgentToolStaticArgumentProperties(
308+
is_sensitive=False, value="v"
309+
),
310+
},
311+
)
312+
handler = StaticArgsHandler()
313+
processed_tools = handler.initialize([tool], EmptyInput(), EmptyInput)
314+
assert processed_tools[0] is tool
315+
316+
call = _make_tool_call("test_tool")
317+
handler.apply_to_response([call])
318+
assert call["args"] == {"x": "v"}
319+
253320
def test_initialize_skips_nonexistent_schema_fields(self):
254321
"""Test that static properties referencing nonexistent schema fields are skipped."""
255322
tool = _create_tool(
@@ -275,6 +342,53 @@ def test_initialize_skips_nonexistent_schema_fields(self):
275342
assert host_def["enum"] == ["api.example.com"]
276343
assert "nonexistent_field" not in schema["properties"]
277344

345+
def test_static_arg_skipped_from_schema_is_not_applied_to_tool_call(self):
346+
"""
347+
A static argument whose path cannot be applied to the schema (e.g. the
348+
Integration-Service ``generateSchema`` button element, which is not a real
349+
schema property) is correctly skipped during schema modification but must
350+
also be stripped from the values threaded into the tool call. Otherwise the
351+
leaked key is rejected by the synthesized strict (``extra='forbid'``) model.
352+
"""
353+
dict_schema = {
354+
"type": "object",
355+
"additionalProperties": False,
356+
"properties": {"query": {"type": "string"}},
357+
"required": ["query"],
358+
}
359+
360+
async def tool_fn(**kwargs: Any) -> str:
361+
return "ok"
362+
363+
tool = StructuredToolWithArgumentProperties(
364+
name="Search_using_String",
365+
description="An Integration-Service tool",
366+
args_schema=dict_schema,
367+
coroutine=tool_fn,
368+
output_type=None,
369+
argument_properties={
370+
"$['generateSchema']": AgentToolStaticArgumentProperties(
371+
is_sensitive=False, value=None
372+
),
373+
},
374+
)
375+
handler = StaticArgsHandler()
376+
processed_tools = handler.initialize([tool], EmptyInput(), EmptyInput)
377+
modified_tool = processed_tools[0]
378+
379+
call = _make_tool_call("Search_using_String", {"query": "hello"})
380+
handler.apply_to_response([call])
381+
382+
# The un-inlineable path must not leak into the tool call args.
383+
assert "generateSchema" not in call["args"]
384+
assert call["args"] == {"query": "hello"}
385+
386+
# The applied args must validate against the synthesized strict model.
387+
assert isinstance(modified_tool.args_schema, type) and issubclass(
388+
modified_tool.args_schema, BaseModel
389+
)
390+
modified_tool.args_schema.model_validate(call["args"])
391+
278392

279393
class TestApplyStaticArgs:
280394
"""Test cases for apply_static_args function."""

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)