Merge branch 'zustand/io/migration' into state_theories

This commit is contained in:
Gabriel Luiz Freitas Almeida 2024-03-04 18:53:12 -03:00
commit 481df3de8f
67 changed files with 798 additions and 599 deletions

View file

@ -114,7 +114,7 @@ async def build_vertex(
vertex = graph.get_vertex(vertex_id)
try:
if not vertex.pinned or not vertex._built:
if not vertex.frozen or not vertex._built:
inputs_dict = inputs.model_dump() if inputs else {}
await vertex.build(user_id=current_user.id, inputs=inputs_dict)
@ -233,7 +233,7 @@ async def build_vertex_stream(
)
yield str(stream_data)
elif not vertex.pinned or not vertex._built:
elif not vertex.frozen or not vertex._built:
logger.debug(f"Streaming vertex {vertex_id}")
stream_data = StreamData(
event="message",

View file

@ -0,0 +1,89 @@
from concurrent import futures
from pathlib import Path
from typing import List, Optional, Text
from langflow.schema.schema import Record
def is_hidden(path: Path) -> bool:
return path.name.startswith(".")
def retrieve_file_paths(
path: str,
types: List[str],
load_hidden: bool,
recursive: bool,
depth: int,
) -> List[str]:
path_obj = Path(path)
if not path_obj.exists() or not path_obj.is_dir():
raise ValueError(f"Path {path} must exist and be a directory.")
def match_types(p: Path) -> bool:
return any(p.suffix == f".{t}" for t in types) if types else True
def is_not_hidden(p: Path) -> bool:
return not is_hidden(p) or load_hidden
def walk_level(directory: Path, max_depth: int):
directory = directory.resolve()
prefix_length = len(directory.parts)
for p in directory.rglob("*" if recursive else "[!.]*"):
if len(p.parts) - prefix_length <= max_depth:
yield p
glob = "**/*" if recursive else "*"
paths = walk_level(path_obj, depth) if depth else path_obj.glob(glob)
file_paths = [
Text(p) for p in paths if p.is_file() and match_types(p) and is_not_hidden(p)
]
return file_paths
def parse_file_to_record(file_path: str, silent_errors: bool) -> Optional[Record]:
# Use the partition function to load the file
from unstructured.partition.auto import partition # type: ignore
try:
elements = partition(file_path)
except Exception as e:
if not silent_errors:
raise ValueError(f"Error loading file {file_path}: {e}") from e
return None
# Create a Record
text = "\n\n".join([Text(el) for el in elements])
metadata = elements.metadata if hasattr(elements, "metadata") else {}
metadata["file_path"] = file_path
record = Record(text=text, data=metadata)
return record
def get_elements(
file_paths: List[str],
silent_errors: bool,
max_concurrency: int,
use_multithreading: bool,
) -> List[Optional[Record]]:
if use_multithreading:
records = parallel_load_records(file_paths, silent_errors, max_concurrency)
else:
records = [
parse_file_to_record(file_path, silent_errors) for file_path in file_paths
]
records = list(filter(None, records))
return records
def parallel_load_records(
file_paths: List[str], silent_errors: bool, max_concurrency: int
) -> List[Optional[Record]]:
with futures.ThreadPoolExecutor(max_workers=max_concurrency) as executor:
loaded_files = executor.map(
lambda file_path: parse_file_to_record(file_path, silent_errors),
file_paths,
)
# loaded_files is an iterator, so we need to convert it to a list
return list(loaded_files)

View file

@ -0,0 +1,76 @@
from typing import Any, Dict, List, Optional
from langflow import CustomComponent
from langflow.base.data.utils import (
parallel_load_records,
parse_file_to_record,
retrieve_file_paths,
)
from langflow.schema import Record
class DirectoryComponent(CustomComponent):
display_name = "Directory"
description = "Load files from a directory."
def build_config(self) -> Dict[str, Any]:
return {
"path": {"display_name": "Path"},
"types": {
"display_name": "Types",
"info": "File types to load. Leave empty to load all types.",
},
"depth": {"display_name": "Depth", "info": "Depth to search for files."},
"max_concurrency": {"display_name": "Max Concurrency", "advanced": True},
"load_hidden": {
"display_name": "Load Hidden",
"advanced": True,
"info": "If true, hidden files will be loaded.",
},
"recursive": {
"display_name": "Recursive",
"advanced": True,
"info": "If true, the search will be recursive.",
},
"silent_errors": {
"display_name": "Silent Errors",
"advanced": True,
"info": "If true, errors will not raise an exception.",
},
"use_multithreading": {
"display_name": "Use Multithreading",
"advanced": True,
},
}
def build(
self,
path: str,
types: Optional[List[str]] = None,
depth: int = 0,
max_concurrency: int = 2,
load_hidden: bool = False,
recursive: bool = True,
silent_errors: bool = False,
use_multithreading: bool = True,
) -> List[Optional[Record]]:
if types is None:
types = []
resolved_path = self.resolve_path(path)
file_paths = retrieve_file_paths(
resolved_path, types, load_hidden, recursive, depth
)
loaded_records = []
if use_multithreading:
loaded_records = parallel_load_records(
file_paths, silent_errors, max_concurrency
)
else:
loaded_records = [
parse_file_to_record(file_path, silent_errors)
for file_path in file_paths
]
loaded_records = list(filter(None, loaded_records))
self.status = loaded_records
return loaded_records

View file

@ -0,0 +1,28 @@
from typing import Any, Dict, Optional
from langflow import CustomComponent
from langflow.base.data.utils import parse_file_to_record
from langflow.schema import Record
class FileComponent(CustomComponent):
display_name = "File"
description = "Load a file."
def build_config(self) -> Dict[str, Any]:
return {
"path": {"display_name": "Path"},
"silent_errors": {
"display_name": "Silent Errors",
"advanced": True,
"info": "If true, errors will not raise an exception.",
},
}
def build(
self,
path: str,
silent_errors: bool = False,
) -> Optional[Record]:
resolved_path = self.resolve_path(path)
return parse_file_to_record(resolved_path, silent_errors)

View file

@ -0,0 +1,26 @@
from typing import Any, Dict, Optional
from langchain_community.document_loaders.url import UnstructuredURLLoader
from langflow import CustomComponent
from langflow.schema import Record
class URLComponent(CustomComponent):
display_name = "URL"
description = "Load a URL."
def build_config(self) -> Dict[str, Any]:
return {
"urls": {"display_name": "URL"},
}
async def build(
self,
urls: list[str],
) -> Optional[Record]:
loader = UnstructuredURLLoader(urls=urls)
docs = loader.load()
records = self.to_records(docs)
return records

View file

@ -40,7 +40,9 @@ class UrlLoaderComponent(CustomComponent):
except Exception as e:
raise ValueError(f"No loader found for: {web_path}") from e
docs = loader_instance.load()
avg_length = sum(len(doc.page_content) for doc in docs if hasattr(doc, "page_content")) / len(docs)
avg_length = sum(
len(doc.page_content) for doc in docs if hasattr(doc, "page_content")
) / len(docs)
self.status = f"""{len(docs)} documents)
\nAvg. Document Length (characters): {int(avg_length)}
Documents: {docs[:3]}..."""

View file

@ -1,161 +0,0 @@
from concurrent import futures
from pathlib import Path
from typing import Any, Dict, List, Optional, Text
from langflow import CustomComponent
from langflow.schema import Record
class GatherRecordsComponent(CustomComponent):
display_name = "Gather Records"
description = "Gather records from a directory."
def build_config(self) -> Dict[str, Any]:
return {
"path": {"display_name": "Path"},
"types": {
"display_name": "Types",
"info": "File types to load. Leave empty to load all types.",
},
"depth": {"display_name": "Depth", "info": "Depth to search for files."},
"max_concurrency": {"display_name": "Max Concurrency", "advanced": True},
"load_hidden": {
"display_name": "Load Hidden",
"advanced": True,
"info": "If true, hidden files will be loaded.",
},
"recursive": {
"display_name": "Recursive",
"advanced": True,
"info": "If true, the search will be recursive.",
},
"silent_errors": {
"display_name": "Silent Errors",
"advanced": True,
"info": "If true, errors will not raise an exception.",
},
"use_multithreading": {
"display_name": "Use Multithreading",
"advanced": True,
},
}
def is_hidden(self, path: Path) -> bool:
return path.name.startswith(".")
def retrieve_file_paths(
self,
path: str,
types: List[str],
load_hidden: bool,
recursive: bool,
depth: int,
) -> List[str]:
path_obj = Path(path)
if not path_obj.exists() or not path_obj.is_dir():
raise ValueError(f"Path {path} must exist and be a directory.")
def match_types(p: Path) -> bool:
return any(p.suffix == f".{t}" for t in types) if types else True
def is_not_hidden(p: Path) -> bool:
return not self.is_hidden(p) or load_hidden
def walk_level(directory: Path, max_depth: int):
directory = directory.resolve()
prefix_length = len(directory.parts)
for p in directory.rglob("*" if recursive else "[!.]*"):
if len(p.parts) - prefix_length <= max_depth:
yield p
glob = "**/*" if recursive else "*"
paths = walk_level(path_obj, depth) if depth else path_obj.glob(glob)
file_paths = [
Text(p)
for p in paths
if p.is_file() and match_types(p) and is_not_hidden(p)
]
return file_paths
def parse_file_to_record(
self, file_path: str, silent_errors: bool
) -> Optional[Record]:
# Use the partition function to load the file
from unstructured.partition.auto import partition # type: ignore
try:
elements = partition(file_path)
except Exception as e:
if not silent_errors:
raise ValueError(f"Error loading file {file_path}: {e}") from e
return None
# Create a Record
text = "\n\n".join([Text(el) for el in elements])
metadata = elements.metadata if hasattr(elements, "metadata") else {}
metadata["file_path"] = file_path
record = Record(text=text, data=metadata)
return record
def get_elements(
self,
file_paths: List[str],
silent_errors: bool,
max_concurrency: int,
use_multithreading: bool,
) -> List[Optional[Record]]:
if use_multithreading:
records = self.parallel_load_records(
file_paths, silent_errors, max_concurrency
)
else:
records = [
self.parse_file_to_record(file_path, silent_errors)
for file_path in file_paths
]
records = list(filter(None, records))
return records
def parallel_load_records(
self, file_paths: List[str], silent_errors: bool, max_concurrency: int
) -> List[Optional[Record]]:
with futures.ThreadPoolExecutor(max_workers=max_concurrency) as executor:
loaded_files = executor.map(
lambda file_path: self.parse_file_to_record(file_path, silent_errors),
file_paths,
)
# loaded_files is an iterator, so we need to convert it to a list
return list(loaded_files)
def build(
self,
path: str,
types: Optional[List[str]] = None,
depth: int = 0,
max_concurrency: int = 2,
load_hidden: bool = False,
recursive: bool = True,
silent_errors: bool = False,
use_multithreading: bool = True,
) -> List[Optional[Record]]:
if types is None:
types = []
resolved_path = self.resolve_path(path)
file_paths = self.retrieve_file_paths(
resolved_path, types, load_hidden, recursive, depth
)
loaded_records = []
if use_multithreading:
loaded_records = self.parallel_load_records(
file_paths, silent_errors, max_concurrency
)
else:
loaded_records = [
self.parse_file_to_record(file_path, silent_errors)
for file_path in file_paths
]
loaded_records = list(filter(None, loaded_records))
self.status = loaded_records
return loaded_records

View file

@ -1,6 +1,6 @@
from typing import Optional, Union
from langflow.components.io.base.chat import ChatComponent
from langflow.base.io.chat import ChatComponent
from langflow.field_typing import Text
from langflow.schema import Record

View file

@ -1,6 +1,6 @@
from typing import Optional
from langflow.components.io.base.text import TextComponent
from langflow.base.io.text import TextComponent
from langflow.field_typing import Text

View file

@ -1,6 +1,6 @@
from typing import Optional, Union
from langflow.components.io.base.chat import ChatComponent
from langflow.base.io.chat import ChatComponent
from langflow.field_typing import Text
from langflow.schema import Record

View file

@ -1,6 +1,6 @@
from typing import Optional
from langflow.components.io.base.text import TextComponent
from langflow.base.io.text import TextComponent
from langflow.field_typing import Text

View file

@ -1,48 +0,0 @@
# Implement ShouldRunNext component
from langchain_core.prompts import PromptTemplate
from langflow import CustomComponent
from langflow.field_typing import BaseLanguageModel, Prompt
class ShouldRunNext(CustomComponent):
display_name = "Should Run Next"
description = "Decides whether to run the next component."
def build_config(self):
return {
"prompt": {
"display_name": "Prompt",
"info": "The prompt to use for the decision. It should generate a boolean response (True or False).",
},
"llm": {
"display_name": "LLM",
"info": "The language model to use for the decision.",
},
}
def build(self, template: Prompt, llm: BaseLanguageModel, **kwargs) -> bool:
# This is a simple component that always returns True
prompt_template = PromptTemplate.from_template(template)
attributes_to_check = ["text", "page_content"]
for key, value in kwargs.items():
for attribute in attributes_to_check:
if hasattr(value, attribute):
kwargs[key] = getattr(value, attribute)
chain = prompt_template | llm
result = chain.invoke(kwargs)
if hasattr(result, "content") and isinstance(result.content, str):
result = result.content
elif isinstance(result, str):
result = result
else:
result = result.get("response")
if result.lower() not in ["true", "false"]:
raise ValueError("The prompt should generate a boolean response (True or False).")
# The string should be the words true or false
# if not raise an error
bool_result = result.lower() == "true"
return bool_result

View file

@ -0,0 +1,109 @@
import asyncio
from typing import List, Optional, Union
import httpx
import requests
from langflow import CustomComponent
from langflow.schema import Record
from langflow.services.database.models.base import orjson_dumps
class APIRequest(CustomComponent):
display_name: str = "API Request"
description: str = "Make an HTTP request to the given URL."
output_types: list[str] = ["Record"]
documentation: str = "https://docs.langflow.org/components/utilities#api-request"
beta: bool = True
field_config = {
"url": {"display_name": "URL", "info": "The URL to make the request to."},
"method": {
"display_name": "Method",
"info": "The HTTP method to use.",
"field_type": "str",
"options": ["GET", "POST", "PATCH", "PUT"],
"value": "GET",
},
"headers": {
"display_name": "Headers",
"info": "The headers to send with the request.",
},
"record": {
"display_name": "Record",
"info": "The record to send with the request (for POST, PATCH, PUT).",
},
"timeout": {
"display_name": "Timeout",
"field_type": "int",
"info": "The timeout to use for the request.",
"value": 5,
},
}
async def make_request(
self,
session: requests.Session,
method: str,
url: str,
headers: Optional[dict] = None,
record: Optional[Record] = None,
timeout: int = 5,
) -> Record:
method = method.upper()
if method not in ["GET", "POST", "PATCH", "PUT"]:
raise ValueError(f"Unsupported method: {method}")
data = record.text if record else None
try:
async with httpx.AsyncClient() as client:
response = await client.request(
method, url, headers=headers, content=data, timeout=timeout
)
try:
response_json = response.json()
result = orjson_dumps(response_json, indent_2=False)
except Exception:
result = response.text
return Record(
text=result,
data={
"source": url,
"headers": headers,
"status_code": response.status_code,
},
)
except httpx.TimeoutException:
return Record(
text="Request Timed Out",
data={"source": url, "headers": headers, "status_code": 408},
)
except Exception as exc:
return Record(
text=str(exc),
data={"source": url, "headers": headers, "status_code": 500},
)
async def build(
self,
method: str,
url: List[str],
headers: Optional[dict] = None,
record: Optional[Union[Record, List[Record]]] = None,
timeout: int = 5,
) -> List[Record]:
if headers is None:
headers = {}
urls = url if isinstance(url, list) else [url]
records = (
record
if isinstance(record, list)
else [record] if record else [None] * len(urls)
)
results = await asyncio.gather(
*[
self.make_request(method, u, headers, doc, timeout)
for u, doc in zip(urls, records)
]
)
return results

View file

@ -4,6 +4,7 @@ from langflow.field_typing import Data
class Component(CustomComponent):
documentation: str = "http://docs.langflow.org/components/custom"
icon = "custom_components"
def build_config(self):
return {"param": {"display_name": "Parameter"}}

View file

@ -1,75 +0,0 @@
from typing import Optional, Text
import requests
from langchain_core.documents import Document
from langflow import CustomComponent
from langflow.services.database.models.base import orjson_dumps
class GetRequest(CustomComponent):
display_name: str = "GET Request"
description: str = "Make a GET request to the given URL."
output_types: list[str] = ["Document"]
documentation: str = "https://docs.langflow.org/components/utilities#get-request"
beta: bool = True
field_config = {
"url": {
"display_name": "URL",
"info": "The URL to make the request to",
"is_list": True,
},
"headers": {
"display_name": "Headers",
"info": "The headers to send with the request.",
},
"code": {"show": False},
"timeout": {
"display_name": "Timeout",
"field_type": "int",
"info": "The timeout to use for the request.",
"value": 5,
},
}
def get_document(self, session: requests.Session, url: str, headers: Optional[dict], timeout: int) -> Document:
try:
response = session.get(url, headers=headers, timeout=int(timeout))
try:
response_json = response.json()
result = orjson_dumps(response_json, indent_2=False)
except Exception:
result = response.text
self.repr_value = result
return Document(
page_content=result,
metadata={
"source": url,
"headers": headers,
"status_code": response.status_code,
},
)
except requests.Timeout:
return Document(
page_content="Request Timed Out",
metadata={"source": url, "headers": headers, "status_code": 408},
)
except Exception as exc:
return Document(
page_content=Text(exc),
metadata={"source": url, "headers": headers, "status_code": 500},
)
def build(
self,
url: str,
headers: Optional[dict] = None,
timeout: int = 5,
) -> list[Document]:
if headers is None:
headers = {}
urls = url if isinstance(url, list) else [url]
with requests.Session() as session:
documents = [self.get_document(session, u, headers, timeout) for u in urls]
self.repr_value = documents
return documents

View file

@ -0,0 +1,19 @@
from typing import List
from langflow import CustomComponent
from langflow.schema import Record
class ListFlowsComponent(CustomComponent):
display_name = "List Flows"
description = "A component to list all available flows."
def build_config(self):
return {}
def build(
self,
) -> List[Record]:
flows = self.list_flows()
self.status = flows
return flows

View file

@ -1,78 +0,0 @@
from typing import Optional, Text
import requests
from langchain_core.documents import Document
from langflow import CustomComponent
from langflow.services.database.models.base import orjson_dumps
class PostRequest(CustomComponent):
display_name: str = "POST Request"
description: str = "Make a POST request to the given URL."
output_types: list[str] = ["Document"]
documentation: str = "https://docs.langflow.org/components/utilities#post-request"
beta: bool = True
field_config = {
"url": {"display_name": "URL", "info": "The URL to make the request to."},
"headers": {
"display_name": "Headers",
"info": "The headers to send with the request.",
},
"code": {"show": False},
"document": {"display_name": "Document"},
}
def post_document(
self,
session: requests.Session,
document: Document,
url: str,
headers: Optional[dict] = None,
) -> Document:
try:
response = session.post(url, headers=headers, data=document.page_content)
try:
response_json = response.json()
result = orjson_dumps(response_json, indent_2=False)
except Exception:
result = response.text
self.repr_value = result
return Document(
page_content=result,
metadata={
"source": url,
"headers": headers,
"status_code": response,
},
)
except Exception as exc:
return Document(
page_content=Text(exc),
metadata={
"source": url,
"headers": headers,
"status_code": 500,
},
)
def build(
self,
document: Document,
url: str,
headers: Optional[dict] = None,
) -> list[Document]:
if headers is None:
headers = {}
if not isinstance(document, list) and isinstance(document, Document):
documents: list[Document] = [document]
elif isinstance(document, list) and all(isinstance(doc, Document) for doc in document):
documents = document
else:
raise ValueError("document must be a Document or a list of Documents")
with requests.Session() as session:
documents = [self.post_document(session, doc, url, headers) for doc in documents]
self.repr_value = documents
return documents

View file

@ -1,89 +0,0 @@
from typing import List, Optional, Text
import requests
from langchain_core.documents import Document
from langflow import CustomComponent
from langflow.services.database.models.base import orjson_dumps
class UpdateRequest(CustomComponent):
display_name: str = "Update Request"
description: str = "Make a PATCH request to the given URL."
output_types: list[str] = ["Document"]
documentation: str = "https://docs.langflow.org/components/utilities#update-request"
beta: bool = True
field_config = {
"url": {"display_name": "URL", "info": "The URL to make the request to."},
"headers": {
"display_name": "Headers",
"field_type": "NestedDict",
"info": "The headers to send with the request.",
},
"code": {"show": False},
"document": {"display_name": "Document"},
"method": {
"display_name": "Method",
"field_type": "str",
"info": "The HTTP method to use.",
"options": ["PATCH", "PUT"],
"value": "PATCH",
},
}
def update_document(
self,
session: requests.Session,
document: Document,
url: str,
headers: Optional[dict] = None,
method: str = "PATCH",
) -> Document:
try:
if method == "PATCH":
response = session.patch(url, headers=headers, data=document.page_content)
elif method == "PUT":
response = session.put(url, headers=headers, data=document.page_content)
else:
raise ValueError(f"Unsupported method: {method}")
try:
response_json = response.json()
result = orjson_dumps(response_json, indent_2=False)
except Exception:
result = response.text
self.repr_value = result
return Document(
page_content=result,
metadata={
"source": url,
"headers": headers,
"status_code": response.status_code,
},
)
except Exception as exc:
return Document(
page_content=Text(exc),
metadata={"source": url, "headers": headers, "status_code": 500},
)
def build(
self,
method: str,
document: Document,
url: str,
headers: Optional[dict] = None,
) -> List[Document]:
if headers is None:
headers = {}
if not isinstance(document, list) and isinstance(document, Document):
documents: list[Document] = [document]
elif isinstance(document, list) and all(isinstance(doc, Document) for doc in document):
documents = document
else:
raise ValueError("document must be a Document or a list of Documents")
with requests.Session() as session:
documents = [self.update_document(session, doc, url, headers, method) for doc in documents]
self.repr_value = documents
return documents

View file

@ -357,6 +357,11 @@ class Graph:
for vertex_id in removed_vertex_ids:
self.remove_vertex(vertex_id)
# The order here matters because adding the vertex is required
# if any of them have edges that point to any of the new vertices
# By adding them first, them adding the edges we ensure that the
# edges have valid vertices to point to
# Add new vertices
for vertex_id in new_vertex_ids:
new_vertex = other.get_vertex(vertex_id)
@ -366,6 +371,8 @@ class Graph:
for vertex_id in new_vertex_ids:
new_vertex = other.get_vertex(vertex_id)
self._update_edges(new_vertex)
# Graph is set at the end because the edges come from the graph
# and the other graph is where the new edges and vertices come from
new_vertex.graph = self
# Update existing vertices that have changed
@ -395,9 +402,9 @@ class Graph:
vertex.params = {}
vertex._build_params()
vertex.graph = self
# If the vertex is pinned, we don't want
# If the vertex is frozen, we don't want
# to reset the results nor the _built attribute
if not vertex.pinned:
if not vertex.frozen:
vertex._built = False
vertex.result = None
vertex.artifacts = {}
@ -410,7 +417,7 @@ class Graph:
for vid in [edge.source_id, edge.target_id]:
if vid in self.vertex_map:
_vertex = self.vertex_map[vid]
if not _vertex.pinned:
if not _vertex.frozen:
_vertex._build_params()
def _add_vertex(self, vertex: Vertex) -> None:

View file

@ -191,7 +191,7 @@ class Vertex:
self.base_type = state["base_type"]
self.is_task = state["is_task"]
self.id = state["id"]
self.pinned = state.get("pinned", False)
self.frozen = state.get("frozen", False)
self._parse_data()
if "_built_object" in state:
self._built_object = state["_built_object"]
@ -217,7 +217,7 @@ class Vertex:
self.data = self._data["data"]
self.output = self.data["node"]["base_classes"]
self.display_name = self.data["node"].get("display_name", self.id.split("-")[0])
self.pinned = self.data["node"].get("pinned", False)
self.frozen = self.data["node"].get("frozen", False)
self.selected_output_type = self.data["node"].get("selected_output_type")
self.is_input = self.data["node"].get("is_input") or self.is_input
self.is_output = self.data["node"].get("is_output") or self.is_output
@ -676,7 +676,7 @@ class Vertex:
self.build_inactive()
return
if self.pinned and self._built:
if self.frozen and self._built:
return self.get_requester_result(requester)
elif self._built and requester is not None:
# This means that the vertex has already been built

View file

@ -224,7 +224,7 @@ class ChainVertex(Vertex):
if isinstance(value, PromptVertex):
# Build the PromptVertex, passing the tools if available
tools = kwargs.get("tools", None)
self.params[key] = value.build(tools=tools, pinned=force)
self.params[key] = value.build(tools=tools, frozen=force)
await self._build(user_id=user_id)

View file

@ -37,7 +37,7 @@ ATTR_FUNC_MAPPING: dict[str, Callable] = {
"beta": getattr_return_bool,
"documentation": getattr_return_str,
"icon": validate_icon,
"pinned": getattr_return_bool,
"frozen": getattr_return_bool,
"is_input": getattr_return_bool,
"is_output": getattr_return_bool,
}

View file

@ -21,7 +21,9 @@ class ComponentFunctionEntrypointNameNullError(HTTPException):
class Component:
ERROR_CODE_NULL: ClassVar[str] = "Python code must be provided."
ERROR_FUNCTION_ENTRYPOINT_NAME_NULL: ClassVar[str] = "The name of the entrypoint function must be provided."
ERROR_FUNCTION_ENTRYPOINT_NAME_NULL: ClassVar[str] = (
"The name of the entrypoint function must be provided."
)
code: Optional[str] = None
_function_entrypoint_name: str = "build"
@ -39,7 +41,8 @@ class Component:
def __setattr__(self, key, value):
if key == "_user_id" and hasattr(self, "_user_id"):
warnings.warn("user_id is immutable and cannot be changed.")
super().__setattr__(key, value)
else:
super().__setattr__(key, value)
@cachedmethod(cache=operator.attrgetter("cache"))
def get_code_tree(self, code: str):

View file

@ -58,8 +58,8 @@ class CustomComponent(Component):
"""The field configuration of the component. Defaults to an empty dictionary."""
field_order: Optional[List[str]] = None
"""The field order of the component. Defaults to an empty list."""
pinned: Optional[bool] = False
"""The default pinned state of the component. Defaults to False."""
frozen: Optional[bool] = False
"""The default frozen state of the component. Defaults to False."""
build_parameters: Optional[dict] = None
"""The build parameters of the component. Defaults to None."""
selected_output_type: Optional[str] = None
@ -73,6 +73,7 @@ class CustomComponent(Component):
user_id: Optional[Union[UUID, str]] = None
status: Optional[Any] = None
"""The status of the component. This is displayed on the frontend. Defaults to None."""
_flows_records: Optional[List[Record]] = None
def update_state(self, name: str, value: Any):
try:
@ -344,14 +345,34 @@ class CustomComponent(Component):
async def run_flow(
self,
input_value: Union[str, list[str]],
flow_id: str,
flow_id: Optional[str] = None,
flow_name: Optional[str] = None,
tweaks: Optional[dict] = None,
) -> Any:
if not flow_id and not flow_name:
raise ValueError("Flow ID or Flow Name is required")
if not self._flows_records:
self.list_flows()
if not flow_id and self._flows_records:
flow_ids = [
flow.data["id"]
for flow in self._flows_records
if flow.data["name"] == flow_name
]
if not flow_ids:
raise ValueError(f"Flow {flow_name} not found")
elif len(flow_ids) > 1:
raise ValueError(f"Multiple flows found with the name {flow_name}")
flow_id = flow_ids[0]
if not flow_id:
raise ValueError(f"Flow {flow_name} not found")
graph = await self.load_flow(flow_id, tweaks)
input_value_dict = {"input_value": input_value}
return await graph.run(input_value_dict, stream=False)
def list_flows(self, *, get_session: Optional[Callable] = None) -> List[Flow]:
def list_flows(self, *, get_session: Optional[Callable] = None) -> List[Record]:
if not self._user_id:
raise ValueError("Session is invalid")
try:
@ -359,11 +380,16 @@ class CustomComponent(Component):
db_service = get_db_service()
with get_session(db_service) as session:
flows = session.exec(
select(Flow).where(Flow.user_id == self._user_id)
select(Flow)
.where(Flow.user_id == self._user_id)
.where(Flow.is_component == False)
).all()
return flows
flows_records = [flow.to_record() for flow in flows]
self._flows_records = flows_records
return flows_records
except Exception as e:
raise ValueError("Session is invalid") from e
raise ValueError(f"Error listing flows: {e}")
def build(self, *args: Any, **kwargs: Any) -> Any:
raise NotImplementedError

View file

@ -7,6 +7,8 @@ from typing import Any, Dict, List, Optional, Union
from uuid import UUID
from fastapi import HTTPException
from loguru import logger
from langflow.field_typing.range_spec import RangeSpec
from langflow.interface.custom.attributes import ATTR_FUNC_MAPPING
from langflow.interface.custom.code_parser.utils import extract_inner_type
@ -23,7 +25,6 @@ from langflow.template.frontend_node.custom_components import (
)
from langflow.utils import validate
from langflow.utils.util import get_base_classes
from loguru import logger
def add_output_types(
@ -249,10 +250,12 @@ def run_build_config(
# Allow user to build TemplateField as well
# as a dict with the same keys as TemplateField
field_dict = get_field_dict(field)
# This has to be done to set refresh if options or value are callable
update_field_dict(field_dict)
if update_field is not None and field_name != update_field:
continue
try:
update_field_dict(field_dict)
update_field_dict(field_dict, call=True)
build_config[field_name] = field_dict
except Exception as exc:
logger.error(f"Error while getting build_config: {str(exc)}")
@ -399,15 +402,17 @@ def build_custom_components(settings_service):
return custom_components_from_file
def update_field_dict(field_dict):
def update_field_dict(field_dict, call=False):
"""Update the field dictionary by calling options() or value() if they are callable"""
if "options" in field_dict and callable(field_dict["options"]):
field_dict["options"] = field_dict["options"]()
if call:
field_dict["options"] = field_dict["options"]()
# Also update the "refresh" key
field_dict["refresh"] = True
if "value" in field_dict and callable(field_dict["value"]):
field_dict["value"] = field_dict["value"]()
if call:
field_dict["value"] = field_dict["value"]()
field_dict["refresh"] = True
# Let's check if "range_spec" is a RangeSpec object

View file

@ -30,6 +30,7 @@ from langflow.interface.retrievers.base import retriever_creator
from langflow.interface.toolkits.base import toolkits_creator
from langflow.interface.utils import load_file_into_dict
from langflow.interface.wrappers.base import wrapper_creator
from langflow.schema.schema import Record
from langflow.utils import validate
if TYPE_CHECKING:
@ -143,9 +144,13 @@ async def instantiate_based_on_type(
return class_object(**params)
async def instantiate_custom_component(node_type, class_object, params, user_id, vertex):
async def instantiate_custom_component(
node_type, class_object, params, user_id, vertex
):
params_copy = params.copy()
class_object: Type["CustomComponent"] = eval_custom_component_code(params_copy.pop("code"))
class_object: Type["CustomComponent"] = eval_custom_component_code(
params_copy.pop("code")
)
custom_component: "CustomComponent" = class_object(
user_id=user_id,
parameters=params_copy,
@ -165,8 +170,10 @@ async def instantiate_custom_component(node_type, class_object, params, user_id,
else:
# Call the build method directly if it's sync
build_result = custom_component.build(**params_copy)
return custom_component, build_result, {"repr": custom_component.custom_repr()}
custom_repr = custom_component.custom_repr()
if not custom_repr and isinstance(build_result, (dict, Record, str)):
custom_repr = build_result
return custom_component, build_result, {"repr": custom_repr}
def instantiate_wrapper(node_type, class_object, params):
@ -219,7 +226,9 @@ def instantiate_memory(node_type, class_object, params):
# I want to catch a specific attribute error that happens
# when the object does not have a cursor attribute
except Exception as exc:
if "object has no attribute 'cursor'" in str(exc) or 'object has no field "conn"' in str(exc):
if "object has no attribute 'cursor'" in str(
exc
) or 'object has no field "conn"' in str(exc):
raise AttributeError(
(
"Failed to build connection to database."
@ -262,7 +271,9 @@ def instantiate_agent(node_type, class_object: Type[agent_module.Agent], params:
if class_method := getattr(class_object, method, None):
agent = class_method(**params)
tools = params.get("tools", [])
return AgentExecutor.from_agent_and_tools(agent=agent, tools=tools, handle_parsing_errors=True)
return AgentExecutor.from_agent_and_tools(
agent=agent, tools=tools, handle_parsing_errors=True
)
return load_agent_executor(class_object, params)
@ -318,7 +329,11 @@ def instantiate_embedding(node_type, class_object, params: Dict):
try:
return class_object(**params)
except ValidationError:
params = {key: value for key, value in params.items() if key in class_object.model_fields}
params = {
key: value
for key, value in params.items()
if key in class_object.model_fields
}
return class_object(**params)
@ -330,7 +345,9 @@ def instantiate_vectorstore(class_object: Type[VectorStore], params: Dict):
if "texts" in params:
params["documents"] = params.pop("texts")
if "documents" in params:
params["documents"] = [doc for doc in params["documents"] if isinstance(doc, Document)]
params["documents"] = [
doc for doc in params["documents"] if isinstance(doc, Document)
]
if initializer := vecstore_initializer.get(class_object.__name__):
vecstore = initializer(class_object, params)
else:
@ -345,7 +362,9 @@ def instantiate_vectorstore(class_object: Type[VectorStore], params: Dict):
return vecstore
def instantiate_documentloader(node_type: str, class_object: Type[BaseLoader], params: Dict):
def instantiate_documentloader(
node_type: str, class_object: Type[BaseLoader], params: Dict
):
if "file_filter" in params:
# file_filter will be a string but we need a function
# that will be used to filter the files using file_filter
@ -354,13 +373,17 @@ def instantiate_documentloader(node_type: str, class_object: Type[BaseLoader], p
# in x and if it is, we will return True
file_filter = params.pop("file_filter")
extensions = file_filter.split(",")
params["file_filter"] = lambda x: any(extension.strip() in x for extension in extensions)
params["file_filter"] = lambda x: any(
extension.strip() in x for extension in extensions
)
metadata = params.pop("metadata", None)
if metadata and isinstance(metadata, str):
try:
metadata = orjson.loads(metadata)
except json.JSONDecodeError as exc:
raise ValueError("The metadata you provided is not a valid JSON string.") from exc
raise ValueError(
"The metadata you provided is not a valid JSON string."
) from exc
if node_type == "WebBaseLoader":
if web_path := params.pop("web_path", None):
@ -393,12 +416,16 @@ def instantiate_textsplitter(
"Try changing the chunk_size of the Text Splitter."
) from exc
if ("separator_type" in params and params["separator_type"] == "Text") or "separator_type" not in params:
if (
"separator_type" in params and params["separator_type"] == "Text"
) or "separator_type" not in params:
params.pop("separator_type", None)
# separators might come in as an escaped string like \\n
# so we need to convert it to a string
if "separators" in params:
params["separators"] = params["separators"].encode().decode("unicode-escape")
params["separators"] = (
params["separators"].encode().decode("unicode-escape")
)
text_splitter = class_object(**params)
else:
from langchain.text_splitter import Language
@ -425,7 +452,8 @@ def replace_zero_shot_prompt_with_prompt_template(nodes):
tools = [
tool
for tool in nodes
if tool["type"] != "chatOutputNode" and "Tool" in tool["data"]["node"]["base_classes"]
if tool["type"] != "chatOutputNode"
and "Tool" in tool["data"]["node"]["base_classes"]
]
node["data"] = build_prompt_template(prompt=node["data"], tools=tools)
break
@ -439,7 +467,9 @@ def load_agent_executor(agent_class: type[agent_module.Agent], params, **kwargs)
# agent has hidden args for memory. might need to be support
# memory = params["memory"]
# if allowed_tools is not a list or set, make it a list
if not isinstance(allowed_tools, (list, set)) and isinstance(allowed_tools, BaseTool):
if not isinstance(allowed_tools, (list, set)) and isinstance(
allowed_tools, BaseTool
):
allowed_tools = [allowed_tools]
tool_names = [tool.name for tool in allowed_tools]
# Agent class requires an output_parser but Agent classes
@ -467,7 +497,10 @@ def build_prompt_template(prompt, tools):
format_instructions = prompt["node"]["template"]["format_instructions"]["value"]
tool_strings = "\n".join(
[f"{tool['data']['node']['name']}: {tool['data']['node']['description']}" for tool in tools]
[
f"{tool['data']['node']['name']}: {tool['data']['node']['description']}"
for tool in tools
]
)
tool_names = ", ".join([tool["data"]["node"]["name"] for tool in tools])
format_instructions = format_instructions.format(tool_names=tool_names)

View file

@ -7,6 +7,8 @@ from uuid import UUID, uuid4
from pydantic import field_serializer, field_validator
from sqlmodel import JSON, Column, Field, Relationship, SQLModel
from langflow.schema.schema import Record
if TYPE_CHECKING:
from langflow.services.database.models.user import User
@ -16,7 +18,9 @@ class FlowBase(SQLModel):
description: Optional[str] = Field(index=True, nullable=True, default=None)
data: Optional[Dict] = Field(default=None, nullable=True)
is_component: Optional[bool] = Field(default=False, nullable=True)
updated_at: Optional[datetime] = Field(default_factory=datetime.utcnow, nullable=True)
updated_at: Optional[datetime] = Field(
default_factory=datetime.utcnow, nullable=True
)
folder: Optional[str] = Field(default=None, nullable=True)
@field_validator("data")
@ -57,6 +61,18 @@ class Flow(FlowBase, table=True):
user_id: UUID = Field(index=True, foreign_key="user.id", nullable=True)
user: "User" = Relationship(back_populates="flows")
def to_record(self):
serialized = self.model_dump()
data = {
"id": serialized.pop("id"),
"data": serialized.pop("data"),
"name": serialized.pop("name"),
"description": serialized.pop("description"),
"updated_at": serialized.pop("updated_at"),
}
record = Record(text=data.get("name"), data=data)
return record
class FlowCreate(FlowBase):
user_id: Optional[UUID] = None

View file

@ -71,8 +71,8 @@ class FrontendNode(BaseModel):
"""Full path of the frontend node."""
field_formatters: FieldFormatters = Field(default_factory=FieldFormatters)
"""Field formatters for the frontend node."""
pinned: bool = False
"""Whether the frontend node is pinned."""
frozen: bool = False
"""Whether the frontend node is frozen."""
beta: bool = False
error: Optional[str] = None
@ -171,7 +171,9 @@ class FrontendNode(BaseModel):
return _type
@staticmethod
def handle_special_field(field, key: str, _type: str, SPECIAL_FIELD_HANDLERS) -> str:
def handle_special_field(
field, key: str, _type: str, SPECIAL_FIELD_HANDLERS
) -> str:
"""Handles special field by using the respective handler if present."""
handler = SPECIAL_FIELD_HANDLERS.get(key)
return handler(field) if handler else _type
@ -182,7 +184,11 @@ class FrontendNode(BaseModel):
if "dict" in _type.lower() and field.name == "dict_":
field.field_type = "file"
field.file_types = [".json", ".yaml", ".yml"]
elif _type.startswith("Dict") or _type.startswith("Mapping") or _type.startswith("dict"):
elif (
_type.startswith("Dict")
or _type.startswith("Mapping")
or _type.startswith("dict")
):
field.field_type = "dict"
return _type
@ -193,7 +199,9 @@ class FrontendNode(BaseModel):
field.value = value["default"]
@staticmethod
def handle_specific_field_values(field: TemplateField, key: str, name: Optional[str] = None) -> None:
def handle_specific_field_values(
field: TemplateField, key: str, name: Optional[str] = None
) -> None:
"""Handles specific field values for certain fields."""
if key == "headers":
field.value = """{"Authorization": "Bearer <token>"}"""
@ -201,7 +209,9 @@ class FrontendNode(BaseModel):
FrontendNode._handle_api_key_specific_field_values(field, key, name)
@staticmethod
def _handle_model_specific_field_values(field: TemplateField, key: str, name: Optional[str] = None) -> None:
def _handle_model_specific_field_values(
field: TemplateField, key: str, name: Optional[str] = None
) -> None:
"""Handles specific field values related to models."""
model_dict = {
"OpenAI": constants.OPENAI_MODELS,
@ -214,7 +224,9 @@ class FrontendNode(BaseModel):
field.is_list = True
@staticmethod
def _handle_api_key_specific_field_values(field: TemplateField, key: str, name: Optional[str] = None) -> None:
def _handle_api_key_specific_field_values(
field: TemplateField, key: str, name: Optional[str] = None
) -> None:
"""Handles specific field values related to API keys."""
if "api_key" in key and "OpenAI" in str(name):
field.display_name = "OpenAI API Key"
@ -254,7 +266,10 @@ class FrontendNode(BaseModel):
@staticmethod
def should_be_password(key: str, show: bool) -> bool:
"""Determines whether the field should be a password field."""
return any(text in key.lower() for text in {"password", "token", "api", "key"}) and show
return (
any(text in key.lower() for text in {"password", "token", "api", "key"})
and show
)
@staticmethod
def should_be_multiline(key: str) -> bool:

View file

@ -15,7 +15,7 @@ from langflow.template.template.base import Template
class MemoryFrontendNode(FrontendNode):
pinned: bool = True
frozen: bool = True
def add_extra_fields(self) -> None:
# chat history should have another way to add common field?
@ -80,7 +80,9 @@ class MemoryFrontendNode(FrontendNode):
field.show = True
field.advanced = False
field.value = ""
field.info = INPUT_KEY_INFO if field.name == "input_key" else OUTPUT_KEY_INFO
field.info = (
INPUT_KEY_INFO if field.name == "input_key" else OUTPUT_KEY_INFO
)
if field.name == "memory_key":
field.value = "chat_history"

View file

@ -16,6 +16,7 @@ import PromptAreaComponent from "../../../../components/promptComponent";
import TextAreaComponent from "../../../../components/textAreaComponent";
import ToggleShadComponent from "../../../../components/toggleShadComponent";
import { Button } from "../../../../components/ui/button";
import { RefreshButton } from "../../../../components/ui/refreshButton";
import {
INPUT_HANDLER_HOVER,
LANGFLOW_SUPPORTED_TYPES,
@ -68,7 +69,7 @@ export default function ParameterComponent({
const nodes = useFlowStore((state) => state.nodes);
const edges = useFlowStore((state) => state.edges);
const setNode = useFlowStore((state) => state.setNode);
const [isLoading, setIsLoading] = useState(false);
const flow = currentFlow?.data?.nodes ?? null;
const groupedEdge = useRef(null);
@ -85,7 +86,12 @@ export default function ParameterComponent({
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const handleUpdateValues = async (name: string, data: NodeDataType) => {
const handleUpdateValues = async (
name: string,
data: NodeDataType,
delayAnimation: boolean = true
) => {
setIsLoading(true);
const code = data.node?.template["code"]?.value;
if (!code) {
console.error("Code not found in the template");
@ -95,13 +101,48 @@ export default function ParameterComponent({
try {
const res = await postCustomComponentUpdate(code, name);
if (res.status === 200 && data.node?.template) {
data.node!.template[name] = res.data.template[name];
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
newNode.data.node.template[name] = res.data.template[name];
return newNode;
});
}
} catch (err) {
setErrorData(err as { title: string; list?: Array<string> });
}
renderTooltips();
if (delayAnimation) {
try {
// Wait for at least 500 milliseconds
await new Promise((resolve) => setTimeout(resolve, 500));
// Continue with the request
// If the request takes longer than 500 milliseconds, it will not wait an additional 500 milliseconds
} catch (error) {
console.error("Error occurred while waiting for refresh:", error);
} finally {
setIsLoading(false);
}
} else setIsLoading(false);
};
useEffect(() => {
function fetchData() {
if (
data.node?.template[name]?.refresh &&
Object.keys(data.node?.template[name]?.options ?? {}).length === 0
) {
handleUpdateValues(name, data, false);
}
}
fetchData();
}, []);
const handleOnNewValue = (
newValue: string | string[] | boolean | Object[]
): void => {
@ -314,16 +355,25 @@ export default function ParameterComponent({
<div
className={
"w-full truncate text-sm" +
(left ? "" : " text-end") +
(left ? "" : " flex items-center justify-end gap-2") +
(info !== "" ? " flex items-center" : "")
}
>
{!left && data.node?.frozen && (
<div>
<IconComponent className="h-5 w-5 text-ice" name={"Snowflake"} />
</div>
)}
{proxy ? (
<ShadTooltip content={<span>{proxy.id}</span>}>
<span>{title}</span>
<span className={!left && data.node?.frozen ? " text-ice" : ""}>
{title}
</span>
</ShadTooltip>
) : (
title
<span className={!left && data.node?.frozen ? " text-ice" : ""}>
{title}
</span>
)}
<span className={(info === "" ? "" : "ml-1 ") + " text-status-red"}>
{required ? " *" : ""}
@ -390,16 +440,31 @@ export default function ParameterComponent({
!data.node?.template[name].options ? (
<div className="mt-2 w-full">
{data.node?.template[name].list ? (
<InputListComponent
disabled={disabled}
value={
!data.node.template[name].value ||
data.node.template[name].value === ""
? [""]
: data.node.template[name].value
}
onChange={handleOnNewValue}
/>
<div className="w-5/6 flex-grow">
<InputListComponent
disabled={disabled}
value={
!data.node.template[name].value ||
data.node.template[name].value === ""
? [""]
: data.node.template[name].value
}
onChange={handleOnNewValue}
/>
{data.node?.template[name].refresh && (
<div className="w-1/6">
<RefreshButton
isLoading={isLoading}
disabled={disabled}
name={name}
data={data}
className="extra-side-bar-buttons ml-2 mt-1"
handleUpdateValues={handleUpdateValues}
id={"refresh-button-" + name}
/>
</div>
)}
</div>
) : data.node?.template[name].multiline ? (
<TextAreaComponent
disabled={disabled}
@ -420,14 +485,17 @@ export default function ParameterComponent({
/>
</div>
{data.node?.template[name].refresh && (
<button
className="extra-side-bar-buttons ml-2 mt-1 w-1/6"
onClick={() => {
handleUpdateValues(name, data);
}}
>
<IconComponent name="RefreshCcw" />
</button>
<div className="w-1/6">
<RefreshButton
isLoading={isLoading}
disabled={disabled}
name={name}
data={data}
className="extra-side-bar-buttons ml-2 mt-1"
handleUpdateValues={handleUpdateValues}
id={"refresh-button-" + name}
/>
</div>
)}
</div>
)}
@ -454,11 +522,14 @@ export default function ParameterComponent({
</div>
) : left === true &&
type === "str" &&
data.node?.template[name].options ? (
(data.node?.template[name].options ||
data.node?.template[name]?.refresh) ? (
// TODO: Improve CSS
<div className="mt-2 flex w-full items-center">
<div className="w-5/6 flex-grow">
<Dropdown
disabled={disabled}
isLoading={isLoading}
options={data.node.template[name].options}
onSelect={handleOnNewValue}
value={data.node.template[name].value ?? "Choose an option"}
@ -466,14 +537,17 @@ export default function ParameterComponent({
/>
</div>
{data.node?.template[name].refresh && (
<button
className="extra-side-bar-buttons ml-2 mt-1 w-1/6"
onClick={() => {
handleUpdateValues(name, data);
}}
>
<IconComponent name="RefreshCcw" />
</button>
<div className="w-1/6">
<RefreshButton
isLoading={isLoading}
disabled={disabled}
name={name}
data={data}
className="extra-side-bar-buttons ml-2 mt-1"
handleUpdateValues={handleUpdateValues}
id={"refresh-button-" + name}
/>
</div>
)}
</div>
) : left === true && type === "code" ? (

View file

@ -108,7 +108,7 @@ export default function GenericNode({
if (duration === undefined) {
return "";
} else {
return `Duration: ${duration}`;
return `${duration}`;
}
};
const durationString = getDurationString(validationStatus?.data.duration);
@ -758,15 +758,6 @@ export default function GenericNode({
showNode={showNode}
/>
)}
<div>
{lastRunTime && (
<div className="flex justify-center text-muted-foreground">
{lastRunTime.split("\n").map((line, index) => (
<div key={index}>{line}</div>
))}
</div>
)}
</div>
</>
</div>
)}

View file

@ -5,6 +5,8 @@ import { classNames } from "../../utils/utils";
import IconComponent from "../genericIconComponent";
export default function Dropdown({
disabled,
isLoading,
value,
options,
onSelect,
@ -27,6 +29,7 @@ export default function Dropdown({
<>
<Listbox
value={internalValue}
disabled={disabled}
onChange={(value) => {
setInternalValue(value);
onSelect(value);
@ -129,11 +132,17 @@ export default function Dropdown({
</>
) : (
<>
<div>
<span className="text-sm italic">
No parameters are available for display.
</span>
</div>
{(!isLoading && (
<div>
<span className="text-sm italic">
No parameters are available for display.
</span>
</div>
)) || (
<div>
<span className="text-sm italic">Loading...</span>
</div>
)}
</>
)}
</>

View file

@ -1,4 +1,5 @@
import { forwardRef } from "react";
import dynamicIconImports from "lucide-react/dynamicIconImports";
import { Suspense, forwardRef, lazy } from "react";
import { IconComponentProps } from "../../types/components";
import { nodeIconsLucide } from "../../utils/styleUtils";
@ -14,7 +15,13 @@ const ForwardedIconComponent = forwardRef(
}: IconComponentProps,
ref
) => {
const TargetIcon = nodeIconsLucide[name] ?? nodeIconsLucide["unknown"];
let TargetIcon = nodeIconsLucide[name];
if (!TargetIcon) {
// check if name exists in dynamicIconImports
if (!dynamicIconImports[name]) {
TargetIcon = nodeIconsLucide["unknown"];
} else TargetIcon = lazy(dynamicIconImports[name]);
}
const style = {
strokeWidth: strokeWidth ?? 1.5,
@ -22,13 +29,21 @@ const ForwardedIconComponent = forwardRef(
...(iconColor && { color: iconColor, stroke: stroke }),
};
if (!TargetIcon) {
return null; // Render nothing until the icon is loaded
}
const fallback = (
<div style={{ background: "#ddd", width: 24, height: 24 }} />
);
return (
<TargetIcon
className={className}
style={style}
ref={ref}
data-testid={id ? `${id}-${name}` : `icon-${name}`}
/>
<Suspense fallback={fallback}>
<TargetIcon
className={className}
style={style}
ref={ref}
data-testid={id ? `${id}-${name}` : `icon-${name}`}
/>
</Suspense>
);
}
);

View file

@ -4,7 +4,7 @@ import * as React from "react";
import { cn } from "../../utils/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
{
variants: {
variant: {

View file

@ -0,0 +1,55 @@
import IconComponent from "../../components/genericIconComponent";
import { NodeDataType } from "../../types/flow";
import { cn } from "../../utils/utils";
import { Button } from "./button";
function RefreshButton({
isLoading,
disabled,
name,
data,
handleUpdateValues,
className,
id,
}: {
isLoading: boolean;
disabled: boolean;
name: string;
data: NodeDataType;
className?: string;
handleUpdateValues: (name: string, data: NodeDataType) => void;
id: string;
}) {
const handleClick = async () => {
if (disabled) return;
handleUpdateValues(name, data);
};
const classNames = cn(className, disabled ? "cursor-not-allowed" : "");
// icon class name should take into account the disabled state and the loading state
const disabledIconTextClass = disabled ? "text-muted-foreground" : "";
const iconClassName = cn(
"h-4 w-4",
isLoading ? "animate-spin" : "animate-wiggle",
disabledIconTextClass
);
return (
<Button
variant="primary"
disabled={disabled}
className={classNames}
onClick={handleClick}
id={id}
>
<IconComponent
name={isLoading ? "Loader2" : "RefreshCcw"}
className={iconClassName}
id={id + "-icon"}
/>
</Button>
);
}
export { RefreshButton };

View file

@ -31,6 +31,7 @@ import {
getNodeId,
isValidConnection,
reconnectEdges,
scapeJSONParse,
validateSelection,
} from "../../../../utils/reactflowUtils";
import { getRandomName, isWrappedWithClass } from "../../../../utils/utils";
@ -108,7 +109,7 @@ export default function Page({
...old.data,
node: {
...old.data.node,
pinned: old.data?.node?.pinned ? false : true,
frozen: old.data?.node?.frozen ? false : true,
},
},
}));
@ -320,6 +321,8 @@ export default function Page({
(oldEdge: Edge, newConnection: Connection) => {
if (isValidConnection(newConnection, nodes, edges)) {
edgeUpdateSuccessful.current = true;
oldEdge.data.targetHandle = scapeJSONParse(newConnection.targetHandle!);
oldEdge.data.sourceHandle = scapeJSONParse(newConnection.sourceHandle!);
setEdges((els) => updateEdge(oldEdge, newConnection, els));
}
},
@ -488,4 +491,4 @@ export default function Page({
</main>
</div>
);
}
}

View file

@ -26,6 +26,7 @@ import {
} from "../../../../utils/utils";
import DisclosureComponent from "../DisclosureComponent";
import SidebarDraggableComponent from "./sideBarDraggableComponent";
import { sortKeys } from "./utils";
export default function ExtraSidebar(): JSX.Element {
const data = useTypesStore((state) => state.data);
@ -320,19 +321,7 @@ export default function ExtraSidebar(): JSX.Element {
<div className="side-bar-components-div-arrangement">
{Object.keys(dataFilter)
.sort((a, b) => {
if (a.toLowerCase() === "saved_components") {
return -1;
} else if (b.toLowerCase() === "saved_components") {
return 1;
} else if (a.toLowerCase() === "custom_components") {
return -2;
} else if (b.toLowerCase() === "custom_components") {
return 2;
} else {
return a.localeCompare(b);
}
})
.sort(sortKeys)
.map((SBSectionName: keyof APIObjectType, index) =>
Object.keys(dataFilter[SBSectionName]).length > 0 ? (
<DisclosureComponent

View file

@ -0,0 +1,31 @@
export function sortKeys(a: string, b: string) {
// Define the order of specific keys
const order = [
"saved_components",
"inputs",
"outputs",
"data",
"utilities",
"models",
];
const indexA = order.indexOf(a.toLowerCase());
const indexB = order.indexOf(b.toLowerCase());
// Check if both keys are in the predefined order
if (indexA !== -1 && indexB !== -1) {
return indexA - indexB;
}
// If only 'a' is in the predefined order, it should come first
if (indexA !== -1) {
return -1;
}
// If only 'b' is in the predefined order, it should come first
if (indexB !== -1) {
return 1;
}
// If neither 'a' nor 'b' are in the predefined order, sort them alphabetically
return a.localeCompare(b);
}

View file

@ -62,7 +62,7 @@ export default function NodeToolbarComponent({
const isMinimal = numberOfHandles <= 1;
const isGroup = data.node?.flow ? true : false;
const pinned = data.node?.pinned ?? false;
const frozen = data.node?.frozen ?? false;
const paste = useFlowStore((state) => state.paste);
const nodes = useFlowStore((state) => state.nodes);
const edges = useFlowStore((state) => state.edges);
@ -267,7 +267,7 @@ export default function NodeToolbarComponent({
</button>
</ShadTooltip>
<ShadTooltip content="Pin" side="top">
<ShadTooltip content="Freeze" side="top">
<button
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"
@ -280,17 +280,18 @@ export default function NodeToolbarComponent({
...old.data,
node: {
...old.data.node,
pinned: old.data?.node?.pinned ? false : true,
frozen: old.data?.node?.frozen ? false : true,
},
},
}));
}}
>
<IconComponent
name="Pin"
name="Snowflake"
className={cn(
"h-4 w-4 transition-all",
pinned ? "animate-wiggle fill-current" : ""
// TODO UPDATE THIS COLOR TO BE A VARIABLE
frozen ? "animate-wiggle text-ice" : ""
)}
/>
</button>

View file

@ -13,7 +13,6 @@ import {
FLOW_BUILD_SUCCESS_ALERT,
MISSED_ERROR_ALERT,
} from "../constants/alerts_constants";
import { RUN_TIMESTAMP_PREFIX } from "../constants/constants";
import { BuildStatus } from "../constants/enums";
import { getFlowPool } from "../controllers/API";
import { VertexBuildTypeAPI } from "../types/api";
@ -188,10 +187,12 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
typeof change === "function"
? change(get().nodes.find((node) => node.id === id)!)
: change;
get().setNodes((oldNodes) =>
oldNodes.map((node) => {
if (node.id === id) {
if ((node.data as NodeDataType).node?.frozen) {
(newChange.data as NodeDataType).node!.frozen = false;
}
return newChange;
}
return node;
@ -572,9 +573,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
};
if (status == BuildStatus.BUILT) {
const timestamp_string = new Date(Date.now()).toLocaleString();
newFlowBuildStatus[
id
].timestamp = `${RUN_TIMESTAMP_PREFIX} ${timestamp_string}`;
newFlowBuildStatus[id].timestamp = timestamp_string;
}
console.log("updateBuildStatus", newFlowBuildStatus);
});

View file

@ -316,7 +316,7 @@
@apply border-none ring ring-[#FF9090];
}
.built-invalid-status-dark {
@apply border-none ring ring-[#751C1C]
@apply border-none ring ring-[#751C1C];
}
.building-status {
@apply border-none ring;
@ -431,7 +431,9 @@
.code-area-external-link:hover {
@apply hover:text-accent-foreground;
}
.dropdown-component-disabled {
@apply pointer-events-none cursor-not-allowed;
}
.dropdown-component-outline {
@apply input-edit-node relative pr-8;
}
@ -441,11 +443,17 @@
.dropdown-component-display {
@apply block w-full truncate bg-background;
}
.dropdown-component-display-disabled {
@apply text-muted-foreground;
}
.dropdown-component-arrow {
@apply pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2;
}
.dropdown-component-arrow-color {
@apply extra-side-bar-save-disable h-5 w-5;
@apply h-5 w-5 text-accent-foreground;
}
.dropdown-component-arrow-color-disable {
@apply h-5 w-5 text-muted-foreground;
}
.dropdown-component-options {
@apply z-10 mt-1 max-h-60 overflow-auto rounded-md bg-background py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm;
@ -902,7 +910,7 @@
@apply flex-max-width px-2 py-6 pl-4 pr-9;
}
.form-modal-chatbot-icon {
@apply flex flex-col mb-3 ml-3 mr-6 mt-1;
@apply mb-3 ml-3 mr-6 mt-1 flex flex-col;
}
.form-modal-chat-image {
@apply flex flex-col items-center gap-1;

View file

@ -27,6 +27,7 @@
--radius: 0.5rem;
--ring: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
--round-btn-shadow: #00000063;
--ice: #31a3cc;
--error-background: #fef2f2;
--error-foreground: #991b1b;
@ -67,6 +68,7 @@
.dark {
--background: 224 35% 7.5%; /* hsl(224 40% 10%) */
--foreground: 213 31% 80%; /* hsl(213 31% 91%) */
--ice: #60A5FA;
--muted: 223 27% 11%; /* hsl(223 27% 11%) */
--muted-foreground: 215.4 16.3% 56.9%; /* hsl(215 16% 56%) */

View file

@ -27,7 +27,7 @@ export type APIClassType = {
documentation: string;
error?: string;
official?: boolean;
pinned?: boolean;
frozen?: boolean;
flow?: FlowType;
[key: string]:
| Array<string>
@ -54,6 +54,7 @@ export type TemplateVariableType = {
input_types?: Array<string>;
display_name?: string;
name?: string;
refresh?: boolean;
[key: string]: any;
};
export type sendAllProps = {

View file

@ -30,6 +30,8 @@ export type ToggleComponentType = {
editNode?: boolean;
};
export type DropDownComponentType = {
disabled?: boolean;
isLoading?: boolean;
value: string;
options: string[];
onSelect: (value: string) => void;

View file

@ -126,7 +126,8 @@ export async function buildVertices({
if (validateNodes) {
try {
validateNodes(verticesIds);
const nodes = useFlowStore.getState().nodes;
validateNodes(nodes.map((node) => node.id));
} catch (e) {
return;
}

View file

@ -8,7 +8,6 @@ import {
Bot,
Boxes,
Braces,
Cable,
Check,
CheckCircle2,
ChevronDown,
@ -46,6 +45,7 @@ import {
FileUp,
Fingerprint,
FlaskConical,
FolderOpen,
FolderPlus,
FormInput,
Forward,
@ -99,6 +99,7 @@ import {
Share2,
Shield,
Sliders,
Snowflake,
Sparkles,
Square,
Store,
@ -204,6 +205,9 @@ export const gradients = [
];
export const nodeColors: { [char: string]: string } = {
inputs: "#9AAE42",
outputs: "#AA2411",
data: "#6344BE",
prompts: "#4367BF",
models: "#AA2411",
model_specs: "#6344BE",
@ -224,16 +228,19 @@ export const nodeColors: { [char: string]: string } = {
toolkits: "#DB2C2C",
wrappers: "#E6277A",
utilities: "#31A3CC",
langchain_utilities: "#31A3CC",
output_parsers: "#E6A627",
str: "#31a3cc",
Text: "#31a3cc",
retrievers: "#e6b25a",
unknown: "#9CA3AF",
custom_components: "#ab11ab",
io: "#e6b25a",
};
export const nodeNames: { [char: string]: string } = {
inputs: "Inputs",
outputs: "Outputs",
data: "Data",
prompts: "Prompts",
models: "Language Models",
model_specs: "Model Specs",
@ -252,13 +259,16 @@ export const nodeNames: { [char: string]: string } = {
textsplitters: "Text Splitters",
retrievers: "Retrievers",
utilities: "Utilities",
langchain_utilities: "Langchain Utilities",
output_parsers: "Output Parsers",
custom_components: "Custom",
io: "I/O",
unknown: "Other",
};
export const nodeIconsLucide: iconsType = {
inputs: Download,
outputs: Upload,
data: FolderOpen,
AzureChatOpenAi: AzureIcon,
Ollama: OllamaIcon,
ChatOllama: OllamaIcon,
@ -341,6 +351,7 @@ export const nodeIconsLucide: iconsType = {
textsplitters: Scissors,
wrappers: Gift,
utilities: Wand2,
langchain_utilities: Wand2,
WolframAlphaAPIWrapper: SvgWolfram,
output_parsers: Compass,
retrievers: FileSearch,
@ -383,6 +394,7 @@ export const nodeIconsLucide: iconsType = {
Clipboard,
Code2,
Variable,
Snowflake,
Store,
Download,
Eraser,
@ -446,7 +458,6 @@ export const nodeIconsLucide: iconsType = {
TerminalSquare,
TextCursorInput,
Repeat,
io: Cable,
Sliders,
ScreenShare,
Code,

View file

@ -87,6 +87,7 @@ module.exports = {
"beta-foreground": "var(--beta-foreground)",
"chat-bot-icon": "var(--chat-bot-icon)",
"chat-user-icon": "var(--chat-user-icon)",
"ice": "var(--ice)",
white: "var(--white)",
border: "hsl(var(--border))",