Merge branch 'zustand/io/migration' into state_theories
This commit is contained in:
commit
481df3de8f
67 changed files with 798 additions and 599 deletions
|
|
@ -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",
|
||||
|
|
|
|||
89
src/backend/langflow/base/data/utils.py
Normal file
89
src/backend/langflow/base/data/utils.py
Normal 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)
|
||||
76
src/backend/langflow/components/data/Directory.py
Normal file
76
src/backend/langflow/components/data/Directory.py
Normal 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
|
||||
28
src/backend/langflow/components/data/File.py
Normal file
28
src/backend/langflow/components/data/File.py
Normal 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)
|
||||
26
src/backend/langflow/components/data/URL.py
Normal file
26
src/backend/langflow/components/data/URL.py
Normal 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
|
||||
|
|
@ -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]}..."""
|
||||
0
src/backend/langflow/components/data/__init__.py
Normal file
0
src/backend/langflow/components/data/__init__.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
0
src/backend/langflow/components/inputs/__init__.py
Normal file
0
src/backend/langflow/components/inputs/__init__.py
Normal 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
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
0
src/backend/langflow/components/outputs/__init__.py
Normal file
0
src/backend/langflow/components/outputs/__init__.py
Normal 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
|
||||
109
src/backend/langflow/components/utilities/APIRequest.py
Normal file
109
src/backend/langflow/components/utilities/APIRequest.py
Normal 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
|
||||
|
|
@ -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"}}
|
||||
|
|
@ -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
|
||||
19
src/backend/langflow/components/utilities/ListFlows.py
Normal file
19
src/backend/langflow/components/utilities/ListFlows.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
0
src/backend/langflow/components/utilities/__init__.py
Normal file
0
src/backend/langflow/components/utilities/__init__.py
Normal 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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" ? (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
55
src/frontend/src/components/ui/refreshButton.tsx
Normal file
55
src/frontend/src/components/ui/refreshButton.tsx
Normal 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 };
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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%) */
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ export type ToggleComponentType = {
|
|||
editNode?: boolean;
|
||||
};
|
||||
export type DropDownComponentType = {
|
||||
disabled?: boolean;
|
||||
isLoading?: boolean;
|
||||
value: string;
|
||||
options: string[];
|
||||
onSelect: (value: string) => void;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue