feat: working version of OpenAPITollkit

This commit is contained in:
Gabriel Almeida 2023-03-31 18:16:20 -03:00
commit c6d0a8d8fa
9 changed files with 106 additions and 63 deletions

View file

@ -30,6 +30,7 @@ def serve(
timeout: int = 60, timeout: int = 60,
port: int = 7860, port: int = 7860,
config: str = "config.yaml", config: str = "config.yaml",
log_level: str = "info",
): ):
update_settings(config) update_settings(config)
app = create_app() app = create_app()
@ -54,7 +55,7 @@ def serve(
# MacOS requires a env variable to be set to use gunicorn # MacOS requires a env variable to be set to use gunicorn
import uvicorn import uvicorn
uvicorn.run(app, host=host, port=port, log_level="info") uvicorn.run(app, host=host, port=port, log_level=log_level)
else: else:
from langflow.server import LangflowApplication from langflow.server import LangflowApplication

View file

@ -6,9 +6,11 @@ from langflow.api.base import Code, ValidationResponse
from langflow.interface.run import process_graph from langflow.interface.run import process_graph
from langflow.interface.types import build_langchain_types_dict from langflow.interface.types import build_langchain_types_dict
from langflow.utils.validate import validate_code from langflow.utils.validate import validate_code
import logging
# build router # build router
router = APIRouter() router = APIRouter()
logger = logging.getLogger(__name__)
@router.get("/all") @router.get("/all")
@ -21,6 +23,8 @@ def get_load(data: Dict[str, Any]):
try: try:
return process_graph(data) return process_graph(data)
except Exception as e: except Exception as e:
# Log stack trace
logger.exception(e)
raise HTTPException(status_code=500, detail=str(e)) from e raise HTTPException(status_code=500, detail=str(e)) from e

View file

@ -6,11 +6,15 @@
import types import types
from copy import deepcopy from copy import deepcopy
from typing import Any, Dict, List 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_dict
from langflow.interface import loading from langflow.interface import loading
from langflow.interface.listing import ALL_TYPES_DICT from langflow.interface.listing import ALL_TYPES_DICT
from langflow.interface.tools.base import tool_creator
import logging
logger = logging.getLogger(__name__)
class Node: class Node:
@ -86,31 +90,38 @@ class Node:
type_to_load = value.get("suffixes") type_to_load = value.get("suffixes")
file_name = value.get("value") file_name = value.get("value")
content = value.get("content") content = value.get("content")
# Now
loaded_dict = load_dict(file_name, content, type_to_load) loaded_dict = load_dict(file_name, content, type_to_load)
params[key] = loaded_dict 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 # Get the edge that connects to this node
edge = next( try:
( edge = next(
edge (
for edge in self.edges edge
if edge.target == self and edge.matched_type in value["type"] for edge in self.edges
), if edge.target == self
None, 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 # Get the output of the node that the edge connects to
# if the value['list'] is True, then there will be more # if the value['list'] is True, then there will be more
# than one time setting to params[key] # than one time setting to params[key]
# so we need to append to a list if it exists # so we need to append to a list if it exists
# or create a new list if it doesn't # or create a new list if it doesn't
if edge is None and value["required"]: if edge is None and value["required"]:
# break line # break line
raise ValueError( raise ValueError(
f"Required input {key} for module {self.node_type} not found" f"Required input {key} for module {self.node_type} not found"
) )
if value["list"]: elif value["list"]:
if key in params: if key in params:
params[key].append(edge.source) params[key].append(edge.source)
else: else:
@ -134,11 +145,15 @@ class Node:
# and continue # and continue
# Another aspect is that the node_type is the class that we need to import # Another aspect is that the node_type is the class that we need to import
# and instantiate with these built params # and instantiate with these built params
logger.debug(f"Building {self.node_type}")
# Build each node in the params dict # Build each node in the params dict
for key, value in self.params.items(): for key, value in self.params.copy().items():
# Check if Node or list of Nodes # Check if Node or list of Nodes and not self
# to avoid recursion
if isinstance(value, Node): if isinstance(value, Node):
if value == self:
del self.params[key]
continue
result = value.build() result = value.build()
# If the key is "func", then we need to use the run method # If the key is "func", then we need to use the run method
if key == "func" and not isinstance(result, types.FunctionType): if key == "func" and not isinstance(result, types.FunctionType):
@ -220,6 +235,15 @@ class Edge:
), ),
None, 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: def __repr__(self) -> str:
return ( return (

View file

@ -0,0 +1 @@
DIRECT_TYPES = ["str", "bool", "code", "int", "float", "Any"]

View file

@ -39,9 +39,19 @@ class Graph:
edge.source.add_edge(edge) edge.source.add_edge(edge)
edge.target.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: for node in self.nodes:
node._build_params() 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]: def get_node(self, node_id: str) -> Union[None, Node]:
return next((node for node in self.nodes if node.id == node_id), None) return next((node for node in self.nodes if node.id == node_id), None)

View file

@ -3,12 +3,14 @@ from copy import deepcopy
from typing import Any, Dict, List, Optional, Union from typing import Any, Dict, List, Optional, Union
from langflow.graph.base import Node from langflow.graph.base import Node
from langflow.graph.utils import extract_input_variables_from_prompt
from langflow.interface.toolkits.base import toolkits_creator from langflow.interface.toolkits.base import toolkits_creator
class AgentNode(Node): class AgentNode(Node):
def __init__(self, data: Dict): def __init__(self, data: Dict):
super().__init__(data, base_type="agents") super().__init__(data, base_type="agents")
self.tools: List[ToolNode] = [] self.tools: List[ToolNode] = []
self.chains: List[ChainNode] = [] self.chains: List[ChainNode] = []
@ -55,14 +57,24 @@ class PromptNode(Node):
tools: Optional[Union[List[Node], List[ToolNode]]] = None, tools: Optional[Union[List[Node], List[ToolNode]]] = None,
) -> Any: ) -> Any:
if not self._built or force: 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 # Check if it is a ZeroShotPrompt and needs a tool
if self.node_type == "ZeroShotPrompt": if "ShotPrompt" in self.node_type:
tools = ( tools = (
[tool_node.build() for tool_node in tools] [tool_node.build() for tool_node in tools]
if tools is not None if tools is not None
else [] else []
) )
self.params["tools"] = tools 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() self._build()
return deepcopy(self._built_object) return deepcopy(self._built_object)
@ -88,42 +100,6 @@ class ChainNode(Node):
return deepcopy(self._built_object) 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): class LLMNode(Node):
def __init__(self, data: Dict): def __init__(self, data: Dict):
super().__init__(data, base_type="llms") super().__init__(data, base_type="llms")
@ -134,6 +110,17 @@ class LLMNode(Node):
return deepcopy(self._built_object) 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): class FileToolNode(ToolNode):
def __init__(self, data: Dict): def __init__(self, data: Dict):
super().__init__(data) super().__init__(data)

View file

@ -1,7 +1,7 @@
import base64 import base64
import json import json
from typing import Dict from typing import Dict
import re
import yaml import yaml
@ -24,3 +24,23 @@ def load_dict(file_name, file_content, accepted_types) -> Dict:
elif suffix in ["yaml", "yml"]: elif suffix in ["yaml", "yml"]:
# Return the yaml content # Return the yaml content
return yaml.safe_load(decoded_string) 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)

View file

@ -1,5 +1,5 @@
import json import json
from typing import Any, Dict, Optional from typing import Any, Callable, Dict, Optional
from langchain.agents import ZeroShotAgent from langchain.agents import ZeroShotAgent
from langchain.agents import agent as agent_module 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): def load_toolkits_executor(node_type: str, toolkit: BaseToolkit, params: dict):
create_function = toolkits_creator.get_create_function(node_type) create_function: Callable = toolkits_creator.get_create_function(node_type)
llm = params.get("llm", None) if llm := params.get("llm"):
if llm:
return create_function(llm=llm, toolkit=toolkit) return create_function(llm=llm, toolkit=toolkit)
return
def load_tools_from_config(tool_list: list[dict]) -> list: def load_tools_from_config(tool_list: list[dict]) -> list:

View file

@ -48,10 +48,8 @@ def get_result_and_thought_using_graph(loaded_langchain, message: str):
) )
thought = output_buffer.getvalue() thought = output_buffer.getvalue()
except Exception as e: except Exception as exc:
result = f"Error: {str(e)}" raise ValueError(f"Error: {str(exc)}") from exc
thought = ""
raise e
return result, thought return result, thought