merge fix

This commit is contained in:
cristhianzl 2023-10-17 09:38:21 -03:00
commit 27b3b77ab0
78 changed files with 5145 additions and 1866 deletions

View file

@ -56,6 +56,14 @@ LANGFLOW_REMOVE_API_KEYS=
# LANGFLOW_REDIS_CACHE_EXPIRE (default: 3600)
LANGFLOW_CACHE_TYPE=
# Auto login
# If set to true then a superuser will be logged in automatically
# and the login page will be skipped, keeping the
# default experience of Langflow
# Values: true, false
# Example: LANGFLOW_AUTO_LOGIN=true
LANGFLOW_AUTO_LOGIN=
# Superuser username
# Example: LANGFLOW_SUPERUSER=admin
LANGFLOW_SUPERUSER=

View file

@ -12,4 +12,4 @@ WORKDIR $HOME/app
COPY --chown=user . $HOME/app
RUN pip install langflow>==0.0.86 -U --user
CMD ["python", "-m", "langflow", "--host", "0.0.0.0", "--port", "7860"]
CMD ["python", "-m", "langflow", "run", "--host", "0.0.0.0", "--port", "7860"]

View file

@ -22,6 +22,13 @@ tests:
@make install_backend
poetry run pytest tests
tests_frontend:
ifeq ($(UI), true)
cd src/frontend && ./run-tests.sh --ui
else
cd src/frontend && ./run-tests.sh
endif
format:
poetry run black .
poetry run ruff . --fix

View file

@ -7,10 +7,11 @@ import useBaseUrl from "@docusaurus/useBaseUrl";
import ZoomableImage from "/src/theme/ZoomableImage.js";
{" "}
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: "img/new_langflow2.gif",
light: "img/new_langflow.gif",
}}
style={{ width: "100%" }}
/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

1852
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -90,6 +90,7 @@ langfuse = "^1.0.13"
pillow = "^10.0.0"
metal-sdk = "^2.2.0"
markupsafe = "^2.1.3"
numexpr = "^2.8.6"
[tool.poetry.group.dev.dependencies]

View file

@ -187,14 +187,18 @@ async def stream_build(
valid = False
update_build_status(cache_service, flow_id, BuildStatus.FAILURE)
response = {
"valid": valid,
"params": params,
"id": vertex.id,
"progress": round(i / number_of_nodes, 2),
}
vertex_id = (
vertex.parent_node_id if vertex.parent_is_top_level else vertex.id
)
if vertex_id in graph.top_level_nodes:
response = {
"valid": valid,
"params": params,
"id": vertex_id,
"progress": round(i / number_of_nodes, 2),
}
yield str(StreamData(event="message", data=response))
yield str(StreamData(event="message", data=response))
langchain_object = graph.build()
# Now we need to check the input_keys to send them to the client

View file

@ -69,7 +69,7 @@ def get_all(
"/process/{flow_id}",
response_model=ProcessResponse,
)
async def process_flow(
async def process(
session: Annotated[Session, Depends(get_session)],
flow_id: str,
inputs: Optional[dict] = None,

View file

@ -1,6 +1,6 @@
from typing import Optional
from langflow import CustomComponent
from langchain.llms import HuggingFaceEndpoint
from langchain.llms.huggingface_endpoint import HuggingFaceEndpoint
from langchain.llms.base import BaseLLM
@ -13,7 +13,6 @@ class HuggingFaceEndpointsComponent(CustomComponent):
"endpoint_url": {"display_name": "Endpoint URL", "password": True},
"task": {
"display_name": "Task",
"type": "select",
"options": ["text2text-generation", "text-generation", "summarization"],
},
"huggingfacehub_api_token": {"display_name": "API token", "password": True},
@ -27,7 +26,7 @@ class HuggingFaceEndpointsComponent(CustomComponent):
def build(
self,
endpoint_url: str,
task="text2text-generation",
task: str = "text2text-generation",
huggingfacehub_api_token: Optional[str] = None,
model_kwargs: Optional[dict] = None,
) -> BaseLLM:
@ -36,6 +35,7 @@ class HuggingFaceEndpointsComponent(CustomComponent):
endpoint_url=endpoint_url,
task=task,
huggingfacehub_api_token=huggingfacehub_api_token,
model_kwargs=model_kwargs,
)
except Exception as e:
raise ValueError("Could not connect to HuggingFace Endpoints API.") from e

View file

@ -5,7 +5,6 @@ from langchain.vectorstores import Vectara
from langchain.schema import Document
from langchain.vectorstores.base import VectorStore
from langchain.schema import BaseRetriever
from langchain.embeddings.base import Embeddings
class VectaraComponent(CustomComponent):
@ -22,7 +21,6 @@ class VectaraComponent(CustomComponent):
"vectara_api_key": {"display_name": "Vectara API Key", "password": True},
"code": {"show": False},
"documents": {"display_name": "Documents"},
"embedding": {"display_name": "Embedding"},
}
def build(
@ -30,21 +28,21 @@ class VectaraComponent(CustomComponent):
vectara_customer_id: str,
vectara_corpus_id: str,
vectara_api_key: str,
embedding: Optional[Embeddings] = None,
documents: Optional[Document] = None,
) -> Union[VectorStore, BaseRetriever]:
# If documents, then we need to create a Vectara instance using .from_documents
if documents is not None and embedding is not None:
if documents is not None:
return Vectara.from_documents(
documents=documents, # type: ignore
vectara_customer_id=vectara_customer_id,
vectara_corpus_id=vectara_corpus_id,
vectara_api_key=vectara_api_key,
embedding=embedding,
source="langflow",
)
return Vectara(
vectara_customer_id=vectara_customer_id,
vectara_corpus_id=vectara_corpus_id,
vectara_api_key=vectara_api_key,
source="langflow",
)

View file

@ -1,3 +1,53 @@
from .base import NestedDict
# LANGCHAIN_BASE_TYPES = {
# "Chain": Chain,
# "AgentExecutor": AgentExecutor,
# "Tool": Tool,
# "BaseLLM": BaseLLM,
# "PromptTemplate": PromptTemplate,
# "BaseLoader": BaseLoader,
# "Document": Document,
# "TextSplitter": TextSplitter,
# "VectorStore": VectorStore,
# "Embeddings": Embeddings,
# "BaseRetriever": BaseRetriever,
# "BaseOutputParser": BaseOutputParser,
# "BaseMemory": BaseMemory,
# "BaseChatMemory": BaseChatMemory,
# }
from .constants import (
Tool,
PromptTemplate,
Chain,
BaseChatMemory,
BaseLLM,
BaseLoader,
BaseMemory,
BaseOutputParser,
BaseRetriever,
VectorStore,
Embeddings,
TextSplitter,
Document,
AgentExecutor,
NestedDict,
Data,
)
__all__ = ["NestedDict"]
__all__ = [
"NestedDict",
"Data",
"Tool",
"PromptTemplate",
"Chain",
"BaseChatMemory",
"BaseLLM",
"BaseLoader",
"BaseMemory",
"BaseOutputParser",
"BaseRetriever",
"VectorStore",
"Embeddings",
"TextSplitter",
"Document",
"AgentExecutor",
]

View file

@ -1,4 +0,0 @@
from typing import Union, Dict
# Type alias for more complex dicts
NestedDict = Dict[str, Union[str, Dict]]

View file

@ -0,0 +1,50 @@
from langchain.agents.agent import AgentExecutor
from langchain.chains.base import Chain
from langchain.document_loaders.base import BaseLoader
from langchain.llms.base import BaseLLM
from langchain.memory.chat_memory import BaseChatMemory
from langchain.prompts import PromptTemplate
from langchain.schema import BaseOutputParser, BaseRetriever, Document
from langchain.schema.embeddings import Embeddings
from langchain.schema.memory import BaseMemory
from langchain.text_splitter import TextSplitter
from langchain.tools import Tool
from langchain.vectorstores.base import VectorStore
from typing import Union, Dict
# Type alias for more complex dicts
NestedDict = Dict[str, Union[str, Dict]]
class Data:
pass
LANGCHAIN_BASE_TYPES = {
"Chain": Chain,
"AgentExecutor": AgentExecutor,
"Tool": Tool,
"BaseLLM": BaseLLM,
"PromptTemplate": PromptTemplate,
"BaseLoader": BaseLoader,
"Document": Document,
"TextSplitter": TextSplitter,
"VectorStore": VectorStore,
"Embeddings": Embeddings,
"BaseRetriever": BaseRetriever,
"BaseOutputParser": BaseOutputParser,
"BaseMemory": BaseMemory,
"BaseChatMemory": BaseChatMemory,
}
# Langchain base types plus Python base types
CUSTOM_COMPONENT_SUPPORTED_TYPES = {
**LANGCHAIN_BASE_TYPES,
"str": str,
"int": int,
"float": float,
"bool": bool,
"list": list,
"dict": dict,
"NestedDict": NestedDict,
"Data": Data,
}

View file

@ -1,28 +1,79 @@
from loguru import logger
from typing import TYPE_CHECKING
from pydantic import BaseModel, Field
from typing import List, Optional
if TYPE_CHECKING:
from langflow.graph.vertex.base import Vertex
class SourceHandle(BaseModel):
baseClasses: List[str] = Field(
..., description="List of base classes for the source handle."
)
dataType: str = Field(..., description="Data type for the source handle.")
id: str = Field(..., description="Unique identifier for the source handle.")
class TargetHandle(BaseModel):
fieldName: str = Field(..., description="Field name for the target handle.")
id: str = Field(..., description="Unique identifier for the target handle.")
inputTypes: Optional[List[str]] = Field(
None, description="List of input types for the target handle."
)
type: str = Field(..., description="Type of the target handle.")
class Edge:
def __init__(self, source: "Vertex", target: "Vertex", edge: dict):
self.source: "Vertex" = source
self.target: "Vertex" = target
self.source_handle = edge.get("sourceHandle", "")
self.target_handle = edge.get("targetHandle", "")
# 'BaseLoader;BaseOutputParser|documents|PromptTemplate-zmTlD'
# target_param is documents
self.target_param = self.target_handle.split("|")[1]
if data := edge.get("data", {}):
self._source_handle = data.get("sourceHandle", {})
self._target_handle = data.get("targetHandle", {})
self.source_handle: SourceHandle = SourceHandle(**self._source_handle)
self.target_handle: TargetHandle = TargetHandle(**self._target_handle)
self.target_param = self.target_handle.fieldName
# validate handles
self.validate_handles()
else:
# Logging here because this is a breaking change
logger.error("Edge data is empty")
self._source_handle = edge.get("sourceHandle", "")
self._target_handle = edge.get("targetHandle", "")
# 'BaseLoader;BaseOutputParser|documents|PromptTemplate-zmTlD'
# target_param is documents
self.target_param = self._target_handle.split("|")[1]
# Validate in __init__ to fail fast
self.validate_edge()
def validate_handles(self) -> None:
if self.target_handle.inputTypes is None:
self.valid_handles = (
self.target_handle.type in self.source_handle.baseClasses
)
else:
self.valid_handles = (
any(
baseClass in self.target_handle.inputTypes
for baseClass in self.source_handle.baseClasses
)
or self.target_handle.type in self.source_handle.baseClasses
)
if not self.valid_handles:
logger.debug(self.source_handle)
logger.debug(self.target_handle)
raise ValueError(
f"Edge between {self.source.vertex_type} and {self.target.vertex_type} "
f"has invalid handles"
)
def __setstate__(self, state):
self.source = state["source"]
self.target = state["target"]
self.target_param = state["target_param"]
self.source_handle = state["source_handle"]
self.target_handle = state["target_handle"]
self.source_handle = state.get("source_handle")
self.target_handle = state.get("target_handle")
def reset(self) -> None:
self.source._build_params()

View file

@ -2,6 +2,7 @@ from typing import Dict, Generator, List, Type, Union
from langflow.graph.edge.base import Edge
from langflow.graph.graph.constants import lazy_load_vertex_dict
from langflow.graph.graph.utils import process_flow
from langflow.graph.vertex.base import Vertex
from langflow.graph.vertex.types import (
FileToolVertex,
@ -19,11 +20,21 @@ class Graph:
def __init__(
self,
nodes: List[Dict[str, Union[str, Dict[str, Union[str, List[str]]]]]],
nodes: List[Dict],
edges: List[Dict[str, str]],
) -> None:
self._nodes = nodes
self._edges = edges
self.raw_graph_data = {"nodes": nodes, "edges": edges}
self.top_level_nodes = []
for node in self._nodes:
if node_id := node.get("id"):
self.top_level_nodes.append(node_id)
self._graph_data = process_flow(self.raw_graph_data)
self._nodes = self._graph_data["nodes"]
self._edges = self._graph_data["edges"]
self._build_graph()
def __setstate__(self, state):
@ -50,6 +61,7 @@ class Graph:
edges = payload["edges"]
return cls(nodes, edges)
except KeyError as exc:
logger.exception(exc)
raise ValueError(
f"Invalid payload. Expected keys 'nodes' and 'edges'. Found {list(payload.keys())}"
) from exc
@ -215,7 +227,9 @@ class Graph:
node_lc_type: str = node_data["node"]["template"]["_type"] # type: ignore
VertexClass = self._get_vertex_class(node_type, node_lc_type)
nodes.append(VertexClass(node))
vertex = VertexClass(node)
vertex.set_top_level(self.top_level_nodes)
nodes.append(vertex)
return nodes

View file

@ -0,0 +1,230 @@
from collections import deque
import copy
def find_last_node(nodes, edges):
"""
This function receives a flow and returns the last node.
"""
return next((n for n in nodes if all(e["source"] != n["id"] for e in edges)), None)
def add_parent_node_id(nodes, parent_node_id):
"""
This function receives a list of nodes and adds a parent_node_id to each node.
"""
for node in nodes:
node["parent_node_id"] = parent_node_id
def ungroup_node(group_node_data, base_flow):
template, flow = (
group_node_data["node"]["template"],
group_node_data["node"]["flow"],
)
parent_node_id = group_node_data["id"]
g_nodes = flow["data"]["nodes"]
add_parent_node_id(g_nodes, parent_node_id)
g_edges = flow["data"]["edges"]
# Redirect edges to the correct proxy node
updated_edges = get_updated_edges(
base_flow, g_nodes, g_edges, group_node_data["id"]
)
# Update template values
update_template(template, g_nodes)
nodes = [
n for n in base_flow["nodes"] if n["id"] != group_node_data["id"]
] + g_nodes
edges = (
[
e
for e in base_flow["edges"]
if e["target"] != group_node_data["id"]
and e["source"] != group_node_data["id"]
]
+ g_edges
+ updated_edges
)
base_flow["nodes"] = nodes
base_flow["edges"] = edges
return nodes
def process_flow(flow_object):
cloned_flow = copy.deepcopy(flow_object)
processed_nodes = set() # To keep track of processed nodes
def process_node(node):
node_id = node.get("id")
# If node already processed, skip
if node_id in processed_nodes:
return
if (
node.get("data")
and node["data"].get("node")
and node["data"]["node"].get("flow")
):
process_flow(node["data"]["node"]["flow"]["data"])
new_nodes = ungroup_node(node["data"], cloned_flow)
# Add new nodes to the queue for future processing
nodes_to_process.extend(new_nodes)
# Mark node as processed
processed_nodes.add(node_id)
nodes_to_process = deque(cloned_flow["nodes"])
while nodes_to_process:
node = nodes_to_process.popleft()
process_node(node)
return cloned_flow
def update_template(template, g_nodes):
"""
Updates the template of a node in a graph with the given template.
Args:
template (dict): The new template to update the node with.
g_nodes (list): The list of nodes in the graph.
Returns:
None
"""
for _, value in template.items():
if not value.get("proxy"):
continue
proxy_dict = value["proxy"]
field, id_ = proxy_dict["field"], proxy_dict["id"]
node_index = next((i for i, n in enumerate(g_nodes) if n["id"] == id_), -1)
if node_index != -1:
display_name = None
show = g_nodes[node_index]["data"]["node"]["template"][field]["show"]
advanced = g_nodes[node_index]["data"]["node"]["template"][field][
"advanced"
]
if "display_name" in g_nodes[node_index]["data"]["node"]["template"][field]:
display_name = g_nodes[node_index]["data"]["node"]["template"][field][
"display_name"
]
else:
display_name = g_nodes[node_index]["data"]["node"]["template"][field][
"name"
]
g_nodes[node_index]["data"]["node"]["template"][field] = value
g_nodes[node_index]["data"]["node"]["template"][field]["show"] = show
g_nodes[node_index]["data"]["node"]["template"][field][
"advanced"
] = advanced
g_nodes[node_index]["data"]["node"]["template"][field][
"display_name"
] = display_name
def update_target_handle(new_edge, g_nodes, group_node_id):
"""
Updates the target handle of a given edge if it is a proxy node.
Args:
new_edge (dict): The edge to update.
g_nodes (list): The list of nodes in the graph.
group_node_id (str): The ID of the group node.
Returns:
dict: The updated edge.
"""
target_handle = new_edge["data"]["targetHandle"]
if target_handle.get("proxy"):
proxy_id = target_handle["proxy"]["id"]
if node := next((n for n in g_nodes if n["id"] == proxy_id), None):
set_new_target_handle(proxy_id, new_edge, target_handle, node)
return new_edge
def set_new_target_handle(proxy_id, new_edge, target_handle, node):
"""
Sets a new target handle for a given edge.
Args:
proxy_id (str): The ID of the proxy.
new_edge (dict): The new edge to be created.
target_handle (dict): The target handle of the edge.
node (dict): The node containing the edge.
Returns:
None
"""
new_edge["target"] = proxy_id
_type = target_handle.get("type")
if _type is None:
raise KeyError("The 'type' key must be present in target_handle.")
field = target_handle["proxy"]["field"]
new_target_handle = {
"fieldName": field,
"type": _type,
"id": proxy_id,
}
if node["data"]["node"].get("flow"):
new_target_handle["proxy"] = {
"field": node["data"]["node"]["template"][field]["proxy"]["field"],
"id": node["data"]["node"]["template"][field]["proxy"]["id"],
}
if input_types := target_handle.get("inputTypes"):
new_target_handle["inputTypes"] = input_types
new_edge["data"]["targetHandle"] = new_target_handle
def update_source_handle(new_edge, g_nodes, g_edges):
"""
Updates the source handle of a given edge to the last node in the flow data.
Args:
new_edge (dict): The edge to update.
flow_data (dict): The flow data containing the nodes and edges.
Returns:
dict: The updated edge with the new source handle.
"""
last_node = copy.deepcopy(find_last_node(g_nodes, g_edges))
new_edge["source"] = last_node["id"]
new_source_handle = new_edge["data"]["sourceHandle"]
new_source_handle["id"] = last_node["id"]
new_edge["data"]["sourceHandle"] = new_source_handle
return new_edge
def get_updated_edges(base_flow, g_nodes, g_edges, group_node_id):
"""
Given a base flow, a list of graph nodes and a group node id, returns a list of updated edges.
An updated edge is an edge that has its target or source handle updated based on the group node id.
Args:
base_flow (dict): The base flow containing a list of edges.
g_nodes (list): A list of graph nodes.
group_node_id (str): The id of the group node.
Returns:
list: A list of updated edges.
"""
updated_edges = []
for edge in base_flow["edges"]:
new_edge = copy.deepcopy(edge)
if new_edge["target"] == group_node_id:
new_edge = update_target_handle(new_edge, g_nodes, group_node_id)
if new_edge["source"] == group_node_id:
new_edge = update_source_handle(new_edge, g_nodes, g_edges)
if edge["target"] == group_node_id or edge["source"] == group_node_id:
updated_edges.append(new_edge)
return updated_edges

View file

@ -38,6 +38,8 @@ class Vertex:
self.task_id: Optional[str] = None
self.is_task = is_task
self.params = params or {}
self.parent_node_id: Optional[str] = self._data.get("parent_node_id")
self.parent_is_top_level = False
def reset_params(self):
for edge in self.edges:
@ -88,6 +90,11 @@ class Vertex:
self._built = False
self.artifacts: Dict[str, Any] = {}
self.task_id: Optional[str] = None
self.parent_node_id = state["parent_node_id"]
self.parent_is_top_level = state["parent_is_top_level"]
def set_top_level(self, top_level_nodes: List[str]) -> None:
self.parent_is_top_level = self.parent_node_id in top_level_nodes
def _parse_data(self) -> None:
self.data = self._data["data"]
@ -209,6 +216,16 @@ class Vertex:
}
elif isinstance(_value, dict):
params[key] = _value
elif value.get("type") == "int" and value.get("value") is not None:
try:
params[key] = int(value.get("value"))
except ValueError:
params[key] = value.get("value")
elif value.get("type") == "float" and value.get("value") is not None:
try:
params[key] = float(value.get("value"))
except ValueError:
params[key] = value.get("value")
else:
params[key] = value.get("value")

View file

@ -1,65 +1,33 @@
from langchain.prompts import PromptTemplate
from langchain.chains.base import Chain
from langchain.document_loaders.base import BaseLoader
from langchain.schema.embeddings import Embeddings
from langchain.llms.base import BaseLLM
from langchain.schema import BaseRetriever, Document
from langchain.text_splitter import TextSplitter
from langchain.tools import Tool
from langchain.vectorstores.base import VectorStore
from langchain.schema import BaseOutputParser
from langchain.schema.memory import BaseMemory
from langchain.memory.chat_memory import BaseChatMemory
from langchain.agents.agent import AgentExecutor
LANGCHAIN_BASE_TYPES = {
"Chain": Chain,
"AgentExecutor": AgentExecutor,
"Tool": Tool,
"BaseLLM": BaseLLM,
"PromptTemplate": PromptTemplate,
"BaseLoader": BaseLoader,
"Document": Document,
"TextSplitter": TextSplitter,
"VectorStore": VectorStore,
"Embeddings": Embeddings,
"BaseRetriever": BaseRetriever,
"BaseOutputParser": BaseOutputParser,
"BaseMemory": BaseMemory,
"BaseChatMemory": BaseChatMemory,
}
# Langchain base types plus Python base types
CUSTOM_COMPONENT_SUPPORTED_TYPES = {
**LANGCHAIN_BASE_TYPES,
"str": str,
"int": int,
"float": float,
"bool": bool,
"list": list,
"dict": dict,
}
DEFAULT_CUSTOM_COMPONENT_CODE = """from langflow import CustomComponent
from langchain.llms.base import BaseLLM
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain.schema import Document
from langflow.field_typing import (
Tool,
PromptTemplate,
Chain,
BaseChatMemory,
BaseLLM,
BaseLoader,
BaseMemory,
BaseOutputParser,
BaseRetriever,
VectorStore,
Embeddings,
TextSplitter,
Document,
AgentExecutor,
NestedDict,
Data,
)
import requests
class YourComponent(CustomComponent):
class Component(CustomComponent):
display_name: str = "Custom Component"
description: str = "Create any custom component you want!"
def build_config(self):
return { "url": { "multiline": True, "required": True } }
return {"param": {"display_name": "Parameter"}}
def build(self, param: Data) -> Data:
return param
def build(self, url: str, llm: BaseLLM, prompt: PromptTemplate) -> Document:
response = requests.get(url)
chain = LLMChain(llm=llm, prompt=prompt)
result = chain.run(response.text[:300])
return Document(page_content=str(result))
"""

View file

@ -1,7 +1,7 @@
from typing import Any, Callable, List, Optional, Union
from uuid import UUID
from fastapi import HTTPException
from langflow.interface.custom.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES
from langflow.field_typing.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES
from langflow.interface.custom.component import Component
from langflow.interface.custom.directory_reader import DirectoryReader
from langflow.services.getters import get_db_service
@ -108,6 +108,9 @@ class CustomComponent(Component, extra=Extra.allow):
),
},
)
elif not arg.get("type"):
# Set the type to Data
arg["type"] = "Data"
return args
@property

