diff --git a/src/backend/base/langflow/graph/vertex/base.py b/src/backend/base/langflow/graph/vertex/base.py index 561a91848..77b4e7192 100644 --- a/src/backend/base/langflow/graph/vertex/base.py +++ b/src/backend/base/langflow/graph/vertex/base.py @@ -201,7 +201,9 @@ class Vertex: self.description = self.data["node"].get("description", "") self.frozen = self.data["node"].get("frozen", False) - self.selected_output_type = self.data["node"].get("selected_output_type") + self.selected_output_type = ( + str(self.data.get("selected_output_type")).strip() if self.data.get("selected_output_type") else None + ) 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 template_dicts = {key: value for key, value in self.data["node"]["template"].items() if isinstance(value, dict)} diff --git a/src/backend/base/langflow/initial_setup/starter_projects/VectorStore-RAG-Flows.json b/src/backend/base/langflow/initial_setup/starter_projects/VectorStore-RAG-Flows.json index 489d1cc19..097fdbbc2 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/VectorStore-RAG-Flows.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/VectorStore-RAG-Flows.json @@ -358,7 +358,7 @@ "list": false, "show": true, "multiline": true, - "value": "from typing import Dict, List, Optional\n\nfrom langchain_openai.embeddings.base import OpenAIEmbeddings\nfrom pydantic.v1 import SecretStr\n\nfrom langflow.custom import CustomComponent\nfrom langflow.field_typing import Embeddings, NestedDict\n\n\nclass OpenAIEmbeddingsComponent(CustomComponent):\n display_name = \"OpenAI Embeddings\"\n description = \"Generate embeddings using OpenAI models.\"\n\n def build_config(self):\n return {\n \"allowed_special\": {\n \"display_name\": \"Allowed Special\",\n \"advanced\": True,\n \"field_type\": \"str\",\n \"is_list\": True,\n },\n \"default_headers\": {\n \"display_name\": \"Default Headers\",\n \"advanced\": True,\n \"field_type\": \"dict\",\n },\n \"default_query\": {\n \"display_name\": \"Default Query\",\n \"advanced\": True,\n \"field_type\": \"NestedDict\",\n },\n \"disallowed_special\": {\n \"display_name\": \"Disallowed Special\",\n \"advanced\": True,\n \"field_type\": \"str\",\n \"is_list\": True,\n },\n \"chunk_size\": {\"display_name\": \"Chunk Size\", \"advanced\": True},\n \"client\": {\"display_name\": \"Client\", \"advanced\": True},\n \"deployment\": {\"display_name\": \"Deployment\", \"advanced\": True},\n \"embedding_ctx_length\": {\n \"display_name\": \"Embedding Context Length\",\n \"advanced\": True,\n },\n \"max_retries\": {\"display_name\": \"Max Retries\", \"advanced\": True},\n \"model\": {\n \"display_name\": \"Model\",\n \"advanced\": False,\n \"options\": [\n \"text-embedding-3-small\",\n \"text-embedding-3-large\",\n \"text-embedding-ada-002\",\n ],\n },\n \"model_kwargs\": {\"display_name\": \"Model Kwargs\", \"advanced\": True},\n \"openai_api_base\": {\n \"display_name\": \"OpenAI API Base\",\n \"password\": True,\n \"advanced\": True,\n },\n \"openai_api_key\": {\"display_name\": \"OpenAI API Key\", \"password\": True},\n \"openai_api_type\": {\n \"display_name\": \"OpenAI API Type\",\n \"advanced\": True,\n \"password\": True,\n },\n \"openai_api_version\": {\n \"display_name\": \"OpenAI API Version\",\n \"advanced\": True,\n },\n \"openai_organization\": {\n \"display_name\": \"OpenAI Organization\",\n \"advanced\": True,\n },\n \"openai_proxy\": {\"display_name\": \"OpenAI Proxy\", \"advanced\": True},\n \"request_timeout\": {\"display_name\": \"Request Timeout\", \"advanced\": True},\n \"show_progress_bar\": {\n \"display_name\": \"Show Progress Bar\",\n \"advanced\": True,\n },\n \"skip_empty\": {\"display_name\": \"Skip Empty\", \"advanced\": True},\n \"tiktoken_model_name\": {\n \"display_name\": \"TikToken Model Name\",\n \"advanced\": True,\n },\n \"tiktoken_enable\": {\"display_name\": \"TikToken Enable\", \"advanced\": True},\n }\n\n def build(\n self,\n openai_api_key: str,\n default_headers: Optional[Dict[str, str]] = None,\n default_query: Optional[NestedDict] = {},\n allowed_special: List[str] = [],\n disallowed_special: List[str] = [\"all\"],\n chunk_size: int = 1000,\n deployment: str = \"text-embedding-ada-002\",\n embedding_ctx_length: int = 8191,\n max_retries: int = 6,\n model: str = \"text-embedding-ada-002\",\n model_kwargs: NestedDict = {},\n openai_api_base: Optional[str] = None,\n openai_api_type: Optional[str] = None,\n openai_api_version: Optional[str] = None,\n openai_organization: Optional[str] = None,\n openai_proxy: Optional[str] = None,\n request_timeout: Optional[float] = None,\n show_progress_bar: bool = False,\n skip_empty: bool = False,\n tiktoken_enable: bool = True,\n tiktoken_model_name: Optional[str] = None,\n ) -> Embeddings:\n # This is to avoid errors with Vector Stores (e.g Chroma)\n if disallowed_special == [\"all\"]:\n disallowed_special = \"all\" # type: ignore\n if openai_api_key:\n api_key = SecretStr(openai_api_key)\n else:\n api_key = None\n\n return OpenAIEmbeddings(\n tiktoken_enabled=tiktoken_enable,\n default_headers=default_headers,\n default_query=default_query,\n allowed_special=set(allowed_special),\n disallowed_special=\"all\",\n chunk_size=chunk_size,\n deployment=deployment,\n embedding_ctx_length=embedding_ctx_length,\n max_retries=max_retries,\n model=model,\n model_kwargs=model_kwargs,\n base_url=openai_api_base,\n api_key=api_key,\n openai_api_type=openai_api_type,\n api_version=openai_api_version,\n organization=openai_organization,\n openai_proxy=openai_proxy,\n timeout=request_timeout,\n show_progress_bar=show_progress_bar,\n skip_empty=skip_empty,\n tiktoken_model_name=tiktoken_model_name,\n )\n", + "value": "from typing import Any, Dict, List, Optional\n\nfrom langchain_openai.embeddings.base import OpenAIEmbeddings\nfrom pydantic.v1 import SecretStr\n\nfrom langflow.field_typing import Embeddings, NestedDict\nfrom langflow.interface.custom.custom_component import CustomComponent\n\n\nclass OpenAIEmbeddingsComponent(CustomComponent):\n display_name = \"OpenAI Embeddings\"\n description = \"Generate embeddings using OpenAI models.\"\n\n def build_config(self):\n return {\n \"allowed_special\": {\n \"display_name\": \"Allowed Special\",\n \"advanced\": True,\n \"field_type\": \"str\",\n \"is_list\": True,\n },\n \"default_headers\": {\n \"display_name\": \"Default Headers\",\n \"advanced\": True,\n \"field_type\": \"dict\",\n },\n \"default_query\": {\n \"display_name\": \"Default Query\",\n \"advanced\": True,\n \"field_type\": \"NestedDict\",\n },\n \"disallowed_special\": {\n \"display_name\": \"Disallowed Special\",\n \"advanced\": True,\n \"field_type\": \"str\",\n \"is_list\": True,\n },\n \"chunk_size\": {\"display_name\": \"Chunk Size\", \"advanced\": True},\n \"client\": {\"display_name\": \"Client\", \"advanced\": True},\n \"deployment\": {\"display_name\": \"Deployment\", \"advanced\": True},\n \"embedding_ctx_length\": {\n \"display_name\": \"Embedding Context Length\",\n \"advanced\": True,\n },\n \"max_retries\": {\"display_name\": \"Max Retries\", \"advanced\": True},\n \"model\": {\n \"display_name\": \"Model\",\n \"advanced\": False,\n \"options\": [\n \"text-embedding-3-small\",\n \"text-embedding-3-large\",\n \"text-embedding-ada-002\",\n ],\n },\n \"model_kwargs\": {\"display_name\": \"Model Kwargs\", \"advanced\": True},\n \"openai_api_base\": {\n \"display_name\": \"OpenAI API Base\",\n \"password\": True,\n \"advanced\": True,\n },\n \"openai_api_key\": {\"display_name\": \"OpenAI API Key\", \"password\": True},\n \"openai_api_type\": {\n \"display_name\": \"OpenAI API Type\",\n \"advanced\": True,\n \"password\": True,\n },\n \"openai_api_version\": {\n \"display_name\": \"OpenAI API Version\",\n \"advanced\": True,\n },\n \"openai_organization\": {\n \"display_name\": \"OpenAI Organization\",\n \"advanced\": True,\n },\n \"openai_proxy\": {\"display_name\": \"OpenAI Proxy\", \"advanced\": True},\n \"request_timeout\": {\"display_name\": \"Request Timeout\", \"advanced\": True},\n \"show_progress_bar\": {\n \"display_name\": \"Show Progress Bar\",\n \"advanced\": True,\n },\n \"skip_empty\": {\"display_name\": \"Skip Empty\", \"advanced\": True},\n \"tiktoken_model_name\": {\n \"display_name\": \"TikToken Model Name\",\n \"advanced\": True,\n },\n \"tiktoken_enable\": {\"display_name\": \"TikToken Enable\", \"advanced\": True},\n }\n\n def build(\n self,\n openai_api_key: str,\n default_headers: Optional[Dict[str, str]] = None,\n default_query: Optional[NestedDict] = {},\n allowed_special: List[str] = [],\n disallowed_special: List[str] = [\"all\"],\n chunk_size: int = 1000,\n client: Optional[Any] = None,\n deployment: str = \"text-embedding-ada-002\",\n embedding_ctx_length: int = 8191,\n max_retries: int = 6,\n model: str = \"text-embedding-ada-002\",\n model_kwargs: NestedDict = {},\n openai_api_base: Optional[str] = None,\n openai_api_type: Optional[str] = None,\n openai_api_version: Optional[str] = None,\n openai_organization: Optional[str] = None,\n openai_proxy: Optional[str] = None,\n request_timeout: Optional[float] = None,\n show_progress_bar: bool = False,\n skip_empty: bool = False,\n tiktoken_enable: bool = True,\n tiktoken_model_name: Optional[str] = None,\n ) -> Embeddings:\n # This is to avoid errors with Vector Stores (e.g Chroma)\n if disallowed_special == [\"all\"]:\n disallowed_special = \"all\" # type: ignore\n if openai_api_key:\n api_key = SecretStr(openai_api_key)\n else:\n api_key = None\n\n return OpenAIEmbeddings(\n tiktoken_enabled=tiktoken_enable,\n default_headers=default_headers,\n default_query=default_query,\n allowed_special=set(allowed_special),\n disallowed_special=\"all\",\n chunk_size=chunk_size,\n client=client,\n deployment=deployment,\n embedding_ctx_length=embedding_ctx_length,\n max_retries=max_retries,\n model=model,\n model_kwargs=model_kwargs,\n base_url=openai_api_base,\n api_key=api_key,\n openai_api_type=openai_api_type,\n api_version=openai_api_version,\n organization=openai_organization,\n openai_proxy=openai_proxy,\n timeout=request_timeout,\n show_progress_bar=show_progress_bar,\n skip_empty=skip_empty,\n tiktoken_model_name=tiktoken_model_name,\n )\n", "fileTypes": [], "file_path": "", "password": false, @@ -572,7 +572,7 @@ "advanced": false, "dynamic": false, "info": "", - "load_from_db": true, + "load_from_db": false, "title_case": false, "input_types": [ "Text" @@ -851,7 +851,7 @@ "list": false, "show": true, "multiline": true, - "value": "from typing import Optional\n\nfrom langchain_openai import ChatOpenAI\nfrom pydantic.v1 import SecretStr\n\nfrom langflow.base.constants import STREAM_INFO_TEXT\nfrom langflow.base.models.model import LCModelComponent\nfrom langflow.base.models.openai_constants import MODEL_NAMES\nfrom langflow.field_typing import NestedDict, Text\n\n\nclass OpenAIModelComponent(LCModelComponent):\n display_name = \"OpenAI\"\n description = \"Generates text using OpenAI LLMs.\"\n icon = \"OpenAI\"\n\n field_order = [\n \"max_tokens\",\n \"model_kwargs\",\n \"model_name\",\n \"openai_api_base\",\n \"openai_api_key\",\n \"temperature\",\n \"input_value\",\n \"system_message\",\n \"stream\",\n ]\n\n def build_config(self):\n return {\n \"input_value\": {\"display_name\": \"Input\"},\n \"max_tokens\": {\n \"display_name\": \"Max Tokens\",\n \"advanced\": True,\n \"info\": \"The maximum number of tokens to generate. Set to 0 for unlimited tokens.\",\n },\n \"model_kwargs\": {\n \"display_name\": \"Model Kwargs\",\n \"advanced\": True,\n },\n \"model_name\": {\n \"display_name\": \"Model Name\",\n \"advanced\": False,\n \"options\": MODEL_NAMES,\n },\n \"openai_api_base\": {\n \"display_name\": \"OpenAI API Base\",\n \"advanced\": True,\n \"info\": (\n \"The base URL of the OpenAI API. Defaults to https://api.openai.com/v1.\\n\\n\"\n \"You can change this to use other APIs like JinaChat, LocalAI and Prem.\"\n ),\n },\n \"openai_api_key\": {\n \"display_name\": \"OpenAI API Key\",\n \"info\": \"The OpenAI API Key to use for the OpenAI model.\",\n \"advanced\": False,\n \"password\": True,\n },\n \"temperature\": {\n \"display_name\": \"Temperature\",\n \"advanced\": False,\n \"value\": 0.1,\n },\n \"stream\": {\n \"display_name\": \"Stream\",\n \"info\": STREAM_INFO_TEXT,\n \"advanced\": True,\n },\n \"system_message\": {\n \"display_name\": \"System Message\",\n \"info\": \"System message to pass to the model.\",\n \"advanced\": True,\n },\n }\n\n def build(\n self,\n input_value: Text,\n openai_api_key: str,\n temperature: float,\n model_name: str = \"gpt-4o\",\n max_tokens: Optional[int] = 256,\n model_kwargs: NestedDict = {},\n openai_api_base: Optional[str] = None,\n stream: bool = False,\n system_message: Optional[str] = None,\n ) -> Text:\n if not openai_api_base:\n openai_api_base = \"https://api.openai.com/v1\"\n if openai_api_key:\n api_key = SecretStr(openai_api_key)\n else:\n api_key = None\n\n output = ChatOpenAI(\n max_tokens=max_tokens or None,\n model_kwargs=model_kwargs,\n model=model_name,\n base_url=openai_api_base,\n api_key=api_key,\n temperature=temperature,\n )\n\n return self.get_chat_result(output, stream, input_value, system_message)\n", + "value": "from typing import Optional\n\nfrom langchain_openai import ChatOpenAI\nfrom pydantic.v1 import SecretStr\n\nfrom langflow.base.constants import STREAM_INFO_TEXT\nfrom langflow.base.models.model import LCModelComponent\nfrom langflow.base.models.openai_constants import MODEL_NAMES\nfrom langflow.field_typing import NestedDict, Text\n\n\nclass OpenAIModelComponent(LCModelComponent):\n display_name = \"OpenAI\"\n description = \"Generates text using OpenAI LLMs.\"\n icon = \"OpenAI\"\n\n field_order = [\n \"max_tokens\",\n \"model_kwargs\",\n \"model_name\",\n \"openai_api_base\",\n \"openai_api_key\",\n \"temperature\",\n \"input_value\",\n \"system_message\",\n \"stream\",\n ]\n\n def build_config(self):\n return {\n \"input_value\": {\"display_name\": \"Input\"},\n \"max_tokens\": {\n \"display_name\": \"Max Tokens\",\n \"advanced\": True,\n },\n \"model_kwargs\": {\n \"display_name\": \"Model Kwargs\",\n \"advanced\": True,\n },\n \"model_name\": {\n \"display_name\": \"Model Name\",\n \"advanced\": False,\n \"options\": MODEL_NAMES,\n },\n \"openai_api_base\": {\n \"display_name\": \"OpenAI API Base\",\n \"advanced\": True,\n \"info\": (\n \"The base URL of the OpenAI API. Defaults to https://api.openai.com/v1.\\n\\n\"\n \"You can change this to use other APIs like JinaChat, LocalAI and Prem.\"\n ),\n },\n \"openai_api_key\": {\n \"display_name\": \"OpenAI API Key\",\n \"info\": \"The OpenAI API Key to use for the OpenAI model.\",\n \"advanced\": False,\n \"password\": True,\n },\n \"temperature\": {\n \"display_name\": \"Temperature\",\n \"advanced\": False,\n \"value\": 0.1,\n },\n \"stream\": {\n \"display_name\": \"Stream\",\n \"info\": STREAM_INFO_TEXT,\n \"advanced\": True,\n },\n \"system_message\": {\n \"display_name\": \"System Message\",\n \"info\": \"System message to pass to the model.\",\n \"advanced\": True,\n },\n }\n\n def build(\n self,\n input_value: Text,\n openai_api_key: str,\n temperature: float,\n model_name: str = \"gpt-4o\",\n max_tokens: Optional[int] = 256,\n model_kwargs: NestedDict = {},\n openai_api_base: Optional[str] = None,\n stream: bool = False,\n system_message: Optional[str] = None,\n ) -> Text:\n if not openai_api_base:\n openai_api_base = \"https://api.openai.com/v1\"\n if openai_api_key:\n api_key = SecretStr(openai_api_key)\n else:\n api_key = None\n\n output = ChatOpenAI(\n max_tokens=max_tokens,\n model_kwargs=model_kwargs,\n model=model_name,\n base_url=openai_api_base,\n api_key=api_key,\n temperature=temperature,\n )\n\n return self.get_chat_result(output, stream, input_value, system_message)\n", "fileTypes": [], "file_path": "", "password": false, @@ -877,7 +877,7 @@ "display_name": "Max Tokens", "advanced": true, "dynamic": false, - "info": "The maximum number of tokens to generate. Set to 0 for unlimited tokens.", + "info": "", "load_from_db": false, "title_case": false }, @@ -965,7 +965,7 @@ "advanced": false, "dynamic": false, "info": "The OpenAI API Key to use for the OpenAI model.", - "load_from_db": true, + "load_from_db": false, "title_case": false, "input_types": [ "Text" @@ -1106,7 +1106,7 @@ "list": false, "show": true, "multiline": true, - "value": "from langchain_core.prompts import PromptTemplate\n\nfrom langflow.custom import CustomComponent\nfrom langflow.field_typing import Prompt, TemplateField, Text\n\n\nclass PromptComponent(CustomComponent):\n display_name: str = \"Prompt\"\n description: str = \"Create a prompt template with dynamic variables.\"\n icon = \"prompts\"\n\n def build_config(self):\n return {\n \"template\": TemplateField(display_name=\"Template\"),\n \"code\": TemplateField(advanced=True),\n }\n\n def build(\n self,\n template: Prompt,\n **kwargs,\n ) -> Text:\n from langflow.base.prompts.utils import dict_values_to_string\n\n prompt_template = PromptTemplate.from_template(Text(template))\n kwargs = dict_values_to_string(kwargs)\n kwargs = {k: \"\\n\".join(v) if isinstance(v, list) else v for k, v in kwargs.items()}\n try:\n formated_prompt = prompt_template.format(**kwargs)\n except Exception as exc:\n raise ValueError(f\"Error formatting prompt: {exc}\") from exc\n self.status = f'Prompt:\\n\"{formated_prompt}\"'\n return formated_prompt\n", + "value": "from langchain_core.prompts import PromptTemplate\n\nfrom langflow.field_typing import Prompt, TemplateField, Text\nfrom langflow.interface.custom.custom_component import CustomComponent\n\n\nclass PromptComponent(CustomComponent):\n display_name: str = \"Prompt\"\n description: str = \"Create a prompt template with dynamic variables.\"\n icon = \"prompts\"\n\n def build_config(self):\n return {\n \"template\": TemplateField(display_name=\"Template\"),\n \"code\": TemplateField(advanced=True),\n }\n\n def build(\n self,\n template: Prompt,\n **kwargs,\n ) -> Text:\n from langflow.base.prompts.utils import dict_values_to_string\n\n prompt_template = PromptTemplate.from_template(Text(template))\n kwargs = dict_values_to_string(kwargs)\n kwargs = {k: \"\\n\".join(v) if isinstance(v, list) else v for k, v in kwargs.items()}\n try:\n formated_prompt = prompt_template.format(**kwargs)\n except Exception as exc:\n raise ValueError(f\"Error formatting prompt: {exc}\") from exc\n self.status = f'Prompt:\\n\"{formated_prompt}\"'\n return formated_prompt\n", "fileTypes": [], "file_path": "", "password": false, @@ -1491,7 +1491,7 @@ "list": false, "show": true, "multiline": true, - "value": "from pathlib import Path\nfrom typing import Any, Dict\n\nfrom langflow.base.data.utils import TEXT_FILE_TYPES, parse_text_file_to_record\nfrom langflow.custom import CustomComponent\nfrom langflow.schema import Record\n\n\nclass FileComponent(CustomComponent):\n display_name = \"File\"\n description = \"A generic file loader.\"\n icon = \"file-text\"\n\n def build_config(self) -> Dict[str, Any]:\n return {\n \"path\": {\n \"display_name\": \"Path\",\n \"field_type\": \"file\",\n \"file_types\": TEXT_FILE_TYPES,\n \"info\": f\"Supported file types: {', '.join(TEXT_FILE_TYPES)}\",\n },\n \"silent_errors\": {\n \"display_name\": \"Silent Errors\",\n \"advanced\": True,\n \"info\": \"If true, errors will not raise an exception.\",\n },\n }\n\n def load_file(self, path: str, silent_errors: bool = False) -> Record:\n resolved_path = self.resolve_path(path)\n path_obj = Path(resolved_path)\n extension = path_obj.suffix[1:].lower()\n if extension == \"doc\":\n raise ValueError(\"doc files are not supported. Please save as .docx\")\n if extension not in TEXT_FILE_TYPES:\n raise ValueError(f\"Unsupported file type: {extension}\")\n record = parse_text_file_to_record(resolved_path, silent_errors)\n self.status = record if record else \"No data\"\n return record or Record()\n\n def build(\n self,\n path: str,\n silent_errors: bool = False,\n ) -> Record:\n record = self.load_file(path, silent_errors)\n self.status = record\n return record\n", + "value": "from pathlib import Path\nfrom typing import Any, Dict\n\nfrom langflow.base.data.utils import TEXT_FILE_TYPES, parse_text_file_to_record\nfrom langflow.interface.custom.custom_component import CustomComponent\nfrom langflow.schema import Record\n\n\nclass FileComponent(CustomComponent):\n display_name = \"File\"\n description = \"A generic file loader.\"\n icon = \"file-text\"\n\n def build_config(self) -> Dict[str, Any]:\n return {\n \"path\": {\n \"display_name\": \"Path\",\n \"field_type\": \"file\",\n \"file_types\": TEXT_FILE_TYPES,\n \"info\": f\"Supported file types: {', '.join(TEXT_FILE_TYPES)}\",\n },\n \"silent_errors\": {\n \"display_name\": \"Silent Errors\",\n \"advanced\": True,\n \"info\": \"If true, errors will not raise an exception.\",\n },\n }\n\n def load_file(self, path: str, silent_errors: bool = False) -> Record:\n resolved_path = self.resolve_path(path)\n path_obj = Path(resolved_path)\n extension = path_obj.suffix[1:].lower()\n if extension == \"doc\":\n raise ValueError(\"doc files are not supported. Please save as .docx\")\n if extension not in TEXT_FILE_TYPES:\n raise ValueError(f\"Unsupported file type: {extension}\")\n record = parse_text_file_to_record(resolved_path, silent_errors)\n self.status = record if record else \"No data\"\n return record or Record()\n\n def build(\n self,\n path: str,\n silent_errors: bool = False,\n ) -> Record:\n record = self.load_file(path, silent_errors)\n self.status = record\n return record\n", "fileTypes": [], "file_path": "", "password": false, @@ -1631,7 +1631,7 @@ "list": false, "show": true, "multiline": true, - "value": "from typing import Optional\n\nfrom langchain_core.documents import Document\nfrom langchain_text_splitters import RecursiveCharacterTextSplitter\n\nfrom langflow.custom import CustomComponent\nfrom langflow.schema import Record\nfrom langflow.utils.util import build_loader_repr_from_records, unescape_string\n\n\nclass RecursiveCharacterTextSplitterComponent(CustomComponent):\n display_name: str = \"Recursive Character Text Splitter\"\n description: str = \"Split text into chunks of a specified length.\"\n documentation: str = \"https://docs.langflow.org/components/text-splitters#recursivecharactertextsplitter\"\n\n def build_config(self):\n return {\n \"inputs\": {\n \"display_name\": \"Input\",\n \"info\": \"The texts to split.\",\n \"input_types\": [\"Document\", \"Record\"],\n },\n \"separators\": {\n \"display_name\": \"Separators\",\n \"info\": 'The characters to split on.\\nIf left empty defaults to [\"\\\\n\\\\n\", \"\\\\n\", \" \", \"\"].',\n \"is_list\": True,\n },\n \"chunk_size\": {\n \"display_name\": \"Chunk Size\",\n \"info\": \"The maximum length of each chunk.\",\n \"field_type\": \"int\",\n \"value\": 1000,\n },\n \"chunk_overlap\": {\n \"display_name\": \"Chunk Overlap\",\n \"info\": \"The amount of overlap between chunks.\",\n \"field_type\": \"int\",\n \"value\": 200,\n },\n \"code\": {\"show\": False},\n }\n\n def build(\n self,\n inputs: list[Document],\n separators: Optional[list[str]] = None,\n chunk_size: Optional[int] = 1000,\n chunk_overlap: Optional[int] = 200,\n ) -> list[Record]:\n \"\"\"\n Split text into chunks of a specified length.\n\n Args:\n separators (list[str]): The characters to split on.\n chunk_size (int): The maximum length of each chunk.\n chunk_overlap (int): The amount of overlap between chunks.\n length_function (function): The function to use to calculate the length of the text.\n\n Returns:\n list[str]: The chunks of text.\n \"\"\"\n\n if separators == \"\":\n separators = None\n elif separators:\n # check if the separators list has escaped characters\n # if there are escaped characters, unescape them\n separators = [unescape_string(x) for x in separators]\n\n # Make sure chunk_size and chunk_overlap are ints\n if isinstance(chunk_size, str):\n chunk_size = int(chunk_size)\n if isinstance(chunk_overlap, str):\n chunk_overlap = int(chunk_overlap)\n splitter = RecursiveCharacterTextSplitter(\n separators=separators,\n chunk_size=chunk_size,\n chunk_overlap=chunk_overlap,\n )\n documents = []\n for _input in inputs:\n if isinstance(_input, Record):\n documents.append(_input.to_lc_document())\n else:\n documents.append(_input)\n docs = splitter.split_documents(documents)\n records = self.to_records(docs)\n self.repr_value = build_loader_repr_from_records(records)\n return records\n", + "value": "from typing import Optional\nfrom langchain_core.documents import Document\n\nfrom langflow.interface.custom.custom_component import CustomComponent\nfrom langflow.schema import Record\nfrom langflow.utils.util import build_loader_repr_from_records, unescape_string\nfrom langchain_text_splitters import RecursiveCharacterTextSplitter\n\n\nclass RecursiveCharacterTextSplitterComponent(CustomComponent):\n display_name: str = \"Recursive Character Text Splitter\"\n description: str = \"Split text into chunks of a specified length.\"\n documentation: str = \"https://docs.langflow.org/components/text-splitters#recursivecharactertextsplitter\"\n\n def build_config(self):\n return {\n \"inputs\": {\n \"display_name\": \"Input\",\n \"info\": \"The texts to split.\",\n \"input_types\": [\"Document\", \"Record\"],\n },\n \"separators\": {\n \"display_name\": \"Separators\",\n \"info\": 'The characters to split on.\\nIf left empty defaults to [\"\\\\n\\\\n\", \"\\\\n\", \" \", \"\"].',\n \"is_list\": True,\n },\n \"chunk_size\": {\n \"display_name\": \"Chunk Size\",\n \"info\": \"The maximum length of each chunk.\",\n \"field_type\": \"int\",\n \"value\": 1000,\n },\n \"chunk_overlap\": {\n \"display_name\": \"Chunk Overlap\",\n \"info\": \"The amount of overlap between chunks.\",\n \"field_type\": \"int\",\n \"value\": 200,\n },\n \"code\": {\"show\": False},\n }\n\n def build(\n self,\n inputs: list[Document],\n separators: Optional[list[str]] = None,\n chunk_size: Optional[int] = 1000,\n chunk_overlap: Optional[int] = 200,\n ) -> list[Record]:\n \"\"\"\n Split text into chunks of a specified length.\n\n Args:\n separators (list[str]): The characters to split on.\n chunk_size (int): The maximum length of each chunk.\n chunk_overlap (int): The amount of overlap between chunks.\n length_function (function): The function to use to calculate the length of the text.\n\n Returns:\n list[str]: The chunks of text.\n \"\"\"\n\n if separators == \"\":\n separators = None\n elif separators:\n # check if the separators list has escaped characters\n # if there are escaped characters, unescape them\n separators = [unescape_string(x) for x in separators]\n\n # Make sure chunk_size and chunk_overlap are ints\n if isinstance(chunk_size, str):\n chunk_size = int(chunk_size)\n if isinstance(chunk_overlap, str):\n chunk_overlap = int(chunk_overlap)\n splitter = RecursiveCharacterTextSplitter(\n separators=separators,\n chunk_size=chunk_size,\n chunk_overlap=chunk_overlap,\n )\n documents = []\n for _input in inputs:\n if isinstance(_input, Record):\n documents.append(_input.to_lc_document())\n else:\n documents.append(_input)\n docs = splitter.split_documents(documents)\n records = self.to_records(docs)\n self.repr_value = build_loader_repr_from_records(records)\n return records\n", "fileTypes": [], "file_path": "", "password": false, @@ -2632,7 +2632,7 @@ "list": false, "show": true, "multiline": true, - "value": "from typing import Dict, List, Optional\n\nfrom langchain_openai.embeddings.base import OpenAIEmbeddings\nfrom pydantic.v1 import SecretStr\n\nfrom langflow.custom import CustomComponent\nfrom langflow.field_typing import Embeddings, NestedDict\n\n\nclass OpenAIEmbeddingsComponent(CustomComponent):\n display_name = \"OpenAI Embeddings\"\n description = \"Generate embeddings using OpenAI models.\"\n\n def build_config(self):\n return {\n \"allowed_special\": {\n \"display_name\": \"Allowed Special\",\n \"advanced\": True,\n \"field_type\": \"str\",\n \"is_list\": True,\n },\n \"default_headers\": {\n \"display_name\": \"Default Headers\",\n \"advanced\": True,\n \"field_type\": \"dict\",\n },\n \"default_query\": {\n \"display_name\": \"Default Query\",\n \"advanced\": True,\n \"field_type\": \"NestedDict\",\n },\n \"disallowed_special\": {\n \"display_name\": \"Disallowed Special\",\n \"advanced\": True,\n \"field_type\": \"str\",\n \"is_list\": True,\n },\n \"chunk_size\": {\"display_name\": \"Chunk Size\", \"advanced\": True},\n \"client\": {\"display_name\": \"Client\", \"advanced\": True},\n \"deployment\": {\"display_name\": \"Deployment\", \"advanced\": True},\n \"embedding_ctx_length\": {\n \"display_name\": \"Embedding Context Length\",\n \"advanced\": True,\n },\n \"max_retries\": {\"display_name\": \"Max Retries\", \"advanced\": True},\n \"model\": {\n \"display_name\": \"Model\",\n \"advanced\": False,\n \"options\": [\n \"text-embedding-3-small\",\n \"text-embedding-3-large\",\n \"text-embedding-ada-002\",\n ],\n },\n \"model_kwargs\": {\"display_name\": \"Model Kwargs\", \"advanced\": True},\n \"openai_api_base\": {\n \"display_name\": \"OpenAI API Base\",\n \"password\": True,\n \"advanced\": True,\n },\n \"openai_api_key\": {\"display_name\": \"OpenAI API Key\", \"password\": True},\n \"openai_api_type\": {\n \"display_name\": \"OpenAI API Type\",\n \"advanced\": True,\n \"password\": True,\n },\n \"openai_api_version\": {\n \"display_name\": \"OpenAI API Version\",\n \"advanced\": True,\n },\n \"openai_organization\": {\n \"display_name\": \"OpenAI Organization\",\n \"advanced\": True,\n },\n \"openai_proxy\": {\"display_name\": \"OpenAI Proxy\", \"advanced\": True},\n \"request_timeout\": {\"display_name\": \"Request Timeout\", \"advanced\": True},\n \"show_progress_bar\": {\n \"display_name\": \"Show Progress Bar\",\n \"advanced\": True,\n },\n \"skip_empty\": {\"display_name\": \"Skip Empty\", \"advanced\": True},\n \"tiktoken_model_name\": {\n \"display_name\": \"TikToken Model Name\",\n \"advanced\": True,\n },\n \"tiktoken_enable\": {\"display_name\": \"TikToken Enable\", \"advanced\": True},\n }\n\n def build(\n self,\n openai_api_key: str,\n default_headers: Optional[Dict[str, str]] = None,\n default_query: Optional[NestedDict] = {},\n allowed_special: List[str] = [],\n disallowed_special: List[str] = [\"all\"],\n chunk_size: int = 1000,\n deployment: str = \"text-embedding-ada-002\",\n embedding_ctx_length: int = 8191,\n max_retries: int = 6,\n model: str = \"text-embedding-ada-002\",\n model_kwargs: NestedDict = {},\n openai_api_base: Optional[str] = None,\n openai_api_type: Optional[str] = None,\n openai_api_version: Optional[str] = None,\n openai_organization: Optional[str] = None,\n openai_proxy: Optional[str] = None,\n request_timeout: Optional[float] = None,\n show_progress_bar: bool = False,\n skip_empty: bool = False,\n tiktoken_enable: bool = True,\n tiktoken_model_name: Optional[str] = None,\n ) -> Embeddings:\n # This is to avoid errors with Vector Stores (e.g Chroma)\n if disallowed_special == [\"all\"]:\n disallowed_special = \"all\" # type: ignore\n if openai_api_key:\n api_key = SecretStr(openai_api_key)\n else:\n api_key = None\n\n return OpenAIEmbeddings(\n tiktoken_enabled=tiktoken_enable,\n default_headers=default_headers,\n default_query=default_query,\n allowed_special=set(allowed_special),\n disallowed_special=\"all\",\n chunk_size=chunk_size,\n deployment=deployment,\n embedding_ctx_length=embedding_ctx_length,\n max_retries=max_retries,\n model=model,\n model_kwargs=model_kwargs,\n base_url=openai_api_base,\n api_key=api_key,\n openai_api_type=openai_api_type,\n api_version=openai_api_version,\n organization=openai_organization,\n openai_proxy=openai_proxy,\n timeout=request_timeout,\n show_progress_bar=show_progress_bar,\n skip_empty=skip_empty,\n tiktoken_model_name=tiktoken_model_name,\n )\n", + "value": "from typing import Any, Dict, List, Optional\n\nfrom langchain_openai.embeddings.base import OpenAIEmbeddings\nfrom pydantic.v1 import SecretStr\n\nfrom langflow.field_typing import Embeddings, NestedDict\nfrom langflow.interface.custom.custom_component import CustomComponent\n\n\nclass OpenAIEmbeddingsComponent(CustomComponent):\n display_name = \"OpenAI Embeddings\"\n description = \"Generate embeddings using OpenAI models.\"\n\n def build_config(self):\n return {\n \"allowed_special\": {\n \"display_name\": \"Allowed Special\",\n \"advanced\": True,\n \"field_type\": \"str\",\n \"is_list\": True,\n },\n \"default_headers\": {\n \"display_name\": \"Default Headers\",\n \"advanced\": True,\n \"field_type\": \"dict\",\n },\n \"default_query\": {\n \"display_name\": \"Default Query\",\n \"advanced\": True,\n \"field_type\": \"NestedDict\",\n },\n \"disallowed_special\": {\n \"display_name\": \"Disallowed Special\",\n \"advanced\": True,\n \"field_type\": \"str\",\n \"is_list\": True,\n },\n \"chunk_size\": {\"display_name\": \"Chunk Size\", \"advanced\": True},\n \"client\": {\"display_name\": \"Client\", \"advanced\": True},\n \"deployment\": {\"display_name\": \"Deployment\", \"advanced\": True},\n \"embedding_ctx_length\": {\n \"display_name\": \"Embedding Context Length\",\n \"advanced\": True,\n },\n \"max_retries\": {\"display_name\": \"Max Retries\", \"advanced\": True},\n \"model\": {\n \"display_name\": \"Model\",\n \"advanced\": False,\n \"options\": [\n \"text-embedding-3-small\",\n \"text-embedding-3-large\",\n \"text-embedding-ada-002\",\n ],\n },\n \"model_kwargs\": {\"display_name\": \"Model Kwargs\", \"advanced\": True},\n \"openai_api_base\": {\n \"display_name\": \"OpenAI API Base\",\n \"password\": True,\n \"advanced\": True,\n },\n \"openai_api_key\": {\"display_name\": \"OpenAI API Key\", \"password\": True},\n \"openai_api_type\": {\n \"display_name\": \"OpenAI API Type\",\n \"advanced\": True,\n \"password\": True,\n },\n \"openai_api_version\": {\n \"display_name\": \"OpenAI API Version\",\n \"advanced\": True,\n },\n \"openai_organization\": {\n \"display_name\": \"OpenAI Organization\",\n \"advanced\": True,\n },\n \"openai_proxy\": {\"display_name\": \"OpenAI Proxy\", \"advanced\": True},\n \"request_timeout\": {\"display_name\": \"Request Timeout\", \"advanced\": True},\n \"show_progress_bar\": {\n \"display_name\": \"Show Progress Bar\",\n \"advanced\": True,\n },\n \"skip_empty\": {\"display_name\": \"Skip Empty\", \"advanced\": True},\n \"tiktoken_model_name\": {\n \"display_name\": \"TikToken Model Name\",\n \"advanced\": True,\n },\n \"tiktoken_enable\": {\"display_name\": \"TikToken Enable\", \"advanced\": True},\n }\n\n def build(\n self,\n openai_api_key: str,\n default_headers: Optional[Dict[str, str]] = None,\n default_query: Optional[NestedDict] = {},\n allowed_special: List[str] = [],\n disallowed_special: List[str] = [\"all\"],\n chunk_size: int = 1000,\n client: Optional[Any] = None,\n deployment: str = \"text-embedding-ada-002\",\n embedding_ctx_length: int = 8191,\n max_retries: int = 6,\n model: str = \"text-embedding-ada-002\",\n model_kwargs: NestedDict = {},\n openai_api_base: Optional[str] = None,\n openai_api_type: Optional[str] = None,\n openai_api_version: Optional[str] = None,\n openai_organization: Optional[str] = None,\n openai_proxy: Optional[str] = None,\n request_timeout: Optional[float] = None,\n show_progress_bar: bool = False,\n skip_empty: bool = False,\n tiktoken_enable: bool = True,\n tiktoken_model_name: Optional[str] = None,\n ) -> Embeddings:\n # This is to avoid errors with Vector Stores (e.g Chroma)\n if disallowed_special == [\"all\"]:\n disallowed_special = \"all\" # type: ignore\n if openai_api_key:\n api_key = SecretStr(openai_api_key)\n else:\n api_key = None\n\n return OpenAIEmbeddings(\n tiktoken_enabled=tiktoken_enable,\n default_headers=default_headers,\n default_query=default_query,\n allowed_special=set(allowed_special),\n disallowed_special=\"all\",\n chunk_size=chunk_size,\n client=client,\n deployment=deployment,\n embedding_ctx_length=embedding_ctx_length,\n max_retries=max_retries,\n model=model,\n model_kwargs=model_kwargs,\n base_url=openai_api_base,\n api_key=api_key,\n openai_api_type=openai_api_type,\n api_version=openai_api_version,\n organization=openai_organization,\n openai_proxy=openai_proxy,\n timeout=request_timeout,\n show_progress_bar=show_progress_bar,\n skip_empty=skip_empty,\n tiktoken_model_name=tiktoken_model_name,\n )\n", "fileTypes": [], "file_path": "", "password": false, @@ -2846,7 +2846,7 @@ "advanced": false, "dynamic": false, "info": "", - "load_from_db": true, + "load_from_db": false, "title_case": false, "input_types": [ "Text" diff --git a/src/backend/base/langflow/utils/validate.py b/src/backend/base/langflow/utils/validate.py index 0871dbd82..bd7827199 100644 --- a/src/backend/base/langflow/utils/validate.py +++ b/src/backend/base/langflow/utils/validate.py @@ -253,6 +253,9 @@ def build_class_constructor(compiled_class, exec_globals, class_name): globals()[module_name] = module instance = exec_globals[class_name](*args, **kwargs) + # Get selected type from global scope + if instance.selected_output_type in exec_globals: + instance.selected_output_type = exec_globals[instance.selected_output_type] return instance build_custom_class.__globals__.update(exec_globals) diff --git a/src/frontend/src/customNodes/genericNode/components/HandleTooltipComponent/index.tsx b/src/frontend/src/customNodes/genericNode/components/HandleTooltipComponent/index.tsx new file mode 100644 index 000000000..2dddabbb5 --- /dev/null +++ b/src/frontend/src/customNodes/genericNode/components/HandleTooltipComponent/index.tsx @@ -0,0 +1,31 @@ +import { useRef } from "react"; +import { TOOLTIP_EMPTY } from "../../../../constants/constants"; +import { groupByFamily } from "../../../../utils/utils"; +import TooltipRenderComponent from "../tooltipRenderComponent"; +import { useTypesStore } from "../../../../stores/typesStore"; +import { NodeType } from "../../../../types/flow"; +import useFlowStore from "../../../../stores/flowStore"; + +export default function HandleTooltips({ + left, + tooltipTitle, +}: { + left: boolean; + nodes: NodeType[]; + tooltipTitle: string; +}) { + const myData = useTypesStore((state) => state.data); + const nodes = useFlowStore((state) => state.nodes); + + let groupedObj: any = groupByFamily(myData, tooltipTitle!, left, nodes!); + + if (groupedObj && groupedObj.length > 0) { + //@ts-ignore + return groupedObj.map((item, index) => { + return ; + }); + } else { + //@ts-ignore + return {TOOLTIP_EMPTY}; + } +} diff --git a/src/frontend/src/customNodes/genericNode/components/OutputComponent/index.tsx b/src/frontend/src/customNodes/genericNode/components/OutputComponent/index.tsx new file mode 100644 index 000000000..69ca3965c --- /dev/null +++ b/src/frontend/src/customNodes/genericNode/components/OutputComponent/index.tsx @@ -0,0 +1,59 @@ +import ForwardedIconComponent from "../../../../components/genericIconComponent"; +import { outputComponentType } from "../../../../types/components"; +import { cn } from "../../../../utils/utils"; +import useFlowStore from "../../../../stores/flowStore"; +import { NodeDataType } from "../../../../types/flow"; +import { cloneDeep } from "lodash"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "../../../../components/ui/dropdown-menu"; +import { useUpdateNodeInternals } from "reactflow"; + +export default function OutputComponent({ + selected, + types, + frozen = false, + nodeId, + idx, +}: outputComponentType) { + const setNode = useFlowStore((state) => state.setNode); + const updateNodeInternals = useUpdateNodeInternals(); + + if (types.length < 2) { + return {selected}; + } + + return ( + + + + {selected} + + + + + {types.map((type) => ( + { + // TODO: UDPDATE SET NODE TO NEW NODE FORM + setNode(nodeId, (node) => { + const newNode = cloneDeep(node); + (newNode.data as NodeDataType).node!.outputs![idx].selected = + type; + return newNode; + }); + updateNodeInternals(nodeId); + }} + > + {type} + + ))} + + + ); +} diff --git a/src/frontend/src/customNodes/genericNode/components/parameterComponent/index.tsx b/src/frontend/src/customNodes/genericNode/components/parameterComponent/index.tsx new file mode 100644 index 000000000..fe84645a6 --- /dev/null +++ b/src/frontend/src/customNodes/genericNode/components/parameterComponent/index.tsx @@ -0,0 +1,591 @@ +import { cloneDeep } from "lodash"; +import { ReactNode, useEffect, useRef, useState } from "react"; +import { Handle, Position, useUpdateNodeInternals } from "reactflow"; +import CodeAreaComponent from "../../../../components/codeAreaComponent"; +import DictComponent from "../../../../components/dictComponent"; +import Dropdown from "../../../../components/dropdownComponent"; +import FloatComponent from "../../../../components/floatComponent"; +import ForwardedIconComponent, { + default as IconComponent, +} from "../../../../components/genericIconComponent"; +import InputFileComponent from "../../../../components/inputFileComponent"; +import InputGlobalComponent from "../../../../components/inputGlobalComponent"; +import InputListComponent from "../../../../components/inputListComponent"; +import IntComponent from "../../../../components/intComponent"; +import KeypairListComponent from "../../../../components/keypairListComponent"; +import PromptAreaComponent from "../../../../components/promptComponent"; +import ShadTooltip from "../../../../components/shadTooltipComponent"; +import TextAreaComponent from "../../../../components/textAreaComponent"; +import ToggleShadComponent from "../../../../components/toggleShadComponent"; +import { Button } from "../../../../components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "../../../../components/ui/dropdown-menu"; +import { RefreshButton } from "../../../../components/ui/refreshButton"; +import { + LANGFLOW_SUPPORTED_TYPES, + TOOLTIP_EMPTY, +} from "../../../../constants/constants"; +import { Case } from "../../../../shared/components/caseComponent"; +import useAlertStore from "../../../../stores/alertStore"; +import useFlowStore from "../../../../stores/flowStore"; +import useFlowsManagerStore from "../../../../stores/flowsManagerStore"; +import { useTypesStore } from "../../../../stores/typesStore"; +import { APIClassType } from "../../../../types/api"; +import { ParameterComponentType } from "../../../../types/components"; +import { + debouncedHandleUpdateValues, + handleUpdateValues, +} from "../../../../utils/parameterUtils"; +import { + convertObjToArray, + convertValuesToNumbers, + hasDuplicateKeys, + isValidConnection, + scapedJSONStringfy, +} from "../../../../utils/reactflowUtils"; +import { nodeColors } from "../../../../utils/styleUtils"; +import { classNames, cn, groupByFamily } from "../../../../utils/utils"; +import useFetchDataOnMount from "../../../hooks/use-fetch-data-on-mount"; +import useHandleOnNewValue from "../../../hooks/use-handle-new-value"; +import useHandleNodeClass from "../../../hooks/use-handle-node-class"; +import useHandleRefreshButtonPress from "../../../hooks/use-handle-refresh-buttons"; +import TooltipRenderComponent from "../tooltipRenderComponent"; +import HandleTooltips from "../HandleTooltipComponent"; +import OutputComponent from "../OutputComponent"; + +export default function ParameterComponent({ + left, + id, + data, + tooltipTitle, + title, + color, + type, + name = "", + required = false, + optionalHandle = null, + info = "", + proxy, + showNode, + index, +}: ParameterComponentType): JSX.Element { + const infoHtml = useRef(null); + const nodes = useFlowStore((state) => state.nodes); + const edges = useFlowStore((state) => state.edges); + const setNode = useFlowStore((state) => state.setNode); + const myData = useTypesStore((state) => state.data); + const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot); + const [isLoading, setIsLoading] = useState(false); + const updateNodeInternals = useUpdateNodeInternals(); + const [errorDuplicateKey, setErrorDuplicateKey] = useState(false); + const setFilterEdge = useFlowStore((state) => state.setFilterEdge); + + const { handleOnNewValue: handleOnNewValueHook } = useHandleOnNewValue( + data, + name, + takeSnapshot, + handleUpdateValues, + debouncedHandleUpdateValues, + setNode, + isLoading, + setIsLoading, + ); + + const { handleNodeClass: handleNodeClassHook } = useHandleNodeClass( + data, + name, + takeSnapshot, + setNode, + updateNodeInternals, + ); + + const { handleRefreshButtonPress: handleRefreshButtonPressHook } = + useHandleRefreshButtonPress(setIsLoading, setNode); + + let disabled = + edges.some( + (edge) => + edge.targetHandle === scapedJSONStringfy(proxy ? { ...id, proxy } : id), + ) ?? false; + + const handleRefreshButtonPress = async (name, data) => { + handleRefreshButtonPressHook(name, data); + }; + + useFetchDataOnMount(data, name, handleUpdateValues, setNode, setIsLoading); + + const handleOnNewValue = async ( + newValue: string | string[] | boolean | Object[], + skipSnapshot: boolean | undefined = false, + ): Promise => { + handleOnNewValueHook(newValue, skipSnapshot); + }; + + const handleNodeClass = (newNodeClass: APIClassType, code?: string): void => { + handleNodeClassHook(newNodeClass, code); + }; + + useEffect(() => { + // @ts-ignore + infoHtml.current = ( +
+ {info.split("\n").map((line, index) => ( +

+ {line} +

+ ))} +
+ ); + }, [info]); + + function renderTitle() { + return !left ? ( + + ) : ( + {title} + ); + } + + // If optionalHandle is an empty list, then it is not an optional handle + if (optionalHandle && optionalHandle.length === 0) { + optionalHandle = null; + } + + return !showNode ? ( + left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? ( + <> + ) : ( + + ) + ) : ( + + ); +} diff --git a/src/frontend/src/customNodes/genericNode/index.tsx b/src/frontend/src/customNodes/genericNode/index.tsx index fbbaa82c6..9e22d9ac1 100644 --- a/src/frontend/src/customNodes/genericNode/index.tsx +++ b/src/frontend/src/customNodes/genericNode/index.tsx @@ -553,7 +553,7 @@ export default function GenericNode({ data.node!.template[templateField].show && !data.node!.template[templateField].advanced && ( 0 - ? data.node.output_types.join(" | ") + ? data.node.output_types.join("|") : data.type } tooltipTitle={data.node?.base_classes.join("\n")} @@ -644,6 +645,7 @@ export default function GenericNode({ baseClasses: data.node!.base_classes, id: data.id, dataType: data.type, + idx: 0, }} type={data.node?.base_classes.join("|")} left={false} @@ -803,7 +805,7 @@ export default function GenericNode({ {data.node!.template[templateField].show && !data.node!.template[templateField].advanced ? ( {" "} -
- {data.node!.base_classes.length > 0 && - // if conditional_paths in data.node.conditional_paths - // then buildParameterComponent for each conditionalPath - // else buildParameterComponent for each data.node.base_classes - // first we check if there are any conditional paths - data.node!.conditional_paths && - data.node!.conditional_paths.length > 0 - ? data.node!.conditional_paths.map((conditionalPath) => - buildParameterComponent({ - data, - conditionalPath, - showNode, - left: false, - }) - ) - : buildParameterComponent({ - data, - conditionalPath: null, - showNode, - left: false, + {data.node!.outputs && + data.node!.outputs.length > 0 && + data.node!.outputs.map((output, idx) => ( + + data={data} + color={ + nodeColors[output.selected ?? output.types[0]] ?? + nodeColors[types[output.selected ?? output.types[0]]] ?? + nodeColors[types[data.type]] ?? + nodeColors.unknown + } + title={output.selected ?? output.types[0]} + tooltipTitle={output.selected ?? output.types[0]} + id={{ + baseClasses: [output.selected ?? output.types[0]], + id: data.id, + dataType: data.type, + idx: idx, + }} + type={output.types.join("|")} + left={false} + showNode={showNode} + /> + ))}
)} diff --git a/src/frontend/src/customNodes/hooks/use-fetch-data-on-mount.tsx b/src/frontend/src/customNodes/hooks/use-fetch-data-on-mount.tsx index 3fc3fbe72..0b08cfa1c 100644 --- a/src/frontend/src/customNodes/hooks/use-fetch-data-on-mount.tsx +++ b/src/frontend/src/customNodes/hooks/use-fetch-data-on-mount.tsx @@ -8,7 +8,6 @@ const useFetchDataOnMount = ( name, handleUpdateValues, setNode, - renderTooltips, setIsLoading, ) => { const setErrorData = useAlertStore((state) => state.setErrorData); @@ -44,7 +43,6 @@ const useFetchDataOnMount = ( }); } setIsLoading(false); - renderTooltips(); } } fetchData(); diff --git a/src/frontend/src/customNodes/hooks/use-handle-new-value.tsx b/src/frontend/src/customNodes/hooks/use-handle-new-value.tsx index 7de830eda..c6f026f51 100644 --- a/src/frontend/src/customNodes/hooks/use-handle-new-value.tsx +++ b/src/frontend/src/customNodes/hooks/use-handle-new-value.tsx @@ -9,7 +9,6 @@ const useHandleOnNewValue = ( handleUpdateValues, debouncedHandleUpdateValues, setNode, - renderTooltips, isLoading, setIsLoading, ) => { @@ -65,8 +64,6 @@ const useHandleOnNewValue = ( return newNode; }); - - renderTooltips(); }; return { handleOnNewValue }; diff --git a/src/frontend/src/customNodes/hooks/use-handle-node-class.tsx b/src/frontend/src/customNodes/hooks/use-handle-node-class.tsx index 412658d77..6fb78ce10 100644 --- a/src/frontend/src/customNodes/hooks/use-handle-node-class.tsx +++ b/src/frontend/src/customNodes/hooks/use-handle-node-class.tsx @@ -6,7 +6,6 @@ const useHandleNodeClass = ( takeSnapshot, setNode, updateNodeInternals, - renderTooltips, ) => { const handleNodeClass = (newNodeClass, code) => { if (!data.node) return; @@ -30,8 +29,6 @@ const useHandleNodeClass = ( }); updateNodeInternals(data.id); - - renderTooltips(); }; return { handleNodeClass }; diff --git a/src/frontend/src/customNodes/hooks/use-handle-refresh-buttons.tsx b/src/frontend/src/customNodes/hooks/use-handle-refresh-buttons.tsx index 19f2a3c29..0101e8ee0 100644 --- a/src/frontend/src/customNodes/hooks/use-handle-refresh-buttons.tsx +++ b/src/frontend/src/customNodes/hooks/use-handle-refresh-buttons.tsx @@ -3,7 +3,7 @@ import useAlertStore from "../../stores/alertStore"; import { ResponseErrorDetailAPI } from "../../types/api"; import { handleUpdateValues } from "../../utils/parameterUtils"; -const useHandleRefreshButtonPress = (setIsLoading, setNode, renderTooltips) => { +const useHandleRefreshButtonPress = (setIsLoading, setNode) => { const setErrorData = useAlertStore((state) => state.setErrorData); const handleRefreshButtonPress = async (name, data) => { @@ -30,7 +30,6 @@ const useHandleRefreshButtonPress = (setIsLoading, setNode, renderTooltips) => { }); } setIsLoading(false); - renderTooltips(); }; return { handleRefreshButtonPress }; diff --git a/src/frontend/src/pages/MainPage/services/index.ts b/src/frontend/src/pages/MainPage/services/index.ts index fbfc34389..3e1286d5e 100644 --- a/src/frontend/src/pages/MainPage/services/index.ts +++ b/src/frontend/src/pages/MainPage/services/index.ts @@ -30,12 +30,12 @@ export async function addFolder(data: AddFolderType): Promise { export async function updateFolder( body: FolderType, - folderId: string + folderId: string, ): Promise { try { const response = await api.patch( `${BASE_URL_API}folders/${folderId}`, - body + body, ); return response?.data; } catch (error) { @@ -68,7 +68,7 @@ export async function downloadFlowsFromFolders(folderId: string): Promise<{ }> { try { const response = await api.get( - `${BASE_URL_API}folders/download/${folderId}` + `${BASE_URL_API}folders/download/${folderId}`, ); if (response?.status !== 200) { throw new Error(`HTTP error! status: ${response?.status}`); @@ -82,7 +82,7 @@ export async function downloadFlowsFromFolders(folderId: string): Promise<{ } export async function uploadFlowsFromFolders( - flows: FormData + flows: FormData, ): Promise { try { const response = await api.post(`${BASE_URL_API}folders/upload/`, flows); @@ -99,11 +99,11 @@ export async function uploadFlowsFromFolders( export async function moveFlowToFolder( flowId: string, - folderId: string + folderId: string, ): Promise { try { const response = await api.patch( - `${BASE_URL_API}folders/move_to_folder/${flowId}/${folderId}` + `${BASE_URL_API}folders/move_to_folder/${flowId}/${folderId}`, ); return response?.data; } catch (error) { diff --git a/src/frontend/src/stores/flowsManagerStore.ts b/src/frontend/src/stores/flowsManagerStore.ts index 81637fba0..90bf82169 100644 --- a/src/frontend/src/stores/flowsManagerStore.ts +++ b/src/frontend/src/stores/flowsManagerStore.ts @@ -199,11 +199,10 @@ const useFlowsManagerStore = create((set, get) => ({ position?: XYPosition, fromDragAndDrop?: boolean, ): Promise => { + let flowData = flow + ? processDataFromFlow(flow) + : { nodes: [], edges: [], viewport: { zoom: 1, x: 0, y: 0 } }; if (newProject) { - let flowData = flow - ? processDataFromFlow(flow) - : { nodes: [], edges: [], viewport: { zoom: 1, x: 0, y: 0 } }; - // Create a new flow with a default name if no flow is provided. const folder_id = useFolderStore.getState().folderUrl; const my_collection_id = useFolderStore.getState().myCollectionId; @@ -272,7 +271,7 @@ const useFlowsManagerStore = create((set, get) => ({ useFlowStore .getState() .paste( - { nodes: flow!.data!.nodes, edges: flow!.data!.edges }, + { nodes: flowData?.nodes, edges: flowData?.edges }, position ?? { x: 10, y: 10 }, ); } diff --git a/src/frontend/src/types/api/index.ts b/src/frontend/src/types/api/index.ts index dccab1b31..0fb0595b2 100644 --- a/src/frontend/src/types/api/index.ts +++ b/src/frontend/src/types/api/index.ts @@ -28,6 +28,7 @@ export type APIClassType = { documentation: string; error?: string; official?: boolean; + outputs?: Array<{ types: Array; selected?: string }>; frozen?: boolean; flow?: FlowType; field_order?: string[]; @@ -39,7 +40,8 @@ export type APIClassType = { | FlowType | CustomFieldsType | boolean - | undefined; + | undefined + | Array<{ types: Array; selected?: string }>; }; export type TemplateVariableType = { diff --git a/src/frontend/src/types/components/index.ts b/src/frontend/src/types/components/index.ts index 2433d11d2..b8aba6eea 100644 --- a/src/frontend/src/types/components/index.ts +++ b/src/frontend/src/types/components/index.ts @@ -68,7 +68,7 @@ export type ParameterComponentType = { info?: string; proxy?: { field: string; id: string }; showNode?: boolean; - index?: string; + index: number; onCloseModal?: (close: boolean) => void; }; export type InputListComponentType = { @@ -112,6 +112,14 @@ export type TextAreaComponentType = { readonly?: boolean; }; +export type outputComponentType = { + types: string[]; + selected: string; + nodeId: string; + frozen?: boolean; + idx: number; +}; + export type PromptAreaComponentType = { field_name?: string; nodeClass?: APIClassType; diff --git a/src/frontend/src/types/flow/index.ts b/src/frontend/src/types/flow/index.ts index d23ee951a..6c30dae40 100644 --- a/src/frontend/src/types/flow/index.ts +++ b/src/frontend/src/types/flow/index.ts @@ -34,6 +34,7 @@ export type NodeDataType = { node?: APIClassType; id: string; output_types?: string[]; + selected_output_type?: string; buildStatus?: BuildStatus; }; // FlowStyleType is the type of the style object that is used to style the @@ -58,6 +59,7 @@ export type sourceHandleType = { id: string; baseClasses: string[]; conditionalPath?: string | null; + idx: number; }; //left side export type targetHandleType = { diff --git a/src/frontend/src/utils/buildUtils.ts b/src/frontend/src/utils/buildUtils.ts index 00eca937a..70c5b4069 100644 --- a/src/frontend/src/utils/buildUtils.ts +++ b/src/frontend/src/utils/buildUtils.ts @@ -16,7 +16,7 @@ type BuildVerticesParams = { onBuildUpdate?: ( data: VertexBuildTypeAPI, status: BuildStatus, - buildId: string + buildId: string, ) => void; // Replace any with the actual type if it's not any onBuildComplete?: (allNodesValid: boolean) => void; onBuildError?: (title, list, idList: VertexLayerElementType[]) => void; @@ -53,7 +53,7 @@ export async function updateVerticesOrder( startNodeId?: string | null, stopNodeId?: string | null, nodes?: Node[], - edges?: Edge[] + edges?: Edge[], ): Promise<{ verticesLayers: VertexLayerElementType[][]; verticesIds: string[]; @@ -69,7 +69,7 @@ export async function updateVerticesOrder( startNodeId, stopNodeId, nodes, - edges + edges, ); } catch (error: any) { setErrorData({ @@ -125,7 +125,7 @@ export async function buildVertices({ startNodeId, stopNodeId, nodes, - edges + edges, ); if (onValidateNodes) { try { @@ -187,14 +187,14 @@ export async function buildVertices({ onBuildUpdate( getInactiveVertexData(element.id), BuildStatus.INACTIVE, - runId + runId, ); } if (element.reference) { onBuildUpdate( getInactiveVertexData(element.reference), BuildStatus.INACTIVE, - runId + runId, ); } buildResults.push(false); @@ -219,7 +219,7 @@ export async function buildVertices({ if (stop) { return; } - }) + }), ); // Once the current layer is built, move to the next layer currentLayerIndex += 1; @@ -262,7 +262,7 @@ async function buildVertex({ onBuildError!( "Error Building Component", [buildData.params], - verticesIds.map((id) => ({ id })) + verticesIds.map((id) => ({ id })), ); stopBuild(); } @@ -273,7 +273,7 @@ async function buildVertex({ onBuildError!( "Error Building Component", [(error as AxiosError).response?.data?.detail ?? "Unknown Error"], - verticesIds.map((id) => ({ id })) + verticesIds.map((id) => ({ id })), ); stopBuild(); } diff --git a/src/frontend/src/utils/reactflowUtils.ts b/src/frontend/src/utils/reactflowUtils.ts index b379b445e..a51baee23 100644 --- a/src/frontend/src/utils/reactflowUtils.ts +++ b/src/frontend/src/utils/reactflowUtils.ts @@ -16,6 +16,8 @@ import { specialCharsRegex, } from "../constants/constants"; import { downloadFlowsFromDatabase } from "../controllers/API"; +import getFieldTitle from "../customNodes/utils/get-field-title"; +import { DESCRIPTIONS } from "../flow_constants"; import { APIClassType, APIKindType, @@ -37,8 +39,6 @@ import { updateEdgesHandleIdsType, } from "../types/utils/reactflowUtils"; import { createRandomKey, toTitleCase } from "./utils"; -import { DESCRIPTIONS } from "../flow_constants"; -import getFieldTitle from "../customNodes/utils/get-field-title"; const uid = new ShortUniqueId({ length: 5 }); export function checkChatInput(nodes: Node[]) { @@ -46,6 +46,7 @@ export function checkChatInput(nodes: Node[]) { } export function cleanEdges(nodes: Node[], edges: Edge[]) { + console.log("cleanEdges"); let newEdges = cloneDeep(edges); edges.forEach((edge) => { // check if the source and target node still exists @@ -75,10 +76,12 @@ export function cleanEdges(nodes: Node[], edges: Edge[]) { } } if (sourceHandle) { + const index = scapeJSONParse(sourceHandle).idx ?? 0; const id: sourceHandleType = { id: sourceNode.data.id, - baseClasses: sourceNode.data.node!.base_classes, + baseClasses: [sourceNode.data.node.outputs[index].selected], dataType: sourceNode.data.type, + idx: index, }; if (scapedJSONStringfy(id) !== sourceHandle) { newEdges = newEdges.filter((e) => e.id !== edge.id); @@ -191,8 +194,8 @@ export const processDataFromFlow = (flow: FlowType, refreshIds = true) => { let data = flow?.data ? flow.data : null; if (data) { processFlowEdges(flow); - //prevent node update for now - // processFlowNodes(flow); + //add dropdown option to nodeOutputs + processFlowNodes(flow); //add animation to text type edges updateEdges(data.edges); // updateNodes(data.nodes, data.edges); @@ -397,6 +400,7 @@ export function updateEdgesHandleIds({ id: sourceNode.data.id, baseClasses: sourceNode.data.node!.base_classes, dataType: sourceNode.data.type, + idx: 0, }; } edge.sourceHandle = scapedJSONStringfy(newSource!); @@ -410,6 +414,49 @@ export function updateEdgesHandleIds({ return newEdges; } +export function updateNewOutput({ nodes, edges }: updateEdgesHandleIdsType) { + console.log("updateNewOutput"); + let newEdges = cloneDeep(edges); + let newNodes = cloneDeep(nodes); + newEdges.forEach((edge) => { + if (edge.sourceHandle && edge.targetHandle) { + let newSourceHandle: sourceHandleType = scapeJSONParse(edge.sourceHandle); + let newTargetHandle: targetHandleType = scapeJSONParse(edge.targetHandle); + let intersection; + if (newTargetHandle.inputTypes && newTargetHandle.inputTypes.length > 0) { + //conjuction subtraction + intersection = newSourceHandle.baseClasses.filter((type) => + newTargetHandle.inputTypes!.includes(type), + ); + } else { + intersection = newSourceHandle.baseClasses.filter( + (type) => type === newTargetHandle.type, + ); + } + const selected = intersection[0]; + newSourceHandle.baseClasses = [selected]; + const id = newSourceHandle.id; + newSourceHandle.idx = 0; + const sourceNodeIndex = newNodes.findIndex((node) => node.id === id); + if (sourceNodeIndex > -1) { + const sourceNode = newNodes[sourceNodeIndex]; + if ( + !sourceNode.data.node?.outputs || + sourceNode.data.node!.outputs!.length === 0 + ) { + sourceNode.data.node!.outputs = [ + { types: sourceNode.data.node!.base_classes, selected: selected }, + ]; + } + } + edge.sourceHandle = scapedJSONStringfy(newSourceHandle); + edge.data.sourceHandle = newSourceHandle; + } + }); + + return { nodes: newNodes, edges: newEdges }; +} + export function handleKeyDown( e: | React.KeyboardEvent @@ -561,6 +608,10 @@ export function checkOldEdgesHandles(edges: Edge[]): boolean { ); } +export function checkOldNodesOutput(nodes: NodeType[]): boolean { + return nodes.some((node) => !node.data.node?.outputs); +} + export function customStringify(obj: any): string { if (typeof obj === "undefined") { return "null"; @@ -1017,6 +1068,15 @@ export function processFlowEdges(flow: FlowType) { }); } +export function processFlowNodes(flow: FlowType) { + if (!flow.data || !flow.data.nodes) return; + if (checkOldNodesOutput(flow.data.nodes)) { + const { nodes, edges } = updateNewOutput(flow.data); + flow.data.nodes = nodes; + flow.data.edges = edges; + } +} + export function expandGroupNode( id: string, flow: FlowType, diff --git a/src/frontend/tests/end-to-end/chatInputOutput.spec.ts b/src/frontend/tests/end-to-end/chatInputOutput.spec.ts index 0828e66e6..b5ab8ac42 100644 --- a/src/frontend/tests/end-to-end/chatInputOutput.spec.ts +++ b/src/frontend/tests/end-to-end/chatInputOutput.spec.ts @@ -24,7 +24,7 @@ test("chat_io_teste", async ({ page }) => { const jsonContent = readFileSync( "tests/end-to-end/assets/ChatTest.json", - "utf-8" + "utf-8", ); await page.getByTestId("blank-flow").click(); @@ -47,7 +47,7 @@ test("chat_io_teste", async ({ page }) => { "drop", { dataTransfer, - } + }, ); await page.getByLabel("fit view").click(); await page.getByText("Playground", { exact: true }).click(); diff --git a/src/frontend/tests/end-to-end/chatInputOutputUser.spec.ts b/src/frontend/tests/end-to-end/chatInputOutputUser.spec.ts index 7c442116e..9399485d3 100644 --- a/src/frontend/tests/end-to-end/chatInputOutputUser.spec.ts +++ b/src/frontend/tests/end-to-end/chatInputOutputUser.spec.ts @@ -57,7 +57,7 @@ test("user must interact with chat with Input/Output", async ({ page }) => { .getByTestId("textarea-input_value") .nth(1) .fill( - "testtesttesttesttesttestte;.;.,;,.;,.;.,;,..,;;;;;;;;;;;;;;;;;;;;;,;.;,.;,.,;.,;.;.,~~çççççççççççççççççççççççççççççççççççççççisdajfdasiopjfaodisjhvoicxjiovjcxizopjviopasjioasfhjaiohf23432432432423423sttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttestççççççççççççççççççççççççççççççççç,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,!" + "testtesttesttesttesttestte;.;.,;,.;,.;.,;,..,;;;;;;;;;;;;;;;;;;;;;,;.;,.;,.,;.,;.;.,~~çççççççççççççççççççççççççççççççççççççççisdajfdasiopjfaodisjhvoicxjiovjcxizopjviopasjioasfhjaiohf23432432432423423sttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttestççççççççççççççççççççççççççççççççç,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,!", ); await page .getByTestId("popover-anchor-input-sender_name") @@ -85,8 +85,8 @@ test("user must interact with chat with Input/Output", async ({ page }) => { await page .getByText( "testtesttesttesttesttestte;.;.,;,.;,.;.,;,..,;;;;;;;;;;;;;;;;;;;;;,;.;,.;,.,;.,;.;.,~~çççççççççççççççççççççççççççççççççççççççisdajfdasiopjfaodisjhvoicxjiovjcxizopjviopasjioasfhjaiohf23432432432423423sttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttestççççççççççççççççççççççççççççççççç,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,!", - { exact: true } + { exact: true }, ) - .isVisible() + .isVisible(), ); }); diff --git a/src/frontend/tests/end-to-end/inputListComponent.spec.ts b/src/frontend/tests/end-to-end/inputListComponent.spec.ts index a434a4529..ab687e7cb 100644 --- a/src/frontend/tests/end-to-end/inputListComponent.spec.ts +++ b/src/frontend/tests/end-to-end/inputListComponent.spec.ts @@ -41,19 +41,19 @@ test("InputListComponent", async ({ page }) => { await page.getByTestId("edit-button-modal").click(); expect( - await page.getByTestId("showmetadata_indexing_exclude").isChecked() + await page.getByTestId("showmetadata_indexing_exclude").isChecked(), ).toBeFalsy(); await page.getByTestId("showmetadata_indexing_exclude").click(); expect( - await page.getByTestId("showmetadata_indexing_exclude").isChecked() + await page.getByTestId("showmetadata_indexing_exclude").isChecked(), ).toBeTruthy(); expect( - await page.getByTestId("showmetadata_indexing_include").isChecked() + await page.getByTestId("showmetadata_indexing_include").isChecked(), ).toBeFalsy(); await page.getByTestId("showmetadata_indexing_include").click(); expect( - await page.getByTestId("showmetadata_indexing_include").isChecked() + await page.getByTestId("showmetadata_indexing_include").isChecked(), ).toBeTruthy(); await page @@ -93,7 +93,7 @@ test("InputListComponent", async ({ page }) => { .click(); const plusButtonLocator = page.getByTestId( - "input-list-plus-btn_metadata_indexing_include-1" + "input-list-plus-btn_metadata_indexing_include-1", ); const elementCount = await plusButtonLocator?.count(); @@ -164,12 +164,12 @@ test("InputListComponent", async ({ page }) => { .click(); const plusButtonLocatorEdit0 = await page.getByTestId( - "input-list-plus-btn-edit_metadata_indexing_include-0" + "input-list-plus-btn-edit_metadata_indexing_include-0", ); const elementCountEdit0 = await plusButtonLocatorEdit0?.count(); const plusButtonLocatorEdit2 = await page.getByTestId( - "input-list-plus-btn-edit_metadata_indexing_include-2" + "input-list-plus-btn-edit_metadata_indexing_include-2", ); const elementCountEdit2 = await plusButtonLocatorEdit2?.count(); @@ -178,13 +178,13 @@ test("InputListComponent", async ({ page }) => { } const minusButtonLocatorEdit1 = await page.getByTestId( - "input-list-minus-btn-edit_metadata_indexing_include-1" + "input-list-minus-btn-edit_metadata_indexing_include-1", ); const elementCountMinusEdit1 = await minusButtonLocatorEdit1?.count(); const minusButtonLocatorEdit2 = await page.getByTestId( - "input-list-minus-btn-edit_metadata_indexing_include-2" + "input-list-minus-btn-edit_metadata_indexing_include-2", ); const elementCountMinusEdit2 = await minusButtonLocatorEdit2?.count();