From 038f828ec4b284bb431a46faf21c75c5e8fbe0ed Mon Sep 17 00:00:00 2001 From: Tuana Celik Date: Fri, 14 Jun 2024 11:24:54 +0200 Subject: [PATCH 01/16] adding the OpenAIFunctionCaller --- .../components/tools/__init__.py | 3 + .../components/tools/openai/__init__.py | 3 + .../tools/openai/function_caller.py | 59 +++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 haystack_experimental/components/tools/__init__.py create mode 100644 haystack_experimental/components/tools/openai/__init__.py create mode 100644 haystack_experimental/components/tools/openai/function_caller.py diff --git a/haystack_experimental/components/tools/__init__.py b/haystack_experimental/components/tools/__init__.py new file mode 100644 index 00000000..c1764a6e --- /dev/null +++ b/haystack_experimental/components/tools/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 diff --git a/haystack_experimental/components/tools/openai/__init__.py b/haystack_experimental/components/tools/openai/__init__.py new file mode 100644 index 00000000..c1764a6e --- /dev/null +++ b/haystack_experimental/components/tools/openai/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 diff --git a/haystack_experimental/components/tools/openai/function_caller.py b/haystack_experimental/components/tools/openai/function_caller.py new file mode 100644 index 00000000..6bb1c42b --- /dev/null +++ b/haystack_experimental/components/tools/openai/function_caller.py @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 + +import json +from typing import List + +from haystack import component +from haystack.dataclasses import ChatMessage + + +@component +class OpenAIFunctionCaller: + """ + The OpenAIFunctionCaller expects a list of ChatMessages and if there is a tool call with a function name and arguments, it runs the function and returns the + result as a ChatMessage from role = 'function' + """ + + def __init__(self, available_functions): + """ + Initialize the OpenAIFunctionCaller component. + :param available_functions: A dictionary of available functions. This dictionary expects key value pairs of function name, and the function itslelf. For example {"weather_function": weather_function} + """ + self.available_functions = available_functions + + @component.output_types( + function_replies=List[ChatMessage], assistant_replies=List[ChatMessage] + ) + def run(self, messages: List[ChatMessage]): + """ + Evaluates `messages` and invokes available functions if the messages contain tool_calls. + :param messages: A list of messages generated from the `OpenAIChatGenerator` + :returns: This component returns a list of messages in one of two outputs + - `function_replies`: List of ChatMessages containing the result of a function invocation. This message comes from role = 'function'. If the function name was hallucinated or wrong, an assistant message explaining as such is returned + - `assistant_replies`: List of ChatMessages containing a regular assistant reply. In this case, there were no tool_calls in the received messages + """ + if messages[0].meta["finish_reason"] == "tool_calls": + function_calls = json.loads(messages[0].content) + for function_call in function_calls: + function_name = function_call["function"]["name"] + function_args = json.loads(function_call["function"]["arguments"]) + if function_name in self.available_functions: + function_to_call = self.available_functions[function_name] + function_response = function_to_call(**function_args) + messages.append( + ChatMessage.from_function( + content=json.dumps(function_response), name=function_name + ) + ) + else: + messages.append( + ChatMessage.from_assistant( + """I'm sorry, I tried to run a function that did not exist. + Would you like me to correct it and try again?""" + ) + ) + return {"function_replies": messages} + else: + return {"assistant_replies": messages} From edd90d3d21cb779cf3725b7bb3ee9e1d7a89cc5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tuana=20=C3=87elik?= Date: Mon, 17 Jun 2024 15:16:33 +0200 Subject: [PATCH 02/16] Update haystack_experimental/components/tools/openai/function_caller.py Co-authored-by: Massimiliano Pippi --- .../components/tools/openai/function_caller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/haystack_experimental/components/tools/openai/function_caller.py b/haystack_experimental/components/tools/openai/function_caller.py index 6bb1c42b..30aac4fc 100644 --- a/haystack_experimental/components/tools/openai/function_caller.py +++ b/haystack_experimental/components/tools/openai/function_caller.py @@ -16,7 +16,7 @@ class OpenAIFunctionCaller: result as a ChatMessage from role = 'function' """ - def __init__(self, available_functions): + def __init__(self, available_functions: Dict[str, Callable[...]): """ Initialize the OpenAIFunctionCaller component. :param available_functions: A dictionary of available functions. This dictionary expects key value pairs of function name, and the function itslelf. For example {"weather_function": weather_function} From 960162000df088fb9c3fa80d132f10c2241b3e81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tuana=20=C3=87elik?= Date: Mon, 17 Jun 2024 15:16:43 +0200 Subject: [PATCH 03/16] Update haystack_experimental/components/tools/openai/function_caller.py Co-authored-by: Massimiliano Pippi --- .../components/tools/openai/function_caller.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/haystack_experimental/components/tools/openai/function_caller.py b/haystack_experimental/components/tools/openai/function_caller.py index 30aac4fc..bd1ea823 100644 --- a/haystack_experimental/components/tools/openai/function_caller.py +++ b/haystack_experimental/components/tools/openai/function_caller.py @@ -55,5 +55,4 @@ def run(self, messages: List[ChatMessage]): ) ) return {"function_replies": messages} - else: - return {"assistant_replies": messages} + return {"assistant_replies": messages} From 3fea72f73658fa591573c9234dcf626b61170db8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tuana=20=C3=87elik?= Date: Mon, 17 Jun 2024 15:17:18 +0200 Subject: [PATCH 04/16] Update haystack_experimental/components/tools/openai/function_caller.py Co-authored-by: Madeesh Kannan --- .../components/tools/openai/function_caller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/haystack_experimental/components/tools/openai/function_caller.py b/haystack_experimental/components/tools/openai/function_caller.py index bd1ea823..8c829d37 100644 --- a/haystack_experimental/components/tools/openai/function_caller.py +++ b/haystack_experimental/components/tools/openai/function_caller.py @@ -19,7 +19,7 @@ class OpenAIFunctionCaller: def __init__(self, available_functions: Dict[str, Callable[...]): """ Initialize the OpenAIFunctionCaller component. - :param available_functions: A dictionary of available functions. This dictionary expects key value pairs of function name, and the function itslelf. For example {"weather_function": weather_function} + :param available_functions: A dictionary of available functions. This dictionary expects key value pairs of function name, and the function itself. For example, `{"weather_function": weather_function}` """ self.available_functions = available_functions From 16fb540c06d41bef20f6826c4ed5f14e668d2c4a Mon Sep 17 00:00:00 2001 From: Tuana Celik Date: Mon, 17 Jun 2024 15:49:48 +0200 Subject: [PATCH 05/16] adding serialization --- .../tools/openai/function_caller.py | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/haystack_experimental/components/tools/openai/function_caller.py b/haystack_experimental/components/tools/openai/function_caller.py index 8c829d37..714dcc30 100644 --- a/haystack_experimental/components/tools/openai/function_caller.py +++ b/haystack_experimental/components/tools/openai/function_caller.py @@ -3,10 +3,11 @@ # SPDX-License-Identifier: Apache-2.0 import json -from typing import List +from typing import Any, List, Dict, Callable -from haystack import component +from haystack import component, default_from_dict, default_to_dict from haystack.dataclasses import ChatMessage +from haystack.utils import serialize_callable, deserialize_callable @component @@ -16,13 +17,53 @@ class OpenAIFunctionCaller: result as a ChatMessage from role = 'function' """ - def __init__(self, available_functions: Dict[str, Callable[...]): + def __init__(self, available_functions: Dict[str, Callable]): """ Initialize the OpenAIFunctionCaller component. :param available_functions: A dictionary of available functions. This dictionary expects key value pairs of function name, and the function itself. For example, `{"weather_function": weather_function}` """ self.available_functions = available_functions + def to_dict(self) -> Dict[str, Any]: + """ + Serializes the component to a dictionary. + + :returns: + Dictionary with serialized data. + """ + available_function_callbacks = {} + for function in self.available_functions: + available_function_callbacks[function] = ( + serialize_callable(self.available_functions[function]) + if function + else None + ) + serialization_dict = default_to_dict( + self, available_functions=available_function_callbacks + ) + return serialization_dict + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "OpenAIFunctionCaller": + """ + Deserializes the component from a dictionary. + + :param data: + The dictionary to deserialize from. + :returns: + The deserialized component. + """ + init_params = data.get("init_parameters", {}) + available_function_callback_handler = init_params.get("available_functions") + if available_function_callback_handler: + for callback in data["init_parameters"]["available_functions"]: + print(callback) + deserialize_callable( + data["init_parameters"]["available_functions"][callback] + ) + + return default_from_dict(cls, data) + @component.output_types( function_replies=List[ChatMessage], assistant_replies=List[ChatMessage] ) From 3e6f8312be3c9c5ad8a29fcdbaf1e7741f4028da Mon Sep 17 00:00:00 2001 From: Tuana Celik Date: Mon, 17 Jun 2024 16:29:51 +0200 Subject: [PATCH 06/16] resolve comments --- .../tools/openai/function_caller.py | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/haystack_experimental/components/tools/openai/function_caller.py b/haystack_experimental/components/tools/openai/function_caller.py index 714dcc30..3a5500e7 100644 --- a/haystack_experimental/components/tools/openai/function_caller.py +++ b/haystack_experimental/components/tools/openai/function_caller.py @@ -9,6 +9,9 @@ from haystack.dataclasses import ChatMessage from haystack.utils import serialize_callable, deserialize_callable +_FUNCTION_NAME_FAILURE = "I'm sorry, I tried to run a function that did not exist. Would you like me to correct it and try again?" +_FUNCTION_RUN_FAILURE = "Seems there was an error while runnign the function: {error}" + @component class OpenAIFunctionCaller: @@ -82,18 +85,21 @@ def run(self, messages: List[ChatMessage]): function_args = json.loads(function_call["function"]["arguments"]) if function_name in self.available_functions: function_to_call = self.available_functions[function_name] - function_response = function_to_call(**function_args) - messages.append( - ChatMessage.from_function( - content=json.dumps(function_response), name=function_name + try: + function_response = function_to_call(**function_args) + messages.append( + ChatMessage.from_function( + content=json.dumps(function_response), + name=function_name, + ) ) - ) - else: - messages.append( - ChatMessage.from_assistant( - """I'm sorry, I tried to run a function that did not exist. - Would you like me to correct it and try again?""" + except BaseException as e: + messages.append( + ChatMessage.from_assistant( + _FUNCTION_RUN_FAILURE.format(error=e) + ) ) - ) + else: + messages.append(ChatMessage.from_assistant(_FUNCTION_NAME_FAILURE)) return {"function_replies": messages} return {"assistant_replies": messages} From 26a71ed6ad04ed3e789410a47f85f00e37c18504 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tuana=20=C3=87elik?= Date: Mon, 17 Jun 2024 17:33:28 +0200 Subject: [PATCH 07/16] Update haystack_experimental/components/tools/openai/function_caller.py Co-authored-by: Massimiliano Pippi --- .../components/tools/openai/function_caller.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/haystack_experimental/components/tools/openai/function_caller.py b/haystack_experimental/components/tools/openai/function_caller.py index 3a5500e7..306c5ff8 100644 --- a/haystack_experimental/components/tools/openai/function_caller.py +++ b/haystack_experimental/components/tools/openai/function_caller.py @@ -56,14 +56,10 @@ def from_dict(cls, data: Dict[str, Any]) -> "OpenAIFunctionCaller": :returns: The deserialized component. """ - init_params = data.get("init_parameters", {}) - available_function_callback_handler = init_params.get("available_functions") - if available_function_callback_handler: - for callback in data["init_parameters"]["available_functions"]: - print(callback) - deserialize_callable( - data["init_parameters"]["available_functions"][callback] - ) + available_function_paths = data.get("init_parameters", {}).get("available_functions") + available_functions = {} + for name, path in available_function_paths.items(): + available_functions[name] = deserialize_callable(path) return default_from_dict(cls, data) From efbd6b7e9be92f553bcbefcd71b47ab3755a1ef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tuana=20=C3=87elik?= Date: Mon, 17 Jun 2024 17:33:36 +0200 Subject: [PATCH 08/16] Update haystack_experimental/components/tools/openai/function_caller.py Co-authored-by: Massimiliano Pippi --- .../components/tools/openai/function_caller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/haystack_experimental/components/tools/openai/function_caller.py b/haystack_experimental/components/tools/openai/function_caller.py index 306c5ff8..80544068 100644 --- a/haystack_experimental/components/tools/openai/function_caller.py +++ b/haystack_experimental/components/tools/openai/function_caller.py @@ -61,7 +61,7 @@ def from_dict(cls, data: Dict[str, Any]) -> "OpenAIFunctionCaller": for name, path in available_function_paths.items(): available_functions[name] = deserialize_callable(path) - return default_from_dict(cls, data) + return default_from_dict(cls, available_functions) @component.output_types( function_replies=List[ChatMessage], assistant_replies=List[ChatMessage] From 652bdc28a287a941ea6136b10c9f9575e9b900a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tuana=20=C3=87elik?= Date: Mon, 17 Jun 2024 17:33:43 +0200 Subject: [PATCH 09/16] Update haystack_experimental/components/tools/openai/function_caller.py Co-authored-by: Massimiliano Pippi --- .../components/tools/openai/function_caller.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/haystack_experimental/components/tools/openai/function_caller.py b/haystack_experimental/components/tools/openai/function_caller.py index 80544068..f16c647c 100644 --- a/haystack_experimental/components/tools/openai/function_caller.py +++ b/haystack_experimental/components/tools/openai/function_caller.py @@ -34,13 +34,9 @@ def to_dict(self) -> Dict[str, Any]: :returns: Dictionary with serialized data. """ - available_function_callbacks = {} - for function in self.available_functions: - available_function_callbacks[function] = ( - serialize_callable(self.available_functions[function]) - if function - else None - ) + available_function_paths = {} + for name, function in self.available_functions.items(): + available_function_paths[name] = serialize_callable(function) serialization_dict = default_to_dict( self, available_functions=available_function_callbacks ) From 21f31a57ceddda7fbe21201973d20013a127a35e Mon Sep 17 00:00:00 2001 From: Tuana Celik Date: Mon, 17 Jun 2024 18:16:15 +0200 Subject: [PATCH 10/16] fix errors --- .../components/tools/openai/function_caller.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/haystack_experimental/components/tools/openai/function_caller.py b/haystack_experimental/components/tools/openai/function_caller.py index f16c647c..bca1dc23 100644 --- a/haystack_experimental/components/tools/openai/function_caller.py +++ b/haystack_experimental/components/tools/openai/function_caller.py @@ -38,7 +38,7 @@ def to_dict(self) -> Dict[str, Any]: for name, function in self.available_functions.items(): available_function_paths[name] = serialize_callable(function) serialization_dict = default_to_dict( - self, available_functions=available_function_callbacks + self, available_functions=available_function_paths ) return serialization_dict @@ -56,8 +56,7 @@ def from_dict(cls, data: Dict[str, Any]) -> "OpenAIFunctionCaller": available_functions = {} for name, path in available_function_paths.items(): available_functions[name] = deserialize_callable(path) - - return default_from_dict(cls, available_functions) + return cls(available_functions) @component.output_types( function_replies=List[ChatMessage], assistant_replies=List[ChatMessage] From a31eb9fe1e42bb330b291e51476550f84cf37350 Mon Sep 17 00:00:00 2001 From: Massimiliano Pippi Date: Tue, 18 Jun 2024 08:54:59 +0200 Subject: [PATCH 11/16] linter + format --- .../tools/openai/function_caller.py | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/haystack_experimental/components/tools/openai/function_caller.py b/haystack_experimental/components/tools/openai/function_caller.py index bca1dc23..540f9fa8 100644 --- a/haystack_experimental/components/tools/openai/function_caller.py +++ b/haystack_experimental/components/tools/openai/function_caller.py @@ -3,27 +3,34 @@ # SPDX-License-Identifier: Apache-2.0 import json -from typing import Any, List, Dict, Callable +from typing import Any, Callable, Dict, List -from haystack import component, default_from_dict, default_to_dict +from haystack import component, default_to_dict from haystack.dataclasses import ChatMessage -from haystack.utils import serialize_callable, deserialize_callable +from haystack.utils import deserialize_callable, serialize_callable -_FUNCTION_NAME_FAILURE = "I'm sorry, I tried to run a function that did not exist. Would you like me to correct it and try again?" +_FUNCTION_NAME_FAILURE = ( + "I'm sorry, I tried to run a function that did not exist. Would you like me to correct it and try again?" +) _FUNCTION_RUN_FAILURE = "Seems there was an error while runnign the function: {error}" @component class OpenAIFunctionCaller: """ - The OpenAIFunctionCaller expects a list of ChatMessages and if there is a tool call with a function name and arguments, it runs the function and returns the - result as a ChatMessage from role = 'function' + OpenAIFunctionCaller processes a list of chat messages and call Python functions when needed. + + The OpenAIFunctionCaller expects a list of ChatMessages and if there is a tool call with a function name and + arguments, it runs the function and returns the result as a ChatMessage from role = 'function' """ def __init__(self, available_functions: Dict[str, Callable]): """ Initialize the OpenAIFunctionCaller component. - :param available_functions: A dictionary of available functions. This dictionary expects key value pairs of function name, and the function itself. For example, `{"weather_function": weather_function}` + + :param available_functions: + A dictionary of available functions. This dictionary expects key value pairs of function name, + and the function itself. For example, `{"weather_function": weather_function}` """ self.available_functions = available_functions @@ -37,9 +44,7 @@ def to_dict(self) -> Dict[str, Any]: available_function_paths = {} for name, function in self.available_functions.items(): available_function_paths[name] = serialize_callable(function) - serialization_dict = default_to_dict( - self, available_functions=available_function_paths - ) + serialization_dict = default_to_dict(self, available_functions=available_function_paths) return serialization_dict @classmethod @@ -55,19 +60,21 @@ def from_dict(cls, data: Dict[str, Any]) -> "OpenAIFunctionCaller": available_function_paths = data.get("init_parameters", {}).get("available_functions") available_functions = {} for name, path in available_function_paths.items(): - available_functions[name] = deserialize_callable(path) + available_functions[name] = deserialize_callable(path) return cls(available_functions) - @component.output_types( - function_replies=List[ChatMessage], assistant_replies=List[ChatMessage] - ) + @component.output_types(function_replies=List[ChatMessage], assistant_replies=List[ChatMessage]) def run(self, messages: List[ChatMessage]): """ Evaluates `messages` and invokes available functions if the messages contain tool_calls. + :param messages: A list of messages generated from the `OpenAIChatGenerator` :returns: This component returns a list of messages in one of two outputs - - `function_replies`: List of ChatMessages containing the result of a function invocation. This message comes from role = 'function'. If the function name was hallucinated or wrong, an assistant message explaining as such is returned - - `assistant_replies`: List of ChatMessages containing a regular assistant reply. In this case, there were no tool_calls in the received messages + - `function_replies`: List of ChatMessages containing the result of a function invocation. + This message comes from role = 'function'. If the function name was hallucinated or wrong, + an assistant message explaining as such is returned + - `assistant_replies`: List of ChatMessages containing a regular assistant reply. In this case, + there were no tool_calls in the received messages """ if messages[0].meta["finish_reason"] == "tool_calls": function_calls = json.loads(messages[0].content) @@ -85,11 +92,7 @@ def run(self, messages: List[ChatMessage]): ) ) except BaseException as e: - messages.append( - ChatMessage.from_assistant( - _FUNCTION_RUN_FAILURE.format(error=e) - ) - ) + messages.append(ChatMessage.from_assistant(_FUNCTION_RUN_FAILURE.format(error=e))) else: messages.append(ChatMessage.from_assistant(_FUNCTION_NAME_FAILURE)) return {"function_replies": messages} From 70f4c7b36b8b83cbdcaade01b9dbb0a83b4e3751 Mon Sep 17 00:00:00 2001 From: Massimiliano Pippi Date: Tue, 18 Jun 2024 09:20:31 +0200 Subject: [PATCH 12/16] Update function_caller.py --- .../components/tools/openai/function_caller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/haystack_experimental/components/tools/openai/function_caller.py b/haystack_experimental/components/tools/openai/function_caller.py index 540f9fa8..eca66fcb 100644 --- a/haystack_experimental/components/tools/openai/function_caller.py +++ b/haystack_experimental/components/tools/openai/function_caller.py @@ -91,7 +91,7 @@ def run(self, messages: List[ChatMessage]): name=function_name, ) ) - except BaseException as e: + except Exception as e: messages.append(ChatMessage.from_assistant(_FUNCTION_RUN_FAILURE.format(error=e))) else: messages.append(ChatMessage.from_assistant(_FUNCTION_NAME_FAILURE)) From 2b5fd75200d53ee3b7694b7bc9dc789d682d5a4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tuana=20=C3=87elik?= Date: Tue, 18 Jun 2024 12:04:43 +0200 Subject: [PATCH 13/16] Update haystack_experimental/components/tools/openai/function_caller.py Co-authored-by: Madeesh Kannan --- .../components/tools/openai/function_caller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/haystack_experimental/components/tools/openai/function_caller.py b/haystack_experimental/components/tools/openai/function_caller.py index eca66fcb..339b8a97 100644 --- a/haystack_experimental/components/tools/openai/function_caller.py +++ b/haystack_experimental/components/tools/openai/function_caller.py @@ -12,7 +12,7 @@ _FUNCTION_NAME_FAILURE = ( "I'm sorry, I tried to run a function that did not exist. Would you like me to correct it and try again?" ) -_FUNCTION_RUN_FAILURE = "Seems there was an error while runnign the function: {error}" +_FUNCTION_RUN_FAILURE = "Seems there was an error while running the function: {error}" @component From 1f703e94387ea242476c3e071b1fb2fadd63ea9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tuana=20=C3=87elik?= Date: Tue, 18 Jun 2024 12:20:09 +0200 Subject: [PATCH 14/16] Update haystack_experimental/components/tools/openai/function_caller.py Co-authored-by: Massimiliano Pippi --- .../components/tools/openai/function_caller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/haystack_experimental/components/tools/openai/function_caller.py b/haystack_experimental/components/tools/openai/function_caller.py index 339b8a97..c4d00b72 100644 --- a/haystack_experimental/components/tools/openai/function_caller.py +++ b/haystack_experimental/components/tools/openai/function_caller.py @@ -61,7 +61,8 @@ def from_dict(cls, data: Dict[str, Any]) -> "OpenAIFunctionCaller": available_functions = {} for name, path in available_function_paths.items(): available_functions[name] = deserialize_callable(path) - return cls(available_functions) + data["init_parameters"]["available_functions"] = available_functions + return default_to_dict(cls, data) @component.output_types(function_replies=List[ChatMessage], assistant_replies=List[ChatMessage]) def run(self, messages: List[ChatMessage]): From 1021809bd1117dff45450ea9a08f82b03d08372a Mon Sep 17 00:00:00 2001 From: Tuana Celik Date: Tue, 18 Jun 2024 13:34:32 +0200 Subject: [PATCH 15/16] adding tests and imports to inits --- haystack_experimental/components/__init__.py | 4 ++ .../components/tools/__init__.py | 4 ++ .../components/tools/openai/__init__.py | 4 ++ .../tools/openai/function_caller.py | 4 +- test/components/__init__.py | 3 + test/components/tools/__init__.py | 3 + test/components/tools/openai/__init__.py | 3 + .../tools/openai/test_function_caller.py | 69 +++++++++++++++++++ 8 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 test/components/__init__.py create mode 100644 test/components/tools/__init__.py create mode 100644 test/components/tools/openai/__init__.py create mode 100644 test/components/tools/openai/test_function_caller.py diff --git a/haystack_experimental/components/__init__.py b/haystack_experimental/components/__init__.py index c1764a6e..96af4078 100644 --- a/haystack_experimental/components/__init__.py +++ b/haystack_experimental/components/__init__.py @@ -1,3 +1,7 @@ # SPDX-FileCopyrightText: 2022-present deepset GmbH # # SPDX-License-Identifier: Apache-2.0 + +from .tools import OpenAIFunctionCaller + +_all_ = [ "OpenAIFunctionCaller"] diff --git a/haystack_experimental/components/tools/__init__.py b/haystack_experimental/components/tools/__init__.py index c1764a6e..65434145 100644 --- a/haystack_experimental/components/tools/__init__.py +++ b/haystack_experimental/components/tools/__init__.py @@ -1,3 +1,7 @@ # SPDX-FileCopyrightText: 2022-present deepset GmbH # # SPDX-License-Identifier: Apache-2.0 + +from .openai.function_caller import OpenAIFunctionCaller + +_all_ = ["OpenAIFunctionCaller"] diff --git a/haystack_experimental/components/tools/openai/__init__.py b/haystack_experimental/components/tools/openai/__init__.py index c1764a6e..1c3f4517 100644 --- a/haystack_experimental/components/tools/openai/__init__.py +++ b/haystack_experimental/components/tools/openai/__init__.py @@ -1,3 +1,7 @@ # SPDX-FileCopyrightText: 2022-present deepset GmbH # # SPDX-License-Identifier: Apache-2.0 + +from .function_caller import OpenAIFunctionCaller + +_all_ = [ "OpenAIFunctionCaller"] diff --git a/haystack_experimental/components/tools/openai/function_caller.py b/haystack_experimental/components/tools/openai/function_caller.py index c4d00b72..3109c0a4 100644 --- a/haystack_experimental/components/tools/openai/function_caller.py +++ b/haystack_experimental/components/tools/openai/function_caller.py @@ -5,7 +5,7 @@ import json from typing import Any, Callable, Dict, List -from haystack import component, default_to_dict +from haystack import component, default_from_dict, default_to_dict from haystack.dataclasses import ChatMessage from haystack.utils import deserialize_callable, serialize_callable @@ -62,7 +62,7 @@ def from_dict(cls, data: Dict[str, Any]) -> "OpenAIFunctionCaller": for name, path in available_function_paths.items(): available_functions[name] = deserialize_callable(path) data["init_parameters"]["available_functions"] = available_functions - return default_to_dict(cls, data) + return default_from_dict(cls, data) @component.output_types(function_replies=List[ChatMessage], assistant_replies=List[ChatMessage]) def run(self, messages: List[ChatMessage]): diff --git a/test/components/__init__.py b/test/components/__init__.py new file mode 100644 index 00000000..3f4ac9d8 --- /dev/null +++ b/test/components/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/test/components/tools/__init__.py b/test/components/tools/__init__.py new file mode 100644 index 00000000..3f4ac9d8 --- /dev/null +++ b/test/components/tools/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/test/components/tools/openai/__init__.py b/test/components/tools/openai/__init__.py new file mode 100644 index 00000000..3f4ac9d8 --- /dev/null +++ b/test/components/tools/openai/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/test/components/tools/openai/test_function_caller.py b/test/components/tools/openai/test_function_caller.py new file mode 100644 index 00000000..9182120f --- /dev/null +++ b/test/components/tools/openai/test_function_caller.py @@ -0,0 +1,69 @@ +# SPDX-FileCopyrightText: 2023-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 +import os +import json +import pytest + +# from haystack.utils import Secret +from haystack_experimental.components.tools import OpenAIFunctionCaller +from haystack.dataclasses import ChatMessage + +WEATHER_INFO = { + "Berlin": {"weather": "mostly sunny", "temperature": 7, "unit": "celsius"}, + "Paris": {"weather": "mostly cloudy", "temperature": 8, "unit": "celsius"}, + "Rome": {"weather": "sunny", "temperature": 14, "unit": "celsius"}, + "Madrid": {"weather": "sunny", "temperature": 10, "unit": "celsius"}, + "London": {"weather": "cloudy", "temperature": 9, "unit": "celsius"}, +} + + +def mock_weather_func(location): + if location in WEATHER_INFO: + return WEATHER_INFO[location] + else: + return {"weather": "sunny", "temperature": 21.8, "unit": "fahrenheit"} + +class TestOpenAIFunctionCaller: + + def test_init(self, monkeypatch): + component = OpenAIFunctionCaller(available_functions = {"mock_weather_func": mock_weather_func}) + assert component.available_functions == {"mock_weather_func": mock_weather_func} + + def test_successful_function_call(self, monkeypatch): + component = OpenAIFunctionCaller(available_functions = {"mock_weather_func": mock_weather_func}) + mock_assistant_message = ChatMessage.from_assistant(content='[{"id": "mock-id", "function": {"arguments": "{\\"location\\":\\"Berlin\\"}", "name": "mock_weather_func"}, "type": "function"}]', + meta={"finish_reason": "tool_calls"}) + result = component.run(messages=[mock_assistant_message]) + result_obj = json.loads(result["function_replies"][-1].content) + assert result_obj['weather'] == WEATHER_INFO['Berlin']['weather'] + assert result_obj['temperature'] == WEATHER_INFO['Berlin']['temperature'] + assert result_obj['unit'] == WEATHER_INFO['Berlin']['unit'] + + + def test_failing_function_call(self, monkeypatch): + component = OpenAIFunctionCaller(available_functions = {"mock_weather_func": mock_weather_func}) + mock_assistant_message = ChatMessage.from_assistant(content='[{"id": "mock-id", "function": {"arguments": "{\\"location\\":\\"Berlin\\"}", "name": "mock_weather"}, "type": "function"}]', + meta={"finish_reason": "tool_calls"}) + result = component.run(messages=[mock_assistant_message]) + assert result["function_replies"][-1].content == "I'm sorry, I tried to run a function that did not exist. Would you like me to correct it and try again?" + + def test_to_dict(self, monkeypatch): + component = OpenAIFunctionCaller(available_functions = {"mock_weather_func": mock_weather_func}) + data = component.to_dict() + assert data == { + "type": "haystack_experimental.components.tools.openai.function_caller.OpenAIFunctionCaller", + "init_parameters": { + "available_functions": {'mock_weather_func': 'test.components.tools.openai.test_function_caller.mock_weather_func'} + }, + } + + def test_from_dict(self, monkeypatch): + data = { + "type": "haystack_experimental.components.tools.openai.function_caller.OpenAIFunctionCaller", + "init_parameters": { + "available_functions": {'mock_weather_func': 'test.components.tools.openai.test_function_caller.mock_weather_func'}, + }, + } + component: OpenAIFunctionCaller = OpenAIFunctionCaller.from_dict(data) + assert component.available_functions == {'mock_weather_func': mock_weather_func} \ No newline at end of file From 9e287d4014a508246ae01521c7de187424884aab Mon Sep 17 00:00:00 2001 From: Tuana Celik Date: Tue, 18 Jun 2024 13:55:02 +0200 Subject: [PATCH 16/16] disable broad-except --- haystack_experimental/components/tools/openai/function_caller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/haystack_experimental/components/tools/openai/function_caller.py b/haystack_experimental/components/tools/openai/function_caller.py index 3109c0a4..0c12b963 100644 --- a/haystack_experimental/components/tools/openai/function_caller.py +++ b/haystack_experimental/components/tools/openai/function_caller.py @@ -92,6 +92,7 @@ def run(self, messages: List[ChatMessage]): name=function_name, ) ) + # pylint: disable=broad-exception-caught except Exception as e: messages.append(ChatMessage.from_assistant(_FUNCTION_RUN_FAILURE.format(error=e))) else: