Skip to content

Commit 37466ba

Browse files
authored
fix: fix bedrock tool binding (#640)
1 parent e0ae877 commit 37466ba

6 files changed

Lines changed: 284 additions & 5 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.7.12"
3+
version = "0.7.8"
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/chat/handlers/bedrock.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ def get_tool_binding_kwargs(
8383
parallel_tool_calls: bool = True,
8484
strict_mode: bool = False,
8585
) -> dict[str, Any]:
86-
return {}
86+
return {
87+
"tool_choice": tool_choice,
88+
}
8789

8890
def check_stop_reason(self, response: AIMessage) -> None:
8991
"""Check Bedrock stop reason and raise exception for faulty terminations.

src/uipath_langchain/chat/handlers/gemini.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,15 +123,13 @@ def get_tool_binding_kwargs(
123123
parallel_tool_calls: bool = True,
124124
strict_mode: bool = False,
125125
) -> dict[str, Any]:
126-
tool_names = [tool.name for tool in tools]
127126
mode = tool_choice.upper()
128127
if strict_mode:
129128
mode = "VALIDATED"
130129
return {
131130
"tool_config": {
132131
"function_calling_config": {
133132
"mode": mode,
134-
"allowed_function_names": tool_names,
135133
}
136134
}
137135
}

tests/chat/handlers/__init__.py

Whitespace-only changes.
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
"""Tests for get_tool_binding_kwargs across all payload handlers."""
2+
3+
from unittest.mock import Mock
4+
5+
from langchain_core.tools import BaseTool
6+
7+
from uipath_langchain.chat.handlers.anthropic import AnthropicPayloadHandler
8+
from uipath_langchain.chat.handlers.base import DefaultModelPayloadHandler
9+
from uipath_langchain.chat.handlers.bedrock import BedrockPayloadHandler
10+
from uipath_langchain.chat.handlers.gemini import GeminiPayloadHandler
11+
from uipath_langchain.chat.handlers.openai import OpenAIPayloadHandler
12+
13+
14+
def _make_tools(*names: str) -> list[Mock]:
15+
"""Create a list of mock tools with the given names."""
16+
tools = []
17+
for name in names:
18+
tool = Mock(spec=BaseTool)
19+
tool.name = name
20+
tools.append(tool)
21+
return tools
22+
23+
24+
# ---------------------------------------------------------------------------
25+
# Default handler
26+
# ---------------------------------------------------------------------------
27+
28+
29+
class TestDefaultGetToolBindingKwargs:
30+
"""DefaultModelPayloadHandler returns only tool_choice."""
31+
32+
def setup_method(self):
33+
self.handler = DefaultModelPayloadHandler()
34+
self.tools = _make_tools("tool_a")
35+
36+
def test_tool_choice_auto(self):
37+
result = self.handler.get_tool_binding_kwargs(
38+
tools=self.tools, tool_choice="auto"
39+
)
40+
assert result == {"tool_choice": "auto"}
41+
42+
def test_tool_choice_any(self):
43+
result = self.handler.get_tool_binding_kwargs(
44+
tools=self.tools, tool_choice="any"
45+
)
46+
assert result == {"tool_choice": "any"}
47+
48+
def test_extra_params_not_leaked(self):
49+
"""parallel_tool_calls and strict_mode must not appear in the result."""
50+
result = self.handler.get_tool_binding_kwargs(
51+
tools=self.tools,
52+
tool_choice="auto",
53+
parallel_tool_calls=True,
54+
strict_mode=True,
55+
)
56+
assert list(result.keys()) == ["tool_choice"]
57+
58+
59+
# ---------------------------------------------------------------------------
60+
# OpenAI handler
61+
# ---------------------------------------------------------------------------
62+
63+
64+
class TestOpenAIGetToolBindingKwargs:
65+
"""OpenAIPayloadHandler returns tool_choice, parallel_tool_calls, strict."""
66+
67+
def setup_method(self):
68+
self.handler = OpenAIPayloadHandler()
69+
self.tools = _make_tools("tool_a")
70+
71+
def test_tool_choice_auto(self):
72+
result = self.handler.get_tool_binding_kwargs(
73+
tools=self.tools, tool_choice="auto"
74+
)
75+
assert result["tool_choice"] == "auto"
76+
77+
def test_tool_choice_any(self):
78+
result = self.handler.get_tool_binding_kwargs(
79+
tools=self.tools, tool_choice="any"
80+
)
81+
assert result["tool_choice"] == "any"
82+
83+
def test_parallel_tool_calls_true(self):
84+
result = self.handler.get_tool_binding_kwargs(
85+
tools=self.tools, tool_choice="auto", parallel_tool_calls=True
86+
)
87+
assert result["parallel_tool_calls"] is True
88+
89+
def test_parallel_tool_calls_false(self):
90+
result = self.handler.get_tool_binding_kwargs(
91+
tools=self.tools, tool_choice="auto", parallel_tool_calls=False
92+
)
93+
assert result["parallel_tool_calls"] is False
94+
95+
def test_strict_mode_true(self):
96+
result = self.handler.get_tool_binding_kwargs(
97+
tools=self.tools, tool_choice="auto", strict_mode=True
98+
)
99+
assert result["strict"] is True
100+
101+
def test_strict_mode_false(self):
102+
result = self.handler.get_tool_binding_kwargs(
103+
tools=self.tools, tool_choice="auto", strict_mode=False
104+
)
105+
assert result["strict"] is False
106+
107+
def test_all_keys_present(self):
108+
result = self.handler.get_tool_binding_kwargs(
109+
tools=self.tools,
110+
tool_choice="any",
111+
parallel_tool_calls=False,
112+
strict_mode=True,
113+
)
114+
assert set(result.keys()) == {"tool_choice", "parallel_tool_calls", "strict"}
115+
116+
117+
# ---------------------------------------------------------------------------
118+
# Anthropic handler
119+
# ---------------------------------------------------------------------------
120+
121+
122+
class TestAnthropicGetToolBindingKwargs:
123+
"""AnthropicPayloadHandler returns tool_choice, parallel_tool_calls, strict."""
124+
125+
def setup_method(self):
126+
self.handler = AnthropicPayloadHandler()
127+
self.tools = _make_tools("tool_a")
128+
129+
def test_tool_choice_auto(self):
130+
result = self.handler.get_tool_binding_kwargs(
131+
tools=self.tools, tool_choice="auto"
132+
)
133+
assert result["tool_choice"] == "auto"
134+
135+
def test_tool_choice_any(self):
136+
result = self.handler.get_tool_binding_kwargs(
137+
tools=self.tools, tool_choice="any"
138+
)
139+
assert result["tool_choice"] == "any"
140+
141+
def test_parallel_tool_calls_true(self):
142+
result = self.handler.get_tool_binding_kwargs(
143+
tools=self.tools, tool_choice="auto", parallel_tool_calls=True
144+
)
145+
assert result["parallel_tool_calls"] is True
146+
147+
def test_parallel_tool_calls_false(self):
148+
result = self.handler.get_tool_binding_kwargs(
149+
tools=self.tools, tool_choice="auto", parallel_tool_calls=False
150+
)
151+
assert result["parallel_tool_calls"] is False
152+
153+
def test_strict_mode_true(self):
154+
result = self.handler.get_tool_binding_kwargs(
155+
tools=self.tools, tool_choice="auto", strict_mode=True
156+
)
157+
assert result["strict"] is True
158+
159+
def test_strict_mode_false(self):
160+
result = self.handler.get_tool_binding_kwargs(
161+
tools=self.tools, tool_choice="auto", strict_mode=False
162+
)
163+
assert result["strict"] is False
164+
165+
def test_all_keys_present(self):
166+
result = self.handler.get_tool_binding_kwargs(
167+
tools=self.tools,
168+
tool_choice="any",
169+
parallel_tool_calls=True,
170+
strict_mode=False,
171+
)
172+
assert set(result.keys()) == {"tool_choice", "parallel_tool_calls", "strict"}
173+
174+
175+
# ---------------------------------------------------------------------------
176+
# Gemini handler
177+
# ---------------------------------------------------------------------------
178+
179+
180+
class TestGeminiGetToolBindingKwargs:
181+
"""GeminiPayloadHandler returns a nested tool_config dict."""
182+
183+
def setup_method(self):
184+
self.handler = GeminiPayloadHandler()
185+
self.tools = _make_tools("get_weather", "search")
186+
187+
def test_mode_auto(self):
188+
result = self.handler.get_tool_binding_kwargs(
189+
tools=self.tools, tool_choice="auto"
190+
)
191+
config = result["tool_config"]["function_calling_config"]
192+
assert config["mode"] == "AUTO"
193+
194+
def test_mode_any(self):
195+
result = self.handler.get_tool_binding_kwargs(
196+
tools=self.tools, tool_choice="any"
197+
)
198+
config = result["tool_config"]["function_calling_config"]
199+
assert config["mode"] == "ANY"
200+
201+
def test_strict_mode_overrides_to_validated(self):
202+
result = self.handler.get_tool_binding_kwargs(
203+
tools=self.tools, tool_choice="auto", strict_mode=True
204+
)
205+
config = result["tool_config"]["function_calling_config"]
206+
assert config["mode"] == "VALIDATED"
207+
208+
def test_strict_mode_overrides_any_to_validated(self):
209+
result = self.handler.get_tool_binding_kwargs(
210+
tools=self.tools, tool_choice="any", strict_mode=True
211+
)
212+
config = result["tool_config"]["function_calling_config"]
213+
assert config["mode"] == "VALIDATED"
214+
215+
def test_only_tool_config_key(self):
216+
"""parallel_tool_calls and strict do not leak as top-level keys."""
217+
result = self.handler.get_tool_binding_kwargs(
218+
tools=self.tools,
219+
tool_choice="auto",
220+
parallel_tool_calls=True,
221+
strict_mode=False,
222+
)
223+
assert list(result.keys()) == ["tool_config"]
224+
225+
226+
# ---------------------------------------------------------------------------
227+
# Bedrock handler
228+
# ---------------------------------------------------------------------------
229+
230+
231+
class TestBedrockGetToolBindingKwargs:
232+
"""BedrockPayloadHandler returns only tool_choice."""
233+
234+
def setup_method(self):
235+
self.handler = BedrockPayloadHandler()
236+
self.tools = _make_tools("tool_a")
237+
238+
def test_tool_choice_auto(self):
239+
result = self.handler.get_tool_binding_kwargs(
240+
tools=self.tools, tool_choice="auto"
241+
)
242+
assert result == {"tool_choice": "auto"}
243+
244+
def test_tool_choice_any(self):
245+
result = self.handler.get_tool_binding_kwargs(
246+
tools=self.tools, tool_choice="any"
247+
)
248+
assert result == {"tool_choice": "any"}
249+
250+
def test_result_contains_tool_choice_key(self):
251+
"""Regression: previously returned empty dict, losing tool_choice."""
252+
result = self.handler.get_tool_binding_kwargs(
253+
tools=self.tools, tool_choice="any"
254+
)
255+
assert "tool_choice" in result
256+
257+
def test_parallel_tool_calls_not_included(self):
258+
"""Bedrock does not support parallel_tool_calls in binding kwargs."""
259+
result = self.handler.get_tool_binding_kwargs(
260+
tools=self.tools, tool_choice="auto", parallel_tool_calls=True
261+
)
262+
assert "parallel_tool_calls" not in result
263+
264+
def test_strict_mode_not_included(self):
265+
"""Bedrock does not support strict mode in binding kwargs."""
266+
result = self.handler.get_tool_binding_kwargs(
267+
tools=self.tools, tool_choice="auto", strict_mode=True
268+
)
269+
assert "strict" not in result
270+
271+
def test_only_tool_choice_returned(self):
272+
"""Ensure exactly one key is returned regardless of input params."""
273+
result = self.handler.get_tool_binding_kwargs(
274+
tools=self.tools,
275+
tool_choice="any",
276+
parallel_tool_calls=True,
277+
strict_mode=True,
278+
)
279+
assert list(result.keys()) == ["tool_choice"]

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)