Merge remote-tracking branch 'origin/zustand/io/migration' into globalVariables

This commit is contained in:
Lucas Oliveira 2024-03-21 15:35:52 +01:00
commit 99561d0d4c
26 changed files with 1381 additions and 887 deletions

1368
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "langflow" name = "langflow"
version = "0.7.0a0" version = "1.0.0a0"
description = "A Python package with a built-in web application" description = "A Python package with a built-in web application"
authors = ["Logspace <contact@logspace.ai>"] authors = ["Logspace <contact@logspace.ai>"]
maintainers = [ maintainers = [
@ -27,7 +27,7 @@ langflow = "langflow.__main__:main"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = ">=3.9,<3.12" python = ">=3.10,<3.12"
duckdb = "^0.9.2" duckdb = "^0.9.2"
fastapi = "^0.109.0" fastapi = "^0.109.0"
uvicorn = "^0.27.0" uvicorn = "^0.27.0"
@ -50,7 +50,6 @@ fake-useragent = "^1.4.0"
docstring-parser = "^0.15" docstring-parser = "^0.15"
psycopg2-binary = "^2.9.6" psycopg2-binary = "^2.9.6"
pyarrow = "^14.0.0" pyarrow = "^14.0.0"
tiktoken = "~0.6.0"
wikipedia = "^1.4.0" wikipedia = "^1.4.0"
qdrant-client = "^1.7.0" qdrant-client = "^1.7.0"
weaviate-client = "*" weaviate-client = "*"
@ -60,7 +59,7 @@ cohere = "^4.47.0"
python-multipart = "^0.0.7" python-multipart = "^0.0.7"
sqlmodel = "^0.0.14" sqlmodel = "^0.0.14"
faiss-cpu = "^1.7.4" faiss-cpu = "^1.7.4"
anthropic = "^0.15.0" anthropic = "^0.21.0"
orjson = "3.9.15" orjson = "3.9.15"
multiprocess = "^0.70.14" multiprocess = "^0.70.14"
cachetools = "^5.3.1" cachetools = "^5.3.1"
@ -96,7 +95,7 @@ extract-msg = "^0.47.0"
jq = { version = "^1.6.0", markers = "sys_platform != 'win32'" } jq = { version = "^1.6.0", markers = "sys_platform != 'win32'" }
boto3 = "^1.34.0" boto3 = "^1.34.0"
numexpr = "^2.8.6" numexpr = "^2.8.6"
qianfan = "0.3.0" qianfan = "0.3.5"
pgvector = "^0.2.3" pgvector = "^0.2.3"
pyautogen = "^0.2.0" pyautogen = "^0.2.0"
langchain-google-genai = "^0.0.6" langchain-google-genai = "^0.0.6"
@ -104,9 +103,11 @@ elasticsearch = "^8.12.0"
pytube = "^15.0.0" pytube = "^15.0.0"
python-socketio = "^5.11.0" python-socketio = "^5.11.0"
llama-index = "^0.10.13" llama-index = "^0.10.13"
langchain-openai = "^0.0.6" langchain-openai = "^0.0.5"
unstructured = { extras = ["md"], version = "^0.12.4" } unstructured = { extras = ["md"], version = "^0.12.4" }
dspy-ai = "^2.4.0" dspy-ai = "^2.4.0"
crewai = "^0.22.5"
langchain-anthropic = "^0.1.4"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pytest-asyncio = "^0.23.1" pytest-asyncio = "^0.23.1"

View file

@ -13,6 +13,7 @@ from langflow.services.store.schema import StoreComponentCreate
from langflow.services.store.utils import get_lf_version_from_pypi from langflow.services.store.utils import get_lf_version_from_pypi
if TYPE_CHECKING: if TYPE_CHECKING:
from langflow.graph.vertex.base import Vertex
from langflow.services.database.models.flow.model import Flow from langflow.services.database.models.flow.model import Flow
@ -238,3 +239,62 @@ def format_exception_message(exc: Exception) -> str:
if isinstance(causing_exception, SyntaxError): if isinstance(causing_exception, SyntaxError):
return format_syntax_error_message(causing_exception) return format_syntax_error_message(causing_exception)
return str(exc) return str(exc)
async def get_next_runnable_vertices(
graph: Graph,
vertex: "Vertex",
vertex_id: str,
chat_service: ChatService,
flow_id: str,
):
"""
Retrieves the next runnable vertices in the graph for a given vertex.
Args:
graph (Graph): The graph object representing the flow.
vertex (Vertex): The current vertex.
vertex_id (str): The ID of the current vertex.
chat_service (ChatService): The chat service object.
flow_id (str): The ID of the flow.
Returns:
list: A list of IDs of the next runnable vertices.
"""
async with chat_service._cache_locks[flow_id] as lock:
graph.remove_from_predecessors(vertex_id)
direct_successors_ready = [v for v in vertex.successors_ids if graph.is_vertex_runnable(v)]
if not direct_successors_ready:
# No direct successors ready, look for runnable predecessors of successors
next_runnable_vertices = graph.find_runnable_predecessors_for_successors(vertex_id)
else:
next_runnable_vertices = direct_successors_ready
for v_id in set(next_runnable_vertices): # Use set to avoid duplicates
graph.vertices_to_run.remove(v_id)
graph.remove_from_predecessors(v_id)
await chat_service.set_cache(flow_id=flow_id, data=graph, lock=lock)
return next_runnable_vertices
def get_top_level_vertices(graph, vertices_ids):
"""
Retrieves the top-level vertices from the given graph based on the provided vertex IDs.
Args:
graph (Graph): The graph object containing the vertices.
vertices_ids (list): A list of vertex IDs.
Returns:
list: A list of top-level vertex IDs.
"""
top_level_vertices = []
for vertex_id in vertices_ids:
vertex = graph.get_vertex(vertex_id)
if vertex.parent_is_top_level:
top_level_vertices.append(vertex.parent_node_id)
else:
top_level_vertices.append(vertex_id)
return top_level_vertices

View file

@ -10,6 +10,8 @@ from langflow.api.utils import (
build_and_cache_graph, build_and_cache_graph,
format_elapsed_time, format_elapsed_time,
format_exception_message, format_exception_message,
get_next_runnable_vertices,
get_top_level_vertices,
) )
from langflow.api.v1.schemas import ( from langflow.api.v1.schemas import (
InputValueRequest, InputValueRequest,
@ -95,7 +97,8 @@ async def build_vertex(
"""Build a vertex instead of the entire graph.""" """Build a vertex instead of the entire graph."""
start_time = time.perf_counter() start_time = time.perf_counter()
next_vertices_ids = [] next_runnable_vertices = []
top_level_vertices = []
try: try:
start_time = time.perf_counter() start_time = time.perf_counter()
cache = await chat_service.get_cache(flow_id) cache = await chat_service.get_cache(flow_id)
@ -121,12 +124,9 @@ async def build_vertex(
artifacts = vertex.artifacts artifacts = vertex.artifacts
else: else:
raise ValueError(f"No result found for vertex {vertex_id}") raise ValueError(f"No result found for vertex {vertex_id}")
async with chat_service._cache_locks[flow_id] as lock:
graph.remove_from_predecessors(vertex_id)
next_vertices_ids = vertex.successors_ids
next_vertices_ids = [v for v in next_vertices_ids if graph.should_run_vertex(v)]
await chat_service.set_cache(flow_id=flow_id, data=graph, lock=lock)
next_runnable_vertices = await get_next_runnable_vertices(graph, vertex, vertex_id, chat_service, flow_id)
top_level_vertices = get_top_level_vertices(graph, next_runnable_vertices)
result_data_response = ResultDataResponse(**result_dict.model_dump()) result_data_response = ResultDataResponse(**result_dict.model_dump())
except Exception as exc: except Exception as exc:
@ -166,12 +166,13 @@ async def build_vertex(
# to stop the build of the graph at a certain vertex # to stop the build of the graph at a certain vertex
# if it is in next_vertices_ids, we need to remove other # if it is in next_vertices_ids, we need to remove other
# vertices from next_vertices_ids # vertices from next_vertices_ids
if graph.stop_vertex and graph.stop_vertex in next_vertices_ids: if graph.stop_vertex and graph.stop_vertex in next_runnable_vertices:
next_vertices_ids = [graph.stop_vertex] next_runnable_vertices = [graph.stop_vertex]
build_response = VertexBuildResponse( build_response = VertexBuildResponse(
inactivated_vertices=inactivated_vertices, inactivated_vertices=inactivated_vertices,
next_vertices_ids=next_vertices_ids, next_vertices_ids=next_runnable_vertices,
top_level_vertices=top_level_vertices,
valid=valid, valid=valid,
params=params, params=params,
id=vertex.id, id=vertex.id,
@ -201,7 +202,7 @@ async def build_vertex_stream(
async def stream_vertex(): async def stream_vertex():
try: try:
if not session_id: if not session_id:
cache = chat_service.get_cache(flow_id) cache = await chat_service.get_cache(flow_id)
if not cache: if not cache:
# If there's no cache # If there's no cache
raise ValueError(f"No cache found for {flow_id}.") raise ValueError(f"No cache found for {flow_id}.")
@ -251,7 +252,7 @@ async def build_vertex_stream(
raise ValueError(f"No result found for vertex {vertex_id}") raise ValueError(f"No result found for vertex {vertex_id}")
except Exception as exc: except Exception as exc:
logger.error(f"Error building vertex: {exc}") logger.exception(f"Error building vertex: {exc}")
yield str(StreamData(event="error", data={"error": str(exc)})) yield str(StreamData(event="error", data={"error": str(exc)}))
finally: finally:
logger.debug("Closing stream") logger.debug("Closing stream")

View file

@ -247,6 +247,7 @@ class VertexBuildResponse(BaseModel):
id: Optional[str] = None id: Optional[str] = None
inactivated_vertices: Optional[List[str]] = None inactivated_vertices: Optional[List[str]] = None
next_vertices_ids: Optional[List[str]] = None next_vertices_ids: Optional[List[str]] = None
top_level_vertices: Optional[List[str]] = None
valid: bool valid: bool
params: Optional[Any] = Field(default_factory=dict) params: Optional[Any] = Field(default_factory=dict)
"""JSON string of the params.""" """JSON string of the params."""

View file

@ -44,13 +44,13 @@ def post_validate_prompt(prompt_request: ValidatePromptRequest):
input_variables=input_variables, input_variables=input_variables,
frontend_node=None, frontend_node=None,
) )
if not prompt_request.custom_fields: if not prompt_request.frontend_node.custom_fields:
prompt_request.custom_fields = defaultdict(list) prompt_request.frontend_node.custom_fields = defaultdict(list)
old_custom_fields = get_old_custom_fields(prompt_request.custom_fields, prompt_request.name) old_custom_fields = get_old_custom_fields(prompt_request.frontend_node.custom_fields, prompt_request.name)
add_new_variables_to_template( add_new_variables_to_template(
input_variables, input_variables,
prompt_request.custom_fields, prompt_request.frontend_node.custom_fields,
prompt_request.frontend_node.template, prompt_request.frontend_node.template,
prompt_request.name, prompt_request.name,
) )
@ -58,13 +58,25 @@ def post_validate_prompt(prompt_request: ValidatePromptRequest):
remove_old_variables_from_template( remove_old_variables_from_template(
old_custom_fields, old_custom_fields,
input_variables, input_variables,
prompt_request.custom_fields, prompt_request.frontend_node.custom_fields,
prompt_request.frontend_node.template, prompt_request.frontend_node.template,
prompt_request.name, prompt_request.name,
) )
update_input_variables_field(input_variables, prompt_request.frontend_node.template) update_input_variables_field(input_variables, prompt_request.frontend_node.template)
# If frontend_node.template contains only one field that is type == 'prompt', then we can remove all fields that are not
# 'code', and not in the input_variables list.
prompt_fields = [
key
for key, field in prompt_request.frontend_node.template.items()
if isinstance(field, dict) and field["type"] == "prompt"
]
if len(prompt_fields) == 1:
for key, field in prompt_request.frontend_node.template.copy().items():
if isinstance(field, dict) and field["type"] != "code" and key not in input_variables + prompt_fields:
del prompt_request.frontend_node.template[key]
return PromptValidationResponse( return PromptValidationResponse(
input_variables=input_variables, input_variables=input_variables,
frontend_node=prompt_request.frontend_node, frontend_node=prompt_request.frontend_node,

View file

@ -1,6 +1,6 @@
from typing import Optional from typing import Optional
from langchain_community.chat_models.anthropic import ChatAnthropic from langchain_anthropic.chat_models import ChatAnthropic
from pydantic.v1 import SecretStr from pydantic.v1 import SecretStr
from langflow.components.models.base.model import LCModelComponent from langflow.components.models.base.model import LCModelComponent

View file

@ -35,6 +35,14 @@ class Graph:
edges: List[Dict[str, str]], edges: List[Dict[str, str]],
flow_id: Optional[str] = None, flow_id: Optional[str] = None,
) -> None: ) -> None:
"""
Initializes a new instance of the Graph class.
Args:
nodes (List[Dict]): A list of dictionaries representing the vertices of the graph.
edges (List[Dict[str, str]]): A list of dictionaries representing the edges of the graph.
flow_id (Optional[str], optional): The ID of the flow. Defaults to None.
"""
self._vertices = nodes self._vertices = nodes
self._edges = edges self._edges = edges
self.raw_graph_data = {"nodes": nodes, "edges": edges} self.raw_graph_data = {"nodes": nodes, "edges": edges}
@ -71,11 +79,26 @@ class Graph:
self.state_manager = GraphStateManager() self.state_manager = GraphStateManager()
def get_state(self, name: str) -> Optional[Record]: def get_state(self, name: str) -> Optional[Record]:
"""Returns the state of the graph.""" """
Returns the state of the graph with the given name.
Args:
name (str): The name of the state.
Returns:
Optional[Record]: The state record, or None if the state does not exist.
"""
return self.state_manager.get_state(name, run_id=self._run_id) return self.state_manager.get_state(name, run_id=self._run_id)
def update_state(self, name: str, record: Union[str, Record], caller: Optional[str] = None) -> None: def update_state(self, name: str, record: Union[str, Record], caller: Optional[str] = None) -> None:
"""Updates the state of the graph.""" """
Updates the state of the graph with the given name.
Args:
name (str): The name of the state.
record (Union[str, Record]): The new state record.
caller (Optional[str], optional): The ID of the vertex that is updating the state. Defaults to None.
"""
if caller: if caller:
# If there is a caller which is a vertex_id, I want to activate # If there is a caller which is a vertex_id, I want to activate
# all StateVertex in self.vertices that are not the caller # all StateVertex in self.vertices that are not the caller
@ -86,6 +109,13 @@ class Graph:
self.state_manager.update_state(name, record, run_id=self._run_id) self.state_manager.update_state(name, record, run_id=self._run_id)
def activate_state_vertices(self, name: str, caller: str): def activate_state_vertices(self, name: str, caller: str):
"""
Activates the state vertices in the graph with the given name and caller.
Args:
name (str): The name of the state.
caller (str): The ID of the vertex that is updating the state.
"""
vertices_ids = [] vertices_ids = []
for vertex_id in self._is_state_vertices: for vertex_id in self._is_state_vertices:
if vertex_id == caller: if vertex_id == caller:
@ -104,10 +134,20 @@ class Graph:
self.vertices_to_run.update(vertices_ids) self.vertices_to_run.update(vertices_ids)
def reset_activated_vertices(self): def reset_activated_vertices(self):
"""
Resets the activated vertices in the graph.
"""
self.activated_vertices = [] self.activated_vertices = []
def append_state(self, name: str, record: Union[str, Record], caller: Optional[str] = None) -> None: def append_state(self, name: str, record: Union[str, Record], caller: Optional[str] = None) -> None:
"""Appends the state of the graph.""" """
Appends the state of the graph with the given name.
Args:
name (str): The name of the state.
record (Union[str, Record]): The state record to append.
caller (Optional[str], optional): The ID of the vertex that is updating the state. Defaults to None.
"""
if caller: if caller:
self.activate_state_vertices(name, caller) self.activate_state_vertices(name, caller)
@ -115,17 +155,38 @@ class Graph:
@property @property
def run_id(self): def run_id(self):
"""
The ID of the current run.
Returns:
str: The run ID.
Raises:
ValueError: If the run ID is not set.
"""
if not self._run_id: if not self._run_id:
raise ValueError("Run ID not set") raise ValueError("Run ID not set")
return self._run_id return self._run_id
def set_run_id(self, run_id: str): def set_run_id(self, run_id: str):
"""
Sets the ID of the current run.
Args:
run_id (str): The run ID.
"""
for vertex in self.vertices: for vertex in self.vertices:
self.state_manager.subscribe(run_id, vertex.update_graph_state) self.state_manager.subscribe(run_id, vertex.update_graph_state)
self._run_id = run_id self._run_id = run_id
@property @property
def sorted_vertices_layers(self) -> List[List[str]]: def sorted_vertices_layers(self) -> List[List[str]]:
"""
The sorted layers of vertices in the graph.
Returns:
List[List[str]]: The sorted layers of vertices.
"""
if not self._sorted_vertices_layers: if not self._sorted_vertices_layers:
self.sort_vertices() self.sort_vertices()
return self._sorted_vertices_layers return self._sorted_vertices_layers
@ -148,7 +209,19 @@ class Graph:
stream: bool, stream: bool,
session_id: str, session_id: str,
) -> List[Optional["ResultData"]]: ) -> List[Optional["ResultData"]]:
"""Runs the graph with the given inputs.""" """
Runs the graph with the given inputs.
Args:
inputs (Dict[str, str]): The input values for the graph.
input_components (list[str]): The components to run for the inputs.
outputs (list[str]): The outputs to retrieve from the graph.
stream (bool): Whether to stream the results or not.
session_id (str): The session ID for the graph.
Returns:
List[Optional["ResultData"]]: The outputs of the graph.
"""
for vertex_id in self._is_input_vertices: for vertex_id in self._is_input_vertices:
vertex = self.get_vertex(vertex_id) vertex = self.get_vertex(vertex_id)
if input_components and (vertex_id not in input_components or vertex.display_name not in input_components): if input_components and (vertex_id not in input_components or vertex.display_name not in input_components):
@ -190,7 +263,19 @@ class Graph:
session_id: Optional[str] = None, session_id: Optional[str] = None,
stream: bool = False, stream: bool = False,
) -> List[RunOutputs]: ) -> List[RunOutputs]:
"""Runs the graph with the given inputs.""" """
Runs the graph with the given inputs.
Args:
inputs (list[Dict[str, str]]): The input values for the graph.
inputs_components (Optional[list[list[str]]], optional): The components to run for the inputs. Defaults to None.
outputs (Optional[list[str]], optional): The outputs to retrieve from the graph. Defaults to None.
session_id (Optional[str], optional): The session ID for the graph. Defaults to None.
stream (bool, optional): Whether to stream the results or not. Defaults to False.
Returns:
List[RunOutputs]: The outputs of the graph.
"""
# inputs is {"message": "Hello, world!"} # inputs is {"message": "Hello, world!"}
# we need to go through self.inputs and update the self._raw_params # we need to go through self.inputs and update the self._raw_params
# of the vertices that are inputs # of the vertices that are inputs
@ -218,16 +303,23 @@ class Graph:
vertex_outputs.append(run_output_object) vertex_outputs.append(run_output_object)
return vertex_outputs return vertex_outputs
# vertices_layers is a list of lists ordered by the order the vertices
# should be built.
# We need to create a new method that will take the vertices_layers
# and return the next vertex to be built.
def next_vertex_to_build(self): def next_vertex_to_build(self):
"""Returns the next vertex to be built.""" """
Returns the next vertex to be built.
Yields:
str: The ID of the next vertex to be built.
"""
yield from chain.from_iterable(self.vertices_layers) yield from chain.from_iterable(self.vertices_layers)
@property @property
def metadata(self): def metadata(self):
"""
The metadata of the graph.
Returns:
dict: The metadata of the graph.
"""
return { return {
"runs": self._runs, "runs": self._runs,
"updates": self._updates, "updates": self._updates,
@ -235,12 +327,19 @@ class Graph:
} }
def build_graph_maps(self): def build_graph_maps(self):
"""
Builds the adjacency maps for the graph.
"""
self.predecessor_map, self.successor_map = self.build_adjacency_maps() self.predecessor_map, self.successor_map = self.build_adjacency_maps()
self.in_degree_map = self.build_in_degree() self.in_degree_map = self.build_in_degree()
self.parent_child_map = self.build_parent_child_map() self.parent_child_map = self.build_parent_child_map()
def reset_inactivated_vertices(self): def reset_inactivated_vertices(self):
"""
Resets the inactivated vertices in the graph.
"""
self.inactivated_vertices = []
self.inactivated_vertices = set() self.inactivated_vertices = set()
def mark_all_vertices(self, state: str): def mark_all_vertices(self, state: str):
@ -377,7 +476,10 @@ class Graph:
# Remove vertices that are not in the other graph # Remove vertices that are not in the other graph
for vertex_id in removed_vertex_ids: for vertex_id in removed_vertex_ids:
try:
self.remove_vertex(vertex_id) self.remove_vertex(vertex_id)
except ValueError:
pass
# The order here matters because adding the vertex is required # The order here matters because adding the vertex is required
# if any of them have edges that point to any of the new vertices # if any of them have edges that point to any of the new vertices
@ -741,8 +843,11 @@ class Graph:
vertex_data = vertex["data"] vertex_data = vertex["data"]
vertex_type: str = vertex_data["type"] # type: ignore vertex_type: str = vertex_data["type"] # type: ignore
vertex_base_type: str = vertex_data["node"]["template"]["_type"] # type: ignore vertex_base_type: str = vertex_data["node"]["template"]["_type"] # type: ignore
if "id" not in vertex_data:
raise ValueError(f"Vertex data for {vertex_data['display_name']} does not contain an id")
VertexClass = self._get_vertex_class(vertex_type, vertex_base_type, vertex_data["id"]) VertexClass = self._get_vertex_class(vertex_type, vertex_base_type, vertex_data["id"])
vertex_instance = VertexClass(vertex, graph=self) vertex_instance = VertexClass(vertex, graph=self)
vertex_instance.set_top_level(self.top_level_vertices) vertex_instance.set_top_level(self.top_level_vertices)
vertices.append(vertex_instance) vertices.append(vertex_instance)
@ -953,22 +1058,26 @@ class Graph:
# Return just the first layer # Return just the first layer
return first_layer return first_layer
def vertex_has_no_more_predecessors(self, vertex_id: str) -> bool: def is_vertex_runnable(self, vertex_id: str) -> bool:
"""Returns whether a vertex has no more predecessors.""" """Returns whether a vertex is runnable."""
return not self.run_predecessors.get(vertex_id) return vertex_id in self.vertices_to_run and not self.run_predecessors.get(vertex_id)
def should_run_vertex(self, vertex_id: str) -> bool: def find_runnable_predecessors_for_successors(self, vertex_id: str) -> List[str]:
"""Returns whether a component should be run.""" """
# the self.run_map is a map of vertex_id to a list of predecessors For each successor of the current vertex, find runnable predecessors if any.
# each time a vertex is run, we remove it from the list of predecessors This checks the direct predecessors of each successor to identify any that are
# if a vertex has no more predecessors, it should be run immediately runnable, expanding the search to ensure progress can be made.
should_run = vertex_id in self.vertices_to_run and self.vertex_has_no_more_predecessors(vertex_id) """
runnable_vertices = []
visited = set()
if should_run: for successor_id in self.run_map.get(vertex_id, []):
self.vertices_to_run.remove(vertex_id) for predecessor_id in self.run_predecessors.get(successor_id, []):
# remove the vertex from the run_map if predecessor_id not in visited and self.is_vertex_runnable(predecessor_id):
self.remove_from_predecessors(vertex_id) runnable_vertices.append(predecessor_id)
return should_run visited.add(predecessor_id)
return runnable_vertices
def remove_from_predecessors(self, vertex_id: str): def remove_from_predecessors(self, vertex_id: str):
predecessors = self.run_map.get(vertex_id, []) predecessors = self.run_map.get(vertex_id, [])

View file

@ -1,4 +1,3 @@
import { cloneDeep } from "lodash";
import useFlowStore from "../../stores/flowStore"; import useFlowStore from "../../stores/flowStore";
import { IOOutputProps } from "../../types/components"; import { IOOutputProps } from "../../types/components";
import { Textarea } from "../ui/textarea"; import { Textarea } from "../ui/textarea";

View file

@ -105,7 +105,13 @@ export default function IOView({
return ( return (
<BaseModal <BaseModal
size={haveChat ? (selectedTab === 0 ? "large-thin" : "large") : "small"} size={
haveChat || selectedViewField
? selectedTab === 0
? "large-thin"
: "large"
: "small"
}
open={open} open={open}
setOpen={setOpen} setOpen={setOpen}
disable={disable} disable={disable}
@ -123,13 +129,13 @@ export default function IOView({
</div> </div>
</BaseModal.Header> </BaseModal.Header>
<BaseModal.Content> <BaseModal.Content>
<div className="flex h-full flex-col"> <div className="flex h-full flex-col overflow-hidden">
<div className="flex-max-width mt-2 h-full"> <div className="flex-max-width mt-2 h-full">
{selectedTab !== 0 && ( {selectedTab !== 0 && (
<div <div
className={cn( className={cn(
"mr-6 flex h-full w-2/6 flex-shrink-0 flex-col justify-start", "mr-6 flex h-full w-2/6 flex-shrink-0 flex-col justify-start transition-all duration-300",
haveChat ? "w-2/6" : "w-full" haveChat || selectedViewField ? "w-2/6" : "w-full"
)} )}
> >
<Tabs <Tabs
@ -179,7 +185,6 @@ export default function IOView({
<Badge variant="gray" size="md"> <Badge variant="gray" size="md">
{node.data.node.display_name} {node.data.node.display_name}
</Badge> </Badge>
{haveChat && (
<div <div
className="-mb-1 pr-4" className="-mb-1 pr-4"
onClick={(event) => { onClick={(event) => {
@ -192,7 +197,6 @@ export default function IOView({
name="ExternalLink" name="ExternalLink"
></IconComponent> ></IconComponent>
</div> </div>
)}
</div> </div>
} }
key={index} key={index}
@ -248,7 +252,6 @@ export default function IOView({
</Badge> </Badge>
</div> </div>
</ShadTooltip> </ShadTooltip>
{haveChat && (
<div <div
className="-mb-1 pr-4" className="-mb-1 pr-4"
onClick={(event) => { onClick={(event) => {
@ -261,7 +264,6 @@ export default function IOView({
name="ExternalLink" name="ExternalLink"
></IconComponent> ></IconComponent>
</div> </div>
)}
</div> </div>
} }
key={index} key={index}
@ -287,12 +289,12 @@ export default function IOView({
</div> </div>
)} )}
{haveChat ? ( {haveChat || selectedViewField ? (
<div className="flex h-full min-w-96 flex-grow"> <div className="flex h-full min-w-96 flex-grow">
{selectedViewField && ( {selectedViewField && (
<div <div
className={cn( className={cn(
"flex h-full w-full flex-col items-start gap-4 p-4", "flex h-full w-full flex-col items-start gap-4 pt-4",
!selectedViewField ? "hidden" : "" !selectedViewField ? "hidden" : ""
)} )}
> >
@ -347,7 +349,7 @@ export default function IOView({
</BaseModal.Content> </BaseModal.Content>
<BaseModal.Footer> <BaseModal.Footer>
{!haveChat && ( {!haveChat && (
<div className="flex w-full justify-end pt-6"> <div className="flex w-full justify-end pt-2">
<Button <Button
variant={"outline"} variant={"outline"}
className="flex gap-2 px-3" className="flex gap-2 px-3"

View file

@ -29,6 +29,7 @@ export const MenuBar = ({
const addFlow = useFlowsManagerStore((state) => state.addFlow); const addFlow = useFlowsManagerStore((state) => state.addFlow);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow); const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const undo = useFlowsManagerStore((state) => state.undo); const undo = useFlowsManagerStore((state) => state.undo);
const redo = useFlowsManagerStore((state) => state.redo); const redo = useFlowsManagerStore((state) => state.redo);
const saveLoading = useFlowsManagerStore((state) => state.saveLoading); const saveLoading = useFlowsManagerStore((state) => state.saveLoading);
@ -38,11 +39,21 @@ export const MenuBar = ({
const navigate = useNavigate(); const navigate = useNavigate();
const isBuilding = useFlowStore((state) => state.isBuilding); const isBuilding = useFlowStore((state) => state.isBuilding);
function handleAddFlow() { function handleAddFlow(duplicate?: boolean) {
try { try {
if (duplicate) {
if (!currentFlow) {
throw new Error("No flow to duplicate");
}
addFlow(true, currentFlow).then((id) => {
setSuccessData({ title: "Flow duplicated successfully" });
navigate("/flow/" + id);
});
} else {
addFlow(true).then((id) => { addFlow(true).then((id) => {
navigate("/flow/" + id); navigate("/flow/" + id);
}); });
}
} catch (err) { } catch (err) {
setErrorData(err as { title: string; list?: Array<string> }); setErrorData(err as { title: string; list?: Array<string> });
} }
@ -62,7 +73,7 @@ export const MenuBar = ({
<button <button
onClick={() => { onClick={() => {
removeFunction(nodes); removeFunction(nodes);
navigate(-1); navigate("/");
}} }}
> >
<IconComponent name="ChevronLeft" className="w-4" /> <IconComponent name="ChevronLeft" className="w-4" />
@ -88,6 +99,15 @@ export const MenuBar = ({
<IconComponent name="Plus" className="header-menu-options" /> <IconComponent name="Plus" className="header-menu-options" />
New New
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
handleAddFlow(true);
}}
className="cursor-pointer"
>
<IconComponent name="Copy" className="header-menu-options" />
Duplicate
</DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {

View file

@ -1,6 +1,7 @@
import * as Form from "@radix-ui/react-form"; import * as Form from "@radix-ui/react-form";
import { PopoverAnchor } from "@radix-ui/react-popover"; import { PopoverAnchor } from "@radix-ui/react-popover";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import useAlertStore from "../../stores/alertStore";
import { InputComponentType } from "../../types/components"; import { InputComponentType } from "../../types/components";
import { handleKeyDown } from "../../utils/reactflowUtils"; import { handleKeyDown } from "../../utils/reactflowUtils";
import { classNames, cn } from "../../utils/utils"; import { classNames, cn } from "../../utils/utils";
@ -37,6 +38,7 @@ export default function InputComponent({
optionsButton, optionsButton,
optionButton, optionButton,
}: InputComponentType): JSX.Element { }: InputComponentType): JSX.Element {
const setErrorData = useAlertStore.getState().setErrorData;
const [pwdVisible, setPwdVisible] = useState(false); const [pwdVisible, setPwdVisible] = useState(false);
const refInput = useRef<HTMLInputElement>(null); const refInput = useRef<HTMLInputElement>(null);
const [showOptions, setShowOptions] = useState<boolean>(false); const [showOptions, setShowOptions] = useState<boolean>(false);
@ -119,7 +121,22 @@ export default function InputComponent({
)} )}
placeholder={password && editNode ? "Key" : placeholder} placeholder={password && editNode ? "Key" : placeholder}
onChange={(e) => { onChange={(e) => {
// if the user copies a password from another input
// it might come as ••••••••••• it causes errors
// in ascii encoding, so we need to handle it
if (password) {
// check if all chars are •
if (e.target.value.split("").every((char) => char === "•")) {
setErrorData({
title: `Invalid characters: ${e.target.value}`,
list: [
"It seems you are trying to paste a password. Make sure the value is visible before copying from another field.",
],
});
}
onChange && onChange(e.target.value); onChange && onChange(e.target.value);
}
}} }}
onKeyDown={(e) => { onKeyDown={(e) => {
handleKeyDown(e, value, ""); handleKeyDown(e, value, "");

View file

@ -1,7 +1,6 @@
// src/constants/constants.ts // src/constants/constants.ts
import { languageMap } from "../types/components"; import { languageMap } from "../types/components";
import { FlowType } from "../types/flow";
/** /**
* invalid characters for flow name * invalid characters for flow name
@ -750,75 +749,5 @@ export const NATIVE_CATEGORIES = [
"helpers", "helpers",
"experimental", "experimental",
]; ];
/*
Data ingestion
Basic Prompting
Chat com memória
Working with data (file/website)
API requests
Vector Store
Assistant
*/
export const EXAMPLES_MOCK: FlowType[] = [ export const SAVE_DEBOUNCE_TIME = 500;
{
name: "Working with data",
id: "Working with data Description",
data: {
nodes: [],
edges: [],
viewport: { zoom: 1, x: 1, y: 1 },
},
description: "This flow represents the first process in our application.",
folder: STARTER_FOLDER_NAME,
user_id: undefined,
},
{
name: "Basic Prompting",
id: "Basic Prompting Description",
data: {
nodes: [],
edges: [],
viewport: { zoom: 1, x: 1, y: 1 },
},
description: "This flow represents the first process in our application.",
folder: STARTER_FOLDER_NAME,
user_id: undefined,
},
{
name: "Chat with memory",
id: "Chat with memory Description",
data: {
nodes: [],
edges: [],
viewport: { zoom: 1, x: 1, y: 1 },
},
description: "This flow represents the first process in our application.",
folder: STARTER_FOLDER_NAME,
user_id: undefined,
},
{
name: "API requests",
id: "API requests Description",
data: {
nodes: [],
edges: [],
viewport: { zoom: 1, x: 1, y: 1 },
},
description: "This flow represents the first process in our application.",
folder: STARTER_FOLDER_NAME,
user_id: undefined,
},
{
name: "Assistant",
id: "Assistant Description",
data: {
nodes: [],
edges: [],
viewport: { zoom: 1, x: 1, y: 1 },
},
description: "This flow represents the first process in our application.",
folder: STARTER_FOLDER_NAME,
user_id: undefined,
},
];

View file

@ -10,6 +10,7 @@ import {
DialogTrigger, DialogTrigger,
} from "../../components/ui/dialog"; } from "../../components/ui/dialog";
import { modalHeaderType } from "../../types/components"; import { modalHeaderType } from "../../types/components";
import { cn } from "../../utils/utils";
type ContentProps = { children: ReactNode }; type ContentProps = { children: ReactNode };
type HeaderProps = { children: ReactNode; description: string }; type HeaderProps = { children: ReactNode; description: string };
@ -157,11 +158,15 @@ function BaseModal({
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
{triggerChild} {triggerChild}
<DialogContent className={minWidth}> <DialogContent className={cn(minWidth, "duration-300")}>
<div className="truncate-doubleline word-break-break-word"> <div className="truncate-doubleline word-break-break-word">
{headerChild} {headerChild}
</div> </div>
<div className={`flex flex-col ${height!} w-full `}>{ContentChild}</div> <div
className={`flex flex-col ${height!} w-full transition-all duration-300`}
>
{ContentChild}
</div>
{ContentFooter && ( {ContentFooter && (
<div className="flex flex-row-reverse">{ContentFooter}</div> <div className="flex flex-row-reverse">{ContentFooter}</div>
)} )}

View file

@ -146,7 +146,14 @@ export default function CodeAreaModal({
setCode(value); setCode(value);
}, [value, open]); }, [value, open]);
const handlePreventEsc = (e: React.KeyboardEvent) => {
if (e.key === "Escape") {
e.preventDefault();
}
};
return ( return (
<div onKeyDown={(e) => handlePreventEsc(e)}>
<BaseModal open={open} setOpen={setOpen}> <BaseModal open={open} setOpen={setOpen}>
<BaseModal.Trigger>{children}</BaseModal.Trigger> <BaseModal.Trigger>{children}</BaseModal.Trigger>
<BaseModal.Header description={CODE_PROMPT_DIALOG_SUBTITLE}> <BaseModal.Header description={CODE_PROMPT_DIALOG_SUBTITLE}>
@ -216,5 +223,6 @@ export default function CodeAreaModal({
</div> </div>
</BaseModal.Content> </BaseModal.Content>
</BaseModal> </BaseModal>
</div>
); );
} }

View file

@ -1,4 +1,4 @@
import _ from "lodash"; import _, { cloneDeep } from "lodash";
import { MouseEvent, useCallback, useEffect, useRef, useState } from "react"; import { MouseEvent, useCallback, useEffect, useRef, useState } from "react";
import ReactFlow, { import ReactFlow, {
Background, Background,
@ -32,6 +32,7 @@ import {
isValidConnection, isValidConnection,
reconnectEdges, reconnectEdges,
scapeJSONParse, scapeJSONParse,
updateIds,
validateSelection, validateSelection,
} from "../../../../utils/reactflowUtils"; } from "../../../../utils/reactflowUtils";
import { getRandomName, isWrappedWithClass } from "../../../../utils/utils"; import { getRandomName, isWrappedWithClass } from "../../../../utils/utils";
@ -97,6 +98,56 @@ export default function Page({
useEffect(() => { useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => { const onKeyDown = (event: KeyboardEvent) => {
const selectedNode = nodes.filter((obj) => obj.selected); const selectedNode = nodes.filter((obj) => obj.selected);
if (
selectionMenuVisible &&
(event.ctrlKey || event.metaKey) &&
event.key === "g"
) {
event.preventDefault();
takeSnapshot();
if (validateSelection(lastSelection!, edges).length === 0) {
const clonedNodes = cloneDeep(nodes);
const clonedEdges = cloneDeep(edges);
const clonedSelection = cloneDeep(lastSelection);
updateIds(
{ nodes: clonedNodes, edges: clonedEdges },
clonedSelection!
);
const { newFlow, removedEdges } = generateFlow(
clonedSelection!,
clonedNodes,
clonedEdges,
getRandomName()
);
const newGroupNode = generateNodeFromFlow(newFlow, getNodeId);
const newEdges = reconnectEdges(newGroupNode, removedEdges);
setNodes([
...clonedNodes.filter(
(oldNodes) =>
!clonedSelection?.nodes.some(
(selectionNode) => selectionNode.id === oldNodes.id
)
),
newGroupNode,
]);
setEdges([
...clonedEdges.filter(
(oldEdge) =>
!clonedSelection!.nodes.some(
(selectionNode) =>
selectionNode.id === oldEdge.target ||
selectionNode.id === oldEdge.source
)
),
...newEdges,
]);
} else {
setErrorData({
title: INVALID_SELECTION_ERROR_ALERT,
list: validateSelection(lastSelection!, edges),
});
}
}
if ( if (
(event.ctrlKey || event.metaKey) && (event.ctrlKey || event.metaKey) &&
event.key === "p" && event.key === "p" &&
@ -201,7 +252,7 @@ export default function Page({
document.removeEventListener("keydown", onKeyDown); document.removeEventListener("keydown", onKeyDown);
document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mousemove", handleMouseMove);
}; };
}, [lastCopiedSelection, lastSelection, takeSnapshot]); }, [lastCopiedSelection, lastSelection, takeSnapshot, selectionMenuVisible]);
useEffect(() => { useEffect(() => {
if (reactFlowInstance && currentFlowId) { if (reactFlowInstance && currentFlowId) {
@ -437,10 +488,17 @@ export default function Page({
if ( if (
validateSelection(lastSelection!, edges).length === 0 validateSelection(lastSelection!, edges).length === 0
) { ) {
const clonedNodes = cloneDeep(nodes);
const clonedEdges = cloneDeep(edges);
const clonedSelection = cloneDeep(lastSelection);
updateIds(
{ nodes: clonedNodes, edges: clonedEdges },
clonedSelection!
);
const { newFlow, removedEdges } = generateFlow( const { newFlow, removedEdges } = generateFlow(
lastSelection!, clonedSelection!,
nodes, clonedNodes,
edges, clonedEdges,
getRandomName() getRandomName()
); );
const newGroupNode = generateNodeFromFlow( const newGroupNode = generateNodeFromFlow(
@ -451,20 +509,20 @@ export default function Page({
newGroupNode, newGroupNode,
removedEdges removedEdges
); );
setNodes((oldNodes) => [ setNodes([
...oldNodes.filter( ...clonedNodes.filter(
(oldNodes) => (oldNodes) =>
!lastSelection?.nodes.some( !clonedSelection?.nodes.some(
(selectionNode) => (selectionNode) =>
selectionNode.id === oldNodes.id selectionNode.id === oldNodes.id
) )
), ),
newGroupNode, newGroupNode,
]); ]);
setEdges((oldEdges) => [ setEdges([
...oldEdges.filter( ...clonedEdges.filter(
(oldEdge) => (oldEdge) =>
!lastSelection!.nodes.some( !clonedSelection!.nodes.some(
(selectionNode) => (selectionNode) =>
selectionNode.id === oldEdge.target || selectionNode.id === oldEdge.target ||
selectionNode.id === oldEdge.source selectionNode.id === oldEdge.source

View file

@ -1,4 +1,4 @@
import { cloneDeep } from "lodash"; import { cloneDeep, zip } from "lodash";
import { import {
Edge, Edge,
EdgeChange, EdgeChange,
@ -26,6 +26,7 @@ import {
ChatOutputType, ChatOutputType,
FlowPoolObjectType, FlowPoolObjectType,
FlowStoreType, FlowStoreType,
VertexLayerElementType,
chatInputType, chatInputType,
} from "../types/zustand/flow"; } from "../types/zustand/flow";
import { buildVertices } from "../utils/buildUtils"; import { buildVertices } from "../utils/buildUtils";
@ -36,6 +37,7 @@ import {
getNodeId, getNodeId,
scapeJSONParse, scapeJSONParse,
scapedJSONStringfy, scapedJSONStringfy,
updateGroupRecursion,
validateNodes, validateNodes,
} from "../utils/reactflowUtils"; } from "../utils/reactflowUtils";
import { getInputsAndOutputs } from "../utils/storeUtils"; import { getInputsAndOutputs } from "../utils/storeUtils";
@ -221,6 +223,8 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
); );
}, },
paste: (selection, position) => { paste: (selection, position) => {
function updateGroup() {}
if ( if (
selection.nodes.some((node) => node.data.type === "ChatInput") && selection.nodes.some((node) => node.data.type === "ChatInput") &&
checkChatInput(get().nodes) checkChatInput(get().nodes)
@ -257,6 +261,8 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
let newId = getNodeId(node.data.type); let newId = getNodeId(node.data.type);
idsMap[node.id] = newId; idsMap[node.id] = newId;
updateGroupRecursion(node, selection.edges);
// Create a new node object // Create a new node object
const newNode: NodeType = { const newNode: NodeType = {
id: newId, id: newId,
@ -459,9 +465,18 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
// verticesLayers is a list of list of vertices ids, where each list is a layer of vertices // verticesLayers is a list of list of vertices ids, where each list is a layer of vertices
// we want to add a new layer (next_vertices_ids) to the list of layers (verticesLayers) // we want to add a new layer (next_vertices_ids) to the list of layers (verticesLayers)
// and the values of next_vertices_ids to the list of vertices ids (verticesIds) // and the values of next_vertices_ids to the list of vertices ids (verticesIds)
// const nextVertices will be the zip of vertexBuildData.next_vertices_ids and
// vertexBuildData.top_level_vertices
// the VertexLayerElementType as {id: next_vertices_id, layer: top_level_vertex}
const nextVertices: VertexLayerElementType[] = zip(
vertexBuildData.next_vertices_ids,
vertexBuildData.top_level_vertices
).map(([id, reference]) => ({ id: id!, reference }));
const newLayers = [ const newLayers = [
...get().verticesBuild!.verticesLayers, ...get().verticesBuild!.verticesLayers,
vertexBuildData.next_vertices_ids, nextVertices,
]; ];
const newIds = [ const newIds = [
...get().verticesBuild!.verticesIds, ...get().verticesBuild!.verticesIds,
@ -473,7 +488,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
runId: runId, runId: runId,
}); });
get().updateBuildStatus( get().updateBuildStatus(
vertexBuildData.next_vertices_ids, vertexBuildData.top_level_vertices,
BuildStatus.TO_BUILD BuildStatus.TO_BUILD
); );
} }
@ -508,12 +523,19 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
get().setIsBuilding(false); get().setIsBuilding(false);
}, },
onBuildUpdate: handleBuildUpdate, onBuildUpdate: handleBuildUpdate,
onBuildError: (title, list, idList) => { onBuildError: (title: string, list: string[], elementList) => {
const idList = elementList
.map((element) => element.id)
.filter(Boolean) as string[];
useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILT); useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILT);
setErrorData({ list, title }); setErrorData({ list, title });
get().setIsBuilding(false); get().setIsBuilding(false);
}, },
onBuildStart: (idList) => { onBuildStart: (elementList) => {
const idList = elementList
// reference is the id of the vertex or the id of the parent in a group node
.map((element) => element.reference)
.filter(Boolean) as string[];
useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILDING); useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILDING);
}, },
validateNodes: validateSubgraph, validateNodes: validateSubgraph,
@ -531,7 +553,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
updateVerticesBuild: ( updateVerticesBuild: (
vertices: { vertices: {
verticesIds: string[]; verticesIds: string[];
verticesLayers: string[][]; verticesLayers: VertexLayerElementType[][];
runId: string; runId: string;
} | null } | null
) => { ) => {
@ -562,6 +584,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
}, },
updateBuildStatus: (nodeIdList: string[], status: BuildStatus) => { updateBuildStatus: (nodeIdList: string[], status: BuildStatus) => {
const newFlowBuildStatus = { ...get().flowBuildStatus }; const newFlowBuildStatus = { ...get().flowBuildStatus };
console.log("newFlowBuildStatus", newFlowBuildStatus);
nodeIdList.forEach((id) => { nodeIdList.forEach((id) => {
newFlowBuildStatus[id] = { newFlowBuildStatus[id] = {
status, status,

View file

@ -1,8 +1,11 @@
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { cloneDeep } from "lodash"; import { cloneDeep, debounce } from "lodash";
import { Edge, Node, Viewport, XYPosition } from "reactflow"; import { Edge, Node, Viewport, XYPosition } from "reactflow";
import { create } from "zustand"; import { create } from "zustand";
import { STARTER_FOLDER_NAME } from "../constants/constants"; import {
SAVE_DEBOUNCE_TIME,
STARTER_FOLDER_NAME,
} from "../constants/constants";
import { import {
deleteFlowFromDatabase, deleteFlowFromDatabase,
readFlowsFromDatabase, readFlowsFromDatabase,
@ -92,22 +95,18 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
}); });
}); });
}, },
autoSaveCurrentFlow: (nodes: Node[], edges: Edge[], viewport: Viewport) => { autoSaveCurrentFlow: debounce(
// Clear the previous timeout if it exists. (nodes: Node[], edges: Edge[], viewport: Viewport) => {
if (saveTimeoutId) {
clearTimeout(saveTimeoutId);
}
set({ saveLoading: true }); set({ saveLoading: true });
// Set up a new timeout.
saveTimeoutId = setTimeout(() => {
if (get().currentFlow) { if (get().currentFlow) {
get().saveFlow( get().saveFlow(
{ ...get().currentFlow!, data: { nodes, edges, viewport } }, { ...get().currentFlow!, data: { nodes, edges, viewport } },
true true
); );
} }
}, 500); // Delay of 500ms because chat message depends on it.
}, },
SAVE_DEBOUNCE_TIME
),
saveFlow: (flow: FlowType, silent?: boolean) => { saveFlow: (flow: FlowType, silent?: boolean) => {
set({ saveLoading: true }); set({ saveLoading: true });
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {

View file

@ -146,6 +146,7 @@ export type VertexBuildTypeAPI = {
id: string; id: string;
inactivated_vertices: Array<string> | null; inactivated_vertices: Array<string> | null;
next_vertices_ids: Array<string>; next_vertices_ids: Array<string>;
top_level_vertices: Array<string>;
run_id: string; run_id: string;
valid: boolean; valid: boolean;
params: string; params: string;

View file

@ -35,6 +35,11 @@ export type FlowPoolObjectType = {
buildId: string; buildId: string;
}; };
export type VertexLayerElementType = {
id: string;
reference?: string;
};
export type FlowPoolType = { export type FlowPoolType = {
[key: string]: Array<FlowPoolObjectType>; [key: string]: Array<FlowPoolObjectType>;
}; };
@ -103,7 +108,7 @@ export type FlowStoreType = {
updateVerticesBuild: ( updateVerticesBuild: (
vertices: { vertices: {
verticesIds: string[]; verticesIds: string[];
verticesLayers: string[][]; verticesLayers: VertexLayerElementType[][];
runId: string; runId: string;
} | null } | null
) => void; ) => void;
@ -111,7 +116,7 @@ export type FlowStoreType = {
removeFromVerticesBuild: (vertices: string[]) => void; removeFromVerticesBuild: (vertices: string[]) => void;
verticesBuild: { verticesBuild: {
verticesIds: string[]; verticesIds: string[];
verticesLayers: string[][]; verticesLayers: VertexLayerElementType[][];
runId: string; runId: string;
} | null; } | null;
updateBuildStatus: (nodeId: string[], status: BuildStatus) => void; updateBuildStatus: (nodeId: string[], status: BuildStatus) => void;

View file

@ -4,6 +4,7 @@ import { getVerticesOrder, postBuildVertex } from "../controllers/API";
import useAlertStore from "../stores/alertStore"; import useAlertStore from "../stores/alertStore";
import useFlowStore from "../stores/flowStore"; import useFlowStore from "../stores/flowStore";
import { VertexBuildTypeAPI } from "../types/api"; import { VertexBuildTypeAPI } from "../types/api";
import { VertexLayerElementType } from "../types/zustand/flow";
type BuildVerticesParams = { type BuildVerticesParams = {
flowId: string; // Assuming FlowType is the type for your flow flowId: string; // Assuming FlowType is the type for your flow
@ -17,8 +18,8 @@ type BuildVerticesParams = {
buildId: string buildId: string
) => void; // Replace any with the actual type if it's not any ) => void; // Replace any with the actual type if it's not any
onBuildComplete?: (allNodesValid: boolean) => void; onBuildComplete?: (allNodesValid: boolean) => void;
onBuildError?: (title, list, idList: string[]) => void; onBuildError?: (title, list, idList: VertexLayerElementType[]) => void;
onBuildStart?: (idList: string[]) => void; onBuildStart?: (idList: VertexLayerElementType[]) => void;
validateNodes?: (nodes: string[]) => void; validateNodes?: (nodes: string[]) => void;
}; };
@ -35,6 +36,7 @@ function getInactiveVertexData(vertexId: string): VertexBuildTypeAPI {
inactivated_vertices: null, inactivated_vertices: null,
run_id: "", run_id: "",
next_vertices_ids: [], next_vertices_ids: [],
top_level_vertices: [],
inactive_vertices: null, inactive_vertices: null,
valid: false, valid: false,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
@ -48,7 +50,7 @@ export async function updateVerticesOrder(
startNodeId?: string | null, startNodeId?: string | null,
stopNodeId?: string | null stopNodeId?: string | null
): Promise<{ ): Promise<{
verticesLayers: string[][]; verticesLayers: VertexLayerElementType[][];
verticesIds: string[]; verticesIds: string[];
runId: string; runId: string;
}> { }> {
@ -66,7 +68,14 @@ export async function updateVerticesOrder(
useFlowStore.getState().setIsBuilding(false); useFlowStore.getState().setIsBuilding(false);
throw new Error("Invalid nodes"); throw new Error("Invalid nodes");
} }
let verticesLayers: Array<Array<string>> = [orderResponse.data.ids]; // orderResponse.data.ids,
// for each id we need to build the VertexLayerElementType object as
// {id: id, reference: id}
let verticesLayers: Array<Array<VertexLayerElementType>> =
orderResponse.data.ids.map((id: string) => {
return [{ id: id, reference: id }];
});
const runId = orderResponse.data.run_id; const runId = orderResponse.data.run_id;
// if (nodeId) { // if (nodeId) {
// for (let i = 0; i < verticesOrder.length; i += 1) { // for (let i = 0; i < verticesOrder.length; i += 1) {
@ -160,17 +169,17 @@ export async function buildVertices({
if (onBuildStart) onBuildStart(currentLayer); if (onBuildStart) onBuildStart(currentLayer);
// Build each vertex in the current layer // Build each vertex in the current layer
await Promise.all( await Promise.all(
currentLayer.map(async (vertexId) => { currentLayer.map(async (element) => {
// Check if id is in the list of inactive nodes // Check if id is in the list of inactive nodes
if ( if (
!useFlowStore !useFlowStore
.getState() .getState()
.verticesBuild?.verticesIds.includes(vertexId) && .verticesBuild?.verticesIds.includes(element.id) &&
onBuildUpdate onBuildUpdate
) { ) {
// If it is, skip building and set the state to inactive // If it is, skip building and set the state to inactive
onBuildUpdate( onBuildUpdate(
getInactiveVertexData(vertexId), getInactiveVertexData(element.id),
BuildStatus.INACTIVE, BuildStatus.INACTIVE,
runId runId
); );
@ -181,7 +190,7 @@ export async function buildVertices({
// Build the vertex // Build the vertex
await buildVertex({ await buildVertex({
flowId, flowId,
id: vertexId, id: element.id,
input_value, input_value,
onBuildUpdate: (data: VertexBuildTypeAPI, status: BuildStatus) => { onBuildUpdate: (data: VertexBuildTypeAPI, status: BuildStatus) => {
if (onBuildUpdate) onBuildUpdate(data, status, runId); if (onBuildUpdate) onBuildUpdate(data, status, runId);
@ -226,7 +235,7 @@ async function buildVertex({
id: string; id: string;
input_value: string; input_value: string;
onBuildUpdate?: (data: any, status: BuildStatus) => void; onBuildUpdate?: (data: any, status: BuildStatus) => void;
onBuildError?: (title, list, idList: string[]) => void; onBuildError?: (title, list, idList: VertexLayerElementType[]) => void;
verticesIds: string[]; verticesIds: string[];
buildResults: boolean[]; buildResults: boolean[];
stopBuild: () => void; stopBuild: () => void;
@ -240,7 +249,7 @@ async function buildVertex({
onBuildError!( onBuildError!(
"Error Building Component", "Error Building Component",
[buildData.params], [buildData.params],
verticesIds verticesIds.map((id) => ({ id }))
); );
stopBuild(); stopBuild();
} }
@ -251,7 +260,7 @@ async function buildVertex({
onBuildError!( onBuildError!(
"Error Building Component", "Error Building Component",
[(error as AxiosError<any>).response?.data?.detail ?? "Unknown Error"], [(error as AxiosError<any>).response?.data?.detail ?? "Unknown Error"],
verticesIds verticesIds.map((id) => ({ id }))
); );
stopBuild(); stopBuild();
} }

View file

@ -204,23 +204,33 @@ export const processDataFromFlow = (flow: FlowType, refreshIds = true) => {
return data; return data;
}; };
export function updateIds(newFlow: ReactFlowJsonObject) { export function updateIds(
{ edges, nodes }: { edges: Edge[]; nodes: Node[] },
selection?: { edges: Edge[]; nodes: Node[] }
) {
let idsMap = {}; let idsMap = {};
const selectionIds = selection?.nodes.map((n) => n.id);
if (newFlow.nodes) if (nodes) {
newFlow.nodes.forEach((node: NodeType) => { nodes.forEach((node: NodeType) => {
// Generate a unique node ID // Generate a unique node ID
let newId = getNodeId( let newId = getNodeId(node.data.type);
node.data.node?.flow ? "GroupNode" : node.data.type if (selection && !selectionIds?.includes(node.id)) {
); newId = node.id;
}
idsMap[node.id] = newId; idsMap[node.id] = newId;
node.id = newId; node.id = newId;
node.data.id = newId; node.data.id = newId;
// Add the new node to the list of nodes in state // Add the new node to the list of nodes in state
}); });
selection?.nodes.forEach((sNode: NodeType) => {
if (newFlow.edges) let newId = idsMap[sNode.id];
newFlow.edges.forEach((edge: Edge) => { sNode.id = newId;
sNode.data.id = newId;
});
}
const concatedEdges = [...edges, ...(selection?.edges ?? [])];
if (concatedEdges)
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(
@ -273,6 +283,8 @@ export function validateNode(node: NodeType, edges: Edge[]): Array<string> {
node: { template }, node: { template },
} = node.data; } = node.data;
const displayName = node.data.node.display_name;
return Object.keys(template).reduce((errors: Array<string>, t) => { return Object.keys(template).reduce((errors: Array<string>, t) => {
if ( if (
template[t].required && template[t].required &&
@ -288,7 +300,9 @@ export function validateNode(node: NodeType, edges: Edge[]): Array<string> {
node.id node.id
) )
) { ) {
errors.push(`${type} is missing ${getFieldTitle(template, t)}.`); errors.push(
`${displayName || type} is missing ${getFieldTitle(template, t)}.`
);
} else if ( } else if (
template[t].type === "dict" && template[t].type === "dict" &&
template[t].required && template[t].required &&
@ -633,8 +647,8 @@ export function generateFlow(
} }
export function reconnectEdges(groupNode: NodeType, excludedEdges: Edge[]) { export function reconnectEdges(groupNode: NodeType, excludedEdges: Edge[]) {
let newEdges = cloneDeep(excludedEdges);
if (!groupNode.data.node!.flow) return []; if (!groupNode.data.node!.flow) return [];
let newEdges = cloneDeep(excludedEdges);
const { nodes, edges } = groupNode.data.node!.flow!.data!; const { nodes, edges } = groupNode.data.node!.flow!.data!;
const lastNode = findLastNode(groupNode.data.node!.flow!.data!); const lastNode = findLastNode(groupNode.data.node!.flow!.data!);
newEdges.forEach((edge) => { newEdges.forEach((edge) => {
@ -951,7 +965,7 @@ export function connectedInputNodesOnHandle(
return connectedNodes; return connectedNodes;
} }
function updateProxyIdsOnTemplate( export function updateProxyIdsOnTemplate(
template: APITemplateType, template: APITemplateType,
idsMap: { [key: string]: string } idsMap: { [key: string]: string }
) { ) {
@ -962,12 +976,16 @@ function updateProxyIdsOnTemplate(
}); });
} }
function updateEdgesIds(edges: Edge[], idsMap: { [key: string]: string }) { export function updateEdgesIds(
edges: Edge[],
idsMap: { [key: string]: string }
) {
edges.forEach((edge) => { edges.forEach((edge) => {
let targetHandle: targetHandleType = edge.data.targetHandle; let targetHandle: targetHandleType = edge.data.targetHandle;
if (targetHandle.proxy && idsMap[targetHandle.proxy!.id]) { if (targetHandle.proxy && idsMap[targetHandle.proxy!.id]) {
targetHandle.proxy!.id = idsMap[targetHandle.proxy!.id]; targetHandle.proxy!.id = idsMap[targetHandle.proxy!.id];
} }
console.log("edge", edge);
edge.data.targetHandle = targetHandle; edge.data.targetHandle = targetHandle;
edge.targetHandle = scapedJSONStringfy(targetHandle); edge.targetHandle = scapedJSONStringfy(targetHandle);
}); });
@ -1255,3 +1273,18 @@ export function isInputType(type: string): boolean {
export function isOutputType(type: string): boolean { export function isOutputType(type: string): boolean {
return OUTPUT_TYPES.has(type); return OUTPUT_TYPES.has(type);
} }
export function updateGroupRecursion(groupNode: NodeType, edges: Edge[]) {
if (groupNode.data.node?.flow) {
groupNode.data.node.flow.data!.nodes.forEach((node) => {
if (node.data.node?.flow) {
updateGroupRecursion(node, node.data.node.flow.data!.edges);
}
});
let newFlow = groupNode.data.node!.flow;
const idsMap = updateIds(newFlow.data!);
updateProxyIdsOnTemplate(groupNode.data.node!.template, idsMap);
let flowEdges = edges;
updateEdgesIds(flowEdges, idsMap);
}
}

View file

@ -705,3 +705,8 @@ export function sortFields(a, b, fieldOrder) {
// You might want to sort them alphabetically or in another specific manner // You might want to sort them alphabetically or in another specific manner
return a.localeCompare(b); return a.localeCompare(b);
} }
export function freezeObject(obj: any) {
if (!obj) return obj;
return JSON.parse(JSON.stringify(obj));
}

View file

@ -30,6 +30,7 @@ if TYPE_CHECKING:
def pytest_configure(): def pytest_configure():
pytest.BASIC_EXAMPLE_PATH = Path(__file__).parent.absolute() / "data" / "basic_example.json" pytest.BASIC_EXAMPLE_PATH = Path(__file__).parent.absolute() / "data" / "basic_example.json"
pytest.COMPLEX_EXAMPLE_PATH = Path(__file__).parent.absolute() / "data" / "complex_example.json" pytest.COMPLEX_EXAMPLE_PATH = Path(__file__).parent.absolute() / "data" / "complex_example.json"
pytest.COMPLEX_DEPS_EXAMPLE_PATH = Path(__file__).parent.absolute() / "data" / "complex_deps_example.json"
pytest.OPENAPI_EXAMPLE_PATH = Path(__file__).parent.absolute() / "data" / "Openapi.json" pytest.OPENAPI_EXAMPLE_PATH = Path(__file__).parent.absolute() / "data" / "Openapi.json"
pytest.GROUPED_CHAT_EXAMPLE_PATH = Path(__file__).parent.absolute() / "data" / "grouped_chat.json" pytest.GROUPED_CHAT_EXAMPLE_PATH = Path(__file__).parent.absolute() / "data" / "grouped_chat.json"
pytest.ONE_GROUPED_CHAT_EXAMPLE_PATH = Path(__file__).parent.absolute() / "data" / "one_group_chat.json" pytest.ONE_GROUPED_CHAT_EXAMPLE_PATH = Path(__file__).parent.absolute() / "data" / "one_group_chat.json"
@ -192,6 +193,16 @@ def json_vector_store():
return f.read() return f.read()
@pytest.fixture
def complex_graph_with_groups():
with open(pytest.COMPLEX_DEPS_EXAMPLE_PATH, "r") as f:
flow_graph = json.load(f)
data_graph = flow_graph["data"]
nodes = data_graph["nodes"]
edges = data_graph["edges"]
return Graph(nodes, edges)
@pytest.fixture(name="client", autouse=True) @pytest.fixture(name="client", autouse=True)
def client_fixture(session: Session, monkeypatch): def client_fixture(session: Session, monkeypatch):
# Set the database url to a test database # Set the database url to a test database

View file

@ -430,3 +430,15 @@ async def test_pickle_each_vertex(json_vector_store):
assert pickled is not None assert pickled is not None
unpickled = pickle.loads(pickled) unpickled = pickle.loads(pickled)
assert unpickled is not None assert unpickled is not None
@pytest.mark.asyncio
async def test_build_ordering(complex_graph_with_groups):
sorted_vertices = complex_graph_with_groups.sort_vertices(stop_component_id="ChatInput-Ay8QQ")
assert sorted_vertices == [
"ChatInput-Ay8QQ",
"RecordsAsText-vkx2A",
"FileLoader-Vo1Cq",
]
sorted_vertices = complex_graph_with_groups.sort_vertices()