Merge branch 'toolkits' into inputFile
This commit is contained in:
commit
d8f8364b9e
17 changed files with 576 additions and 337 deletions
|
|
@ -22,8 +22,11 @@ tools:
|
|||
- Serper Search
|
||||
- Tool
|
||||
- PythonFunction
|
||||
- JsonSpec
|
||||
|
||||
memories:
|
||||
# - ConversationBufferMemory
|
||||
|
||||
|
||||
|
||||
dev: false
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
from langflow.graph.graph import Graph
|
||||
from langflow.graph.base import Node, Edge
|
||||
|
||||
__all__ = ["Graph", "Node", "Edge"]
|
||||
213
src/backend/langflow/graph/base.py
Normal file
213
src/backend/langflow/graph/base.py
Normal file
|
|
@ -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})"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
99
src/backend/langflow/graph/nodes.py
Normal file
99
src/backend/langflow/graph/nodes.py
Normal file
|
|
@ -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)
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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<React.SV
|
|||
memories: CpuChipIcon,
|
||||
llms: LightBulbIcon,
|
||||
prompts: CommandLineIcon,
|
||||
tools: WrenchScrewdriverIcon,
|
||||
tools: WrenchIcon,
|
||||
advanced: ComputerDesktopIcon,
|
||||
chat: Bars3CenterLeftIcon,
|
||||
docloaders:Bars3CenterLeftIcon,
|
||||
toolkits:WrenchScrewdriverIcon,
|
||||
wrappers:GiftIcon,
|
||||
unknown:QuestionMarkCircleIcon
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue