🚀 feat(langflow): rename PythonFunction to PythonFunctionTool for better semantics

🚀 feat(langflow): add PythonFunctionToolNode to the frontend node tools
🚀 feat(langflow): add PythonFunctionTool to the custom tools
🚀 feat(langflow): add get_function to importing utils to get the function from code
🚀 feat(langflow): add func parameter to PythonFunctionTool to store the function
🚀 feat(langflow): add name and description parameters to PythonFunctionTool
🚀 feat(langflow): update instantiate_tool to use PythonFunctionTool instead of PythonFunction
🚀 feat(langflow): update constants to use PythonFunctionTool instead of PythonFunction
🚀 feat(langflow): update custom.py to use PythonFunctionTool instead of PythonFunction
🚀 feat(langflow): update loading.py to use get_function and PythonFunctionTool
🚀 feat(langflow): update utils.py to use get_function
🚀 feat(langflow): update test_custom_types.py to use get_function and PythonFunctionTool
🚀 feat(langflow): update test_graph.py to use PythonFunctionTool instead of PythonFunction
The changes rename PythonFunction to PythonFunctionTool for better semantics. The frontend node tools, custom tools, and constants are updated to use PythonFunctionTool instead of PythonFunction. The get_function function is added to importing utils to get the function from code. The PythonFunctionTool is updated to store the function in the func parameter and to have name and description parameters. The instantiate_tool, loading.py, and utils.py are updated to use get_function and PythonFunctionTool. The test_custom_types.py and test_graph.py are updated to use PythonFunctionTool instead of PythonFunction.
This commit is contained in:
Gabriel Luiz Freitas Almeida 2023-05-31 15:41:16 -03:00
commit be09849081
10 changed files with 73 additions and 31 deletions

View file

@ -79,7 +79,7 @@ tools:
- Calculator - Calculator
- Serper Search - Serper Search
- Tool - Tool
- PythonFunction - PythonFunctionTool
- JsonSpec - JsonSpec
- News API - News API
- TMDB API - TMDB API

View file

