From f4b51514a1793b95a5b77ac4f295c97e68d5d15d Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Wed, 29 Mar 2023 16:12:59 -0300 Subject: [PATCH 01/59] refac: change graph module structure --- src/backend/langflow/graph/__init__.py | 4 + src/backend/langflow/graph/base.py | 213 +++++++++++++++ src/backend/langflow/graph/graph.py | 301 +--------------------- src/backend/langflow/graph/nodes.py | 99 +++++++ src/backend/langflow/interface/loading.py | 2 +- tests/test_graph.py | 2 +- tests/test_loading.py | 2 +- 7 files changed, 331 insertions(+), 292 deletions(-) create mode 100644 src/backend/langflow/graph/base.py create mode 100644 src/backend/langflow/graph/nodes.py diff --git a/src/backend/langflow/graph/__init__.py b/src/backend/langflow/graph/__init__.py index e69de29bb..3afa92b80 100644 --- a/src/backend/langflow/graph/__init__.py +++ b/src/backend/langflow/graph/__init__.py @@ -0,0 +1,4 @@ +from langflow.graph.graph import Graph +from langflow.graph.base import Node, Edge + +__all__ = ["Graph", "Node", "Edge"] diff --git a/src/backend/langflow/graph/base.py b/src/backend/langflow/graph/base.py new file mode 100644 index 000000000..65e81f934 --- /dev/null +++ b/src/backend/langflow/graph/base.py @@ -0,0 +1,213 @@ +# Description: Graph class for building a graph of nodes and edges +# Insights: +# - Defer prompts building to the last moment or when they have all the tools +# - Build each inner agent first, then build the outer agent + +from copy import deepcopy +import types +from typing import Any, Dict, List, Optional, Union +from langflow.utils import payload +from langflow.interface.listing import ALL_TYPES_DICT, ALL_TOOLS_NAMES, TOOLS_DICT +from langflow.interface import loading + + +class Node: + def __init__(self, data: Dict): + self.id: str = data["id"] + self._data = data + self.edges: List[Edge] = [] + self._parse_data() + self._built_object = None + self._built = False + + def _parse_data(self) -> None: + self.data = self._data["data"] + self.output = self.data["node"]["base_classes"] + template_dicts = { + key: value + for key, value in self.data["node"]["template"].items() + if isinstance(value, dict) + } + + self.required_inputs = [ + template_dicts[key]["type"] + for key, value in template_dicts.items() + if value["required"] + ] + self.optional_inputs = [ + template_dicts[key]["type"] + for key, value in template_dicts.items() + if not value["required"] + ] + + template_dict = self.data["node"]["template"] + self.node_type = ( + self.data["type"] if "Tool" not in self.output else template_dict["_type"] + ) + + def _build_params(self): + # Some params are required, some are optional + # but most importantly, some params are python base classes + # like str and others are LangChain objects like LLMChain, BasePromptTemplate + # so we need to be able to distinguish between the two + + # The dicts with "type" == "str" are the ones that are python base classes + # and most likely have a "value" key + + # So for each key besides "_type" in the template dict, we have a dict + # with a "type" key. If the type is not "str", then we need to get the + # edge that connects to that node and get the Node with the required data + # and use that as the value for the param + # If the type is "str", then we need to get the value of the "value" key + # and use that as the value for the param + template_dict = { + key: value + for key, value in self.data["node"]["template"].items() + if isinstance(value, dict) + } + params = {} + for key, value in template_dict.items(): + if key == "_type": + continue + # If the type is not transformable to a python base class + # then we need to get the edge that connects to this node + if value["type"] not in ["str", "bool", "code"]: + # Get the edge that connects to this node + edge = next( + ( + edge + for edge in self.edges + if edge.target == self and edge.matched_type in value["type"] + ), + None, + ) + # Get the output of the node that the edge connects to + # if the value['list'] is True, then there will be more + # than one time setting to params[key] + # so we need to append to a list if it exists + # or create a new list if it doesn't + if edge is None and value["required"]: + # break line + raise ValueError( + f"Required input {key} for module {self.node_type} not found" + ) + if value["list"]: + if key in params: + params[key].append(edge.source) + else: + params[key] = [edge.source] + elif value["required"] or edge is not None: + params[key] = edge.source + elif value["required"] or value.get("value"): + params[key] = value["value"] + + # Add _type to params + self.params = params + + def _build(self): + # The params dict is used to build the module + # it contains values and keys that point to nodes which + # have their own params dict + # When build is called, we iterate through the params dict + # and if the value is a node, we call build on that node + # and use the output of that build as the value for the param + # if the value is not a node, then we use the value as the param + # and continue + # Another aspect is that the node_type is the class that we need to import + # and instantiate with these built params + + # Build each node in the params dict + for key, value in self.params.items(): + # Check if Node or list of Nodes + if isinstance(value, Node): + result = value.build() + # If the key is "func", then we need to use the run method + if key == "func" and not isinstance(result, types.FunctionType): + # func can be PythonFunction(code='\ndef upper_case(text: str) -> str:\n return text.upper()\n') + # so we need to check if there is an attribute called run + if hasattr(result, "run"): + result = result.run # type: ignore + elif hasattr(result, "get_function"): + result = result.get_function() # type: ignore + self.params[key] = result + elif isinstance(value, list) and all( + isinstance(node, Node) for node in value + ): + self.params[key] = [node.build() for node in value] # type: ignore + + # Get the class from LANGCHAIN_TYPES_DICT + # and instantiate it with the params + # and return the instance + for base_type, value in ALL_TYPES_DICT.items(): + if base_type == "tools": + value = TOOLS_DICT + + if self.node_type in value: + self._built_object = loading.instantiate_class( + node_type=self.node_type, + base_type=base_type, + params=self.params, + ) + break + + if self._built_object is None: + raise ValueError(f"Node type {self.node_type} not found") + + self._built = True + + def build(self, force: bool = False) -> Any: + if not self._built or force: + self._build() + return deepcopy(self._built_object) + + def add_edge(self, edge: "Edge") -> None: + self.edges.append(edge) + + def __repr__(self) -> str: + return f"Node(id={self.id}, data={self.data})" + + def __eq__(self, __o: object) -> bool: + return self.id == __o.id if isinstance(__o, Node) else False + + def __hash__(self) -> int: + return id(self) + + +class Edge: + def __init__(self, source: "Node", target: "Node"): + self.source: "Node" = source + self.target: "Node" = target + self.validate_edge() + + def validate_edge(self) -> None: + # Validate that the outputs of the source node are valid inputs + # for the target node + self.source_types = self.source.output + self.target_reqs = self.target.required_inputs + self.target.optional_inputs + # Both lists contain strings and sometimes a string contains the value we are + # looking for e.g. comgin_out=["Chain"] and target_reqs=["LLMChain"] + # so we need to check if any of the strings in source_types is in target_reqs + self.valid = any( + output in target_req + for output in self.source_types + for target_req in self.target_reqs + ) + # Get what type of input the target node is expecting + + self.matched_type = next( + ( + output + for output in self.source_types + for target_req in self.target_reqs + if output in target_req + ), + None, + ) + + def __repr__(self) -> str: + return ( + f"Edge(source={self.source.id}, target={self.target.id}, valid={self.valid}" + f", matched_type={self.matched_type})" + ) + + diff --git a/src/backend/langflow/graph/graph.py b/src/backend/langflow/graph/graph.py index 8e3c06ef5..91b225986 100644 --- a/src/backend/langflow/graph/graph.py +++ b/src/backend/langflow/graph/graph.py @@ -1,296 +1,17 @@ -# Description: Graph class for building a graph of nodes and edges -# Insights: -# - Defer prompts building to the last moment or when they have all the tools -# - Build each inner agent first, then build the outer agent - from copy import deepcopy import types -from typing import Any, Dict, List, Optional, Union +from typing import Dict, List, Union from langflow.utils import payload -from langflow.interface.listing import ALL_TYPES_DICT, ALL_TOOLS_NAMES, TOOLS_DICT -from langflow.interface import loading +from langflow.interface.listing import ALL_TOOLS_NAMES - -class Node: - def __init__(self, data: Dict): - self.id: str = data["id"] - self._data = data - self.edges: List[Edge] = [] - self._parse_data() - self._built_object = None - self._built = False - - def _parse_data(self) -> None: - self.data = self._data["data"] - self.output = self.data["node"]["base_classes"] - template_dicts = { - key: value - for key, value in self.data["node"]["template"].items() - if isinstance(value, dict) - } - - self.required_inputs = [ - template_dicts[key]["type"] - for key, value in template_dicts.items() - if value["required"] - ] - self.optional_inputs = [ - template_dicts[key]["type"] - for key, value in template_dicts.items() - if not value["required"] - ] - - template_dict = self.data["node"]["template"] - self.node_type = ( - self.data["type"] if "Tool" not in self.output else template_dict["_type"] - ) - - def _build_params(self): - # Some params are required, some are optional - # but most importantly, some params are python base classes - # like str and others are LangChain objects like LLMChain, BasePromptTemplate - # so we need to be able to distinguish between the two - - # The dicts with "type" == "str" are the ones that are python base classes - # and most likely have a "value" key - - # So for each key besides "_type" in the template dict, we have a dict - # with a "type" key. If the type is not "str", then we need to get the - # edge that connects to that node and get the Node with the required data - # and use that as the value for the param - # If the type is "str", then we need to get the value of the "value" key - # and use that as the value for the param - template_dict = { - key: value - for key, value in self.data["node"]["template"].items() - if isinstance(value, dict) - } - params = {} - for key, value in template_dict.items(): - if key == "_type": - continue - # If the type is not transformable to a python base class - # then we need to get the edge that connects to this node - if value["type"] not in ["str", "bool", "code"]: - # Get the edge that connects to this node - edge = next( - ( - edge - for edge in self.edges - if edge.target == self and edge.matched_type in value["type"] - ), - None, - ) - # Get the output of the node that the edge connects to - # if the value['list'] is True, then there will be more - # than one time setting to params[key] - # so we need to append to a list if it exists - # or create a new list if it doesn't - if edge is None and value["required"]: - # break line - raise ValueError( - f"Required input {key} for module {self.node_type} not found" - ) - if value["list"]: - if key in params: - params[key].append(edge.source) - else: - params[key] = [edge.source] - elif value["required"] or edge is not None: - params[key] = edge.source - elif value["required"] or value.get("value"): - params[key] = value["value"] - - # Add _type to params - self.params = params - - def _build(self): - # The params dict is used to build the module - # it contains values and keys that point to nodes which - # have their own params dict - # When build is called, we iterate through the params dict - # and if the value is a node, we call build on that node - # and use the output of that build as the value for the param - # if the value is not a node, then we use the value as the param - # and continue - # Another aspect is that the node_type is the class that we need to import - # and instantiate with these built params - - # Build each node in the params dict - for key, value in self.params.items(): - # Check if Node or list of Nodes - if isinstance(value, Node): - result = value.build() - # If the key is "func", then we need to use the run method - if key == "func" and not isinstance(result, types.FunctionType): - # func can be PythonFunction(code='\ndef upper_case(text: str) -> str:\n return text.upper()\n') - # so we need to check if there is an attribute called run - if hasattr(result, "run"): - result = result.run # type: ignore - elif hasattr(result, "get_function"): - result = result.get_function() # type: ignore - self.params[key] = result - elif isinstance(value, list) and all( - isinstance(node, Node) for node in value - ): - self.params[key] = [node.build() for node in value] # type: ignore - - # Get the class from LANGCHAIN_TYPES_DICT - # and instantiate it with the params - # and return the instance - for base_type, value in ALL_TYPES_DICT.items(): - if base_type == "tools": - value = TOOLS_DICT - - if self.node_type in value: - self._built_object = loading.instantiate_class( - node_type=self.node_type, - base_type=base_type, - params=self.params, - ) - break - - if self._built_object is None: - raise ValueError(f"Node type {self.node_type} not found") - - self._built = True - - def build(self, force: bool = False) -> Any: - if not self._built or force: - self._build() - return deepcopy(self._built_object) - - def add_edge(self, edge: "Edge") -> None: - self.edges.append(edge) - - def __repr__(self) -> str: - return f"Node(id={self.id}, data={self.data})" - - def __eq__(self, __o: object) -> bool: - return self.id == __o.id if isinstance(__o, Node) else False - - def __hash__(self) -> int: - return id(self) - - -class AgentNode(Node): - def __init__(self, data: Dict): - super().__init__(data) - self.tools: List[ToolNode] = [] - self.chains: List[ChainNode] = [] - - def _set_tools_and_chains(self) -> None: - for edge in self.edges: - source_node = edge.source - if isinstance(source_node, ToolNode): - self.tools.append(source_node) - elif isinstance(source_node, ChainNode): - self.chains.append(source_node) - - def build(self, force: bool = False) -> Any: - if not self._built or force: - self._set_tools_and_chains() - # First, build the tools - for tool_node in self.tools: - tool_node.build() - - # Next, build the chains and the rest - for chain_node in self.chains: - chain_node.build(tools=self.tools) - - self._build() - return deepcopy(self._built_object) - - -class Edge: - def __init__(self, source: "Node", target: "Node"): - self.source: "Node" = source - self.target: "Node" = target - self.validate_edge() - - def validate_edge(self) -> None: - # Validate that the outputs of the source node are valid inputs - # for the target node - self.source_types = self.source.output - self.target_reqs = self.target.required_inputs + self.target.optional_inputs - # Both lists contain strings and sometimes a string contains the value we are - # looking for e.g. comgin_out=["Chain"] and target_reqs=["LLMChain"] - # so we need to check if any of the strings in source_types is in target_reqs - self.valid = any( - output in target_req - for output in self.source_types - for target_req in self.target_reqs - ) - # Get what type of input the target node is expecting - - self.matched_type = next( - ( - output - for output in self.source_types - for target_req in self.target_reqs - if output in target_req - ), - None, - ) - - def __repr__(self) -> str: - return ( - f"Edge(source={self.source.id}, target={self.target.id}, valid={self.valid}" - f", matched_type={self.matched_type})" - ) - - -class ToolNode(Node): - def __init__(self, data: Dict): - super().__init__(data) - - def build(self, force: bool = False) -> Any: - if not self._built or force: - self._build() - return deepcopy(self._built_object) - - -class PromptNode(Node): - def __init__(self, data: Dict): - super().__init__(data) - - def build( - self, - force: bool = False, - tools: Optional[Union[List[Node], List[ToolNode]]] = None, - ) -> Any: - if not self._built or force: - # Check if it is a ZeroShotPrompt and needs a tool - if self.node_type == "ZeroShotPrompt": - tools = ( - [tool_node.build() for tool_node in tools] - if tools is not None - else [] - ) - self.params["tools"] = tools - - self._build() - return deepcopy(self._built_object) - - -class ChainNode(Node): - def __init__(self, data: Dict): - super().__init__(data) - - def build( - self, - force: bool = False, - tools: Optional[Union[List[Node], List[ToolNode]]] = None, - ) -> Any: - if not self._built or force: - # Check if the chain requires a PromptNode - for key, value in self.params.items(): - if isinstance(value, PromptNode): - # Build the PromptNode, passing the tools if available - self.params[key] = value.build(tools=tools, force=force) - - self._build() - return deepcopy(self._built_object) +from langflow.graph.base import Node, Edge +from langflow.graph.nodes import ( + AgentNode, + ChainNode, + PromptNode, + ToolkitNode, + ToolNode, +) class Graph: @@ -373,6 +94,8 @@ class Graph: nodes.append(ChainNode(node)) elif "tool" in node_type.lower() or node_lc_type in ALL_TOOLS_NAMES: nodes.append(ToolNode(node)) + elif "toolkit" in node_type.lower(): + nodes.append(ToolkitNode(node)) else: nodes.append(Node(node)) return nodes diff --git a/src/backend/langflow/graph/nodes.py b/src/backend/langflow/graph/nodes.py new file mode 100644 index 000000000..963b6cb45 --- /dev/null +++ b/src/backend/langflow/graph/nodes.py @@ -0,0 +1,99 @@ +from copy import deepcopy +import types +from typing import Any, Dict, List, Optional, Union + +from langflow.interface.listing import ALL_TYPES_DICT, TOOLS_DICT +from langflow.interface import loading +from langflow.graph.base import Node + + +class AgentNode(Node): + def __init__(self, data: Dict): + super().__init__(data) + self.tools: List[ToolNode] = [] + self.chains: List[ChainNode] = [] + + def _set_tools_and_chains(self) -> None: + for edge in self.edges: + source_node = edge.source + if isinstance(source_node, ToolNode): + self.tools.append(source_node) + elif isinstance(source_node, ChainNode): + self.chains.append(source_node) + + def build(self, force: bool = False) -> Any: + if not self._built or force: + self._set_tools_and_chains() + # First, build the tools + for tool_node in self.tools: + tool_node.build() + + # Next, build the chains and the rest + for chain_node in self.chains: + chain_node.build(tools=self.tools) + + self._build() + return deepcopy(self._built_object) + + +class ToolNode(Node): + def __init__(self, data: Dict): + super().__init__(data) + + def build(self, force: bool = False) -> Any: + if not self._built or force: + self._build() + return deepcopy(self._built_object) + + +class PromptNode(Node): + def __init__(self, data: Dict): + super().__init__(data) + + def build( + self, + force: bool = False, + tools: Optional[Union[List[Node], List[ToolNode]]] = None, + ) -> Any: + if not self._built or force: + # Check if it is a ZeroShotPrompt and needs a tool + if self.node_type == "ZeroShotPrompt": + tools = ( + [tool_node.build() for tool_node in tools] + if tools is not None + else [] + ) + self.params["tools"] = tools + + self._build() + return deepcopy(self._built_object) + + +class ChainNode(Node): + def __init__(self, data: Dict): + super().__init__(data) + + def build( + self, + force: bool = False, + tools: Optional[Union[List[Node], List[ToolNode]]] = None, + ) -> Any: + if not self._built or force: + # Check if the chain requires a PromptNode + for key, value in self.params.items(): + if isinstance(value, PromptNode): + # Build the PromptNode, passing the tools if available + self.params[key] = value.build(tools=tools, force=force) + + self._build() + return deepcopy(self._built_object) + + +class ToolkitNode(Node): + def __init__(self, data: Dict): + super().__init__(data) + + def build(self, force: bool = False) -> Any: + if not self._built or force: + self._build() + return deepcopy(self._built_object) diff --git a/src/backend/langflow/interface/loading.py b/src/backend/langflow/interface/loading.py index 1c73368e9..d9ed2552a 100644 --- a/src/backend/langflow/interface/loading.py +++ b/src/backend/langflow/interface/loading.py @@ -53,7 +53,7 @@ def instantiate_class(node_type: str, base_type: str, params: Dict) -> Any: def load_flow_from_json(path: str): # This is done to avoid circular imports - from langflow.graph.graph import Graph + from langflow.graph import Graph """Load flow from json file""" with open(path, "r") as f: diff --git a/tests/test_graph.py b/tests/test_graph.py index dfb0e2323..43669d457 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -1,5 +1,5 @@ import json -from langflow.graph.graph import Edge, Graph, Node +from langflow.graph import Edge, Node, Graph import pytest from langflow.utils.payload import build_json, get_root_node from langchain.agents import AgentExecutor diff --git a/tests/test_loading.py b/tests/test_loading.py index a824ec4e5..b85d45e86 100644 --- a/tests/test_loading.py +++ b/tests/test_loading.py @@ -1,5 +1,5 @@ import json -from langflow.graph.graph import Graph +from langflow.graph import Graph import pytest from langflow import load_flow_from_json From c9df633328f40397b017d2f2e5d41681421d6b0a Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Wed, 29 Mar 2023 16:15:23 -0300 Subject: [PATCH 02/59] fix: remove unused file --- src/backend/langflow/graph/utils.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/backend/langflow/graph/utils.py diff --git a/src/backend/langflow/graph/utils.py b/src/backend/langflow/graph/utils.py deleted file mode 100644 index e69de29bb..000000000 From 5f701d46c8a5c6bf6338695d0855bb201927c4de Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Wed, 29 Mar 2023 20:26:05 -0300 Subject: [PATCH 03/59] feat: adding wrappers and toolkits --- src/frontend/src/utils.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/frontend/src/utils.ts b/src/frontend/src/utils.ts index 796cd6de4..dc59c6640 100644 --- a/src/frontend/src/utils.ts +++ b/src/frontend/src/utils.ts @@ -5,8 +5,10 @@ import { LightBulbIcon, CommandLineIcon, WrenchScrewdriverIcon, + WrenchIcon, ComputerDesktopIcon, Bars3CenterLeftIcon, + GiftIcon, PaperClipIcon, QuestionMarkCircleIcon, } from "@heroicons/react/24/outline"; @@ -88,6 +90,8 @@ export const nodeNames:{[char: string]: string} = { advanced: "Advanced", chat: "Chat", docloaders:"Document Loader", + toolkits:"Toolkits", + wrappers:"Wrappers", unknown:"Unknown" }; @@ -97,10 +101,12 @@ export const nodeIcons:{[char: string]: React.ForwardRefExoticComponent Date: Wed, 29 Mar 2023 20:26:38 -0300 Subject: [PATCH 04/59] feat: adding broken modules for testing --- .../langflow/interface/custom_lists.py | 23 +++++- src/backend/langflow/interface/listing.py | 26 ++++++- src/backend/langflow/interface/signature.py | 33 +++++++++ src/backend/langflow/interface/types.py | 14 ++++ src/backend/langflow/utils/util.py | 70 +++++++++++++++++-- 5 files changed, 155 insertions(+), 11 deletions(-) diff --git a/src/backend/langflow/interface/custom_lists.py b/src/backend/langflow/interface/custom_lists.py index 3e8a800f9..2cf57cc39 100644 --- a/src/backend/langflow/interface/custom_lists.py +++ b/src/backend/langflow/interface/custom_lists.py @@ -1,8 +1,11 @@ ## LLM from typing import Any -from langchain import llms +from langchain import llms, requests from langchain.llms.openai import OpenAIChat +from langchain.agents import agent_toolkits +from langflow.interface.importing.utils import import_class + llm_type_to_cls_dict = llms.type_to_cls_dict llm_type_to_cls_dict["openai-chat"] = OpenAIChat @@ -41,3 +44,21 @@ memory_type_to_cls_dict: dict[str, Any] = { # chain_type_to_cls_dict = type_to_loader_dict # chain_type_to_cls_dict["conversation_chain"] = ConversationChain + +toolkit_type_to_loader_dict: dict[str, Any] = { + toolkit_name: import_class(f"langchain.agents.agent_toolkits.{toolkit_name}") + # if toolkit_name is lower case it is a loader + for toolkit_name in agent_toolkits.__all__ + if toolkit_name.islower() +} + +toolkit_type_to_cls_dict: dict[str, Any] = { + toolkit_name: import_class(f"langchain.agents.agent_toolkits.{toolkit_name}") + # if toolkit_name is not lower case it is a class + for toolkit_name in agent_toolkits.__all__ + if not toolkit_name.islower() +} + +wrapper_type_to_cls_dict: dict[str, Any] = { + wrapper.__name__: wrapper for wrapper in [requests.RequestsWrapper] +} diff --git a/src/backend/langflow/interface/listing.py b/src/backend/langflow/interface/listing.py index 53d34e2a5..fcd44c6b0 100644 --- a/src/backend/langflow/interface/listing.py +++ b/src/backend/langflow/interface/listing.py @@ -1,5 +1,6 @@ from langchain import agents, chains, prompts - +from langchain.agents import agent_toolkits +from langchain import requests from langflow.custom import customs from langflow.interface.custom_lists import ( llm_type_to_cls_dict, @@ -10,11 +11,14 @@ from langflow.utils import util from langchain.agents.load_tools import get_all_tool_names from langchain.agents import Tool from langflow.interface.custom_types import PythonFunction +from langchain.tools.json.tool import JsonSpec - +OTHER_TOOLS = {"JsonSpec": JsonSpec} CUSTOM_TOOLS = {"Tool": Tool, "PythonFunction": PythonFunction} TOOLS_DICT = util.get_tools_dict() -ALL_TOOLS_NAMES = set(get_all_tool_names() + list(CUSTOM_TOOLS.keys())) +ALL_TOOLS_NAMES = set( + get_all_tool_names() + list(CUSTOM_TOOLS.keys()) + list(OTHER_TOOLS.keys()) +) def get_type_dict(): @@ -25,6 +29,8 @@ def get_type_dict(): "llms": list_llms, "tools": list_tools, "memories": list_memories, + "toolkits": list_toolkis, + "wrappers": list_wrappers, } @@ -33,6 +39,11 @@ def list_type(object_type: str): return get_type_dict().get(object_type, lambda: None)() +def list_wrappers(): + """List all wrapper types""" + return [requests.RequestsWrapper.__name__] + + def list_agents(): """List all agent types""" return [ @@ -42,6 +53,11 @@ def list_agents(): ] +def list_toolkis(): + """List all toolkit types""" + return agent_toolkits.__all__ + + def list_prompts(): """List all prompt types""" custom_prompts = customs.get_custom_nodes("prompts") @@ -60,6 +76,10 @@ def list_tools(): for tool in ALL_TOOLS_NAMES: tool_params = util.get_tool_params(util.get_tool_by_name(tool)) + + if "name" not in tool_params: + tool_params["name"] = tool + if tool_params and ( tool_params.get("name") in settings.tools or (tool_params.get("name") and settings.dev) diff --git a/src/backend/langflow/interface/signature.py b/src/backend/langflow/interface/signature.py index 390f64671..1426af4e1 100644 --- a/src/backend/langflow/interface/signature.py +++ b/src/backend/langflow/interface/signature.py @@ -12,7 +12,11 @@ from langflow.custom import customs from langflow.interface.custom_lists import ( llm_type_to_cls_dict, memory_type_to_cls_dict, + toolkit_type_to_cls_dict, + toolkit_type_to_loader_dict, + wrapper_type_to_cls_dict, ) + from langflow.interface.listing import CUSTOM_TOOLS, ALL_TOOLS_NAMES from langflow.template.template import Field, Template from langflow.utils import util @@ -21,15 +25,44 @@ from langflow.utils import util def get_signature(name: str, object_type: str): """Get the signature of an object.""" return { + "toolkits": get_toolkit_signature, "chains": get_chain_signature, "agents": get_agent_signature, "prompts": get_prompt_signature, "llms": get_llm_signature, # "memories": get_memory_signature, "tools": get_tool_signature, + "wrappers": get_wrapper_signature, }.get(object_type, lambda name: f"Invalid type: {name}")(name) +def get_toolkit_signature(name: str): + """Get the signature of a toolkit.""" + try: + if name.islower(): + pass + # return util.build_template_from_function( + # name, toolkit_type_to_loader_dict, add_function=True + # ) + else: + return util.build_template_from_class( + name, toolkit_type_to_cls_dict, add_function=True + ) + except ValueError as exc: + raise ValueError("Toolkit not found") from exc + + +def get_wrapper_signature(name: str): + """Get the signature of a wrapper.""" + try: + return util.build_template_from_class( + name, + wrapper_type_to_cls_dict, + ) + except ValueError as exc: + raise ValueError("Wrapper not found") from exc + + def get_chain_signature(name: str): """Get the chain type by signature.""" try: diff --git a/src/backend/langflow/interface/types.py b/src/backend/langflow/interface/types.py index 595addb50..89f9877ca 100644 --- a/src/backend/langflow/interface/types.py +++ b/src/backend/langflow/interface/types.py @@ -33,4 +33,18 @@ def build_langchain_types_dict(): for memory in list_type("memories") }, "tools": {tool: get_signature(tool, "tools") for tool in list_type("tools")}, + "toolkits": get_toolkits(), + "wrappers": { + wrapper: get_signature(wrapper, "wrappers") + for wrapper in list_type("wrappers") + }, } + + +def get_toolkits(): + """Get a list of all toolkits""" + result = {} + for toolkit in list_type("toolkits"): + if sig := get_signature(toolkit, "toolkits"): + result[toolkit] = sig + return result diff --git a/src/backend/langflow/utils/util.py b/src/backend/langflow/utils/util.py index 664630890..652291992 100644 --- a/src/backend/langflow/utils/util.py +++ b/src/backend/langflow/utils/util.py @@ -3,7 +3,6 @@ import importlib import inspect import re from typing import Dict, Optional, Union - from langchain.agents.load_tools import ( _BASE_TOOLS, _EXTRA_LLM_TOOLS, @@ -18,6 +17,49 @@ from langchain.agents.tools import Tool from langflow.utils import constants +def build_template_from_parameters( + name: str, type_to_loader_dict: Dict, add_function: bool = False +): + # Retrieve the function that matches the provided name + func = None + for _, v in type_to_loader_dict.items(): + if v.__name__ == name: + func = v + break + + if func is None: + raise ValueError(f"{name} not found") + + # Process parameters + parameters = func.__annotations__ + variables = {} + for param_name, param_type in parameters.items(): + if param_name in ["return", "kwargs"]: + continue + + variables[param_name] = { + "type": param_type.__name__, + "default": parameters[param_name].__repr_args__()[0][1], + # Op + "placeholder": "", + } + + # Get the base classes of the return type + return_type = parameters.get("return") + base_classes = get_base_classes(return_type) if return_type else [] + if add_function: + base_classes.append("function") + + # Get the function's docstring + docs = inspect.getdoc(func) or "" + + return { + "template": format_dict(variables, name), + "description": docs["Description"], + "base_classes": base_classes, + } + + def build_template_from_function( name: str, type_to_loader_dict: Dict, add_function: bool = False ): @@ -37,7 +79,7 @@ def build_template_from_function( variables = {"_type": _type} for class_field_items, value in _class.__fields__.items(): - if class_field_items in ["callback_manager", "requests_wrapper"]: + if class_field_items in ["callback_manager"]: continue variables[class_field_items] = {} for name_, value_ in value.__repr_args__(): @@ -150,7 +192,7 @@ def get_default_factory(module: str, function: str): def get_tools_dict(): """Get the tools dictionary.""" - from langflow.interface.listing import CUSTOM_TOOLS + from langflow.interface.listing import CUSTOM_TOOLS, OTHER_TOOLS tools = { **_BASE_TOOLS, @@ -158,6 +200,7 @@ def get_tools_dict(): **{k: v[0] for k, v in _EXTRA_LLM_TOOLS.items()}, **{k: v[0] for k, v in _EXTRA_OPTIONAL_TOOLS.items()}, **CUSTOM_TOOLS, + **OTHER_TOOLS, } return tools @@ -170,15 +213,15 @@ def get_tool_by_name(name: str): return tools[name] -def get_tool_params(tool, **kwargs) -> Union[Dict, None]: +def get_tool_params(tool, **kwargs) -> Dict: # Parse the function code into an abstract syntax tree # Define if it is a function or a class if inspect.isfunction(tool): - return get_func_tool_params(tool, **kwargs) + return get_func_tool_params(tool, **kwargs) or {} elif inspect.isclass(tool): # Get the parameters necessary to # instantiate the class - return get_class_tool_params(tool, **kwargs) + return get_class_tool_params(tool, **kwargs) or {} else: raise ValueError("Tool must be a function or class.") @@ -373,7 +416,20 @@ def format_dict(d, name: Optional[str] = None): ) # Add multline - value["multiline"] = key in ["suffix", "prefix", "template", "examples", "code"] + value["multiline"] = key in [ + "suffix", + "prefix", + "template", + "examples", + "code", + "headers", + ] + + # Replace dict type with str + if "dict" in value["type"].lower(): + value["type"] = "str" + + value["file"] = key in ["dict_"] # Replace default value with actual value if "default" in value: From 0d10c7ba05240c9ccf6eb44dc6f31f23b756b662 Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Wed, 29 Mar 2023 20:27:28 -0300 Subject: [PATCH 05/59] fix: adding deps --- poetry.lock | 111 ++++++++++++++++++++++--------- pyproject.toml | 2 +- src/backend/langflow/config.yaml | 3 + 3 files changed, 83 insertions(+), 33 deletions(-) diff --git a/poetry.lock b/poetry.lock index 73235eb67..0e8e77b2d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -239,37 +239,37 @@ lxml = ["lxml"] [[package]] name = "black" -version = "23.1.0" +version = "23.3.0" description = "The uncompromising code formatter." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "black-23.1.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221"}, - {file = "black-23.1.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26"}, - {file = "black-23.1.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b"}, - {file = "black-23.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104"}, - {file = "black-23.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074"}, - {file = "black-23.1.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27"}, - {file = "black-23.1.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648"}, - {file = "black-23.1.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958"}, - {file = "black-23.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a"}, - {file = "black-23.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481"}, - {file = "black-23.1.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad"}, - {file = "black-23.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8"}, - {file = "black-23.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24"}, - {file = "black-23.1.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6"}, - {file = "black-23.1.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd"}, - {file = "black-23.1.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580"}, - {file = "black-23.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468"}, - {file = "black-23.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753"}, - {file = "black-23.1.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651"}, - {file = "black-23.1.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06"}, - {file = "black-23.1.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739"}, - {file = "black-23.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9"}, - {file = "black-23.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555"}, - {file = "black-23.1.0-py3-none-any.whl", hash = "sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32"}, - {file = "black-23.1.0.tar.gz", hash = "sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, + {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, + {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, + {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, + {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, + {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, + {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, + {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, + {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, + {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, + {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, + {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, + {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, + {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, ] [package.dependencies] @@ -715,6 +715,18 @@ files = [ {file = "frozenlist-1.3.3.tar.gz", hash = "sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a"}, ] +[[package]] +name = "geojson" +version = "2.5.0" +description = "Python bindings and utilities for GeoJSON" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "geojson-2.5.0-py2.py3-none-any.whl", hash = "sha256:ccbd13368dd728f4e4f13ffe6aaf725b6e802c692ba0dde628be475040c534ba"}, + {file = "geojson-2.5.0.tar.gz", hash = "sha256:6e4bb7ace4226a45d9c8c8b1348b3fc43540658359f93c3f7e03efa9f15f658a"}, +] + [[package]] name = "google-api-core" version = "2.11.0" @@ -1183,14 +1195,14 @@ test = ["ipykernel", "pre-commit", "pytest", "pytest-cov", "pytest-timeout"] [[package]] name = "langchain" -version = "0.0.113" +version = "0.0.125" description = "Building applications with LLMs through composability" category = "main" optional = false python-versions = ">=3.8.1,<4.0" files = [ - {file = "langchain-0.0.113-py3-none-any.whl", hash = "sha256:9e146d116fd3b9b2210c8c447cabfa20ef27c26ea3f2bc986eab97d1dad0aab6"}, - {file = "langchain-0.0.113.tar.gz", hash = "sha256:a494fe02bc63da4bcda7da8d7f4a346522fbc87f0a4955b72519ec2ed86bf906"}, + {file = "langchain-0.0.125-py3-none-any.whl", hash = "sha256:678cf9d6b0d2b48fab574b5e6faa3bf6e9d249847f3956cf0970c7d48724ec43"}, + {file = "langchain-0.0.125.tar.gz", hash = "sha256:af54d190bd0ae8cab633c1b6a652c76aae685d6eb27ff39d3f9b24d27ba9f1af"}, ] [package.dependencies] @@ -1198,13 +1210,14 @@ aiohttp = ">=3.8.3,<4.0.0" dataclasses-json = ">=0.5.7,<0.6.0" numpy = ">=1,<2" pydantic = ">=1,<2" -PyYAML = ">=6,<7" +pyowm = ">=3.3.0,<4.0.0" +PyYAML = ">=5.4.1" requests = ">=2,<3" SQLAlchemy = ">=1,<2" tenacity = ">=8.1.0,<9.0.0" [package.extras] -all = ["aleph-alpha-client (>=2.15.0,<3.0.0)", "anthropic (>=0.2.2,<0.3.0)", "beautifulsoup4 (>=4,<5)", "cohere (>=3,<4)", "deeplake (>=3.2.9,<4.0.0)", "elasticsearch (>=8,<9)", "faiss-cpu (>=1,<2)", "google-api-python-client (==2.70.0)", "google-search-results (>=2,<3)", "huggingface_hub (>=0,<1)", "jinja2 (>=3,<4)", "manifest-ml (>=0.0.1,<0.0.2)", "networkx (>=2.6.3,<3.0.0)", "nlpcloud (>=1,<2)", "nltk (>=3,<4)", "nomic (>=1.0.43,<2.0.0)", "openai (>=0,<1)", "opensearch-py (>=2.0.0,<3.0.0)", "pgvector (>=0.1.6,<0.2.0)", "pinecone-client (>=2,<3)", "psycopg2-binary (>=2.9.5,<3.0.0)", "pypdf (>=3.4.0,<4.0.0)", "qdrant-client (>=1.0.4,<2.0.0)", "redis (>=4,<5)", "sentence-transformers (>=2,<3)", "spacy (>=3,<4)", "tensorflow-text (>=2.11.0,<3.0.0)", "tiktoken (>=0,<1)", "torch (>=1,<2)", "transformers (>=4,<5)", "weaviate-client (>=3,<4)", "wikipedia (>=1,<2)", "wolframalpha (==5.0.0)"] +all = ["aleph-alpha-client (>=2.15.0,<3.0.0)", "anthropic (>=0.2.2,<0.3.0)", "beautifulsoup4 (>=4,<5)", "cohere (>=3,<4)", "deeplake (>=3.2.9,<4.0.0)", "elasticsearch (>=8,<9)", "faiss-cpu (>=1,<2)", "google-api-python-client (==2.70.0)", "google-search-results (>=2,<3)", "huggingface_hub (>=0,<1)", "jina (>=3.14,<4.0)", "jinja2 (>=3,<4)", "manifest-ml (>=0.0.1,<0.0.2)", "networkx (>=2.6.3,<3.0.0)", "nlpcloud (>=1,<2)", "nltk (>=3,<4)", "nomic (>=1.0.43,<2.0.0)", "openai (>=0,<1)", "opensearch-py (>=2.0.0,<3.0.0)", "pgvector (>=0.1.6,<0.2.0)", "pinecone-client (>=2,<3)", "psycopg2-binary (>=2.9.5,<3.0.0)", "pypdf (>=3.4.0,<4.0.0)", "qdrant-client (>=1.0.4,<2.0.0)", "redis (>=4,<5)", "sentence-transformers (>=2,<3)", "spacy (>=3,<4)", "tensorflow-text (>=2.11.0,<3.0.0)", "tiktoken (>=0.3.2,<0.4.0)", "torch (>=1,<2)", "transformers (>=4,<5)", "weaviate-client (>=3,<4)", "wikipedia (>=1,<2)", "wolframalpha (==5.0.0)"] llms = ["anthropic (>=0.2.2,<0.3.0)", "cohere (>=3,<4)", "huggingface_hub (>=0,<1)", "manifest-ml (>=0.0.1,<0.0.2)", "nlpcloud (>=1,<2)", "openai (>=0,<1)", "torch (>=1,<2)", "transformers (>=4,<5)"] [[package]] @@ -1809,6 +1822,26 @@ files = [ [package.extras] plugins = ["importlib-metadata"] +[[package]] +name = "pyowm" +version = "3.3.0" +description = "A Python wrapper around OpenWeatherMap web APIs" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyowm-3.3.0-py3-none-any.whl", hash = "sha256:86463108e7613171531ba306040b43c972b3fc0b0acf73b12c50910cdd2107ab"}, + {file = "pyowm-3.3.0.tar.gz", hash = "sha256:8196f77c91eac680676ed5ee484aae8a165408055e3e2b28025cbf60b8681e03"}, +] + +[package.dependencies] +geojson = ">=2.3.0,<3" +PySocks = ">=1.7.1,<2" +requests = [ + {version = ">=2.20.0,<3"}, + {version = "*", extras = ["socks"]}, +] + [[package]] name = "pyparsing" version = "3.0.9" @@ -1824,6 +1857,19 @@ files = [ [package.extras] diagrams = ["jinja2", "railroad-diagrams"] +[[package]] +name = "pysocks" +version = "1.7.1" +description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, + {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, + {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, +] + [[package]] name = "pytest" version = "7.2.2" @@ -2043,6 +2089,7 @@ files = [ certifi = ">=2017.4.17" charset-normalizer = ">=2,<4" idna = ">=2.5,<4" +PySocks = {version = ">=1.5.6,<1.5.7 || >1.5.7", optional = true, markers = "extra == \"socks\""} urllib3 = ">=1.21.1,<1.27" [package.extras] @@ -2635,4 +2682,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "18b858c93c242f3b53e9f77284904aa0eabb4c955f905cfe5fb227a6785bfabc" +content-hash = "2c201e79c486802be55495286b288ea79caa4bec2dc74a8eca90030116b6f8a6" diff --git a/pyproject.toml b/pyproject.toml index c7d2d8767..b67f87595 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ google-search-results = "^2.4.1" google-api-python-client = "^2.79.0" typer = "^0.7.0" gunicorn = "^20.1.0" -langchain = "^0.0.113" +langchain = "^0.0.125" openai = "^0.27.2" types-pyyaml = "^6.0.12.8" diff --git a/src/backend/langflow/config.yaml b/src/backend/langflow/config.yaml index 301d89a34..4e3c3e6b7 100644 --- a/src/backend/langflow/config.yaml +++ b/src/backend/langflow/config.yaml @@ -22,8 +22,11 @@ tools: - Serper Search - Tool - PythonFunction + - JsonSpec memories: # - ConversationBufferMemory + + dev: false From 48995bc3958dff55e63a07de4b11e9c14f59d0df Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Wed, 29 Mar 2023 20:27:39 -0300 Subject: [PATCH 06/59] implemented inputFile component --- .../components/parameterComponent/index.tsx | 16 ++++- .../components/inputFileComponent/index.tsx | 58 +++++++++++++++++++ src/frontend/src/contexts/tabsContext.tsx | 3 +- src/frontend/src/types/flow/index.ts | 1 + 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 src/frontend/src/components/inputFileComponent/index.tsx diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index c4b4346fa..203907ff1 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -11,6 +11,7 @@ import { ParameterComponentType } from "../../../../types/components"; import FloatComponent from "../../../../components/floatComponent"; import Dropdown from "../../../../components/dropdownComponent"; import CodeAreaComponent from "../../../../components/codeAreaComponent"; +import InputFileComponent from "../../../../components/inputFileComponent"; export default function ParameterComponent({ left, @@ -54,7 +55,12 @@ export default function ParameterComponent({ {title} {required ? " *" : ""} - {left && (type === "str" || type === "bool" || type === "float"||type=="code") ? ( + {left && + (type === "str" || + type === "bool" || + type === "float" || + type === "code" || + type === "file") ? ( <> ) : ( @@ -148,6 +154,14 @@ export default function ParameterComponent({ data.node.template[name].value = t; }} /> + ) : left === true && type === "file" ? ( + { + data.node.template[name].value = t; + }} + > ) : ( <> )} diff --git a/src/frontend/src/components/inputFileComponent/index.tsx b/src/frontend/src/components/inputFileComponent/index.tsx new file mode 100644 index 000000000..c1dfae10c --- /dev/null +++ b/src/frontend/src/components/inputFileComponent/index.tsx @@ -0,0 +1,58 @@ +import { DocumentMagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { useContext, useEffect, useState } from "react"; +import { TextAreaComponentType } from "../../types/components"; + +export default function InputFileComponent({ + value, + onChange, + disabled, +}: TextAreaComponentType) { + const [myValue, setMyValue] = useState(value); + useEffect(() => { + if (disabled) { + setMyValue(""); + onChange(""); + } + }, [disabled, onChange]); + + const handleButtonClick = () => { + const input = document.createElement("input"); + input.type = "file"; + // input.accept = ".yaml"; + input.style.display = "none"; + input.onchange = (e: Event) => { + const file = (e.target as HTMLInputElement).files?.[0]; + //check file type + // file.name.endsWith(".yaml") + if (file) { + setMyValue(file.name); + onChange(file.name); + } + }; + input.click(); + }; + + return ( +
+
+ + {myValue !== "" ? myValue : "No file"} + + +
+
+ ); +} diff --git a/src/frontend/src/contexts/tabsContext.tsx b/src/frontend/src/contexts/tabsContext.tsx index 10c0b943e..1c75a1466 100644 --- a/src/frontend/src/contexts/tabsContext.tsx +++ b/src/frontend/src/contexts/tabsContext.tsx @@ -16,7 +16,7 @@ const TabsContextInitialValue: TabsContextType = { uploadFlow: () => {}, lockChat: false, setLockChat:(prevState:boolean)=>{}, - hardReset:()=>{} + hardReset:()=>{}, }; export const TabsContext = createContext( @@ -142,6 +142,7 @@ export function TabsProvider({ children }: { children: ReactNode }) { id: id.toString(), data, chat: flow ? flow.chat : [], + files:{} }; // Increment the ID counter. diff --git a/src/frontend/src/types/flow/index.ts b/src/frontend/src/types/flow/index.ts index 50b0cab7a..90dc6ac5f 100644 --- a/src/frontend/src/types/flow/index.ts +++ b/src/frontend/src/types/flow/index.ts @@ -8,6 +8,7 @@ export type FlowType = { data: ReactFlowJsonObject; chat: Array; description:string; + files:{[char: string]: string}; }; export type NodeType = {id:string,type:string,position:XYPosition,data:NodeDataType} export type NodeDataType = {type:string,node?:APIClassType,id:string,value:any} \ No newline at end of file From a38d71bc4f87dcc42370a16c6da3f119f9ccd974 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Wed, 29 Mar 2023 21:09:24 -0300 Subject: [PATCH 07/59] ui ready, need to implement dinamic file type check --- .../components/parameterComponent/index.tsx | 4 ++-- .../src/CustomNodes/GenericNode/index.tsx | 1 + .../components/codeAreaComponent/index.tsx | 2 +- .../components/inputFileComponent/index.tsx | 23 +++++++++++-------- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 203907ff1..ab4ae8dca 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -154,10 +154,10 @@ export default function ParameterComponent({ data.node.template[name].value = t; }} /> - ) : left === true && type === "file" ? ( + ) : (left === true && type === "file")||data.type==="JsonSpec" ? ( { data.node.template[name].value = t; }} diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index 6e3aa6e67..368b3c2ed 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -22,6 +22,7 @@ export default function GenericNode({ const showError = useRef(true); const { types, deleteNode } = useContext(typesContext); const Icon = nodeIcons[types[data.type]]; + console.log(data) if (!Icon) { if (showError.current) { setErrorData({ diff --git a/src/frontend/src/components/codeAreaComponent/index.tsx b/src/frontend/src/components/codeAreaComponent/index.tsx index 95aa720b0..756296970 100644 --- a/src/frontend/src/components/codeAreaComponent/index.tsx +++ b/src/frontend/src/components/codeAreaComponent/index.tsx @@ -23,7 +23,7 @@ export default function CodeAreaComponent({
diff --git a/src/frontend/src/components/inputFileComponent/index.tsx b/src/frontend/src/components/inputFileComponent/index.tsx index c1dfae10c..f8a9f942e 100644 --- a/src/frontend/src/components/inputFileComponent/index.tsx +++ b/src/frontend/src/components/inputFileComponent/index.tsx @@ -1,5 +1,6 @@ import { DocumentMagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { useContext, useEffect, useState } from "react"; +import { alertContext } from "../../contexts/alertContext"; import { TextAreaComponentType } from "../../types/components"; export default function InputFileComponent({ @@ -8,6 +9,7 @@ export default function InputFileComponent({ disabled, }: TextAreaComponentType) { const [myValue, setMyValue] = useState(value); + const { setErrorData } = useContext(alertContext); useEffect(() => { if (disabled) { setMyValue(""); @@ -18,15 +20,20 @@ export default function InputFileComponent({ const handleButtonClick = () => { const input = document.createElement("input"); input.type = "file"; - // input.accept = ".yaml"; + input.accept = ".json"; input.style.display = "none"; + input.multiple = false; input.onchange = (e: Event) => { const file = (e.target as HTMLInputElement).files?.[0]; - //check file type - // file.name.endsWith(".yaml") - if (file) { + if (file && file.name.endsWith(".json")) { setMyValue(file.name); onChange(file.name); + } else { + setErrorData({ + title: + "Please select a valid file. Only files this files are allowed:", + list: ["*.json"], + }); } }; input.click(); @@ -41,16 +48,14 @@ export default function InputFileComponent({
{myValue !== "" ? myValue : "No file"} -
From 7529892ebc1a7a214806371f038d193f54b6a684 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Wed, 29 Mar 2023 22:51:46 -0300 Subject: [PATCH 08/59] file component almost ready, need to implemente backend connection --- .../GenericNode/components/parameterComponent/index.tsx | 4 +++- src/frontend/src/CustomNodes/GenericNode/index.tsx | 1 - src/frontend/src/components/inputFileComponent/index.tsx | 9 +++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index ab4ae8dca..aaaade2fa 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -159,7 +159,9 @@ export default function ParameterComponent({ disabled={disabled} value={data.node.template[name]?.value ?? ""} onChange={(t: string) => { - data.node.template[name].value = t; + if(data.node.template[name]?.value){ + data.node.template[name].value = t; + } }} >
) : ( diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index 368b3c2ed..6e3aa6e67 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -22,7 +22,6 @@ export default function GenericNode({ const showError = useRef(true); const { types, deleteNode } = useContext(typesContext); const Icon = nodeIcons[types[data.type]]; - console.log(data) if (!Icon) { if (showError.current) { setErrorData({ diff --git a/src/frontend/src/components/inputFileComponent/index.tsx b/src/frontend/src/components/inputFileComponent/index.tsx index f8a9f942e..b7a8a0bf0 100644 --- a/src/frontend/src/components/inputFileComponent/index.tsx +++ b/src/frontend/src/components/inputFileComponent/index.tsx @@ -17,6 +17,12 @@ export default function InputFileComponent({ } }, [disabled, onChange]); + function attachFile(fileReadEvent: ProgressEvent) { + fileReadEvent.preventDefault(); + const file = fileReadEvent.target.result; + console.log(file); + } + const handleButtonClick = () => { const input = document.createElement("input"); input.type = "file"; @@ -25,7 +31,10 @@ export default function InputFileComponent({ input.multiple = false; input.onchange = (e: Event) => { const file = (e.target as HTMLInputElement).files?.[0]; + const fileData = new FileReader(); + fileData.onload = attachFile; if (file && file.name.endsWith(".json")) { + fileData.readAsDataURL(file); setMyValue(file.name); onChange(file.name); } else { From 119585b2f09df62211d81907501e071ffae273df Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Wed, 29 Mar 2023 22:52:40 -0300 Subject: [PATCH 09/59] removed file section from flow structure --- src/frontend/src/contexts/tabsContext.tsx | 1 - src/frontend/src/types/flow/index.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/frontend/src/contexts/tabsContext.tsx b/src/frontend/src/contexts/tabsContext.tsx index 1c75a1466..6626ff0cc 100644 --- a/src/frontend/src/contexts/tabsContext.tsx +++ b/src/frontend/src/contexts/tabsContext.tsx @@ -142,7 +142,6 @@ export function TabsProvider({ children }: { children: ReactNode }) { id: id.toString(), data, chat: flow ? flow.chat : [], - files:{} }; // Increment the ID counter. diff --git a/src/frontend/src/types/flow/index.ts b/src/frontend/src/types/flow/index.ts index 90dc6ac5f..50b0cab7a 100644 --- a/src/frontend/src/types/flow/index.ts +++ b/src/frontend/src/types/flow/index.ts @@ -8,7 +8,6 @@ export type FlowType = { data: ReactFlowJsonObject; chat: Array; description:string; - files:{[char: string]: string}; }; export type NodeType = {id:string,type:string,position:XYPosition,data:NodeDataType} export type NodeDataType = {type:string,node?:APIClassType,id:string,value:any} \ No newline at end of file From 22c3c83d6a98187e8b2c598b6da697d0771c9ec2 Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Thu, 30 Mar 2023 08:48:41 -0300 Subject: [PATCH 10/59] feat: jsonagent --- src/backend/langflow/custom/customs.py | 1 + .../langflow/interface/custom_lists.py | 1 + .../langflow/interface/custom_types.py | 34 +++++++++++++++++++ src/backend/langflow/interface/listing.py | 4 +-- src/backend/langflow/interface/signature.py | 4 ++- src/backend/langflow/template/nodes.py | 32 +++++++++++++++++ src/backend/langflow/utils/util.py | 8 ++++- 7 files changed, 80 insertions(+), 4 deletions(-) diff --git a/src/backend/langflow/custom/customs.py b/src/backend/langflow/custom/customs.py index 877a06387..c856996a3 100644 --- a/src/backend/langflow/custom/customs.py +++ b/src/backend/langflow/custom/customs.py @@ -4,6 +4,7 @@ from langflow.template import nodes CUSTOM_NODES = { "prompts": {**nodes.ZeroShotPromptNode().to_dict()}, "tools": {**nodes.PythonFunctionNode().to_dict(), **nodes.ToolNode().to_dict()}, + "agents": {**nodes.JsonAgentNode().to_dict()}, } diff --git a/src/backend/langflow/interface/custom_lists.py b/src/backend/langflow/interface/custom_lists.py index 2cf57cc39..9875a8b9d 100644 --- a/src/backend/langflow/interface/custom_lists.py +++ b/src/backend/langflow/interface/custom_lists.py @@ -59,6 +59,7 @@ toolkit_type_to_cls_dict: dict[str, Any] = { if not toolkit_name.islower() } + wrapper_type_to_cls_dict: dict[str, Any] = { wrapper.__name__: wrapper for wrapper in [requests.RequestsWrapper] } diff --git a/src/backend/langflow/interface/custom_types.py b/src/backend/langflow/interface/custom_types.py index 05d77fd1d..7943f99f0 100644 --- a/src/backend/langflow/interface/custom_types.py +++ b/src/backend/langflow/interface/custom_types.py @@ -1,6 +1,12 @@ from typing import Callable, Optional +from langchain import LLMChain, PromptTemplate +from langchain.agents import AgentExecutor, ZeroShotAgent from langflow.utils import validate from pydantic import BaseModel, validator +from langchain.agents.agent_toolkits.json.prompt import JSON_PREFIX, JSON_SUFFIX +from langchain.agents.mrkl.prompt import FORMAT_INSTRUCTIONS +from langchain.agents.agent_toolkits.json.toolkit import JsonToolkit +from langchain.schema import BaseLanguageModel class Function(BaseModel): @@ -33,3 +39,31 @@ class PythonFunction(Function): """Python function""" code: str + + +class JsonAgent(BaseModel): + """Json agent""" + + toolkit: JsonToolkit + llm: BaseLanguageModel + + def __init__(self, toolkit: JsonToolkit, llm: BaseLanguageModel): + super().__init__(toolkit=toolkit, llm=llm) + self.toolkit = toolkit + tools = self.toolkit.get_tools() + tool_names = [tool.name for tool in tools] + prompt = ZeroShotAgent.create_prompt( + tools, + prefix=JSON_PREFIX, + suffix=JSON_SUFFIX, + format_instructions=FORMAT_INSTRUCTIONS, + input_variables=None, + ) + llm_chain = LLMChain( + llm=llm, + prompt=prompt, + ) + agent = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names) + return AgentExecutor.from_agent_and_tools( + agent=agent, tools=tools, verbose=True + ) diff --git a/src/backend/langflow/interface/listing.py b/src/backend/langflow/interface/listing.py index fcd44c6b0..c67b3ab83 100644 --- a/src/backend/langflow/interface/listing.py +++ b/src/backend/langflow/interface/listing.py @@ -10,7 +10,7 @@ from langflow.settings import settings from langflow.utils import util from langchain.agents.load_tools import get_all_tool_names from langchain.agents import Tool -from langflow.interface.custom_types import PythonFunction +from langflow.interface.custom_types import JsonAgent, PythonFunction from langchain.tools.json.tool import JsonSpec OTHER_TOOLS = {"JsonSpec": JsonSpec} @@ -50,7 +50,7 @@ def list_agents(): agent.__name__ for agent in agents.loading.AGENT_TO_CLASS.values() if agent.__name__ in settings.agents or settings.dev - ] + ] + [JsonAgent.__name__] def list_toolkis(): diff --git a/src/backend/langflow/interface/signature.py b/src/backend/langflow/interface/signature.py index 1426af4e1..80a544d31 100644 --- a/src/backend/langflow/interface/signature.py +++ b/src/backend/langflow/interface/signature.py @@ -40,7 +40,7 @@ def get_toolkit_signature(name: str): """Get the signature of a toolkit.""" try: if name.islower(): - pass + ... # return util.build_template_from_function( # name, toolkit_type_to_loader_dict, add_function=True # ) @@ -77,6 +77,8 @@ def get_chain_signature(name: str): def get_agent_signature(name: str): """Get the signature of an agent.""" try: + if name in customs.get_custom_nodes("agents").keys(): + return customs.get_custom_nodes("agents")[name] return util.build_template_from_class( name, agents.loading.AGENT_TO_CLASS, add_function=True ) diff --git a/src/backend/langflow/template/nodes.py b/src/backend/langflow/template/nodes.py index 642d0c237..f9826d89f 100644 --- a/src/backend/langflow/template/nodes.py +++ b/src/backend/langflow/template/nodes.py @@ -112,3 +112,35 @@ class ToolNode(FrontendNode): def to_dict(self): return super().to_dict() + + +class JsonAgentNode(FrontendNode): + name: str = "JsonAgent" + template: Template = Template( + type_name="json_agent", + fields=[ + Field( + field_type="BaseToolkit", + required=True, + placeholder="", + is_list=False, + show=True, + value="", + name="toolkit", + ), + Field( + field_type="BaseLanguageModel", + required=True, + placeholder="", + is_list=False, + show=True, + value="", + name="LLM", + ), + ], + ) + description: str = """Construct a json agent from an LLM and tools.""" + base_classes: list[str] = ["BaseAgent"] + + def to_dict(self): + return super().to_dict() diff --git a/src/backend/langflow/utils/util.py b/src/backend/langflow/utils/util.py index 652291992..c7dfc230e 100644 --- a/src/backend/langflow/utils/util.py +++ b/src/backend/langflow/utils/util.py @@ -406,6 +406,7 @@ def format_dict(d, name: Optional[str] = None): "examples", "temperature", "model_name", + "headers", ] or "api_key" in key ) @@ -427,7 +428,7 @@ def format_dict(d, name: Optional[str] = None): # Replace dict type with str if "dict" in value["type"].lower(): - value["type"] = "str" + value["type"] = "code" value["file"] = key in ["dict_"] @@ -436,6 +437,11 @@ def format_dict(d, name: Optional[str] = None): value["value"] = value["default"] value.pop("default") + if key == "headers": + value[ + "value" + ] = """{'Authorization': + 'Bearer '}""" # Add options to openai if name == "OpenAI" and key == "model_name": value["options"] = constants.OPENAI_MODELS From e049438cc5afba94ae7398048dbf157fd18339f9 Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Thu, 30 Mar 2023 15:44:31 -0300 Subject: [PATCH 11/59] refact: change folder structure --- src/backend/langflow/interface/agents/__init__.py | 3 +++ src/backend/langflow/interface/{agents.py => agents/base.py} | 0 src/backend/langflow/interface/chains/__init__.py | 3 +++ src/backend/langflow/interface/{chains.py => chains/base.py} | 0 src/backend/langflow/interface/llms/__init__.py | 3 +++ src/backend/langflow/interface/{llms.py => llms/base.py} | 0 src/backend/langflow/interface/memories/__init__.py | 3 +++ .../langflow/interface/{memories.py => memories/base.py} | 0 src/backend/langflow/interface/prompts/__init__.py | 3 +++ src/backend/langflow/interface/{prompts.py => prompts/base.py} | 0 src/backend/langflow/interface/tools/__init__.py | 3 +++ src/backend/langflow/interface/{tools.py => tools/base.py} | 0 src/backend/langflow/interface/types.py | 2 +- 13 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 src/backend/langflow/interface/agents/__init__.py rename src/backend/langflow/interface/{agents.py => agents/base.py} (100%) create mode 100644 src/backend/langflow/interface/chains/__init__.py rename src/backend/langflow/interface/{chains.py => chains/base.py} (100%) create mode 100644 src/backend/langflow/interface/llms/__init__.py rename src/backend/langflow/interface/{llms.py => llms/base.py} (100%) create mode 100644 src/backend/langflow/interface/memories/__init__.py rename src/backend/langflow/interface/{memories.py => memories/base.py} (100%) create mode 100644 src/backend/langflow/interface/prompts/__init__.py rename src/backend/langflow/interface/{prompts.py => prompts/base.py} (100%) create mode 100644 src/backend/langflow/interface/tools/__init__.py rename src/backend/langflow/interface/{tools.py => tools/base.py} (100%) diff --git a/src/backend/langflow/interface/agents/__init__.py b/src/backend/langflow/interface/agents/__init__.py new file mode 100644 index 000000000..df15bc39b --- /dev/null +++ b/src/backend/langflow/interface/agents/__init__.py @@ -0,0 +1,3 @@ +from langflow.interface.agents.base import AgentCreator + +__all__ = ["AgentCreator"] diff --git a/src/backend/langflow/interface/agents.py b/src/backend/langflow/interface/agents/base.py similarity index 100% rename from src/backend/langflow/interface/agents.py rename to src/backend/langflow/interface/agents/base.py diff --git a/src/backend/langflow/interface/chains/__init__.py b/src/backend/langflow/interface/chains/__init__.py new file mode 100644 index 000000000..2e5570b3c --- /dev/null +++ b/src/backend/langflow/interface/chains/__init__.py @@ -0,0 +1,3 @@ +from langflow.interface.chains.base import ChainCreator + +__all__ = ["ChainCreator"] diff --git a/src/backend/langflow/interface/chains.py b/src/backend/langflow/interface/chains/base.py similarity index 100% rename from src/backend/langflow/interface/chains.py rename to src/backend/langflow/interface/chains/base.py diff --git a/src/backend/langflow/interface/llms/__init__.py b/src/backend/langflow/interface/llms/__init__.py new file mode 100644 index 000000000..c5d7186fb --- /dev/null +++ b/src/backend/langflow/interface/llms/__init__.py @@ -0,0 +1,3 @@ +from langflow.interface.llms.base import LLMCreator + +__all__ = ["LLMCreator"] diff --git a/src/backend/langflow/interface/llms.py b/src/backend/langflow/interface/llms/base.py similarity index 100% rename from src/backend/langflow/interface/llms.py rename to src/backend/langflow/interface/llms/base.py diff --git a/src/backend/langflow/interface/memories/__init__.py b/src/backend/langflow/interface/memories/__init__.py new file mode 100644 index 000000000..845eb29fe --- /dev/null +++ b/src/backend/langflow/interface/memories/__init__.py @@ -0,0 +1,3 @@ +from langflow.interface.memories.base import MemoryCreator + +__all__ = ["MemoryCreator"] diff --git a/src/backend/langflow/interface/memories.py b/src/backend/langflow/interface/memories/base.py similarity index 100% rename from src/backend/langflow/interface/memories.py rename to src/backend/langflow/interface/memories/base.py diff --git a/src/backend/langflow/interface/prompts/__init__.py b/src/backend/langflow/interface/prompts/__init__.py new file mode 100644 index 000000000..2a81e8bf0 --- /dev/null +++ b/src/backend/langflow/interface/prompts/__init__.py @@ -0,0 +1,3 @@ +from langflow.interface.prompts.base import PromptCreator + +__all__ = ["PromptCreator"] diff --git a/src/backend/langflow/interface/prompts.py b/src/backend/langflow/interface/prompts/base.py similarity index 100% rename from src/backend/langflow/interface/prompts.py rename to src/backend/langflow/interface/prompts/base.py diff --git a/src/backend/langflow/interface/tools/__init__.py b/src/backend/langflow/interface/tools/__init__.py new file mode 100644 index 000000000..148892e90 --- /dev/null +++ b/src/backend/langflow/interface/tools/__init__.py @@ -0,0 +1,3 @@ +from langflow.interface.tools.base import ToolCreator + +__all__ = ["ToolCreator"] diff --git a/src/backend/langflow/interface/tools.py b/src/backend/langflow/interface/tools/base.py similarity index 100% rename from src/backend/langflow/interface/tools.py rename to src/backend/langflow/interface/tools/base.py diff --git a/src/backend/langflow/interface/types.py b/src/backend/langflow/interface/types.py index 726c2b01d..633f31dfc 100644 --- a/src/backend/langflow/interface/types.py +++ b/src/backend/langflow/interface/types.py @@ -1,7 +1,7 @@ from langflow.interface.agents import AgentCreator from langflow.interface.listing import list_type from langflow.interface.llms import LLMCreator -from langflow.interface.memories import MemoryCreator +from langflow.interface.memories.base import MemoryCreator from langflow.interface.prompts import PromptCreator from langflow.interface.signature import get_signature from langchain import chains From 064cf8120f2f2e6fc95092daa476e4344e47680e Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Thu, 30 Mar 2023 16:34:52 -0300 Subject: [PATCH 12/59] fix: change typing for linting and formatting --- src/backend/langflow/graph/graph.py | 2 -- src/backend/langflow/graph/nodes.py | 3 --- src/backend/langflow/interface/signature.py | 1 - src/backend/langflow/settings.py | 16 ++++++++-------- src/backend/langflow/utils/util.py | 2 +- 5 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/backend/langflow/graph/graph.py b/src/backend/langflow/graph/graph.py index 91b225986..de8469641 100644 --- a/src/backend/langflow/graph/graph.py +++ b/src/backend/langflow/graph/graph.py @@ -1,5 +1,3 @@ -from copy import deepcopy -import types from typing import Dict, List, Union from langflow.utils import payload from langflow.interface.listing import ALL_TOOLS_NAMES diff --git a/src/backend/langflow/graph/nodes.py b/src/backend/langflow/graph/nodes.py index 963b6cb45..43350b3d9 100644 --- a/src/backend/langflow/graph/nodes.py +++ b/src/backend/langflow/graph/nodes.py @@ -1,9 +1,6 @@ from copy import deepcopy -import types from typing import Any, Dict, List, Optional, Union -from langflow.interface.listing import ALL_TYPES_DICT, TOOLS_DICT -from langflow.interface import loading from langflow.graph.base import Node diff --git a/src/backend/langflow/interface/signature.py b/src/backend/langflow/interface/signature.py index 80a544d31..f9de437a7 100644 --- a/src/backend/langflow/interface/signature.py +++ b/src/backend/langflow/interface/signature.py @@ -13,7 +13,6 @@ from langflow.interface.custom_lists import ( llm_type_to_cls_dict, memory_type_to_cls_dict, toolkit_type_to_cls_dict, - toolkit_type_to_loader_dict, wrapper_type_to_cls_dict, ) diff --git a/src/backend/langflow/settings.py b/src/backend/langflow/settings.py index f4dd4ae30..2c8d3e8c6 100644 --- a/src/backend/langflow/settings.py +++ b/src/backend/langflow/settings.py @@ -1,18 +1,18 @@ import os -from typing import List, Optional +from typing import List import yaml from pydantic import BaseSettings, Field, root_validator class Settings(BaseSettings): - chains: Optional[List[str]] = Field(...) - agents: Optional[List[str]] = Field(...) - prompts: Optional[List[str]] = Field(...) - llms: Optional[List[str]] = Field(...) - tools: Optional[List[str]] = Field(...) - memories: Optional[List[str]] = Field(...) - dev: bool = Field(...) + chains: List[str] = Field(default=[]) + agents: List[str] = Field(default=[]) + prompts: List[str] = Field(default=[]) + llms: List[str] = Field(default=[]) + tools: List[str] = Field(default=[]) + memories: List[str] = Field(default=[]) + dev: bool = Field(default=False) class Config: validate_assignment = True diff --git a/src/backend/langflow/utils/util.py b/src/backend/langflow/utils/util.py index c7dfc230e..10048c831 100644 --- a/src/backend/langflow/utils/util.py +++ b/src/backend/langflow/utils/util.py @@ -55,7 +55,7 @@ def build_template_from_parameters( return { "template": format_dict(variables, name), - "description": docs["Description"], + "description": docs["Description"], # type: ignore "base_classes": base_classes, } From cfbd22d2b085f6438315b692ca8f5d69cb2309a3 Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Thu, 30 Mar 2023 16:35:08 -0300 Subject: [PATCH 13/59] fix: set up creators --- src/backend/langflow/graph/base.py | 5 ++--- src/backend/langflow/interface/agents/base.py | 3 +++ src/backend/langflow/interface/base.py | 12 ++++++----- src/backend/langflow/interface/chains/base.py | 5 +++-- .../langflow/interface/custom_types.py | 10 +++++++-- src/backend/langflow/interface/llms/base.py | 3 +++ .../langflow/interface/memories/base.py | 3 +++ .../langflow/interface/prompts/base.py | 3 +++ src/backend/langflow/interface/tools/base.py | 7 +++++-- src/backend/langflow/interface/types.py | 21 ++++++------------- 10 files changed, 43 insertions(+), 29 deletions(-) diff --git a/src/backend/langflow/graph/base.py b/src/backend/langflow/graph/base.py index 65e81f934..1b1fb7459 100644 --- a/src/backend/langflow/graph/base.py +++ b/src/backend/langflow/graph/base.py @@ -5,9 +5,8 @@ from copy import deepcopy import types -from typing import Any, Dict, List, Optional, Union -from langflow.utils import payload -from langflow.interface.listing import ALL_TYPES_DICT, ALL_TOOLS_NAMES, TOOLS_DICT +from typing import Any, Dict, List +from langflow.interface.listing import ALL_TYPES_DICT, TOOLS_DICT from langflow.interface import loading diff --git a/src/backend/langflow/interface/agents/base.py b/src/backend/langflow/interface/agents/base.py index b271e46b1..c1d08b551 100644 --- a/src/backend/langflow/interface/agents/base.py +++ b/src/backend/langflow/interface/agents/base.py @@ -26,3 +26,6 @@ class AgentCreator(LangChainTypeCreator): for agent in self.type_to_loader_dict.values() if agent.__name__ in settings.agents or settings.dev ] + + +agent_creator = AgentCreator() diff --git a/src/backend/langflow/interface/base.py b/src/backend/langflow/interface/base.py index 3ae5e7c08..4e1bd1fce 100644 --- a/src/backend/langflow/interface/base.py +++ b/src/backend/langflow/interface/base.py @@ -1,4 +1,4 @@ -from typing import Dict, List +from typing import Any, Dict, List, Optional from pydantic import BaseModel from abc import ABC, abstractmethod from langflow.template.template import Template, Field, FrontendNode @@ -16,23 +16,25 @@ class LangChainTypeCreator(BaseModel, ABC): pass @abstractmethod - def get_signature(self, name: str) -> Dict: + def get_signature(self, name: str) -> Optional[Dict[Any, Any]]: pass @abstractmethod def to_list(self) -> List[str]: pass - def to_dict(self): - result = {self.type_name: {}} # type: Dict + def to_dict(self) -> Dict: + result: Dict = {self.type_name: {}} for name in self.to_list(): - result[self.type_name][name] = self.get_signature(name) + result[self.type_name][name] = self.frontend_node(name).to_dict() return result def frontend_node(self, name) -> FrontendNode: signature = self.get_signature(name) + if signature is None: + raise ValueError(f"{name} not found") fields = [ Field( name=key, diff --git a/src/backend/langflow/interface/chains/base.py b/src/backend/langflow/interface/chains/base.py index 4fb323330..1e0d4fd57 100644 --- a/src/backend/langflow/interface/chains/base.py +++ b/src/backend/langflow/interface/chains/base.py @@ -1,7 +1,5 @@ from typing import Dict, List from langflow.interface.base import LangChainTypeCreator -from langflow.interface.signature import get_chain_signature -from langflow.template.template import Field, FrontendNode, Template from langflow.utils.util import build_template_from_function from langflow.settings import settings from langchain.chains import loading as chains_loading @@ -33,3 +31,6 @@ class ChainCreator(LangChainTypeCreator): or settings.dev ) ] + + +chain_creator = ChainCreator() diff --git a/src/backend/langflow/interface/custom_types.py b/src/backend/langflow/interface/custom_types.py index 7943f99f0..52d05b218 100644 --- a/src/backend/langflow/interface/custom_types.py +++ b/src/backend/langflow/interface/custom_types.py @@ -1,5 +1,5 @@ from typing import Callable, Optional -from langchain import LLMChain, PromptTemplate +from langchain import LLMChain from langchain.agents import AgentExecutor, ZeroShotAgent from langflow.utils import validate from pydantic import BaseModel, validator @@ -64,6 +64,12 @@ class JsonAgent(BaseModel): prompt=prompt, ) agent = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names) - return AgentExecutor.from_agent_and_tools( + self.agent_executor = AgentExecutor.from_agent_and_tools( agent=agent, tools=tools, verbose=True ) + + def __call__(self, *args, **kwargs): + return self.agent_executor(*args, **kwargs) + + def run(self, *args, **kwargs): + return self.agent_executor.run(*args, **kwargs) diff --git a/src/backend/langflow/interface/llms/base.py b/src/backend/langflow/interface/llms/base.py index 60dbdb313..02ca8457c 100644 --- a/src/backend/langflow/interface/llms/base.py +++ b/src/backend/langflow/interface/llms/base.py @@ -25,3 +25,6 @@ class LLMCreator(LangChainTypeCreator): for llm in self.type_to_loader_dict.values() if llm.__name__ in settings.llms or settings.dev ] + + +llm_creator = LLMCreator() diff --git a/src/backend/langflow/interface/memories/base.py b/src/backend/langflow/interface/memories/base.py index f14da7e70..f3dc7279d 100644 --- a/src/backend/langflow/interface/memories/base.py +++ b/src/backend/langflow/interface/memories/base.py @@ -25,3 +25,6 @@ class MemoryCreator(LangChainTypeCreator): for memory in self.type_to_loader_dict.values() if memory.__name__ in settings.memories or settings.dev ] + + +memory_creator = MemoryCreator() diff --git a/src/backend/langflow/interface/prompts/base.py b/src/backend/langflow/interface/prompts/base.py index 0b32b6313..b3f96f69e 100644 --- a/src/backend/langflow/interface/prompts/base.py +++ b/src/backend/langflow/interface/prompts/base.py @@ -30,3 +30,6 @@ class PromptCreator(LangChainTypeCreator): or settings.dev ] return library_prompts + list(custom_prompts.keys()) + + +prompt_creator = PromptCreator() diff --git a/src/backend/langflow/interface/tools/base.py b/src/backend/langflow/interface/tools/base.py index c0a68c691..9b9a072c1 100644 --- a/src/backend/langflow/interface/tools/base.py +++ b/src/backend/langflow/interface/tools/base.py @@ -18,7 +18,7 @@ class ToolCreator(LangChainTypeCreator): @property def type_to_loader_dict(self) -> Dict: - return ALL_TOOLS_NAMES + return util.get_tools_dict() def get_signature(self, name: str) -> Dict | None: """Get the signature of a tool.""" @@ -26,7 +26,7 @@ class ToolCreator(LangChainTypeCreator): NODE_INPUTS = ["llm", "func"] base_classes = ["Tool"] all_tools = {} - for tool in ALL_TOOLS_NAMES: + for tool in self.type_to_loader_dict.keys(): if tool_params := util.get_tool_params(util.get_tool_by_name(tool)): tool_name = tool_params.get("name") or str(tool) all_tools[tool_name] = {"type": tool, "params": tool_params} @@ -126,3 +126,6 @@ class ToolCreator(LangChainTypeCreator): # Add Tool custom_tools = customs.get_custom_nodes("tools") return tools + list(custom_tools.keys()) + + +tool_creator = ToolCreator() diff --git a/src/backend/langflow/interface/types.py b/src/backend/langflow/interface/types.py index 633f31dfc..f90ebe5a6 100644 --- a/src/backend/langflow/interface/types.py +++ b/src/backend/langflow/interface/types.py @@ -1,12 +1,9 @@ -from langflow.interface.agents import AgentCreator -from langflow.interface.listing import list_type -from langflow.interface.llms import LLMCreator -from langflow.interface.memories.base import MemoryCreator -from langflow.interface.prompts import PromptCreator -from langflow.interface.signature import get_signature -from langchain import chains -from langflow.interface.chains import ChainCreator -from langflow.interface.tools import ToolCreator +from langflow.interface.agents.base import agent_creator +from langflow.interface.llms.base import llm_creator +from langflow.interface.memories.base import memory_creator +from langflow.interface.prompts.base import prompt_creator +from langflow.interface.chains.base import chain_creator +from langflow.interface.tools.base import tool_creator def get_type_list(): @@ -23,12 +20,6 @@ def get_type_list(): def build_langchain_types_dict(): """Build a dictionary of all langchain types""" - chain_creator = ChainCreator() - agent_creator = AgentCreator() - prompt_creator = PromptCreator() - tool_creator = ToolCreator() - llm_creator = LLMCreator() - memory_creator = MemoryCreator() all_types = {} From 7c86f38fb3fa47270acb6a4ed24071ba0ed51b75 Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Thu, 30 Mar 2023 18:09:12 -0300 Subject: [PATCH 14/59] fix: settings now can be set from cli --- src/backend/langflow/__main__.py | 14 +++++++++++++- src/backend/langflow/settings.py | 32 +++++++++++++++++++++----------- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/backend/langflow/__main__.py b/src/backend/langflow/__main__.py index ea9386d7f..4171e4da6 100644 --- a/src/backend/langflow/__main__.py +++ b/src/backend/langflow/__main__.py @@ -2,6 +2,7 @@ import logging import multiprocessing import platform from pathlib import Path +from langflow.settings import settings import typer from fastapi.staticfiles import StaticFiles @@ -17,9 +18,20 @@ def get_number_of_workers(workers=None): return workers +def update_settings(config: str): + """Update the settings from a config file.""" + if config: + settings.update_from_yaml(config) + + def serve( - host: str = "127.0.0.1", workers: int = 1, timeout: int = 60, port: int = 7860 + host: str = "127.0.0.1", + workers: int = 1, + timeout: int = 60, + port: int = 7860, + config: str = "config.yaml", ): + update_settings(config) app = create_app() # get the directory of the current file path = Path(__file__).parent diff --git a/src/backend/langflow/settings.py b/src/backend/langflow/settings.py index 2c8d3e8c6..13ce50b2a 100644 --- a/src/backend/langflow/settings.py +++ b/src/backend/langflow/settings.py @@ -2,28 +2,39 @@ import os from typing import List import yaml -from pydantic import BaseSettings, Field, root_validator +from pydantic import BaseSettings, root_validator class Settings(BaseSettings): - chains: List[str] = Field(default=[]) - agents: List[str] = Field(default=[]) - prompts: List[str] = Field(default=[]) - llms: List[str] = Field(default=[]) - tools: List[str] = Field(default=[]) - memories: List[str] = Field(default=[]) - dev: bool = Field(default=False) + chains: List[str] = [] + agents: List[str] = [] + prompts: List[str] = [] + llms: List[str] = [] + tools: List[str] = [] + memories: List[str] = [] + dev: bool = False class Config: validate_assignment = True + extra = "ignore" - @root_validator + @root_validator(allow_reuse=True) def validate_lists(cls, values): for key, value in values.items(): if key != "dev" and not value: values[key] = [] return values + def update_from_yaml(self, file_path: str): + new_settings = load_settings_from_yaml(file_path) + self.chains = new_settings.chains or [] + self.agents = new_settings.agents or [] + self.prompts = new_settings.prompts or [] + self.llms = new_settings.llms or [] + self.tools = new_settings.tools or [] + self.memories = new_settings.memories or [] + self.dev = new_settings.dev or False + def save_settings_to_yaml(settings: Settings, file_path: str): with open(file_path, "w") as f: @@ -41,9 +52,8 @@ def load_settings_from_yaml(file_path: str) -> Settings: with open(file_path, "r") as f: settings_dict = yaml.safe_load(f) - a = Settings.parse_obj(settings_dict) - return a + return Settings(**settings_dict) settings = load_settings_from_yaml("config.yaml") From 18b3fa6c3404bd6e5b79db471f8dfa5b82142e5f Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Thu, 30 Mar 2023 18:13:35 -0300 Subject: [PATCH 15/59] refac: listing and signature for tools moved --- src/backend/langflow/api/list_endpoints.py | 58 ----- src/backend/langflow/api/signature.py | 63 ------ src/backend/langflow/config.yaml | 5 - src/backend/langflow/graph/base.py | 7 +- src/backend/langflow/graph/graph.py | 2 +- .../langflow/interface/importing/utils.py | 2 +- src/backend/langflow/interface/listing.py | 129 ++--------- src/backend/langflow/interface/signature.py | 204 ------------------ src/backend/langflow/interface/tools/base.py | 21 +- .../langflow/interface/tools/constants.py | 11 + src/backend/langflow/interface/tools/util.py | 122 +++++++++++ src/backend/langflow/main.py | 4 - src/backend/langflow/utils/util.py | 125 +---------- tests/test_endpoints.py | 2 +- 14 files changed, 169 insertions(+), 586 deletions(-) delete mode 100644 src/backend/langflow/api/list_endpoints.py delete mode 100644 src/backend/langflow/api/signature.py delete mode 100644 src/backend/langflow/interface/signature.py create mode 100644 src/backend/langflow/interface/tools/constants.py create mode 100644 src/backend/langflow/interface/tools/util.py diff --git a/src/backend/langflow/api/list_endpoints.py b/src/backend/langflow/api/list_endpoints.py deleted file mode 100644 index 15946a2db..000000000 --- a/src/backend/langflow/api/list_endpoints.py +++ /dev/null @@ -1,58 +0,0 @@ -from fastapi import APIRouter - -from langflow.interface.listing import list_type - -# build router -router = APIRouter( - prefix="/list", - tags=["list"], -) - - -@router.get("/") -def read_items(): - """List all components""" - return [ - "chains", - "agents", - "prompts", - "llms", - "tools", - ] - - -@router.get("/chains") -def list_chains(): - """List all chain types""" - return list_type("chains") - - -@router.get("/agents") -def list_agents(): - """List all agent types""" - # return list(agents.loading.AGENT_TO_CLASS.keys()) - return list_type("agents") - - -@router.get("/prompts") -def list_prompts(): - """List all prompt types""" - return list_type("prompts") - - -@router.get("/llms") -def list_llms(): - """List all llm types""" - return list_type("llms") - - -@router.get("/memories") -def list_memories(): - """List all memory types""" - return list_type("memories") - - -@router.get("/tools") -def list_tools(): - """List all load tools""" - return list_type("tools") diff --git a/src/backend/langflow/api/signature.py b/src/backend/langflow/api/signature.py deleted file mode 100644 index 96b654dbe..000000000 --- a/src/backend/langflow/api/signature.py +++ /dev/null @@ -1,63 +0,0 @@ -from fastapi import APIRouter, HTTPException - -from langflow.interface.signature import get_signature - -# build router -router = APIRouter( - prefix="/signatures", - tags=["signatures"], -) - - -@router.get("/chain") -def get_chain(name: str): - """Get the signature of a chain.""" - try: - return get_signature(name, "chains") - except ValueError as exc: - raise HTTPException(status_code=404, detail="Chain not found") from exc - - -@router.get("/agent") -def get_agent(name: str): - """Get the signature of an agent.""" - try: - return get_signature(name, "agents") - except ValueError as exc: - raise HTTPException(status_code=404, detail="Agent not found") from exc - - -@router.get("/prompt") -def get_prompt(name: str): - """Get the signature of a prompt.""" - try: - return get_signature(name, "prompts") - except ValueError as exc: - raise HTTPException(status_code=404, detail="Prompt not found") from exc - - -@router.get("/llm") -def get_llm(name: str): - """Get the signature of an llm.""" - try: - return get_signature(name, "llms") - except ValueError as exc: - raise HTTPException(status_code=404, detail="LLM not found") from exc - - -@router.get("/memory") -def get_memory(name: str): - """Get the signature of a memory.""" - try: - return get_signature(name, "memories") - except ValueError as exc: - raise HTTPException(status_code=404, detail="Memory not found") from exc - - -@router.get("/tool") -def get_tool(name: str): - """Get the signature of a tool.""" - try: - return get_signature(name, "tools") - except ValueError as exc: - raise HTTPException(status_code=404, detail="Tool not found") from exc diff --git a/src/backend/langflow/config.yaml b/src/backend/langflow/config.yaml index 4e3c3e6b7..8a7d6c00b 100644 --- a/src/backend/langflow/config.yaml +++ b/src/backend/langflow/config.yaml @@ -24,9 +24,4 @@ tools: - PythonFunction - JsonSpec -memories: - # - ConversationBufferMemory - - - dev: false diff --git a/src/backend/langflow/graph/base.py b/src/backend/langflow/graph/base.py index 1b1fb7459..ca0bdb823 100644 --- a/src/backend/langflow/graph/base.py +++ b/src/backend/langflow/graph/base.py @@ -6,8 +6,9 @@ from copy import deepcopy import types from typing import Any, Dict, List -from langflow.interface.listing import ALL_TYPES_DICT, TOOLS_DICT +from langflow.interface.listing import ALL_TYPES_DICT from langflow.interface import loading +from langflow.interface.tools.base import tool_creator class Node: @@ -139,7 +140,7 @@ class Node: # and return the instance for base_type, value in ALL_TYPES_DICT.items(): if base_type == "tools": - value = TOOLS_DICT + value = tool_creator.type_to_loader_dict if self.node_type in value: self._built_object = loading.instantiate_class( @@ -208,5 +209,3 @@ class Edge: f"Edge(source={self.source.id}, target={self.target.id}, valid={self.valid}" f", matched_type={self.matched_type})" ) - - diff --git a/src/backend/langflow/graph/graph.py b/src/backend/langflow/graph/graph.py index de8469641..e7110e977 100644 --- a/src/backend/langflow/graph/graph.py +++ b/src/backend/langflow/graph/graph.py @@ -1,6 +1,6 @@ from typing import Dict, List, Union from langflow.utils import payload -from langflow.interface.listing import ALL_TOOLS_NAMES +from langflow.interface.tools.constants import ALL_TOOLS_NAMES from langflow.graph.base import Node, Edge from langflow.graph.nodes import ( diff --git a/src/backend/langflow/interface/importing/utils.py b/src/backend/langflow/interface/importing/utils.py index 5524e2d32..33ea6f7d9 100644 --- a/src/backend/langflow/interface/importing/utils.py +++ b/src/backend/langflow/interface/importing/utils.py @@ -8,7 +8,7 @@ from langchain.agents import Agent from langchain.chains.base import Chain from langchain.llms.base import BaseLLM from langchain.tools import BaseTool -from langflow.utils.util import get_tool_by_name +from langflow.interface.tools.util import get_tool_by_name def import_module(module_path: str) -> Any: diff --git a/src/backend/langflow/interface/listing.py b/src/backend/langflow/interface/listing.py index c67b3ab83..c46c272a5 100644 --- a/src/backend/langflow/interface/listing.py +++ b/src/backend/langflow/interface/listing.py @@ -1,126 +1,23 @@ -from langchain import agents, chains, prompts -from langchain.agents import agent_toolkits -from langchain import requests -from langflow.custom import customs -from langflow.interface.custom_lists import ( - llm_type_to_cls_dict, - memory_type_to_cls_dict, -) -from langflow.settings import settings -from langflow.utils import util -from langchain.agents.load_tools import get_all_tool_names -from langchain.agents import Tool -from langflow.interface.custom_types import JsonAgent, PythonFunction -from langchain.tools.json.tool import JsonSpec - -OTHER_TOOLS = {"JsonSpec": JsonSpec} -CUSTOM_TOOLS = {"Tool": Tool, "PythonFunction": PythonFunction} -TOOLS_DICT = util.get_tools_dict() -ALL_TOOLS_NAMES = set( - get_all_tool_names() + list(CUSTOM_TOOLS.keys()) + list(OTHER_TOOLS.keys()) -) +from langflow.interface.agents.base import agent_creator +from langflow.interface.chains.base import chain_creator +from langflow.interface.llms.base import llm_creator +from langflow.interface.memories.base import memory_creator +from langflow.interface.prompts.base import prompt_creator +from langflow.interface.tools.base import tool_creator def get_type_dict(): return { - "chains": list_chain_types, - "agents": list_agents, - "prompts": list_prompts, - "llms": list_llms, - "tools": list_tools, - "memories": list_memories, - "toolkits": list_toolkis, - "wrappers": list_wrappers, + "agents": agent_creator.to_list(), + "prompts": prompt_creator.to_list(), + "llms": llm_creator.to_list(), + "tools": tool_creator.to_list(), + "chains": chain_creator.to_list(), + "memory": memory_creator.to_list(), } -def list_type(object_type: str): - """List all components""" - return get_type_dict().get(object_type, lambda: None)() - - -def list_wrappers(): - """List all wrapper types""" - return [requests.RequestsWrapper.__name__] - - -def list_agents(): - """List all agent types""" - return [ - agent.__name__ - for agent in agents.loading.AGENT_TO_CLASS.values() - if agent.__name__ in settings.agents or settings.dev - ] + [JsonAgent.__name__] - - -def list_toolkis(): - """List all toolkit types""" - return agent_toolkits.__all__ - - -def list_prompts(): - """List all prompt types""" - custom_prompts = customs.get_custom_nodes("prompts") - library_prompts = [ - prompt.__annotations__["return"].__name__ - for prompt in prompts.loading.type_to_loader_dict.values() - if prompt.__annotations__["return"].__name__ in settings.prompts or settings.dev - ] - return library_prompts + list(custom_prompts.keys()) - - -def list_tools(): - """List all load tools""" - - tools = [] - - for tool in ALL_TOOLS_NAMES: - tool_params = util.get_tool_params(util.get_tool_by_name(tool)) - - if "name" not in tool_params: - tool_params["name"] = tool - - if tool_params and ( - tool_params.get("name") in settings.tools - or (tool_params.get("name") and settings.dev) - ): - tools.append(tool_params["name"]) - - # Add Tool - custom_tools = customs.get_custom_nodes("tools") - return tools + list(custom_tools.keys()) - - -def list_llms(): - """List all llm types""" - return [ - llm.__name__ - for llm in llm_type_to_cls_dict.values() - if llm.__name__ in settings.llms or settings.dev - ] - - -def list_chain_types(): - """List all chain types""" - return [ - chain.__annotations__["return"].__name__ - for chain in chains.loading.type_to_loader_dict.values() - if chain.__annotations__["return"].__name__ in settings.chains or settings.dev - ] - - -def list_memories(): - """List all memory types""" - return [ - memory.__name__ - for memory in memory_type_to_cls_dict.values() - if memory.__name__ in settings.memories or settings.dev - ] - - -LANGCHAIN_TYPES_DICT = { - k: list_function() for k, list_function in get_type_dict().items() -} +LANGCHAIN_TYPES_DICT = get_type_dict() # Now we'll build a dict with Langchain types and ours diff --git a/src/backend/langflow/interface/signature.py b/src/backend/langflow/interface/signature.py deleted file mode 100644 index f9de437a7..000000000 --- a/src/backend/langflow/interface/signature.py +++ /dev/null @@ -1,204 +0,0 @@ -from typing import Any, Dict # noqa: F401 - -from langchain import agents, chains, prompts -from langchain.agents.load_tools import ( - _BASE_TOOLS, - _EXTRA_LLM_TOOLS, - _EXTRA_OPTIONAL_TOOLS, - _LLM_TOOLS, -) - -from langflow.custom import customs -from langflow.interface.custom_lists import ( - llm_type_to_cls_dict, - memory_type_to_cls_dict, - toolkit_type_to_cls_dict, - wrapper_type_to_cls_dict, -) - -from langflow.interface.listing import CUSTOM_TOOLS, ALL_TOOLS_NAMES -from langflow.template.template import Field, Template -from langflow.utils import util - - -def get_signature(name: str, object_type: str): - """Get the signature of an object.""" - return { - "toolkits": get_toolkit_signature, - "chains": get_chain_signature, - "agents": get_agent_signature, - "prompts": get_prompt_signature, - "llms": get_llm_signature, - # "memories": get_memory_signature, - "tools": get_tool_signature, - "wrappers": get_wrapper_signature, - }.get(object_type, lambda name: f"Invalid type: {name}")(name) - - -def get_toolkit_signature(name: str): - """Get the signature of a toolkit.""" - try: - if name.islower(): - ... - # return util.build_template_from_function( - # name, toolkit_type_to_loader_dict, add_function=True - # ) - else: - return util.build_template_from_class( - name, toolkit_type_to_cls_dict, add_function=True - ) - except ValueError as exc: - raise ValueError("Toolkit not found") from exc - - -def get_wrapper_signature(name: str): - """Get the signature of a wrapper.""" - try: - return util.build_template_from_class( - name, - wrapper_type_to_cls_dict, - ) - except ValueError as exc: - raise ValueError("Wrapper not found") from exc - - -def get_chain_signature(name: str): - """Get the chain type by signature.""" - try: - return util.build_template_from_function( - name, chains.loading.type_to_loader_dict, add_function=True - ) - - except ValueError as exc: - raise ValueError("Chain not found") from exc - - -def get_agent_signature(name: str): - """Get the signature of an agent.""" - try: - if name in customs.get_custom_nodes("agents").keys(): - return customs.get_custom_nodes("agents")[name] - return util.build_template_from_class( - name, agents.loading.AGENT_TO_CLASS, add_function=True - ) - except ValueError as exc: - raise ValueError("Agent not found") from exc - - -def get_prompt_signature(name: str): - """Get the signature of a prompt.""" - try: - if name in customs.get_custom_nodes("prompts").keys(): - return customs.get_custom_nodes("prompts")[name] - return util.build_template_from_function( - name, prompts.loading.type_to_loader_dict - ) - except ValueError as exc: - raise ValueError("Prompt not found") from exc - - -def get_llm_signature(name: str): - """Get the signature of an llm.""" - try: - return util.build_template_from_class(name, llm_type_to_cls_dict) - except ValueError as exc: - raise ValueError("LLM not found") from exc - - -def get_memory_signature(name: str): - """Get the signature of a memory.""" - try: - return util.build_template_from_class(name, memory_type_to_cls_dict) - except ValueError as exc: - raise ValueError("Memory not found") from exc - - -def get_tool_signature(name: str): - """Get the signature of a tool.""" - - NODE_INPUTS = ["llm", "func"] - base_classes = ["Tool"] - all_tools = {} - for tool in ALL_TOOLS_NAMES: - if tool_params := util.get_tool_params(util.get_tool_by_name(tool)): - tool_name = tool_params.get("name") or str(tool) - all_tools[tool_name] = {"type": tool, "params": tool_params} - - # Raise error if name is not in tools - if name not in all_tools.keys(): - raise ValueError("Tool not found") - - type_dict = { - "str": Field( - field_type="str", - required=True, - is_list=False, - show=True, - placeholder="", - value="", - ), - "llm": Field(field_type="BaseLLM", required=True, is_list=False, show=True), - "func": Field( - field_type="function", - required=True, - is_list=False, - show=True, - multiline=True, - ), - "code": Field( - field_type="str", - required=True, - is_list=False, - show=True, - value="", - multiline=True, - ), - } - - tool_type: str = all_tools[name]["type"] # type: ignore - - if tool_type in _BASE_TOOLS: - params = [] - elif tool_type in _LLM_TOOLS: - params = ["llm"] - elif tool_type in _EXTRA_LLM_TOOLS: - _, extra_keys = _EXTRA_LLM_TOOLS[tool_type] - params = ["llm"] + extra_keys - elif tool_type in _EXTRA_OPTIONAL_TOOLS: - _, extra_keys = _EXTRA_OPTIONAL_TOOLS[tool_type] - params = extra_keys - elif tool_type == "Tool": - params = ["name", "description", "func"] - elif tool_type in CUSTOM_TOOLS: - # Get custom tool params - params = all_tools[name]["params"] # type: ignore - base_classes = ["function"] - if node := customs.get_custom_nodes("tools").get(tool_type): - return node - - else: - params = [] - - # Copy the field and add the name - fields = [] - for param in params: - if param in NODE_INPUTS: - field = type_dict[param].copy() - else: - field = type_dict["str"].copy() - field.name = param - if param == "aiosession": - field.show = False - field.required = False - fields.append(field) - - template = Template(fields=fields, type_name=tool_type) - - tool_params = util.get_tool_params(util.get_tool_by_name(tool_type)) - if tool_params is None: - tool_params = {} - return { - "template": util.format_dict(template.to_dict()), - **tool_params, - "base_classes": base_classes, - } diff --git a/src/backend/langflow/interface/tools/base.py b/src/backend/langflow/interface/tools/base.py index 9b9a072c1..496fef878 100644 --- a/src/backend/langflow/interface/tools/base.py +++ b/src/backend/langflow/interface/tools/base.py @@ -1,5 +1,6 @@ from langflow.custom import customs -from langflow.interface.listing import ALL_TOOLS_NAMES, CUSTOM_TOOLS +from langflow.interface.tools.constants import ALL_TOOLS_NAMES, CUSTOM_TOOLS +import langflow.interface.tools.util from langflow.template.template import Field, Template from langflow.utils import util from langflow.settings import settings @@ -11,14 +12,18 @@ from langchain.agents.load_tools import ( _EXTRA_OPTIONAL_TOOLS, _LLM_TOOLS, ) +from langflow.interface.tools.util import get_tools_dict class ToolCreator(LangChainTypeCreator): type_name: str = "tools" + tools_dict: Dict | None = None @property def type_to_loader_dict(self) -> Dict: - return util.get_tools_dict() + if self.tools_dict is None: + self.tools_dict = get_tools_dict() + return self.tools_dict def get_signature(self, name: str) -> Dict | None: """Get the signature of a tool.""" @@ -27,7 +32,9 @@ class ToolCreator(LangChainTypeCreator): base_classes = ["Tool"] all_tools = {} for tool in self.type_to_loader_dict.keys(): - if tool_params := util.get_tool_params(util.get_tool_by_name(tool)): + if tool_params := langflow.interface.tools.util.get_tool_params( + langflow.interface.tools.util.get_tool_by_name(tool) + ): tool_name = tool_params.get("name") or str(tool) all_tools[tool_name] = {"type": tool, "params": tool_params} @@ -101,7 +108,9 @@ class ToolCreator(LangChainTypeCreator): template = Template(fields=fields, type_name=tool_type) - tool_params = util.get_tool_params(util.get_tool_by_name(tool_type)) + tool_params = langflow.interface.tools.util.get_tool_params( + langflow.interface.tools.util.get_tool_by_name(tool_type) + ) if tool_params is None: tool_params = {} return { @@ -116,7 +125,9 @@ class ToolCreator(LangChainTypeCreator): tools = [] for tool in ALL_TOOLS_NAMES: - tool_params = util.get_tool_params(util.get_tool_by_name(tool)) + tool_params = langflow.interface.tools.util.get_tool_params( + langflow.interface.tools.util.get_tool_by_name(tool) + ) if tool_params and ( tool_params.get("name") in settings.tools or (tool_params.get("name") and settings.dev) diff --git a/src/backend/langflow/interface/tools/constants.py b/src/backend/langflow/interface/tools/constants.py new file mode 100644 index 000000000..b1e412e7d --- /dev/null +++ b/src/backend/langflow/interface/tools/constants.py @@ -0,0 +1,11 @@ +from langchain.agents.load_tools import get_all_tool_names +from langchain.agents import Tool +from langflow.interface.custom_types import PythonFunction +from langchain.tools.json.tool import JsonSpec + + +OTHER_TOOLS = {"JsonSpec": JsonSpec} +CUSTOM_TOOLS = {"Tool": Tool, "PythonFunction": PythonFunction} +ALL_TOOLS_NAMES = set( + get_all_tool_names() + list(CUSTOM_TOOLS.keys()) + list(OTHER_TOOLS.keys()) +) diff --git a/src/backend/langflow/interface/tools/util.py b/src/backend/langflow/interface/tools/util.py new file mode 100644 index 000000000..2ec273e67 --- /dev/null +++ b/src/backend/langflow/interface/tools/util.py @@ -0,0 +1,122 @@ +import ast +import inspect +from typing import Dict, Union +from langchain.agents.load_tools import ( + _BASE_TOOLS, + _EXTRA_LLM_TOOLS, + _EXTRA_OPTIONAL_TOOLS, + _LLM_TOOLS, +) +from langchain.agents.tools import Tool +from langflow.interface.tools.constants import CUSTOM_TOOLS, OTHER_TOOLS + + +def get_tools_dict(): + """Get the tools dictionary.""" + + return { + **_BASE_TOOLS, + **_LLM_TOOLS, + **{k: v[0] for k, v in _EXTRA_LLM_TOOLS.items()}, + **{k: v[0] for k, v in _EXTRA_OPTIONAL_TOOLS.items()}, + **CUSTOM_TOOLS, + **OTHER_TOOLS, + } + + +def get_tool_by_name(name: str): + """Get a tool from the tools dictionary.""" + tools = get_tools_dict() + if name not in tools: + raise ValueError(f"{name} not found.") + return tools[name] + + +def get_func_tool_params(func, **kwargs) -> Union[Dict, None]: + tree = ast.parse(inspect.getsource(func)) + + # Iterate over the statements in the abstract syntax tree + for node in ast.walk(tree): + # Find the first return statement + if isinstance(node, ast.Return): + tool = node.value + if isinstance(tool, ast.Call): + if isinstance(tool.func, ast.Name) and tool.func.id == "Tool": + if tool.keywords: + tool_params = {} + for keyword in tool.keywords: + if keyword.arg == "name": + tool_params["name"] = ast.literal_eval(keyword.value) + elif keyword.arg == "description": + tool_params["description"] = ast.literal_eval( + keyword.value + ) + + return tool_params + return { + "name": ast.literal_eval(tool.args[0]), + "description": ast.literal_eval(tool.args[2]), + } + # + else: + # get the class object from the return statement + try: + class_obj = eval( + compile(ast.Expression(tool), "", "eval") + ) + except Exception: + return None + + return { + "name": getattr(class_obj, "name"), + "description": getattr(class_obj, "description"), + } + # Return None if no return statement was found + return None + + +def get_class_tool_params(cls, **kwargs) -> Union[Dict, None]: + tree = ast.parse(inspect.getsource(cls)) + + tool_params = {} + + # Iterate over the statements in the abstract syntax tree + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef): + # Find the class definition and look for methods + for stmt in node.body: + if isinstance(stmt, ast.FunctionDef) and stmt.name == "__init__": + # There is no assignment statements in the __init__ method + # So we need to get the params from the function definition + for arg in stmt.args.args: + if arg.arg == "name": + # It should be the name of the class + tool_params[arg.arg] = cls.__name__ + elif arg.arg == "self": + continue + # If there is not default value, set it to an empty string + else: + try: + annotation = ast.literal_eval(arg.annotation) # type: ignore + tool_params[arg.arg] = annotation + except ValueError: + tool_params[arg.arg] = "" + # Get the attribute name and the annotation + elif cls != Tool and isinstance(stmt, ast.AnnAssign): + # Get the attribute name and the annotation + tool_params[stmt.target.id] = "" # type: ignore + + return tool_params + + +def get_tool_params(tool, **kwargs) -> Dict: + # Parse the function code into an abstract syntax tree + # Define if it is a function or a class + if inspect.isfunction(tool): + return get_func_tool_params(tool, **kwargs) or {} + elif inspect.isclass(tool): + # Get the parameters necessary to + # instantiate the class + return get_class_tool_params(tool, **kwargs) or {} + else: + raise ValueError("Tool must be a function or class.") diff --git a/src/backend/langflow/main.py b/src/backend/langflow/main.py index a2a02465e..21d17690a 100644 --- a/src/backend/langflow/main.py +++ b/src/backend/langflow/main.py @@ -2,8 +2,6 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from langflow.api.endpoints import router as endpoints_router -from langflow.api.list_endpoints import router as list_router -from langflow.api.signature import router as signatures_router def create_app(): @@ -23,8 +21,6 @@ def create_app(): ) app.include_router(endpoints_router) - app.include_router(list_router) - app.include_router(signatures_router) return app diff --git a/src/backend/langflow/utils/util.py b/src/backend/langflow/utils/util.py index 10048c831..23bff03ad 100644 --- a/src/backend/langflow/utils/util.py +++ b/src/backend/langflow/utils/util.py @@ -1,17 +1,7 @@ -import ast import importlib import inspect import re -from typing import Dict, Optional, Union -from langchain.agents.load_tools import ( - _BASE_TOOLS, - _EXTRA_LLM_TOOLS, - _EXTRA_OPTIONAL_TOOLS, - _LLM_TOOLS, -) - - -from langchain.agents.tools import Tool +from typing import Dict, Optional from langflow.utils import constants @@ -190,119 +180,6 @@ def get_default_factory(module: str, function: str): return None -def get_tools_dict(): - """Get the tools dictionary.""" - from langflow.interface.listing import CUSTOM_TOOLS, OTHER_TOOLS - - tools = { - **_BASE_TOOLS, - **_LLM_TOOLS, - **{k: v[0] for k, v in _EXTRA_LLM_TOOLS.items()}, - **{k: v[0] for k, v in _EXTRA_OPTIONAL_TOOLS.items()}, - **CUSTOM_TOOLS, - **OTHER_TOOLS, - } - return tools - - -def get_tool_by_name(name: str): - """Get a tool from the tools dictionary.""" - tools = get_tools_dict() - if name not in tools: - raise ValueError(f"{name} not found.") - return tools[name] - - -def get_tool_params(tool, **kwargs) -> Dict: - # Parse the function code into an abstract syntax tree - # Define if it is a function or a class - if inspect.isfunction(tool): - return get_func_tool_params(tool, **kwargs) or {} - elif inspect.isclass(tool): - # Get the parameters necessary to - # instantiate the class - return get_class_tool_params(tool, **kwargs) or {} - else: - raise ValueError("Tool must be a function or class.") - - -def get_func_tool_params(func, **kwargs) -> Union[Dict, None]: - tree = ast.parse(inspect.getsource(func)) - - # Iterate over the statements in the abstract syntax tree - for node in ast.walk(tree): - # Find the first return statement - if isinstance(node, ast.Return): - tool = node.value - if isinstance(tool, ast.Call): - if isinstance(tool.func, ast.Name) and tool.func.id == "Tool": - if tool.keywords: - tool_params = {} - for keyword in tool.keywords: - if keyword.arg == "name": - tool_params["name"] = ast.literal_eval(keyword.value) - elif keyword.arg == "description": - tool_params["description"] = ast.literal_eval( - keyword.value - ) - - return tool_params - return { - "name": ast.literal_eval(tool.args[0]), - "description": ast.literal_eval(tool.args[2]), - } - # - else: - # get the class object from the return statement - try: - class_obj = eval( - compile(ast.Expression(tool), "", "eval") - ) - except Exception: - return None - - return { - "name": getattr(class_obj, "name"), - "description": getattr(class_obj, "description"), - } - # Return None if no return statement was found - return None - - -def get_class_tool_params(cls, **kwargs) -> Union[Dict, None]: - tree = ast.parse(inspect.getsource(cls)) - - tool_params = {} - - # Iterate over the statements in the abstract syntax tree - for node in ast.walk(tree): - if isinstance(node, ast.ClassDef): - # Find the class definition and look for methods - for stmt in node.body: - if isinstance(stmt, ast.FunctionDef) and stmt.name == "__init__": - # There is no assignment statements in the __init__ method - # So we need to get the params from the function definition - for arg in stmt.args.args: - if arg.arg == "name": - # It should be the name of the class - tool_params[arg.arg] = cls.__name__ - elif arg.arg == "self": - continue - # If there is not default value, set it to an empty string - else: - try: - annotation = ast.literal_eval(arg.annotation) # type: ignore - tool_params[arg.arg] = annotation - except ValueError: - tool_params[arg.arg] = "" - # Get the attribute name and the annotation - elif cls != Tool and isinstance(stmt, ast.AnnAssign): - # Get the attribute name and the annotation - tool_params[stmt.target.id] = "" # type: ignore - - return tool_params - - def get_class_doc(class_name): """ Extracts information from the docstring of a given class. diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index 36daa926e..e6eb8b85c 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -1,4 +1,4 @@ -from langflow.interface.listing import CUSTOM_TOOLS +from langflow.interface.tools.constants import CUSTOM_TOOLS from fastapi.testclient import TestClient From 2db74fc30ed64d9d4e4baebbfba8d866243c1a1a Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Thu, 30 Mar 2023 19:39:44 -0300 Subject: [PATCH 16/59] feat: implement file type --- src/backend/langflow/interface/agents/base.py | 4 +- .../{custom_types.py => agents/custom.py} | 38 +------------------ src/backend/langflow/interface/base.py | 15 ++++++-- src/backend/langflow/interface/chains/base.py | 4 +- .../langflow/interface/custom/__init__.py | 0 .../langflow/interface/custom/types.py | 37 ++++++++++++++++++ src/backend/langflow/interface/listing.py | 2 + src/backend/langflow/interface/llms/base.py | 4 +- .../langflow/interface/memories/base.py | 4 +- .../langflow/interface/prompts/base.py | 4 +- .../langflow/interface/toolkits/__init__.py | 0 .../langflow/interface/toolkits/base.py | 34 +++++++++++++++++ src/backend/langflow/interface/tools/base.py | 38 +++++++++++++------ .../langflow/interface/tools/constants.py | 2 +- src/backend/langflow/interface/types.py | 2 + src/backend/langflow/template/template.py | 13 ++++++- src/backend/langflow/utils/util.py | 7 +++- tests/test_custom_types.py | 2 +- 18 files changed, 149 insertions(+), 61 deletions(-) rename src/backend/langflow/interface/{custom_types.py => agents/custom.py} (63%) create mode 100644 src/backend/langflow/interface/custom/__init__.py create mode 100644 src/backend/langflow/interface/custom/types.py create mode 100644 src/backend/langflow/interface/toolkits/__init__.py create mode 100644 src/backend/langflow/interface/toolkits/base.py diff --git a/src/backend/langflow/interface/agents/base.py b/src/backend/langflow/interface/agents/base.py index c1d08b551..847336a24 100644 --- a/src/backend/langflow/interface/agents/base.py +++ b/src/backend/langflow/interface/agents/base.py @@ -10,7 +10,9 @@ class AgentCreator(LangChainTypeCreator): @property def type_to_loader_dict(self) -> Dict: - return loading.AGENT_TO_CLASS + if self.type_dict is None: + self.type_dict = loading.AGENT_TO_CLASS + return self.type_dict def get_signature(self, name: str) -> Dict | None: try: diff --git a/src/backend/langflow/interface/custom_types.py b/src/backend/langflow/interface/agents/custom.py similarity index 63% rename from src/backend/langflow/interface/custom_types.py rename to src/backend/langflow/interface/agents/custom.py index 52d05b218..96c2f5a09 100644 --- a/src/backend/langflow/interface/custom_types.py +++ b/src/backend/langflow/interface/agents/custom.py @@ -1,44 +1,10 @@ -from typing import Callable, Optional from langchain import LLMChain from langchain.agents import AgentExecutor, ZeroShotAgent -from langflow.utils import validate -from pydantic import BaseModel, validator from langchain.agents.agent_toolkits.json.prompt import JSON_PREFIX, JSON_SUFFIX -from langchain.agents.mrkl.prompt import FORMAT_INSTRUCTIONS from langchain.agents.agent_toolkits.json.toolkit import JsonToolkit +from langchain.agents.mrkl.prompt import FORMAT_INSTRUCTIONS from langchain.schema import BaseLanguageModel - - -class Function(BaseModel): - code: str - function: Optional[Callable] = None - imports: Optional[str] = None - - # Eval code and store the function - def __init__(self, **data): - super().__init__(**data) - - # Validate the function - @validator("code") - def validate_func(cls, v): - try: - validate.eval_function(v) - except Exception as e: - raise e - - 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): - """Python function""" - - code: str +from pydantic import BaseModel class JsonAgent(BaseModel): diff --git a/src/backend/langflow/interface/base.py b/src/backend/langflow/interface/base.py index 4e1bd1fce..3e67830a4 100644 --- a/src/backend/langflow/interface/base.py +++ b/src/backend/langflow/interface/base.py @@ -9,11 +9,14 @@ from langflow.template.template import Template, Field, FrontendNode class LangChainTypeCreator(BaseModel, ABC): type_name: str + type_dict: Optional[Dict] = None @property @abstractmethod def type_to_loader_dict(self) -> Dict: - pass + if self.type_dict is None: + raise NotImplementedError + return self.type_dict @abstractmethod def get_signature(self, name: str) -> Optional[Dict[Any, Any]]: @@ -27,7 +30,10 @@ class LangChainTypeCreator(BaseModel, ABC): result: Dict = {self.type_name: {}} for name in self.to_list(): - result[self.type_name][name] = self.frontend_node(name).to_dict() + # frontend_node.to_dict() returns a dict with the following structure: + # {name: {template: {fields}, description: str}} + # so we should update the result dict + result[self.type_name].update(self.frontend_node(name).to_dict()) return result @@ -45,6 +51,9 @@ class LangChainTypeCreator(BaseModel, ABC): show=value.get("show", True), multiline=value.get("multiline", False), value=value.get("value", None), + suffixes=value.get("suffixes", []), + file_types=value.get("fileTypes", []), + content=value.get("content", None), ) for key, value in signature["template"].items() if key != "_type" @@ -52,7 +61,7 @@ class LangChainTypeCreator(BaseModel, ABC): template = Template(type_name=name, fields=fields) return FrontendNode( template=template, - description=signature["description"], + description=signature.get("description", ""), base_classes=signature["base_classes"], name=name, ) diff --git a/src/backend/langflow/interface/chains/base.py b/src/backend/langflow/interface/chains/base.py index 1e0d4fd57..f787ceebb 100644 --- a/src/backend/langflow/interface/chains/base.py +++ b/src/backend/langflow/interface/chains/base.py @@ -12,7 +12,9 @@ class ChainCreator(LangChainTypeCreator): @property def type_to_loader_dict(self) -> Dict: - return chains_loading.type_to_loader_dict + if self.type_dict is None: + self.type_dict = chains_loading.type_to_loader_dict + return self.type_dict def get_signature(self, name: str) -> Dict | None: try: diff --git a/src/backend/langflow/interface/custom/__init__.py b/src/backend/langflow/interface/custom/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/langflow/interface/custom/types.py b/src/backend/langflow/interface/custom/types.py new file mode 100644 index 000000000..40029112f --- /dev/null +++ b/src/backend/langflow/interface/custom/types.py @@ -0,0 +1,37 @@ +from langflow.utils import validate +from pydantic import BaseModel, validator + + +from typing import Callable, Optional + + +class Function(BaseModel): + code: str + function: Optional[Callable] = None + imports: Optional[str] = None + + # Eval code and store the function + def __init__(self, **data): + super().__init__(**data) + + # Validate the function + @validator("code") + def validate_func(cls, v): + try: + validate.eval_function(v) + except Exception as e: + raise e + + 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): + """Python function""" + + code: str diff --git a/src/backend/langflow/interface/listing.py b/src/backend/langflow/interface/listing.py index c46c272a5..c2462d15e 100644 --- a/src/backend/langflow/interface/listing.py +++ b/src/backend/langflow/interface/listing.py @@ -3,6 +3,7 @@ from langflow.interface.chains.base import chain_creator from langflow.interface.llms.base import llm_creator from langflow.interface.memories.base import memory_creator from langflow.interface.prompts.base import prompt_creator +from langflow.interface.toolkits.base import toolkits_creator from langflow.interface.tools.base import tool_creator @@ -14,6 +15,7 @@ def get_type_dict(): "tools": tool_creator.to_list(), "chains": chain_creator.to_list(), "memory": memory_creator.to_list(), + "toolkits": toolkits_creator.to_list(), } diff --git a/src/backend/langflow/interface/llms/base.py b/src/backend/langflow/interface/llms/base.py index 02ca8457c..b00fe3a84 100644 --- a/src/backend/langflow/interface/llms/base.py +++ b/src/backend/langflow/interface/llms/base.py @@ -10,7 +10,9 @@ class LLMCreator(LangChainTypeCreator): @property def type_to_loader_dict(self) -> Dict: - return llm_type_to_cls_dict + if self.type_dict is None: + self.type_dict = llm_type_to_cls_dict + return self.type_dict def get_signature(self, name: str) -> Dict | None: """Get the signature of an llm.""" diff --git a/src/backend/langflow/interface/memories/base.py b/src/backend/langflow/interface/memories/base.py index f3dc7279d..d1ae4f3ff 100644 --- a/src/backend/langflow/interface/memories/base.py +++ b/src/backend/langflow/interface/memories/base.py @@ -10,7 +10,9 @@ class MemoryCreator(LangChainTypeCreator): @property def type_to_loader_dict(self) -> Dict: - return memory_type_to_cls_dict + if self.type_dict is None: + self.type_dict = memory_type_to_cls_dict + return self.type_dict def get_signature(self, name: str) -> Dict | None: """Get the signature of a memory.""" diff --git a/src/backend/langflow/interface/prompts/base.py b/src/backend/langflow/interface/prompts/base.py index b3f96f69e..de77101f7 100644 --- a/src/backend/langflow/interface/prompts/base.py +++ b/src/backend/langflow/interface/prompts/base.py @@ -11,7 +11,9 @@ class PromptCreator(LangChainTypeCreator): @property def type_to_loader_dict(self) -> Dict: - return loading.type_to_loader_dict + if self.type_dict is None: + self.type_dict = loading.type_to_loader_dict + return self.type_dict def get_signature(self, name: str) -> Dict | None: try: diff --git a/src/backend/langflow/interface/toolkits/__init__.py b/src/backend/langflow/interface/toolkits/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/langflow/interface/toolkits/base.py b/src/backend/langflow/interface/toolkits/base.py new file mode 100644 index 000000000..8eef3d8f8 --- /dev/null +++ b/src/backend/langflow/interface/toolkits/base.py @@ -0,0 +1,34 @@ +from langflow.interface.base import LangChainTypeCreator +from langflow.utils.util import build_template_from_class +from typing import Dict, List +from langchain.agents import agent_toolkits +from langflow.interface.importing.utils import import_class + + +class ToolkitCreator(LangChainTypeCreator): + type_name: str = "toolkits" + + @property + def type_to_loader_dict(self) -> Dict: + if self.type_dict is None: + self.type_dict = { + toolkit_name: import_class( + f"langchain.agents.agent_toolkits.{toolkit_name}" + ) + # if toolkit_name is not lower case it is a class + for toolkit_name in agent_toolkits.__all__ + if not toolkit_name.islower() + } + return self.type_dict + + def get_signature(self, name: str) -> Dict | None: + try: + return build_template_from_class(name, self.type_to_loader_dict) + except ValueError as exc: + raise ValueError("Prompt not found") from exc + + def to_list(self) -> List[str]: + return list(self.type_to_loader_dict.keys()) + + +toolkits_creator = ToolkitCreator() diff --git a/src/backend/langflow/interface/tools/base.py b/src/backend/langflow/interface/tools/base.py index 496fef878..2e00ea0d2 100644 --- a/src/backend/langflow/interface/tools/base.py +++ b/src/backend/langflow/interface/tools/base.py @@ -1,6 +1,9 @@ from langflow.custom import customs -from langflow.interface.tools.constants import ALL_TOOLS_NAMES, CUSTOM_TOOLS -import langflow.interface.tools.util +from langflow.interface.tools.constants import ( + ALL_TOOLS_NAMES, + CUSTOM_TOOLS, + OTHER_TOOLS, +) from langflow.template.template import Field, Template from langflow.utils import util from langflow.settings import settings @@ -12,7 +15,11 @@ from langchain.agents.load_tools import ( _EXTRA_OPTIONAL_TOOLS, _LLM_TOOLS, ) -from langflow.interface.tools.util import get_tools_dict +from langflow.interface.tools.util import ( + get_tool_by_name, + get_tools_dict, + get_tool_params, +) class ToolCreator(LangChainTypeCreator): @@ -32,9 +39,7 @@ class ToolCreator(LangChainTypeCreator): base_classes = ["Tool"] all_tools = {} for tool in self.type_to_loader_dict.keys(): - if tool_params := langflow.interface.tools.util.get_tool_params( - langflow.interface.tools.util.get_tool_by_name(tool) - ): + if tool_params := get_tool_params(get_tool_by_name(tool)): tool_name = tool_params.get("name") or str(tool) all_tools[tool_name] = {"type": tool, "params": tool_params} @@ -67,6 +72,13 @@ class ToolCreator(LangChainTypeCreator): value="", multiline=True, ), + "dict_": Field( + field_type="file", + required=True, + is_list=False, + show=True, + value="", + ), } tool_type: str = all_tools[name]["type"] # type: ignore @@ -89,6 +101,8 @@ class ToolCreator(LangChainTypeCreator): base_classes = ["function"] if node := customs.get_custom_nodes("tools").get(tool_type): return node + elif tool_type in OTHER_TOOLS: + params = all_tools[name]["params"] # type: ignore else: params = [] @@ -108,9 +122,7 @@ class ToolCreator(LangChainTypeCreator): template = Template(fields=fields, type_name=tool_type) - tool_params = langflow.interface.tools.util.get_tool_params( - langflow.interface.tools.util.get_tool_by_name(tool_type) - ) + tool_params = get_tool_params(get_tool_by_name(tool_type)) if tool_params is None: tool_params = {} return { @@ -125,9 +137,11 @@ class ToolCreator(LangChainTypeCreator): tools = [] for tool in ALL_TOOLS_NAMES: - tool_params = langflow.interface.tools.util.get_tool_params( - langflow.interface.tools.util.get_tool_by_name(tool) - ) + tool_params = get_tool_params(get_tool_by_name(tool)) + + if tool_params and not tool_params.get("name"): + tool_params["name"] = tool + if tool_params and ( tool_params.get("name") in settings.tools or (tool_params.get("name") and settings.dev) diff --git a/src/backend/langflow/interface/tools/constants.py b/src/backend/langflow/interface/tools/constants.py index b1e412e7d..b5db816a5 100644 --- a/src/backend/langflow/interface/tools/constants.py +++ b/src/backend/langflow/interface/tools/constants.py @@ -1,6 +1,6 @@ from langchain.agents.load_tools import get_all_tool_names from langchain.agents import Tool -from langflow.interface.custom_types import PythonFunction +from langflow.interface.custom.types import PythonFunction from langchain.tools.json.tool import JsonSpec diff --git a/src/backend/langflow/interface/types.py b/src/backend/langflow/interface/types.py index f90ebe5a6..0d0d5d595 100644 --- a/src/backend/langflow/interface/types.py +++ b/src/backend/langflow/interface/types.py @@ -3,6 +3,7 @@ from langflow.interface.llms.base import llm_creator from langflow.interface.memories.base import memory_creator from langflow.interface.prompts.base import prompt_creator from langflow.interface.chains.base import chain_creator +from langflow.interface.toolkits.base import toolkits_creator from langflow.interface.tools.base import tool_creator @@ -30,6 +31,7 @@ def build_langchain_types_dict(): llm_creator, memory_creator, tool_creator, + toolkits_creator, ] all_types = {} diff --git a/src/backend/langflow/template/template.py b/src/backend/langflow/template/template.py index 21b23486a..ee498ef21 100644 --- a/src/backend/langflow/template/template.py +++ b/src/backend/langflow/template/template.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Union from pydantic import BaseModel @@ -10,6 +10,9 @@ class Field(BaseModel): show: bool = True multiline: bool = False value: Any = None + suffixes: list[str] = [] + file_types: list[str] = [] + content: Union[str, None] = None # _name will be used to store the name of the field # in the template name: str = "" @@ -18,10 +21,16 @@ class Field(BaseModel): result = self.dict() # Remove key if it is None for key in list(result.keys()): - if result[key] is None: + if result[key] is None or result[key] == []: del result[key] result["type"] = result.pop("field_type") result["list"] = result.pop("is_list") + + if result.get("file_types"): + result["fileTypes"] = result.pop("file_types") + + if self.field_type == "file": + result["content"] = self.content return result diff --git a/src/backend/langflow/utils/util.py b/src/backend/langflow/utils/util.py index 23bff03ad..3fd4a169e 100644 --- a/src/backend/langflow/utils/util.py +++ b/src/backend/langflow/utils/util.py @@ -272,6 +272,8 @@ def format_dict(d, name: Optional[str] = None): # Change type from str to Tool value["type"] = "Tool" if key in ["allowed_tools"] else _type + value["type"] = "int" if key in ["max_value_length"] else value["type"] + # Show or not field value["show"] = bool( (value["required"] and key not in ["input_variables"]) @@ -307,7 +309,10 @@ def format_dict(d, name: Optional[str] = None): if "dict" in value["type"].lower(): value["type"] = "code" - value["file"] = key in ["dict_"] + if key == "dict_": + value["type"] = "file" + value["suffixes"] = [".json", ".yaml", ".yml"] + value["fileTypes"] = ["json", "yaml", "yml"] # Replace default value with actual value if "default" in value: diff --git a/tests/test_custom_types.py b/tests/test_custom_types.py index 42da01696..157779ae4 100644 --- a/tests/test_custom_types.py +++ b/tests/test_custom_types.py @@ -1,5 +1,5 @@ # Test this: -from langflow.interface.custom_types import PythonFunction +from langflow.interface.custom.types import PythonFunction from langflow.utils import constants import pytest From b567a4473ca60f230bea28f1ad92793672e36d49 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Thu, 30 Mar 2023 20:38:45 -0300 Subject: [PATCH 17/59] input file component working --- .../components/parameterComponent/index.tsx | 32 ++++++++++++++++--- .../components/inputFileComponent/index.tsx | 25 +++++++++++---- src/frontend/src/contexts/tabsContext.tsx | 17 +++++++--- src/frontend/src/types/components/index.ts | 9 ++++++ src/frontend/src/types/tabs/index.ts | 1 + 5 files changed, 68 insertions(+), 16 deletions(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index aaaade2fa..4be3ff402 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -12,6 +12,8 @@ import FloatComponent from "../../../../components/floatComponent"; import Dropdown from "../../../../components/dropdownComponent"; import CodeAreaComponent from "../../../../components/codeAreaComponent"; import InputFileComponent from "../../../../components/inputFileComponent"; +import { TabsContext } from "../../../../contexts/tabsContext"; +import IntComponent from "../../../../components/intComponent"; export default function ParameterComponent({ left, @@ -44,6 +46,7 @@ export default function ParameterComponent({ const { reactFlowInstance } = useContext(typesContext); let disabled = reactFlowInstance?.getEdges().some((e) => e.targetHandle === id) ?? false; + const { save } = useContext(TabsContext); return (
{ data.node.template[name].value = t; + save(); }} /> ) : data.node.template[name].multiline ? ( @@ -106,6 +110,7 @@ export default function ParameterComponent({ value={data.node.template[name].value ?? ""} onChange={(t: string) => { data.node.template[name].value = t; + save(); }} /> ) : ( @@ -115,6 +120,7 @@ export default function ParameterComponent({ value={data.node.template[name].value ?? ""} onChange={(t) => { data.node.template[name].value = t; + save(); }} /> )} @@ -127,6 +133,7 @@ export default function ParameterComponent({ setEnabled={(t) => { data.node.template[name].value = t; setEnabled(t); + save(); }} />
@@ -136,6 +143,7 @@ export default function ParameterComponent({ value={data.node.template[name].value ?? ""} onChange={(t) => { data.node.template[name].value = t; + save(); }} /> ) : left === true && @@ -152,18 +160,32 @@ export default function ParameterComponent({ value={data.node.template[name].value ?? ""} onChange={(t: string) => { data.node.template[name].value = t; + save(); }} /> - ) : (left === true && type === "file")||data.type==="JsonSpec" ? ( + ) : left === true && type === "file" ? ( { - if(data.node.template[name]?.value){ - data.node.template[name].value = t; - } + data.node.template[name].value = t; + }} + fileTypes={data.node.template[name].fileTypes} + suffixes={data.node.template[name].suffixes} + onFileChange={(t: string) => { + data.node.template[name].content = t; + save(); }} > + ) : left === true && type === "int" ? ( + { + data.node.template[name].value = t; + save(); + }} + /> ) : ( <> )} diff --git a/src/frontend/src/components/inputFileComponent/index.tsx b/src/frontend/src/components/inputFileComponent/index.tsx index b7a8a0bf0..36d13e918 100644 --- a/src/frontend/src/components/inputFileComponent/index.tsx +++ b/src/frontend/src/components/inputFileComponent/index.tsx @@ -1,39 +1,52 @@ import { DocumentMagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { useContext, useEffect, useState } from "react"; import { alertContext } from "../../contexts/alertContext"; -import { TextAreaComponentType } from "../../types/components"; +import { FileComponentType } from "../../types/components"; export default function InputFileComponent({ value, onChange, disabled, -}: TextAreaComponentType) { + suffixes, + fileTypes, + onFileChange +}: FileComponentType) { const [myValue, setMyValue] = useState(value); const { setErrorData } = useContext(alertContext); useEffect(() => { if (disabled) { setMyValue(""); onChange(""); + onFileChange("") } }, [disabled, onChange]); function attachFile(fileReadEvent: ProgressEvent) { fileReadEvent.preventDefault(); const file = fileReadEvent.target.result; - console.log(file); + onFileChange(file as string) + } + + function checkFileType(fileName:string):boolean{ + for (let index = 0; index < suffixes.length; index++) { + if(fileName.endsWith(suffixes[index])){ + return true + } + } + return false } const handleButtonClick = () => { const input = document.createElement("input"); input.type = "file"; - input.accept = ".json"; + input.accept = suffixes.join(","); input.style.display = "none"; input.multiple = false; input.onchange = (e: Event) => { const file = (e.target as HTMLInputElement).files?.[0]; const fileData = new FileReader(); fileData.onload = attachFile; - if (file && file.name.endsWith(".json")) { + if (file && checkFileType(file.name)) { fileData.readAsDataURL(file); setMyValue(file.name); onChange(file.name); @@ -41,7 +54,7 @@ export default function InputFileComponent({ setErrorData({ title: "Please select a valid file. Only files this files are allowed:", - list: ["*.json"], + list: fileTypes, }); } }; diff --git a/src/frontend/src/contexts/tabsContext.tsx b/src/frontend/src/contexts/tabsContext.tsx index 6626ff0cc..a11f0339d 100644 --- a/src/frontend/src/contexts/tabsContext.tsx +++ b/src/frontend/src/contexts/tabsContext.tsx @@ -5,6 +5,7 @@ import { normalCaseToSnakeCase } from "../utils"; import { alertContext } from "./alertContext"; const TabsContextInitialValue: TabsContextType = { + save:()=>{}, tabIndex: 0, setTabIndex: (index: number) => {}, flows: [], @@ -35,15 +36,20 @@ export function TabsProvider({ children }: { children: ReactNode }) { newNodeId.current = newNodeId.current + 1; return newNodeId.current; } + function save(){ + if (flows.length !== 0) + window.localStorage.setItem( + "tabsData", + JSON.stringify({ tabIndex, flows, id, nodeId: newNodeId.current }) + ); + } useEffect(() => { //save tabs locally - if (flows.length !== 0) - window.localStorage.setItem( - "tabsData", - JSON.stringify({ tabIndex, flows, id, nodeId: newNodeId.current }) - ); + save() }, [flows, id, tabIndex, newNodeId]); + + useEffect(() => { //get tabs locally saved let cookie = window.localStorage.getItem("tabsData"); @@ -177,6 +183,7 @@ export function TabsProvider({ children }: { children: ReactNode }) { return ( void; + value: string; + suffixes:Array; + fileTypes:Array; + onFileChange:(value: string) => void; +}; + export type DisclosureComponentType = { children: ReactNode; button: { diff --git a/src/frontend/src/types/tabs/index.ts b/src/frontend/src/types/tabs/index.ts index e872a3f58..625073fb3 100644 --- a/src/frontend/src/types/tabs/index.ts +++ b/src/frontend/src/types/tabs/index.ts @@ -1,6 +1,7 @@ import { FlowType } from "../flow"; export type TabsContextType = { + save:()=>void; tabIndex: number; setTabIndex: (index: number) => void; flows: Array; From 9919b1b8ccb983193ce76468126e08fef53dce31 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Thu, 30 Mar 2023 20:49:45 -0300 Subject: [PATCH 18/59] created intComponent --- .../src/components/intComponent/index.tsx | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/frontend/src/components/intComponent/index.tsx diff --git a/src/frontend/src/components/intComponent/index.tsx b/src/frontend/src/components/intComponent/index.tsx new file mode 100644 index 000000000..520a78c0d --- /dev/null +++ b/src/frontend/src/components/intComponent/index.tsx @@ -0,0 +1,38 @@ +import { useEffect, useState } from "react"; +import { FloatComponentType } from "../../types/components"; + +export default function IntComponent({ + value, + onChange, + disabled, +}: FloatComponentType) { + const [myValue, setMyValue] = useState(value ?? ""); + useEffect(() => { + if (disabled) { + setMyValue(""); + onChange(""); + } + }, [disabled, onChange]); + return ( +
+ { + if (event.key !== 'Backspace' && event.key !== 'Enter' && event.key !== 'Delete' && event.key !== 'ArrowLeft' && event.key !== 'ArrowRight' && !/^[-]?\d*$/.test(event.key)) { + event.preventDefault(); + } + }} + type="number" + value={myValue} + className={ + "block w-full form-input dark:bg-gray-900 arrow-hide dark:border-gray-600 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + + (disabled ? " bg-gray-200 dark:bg-gray-700" : "") + } + placeholder="Type a integer number" + onChange={(e) => { + setMyValue(e.target.value); + onChange(e.target.value); + }} + /> +
+ ); +} From fdb058978eed959630a94217c70f4844b88d7f19 Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Thu, 30 Mar 2023 21:15:38 -0300 Subject: [PATCH 19/59] feat: implement requests wrapper --- src/backend/langflow/config.yaml | 4 +++ src/backend/langflow/interface/agents/base.py | 6 ++++ .../langflow/interface/prompts/base.py | 4 +-- src/backend/langflow/interface/types.py | 2 ++ .../langflow/interface/wrappers/__init__.py | 0 .../langflow/interface/wrappers/base.py | 30 +++++++++++++++++++ src/backend/langflow/settings.py | 2 ++ src/backend/langflow/template/nodes.py | 2 +- src/backend/langflow/template/template.py | 1 + 9 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 src/backend/langflow/interface/wrappers/__init__.py create mode 100644 src/backend/langflow/interface/wrappers/base.py diff --git a/src/backend/langflow/config.yaml b/src/backend/langflow/config.yaml index 8a7d6c00b..b8c2be6da 100644 --- a/src/backend/langflow/config.yaml +++ b/src/backend/langflow/config.yaml @@ -6,6 +6,7 @@ chains: agents: - ZeroShotAgent + - JsonAgent prompts: - PromptTemplate @@ -24,4 +25,7 @@ tools: - PythonFunction - JsonSpec +wrappers: + - RequestsWrapper + dev: false diff --git a/src/backend/langflow/interface/agents/base.py b/src/backend/langflow/interface/agents/base.py index 847336a24..e474ba1a6 100644 --- a/src/backend/langflow/interface/agents/base.py +++ b/src/backend/langflow/interface/agents/base.py @@ -1,8 +1,10 @@ from langchain.agents import loading +from langflow.custom.customs import get_custom_nodes from langflow.interface.base import LangChainTypeCreator from langflow.utils.util import build_template_from_class from langflow.settings import settings from typing import Dict, List +from langflow.interface.agents.custom import JsonAgent class AgentCreator(LangChainTypeCreator): @@ -12,10 +14,14 @@ class AgentCreator(LangChainTypeCreator): def type_to_loader_dict(self) -> Dict: if self.type_dict is None: self.type_dict = loading.AGENT_TO_CLASS + # Add JsonAgent to the list of agents + self.type_dict["JsonAgent"] = JsonAgent return self.type_dict def get_signature(self, name: str) -> Dict | None: try: + if name in get_custom_nodes(self.type_name).keys(): + return get_custom_nodes(self.type_name)[name] return build_template_from_class( name, self.type_to_loader_dict, add_function=True ) diff --git a/src/backend/langflow/interface/prompts/base.py b/src/backend/langflow/interface/prompts/base.py index de77101f7..cf24ed9cf 100644 --- a/src/backend/langflow/interface/prompts/base.py +++ b/src/backend/langflow/interface/prompts/base.py @@ -17,8 +17,8 @@ class PromptCreator(LangChainTypeCreator): def get_signature(self, name: str) -> Dict | None: try: - if name in get_custom_nodes("prompts").keys(): - return get_custom_nodes("prompts")[name] + if name in get_custom_nodes(self.type_name).keys(): + return get_custom_nodes(self.type_name)[name] return build_template_from_function(name, self.type_to_loader_dict) except ValueError as exc: raise ValueError("Prompt not found") from exc diff --git a/src/backend/langflow/interface/types.py b/src/backend/langflow/interface/types.py index 0d0d5d595..69d259a59 100644 --- a/src/backend/langflow/interface/types.py +++ b/src/backend/langflow/interface/types.py @@ -5,6 +5,7 @@ from langflow.interface.prompts.base import prompt_creator from langflow.interface.chains.base import chain_creator from langflow.interface.toolkits.base import toolkits_creator from langflow.interface.tools.base import tool_creator +from langflow.interface.wrappers.base import wrapper_creator def get_type_list(): @@ -32,6 +33,7 @@ def build_langchain_types_dict(): memory_creator, tool_creator, toolkits_creator, + wrapper_creator, ] all_types = {} diff --git a/src/backend/langflow/interface/wrappers/__init__.py b/src/backend/langflow/interface/wrappers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/langflow/interface/wrappers/base.py b/src/backend/langflow/interface/wrappers/base.py new file mode 100644 index 000000000..81d8482b7 --- /dev/null +++ b/src/backend/langflow/interface/wrappers/base.py @@ -0,0 +1,30 @@ +from langchain import requests +from langflow.interface.base import LangChainTypeCreator +from langflow.utils.util import build_template_from_class +from langflow.settings import settings +from langflow.custom.customs import get_custom_nodes +from typing import Dict, List + + +class WrapperCreator(LangChainTypeCreator): + type_name: str = "wrappers" + + @property + def type_to_loader_dict(self) -> Dict: + if self.type_dict is None: + self.type_dict = { + wrapper.__name__: wrapper for wrapper in [requests.RequestsWrapper] + } + return self.type_dict + + def get_signature(self, name: str) -> Dict | None: + try: + return build_template_from_class(name, self.type_to_loader_dict) + except ValueError as exc: + raise ValueError("Wrapper not found") from exc + + def to_list(self) -> List[str]: + return list(self.type_to_loader_dict.keys()) + + +wrapper_creator = WrapperCreator() diff --git a/src/backend/langflow/settings.py b/src/backend/langflow/settings.py index 13ce50b2a..092bea040 100644 --- a/src/backend/langflow/settings.py +++ b/src/backend/langflow/settings.py @@ -12,6 +12,7 @@ class Settings(BaseSettings): llms: List[str] = [] tools: List[str] = [] memories: List[str] = [] + wrappers: List[str] = [] dev: bool = False class Config: @@ -33,6 +34,7 @@ class Settings(BaseSettings): self.llms = new_settings.llms or [] self.tools = new_settings.tools or [] self.memories = new_settings.memories or [] + self.wrappers = new_settings.wrappers or [] self.dev = new_settings.dev or False diff --git a/src/backend/langflow/template/nodes.py b/src/backend/langflow/template/nodes.py index f9826d89f..ef5f71441 100644 --- a/src/backend/langflow/template/nodes.py +++ b/src/backend/langflow/template/nodes.py @@ -140,7 +140,7 @@ class JsonAgentNode(FrontendNode): ], ) description: str = """Construct a json agent from an LLM and tools.""" - base_classes: list[str] = ["BaseAgent"] + base_classes: list[str] = ["AgentExecutor"] def to_dict(self): return super().to_dict() diff --git a/src/backend/langflow/template/template.py b/src/backend/langflow/template/template.py index ee498ef21..b968cc36d 100644 --- a/src/backend/langflow/template/template.py +++ b/src/backend/langflow/template/template.py @@ -13,6 +13,7 @@ class Field(BaseModel): suffixes: list[str] = [] file_types: list[str] = [] content: Union[str, None] = None + password: bool = False # _name will be used to store the name of the field # in the template name: str = "" From c9200c94e3bd808c240260869fd3da74e26f7e6a Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Thu, 30 Mar 2023 21:17:14 -0300 Subject: [PATCH 20/59] fix: password default false --- .../GenericNode/components/parameterComponent/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 4be3ff402..3fef0c422 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -116,7 +116,7 @@ export default function ParameterComponent({ ) : ( { data.node.template[name].value = t; From a81cc1d81e32c803733a6a3b06ce43acdaf3ed1d Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Thu, 30 Mar 2023 21:33:50 -0300 Subject: [PATCH 21/59] fix: add into to not required --- src/backend/langflow/utils/util.py | 1 + .../GenericNode/components/parameterComponent/index.tsx | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/backend/langflow/utils/util.py b/src/backend/langflow/utils/util.py index 3fd4a169e..c6b25fbe6 100644 --- a/src/backend/langflow/utils/util.py +++ b/src/backend/langflow/utils/util.py @@ -286,6 +286,7 @@ def format_dict(d, name: Optional[str] = None): "temperature", "model_name", "headers", + "max_value_length", ] or "api_key" in key ) diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 3fef0c422..2e1d37ea1 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -63,7 +63,8 @@ export default function ParameterComponent({ type === "bool" || type === "float" || type === "code" || - type === "file") ? ( + type === "file" || + type === "int") ? ( <> ) : ( From b71d96a0710fae3676d5f7f77f5eca967fc7b73a Mon Sep 17 00:00:00 2001 From: Ibis Prevedello Date: Thu, 30 Mar 2023 23:14:56 -0300 Subject: [PATCH 22/59] refac: remove unnecessary fields from JsonAgent --- src/backend/langflow/template/nodes.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/backend/langflow/template/nodes.py b/src/backend/langflow/template/nodes.py index ef5f71441..2f20dc1b3 100644 --- a/src/backend/langflow/template/nodes.py +++ b/src/backend/langflow/template/nodes.py @@ -122,19 +122,13 @@ class JsonAgentNode(FrontendNode): Field( field_type="BaseToolkit", required=True, - placeholder="", - is_list=False, show=True, - value="", name="toolkit", ), Field( field_type="BaseLanguageModel", required=True, - placeholder="", - is_list=False, show=True, - value="", name="LLM", ), ], From 4b96a3f22164704049261184a559454271ea53bb Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Fri, 31 Mar 2023 07:49:50 -0300 Subject: [PATCH 23/59] refac: align with the module structure --- src/backend/langflow/template/{template.py => base.py} | 4 +++- src/backend/langflow/template/nodes.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) rename src/backend/langflow/template/{template.py => base.py} (99%) diff --git a/src/backend/langflow/template/template.py b/src/backend/langflow/template/base.py similarity index 99% rename from src/backend/langflow/template/template.py rename to src/backend/langflow/template/base.py index b968cc36d..db5a00c59 100644 --- a/src/backend/langflow/template/template.py +++ b/src/backend/langflow/template/base.py @@ -1,7 +1,9 @@ -from typing import Any, Union from pydantic import BaseModel +from typing import Any, Union + + class Field(BaseModel): field_type: str = "str" required: bool = False diff --git a/src/backend/langflow/template/nodes.py b/src/backend/langflow/template/nodes.py index ef5f71441..beba6029d 100644 --- a/src/backend/langflow/template/nodes.py +++ b/src/backend/langflow/template/nodes.py @@ -1,4 +1,5 @@ -from langflow.template.template import Field, FrontendNode, Template +from langflow.template.base import Field, Template +from langflow.template.base import FrontendNode from langchain.agents.mrkl import prompt from langflow.utils.constants import DEFAULT_PYTHON_FUNCTION From 876a691004ca3d5de9f3aede51066c9f3679b103 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Fri, 31 Mar 2023 09:04:24 -0300 Subject: [PATCH 24/59] Generated output parameters and disable one of them if the other is in use --- .../components/parameterComponent/index.tsx | 8 +- .../src/CustomNodes/GenericNode/index.tsx | 216 +++++++++--------- 2 files changed, 117 insertions(+), 107 deletions(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 2e1d37ea1..15b77392e 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -46,12 +46,14 @@ export default function ParameterComponent({ const { reactFlowInstance } = useContext(typesContext); let disabled = reactFlowInstance?.getEdges().some((e) => e.targetHandle === id) ?? false; + let disabledSource = + reactFlowInstance?.getEdges().some((e) => e.sourceHandle !== id && e.source === data.id) ?? false; const { save } = useContext(TabsContext); return (
<>
@@ -76,8 +78,8 @@ export default function ParameterComponent({ isValidConnection(connection, reactFlowInstance) } className={classNames( - left ? "-ml-0.5 " : "-mr-0.5 ", - "w-3 h-3 rounded-full border-2 bg-white dark:bg-gray-800" + left ? "-ml-0.5 " : ("-mr-0.5 " + (disabledSource ? "pointer-events-none border-gray-400 " : "")), + "w-3 h-3 rounded-full border-2 bg-white dark:bg-gray-800", )} style={{ borderColor: color, diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index 6e3aa6e67..8e81923f0 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -1,9 +1,9 @@ import { TrashIcon } from "@heroicons/react/24/outline"; import { - classNames, - nodeColors, - nodeIcons, - snakeToNormalCase, + classNames, + nodeColors, + nodeIcons, + snakeToNormalCase, } from "../../utils"; import ParameterComponent from "./components/parameterComponent"; import { typesContext } from "../../contexts/typesContext"; @@ -12,108 +12,116 @@ import { NodeDataType } from "../../types/flow"; import { alertContext } from "../../contexts/alertContext"; export default function GenericNode({ - data, - selected, + data, + selected, }: { - data: NodeDataType; - selected: boolean; + data: NodeDataType; + selected: boolean; }) { - const { setErrorData } = useContext(alertContext); - const showError = useRef(true); - const { types, deleteNode } = useContext(typesContext); - const Icon = nodeIcons[types[data.type]]; - if (!Icon) { - if (showError.current) { - setErrorData({ - title: data.type - ? `The ${data.type} node could not be rendered, please review your json file` - : "There was a node that can't be rendered, please review your json file", - }); - showError.current = false; - } - return; - } - return ( -
-
-
- -
{data.type}
-
- -
+ const { setErrorData } = useContext(alertContext); + const showError = useRef(true); + const { types, deleteNode } = useContext(typesContext); + const Icon = nodeIcons[types[data.type]]; + if (!Icon) { + if (showError.current) { + setErrorData({ + title: data.type + ? `The ${data.type} node could not be rendered, please review your json file` + : "There was a node that can't be rendered, please review your json file", + }); + showError.current = false; + } + return; + } + return ( +
+
+
+ +
{data.type}
+
+ +
-
-
- {data.node.description} -
+
+
+ {data.node.description} +
- <> - {Object.keys(data.node.template) - .filter((t) => t.charAt(0) !== "_") - .map((t: string, idx) => ( -
- {idx === 0 ? ( -
- Inputs: -
- ) : ( - <> - )} - {data.node.template[t].show ? ( - - ) : ( - <> - )} -
- ))} -
- Output: -
- - -
-
- ); + <> + {Object.keys(data.node.template) + .filter((t) => t.charAt(0) !== "_") + .map((t: string, idx) => ( +
+ {idx === 0 ? ( +
+ Inputs: +
+ ) : ( + <> + )} + {data.node.template[t].show ? ( + + ) : ( + <> + )} +
+ ))} + {data.node.base_classes.map((c, idx) => ( +
+ {idx === 0 ? ( +
+ Outputs: +
+ ) : ( + <> + )} + +
+ ))} + +
+
+ ); } From 369ce5feb507c53dcace828303b7a3b21ded0607 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Fri, 31 Mar 2023 09:15:40 -0300 Subject: [PATCH 25/59] Implemented only one node with both types --- .../components/parameterComponent/index.tsx | 10 +- .../src/CustomNodes/GenericNode/index.tsx | 216 +++++++++--------- 2 files changed, 108 insertions(+), 118 deletions(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 15b77392e..d2d561ae3 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -46,17 +46,15 @@ export default function ParameterComponent({ const { reactFlowInstance } = useContext(typesContext); let disabled = reactFlowInstance?.getEdges().some((e) => e.targetHandle === id) ?? false; - let disabledSource = - reactFlowInstance?.getEdges().some((e) => e.sourceHandle !== id && e.source === data.id) ?? false; const { save } = useContext(TabsContext); return (
<> -
+
{title} {required ? " *" : ""}
@@ -78,8 +76,8 @@ export default function ParameterComponent({ isValidConnection(connection, reactFlowInstance) } className={classNames( - left ? "-ml-0.5 " : ("-mr-0.5 " + (disabledSource ? "pointer-events-none border-gray-400 " : "")), - "w-3 h-3 rounded-full border-2 bg-white dark:bg-gray-800", + left ? "-ml-0.5 " : "-mr-0.5 ", + "w-3 h-3 rounded-full border-2 bg-white dark:bg-gray-800" )} style={{ borderColor: color, diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index 8e81923f0..1ed9f5f12 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -1,9 +1,9 @@ import { TrashIcon } from "@heroicons/react/24/outline"; import { - classNames, - nodeColors, - nodeIcons, - snakeToNormalCase, + classNames, + nodeColors, + nodeIcons, + snakeToNormalCase, } from "../../utils"; import ParameterComponent from "./components/parameterComponent"; import { typesContext } from "../../contexts/typesContext"; @@ -12,116 +12,108 @@ import { NodeDataType } from "../../types/flow"; import { alertContext } from "../../contexts/alertContext"; export default function GenericNode({ - data, - selected, + data, + selected, }: { - data: NodeDataType; - selected: boolean; + data: NodeDataType; + selected: boolean; }) { - const { setErrorData } = useContext(alertContext); - const showError = useRef(true); - const { types, deleteNode } = useContext(typesContext); - const Icon = nodeIcons[types[data.type]]; - if (!Icon) { - if (showError.current) { - setErrorData({ - title: data.type - ? `The ${data.type} node could not be rendered, please review your json file` - : "There was a node that can't be rendered, please review your json file", - }); - showError.current = false; - } - return; - } - return ( -
-
-
- -
{data.type}
-
- -
+ const { setErrorData } = useContext(alertContext); + const showError = useRef(true); + const { types, deleteNode } = useContext(typesContext); + const Icon = nodeIcons[types[data.type]]; + if (!Icon) { + if (showError.current) { + setErrorData({ + title: data.type + ? `The ${data.type} node could not be rendered, please review your json file` + : "There was a node that can't be rendered, please review your json file", + }); + showError.current = false; + } + return; + } + return ( +
+
+
+ +
{data.type}
+
+ +
-
-
- {data.node.description} -
+
+
+ {data.node.description} +
- <> - {Object.keys(data.node.template) - .filter((t) => t.charAt(0) !== "_") - .map((t: string, idx) => ( -
- {idx === 0 ? ( -
- Inputs: -
- ) : ( - <> - )} - {data.node.template[t].show ? ( - - ) : ( - <> - )} -
- ))} - {data.node.base_classes.map((c, idx) => ( -
- {idx === 0 ? ( -
- Outputs: -
- ) : ( - <> - )} - -
- ))} - -
-
- ); + <> + {Object.keys(data.node.template) + .filter((t) => t.charAt(0) !== "_") + .map((t: string, idx) => ( +
+ {idx === 0 ? ( +
+ Inputs +
+ ) : ( + <> + )} + {data.node.template[t].show ? ( + + ) : ( + <> + )} +
+ ))} +
+ Output +
+ + +
+
+ ); } From a6cba9a3d577436e433e5f121660519f64a9aa7c Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Fri, 31 Mar 2023 09:16:32 -0300 Subject: [PATCH 26/59] Original approach --- src/frontend/src/CustomNodes/GenericNode/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index 1ed9f5f12..0f9feeb7e 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -106,7 +106,7 @@ export default function GenericNode({ Date: Fri, 31 Mar 2023 13:58:03 -0300 Subject: [PATCH 27/59] bump langchain version --- poetry.lock | 72 +++++++++----------------------------------------- pyproject.toml | 2 +- 2 files changed, 14 insertions(+), 60 deletions(-) diff --git a/poetry.lock b/poetry.lock index 0e8e77b2d..9462c63cf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -715,18 +715,6 @@ files = [ {file = "frozenlist-1.3.3.tar.gz", hash = "sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a"}, ] -[[package]] -name = "geojson" -version = "2.5.0" -description = "Python bindings and utilities for GeoJSON" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "geojson-2.5.0-py2.py3-none-any.whl", hash = "sha256:ccbd13368dd728f4e4f13ffe6aaf725b6e802c692ba0dde628be475040c534ba"}, - {file = "geojson-2.5.0.tar.gz", hash = "sha256:6e4bb7ace4226a45d9c8c8b1348b3fc43540658359f93c3f7e03efa9f15f658a"}, -] - [[package]] name = "google-api-core" version = "2.11.0" @@ -771,14 +759,14 @@ uritemplate = ">=3.0.1,<5" [[package]] name = "google-auth" -version = "2.17.0" +version = "2.17.1" description = "Google Authentication Library" category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" files = [ - {file = "google-auth-2.17.0.tar.gz", hash = "sha256:f51d26ebb3e5d723b9a7dbd310b6c88654ef1ad1fc35750d1fdba48ca4d82f52"}, - {file = "google_auth-2.17.0-py2.py3-none-any.whl", hash = "sha256:45ba9b4b3e49406de3c5451697820694b2f6ce8a6b75bb187852fdae231dab94"}, + {file = "google-auth-2.17.1.tar.gz", hash = "sha256:8f379b46bad381ad2a0b989dfb0c13ad28d3c2a79f27348213f8946a1d15d55a"}, + {file = "google_auth-2.17.1-py2.py3-none-any.whl", hash = "sha256:357ff22a75b4c0f6093470f21816a825d2adee398177569824e37b6c10069e19"}, ] [package.dependencies] @@ -1091,14 +1079,14 @@ test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio" [[package]] name = "ipython" -version = "8.11.0" +version = "8.12.0" description = "IPython: Productive Interactive Computing" category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "ipython-8.11.0-py3-none-any.whl", hash = "sha256:5b54478e459155a326bf5f42ee4f29df76258c0279c36f21d71ddb560f88b156"}, - {file = "ipython-8.11.0.tar.gz", hash = "sha256:735cede4099dbc903ee540307b9171fbfef4aa75cfcacc5a273b2cda2f02be04"}, + {file = "ipython-8.12.0-py3-none-any.whl", hash = "sha256:1c183bf61b148b00bcebfa5d9b39312733ae97f6dad90d7e9b4d86c8647f498c"}, + {file = "ipython-8.12.0.tar.gz", hash = "sha256:a950236df04ad75b5bc7f816f9af3d74dc118fd42f2ff7e80e8e60ca1f182e2d"}, ] [package.dependencies] @@ -1114,6 +1102,7 @@ prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0" pygments = ">=2.4.0" stack-data = "*" traitlets = ">=5" +typing-extensions = {version = "*", markers = "python_version < \"3.10\""} [package.extras] all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] @@ -1195,14 +1184,14 @@ test = ["ipykernel", "pre-commit", "pytest", "pytest-cov", "pytest-timeout"] [[package]] name = "langchain" -version = "0.0.125" +version = "0.0.127" description = "Building applications with LLMs through composability" category = "main" optional = false python-versions = ">=3.8.1,<4.0" files = [ - {file = "langchain-0.0.125-py3-none-any.whl", hash = "sha256:678cf9d6b0d2b48fab574b5e6faa3bf6e9d249847f3956cf0970c7d48724ec43"}, - {file = "langchain-0.0.125.tar.gz", hash = "sha256:af54d190bd0ae8cab633c1b6a652c76aae685d6eb27ff39d3f9b24d27ba9f1af"}, + {file = "langchain-0.0.127-py3-none-any.whl", hash = "sha256:04ba053881e6098e80e0f4afc8922f3fe78923b160fd12d856aebce49c261918"}, + {file = "langchain-0.0.127.tar.gz", hash = "sha256:e8a3b67fd86a6f79c4334f0a7588c9476fcb57b27a8fb0e617f47c01eaab8be8"}, ] [package.dependencies] @@ -1210,15 +1199,14 @@ aiohttp = ">=3.8.3,<4.0.0" dataclasses-json = ">=0.5.7,<0.6.0" numpy = ">=1,<2" pydantic = ">=1,<2" -pyowm = ">=3.3.0,<4.0.0" PyYAML = ">=5.4.1" requests = ">=2,<3" SQLAlchemy = ">=1,<2" tenacity = ">=8.1.0,<9.0.0" [package.extras] -all = ["aleph-alpha-client (>=2.15.0,<3.0.0)", "anthropic (>=0.2.2,<0.3.0)", "beautifulsoup4 (>=4,<5)", "cohere (>=3,<4)", "deeplake (>=3.2.9,<4.0.0)", "elasticsearch (>=8,<9)", "faiss-cpu (>=1,<2)", "google-api-python-client (==2.70.0)", "google-search-results (>=2,<3)", "huggingface_hub (>=0,<1)", "jina (>=3.14,<4.0)", "jinja2 (>=3,<4)", "manifest-ml (>=0.0.1,<0.0.2)", "networkx (>=2.6.3,<3.0.0)", "nlpcloud (>=1,<2)", "nltk (>=3,<4)", "nomic (>=1.0.43,<2.0.0)", "openai (>=0,<1)", "opensearch-py (>=2.0.0,<3.0.0)", "pgvector (>=0.1.6,<0.2.0)", "pinecone-client (>=2,<3)", "psycopg2-binary (>=2.9.5,<3.0.0)", "pypdf (>=3.4.0,<4.0.0)", "qdrant-client (>=1.0.4,<2.0.0)", "redis (>=4,<5)", "sentence-transformers (>=2,<3)", "spacy (>=3,<4)", "tensorflow-text (>=2.11.0,<3.0.0)", "tiktoken (>=0.3.2,<0.4.0)", "torch (>=1,<2)", "transformers (>=4,<5)", "weaviate-client (>=3,<4)", "wikipedia (>=1,<2)", "wolframalpha (==5.0.0)"] -llms = ["anthropic (>=0.2.2,<0.3.0)", "cohere (>=3,<4)", "huggingface_hub (>=0,<1)", "manifest-ml (>=0.0.1,<0.0.2)", "nlpcloud (>=1,<2)", "openai (>=0,<1)", "torch (>=1,<2)", "transformers (>=4,<5)"] +all = ["aleph-alpha-client (>=2.15.0,<3.0.0)", "anthropic (>=0.2.4,<0.3.0)", "beautifulsoup4 (>=4,<5)", "boto3 (>=1.26.96,<2.0.0)", "cohere (>=3,<4)", "deeplake (>=3.2.9,<4.0.0)", "elasticsearch (>=8,<9)", "faiss-cpu (>=1,<2)", "google-api-python-client (==2.70.0)", "google-search-results (>=2,<3)", "huggingface_hub (>=0,<1)", "jina (>=3.14,<4.0)", "jinja2 (>=3,<4)", "manifest-ml (>=0.0.1,<0.0.2)", "networkx (>=2.6.3,<3.0.0)", "nlpcloud (>=1,<2)", "nltk (>=3,<4)", "nomic (>=1.0.43,<2.0.0)", "openai (>=0,<1)", "opensearch-py (>=2.0.0,<3.0.0)", "pgvector (>=0.1.6,<0.2.0)", "pinecone-client (>=2,<3)", "psycopg2-binary (>=2.9.5,<3.0.0)", "pyowm (>=3.3.0,<4.0.0)", "pypdf (>=3.4.0,<4.0.0)", "qdrant-client (>=1.0.4,<2.0.0)", "redis (>=4,<5)", "sentence-transformers (>=2,<3)", "spacy (>=3,<4)", "tensorflow-text (>=2.11.0,<3.0.0)", "tiktoken (>=0.3.2,<0.4.0)", "torch (>=1,<2)", "transformers (>=4,<5)", "weaviate-client (>=3,<4)", "wikipedia (>=1,<2)", "wolframalpha (==5.0.0)"] +llms = ["anthropic (>=0.2.4,<0.3.0)", "cohere (>=3,<4)", "huggingface_hub (>=0,<1)", "manifest-ml (>=0.0.1,<0.0.2)", "nlpcloud (>=1,<2)", "openai (>=0,<1)", "torch (>=1,<2)", "transformers (>=4,<5)"] [[package]] name = "markdown-it-py" @@ -1822,26 +1810,6 @@ files = [ [package.extras] plugins = ["importlib-metadata"] -[[package]] -name = "pyowm" -version = "3.3.0" -description = "A Python wrapper around OpenWeatherMap web APIs" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pyowm-3.3.0-py3-none-any.whl", hash = "sha256:86463108e7613171531ba306040b43c972b3fc0b0acf73b12c50910cdd2107ab"}, - {file = "pyowm-3.3.0.tar.gz", hash = "sha256:8196f77c91eac680676ed5ee484aae8a165408055e3e2b28025cbf60b8681e03"}, -] - -[package.dependencies] -geojson = ">=2.3.0,<3" -PySocks = ">=1.7.1,<2" -requests = [ - {version = ">=2.20.0,<3"}, - {version = "*", extras = ["socks"]}, -] - [[package]] name = "pyparsing" version = "3.0.9" @@ -1857,19 +1825,6 @@ files = [ [package.extras] diagrams = ["jinja2", "railroad-diagrams"] -[[package]] -name = "pysocks" -version = "1.7.1" -description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, - {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, - {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, -] - [[package]] name = "pytest" version = "7.2.2" @@ -2089,7 +2044,6 @@ files = [ certifi = ">=2017.4.17" charset-normalizer = ">=2,<4" idna = ">=2.5,<4" -PySocks = {version = ">=1.5.6,<1.5.7 || >1.5.7", optional = true, markers = "extra == \"socks\""} urllib3 = ">=1.21.1,<1.27" [package.extras] @@ -2682,4 +2636,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "2c201e79c486802be55495286b288ea79caa4bec2dc74a8eca90030116b6f8a6" +content-hash = "afeeaa3c4d0aee2a52be1ccfeff2c47cca9f6af446b5cf4e422fcbb214eec762" diff --git a/pyproject.toml b/pyproject.toml index b67f87595..96be32a58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ google-search-results = "^2.4.1" google-api-python-client = "^2.79.0" typer = "^0.7.0" gunicorn = "^20.1.0" -langchain = "^0.0.125" +langchain = "^0.0.127" openai = "^0.27.2" types-pyyaml = "^6.0.12.8" From 44980c2220421a0689511db74e539816a5bcd074 Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Fri, 31 Mar 2023 13:58:43 -0300 Subject: [PATCH 28/59] bump version to 0.0.52 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 96be32a58..3f202b843 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langflow" -version = "0.0.50" +version = "0.0.52" description = "A Python package with a built-in web application" authors = ["Logspace "] maintainers = [ From fb170aea66cbedad6206b4cf907764db6802e5ed Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Fri, 31 Mar 2023 13:59:10 -0300 Subject: [PATCH 29/59] fix: make class part of base_classes --- src/backend/langflow/utils/util.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/backend/langflow/utils/util.py b/src/backend/langflow/utils/util.py index c6b25fbe6..c075f10db 100644 --- a/src/backend/langflow/utils/util.py +++ b/src/backend/langflow/utils/util.py @@ -158,17 +158,23 @@ def get_base_classes(cls): """Get the base classes of a class. These are used to determine the output of the nodes. """ - bases = cls.__bases__ - if not bases: - return [] - else: + if bases := cls.__bases__: result = [] for base in bases: if any(type in base.__module__ for type in ["pydantic", "abc"]): continue result.append(base.__name__) - result.extend(get_base_classes(base)) - return result + base_classes = get_base_classes(base) + # check if the base_classes are in the result + # if not, add them + for base_class in base_classes: + if base_class not in result: + result.append(base_class) + else: + result = [cls.__name__] + if not result: + result = [cls.__name__] + return list(set(result)) def get_default_factory(module: str, function: str): From 5b557be5a8c54209f759375b6beea974f7835420 Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Fri, 31 Mar 2023 13:59:27 -0300 Subject: [PATCH 30/59] fix: json agent with wrong field name --- src/backend/langflow/template/nodes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/langflow/template/nodes.py b/src/backend/langflow/template/nodes.py index afc5eca45..bb4582ec9 100644 --- a/src/backend/langflow/template/nodes.py +++ b/src/backend/langflow/template/nodes.py @@ -130,7 +130,7 @@ class JsonAgentNode(FrontendNode): field_type="BaseLanguageModel", required=True, show=True, - name="LLM", + name="llm", ), ], ) From f90eedf5b57911f02c6a7ffe256cdeeee55b7e1c Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Fri, 31 Mar 2023 14:00:04 -0300 Subject: [PATCH 31/59] feat: first version of FieldCreator --- src/backend/langflow/template/base.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/backend/langflow/template/base.py b/src/backend/langflow/template/base.py index db5a00c59..a1e5d21a0 100644 --- a/src/backend/langflow/template/base.py +++ b/src/backend/langflow/template/base.py @@ -2,9 +2,10 @@ from pydantic import BaseModel from typing import Any, Union +from abc import ABC -class Field(BaseModel): +class FieldCreator(BaseModel, ABC): field_type: str = "str" required: bool = False placeholder: str = "" @@ -37,6 +38,10 @@ class Field(BaseModel): return result +class Field(FieldCreator): + pass + + class Template(BaseModel): type_name: str fields: list[Field] From 5b277913cf0f1affc62c7ef06b2b0d6e46fea92a Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Fri, 31 Mar 2023 14:00:47 -0300 Subject: [PATCH 32/59] fix: improvement to tools dict name --- src/backend/langflow/interface/tools/constants.py | 4 ++-- src/backend/langflow/interface/tools/util.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/backend/langflow/interface/tools/constants.py b/src/backend/langflow/interface/tools/constants.py index b5db816a5..744f9cfd8 100644 --- a/src/backend/langflow/interface/tools/constants.py +++ b/src/backend/langflow/interface/tools/constants.py @@ -4,8 +4,8 @@ from langflow.interface.custom.types import PythonFunction from langchain.tools.json.tool import JsonSpec -OTHER_TOOLS = {"JsonSpec": JsonSpec} +FILE_TOOLS = {"JsonSpec": JsonSpec} CUSTOM_TOOLS = {"Tool": Tool, "PythonFunction": PythonFunction} ALL_TOOLS_NAMES = set( - get_all_tool_names() + list(CUSTOM_TOOLS.keys()) + list(OTHER_TOOLS.keys()) + get_all_tool_names() + list(CUSTOM_TOOLS.keys()) + list(FILE_TOOLS.keys()) ) diff --git a/src/backend/langflow/interface/tools/util.py b/src/backend/langflow/interface/tools/util.py index 2ec273e67..92ea40b88 100644 --- a/src/backend/langflow/interface/tools/util.py +++ b/src/backend/langflow/interface/tools/util.py @@ -8,7 +8,7 @@ from langchain.agents.load_tools import ( _LLM_TOOLS, ) from langchain.agents.tools import Tool -from langflow.interface.tools.constants import CUSTOM_TOOLS, OTHER_TOOLS +from langflow.interface.tools.constants import CUSTOM_TOOLS, FILE_TOOLS def get_tools_dict(): @@ -20,7 +20,7 @@ def get_tools_dict(): **{k: v[0] for k, v in _EXTRA_LLM_TOOLS.items()}, **{k: v[0] for k, v in _EXTRA_OPTIONAL_TOOLS.items()}, **CUSTOM_TOOLS, - **OTHER_TOOLS, + **FILE_TOOLS, } From 0858734eb09266470f1df43b9bfcf7d3183e3852 Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Fri, 31 Mar 2023 14:01:35 -0300 Subject: [PATCH 33/59] fix: removing dicts from inside class to stop recreating it --- src/backend/langflow/interface/tools/base.py | 87 ++++++++++---------- 1 file changed, 42 insertions(+), 45 deletions(-) diff --git a/src/backend/langflow/interface/tools/base.py b/src/backend/langflow/interface/tools/base.py index 2e00ea0d2..0bb395ac9 100644 --- a/src/backend/langflow/interface/tools/base.py +++ b/src/backend/langflow/interface/tools/base.py @@ -2,9 +2,10 @@ from langflow.custom import customs from langflow.interface.tools.constants import ( ALL_TOOLS_NAMES, CUSTOM_TOOLS, - OTHER_TOOLS, + FILE_TOOLS, ) -from langflow.template.template import Field, Template +from langflow.template.base import Field +from langflow.template.base import Template from langflow.utils import util from langflow.settings import settings from langflow.interface.base import LangChainTypeCreator @@ -22,6 +23,41 @@ from langflow.interface.tools.util import ( ) +TOOL_INPUTS = { + "str": Field( + field_type="str", + required=True, + is_list=False, + show=True, + placeholder="", + value="", + ), + "llm": Field(field_type="BaseLLM", required=True, is_list=False, show=True), + "func": Field( + field_type="function", + required=True, + is_list=False, + show=True, + multiline=True, + ), + "code": Field( + field_type="str", + required=True, + is_list=False, + show=True, + value="", + multiline=True, + ), + "dict_": Field( + field_type="file", + required=True, + is_list=False, + show=True, + value="", + ), +} + + class ToolCreator(LangChainTypeCreator): type_name: str = "tools" tools_dict: Dict | None = None @@ -35,7 +71,6 @@ class ToolCreator(LangChainTypeCreator): def get_signature(self, name: str) -> Dict | None: """Get the signature of a tool.""" - NODE_INPUTS = ["llm", "func"] base_classes = ["Tool"] all_tools = {} for tool in self.type_to_loader_dict.keys(): @@ -47,40 +82,6 @@ class ToolCreator(LangChainTypeCreator): if name not in all_tools.keys(): raise ValueError("Tool not found") - type_dict = { - "str": Field( - field_type="str", - required=True, - is_list=False, - show=True, - placeholder="", - value="", - ), - "llm": Field(field_type="BaseLLM", required=True, is_list=False, show=True), - "func": Field( - field_type="function", - required=True, - is_list=False, - show=True, - multiline=True, - ), - "code": Field( - field_type="str", - required=True, - is_list=False, - show=True, - value="", - multiline=True, - ), - "dict_": Field( - field_type="file", - required=True, - is_list=False, - show=True, - value="", - ), - } - tool_type: str = all_tools[name]["type"] # type: ignore if tool_type in _BASE_TOOLS: @@ -101,8 +102,9 @@ class ToolCreator(LangChainTypeCreator): base_classes = ["function"] if node := customs.get_custom_nodes("tools").get(tool_type): return node - elif tool_type in OTHER_TOOLS: + elif tool_type in FILE_TOOLS: params = all_tools[name]["params"] # type: ignore + base_classes += [name] else: params = [] @@ -110,10 +112,7 @@ class ToolCreator(LangChainTypeCreator): # Copy the field and add the name fields = [] for param in params: - if param in NODE_INPUTS: - field = type_dict[param].copy() - else: - field = type_dict["str"].copy() + field = TOOL_INPUTS.get(param, TOOL_INPUTS["str"]) field.name = param if param == "aiosession": field.show = False @@ -122,9 +121,7 @@ class ToolCreator(LangChainTypeCreator): template = Template(fields=fields, type_name=tool_type) - tool_params = get_tool_params(get_tool_by_name(tool_type)) - if tool_params is None: - tool_params = {} + tool_params = all_tools[name]["params"] return { "template": util.format_dict(template.to_dict()), **tool_params, From 7c84cbf0b24b09e1f7d0bd3350f38dbc4b27d9f7 Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Fri, 31 Mar 2023 14:02:43 -0300 Subject: [PATCH 34/59] feat: implement importing using from --- .../langflow/interface/importing/utils.py | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/backend/langflow/interface/importing/utils.py b/src/backend/langflow/interface/importing/utils.py index 33ea6f7d9..a819b8390 100644 --- a/src/backend/langflow/interface/importing/utils.py +++ b/src/backend/langflow/interface/importing/utils.py @@ -8,12 +8,23 @@ from langchain.agents import Agent from langchain.chains.base import Chain from langchain.llms.base import BaseLLM from langchain.tools import BaseTool + +from langflow.interface.agents.custom import CUSTOM_AGENTS from langflow.interface.tools.util import get_tool_by_name def import_module(module_path: str) -> Any: """Import module from module path""" - return importlib.import_module(module_path) + if "from" not in module_path: + # Import the module using the module path + return importlib.import_module(module_path) + # Split the module path into its components + _, module_path, _, object_name = module_path.split() + + # Import the module using the module path + module = importlib.import_module(module_path) + + return getattr(module, object_name) def import_by_type(_type: str, name: str) -> Any: @@ -24,6 +35,8 @@ def import_by_type(_type: str, name: str) -> Any: "llms": import_llm, "tools": import_tool, "chains": import_chain, + "toolkits": import_toolkit, + "wrappers": import_wrapper, } return func_dict[_type](name) @@ -42,8 +55,20 @@ def import_prompt(prompt: str) -> PromptTemplate: return import_class(f"langchain.prompts.{prompt}") +def import_wrapper(wrapper: str) -> Any: + """Import wrapper from wrapper name""" + return import_module(f"from langchain.requests import {wrapper}") + + +def import_toolkit(toolkit: str) -> Any: + """Import toolkit from toolkit name""" + return import_module(f"from langchain.agents.agent_toolkits import {toolkit}") + + def import_agent(agent: str) -> Agent: """Import agent from agent name""" + # check for custom agent + return import_class(f"langchain.agents.{agent}") From fbca59d42c16b1cd18cefe6a31cd0d88430a21db Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Fri, 31 Mar 2023 14:03:32 -0300 Subject: [PATCH 35/59] feat: implemented file loading and toolkit --- src/backend/langflow/graph/base.py | 45 +++++++++++++------ .../langflow/interface/toolkits/base.py | 37 +++++++++++++-- 2 files changed, 64 insertions(+), 18 deletions(-) diff --git a/src/backend/langflow/graph/base.py b/src/backend/langflow/graph/base.py index ca0bdb823..38f3d1746 100644 --- a/src/backend/langflow/graph/base.py +++ b/src/backend/langflow/graph/base.py @@ -3,19 +3,22 @@ # - Defer prompts building to the last moment or when they have all the tools # - Build each inner agent first, then build the outer agent -from copy import deepcopy import types +from copy import deepcopy from typing import Any, Dict, List -from langflow.interface.listing import ALL_TYPES_DICT + +from langflow.graph.utils import load_dict from langflow.interface import loading +from langflow.interface.listing import ALL_TYPES_DICT from langflow.interface.tools.base import tool_creator class Node: - def __init__(self, data: Dict): + def __init__(self, data: Dict, base_type: str | None = None) -> None: self.id: str = data["id"] self._data = data self.edges: List[Edge] = [] + self.base_type: str | None = base_type self._parse_data() self._built_object = None self._built = False @@ -44,6 +47,11 @@ class Node: self.node_type = ( self.data["type"] if "Tool" not in self.output else template_dict["_type"] ) + if self.base_type is None: + for base_type, value in ALL_TYPES_DICT.items(): + if self.node_type in value: + self.base_type = base_type + break def _build_params(self): # Some params are required, some are optional @@ -71,7 +79,18 @@ class Node: continue # If the type is not transformable to a python base class # then we need to get the edge that connects to this node - if value["type"] not in ["str", "bool", "code"]: + if value["type"] == "file": + # Load the type in value.get('suffixes') using + # what is inside value.get('content') + # value.get('value') is the file name + type_to_load = value.get("suffixes") + file_name = value.get("value") + content = value.get("content") + # Now + loaded_dict = load_dict(file_name, content, type_to_load) + params[key] = loaded_dict + + elif value["type"] not in ["str", "bool", "code", "int", "float"]: # Get the edge that connects to this node edge = next( ( @@ -138,17 +157,15 @@ class Node: # Get the class from LANGCHAIN_TYPES_DICT # and instantiate it with the params # and return the instance - for base_type, value in ALL_TYPES_DICT.items(): - if base_type == "tools": - value = tool_creator.type_to_loader_dict - if self.node_type in value: - self._built_object = loading.instantiate_class( - node_type=self.node_type, - base_type=base_type, - params=self.params, - ) - break + try: + self._built_object = loading.instantiate_class( + node_type=self.node_type, + base_type=self.base_type, + params=self.params, + ) + except Exception as exc: + raise ValueError(f"Error building node {self.node_type}") from exc if self._built_object is None: raise ValueError(f"Node type {self.node_type} not found") diff --git a/src/backend/langflow/interface/toolkits/base.py b/src/backend/langflow/interface/toolkits/base.py index 8eef3d8f8..45f599491 100644 --- a/src/backend/langflow/interface/toolkits/base.py +++ b/src/backend/langflow/interface/toolkits/base.py @@ -1,12 +1,28 @@ -from langflow.interface.base import LangChainTypeCreator -from langflow.utils.util import build_template_from_class -from typing import Dict, List +from typing import Callable, Dict, List + from langchain.agents import agent_toolkits -from langflow.interface.importing.utils import import_class + +from langflow.interface.base import LangChainTypeCreator +from langflow.interface.importing.utils import import_class, import_module +from langflow.utils.util import build_template_from_class class ToolkitCreator(LangChainTypeCreator): type_name: str = "toolkits" + all_types: List[str] = agent_toolkits.__all__ + create_functions: Dict = { + "JsonToolkit": [], + "SQLDatabaseToolkit": [], + "OpenAPIToolkit": ["create_openapi_agent"], + "VectorStoreToolkit": [ + "create_vectorstore_agent", + "create_vectorstore_router_agent", + "VectorStoreInfo", + ], + "ZapierToolkit": [], + "PandasToolkit": ["create_pandas_dataframe_agent"], + "CSVToolkit": ["create_csv_agent"], + } @property def type_to_loader_dict(self) -> Dict: @@ -19,6 +35,7 @@ class ToolkitCreator(LangChainTypeCreator): for toolkit_name in agent_toolkits.__all__ if not toolkit_name.islower() } + return self.type_dict def get_signature(self, name: str) -> Dict | None: @@ -30,5 +47,17 @@ class ToolkitCreator(LangChainTypeCreator): def to_list(self) -> List[str]: return list(self.type_to_loader_dict.keys()) + def get_create_function(self, name: str) -> Callable | None: + if loader_name := self.create_functions.get(name, None): + # import loader + return import_module( + f"from langchain.agents.agent_toolkits import {loader_name[0]}" + ) + return None + + def has_create_function(self, name: str) -> bool: + # check if the function list is not empty + return bool(self.create_functions.get(name, None)) + toolkits_creator = ToolkitCreator() From 203b8ff6fef3edec7648da5b16dde2a963cf745f Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Fri, 31 Mar 2023 14:06:40 -0300 Subject: [PATCH 36/59] formatting --- src/backend/langflow/__main__.py | 2 +- src/backend/langflow/api/endpoints.py | 3 +-- src/backend/langflow/custom/customs.py | 1 - src/backend/langflow/graph/__init__.py | 2 +- src/backend/langflow/interface/base.py | 7 ++--- src/backend/langflow/interface/chains/base.py | 8 +++--- .../langflow/interface/custom/types.py | 6 ++--- .../langflow/interface/custom_lists.py | 4 +-- src/backend/langflow/interface/llms/base.py | 5 ++-- .../langflow/interface/memories/base.py | 5 ++-- .../langflow/interface/prompts/base.py | 12 +++++---- src/backend/langflow/interface/run.py | 3 ++- src/backend/langflow/interface/tools/base.py | 26 +++++++++---------- .../langflow/interface/tools/constants.py | 4 +-- src/backend/langflow/interface/tools/util.py | 2 ++ .../langflow/interface/wrappers/base.py | 12 +++++---- src/backend/langflow/template/base.py | 7 +++-- src/backend/langflow/template/nodes.py | 4 +-- src/backend/langflow/utils/util.py | 1 - tests/test_custom_types.py | 2 +- tests/test_graph.py | 5 ++-- tests/test_loading.py | 8 +++--- tests/test_validate_code.py | 7 ++--- 23 files changed, 73 insertions(+), 63 deletions(-) diff --git a/src/backend/langflow/__main__.py b/src/backend/langflow/__main__.py index 4171e4da6..fe3bac79d 100644 --- a/src/backend/langflow/__main__.py +++ b/src/backend/langflow/__main__.py @@ -2,12 +2,12 @@ import logging import multiprocessing import platform from pathlib import Path -from langflow.settings import settings import typer from fastapi.staticfiles import StaticFiles from langflow.main import create_app +from langflow.settings import settings logger = logging.getLogger(__name__) diff --git a/src/backend/langflow/api/endpoints.py b/src/backend/langflow/api/endpoints.py index e0f1ac0f1..2e52fc83d 100644 --- a/src/backend/langflow/api/endpoints.py +++ b/src/backend/langflow/api/endpoints.py @@ -1,13 +1,12 @@ from typing import Any, Dict from fastapi import APIRouter, HTTPException -from langflow.api.base import Code, ValidationResponse +from langflow.api.base import Code, ValidationResponse from langflow.interface.run import process_graph from langflow.interface.types import build_langchain_types_dict from langflow.utils.validate import validate_code - # build router router = APIRouter() diff --git a/src/backend/langflow/custom/customs.py b/src/backend/langflow/custom/customs.py index c856996a3..fa14fb2e5 100644 --- a/src/backend/langflow/custom/customs.py +++ b/src/backend/langflow/custom/customs.py @@ -1,6 +1,5 @@ from langflow.template import nodes - CUSTOM_NODES = { "prompts": {**nodes.ZeroShotPromptNode().to_dict()}, "tools": {**nodes.PythonFunctionNode().to_dict(), **nodes.ToolNode().to_dict()}, diff --git a/src/backend/langflow/graph/__init__.py b/src/backend/langflow/graph/__init__.py index 3afa92b80..097b7a695 100644 --- a/src/backend/langflow/graph/__init__.py +++ b/src/backend/langflow/graph/__init__.py @@ -1,4 +1,4 @@ +from langflow.graph.base import Edge, Node from langflow.graph.graph import Graph -from langflow.graph.base import Node, Edge __all__ = ["Graph", "Node", "Edge"] diff --git a/src/backend/langflow/interface/base.py b/src/backend/langflow/interface/base.py index 3e67830a4..66a3e3c29 100644 --- a/src/backend/langflow/interface/base.py +++ b/src/backend/langflow/interface/base.py @@ -1,8 +1,9 @@ -from typing import Any, Dict, List, Optional -from pydantic import BaseModel from abc import ABC, abstractmethod -from langflow.template.template import Template, Field, FrontendNode +from typing import Any, Dict, List, Optional +from pydantic import BaseModel + +from langflow.template.base import Field, FrontendNode, Template # Assuming necessary imports for Field, Template, and FrontendNode classes diff --git a/src/backend/langflow/interface/chains/base.py b/src/backend/langflow/interface/chains/base.py index f787ceebb..e24cba99b 100644 --- a/src/backend/langflow/interface/chains/base.py +++ b/src/backend/langflow/interface/chains/base.py @@ -1,9 +1,11 @@ from typing import Dict, List -from langflow.interface.base import LangChainTypeCreator -from langflow.utils.util import build_template_from_function -from langflow.settings import settings + from langchain.chains import loading as chains_loading +from langflow.interface.base import LangChainTypeCreator +from langflow.settings import settings +from langflow.utils.util import build_template_from_function + # Assuming necessary imports for Field, Template, and FrontendNode classes diff --git a/src/backend/langflow/interface/custom/types.py b/src/backend/langflow/interface/custom/types.py index 40029112f..4c641f388 100644 --- a/src/backend/langflow/interface/custom/types.py +++ b/src/backend/langflow/interface/custom/types.py @@ -1,8 +1,8 @@ -from langflow.utils import validate +from typing import Callable, Optional + from pydantic import BaseModel, validator - -from typing import Callable, Optional +from langflow.utils import validate class Function(BaseModel): diff --git a/src/backend/langflow/interface/custom_lists.py b/src/backend/langflow/interface/custom_lists.py index 9875a8b9d..c5794a26d 100644 --- a/src/backend/langflow/interface/custom_lists.py +++ b/src/backend/langflow/interface/custom_lists.py @@ -2,10 +2,10 @@ from typing import Any from langchain import llms, requests -from langchain.llms.openai import OpenAIChat from langchain.agents import agent_toolkits -from langflow.interface.importing.utils import import_class +from langchain.llms.openai import OpenAIChat +from langflow.interface.importing.utils import import_class llm_type_to_cls_dict = llms.type_to_cls_dict llm_type_to_cls_dict["openai-chat"] = OpenAIChat diff --git a/src/backend/langflow/interface/llms/base.py b/src/backend/langflow/interface/llms/base.py index b00fe3a84..688845301 100644 --- a/src/backend/langflow/interface/llms/base.py +++ b/src/backend/langflow/interface/llms/base.py @@ -1,8 +1,9 @@ +from typing import Dict, List + +from langflow.interface.base import LangChainTypeCreator from langflow.interface.custom_lists import llm_type_to_cls_dict from langflow.settings import settings -from langflow.interface.base import LangChainTypeCreator from langflow.utils.util import build_template_from_class -from typing import Dict, List class LLMCreator(LangChainTypeCreator): diff --git a/src/backend/langflow/interface/memories/base.py b/src/backend/langflow/interface/memories/base.py index d1ae4f3ff..99af98a1b 100644 --- a/src/backend/langflow/interface/memories/base.py +++ b/src/backend/langflow/interface/memories/base.py @@ -1,8 +1,9 @@ +from typing import Dict, List + +from langflow.interface.base import LangChainTypeCreator from langflow.interface.custom_lists import memory_type_to_cls_dict from langflow.settings import settings -from langflow.interface.base import LangChainTypeCreator from langflow.utils.util import build_template_from_class -from typing import Dict, List class MemoryCreator(LangChainTypeCreator): diff --git a/src/backend/langflow/interface/prompts/base.py b/src/backend/langflow/interface/prompts/base.py index cf24ed9cf..f730481a9 100644 --- a/src/backend/langflow/interface/prompts/base.py +++ b/src/backend/langflow/interface/prompts/base.py @@ -1,10 +1,12 @@ -from langchain.prompts import loading -from langflow.interface.base import LangChainTypeCreator -from langflow.utils.util import build_template_from_function -from langflow.settings import settings -from langflow.custom.customs import get_custom_nodes from typing import Dict, List +from langchain.prompts import loading + +from langflow.custom.customs import get_custom_nodes +from langflow.interface.base import LangChainTypeCreator +from langflow.settings import settings +from langflow.utils.util import build_template_from_function + class PromptCreator(LangChainTypeCreator): type_name: str = "prompts" diff --git a/src/backend/langflow/interface/run.py b/src/backend/langflow/interface/run.py index f4363562c..b6853a1c0 100644 --- a/src/backend/langflow/interface/run.py +++ b/src/backend/langflow/interface/run.py @@ -3,9 +3,9 @@ import io import re from typing import Any, Dict +from langflow.graph.graph import Graph from langflow.interface import loading from langflow.utils import payload -from langflow.graph.graph import Graph def process_graph(data_graph: Dict[str, Any]): @@ -51,6 +51,7 @@ def get_result_and_thought_using_graph(loaded_langchain, message: str): except Exception as e: result = f"Error: {str(e)}" thought = "" + raise e return result, thought diff --git a/src/backend/langflow/interface/tools/base.py b/src/backend/langflow/interface/tools/base.py index 0bb395ac9..31e638955 100644 --- a/src/backend/langflow/interface/tools/base.py +++ b/src/backend/langflow/interface/tools/base.py @@ -1,27 +1,27 @@ -from langflow.custom import customs -from langflow.interface.tools.constants import ( - ALL_TOOLS_NAMES, - CUSTOM_TOOLS, - FILE_TOOLS, -) -from langflow.template.base import Field -from langflow.template.base import Template -from langflow.utils import util -from langflow.settings import settings -from langflow.interface.base import LangChainTypeCreator from typing import Dict, List + from langchain.agents.load_tools import ( _BASE_TOOLS, _EXTRA_LLM_TOOLS, _EXTRA_OPTIONAL_TOOLS, _LLM_TOOLS, ) + +from langflow.custom import customs +from langflow.interface.base import LangChainTypeCreator +from langflow.interface.tools.constants import ( + ALL_TOOLS_NAMES, + CUSTOM_TOOLS, + FILE_TOOLS, +) from langflow.interface.tools.util import ( get_tool_by_name, - get_tools_dict, get_tool_params, + get_tools_dict, ) - +from langflow.settings import settings +from langflow.template.base import Field, Template +from langflow.utils import util TOOL_INPUTS = { "str": Field( diff --git a/src/backend/langflow/interface/tools/constants.py b/src/backend/langflow/interface/tools/constants.py index 744f9cfd8..caab662a9 100644 --- a/src/backend/langflow/interface/tools/constants.py +++ b/src/backend/langflow/interface/tools/constants.py @@ -1,8 +1,8 @@ -from langchain.agents.load_tools import get_all_tool_names from langchain.agents import Tool -from langflow.interface.custom.types import PythonFunction +from langchain.agents.load_tools import get_all_tool_names from langchain.tools.json.tool import JsonSpec +from langflow.interface.custom.types import PythonFunction FILE_TOOLS = {"JsonSpec": JsonSpec} CUSTOM_TOOLS = {"Tool": Tool, "PythonFunction": PythonFunction} diff --git a/src/backend/langflow/interface/tools/util.py b/src/backend/langflow/interface/tools/util.py index 92ea40b88..42bc64797 100644 --- a/src/backend/langflow/interface/tools/util.py +++ b/src/backend/langflow/interface/tools/util.py @@ -1,6 +1,7 @@ import ast import inspect from typing import Dict, Union + from langchain.agents.load_tools import ( _BASE_TOOLS, _EXTRA_LLM_TOOLS, @@ -8,6 +9,7 @@ from langchain.agents.load_tools import ( _LLM_TOOLS, ) from langchain.agents.tools import Tool + from langflow.interface.tools.constants import CUSTOM_TOOLS, FILE_TOOLS diff --git a/src/backend/langflow/interface/wrappers/base.py b/src/backend/langflow/interface/wrappers/base.py index 81d8482b7..544d361a4 100644 --- a/src/backend/langflow/interface/wrappers/base.py +++ b/src/backend/langflow/interface/wrappers/base.py @@ -1,10 +1,12 @@ -from langchain import requests -from langflow.interface.base import LangChainTypeCreator -from langflow.utils.util import build_template_from_class -from langflow.settings import settings -from langflow.custom.customs import get_custom_nodes from typing import Dict, List +from langchain import requests + +from langflow.custom.customs import get_custom_nodes +from langflow.interface.base import LangChainTypeCreator +from langflow.settings import settings +from langflow.utils.util import build_template_from_class + class WrapperCreator(LangChainTypeCreator): type_name: str = "wrappers" diff --git a/src/backend/langflow/template/base.py b/src/backend/langflow/template/base.py index a1e5d21a0..96805de68 100644 --- a/src/backend/langflow/template/base.py +++ b/src/backend/langflow/template/base.py @@ -1,8 +1,7 @@ -from pydantic import BaseModel - - -from typing import Any, Union from abc import ABC +from typing import Any, Union + +from pydantic import BaseModel class FieldCreator(BaseModel, ABC): diff --git a/src/backend/langflow/template/nodes.py b/src/backend/langflow/template/nodes.py index bb4582ec9..76d889b32 100644 --- a/src/backend/langflow/template/nodes.py +++ b/src/backend/langflow/template/nodes.py @@ -1,6 +1,6 @@ -from langflow.template.base import Field, Template -from langflow.template.base import FrontendNode from langchain.agents.mrkl import prompt + +from langflow.template.base import Field, FrontendNode, Template from langflow.utils.constants import DEFAULT_PYTHON_FUNCTION diff --git a/src/backend/langflow/utils/util.py b/src/backend/langflow/utils/util.py index c075f10db..961058950 100644 --- a/src/backend/langflow/utils/util.py +++ b/src/backend/langflow/utils/util.py @@ -3,7 +3,6 @@ import inspect import re from typing import Dict, Optional - from langflow.utils import constants diff --git a/tests/test_custom_types.py b/tests/test_custom_types.py index 157779ae4..f42421bd1 100644 --- a/tests/test_custom_types.py +++ b/tests/test_custom_types.py @@ -1,7 +1,7 @@ # Test this: +import pytest from langflow.interface.custom.types import PythonFunction from langflow.utils import constants -import pytest def test_python_function(): diff --git a/tests/test_graph.py b/tests/test_graph.py index 43669d457..6dbe8a7e3 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -1,8 +1,9 @@ import json -from langflow.graph import Edge, Node, Graph + import pytest -from langflow.utils.payload import build_json, get_root_node from langchain.agents import AgentExecutor +from langflow.graph import Edge, Graph, Node +from langflow.utils.payload import build_json, get_root_node # Test cases for the graph module diff --git a/tests/test_loading.py b/tests/test_loading.py index b85d45e86..444c85fd9 100644 --- a/tests/test_loading.py +++ b/tests/test_loading.py @@ -1,10 +1,10 @@ import json -from langflow.graph import Graph -import pytest -from langflow import load_flow_from_json -from langflow.utils.payload import get_root_node +import pytest from langchain.agents import AgentExecutor +from langflow import load_flow_from_json +from langflow.graph import Graph +from langflow.utils.payload import get_root_node def test_load_flow_from_json(): diff --git a/tests/test_validate_code.py b/tests/test_validate_code.py index 9cb47f6e1..cacf3669c 100644 --- a/tests/test_validate_code.py +++ b/tests/test_validate_code.py @@ -1,12 +1,13 @@ +from unittest import mock + +import pytest from langflow.utils.validate import ( create_function, + execute_function, extract_function_name, validate_code, - execute_function, ) -import pytest from requests.exceptions import MissingSchema -from unittest import mock def test_validate_code(): From d93413ee5671c2f391d0abd33d590c8100d8b96a Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Fri, 31 Mar 2023 14:07:02 -0300 Subject: [PATCH 37/59] feat: JsonAgent implementation --- .../langflow/interface/agents/custom.py | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/backend/langflow/interface/agents/custom.py b/src/backend/langflow/interface/agents/custom.py index 96c2f5a09..cc998fc12 100644 --- a/src/backend/langflow/interface/agents/custom.py +++ b/src/backend/langflow/interface/agents/custom.py @@ -1,3 +1,5 @@ +from typing import Optional + from langchain import LLMChain from langchain.agents import AgentExecutor, ZeroShotAgent from langchain.agents.agent_toolkits.json.prompt import JSON_PREFIX, JSON_SUFFIX @@ -7,16 +9,19 @@ from langchain.schema import BaseLanguageModel from pydantic import BaseModel -class JsonAgent(BaseModel): +class JsonAgent(AgentExecutor): """Json agent""" - toolkit: JsonToolkit - llm: BaseLanguageModel + @classmethod + def initialize(cls, *args, **kwargs): + return cls.from_toolkit_and_llm(*args, **kwargs) - def __init__(self, toolkit: JsonToolkit, llm: BaseLanguageModel): - super().__init__(toolkit=toolkit, llm=llm) - self.toolkit = toolkit - tools = self.toolkit.get_tools() + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @classmethod + def from_toolkit_and_llm(cls, toolkit: JsonToolkit, llm: BaseLanguageModel): + tools = toolkit.get_tools() tool_names = [tool.name for tool in tools] prompt = ZeroShotAgent.create_prompt( tools, @@ -30,12 +35,12 @@ class JsonAgent(BaseModel): prompt=prompt, ) agent = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names) - self.agent_executor = AgentExecutor.from_agent_and_tools( - agent=agent, tools=tools, verbose=True - ) - - def __call__(self, *args, **kwargs): - return self.agent_executor(*args, **kwargs) + return cls.from_agent_and_tools(agent=agent, tools=tools, verbose=True) def run(self, *args, **kwargs): - return self.agent_executor.run(*args, **kwargs) + return super().run(*args, **kwargs) + + +CUSTOM_AGENTS = { + "JsonAgent": JsonAgent, +} From ef4fe40ce4ab7f2a31d692d386aa5f35e20f69b6 Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Fri, 31 Mar 2023 14:07:23 -0300 Subject: [PATCH 38/59] feat: toolkit node implementation --- src/backend/langflow/graph/graph.py | 32 ++++++++++---- src/backend/langflow/graph/nodes.py | 68 +++++++++++++++++++++++++++-- 2 files changed, 88 insertions(+), 12 deletions(-) diff --git a/src/backend/langflow/graph/graph.py b/src/backend/langflow/graph/graph.py index e7110e977..cd4632b61 100644 --- a/src/backend/langflow/graph/graph.py +++ b/src/backend/langflow/graph/graph.py @@ -1,15 +1,25 @@ from typing import Dict, List, Union -from langflow.utils import payload -from langflow.interface.tools.constants import ALL_TOOLS_NAMES -from langflow.graph.base import Node, Edge +from langflow.graph.base import Edge, Node from langflow.graph.nodes import ( AgentNode, ChainNode, + FileToolNode, + LLMNode, PromptNode, ToolkitNode, ToolNode, + WrapperNode, ) +from langflow.interface.agents.base import agent_creator +from langflow.interface.chains.base import chain_creator +from langflow.interface.llms.base import llm_creator +from langflow.interface.prompts.base import prompt_creator +from langflow.interface.toolkits.base import toolkits_creator +from langflow.interface.tools.base import tool_creator +from langflow.interface.tools.constants import ALL_TOOLS_NAMES, FILE_TOOLS +from langflow.interface.wrappers.base import wrapper_creator +from langflow.utils import payload class Graph: @@ -84,16 +94,22 @@ class Graph: node_type: str = node_data["type"] # type: ignore node_lc_type: str = node_data["node"]["template"]["_type"] # type: ignore - if node_type in {"ZeroShotPrompt", "PromptTemplate"}: + if node_type in prompt_creator.to_list(): nodes.append(PromptNode(node)) - elif "agent" in node_type.lower(): + elif node_type in agent_creator.to_list(): nodes.append(AgentNode(node)) - elif "chain" in node_type.lower(): + elif node_type in chain_creator.to_list(): nodes.append(ChainNode(node)) - elif "tool" in node_type.lower() or node_lc_type in ALL_TOOLS_NAMES: + elif node_type in tool_creator.to_list() or node_lc_type in ALL_TOOLS_NAMES: + if node_type in FILE_TOOLS: + nodes.append(FileToolNode(node)) nodes.append(ToolNode(node)) - elif "toolkit" in node_type.lower(): + elif node_type in toolkits_creator.to_list(): nodes.append(ToolkitNode(node)) + elif node_type in wrapper_creator.to_list(): + nodes.append(WrapperNode(node)) + elif node_type in llm_creator.to_list(): + nodes.append(LLMNode(node)) else: nodes.append(Node(node)) return nodes diff --git a/src/backend/langflow/graph/nodes.py b/src/backend/langflow/graph/nodes.py index 43350b3d9..245e3f2f5 100644 --- a/src/backend/langflow/graph/nodes.py +++ b/src/backend/langflow/graph/nodes.py @@ -1,12 +1,14 @@ +import json from copy import deepcopy from typing import Any, Dict, List, Optional, Union from langflow.graph.base import Node +from langflow.interface.toolkits.base import toolkits_creator class AgentNode(Node): def __init__(self, data: Dict): - super().__init__(data) + super().__init__(data, base_type="agents") self.tools: List[ToolNode] = [] self.chains: List[ChainNode] = [] @@ -35,7 +37,7 @@ class AgentNode(Node): class ToolNode(Node): def __init__(self, data: Dict): - super().__init__(data) + super().__init__(data, base_type="tools") def build(self, force: bool = False) -> Any: if not self._built or force: @@ -45,7 +47,7 @@ class ToolNode(Node): class PromptNode(Node): def __init__(self, data: Dict): - super().__init__(data) + super().__init__(data, base_type="prompts") def build( self, @@ -68,7 +70,7 @@ class PromptNode(Node): class ChainNode(Node): def __init__(self, data: Dict): - super().__init__(data) + super().__init__(data, base_type="chains") def build( self, @@ -87,6 +89,52 @@ class ChainNode(Node): class ToolkitNode(Node): + def __init__(self, data: Dict): + super().__init__(data, base_type="toolkits") + + def build(self, force: bool = False) -> Any: + if not self._built or force: + if toolkits_creator.has_create_function(self.node_type): + self.find_llm() + self._build() + # Now that the toolkit is built, we need to find the llm + # and add it to the self.params + + # go through the edges and find the llm + + return deepcopy(self._built_object) + + def find_llm(self, node=None, edges_visited=[]) -> None: + if node is None: + node = self + # Move recursively through the edges + # the targets of this node edges are this node + # If we find an LLMNode, we add it to the params + if len(node.edges) == 1: + return + for edge in node.edges: + source = edge.source + if source in edges_visited: + continue + edges_visited.append(source) + if isinstance(source, LLMNode): + self.params["llm"] = source.build() + break + else: + self.find_llm(source, edges_visited) + + +class LLMNode(Node): + def __init__(self, data: Dict): + super().__init__(data, base_type="llms") + + def build(self, force: bool = False) -> Any: + if not self._built or force: + self._build() + return deepcopy(self._built_object) + + +class FileToolNode(ToolNode): def __init__(self, data: Dict): super().__init__(data) @@ -94,3 +142,15 @@ class ToolkitNode(Node): if not self._built or force: self._build() return deepcopy(self._built_object) + + +class WrapperNode(Node): + def __init__(self, data: Dict): + super().__init__(data, base_type="wrappers") + + def build(self, force: bool = False) -> Any: + if not self._built or force: + if "headers" in self.params: + self.params["headers"] = eval(self.params["headers"]) + self._build() + return deepcopy(self._built_object) From 92c3bb19b057a9c2adad6cd760c37199096de72d Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Fri, 31 Mar 2023 14:07:58 -0300 Subject: [PATCH 39/59] fix: formatting and utils --- src/backend/langflow/graph/utils.py | 26 ++++++++++ src/backend/langflow/interface/agents/base.py | 17 +++--- src/backend/langflow/interface/listing.py | 2 + src/backend/langflow/interface/loading.py | 52 +++++++++++++------ src/backend/langflow/interface/types.py | 2 +- src/backend/langflow/template/fields.py | 0 tests/conftest.py | 1 + 7 files changed, 75 insertions(+), 25 deletions(-) create mode 100644 src/backend/langflow/graph/utils.py create mode 100644 src/backend/langflow/template/fields.py diff --git a/src/backend/langflow/graph/utils.py b/src/backend/langflow/graph/utils.py new file mode 100644 index 000000000..89e3a0f48 --- /dev/null +++ b/src/backend/langflow/graph/utils.py @@ -0,0 +1,26 @@ +import base64 +import json +from typing import Dict + +import yaml + + +def load_dict(file_name, file_content, accepted_types) -> Dict: + """Load a file from a string.""" + # Check if the file is accepted + if not any(file_name.endswith(suffix) for suffix in accepted_types): + raise ValueError(f"File {file_name} is not accepted") + # Get the suffix + suffix = file_name.split(".")[-1] + # file_content == 'data:application/x-yaml;base64,b3BlbmFwaTogIjMuMC4wIg...' + data = file_content.split(",")[1] + decoded_bytes = base64.b64decode(data) + + # Convert the bytes object to a string + decoded_string = decoded_bytes.decode("utf-8") + if suffix == "json": + # Return the json content + return json.loads(decoded_string) + elif suffix in ["yaml", "yml"]: + # Return the yaml content + return yaml.safe_load(decoded_string) diff --git a/src/backend/langflow/interface/agents/base.py b/src/backend/langflow/interface/agents/base.py index e474ba1a6..35df3bee6 100644 --- a/src/backend/langflow/interface/agents/base.py +++ b/src/backend/langflow/interface/agents/base.py @@ -1,10 +1,12 @@ -from langchain.agents import loading -from langflow.custom.customs import get_custom_nodes -from langflow.interface.base import LangChainTypeCreator -from langflow.utils.util import build_template_from_class -from langflow.settings import settings from typing import Dict, List -from langflow.interface.agents.custom import JsonAgent + +from langchain.agents import loading + +from langflow.custom.customs import get_custom_nodes +from langflow.interface.agents.custom import CUSTOM_AGENTS +from langflow.interface.base import LangChainTypeCreator +from langflow.settings import settings +from langflow.utils.util import build_template_from_class class AgentCreator(LangChainTypeCreator): @@ -15,7 +17,8 @@ class AgentCreator(LangChainTypeCreator): if self.type_dict is None: self.type_dict = loading.AGENT_TO_CLASS # Add JsonAgent to the list of agents - self.type_dict["JsonAgent"] = JsonAgent + for name, agent in CUSTOM_AGENTS.items(): + self.type_dict[name] = agent return self.type_dict def get_signature(self, name: str) -> Dict | None: diff --git a/src/backend/langflow/interface/listing.py b/src/backend/langflow/interface/listing.py index c2462d15e..b11b3cef9 100644 --- a/src/backend/langflow/interface/listing.py +++ b/src/backend/langflow/interface/listing.py @@ -5,6 +5,7 @@ from langflow.interface.memories.base import memory_creator from langflow.interface.prompts.base import prompt_creator from langflow.interface.toolkits.base import toolkits_creator from langflow.interface.tools.base import tool_creator +from langflow.interface.wrappers.base import wrapper_creator def get_type_dict(): @@ -16,6 +17,7 @@ def get_type_dict(): "chains": chain_creator.to_list(), "memory": memory_creator.to_list(), "toolkits": toolkits_creator.to_list(), + "wrappers": wrapper_creator.to_list(), } diff --git a/src/backend/langflow/interface/loading.py b/src/backend/langflow/interface/loading.py index d9ed2552a..adc01c933 100644 --- a/src/backend/langflow/interface/loading.py +++ b/src/backend/langflow/interface/loading.py @@ -1,19 +1,16 @@ import json from typing import Any, Dict, Optional +from langchain.agents import ZeroShotAgent +from langchain.agents import agent as agent_module from langchain.agents.agent import AgentExecutor +from langchain.agents.agent_toolkits.base import BaseToolkit from langchain.agents.load_tools import ( _BASE_TOOLS, _EXTRA_LLM_TOOLS, _EXTRA_OPTIONAL_TOOLS, _LLM_TOOLS, ) -from langchain.agents import agent as agent_module - - -from langflow.interface.importing.utils import import_by_type - -from langchain.agents import ZeroShotAgent from langchain.agents.loading import load_agent_from_config from langchain.agents.tools import Tool from langchain.callbacks.base import BaseCallbackManager @@ -21,20 +18,29 @@ from langchain.chains.loading import load_chain_from_config from langchain.llms.base import BaseLLM from langchain.llms.loading import load_llm_from_config +from langflow.interface.agents.custom import CUSTOM_AGENTS +from langflow.interface.importing.utils import import_by_type +from langflow.interface.toolkits.base import toolkits_creator from langflow.interface.types import get_type_list from langflow.utils import payload, util, validate def instantiate_class(node_type: str, base_type: str, params: Dict) -> Any: """Instantiate class from module type and key, and params""" + if node_type in CUSTOM_AGENTS: + if custom_agent := CUSTOM_AGENTS.get(node_type): + return custom_agent.initialize(**params) + class_object = import_by_type(_type=base_type, name=node_type) + if base_type == "agents": # We need to initialize it differently - allowed_tools = params["allowed_tools"] - llm_chain = params["llm_chain"] - return load_agent_executor(class_object, allowed_tools, llm_chain) - elif base_type == "tools" or node_type != "ZeroShotPrompt": - return class_object(**params) + return load_agent_executor(class_object, params) + elif node_type == "ZeroShotPrompt": + if "tools" not in params: + params["tools"] = [] + return ZeroShotAgent.create_prompt(**params) + elif node_type == "PythonFunction": # If the node_type is "PythonFunction" # we need to get the function from the params @@ -45,10 +51,14 @@ def instantiate_class(node_type: str, base_type: str, params: Dict) -> Any: if isinstance(function_string, str): return validate.eval_function(function_string) raise ValueError("Function should be a string") + elif base_type == "toolkits": + loaded_toolkit = class_object(**params) + # Check if node_type has a loader + if toolkits_creator.has_create_function(node_type): + return load_toolkits_executor(node_type, loaded_toolkit, params) + return loaded_toolkit else: - if "tools" not in params: - params["tools"] = [] - return ZeroShotAgent.create_prompt(**params) + return class_object(**params) def load_flow_from_json(path: str): @@ -122,10 +132,10 @@ def load_agent_executor_from_config( ) -def load_agent_executor( - agent_class: type[agent_module.Agent], allowed_tools, llm_chain, **kwargs -): +def load_agent_executor(agent_class: type[agent_module.Agent], params, **kwargs): """Load agent executor from agent class, tools and chain""" + allowed_tools = params["allowed_tools"] + llm_chain = params["llm_chain"] tool_names = [tool.name for tool in allowed_tools] agent = agent_class(allowed_tools=tool_names, llm_chain=llm_chain) return AgentExecutor.from_agent_and_tools( @@ -135,6 +145,14 @@ def load_agent_executor( ) +def load_toolkits_executor(node_type: str, toolkit: BaseToolkit, params: dict): + create_function = toolkits_creator.get_create_function(node_type) + llm = params.get("llm", None) + if llm: + return create_function(llm=llm, toolkit=toolkit) + return + + def load_tools_from_config(tool_list: list[dict]) -> list: """Load tools based on a config list. diff --git a/src/backend/langflow/interface/types.py b/src/backend/langflow/interface/types.py index 69d259a59..2db27bfc3 100644 --- a/src/backend/langflow/interface/types.py +++ b/src/backend/langflow/interface/types.py @@ -1,8 +1,8 @@ from langflow.interface.agents.base import agent_creator +from langflow.interface.chains.base import chain_creator from langflow.interface.llms.base import llm_creator from langflow.interface.memories.base import memory_creator from langflow.interface.prompts.base import prompt_creator -from langflow.interface.chains.base import chain_creator from langflow.interface.toolkits.base import toolkits_creator from langflow.interface.tools.base import tool_creator from langflow.interface.wrappers.base import wrapper_creator diff --git a/src/backend/langflow/template/fields.py b/src/backend/langflow/template/fields.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/conftest.py b/tests/conftest.py index 5fe1de280..3c9837957 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ from pathlib import Path + import pytest from fastapi.testclient import TestClient From f8a48dd90e7fcdc4af119d426d87de2a4f22753a Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Fri, 31 Mar 2023 14:44:34 -0300 Subject: [PATCH 40/59] fix: return -> raise --- src/backend/langflow/api/endpoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/langflow/api/endpoints.py b/src/backend/langflow/api/endpoints.py index 2e52fc83d..bb93b136f 100644 --- a/src/backend/langflow/api/endpoints.py +++ b/src/backend/langflow/api/endpoints.py @@ -21,7 +21,7 @@ def get_load(data: Dict[str, Any]): try: return process_graph(data) except Exception as e: - return HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=str(e)) from e @router.post("/validate", status_code=200, response_model=ValidationResponse) From fc8dc87022a6a67f9ce2d8ebeba63f8951ea3c2a Mon Sep 17 00:00:00 2001 From: Lucas Eduardo Date: Fri, 31 Mar 2023 14:51:28 -0300 Subject: [PATCH 41/59] Error message included on error popup when sending chat --- src/frontend/src/components/chatComponent/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/components/chatComponent/index.tsx b/src/frontend/src/components/chatComponent/index.tsx index b285186f3..561282291 100644 --- a/src/frontend/src/components/chatComponent/index.tsx +++ b/src/frontend/src/components/chatComponent/index.tsx @@ -103,7 +103,7 @@ export default function Chat({ flow, reactFlowInstance }: ChatType) { setLockChat(false); }) .catch((error) => { - setErrorData({ title: error.message ?? "unknow error" }); + setErrorData({ title: error.message ?? "Unknown Error", list: [error.response.data.detail]}); setLockChat(false); }); } else { From 3b3edee22ca21f99ac14fadf7263970ff1a41148 Mon Sep 17 00:00:00 2001 From: Lucas Eduardo Date: Fri, 31 Mar 2023 14:56:15 -0300 Subject: [PATCH 42/59] Changed favicon and title of project --- src/frontend/public/favicon.ico | Bin 0 -> 104188 bytes src/frontend/public/index.html | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 src/frontend/public/favicon.ico diff --git a/src/frontend/public/favicon.ico b/src/frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0612ba9b3bc8eab598663d2947a35fb7016424f5 GIT binary patch literal 104188 zcmeHQ2V7NU8^4H&nTiAV9%;EvEjKuFlv!qFncGTBMa_kwB3TLM)^dc9BemSQaHEpu zuKY?G_RM>vUZQpk_@&zLZWE?rx(ig_P}$ zEUeQlU2LQCe64r)62&SPL(?UC_UQUL?-GjQzAnyn$Zegj^qW1qc6!6*%I$%pCJuPB zoOinyUiqzOy`E1sZV=hMQ=jsm&UARc+~AF0z3}DNdwQ;Zt?|-54mLK+ZhpIQ=DH`> zz3;x~m5b|N^e+G6z&VBH%$(`8qtVwrhCaFRVE@HEpF7oS@rqjuQg4sHw)2CwJtOv& zJ@;3j&)83Dgn3U$y5Ti_kZbqa#Xj%6`n6{q(vx+5Pp)(t)oy%5ueu%%Yp=NkHCpqs z&+a9`^G*i2#)rDz_21fkfcvemL8W?sKCk?(g@Hxyl)I6#X2yX3T`2VO-4^}3p4>Fd zL0{!Ho1(LeIkj%^X}x9#7rHc9cRl2j{UzJmRcLf~{Jseu!;{NgsuVxPdv=Y`(Y66Q zYK)KT7UH`o&?e>PkrlqFqx`=d^z7J-k(+kMQ;n^*k@9n!b0@0GlC)!ceD-;C@U@=|EW ztGe^={&DN)*?;VNCjJGFsVUBBPe&&>mVMT1aJMT7Q_l7nTg=zBboAe09u+q4>05n_ z&1a;jfiAe{Xj>Pzy4Bmyby>VXS|e9O=cmM}vs%a1ZgRGXmvgL5$X{I+rFHkH zlF)Jan0Z?x%Z&Z2*THWazP)c(rYI8H`;`xg?dLiCL~8$WO;2nY)vWi}u)`^@5A*KU zDA`Hh27BGq{Xiae&dbu&-Z#dF8W#2A*xkj2zZJ+u-I885ceA}_w4)0yG8((ci;SH;6 z|5+p`sb|RM8-?N=n{^xcl)_&s@$MzAP2#qeXy0>y=c!SH>_%+;)Yjd7)?^>=ofV?1 zdmSF{GiJ`=j_r>$3Yc@cTF=q*_4{6pt6qQ5j%#j?M{U#kc8gn8@9dI^?R$8wxTSBh zb4uam7kbW(ZvWYybh@D4Z|}5u>tNS|l{?P*B%=1#<9@5+55)NH``_$=p;L=Y`MyI! z@PHlxz8%UG^Ib4-_lV$>_uBY;eI)YG%-FD`ZDFZT`EA@^Hm2^PDJSBpWva#Y@T2w1 zyz@l|=f;;qFS))D{Y%S`D}C0VoZcvM_nGk8lfz2C(80m^iGj1-r&OK3^+Vh1vjeA3 zK60z(pG5l`P@Gi>LqS`ivO zV{n;DKmB$5@?MXygM;JtuUz;`hP>wgGFsQIPICA=J(v1^>f+-%J~j4OWE;0y=e?Z{ z`Yt`*;iu)_W?IGStGaq+E}z{S;d8+4{L}?5o^h$GJHM+7Hh2(&2BbTJwc#I0KVcS-$D$_4Ha>CTJzMk&Q zfA=~3WL%Y?BJJyZpGof@qjmGYzcrxe*?~L%30-y}b;7d#ryX_i?jNMM7a14d#(&k@ zbw;KP?a}Iq8twl~^-OF!yWuZw{)hQNx`}7X0 z6!K5Gv^yX8by!g5g&Fm-_=KqTP8~vQ-}pB2g_BzX;_bh^;4`;_d!Nmb@otMQyF0`* zpX4?-zSL8`gcI&?WMK&H?@J9XdS1XK59oPPs zZvKZCUmRS^``1?wPHp(xkp;e6kNF&a$ERF;_sve3awv4ccJs07hbqT@_(890&U42O z^oo8rsh>W%TE&xXH@NG&22AwP?`!Pw=aRoVwXb$75{0}gc!t#&x_8Z;9ka%D+fjXd z;d5{I@LJ&T_rIGK2DbJNOPoI~eC?6oz#28Tj;gt})o(W%FSyoz!N<=;SM5D=u1dN6 zp7%R9w$!WFmMyPe-uJ>wL(jV=Zn*uP%W%8##bfNpho-p}eQuZSs-3rE3T;Z<9D3aU zZbHecwNonmG|Hj9bJ((t(hj||en_lW* zw?3!s3ptKR>wZ0~)sVui1MBYjJ-&rXMYfj+db7g5lJn!PoE!K3^8JaP^L{PzZ1ty` zTy+ZAzr=S$o#^Y&`nOu!Yr&c0%@0=E@3vEJGYB_=>z-A zsS`?`ZyGSK!`ul;wcr0d)0J*4G33cnK{aZ9+@a6EqeK2JV^{d#a1YmUhrg|sR;uCF z+Rj%>J0DgB6)mgO+#a)9Uv}3}y<7Ri^M>Dy*>|hyjWr&6+x5Xgetz%xhrjG{v7G-a zn~tA4lG#b&y*A_00$Ws#+ficSnC;&TwHt9epmuzs*P*>zGMx2cudr;V7B_EU;&)Pyc4CJgB4TXjy|;QCR${ktrm zvTW6z%;Bp0kSaBk3P(4ac76NU0lix%H4nM^YW>4EZS={vn%0@z`>3m1y%~4g`G-gP z4FC7H-G%m)3%Ki2<3_l?!&bMY%j3&M#Wxr~zvh023eF|pYLe-}x9^!Ww%m!p z-4!EG%(cHh{NFV(o2GrTGti^N8y#k~9@%SP z*>l@FeRRIjfWywk!#l*LHcSh0J5p`>o_19dyU)E|?S;}|(~DL6B5Y92zYl#ndh?Dg zzkV5C!K>ot0hvZR^jwjV9p_x0KG&sK+gm9sb<4y2rdB;Wz3qkRJGc0){=#?Ljq%%8 z427fj>J*SLq;5j*besbu;S>uWikFRtl&~>HD z&6c5UTa+Idak7o)&J!*rRm11zWIJDnWZNsP*GH5OY2Y>H%LUJMi1>HvEeyIv1HSaA z;QZv?Ps_Bov8i)nmyOGyI-}Zk`}jAX!=5Sqi`J<6S{0|}`}+FYGjw0~N&=*nId zDt2xZu(jT>zplRZ&h_bo&c9l#y3d5BdMz&NygQ@^Qu{cS7iM+MeslIeB~VXh$(INj0Sulb}2 zcHwDl>|KlY8>&yfWLJAmsjQ*Il<@BDQ>KSFObdVGE06E1v{|`v{_uaDpWgaeV9FEQ zmTg;H@43w8>$I#A^hbC4_p&LhKUH&&^U0_E!?rZ-_vS0T zGAWupsY+0J{{vHkZ1+~T`}bt!zRP?@94XZ5?U#FOeL5mwiEs4!aSfhv?=t7yxZiG7 zveCt)75b?A%8;Lfey{RRm7pz)cV9btc&$1TnE64%(Rk;YTf2n4f8zJygZ;we4=;Dq z&6uBBc$}9X;*fQLcWpz?-}aw(q~wM*?Kk&yaZTPfA#qLj{%L>xns#SpP{mUlB8sa+ zKghU&r264q=h|S+k05a z=f7%xJ1Vf^sjJ~jU(R&Xg|6!E#(1ptxYV!FdS}NF-CMQKuR2c0*6TU%DbJTVu*#*z z4v%`ZwpO2gIylkYy^H6Fvp;=)ZgRs0vwhuXZAdzDVdGJs*ByuHwte$?m8jVjr+RHH z_QbgXUyiQ6Ao+6W@=oEQt&4qd<@LmhryTe8RT(}=y}rHs*dNEQN)6Z$b=-YH(9>gs ziwsK{-Fks<@s8YRo$WOFNQ=-ZST^o`PG{3 z2gL_fX)`Z+`i9=Y--^3dJ zC2k&YX@4O)HmQ`}Ik7&*|Knv7{^~U>B6;e9fF{>BL^~yaRnD_(?QV$89%xj=7tvj%*K? zFLgCFb>r>j`#n0nv3cyIVRd>pDD_rI`_5HDD%XA2bNJ2wb$5MZ;h7~17R~i6T))%7 zsHAbHRhWd9Z!vPski^^mTNaes;9R$N%;6cmiY}eA$jjd^@Wr;jHuS8}v4(S+YB=>R zG4t@5=Ii`JuWY{U>-OidWic^@LLEkoyKyFAe34(Te4h|s@A}xu!6}~{sWtgR{Xwyl zr>f`Y?qhTnM*KN>)hks$U$^GKIQ@vpEe7oS%GcgCH0jq%-F}Vl9{J?=>$`dW^md)g z{^QGxD>6=(IH`|gxl5&&9f%x}VW6^xk-n5s^r)L|YxvOh{rkK2#X5_w=Zn3&ZtXne z(`WE|CNE$ShO0E`e_WBV{#iL-<$#p~Rt{J>VC8_71Nq~?f&~kHAk3RLFO`7bdcvYb zi^2&@mo9BX$X_p+wKr_o(3ari<8zy^di82OVeQ(rsf6#o`)&gvv&O^y1)SOvR;^lf zn*f~kgf(l{gcH`UU*CrCaIwsL?^gNe%)9@({O8V{n@afh+i&#*=(GYWSFY3(7B5~r zhmbRu%)bj;pF`NRX_KC?Z{I!z_V3@XC#+kyE`?xzi}$CbmzP%=1MEh+d*jB9dcvMP zdlcBbd9$7XTzv`f2^9(V2b=rXs#X5Rd0T+wuZIpOW13-HQ|$knZ@$qJzWVB`xdh`} z%KcT?{yc)#{tp~DpeMkV(g?Y4Ie#@4E?iij0AIJA0Gg8s&;vc;`|rP3Kj`4-i2pb@ z$;fN%U;NmP9Xs@dAAa~j0oZ;D0rAu{!u_}8y!BAf?Z2SQfByXWcL^N(2@?Oy_z(W9 zAmmNEve&{KV+BFtXNmu1{D-Z@60+B{>=pR?PY9q*PY}AXHlZiv*8YF|@kc#j*REYz zK;i??w1i+;D)Qf&RsIEA{#f%1BFMNWb|C%$wsDG(e@#no0k(gFuzmY>Jwfb$@7}$7 z0%HF_f~ES?wgXyw5n%hN1ff^#RO}x(5Emu(&nfy?eKwAmz~hFoG7Z zhn2U=KYhKc0Ly~@CfVDmDX_%xFe(ov(dQjN_Es?fFJd{L`c>(-n0s*)-GeBvir9tC)f~*T@>6G;W zl>LNY3f~8H-75d|I1e&Y^t_p<_>&;`SX%#~=RXp(agO*o__kES*I$3#f}rgmw%>{Xn|CE(41bQGEr)xx z2=I&X1ep(rpWC@}r=Ea*e*pj-+8w$PIx_*go{I`?&g_<7i<5(7vce*ZYZQvGr+M38t#>|f3~ zfaU~(rTWWnJ68D@Z261+U=A>mfU!`S_luo~{Y(4{-8H%|CiXAmy>?w(@~EFifXz=P zNdNaMg?=XxFox?1LKk8a<(vcN1WO5)>L2YZaf0wmYyaX`5eqmFEY)v*+p)?&zZ^`j z|G3IusD~|Zu1ts{$h<)OfVuWB>-Jlu~ucg{C$lfadJTmlgm%osWwZ7^E%=y<65F^JDWZabT zO*{V={{X*nmw++%I|BY{5TyP4om%Zbe;l^TKX3f|uh_qqdf0GL0@nU^5puBqbKt=_ ztSdc9(6;lS@>ct|ET^sVH;-c|vcx#n^&V2o^ z*gw|%iW9Kr=Rv@4<^0dR*8gOE9pi-Z-VE%suOOga1wzi;v)o;){pXLDR{7_Re~-KU zWBKtquaF>vK62hq&Ih5s^1d&m%JV|G=Wy<7-0S=g#yRCL zcdeQ(4c}(g`Jb&@x0WTCZt20^vC7{v{C?cz5Bq3Nz}eqe0?y_bJ^w4?znq&Df1us} z$J)R0Y#nr4PZ0Z;=inrT{mc2^UZ~T{GThJJ3eNt95Cm_?C(HR?#5ku3*=y$Q3i!8! zAoR=mA4x%L93gM5TfUZ6{smk9*rPv1kn?b2zj97h;s6Yi@dV7_e3EJm>fMb^DoZ$l;3EKWKXZeT#9Cs1G z9~T1GHVIN5>mBt8;8y}c=oPyc{(!H?2~zK2r&jr=pLZ{4^2alAFB1^=DbN4PxG(lE zc8|3`z6&@Uw%>{X9e;};<+p9yrYFce0lKX`--@(00cHOp$n!8_|6&&sN1&`T!P52s z{q9c?I$^`g_%C!yT!49R2my50A!zB8@m%5{;5wEd>zeQp|0BqAPdU&z5%nfon$Fy| zXO(}}=YI>J{8vKKE*W&-jmr&L1LRJpG)2F;RKnhLp#>W(1T? zCP=&>_Alci?C~f;>RBq4aT-405&<&TfUF=`{0pjIV1WV~1Y~Ph2 z_9S*M&&3Ph5nopzSgK!bJFxxH1jKvFdq0G3nZL+54u7Pq4+y?u*JA&&{vq*;*t_JV zKK#Tif)_%P1ce$>W) zQ&a&@k>esFIpR-cY65?1*atoR>ueC2!J@O6s`SOer)$8cO%&Iy^af7u^E zIi3}Y!n(gV0s9A)3C6YS(Yb21|8!Yc<)3x`ub`~|;tT-41=tIFe-Q-P_muO!V&A6h zU-tjyJfQ3wBF>2=;9S8#!lPsHSurW-_FvHDkMm#g1XKI}u!)s~tXh-z9M1kuB*^(1 z?f$>m9qg?-A@4oqToY$rhY&zpGC}AU`xX4mwSPJ9Bj{w2j+n%@FS(LO(rx8AbXLTulLfHRHp1hIQL zcPRERe&}B3f2E$BXBPXHbAs~xAN+vwoHN>eg^(SyhjYa$|Ma+5Q& zr5@~Q2m!IR9RXzy62!lXU5NclJOJCDO~|bt->W=me1m{7-{^Oaft#Ly*x)7sv^FMy z#(o5OPDsXi;3(fYR-XR_t;%;zQ1%)@o(Iy>DSia&24e{K!ux*Tx5_`={tK%75ogQy zv4Sx!21~rFvSa1BIPn7*6N3oI?;(gj%eW}>UW|z+2v|?!H-PyqU}c<@abD~~>Y*&( z33vx~bzh$Q!8rFlA&2Lj(_r^$h;h0Sv~~ykA58#`%KJZ%P9?}bk(7rXDbG10j^LU? zPR}_X0Z)!dy@!=r<)8k17iC`%hi`~mO3R{>;5PU41!nXwx#J>rB;`b4woF!PQU$noMAn}64Ig*0bSc0Yc%WpeY z`4?>Yi~b-U^dw-6*Av7}#Qw#e#Schc>{-Tt*%#BUi;Evn+5Hc&gCC^-`<24>bKK{E z&s#>YRR4$-+z7%Sv3u=v&X_lqAy}&4{I+A2e||ZbUjK2Gzfg~{Z~_5y;ADb~lVaDo zwSSr4gD=W^Md4Rg5$@MIztEwIZy)IQe?jLMg78ag|Kd+!`;Y4Pf35NtCCWqkxXWKi z#(cRA0kOa_!m?${^aP2cWqi|){}|Ji`M&rKq`wg`Cw`eA?ceXzYXAA;uvPwfV3ogl?EA0SKi2U|60r8SkbpB1w+ZI%|HDVfbFp7z-S6uMz29ZEfAjR!R4J?c zO>rjY>yNwrV;|UwfIYx}3GzI!?Av2(H|zVq+V29v2ZR$)|3yO1T+IJntNrJVhgSLL zjc-!xakYPp?XMDW7AAop`**TmE%Cm&_OCtvqk3*85&Qp%Xy+}0@XgYx>bajpoB>J{ zdgNS_#4+%3O9__hA3km>0r8HWpxytMbCvh&ymMaZu*yHLoXcA8ag#rEzX1W~K%)s_ zC!(if|FWJX_r)J*_y47yoClP1e^O5MJ**hme+=Tx#)Pa~v|J9he}jN$bMyqUf62qw z9VJ+*NBIAZ1bGff>|avQnn)1;WT}4h+KyHJ1zY}z_l^+cIbi5!mgi!j%gVVuJR6fj zz_@vhV9Nec|0V(Ul}3t$+eCCfJ^<@44 zf49p3jEVS-vb_ZSR^AgX<#DYN0oRrH|A^lcyO%ut<5_~#d)TQ}{smM1kh?O^N2(`? zos0cT9&up|0c(KW2(an*2x3Pv&Wk@l{n-TA!D|GRy-Se!fY`sUudkjU>k6=mP6SKa z18Dnz0NXbDoU@!~1Z@`yShrN3b4L0RLHHrhFG)OvbQA%8Mfsj5X!R#Z+%I&B{cqZ| zNlyTcZUjryncMcj6RZ3&))iFwBPL!+kaN9a&!X?J1?4#)*#2OG=$FK;(Dj7|xQz5N z>a8K*?opyYe0a&>BJzc?+Ge#VpS`gVtaI zeAaG)_%(@NB(6vO?F6*<2SM5qKOuC=xDWax2q;&YU@7|Y+KyHJS>s&zdgXl=1x5aN zCjK%(>_O}w>ley5!C*I62zk{nsfRea5CJ|Sm>~0i85hM)U=x=JQqR(I zMDO7TeF%BgFKs>em30IeKgIqf1>d6x+Ip7GBkuJefHuDKM~O)on~433y-U0zd9i1~ zU+iAyHDdpgieEuH-ULh2k>B>L@-Oi6PlFDm$#^g0ne2_}rxEh2*XGto43OnHSMh_g&yidE7rHv_AmDKp!8q*!Mb2e0_Z(Lz#a$R1(qiBB2(i(Y+KnU5xv3O z=9B@PkUB~Gk7^3rx5^*FDZuhig$|^O{>pm4?2}>A_{Y9MNNszc%*8e4iJ*5&X z)n8uQ0sYkp@Pmg4GB1#IJ=HjW1mpY>*!t6iy!Q97>QVMq`Df>TL6JY!!+Z!>2Z|zK zP8>~u%?BHx1=1GTIh406u$7hs_<#@s{7p0gHm}V45qEnN^47ZLYhnCvX#nmSguupm zHYyr41QRg7Z$Yqp&kv?WtNe53^M76bIa6=FYvq8I16B@LIbh|0l>?6=2XGE2pMN?> zeUvTRzz;)WG@qig&vIXz_tWXJ+}Gy8Jx6u>ntTfKN(Tmfw0Z6b3Yx?(GjFf+L*5Vf z6$EtJe0s&q`zG?Xeo^W54fiF_ia^ZIUZ0}e*XD@`2vhEpKy99c;4<1sQ5c}j>(lAV zxSuT_rButjug#~Um3cq+d7`bZpEZ9E{Iv9F^ICpr^ICoy%TvC#e$6uYug-(LR18+j zBEO8hRv)x^=szkFEi|K;fw4Tv%Fs`g*XAKMMNTv$MUk|5Kb3tDQEgsrPrR?qtL>Bb zbDP(;r_F2e(dKnI)2HQ!HgB50_O=@P)#TA}j`B6!7yUq1DKB|H<|~Jc{P8Dh^f4-F4KM~ z56PqZoaI|wg3V9oajoCQur%I~9vV-K{AIhC<5(iZEX zx4CbGz1vi*g>EpX-o478t+rTuyN$N_tY$dQPnw0#z1lT(pL-x!k5--w<=!{Wh0HNk zR(&0G&*5G+_p$kH2drb$)8y;VEB)D%-8_TB^Jd&@Lc6};Ma6tIoI_>MuQ%{o&$B-` z_dhp>;u*IWO}b5|{Y;Ibme3Wl4onHAJe zkw5Ux^*4m#4X9k|bwN%ng`l+9wVT-wV;7nX$^cz51CoQC( z`VafR2_NLE@qar}E=aDiPx1ix+(N%4%OrpJ?08ZJx4#54d7P7B_&)0pUAk4F2$&j-hgmxEN&rC)+W%Z}9v_(K+%Q z{7%K0tBk!=(BBHQyF$j#Nt?*mf-K@`2h=$}M}oRP%l-VMkjG1~#T4kYqKn3Ck#y2N z)V4@maE;%s@0pwaG3H!{JQevfh3t=;Q=fK7y(9gMInZ~4IrYpb0~?q~+ovBe=8H)` zt3!`wF2N#l}}2Uyrho{{iF@_FICeI zTi=rxn$>lz^gr76ua*^T{RfTwe}?=DsmtW{_oFU<_`c%seI7_}X#F4kHEiNG#_Sci zS0T5wv;3n>KgR#g82@f!3{c`-Q{x}UAoxdqx4VCCdGM&xkND>;KqBLPbK)OEJeUd| zP0TG1wDAvg?JINIFc$ydJHutoshd+7;L{xOZ>&20<$Q;+smA{G{H`(MUO5j9`WgTB z%Bg+rJ;c8u>i8ErcUoJGN2C%hDdu7?bN)-&lYY_;{}&GWTcOXlc%AR)piQ8^PXM)X{V(QnO%zq7i=GykO_<;V^ zYFU7GJ@T(gIp8)PeIJ6HK8GxnIWF?ea6OspF(M1_&g^#xQl{X+CFoLy&Y?{`=D>Vs z`YEwF&NaZJU%`_wxh6UXd~U!`7ZW`5nnD-(tu{p#!~uGE(VVs*t6gea1U?C}K4eaP z^xFkKCrRydphL$GH9+;4qo&`;oSQtEWe$$!l;Qd}Xi?~AyaWB8XHGrv zY$S0O`uQ7pH!!EJx(w*=51or8@7V_Zp_YD`djn6tf6t=t&`{IQG2lVdPhP;+Rs*a? z-wGep^i%)A8?Kjd4c4M_efIq*&`&)02O5il)_rRFVOM96FP>h)^8X(t`biyp{R%bx zkpB+LQ=QqOmHtfs_pt0=q`|lVKKMtC{7+zhT}AtTd;6#J>|6 zlgju{+#$E>=G2ukh`qVqp3MF^_Om@>^vj-uVa}bWJqPJin?nDy%taB8_-o4;%R>&U zW&fKzfS*p0`GK+W+H27FHi$J9-ZOS2{p2D1e-{1cJ(+XM{x5ixg0in@+ssQI^N?L! z?_vB)-ZLJ>Jfw-Vh4{P^?k&K*zH$wHJ3#(XjKxViQ(QA^-oQ3EPk`S2LR)8iLmH_A zuz$uszsOw2F#q8^DN=lbYW@?1Yi7^61$S)<9efWoEA$&;E%M-Z^jQWpI>P2s*|z~t z>Z={D9i+TuE=oNB{j;@go64ilH`McAw8yyfCi-oRKKpS_q@MqRKQ(auns&~OxxpAy zeVBV4I?Mg2VCtR7f^vZ@f{^YJSN2PfdCF0dSBPWq{nxPAxpn?3h|&V8?$bMu`W zT?J>*KN@ZB0!-zY$m`%yGw6tR&V5Ac?gx1&d!J&9v<3QP4gE@)?^@8dwf&;;4n zh5tLDrXSb1R+g_bh>yDbg?^@x|0A{k`ylk+N`E>B=+~g@%6EQcEf)H(eAidaqul;} z)b$@ePWk33;xXl0r)v9$zn8hF@;%es($4aaGX01FoG=C|-+W|TV{H7By&(91P&1nNXSH3GJ<3Hn{+{E3c z`au86_rFNH#J`Mxm{;pR?{OZ$*pEJed!Nt|0N?q|A>~^`NQ?41$|>I#BJW8%Q{ZaWyn*#i{n6$D8T-Ga z9k!r+?*+0^zVkwvlMkR%`K}ADE8lY={qhYL;L^!dAKL5aPx&4T=_k(_XCmg5Z?7od zT;cqRb_h_uu>w9Q-&T=1H_t+&Pd9Dbxy?ftm2Y4OE}R2_{=saAwu*lFoeSmr7AT{9 z*8)5mO}Pm!)B)I`S?gwO7xK14zsk2LNI!KBV=entzB?g0$N4_)E8m!4898%6*`t5u zTM}$9KYy5uE8hSh{qQf;#Vhw}4?3#6&!6^BT_hiX-=%xiQ{U&D8+~P&bCVb3gPMNg z#q}5QIWp%)e-_yX#QLA|4s`Zokb#dHPgB3aE9G70DqqA0WOMZgJ5b(3PF*rK=1~5m zUDiJhHc#1G>^*6qUwH>K>E~Dk9W-nI7v+_AIg|eUJpTn9Q0Ckm2efl;HT|4(qfh0% z%GAGHF*=9BdENhj56JTT8)cY7H#y$InnO$QU3vGcdd_XIe`Sn>590e}opNj_4>v)- z@(xh?eXavzEH>+%pU{8hU7qUqcjlq{ELG6I^4?9_0)3r9=0`yHeS5zq{E+evMA%|9 zXy>~RTVz4XLGe#59MjX8cmV?QIy)sz)8Rb@$fxLR^m^N(HSmxTUyG2LR`k>BXX+di9E14U z2REc{`XHS?ifNQhdYWRFo~GGrQp0sAC-tNqX?G_2tLT*3ui#;qVz1zZ)PSeG0dISK zIvw@~y3nygXJ(pCZ)Ul4dNbNdr?+AzokBP95WED>Af208^a>x0(5uu>Z`Vy}zm$sJ zLI%FuC8Wy%@({V`4e~M2TO&h`q*wFD_e3-U3;|@kC&HpD#%TBv{zH5_7~lp!?}9P5 zoGDZ>=GbFQ8v=jshB)*?#Fr+^nrZ{}TOf{cMH?=d59GSOv37xX1mjYSjme0W-ZoZ- z*DzMJ0}X#NCP&=BIeA-CWsO}&AH{(GHq5J10m-O81>?S{+&#fRbK~B8bGcK!5`&a6s*hn80Smi$qm2^fZ2PK>bChC?I!~_ zWsZt|OrCKD?_NTzex3NEf1Zi&sBKG|hisc*zIg&V!}F{P&VoN$e6 zK)^p7xalEF?rYE2;vjjH8Hm0j7|&ym@ecaTcm`a`s8h^es{#L=Y@2I^1oBDnXDrWk z1IBS^kIykL1C6=ft3w~VsQ-+w!K+Jx(KOx;geiP8%3cxR*Cx!D{(f}I!tJ^W>H{!}6*i(xm z{)T?`V4n3UI?jr1LwQM9W*4NpZO?n^2{S}uMJu9d$S79f%|c+S2aRAZBXwA z(BuR99dO?X=@quixg%t<&m5f9WgzFOuwi}+GYxG~4k@tZXz)mB6ZpqL?~~X*Wewc_ zME@^fj;3xV%Wt&#JY;#xU{|q_=~&=z3w&*HtvvcrzK;q17e;#(t&rs{x(cWBG>3y?AG zBiA;-w$Vpj@C>Hlbpw2&j*3jl^C<9a8}R923J>*l=zmx4?*cdCAB?s$&R(d?@i*G- z!@h_=Q}C;bwv4%X@T?SQcpfyY7yO|=p|FPzkWD4%ms#`rhlRh;3H=`{_`{}tmh0xG z4;z2*e~JqK3$Rrib8%<6hlM}&KTpBN z-oZG)XC|}xAHz711Y7d~l)o1Zz`qM@Db3)2Lg3rFnW}^K-XO2U|G)=TG*#Bvb@;j> z7=s4`_rK{kIc|{#;Xl`E=|AcJF-E4LKYsVDtg*J_8rp0J{Nso}WOfVqn~ZBwW<2>r z{}11jfZQg%MQSI6nOAHZ3=qHImIrHi)UDws*OnNEv{h z)~4ELYyld&gBQWXA7%BZ&u4dU0Z(PU9M^7vmPG0$`2t>@N7;s^+B9_?{Es%snQNmn zO_f8L7tu!u^^~y$_}|M^Szd<@6@?7OLAFX<#dw)?2>xhGp@Xl0+@^v* zbe?+?7RMhtR2a|$EdTb$M9?>GL^7vxt6xb74Dp-Xe1|7OKg z(uT40LE%rE2L5|h_`EMPSBCq(dv-k*a0ypJ?j&qQcx zxZn7r{dX`Hp9Ai9HTIhZ`{g&9eg+LKwRoAEhyTBeHk9=f{B|`}5Blo_pAks^Lmt8x z^)XeJ*I}=A@Sh_wp76VI%HE;ce{vk7?=<+&8@Tr-+RTgpybRoA{Mrg$Df4pB)Bt?p ze7hLhwFUn^#JCmB@eAV^*WcQi!XHbtI;;zHfzDhb&xtqs;BzpyMSpND0yHGc_yyja z#x*YI=IRgfU5W8si63B3|AKD=pqKT5`$ga#fHp@V{|xXriM59`#t(=Q&*5HuQyl^K zQsDgx#1ARNA2tvNzRUvt%gJ{Fu1^DxybwPm8sdj!(2(nQS0dJJ0X_v%hYWqifSz9@ zegmF?pyfJvuEahVdt*^<0oC%{G5wyg-=)Z~BVbrf`w#oWCGuP|kj!%@E*k za{dBdy-8ll{005wYW^a9GKGx00=7V3!-W5csrYQQA7B(@T~b}%)bHHPAA$etf zpXc=x;sf1&5_b263jaSqPp;OK9wz>z0r)?#^%G0-A7c&Q4b>brw?@T(p5tnRe#$|% zX2mNHga5GMitwWgfO{ZxJyr0h|A9^OEbn))U9od3B6q>oHCx!>=3lKk#w% zO_xCjw>xwEga7CHZ7O7T&-GjQge>c~@E6MbHOCHAH{tso;NRA8tfcQGfd2_9{wH(n zqW?FnpPz#teOA>(wx96-&jbH(;t$)80dJh)pKbA*YY@*vj?W=21YR&MnNR=BxCHUs z9mF1H>8NxHy?G6NMWRi{Kxy#T=P+ko&G{+EEAZnKt{+6~lgiiu*Wy9rOh8d-Lz|+_ zy1@4&a8%+dj0HUVR3G^vknb(D8H%(K@?7V*#CFLa^m$O**1huJ{~GAL5<8%uAn4M| zXtyG8-Ul4d19~Ih0{xt1tjNA#$4AkBEmLiy&7z>;Gqjt8dV17P0{%zXCdXRJ6TJTx z`P0ZJGyVa864BQnQ*D~M4&C`1GL*BK%Z-)e+zB{*gnnZU>km8|`hu~tat$=pg5K<7 z+q5YH@Kt0=8knbisguYPcP~J$bf0~pk3_Z${pb9By!5Y4QRWrIoqrqf=l=6O_fH{% z7U277(4GdJQEZDi6Mxzl^`0r(+5~yzI(9bTukZ!7mE-yY@b3=2QTDIdKl*dh(r7FX zJ7@|TU{WjZsQC|GvyEG@J5S(Wl`_uDALM)sdYTNoO@u7^nQ8-i&=x$r1RF>O&!W(O zj{CpbcF;cGQO$Q(Btedez?;_>di}x_D z6~f;oB$c;v`QiKi2q@H`Um4`68u?lzs7e9Dr*ALKYXV$=7Ja3flDQ0_0-qE|Kn(14;lOf|5XX?90T8{ z!jJXfIK-F+`D{Ez3||TJ8Rc9G(!JpSKJ?9R;Y@-Lk73M${=L+F8T*Y``X%sB*;_;Z zH`yoJi2?ozjHSSXlUz&D)lw>~2a`ar za<+rxKVmH<2EaJ*2l!!hEd~9%p&#I9A`+JZ2&$tizTMECc0N0mkE3m*8JHGjzM5On?|aYx?^L31qaAN*UP?N6JB-;JUF0}TVT_h9>H z=>O3FXWDzrBc}U^JfP2|>^R3m-!j(=!u5Neg`ocjt&GjPYx_W(yXgPX&R5!dk_X=J zLVh=-f6&Z5@gD(Zt<&YVf8qfi6hi<1BmGE+32x@3mgt{42O0gw{*ewgr(Y?P-~Pe> zzVH))D*h*f_nV-PW}VqZ|8dkI*g>{4B#`@0)IYR6Tkut<^c&EfjJcy)|Dd}8(1m-> z0nqOW+8^-nLt75u-%h|7;PgEFaI#@dC)>OWx?c`sPYmnA4v&G}QwIC{3N{dL7=KOb ztKY?n`q!4L;r(u_zLLA=7-_`hS6^5e}H>wj72;{tN0)Mp31ry zN5iF`oKo<>4fOK-WQPCc*aaNIfL9}9b;bW08-LM%I~jjLOOF0m+6E1d(4+kvf51QL zggg9_N%`e8{-FOC9_IL)m;OQjGw|aZ4dd?}=)`dF&g6clCFzG94MTrPv|;wIioZ@n z{ziQ``kx6pjmBS)TSoi^9kLf08%tqFZ6SAMZ58?)XOMp)ai9*v2lyJR2fG;o9ije5 zLVuq^-KM~2G_Dmw|6>f}Z#4I6bNU1Adki-GKj6_2JfOYJLp!6XLp+m?e73bX#Qs%a z_k6~mJlKKsByk50Zm`)XgZ=yD)Hm-z?squ;f`%+>i}dMeC)hCler&8A;685f&uaot&Lwe{y!3V|vQG;hsIqrI%;DY<2C8*0uAqWBusknRVGW*D1IxQ4^4{ zPEi;c6Mu*gr$WZcnF+|aA$)U|XDBdc*+4#{A*<5Hnt*+Bf1cgypc^BgBN=-Du%W83 zv)|AreSQS)XNv(*{}}XZ2lV+2bm0R0!l%*?><_nAZ9)Ij0Fz;_A#zVr=vOuLu?KZi zpdX8Xr=8qG+kVi!;mkwFs~UVywmBcly@-D4lPgPIrnr6-^_6%J^{dcd0M94YWpIBa zd_ofXPX)xlRx|eKP~R82`!RI363R3}{bcaLR$U)H=@x9XFL>V<_s#+Z!y8Ty0z zRVjaz$)Z0fa{%=Z0k6x54|(q0RqzMzy{VDs2DEE=t zet=tb(DXa_NBfDw{g<*-Mg}~djB<+oLIw>`E=!yZ95{F3_e!$Z@2QhI-KkkR-BH3K boh}q`)IB|a>NxHJlzX_Q+%w!~9oGFn6aYPu literal 0 HcmV?d00001 diff --git a/src/frontend/public/index.html b/src/frontend/public/index.html index 57757fb21..b2e9c4b82 100644 --- a/src/frontend/public/index.html +++ b/src/frontend/public/index.html @@ -4,7 +4,8 @@ - LangFLow + + LangFlow From f829cc3b599e44e1d651dfebbdf7767e49ec858b Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Fri, 31 Mar 2023 18:14:05 -0300 Subject: [PATCH 43/59] fix: change field to templatefield This is done to avoid conflict with pydantic --- src/backend/langflow/interface/base.py | 4 ++-- .../langflow/interface/toolkits/base.py | 5 +++-- src/backend/langflow/interface/tools/base.py | 12 +++++------ src/backend/langflow/template/base.py | 6 +++--- src/backend/langflow/template/nodes.py | 20 +++++++++---------- 5 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/backend/langflow/interface/base.py b/src/backend/langflow/interface/base.py index 66a3e3c29..2ec7bc12e 100644 --- a/src/backend/langflow/interface/base.py +++ b/src/backend/langflow/interface/base.py @@ -3,7 +3,7 @@ from typing import Any, Dict, List, Optional from pydantic import BaseModel -from langflow.template.base import Field, FrontendNode, Template +from langflow.template.base import TemplateField, FrontendNode, Template # Assuming necessary imports for Field, Template, and FrontendNode classes @@ -43,7 +43,7 @@ class LangChainTypeCreator(BaseModel, ABC): if signature is None: raise ValueError(f"{name} not found") fields = [ - Field( + TemplateField( name=key, field_type=value["type"], required=value.get("required", False), diff --git a/src/backend/langflow/interface/toolkits/base.py b/src/backend/langflow/interface/toolkits/base.py index 45f599491..0af46e504 100644 --- a/src/backend/langflow/interface/toolkits/base.py +++ b/src/backend/langflow/interface/toolkits/base.py @@ -47,13 +47,14 @@ class ToolkitCreator(LangChainTypeCreator): def to_list(self) -> List[str]: return list(self.type_to_loader_dict.keys()) - def get_create_function(self, name: str) -> Callable | None: + def get_create_function(self, name: str) -> Callable: if loader_name := self.create_functions.get(name, None): # import loader return import_module( f"from langchain.agents.agent_toolkits import {loader_name[0]}" ) - return None + else: + raise ValueError("Loader not found") def has_create_function(self, name: str) -> bool: # check if the function list is not empty diff --git a/src/backend/langflow/interface/tools/base.py b/src/backend/langflow/interface/tools/base.py index 31e638955..d7bf655e2 100644 --- a/src/backend/langflow/interface/tools/base.py +++ b/src/backend/langflow/interface/tools/base.py @@ -20,11 +20,11 @@ from langflow.interface.tools.util import ( get_tools_dict, ) from langflow.settings import settings -from langflow.template.base import Field, Template +from langflow.template.base import TemplateField, Template from langflow.utils import util TOOL_INPUTS = { - "str": Field( + "str": TemplateField( field_type="str", required=True, is_list=False, @@ -32,15 +32,15 @@ TOOL_INPUTS = { placeholder="", value="", ), - "llm": Field(field_type="BaseLLM", required=True, is_list=False, show=True), - "func": Field( + "llm": TemplateField(field_type="BaseLLM", required=True, is_list=False, show=True), + "func": TemplateField( field_type="function", required=True, is_list=False, show=True, multiline=True, ), - "code": Field( + "code": TemplateField( field_type="str", required=True, is_list=False, @@ -48,7 +48,7 @@ TOOL_INPUTS = { value="", multiline=True, ), - "dict_": Field( + "dict_": TemplateField( field_type="file", required=True, is_list=False, diff --git a/src/backend/langflow/template/base.py b/src/backend/langflow/template/base.py index 96805de68..bcd6ed162 100644 --- a/src/backend/langflow/template/base.py +++ b/src/backend/langflow/template/base.py @@ -4,7 +4,7 @@ from typing import Any, Union from pydantic import BaseModel -class FieldCreator(BaseModel, ABC): +class TemplateFieldCreator(BaseModel, ABC): field_type: str = "str" required: bool = False placeholder: str = "" @@ -37,13 +37,13 @@ class FieldCreator(BaseModel, ABC): return result -class Field(FieldCreator): +class TemplateField(TemplateFieldCreator): pass class Template(BaseModel): type_name: str - fields: list[Field] + fields: list[TemplateField] def to_dict(self): result = {field.name: field.to_dict() for field in self.fields} diff --git a/src/backend/langflow/template/nodes.py b/src/backend/langflow/template/nodes.py index 76d889b32..fae298f2d 100644 --- a/src/backend/langflow/template/nodes.py +++ b/src/backend/langflow/template/nodes.py @@ -1,6 +1,6 @@ from langchain.agents.mrkl import prompt -from langflow.template.base import Field, FrontendNode, Template +from langflow.template.base import TemplateField, FrontendNode, Template from langflow.utils.constants import DEFAULT_PYTHON_FUNCTION @@ -9,7 +9,7 @@ class ZeroShotPromptNode(FrontendNode): template: Template = Template( type_name="zero_shot", fields=[ - Field( + TemplateField( field_type="str", required=False, placeholder="", @@ -19,7 +19,7 @@ class ZeroShotPromptNode(FrontendNode): value=prompt.PREFIX, name="prefix", ), - Field( + TemplateField( field_type="str", required=True, placeholder="", @@ -29,7 +29,7 @@ class ZeroShotPromptNode(FrontendNode): value=prompt.SUFFIX, name="suffix", ), - Field( + TemplateField( field_type="str", required=False, placeholder="", @@ -53,7 +53,7 @@ class PythonFunctionNode(FrontendNode): template: Template = Template( type_name="python_function", fields=[ - Field( + TemplateField( field_type="code", required=True, placeholder="", @@ -76,7 +76,7 @@ class ToolNode(FrontendNode): template: Template = Template( type_name="tool", fields=[ - Field( + TemplateField( field_type="str", required=True, placeholder="", @@ -86,7 +86,7 @@ class ToolNode(FrontendNode): value="", name="name", ), - Field( + TemplateField( field_type="str", required=True, placeholder="", @@ -96,7 +96,7 @@ class ToolNode(FrontendNode): value="", name="description", ), - Field( + TemplateField( field_type="str", required=True, placeholder="", @@ -120,13 +120,13 @@ class JsonAgentNode(FrontendNode): template: Template = Template( type_name="json_agent", fields=[ - Field( + TemplateField( field_type="BaseToolkit", required=True, show=True, name="toolkit", ), - Field( + TemplateField( field_type="BaseLanguageModel", required=True, show=True, From c6d0a8d8faa1ecf93ae7066b9431b9dc47cca221 Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Fri, 31 Mar 2023 18:16:20 -0300 Subject: [PATCH 44/59] feat: working version of OpenAPITollkit --- src/backend/langflow/__main__.py | 3 +- src/backend/langflow/api/endpoints.py | 4 ++ src/backend/langflow/graph/base.py | 54 ++++++++++++++------ src/backend/langflow/graph/constants.py | 1 + src/backend/langflow/graph/graph.py | 10 ++++ src/backend/langflow/graph/nodes.py | 61 +++++++++-------------- src/backend/langflow/graph/utils.py | 22 +++++++- src/backend/langflow/interface/loading.py | 8 ++- src/backend/langflow/interface/run.py | 6 +-- 9 files changed, 106 insertions(+), 63 deletions(-) create mode 100644 src/backend/langflow/graph/constants.py diff --git a/src/backend/langflow/__main__.py b/src/backend/langflow/__main__.py index fe3bac79d..030a41ed0 100644 --- a/src/backend/langflow/__main__.py +++ b/src/backend/langflow/__main__.py @@ -30,6 +30,7 @@ def serve( timeout: int = 60, port: int = 7860, config: str = "config.yaml", + log_level: str = "info", ): update_settings(config) app = create_app() @@ -54,7 +55,7 @@ def serve( # MacOS requires a env variable to be set to use gunicorn import uvicorn - uvicorn.run(app, host=host, port=port, log_level="info") + uvicorn.run(app, host=host, port=port, log_level=log_level) else: from langflow.server import LangflowApplication diff --git a/src/backend/langflow/api/endpoints.py b/src/backend/langflow/api/endpoints.py index bb93b136f..41089e699 100644 --- a/src/backend/langflow/api/endpoints.py +++ b/src/backend/langflow/api/endpoints.py @@ -6,9 +6,11 @@ from langflow.api.base import Code, ValidationResponse from langflow.interface.run import process_graph from langflow.interface.types import build_langchain_types_dict from langflow.utils.validate import validate_code +import logging # build router router = APIRouter() +logger = logging.getLogger(__name__) @router.get("/all") @@ -21,6 +23,8 @@ def get_load(data: Dict[str, Any]): try: return process_graph(data) except Exception as e: + # Log stack trace + logger.exception(e) raise HTTPException(status_code=500, detail=str(e)) from e diff --git a/src/backend/langflow/graph/base.py b/src/backend/langflow/graph/base.py index 38f3d1746..0c4cf8705 100644 --- a/src/backend/langflow/graph/base.py +++ b/src/backend/langflow/graph/base.py @@ -6,11 +6,15 @@ import types from copy import deepcopy from typing import Any, Dict, List +from langflow.graph.constants import DIRECT_TYPES from langflow.graph.utils import load_dict from langflow.interface import loading from langflow.interface.listing import ALL_TYPES_DICT -from langflow.interface.tools.base import tool_creator + +import logging + +logger = logging.getLogger(__name__) class Node: @@ -86,31 +90,38 @@ class Node: type_to_load = value.get("suffixes") file_name = value.get("value") content = value.get("content") - # Now loaded_dict = load_dict(file_name, content, type_to_load) params[key] = loaded_dict - elif value["type"] not in ["str", "bool", "code", "int", "float"]: + # We should check if the type is in something not + # the opposite + elif value["type"] not in DIRECT_TYPES: # Get the edge that connects to this node - edge = next( - ( - edge - for edge in self.edges - if edge.target == self and edge.matched_type in value["type"] - ), - None, - ) + try: + edge = next( + ( + edge + for edge in self.edges + if edge.target == self + and edge.matched_type in value["type"] + ), + None, + ) + + except Exception as e: + raise e # Get the output of the node that the edge connects to # if the value['list'] is True, then there will be more # than one time setting to params[key] # so we need to append to a list if it exists # or create a new list if it doesn't + if edge is None and value["required"]: # break line raise ValueError( f"Required input {key} for module {self.node_type} not found" ) - if value["list"]: + elif value["list"]: if key in params: params[key].append(edge.source) else: @@ -134,11 +145,15 @@ class Node: # and continue # Another aspect is that the node_type is the class that we need to import # and instantiate with these built params - + logger.debug(f"Building {self.node_type}") # Build each node in the params dict - for key, value in self.params.items(): - # Check if Node or list of Nodes + for key, value in self.params.copy().items(): + # Check if Node or list of Nodes and not self + # to avoid recursion if isinstance(value, Node): + if value == self: + del self.params[key] + continue result = value.build() # If the key is "func", then we need to use the run method if key == "func" and not isinstance(result, types.FunctionType): @@ -220,6 +235,15 @@ class Edge: ), None, ) + no_matched_type = self.matched_type is None + if no_matched_type: + logger.debug(self.source_types) + logger.debug(self.target_reqs) + if no_matched_type: + raise ValueError( + f"Edge between {self.source.node_type} and {self.target.node_type} " + f"has no matched type" + ) def __repr__(self) -> str: return ( diff --git a/src/backend/langflow/graph/constants.py b/src/backend/langflow/graph/constants.py new file mode 100644 index 000000000..095843816 --- /dev/null +++ b/src/backend/langflow/graph/constants.py @@ -0,0 +1 @@ +DIRECT_TYPES = ["str", "bool", "code", "int", "float", "Any"] diff --git a/src/backend/langflow/graph/graph.py b/src/backend/langflow/graph/graph.py index cd4632b61..ad6cb8d28 100644 --- a/src/backend/langflow/graph/graph.py +++ b/src/backend/langflow/graph/graph.py @@ -39,9 +39,19 @@ class Graph: edge.source.add_edge(edge) edge.target.add_edge(edge) + # This is a hack to make sure that the LLM node is sent to + # the toolkit node + llm_node = None for node in self.nodes: node._build_params() + if isinstance(node, LLMNode): + llm_node = node + + for node in self.nodes: + if isinstance(node, ToolkitNode): + node.params["llm"] = llm_node + def get_node(self, node_id: str) -> Union[None, Node]: return next((node for node in self.nodes if node.id == node_id), None) diff --git a/src/backend/langflow/graph/nodes.py b/src/backend/langflow/graph/nodes.py index 245e3f2f5..4ed0ba8a3 100644 --- a/src/backend/langflow/graph/nodes.py +++ b/src/backend/langflow/graph/nodes.py @@ -3,12 +3,14 @@ from copy import deepcopy from typing import Any, Dict, List, Optional, Union from langflow.graph.base import Node +from langflow.graph.utils import extract_input_variables_from_prompt from langflow.interface.toolkits.base import toolkits_creator class AgentNode(Node): def __init__(self, data: Dict): super().__init__(data, base_type="agents") + self.tools: List[ToolNode] = [] self.chains: List[ChainNode] = [] @@ -55,14 +57,24 @@ class PromptNode(Node): tools: Optional[Union[List[Node], List[ToolNode]]] = None, ) -> Any: if not self._built or force: + if "input_variables" not in self.params: + self.params["input_variables"] = [] # Check if it is a ZeroShotPrompt and needs a tool - if self.node_type == "ZeroShotPrompt": + if "ShotPrompt" in self.node_type: tools = ( [tool_node.build() for tool_node in tools] if tools is not None else [] ) self.params["tools"] = tools + # Extract the input variables from the prompt + prompt_params = ["prefix", "suffix"] + else: + prompt_params = ["template"] + for param in prompt_params: + prompt_text = self.params[param] + variables = extract_input_variables_from_prompt(prompt_text) + self.params["input_variables"].extend(variables) self._build() return deepcopy(self._built_object) @@ -88,42 +100,6 @@ class ChainNode(Node): return deepcopy(self._built_object) -class ToolkitNode(Node): - def __init__(self, data: Dict): - super().__init__(data, base_type="toolkits") - - def build(self, force: bool = False) -> Any: - if not self._built or force: - if toolkits_creator.has_create_function(self.node_type): - self.find_llm() - self._build() - # Now that the toolkit is built, we need to find the llm - # and add it to the self.params - - # go through the edges and find the llm - - return deepcopy(self._built_object) - - def find_llm(self, node=None, edges_visited=[]) -> None: - if node is None: - node = self - # Move recursively through the edges - # the targets of this node edges are this node - # If we find an LLMNode, we add it to the params - if len(node.edges) == 1: - return - for edge in node.edges: - source = edge.source - if source in edges_visited: - continue - edges_visited.append(source) - if isinstance(source, LLMNode): - self.params["llm"] = source.build() - break - else: - self.find_llm(source, edges_visited) - - class LLMNode(Node): def __init__(self, data: Dict): super().__init__(data, base_type="llms") @@ -134,6 +110,17 @@ class LLMNode(Node): return deepcopy(self._built_object) +class ToolkitNode(Node): + def __init__(self, data: Dict): + super().__init__(data, base_type="toolkits") + + def build(self, force: bool = False) -> Any: + if not self._built or force: + self._build() + + return deepcopy(self._built_object) + + class FileToolNode(ToolNode): def __init__(self, data: Dict): super().__init__(data) diff --git a/src/backend/langflow/graph/utils.py b/src/backend/langflow/graph/utils.py index 89e3a0f48..70f3a3145 100644 --- a/src/backend/langflow/graph/utils.py +++ b/src/backend/langflow/graph/utils.py @@ -1,7 +1,7 @@ import base64 import json from typing import Dict - +import re import yaml @@ -24,3 +24,23 @@ def load_dict(file_name, file_content, accepted_types) -> Dict: elif suffix in ["yaml", "yml"]: # Return the yaml content return yaml.safe_load(decoded_string) + else: + raise ValueError(f"File {file_name} is not accepted") + + +def validate_prompt(prompt: str): + """Validate prompt.""" + if extract_input_variables_from_prompt(prompt): + return prompt + + return fix_prompt(prompt) + + +def fix_prompt(prompt: str): + """Fix prompt.""" + return prompt + " {input}" + + +def extract_input_variables_from_prompt(prompt: str) -> list[str]: + """Extract input variables from prompt.""" + return re.findall(r"{(.*?)}", prompt) diff --git a/src/backend/langflow/interface/loading.py b/src/backend/langflow/interface/loading.py index adc01c933..2556abbdf 100644 --- a/src/backend/langflow/interface/loading.py +++ b/src/backend/langflow/interface/loading.py @@ -1,5 +1,5 @@ import json -from typing import Any, Dict, Optional +from typing import Any, Callable, Dict, Optional from langchain.agents import ZeroShotAgent from langchain.agents import agent as agent_module @@ -146,11 +146,9 @@ def load_agent_executor(agent_class: type[agent_module.Agent], params, **kwargs) def load_toolkits_executor(node_type: str, toolkit: BaseToolkit, params: dict): - create_function = toolkits_creator.get_create_function(node_type) - llm = params.get("llm", None) - if llm: + create_function: Callable = toolkits_creator.get_create_function(node_type) + if llm := params.get("llm"): return create_function(llm=llm, toolkit=toolkit) - return def load_tools_from_config(tool_list: list[dict]) -> list: diff --git a/src/backend/langflow/interface/run.py b/src/backend/langflow/interface/run.py index b6853a1c0..360bad364 100644 --- a/src/backend/langflow/interface/run.py +++ b/src/backend/langflow/interface/run.py @@ -48,10 +48,8 @@ def get_result_and_thought_using_graph(loaded_langchain, message: str): ) thought = output_buffer.getvalue() - except Exception as e: - result = f"Error: {str(e)}" - thought = "" - raise e + except Exception as exc: + raise ValueError(f"Error: {str(exc)}") from exc return result, thought From 9ef10787861d92e7033d9ebabb2bdbf0d1ceff5d Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Fri, 31 Mar 2023 18:19:45 -0300 Subject: [PATCH 45/59] fix: prompt_params now come from params --- src/backend/langflow/graph/nodes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/backend/langflow/graph/nodes.py b/src/backend/langflow/graph/nodes.py index 4ed0ba8a3..c6834e842 100644 --- a/src/backend/langflow/graph/nodes.py +++ b/src/backend/langflow/graph/nodes.py @@ -67,8 +67,9 @@ class PromptNode(Node): else [] ) self.params["tools"] = tools - # Extract the input variables from the prompt - prompt_params = ["prefix", "suffix"] + prompt_params = [ + key for key, value in self.params.items() if value["type"] == "str" + ] else: prompt_params = ["template"] for param in prompt_params: From 021ede2f728d5e012060239f24d3240a36fb8b7b Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Fri, 31 Mar 2023 18:49:31 -0300 Subject: [PATCH 46/59] feat: add toolkits to settings --- src/backend/langflow/config.yaml | 4 ++++ src/backend/langflow/interface/toolkits/base.py | 3 ++- src/backend/langflow/settings.py | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/backend/langflow/config.yaml b/src/backend/langflow/config.yaml index b8c2be6da..114db79d4 100644 --- a/src/backend/langflow/config.yaml +++ b/src/backend/langflow/config.yaml @@ -28,4 +28,8 @@ tools: wrappers: - RequestsWrapper +toolkits: + - OpenAPIToolkit + - JsonToolkit + dev: false diff --git a/src/backend/langflow/interface/toolkits/base.py b/src/backend/langflow/interface/toolkits/base.py index 0af46e504..c78a9c051 100644 --- a/src/backend/langflow/interface/toolkits/base.py +++ b/src/backend/langflow/interface/toolkits/base.py @@ -4,6 +4,7 @@ from langchain.agents import agent_toolkits from langflow.interface.base import LangChainTypeCreator from langflow.interface.importing.utils import import_class, import_module +from langflow.settings import settings from langflow.utils.util import build_template_from_class @@ -33,7 +34,7 @@ class ToolkitCreator(LangChainTypeCreator): ) # if toolkit_name is not lower case it is a class for toolkit_name in agent_toolkits.__all__ - if not toolkit_name.islower() + if not toolkit_name.islower() and toolkit_name in settings.toolkits } return self.type_dict diff --git a/src/backend/langflow/settings.py b/src/backend/langflow/settings.py index 092bea040..a8c2edc4a 100644 --- a/src/backend/langflow/settings.py +++ b/src/backend/langflow/settings.py @@ -13,6 +13,7 @@ class Settings(BaseSettings): tools: List[str] = [] memories: List[str] = [] wrappers: List[str] = [] + toolkits: List[str] = [] dev: bool = False class Config: @@ -35,6 +36,7 @@ class Settings(BaseSettings): self.tools = new_settings.tools or [] self.memories = new_settings.memories or [] self.wrappers = new_settings.wrappers or [] + self.toolkits = new_settings.toolkits or [] self.dev = new_settings.dev or False From be71829493f5bcee58081cf6ac3c334820873dad Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Fri, 31 Mar 2023 22:05:08 -0300 Subject: [PATCH 47/59] fix: tools now show the params properly --- src/backend/langflow/interface/tools/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/langflow/interface/tools/base.py b/src/backend/langflow/interface/tools/base.py index d7bf655e2..8d14e3612 100644 --- a/src/backend/langflow/interface/tools/base.py +++ b/src/backend/langflow/interface/tools/base.py @@ -112,7 +112,7 @@ class ToolCreator(LangChainTypeCreator): # Copy the field and add the name fields = [] for param in params: - field = TOOL_INPUTS.get(param, TOOL_INPUTS["str"]) + field = TOOL_INPUTS.get(param, TOOL_INPUTS["str"]).copy() field.name = param if param == "aiosession": field.show = False From e28c3dc18eec13326716b5a0e1d592bdcb06068a Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Fri, 31 Mar 2023 23:15:22 -0300 Subject: [PATCH 48/59] feat: new tests --- tests/test_creators.py | 49 +++++++ tests/test_frontend_nodes.py | 60 +++++++++ tests/test_template.py | 242 +++++++++++++++++++++++++++++++++++ 3 files changed, 351 insertions(+) create mode 100644 tests/test_creators.py create mode 100644 tests/test_frontend_nodes.py create mode 100644 tests/test_template.py diff --git a/tests/test_creators.py b/tests/test_creators.py new file mode 100644 index 000000000..08b2fc9d1 --- /dev/null +++ b/tests/test_creators.py @@ -0,0 +1,49 @@ +from typing import Dict, List +from langflow.interface.base import LangChainTypeCreator +from langflow.interface.agents.base import AgentCreator +import pytest + + +@pytest.fixture +def sample_lang_chain_type_creator() -> LangChainTypeCreator: + class SampleLangChainTypeCreator(LangChainTypeCreator): + type_name: str = "test_type" + + def type_to_loader_dict(self) -> Dict: + return {"test_type": "TestClass"} + + def to_list(self) -> List[str]: + return ["node1", "node2"] + + def get_signature(self, name: str) -> Dict: + return { + "template": {"test_field": {"type": "str"}}, + "description": "test description", + "base_classes": ["base_class1", "base_class2"], + } + + return SampleLangChainTypeCreator() + + +@pytest.fixture +def sample_agent_creator() -> AgentCreator: + return AgentCreator() + + +def test_lang_chain_type_creator_to_dict( + sample_lang_chain_type_creator: LangChainTypeCreator, +): + type_dict = sample_lang_chain_type_creator.to_dict() + assert len(type_dict) == 1 + assert "test_type" in type_dict + assert "node1" in type_dict["test_type"] + assert "node2" in type_dict["test_type"] + assert "template" in type_dict["test_type"]["node1"] + assert "description" in type_dict["test_type"]["node1"] + assert "base_classes" in type_dict["test_type"]["node1"] + + +def test_agent_creator_type_to_loader_dict(sample_agent_creator: AgentCreator): + type_to_loader_dict = sample_agent_creator.type_to_loader_dict + assert len(type_to_loader_dict) > 0 + assert "JsonAgent" diff --git a/tests/test_frontend_nodes.py b/tests/test_frontend_nodes.py new file mode 100644 index 000000000..75b0b741f --- /dev/null +++ b/tests/test_frontend_nodes.py @@ -0,0 +1,60 @@ +import pytest +from typing import Dict, List +from langflow.template.base import TemplateField, FrontendNode, Template +from langflow.interface.base import LangChainTypeCreator +from langflow.interface.agents.base import AgentCreator + + +@pytest.fixture +def sample_template_field() -> TemplateField: + return TemplateField(name="test_field", field_type="str") + + +@pytest.fixture +def sample_template(sample_template_field: TemplateField) -> Template: + return Template(type_name="test_template", fields=[sample_template_field]) + + +@pytest.fixture +def sample_frontend_node(sample_template: Template) -> FrontendNode: + return FrontendNode( + template=sample_template, + description="test description", + base_classes=["base_class1", "base_class2"], + name="test_frontend_node", + ) + + +def test_template_field_defaults(sample_template_field: TemplateField): + assert sample_template_field.field_type == "str" + assert sample_template_field.required == False + assert sample_template_field.placeholder == "" + assert sample_template_field.is_list == False + assert sample_template_field.show == True + assert sample_template_field.multiline == False + assert sample_template_field.value == None + assert sample_template_field.suffixes == [] + assert sample_template_field.file_types == [] + assert sample_template_field.content == None + assert sample_template_field.password == False + assert sample_template_field.name == "test_field" + + +def test_template_to_dict( + sample_template: Template, sample_template_field: TemplateField +): + template_dict = sample_template.to_dict() + assert template_dict["_type"] == "test_template" + assert len(template_dict) == 2 # _type and test_field + assert "test_field" in template_dict + assert "type" in template_dict["test_field"] + assert "required" in template_dict["test_field"] + + +def test_frontend_node_to_dict(sample_frontend_node: FrontendNode): + node_dict = sample_frontend_node.to_dict() + assert len(node_dict) == 1 + assert "test_frontend_node" in node_dict + assert "description" in node_dict["test_frontend_node"] + assert "template" in node_dict["test_frontend_node"] + assert "base_classes" in node_dict["test_frontend_node"] diff --git a/tests/test_template.py b/tests/test_template.py new file mode 100644 index 000000000..102df3b56 --- /dev/null +++ b/tests/test_template.py @@ -0,0 +1,242 @@ +from langflow.utils.constants import CHAT_OPENAI_MODELS, OPENAI_MODELS +from pydantic import BaseModel +import pytest +import re +import importlib +from typing import Dict +from langflow.utils.util import ( + build_template_from_class, + format_dict, + get_base_classes, + get_default_factory, + get_class_doc, +) + + +# Dummy classes for testing purposes +class Parent(BaseModel): + """Parent Class""" + + parent_field: str + + +class Child(Parent): + """Child Class""" + + child_field: int + + +# Test build_template_from_class +def test_build_template_from_class(): + type_to_cls_dict: Dict[str, type] = {"parent": Parent, "child": Child} + + # Test valid input + result = build_template_from_class("Child", type_to_cls_dict) + assert "template" in result + assert "description" in result + assert "base_classes" in result + assert "Child" in result["base_classes"] + assert "Parent" in result["base_classes"] + assert result["description"] == "Child Class" + + # Test invalid input + with pytest.raises(ValueError, match="InvalidClass not found."): + build_template_from_class("InvalidClass", type_to_cls_dict) + + +# Test format_dict +def test_format_dict(): + # Test 1: Optional type removal + input_dict = { + "field1": {"type": "Optional[str]", "required": False}, + } + expected_output = { + "field1": { + "type": "str", + "required": False, + "list": False, + "show": False, + "password": False, + "multiline": False, + }, + } + assert format_dict(input_dict) == expected_output + + # Test 2: List type processing + input_dict = { + "field1": {"type": "List[str]", "required": False}, + } + expected_output = { + "field1": { + "type": "str", + "required": False, + "list": True, + "show": False, + "password": False, + "multiline": False, + }, + } + assert format_dict(input_dict) == expected_output + + # Test 3: Mapping type replacement + input_dict = { + "field1": {"type": "Mapping[str, int]", "required": False}, + } + expected_output = { + "field1": { + "type": "code", # Mapping type is replaced with dict which is replaced with code + "required": False, + "list": False, + "show": False, + "password": False, + "multiline": False, + }, + } + assert format_dict(input_dict) == expected_output + + # Test 4: Replace default value with actual value + input_dict = { + "field1": {"type": "str", "required": False, "default": "test"}, + } + expected_output = { + "field1": { + "type": "str", + "required": False, + "list": False, + "show": False, + "password": False, + "multiline": False, + "value": "test", + }, + } + assert format_dict(input_dict) == expected_output + + # Test 5: Add password field + input_dict = { + "field1": {"type": "str", "required": False}, + "api_key": {"type": "str", "required": False}, + } + expected_output = { + "field1": { + "type": "str", + "required": False, + "list": False, + "show": False, + "password": False, + "multiline": False, + }, + "api_key": { + "type": "str", + "required": False, + "list": False, + "show": True, + "password": True, + "multiline": False, + }, + } + assert format_dict(input_dict) == expected_output + + # Test 6: Add multiline + input_dict = { + "field1": {"type": "str", "required": False}, + "prefix": {"type": "str", "required": False}, + } + expected_output = { + "field1": { + "type": "str", + "required": False, + "list": False, + "show": False, + "password": False, + "multiline": False, + }, + "prefix": { + "type": "str", + "required": False, + "list": False, + "show": True, + "password": False, + "multiline": True, + }, + } + assert format_dict(input_dict) == expected_output + + # Test 7: Check class name-specific cases (OpenAI, OpenAIChat) + input_dict = { + "model_name": {"type": "str", "required": False}, + } + expected_output_openai = { + "model_name": { + "type": "str", + "required": False, + "list": True, + "show": True, + "password": False, + "multiline": False, + "options": OPENAI_MODELS, + }, + } + expected_output_openai_chat = { + "model_name": { + "type": "str", + "required": False, + "list": True, + "show": True, + "password": False, + "multiline": False, + "options": CHAT_OPENAI_MODELS, + }, + } + assert format_dict(input_dict, "OpenAI") == expected_output_openai + assert format_dict(input_dict, "OpenAIChat") == expected_output_openai_chat + + # Test 8: Replace dict type with str + input_dict = { + "field1": {"type": "Dict[str, int]", "required": False}, + } + expected_output = { + "field1": { + "type": "code", + "required": False, + "list": False, + "show": False, + "password": False, + "multiline": False, + }, + } + assert format_dict(input_dict) == expected_output + + +# Test get_base_classes +def test_get_base_classes(): + base_classes_parent = get_base_classes(Parent) + base_classes_child = get_base_classes(Child) + + assert "Parent" in base_classes_parent + assert "Child" in base_classes_child + assert "Parent" in base_classes_child + + +# Test get_default_factory +def test_get_default_factory(): + module_name = "langflow.utils.util" + function_repr = "" + + def dummy_function(): + return "default_value" + + # Add dummy_function to your_module + setattr(importlib.import_module(module_name), "dummy_function", dummy_function) + + default_value = get_default_factory(module_name, function_repr) + + assert default_value == "default_value" + + +# Test get_class_doc +def test_get_class_doc(): + class_doc_parent = get_class_doc(Parent) + class_doc_child = get_class_doc(Child) + + assert class_doc_parent["Description"] == "Parent Class" + assert class_doc_child["Description"] == "Child Class" From b0615e9a4e09707bb67eb1aae581b3956c453d16 Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Fri, 31 Mar 2023 23:15:46 -0300 Subject: [PATCH 49/59] fix: remove format_instructions from input_v extrac --- src/backend/langflow/graph/nodes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/backend/langflow/graph/nodes.py b/src/backend/langflow/graph/nodes.py index c6834e842..7f29e3aa7 100644 --- a/src/backend/langflow/graph/nodes.py +++ b/src/backend/langflow/graph/nodes.py @@ -68,7 +68,9 @@ class PromptNode(Node): ) self.params["tools"] = tools prompt_params = [ - key for key, value in self.params.items() if value["type"] == "str" + key + for key, value in self.params.items() + if isinstance(value, str) and key != "format_instructions" ] else: prompt_params = ["template"] From 48d2ab27daeed601ab5f6532f3e055f9a6436ba1 Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Fri, 31 Mar 2023 23:16:16 -0300 Subject: [PATCH 50/59] fix: added options and cls to base_classes --- src/backend/langflow/template/base.py | 92 ++++++++++++++++++++++++++- src/backend/langflow/utils/util.py | 4 +- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/src/backend/langflow/template/base.py b/src/backend/langflow/template/base.py index bcd6ed162..e050be55e 100644 --- a/src/backend/langflow/template/base.py +++ b/src/backend/langflow/template/base.py @@ -1,5 +1,6 @@ from abc import ABC -from typing import Any, Union +from typing import Any, Optional, Union, Dict +from langflow.utils import constants from pydantic import BaseModel @@ -16,6 +17,7 @@ class TemplateFieldCreator(BaseModel, ABC): file_types: list[str] = [] content: Union[str, None] = None password: bool = False + options: list[str] = [] # _name will be used to store the name of the field # in the template name: str = "" @@ -36,6 +38,88 @@ class TemplateFieldCreator(BaseModel, ABC): result["content"] = self.content return result + def process_field( + self, key: str, value: Dict[str, Any], name: Optional[str] = None + ) -> None: + _type = value["type"] + + # Remove 'Optional' wrapper + if "Optional" in _type: + _type = _type.replace("Optional[", "")[:-1] + + # Check for list type + if "List" in _type: + _type = _type.replace("List[", "")[:-1] + self.is_list = True + else: + self.is_list = False + + # Replace 'Mapping' with 'dict' + if "Mapping" in _type: + _type = _type.replace("Mapping", "dict") + + # Change type from str to Tool + self.field_type = "Tool" if key in ["allowed_tools"] else _type + + self.field_type = "int" if key in ["max_value_length"] else self.field_type + + # Show or not field + self.show = bool( + (self.required and key not in ["input_variables"]) + or key + in [ + "allowed_tools", + "memory", + "prefix", + "examples", + "temperature", + "model_name", + "headers", + "max_value_length", + ] + or "api_key" in key + ) + + # Add password field + self.password = any( + text in key.lower() for text in ["password", "token", "api", "key"] + ) + + # Add multline + self.multiline = key in [ + "suffix", + "prefix", + "template", + "examples", + "code", + "headers", + ] + + # Replace dict type with str + if "dict" in self.field_type.lower(): + self.field_type = "code" + + if key == "dict_": + self.field_type = "file" + self.suffixes = [".json", ".yaml", ".yml"] + self.file_types = ["json", "yaml", "yml"] + + # Replace default value with actual value + if "default" in value: + self.value = value["default"] + + if key == "headers": + self.value = """{'Authorization': + 'Bearer '}""" + + # Add options to openai + if name == "OpenAI" and key == "model_name": + self.options = constants.OPENAI_MODELS + self.is_list = True + elif name == "OpenAIChat" and key == "model_name": + self.options = constants.CHAT_OPENAI_MODELS + self.is_list = True + class TemplateField(TemplateFieldCreator): pass @@ -45,7 +129,13 @@ class Template(BaseModel): type_name: str fields: list[TemplateField] + def process_fields(self, name: Optional[str] = None) -> None: + for field in self.fields: + signature = field.to_dict() + field.process_field(field.name, signature, name) + def to_dict(self): + self.process_fields(self.type_name) result = {field.name: field.to_dict() for field in self.fields} result["_type"] = self.type_name # type: ignore return result diff --git a/src/backend/langflow/utils/util.py b/src/backend/langflow/utils/util.py index 961058950..2fee19475 100644 --- a/src/backend/langflow/utils/util.py +++ b/src/backend/langflow/utils/util.py @@ -173,7 +173,7 @@ def get_base_classes(cls): result = [cls.__name__] if not result: result = [cls.__name__] - return list(set(result)) + return list(set(result + [cls.__name__])) def get_default_factory(module: str, function: str): @@ -333,8 +333,10 @@ def format_dict(d, name: Optional[str] = None): # Add options to openai if name == "OpenAI" and key == "model_name": value["options"] = constants.OPENAI_MODELS + value["list"] = True elif name == "OpenAIChat" and key == "model_name": value["options"] = constants.CHAT_OPENAI_MODELS + value["list"] = True return d From 0fc454f9b7fb48d613cec8ba8d2ece408386fd74 Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Sat, 1 Apr 2023 00:01:29 -0300 Subject: [PATCH 51/59] feat: added more tests for nodes --- tests/conftest.py | 3 + tests/data/Openapi.json | 445 ++++++++++++++++++++++++++++++++++++++++ tests/test_graph.py | 285 ++++++++++++++++++------- 3 files changed, 662 insertions(+), 71 deletions(-) create mode 100644 tests/data/Openapi.json diff --git a/tests/conftest.py b/tests/conftest.py index 3c9837957..e6eb3562f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,6 +11,9 @@ def pytest_configure(): pytest.COMPLEX_EXAMPLE_PATH = ( Path(__file__).parent.absolute() / "data" / "complex_example.json" ) + pytest.OPENAPI_EXAMPLE_PATH = ( + Path(__file__).parent.absolute() / "data" / "Openapi.json" + ) pytest.CODE_WITH_SYNTAX_ERROR = """ def get_text(): diff --git a/tests/data/Openapi.json b/tests/data/Openapi.json new file mode 100644 index 000000000..d51404bbb --- /dev/null +++ b/tests/data/Openapi.json @@ -0,0 +1,445 @@ +{ + "description": "", + "name": "openapi", + "id": "1", + "data": { + "nodes": [ + { + "width": 384, + "height": 311, + "id": "dndnode_19", + "type": "genericNode", + "position": { + "x": -207.85635949789724, + "y": -105.73915116823618 + }, + "data": { + "type": "JsonToolkit", + "node": { + "template": { + "spec": { + "required": true, + "placeholder": "", + "show": true, + "multiline": false, + "password": false, + "name": "spec", + "type": "JsonSpec", + "list": false + }, + "_type": "JsonToolkit" + }, + "description": "Toolkit for interacting with a JSON spec.", + "base_classes": [ + "BaseToolkit" + ] + }, + "id": "dndnode_19", + "value": null + }, + "selected": false, + "positionAbsolute": { + "x": -207.85635949789724, + "y": -105.73915116823618 + }, + "dragging": false + }, + { + "width": 384, + "height": 351, + "id": "dndnode_32", + "type": "genericNode", + "position": { + "x": 745.308873444751, + "y": -37.007911201107675 + }, + "data": { + "type": "OpenAPIToolkit", + "node": { + "template": { + "json_agent": { + "required": true, + "placeholder": "", + "show": true, + "multiline": false, + "password": false, + "name": "json_agent", + "type": "AgentExecutor", + "list": false + }, + "requests_wrapper": { + "required": true, + "placeholder": "", + "show": true, + "multiline": false, + "password": false, + "name": "requests_wrapper", + "type": "RequestsWrapper", + "list": false + }, + "_type": "OpenAPIToolkit" + }, + "description": "Toolkit for interacting with a OpenAPI api.", + "base_classes": [ + "BaseToolkit" + ] + }, + "id": "dndnode_32", + "value": null + }, + "selected": false, + "positionAbsolute": { + "x": 745.308873444751, + "y": -37.007911201107675 + }, + "dragging": false + }, + { + "width": 384, + "height": 351, + "id": "dndnode_33", + "type": "genericNode", + "position": { + "x": 281.30887344475104, + "y": 2.9920887988923255 + }, + "data": { + "type": "JsonAgent", + "node": { + "template": { + "toolkit": { + "required": true, + "placeholder": "", + "show": true, + "multiline": false, + "password": false, + "name": "toolkit", + "type": "BaseToolkit", + "list": false + }, + "llm": { + "required": true, + "placeholder": "", + "show": true, + "multiline": false, + "password": false, + "name": "llm", + "type": "BaseLanguageModel", + "list": false + }, + "_type": "JsonAgent" + }, + "description": "Construct a json agent from an LLM and tools.", + "base_classes": [ + "AgentExecutor" + ] + }, + "id": "dndnode_33", + "value": null + }, + "selected": false, + "dragging": false, + "positionAbsolute": { + "x": 281.30887344475104, + "y": 2.9920887988923255 + } + }, + { + "width": 384, + "height": 349, + "id": "dndnode_34", + "type": "genericNode", + "position": { + "x": 301.30887344475104, + "y": 532.9920887988924 + }, + "data": { + "type": "RequestsWrapper", + "node": { + "template": { + "headers": { + "required": false, + "placeholder": "", + "show": true, + "multiline": true, + "value": "{'Authorization':\n 'Bearer '}", + "password": false, + "name": "headers", + "type": "code", + "list": false + }, + "aiosession": { + "required": false, + "placeholder": "", + "show": false, + "multiline": false, + "password": false, + "name": "aiosession", + "type": "ClientSession", + "list": false + }, + "_type": "RequestsWrapper" + }, + "description": "Lightweight wrapper around requests library.", + "base_classes": [ + "RequestsWrapper" + ] + }, + "id": "dndnode_34", + "value": null + }, + "positionAbsolute": { + "x": 301.30887344475104, + "y": 532.9920887988924 + } + }, + { + "width": 384, + "height": 407, + "id": "dndnode_35", + "type": "genericNode", + "position": { + "x": -754.691126555249, + "y": -37.00791120110762 + }, + "data": { + "type": "JsonSpec", + "node": { + "template": { + "dict_": { + "required": true, + "placeholder": "", + "show": true, + "multiline": false, + "value": "api-with-examples.yaml", + "suffixes": [ + ".json", + ".yaml", + ".yml" + ], + "password": false, + "name": "dict_", + "type": "file", + "list": false, + "fileTypes": [ + "json", + "yaml", + "yml" + ], + "content": "data:application/x-yaml;base64,b3BlbmFwaTogIjMuMC4wIgppbmZvOgogIHRpdGxlOiBTaW1wbGUgQVBJIG92ZXJ2aWV3CiAgdmVyc2lvbjogMi4wLjAKcGF0aHM6CiAgLzoKICAgIGdldDoKICAgICAgb3BlcmF0aW9uSWQ6IGxpc3RWZXJzaW9uc3YyCiAgICAgIHN1bW1hcnk6IExpc3QgQVBJIHZlcnNpb25zCiAgICAgIHJlc3BvbnNlczoKICAgICAgICAnMjAwJzoKICAgICAgICAgIGRlc2NyaXB0aW9uOiB8LQogICAgICAgICAgICAyMDAgcmVzcG9uc2UKICAgICAgICAgIGNvbnRlbnQ6CiAgICAgICAgICAgIGFwcGxpY2F0aW9uL2pzb246CiAgICAgICAgICAgICAgZXhhbXBsZXM6IAogICAgICAgICAgICAgICAgZm9vOgogICAgICAgICAgICAgICAgICB2YWx1ZToKICAgICAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICAgICAidmVyc2lvbnMiOiBbCiAgICAgICAgICAgICAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzdGF0dXMiOiAiQ1VSUkVOVCIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAidXBkYXRlZCI6ICIyMDExLTAxLTIxVDExOjMzOjIxWiIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAiaWQiOiAidjIuMCIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAibGlua3MiOiBbCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiaHJlZiI6ICJodHRwOi8vMTI3LjAuMC4xOjg3NzQvdjIvIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInJlbCI6ICJzZWxmIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIF0KICAgICAgICAgICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgInN0YXR1cyI6ICJFWFBFUklNRU5UQUwiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgInVwZGF0ZWQiOiAiMjAxMy0wNy0yM1QxMTozMzoyMVoiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgImlkIjogInYzLjAiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgImxpbmtzIjogWwogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImhyZWYiOiAiaHR0cDovLzEyNy4wLjAuMTo4Nzc0L3YzLyIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJyZWwiOiAic2VsZiIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICBdCiAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgIF0KICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgJzMwMCc6CiAgICAgICAgICBkZXNjcmlwdGlvbjogfC0KICAgICAgICAgICAgMzAwIHJlc3BvbnNlCiAgICAgICAgICBjb250ZW50OgogICAgICAgICAgICBhcHBsaWNhdGlvbi9qc29uOiAKICAgICAgICAgICAgICBleGFtcGxlczogCiAgICAgICAgICAgICAgICBmb286CiAgICAgICAgICAgICAgICAgIHZhbHVlOiB8CiAgICAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICAgInZlcnNpb25zIjogWwogICAgICAgICAgICAgICAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzdGF0dXMiOiAiQ1VSUkVOVCIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAidXBkYXRlZCI6ICIyMDExLTAxLTIxVDExOjMzOjIxWiIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAiaWQiOiAidjIuMCIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAibGlua3MiOiBbCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiaHJlZiI6ICJodHRwOi8vMTI3LjAuMC4xOjg3NzQvdjIvIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInJlbCI6ICJzZWxmIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIF0KICAgICAgICAgICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgInN0YXR1cyI6ICJFWFBFUklNRU5UQUwiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgInVwZGF0ZWQiOiAiMjAxMy0wNy0yM1QxMTozMzoyMVoiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgImlkIjogInYzLjAiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgImxpbmtzIjogWwogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImhyZWYiOiAiaHR0cDovLzEyNy4wLjAuMTo4Nzc0L3YzLyIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJyZWwiOiAic2VsZiIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICBdCiAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICBdCiAgICAgICAgICAgICAgICAgICB9CiAgL3YyOgogICAgZ2V0OgogICAgICBvcGVyYXRpb25JZDogZ2V0VmVyc2lvbkRldGFpbHN2MgogICAgICBzdW1tYXJ5OiBTaG93IEFQSSB2ZXJzaW9uIGRldGFpbHMKICAgICAgcmVzcG9uc2VzOgogICAgICAgICcyMDAnOgogICAgICAgICAgZGVzY3JpcHRpb246IHwtCiAgICAgICAgICAgIDIwMCByZXNwb25zZQogICAgICAgICAgY29udGVudDoKICAgICAgICAgICAgYXBwbGljYXRpb24vanNvbjogCiAgICAgICAgICAgICAgZXhhbXBsZXM6CiAgICAgICAgICAgICAgICBmb286CiAgICAgICAgICAgICAgICAgIHZhbHVlOgogICAgICAgICAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICAgICAgICJ2ZXJzaW9uIjogewogICAgICAgICAgICAgICAgICAgICAgICAic3RhdHVzIjogIkNVUlJFTlQiLAogICAgICAgICAgICAgICAgICAgICAgICAidXBkYXRlZCI6ICIyMDExLTAxLTIxVDExOjMzOjIxWiIsCiAgICAgICAgICAgICAgICAgICAgICAgICJtZWRpYS10eXBlcyI6IFsKICAgICAgICAgICAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJiYXNlIjogImFwcGxpY2F0aW9uL3htbCIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vcGVuc3RhY2suY29tcHV0ZSt4bWw7dmVyc2lvbj0yIgogICAgICAgICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiYmFzZSI6ICJhcHBsaWNhdGlvbi9qc29uIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9wZW5zdGFjay5jb21wdXRlK2pzb247dmVyc2lvbj0yIgogICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgXSwKICAgICAgICAgICAgICAgICAgICAgICAgImlkIjogInYyLjAiLAogICAgICAgICAgICAgICAgICAgICAgICAibGlua3MiOiBbCiAgICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiaHJlZiI6ICJodHRwOi8vMTI3LjAuMC4xOjg3NzQvdjIvIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInJlbCI6ICJzZWxmIgogICAgICAgICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiaHJlZiI6ICJodHRwOi8vZG9jcy5vcGVuc3RhY2sub3JnL2FwaS9vcGVuc3RhY2stY29tcHV0ZS8yL29zLWNvbXB1dGUtZGV2Z3VpZGUtMi5wZGYiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJhcHBsaWNhdGlvbi9wZGYiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAicmVsIjogImRlc2NyaWJlZGJ5IgogICAgICAgICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiaHJlZiI6ICJodHRwOi8vZG9jcy5vcGVuc3RhY2sub3JnL2FwaS9vcGVuc3RhY2stY29tcHV0ZS8yL3dhZGwvb3MtY29tcHV0ZS0yLndhZGwiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJhcHBsaWNhdGlvbi92bmQuc3VuLndhZGwreG1sIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInJlbCI6ICJkZXNjcmliZWRieSIKICAgICAgICAgICAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJocmVmIjogImh0dHA6Ly9kb2NzLm9wZW5zdGFjay5vcmcvYXBpL29wZW5zdGFjay1jb21wdXRlLzIvd2FkbC9vcy1jb21wdXRlLTIud2FkbCIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJhcHBsaWNhdGlvbi92bmQuc3VuLndhZGwreG1sIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJyZWwiOiAiZGVzY3JpYmVkYnkiCiAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICBdCiAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICcyMDMnOgogICAgICAgICAgZGVzY3JpcHRpb246IHwtCiAgICAgICAgICAgIDIwMyByZXNwb25zZQogICAgICAgICAgY29udGVudDoKICAgICAgICAgICAgYXBwbGljYXRpb24vanNvbjogCiAgICAgICAgICAgICAgZXhhbXBsZXM6CiAgICAgICAgICAgICAgICBmb286CiAgICAgICAgICAgICAgICAgIHZhbHVlOgogICAgICAgICAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICAgICAgICJ2ZXJzaW9uIjogewogICAgICAgICAgICAgICAgICAgICAgICAic3RhdHVzIjogIkNVUlJFTlQiLAogICAgICAgICAgICAgICAgICAgICAgICAidXBkYXRlZCI6ICIyMDExLTAxLTIxVDExOjMzOjIxWiIsCiAgICAgICAgICAgICAgICAgICAgICAgICJtZWRpYS10eXBlcyI6IFsKICAgICAgICAgICAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJiYXNlIjogImFwcGxpY2F0aW9uL3htbCIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vcGVuc3RhY2suY29tcHV0ZSt4bWw7dmVyc2lvbj0yIgogICAgICAgICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiYmFzZSI6ICJhcHBsaWNhdGlvbi9qc29uIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9wZW5zdGFjay5jb21wdXRlK2pzb247dmVyc2lvbj0yIgogICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgXSwKICAgICAgICAgICAgICAgICAgICAgICAgImlkIjogInYyLjAiLAogICAgICAgICAgICAgICAgICAgICAgICAibGlua3MiOiBbCiAgICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiaHJlZiI6ICJodHRwOi8vMjMuMjUzLjIyOC4yMTE6ODc3NC92Mi8iLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAicmVsIjogInNlbGYiCiAgICAgICAgICAgICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJocmVmIjogImh0dHA6Ly9kb2NzLm9wZW5zdGFjay5vcmcvYXBpL29wZW5zdGFjay1jb21wdXRlLzIvb3MtY29tcHV0ZS1kZXZndWlkZS0yLnBkZiIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlIjogImFwcGxpY2F0aW9uL3BkZiIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJyZWwiOiAiZGVzY3JpYmVkYnkiCiAgICAgICAgICAgICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJocmVmIjogImh0dHA6Ly9kb2NzLm9wZW5zdGFjay5vcmcvYXBpL29wZW5zdGFjay1jb21wdXRlLzIvd2FkbC9vcy1jb21wdXRlLTIud2FkbCIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlIjogImFwcGxpY2F0aW9uL3ZuZC5zdW4ud2FkbCt4bWwiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAicmVsIjogImRlc2NyaWJlZGJ5IgogICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgXQogICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgIH0K" + }, + "max_value_length": { + "required": false, + "placeholder": "", + "show": true, + "multiline": false, + "value": "4000", + "password": false, + "name": "max_value_length", + "type": "int", + "list": false + }, + "_type": "JsonSpec" + }, + "description": "", + "base_classes": [ + "Tool", + "JsonSpec" + ] + }, + "id": "dndnode_35", + "value": null + }, + "selected": false, + "dragging": false, + "positionAbsolute": { + "x": -754.691126555249, + "y": -37.00791120110762 + } + }, + { + "width": 384, + "height": 563, + "id": "dndnode_36", + "type": "genericNode", + "position": { + "x": -310.69112655524896, + "y": 514.9920887988924 + }, + "data": { + "type": "OpenAIChat", + "node": { + "template": { + "cache": { + "required": false, + "placeholder": "", + "show": false, + "multiline": false, + "password": false, + "name": "cache", + "type": "bool", + "list": false + }, + "verbose": { + "required": false, + "placeholder": "", + "show": false, + "multiline": false, + "value": false, + "password": false, + "name": "verbose", + "type": "bool", + "list": false + }, + "client": { + "required": false, + "placeholder": "", + "show": false, + "multiline": false, + "password": false, + "name": "client", + "type": "Any", + "list": false + }, + "model_name": { + "required": false, + "placeholder": "", + "show": true, + "multiline": false, + "value": "gpt-3.5-turbo", + "password": false, + "name": "model_name", + "type": "str", + "list": false + }, + "model_kwargs": { + "required": false, + "placeholder": "", + "show": false, + "multiline": false, + "password": false, + "name": "model_kwargs", + "type": "code", + "list": false + }, + "openai_api_key": { + "required": false, + "placeholder": "", + "show": true, + "multiline": false, + "password": false, + "name": "openai_api_key", + "type": "str", + "list": false, + "value": "sk-" + }, + "max_retries": { + "required": false, + "placeholder": "", + "show": false, + "multiline": false, + "value": 6, + "password": false, + "name": "max_retries", + "type": "int", + "list": false + }, + "prefix_messages": { + "required": false, + "placeholder": "", + "show": false, + "multiline": false, + "password": false, + "name": "prefix_messages", + "type": "Any", + "list": true + }, + "streaming": { + "required": false, + "placeholder": "", + "show": false, + "multiline": false, + "value": false, + "password": false, + "name": "streaming", + "type": "bool", + "list": false + }, + "_type": "OpenAIChat" + }, + "description": "Wrapper around OpenAI Chat large language models.To use, you should have the ``openai`` python package installed, and theenvironment variable ``OPENAI_API_KEY`` set with your API key.Any parameters that are valid to be passed to the openai.create call can be passedin, even if not explicitly saved on this class.", + "base_classes": [ + "BaseLanguageModel", + "BaseLLM" + ] + }, + "id": "dndnode_36", + "value": null + }, + "selected": false, + "dragging": false, + "positionAbsolute": { + "x": -310.69112655524896, + "y": 514.9920887988924 + } + } + ], + "edges": [ + { + "source": "dndnode_19", + "sourceHandle": "JsonToolkit|dndnode_19|BaseToolkit", + "target": "dndnode_33", + "targetHandle": "BaseToolkit|toolkit|dndnode_33", + "className": "animate-pulse", + "id": "reactflow__edge-dndnode_19JsonToolkit|dndnode_19|BaseToolkit-dndnode_33BaseToolkit|toolkit|dndnode_33", + "selected": false + }, + { + "source": "dndnode_33", + "sourceHandle": "JsonAgent|dndnode_33|AgentExecutor", + "target": "dndnode_32", + "targetHandle": "AgentExecutor|json_agent|dndnode_32", + "className": "animate-pulse", + "id": "reactflow__edge-dndnode_33JsonAgent|dndnode_33|AgentExecutor-dndnode_32AgentExecutor|json_agent|dndnode_32", + "selected": false + }, + { + "source": "dndnode_34", + "sourceHandle": "RequestsWrapper|dndnode_34|RequestsWrapper", + "target": "dndnode_32", + "targetHandle": "RequestsWrapper|requests_wrapper|dndnode_32", + "className": "animate-pulse", + "id": "reactflow__edge-dndnode_34RequestsWrapper|dndnode_34|RequestsWrapper-dndnode_32RequestsWrapper|requests_wrapper|dndnode_32", + "selected": false + }, + { + "source": "dndnode_35", + "sourceHandle": "JsonSpec|dndnode_35|Tool|JsonSpec", + "target": "dndnode_19", + "targetHandle": "JsonSpec|spec|dndnode_19", + "className": "animate-pulse", + "id": "reactflow__edge-dndnode_35JsonSpec|dndnode_35|Tool|JsonSpec-dndnode_19JsonSpec|spec|dndnode_19", + "selected": false + }, + { + "source": "dndnode_36", + "sourceHandle": "OpenAIChat|dndnode_36|BaseLanguageModel|BaseLLM", + "target": "dndnode_33", + "targetHandle": "BaseLanguageModel|llm|dndnode_33", + "className": "animate-pulse", + "id": "reactflow__edge-dndnode_36OpenAIChat|dndnode_36|BaseLanguageModel|BaseLLM-dndnode_33BaseLanguageModel|llm|dndnode_33" + } + ], + "viewport": { + "x": 0, + "y": 0, + "zoom": 1 + } + }, + "chat": [ + { + "message": "test", + "isSend": true + } + ] +} \ No newline at end of file diff --git a/tests/test_graph.py b/tests/test_graph.py index 6dbe8a7e3..9bc7b4220 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -1,16 +1,36 @@ import json +from langflow.graph.nodes import ( + WrapperNode, + AgentNode, + ToolNode, + ChainNode, + PromptNode, + LLMNode, + ToolkitNode, + FileToolNode, +) import pytest from langchain.agents import AgentExecutor from langflow.graph import Edge, Graph, Node from langflow.utils.payload import build_json, get_root_node + # Test cases for the graph module +# now we have three types of graph: +# BASIC_EXAMPLE_PATH, COMPLEX_EXAMPLE_PATH, OPENAPI_EXAMPLE_PATH -def get_graph(basic=True): + +def get_graph(_type="basic"): """Get a graph from a json file""" - path = pytest.BASIC_EXAMPLE_PATH if basic else pytest.COMPLEX_EXAMPLE_PATH + if _type == "basic": + path = pytest.BASIC_EXAMPLE_PATH + elif _type == "complex": + path = pytest.COMPLEX_EXAMPLE_PATH + elif _type == "openapi": + path = pytest.OPENAPI_EXAMPLE_PATH + with open(path, "r") as f: flow_graph = json.load(f) data_graph = flow_graph["data"] @@ -19,26 +39,94 @@ def get_graph(basic=True): return Graph(nodes, edges) -def test_get_nodes_with_target(): +@pytest.fixture +def basic_graph(): + return get_graph() + + +@pytest.fixture +def complex_graph(): + return get_graph("complex") + + +@pytest.fixture +def openapi_graph(): + return get_graph("openapi") + + +def get_node_by_type(graph, node_type): + """Get a node by type""" + return next((node for node in graph.nodes if isinstance(node, node_type)), None) + + +def test_graph_structure(basic_graph): + assert isinstance(basic_graph, Graph) + assert len(basic_graph.nodes) > 0 + assert len(basic_graph.edges) > 0 + for node in basic_graph.nodes: + assert isinstance(node, Node) + for edge in basic_graph.edges: + assert isinstance(edge, Edge) + assert edge.source in basic_graph.nodes + assert edge.target in basic_graph.nodes + + +def test_circular_dependencies(basic_graph): + assert isinstance(basic_graph, Graph) + + def check_circular(node, visited): + visited.add(node) + neighbors = basic_graph.get_nodes_with_target(node) + for neighbor in neighbors: + if neighbor in visited: + return True + if check_circular(neighbor, visited.copy()): + return True + return False + + for node in basic_graph.nodes: + assert not check_circular(node, set()) + + +def test_invalid_node_types(): + graph_data = { + "nodes": [ + { + "id": "1", + "data": { + "node": { + "base_classes": ["BaseClass"], + "template": { + "_type": "InvalidNodeType", + }, + }, + }, + }, + ], + "edges": [], + } + with pytest.raises(Exception): + Graph(graph_data["nodes"], graph_data["edges"]) + + +def test_get_nodes_with_target(basic_graph): """Test getting connected nodes""" - graph = get_graph() - assert isinstance(graph, Graph) + assert isinstance(basic_graph, Graph) # Get root node - root = get_root_node(graph) + root = get_root_node(basic_graph) assert root is not None - connected_nodes = graph.get_nodes_with_target(root) + connected_nodes = basic_graph.get_nodes_with_target(root) assert connected_nodes is not None -def test_get_node_neighbors_basic(): +def test_get_node_neighbors_basic(basic_graph): """Test getting node neighbors""" - graph = get_graph(basic=True) - assert isinstance(graph, Graph) + assert isinstance(basic_graph, Graph) # Get root node - root = get_root_node(graph) + root = get_root_node(basic_graph) assert root is not None - neighbors = graph.get_node_neighbors(root) + neighbors = basic_graph.get_node_neighbors(root) assert neighbors is not None assert isinstance(neighbors, dict) # Root Node is an Agent, it requires an LLMChain and tools @@ -57,7 +145,7 @@ def test_get_node_neighbors_basic(): for neighbor, val in neighbors.items() if "Chain" in neighbor.data["type"] and val ) - chain_neighbors = graph.get_node_neighbors(chain) + chain_neighbors = basic_graph.get_node_neighbors(chain) assert chain_neighbors is not None assert isinstance(chain_neighbors, dict) # Check if there is a LLM in the chain's neighbors @@ -74,15 +162,13 @@ def test_get_node_neighbors_basic(): ) -def test_get_node_neighbors_complex(): +def test_get_node_neighbors_complex(complex_graph): """Test getting node neighbors""" - - graph = get_graph(basic=False) - assert isinstance(graph, Graph) + assert isinstance(complex_graph, Graph) # Get root node - root = get_root_node(graph) + root = get_root_node(complex_graph) assert root is not None - neighbors = graph.get_nodes_with_target(root) + neighbors = complex_graph.get_nodes_with_target(root) assert neighbors is not None # Neighbors should be a list of nodes assert isinstance(neighbors, list) @@ -93,7 +179,7 @@ def test_get_node_neighbors_complex(): assert any("Tool" in neighbor.data["type"] for neighbor in neighbors) # Now on to the Chain's neighbors chain = next(neighbor for neighbor in neighbors if "Chain" in neighbor.data["type"]) - chain_neighbors = graph.get_nodes_with_target(chain) + chain_neighbors = complex_graph.get_nodes_with_target(chain) assert chain_neighbors is not None # Check if there is a LLM in the chain's neighbors assert any("OpenAI" in neighbor.data["type"] for neighbor in chain_neighbors) @@ -101,7 +187,7 @@ def test_get_node_neighbors_complex(): assert any("Prompt" in neighbor.data["type"] for neighbor in chain_neighbors) # Now on to the Tool's neighbors tool = next(neighbor for neighbor in neighbors if "Tool" in neighbor.data["type"]) - tool_neighbors = graph.get_nodes_with_target(tool) + tool_neighbors = complex_graph.get_nodes_with_target(tool) assert tool_neighbors is not None # Check if there is an Agent in the tool's neighbors assert any("Agent" in neighbor.data["type"] for neighbor in tool_neighbors) @@ -109,7 +195,7 @@ def test_get_node_neighbors_complex(): agent = next( neighbor for neighbor in tool_neighbors if "Agent" in neighbor.data["type"] ) - agent_neighbors = graph.get_nodes_with_target(agent) + agent_neighbors = complex_graph.get_nodes_with_target(agent) assert agent_neighbors is not None # Check if there is a Tool in the agent's neighbors assert any("Tool" in neighbor.data["type"] for neighbor in agent_neighbors) @@ -117,62 +203,57 @@ def test_get_node_neighbors_complex(): tool = next( neighbor for neighbor in agent_neighbors if "Tool" in neighbor.data["type"] ) - tool_neighbors = graph.get_nodes_with_target(tool) + 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) -def test_get_node(): +def test_get_node(basic_graph): """Test getting a single node""" - graph = get_graph() - node_id = graph.nodes[0].id - node = graph.get_node(node_id) + node_id = basic_graph.nodes[0].id + node = basic_graph.get_node(node_id) assert isinstance(node, Node) assert node.id == node_id -def test_build_nodes(): +def test_build_nodes(basic_graph): """Test building nodes""" - graph = get_graph() - assert len(graph.nodes) == len(graph._nodes) - for node in graph.nodes: + + assert len(basic_graph.nodes) == len(basic_graph._nodes) + for node in basic_graph.nodes: assert isinstance(node, Node) -def test_build_edges(): +def test_build_edges(basic_graph): """Test building edges""" - graph = get_graph() - assert len(graph.edges) == len(graph._edges) - for edge in graph.edges: + assert len(basic_graph.edges) == len(basic_graph._edges) + for edge in basic_graph.edges: assert isinstance(edge, Edge) assert isinstance(edge.source, Node) assert isinstance(edge.target, Node) -def test_get_root_node(): +def test_get_root_node(basic_graph, complex_graph): """Test getting root node""" - graph = get_graph(basic=True) - assert isinstance(graph, Graph) - root = get_root_node(graph) + assert isinstance(basic_graph, Graph) + root = get_root_node(basic_graph) assert root is not None assert isinstance(root, Node) assert root.data["type"] == "ZeroShotAgent" # For complex example, the root node is a ZeroShotAgent too - graph = get_graph(basic=False) - assert isinstance(graph, Graph) - root = get_root_node(graph) + assert isinstance(complex_graph, Graph) + root = get_root_node(complex_graph) assert root is not None assert isinstance(root, Node) assert root.data["type"] == "ZeroShotAgent" -def test_build_json(): +def test_build_json(basic_graph): """Test building JSON from graph""" - graph = get_graph() - assert isinstance(graph, Graph) - root = get_root_node(graph) - json_data = build_json(root, graph) + assert isinstance(basic_graph, Graph) + root = get_root_node(basic_graph) + json_data = build_json(root, basic_graph) assert isinstance(json_data, dict) assert json_data["_type"] == "zero-shot-react-description" assert isinstance(json_data["llm_chain"], dict) @@ -188,38 +269,37 @@ def test_build_json(): assert all(isinstance(val, str) for val in json_data["return_values"]) -def test_validate_edges(): +def test_validate_edges(basic_graph): """Test validating edges""" - graph = get_graph() - assert isinstance(graph, Graph) + + assert isinstance(basic_graph, Graph) # all edges should be valid - assert all(edge.valid for edge in graph.edges) + assert all(edge.valid for edge in basic_graph.edges) -def test_matched_type(): +def test_matched_type(basic_graph): """Test matched type attribute in Edge""" - graph = get_graph() - assert isinstance(graph, Graph) + assert isinstance(basic_graph, Graph) # all edges should be valid - assert all(edge.valid for edge in graph.edges) + assert all(edge.valid for edge in basic_graph.edges) # all edges should have a matched_type attribute - assert all(hasattr(edge, "matched_type") for edge in graph.edges) + assert all(hasattr(edge, "matched_type") for edge in basic_graph.edges) # The matched_type attribute should be in the source_types attr - assert all(edge.matched_type in edge.source_types for edge in graph.edges) + assert all(edge.matched_type in edge.source_types for edge in basic_graph.edges) -def test_build_params(): +def test_build_params(basic_graph): """Test building params""" - graph = get_graph() - assert isinstance(graph, Graph) + + assert isinstance(basic_graph, Graph) # all edges should be valid - assert all(edge.valid for edge in graph.edges) + assert all(edge.valid for edge in basic_graph.edges) # all edges should have a matched_type attribute - assert all(hasattr(edge, "matched_type") for edge in graph.edges) + assert all(hasattr(edge, "matched_type") for edge in basic_graph.edges) # The matched_type attribute should be in the source_types attr - assert all(edge.matched_type in edge.source_types for edge in graph.edges) + assert all(edge.matched_type in edge.source_types for edge in basic_graph.edges) # Get the root node - root = get_root_node(graph) + root = get_root_node(basic_graph) # Root node is a ZeroShotAgent # which requires an llm_chain, allowed_tools and return_values assert isinstance(root.params, dict) @@ -261,7 +341,7 @@ def test_build_params(): assert isinstance(llm_node.params["model_name"], str) -def test_build(): +def test_build(basic_graph, complex_graph): """Test Node's build method""" # def build(self): # # The params dict is used to build the module @@ -284,18 +364,81 @@ def test_build(): # # and instantiate it with the params # # and return the instance # return LANGCHAIN_TYPES_DICT[self.node_type](**self.params) - graph = get_graph() - assert isinstance(graph, Graph) + + assert isinstance(basic_graph, Graph) # Now we test the build method # Build the Agent - agent = graph.build() + agent = basic_graph.build() # The agent should be a AgentExecutor assert isinstance(agent, AgentExecutor) # Now we test the complex example - graph = get_graph(basic=False) - assert isinstance(graph, Graph) + assert isinstance(complex_graph, Graph) # Now we test the build method - agent = graph.build() + agent = complex_graph.build() # The agent should be a AgentExecutor assert isinstance(agent, AgentExecutor) + + +def test_agent_node_build(basic_graph): + agent_node = get_node_by_type(basic_graph, AgentNode) + assert agent_node is not None + built_object = agent_node.build() + assert built_object is not None + # Add any further assertions specific to the AgentNode's build() method + + +def test_tool_node_build(basic_graph): + tool_node = get_node_by_type(basic_graph, ToolNode) + assert tool_node is not None + built_object = tool_node.build() + assert built_object is not None + # Add any further assertions specific to the ToolNode's build() method + + +def test_chain_node_build(complex_graph): + chain_node = get_node_by_type(complex_graph, ChainNode) + assert chain_node is not None + built_object = chain_node.build() + assert built_object is not None + # Add any further assertions specific to the ChainNode's build() method + + +def test_prompt_node_build(complex_graph): + prompt_node = get_node_by_type(complex_graph, PromptNode) + assert prompt_node is not None + built_object = prompt_node.build() + assert built_object is not None + # Add any further assertions specific to the PromptNode's build() method + + +def test_llm_node_build(basic_graph): + llm_node = get_node_by_type(basic_graph, LLMNode) + assert llm_node is not None + built_object = llm_node.build() + assert built_object is not None + # Add any further assertions specific to the LLMNode's build() method + + +def test_toolkit_node_build(openapi_graph): + toolkit_node = get_node_by_type(openapi_graph, ToolkitNode) + assert toolkit_node is not None + built_object = toolkit_node.build() + assert built_object is not None + # Add any further assertions specific to the ToolkitNode's build() method + + +def test_file_tool_node_build(openapi_graph): + file_tool_node = get_node_by_type(openapi_graph, FileToolNode) + assert file_tool_node is not None + built_object = file_tool_node.build() + assert built_object is not None + # Add any further assertions specific to the FileToolNode's build() method + + +def test_wrapper_node_build(openapi_graph): + wrapper_node = get_node_by_type(openapi_graph, WrapperNode) + assert wrapper_node is not None + built_object = wrapper_node.build() + assert built_object is not None + # Add any further assertions specific to the WrapperNode's build() method From f30d2d333061601c59caa7a9548510f8778186b4 Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Sat, 1 Apr 2023 00:11:36 -0300 Subject: [PATCH 52/59] feat: a few more tests --- tests/test_template.py | 51 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/tests/test_template.py b/tests/test_template.py index 102df3b56..ed22bae3d 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -3,9 +3,10 @@ from pydantic import BaseModel import pytest import re import importlib -from typing import Dict +from typing import Dict, List, Optional from langflow.utils.util import ( build_template_from_class, + build_template_from_function, format_dict, get_base_classes, get_default_factory, @@ -26,6 +27,54 @@ class Child(Parent): child_field: int +class ExampleClass1(BaseModel): + """Example class 1.""" + + def __init__(self, data: Optional[List[int]] = None): + self.data = data or [1, 2, 3] + + +class ExampleClass2(BaseModel): + """Example class 2.""" + + def __init__(self, data: Optional[Dict[str, int]] = None): + self.data = data or {"a": 1, "b": 2, "c": 3} + + +def example_loader_1() -> ExampleClass1: + """Example loader function 1.""" + return ExampleClass1() + + +def example_loader_2() -> ExampleClass2: + """Example loader function 2.""" + return ExampleClass2() + + +def test_build_template_from_function(): + type_to_loader_dict = { + "example1": example_loader_1, + "example2": example_loader_2, + } + + # Test with valid name + result = build_template_from_function("ExampleClass1", type_to_loader_dict) + + assert "template" in result + assert "description" in result + assert "base_classes" in result + + # Test with add_function=True + result_with_function = build_template_from_function( + "ExampleClass1", type_to_loader_dict, add_function=True + ) + assert "function" in result_with_function["base_classes"] + + # Test with invalid name + with pytest.raises(ValueError, match=r".* not found"): + build_template_from_function("NonExistent", type_to_loader_dict) + + # Test build_template_from_class def test_build_template_from_class(): type_to_cls_dict: Dict[str, type] = {"parent": Parent, "child": Child} From 5125fde38af62a66373971fe9e2f134e0435392c Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Sat, 1 Apr 2023 00:11:55 -0300 Subject: [PATCH 53/59] fix: base_classes were being passed incorrectly --- src/backend/langflow/utils/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/langflow/utils/util.py b/src/backend/langflow/utils/util.py index 2fee19475..2f6b66a43 100644 --- a/src/backend/langflow/utils/util.py +++ b/src/backend/langflow/utils/util.py @@ -98,7 +98,7 @@ def build_template_from_function( return { "template": format_dict(variables, name), "description": docs["Description"], - "base_classes": get_base_classes(_class), + "base_classes": base_classes, } From 2235767d22913b52eafd12a55de18141697fa120 Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Sat, 1 Apr 2023 10:58:46 -0300 Subject: [PATCH 54/59] feat: implemented caching of agent --- src/backend/langflow/cache/__init__.py | 0 src/backend/langflow/cache/utils.py | 49 ++++++++++++++++++++ src/backend/langflow/interface/run.py | 38 +++++++++++---- tests/test_cache.py | 64 ++++++++++++++++++++++++++ 4 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 src/backend/langflow/cache/__init__.py create mode 100644 src/backend/langflow/cache/utils.py create mode 100644 tests/test_cache.py diff --git a/src/backend/langflow/cache/__init__.py b/src/backend/langflow/cache/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/langflow/cache/utils.py b/src/backend/langflow/cache/utils.py new file mode 100644 index 000000000..2c4acd074 --- /dev/null +++ b/src/backend/langflow/cache/utils.py @@ -0,0 +1,49 @@ +import contextlib +import hashlib +import json +import os +from pathlib import Path +import tempfile +import dill + +PREFIX = "langflow_cache" + + +def clear_old_cache_files(max_cache_size: int = 10): + cache_dir = Path(tempfile.gettempdir()) + cache_files = list(cache_dir.glob(f"{PREFIX}_*.dill")) + + if len(cache_files) > max_cache_size: + cache_files_sorted_by_mtime = sorted( + cache_files, key=lambda x: x.stat().st_mtime, reverse=True + ) + + for cache_file in cache_files_sorted_by_mtime[max_cache_size:]: + with contextlib.suppress(OSError): + os.remove(cache_file) + + +def remove_position_info(node): + node.pop("position", None) + + +def compute_hash(graph_data): + for node in graph_data["nodes"]: + remove_position_info(node) + + cleaned_graph_json = json.dumps(graph_data, sort_keys=True) + return hashlib.sha256(cleaned_graph_json.encode("utf-8")).hexdigest() + + +def save_cache(hash_val, chat_data): + cache_path = Path(tempfile.gettempdir()) / f"{PREFIX}_{hash_val}.dill" + with cache_path.open("wb") as cache_file: + dill.dump(chat_data, cache_file) + + +def load_cache(hash_val): + cache_path = Path(tempfile.gettempdir()) / f"{PREFIX}_{hash_val}.dill" + if cache_path.exists(): + with cache_path.open("rb") as cache_file: + return dill.load(cache_file) + return None diff --git a/src/backend/langflow/interface/run.py b/src/backend/langflow/interface/run.py index 360bad364..998ee297d 100644 --- a/src/backend/langflow/interface/run.py +++ b/src/backend/langflow/interface/run.py @@ -2,30 +2,48 @@ import contextlib import io import re from typing import Any, Dict +from langflow.cache.utils import compute_hash, load_cache, save_cache from langflow.graph.graph import Graph from langflow.interface import loading from langflow.utils import payload +def load_langchain_object(data_graph): + computed_hash = compute_hash(data_graph) + + # Load langchain_object from cache if it exists + langchain_object = load_cache(computed_hash) + if langchain_object is None: + nodes = data_graph["nodes"] + # Add input variables + nodes = payload.extract_input_variables(nodes) + # Nodes, edges and root node + edges = data_graph["edges"] + graph = Graph(nodes, edges) + + langchain_object = graph.build() + + return computed_hash, langchain_object + + def process_graph(data_graph: Dict[str, Any]): """ Process graph by extracting input variables and replacing ZeroShotPrompt with PromptTemplate,then run the graph and return the result and thought. """ - nodes = data_graph["nodes"] - # Add input variables - # ? Is this necessary? - nodes = payload.extract_input_variables(nodes) - # Nodes, edges and root node - edges = data_graph["edges"] - graph = Graph(nodes, edges) - - langchain_object = graph.build() + # Load langchain object + computed_hash, langchain_object = load_langchain_object(data_graph) message = data_graph["message"] - # Process json + + # Generate result and thought result, thought = get_result_and_thought_using_graph(langchain_object, message) + # Save langchain_object to cache + # We have to save it here because if the + # memory is updated we need to keep the new values + save_cache(computed_hash, langchain_object) + return { "result": result, "thought": re.sub( diff --git a/tests/test_cache.py b/tests/test_cache.py new file mode 100644 index 000000000..0d9102b49 --- /dev/null +++ b/tests/test_cache.py @@ -0,0 +1,64 @@ +import json +import hashlib +from pathlib import Path +import dill +import tempfile +from langflow.cache.utils import compute_hash, load_cache, save_cache, PREFIX +from langflow.interface.run import load_langchain_object, process_graph +import pytest + + +def get_graph(_type="basic"): + """Get a graph from a json file""" + if _type == "basic": + path = pytest.BASIC_EXAMPLE_PATH + elif _type == "complex": + path = pytest.COMPLEX_EXAMPLE_PATH + elif _type == "openapi": + path = pytest.OPENAPI_EXAMPLE_PATH + + with open(path, "r") as f: + flow_graph = json.load(f) + return flow_graph["data"] + + +@pytest.fixture +def basic_data_graph(): + return get_graph() + + +@pytest.fixture +def complex_data_graph(): + return get_graph("complex") + + +@pytest.fixture +def openapi_data_graph(): + return get_graph("openapi") + + +def langchain_objects_are_equal(obj1, obj2): + return str(obj1) == str(obj2) + + +def test_cache_creation(basic_data_graph): + # Compute hash for the input data_graph + computed_hash = compute_hash(basic_data_graph) + + # Call process_graph function to build and cache the langchain_object + _ = load_langchain_object(basic_data_graph) + + # Check if the cache file exists + cache_file = Path(tempfile.gettempdir()) / f"{PREFIX}_{computed_hash}.dill" + assert cache_file.exists() + + +def test_cache_reuse(basic_data_graph): + # Call process_graph function to build and cache the langchain_object + result1 = load_langchain_object(basic_data_graph) + + # Call process_graph function again to use the cached langchain_object + result2 = load_langchain_object(basic_data_graph) + + # Compare the results to ensure the same langchain_object was used + assert langchain_objects_are_equal(result1, result2) From 7052508fb02a985eb4b620ada119e42b6439b587 Mon Sep 17 00:00:00 2001 From: Ibis Prevedello Date: Sat, 1 Apr 2023 16:17:31 -0300 Subject: [PATCH 55/59] feat: add csv agent --- poetry.lock | 65 ++++++++++++++++++- pyproject.toml | 1 + src/backend/langflow/config.yaml | 1 + src/backend/langflow/custom/customs.py | 2 +- src/backend/langflow/graph/base.py | 4 +- src/backend/langflow/graph/utils.py | 10 ++- .../langflow/interface/agents/custom.py | 54 +++++++++++++++ src/backend/langflow/template/base.py | 1 + src/backend/langflow/template/nodes.py | 29 +++++++++ 9 files changed, 160 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9462c63cf..9043bccec 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1524,6 +1524,55 @@ files = [ {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, ] +[[package]] +name = "pandas" +version = "1.5.3" +description = "Powerful data structures for data analysis, time series, and statistics" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3749077d86e3a2f0ed51367f30bf5b82e131cc0f14260c4d3e499186fccc4406"}, + {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:972d8a45395f2a2d26733eb8d0f629b2f90bebe8e8eddbb8829b180c09639572"}, + {file = "pandas-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50869a35cbb0f2e0cd5ec04b191e7b12ed688874bd05dd777c19b28cbea90996"}, + {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3ac844a0fe00bfaeb2c9b51ab1424e5c8744f89860b138434a363b1f620f354"}, + {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0a56cef15fd1586726dace5616db75ebcfec9179a3a55e78f72c5639fa2a23"}, + {file = "pandas-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:478ff646ca42b20376e4ed3fa2e8d7341e8a63105586efe54fa2508ee087f328"}, + {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6973549c01ca91ec96199e940495219c887ea815b2083722821f1d7abfa2b4dc"}, + {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c39a8da13cede5adcd3be1182883aea1c925476f4e84b2807a46e2775306305d"}, + {file = "pandas-1.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f76d097d12c82a535fda9dfe5e8dd4127952b45fea9b0276cb30cca5ea313fbc"}, + {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e474390e60ed609cec869b0da796ad94f420bb057d86784191eefc62b65819ae"}, + {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f2b952406a1588ad4cad5b3f55f520e82e902388a6d5a4a91baa8d38d23c7f6"}, + {file = "pandas-1.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc4c368f42b551bf72fac35c5128963a171b40dce866fb066540eeaf46faa003"}, + {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:14e45300521902689a81f3f41386dc86f19b8ba8dd5ac5a3c7010ef8d2932813"}, + {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9842b6f4b8479e41968eced654487258ed81df7d1c9b7b870ceea24ed9459b31"}, + {file = "pandas-1.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:26d9c71772c7afb9d5046e6e9cf42d83dd147b5cf5bcb9d97252077118543792"}, + {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fbcb19d6fceb9e946b3e23258757c7b225ba450990d9ed63ccceeb8cae609f7"}, + {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:565fa34a5434d38e9d250af3c12ff931abaf88050551d9fbcdfafca50d62babf"}, + {file = "pandas-1.5.3-cp38-cp38-win32.whl", hash = "sha256:87bd9c03da1ac870a6d2c8902a0e1fd4267ca00f13bc494c9e5a9020920e1d51"}, + {file = "pandas-1.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:41179ce559943d83a9b4bbacb736b04c928b095b5f25dd2b7389eda08f46f373"}, + {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c74a62747864ed568f5a82a49a23a8d7fe171d0c69038b38cedf0976831296fa"}, + {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c4c00e0b0597c8e4f59e8d461f797e5d70b4d025880516a8261b2817c47759ee"}, + {file = "pandas-1.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a50d9a4336a9621cab7b8eb3fb11adb82de58f9b91d84c2cd526576b881a0c5a"}, + {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd05f7783b3274aa206a1af06f0ceed3f9b412cf665b7247eacd83be41cf7bf0"}, + {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f69c4029613de47816b1bb30ff5ac778686688751a5e9c99ad8c7031f6508e5"}, + {file = "pandas-1.5.3-cp39-cp39-win32.whl", hash = "sha256:7cec0bee9f294e5de5bbfc14d0573f65526071029d036b753ee6507d2a21480a"}, + {file = "pandas-1.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:dfd681c5dc216037e0b0a2c821f5ed99ba9f03ebcf119c7dac0e9a7b960b9ec9"}, + {file = "pandas-1.5.3.tar.gz", hash = "sha256:74a3fd7e5a7ec052f183273dc7b0acd3a863edf7520f5d3a1765c04ffdb3b0b1"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.20.3", markers = "python_version < \"3.10\""}, + {version = ">=1.21.0", markers = "python_version >= \"3.10\""}, + {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, +] +python-dateutil = ">=2.8.1" +pytz = ">=2020.1" + +[package.extras] +test = ["hypothesis (>=5.5.3)", "pytest (>=6.0)", "pytest-xdist (>=1.31)"] + [[package]] name = "parso" version = "0.8.3" @@ -1853,7 +1902,7 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2. name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" -category = "dev" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -1864,6 +1913,18 @@ files = [ [package.dependencies] six = ">=1.5" +[[package]] +name = "pytz" +version = "2023.3" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, + {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, +] + [[package]] name = "pywin32" version = "306" @@ -2636,4 +2697,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "afeeaa3c4d0aee2a52be1ccfeff2c47cca9f6af446b5cf4e422fcbb214eec762" +content-hash = "99d1b3923d427a2bdce635e88aca5f9dd2af850a31b94d290cbba52c8cf533f8" diff --git a/pyproject.toml b/pyproject.toml index 3f202b843..4e415e71f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ gunicorn = "^20.1.0" langchain = "^0.0.127" openai = "^0.27.2" types-pyyaml = "^6.0.12.8" +pandas = "^1.5.3" [tool.poetry.group.dev.dependencies] black = "^23.1.0" diff --git a/src/backend/langflow/config.yaml b/src/backend/langflow/config.yaml index b8c2be6da..dede4cee6 100644 --- a/src/backend/langflow/config.yaml +++ b/src/backend/langflow/config.yaml @@ -7,6 +7,7 @@ chains: agents: - ZeroShotAgent - JsonAgent + - CSVAgent prompts: - PromptTemplate diff --git a/src/backend/langflow/custom/customs.py b/src/backend/langflow/custom/customs.py index fa14fb2e5..6a70732a0 100644 --- a/src/backend/langflow/custom/customs.py +++ b/src/backend/langflow/custom/customs.py @@ -3,7 +3,7 @@ from langflow.template import nodes CUSTOM_NODES = { "prompts": {**nodes.ZeroShotPromptNode().to_dict()}, "tools": {**nodes.PythonFunctionNode().to_dict(), **nodes.ToolNode().to_dict()}, - "agents": {**nodes.JsonAgentNode().to_dict()}, + "agents": {**nodes.JsonAgentNode().to_dict(), **nodes.CSVAgentNode().to_dict()}, } diff --git a/src/backend/langflow/graph/base.py b/src/backend/langflow/graph/base.py index 0c4cf8705..f4b41bbfc 100644 --- a/src/backend/langflow/graph/base.py +++ b/src/backend/langflow/graph/base.py @@ -8,7 +8,7 @@ from copy import deepcopy from typing import Any, Dict, List from langflow.graph.constants import DIRECT_TYPES -from langflow.graph.utils import load_dict +from langflow.graph.utils import load_file from langflow.interface import loading from langflow.interface.listing import ALL_TYPES_DICT @@ -90,7 +90,7 @@ class Node: type_to_load = value.get("suffixes") file_name = value.get("value") content = value.get("content") - loaded_dict = load_dict(file_name, content, type_to_load) + loaded_dict = load_file(file_name, content, type_to_load) params[key] = loaded_dict # We should check if the type is in something not diff --git a/src/backend/langflow/graph/utils.py b/src/backend/langflow/graph/utils.py index 70f3a3145..ca728390d 100644 --- a/src/backend/langflow/graph/utils.py +++ b/src/backend/langflow/graph/utils.py @@ -1,11 +1,13 @@ import base64 import json -from typing import Dict +from typing import Any import re import yaml +import csv +import io -def load_dict(file_name, file_content, accepted_types) -> Dict: +def load_file(file_name, file_content, accepted_types) -> Any: """Load a file from a string.""" # Check if the file is accepted if not any(file_name.endswith(suffix) for suffix in accepted_types): @@ -24,6 +26,10 @@ def load_dict(file_name, file_content, accepted_types) -> Dict: elif suffix in ["yaml", "yml"]: # Return the yaml content return yaml.safe_load(decoded_string) + elif suffix == "csv": + # Load the csv content + csv_reader = csv.DictReader(io.StringIO(decoded_string)) + return list(csv_reader) else: raise ValueError(f"File {file_name} is not accepted") diff --git a/src/backend/langflow/interface/agents/custom.py b/src/backend/langflow/interface/agents/custom.py index cc998fc12..c056b7c72 100644 --- a/src/backend/langflow/interface/agents/custom.py +++ b/src/backend/langflow/interface/agents/custom.py @@ -7,6 +7,14 @@ from langchain.agents.agent_toolkits.json.toolkit import JsonToolkit from langchain.agents.mrkl.prompt import FORMAT_INSTRUCTIONS from langchain.schema import BaseLanguageModel from pydantic import BaseModel +from langchain.llms.base import BaseLLM +from typing import Any, Optional +from langchain.agents.agent_toolkits.pandas.base import create_pandas_dataframe_agent +from pathlib import Path + +from langchain.agents.agent_toolkits.pandas.prompt import PREFIX as PANDAS_PREFIX +from langchain.agents.agent_toolkits.pandas.prompt import SUFFIX as PANDAS_SUFFIX +from langchain.tools.python.tool import PythonAstREPLTool class JsonAgent(AgentExecutor): @@ -41,6 +49,52 @@ class JsonAgent(AgentExecutor): return super().run(*args, **kwargs) +class CSVAgent(AgentExecutor): + """CSV agent""" + + @classmethod + def initialize(cls, *args, **kwargs): + return cls.from_toolkit_and_llm(*args, **kwargs) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @classmethod + def from_toolkit_and_llm( + cls, + path: dict, + llm: BaseLanguageModel, + pandas_kwargs: Optional[dict] = None, + **kwargs: Any + ): + import pandas as pd + + _kwargs = pandas_kwargs or {} + df = pd.DataFrame.from_dict(path, **_kwargs) + + tools = [PythonAstREPLTool(locals={"df": df})] + prompt = ZeroShotAgent.create_prompt( + tools, + prefix=PANDAS_PREFIX, + suffix=PANDAS_SUFFIX, + input_variables=["df", "input", "agent_scratchpad"], + ) + partial_prompt = prompt.partial(df=str(df.head())) + llm_chain = LLMChain( + llm=llm, + prompt=partial_prompt, + callback_manager=None, + ) + tool_names = [tool.name for tool in tools] + agent = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names, **kwargs) + + return cls.from_agent_and_tools(agent=agent, tools=tools, verbose=True) + + def run(self, *args, **kwargs): + return super().run(*args, **kwargs) + + CUSTOM_AGENTS = { "JsonAgent": JsonAgent, + "CSVAgent": CSVAgent, } diff --git a/src/backend/langflow/template/base.py b/src/backend/langflow/template/base.py index bcd6ed162..7329df04d 100644 --- a/src/backend/langflow/template/base.py +++ b/src/backend/langflow/template/base.py @@ -13,6 +13,7 @@ class TemplateFieldCreator(BaseModel, ABC): multiline: bool = False value: Any = None suffixes: list[str] = [] + fileTypes: list[str] = [] file_types: list[str] = [] content: Union[str, None] = None password: bool = False diff --git a/src/backend/langflow/template/nodes.py b/src/backend/langflow/template/nodes.py index fae298f2d..96219e000 100644 --- a/src/backend/langflow/template/nodes.py +++ b/src/backend/langflow/template/nodes.py @@ -139,3 +139,32 @@ class JsonAgentNode(FrontendNode): def to_dict(self): return super().to_dict() + + +class CSVAgentNode(FrontendNode): + name: str = "CSVAgent" + template: Template = Template( + type_name="csv_agent", + fields=[ + TemplateField( + field_type="file", + required=True, + show=True, + name="path", + value="", + suffixes=[".csv"], + fileTypes=["csv"], + ), + TemplateField( + field_type="BaseLanguageModel", + required=True, + show=True, + name="llm", + ), + ], + ) + description: str = """Construct a json agent from a CSV and tools.""" + base_classes: list[str] = ["AgentExecutor"] + + def to_dict(self): + return super().to_dict() From 74656b63ac596685ef9b192acc88acf1aebcd001 Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Sat, 1 Apr 2023 17:07:18 -0300 Subject: [PATCH 56/59] fix: use the correct chat model --- src/backend/langflow/interface/custom_lists.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/langflow/interface/custom_lists.py b/src/backend/langflow/interface/custom_lists.py index c5794a26d..91a62d651 100644 --- a/src/backend/langflow/interface/custom_lists.py +++ b/src/backend/langflow/interface/custom_lists.py @@ -3,12 +3,12 @@ from typing import Any from langchain import llms, requests from langchain.agents import agent_toolkits -from langchain.llms.openai import OpenAIChat +from langchain.chat_models import ChatOpenAI from langflow.interface.importing.utils import import_class llm_type_to_cls_dict = llms.type_to_cls_dict -llm_type_to_cls_dict["openai-chat"] = OpenAIChat +llm_type_to_cls_dict["openai-chat"] = ChatOpenAI ## Memory From 32e73550dcec565a6beb2c6c0ca7284953525803 Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Sat, 1 Apr 2023 17:25:19 -0300 Subject: [PATCH 57/59] feat: add dill --- poetry.lock | 17 ++++++++++++++++- pyproject.toml | 1 + 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 9462c63cf..1cb90ff46 100644 --- a/poetry.lock +++ b/poetry.lock @@ -579,6 +579,21 @@ files = [ {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, ] +[[package]] +name = "dill" +version = "0.3.6" +description = "serialize all of python" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"}, + {file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] + [[package]] name = "exceptiongroup" version = "1.1.1" @@ -2636,4 +2651,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "afeeaa3c4d0aee2a52be1ccfeff2c47cca9f6af446b5cf4e422fcbb214eec762" +content-hash = "d55f52aa7fb9eadb021289640b342e9901eac33a36f0d7a3a1d26d89c3b0d573" diff --git a/pyproject.toml b/pyproject.toml index 3f202b843..d6be731c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ gunicorn = "^20.1.0" langchain = "^0.0.127" openai = "^0.27.2" types-pyyaml = "^6.0.12.8" +dill = "^0.3.6" [tool.poetry.group.dev.dependencies] black = "^23.1.0" From 737330e3f11298ea750861eecba7aff39e35f5e3 Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Sat, 1 Apr 2023 17:27:35 -0300 Subject: [PATCH 58/59] feat: change log level to debug in dev --- dev.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev.Dockerfile b/dev.Dockerfile index 0f559a0cf..7e439c69a 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -15,4 +15,4 @@ COPY ./ ./ # Install dependencies RUN poetry config virtualenvs.create false && poetry install --no-interaction --no-ansi -CMD ["uvicorn", "langflow.main:app", "--host", "0.0.0.0", "--port", "5003", "--reload"] \ No newline at end of file +CMD ["uvicorn", "langflow.main:app", "--host", "0.0.0.0", "--port", "5003", "--reload", "log-level", "debug"] \ No newline at end of file From 17ad4954ef3259ded31eef55241c733ebc2a84a8 Mon Sep 17 00:00:00 2001 From: Ibis Prevedello Date: Sat, 1 Apr 2023 18:03:14 -0300 Subject: [PATCH 59/59] refac: fix linting --- src/backend/langflow/cache/utils.py | 2 +- src/backend/langflow/graph/nodes.py | 2 - .../langflow/interface/agents/custom.py | 9 +- .../langflow/interface/custom_lists.py | 226 +++++++++--------- .../langflow/interface/importing/utils.py | 1 - src/backend/langflow/interface/loading.py | 2 +- .../langflow/interface/wrappers/base.py | 2 - tests/test_cache.py | 6 +- tests/test_creators.py | 2 +- tests/test_frontend_nodes.py | 18 +- tests/test_template.py | 1 - 11 files changed, 131 insertions(+), 140 deletions(-) diff --git a/src/backend/langflow/cache/utils.py b/src/backend/langflow/cache/utils.py index 1d1f31de1..514d991e5 100644 --- a/src/backend/langflow/cache/utils.py +++ b/src/backend/langflow/cache/utils.py @@ -5,7 +5,7 @@ import os import tempfile from pathlib import Path -import dill +import dill # type: ignore PREFIX = "langflow_cache" diff --git a/src/backend/langflow/graph/nodes.py b/src/backend/langflow/graph/nodes.py index 7f29e3aa7..b465e3817 100644 --- a/src/backend/langflow/graph/nodes.py +++ b/src/backend/langflow/graph/nodes.py @@ -1,10 +1,8 @@ -import json from copy import deepcopy from typing import Any, Dict, List, Optional, Union from langflow.graph.base import Node from langflow.graph.utils import extract_input_variables_from_prompt -from langflow.interface.toolkits.base import toolkits_creator class AgentNode(Node): diff --git a/src/backend/langflow/interface/agents/custom.py b/src/backend/langflow/interface/agents/custom.py index 64682ef4a..653e3b0be 100644 --- a/src/backend/langflow/interface/agents/custom.py +++ b/src/backend/langflow/interface/agents/custom.py @@ -1,18 +1,14 @@ -from pathlib import Path from typing import Any, Optional from langchain import LLMChain from langchain.agents import AgentExecutor, ZeroShotAgent from langchain.agents.agent_toolkits.json.prompt import JSON_PREFIX, JSON_SUFFIX from langchain.agents.agent_toolkits.json.toolkit import JsonToolkit -from langchain.agents.agent_toolkits.pandas.base import create_pandas_dataframe_agent from langchain.agents.agent_toolkits.pandas.prompt import PREFIX as PANDAS_PREFIX from langchain.agents.agent_toolkits.pandas.prompt import SUFFIX as PANDAS_SUFFIX from langchain.agents.mrkl.prompt import FORMAT_INSTRUCTIONS -from langchain.llms.base import BaseLLM from langchain.schema import BaseLanguageModel from langchain.tools.python.tool import PythonAstREPLTool -from pydantic import BaseModel class JsonAgent(AgentExecutor): @@ -65,12 +61,12 @@ class CSVAgent(AgentExecutor): pandas_kwargs: Optional[dict] = None, **kwargs: Any ): - import pandas as pd + import pandas as pd # type: ignore _kwargs = pandas_kwargs or {} df = pd.DataFrame.from_dict(path, **_kwargs) - tools = [PythonAstREPLTool(locals={"df": df})] + tools = [PythonAstREPLTool(locals={"df": df})] # type: ignore prompt = ZeroShotAgent.create_prompt( tools, prefix=PANDAS_PREFIX, @@ -81,7 +77,6 @@ class CSVAgent(AgentExecutor): llm_chain = LLMChain( llm=llm, prompt=partial_prompt, - callback_manager=None, ) tool_names = [tool.name for tool in tools] agent = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names, **kwargs) diff --git a/src/backend/langflow/interface/custom_lists.py b/src/backend/langflow/interface/custom_lists.py index c4522848a..59f163338 100644 --- a/src/backend/langflow/interface/custom_lists.py +++ b/src/backend/langflow/interface/custom_lists.py @@ -1,18 +1,11 @@ -## LLM from typing import Any +## LLM from langchain import llms, requests from langchain.agents import agent_toolkits from langchain.chat_models import ChatOpenAI -from langflow.interface.importing.utils import import_class - -llm_type_to_cls_dict = llms.type_to_cls_dict -llm_type_to_cls_dict["openai-chat"] = ChatOpenAI - - ## Memory - # from langchain.memory.buffer_window import ConversationBufferWindowMemory # from langchain.memory.chat_memory import ChatMessageHistory # from langchain.memory.combined import CombinedMemory @@ -22,108 +15,7 @@ llm_type_to_cls_dict["openai-chat"] = ChatOpenAI # from langchain.memory.simple import SimpleMemory # from langchain.memory.summary import ConversationSummaryMemory # from langchain.memory.summary_buffer import ConversationSummaryBufferMemory - -memory_type_to_cls_dict: dict[str, Any] = { - # "CombinedMemory": CombinedMemory, - # "ConversationBufferWindowMemory": ConversationBufferWindowMemory, - # "ConversationBufferMemory": ConversationBufferMemory, - # "SimpleMemory": SimpleMemory, - # "ConversationSummaryBufferMemory": ConversationSummaryBufferMemory, - # "ConversationKGMemory": ConversationKGMemory, - # "ConversationEntityMemory": ConversationEntityMemory, - # "ConversationSummaryMemory": ConversationSummaryMemory, - # "ChatMessageHistory": ChatMessageHistory, - # "ConversationStringBufferMemory": ConversationStringBufferMemory, - # "ReadOnlySharedMemory": ReadOnlySharedMemory, -} - - -## Chain -# from langchain.chains.loading import type_to_loader_dict -# from langchain.chains.conversation.base import ConversationChain - -# chain_type_to_cls_dict = type_to_loader_dict -# chain_type_to_cls_dict["conversation_chain"] = ConversationChain - -toolkit_type_to_loader_dict: dict[str, Any] = { - toolkit_name: import_class(f"langchain.agents.agent_toolkits.{toolkit_name}") - # if toolkit_name is lower case it is a loader - for toolkit_name in agent_toolkits.__all__ - if toolkit_name.islower() -} - -toolkit_type_to_cls_dict: dict[str, Any] = { - toolkit_name: import_class(f"langchain.agents.agent_toolkits.{toolkit_name}") - # if toolkit_name is not lower case it is a class - for toolkit_name in agent_toolkits.__all__ - if not toolkit_name.islower() -} - - -wrapper_type_to_cls_dict: dict[str, Any] = { - wrapper.__name__: wrapper for wrapper in [requests.RequestsWrapper] -} - -## Embeddings -from langchain.embeddings import ( - CohereEmbeddings, - FakeEmbeddings, - HuggingFaceEmbeddings, - HuggingFaceHubEmbeddings, - HuggingFaceInstructEmbeddings, - OpenAIEmbeddings, - SelfHostedEmbeddings, - SelfHostedHuggingFaceEmbeddings, - SelfHostedHuggingFaceInstructEmbeddings, - # SagemakerEndpointEmbeddings, - TensorflowHubEmbeddings, -) - -embedding_type_to_cls_dict = { - "OpenAIEmbeddings": OpenAIEmbeddings, - "HuggingFaceEmbeddings": HuggingFaceEmbeddings, - "CohereEmbeddings": CohereEmbeddings, - "HuggingFaceHubEmbeddings": HuggingFaceHubEmbeddings, - "TensorflowHubEmbeddings": TensorflowHubEmbeddings, - # "SagemakerEndpointEmbeddings": SagemakerEndpointEmbeddings, - "HuggingFaceInstructEmbeddings": HuggingFaceInstructEmbeddings, - "SelfHostedEmbeddings": SelfHostedEmbeddings, - "SelfHostedHuggingFaceEmbeddings": SelfHostedHuggingFaceEmbeddings, - "SelfHostedHuggingFaceInstructEmbeddings": SelfHostedHuggingFaceInstructEmbeddings, - "FakeEmbeddings": FakeEmbeddings, -} - -## Vector Stores -from langchain.vectorstores import ( - FAISS, - AtlasDB, - Chroma, - DeepLake, - ElasticVectorSearch, - Milvus, - OpenSearchVectorSearch, - Pinecone, - Qdrant, - VectorStore, - Weaviate, -) - -vectorstores_type_to_cls_dict = { - "ElasticVectorSearch": ElasticVectorSearch, - "FAISS": FAISS, - "VectorStore": VectorStore, - "Pinecone": Pinecone, - "Weaviate": Weaviate, - "Qdrant": Qdrant, - "Milvus": Milvus, - "Chroma": Chroma, - "OpenSearchVectorSearch": OpenSearchVectorSearch, - "AtlasDB": AtlasDB, - "DeepLake": DeepLake, -} - ## Document Loaders - from langchain.document_loaders import ( AirbyteJSONLoader, AZLyricsLoader, @@ -173,6 +65,122 @@ from langchain.document_loaders import ( YoutubeLoader, ) +## Embeddings +from langchain.embeddings import ( + CohereEmbeddings, + FakeEmbeddings, + HuggingFaceEmbeddings, + HuggingFaceHubEmbeddings, + HuggingFaceInstructEmbeddings, + OpenAIEmbeddings, + SelfHostedEmbeddings, + SelfHostedHuggingFaceEmbeddings, + SelfHostedHuggingFaceInstructEmbeddings, + # SagemakerEndpointEmbeddings, + TensorflowHubEmbeddings, +) + +## Vector Stores +from langchain.vectorstores import ( + FAISS, + AtlasDB, + Chroma, + DeepLake, + ElasticVectorSearch, + Milvus, + OpenSearchVectorSearch, + Pinecone, + Qdrant, + VectorStore, + Weaviate, +) + +## Toolkits +from langflow.interface.importing.utils import import_class + +## LLM + +llm_type_to_cls_dict = llms.type_to_cls_dict +llm_type_to_cls_dict["openai-chat"] = ChatOpenAI # type: ignore + + +## Memory + +memory_type_to_cls_dict: dict[str, Any] = { + # "CombinedMemory": CombinedMemory, + # "ConversationBufferWindowMemory": ConversationBufferWindowMemory, + # "ConversationBufferMemory": ConversationBufferMemory, + # "SimpleMemory": SimpleMemory, + # "ConversationSummaryBufferMemory": ConversationSummaryBufferMemory, + # "ConversationKGMemory": ConversationKGMemory, + # "ConversationEntityMemory": ConversationEntityMemory, + # "ConversationSummaryMemory": ConversationSummaryMemory, + # "ChatMessageHistory": ChatMessageHistory, + # "ConversationStringBufferMemory": ConversationStringBufferMemory, + # "ReadOnlySharedMemory": ReadOnlySharedMemory, +} + + +## Chain +# from langchain.chains.loading import type_to_loader_dict +# from langchain.chains.conversation.base import ConversationChain + +# chain_type_to_cls_dict = type_to_loader_dict +# chain_type_to_cls_dict["conversation_chain"] = ConversationChain + +toolkit_type_to_loader_dict: dict[str, Any] = { + toolkit_name: import_class(f"langchain.agents.agent_toolkits.{toolkit_name}") + # if toolkit_name is lower case it is a loader + for toolkit_name in agent_toolkits.__all__ + if toolkit_name.islower() +} + +toolkit_type_to_cls_dict: dict[str, Any] = { + toolkit_name: import_class(f"langchain.agents.agent_toolkits.{toolkit_name}") + # if toolkit_name is not lower case it is a class + for toolkit_name in agent_toolkits.__all__ + if not toolkit_name.islower() +} + + +wrapper_type_to_cls_dict: dict[str, Any] = { + wrapper.__name__: wrapper for wrapper in [requests.RequestsWrapper] +} + +## Embeddings + +embedding_type_to_cls_dict = { + "OpenAIEmbeddings": OpenAIEmbeddings, + "HuggingFaceEmbeddings": HuggingFaceEmbeddings, + "CohereEmbeddings": CohereEmbeddings, + "HuggingFaceHubEmbeddings": HuggingFaceHubEmbeddings, + "TensorflowHubEmbeddings": TensorflowHubEmbeddings, + # "SagemakerEndpointEmbeddings": SagemakerEndpointEmbeddings, + "HuggingFaceInstructEmbeddings": HuggingFaceInstructEmbeddings, + "SelfHostedEmbeddings": SelfHostedEmbeddings, + "SelfHostedHuggingFaceEmbeddings": SelfHostedHuggingFaceEmbeddings, + "SelfHostedHuggingFaceInstructEmbeddings": SelfHostedHuggingFaceInstructEmbeddings, + "FakeEmbeddings": FakeEmbeddings, +} + +## Vector Stores + +vectorstores_type_to_cls_dict = { + "ElasticVectorSearch": ElasticVectorSearch, + "FAISS": FAISS, + "VectorStore": VectorStore, + "Pinecone": Pinecone, + "Weaviate": Weaviate, + "Qdrant": Qdrant, + "Milvus": Milvus, + "Chroma": Chroma, + "OpenSearchVectorSearch": OpenSearchVectorSearch, + "AtlasDB": AtlasDB, + "DeepLake": DeepLake, +} + +## Document Loaders + documentloaders_type_to_cls_dict = { "UnstructuredFileLoader": UnstructuredFileLoader, "UnstructuredFileIOLoader": UnstructuredFileIOLoader, diff --git a/src/backend/langflow/interface/importing/utils.py b/src/backend/langflow/interface/importing/utils.py index a819b8390..f054ddc26 100644 --- a/src/backend/langflow/interface/importing/utils.py +++ b/src/backend/langflow/interface/importing/utils.py @@ -9,7 +9,6 @@ from langchain.chains.base import Chain from langchain.llms.base import BaseLLM from langchain.tools import BaseTool -from langflow.interface.agents.custom import CUSTOM_AGENTS from langflow.interface.tools.util import get_tool_by_name diff --git a/src/backend/langflow/interface/loading.py b/src/backend/langflow/interface/loading.py index 2556abbdf..98506f73d 100644 --- a/src/backend/langflow/interface/loading.py +++ b/src/backend/langflow/interface/loading.py @@ -29,7 +29,7 @@ def instantiate_class(node_type: str, base_type: str, params: Dict) -> Any: """Instantiate class from module type and key, and params""" if node_type in CUSTOM_AGENTS: if custom_agent := CUSTOM_AGENTS.get(node_type): - return custom_agent.initialize(**params) + return custom_agent.initialize(**params) # type: ignore class_object = import_by_type(_type=base_type, name=node_type) diff --git a/src/backend/langflow/interface/wrappers/base.py b/src/backend/langflow/interface/wrappers/base.py index 544d361a4..abfa559a1 100644 --- a/src/backend/langflow/interface/wrappers/base.py +++ b/src/backend/langflow/interface/wrappers/base.py @@ -2,9 +2,7 @@ from typing import Dict, List from langchain import requests -from langflow.custom.customs import get_custom_nodes from langflow.interface.base import LangChainTypeCreator -from langflow.settings import settings from langflow.utils.util import build_template_from_class diff --git a/tests/test_cache.py b/tests/test_cache.py index 04c359ac4..c52ceb38a 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -1,12 +1,10 @@ -import hashlib import json import tempfile from pathlib import Path -import dill import pytest -from langflow.cache.utils import PREFIX, compute_hash, load_cache, save_cache -from langflow.interface.run import load_langchain_object, process_graph +from langflow.cache.utils import PREFIX, compute_hash +from langflow.interface.run import load_langchain_object def get_graph(_type="basic"): diff --git a/tests/test_creators.py b/tests/test_creators.py index 42ee2fbca..5453b57eb 100644 --- a/tests/test_creators.py +++ b/tests/test_creators.py @@ -10,7 +10,7 @@ def sample_lang_chain_type_creator() -> LangChainTypeCreator: class SampleLangChainTypeCreator(LangChainTypeCreator): type_name: str = "test_type" - def type_to_loader_dict(self) -> Dict: + def type_to_loader_dict(self) -> Dict: # type: ignore return {"test_type": "TestClass"} def to_list(self) -> List[str]: diff --git a/tests/test_frontend_nodes.py b/tests/test_frontend_nodes.py index 652da90db..673bdaec0 100644 --- a/tests/test_frontend_nodes.py +++ b/tests/test_frontend_nodes.py @@ -1,8 +1,4 @@ -from typing import Dict, List - import pytest -from langflow.interface.agents.base import AgentCreator -from langflow.interface.base import LangChainTypeCreator from langflow.template.base import FrontendNode, Template, TemplateField @@ -28,16 +24,16 @@ def sample_frontend_node(sample_template: Template) -> FrontendNode: def test_template_field_defaults(sample_template_field: TemplateField): assert sample_template_field.field_type == "str" - assert sample_template_field.required == False + assert sample_template_field.required is False assert sample_template_field.placeholder == "" - assert sample_template_field.is_list == False - assert sample_template_field.show == True - assert sample_template_field.multiline == False - assert sample_template_field.value == None + assert sample_template_field.is_list is False + assert sample_template_field.show is True + assert sample_template_field.multiline is False + assert sample_template_field.value is None assert sample_template_field.suffixes == [] assert sample_template_field.file_types == [] - assert sample_template_field.content == None - assert sample_template_field.password == False + assert sample_template_field.content is None + assert sample_template_field.password is False assert sample_template_field.name == "test_field" diff --git a/tests/test_template.py b/tests/test_template.py index f557256e0..9f7d78c55 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -1,5 +1,4 @@ import importlib -import re from typing import Dict, List, Optional import pytest