View file

@ -4,7 +4,7 @@ from typing import Any, List
from langflow.api.utils import get_new_key
from langflow.interface.agents.base import agent_creator
from langflow.interface.chains.base import chain_creator
from langflow.interface.custom.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES
from langflow.field_typing.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES
from langflow.interface.custom.utils import extract_inner_type
from langflow.interface.document_loaders.base import documentloader_creator
from langflow.interface.embeddings.base import embedding_creator
@ -288,6 +288,24 @@ def add_base_classes(frontend_node, return_types: List[str]):
frontend_node.get("base_classes").append(base_class)
def add_output_types(frontend_node, return_types: List[str]):
"""Add output types to the frontend node"""
for return_type in return_types:
if return_type not in CUSTOM_COMPONENT_SUPPORTED_TYPES or return_type is None:
raise HTTPException(
status_code=400,
detail={
"error": (
"Invalid return type should be one of: "
f"{list(CUSTOM_COMPONENT_SUPPORTED_TYPES.keys())}"
),
"traceback": traceback.format_exc(),
},
)
frontend_node.get("output_types").append(return_type)
def build_langchain_template_custom_component(custom_component: CustomComponent):
"""Build a custom component template for the langchain"""
try:
@ -314,6 +332,9 @@ def build_langchain_template_custom_component(custom_component: CustomComponent)
add_base_classes(
frontend_node, custom_component.get_function_entrypoint_return_type
)
add_output_types(
frontend_node, custom_component.get_function_entrypoint_return_type
)
logger.debug("Added base classes")
return frontend_node
except Exception as exc:

View file

@ -34,7 +34,9 @@ def get_langfuse_callback(trace_id):
if langfuse := LangfuseInstance.get():
logger.debug("Langfuse credentials found")
try:
trace = langfuse.trace(CreateTrace(id=trace_id))
trace = langfuse.trace(
CreateTrace(name="langflow-" + trace_id, id=trace_id)
)
return trace.getNewHandler()
except Exception as exc:
logger.error(f"Error initializing langfuse callback: {exc}")

View file

@ -164,7 +164,7 @@ class FrontendNode(BaseModel):
) -> None:
"""Handles specific field values for certain fields."""
if key == "headers":
field.value = """{'Authorization': 'Bearer <token>'}"""
field.value = """{"Authorization": "Bearer <token>"}"""
FrontendNode._handle_model_specific_field_values(field, key, name)
FrontendNode._handle_api_key_specific_field_values(field, key, name)
@ -249,4 +249,4 @@ class FrontendNode(BaseModel):
if "default" in value:
field.value = value["default"]
if key == "headers":
field.value = """{'Authorization': 'Bearer <token>'}"""
field.value = """{"Authorization": "Bearer <token>"}"""

View file

@ -2,6 +2,7 @@ from langflow.template.field.base import TemplateField
from langflow.template.frontend_node.base import FrontendNode
from langflow.template.template.base import Template
from langflow.interface.custom.constants import DEFAULT_CUSTOM_COMPONENT_CODE
from typing import Optional
class CustomComponentFrontendNode(FrontendNode):
@ -24,7 +25,7 @@ class CustomComponentFrontendNode(FrontendNode):
)
],
)
description: str = "Create any custom component you want!"
description: Optional[str] = None
base_classes: list[str] = []
def to_dict(self):

View file

@ -145,7 +145,7 @@ class HeadersDefaultValueFormatter(FieldFormatter):
def format(self, field: TemplateField, name: Optional[str] = None) -> None:
key = field.name
if key == "headers":
field.value = """{'Authorization': 'Bearer <token>'}"""
field.value = """{"Authorization": "Bearer <token>"}"""
class DictCodeFileFormatter(FieldFormatter):

View file

@ -44,6 +44,7 @@ class PromptFrontendNode(FrontendNode):
# All prompt fields should be password=False
field.password = False
field.dynamic = True
class PromptTemplateNode(FrontendNode):

View file

