diff --git a/src/backend/langflow/api/v1/chat.py b/src/backend/langflow/api/v1/chat.py index 9f0259771..b72504da6 100644 --- a/src/backend/langflow/api/v1/chat.py +++ b/src/backend/langflow/api/v1/chat.py @@ -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", diff --git a/src/backend/langflow/components/custom_components/__init__.py b/src/backend/langflow/base/__init__.py similarity index 100% rename from src/backend/langflow/components/custom_components/__init__.py rename to src/backend/langflow/base/__init__.py diff --git a/src/backend/langflow/components/io/base/__init__.py b/src/backend/langflow/base/data/__init__.py similarity index 100% rename from src/backend/langflow/components/io/base/__init__.py rename to src/backend/langflow/base/data/__init__.py diff --git a/src/backend/langflow/base/data/utils.py b/src/backend/langflow/base/data/utils.py new file mode 100644 index 000000000..2219310ef --- /dev/null +++ b/src/backend/langflow/base/data/utils.py @@ -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) diff --git a/src/backend/langflow/components/routing/__init__.py b/src/backend/langflow/base/io/__init__.py similarity index 100% rename from src/backend/langflow/components/routing/__init__.py rename to src/backend/langflow/base/io/__init__.py diff --git a/src/backend/langflow/components/io/base/chat.py b/src/backend/langflow/base/io/chat.py similarity index 100% rename from src/backend/langflow/components/io/base/chat.py rename to src/backend/langflow/base/io/chat.py diff --git a/src/backend/langflow/components/io/base/text.py b/src/backend/langflow/base/io/text.py similarity index 100% rename from src/backend/langflow/components/io/base/text.py rename to src/backend/langflow/base/io/text.py diff --git a/src/backend/langflow/components/data/Directory.py b/src/backend/langflow/components/data/Directory.py new file mode 100644 index 000000000..327f270cc --- /dev/null +++ b/src/backend/langflow/components/data/Directory.py @@ -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 diff --git a/src/backend/langflow/components/data/File.py b/src/backend/langflow/components/data/File.py new file mode 100644 index 000000000..dbc14abc4 --- /dev/null +++ b/src/backend/langflow/components/data/File.py @@ -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) diff --git a/src/backend/langflow/components/documentloaders/FileLoader.py b/src/backend/langflow/components/data/FileLoader.py similarity index 100% rename from src/backend/langflow/components/documentloaders/FileLoader.py rename to src/backend/langflow/components/data/FileLoader.py diff --git a/src/backend/langflow/components/data/URL.py b/src/backend/langflow/components/data/URL.py new file mode 100644 index 000000000..08eafeaa3 --- /dev/null +++ b/src/backend/langflow/components/data/URL.py @@ -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 diff --git a/src/backend/langflow/components/documentloaders/UrlLoader.py b/src/backend/langflow/components/data/UrlLoader.py similarity index 91% rename from src/backend/langflow/components/documentloaders/UrlLoader.py rename to src/backend/langflow/components/data/UrlLoader.py index eb60ac572..c1346f142 100644 --- a/src/backend/langflow/components/documentloaders/UrlLoader.py +++ b/src/backend/langflow/components/data/UrlLoader.py @@ -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]}...""" diff --git a/src/backend/langflow/components/data/__init__.py b/src/backend/langflow/components/data/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/langflow/components/documentloaders/GatherRecords.py b/src/backend/langflow/components/documentloaders/GatherRecords.py deleted file mode 100644 index ac298c092..000000000 --- a/src/backend/langflow/components/documentloaders/GatherRecords.py +++ /dev/null @@ -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 diff --git a/src/backend/langflow/components/io/ChatInput.py b/src/backend/langflow/components/inputs/ChatInput.py similarity index 92% rename from src/backend/langflow/components/io/ChatInput.py rename to src/backend/langflow/components/inputs/ChatInput.py index de8ce14cb..e5867751c 100644 --- a/src/backend/langflow/components/io/ChatInput.py +++ b/src/backend/langflow/components/inputs/ChatInput.py @@ -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 diff --git a/src/backend/langflow/components/io/TextInput.py b/src/backend/langflow/components/inputs/TextInput.py similarity index 84% rename from src/backend/langflow/components/io/TextInput.py rename to src/backend/langflow/components/inputs/TextInput.py index dc7ff1a73..034cf527b 100644 --- a/src/backend/langflow/components/io/TextInput.py +++ b/src/backend/langflow/components/inputs/TextInput.py @@ -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 diff --git a/src/backend/langflow/components/inputs/__init__.py b/src/backend/langflow/components/inputs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/langflow/components/utilities/BingSearchAPIWrapper.py b/src/backend/langflow/components/langchain_utilities/BingSearchAPIWrapper.py similarity index 100% rename from src/backend/langflow/components/utilities/BingSearchAPIWrapper.py rename to src/backend/langflow/components/langchain_utilities/BingSearchAPIWrapper.py diff --git a/src/backend/langflow/components/utilities/DocumentToRecord.py b/src/backend/langflow/components/langchain_utilities/DocumentToRecord.py similarity index 100% rename from src/backend/langflow/components/utilities/DocumentToRecord.py rename to src/backend/langflow/components/langchain_utilities/DocumentToRecord.py diff --git a/src/backend/langflow/components/utilities/GoogleSearchAPIWrapper.py b/src/backend/langflow/components/langchain_utilities/GoogleSearchAPIWrapper.py similarity index 100% rename from src/backend/langflow/components/utilities/GoogleSearchAPIWrapper.py rename to src/backend/langflow/components/langchain_utilities/GoogleSearchAPIWrapper.py diff --git a/src/backend/langflow/components/utilities/GoogleSerperAPIWrapper.py b/src/backend/langflow/components/langchain_utilities/GoogleSerperAPIWrapper.py similarity index 100% rename from src/backend/langflow/components/utilities/GoogleSerperAPIWrapper.py rename to src/backend/langflow/components/langchain_utilities/GoogleSerperAPIWrapper.py diff --git a/src/backend/langflow/components/utilities/JSONDocumentBuilder.py b/src/backend/langflow/components/langchain_utilities/JSONDocumentBuilder.py similarity index 100% rename from src/backend/langflow/components/utilities/JSONDocumentBuilder.py rename to src/backend/langflow/components/langchain_utilities/JSONDocumentBuilder.py diff --git a/src/backend/langflow/components/utilities/SQLDatabase.py b/src/backend/langflow/components/langchain_utilities/SQLDatabase.py similarity index 100% rename from src/backend/langflow/components/utilities/SQLDatabase.py rename to src/backend/langflow/components/langchain_utilities/SQLDatabase.py diff --git a/src/backend/langflow/components/utilities/SearxSearchWrapper.py b/src/backend/langflow/components/langchain_utilities/SearxSearchWrapper.py similarity index 100% rename from src/backend/langflow/components/utilities/SearxSearchWrapper.py rename to src/backend/langflow/components/langchain_utilities/SearxSearchWrapper.py diff --git a/src/backend/langflow/components/utilities/SerpAPIWrapper.py b/src/backend/langflow/components/langchain_utilities/SerpAPIWrapper.py similarity index 100% rename from src/backend/langflow/components/utilities/SerpAPIWrapper.py rename to src/backend/langflow/components/langchain_utilities/SerpAPIWrapper.py diff --git a/src/backend/langflow/components/utilities/WikipediaAPIWrapper.py b/src/backend/langflow/components/langchain_utilities/WikipediaAPIWrapper.py similarity index 100% rename from src/backend/langflow/components/utilities/WikipediaAPIWrapper.py rename to src/backend/langflow/components/langchain_utilities/WikipediaAPIWrapper.py diff --git a/src/backend/langflow/components/utilities/WolframAlphaAPIWrapper.py b/src/backend/langflow/components/langchain_utilities/WolframAlphaAPIWrapper.py similarity index 100% rename from src/backend/langflow/components/utilities/WolframAlphaAPIWrapper.py rename to src/backend/langflow/components/langchain_utilities/WolframAlphaAPIWrapper.py diff --git a/src/backend/langflow/components/io/ChatOutput.py b/src/backend/langflow/components/outputs/ChatOutput.py similarity index 92% rename from src/backend/langflow/components/io/ChatOutput.py rename to src/backend/langflow/components/outputs/ChatOutput.py index a528b65b8..aa61159c9 100644 --- a/src/backend/langflow/components/io/ChatOutput.py +++ b/src/backend/langflow/components/outputs/ChatOutput.py @@ -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 diff --git a/src/backend/langflow/components/io/TextOutput.py b/src/backend/langflow/components/outputs/TextOutput.py similarity index 87% rename from src/backend/langflow/components/io/TextOutput.py rename to src/backend/langflow/components/outputs/TextOutput.py index f92e09146..c971a9699 100644 --- a/src/backend/langflow/components/io/TextOutput.py +++ b/src/backend/langflow/components/outputs/TextOutput.py @@ -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 diff --git a/src/backend/langflow/components/outputs/__init__.py b/src/backend/langflow/components/outputs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/langflow/components/routing/ShouldRunNext.py b/src/backend/langflow/components/routing/ShouldRunNext.py deleted file mode 100644 index 3cfde3c22..000000000 --- a/src/backend/langflow/components/routing/ShouldRunNext.py +++ /dev/null @@ -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 diff --git a/src/backend/langflow/components/utilities/APIRequest.py b/src/backend/langflow/components/utilities/APIRequest.py new file mode 100644 index 000000000..2e73979ff --- /dev/null +++ b/src/backend/langflow/components/utilities/APIRequest.py @@ -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 diff --git a/src/backend/langflow/components/custom_components/CustomComponent.py b/src/backend/langflow/components/utilities/CustomComponent.py similarity index 91% rename from src/backend/langflow/components/custom_components/CustomComponent.py rename to src/backend/langflow/components/utilities/CustomComponent.py index 533ccb727..c45b5effd 100644 --- a/src/backend/langflow/components/custom_components/CustomComponent.py +++ b/src/backend/langflow/components/utilities/CustomComponent.py @@ -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"}} diff --git a/src/backend/langflow/components/utilities/GetRequest.py b/src/backend/langflow/components/utilities/GetRequest.py deleted file mode 100644 index d6ee5a44f..000000000 --- a/src/backend/langflow/components/utilities/GetRequest.py +++ /dev/null @@ -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 diff --git a/src/backend/langflow/components/utilities/ListFlows.py b/src/backend/langflow/components/utilities/ListFlows.py new file mode 100644 index 000000000..724cd9a82 --- /dev/null +++ b/src/backend/langflow/components/utilities/ListFlows.py @@ -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 diff --git a/src/backend/langflow/components/utilities/PostRequest.py b/src/backend/langflow/components/utilities/PostRequest.py deleted file mode 100644 index befc006c8..000000000 --- a/src/backend/langflow/components/utilities/PostRequest.py +++ /dev/null @@ -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 diff --git a/src/backend/langflow/components/utilities/UpdateRequest.py b/src/backend/langflow/components/utilities/UpdateRequest.py deleted file mode 100644 index 41a57eda6..000000000 --- a/src/backend/langflow/components/utilities/UpdateRequest.py +++ /dev/null @@ -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 diff --git a/src/backend/langflow/components/utilities/__init__.py b/src/backend/langflow/components/utilities/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/langflow/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py index 97246aee1..f08cd1193 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -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: diff --git a/src/backend/langflow/graph/vertex/base.py b/src/backend/langflow/graph/vertex/base.py index f4e4823be..66fdd44c6 100644 --- a/src/backend/langflow/graph/vertex/base.py +++ b/src/backend/langflow/graph/vertex/base.py @@ -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 diff --git a/src/backend/langflow/graph/vertex/types.py b/src/backend/langflow/graph/vertex/types.py index ac9e1d0bc..1bfd7cacf 100644 --- a/src/backend/langflow/graph/vertex/types.py +++ b/src/backend/langflow/graph/vertex/types.py @@ -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) diff --git a/src/backend/langflow/interface/custom/attributes.py b/src/backend/langflow/interface/custom/attributes.py index 9b91af43c..7bcfb5f4b 100644 --- a/src/backend/langflow/interface/custom/attributes.py +++ b/src/backend/langflow/interface/custom/attributes.py @@ -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, } diff --git a/src/backend/langflow/interface/custom/custom_component/component.py b/src/backend/langflow/interface/custom/custom_component/component.py index 13f185ed9..a889fa7b9 100644 --- a/src/backend/langflow/interface/custom/custom_component/component.py +++ b/src/backend/langflow/interface/custom/custom_component/component.py @@ -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): diff --git a/src/backend/langflow/interface/custom/custom_component/custom_component.py b/src/backend/langflow/interface/custom/custom_component/custom_component.py index ced2c7a78..d2913be3d 100644 --- a/src/backend/langflow/interface/custom/custom_component/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component/custom_component.py @@ -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 diff --git a/src/backend/langflow/interface/custom/utils.py b/src/backend/langflow/interface/custom/utils.py index 2f7257417..85f636811 100644 --- a/src/backend/langflow/interface/custom/utils.py +++ b/src/backend/langflow/interface/custom/utils.py @@ -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 diff --git a/src/backend/langflow/interface/initialize/loading.py b/src/backend/langflow/interface/initialize/loading.py index d9b678639..7bd8e34f4 100644 --- a/src/backend/langflow/interface/initialize/loading.py +++ b/src/backend/langflow/interface/initialize/loading.py @@ -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) diff --git a/src/backend/langflow/services/database/models/flow/model.py b/src/backend/langflow/services/database/models/flow/model.py index d942fa93c..1211c40ed 100644 --- a/src/backend/langflow/services/database/models/flow/model.py +++ b/src/backend/langflow/services/database/models/flow/model.py @@ -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 diff --git a/src/backend/langflow/template/frontend_node/base.py b/src/backend/langflow/template/frontend_node/base.py index 2a19ec9c9..bcbbb36c1 100644 --- a/src/backend/langflow/template/frontend_node/base.py +++ b/src/backend/langflow/template/frontend_node/base.py @@ -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 "}""" @@ -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: diff --git a/src/backend/langflow/template/frontend_node/memories.py b/src/backend/langflow/template/frontend_node/memories.py index 588bcc2c0..f1c326810 100644 --- a/src/backend/langflow/template/frontend_node/memories.py +++ b/src/backend/langflow/template/frontend_node/memories.py @@ -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" diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 385d441d3..83d916a9d 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -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 }); } + + 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({
+ {!left && data.node?.frozen && ( +
+ +
+ )} {proxy ? ( {proxy.id}}> - {title} + + {title} + ) : ( - title + + {title} + )} {required ? " *" : ""} @@ -390,16 +440,31 @@ export default function ParameterComponent({ !data.node?.template[name].options ? (
{data.node?.template[name].list ? ( - +
+ + {data.node?.template[name].refresh && ( +
+ +
+ )} +
) : data.node?.template[name].multiline ? (
{data.node?.template[name].refresh && ( - +
+ +
)}
)} @@ -454,11 +522,14 @@ export default function ParameterComponent({ ) : left === true && type === "str" && - data.node?.template[name].options ? ( + (data.node?.template[name].options || + data.node?.template[name]?.refresh) ? ( // TODO: Improve CSS
{data.node?.template[name].refresh && ( - +
+ +
)}
) : left === true && type === "code" ? ( diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index 72bedfcb3..80e5ad0f7 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -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} /> )} -
- {lastRunTime && ( -
- {lastRunTime.split("\n").map((line, index) => ( -
{line}
- ))} -
- )} -
)} diff --git a/src/frontend/src/components/dropdownComponent/index.tsx b/src/frontend/src/components/dropdownComponent/index.tsx index db488aaac..48a610d29 100644 --- a/src/frontend/src/components/dropdownComponent/index.tsx +++ b/src/frontend/src/components/dropdownComponent/index.tsx @@ -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({ <> { setInternalValue(value); onSelect(value); @@ -129,11 +132,17 @@ export default function Dropdown({ ) : ( <> -
- - No parameters are available for display. - -
+ {(!isLoading && ( +
+ + No parameters are available for display. + +
+ )) || ( +
+ Loading... +
+ )} )} diff --git a/src/frontend/src/components/genericIconComponent/index.tsx b/src/frontend/src/components/genericIconComponent/index.tsx index 62299bdf6..af3fe061b 100644 --- a/src/frontend/src/components/genericIconComponent/index.tsx +++ b/src/frontend/src/components/genericIconComponent/index.tsx @@ -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 = ( +
+ ); return ( - + + + ); } ); diff --git a/src/frontend/src/components/ui/button.tsx b/src/frontend/src/components/ui/button.tsx index a43ce3359..262de49cc 100644 --- a/src/frontend/src/components/ui/button.tsx +++ b/src/frontend/src/components/ui/button.tsx @@ -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: { diff --git a/src/frontend/src/components/ui/refreshButton.tsx b/src/frontend/src/components/ui/refreshButton.tsx new file mode 100644 index 000000000..bf41ba114 --- /dev/null +++ b/src/frontend/src/components/ui/refreshButton.tsx @@ -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 ( + + ); +} + +export { RefreshButton }; diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index cac8b8faa..d740129e4 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -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({
); -} \ No newline at end of file +} diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx index fe78895c0..299507bbd 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx @@ -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 {
{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 ? ( state.paste); const nodes = useFlowStore((state) => state.nodes); const edges = useFlowStore((state) => state.edges); @@ -267,7 +267,7 @@ export default function NodeToolbarComponent({ - + diff --git a/src/frontend/src/stores/flowStore.ts b/src/frontend/src/stores/flowStore.ts index a9e37975d..48431e069 100644 --- a/src/frontend/src/stores/flowStore.ts +++ b/src/frontend/src/stores/flowStore.ts @@ -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((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((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); }); diff --git a/src/frontend/src/style/applies.css b/src/frontend/src/style/applies.css index 77a760255..cca804fc7 100644 --- a/src/frontend/src/style/applies.css +++ b/src/frontend/src/style/applies.css @@ -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; diff --git a/src/frontend/src/style/index.css b/src/frontend/src/style/index.css index ba0496071..fb4242661 100644 --- a/src/frontend/src/style/index.css +++ b/src/frontend/src/style/index.css @@ -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%) */ diff --git a/src/frontend/src/types/api/index.ts b/src/frontend/src/types/api/index.ts index e0b731caa..bcbb31730 100644 --- a/src/frontend/src/types/api/index.ts +++ b/src/frontend/src/types/api/index.ts @@ -27,7 +27,7 @@ export type APIClassType = { documentation: string; error?: string; official?: boolean; - pinned?: boolean; + frozen?: boolean; flow?: FlowType; [key: string]: | Array @@ -54,6 +54,7 @@ export type TemplateVariableType = { input_types?: Array; display_name?: string; name?: string; + refresh?: boolean; [key: string]: any; }; export type sendAllProps = { diff --git a/src/frontend/src/types/components/index.ts b/src/frontend/src/types/components/index.ts index cd23ae31b..aef92b1a4 100644 --- a/src/frontend/src/types/components/index.ts +++ b/src/frontend/src/types/components/index.ts @@ -30,6 +30,8 @@ export type ToggleComponentType = { editNode?: boolean; }; export type DropDownComponentType = { + disabled?: boolean; + isLoading?: boolean; value: string; options: string[]; onSelect: (value: string) => void; diff --git a/src/frontend/src/utils/buildUtils.ts b/src/frontend/src/utils/buildUtils.ts index 9aa732e03..34edd1592 100644 --- a/src/frontend/src/utils/buildUtils.ts +++ b/src/frontend/src/utils/buildUtils.ts @@ -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; } diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index 7d84772b8..601066407 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -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, diff --git a/src/frontend/tailwind.config.js b/src/frontend/tailwind.config.js index ba37b1b62..80a5416c3 100644 --- a/src/frontend/tailwind.config.js +++ b/src/frontend/tailwind.config.js @@ -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))",