feat: import Graph without position information (#3203)

* refactor: Add utility functions for getting handle IDs in CustomNodes

- Added `getRightHandleId` function to generate the right handle ID for source handles.
- Added `getLeftHandleId` function to generate the left handle ID for target handles.
- These functions improve code readability and maintainability by encapsulating the logic for generating handle IDs.

* refactor: Add type for escaped handle IDs in edges to improve type safety in reactflowUtils

* feat: Add function to escape handle IDs in edges, enhancing edge management in reactflowUtils

* feat: Add function to check edges without escaped handle IDs, improving edge validation in reactflowUtils

* feat: Enhance edge processing in reactflowUtils to handle edges without escaped handle IDs more effectively

* feat: Add layoutUtils module for handling node layout using elkjs

* feat: update processDataFromFlow to add layout to nodes if needed

* Refactor import paths to use 'initialize' module in 'base.py'

* feat: Add method to set class source code and integrate it with frontend node input field

* refactor: Update sourceHandle dataType to use custom component class name

* fix: Raise error for unknown vertex types instead of returning default Vertex class

* refactor: Remove redundant call to _import_vertex_types() in VertexTypesDict initialization

* refactor: Simplify add_code_field by removing unnecessary field_config parameter from function signature

* feat: Add elkjs dependency to package.json and package-lock.json for enhanced functionality in the frontend

* refactor: Update fields type in Template class to use InputTypes for improved type safety

* refactor: Reorganize imports in __init__.py for better structure and consistency across the inputs module

* refactor: Clean up imports in types.py for better organization and consistency in the graph vertex module

* refactor: Change vertex type annotations to strings for better compatibility and consistency in the graph module methods

* refactor: Update component instantiation to include _code parameter and fix input type annotations for improved type handling

* refactor: Remove unused CustomComponent import from __init__.py for cleaner module structure and improved organization

* refactor: Modify custom_component instantiation to include _code argument for enhanced functionality and clarity in CodeParser class

* refactor: Update CustomComponent import in __init__.py for improved module structure and organization

* refactor: Update launch.json to include correct path for backend source files

* refactor: Update dependencies in poetry.lock to latest versions and resolve merge conflicts in backend files

* refactor: Update dataType assignment in Component class to use component name if available, or fallback to class name

* refactor: Correct flow_id reference in MemoryComponent to improve clarity and consistency in memory handling

* refactor: Update import path for DefaultPromptField to improve code organization and maintainability in api_utils.py

* refactor: Add loading module to __init__.py for improved organization of interface package

* refactor: Clean up imports in base.py and enforce edge validation in Graph class for improved maintainability and error handling

* refactor: Remove edge component additions in test_base.py to streamline graph tests and emphasize error handling for unprepared graphs

* refactor: Mark @clack/prompts is-unicode-supported as extraneous in package-lock.json for better dependency management

* refactor: Update dataType assignment in Component class to use component name if available, or fallback to class name

* refactor: Fix edge existence check in Graph class to use correct variable, ensuring accurate validation of graph structure

* refactor: Add test for graph with edge and improve graph preparation logic

* refactor: Set default node type to "genericNode" in getLayoutedNodes for consistent layout structure

* create consts for node widht and height

* refactor: Catch and log errors when processing flow data in flowsManagerStore and reactflowUtils

* [autofix.ci] apply automated fixes

* fix: Validate custom components for source and target vertices in Graph edges

* test: Add fixture for client and raise TypeError for invalid class parsing in CodeParser tests

* test: Add unit test for listing flows as Flow objects in custom component with client

* test: Update assertions for memory chatbot component types in unit tests

* test: Refactor assertions to use updated component names in vector store RAG unit tests

* fix: Change error handling to return default Vertex for unknown node types in graph class

* [autofix.ci] apply automated fixes

* test: Add pytest fixture for CustomComponent in unit tests to enhance test structure and readability

* chore: Update component names in vector store RAG unit tests

* test: Refactor imports and make flow name generation unique in database unit tests

* chore: Remove unused upload and flow management functions from flowsManagerStore for cleaner codebase

* chore: Await processDataFromFlow in useAddFlow hook

* chore: Correct NODE_HEIGHT calculation to use NODE_WIDTH constant for consistency in constants file

* chore: Remove extraneous flag for is-unicode-supported in package-lock.json for cleaner dependency management

---------

Co-authored-by: anovazzi1 <otavio2204@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Gabriel Luiz Freitas Almeida 2024-08-07 11:04:48 -03:00 • committed by GitHub
commit 919073a0db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 364 additions and 106 deletions

3
.vscode/launch.json vendored
View file

@ -1,7 +1,6 @@
{ {
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Debug Backend", "name": "Debug Backend",
"type": "debugpy", "type": "debugpy",
@ -18,7 +17,7 @@
"--loop", "--loop",
"asyncio", "asyncio",
"--reload-include", "--reload-include",
"src/backend/*" "./src/backend/*"
], ],
"jinja": true, "jinja": true,
"justMyCode": false, "justMyCode": false,

8
poetry.lock generated
View file

@ -4745,19 +4745,19 @@ tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<9.0.0"
[[package]] [[package]]
name = "langchain-anthropic" name = "langchain-anthropic"
version = "0.1.20" version = "0.1.21"
description = "An integration package connecting AnthropicMessages and LangChain" description = "An integration package connecting AnthropicMessages and LangChain"
optional = false optional = false
python-versions = "<4.0,>=3.8.1" python-versions = "<4.0,>=3.8.1"
files = [ files = [
{file = "langchain_anthropic-0.1.20-py3-none-any.whl", hash = "sha256:3a0d89ac6856be98beb3ec63813393bf29af3c5134247979c055938e741b7d9d"}, {file = "langchain_anthropic-0.1.21-py3-none-any.whl", hash = "sha256:74094162e0badd9f5d275a1e2c4019303fbb45638091c202dcaab5f88fda97aa"},
{file = "langchain_anthropic-0.1.20.tar.gz", hash = "sha256:cb9607fecfc0f0de49b79dd0fc066790e2877873ef753abd98d2ae38d6e0f5b2"}, {file = "langchain_anthropic-0.1.21.tar.gz", hash = "sha256:04131e024f79c6a60837a4f3b1399b90440da7a9b0c984b92704e66c317f6c5b"},
] ]
[package.dependencies] [package.dependencies]
anthropic = ">=0.28.0,<1" anthropic = ">=0.28.0,<1"
defusedxml = ">=0.7.1,<0.8.0" defusedxml = ">=0.7.1,<0.8.0"
langchain-core = ">=0.2.17,<0.3" langchain-core = ">=0.2.24,<0.3"
[[package]] [[package]]
name = "langchain-astradb" name = "langchain-astradb"

View file

@ -1,13 +1,13 @@
from langchain.memory import ConversationBufferMemory
from langflow.custom import Component from langflow.custom import Component
from langflow.field_typing import BaseChatMemory
from langflow.helpers.data import data_to_text from langflow.helpers.data import data_to_text
from langflow.inputs import HandleInput from langflow.inputs import HandleInput
from langflow.io import DropdownInput, IntInput, MessageTextInput, MultilineInput, Output from langflow.io import DropdownInput, IntInput, MessageTextInput, MultilineInput, Output
from langflow.memory import get_messages, LCBuiltinChatMemory from langflow.memory import LCBuiltinChatMemory, get_messages
from langflow.schema import Data from langflow.schema import Data
from langflow.schema.message import Message from langflow.schema.message import Message
from langflow.field_typing import BaseChatMemory
from langchain.memory import ConversationBufferMemory
from langflow.utils.constants import MESSAGE_SENDER_AI, MESSAGE_SENDER_USER from langflow.utils.constants import MESSAGE_SENDER_AI, MESSAGE_SENDER_USER
@ -118,5 +118,5 @@ class MemoryComponent(Component):
if self.memory: if self.memory:
chat_memory = self.memory chat_memory = self.memory
else: else:
chat_memory = LCBuiltinChatMemory(flow_id=self.graph.flow_id, session_id=self.session_id) chat_memory = LCBuiltinChatMemory(flow_id=self.flow_id, session_id=self.session_id)
return ConversationBufferMemory(chat_memory=chat_memory) return ConversationBufferMemory(chat_memory=chat_memory)

View file

@ -1,4 +1,4 @@
from langflow.custom.custom_component import CustomComponent
from langflow.custom.custom_component.component import Component from langflow.custom.custom_component.component import Component
from langflow.custom.custom_component.custom_component import CustomComponent
__all__ = ["CustomComponent", "Component"] __all__ = ["CustomComponent", "Component"]

View file

@ -384,7 +384,7 @@ class CodeParser:
def execute_and_inspect_classes(self, code: str): def execute_and_inspect_classes(self, code: str):
custom_component_class = eval_custom_component_code(code) custom_component_class = eval_custom_component_code(code)
custom_component = custom_component_class() custom_component = custom_component_class(_code=code)
dunder_class = custom_component.__class__ dunder_class = custom_component.__class__
# Get the base classes at two levels of inheritance # Get the base classes at two levels of inheritance
bases = [] bases = []

View file

@ -1,3 +0,0 @@
from .custom_component import CustomComponent
__all__ = ["CustomComponent"]

View file

@ -97,7 +97,7 @@ class BaseComponent:
return {} return {}
cc_class = eval_custom_component_code(self._code) cc_class = eval_custom_component_code(self._code)
component_instance = cc_class() component_instance = cc_class(_code=self._code)
template_config = self.get_template_config(component_instance) template_config = self.get_template_config(component_instance)
return template_config return template_config

View file

@ -6,9 +6,7 @@ import nanoid # type: ignore
import yaml import yaml
from pydantic import BaseModel from pydantic import BaseModel
from langflow.graph.edge.schema import EdgeData
from langflow.helpers.custom import format_type from langflow.helpers.custom import format_type
from langflow.inputs.inputs import InputTypes
from langflow.schema.artifact import get_artifact_type, post_process_raw from langflow.schema.artifact import get_artifact_type, post_process_raw
from langflow.schema.data import Data from langflow.schema.data import Data
from langflow.schema.message import Message from langflow.schema.message import Message
@ -20,13 +18,15 @@ from langflow.utils.async_helpers import run_until_complete
from .custom_component import CustomComponent from .custom_component import CustomComponent
if TYPE_CHECKING: if TYPE_CHECKING:
from langflow.graph.edge.schema import EdgeData
from langflow.graph.vertex.base import Vertex from langflow.graph.vertex.base import Vertex
from langflow.inputs.inputs import InputTypes
BACKWARDS_COMPATIBLE_ATTRIBUTES = ["user_id", "vertex", "tracing_service"] BACKWARDS_COMPATIBLE_ATTRIBUTES = ["user_id", "vertex", "tracing_service"]
class Component(CustomComponent): class Component(CustomComponent):
inputs: List[InputTypes] = [] inputs: List["InputTypes"] = []
outputs: List[Output] = [] outputs: List[Output] = []
code_class_base_inheritance: ClassVar[str] = "Component" code_class_base_inheritance: ClassVar[str] = "Component"
_output_logs: dict[str, Log] = {} _output_logs: dict[str, Log] = {}
@ -41,7 +41,7 @@ class Component(CustomComponent):
config[key] = value config[key] = value
else: else:
inputs[key] = value inputs[key] = value
self._inputs: dict[str, InputTypes] = {} self._inputs: dict[str, "InputTypes"] = {}
self._outputs: dict[str, Output] = {} self._outputs: dict[str, Output] = {}
self._results: dict[str, Any] = {} self._results: dict[str, Any] = {}
self._attributes: dict[str, Any] = {} self._attributes: dict[str, Any] = {}
@ -64,6 +64,20 @@ class Component(CustomComponent):
self.map_outputs(self.outputs) self.map_outputs(self.outputs)
# Set output types # Set output types
self._set_output_types() self._set_output_types()
self.set_class_code()
def set_class_code(self):
# Get the source code of the calling class
if self._code:
return
try:
module = inspect.getmodule(self.__class__)
if module is None:
raise ValueError("Could not find module for class")
class_code = inspect.getsource(module)
self._code = class_code
except OSError:
raise ValueError(f"Could not find source code for {self.__class__.__name__}")
def set(self, **kwargs): def set(self, **kwargs):
""" """
@ -174,7 +188,7 @@ class Component(CustomComponent):
raise ValueError("Output name cannot be None.") raise ValueError("Output name cannot be None.")
self._outputs[output.name] = output self._outputs[output.name] = output
def map_inputs(self, inputs: List[InputTypes]): def map_inputs(self, inputs: List["InputTypes"]):
""" """
Maps the given inputs to the component. Maps the given inputs to the component.
@ -272,7 +286,7 @@ class Component(CustomComponent):
"target": self._id, "target": self._id,
"data": { "data": {
"sourceHandle": { "sourceHandle": {
"dataType": self.name, "dataType": component.name or component.__class__.__name__,
"id": component._id, "id": component._id,
"name": output.name, "name": output.name,
"output_types": output.types, "output_types": output.types,
@ -366,6 +380,21 @@ class Component(CustomComponent):
self._map_parameters_on_template(frontend_node_dict["template"]) self._map_parameters_on_template(frontend_node_dict["template"])
frontend_node = ComponentFrontendNode.from_dict(frontend_node_dict) frontend_node = ComponentFrontendNode.from_dict(frontend_node_dict)
if not self._code:
self.set_class_code()
code_field = Input(
dynamic=True,
required=True,
placeholder="",
multiline=True,
value=self._code,
password=False,
name="code",
advanced=True,
field_type="code",
is_list=False,
)
frontend_node.template.add_field(code_field)
for output in frontend_node.outputs: for output in frontend_node.outputs:
if output.types: if output.types:
@ -379,7 +408,7 @@ class Component(CustomComponent):
data = { data = {
"data": { "data": {
"node": frontend_node.to_dict(keep_name=False), "node": frontend_node.to_dict(keep_name=False),
"type": self.__class__.__name__, "type": self.name or self.__class__.__name__,
} }
} }
return data return data

View file

@ -283,7 +283,7 @@ def get_component_instance(custom_component: CustomComponent, user_id: Optional[
) from exc ) from exc
try: try:
custom_instance = custom_class(_user_id=user_id) custom_instance = custom_class(_user_id=user_id, _code=custom_component._code)
return custom_instance return custom_instance
except Exception as exc: except Exception as exc:
logger.error(f"Error while instantiating custom component: {str(exc)}") logger.error(f"Error while instantiating custom component: {str(exc)}")
@ -339,7 +339,7 @@ def run_build_config(
raise exc raise exc
def add_code_field(frontend_node: CustomComponentFrontendNode, raw_code, field_config): def add_code_field(frontend_node: CustomComponentFrontendNode, raw_code):
code_field = Input( code_field = Input(
dynamic=True, dynamic=True,
required=True, required=True,
@ -364,7 +364,7 @@ def build_custom_component_template_from_inputs(
cc_instance = get_component_instance(custom_component, user_id=user_id) cc_instance = get_component_instance(custom_component, user_id=user_id)
field_config = cc_instance.get_template_config(cc_instance) field_config = cc_instance.get_template_config(cc_instance)
frontend_node = ComponentFrontendNode.from_inputs(**field_config) frontend_node = ComponentFrontendNode.from_inputs(**field_config)
frontend_node = add_code_field(frontend_node, custom_component._code, field_config.get("code", {})) frontend_node = add_code_field(frontend_node, custom_component._code)
# But we now need to calculate the return_type of the methods in the outputs # But we now need to calculate the return_type of the methods in the outputs
for output in frontend_node.outputs: for output in frontend_node.outputs:
if output.types: if output.types:
@ -408,7 +408,7 @@ def build_custom_component_template(
add_extra_fields(frontend_node, field_config, entrypoint_args) add_extra_fields(frontend_node, field_config, entrypoint_args)
frontend_node = add_code_field(frontend_node, custom_component._code, field_config.get("code", {})) frontend_node = add_code_field(frontend_node, custom_component._code)
add_base_classes(frontend_node, custom_component.get_function_entrypoint_return_type) add_base_classes(frontend_node, custom_component.get_function_entrypoint_return_type)
add_output_types(frontend_node, custom_component.get_function_entrypoint_return_type) add_output_types(frontend_node, custom_component.get_function_entrypoint_return_type)

View file

@ -182,12 +182,17 @@ class Graph:
if not isinstance(target_vertex, ComponentVertex): if not isinstance(target_vertex, ComponentVertex):
raise ValueError(f"Target vertex {target_id} is not a component vertex.") raise ValueError(f"Target vertex {target_id} is not a component vertex.")
output_name, input_name = output_input_tuple output_name, input_name = output_input_tuple
if source_vertex._custom_component is None:
raise ValueError(f"Source vertex {source_id} does not have a custom component.")
if target_vertex._custom_component is None:
raise ValueError(f"Target vertex {target_id} does not have a custom component.")
edge_data: EdgeData = { edge_data: EdgeData = {
"source": source_id, "source": source_id,
"target": target_id, "target": target_id,
"data": { "data": {
"sourceHandle": { "sourceHandle": {
"dataType": source_vertex.base_name, "dataType": source_vertex._custom_component.name
or source_vertex._custom_component.__class__.__name__,
"id": source_vertex.id, "id": source_vertex.id,
"name": output_name, "name": output_name,
"output_types": source_vertex.get_output(output_name).types, "output_types": source_vertex.get_output(output_name).types,
@ -676,7 +681,7 @@ class Graph:
"flow_name": self.flow_name, "flow_name": self.flow_name,
} }
def build_graph_maps(self, edges: Optional[List[ContractEdge]] = None, vertices: Optional[List[Vertex]] = None): def build_graph_maps(self, edges: Optional[List[ContractEdge]] = None, vertices: Optional[List["Vertex"]] = None):
""" """
Builds the adjacency maps for the graph. Builds the adjacency maps for the graph.
""" """
@ -738,7 +743,7 @@ class Graph:
return edge return edge
return None return None
def build_parent_child_map(self, vertices: List[Vertex]): def build_parent_child_map(self, vertices: List["Vertex"]):
parent_child_map = defaultdict(list) parent_child_map = defaultdict(list)
for vertex in vertices: for vertex in vertices:
parent_child_map[vertex.id] = [child.id for child in self.get_successors(vertex)] parent_child_map[vertex.id] = [child.id for child in self.get_successors(vertex)]
@ -834,7 +839,7 @@ class Graph:
# both graphs have the same vertices and edges # both graphs have the same vertices and edges
# but the data of the vertices might be different # but the data of the vertices might be different
def update_edges_from_vertex(self, vertex: Vertex, other_vertex: Vertex) -> None: def update_edges_from_vertex(self, vertex: "Vertex", other_vertex: "Vertex") -> None:
"""Updates the edges of a vertex in the Graph.""" """Updates the edges of a vertex in the Graph."""
new_edges = [] new_edges = []
for edge in self.edges: for edge in self.edges:
@ -844,13 +849,13 @@ class Graph:
new_edges += other_vertex.edges new_edges += other_vertex.edges
self.edges = new_edges self.edges = new_edges
def vertex_data_is_identical(self, vertex: Vertex, other_vertex: Vertex) -> bool: def vertex_data_is_identical(self, vertex: "Vertex", other_vertex: "Vertex") -> bool:
data_is_equivalent = vertex == other_vertex data_is_equivalent = vertex == other_vertex
if not data_is_equivalent: if not data_is_equivalent:
return False return False
return self.vertex_edges_are_identical(vertex, other_vertex) return self.vertex_edges_are_identical(vertex, other_vertex)
def vertex_edges_are_identical(self, vertex: Vertex, other_vertex: Vertex) -> bool: def vertex_edges_are_identical(self, vertex: "Vertex", other_vertex: "Vertex") -> bool:
same_length = len(vertex.edges) == len(other_vertex.edges) same_length = len(vertex.edges) == len(other_vertex.edges)
if not same_length: if not same_length:
return False return False
@ -909,7 +914,7 @@ class Graph:
self.increment_update_count() self.increment_update_count()
return self return self
def update_vertex_from_another(self, vertex: Vertex, other_vertex: Vertex) -> None: def update_vertex_from_another(self, vertex: "Vertex", other_vertex: "Vertex") -> None:
""" """
Updates a vertex from another vertex. Updates a vertex from another vertex.
@ -947,12 +952,12 @@ class Graph:
self.vertices.append(vertex) self.vertices.append(vertex)
self.vertex_map[vertex.id] = vertex self.vertex_map[vertex.id] = vertex
def add_vertex(self, vertex: Vertex) -> None: def add_vertex(self, vertex: "Vertex") -> None:
"""Adds a new vertex to the graph.""" """Adds a new vertex to the graph."""
self._add_vertex(vertex) self._add_vertex(vertex)
self._update_edges(vertex) self._update_edges(vertex)
def _update_edges(self, vertex: Vertex) -> None: def _update_edges(self, vertex: "Vertex") -> None:
"""Updates the edges of a vertex.""" """Updates the edges of a vertex."""
# Vertex has edges, so we need to update the edges # Vertex has edges, so we need to update the edges
for edge in vertex.edges: for edge in vertex.edges:
@ -987,19 +992,19 @@ class Graph:
for vertex in self.vertices: for vertex in self.vertices:
vertex._build_params() vertex._build_params()
def _validate_vertex(self, vertex: Vertex) -> bool: def _validate_vertex(self, vertex: "Vertex") -> bool:
"""Validates a vertex.""" """Validates a vertex."""
# All vertices that do not have edges are invalid # All vertices that do not have edges are invalid
return len(self.get_vertex_edges(vertex.id)) > 0 return len(self.get_vertex_edges(vertex.id)) > 0
def get_vertex(self, vertex_id: str, silent: bool = False) -> Vertex: def get_vertex(self, vertex_id: str, silent: bool = False) -> "Vertex":
"""Returns a vertex by id.""" """Returns a vertex by id."""
try: try:
return self.vertex_map[vertex_id] return self.vertex_map[vertex_id]
except KeyError: except KeyError:
raise ValueError(f"Vertex {vertex_id} not found") raise ValueError(f"Vertex {vertex_id} not found")
def get_root_of_group_node(self, vertex_id: str) -> Vertex: def get_root_of_group_node(self, vertex_id: str) -> "Vertex":
"""Returns the root of a group node.""" """Returns the root of a group node."""
if vertex_id in self.top_level_vertices: if vertex_id in self.top_level_vertices:
# Get all vertices with vertex_id as .parent_node_id # Get all vertices with vertex_id as .parent_node_id
@ -1157,9 +1162,9 @@ class Graph:
or (edge.target_id == vertex_id and is_target is not False) or (edge.target_id == vertex_id and is_target is not False)
] ]
def get_vertices_with_target(self, vertex_id: str) -> List[Vertex]: def get_vertices_with_target(self, vertex_id: str) -> List["Vertex"]:
"""Returns the vertices connected to a vertex.""" """Returns the vertices connected to a vertex."""
vertices: List[Vertex] = [] vertices: List["Vertex"] = []
for edge in self.edges: for edge in self.edges:
if edge.target_id == vertex_id: if edge.target_id == vertex_id:
vertex = self.get_vertex(edge.source_id) vertex = self.get_vertex(edge.source_id)
@ -1246,7 +1251,7 @@ class Graph:
"""Executes tasks in parallel, handling exceptions for each task.""" """Executes tasks in parallel, handling exceptions for each task."""
results = [] results = []
completed_tasks = await asyncio.gather(*tasks, return_exceptions=True) completed_tasks = await asyncio.gather(*tasks, return_exceptions=True)
vertices: List[Vertex] = [] vertices: List["Vertex"] = []
for i, result in enumerate(completed_tasks): for i, result in enumerate(completed_tasks):
task_name = tasks[i].get_name() task_name = tasks[i].get_name()
@ -1273,7 +1278,7 @@ class Graph:
no_duplicate_results = list(set(results)) no_duplicate_results = list(set(results))
return no_duplicate_results return no_duplicate_results
def topological_sort(self) -> List[Vertex]: def topological_sort(self) -> List["Vertex"]:
""" """
Performs a topological sort of the vertices in the graph. Performs a topological sort of the vertices in the graph.
@ -1306,7 +1311,7 @@ class Graph:
return list(reversed(sorted_vertices)) return list(reversed(sorted_vertices))
def generator_build(self) -> Generator[Vertex, None, None]: def generator_build(self) -> Generator["Vertex", None, None]:
"""Builds each vertex in the graph and yields it.""" """Builds each vertex in the graph and yields it."""
sorted_vertices = self.topological_sort() sorted_vertices = self.topological_sort()
logger.debug("There are %s vertices in the graph", len(sorted_vertices)) logger.debug("There are %s vertices in the graph", len(sorted_vertices))
@ -1316,7 +1321,7 @@ class Graph:
"""Returns the predecessors of a vertex.""" """Returns the predecessors of a vertex."""
return [self.get_vertex(source_id) for source_id in self.predecessor_map.get(vertex.id, [])] return [self.get_vertex(source_id) for source_id in self.predecessor_map.get(vertex.id, [])]
def get_all_successors(self, vertex: Vertex, recursive=True, flat=True): def get_all_successors(self, vertex: "Vertex", recursive=True, flat=True):
# Recursively get the successors of the current vertex # Recursively get the successors of the current vertex
# successors = vertex.successors # successors = vertex.successors
# if not successors: # if not successors:
@ -1353,13 +1358,13 @@ class Graph:
successors_result.append([successor]) successors_result.append([successor])
return successors_result return successors_result
def get_successors(self, vertex: Vertex) -> List[Vertex]: def get_successors(self, vertex: "Vertex") -> List["Vertex"]:
"""Returns the successors of a vertex.""" """Returns the successors of a vertex."""
return [self.get_vertex(target_id) for target_id in self.successor_map.get(vertex.id, [])] return [self.get_vertex(target_id) for target_id in self.successor_map.get(vertex.id, [])]
def get_vertex_neighbors(self, vertex: Vertex) -> Dict[Vertex, int]: def get_vertex_neighbors(self, vertex: "Vertex") -> Dict["Vertex", int]:
"""Returns the neighbors of a vertex.""" """Returns the neighbors of a vertex."""
neighbors: Dict[Vertex, int] = {} neighbors: Dict["Vertex", int] = {}
for edge in self.edges: for edge in self.edges:
if edge.source_id == vertex.id: if edge.source_id == vertex.id:
neighbor = self.get_vertex(edge.target_id) neighbor = self.get_vertex(edge.target_id)
@ -1387,7 +1392,8 @@ class Graph:
for edge in self._edges: for edge in self._edges:
new_edge = self.build_edge(edge) new_edge = self.build_edge(edge)
edges.add(new_edge) edges.add(new_edge)
if self.vertices and not edges:
raise ValueError("Graph has vertices but no edges")
return list(edges) return list(edges)
def build_edge(self, edge: EdgeData) -> ContractEdge: def build_edge(self, edge: EdgeData) -> ContractEdge:
@ -1401,7 +1407,7 @@ class Graph:
new_edge = ContractEdge(source, target, edge) new_edge = ContractEdge(source, target, edge)
return new_edge return new_edge
def _get_vertex_class(self, node_type: str, node_base_type: str, node_id: str) -> Type[Vertex]: def _get_vertex_class(self, node_type: str, node_base_type: str, node_id: str) -> Type["Vertex"]:
"""Returns the node class based on the node type.""" """Returns the node class based on the node type."""
# First we check for the node_base_type # First we check for the node_base_type
node_name = node_id.split("-")[0] node_name = node_id.split("-")[0]
@ -1416,15 +1422,11 @@ class Graph:
if node_type in lazy_load_vertex_dict.VERTEX_TYPE_MAP: if node_type in lazy_load_vertex_dict.VERTEX_TYPE_MAP:
return lazy_load_vertex_dict.VERTEX_TYPE_MAP[node_type] return lazy_load_vertex_dict.VERTEX_TYPE_MAP[node_type]
return ( return Vertex
lazy_load_vertex_dict.VERTEX_TYPE_MAP[node_base_type]
if node_base_type in lazy_load_vertex_dict.VERTEX_TYPE_MAP
else Vertex
)
def _build_vertices(self) -> List[Vertex]: def _build_vertices(self) -> List["Vertex"]:
"""Builds the vertices of the graph.""" """Builds the vertices of the graph."""
vertices: List[Vertex] = [] vertices: List["Vertex"] = []
for frontend_data in self._vertices: for frontend_data in self._vertices:
try: try:
vertex_instance = self.get_vertex(frontend_data["id"]) vertex_instance = self.get_vertex(frontend_data["id"])
@ -1452,6 +1454,7 @@ class Graph:
raise ValueError("You can only provide one of stop_component_id or start_component_id") raise ValueError("You can only provide one of stop_component_id or start_component_id")
self.validate_stream() self.validate_stream()
self.edges = self._build_edges() self.edges = self._build_edges()
if stop_component_id or start_component_id: if stop_component_id or start_component_id:
try: try:
first_layer = self.sort_vertices(stop_component_id, start_component_id) first_layer = self.sort_vertices(stop_component_id, start_component_id)
@ -1485,7 +1488,7 @@ class Graph:
def layered_topological_sort( def layered_topological_sort(
self, self,
vertices: List[Vertex], vertices: List["Vertex"],
filter_graphs: bool = False, filter_graphs: bool = False,
) -> List[List[str]]: ) -> List[List[str]]:
"""Performs a layered topological sort of the vertices in the graph.""" """Performs a layered topological sort of the vertices in the graph."""

View file

@ -19,7 +19,7 @@ def _import_vertex_types():
class VertexTypesDict(LazyLoadDictBase): class VertexTypesDict(LazyLoadDictBase):
def __init__(self): def __init__(self):
self._all_types_dict = None self._all_types_dict = None
self._types = _import_vertex_types() self._types = _import_vertex_types
@property @property
def VERTEX_TYPE_MAP(self): def VERTEX_TYPE_MAP(self):
@ -33,14 +33,15 @@ class VertexTypesDict(LazyLoadDictBase):
} }
def get_type_dict(self): def get_type_dict(self):
types = self._types()
return { return {
**{t: self._types.CustomComponentVertex for t in ["CustomComponent"]}, **{t: types.CustomComponentVertex for t in ["CustomComponent"]},
**{t: self._types.ComponentVertex for t in ["Component"]}, **{t: types.ComponentVertex for t in ["Component"]},
**{t: self._types.InterfaceVertex for t in CHAT_COMPONENTS}, **{t: types.InterfaceVertex for t in CHAT_COMPONENTS},
} }
def get_custom_component_vertex_type(self): def get_custom_component_vertex_type(self):
return self._types.CustomComponentVertex return self._types().CustomComponentVertex
lazy_load_vertex_dict = VertexTypesDict() lazy_load_vertex_dict = VertexTypesDict()

View file

@ -14,7 +14,7 @@ from langflow.exceptions.component import ComponentBuildException
from langflow.graph.schema import INPUT_COMPONENTS, OUTPUT_COMPONENTS, InterfaceComponentTypes, ResultData from langflow.graph.schema import INPUT_COMPONENTS, OUTPUT_COMPONENTS, InterfaceComponentTypes, ResultData
from langflow.graph.utils import UnbuiltObject, UnbuiltResult, log_transaction from langflow.graph.utils import UnbuiltObject, UnbuiltResult, log_transaction
from langflow.graph.vertex.schema import NodeData from langflow.graph.vertex.schema import NodeData
from langflow.interface.initialize import loading from langflow.interface import initialize
from langflow.interface.listing import lazy_load_dict from langflow.interface.listing import lazy_load_dict
from langflow.schema.artifact import ArtifactType from langflow.schema.artifact import ArtifactType
from langflow.schema.data import Data from langflow.schema.data import Data
@ -453,10 +453,10 @@ class Vertex:
raise ValueError(f"Base type for vertex {self.display_name} not found") raise ValueError(f"Base type for vertex {self.display_name} not found")
if not self._custom_component: if not self._custom_component:
custom_component, custom_params = await loading.instantiate_class(user_id=user_id, vertex=self) custom_component, custom_params = await initialize.loading.instantiate_class(user_id=user_id, vertex=self)
else: else:
custom_component = self._custom_component custom_component = self._custom_component
custom_params = loading.get_params(self.params) custom_params = initialize.loading.get_params(self.params)
await self._build_results(custom_component, custom_params, fallback_to_env_vars) await self._build_results(custom_component, custom_params, fallback_to_env_vars)
@ -671,7 +671,7 @@ class Vertex:
async def _build_results(self, custom_component, custom_params, fallback_to_env_vars=False): async def _build_results(self, custom_component, custom_params, fallback_to_env_vars=False):
try: try:
result = await loading.get_instance_results( result = await initialize.loading.get_instance_results(
custom_component=custom_component, custom_component=custom_component,
custom_params=custom_params, custom_params=custom_params,
vertex=self, vertex=self,

View file

@ -7,7 +7,7 @@ from langchain_core.messages import AIMessage, AIMessageChunk
from loguru import logger from loguru import logger
from langflow.graph.schema import CHAT_COMPONENTS, RECORDS_COMPONENTS, InterfaceComponentTypes, ResultData from langflow.graph.schema import CHAT_COMPONENTS, RECORDS_COMPONENTS, InterfaceComponentTypes, ResultData
from langflow.graph.utils import UnbuiltObject, log_transaction, log_vertex_build, serialize_field from langflow.graph.utils import UnbuiltObject, log_transaction, serialize_field
from langflow.graph.vertex.base import Vertex from langflow.graph.vertex.base import Vertex
from langflow.graph.vertex.schema import NodeData from langflow.graph.vertex.schema import NodeData
from langflow.inputs.inputs import InputTypes from langflow.inputs.inputs import InputTypes
@ -15,6 +15,7 @@ from langflow.schema import Data
from langflow.schema.artifact import ArtifactType from langflow.schema.artifact import ArtifactType
from langflow.schema.message import Message from langflow.schema.message import Message
from langflow.schema.schema import INPUT_FIELD_NAME from langflow.schema.schema import INPUT_FIELD_NAME
from langflow.graph.utils import log_vertex_build
from langflow.template.field.base import UNDEFINED, Output from langflow.template.field.base import UNDEFINED, Output
from langflow.utils.schemas import ChatOutputResponse, DataOutputResponse from langflow.utils.schemas import ChatOutputResponse, DataOutputResponse
from langflow.utils.util import unescape_string from langflow.utils.util import unescape_string

View file

@ -1,23 +1,24 @@
from .inputs import ( from .inputs import (
BoolInput, BoolInput,
DataInput, DataInput,
DefaultPromptField,
DictInput, DictInput,
DropdownInput, DropdownInput,
MultiselectInput,
FileInput, FileInput,
FloatInput, FloatInput,
HandleInput, HandleInput,
Input,
IntInput, IntInput,
MessageInput, MessageInput,
MessageTextInput, MessageTextInput,
MultilineInput, MultilineInput,
MultilineSecretInput, MultilineSecretInput,
MultiselectInput,
NestedDictInput, NestedDictInput,
PromptInput, PromptInput,
SecretStrInput, SecretStrInput,
StrInput, StrInput,
TableInput, TableInput,
DefaultPromptField,
) )
__all__ = [ __all__ = [
@ -39,5 +40,6 @@ __all__ = [
"StrInput", "StrInput",
"MessageTextInput", "MessageTextInput",
"TableInput", "TableInput",
"Input",
"DefaultPromptField", "DefaultPromptField",
] ]

View file

@ -0,0 +1,3 @@
from . import loading
__all__ = ["loading"]

View file

@ -9,7 +9,7 @@ from langflow.utils.constants import DIRECT_TYPES
class Template(BaseModel): class Template(BaseModel):
type_name: str = Field(serialization_alias="_type") type_name: str = Field(serialization_alias="_type")
fields: list[Union[Input, InputTypes]] fields: list[InputTypes]
def process_fields( def process_fields(
self, self,

View file

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. # This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
[[package]] [[package]]
name = "aiofiles" name = "aiofiles"
@ -860,6 +860,7 @@ wrapt = ">=1.10,<2"
dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"]
[[package]] [[package]]
<<<<<<< Updated upstream
name = "devtools" name = "devtools"
version = "0.12.2" version = "0.12.2"
description = "Python's missing debug print command, and more." description = "Python's missing debug print command, and more."
@ -876,6 +877,8 @@ executing = ">=1.1.1"
pygments = ">=2.15.0" pygments = ">=2.15.0"
[[package]] [[package]]
=======
>>>>>>> Stashed changes
name = "dictdiffer" name = "dictdiffer"
version = "0.9.0" version = "0.9.0"
description = "Dictdiffer is a library that helps you to diff and patch dictionaries." description = "Dictdiffer is a library that helps you to diff and patch dictionaries."
@ -5256,4 +5259,8 @@ local = []
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.10,<3.13" python-versions = ">=3.10,<3.13"
<<<<<<< Updated upstream
content-hash = "877648288f4f9d5d3304c36853d1b2c5f833d673665bf9b1c707c37bbf02e6ad" content-hash = "877648288f4f9d5d3304c36853d1b2c5f833d673665bf9b1c707c37bbf02e6ad"
=======
content-hash = "bbbc234350488c8293c4b74695db9757be15cabbf6df99ce24d569aecdcd503b"
>>>>>>> Stashed changes

View file

@ -21,13 +21,23 @@ async def test_graph_not_prepared():
graph = Graph() graph = Graph()
graph.add_component("chat_input", chat_input) graph.add_component("chat_input", chat_input)
graph.add_component("chat_output", chat_output) graph.add_component("chat_output", chat_output)
graph.add_component_edge("chat_input", (chat_input.outputs[0].name, chat_input.inputs[0].name), "chat_output")
with pytest.raises(ValueError): with pytest.raises(ValueError):
await graph.astep() await graph.astep()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_graph(): async def test_graph():
chat_input = ChatInput()
chat_output = ChatOutput()
graph = Graph()
graph.add_component("chat_input", chat_input)
graph.add_component("chat_output", chat_output)
with pytest.raises(ValueError, match="Graph has vertices but no edges"):
graph.prepare()
@pytest.mark.asyncio
async def test_graph_with_edge():
chat_input = ChatInput() chat_input = ChatInput()
chat_output = ChatOutput() chat_output = ChatOutput()
graph = Graph() graph = Graph()

View file

@ -94,16 +94,16 @@ def test_memory_chatbot_dump_components_and_edges(memory_chatbot_graph: Graph):
assert nodes[0]["data"]["type"] == "ChatInput" assert nodes[0]["data"]["type"] == "ChatInput"
assert nodes[0]["id"] == "chat_input" assert nodes[0]["id"] == "chat_input"
assert nodes[1]["data"]["type"] == "MemoryComponent" assert nodes[1]["data"]["type"] == "Memory"
assert nodes[1]["id"] == "chat_memory" assert nodes[1]["id"] == "chat_memory"
assert nodes[2]["data"]["type"] == "ChatOutput" assert nodes[2]["data"]["type"] == "ChatOutput"
assert nodes[2]["id"] == "chat_output" assert nodes[2]["id"] == "chat_output"
assert nodes[3]["data"]["type"] == "OpenAIModelComponent" assert nodes[3]["data"]["type"] == "OpenAIModel"
assert nodes[3]["id"] == "openai" assert nodes[3]["id"] == "openai"
assert nodes[4]["data"]["type"] == "PromptComponent" assert nodes[4]["data"]["type"] == "Prompt"
assert nodes[4]["id"] == "prompt" assert nodes[4]["id"] == "prompt"
# Check edges # Check edges

View file

@ -134,16 +134,16 @@ def test_vector_store_rag_dump_components_and_edges(ingestion_graph, rag_graph):
ingestion_nodes = sorted(ingestion_nodes, key=lambda x: x["id"]) ingestion_nodes = sorted(ingestion_nodes, key=lambda x: x["id"])
# Check components in the ingestion graph # Check components in the ingestion graph
assert ingestion_nodes[0]["data"]["type"] == "FileComponent" assert ingestion_nodes[0]["data"]["type"] == "File"
assert ingestion_nodes[0]["id"] == "file-123" assert ingestion_nodes[0]["id"] == "file-123"
assert ingestion_nodes[1]["data"]["type"] == "OpenAIEmbeddingsComponent" assert ingestion_nodes[1]["data"]["type"] == "OpenAIEmbeddings"
assert ingestion_nodes[1]["id"] == "openai-embeddings-123" assert ingestion_nodes[1]["id"] == "openai-embeddings-123"
assert ingestion_nodes[2]["data"]["type"] == "SplitTextComponent" assert ingestion_nodes[2]["data"]["type"] == "SplitText"
assert ingestion_nodes[2]["id"] == "text-splitter-123" assert ingestion_nodes[2]["id"] == "text-splitter-123"
assert ingestion_nodes[3]["data"]["type"] == "AstraVectorStoreComponent" assert ingestion_nodes[3]["data"]["type"] == "AstraDB"
assert ingestion_nodes[3]["id"] == "vector-store-123" assert ingestion_nodes[3]["id"] == "vector-store-123"
# Check edges in the ingestion graph # Check edges in the ingestion graph
@ -178,19 +178,19 @@ def test_vector_store_rag_dump_components_and_edges(ingestion_graph, rag_graph):
assert rag_nodes[1]["data"]["type"] == "ChatOutput" assert rag_nodes[1]["data"]["type"] == "ChatOutput"
assert rag_nodes[1]["id"] == "chatoutput-123" assert rag_nodes[1]["id"] == "chatoutput-123"
assert rag_nodes[2]["data"]["type"] == "OpenAIModelComponent" assert rag_nodes[2]["data"]["type"] == "OpenAIModel"
assert rag_nodes[2]["id"] == "openai-123" assert rag_nodes[2]["id"] == "openai-123"
assert rag_nodes[3]["data"]["type"] == "OpenAIEmbeddingsComponent" assert rag_nodes[3]["data"]["type"] == "OpenAIEmbeddings"
assert rag_nodes[3]["id"] == "openai-embeddings-124" assert rag_nodes[3]["id"] == "openai-embeddings-124"
assert rag_nodes[4]["data"]["type"] == "ParseDataComponent" assert rag_nodes[4]["data"]["type"] == "ParseData"
assert rag_nodes[4]["id"] == "parse-data-123" assert rag_nodes[4]["id"] == "parse-data-123"
assert rag_nodes[5]["data"]["type"] == "PromptComponent" assert rag_nodes[5]["data"]["type"] == "Prompt"
assert rag_nodes[5]["id"] == "prompt-123" assert rag_nodes[5]["id"] == "prompt-123"
assert rag_nodes[6]["data"]["type"] == "AstraVectorStoreComponent" assert rag_nodes[6]["data"]["type"] == "AstraDB"
assert rag_nodes[6]["id"] == "rag-vector-store-123" assert rag_nodes[6]["id"] == "rag-vector-store-123"
# Check edges in the RAG graph # Check edges in the RAG graph

View file

@ -9,7 +9,12 @@ from langflow.custom import Component, CustomComponent
from langflow.custom.code_parser.code_parser import CodeParser, CodeSyntaxError from langflow.custom.code_parser.code_parser import CodeParser, CodeSyntaxError
from langflow.custom.custom_component.base_component import BaseComponent, ComponentCodeNullError from langflow.custom.custom_component.base_component import BaseComponent, ComponentCodeNullError
from langflow.custom.utils import build_custom_component_template from langflow.custom.utils import build_custom_component_template
from langflow.services.database.models.flow import Flow, FlowCreate from langflow.services.database.models.flow import FlowCreate
@pytest.fixture
def client():
pass
@pytest.fixture @pytest.fixture
@ -168,7 +173,7 @@ def test_code_parser_parse_classes():
""" """
Test the parse_classes method of the CodeParser class. Test the parse_classes method of the CodeParser class.
""" """
parser = CodeParser("class Test: pass") parser = CodeParser("from langflow.custom import Component\n\nclass Test(Component): pass")
tree = parser.get_tree() tree = parser.get_tree()
for node in ast.walk(tree): for node in ast.walk(tree):
if isinstance(node, ast.ClassDef): if isinstance(node, ast.ClassDef):
@ -177,6 +182,18 @@ def test_code_parser_parse_classes():
assert parser.data["classes"][0]["name"] == "Test" assert parser.data["classes"][0]["name"] == "Test"
def test_code_parser_parse_classes_raises():
"""
Test the parse_classes method of the CodeParser class.
"""
parser = CodeParser("class Test: pass")
tree = parser.get_tree()
with pytest.raises(TypeError):
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
parser.parse_classes(node)
def test_code_parser_parse_global_vars(): def test_code_parser_parse_global_vars():
""" """
Test the parse_global_vars method of the CodeParser class. Test the parse_global_vars method of the CodeParser class.
@ -487,11 +504,6 @@ def test_list_flows_return_type(component):
assert isinstance(flows, list) assert isinstance(flows, list)
def test_list_flows_flow_objects(component):
flows = component.list_flows()
assert all(isinstance(flow, Flow) for flow in flows)
def test_build_config_return_type(component): def test_build_config_return_type(component):
config = component.build_config() config = component.build_config()
assert isinstance(config, dict) assert isinstance(config, dict)

View file

@ -0,0 +1,23 @@
import pytest
from langflow.custom.custom_component.custom_component import CustomComponent
from langflow.services.database.models.flow import Flow
@pytest.fixture
def component(client, active_user):
return CustomComponent(
user_id=active_user.id,
field_config={
"fields": {
"llm": {"type": "str"},
"url": {"type": "str"},
"year": {"type": "int"},
}
},
)
def test_list_flows_flow_objects(component):
flows = component.list_flows()
assert all(isinstance(flow, Flow) for flow in flows)

View file

@ -1,26 +1,26 @@
import json import json
from collections import namedtuple
from uuid import UUID, uuid4 from uuid import UUID, uuid4
from langflow.graph.utils import log_transaction, log_vertex_build
import orjson import orjson
import pytest import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from sqlmodel import Session from sqlmodel import Session
from langflow.api.v1.schemas import FlowListCreate, ResultDataResponse from langflow.api.v1.schemas import FlowListCreate, ResultDataResponse
from langflow.initial_setup.setup import load_starter_projects, load_flows_from_directory from langflow.graph.utils import log_transaction, log_vertex_build
from langflow.initial_setup.setup import load_flows_from_directory, load_starter_projects
from langflow.services.database.models.base import orjson_dumps from langflow.services.database.models.base import orjson_dumps
from langflow.services.database.models.flow import Flow, FlowCreate, FlowUpdate from langflow.services.database.models.flow import Flow, FlowCreate, FlowUpdate
from langflow.services.database.models.transactions.crud import get_transactions_by_flow_id from langflow.services.database.models.transactions.crud import get_transactions_by_flow_id
from langflow.services.database.utils import session_getter, migrate_transactions_from_monitor_service_to_database from langflow.services.database.utils import migrate_transactions_from_monitor_service_to_database, session_getter
from langflow.services.deps import get_db_service, get_monitor_service, session_scope from langflow.services.deps import get_db_service, get_monitor_service, session_scope
from langflow.services.monitor.schema import TransactionModel from langflow.services.monitor.schema import TransactionModel
from langflow.services.monitor.utils import ( from langflow.services.monitor.utils import (
add_row_to_table,
drop_and_create_table_if_schema_mismatch, drop_and_create_table_if_schema_mismatch,
new_duckdb_locked_connection, new_duckdb_locked_connection,
add_row_to_table,
) )
from collections import namedtuple
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@ -46,7 +46,7 @@ def test_create_flow(client: TestClient, json_flow: str, active_user, logged_in_
assert response.json()["name"] == flow.name assert response.json()["name"] == flow.name
assert response.json()["data"] == flow.data assert response.json()["data"] == flow.data
# flow is optional so we can create a flow without a flow # flow is optional so we can create a flow without a flow
flow = FlowCreate(name="Test Flow") flow = FlowCreate(name=str(uuid4()))
response = client.post("api/v1/flows/", json=flow.model_dump(exclude_unset=True), headers=logged_in_headers) response = client.post("api/v1/flows/", json=flow.model_dump(exclude_unset=True), headers=logged_in_headers)
assert response.status_code == 201 assert response.status_code == 201
assert response.json()["name"] == flow.name assert response.json()["name"] == flow.name

View file

@ -45,6 +45,7 @@
"cmdk": "^1.0.0", "cmdk": "^1.0.0",
"dompurify": "^3.1.5", "dompurify": "^3.1.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"elkjs": "^0.9.3",
"emoji-regex": "^10.3.0", "emoji-regex": "^10.3.0",
"esbuild": "^0.21.5", "esbuild": "^0.21.5",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
@ -7424,6 +7425,11 @@
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.806.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.806.tgz",
"integrity": "sha512-nkoEX2QIB8kwCOtvtgwhXWy2IHVcOLQZu9Qo36uaGB835mdX/h8uLRlosL6QIhLVUnAiicXRW00PwaPZC74Nrg==" "integrity": "sha512-nkoEX2QIB8kwCOtvtgwhXWy2IHVcOLQZu9Qo36uaGB835mdX/h8uLRlosL6QIhLVUnAiicXRW00PwaPZC74Nrg=="
}, },
"node_modules/elkjs": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz",
"integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ=="
},
"node_modules/emoji-regex": { "node_modules/emoji-regex": {
"version": "10.3.0", "version": "10.3.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",

View file

@ -40,6 +40,7 @@
"cmdk": "^1.0.0", "cmdk": "^1.0.0",
"dompurify": "^3.1.5", "dompurify": "^3.1.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"elkjs": "^0.9.3",
"emoji-regex": "^10.3.0", "emoji-regex": "^10.3.0",
"esbuild": "^0.21.5", "esbuild": "^0.21.5",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",

View file

@ -0,0 +1,30 @@
import { sourceHandleType, targetHandleType } from "@/types/flow";
import { scapedJSONStringfy } from "@/utils/reactflowUtils";
export function getRightHandleId({
output_types,
id,
dataType,
name,
}: sourceHandleType): string {
return scapedJSONStringfy({
dataType,
id,
output_types,
name,
});
}
export function getLeftHandleId({
inputTypes,
type,
fieldName,
id,
}: targetHandleType): string {
return scapedJSONStringfy({
inputTypes,
id,
type,
fieldName,
});
}

View file

@ -879,3 +879,6 @@ export const LANGFLOW_ACCESS_TOKEN_EXPIRE_SECONDS = 60 * 60 - 60 * 60 * 0.1;
export const LANGFLOW_ACCESS_TOKEN_EXPIRE_SECONDS_ENV = export const LANGFLOW_ACCESS_TOKEN_EXPIRE_SECONDS_ENV =
Number(process.env.ACCESS_TOKEN_EXPIRE_SECONDS) - Number(process.env.ACCESS_TOKEN_EXPIRE_SECONDS) -
Number(process.env.ACCESS_TOKEN_EXPIRE_SECONDS) * 0.1; Number(process.env.ACCESS_TOKEN_EXPIRE_SECONDS) * 0.1;
export const NODE_WIDTH = 384;
export const NODE_HEIGHT = NODE_WIDTH * 3;

View file

@ -34,7 +34,7 @@ const useAddFlow = () => {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const flow = cloneDeep(params?.flow) ?? undefined; const flow = cloneDeep(params?.flow) ?? undefined;
let flowData = flow let flowData = flow
? processDataFromFlow(flow) ? await processDataFromFlow(flow)
: { nodes: [], edges: [], viewport: { zoom: 1, x: 0, y: 0 } }; : { nodes: [], edges: [], viewport: { zoom: 1, x: 0, y: 0 } };
flowData?.nodes.forEach((node) => { flowData?.nodes.forEach((node) => {
updateGroupRecursion( updateGroupRecursion(

View file

@ -6,6 +6,10 @@ export type unselectAllNodesType = {
data: Node[]; data: Node[];
}; };
export type addEscapedHandleIdsToEdgesType = {
edges: Edge[];
};
export type updateEdgesHandleIdsType = { export type updateEdgesHandleIdsType = {
nodes: NodeType[]; nodes: NodeType[];
edges: Edge[]; edges: Edge[];

View file

@ -0,0 +1,76 @@
import { NODE_HEIGHT, NODE_WIDTH } from "@/constants/constants";
import { NodeType } from "@/types/flow";
import ELK, { ElkNode } from "elkjs/lib/elk.bundled.js";
import { cloneDeep } from "lodash";
import { Edge } from "reactflow";
const layoutOptions = {
"elk.algorithm": "layered",
"elk.direction": "RIGHT",
"elk.layered.spacing.edgeNodeBetweenLayers": "40",
"elk.spacing.nodeNode": "40",
"elk.layered.nodePlacement.strategy": "SIMPLE",
};
const elk = new ELK();
// uses elkjs to give each node a layouted position
export const getLayoutedNodes = async (nodes: NodeType[], edges: Edge[]) => {
const graph = {
id: "root",
layoutOptions,
children: cloneDeep(nodes).map((n) => {
const targetPorts = edges
.filter((e) => e.source === n.id)
.map((e) => ({
id: e.sourceHandle,
properties: {
side: "EAST",
},
}));
const sourcePorts = edges
.filter((e) => e.target === n.id)
.map((e) => ({
id: e.targetHandle,
properties: {
side: "WEST",
},
}));
return {
id: n.id,
width: NODE_WIDTH,
height: NODE_HEIGHT,
// ⚠️ we need to tell elk that the ports are fixed, in order to reduce edge crossings
properties: {
"org.eclipse.elk.portConstraints": "FIXED_ORDER",
},
// we are also passing the id, so we can also handle edges without a sourceHandle or targetHandle option
ports: [{ id: n.id }, ...targetPorts, ...sourcePorts],
};
}) as ElkNode[],
edges: edges.map((e) => ({
id: e.id,
sources: [e.sourceHandle || e.source],
targets: [e.targetHandle || e.target],
})),
};
const layoutedGraph = await elk.layout(graph);
const layoutedNodes = nodes.map((node) => {
const layoutedNode = layoutedGraph.children?.find(
(lgNode) => lgNode.id === node.id,
);
return {
...node,
position: {
x: layoutedNode?.x ?? 0,
y: layoutedNode?.y ?? 0,
},
type: "genericNode",
};
});
return layoutedNodes;
};

View file

@ -1,3 +1,7 @@
import {
getLeftHandleId,
getRightHandleId,
} from "@/CustomNodes/utils/get-handle-id";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { import {
Connection, Connection,
@ -34,11 +38,13 @@ import {
targetHandleType, targetHandleType,
} from "../types/flow"; } from "../types/flow";
import { import {
addEscapedHandleIdsToEdgesType,
findLastNodeType, findLastNodeType,
generateFlowType, generateFlowType,
unselectAllNodesType, unselectAllNodesType,
updateEdgesHandleIdsType, updateEdgesHandleIdsType,
} from "../types/utils/reactflowUtils"; } from "../types/utils/reactflowUtils";
import { getLayoutedNodes } from "./layoutUtils";
import { createRandomKey, toTitleCase } from "./utils"; import { createRandomKey, toTitleCase } from "./utils";
const uid = new ShortUniqueId(); const uid = new ShortUniqueId();
@ -274,7 +280,7 @@ export function updateTemplate(
export const processFlows = (DbData: FlowType[], skipUpdate = true) => { export const processFlows = (DbData: FlowType[], skipUpdate = true) => {
let savedComponents: { [key: string]: APIClassType } = {}; let savedComponents: { [key: string]: APIClassType } = {};
DbData.forEach((flow: FlowType) => { DbData.forEach(async (flow: FlowType) => {
try { try {
if (!flow.data) { if (!flow.data) {
return; return;
@ -290,15 +296,24 @@ export const processFlows = (DbData: FlowType[], skipUpdate = true) => {
] = cloneDeep((flow.data.nodes[0].data as NodeDataType).node!); ] = cloneDeep((flow.data.nodes[0].data as NodeDataType).node!);
return; return;
} }
processDataFromFlow(flow, !skipUpdate); await processDataFromFlow(flow, !skipUpdate).catch((e) => {
console.error(e);
});
} catch (e) { } catch (e) {
console.log(e); console.error(e);
} }
}); });
return { data: savedComponents, flows: DbData }; return { data: savedComponents, flows: DbData };
}; };
export const processDataFromFlow = (flow: FlowType, refreshIds = true) => { const needsLayout = (nodes: NodeType[]) => {
return nodes.some((node) => !node.position);
};
export async function processDataFromFlow(
flow: FlowType,
refreshIds = true,
): Promise<ReactFlowJsonObject | null> {
let data = flow?.data ? flow.data : null; let data = flow?.data ? flow.data : null;
if (data) { if (data) {
processFlowEdges(flow); processFlowEdges(flow);
@ -308,9 +323,14 @@ export const processDataFromFlow = (flow: FlowType, refreshIds = true) => {
updateEdges(data.edges); updateEdges(data.edges);
// updateNodes(data.nodes, data.edges); // updateNodes(data.nodes, data.edges);
if (refreshIds) updateIds(data); // Assuming updateIds is defined elsewhere if (refreshIds) updateIds(data); // Assuming updateIds is defined elsewhere
// add layout to nodes if not present
if (needsLayout(data.nodes)) {
const layoutedNodes = await getLayoutedNodes(data.nodes, data.edges);
data.nodes = layoutedNodes;
}
} }
return data; return data;
}; }
export function updateIds( export function updateIds(
{ edges, nodes }: { edges: Edge[]; nodes: Node[] }, { edges, nodes }: { edges: Edge[]; nodes: Node[] },
@ -341,6 +361,7 @@ export function updateIds(
concatedEdges.forEach((edge: Edge) => { concatedEdges.forEach((edge: Edge) => {
edge.source = idsMap[edge.source]; edge.source = idsMap[edge.source];
edge.target = idsMap[edge.target]; edge.target = idsMap[edge.target];
const sourceHandleObject: sourceHandleType = scapeJSONParse( const sourceHandleObject: sourceHandleType = scapeJSONParse(
edge.sourceHandle!, edge.sourceHandle!,
); );
@ -475,6 +496,26 @@ export function addVersionToDuplicates(flow: FlowType, flows: FlowType[]) {
return newName; return newName;
} }
export function addEscapedHandleIdsToEdges({
edges,
}: addEscapedHandleIdsToEdgesType): Edge[] {
let newEdges = cloneDeep(edges);
newEdges.forEach((edge) => {
let escapedSourceHandle = edge.sourceHandle;
let escapedTargetHandle = edge.targetHandle;
if (!escapedSourceHandle) {
let sourceHandle = edge.data?.sourceHandle;
escapedSourceHandle = getRightHandleId(sourceHandle);
edge.sourceHandle = escapedSourceHandle;
}
if (!escapedTargetHandle) {
let targetHandle = edge.data?.targetHandle;
escapedTargetHandle = getLeftHandleId(targetHandle);
edge.targetHandle = escapedTargetHandle;
}
});
return newEdges;
}
export function updateEdgesHandleIds({ export function updateEdgesHandleIds({
edges, edges,
nodes, nodes,
@ -740,6 +781,13 @@ export function checkOldEdgesHandles(edges: Edge[]): boolean {
); );
} }
export function checkEdgeWithoutEscapedHandleIds(edges: Edge[]): boolean {
return edges.some(
(edge) =>
(!edge.sourceHandle || !edge.targetHandle) && edge.data?.sourceHandle,
);
}
export function checkOldNodesOutput(nodes: NodeType[]): boolean { export function checkOldNodesOutput(nodes: NodeType[]): boolean {
return nodes.some((node) => !node.data.node?.outputs); return nodes.some((node) => !node.data.node?.outputs);
} }
@ -1231,7 +1279,10 @@ export function updateEdgesIds(
export function processFlowEdges(flow: FlowType) { export function processFlowEdges(flow: FlowType) {
if (!flow.data || !flow.data.edges) return; if (!flow.data || !flow.data.edges) return;
if (checkOldEdgesHandles(flow.data.edges)) { if (checkEdgeWithoutEscapedHandleIds(flow.data.edges)) {
const newEdges = addEscapedHandleIdsToEdges({ edges: flow.data.edges });
flow.data.edges = newEdges;
} else if (checkOldEdgesHandles(flow.data.edges)) {
const newEdges = updateEdgesHandleIds(flow.data); const newEdges = updateEdgesHandleIds(flow.data);
flow.data.edges = newEdges; flow.data.edges = newEdges;
} }