diff --git a/pyproject.toml b/pyproject.toml index 7d22d6cc0..924ebbeea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-langchain" -version = "0.7.12" +version = "0.7.8" 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/src/uipath_langchain/chat/handlers/bedrock.py b/src/uipath_langchain/chat/handlers/bedrock.py index 35c3127a6..c183d59a9 100644 --- a/src/uipath_langchain/chat/handlers/bedrock.py +++ b/src/uipath_langchain/chat/handlers/bedrock.py @@ -83,7 +83,9 @@ def get_tool_binding_kwargs( parallel_tool_calls: bool = True, strict_mode: bool = False, ) -> dict[str, Any]: - return {} + return { + "tool_choice": tool_choice, + } def check_stop_reason(self, response: AIMessage) -> None: """Check Bedrock stop reason and raise exception for faulty terminations. diff --git a/src/uipath_langchain/chat/handlers/gemini.py b/src/uipath_langchain/chat/handlers/gemini.py index 742cc2d4a..ade921a45 100644 --- a/src/uipath_langchain/chat/handlers/gemini.py +++ b/src/uipath_langchain/chat/handlers/gemini.py @@ -123,7 +123,6 @@ def get_tool_binding_kwargs( parallel_tool_calls: bool = True, strict_mode: bool = False, ) -> dict[str, Any]: - tool_names = [tool.name for tool in tools] mode = tool_choice.upper() if strict_mode: mode = "VALIDATED" @@ -131,7 +130,6 @@ def get_tool_binding_kwargs( "tool_config": { "function_calling_config": { "mode": mode, - "allowed_function_names": tool_names, } } } diff --git a/tests/chat/handlers/__init__.py b/tests/chat/handlers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/chat/handlers/test_tool_binding_kwargs.py b/tests/chat/handlers/test_tool_binding_kwargs.py new file mode 100644 index 000000000..492e3bd33 --- /dev/null +++ b/tests/chat/handlers/test_tool_binding_kwargs.py @@ -0,0 +1,279 @@ +"""Tests for get_tool_binding_kwargs across all payload handlers.""" + +from unittest.mock import Mock + +from langchain_core.tools import BaseTool + +from uipath_langchain.chat.handlers.anthropic import AnthropicPayloadHandler +from uipath_langchain.chat.handlers.base import DefaultModelPayloadHandler +from uipath_langchain.chat.handlers.bedrock import BedrockPayloadHandler +from uipath_langchain.chat.handlers.gemini import GeminiPayloadHandler +from uipath_langchain.chat.handlers.openai import OpenAIPayloadHandler + + +def _make_tools(*names: str) -> list[Mock]: + """Create a list of mock tools with the given names.""" + tools = [] + for name in names: + tool = Mock(spec=BaseTool) + tool.name = name + tools.append(tool) + return tools + + +# --------------------------------------------------------------------------- +# Default handler +# --------------------------------------------------------------------------- + + +class TestDefaultGetToolBindingKwargs: + """DefaultModelPayloadHandler returns only tool_choice.""" + + def setup_method(self): + self.handler = DefaultModelPayloadHandler() + self.tools = _make_tools("tool_a") + + def test_tool_choice_auto(self): + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, tool_choice="auto" + ) + assert result == {"tool_choice": "auto"} + + def test_tool_choice_any(self): + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, tool_choice="any" + ) + assert result == {"tool_choice": "any"} + + def test_extra_params_not_leaked(self): + """parallel_tool_calls and strict_mode must not appear in the result.""" + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, + tool_choice="auto", + parallel_tool_calls=True, + strict_mode=True, + ) + assert list(result.keys()) == ["tool_choice"] + + +# --------------------------------------------------------------------------- +# OpenAI handler +# --------------------------------------------------------------------------- + + +class TestOpenAIGetToolBindingKwargs: + """OpenAIPayloadHandler returns tool_choice, parallel_tool_calls, strict.""" + + def setup_method(self): + self.handler = OpenAIPayloadHandler() + self.tools = _make_tools("tool_a") + + def test_tool_choice_auto(self): + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, tool_choice="auto" + ) + assert result["tool_choice"] == "auto" + + def test_tool_choice_any(self): + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, tool_choice="any" + ) + assert result["tool_choice"] == "any" + + def test_parallel_tool_calls_true(self): + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, tool_choice="auto", parallel_tool_calls=True + ) + assert result["parallel_tool_calls"] is True + + def test_parallel_tool_calls_false(self): + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, tool_choice="auto", parallel_tool_calls=False + ) + assert result["parallel_tool_calls"] is False + + def test_strict_mode_true(self): + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, tool_choice="auto", strict_mode=True + ) + assert result["strict"] is True + + def test_strict_mode_false(self): + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, tool_choice="auto", strict_mode=False + ) + assert result["strict"] is False + + def test_all_keys_present(self): + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, + tool_choice="any", + parallel_tool_calls=False, + strict_mode=True, + ) + assert set(result.keys()) == {"tool_choice", "parallel_tool_calls", "strict"} + + +# --------------------------------------------------------------------------- +# Anthropic handler +# --------------------------------------------------------------------------- + + +class TestAnthropicGetToolBindingKwargs: + """AnthropicPayloadHandler returns tool_choice, parallel_tool_calls, strict.""" + + def setup_method(self): + self.handler = AnthropicPayloadHandler() + self.tools = _make_tools("tool_a") + + def test_tool_choice_auto(self): + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, tool_choice="auto" + ) + assert result["tool_choice"] == "auto" + + def test_tool_choice_any(self): + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, tool_choice="any" + ) + assert result["tool_choice"] == "any" + + def test_parallel_tool_calls_true(self): + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, tool_choice="auto", parallel_tool_calls=True + ) + assert result["parallel_tool_calls"] is True + + def test_parallel_tool_calls_false(self): + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, tool_choice="auto", parallel_tool_calls=False + ) + assert result["parallel_tool_calls"] is False + + def test_strict_mode_true(self): + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, tool_choice="auto", strict_mode=True + ) + assert result["strict"] is True + + def test_strict_mode_false(self): + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, tool_choice="auto", strict_mode=False + ) + assert result["strict"] is False + + def test_all_keys_present(self): + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, + tool_choice="any", + parallel_tool_calls=True, + strict_mode=False, + ) + assert set(result.keys()) == {"tool_choice", "parallel_tool_calls", "strict"} + + +# --------------------------------------------------------------------------- +# Gemini handler +# --------------------------------------------------------------------------- + + +class TestGeminiGetToolBindingKwargs: + """GeminiPayloadHandler returns a nested tool_config dict.""" + + def setup_method(self): + self.handler = GeminiPayloadHandler() + self.tools = _make_tools("get_weather", "search") + + def test_mode_auto(self): + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, tool_choice="auto" + ) + config = result["tool_config"]["function_calling_config"] + assert config["mode"] == "AUTO" + + def test_mode_any(self): + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, tool_choice="any" + ) + config = result["tool_config"]["function_calling_config"] + assert config["mode"] == "ANY" + + def test_strict_mode_overrides_to_validated(self): + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, tool_choice="auto", strict_mode=True + ) + config = result["tool_config"]["function_calling_config"] + assert config["mode"] == "VALIDATED" + + def test_strict_mode_overrides_any_to_validated(self): + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, tool_choice="any", strict_mode=True + ) + config = result["tool_config"]["function_calling_config"] + assert config["mode"] == "VALIDATED" + + def test_only_tool_config_key(self): + """parallel_tool_calls and strict do not leak as top-level keys.""" + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, + tool_choice="auto", + parallel_tool_calls=True, + strict_mode=False, + ) + assert list(result.keys()) == ["tool_config"] + + +# --------------------------------------------------------------------------- +# Bedrock handler +# --------------------------------------------------------------------------- + + +class TestBedrockGetToolBindingKwargs: + """BedrockPayloadHandler returns only tool_choice.""" + + def setup_method(self): + self.handler = BedrockPayloadHandler() + self.tools = _make_tools("tool_a") + + def test_tool_choice_auto(self): + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, tool_choice="auto" + ) + assert result == {"tool_choice": "auto"} + + def test_tool_choice_any(self): + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, tool_choice="any" + ) + assert result == {"tool_choice": "any"} + + def test_result_contains_tool_choice_key(self): + """Regression: previously returned empty dict, losing tool_choice.""" + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, tool_choice="any" + ) + assert "tool_choice" in result + + def test_parallel_tool_calls_not_included(self): + """Bedrock does not support parallel_tool_calls in binding kwargs.""" + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, tool_choice="auto", parallel_tool_calls=True + ) + assert "parallel_tool_calls" not in result + + def test_strict_mode_not_included(self): + """Bedrock does not support strict mode in binding kwargs.""" + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, tool_choice="auto", strict_mode=True + ) + assert "strict" not in result + + def test_only_tool_choice_returned(self): + """Ensure exactly one key is returned regardless of input params.""" + result = self.handler.get_tool_binding_kwargs( + tools=self.tools, + tool_choice="any", + parallel_tool_calls=True, + strict_mode=True, + ) + assert list(result.keys()) == ["tool_choice"] diff --git a/uv.lock b/uv.lock index c98ea6554..3d7e02f5e 100644 --- a/uv.lock +++ b/uv.lock @@ -3324,7 +3324,7 @@ wheels = [ [[package]] name = "uipath-langchain" -version = "0.7.12" +version = "0.7.8" source = { editable = "." } dependencies = [ { name = "httpx" },