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