@ -4,7 +4,7 @@ from langflow.template import frontend_node
CUSTOM_NODES = { CUSTOM_NODES = {
"prompts": {"ZeroShotPrompt": frontend_node.prompts.ZeroShotPromptNode()}, "prompts": {"ZeroShotPrompt": frontend_node.prompts.ZeroShotPromptNode()},
"tools": { "tools": {
"PythonFunction": frontend_node.tools.PythonFunctionNode(), "PythonFunctionTool": frontend_node.tools.PythonFunctionToolNode(),
"Tool": frontend_node.tools.ToolNode(), "Tool": frontend_node.tools.ToolNode(),
}, },
"agents": { "agents": {

View file

@ -9,6 +9,7 @@ from langchain.base_language import BaseLanguageModel
from langchain.chains.base import Chain from langchain.chains.base import Chain
from langchain.chat_models.base import BaseChatModel from langchain.chat_models.base import BaseChatModel
from langchain.tools import BaseTool from langchain.tools import BaseTool
from langflow.utils import validate
def import_module(module_path: str) -> Any: def import_module(module_path: str) -> Any:
@ -147,3 +148,10 @@ def import_utility(utility: str) -> Any:
if utility == "SQLDatabase": if utility == "SQLDatabase":
return import_class(f"langchain.sql_database.{utility}") return import_class(f"langchain.sql_database.{utility}")
return import_class(f"langchain.utilities.{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)

View file

@ -20,12 +20,12 @@ from langchain.llms.loading import load_llm_from_config
from pydantic import ValidationError from pydantic import ValidationError
from langflow.interface.agents.custom import CUSTOM_AGENTS 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.run import fix_memory_inputs
from langflow.interface.toolkits.base import toolkits_creator from langflow.interface.toolkits.base import toolkits_creator
from langflow.interface.types import get_type_list from langflow.interface.types import get_type_list
from langflow.interface.utils import load_file_into_dict 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: 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": if node_type == "JsonSpec":
params["dict_"] = load_file_into_dict(params.pop("path")) params["dict_"] = load_file_into_dict(params.pop("path"))
return class_object(**params) return class_object(**params)
elif node_type == "PythonFunction": elif node_type == "PythonFunctionTool":
function_string = params["code"] params["func"] = get_function(params.get("code"))
if isinstance(function_string, str): return class_object(**params)
return validate.eval_function(function_string)
raise ValueError("Function should be a string")
elif node_type.lower() == "tool": elif node_type.lower() == "tool":
return class_object(**params) return class_object(**params)
return class_object(**params) return class_object(**params)

View file

@ -71,7 +71,8 @@ class ToolCreator(LangChainTypeCreator):
for tool, tool_fcn in ALL_TOOLS_NAMES.items(): for tool, tool_fcn in ALL_TOOLS_NAMES.items():
tool_params = get_tool_params(tool_fcn) 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 in settings.tools or settings.dev:
if tool_name == "JsonSpec": if tool_name == "JsonSpec":

View file

@ -9,10 +9,10 @@ from langchain.agents.load_tools import (
from langchain.tools.json.tool import JsonSpec from langchain.tools.json.tool import JsonSpec
from langflow.interface.importing.utils import import_class 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} 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__} OTHER_TOOLS = {tool: import_class(f"langchain.tools.{tool}") for tool in tools.__all__}

View file

@ -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 pydantic import BaseModel, validator
from langflow.utils import validate from langflow.utils import validate
from langchain.agents.tools import Tool
class Function(BaseModel): class Function(BaseModel):
code: str code: str
function: Optional[Callable] = None
imports: Optional[str] = None imports: Optional[str] = None
# Eval code and store the function # Eval code and store the function
@ -24,14 +25,17 @@ class Function(BaseModel):
return v 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 PythonFunctionTool(Function, Tool):
class PythonFunction(Function):
"""Python function""" """Python function"""
name: str = "Custom Tool"
description: str
code: 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)

View file

@ -59,11 +59,33 @@ class ToolNode(FrontendNode):
return super().to_dict() return super().to_dict()
class PythonFunctionNode(FrontendNode): class PythonFunctionToolNode(FrontendNode):
name: str = "PythonFunction" name: str = "PythonFunctionTool"
template: Template = Template( template: Template = Template(
type_name="python_function", type_name="PythonFunctionTool",
fields=[ 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( TemplateField(
field_type="code", field_type="code",
required=True, required=True,
@ -73,11 +95,11 @@ class PythonFunctionNode(FrontendNode):
value=DEFAULT_PYTHON_FUNCTION, value=DEFAULT_PYTHON_FUNCTION,
name="code", name="code",
advanced=False, advanced=False,
) ),
], ],
) )
description: str = "Python function to be executed." description: str = "Python function to be executed."
base_classes: list[str] = ["function"] base_classes: list[str] = ["Tool"]
def to_dict(self): def to_dict(self):
return super().to_dict() return super().to_dict()

View file

@ -1,16 +1,23 @@
# Test this: # Test this:
from langflow.interface.importing.utils import get_function
import pytest import pytest
from langflow.interface.tools.custom import PythonFunction from langflow.interface.tools.custom import PythonFunctionTool
from langflow.utils import constants from langflow.utils import constants
def test_python_function(): def test_python_function():
"""Test Python function""" """Test Python function"""
func = PythonFunction(code=constants.DEFAULT_PYTHON_FUNCTION) code = constants.DEFAULT_PYTHON_FUNCTION
assert func.get_function()("text") == "text" 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 tool decorator should raise an error if
# the function is not str -> str # the function is not str -> str
# This raises ValidationError # This raises ValidationError
with pytest.raises(SyntaxError): 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
)

View file

@ -156,7 +156,9 @@ def test_get_node_neighbors_complex(complex_graph):
tool_neighbors = complex_graph.get_nodes_with_target(tool) tool_neighbors = complex_graph.get_nodes_with_target(tool)
assert tool_neighbors is not None assert tool_neighbors is not None
# Check if there is a PythonFunction in the tool's neighbors # 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): def test_get_node(basic_graph):