@ -191,7 +191,9 @@ def get_base_classes(cls):
"""Get the base classes of a class.
These are used to determine the output of the nodes.
"""
if bases := cls.__bases__:
if hasattr(cls, "__bases__") and cls.__bases__:
bases = cls.__bases__
result = []
for base in bases:
if any(type in base.__module__ for type in ["pydantic", "abc"]):
@ -431,7 +433,7 @@ def set_headers_value(value: Dict[str, Any]) -> None:
"""
Sets the value for the 'headers' key.
"""
value["value"] = """{'Authorization': 'Bearer <token>'}"""
value["value"] = """{"Authorization": "Bearer <token>"}"""
def add_options_to_field(

View file

@ -22,5 +22,5 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
/test-results/
/playwright-report/
/playwright-report/*/
/playwright/.cache/

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul>
<li>
<a href="./e2e/index.html">e2e report</a>
</li>
<li>
<a href="./onlyFront/index.html">frontEnd Only report</a>
</li>
</ul>
</body>
</html>

View file

@ -20,11 +20,13 @@ export default defineConfig({
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
reporter: [
["html", { open: "never", outputFolder: "playwright-report/test-results" }],
],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
// baseURL: "http://127.0.0.1:3000",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
@ -69,9 +71,16 @@ export default defineConfig({
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
// webServer: [
// {
// command: "npm run backend",
// reuseExistingServer: !process.env.CI,
// timeout: 120 * 1000,
// },
// {
// command: "npm run start",
// url: "http://127.0.0.1:3000",
// reuseExistingServer: !process.env.CI,
// },
// ],
});

75
src/frontend/run-tests.sh Executable file
View file

@ -0,0 +1,75 @@
#!/bin/bash
# Default value for the --ui flag
ui=false
# Parse command-line arguments
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
--ui)
ui=true
shift
;;
*)
echo "Unknown option: $key"
exit 1
;;
esac
shift
done
# Function to forcibly terminate a process by port
terminate_process_by_port() {
port="$1"
echo "Terminating process on port: $port"
fuser -k -n tcp "$port" # Forcefully terminate processes using the specified port
echo "Process terminated."
}
# Trap signals to ensure cleanup on script termination
trap 'terminate_process_by_port 7860; terminate_process_by_port 3000' EXIT
# Navigate to the project root directory (where the Makefile is located)
cd ../../
# Start the frontend using 'make frontend' in the background
make frontend &
# Give some time for the frontend to start (adjust sleep duration as needed)
sleep 10
# Navigate to the test directory
cd src/frontend
# Run frontend only Playwright tests with or without UI based on the --ui flag
if [ "$ui" = true ]; then
PLAYWRIGHT_HTML_REPORT=playwright-report/onlyFront npx playwright test tests/onlyFront --ui --project=chromium
else
PLAYWRIGHT_HTML_REPORT=playwright-report/onlyFront npx playwright test tests/onlyFront --project=chromium
fi
# Navigate back to the project root directory
cd ../../
# Start the backend using 'make backend' in the background
make backend &
# Give some time for the backend to start (adjust sleep duration as needed)
sleep 10
# Navigate back to the test directory
cd src/frontend
# Run Playwright tests with or without UI based on the --ui flag
if [ "$ui" = true ]; then
PLAYWRIGHT_HTML_REPORT=playwright-report/e2e npx playwright test tests/end-to-end --ui --project=chromium
else
PLAYWRIGHT_HTML_REPORT=playwright-report/e2e npx playwright test tests/end-to-end --project=chromium
fi
npx playwright show-report
# After the tests are finished, you can add cleanup or teardown logic here if needed
# The trap will automatically terminate processes by port on script exit

View file

@ -32,6 +32,7 @@ import {
convertValuesToNumbers,
hasDuplicateKeys,
isValidConnection,
scapedJSONStringfy,
} from "../../../../utils/reactflowUtils";
import {
nodeColors,
@ -53,6 +54,7 @@ export default function ParameterComponent({
required = false,
optionalHandle = null,
info = "",
proxy,
showNode,
index = "",
}: ParameterComponentType): JSX.Element {
@ -81,8 +83,9 @@ export default function ParameterComponent({
const { reactFlowInstance, setFilterEdge } = useContext(typesContext);
let disabled =
reactFlowInstance?.getEdges().some((edge) => edge.targetHandle === id) ??
false;
reactFlowInstance
?.getEdges()
.some((edge) => edge.targetHandle === scapedJSONStringfy(id)) ?? false;
const { data: myData } = useContext(typesContext);
@ -113,7 +116,6 @@ export default function ParameterComponent({
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
useEffect(() => {
if (name === "openai_api_base") console.log(info);
// @ts-ignore
infoHtml.current = (
<div className="h-full w-full break-words">
@ -205,33 +207,46 @@ export default function ParameterComponent({
type === "code" ||
type === "prompt" ||
type === "file" ||
type === "int") &&
type === "int" ||
type === "dict" ||
type === "NestedDict") &&
!optionalHandle ? (
<></>
) : (
<ShadTooltip
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
delayDuration={0}
content={refHtml.current}
side={left ? "left" : "right"}
>
<Handle
type={left ? "target" : "source"}
position={left ? Position.Left : Position.Right}
id={id}
isValidConnection={(connection) =>
isValidConnection(connection, reactFlowInstance!)
}
className={classNames(
left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ",
"h-3 w-3 rounded-full border-2 bg-background"
)}
style={{
borderColor: color,
top: position,
}}
></Handle>
</ShadTooltip>
<Button className="h-7 truncate bg-muted p-0 text-sm font-normal text-black hover:bg-muted">
<div className="flex">
<ShadTooltip
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
delayDuration={0}
content={refHtml.current}
side={left ? "left" : "right"}
>
<Handle
type={left ? "target" : "source"}
position={left ? Position.Left : Position.Right}
id={
proxy
? scapedJSONStringfy({ ...id, proxy })
: scapedJSONStringfy(id)
}
isValidConnection={(connection) =>
isValidConnection(connection, reactFlowInstance!)
}
className={classNames(
left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ",
"h-3 w-3 rounded-full border-2 bg-background"
)}
style={{
borderColor: color,
top: position,
}}
onClick={() => {
setFilterEdge(groupedEdge.current);
}}
></Handle>
</ShadTooltip>
</div>
</Button>
)
) : (
<div
@ -246,7 +261,13 @@ export default function ParameterComponent({
(info !== "" ? " flex items-center" : "")
}
>
{title}
{proxy ? (
<ShadTooltip content={<span>{proxy.id}</span>}>
<span>{title}</span>
</ShadTooltip>
) : (
title
)}
<span className="text-status-red">{required ? " *" : ""}</span>
<div className="">
{info !== "" && (
@ -286,7 +307,11 @@ export default function ParameterComponent({
<Handle
type={left ? "target" : "source"}
position={left ? Position.Left : Position.Right}
id={id}
id={
proxy
? scapedJSONStringfy({ ...id, proxy })
: scapedJSONStringfy(id)
}
isValidConnection={(connection) =>
isValidConnection(connection, reactFlowInstance!)
}
@ -372,6 +397,11 @@ export default function ParameterComponent({
) : left === true && type === "code" ? (
<div className="mt-2 w-full">
<CodeAreaComponent
readonly={
data.node?.flow && data.node.template[name].dynamic
? true
: false
}
dynamic={data.node?.template[name].dynamic ?? false}
setNodeClass={(nodeClass) => {
data.node = nodeClass;
@ -408,6 +438,11 @@ export default function ParameterComponent({
) : left === true && type === "prompt" ? (
<div className="mt-2 w-full">
<PromptAreaComponent
readonly={
data.node?.flow && data.node.template[name].dynamic
? true
: false
}
field_name={name}
setNodeClass={(nodeClass) => {
data.node = nodeClass;

View file

@ -4,23 +4,33 @@ import { NodeToolbar, useUpdateNodeInternals } from "reactflow";
import ShadTooltip from "../../components/ShadTooltipComponent";
import Tooltip from "../../components/TooltipComponent";
import IconComponent from "../../components/genericIconComponent";
import InputComponent from "../../components/inputComponent";
import { Textarea } from "../../components/ui/textarea";
import { useSSE } from "../../contexts/SSEContext";
import { TabsContext } from "../../contexts/tabsContext";
import { typesContext } from "../../contexts/typesContext";
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
import { validationStatusType } from "../../types/components";
import { NodeDataType } from "../../types/flow";
import { cleanEdges } from "../../utils/reactflowUtils";
import {
cleanEdges,
handleKeyDown,
scapedJSONStringfy,
} from "../../utils/reactflowUtils";
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
import { classNames, toTitleCase } from "../../utils/utils";
import ParameterComponent from "./components/parameterComponent";
export default function GenericNode({
data: olddata,
xPos,
yPos,
selected,
}: {
data: NodeDataType;
selected: boolean;
xPos: number;
yPos: number;
}): JSX.Element {
const [data, setData] = useState(olddata);
const { updateFlow, flows, tabId } = useContext(TabsContext);
@ -28,6 +38,12 @@ export default function GenericNode({
const { types, deleteNode, reactFlowInstance, setFilterEdge, getFilterEdge } =
useContext(typesContext);
const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
const [inputName, setInputName] = useState(true);
const [nodeName, setNodeName] = useState(data.node!.display_name);
const [inputDescription, setInputDescription] = useState(false);
const [nodeDescription, setNodeDescription] = useState(
data.node?.description!
);
const [validationStatus, setValidationStatus] =
useState<validationStatusType | null>(null);
const [showNode, setShowNode] = useState<boolean>(true);
@ -111,6 +127,7 @@ export default function GenericNode({
<>
<NodeToolbar>
<NodeToolbarComponent
position={{ x: xPos, y: yPos }}
data={data}
setData={setData}
deleteNode={deleteNode}
@ -151,7 +168,7 @@ export default function GenericNode({
}
>
<IconComponent
name={name}
name={data.node?.flow ? "Ungroup" : name}
className={
"generic-node-icon " +
(!showNode && "absolute inset-x-6 h-12 w-12")
@ -160,11 +177,35 @@ export default function GenericNode({
/>
{showNode && (
<div className="generic-node-tooltip-div">
<ShadTooltip content={data.node?.display_name}>
<div className="generic-node-tooltip-div text-primary">
{data.node?.display_name}
{data.node?.flow && inputName ? (
<div>
<InputComponent
autoFocus
onBlur={() => {
setInputName(false);
if (nodeName.trim() !== "") {
setNodeName(nodeName);
data.node!.display_name = nodeName;
} else {
setNodeName(data.node!.display_name);
}
}}
value={nodeName}
onChange={setNodeName}
password={false}
blurOnEnter={true}
/>
</div>
</ShadTooltip>
) : (
<ShadTooltip content={data.node?.display_name}>
<div
className="generic-node-tooltip-div text-primary"
onDoubleClick={() => setInputName(true)}
>
{data.node?.display_name}
</div>
</ShadTooltip>
)}
</div>
)}
</div>
@ -179,16 +220,14 @@ export default function GenericNode({
!data.node!.template[templateField].advanced && (
<ParameterComponent
index={idx.toString()}
key={
(data.node!.template[
templateField
].input_types?.join(";") ??
data.node!.template[templateField].type) +
"|" +
templateField +
"|" +
data.id
}
key={scapedJSONStringfy({
inputTypes:
data.node!.template[templateField].input_types,
type: data.node!.template[templateField].type,
id: data.id,
fieldName: templateField,
proxy: data.node!.template[templateField].proxy,
})}
data={data}
setData={setData}
color={
@ -218,31 +257,31 @@ export default function GenericNode({
data.node?.template[templateField].type
}
required={
data.node?.template[templateField].required
}
id={
(data.node?.template[
templateField
].input_types?.join(";") ??
data.node?.template[templateField].type) +
"|" +
templateField +
"|" +
data.id
data.node!.template[templateField].required
}
id={{
inputTypes:
data.node!.template[templateField].input_types,
type: data.node!.template[templateField].type,
id: data.id,
fieldName: templateField,
}}
left={true}
type={data.node?.template[templateField].type}
optionalHandle={
data.node?.template[templateField].input_types
}
proxy={data.node?.template[templateField].proxy}
showNode={showNode}
/>
)
)}
<ParameterComponent
key={[data.type, data.id, ...data.node!.base_classes].join(
"|"
)}
key={scapedJSONStringfy({
baseClasses: data.node!.base_classes,
id: data.id,
dataType: data.type,
})}
data={data}
setData={setData}
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
@ -253,9 +292,11 @@ export default function GenericNode({
: data.type
}
tooltipTitle={data.node?.base_classes.join("\n")}
id={[data.type, data.id, ...data.node!.base_classes].join(
"|"
)}
id={{
baseClasses: data.node!.base_classes,
id: data.id,
dataType: data.type,
}}
type={data.node?.base_classes.join("|")}
left={false}
showNode={showNode}
@ -331,16 +372,53 @@ export default function GenericNode({
className={
showNode
? "generic-node-desc " +
(data.node?.description !== "" && showNode ? "py-5" : "pb-5")
(data.node?.description !== "" ? "py-5" : "pb-5")
: ""
}
>
{data.node?.description !== "" && showNode && (
<div className="generic-node-desc-text">
{data.node?.description !== "" &&
showNode &&
data.node?.flow &&
inputDescription ? (
<Textarea
autoFocus
onBlur={() => {
setInputDescription(false);
if (nodeDescription.trim() !== "") {
setNodeDescription(nodeDescription);
data.node!.description = nodeDescription;
} else {
setNodeDescription(data.node!.description);
}
}}
value={nodeDescription}
onChange={(e) => setNodeDescription(e.target.value)}
onKeyDown={(e) => {
handleKeyDown(e, nodeDescription, "");
if (
e.key === "Enter" &&
e.shiftKey === false &&
e.ctrlKey === false &&
e.altKey === false
) {
setInputDescription(false);
if (nodeDescription.trim() !== "") {
setNodeDescription(nodeDescription);
data.node!.description = nodeDescription;
} else {
setNodeDescription(data.node!.description);
}
}
}}
/>
) : (
<div
className="generic-node-desc-text"
onDoubleClick={() => setInputDescription(true)}
>
{data.node?.description}
</div>
)}
<>
{Object.keys(data.node!.template)
.filter((templateField) => templateField.charAt(0) !== "_")
@ -350,15 +428,14 @@ export default function GenericNode({
!data.node!.template[templateField].advanced ? (
<ParameterComponent
index={idx.toString()}
key={
(data.node!.template[templateField].input_types?.join(
";"
) ?? data.node!.template[templateField].type) +
"|" +
templateField +
"|" +
data.id
}
key={scapedJSONStringfy({
inputTypes:
data.node!.template[templateField].input_types,
type: data.node!.template[templateField].type,
id: data.id,
fieldName: templateField,
proxy: data.node!.template[templateField].proxy,
})}
data={data}
setData={setData}
color={
@ -386,21 +463,20 @@ export default function GenericNode({
"\n"
) ?? data.node?.template[templateField].type
}
required={data.node?.template[templateField].required}
id={
(data.node?.template[templateField].input_types?.join(
";"
) ?? data.node?.template[templateField].type) +
"|" +
templateField +
"|" +
data.id
}
required={data.node!.template[templateField].required}
id={{
inputTypes:
data.node!.template[templateField].input_types,
type: data.node!.template[templateField].type,
id: data.id,
fieldName: templateField,
}}
left={true}
type={data.node?.template[templateField].type}
optionalHandle={
data.node?.template[templateField].input_types
}
proxy={data.node?.template[templateField].proxy}
showNode={showNode}
/>
) : (
@ -417,7 +493,11 @@ export default function GenericNode({
{" "}
</div>
<ParameterComponent
key={[data.type, data.id, ...data.node!.base_classes].join("|")}
key={scapedJSONStringfy({
baseClasses: data.node!.base_classes,
id: data.id,
dataType: data.type,
})}
data={data}
setData={setData}
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
@ -427,7 +507,11 @@ export default function GenericNode({
: data.type
}
tooltipTitle={data.node?.base_classes.join("\n")}
id={[data.type, data.id, ...data.node!.base_classes].join("|")}
id={{
baseClasses: data.node!.base_classes,
id: data.id,
dataType: data.type,
}}
type={data.node?.base_classes.join("|")}
left={false}
showNode={showNode}

View file

@ -80,61 +80,54 @@ export default function BuildTrigger({
const { flowId } = response.data;
// Step 2: Use the session ID to establish an SSE connection using EventSource
let validationResults: boolean[] = [];
let finished = false;
const apiUrl = `/api/v1/build/stream/${flowId}`;
const eventSource = new EventSource(apiUrl);
return new Promise<boolean>((resolve, reject) => {
const eventSource = new EventSource(apiUrl);
eventSource.onmessage = (event) => {
// If the event is parseable, return
if (!event.data) {
return;
}
const parsedData = JSON.parse(event.data);
// if the event is the end of the stream, close the connection
if (parsedData.end_of_stream) {
// Close the connection and finish
finished = true;
eventSource.onmessage = (event) => {
// If the event is parseable, return
if (!event.data) {
return;
}
const parsedData = JSON.parse(event.data);
// if the event is the end of the stream, close the connection
if (parsedData.end_of_stream) {
eventSource.close();
resolve(validationResults.every((result) => result));
} else if (parsedData.log) {
// If the event is a log, log it
setSuccessData({ title: parsedData.log });
} else if (parsedData.input_keys !== undefined) {
//@ts-ignore
setTabsState((old: TabsState) => {
return {
...old,
[flowId]: {
...old[flowId],
formKeysData: parsedData,
},
};
});
} else {
// Otherwise, process the data
const isValid = processStreamResult(parsedData);
setProgress(parsedData.progress);
validationResults.push(isValid);
}
};
eventSource.onerror = (error: any) => {
console.error("EventSource failed:", error);
if (error.data) {
const parsedData = JSON.parse(error.data);
setErrorData({ title: parsedData.error });
setIsBuilding(false);
}
eventSource.close();
return;
} else if (parsedData.log) {
// If the event is a log, log it
setSuccessData({ title: parsedData.log });
} else if (parsedData.input_keys !== undefined) {
//@ts-ignore
setTabsState((old: TabsState) => {
return {
...old,
[flowId]: {
...old[flowId],
formKeysData: parsedData,
},
};
});
} else {
// Otherwise, process the data
const isValid = processStreamResult(parsedData);
setProgress(parsedData.progress);
validationResults.push(isValid);
}
};
eventSource.onerror = (error: any) => {
console.error("EventSource failed:", error);
eventSource.close();
if (error.data) {
const parsedData = JSON.parse(error.data);
setErrorData({ title: parsedData.error });
setIsBuilding(false);
}
};
// Step 3: Wait for the stream to finish
while (!finished) {
await new Promise((resolve) => setTimeout(resolve, 100));
finished = validationResults.length === flow.data!.nodes.length;
}
// Step 4: Return true if all nodes are valid, false otherwise
return validationResults.every((result) => result);
reject(new Error("Streaming failed"));
};
});
}
function processStreamResult(parsedData: parsedDataType) {

View file

@ -13,6 +13,7 @@ export default function CodeAreaComponent({
dynamic,
setNodeClass,
id = "",
readonly = false,
}: CodeAreaComponentType) {
const [myValue, setMyValue] = useState(
typeof value == "string" ? value : JSON.stringify(value)
@ -31,6 +32,7 @@ export default function CodeAreaComponent({
return (
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
<CodeAreaModal
readonly={readonly}
dynamic={dynamic}
value={myValue}
nodeClass={nodeClass}

View file

@ -63,7 +63,7 @@ export default function CodeTabsComponent({
}, [flow]);
useEffect(() => {
if (tweaks) {
if (tweaks && data) {
unselectAllNodes({
data,
updateNodes: (nodes) => {
@ -604,6 +604,14 @@ export default function CodeTabsComponent({
].type === "prompt" ? (
<div className="mx-auto">
<PromptAreaComponent
readonly={
node.data.node?.flow &&
node.data.node.template[
templateField
].dynamic
? true
: false
}
editNode={true}
disabled={false}
value={
@ -646,6 +654,14 @@ export default function CodeTabsComponent({
<CodeAreaComponent
disabled={false}
editNode={true}
readonly={
node.data.node?.flow &&
node.data.node.template[
templateField
].dynamic
? true
: false
}
value={
!node.data.node.template[
templateField

View file

@ -26,7 +26,7 @@ export const MenuBar = ({ flows, tabId }: menuBarPropsType): JSX.Element => {
function handleAddFlow() {
try {
addFlow(undefined, true).then((id) => {
addFlow(true).then((id) => {
navigate("/flow/" + id);
});
// saveFlowStyleInDataBase();

View file

@ -1,11 +1,13 @@
import * as Form from "@radix-ui/react-form";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { InputComponentType } from "../../types/components";
import { handleKeyDown } from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils";
import { Input } from "../ui/input";
export default function InputComponent({
autoFocus = false,
onBlur,
value,
onChange,
disabled,
@ -16,9 +18,10 @@ export default function InputComponent({
placeholder = "Type something...",
className,
id = "",
blurOnEnter = false,
}: InputComponentType): JSX.Element {
const [pwdVisible, setPwdVisible] = useState(false);
const refInput = useRef<HTMLInputElement>(null);
// Clear component state
useEffect(() => {
if (disabled) {
@ -32,6 +35,9 @@ export default function InputComponent({
<Form.Control asChild>
<Input
id={"form-" + id}
ref={refInput}
onBlur={onBlur}
autoFocus={autoFocus}
type={password && !pwdVisible ? "password" : "text"}
value={value}
disabled={disabled}
@ -51,14 +57,18 @@ export default function InputComponent({
}}
onKeyDown={(e) => {
handleKeyDown(e, value, "");
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
}}
/>
</Form.Control>
) : (
<Input
id={id}
ref={refInput}
type="text"
onBlur={onBlur}
value={value}
autoFocus={autoFocus}
disabled={disabled}
required={required}
className={classNames(
@ -76,6 +86,7 @@ export default function InputComponent({
}}
onKeyDown={(e) => {
handleKeyDown(e, value, "");
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
}}
/>
)}

View file

@ -15,7 +15,8 @@ export default function PromptAreaComponent({
disabled,
editNode = false,
id = "",
}: PromptAreaComponentType) {
readonly = false,
}: PromptAreaComponentType): JSX.Element {
useEffect(() => {
if (disabled) {
onChange("");
@ -23,7 +24,7 @@ export default function PromptAreaComponent({
}, [disabled]);
useEffect(() => {
if (value !== "" && !editNode) {
if (value !== "" && !editNode && !readonly) {
postValidatePrompt(field_name!, value, nodeClass!).then((apiReturn) => {
if (apiReturn.data) {
setNodeClass!(apiReturn.data.frontend_node);
@ -37,6 +38,7 @@ export default function PromptAreaComponent({
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
<GenericModal
id={id}
readonly={readonly}
type={TypeModal.PROMPT}
value={value}
buttonText="Check & Save"

View file

@ -19,7 +19,7 @@ const TooltipContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"overflow-y-auto rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
"z-50 overflow-y-auto rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
className
)}
{...props}

View file

@ -21,10 +21,20 @@ import {
} from "../controllers/API";
import { APIClassType, APITemplateType } from "../types/api";
import { tweakType } from "../types/components";
import { FlowType, NodeDataType, NodeType } from "../types/flow";
import {
FlowType,
NodeDataType,
NodeType,
sourceHandleType,
targetHandleType,
} from "../types/flow";
import { TabsContextType, TabsState } from "../types/tabs";
import {
addVersionToDuplicates,
checkOldEdgesHandles,
scapeJSONParse,
scapedJSONStringfy,
updateEdgesHandleIds,
updateIds,
updateTemplate,
} from "../utils/reactflowUtils";
@ -41,7 +51,7 @@ const TabsContextInitialValue: TabsContextType = {
isLoading: true,
flows: [],
removeFlow: (id: string) => {},
addFlow: async (flowData?: any) => "",
addFlow: async (newProject: boolean, flowData?: FlowType) => "",
updateFlow: (newFlow: FlowType) => {},
incrementNodeId: () => uid(),
downloadFlow: (flow: FlowType) => {},
@ -134,14 +144,18 @@ export function TabsProvider({ children }: { children: ReactNode }) {
if (!flow.data) {
return;
}
processFlowEdges(flow);
processFlowNodes(flow);
processDataFromFlow(flow, false);
} catch (e) {}
});
}
function processFlowEdges(flow: FlowType) {
if (!flow.data || !flow.data.edges) return;
if (checkOldEdgesHandles(flow.data.edges)) {
const newEdges = updateEdgesHandleIds(flow.data);
flow.data.edges = newEdges;
}
//update edges colors
flow.data.edges.forEach((edge) => {
edge.className = "";
edge.style = { stroke: "#555" };
@ -159,6 +173,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
function processFlowNodes(flow: FlowType) {
if (!flow.data || !flow.data.nodes) return;
flow.data.nodes.forEach((node: NodeType) => {
if (node.data.node?.flow) return;
if (skipNodeUpdate.includes(node.data.type)) return;
const template = templates[node.data.type];
if (!template) {
@ -168,6 +183,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
if (Object.keys(template["template"]).length > 0) {
updateDisplay_name(node, template);
updateNodeBaseClasses(node, template);
//update baseclasses in edges
updateNodeEdges(flow, node, template);
updateNodeDescription(node, template);
updateNodeTemplate(node, template);
@ -187,11 +203,12 @@ export function TabsProvider({ children }: { children: ReactNode }) {
) {
flow.data!.edges.forEach((edge) => {
if (edge.source === node.id) {
edge.sourceHandle = edge.sourceHandle
?.split("|")
.slice(0, 2)
.concat(template["base_classes"])
.join("|");
let sourceHandleObject: sourceHandleType = scapeJSONParse(
edge.sourceHandle!
);
sourceHandleObject.baseClasses = template["base_classes"];
edge.data.sourceHandle = sourceHandleObject;
edge.sourceHandle = scapedJSONStringfy(sourceHandleObject);
}
});
}
@ -243,9 +260,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
// simulate a click on the link element to trigger the download
link.click();
setNoticeData({
title: "Warning: Critical data, JSON file may include API keys.",
});
}
function downloadFlows() {
@ -274,16 +288,22 @@ export function TabsProvider({ children }: { children: ReactNode }) {
* The resulting JSON object is passed to the addFlow function.
*/
async function uploadFlow(
newProject?: boolean,
newProject: boolean,
file?: File
): Promise<String | undefined> {
let id;
if (file) {
let text = await file.text();
let fileData = JSON.parse(text);
if (fileData.flows) {
fileData.flows.forEach((flow: FlowType) => {
id = addFlow(newProject, flow);
});
}
// parse the text into a JSON object
let flow: FlowType = JSON.parse(text);
id = await addFlow(flow, newProject);
id = await addFlow(newProject, flow);
} else {
// create a file input
const input = document.createElement("input");
@ -298,7 +318,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
const currentfile = (e.target as HTMLInputElement).files![0];
let text = await currentfile.text();
let flow: FlowType = JSON.parse(text);
const flowId = await addFlow(flow, newProject);
const flowId = await addFlow(newProject, flow);
resolve(flowId);
}
};
@ -349,7 +369,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
* Add a new flow to the list of flows.
* @param flow Optional flow to add.
*/
function paste(
selectionInstance: { nodes: Node[]; edges: Edge[] },
position: { x: number; y: number; paneX?: number; paneY?: number }
@ -398,19 +417,27 @@ export function TabsProvider({ children }: { children: ReactNode }) {
});
reactFlowInstance!.setNodes(nodes);
selectionInstance.edges.forEach((edge) => {
selectionInstance.edges.forEach((edge: Edge) => {
let source = idsMap[edge.source];
let target = idsMap[edge.target];
let sourceHandleSplitted = edge.sourceHandle!.split("|");
let sourceHandle =
sourceHandleSplitted[0] +
"|" +
source +
"|" +
sourceHandleSplitted.slice(2).join("|");
let targetHandleSplitted = edge.targetHandle!.split("|");
let targetHandle =
targetHandleSplitted.slice(0, -1).join("|") + "|" + target;
const sourceHandleObject: sourceHandleType = scapeJSONParse(
edge.sourceHandle!
);
let sourceHandle = scapedJSONStringfy({
...sourceHandleObject,
id: source,
});
sourceHandleObject.id = source;
edge.data.sourceHandle = sourceHandleObject;
const targetHandleObject: targetHandleType = scapeJSONParse(
edge.targetHandle!
);
let targetHandle = scapedJSONStringfy({
...targetHandleObject,
id: target,
});
targetHandleObject.id = target;
edge.data.targetHandle = targetHandleObject;
let id =
"reactflow__edge-" +
source +
@ -427,10 +454,10 @@ export function TabsProvider({ children }: { children: ReactNode }) {
id,
style: { stroke: "#555" },
className:
targetHandle.split("|")[0] === "Text"
targetHandleObject.type === "Text"
? "stroke-gray-800 "
: "stroke-gray-900 ",
animated: targetHandle.split("|")[0] === "Text",
animated: targetHandleObject.type === "Text",
selected: false,
},
edges.map((edge) => ({ ...edge, selected: false }))
@ -440,19 +467,16 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}
const addFlow = async (
flow?: FlowType,
newProject?: Boolean
newProject: Boolean,
flow?: FlowType
): Promise<String | undefined> => {
if (newProject) {
let flowData = extractDataFromFlow(flow!);
if (flowData.description == "") {
flowData.description = getRandomDescription();
}
let flowData = flow
? processDataFromFlow(flow)
: { nodes: [], edges: [], viewport: { zoom: 1, x: 0, y: 0 } };
// Create a new flow with a default name if no flow is provided.
const newFlow = createNewFlow(flowData, flow!);
processFlowEdges(newFlow);
processFlowNodes(newFlow);
const flowName = addVersionToDuplicates(newFlow, flows);
@ -480,31 +504,36 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}
};
const extractDataFromFlow = (flow: FlowType) => {
const processDataFromFlow = (flow: FlowType, refreshIds = true) => {
let data = flow?.data ? flow.data : null;
const description = flow?.description ? flow.description : "";
if (data) {
processFlowEdges(flow);
processFlowNodes(flow);
//add animation to text type edges
updateEdges(data.edges);
updateNodes(data.nodes, data.edges);
updateIds(data, getNodeId); // Assuming updateIds is defined elsewhere
// updateNodes(data.nodes, data.edges);
if (refreshIds) updateIds(data, getNodeId); // Assuming updateIds is defined elsewhere
}
return { data, description };
return data;
};
const updateEdges = (edges: Edge[]) => {
edges.forEach((edge) => {
const targetHandleObject: targetHandleType = scapeJSONParse(
edge.targetHandle!
);
edge.className =
(edge.targetHandle!.split("|")[0] === "Text"
(targetHandleObject.type === "Text"
? "stroke-gray-800 "
: "stroke-gray-900 ") + " stroke-connection";
edge.animated = edge.targetHandle!.split("|")[0] === "Text";
edge.animated = targetHandleObject.type === "Text";
});
};
const updateNodes = (nodes: Node[], edges: Edge[]) => {
nodes.forEach((node) => {
if (node.data.node?.flow) return;
if (skipNodeUpdate.includes(node.data.type)) return;
const template = templates[node.data.type];
if (!template) {
@ -514,12 +543,13 @@ export function TabsProvider({ children }: { children: ReactNode }) {
if (Object.keys(template["template"]).length > 0) {
node.data.node.base_classes = template["base_classes"];
edges.forEach((edge) => {
let sourceHandleObject: sourceHandleType = scapeJSONParse(
edge.sourceHandle!
);
if (edge.source === node.id) {
edge.sourceHandle = edge
.sourceHandle!.split("|")
.slice(0, 2)
.concat(template["base_classes"])
.join("|");
let newSourceHandle = sourceHandleObject;
newSourceHandle.baseClasses.concat(template["base_classes"]);
edge.sourceHandle = scapedJSONStringfy(newSourceHandle);
}
});
node.data.node.description = template["description"];
@ -532,12 +562,12 @@ export function TabsProvider({ children }: { children: ReactNode }) {
};
const createNewFlow = (
flowData: { data: ReactFlowJsonObject | null; description: string },
flowData: ReactFlowJsonObject | null,
flow: FlowType
) => ({
description: flowData.description,
description: flow?.description ?? getRandomDescription(),
name: flow?.name ?? getRandomName(),
data: flowData.data,
data: flowData,
id: "",
});

View file

@ -7,6 +7,7 @@ import {
useRef,
useState,
} from "react";
import ShadTooltip from "../../components/ShadTooltipComponent";
import CodeAreaComponent from "../../components/codeAreaComponent";
import DictComponent from "../../components/dictComponent";
import Dropdown from "../../components/dropdownComponent";
@ -166,11 +167,27 @@ const EditNodeModal = forwardRef(
.map((templateParam, index) => (
<TableRow key={index} className="h-10">
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
{myData.current.node?.template[templateParam].name
? myData.current.node.template[templateParam]
.name
: myData.current.node?.template[templateParam]
.display_name}
<ShadTooltip
content={
myData.current.node?.template[templateParam]
.proxy
? myData.current.node?.template[
templateParam
].proxy?.id
: null
}
>
<span>
{myData.current.node?.template[templateParam]
.display_name
? myData.current.node.template[
templateParam
].display_name
: myData.current.node?.template[
templateParam
].name}
</span>
</ShadTooltip>
</TableCell>
<TableCell className="w-[300px] p-0 text-center text-xs text-foreground ">
{myData.current.node?.template[templateParam]
@ -420,6 +437,14 @@ const EditNodeModal = forwardRef(
.type === "prompt" ? (
<div className="mx-auto">
<PromptAreaComponent
readonly={
myData.current.node?.flow &&
myData.current.node.template[
templateParam
].dynamic
? true
: false
}
field_name={templateParam}
editNode={true}
disabled={disabled}
@ -442,6 +467,14 @@ const EditNodeModal = forwardRef(
.type === "code" ? (
<div className="mx-auto">
<CodeAreaComponent
readonly={
myData.current.node?.flow &&
myData.current.node.template[
templateParam
].dynamic
? true
: false
}
dynamic={
data.node!.template[templateParam]
.dynamic ?? false

View file

@ -24,6 +24,7 @@ export default function CodeAreaModal({
setNodeClass,
children,
dynamic,
readonly = false,
}: codeAreaModalPropsType): JSX.Element {
const [code, setCode] = useState(value);
const { dark } = useContext(darkContext);
@ -152,6 +153,7 @@ export default function CodeAreaModal({
<div className="flex h-full w-full flex-col transition-all">
<div className="h-full w-full">
<AceEditor
readOnly={readonly}
value={code}
mode="python"
height={height ?? "100%"}
@ -191,6 +193,7 @@ export default function CodeAreaModal({
onClick={handleClick}
type="submit"
id="checkAndSaveBtn"
disabled={readonly}
>
Check & Save
</Button>

View file

@ -4,6 +4,7 @@ import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { Checkbox } from "../../components/ui/checkbox";
import { EXPORT_DIALOG_SUBTITLE } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { TabsContext } from "../../contexts/tabsContext";
import { removeApiKeys } from "../../utils/reactflowUtils";
import BaseModal from "../baseModal";
@ -11,7 +12,8 @@ import BaseModal from "../baseModal";
const ExportModal = forwardRef(
(props: { children: ReactNode }, ref): JSX.Element => {
const { flows, tabId, downloadFlow } = useContext(TabsContext);
const [checked, setChecked] = useState(false);
const { setNoticeData } = useContext(alertContext);
const [checked, setChecked] = useState(true);
const flow = flows.find((f) => f.id === tabId);
useEffect(() => {
setName(flow!.name);
@ -44,6 +46,7 @@ const ExportModal = forwardRef(
<div className="mt-3 flex items-center space-x-2">
<Checkbox
id="terms"
checked={checked}
onCheckedChange={(event: boolean) => {
setChecked(event);
}}
@ -52,18 +55,26 @@ const ExportModal = forwardRef(
Save with my API keys
</label>
</div>
<span className="text-xs text-destructive">
Caution: Uncheck this box only removes API keys from fields
specifically designated for API keys.
</span>
</BaseModal.Content>
<BaseModal.Footer>
<Button
onClick={() => {
if (checked)
if (checked) {
downloadFlow(
flows.find((flow) => flow.id === tabId)!,
name!,
description
);
else
setNoticeData({
title:
"Warning: Critical data, JSON file may include API keys.",
});
} else
downloadFlow(
removeApiKeys(flows.find((flow) => flow.id === tabId)!),
name!,

View file

@ -31,6 +31,7 @@ export default function GenericModal({
setNodeClass,
children,
id = "",
readonly = false,
}: genericModalPropsType): JSX.Element {
const [myButtonText] = useState(buttonText);
const [myModalTitle] = useState(modalTitle);
@ -209,7 +210,7 @@ export default function GenericModal({
"flex h-full w-full"
)}
>
{type === TypeModal.PROMPT && isEdit ? (
{type === TypeModal.PROMPT && isEdit && !readonly ? (
<Textarea
id={"modal-" + id}
ref={divRefPrompt}
@ -228,7 +229,7 @@ export default function GenericModal({
handleKeyDown(e, inputValue, "");
}}
/>
) : type === TypeModal.PROMPT && !isEdit ? (
) : type === TypeModal.PROMPT && (!isEdit || readonly) ? (
<SanitizedHTMLWrapper
className={getClassByNumberLength()}
content={coloredContent}
@ -250,6 +251,7 @@ export default function GenericModal({
onKeyDown={(e) => {
handleKeyDown(e, value, "");
}}
readOnly={readonly}
/>
) : (
<></>
@ -307,6 +309,7 @@ export default function GenericModal({
</div>
<Button
id="genericModalBtnSave"
disabled={readonly}
onClick={() => {
switch (myModalType) {
case TypeModal.TEXT:

View file

@ -1,101 +0,0 @@
import { buttonBoxPropsType } from "../../../types/components";
import { classNames } from "../../../utils/utils";
export default function ButtonBox({
onClick,
title,
description,
icon,
bgColor,
textColor,
deactivate,
size,
}: buttonBoxPropsType): JSX.Element {
let bigCircle: string;
let smallCircle: string;
let titleFontSize: string;
let descriptionFontSize: string;
let padding: string;
let marginTop: string;
let height: string;
let width: string;
let textHeight: number;
let textWidth: number;
switch (size) {
case "small":
bigCircle = "h-12 w-12";
smallCircle = "h-8 w-8";
titleFontSize = "text-sm";
descriptionFontSize = "text-xs";
padding = "p-2 py-3";
marginTop = "mt-2";
height = "h-36";
width = "w-32";
break;
case "medium":
bigCircle = "h-16 w-16";
smallCircle = "h-12 w-12";
titleFontSize = "text-base";
descriptionFontSize = "text-sm";
padding = "p-4 py-5";
marginTop = "mt-3";
height = "h-44";
width = "w-36";
break;
case "big":
bigCircle = "h-20 w-20";
smallCircle = "h-16 w-16";
titleFontSize = "text-lg";
descriptionFontSize = "text-sm";
padding = "p-8 py-10";
marginTop = "mt-6";
height = "h-56";
width = "w-44";
break;
default:
bigCircle = "h-20 w-20";
smallCircle = "h-16 w-16";
titleFontSize = "text-lg";
descriptionFontSize = "text-sm";
padding = "p-8 py-10";
marginTop = "mt-6";
height = "h-56";
width = "w-44";
break;
}
return (
<button disabled={deactivate} onClick={onClick}>
<div
className={classNames(
"button-box-modal-div",
bgColor,
height,
width,
padding
)}
>
<div
className={`flex items-center justify-center ${bigCircle} mb-1 rounded-full bg-background/30`}
>
<div
className={`flex items-center justify-center ${smallCircle} rounded-full bg-background`}
>
<div className={textColor}>{icon}</div>
</div>
</div>
<div className="mb-auto mt-auto w-full">
<h3
className={classNames(
"w-full font-semibold text-background truncate-multiline word-break-break-word",
titleFontSize,
marginTop
)}
>
{title}
</h3>
</div>
</div>
</button>
);
}

View file

@ -1,190 +0,0 @@
import {
ArrowLeftIcon,
ArrowUpTrayIcon,
ComputerDesktopIcon,
DocumentDuplicateIcon,
} from "@heroicons/react/24/outline";
import { useContext, useRef, useState } from "react";
import LoadingComponent from "../../components/loadingComponent";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../../components/ui/dialog";
import { IMPORT_DIALOG_SUBTITLE } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { TabsContext } from "../../contexts/tabsContext";
import { getExamples } from "../../controllers/API";
import { FlowType } from "../../types/flow";
import { classNames } from "../../utils/utils";
import ButtonBox from "./buttonBox";
export default function ImportModal(): JSX.Element {
const [open, setOpen] = useState(true);
const { setErrorData } = useContext(alertContext);
const ref = useRef();
const [showExamples, setShowExamples] = useState(false);
const [loadingExamples, setLoadingExamples] = useState(false);
const [examples, setExamples] = useState<FlowType[]>([]);
const { uploadFlow, addFlow } = useContext(TabsContext);
function handleExamples(): void {
setLoadingExamples(true);
getExamples()
.then((result) => {
setLoadingExamples(false);
setExamples(result);
})
.catch((error) =>
setErrorData({
title: "there was an error loading examples, please try again",
list: [error.message],
})
);
}
const [modalOpen, setModalOpen] = useState(false);
return (
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
<DialogTrigger></DialogTrigger>
<DialogContent
className={classNames(
showExamples
? "h-[600px] lg:max-w-[650px]"
: "h-[450px] lg:max-w-[650px]"
)}
>
<DialogHeader>
<DialogTitle className="flex items-center">
{showExamples && (
<>
<div className="dialog-header-modal-div">
<button
type="button"
className="dialog-header-modal-button disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
onClick={() => {
setShowExamples(false);
}}
>
<ArrowLeftIcon
className="ml-1 h-5 w-5 text-foreground"
aria-hidden="true"
/>
</button>
</div>
</>
)}
<span className={classNames(showExamples ? "pl-8 pr-2" : "pr-2")}>
{showExamples ? "Select an example" : "Import"}
</span>
<ArrowUpTrayIcon
className="ml-1 h-5 w-5 text-foreground"
aria-hidden="true"
/>
</DialogTitle>
<DialogDescription>{IMPORT_DIALOG_SUBTITLE}</DialogDescription>
</DialogHeader>
<div
className={classNames(
"dialog-modal-examples-div",
showExamples && !loadingExamples
? "dialog-modal-example-true"
: "dialog-modal-example-false"
)}
>
{!showExamples && (
<div className="dialog-modal-button-box-div">
<ButtonBox
size="big"
bgColor="bg-medium-emerald "
description="Prebuilt Examples"
icon={<DocumentDuplicateIcon className="document-icon" />}
onClick={() => {
setShowExamples(true);
handleExamples();
}}
textColor="text-medium-emerald "
title="Examples"
></ButtonBox>
<ButtonBox
size="big"
bgColor="bg-almost-dark-blue "
description="Import from Local"
icon={<ComputerDesktopIcon className="document-icon" />}
onClick={() => {
uploadFlow();
setModalOpen(false);
}}
textColor="text-almost-dark-blue "
title="Local File"
></ButtonBox>
</div>
)}
{showExamples && loadingExamples && (
<div className="loading-component-div">
<LoadingComponent remSize={30} />
</div>
)}
{showExamples &&
!loadingExamples &&
examples.map((example, index) => {
return (
<div key={example.name} className="m-2">
{" "}
<ButtonBox
size="small"
bgColor="bg-medium-emerald "
description={example.description ?? "Prebuilt Examples"}
icon={
<DocumentDuplicateIcon
strokeWidth={1.5}
className="h-6 w-6 flex-shrink-0"
/>
}
onClick={() => {
addFlow(example, false);
setModalOpen(false);
}}
textColor="text-medium-emerald "
title={example.name}
></ButtonBox>
</div>
);
})}
</div>
<DialogFooter>
<div className="dialog-modal-footer">
<a
href="https://github.com/logspace-ai/langflow_examples"
target="_blank"
className="dialog-modal-footer-link "
rel="noreferrer"
>
<svg
width="24"
viewBox="0 0 98 96"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"
fill="currentColor"
/>
</svg>
<span className="ml-2 ">Langflow Examples</span>
</a>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View file

@ -94,7 +94,7 @@ export default function CommunityPage(): JSX.Element {
size="sm"
className="whitespace-nowrap "
onClick={() => {
addFlow(flow, true).then((id) => {
addFlow(true, flow).then((id) => {
navigate("/flow/" + id);
});
}}

View file

@ -14,6 +14,7 @@ export default function DisclosureComponent({
<div>
<Disclosure.Button className="components-disclosure-arrangement">
<div className="flex gap-4">
{/* BUG ON THIS ICON */}
<Icon strokeWidth={1.5} size={22} className="text-primary" />
<span className="components-disclosure-title">{title}</span>
</div>

View file

@ -33,11 +33,18 @@ import { TabsContext } from "../../../../contexts/tabsContext";
import { typesContext } from "../../../../contexts/typesContext";
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
import { APIClassType } from "../../../../types/api";
import { FlowType, NodeType } from "../../../../types/flow";
import { FlowType, NodeType, targetHandleType } from "../../../../types/flow";
import { TabsState } from "../../../../types/tabs";
import { isValidConnection } from "../../../../utils/reactflowUtils";
import { isWrappedWithClass } from "../../../../utils/utils";
import {
generateFlow,
generateNodeFromFlow,
isValidConnection,
scapeJSONParse,
validateSelection,
} from "../../../../utils/reactflowUtils";
import { getRandomName, isWrappedWithClass } from "../../../../utils/utils";
import ConnectionLineComponent from "../ConnectionLineComponent";
import SelectionMenu from "../SelectionMenuComponent";
import ExtraSidebar from "../extraSidebarComponent";
const nodeTypes = {
@ -238,12 +245,19 @@ export default function Page({
addEdge(
{
...params,
data: {
targetHandle: scapeJSONParse(params.targetHandle!),
sourceHandle: scapeJSONParse(params.sourceHandle!),
},
style: { stroke: "#555" },
className:
(params.targetHandle?.split("|")[0] === "Text"
((scapeJSONParse(params.targetHandle!) as targetHandleType)
.type === "Text"
? "stroke-foreground "
: "stroke-foreground ") + " stroke-connection",
animated: params.targetHandle?.split("|")[0] === "Text",
animated:
(scapeJSONParse(params.targetHandle!) as targetHandleType)
.type === "Text",
},
eds
)
@ -394,7 +408,7 @@ export default function Page({
edgeUpdateSuccessful.current = true;
}, []);
const [selectionEnded, setSelectionEnded] = useState(false);
const [selectionEnded, setSelectionEnded] = useState(true);
const onSelectionEnd = useCallback(() => {
setSelectionEnded(true);
@ -481,6 +495,50 @@ export default function Page({
[&>button]:border-b-border hover:[&>button]:bg-border"
></Controls>
)}
<SelectionMenu
isVisible={selectionMenuVisible}
nodes={lastSelection?.nodes}
onClick={() => {
if (
validateSelection(lastSelection!, edges).length === 0
) {
const { newFlow } = generateFlow(
lastSelection!,
reactFlowInstance!,
getRandomName()
);
const newGroupNode = generateNodeFromFlow(
newFlow,
getNodeId
);
setNodes((oldNodes) => [
...oldNodes.filter(
(oldNodes) =>
!lastSelection?.nodes.some(
(selectionNode) =>
selectionNode.id === oldNodes.id
)
),
newGroupNode,
]);
setEdges((oldEdges) =>
oldEdges.filter(
(oldEdge) =>
!lastSelection!.nodes.some(
(selectionNode) =>
selectionNode.id === oldEdge.target ||
selectionNode.id === oldEdge.source
)
)
);
} else {
setErrorData({
title: "Invalid selection",
list: validateSelection(lastSelection!, edges),
});
}
}}
/>
</ReactFlow>
{!view && (
<Chat flow={flow} reactFlowInstance={reactFlowInstance!} />

View file

@ -0,0 +1,55 @@
import { useEffect, useState } from "react";
import { NodeToolbar } from "reactflow";
import IconComponent from "../../../../components/genericIconComponent";
export default function SelectionMenu({ onClick, nodes, isVisible }) {
const [isOpen, setIsOpen] = useState(false);
const [isTransitioning, setIsTransitioning] = useState(false);
const [lastNodes, setLastNodes] = useState(nodes);
// nodes get saved to not be gone after the toolbar closes
useEffect(() => {
setLastNodes(nodes);
}, [isOpen]);
// transition starts after and ends before the toolbar closes
useEffect(() => {
if (isVisible) {
setIsOpen(true);
setTimeout(() => {
setIsTransitioning(true);
}, 50);
} else {
setIsTransitioning(false);
setTimeout(() => {
setIsOpen(false);
}, 500);
}
}, [isVisible]);
return (
<NodeToolbar
isVisible={isOpen}
offset={5}
nodeId={
lastNodes && lastNodes.length > 0 ? lastNodes.map((n) => n.id) : []
}
>
<div className="h-10 w-28 overflow-hidden">
<div
className={
"h-10 w-24 rounded-md border border-indigo-300 bg-white px-2.5 text-gray-700 shadow-inner transition-all duration-500 ease-in-out dark:bg-gray-800 dark:text-gray-300" +
(isTransitioning ? " translate-y-0" : " translate-y-10")
}
>
<button
className="flex h-full w-full items-center justify-between text-sm hover:text-indigo-500"
onClick={onClick}
>
<IconComponent name="Group" className="w-6" />
Group
</button>
</div>
</div>
</NodeToolbar>
);
}

View file

@ -136,7 +136,7 @@ export default function ExtraSidebar(): JSX.Element {
<button
className="extra-side-bar-buttons"
onClick={() => {
uploadFlow();
uploadFlow(false);
}}
>
<IconComponent name="FileUp" className="side-bar-button-size " />

View file

@ -11,12 +11,17 @@ import {
import { TabsContext } from "../../../../contexts/tabsContext";
import EditNodeModal from "../../../../modals/EditNodeModal";
import { nodeToolbarPropsType } from "../../../../types/components";
import {
expandGroupNode,
updateFlowPosition,
} from "../../../../utils/reactflowUtils";
import { classNames, getRandomKeyByssmm } from "../../../../utils/utils";
export default function NodeToolbarComponent({
data,
setData,
deleteNode,
position,
setShowNode,
numberOfHandles,
showNode,
@ -47,6 +52,8 @@ export default function NodeToolbarComponent({
return true;
}
const isMinimal = canMinimize();
const isGroup = data.node?.flow ? true : false;
const { paste } = useContext(TabsContext);
const reactFlowInstance = useReactFlow();
const [showModalAdvanced, setShowModalAdvanced] = useState(false);
@ -65,6 +72,10 @@ export default function NodeToolbarComponent({
if (event.includes("disabled")) {
return;
}
if (event.includes("ungroup")) {
updateFlowPosition(position, data.node?.flow!);
expandGroupNode(data, reactFlowInstance);
}
};
return (
@ -134,7 +145,7 @@ export default function NodeToolbarComponent({
</a>
</ShadTooltip>
{isMinimal ? (
{isMinimal || isGroup ? (
<Select onValueChange={handleSelectChange} value={selectedValue}>
<ShadTooltip content="More" side="top">
<SelectTrigger>
@ -189,6 +200,17 @@ export default function NodeToolbarComponent({
</div>
</SelectItem>
)}
{isGroup && (
<SelectItem value={getRandomKeyByssmm() + "ungroup"}>
<div className="flex">
<IconComponent
name="Ungroup"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Ungroup{" "}
</div>
</SelectItem>
)}
</SelectContent>
</Select>
) : (
@ -223,29 +245,6 @@ export default function NodeToolbarComponent({
<></>
</EditNodeModal>
)}
{/*
<ShadTooltip content="Edit" side="top">
<div>
<EditNodeModal
data={data}
setData={setData}
nodeLength={nodeLength}
>
<div
className={classNames(
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10" +
(!canMinimize() && " rounded-r-md ") +
(nodeLength == 0
? " text-muted-foreground"
: " text-foreground")
)}
>
<IconComponent name="Settings2" className="h-4 w-4 " />
</div>
</EditNodeModal>
</div>
</ShadTooltip> */}
</span>
</div>
</>

View file

@ -104,7 +104,7 @@ export default function HomePage(): JSX.Element {
<DropdownButton
firstButtonName="New Project"
onFirstBtnClick={() => {
addFlow(null!, true).then((id) => {
addFlow(true).then((id) => {
navigate("/flow/" + id);
});
}}

View file

@ -505,7 +505,7 @@
}
.header-arrangement {
@apply flex-max-width h-12 items-center justify-between border-border bg-muted;
@apply flex-max-width h-12 items-center justify-between border-border bg-background;
}
.header-start-display {
@apply flex w-[30%] items-center justify-start gap-2;
@ -853,7 +853,7 @@
}
.api-modal-tabs {
@apply flex h-full flex-col max-w-full overflow-hidden rounded-md border bg-muted text-center sm:w-[75vw] md:w-[75vw] lg:w-[75vw] xl:w-[76vw] 2xl:w-full;
@apply flex h-full max-w-full flex-col overflow-hidden rounded-md border bg-muted text-center sm:w-[75vw] md:w-[75vw] lg:w-[75vw] xl:w-[76vw] 2xl:w-full;
}
.api-modal-tablist-div {
@apply flex items-center justify-between px-2;

View file

@ -1,10 +1,10 @@
import { Edge, Node, Viewport } from "reactflow";
import { FlowType } from "../flow";
//kind and class are just representative names to represent the actual structure of the object received by the API
export type APIDataType = { [key: string]: APIKindType };
export type APIObjectType = { kind: APIKindType; [key: string]: APIKindType };
export type APIKindType = { class: APIClassType; [key: string]: APIClassType };
export type APITemplateType = {
variable: TemplateVariableType;
[key: string]: TemplateVariableType;
};
@ -23,10 +23,13 @@ export type APIClassType = {
beta?: boolean;
documentation: string;
error?: string;
flow?: FlowType;
[key: string]:
| Array<string>
| string
| APITemplateType
| boolean
| FlowType
| CustomFieldsType
| boolean
| undefined;
@ -38,8 +41,11 @@ export type TemplateVariableType = {
placeholder?: string;
list: boolean;
show: boolean;
readonly: boolean;
multiline?: boolean;
value?: any;
dynamic?: boolean;
proxy?: { id: string; field: string };
input_types?: Array<string>;
[key: string]: any;
};

View file

@ -1,11 +1,13 @@
import { ReactElement, ReactNode } from "react";
import { ReactFlowJsonObject } from "reactflow";
import { ReactFlowJsonObject, XYPosition } from "reactflow";
import { APIClassType, APITemplateType, TemplateVariableType } from "../api";
import { ChatMessageType } from "../chat";
import { FlowStyleType, FlowType, NodeDataType, NodeType } from "../flow/index";
import { typesContextType } from "../typesContext";
import { sourceHandleType, targetHandleType } from "./../flow/index";
export type InputComponentType = {
autoFocus?: boolean;
onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
value: string;
disabled?: boolean;
onChange: (value: string) => void;
@ -18,6 +20,7 @@ export type InputComponentType = {
placeholder?: string;
className?: string;
id?: string;
blurOnEnter?: boolean;
};
export type ToggleComponentType = {
enabled: boolean;
@ -38,7 +41,7 @@ export type ParameterComponentType = {
data: NodeDataType;
setData: (value: NodeDataType) => void;
title: string;
id: string;
id: sourceHandleType | targetHandleType;
color: string;
left: boolean;
type: string | undefined;
@ -48,6 +51,7 @@ export type ParameterComponentType = {
dataContext?: typesContextType;
optionalHandle?: Array<String> | null;
info?: string;
proxy?: { field: string; id: string };
showNode?: boolean;
index?: string;
};
@ -84,6 +88,7 @@ export type TextAreaComponentType = {
value: string;
editNode?: boolean;
id?: string;
readonly?: boolean;
};
export type PromptAreaComponentType = {
@ -93,6 +98,7 @@ export type PromptAreaComponentType = {
disabled: boolean;
onChange: (value: string[] | string) => void;
value: string;
readonly?: boolean;
editNode?: boolean;
id?: string;
};
@ -106,6 +112,7 @@ export type CodeAreaComponentType = {
setNodeClass?: (value: APIClassType) => void;
dynamic?: boolean;
id?: string;
readonly?: boolean;
};
export type FileComponentType = {
@ -433,6 +440,7 @@ export type nodeToolbarPropsType = {
data: NodeDataType;
deleteNode: (idx: string) => void;
setData: (newState: NodeDataType) => void;
position: XYPosition;
setShowNode: (boolean: any) => void;
numberOfHandles: boolean[] | [];
showNode: boolean;
@ -468,6 +476,7 @@ export type codeAreaModalPropsType = {
setNodeClass: (Class: APIClassType) => void | undefined;
children: ReactNode;
dynamic?: boolean;
readonly?: boolean;
};
export type chatMessagePropsType = {
@ -493,6 +502,7 @@ export type genericModalPropsType = {
setNodeClass?: (Class: APIClassType) => void;
children: ReactNode;
id?: string;
readonly?: boolean;
};
export type buttonBoxPropsType = {

View file

@ -13,12 +13,14 @@ export type NodeType = {
type?: string;
position: XYPosition;
data: NodeDataType;
selected?: boolean;
};
export type NodeDataType = {
type: string;
node?: APIClassType;
id: string;
output_types?: string[];
};
// FlowStyleType is the type of the style object that is used to style the
// Flow card with an emoji and a color.
@ -35,3 +37,18 @@ export type TweaksType = Array<
};
} & FlowStyleType
>;
// right side
export type sourceHandleType = {
dataType: string;
id: string;
baseClasses: string[];
};
//left side
export type targetHandleType = {
inputTypes?: string[];
type: string;
fieldName: string;
id: string;
proxy?: { field: string; id: string };
};

View file

@ -9,8 +9,8 @@ export type TabsContextType = {
flows: Array<FlowType>;
removeFlow: (id: string) => void;
addFlow: (
flow?: FlowType,
newProject?: Boolean
newProject: boolean,
flow?: FlowType
) => Promise<String | undefined>;
updateFlow: (newFlow: FlowType) => void;
incrementNodeId: () => string;
@ -23,7 +23,7 @@ export type TabsContextType = {
uploadFlows: () => void;
isBuilt: boolean;
setIsBuilt: (state: boolean) => void;
uploadFlow: (newFlow?: boolean, file?: File) => Promise<String | undefined>;
uploadFlow: (newFlow: boolean, file?: File) => Promise<String | undefined>;
hardReset: () => void;
getNodeId: (nodeType: string) => string;
tabsState: TabsState;

View file

@ -1,5 +1,5 @@
import { Edge, Node } from "reactflow";
import { NodeType } from "../flow";
import { FlowType, NodeType } from "../flow";
export type cleanEdgesType = {
flow: {
@ -11,5 +11,17 @@ export type cleanEdgesType = {
export type unselectAllNodesType = {
updateNodes: (nodes: Node[]) => void;
data: Node[] | null;
data: Node[];
};
export type updateEdgesHandleIdsType = {
nodes: NodeType[];
edges: Edge[];
};
export type generateFlowType = { newFlow: FlowType; removedEdges: Edge[] };
export type findLastNodeType = {
nodes: NodeType[];
edges: Edge[];
};

View file

@ -3,17 +3,30 @@ import {
Connection,
Edge,
Node,
OnSelectionChangeParams,
ReactFlowInstance,
ReactFlowJsonObject,
XYPosition,
} from "reactflow";
import ShortUniqueId from "short-unique-id";
import { specialCharsRegex } from "../constants/constants";
import { APITemplateType } from "../types/api";
import { FlowType, NodeType } from "../types/flow";
import { APITemplateType, TemplateVariableType } from "../types/api";
import {
FlowType,
NodeDataType,
NodeType,
sourceHandleType,
targetHandleType,
} from "../types/flow";
import {
cleanEdgesType,
findLastNodeType,
generateFlowType,
unselectAllNodesType,
updateEdgesHandleIdsType,
} from "../types/utils/reactflowUtils";
import { toNormalCase } from "./utils";
import { toNormalCase, toTitleCase } from "./utils";
const uid = new ShortUniqueId({ length: 5 });
export function cleanEdges({
flow: { edges, nodes },
@ -32,26 +45,30 @@ export function cleanEdges({
const sourceHandle = edge.sourceHandle; //right
const targetHandle = edge.targetHandle; //left
if (targetHandle) {
const field = targetHandle.split("|")[1];
const id =
(targetNode.data.node?.template[field]?.input_types?.join(";") ??
targetNode.data.node?.template[field]?.type) +
"|" +
field +
"|" +
targetNode.data.id;
if (id !== targetHandle) {
const targetHandleObject: targetHandleType =
scapeJSONParse(targetHandle);
const field = targetHandleObject.fieldName;
const id: targetHandleType = {
type: targetNode.data.node!.template[field]?.type,
fieldName: field,
id: targetNode.data.id,
inputTypes: targetNode.data.node!.template[field]?.input_types,
};
if (targetNode.data.node!.template[field]?.proxy) {
id.proxy = targetNode.data.node!.template[field]?.proxy;
}
if (scapedJSONStringfy(id) !== targetHandle) {
newEdges = newEdges.filter((e) => e.id !== edge.id);
}
}
if (sourceHandle) {
const id = [
sourceNode.data.type,
sourceNode.data.id,
...sourceNode.data.node?.base_classes!,
].join("|");
if (id !== sourceHandle) {
newEdges = newEdges.filter((edg) => edg.id !== edge.id);
const id: sourceHandleType = {
id: sourceNode.data.id,
baseClasses: sourceNode.data.node!.base_classes,
dataType: sourceNode.data.type,
};
if (scapedJSONStringfy(id) !== sourceHandle) {
newEdges = newEdges.filter((e) => e.id !== edge.id);
}
}
}
@ -61,7 +78,7 @@ export function cleanEdges({
export function unselectAllNodes({ updateNodes, data }: unselectAllNodesType) {
let newNodes = _.cloneDeep(data);
newNodes!.forEach((node: Node) => {
newNodes.forEach((node: Node) => {
node.selected = false;
});
updateNodes(newNodes!);
@ -71,21 +88,18 @@ export function isValidConnection(
{ source, target, sourceHandle, targetHandle }: Connection,
reactFlowInstance: ReactFlowInstance
) {
const targetHandleObject: targetHandleType = scapeJSONParse(targetHandle!);
const sourceHandleObject: sourceHandleType = scapeJSONParse(sourceHandle!);
if (
targetHandle
?.split("|")[0]
.split(";")
.some((target) => target === sourceHandle?.split("|")[0]) ||
sourceHandle
?.split("|")
.slice(2)
.some((target) =>
targetHandle
?.split("|")[0]
.split(";")
.some((n) => n === target)
) ||
targetHandle?.split("|")[0] === "str"
targetHandleObject.inputTypes?.some(
(n) => n === sourceHandleObject.dataType
) ||
sourceHandleObject.baseClasses.some(
(t) =>
targetHandleObject.inputTypes?.some((n) => n === t) ||
t === targetHandleObject.type
) ||
targetHandleObject.type === "str"
) {
let targetNode = reactFlowInstance?.getNode(target!)?.data?.node;
if (!targetNode) {
@ -97,11 +111,11 @@ export function isValidConnection(
return true;
}
} else if (
(!targetNode.template[targetHandle?.split("|")[1]!].list &&
(!targetNode.template[targetHandleObject.fieldName].list &&
!reactFlowInstance
.getEdges()
.find((e) => e.targetHandle === targetHandle)) ||
targetNode.template[targetHandle?.split("|")[1]!].list
targetNode.template[targetHandleObject.fieldName].list
) {
return true;
}
@ -152,26 +166,36 @@ export function updateIds(
newFlow.nodes.forEach((node: NodeType) => {
// Generate a unique node ID
let newId = getNodeId(node.data.type);
let newId = getNodeId(node.data.node?.flow ? "GroupNode" : node.data.type);
idsMap[node.id] = newId;
node.id = newId;
node.data.id = newId;
// Add the new node to the list of nodes in state
});
newFlow.edges.forEach((edge) => {
newFlow.edges.forEach((edge: Edge) => {
edge.source = idsMap[edge.source];
edge.target = idsMap[edge.target];
let sourceHandleSplitted = edge.sourceHandle!.split("|");
edge.sourceHandle =
sourceHandleSplitted[0] +
"|" +
edge.source +
"|" +
sourceHandleSplitted.slice(2).join("|");
let targetHandleSplitted = edge.targetHandle!.split("|");
edge.targetHandle =
targetHandleSplitted.slice(0, -1).join("|") + "|" + edge.target;
const sourceHandleObject: sourceHandleType = scapeJSONParse(
edge.sourceHandle!
);
edge.sourceHandle = scapedJSONStringfy({
...sourceHandleObject,
id: edge.source,
});
if (edge.data?.sourceHandle?.id) {
edge.data.sourceHandle.id = edge.source;
}
const targetHandleObject: targetHandleType = scapeJSONParse(
edge.targetHandle!
);
edge.targetHandle = scapedJSONStringfy({
...targetHandleObject,
id: edge.target,
});
if (edge.data?.targetHandle?.id) {
edge.data.targetHandle.id = edge.target;
}
edge.id =
"reactflow__edge-" +
edge.source +
@ -210,8 +234,10 @@ export function validateNode(node: NodeType, edges: Edge[]): Array<string> {
template[t].value === "") &&
!edges.some(
(edge) =>
edge.targetHandle?.split("|")[1] === t &&
edge.targetHandle.split("|")[2] === node.id
(scapeJSONParse(edge.targetHandle!) as targetHandleType).fieldName ===
t &&
(scapeJSONParse(edge.targetHandle!) as targetHandleType).id ===
node.id
)
) {
errors.push(
@ -266,6 +292,49 @@ export function addVersionToDuplicates(flow: FlowType, flows: FlowType[]) {
return newName;
}
export function updateEdgesHandleIds({
edges,
nodes,
}: updateEdgesHandleIdsType): Edge[] {
let newEdges = _.cloneDeep(edges);
newEdges.forEach((edge) => {
const sourceNodeId = edge.source;
const targetNodeId = edge.target;
const sourceNode = nodes.find((node) => node.id === sourceNodeId);
const targetNode = nodes.find((node) => node.id === targetNodeId);
let source = edge.sourceHandle;
let target = edge.targetHandle;
//right
let newSource: sourceHandleType;
//left
let newTarget: targetHandleType;
if (target && targetNode) {
let field = target.split("|")[1];
newTarget = {
type: targetNode.data.node!.template[field].type,
fieldName: field,
id: targetNode.data.id,
inputTypes: targetNode.data.node!.template[field].input_types,
};
}
if (source && sourceNode) {
newSource = {
id: sourceNode.data.id,
baseClasses: sourceNode.data.node!.base_classes,
dataType: sourceNode.data.type,
};
}
edge.sourceHandle = scapedJSONStringfy(newSource!);
edge.targetHandle = scapedJSONStringfy(newTarget!);
const newData = {
sourceHandle: scapeJSONParse(edge.sourceHandle),
targetHandle: scapeJSONParse(edge.targetHandle),
};
edge.data = newData;
});
return newEdges;
}
export function handleKeyDown(
e:
| React.KeyboardEvent<HTMLInputElement>
@ -377,3 +446,593 @@ export function convertValuesToNumbers(arr) {
return newObj;
});
}
export function scapedJSONStringfy(json: object): string {
return customStringify(json).replace(/"/g, "œ");
}
export function scapeJSONParse(json: string): any {
let parsed = json.replace(/œ/g, '"');
return JSON.parse(parsed);
}
// this function receives an array of edges and return true if any of the handles are not a json string
export function checkOldEdgesHandles(edges: Edge[]): boolean {
return edges.some(
(edge) =>
!edge.sourceHandle ||
!edge.targetHandle ||
!edge.sourceHandle.includes("{") ||
!edge.targetHandle.includes("{")
);
}
export function customStringify(obj: any): string {
if (typeof obj === "undefined") {
return "null";
}
if (obj === null || typeof obj !== "object") {
if (obj instanceof Date) {
return `"${obj.toISOString()}"`;
}
return JSON.stringify(obj);
}
if (Array.isArray(obj)) {
const arrayItems = obj.map((item) => customStringify(item)).join(",");
return `[${arrayItems}]`;
}
const keys = Object.keys(obj).sort();
const keyValuePairs = keys.map(
(key) => `"${key}":${customStringify(obj[key])}`
);
return `{${keyValuePairs.join(",")}}`;
}
export function getMiddlePoint(nodes: Node[]) {
let middlePointX = 0;
let middlePointY = 0;
nodes.forEach((node) => {
middlePointX += node.position.x;
middlePointY += node.position.y;
});
const totalNodes = nodes.length;
const averageX = middlePointX / totalNodes;
const averageY = middlePointY / totalNodes;
return { x: averageX, y: averageY };
}
export function generateFlow(
selection: OnSelectionChangeParams,
reactFlowInstance: ReactFlowInstance,
name: string
): generateFlowType {
const newFlowData = reactFlowInstance.toObject();
/* remove edges that are not connected to selected nodes on both ends
in future we can save this edges to when ungrouping reconect to the old nodes
*/
newFlowData.edges = selection.edges.filter(
(edge) =>
selection.nodes.some((node) => node.id === edge.target) &&
selection.nodes.some((node) => node.id === edge.source)
);
newFlowData.nodes = selection.nodes;
const newFlow: FlowType = {
data: newFlowData,
name: name,
description: "",
//generating local id instead of using the id from the server, can change in the future
id: uid(),
};
// filter edges that are not connected to selected nodes on both ends
// using O(n²) aproach because the number of edges is small
// in the future we can use a better aproach using a set
return {
newFlow,
removedEdges: selection.edges.filter(
(edge) => !newFlowData.edges.includes(edge)
),
};
}
export function filterFlow(
selection: OnSelectionChangeParams,
reactFlowInstance: ReactFlowInstance
) {
reactFlowInstance.setNodes((nodes) =>
nodes.filter((node) => !selection.nodes.includes(node))
);
reactFlowInstance.setEdges((edges) =>
edges.filter((edge) => !selection.edges.includes(edge))
);
}
export function findLastNode({ nodes, edges }: findLastNodeType) {
/*
this function receives a flow and return the last node
*/
let lastNode = nodes.find((n) => !edges.some((e) => e.source === n.id));
return lastNode;
}
export function updateFlowPosition(NewPosition: XYPosition, flow: FlowType) {
const middlePoint = getMiddlePoint(flow.data!.nodes);
let deltaPosition = {
x: NewPosition.x - middlePoint.x,
y: NewPosition.y - middlePoint.y,
};
flow.data!.nodes.forEach((node) => {
node.position.x += deltaPosition.x;
node.position.y += deltaPosition.y;
});
}
export function concatFlows(
flow: FlowType,
ReactFlowInstance: ReactFlowInstance
) {
const { nodes, edges } = flow.data!;
ReactFlowInstance.addNodes(nodes);
ReactFlowInstance.addEdges(edges);
}
export function validateSelection(
selection: OnSelectionChangeParams,
edges: Edge[]
): Array<string> {
//add edges to selection if selection mode selected only nodes
if (selection.edges.length === 0) {
selection.edges = edges;
}
// get only edges that are connected to the nodes in the selection
// first creates a set of all the nodes ids
let nodesSet = new Set(selection.nodes.map((n) => n.id));
// then filter the edges that are connected to the nodes in the set
let connectedEdges = selection.edges.filter(
(e) => nodesSet.has(e.source) && nodesSet.has(e.target)
);
// add the edges to the selection
selection.edges = connectedEdges;
let errorsArray: Array<string> = [];
// check if there is more than one node
if (selection.nodes.length < 2) {
errorsArray.push("Please select more than one node");
}
//check if there are two or more nodes with free outputs
if (
selection.nodes.filter(
(n) => !selection.edges.some((e) => e.source === n.id)
).length > 1
) {
errorsArray.push("Please select only one node with free outputs");
}
// check if there is any node that does not have any connection
if (
selection.nodes.some(
(node) =>
!selection.edges.some((edge) => edge.target === node.id) &&
!selection.edges.some((edge) => edge.source === node.id)
)
) {
errorsArray.push("Please select only nodes that are connected");
}
return errorsArray;
}
function updateGroupNodeTemplate(template: APITemplateType) {
/*this function receives a template, iterates for it's items
updating the visibility of all basic types setting it to advanced true*/
Object.keys(template).forEach((key) => {
let type = template[key].type;
let input_types = template[key].input_types;
if (
(type === "str" ||
type === "bool" ||
type === "float" ||
type === "code" ||
type === "prompt" ||
type === "file" ||
type === "int" ||
type === "dict" ||
type === "NestedDict") &&
!template[key].required &&
!input_types
) {
template[key].advanced = true;
}
//prevent code fields from showing on the group node
if (type === "code") {
template[key].show = false;
}
});
return template;
}
export function mergeNodeTemplates({
nodes,
edges,
}: {
nodes: NodeType[];
edges: Edge[];
}): APITemplateType {
/* this function receives a flow and iterate throw each node
and merge the templates with only the visible fields
if there are two keys with the same name in the flow, we will update the display name of each one
to show from which node it came from
*/
let template: APITemplateType = {};
nodes.forEach((node) => {
let nodeTemplate = _.cloneDeep(node.data.node!.template);
Object.keys(nodeTemplate)
.filter((field_name) => field_name.charAt(0) !== "_")
.forEach((key) => {
if (!isHandleConnected(edges, key, nodeTemplate[key], node.id)) {
template[key + "_" + node.id] = nodeTemplate[key];
template[key + "_" + node.id].proxy = { id: node.id, field: key };
if (node.type === "groupNode") {
template[key + "_" + node.id].display_name =
node.data.node!.flow!.name + " - " + nodeTemplate[key].name;
} else {
template[key + "_" + node.id].display_name =
//data id already has the node name on it
nodeTemplate[key].display_name
? nodeTemplate[key].display_name
: nodeTemplate[key].name
? toTitleCase(nodeTemplate[key].name)
: toTitleCase(key);
}
}
});
});
return template;
}
function isHandleConnected(
edges: Edge[],
key: string,
field: TemplateVariableType,
nodeId: string
) {
/*
this function receives a flow and a handleId and check if there is a connection with this handle
*/
scapedJSONStringfy({ type: field.type, fieldName: key, id: nodeId });
if (field.proxy) {
if (
edges.some(
(e) =>
e.targetHandle ===
scapedJSONStringfy({
type: field.type,
fieldName: key,
id: nodeId,
proxy: { id: field.proxy!.id, field: field.proxy!.field },
inputTypes: field.input_types,
} as targetHandleType)
)
) {
return true;
}
} else {
if (
edges.some(
(e) =>
e.targetHandle ===
scapedJSONStringfy({
type: field.type,
fieldName: key,
id: nodeId,
inputTypes: field.input_types,
} as targetHandleType)
)
) {
return true;
}
}
return false;
}
export function generateNodeTemplate(Flow: FlowType) {
/*
this function receives a flow and generate a template for the group node
*/
let template = mergeNodeTemplates({
nodes: Flow.data!.nodes,
edges: Flow.data!.edges,
});
updateGroupNodeTemplate(template);
return template;
}
export function generateNodeFromFlow(
flow: FlowType,
getNodeId: (type: string) => string
): NodeType {
const { nodes } = flow.data!;
const outputNode = _.cloneDeep(findLastNode(flow.data!));
const position = getMiddlePoint(nodes);
let data = _.cloneDeep(flow);
const id = getNodeId(outputNode?.data.type!);
const newGroupNode: NodeType = {
data: {
id,
type: outputNode?.data.type!,
node: {
output_types: outputNode!.data.node!.output_types,
display_name: "Group",
documentation: "",
base_classes: outputNode!.data.node!.base_classes,
description: "double click to edit description",
template: generateNodeTemplate(data),
flow: data,
},
},
id,
position,
type: "genericNode",
};
return newGroupNode;
}
export function connectedInputNodesOnHandle(
nodeId: string,
handleId: string,
{ nodes, edges }: { nodes: NodeType[]; edges: Edge[] }
) {
const connectedNodes: Array<{ name: string; id: string; isGroup: boolean }> =
[];
// return the nodes connected to the input handle of the node
const TargetEdges = edges.filter((e) => e.target === nodeId);
TargetEdges.forEach((edge) => {
if (edge.targetHandle === handleId) {
const sourceNode = nodes.find((n) => n.id === edge.source);
if (sourceNode) {
if (sourceNode.type === "groupNode") {
let lastNode = findLastNode(sourceNode.data.node!.flow!.data!);
while (lastNode && lastNode.type === "groupNode") {
lastNode = findLastNode(lastNode.data.node!.flow!.data!);
}
if (lastNode) {
connectedNodes.push({
name: sourceNode.data.node!.flow!.name,
id: lastNode.id,
isGroup: true,
});
}
} else {
connectedNodes.push({
name: sourceNode.data.type,
id: sourceNode.id,
isGroup: false,
});
}
}
}
});
return connectedNodes;
}
export function ungroupNode(
groupNode: NodeDataType,
BaseFlow: ReactFlowJsonObject
) {
const { template, flow } = groupNode.node!;
const gNodes: NodeType[] = flow!.data!.nodes;
const gEdges = flow!.data!.edges;
//redirect edges to correct proxy node
let updatedEdges: Edge[] = [];
BaseFlow.edges.forEach((edge) => {
let newEdge = _.cloneDeep(edge);
if (newEdge.target === groupNode.id) {
const targetHandle: targetHandleType = newEdge.data.targetHandle;
if (targetHandle.proxy) {
let type = targetHandle.type;
let field = targetHandle.proxy.field;
let proxyId = targetHandle.proxy.id;
let inputTypes = targetHandle.inputTypes;
let node: NodeType = gNodes.find((n) => n.id === proxyId)!;
if (node) {
newEdge.target = proxyId;
let newTargetHandle: targetHandleType = {
fieldName: field,
type,
id: proxyId,
inputTypes: inputTypes,
};
if (node.data.node?.flow) {
newTargetHandle.proxy = {
field: node.data.node.template[field].proxy?.field!,
id: node.data.node.template[field].proxy?.id!,
};
}
newEdge.data.targetHandle = newTargetHandle;
newEdge.targetHandle = scapedJSONStringfy(newTargetHandle);
}
}
}
if (newEdge.source === groupNode.id) {
const lastNode = _.cloneDeep(findLastNode(flow!.data!));
newEdge.source = lastNode!.id;
let newSourceHandle: sourceHandleType = scapeJSONParse(
newEdge.sourceHandle!
);
newSourceHandle.id = lastNode!.id;
newEdge.data.sourceHandle = newSourceHandle;
newEdge.sourceHandle = scapedJSONStringfy(newSourceHandle);
}
if (edge.target === groupNode.id || edge.source === groupNode.id) {
updatedEdges.push(newEdge);
}
});
//update template values
Object.keys(template).forEach((key) => {
let { field, id } = template[key].proxy!;
let nodeIndex = gNodes.findIndex((n) => n.id === id);
if (nodeIndex !== -1) {
let display_name: string;
let show = gNodes[nodeIndex].data.node!.template[field].show;
let advanced = gNodes[nodeIndex].data.node!.template[field].advanced;
if (gNodes[nodeIndex].data.node!.template[field].display_name) {
display_name =
gNodes[nodeIndex].data.node!.template[field].display_name;
} else {
display_name = gNodes[nodeIndex].data.node!.template[field].name;
}
gNodes[nodeIndex].data.node!.template[field] = template[key];
gNodes[nodeIndex].data.node!.template[field].show = show;
gNodes[nodeIndex].data.node!.template[field].advanced = advanced;
gNodes[nodeIndex].data.node!.template[field].display_name = display_name;
}
});
const nodes = [
...BaseFlow.nodes.filter((n) => n.id !== groupNode.id),
...gNodes,
];
const edges = [
...BaseFlow.edges.filter(
(e) => e.target !== groupNode.id && e.source !== groupNode.id
),
...gEdges,
...updatedEdges,
];
BaseFlow.nodes = nodes;
BaseFlow.edges = edges;
}
export function expandGroupNode(
groupNode: NodeDataType,
ReactFlowInstance: ReactFlowInstance
) {
const { template, flow } = _.cloneDeep(groupNode.node!);
const gNodes: NodeType[] = flow?.data?.nodes!;
const gEdges = flow!.data!.edges;
console.log(gEdges);
//redirect edges to correct proxy node
let updatedEdges: Edge[] = [];
ReactFlowInstance.getEdges().forEach((edge) => {
let newEdge = _.cloneDeep(edge);
if (newEdge.target === groupNode.id) {
const targetHandle: targetHandleType = newEdge.data.targetHandle;
if (targetHandle.proxy) {
let type = targetHandle.type;
let field = targetHandle.proxy.field;
let proxyId = targetHandle.proxy.id;
let inputTypes = targetHandle.inputTypes;
let node: NodeType = gNodes.find((n) => n.id === proxyId)!;
if (node) {
newEdge.target = proxyId;
let newTargetHandle: targetHandleType = {
fieldName: field,
type,
id: proxyId,
inputTypes: inputTypes,
};
if (node.data.node?.flow) {
newTargetHandle.proxy = {
field: node.data.node.template[field].proxy?.field!,
id: node.data.node.template[field].proxy?.id!,
};
}
newEdge.data.targetHandle = newTargetHandle;
newEdge.targetHandle = scapedJSONStringfy(newTargetHandle);
}
}
}
if (newEdge.source === groupNode.id) {
const lastNode = _.cloneDeep(findLastNode(flow!.data!));
newEdge.source = lastNode!.id;
let newSourceHandle: sourceHandleType = scapeJSONParse(
newEdge.sourceHandle!
);
newSourceHandle.id = lastNode!.id;
newEdge.data.sourceHandle = newSourceHandle;
newEdge.sourceHandle = scapedJSONStringfy(newSourceHandle);
}
if (edge.target === groupNode.id || edge.source === groupNode.id) {
updatedEdges.push(newEdge);
}
});
//update template values
Object.keys(template).forEach((key) => {
let { field, id } = template[key].proxy!;
let nodeIndex = gNodes.findIndex((n) => n.id === id);
if (nodeIndex !== -1) {
let proxy: { id: string; field: string } | undefined;
let display_name: string;
let show = gNodes[nodeIndex].data.node!.template[field].show;
let advanced = gNodes[nodeIndex].data.node!.template[field].advanced;
if (gNodes[nodeIndex].data.node!.template[field].display_name) {
display_name =
gNodes[nodeIndex].data.node!.template[field].display_name;
} else {
display_name = gNodes[nodeIndex].data.node!.template[field].name;
}
if (gNodes[nodeIndex].data.node!.template[field].proxy) {
proxy = gNodes[nodeIndex].data.node!.template[field].proxy;
}
gNodes[nodeIndex].data.node!.template[field] = template[key];
gNodes[nodeIndex].data.node!.template[field].show = show;
gNodes[nodeIndex].data.node!.template[field].advanced = advanced;
gNodes[nodeIndex].data.node!.template[field].display_name = display_name;
gNodes[nodeIndex].selected = false;
if (proxy) {
gNodes[nodeIndex].data.node!.template[field].proxy = proxy;
} else {
delete gNodes[nodeIndex].data.node!.template[field].proxy;
}
}
});
const nodes = [
...ReactFlowInstance.getNodes().filter((n) => n.id !== groupNode.id),
...gNodes,
];
const edges = [
...ReactFlowInstance.getEdges().filter(
(e) => e.target !== groupNode.id && e.source !== groupNode.id
),
...gEdges,
...updatedEdges,
];
console.log(edges);
ReactFlowInstance.setNodes(nodes);
ReactFlowInstance.setEdges(edges);
}
export function processFlow(FlowObject: ReactFlowJsonObject) {
let clonedFLow = _.cloneDeep(FlowObject);
clonedFLow.nodes.forEach((node: NodeType) => {
if (node.data.node?.flow) {
processFlow(node.data.node!.flow!.data!);
ungroupNode(node.data, clonedFLow);
}
});
return clonedFLow;
}
export function getGroupStatus(
flow: FlowType,
ssData: { [key: string]: { valid: boolean; params: string } }
) {
let status = { valid: true, params: "Built sucessfully ✨" };
const { nodes } = flow.data!;
const ids = nodes.map((n: NodeType) => n.data.id);
ids.forEach((id) => console.log(ssData[id]));
ids.forEach((id) => {
if (!ssData[id]) {
status = ssData[id];
return;
}
if (!ssData[id].valid) {
status = { valid: false, params: ssData[id].params };
}
});
return status;
}

View file

@ -34,6 +34,7 @@ import {
Gift,
GitFork,
GithubIcon,
Group,
Hammer,
HelpCircle,
Home,
@ -70,6 +71,7 @@ import {
TerminalSquare,
Trash2,
Undo,
Ungroup,
Unplug,
Upload,
User,
@ -312,8 +314,10 @@ export const nodeIconsLucide: iconsType = {
UserCog2,
Key,
Unplug,
BookMarked,
Group,
ChevronUp,
Ungroup,
BookMarked,
Minus,
Square,
Minimize2,

View file

@ -449,6 +449,29 @@ chat_input_field: Input key that you want the chat to send the user message with
></langflow-chat>`;
}
export function truncateLongId(id: string): string {
let [componentName, newId] = id.split("-");
if (componentName.length > 15) {
componentName = componentName.slice(0, 15);
componentName += "...";
return componentName + "-" + newId;
}
return id;
}
export function extractIdFromLongId(id: string): string {
let [_, newId] = id.split("-");
return newId;
}
export function truncateDisplayName(name: string): string {
if (name.length > 15) {
name = name.slice(0, 15);
name += "...";
}
return name;
}
export function tabsArray(codes: string[], method: number) {
if (!method) return;
if (method === 0) {

View file

@ -120,11 +120,7 @@ test("CodeAreaModalComponent", async ({ page }) => {
await page.locator('//*[@id="showcode"]').click();
expect(await page.locator('//*[@id="showcode"]').isChecked()).toBeTruthy();
await page
.locator(
'//*[@id="radix-:r2l:"]/div[2]/div/div[2]/div/div/div/table/tbody/tr[1]/td[2]/div/div/button/div/span'
)
.click();
await page.locator('//*[@id="code-area-edit0"]').click();
let value = await page.locator('//*[@id="codeValue"]').inputValue();

View file

@ -0,0 +1,100 @@
import { expect, test } from "@playwright/test";
test.describe("Group component tests", () => {
test("group test", async ({ page }) => {
await page.goto("http://localhost:3000/");
await page.getByRole("button", { name: "Community Examples" }).click();
await page
.locator(
"div:nth-child(7) > div:nth-child(2) > .card-component-footer-arrangement > .inline-flex"
)
.click();
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div[1]/div/div[1]/div"
)
.click({
modifiers: ["Control"],
});
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div[2]/div/div[1]/div"
)
.click({
modifiers: ["Control"],
});
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div[3]/div/div[1]/div"
)
.click({
modifiers: ["Control"],
});
await page.getByRole("button", { name: "Group" }).click();
expect(
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div/div"
)
.isVisible()
).toBeTruthy();
await page.getByPlaceholder("Type something...").first().click();
await page.getByPlaceholder("Type something...").first().fill("test");
await page.locator(".side-bar-buttons-arrangement").click();
expect(
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div/div/div[2]/div/div/div[1]/div/div[1]/div/div"
)
.textContent()
).toBe("test");
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div/div"
)
.locator('input[type="text"]')
.click();
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div/div"
)
.locator('input[type="text"]')
.fill("fieldValue");
await page.locator(".side-bar-buttons-arrangement").click();
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div/div/div[1]/div"
)
.click();
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[2]/div/span/button[3]/div/div"
)
.click();
await page.getByLabel("Edit").click();
await page
.getByRole("button", { name: "zero-shot-react-description" })
.click();
await page.getByText("openai-functions").click();
await page.getByRole("button", { name: "Save Changes" }).click();
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[2]/div/span/button[3]/div/div"
)
.click();
await page.getByLabel("Ungroup").click();
await expect(
page.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div/div/div[2]/div[3]/div/div[2]/div[4]/div/div[2]/div/input"
)
).toHaveValue("fieldValue");
expect(
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div/div/div[2]/div[2]/div/div[2]/div[5]/div/div[2]/div/button/span[1]"
)
.textContent()
).toBe("openai-functions");
});
});

View file

@ -1,5 +0,0 @@
import { test } from "@playwright/test";
test("test", async ({ page }) => {
// Recording...
});

View file

@ -35,6 +35,16 @@ def pytest_configure():
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.ONE_GROUPED_CHAT_EXAMPLE_PATH = (
Path(__file__).parent.absolute() / "data" / "one_group_chat.json"
)
pytest.VECTOR_STORE_GROUPED_EXAMPLE_PATH = (
Path(__file__).parent.absolute() / "data" / "vector_store_grouped.json"
)
pytest.BASIC_CHAT_WITH_PROMPT_AND_HISTORY = (
Path(__file__).parent.absolute() / "data" / "BasicChatwithPromptandHistory.json"
)
@ -159,6 +169,24 @@ def json_flow():
return f.read()
@pytest.fixture
def grouped_chat_json_flow():
with open(pytest.GROUPED_CHAT_EXAMPLE_PATH, "r") as f:
return f.read()
@pytest.fixture
def one_grouped_chat_json_flow():
with open(pytest.ONE_GROUPED_CHAT_EXAMPLE_PATH, "r") as f:
return f.read()
@pytest.fixture
def vector_store_grouped_json_flow():
with open(pytest.VECTOR_STORE_GROUPED_EXAMPLE_PATH, "r") as f:
return f.read()
@pytest.fixture
def json_flow_with_prompt_and_history():
with open(pytest.BASIC_CHAT_WITH_PROMPT_AND_HISTORY, "r") as f:

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -1,3 +1,4 @@
import copy
import json
import os
from pathlib import Path
@ -17,6 +18,15 @@ from langflow.graph.vertex.types import (
)
from langflow.processing.process import get_result_and_thought
from langflow.utils.payload import get_root_node
from langflow.graph.graph.utils import (
find_last_node,
set_new_target_handle,
ungroup_node,
process_flow,
update_source_handle,
update_target_handle,
update_template,
)
# Test cases for the graph module
@ -24,6 +34,52 @@ from langflow.utils.payload import get_root_node
# BASIC_EXAMPLE_PATH, COMPLEX_EXAMPLE_PATH, OPENAPI_EXAMPLE_PATH
@pytest.fixture
def sample_template():
return {
"field1": {"proxy": {"field": "some_field", "id": "node1"}},
"field2": {"proxy": {"field": "other_field", "id": "node2"}},
}
@pytest.fixture
def sample_nodes():
return [
{
"id": "node1",
"data": {
"node": {
"template": {
"some_field": {"show": True, "advanced": False, "name": "Name1"}
}
}
},
},
{
"id": "node2",
"data": {
"node": {
"template": {
"other_field": {
"show": False,
"advanced": True,
"display_name": "DisplayName2",
}
}
}
},
},
{
"id": "node3",
"data": {
"node": {
"template": {"unrelated_field": {"show": True, "advanced": True}}
}
},
},
]
def get_node_by_type(graph, node_type: Type[Vertex]) -> Union[Vertex, None]:
"""Get a node by type"""
return next((node for node in graph.nodes if isinstance(node, node_type)), None)
@ -113,55 +169,6 @@ def test_get_node_neighbors_basic(basic_graph):
)
# def test_get_node_neighbors_complex(complex_graph):
# """Test getting node neighbors"""
# assert isinstance(complex_graph, Graph)
# # Get root node
# root = get_root_node(complex_graph)
# assert root is not None
# neighbors = complex_graph.get_nodes_with_target(root)
# assert neighbors is not None
# # Neighbors should be a list of nodes
# assert isinstance(neighbors, list)
# # Root Node is an Agent, it requires an LLMChain and tools
# # We need to check if there is a Chain in the one of the neighbors'
# assert any("Chain" in neighbor.data["type"] for neighbor in neighbors)
# # assert Tool is in the neighbors
# assert any("Tool" in neighbor.data["type"] for neighbor in neighbors)
# # Now on to the Chain's neighbors
# chain = next(neighbor for neighbor in neighbors if "Chain" in neighbor.data["type"])
# chain_neighbors = complex_graph.get_nodes_with_target(chain)
# assert chain_neighbors is not None
# # Check if there is a LLM in the chain's neighbors
# assert any("OpenAI" in neighbor.data["type"] for neighbor in chain_neighbors)
# # Chain should have a Prompt as a neighbor
# assert any("Prompt" in neighbor.data["type"] for neighbor in chain_neighbors)
# # Now on to the Tool's neighbors
# tool = next(neighbor for neighbor in neighbors if "Tool" in neighbor.data["type"])
# tool_neighbors = complex_graph.get_nodes_with_target(tool)
# assert tool_neighbors is not None
# # Check if there is an Agent in the tool's neighbors
# assert any("Agent" in neighbor.data["type"] for neighbor in tool_neighbors)
# # This Agent has a Tool that has a PythonFunction as func
# agent = next(
# neighbor for neighbor in tool_neighbors if "Agent" in neighbor.data["type"]
# )
# agent_neighbors = complex_graph.get_nodes_with_target(agent)
# assert agent_neighbors is not None
# # Check if there is a Tool in the agent's neighbors
# assert any("Tool" in neighbor.data["type"] for neighbor in agent_neighbors)
# # This Tool has a PythonFunction as func
# tool = next(
# neighbor for neighbor in agent_neighbors if "Tool" in neighbor.data["type"]
# )
# tool_neighbors = complex_graph.get_nodes_with_target(tool)
# assert tool_neighbors is not None
# # Check if there is a PythonFunction in the tool's neighbors
# assert any(
# "PythonFunctionTool" in neighbor.data["type"] for neighbor in tool_neighbors
# )
def test_get_node(basic_graph):
"""Test getting a single node"""
node_id = basic_graph.nodes[0].id
@ -322,6 +329,196 @@ def test_get_result_and_thought(basic_graph):
assert isinstance(result, dict)
def test_find_last_node(grouped_chat_json_flow):
grouped_chat_data = json.loads(grouped_chat_json_flow).get("data")
nodes, edges = grouped_chat_data["nodes"], grouped_chat_data["edges"]
last_node = find_last_node(nodes, edges)
assert last_node is not None # Replace with the actual expected value
assert last_node["id"] == "LLMChain-pimAb" # Replace with the actual expected value
def test_ungroup_node(grouped_chat_json_flow):
grouped_chat_data = json.loads(grouped_chat_json_flow).get("data")
group_node = grouped_chat_data["nodes"][
2
] # Assuming the first node is a group node
base_flow = copy.deepcopy(grouped_chat_data)
ungroup_node(group_node["data"], base_flow)
# after ungroup_node is called, the base_flow and grouped_chat_data should be different
assert base_flow != grouped_chat_data
# assert node 2 is not a group node anymore
assert base_flow["nodes"][2]["data"]["node"].get("flow") is None
# assert the edges are updated
assert len(base_flow["edges"]) > len(grouped_chat_data["edges"])
assert base_flow["edges"][0]["source"] == "ConversationBufferMemory-kUMif"
assert base_flow["edges"][0]["target"] == "LLMChain-2P369"
assert base_flow["edges"][1]["source"] == "PromptTemplate-Wjk4g"
assert base_flow["edges"][1]["target"] == "LLMChain-2P369"
assert base_flow["edges"][2]["source"] == "ChatOpenAI-rUJ1b"
assert base_flow["edges"][2]["target"] == "LLMChain-2P369"
def test_process_flow(grouped_chat_json_flow):
grouped_chat_data = json.loads(grouped_chat_json_flow).get("data")
processed_flow = process_flow(grouped_chat_data)
assert processed_flow is not None
assert isinstance(processed_flow, dict)
assert "nodes" in processed_flow
assert "edges" in processed_flow
def test_process_flow_one_group(one_grouped_chat_json_flow):
grouped_chat_data = json.loads(one_grouped_chat_json_flow).get("data")
# There should be only one node
assert len(grouped_chat_data["nodes"]) == 1
# Get the node, it should be a group node
group_node = grouped_chat_data["nodes"][0]
node_data = group_node["data"]["node"]
assert node_data.get("flow") is not None
template_data = node_data["template"]
assert any("openai_api_key" in key for key in template_data.keys())
# Get the openai_api_key dict
openai_api_key = next(
(template_data[key] for key in template_data.keys() if "openai_api_key" in key),
None,
)
assert openai_api_key is not None
assert openai_api_key["value"] == "test"
processed_flow = process_flow(grouped_chat_data)
assert processed_flow is not None
assert isinstance(processed_flow, dict)
assert "nodes" in processed_flow
assert "edges" in processed_flow
# Now get the node that has ChatOpenAI in its id
chat_openai_node = next(
(node for node in processed_flow["nodes"] if "ChatOpenAI" in node["id"]), None
)
assert chat_openai_node is not None
assert (
chat_openai_node["data"]["node"]["template"]["openai_api_key"]["value"]
== "test"
)
def test_process_flow_vector_store_grouped(vector_store_grouped_json_flow):
grouped_chat_data = json.loads(vector_store_grouped_json_flow).get("data")
nodes = grouped_chat_data["nodes"]
assert len(nodes) == 4
# There are two group nodes in this flow
# One of them is inside the other totalling 7 nodes
# 4 nodes grouped, one of these turns into 1 normal node and 1 group node
# This group node has 2 nodes inside it
processed_flow = process_flow(grouped_chat_data)
assert processed_flow is not None
processed_nodes = processed_flow["nodes"]
assert len(processed_nodes) == 7
assert isinstance(processed_flow, dict)
assert "nodes" in processed_flow
assert "edges" in processed_flow
edges = processed_flow["edges"]
# Expected keywords in source and target fields
expected_keywords = [
{"source": "VectorStoreInfo", "target": "VectorStoreAgent"},
{"source": "ChatOpenAI", "target": "VectorStoreAgent"},
{"source": "OpenAIEmbeddings", "target": "Chroma"},
{"source": "Chroma", "target": "VectorStoreInfo"},
{"source": "WebBaseLoader", "target": "RecursiveCharacterTextSplitter"},
{"source": "RecursiveCharacterTextSplitter", "target": "Chroma"},
]
for idx, expected_keyword in enumerate(expected_keywords):
for key, value in expected_keyword.items():
assert (
value in edges[idx][key].split("-")[0]
), f"Edge {idx}, key {key} expected to contain {value} but got {edges[idx][key]}"
def test_update_template(sample_template, sample_nodes):
# Making a deep copy to keep original sample_nodes unchanged
nodes_copy = copy.deepcopy(sample_nodes)
update_template(sample_template, nodes_copy)
# Now, validate the updates.
node1_updated = next((n for n in nodes_copy if n["id"] == "node1"), None)
node2_updated = next((n for n in nodes_copy if n["id"] == "node2"), None)
node3_updated = next((n for n in nodes_copy if n["id"] == "node3"), None)
assert node1_updated["data"]["node"]["template"]["some_field"]["show"] is True
assert node1_updated["data"]["node"]["template"]["some_field"]["advanced"] is False
assert (
node1_updated["data"]["node"]["template"]["some_field"]["display_name"]
== "Name1"
)
assert node2_updated["data"]["node"]["template"]["other_field"]["show"] is False
assert node2_updated["data"]["node"]["template"]["other_field"]["advanced"] is True
assert (
node2_updated["data"]["node"]["template"]["other_field"]["display_name"]
== "DisplayName2"
)
# Ensure node3 remains unchanged
assert node3_updated == sample_nodes[2]
# Test `update_target_handle`
def test_update_target_handle_proxy():
new_edge = {
"data": {
"targetHandle": {
"type": "some_type",
"proxy": {"id": "some_id", "field": ""},
}
}
}
g_nodes = [{"id": "some_id", "data": {"node": {"flow": None}}}]
group_node_id = "group_id"
updated_edge = update_target_handle(new_edge, g_nodes, group_node_id)
assert updated_edge["data"]["targetHandle"] == new_edge["data"]["targetHandle"]
# Test `set_new_target_handle`
def test_set_new_target_handle():
proxy_id = "proxy_id"
new_edge = {"target": None, "data": {"targetHandle": {}}}
target_handle = {"type": "type_1", "proxy": {"field": "field_1"}}
node = {
"data": {
"node": {
"flow": True,
"template": {
"field_1": {"proxy": {"field": "new_field", "id": "new_id"}}
},
}
}
}
set_new_target_handle(proxy_id, new_edge, target_handle, node)
assert new_edge["target"] == "proxy_id"
assert new_edge["data"]["targetHandle"]["fieldName"] == "field_1"
assert new_edge["data"]["targetHandle"]["proxy"] == {
"field": "new_field",
"id": "new_id",
}
# Test `update_source_handle`
def test_update_source_handle():
new_edge = {"source": None, "data": {"sourceHandle": {"id": None}}}
flow_data = {
"nodes": [{"id": "some_node"}, {"id": "last_node"}],
"edges": [{"source": "some_node"}],
}
updated_edge = update_source_handle(
new_edge, flow_data["nodes"], flow_data["edges"]
)
assert updated_edge["source"] == "last_node"
assert updated_edge["data"]["sourceHandle"]["id"] == "last_node"
def test_pickle_graph(json_vector_store):
loaded_json = json.loads(json_vector_store)
graph = Graph.from_payload(loaded_json)

View file

@ -21,7 +21,7 @@ def test_prompt_template(client: TestClient, logged_in_headers):
template = prompt["template"]
assert template["input_variables"] == {
"required": True,
"dynamic": False,
"dynamic": True,
"placeholder": "",
"show": False,
"multiline": False,
@ -35,7 +35,7 @@ def test_prompt_template(client: TestClient, logged_in_headers):
assert template["output_parser"] == {
"required": False,
"dynamic": False,
"dynamic": True,
"placeholder": "",
"show": False,
"multiline": False,
@ -49,7 +49,7 @@ def test_prompt_template(client: TestClient, logged_in_headers):
assert template["partial_variables"] == {
"required": False,
"dynamic": False,
"dynamic": True,
"placeholder": "",
"show": False,
"multiline": False,
@ -63,7 +63,7 @@ def test_prompt_template(client: TestClient, logged_in_headers):
assert template["template"] == {
"required": True,
"dynamic": False,
"dynamic": True,
"placeholder": "",
"show": True,
"multiline": True,
@ -77,7 +77,7 @@ def test_prompt_template(client: TestClient, logged_in_headers):
assert template["template_format"] == {
"required": False,
"dynamic": False,
"dynamic": True,
"placeholder": "",
"show": False,
"multiline": False,
@ -92,7 +92,7 @@ def test_prompt_template(client: TestClient, logged_in_headers):
assert template["validate_template"] == {
"required": False,
"dynamic": False,
"dynamic": True,
"placeholder": "",
"show": False,
"multiline": False,