From be0984908146cc39ddef30bd1f1e27b67fcbb934 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 May 2023 15:41:16 -0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20feat(langflow):=20rename=20Pytho?= =?UTF-8?q?nFunction=20to=20PythonFunctionTool=20for=20better=20semantics?= =?UTF-8?q?=20=F0=9F=9A=80=20feat(langflow):=20add=20PythonFunctionToolNod?= =?UTF-8?q?e=20to=20the=20frontend=20node=20tools=20=F0=9F=9A=80=20feat(la?= =?UTF-8?q?ngflow):=20add=20PythonFunctionTool=20to=20the=20custom=20tools?= =?UTF-8?q?=20=F0=9F=9A=80=20feat(langflow):=20add=20get=5Ffunction=20to?= =?UTF-8?q?=20importing=20utils=20to=20get=20the=20function=20from=20code?= =?UTF-8?q?=20=F0=9F=9A=80=20feat(langflow):=20add=20func=20parameter=20to?= =?UTF-8?q?=20PythonFunctionTool=20to=20store=20the=20function=20?= =?UTF-8?q?=F0=9F=9A=80=20feat(langflow):=20add=20name=20and=20description?= =?UTF-8?q?=20parameters=20to=20PythonFunctionTool=20=F0=9F=9A=80=20feat(l?= =?UTF-8?q?angflow):=20update=20instantiate=5Ftool=20to=20use=20PythonFunc?= =?UTF-8?q?tionTool=20instead=20of=20PythonFunction=20=F0=9F=9A=80=20feat(?= =?UTF-8?q?langflow):=20update=20constants=20to=20use=20PythonFunctionTool?= =?UTF-8?q?=20instead=20of=20PythonFunction=20=F0=9F=9A=80=20feat(langflow?= =?UTF-8?q?):=20update=20custom.py=20to=20use=20PythonFunctionTool=20inste?= =?UTF-8?q?ad=20of=20PythonFunction=20=F0=9F=9A=80=20feat(langflow):=20upd?= =?UTF-8?q?ate=20loading.py=20to=20use=20get=5Ffunction=20and=20PythonFunc?= =?UTF-8?q?tionTool=20=F0=9F=9A=80=20feat(langflow):=20update=20utils.py?= =?UTF-8?q?=20to=20use=20get=5Ffunction=20=F0=9F=9A=80=20feat(langflow):?= =?UTF-8?q?=20update=20test=5Fcustom=5Ftypes.py=20to=20use=20get=5Ffunctio?= =?UTF-8?q?n=20and=20PythonFunctionTool=20=F0=9F=9A=80=20feat(langflow):?= =?UTF-8?q?=20update=20test=5Fgraph.py=20to=20use=20PythonFunctionTool=20i?= =?UTF-8?q?nstead=20of=20PythonFunction=20The=20changes=20rename=20PythonF?= =?UTF-8?q?unction=20to=20PythonFunctionTool=20for=20better=20semantics.?= =?UTF-8?q?=20The=20frontend=20node=20tools,=20custom=20tools,=20and=20con?= =?UTF-8?q?stants=20are=20updated=20to=20use=20PythonFunctionTool=20instea?= =?UTF-8?q?d=20of=20PythonFunction.=20The=20get=5Ffunction=20function=20is?= =?UTF-8?q?=20added=20to=20importing=20utils=20to=20get=20the=20function?= =?UTF-8?q?=20from=20code.=20The=20PythonFunctionTool=20is=20updated=20to?= =?UTF-8?q?=20store=20the=20function=20in=20the=20func=20parameter=20and?= =?UTF-8?q?=20to=20have=20name=20and=20description=20parameters.=20The=20i?= =?UTF-8?q?nstantiate=5Ftool,=20loading.py,=20and=20utils.py=20are=20updat?= =?UTF-8?q?ed=20to=20use=20get=5Ffunction=20and=20PythonFunctionTool.=20Th?= =?UTF-8?q?e=20test=5Fcustom=5Ftypes.py=20and=20test=5Fgraph.py=20are=20up?= =?UTF-8?q?dated=20to=20use=20PythonFunctionTool=20instead=20of=20PythonFu?= =?UTF-8?q?nction.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/config.yaml | 2 +- src/backend/langflow/custom/customs.py | 2 +- .../langflow/interface/importing/utils.py | 8 +++++ src/backend/langflow/interface/loading.py | 12 +++---- src/backend/langflow/interface/tools/base.py | 3 +- .../langflow/interface/tools/constants.py | 4 +-- .../langflow/interface/tools/custom.py | 22 +++++++------ .../langflow/template/frontend_node/tools.py | 32 ++++++++++++++++--- tests/test_custom_types.py | 15 ++++++--- tests/test_graph.py | 4 ++- 10 files changed, 73 insertions(+), 31 deletions(-) diff --git a/src/backend/langflow/config.yaml b/src/backend/langflow/config.yaml index 3b8554360..fe05eb406 100644 --- a/src/backend/langflow/config.yaml +++ b/src/backend/langflow/config.yaml @@ -79,7 +79,7 @@ tools: - Calculator - Serper Search - Tool - - PythonFunction + - PythonFunctionTool - JsonSpec - News API - TMDB API diff --git a/src/backend/langflow/custom/customs.py b/src/backend/langflow/custom/customs.py index ee266b0ee..f7a82e4a3 100644 --- a/src/backend/langflow/custom/customs.py +++ b/src/backend/langflow/custom/customs.py @@ -4,7 +4,7 @@ from langflow.template import frontend_node CUSTOM_NODES = { "prompts": {"ZeroShotPrompt": frontend_node.prompts.ZeroShotPromptNode()}, "tools": { - "PythonFunction": frontend_node.tools.PythonFunctionNode(), + "PythonFunctionTool": frontend_node.tools.PythonFunctionToolNode(), "Tool": frontend_node.tools.ToolNode(), }, "agents": { diff --git a/src/backend/langflow/interface/importing/utils.py b/src/backend/langflow/interface/importing/utils.py index d08e52999..f65376d48 100644 --- a/src/backend/langflow/interface/importing/utils.py +++ b/src/backend/langflow/interface/importing/utils.py @@ -9,6 +9,7 @@ from langchain.base_language import BaseLanguageModel from langchain.chains.base import Chain from langchain.chat_models.base import BaseChatModel from langchain.tools import BaseTool +from langflow.utils import validate def import_module(module_path: str) -> Any: @@ -147,3 +148,10 @@ def import_utility(utility: str) -> Any: if utility == "SQLDatabase": return import_class(f"langchain.sql_database.{utility}") return import_class(f"langchain.utilities.{utility}") + + +def get_function(code): + """Get the function""" + function_name = validate.extract_function_name(code) + + return validate.create_function(code, function_name) diff --git a/src/backend/langflow/interface/loading.py b/src/backend/langflow/interface/loading.py index 69c697823..05ca348a7 100644 --- a/src/backend/langflow/interface/loading.py +++ b/src/backend/langflow/interface/loading.py @@ -20,12 +20,12 @@ from langchain.llms.loading import load_llm_from_config from pydantic import ValidationError from langflow.interface.agents.custom import CUSTOM_AGENTS -from langflow.interface.importing.utils import import_by_type +from langflow.interface.importing.utils import get_function, import_by_type from langflow.interface.run import fix_memory_inputs from langflow.interface.toolkits.base import toolkits_creator from langflow.interface.types import get_type_list from langflow.interface.utils import load_file_into_dict -from langflow.utils import util, validate +from langflow.utils import util def instantiate_class(node_type: str, base_type: str, params: Dict) -> Any: @@ -99,11 +99,9 @@ def instantiate_tool(node_type, class_object, params): if node_type == "JsonSpec": params["dict_"] = load_file_into_dict(params.pop("path")) return class_object(**params) - elif node_type == "PythonFunction": - function_string = params["code"] - if isinstance(function_string, str): - return validate.eval_function(function_string) - raise ValueError("Function should be a string") + elif node_type == "PythonFunctionTool": + params["func"] = get_function(params.get("code")) + return class_object(**params) elif node_type.lower() == "tool": return class_object(**params) return class_object(**params) diff --git a/src/backend/langflow/interface/tools/base.py b/src/backend/langflow/interface/tools/base.py index a8e7045c0..d6b114e4c 100644 --- a/src/backend/langflow/interface/tools/base.py +++ b/src/backend/langflow/interface/tools/base.py @@ -71,7 +71,8 @@ class ToolCreator(LangChainTypeCreator): for tool, tool_fcn in ALL_TOOLS_NAMES.items(): tool_params = get_tool_params(tool_fcn) - tool_name = tool_params.get("name", tool) + + tool_name = tool_params.get("name") or tool if tool_name in settings.tools or settings.dev: if tool_name == "JsonSpec": diff --git a/src/backend/langflow/interface/tools/constants.py b/src/backend/langflow/interface/tools/constants.py index f939d55ad..31c75ec08 100644 --- a/src/backend/langflow/interface/tools/constants.py +++ b/src/backend/langflow/interface/tools/constants.py @@ -9,10 +9,10 @@ from langchain.agents.load_tools import ( from langchain.tools.json.tool import JsonSpec from langflow.interface.importing.utils import import_class -from langflow.interface.tools.custom import PythonFunction +from langflow.interface.tools.custom import PythonFunctionTool FILE_TOOLS = {"JsonSpec": JsonSpec} -CUSTOM_TOOLS = {"Tool": Tool, "PythonFunction": PythonFunction} +CUSTOM_TOOLS = {"Tool": Tool, "PythonFunctionTool": PythonFunctionTool} OTHER_TOOLS = {tool: import_class(f"langchain.tools.{tool}") for tool in tools.__all__} diff --git a/src/backend/langflow/interface/tools/custom.py b/src/backend/langflow/interface/tools/custom.py index 4c641f388..b2d43565d 100644 --- a/src/backend/langflow/interface/tools/custom.py +++ b/src/backend/langflow/interface/tools/custom.py @@ -1,13 +1,14 @@ -from typing import Callable, Optional +from typing import Optional +from langflow.interface.importing.utils import get_function from pydantic import BaseModel, validator from langflow.utils import validate +from langchain.agents.tools import Tool class Function(BaseModel): code: str - function: Optional[Callable] = None imports: Optional[str] = None # Eval code and store the function @@ -24,14 +25,17 @@ class Function(BaseModel): return v - def get_function(self): - """Get the function""" - function_name = validate.extract_function_name(self.code) - return validate.create_function(self.code, function_name) - - -class PythonFunction(Function): +class PythonFunctionTool(Function, Tool): """Python function""" + name: str = "Custom Tool" + description: str code: str + + def ___init__(self, name: str, description: str, code: str): + self.name = name + self.description = description + self.code = code + self.func = get_function(self.code) + super().__init__(name=name, description=description, func=self.func) diff --git a/src/backend/langflow/template/frontend_node/tools.py b/src/backend/langflow/template/frontend_node/tools.py index 2819be4d9..4e97fec8c 100644 --- a/src/backend/langflow/template/frontend_node/tools.py +++ b/src/backend/langflow/template/frontend_node/tools.py @@ -59,11 +59,33 @@ class ToolNode(FrontendNode): return super().to_dict() -class PythonFunctionNode(FrontendNode): - name: str = "PythonFunction" +class PythonFunctionToolNode(FrontendNode): + name: str = "PythonFunctionTool" template: Template = Template( - type_name="python_function", + type_name="PythonFunctionTool", fields=[ + TemplateField( + field_type="str", + required=True, + placeholder="", + is_list=False, + show=True, + multiline=False, + value="", + name="name", + advanced=False, + ), + TemplateField( + field_type="str", + required=True, + placeholder="", + is_list=False, + show=True, + multiline=False, + value="", + name="description", + advanced=False, + ), TemplateField( field_type="code", required=True, @@ -73,11 +95,11 @@ class PythonFunctionNode(FrontendNode): value=DEFAULT_PYTHON_FUNCTION, name="code", advanced=False, - ) + ), ], ) description: str = "Python function to be executed." - base_classes: list[str] = ["function"] + base_classes: list[str] = ["Tool"] def to_dict(self): return super().to_dict() diff --git a/tests/test_custom_types.py b/tests/test_custom_types.py index 399450e2e..7503426ab 100644 --- a/tests/test_custom_types.py +++ b/tests/test_custom_types.py @@ -1,16 +1,23 @@ # Test this: +from langflow.interface.importing.utils import get_function import pytest -from langflow.interface.tools.custom import PythonFunction +from langflow.interface.tools.custom import PythonFunctionTool from langflow.utils import constants def test_python_function(): """Test Python function""" - func = PythonFunction(code=constants.DEFAULT_PYTHON_FUNCTION) - assert func.get_function()("text") == "text" + code = constants.DEFAULT_PYTHON_FUNCTION + func = get_function(code) + func = PythonFunctionTool(name="Test", description="Testing", code=code, func=func) + assert func("text") == "text" # the tool decorator should raise an error if # the function is not str -> str # This raises ValidationError with pytest.raises(SyntaxError): - func = PythonFunction(code=pytest.CODE_WITH_SYNTAX_ERROR) + code = pytest.CODE_WITH_SYNTAX_ERROR + func = get_function(code) + func = PythonFunctionTool( + name="Test", description="Testing", code=code, func=func + ) diff --git a/tests/test_graph.py b/tests/test_graph.py index a0f5945fc..7826870e8 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -156,7 +156,9 @@ def test_get_node_neighbors_complex(complex_graph): tool_neighbors = complex_graph.get_nodes_with_target(tool) assert tool_neighbors is not None # Check if there is a PythonFunction in the tool's neighbors - assert any("PythonFunction" in neighbor.data["type"] for neighbor in tool_neighbors) + assert any( + "PythonFunctionTool" in neighbor.data["type"] for neighbor in tool_neighbors + ) def test_get_node(basic_graph):