From c7464b42b2660001ac087a3130ac94d8f018af8f Mon Sep 17 00:00:00 2001 From: cristhianzl Date: Wed, 6 Mar 2024 23:21:17 -0300 Subject: [PATCH 01/17] =?UTF-8?q?=F0=9F=90=9B=20fix(extraSidebarComponent/?= =?UTF-8?q?index.tsx):=20remove=20unused=20imports=20and=20fix=20broken=20?= =?UTF-8?q?icon=20component=20=E2=9C=A8=20feat(extraSidebarComponent/index?= =?UTF-8?q?.tsx):=20replace=20custom=20icon=20component=20with=20Lucide's?= =?UTF-8?q?=20SparklesIcon=20and=20LinkIcon=20for=20better=20consistency?= =?UTF-8?q?=20and=20maintainability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../extraSidebarComponent/index.tsx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx index bb1e6dac1..38c9f020d 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx @@ -1,12 +1,11 @@ import { cloneDeep } from "lodash"; +import { LinkIcon, SparklesIcon } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import ShadTooltip from "../../../../components/ShadTooltipComponent"; import IconComponent from "../../../../components/genericIconComponent"; import { Input } from "../../../../components/ui/input"; import { Separator } from "../../../../components/ui/separator"; -import { UPLOAD_ERROR_ALERT } from "../../../../constants/alerts_constants"; import { PRIORITY_SIDEBAR_ORDER } from "../../../../constants/constants"; -import ApiModal from "../../../../modals/ApiModal"; import ExportModal from "../../../../modals/exportModal"; import ShareModal from "../../../../modals/shareModal"; import useAlertStore from "../../../../stores/alertStore"; @@ -275,25 +274,25 @@ export default function ExtraSidebar(): JSX.Element { )} {index === PRIORITY_SIDEBAR_ORDER.length - 1 && ( <> - +
{/* BUG ON THIS ICON */} - + Discover More
- +
From a8b53c67d89edd40f4e01e9b83304353f25bd3d7 Mon Sep 17 00:00:00 2001 From: Rodrigo Nader Date: Thu, 7 Mar 2024 02:36:12 -0300 Subject: [PATCH 02/17] Update display names of model components --- src/backend/langflow/components/{prompts => inputs}/Prompt.py | 1 + src/backend/langflow/components/models/AmazonBedrockModel.py | 2 +- src/backend/langflow/components/models/AnthropicModel.py | 2 +- src/backend/langflow/components/models/AzureOpenAIModel.py | 2 +- src/backend/langflow/components/models/BaiduQianfanChatModel.py | 2 +- src/backend/langflow/components/models/CTransformersModel.py | 2 +- src/backend/langflow/components/models/CohereModel.py | 2 +- .../langflow/components/models/GoogleGenerativeAIModel.py | 2 +- src/backend/langflow/components/models/HuggingFaceModel.py | 2 +- src/backend/langflow/components/models/LlamaCppModel.py | 2 +- src/backend/langflow/components/models/OllamaModel.py | 2 +- src/backend/langflow/components/models/OpenAIModel.py | 2 +- src/backend/langflow/components/models/VertexAiModel.py | 2 +- src/backend/langflow/components/prompts/__init_.py | 0 14 files changed, 13 insertions(+), 12 deletions(-) rename src/backend/langflow/components/{prompts => inputs}/Prompt.py (97%) delete mode 100644 src/backend/langflow/components/prompts/__init_.py diff --git a/src/backend/langflow/components/prompts/Prompt.py b/src/backend/langflow/components/inputs/Prompt.py similarity index 97% rename from src/backend/langflow/components/prompts/Prompt.py rename to src/backend/langflow/components/inputs/Prompt.py index a260c1c47..8ee6f0232 100644 --- a/src/backend/langflow/components/prompts/Prompt.py +++ b/src/backend/langflow/components/inputs/Prompt.py @@ -9,6 +9,7 @@ class PromptComponent(CustomComponent): display_name: str = "Prompt" description: str = "A component for creating prompts using templates" beta = True + icon = "terminal-square" def build_config(self): return { diff --git a/src/backend/langflow/components/models/AmazonBedrockModel.py b/src/backend/langflow/components/models/AmazonBedrockModel.py index ce7347fde..00ece9a23 100644 --- a/src/backend/langflow/components/models/AmazonBedrockModel.py +++ b/src/backend/langflow/components/models/AmazonBedrockModel.py @@ -7,7 +7,7 @@ from langflow.field_typing import Text class AmazonBedrockComponent(LCModelComponent): - display_name: str = "Amazon Bedrock Model" + display_name: str = "Amazon Bedrock" description: str = "Generate text using LLM model from Amazon Bedrock." icon = "Amazon" diff --git a/src/backend/langflow/components/models/AnthropicModel.py b/src/backend/langflow/components/models/AnthropicModel.py index fb891f9a3..83a7fd461 100644 --- a/src/backend/langflow/components/models/AnthropicModel.py +++ b/src/backend/langflow/components/models/AnthropicModel.py @@ -8,7 +8,7 @@ from langflow.field_typing import Text class AnthropicLLM(LCModelComponent): - display_name: str = "AnthropicModel" + display_name: str = "Anthropic" description: str = "Generate text using Anthropic Chat&Completion large language models." icon = "Anthropic" diff --git a/src/backend/langflow/components/models/AzureOpenAIModel.py b/src/backend/langflow/components/models/AzureOpenAIModel.py index 81c22d399..5d16cc0c4 100644 --- a/src/backend/langflow/components/models/AzureOpenAIModel.py +++ b/src/backend/langflow/components/models/AzureOpenAIModel.py @@ -9,7 +9,7 @@ from langflow.field_typing import Text class AzureChatOpenAIComponent(LCModelComponent): - display_name: str = "AzureOpenAI Model" + display_name: str = "AzureOpenAI" description: str = "Generate text using LLM model from Azure OpenAI." documentation: str = "https://python.langchain.com/docs/integrations/llms/azure_openai" beta = False diff --git a/src/backend/langflow/components/models/BaiduQianfanChatModel.py b/src/backend/langflow/components/models/BaiduQianfanChatModel.py index 1ef65ee33..76ae69e42 100644 --- a/src/backend/langflow/components/models/BaiduQianfanChatModel.py +++ b/src/backend/langflow/components/models/BaiduQianfanChatModel.py @@ -8,7 +8,7 @@ from langflow.field_typing import Text class QianfanChatEndpointComponent(LCModelComponent): - display_name: str = "QianfanChat Model" + display_name: str = "QianfanChat" description: str = ( "Generate text using Baidu Qianfan chat models. Get more detail from " "https://python.langchain.com/docs/integrations/chat/baidu_qianfan_endpoint." diff --git a/src/backend/langflow/components/models/CTransformersModel.py b/src/backend/langflow/components/models/CTransformersModel.py index 219d74440..160f698db 100644 --- a/src/backend/langflow/components/models/CTransformersModel.py +++ b/src/backend/langflow/components/models/CTransformersModel.py @@ -7,7 +7,7 @@ from langflow.field_typing import Text class CTransformersComponent(LCModelComponent): - display_name = "CTransformersModel" + display_name = "CTransformers" description = "Generate text using CTransformers LLM models" documentation = "https://python.langchain.com/docs/modules/model_io/models/llms/integrations/ctransformers" diff --git a/src/backend/langflow/components/models/CohereModel.py b/src/backend/langflow/components/models/CohereModel.py index c0b603695..1cab1bec7 100644 --- a/src/backend/langflow/components/models/CohereModel.py +++ b/src/backend/langflow/components/models/CohereModel.py @@ -5,7 +5,7 @@ from langflow.field_typing import Text class CohereComponent(LCModelComponent): - display_name = "CohereModel" + display_name = "Cohere" description = "Generate text using Cohere large language models." documentation = "https://python.langchain.com/docs/modules/model_io/models/llms/integrations/cohere" diff --git a/src/backend/langflow/components/models/GoogleGenerativeAIModel.py b/src/backend/langflow/components/models/GoogleGenerativeAIModel.py index c979e6db4..b8c3d3331 100644 --- a/src/backend/langflow/components/models/GoogleGenerativeAIModel.py +++ b/src/backend/langflow/components/models/GoogleGenerativeAIModel.py @@ -8,7 +8,7 @@ from langflow.field_typing import RangeSpec, Text class GoogleGenerativeAIComponent(LCModelComponent): - display_name: str = "Google Generative AIModel" + display_name: str = "Google Generative AI" description: str = "Generate text using Google Generative AI to generate text." icon = "GoogleGenerativeAI" icon = "Google" diff --git a/src/backend/langflow/components/models/HuggingFaceModel.py b/src/backend/langflow/components/models/HuggingFaceModel.py index 33cc57815..c56015741 100644 --- a/src/backend/langflow/components/models/HuggingFaceModel.py +++ b/src/backend/langflow/components/models/HuggingFaceModel.py @@ -8,7 +8,7 @@ from langflow.field_typing import Text class HuggingFaceEndpointsComponent(LCModelComponent): - display_name: str = "Hugging Face Inference API models" + display_name: str = "Hugging Face Inference API" description: str = "Generate text using LLM model from Hugging Face Inference API." icon = "HuggingFace" diff --git a/src/backend/langflow/components/models/LlamaCppModel.py b/src/backend/langflow/components/models/LlamaCppModel.py index 468a1ac8b..48cea7ad0 100644 --- a/src/backend/langflow/components/models/LlamaCppModel.py +++ b/src/backend/langflow/components/models/LlamaCppModel.py @@ -7,7 +7,7 @@ from langflow.field_typing import Text class LlamaCppComponent(LCModelComponent): - display_name = "LlamaCppModel" + display_name = "LlamaCpp" description = "Generate text using llama.cpp model." documentation = "https://python.langchain.com/docs/modules/model_io/models/llms/integrations/llamacpp" diff --git a/src/backend/langflow/components/models/OllamaModel.py b/src/backend/langflow/components/models/OllamaModel.py index 9de1847f0..cfd1e2a2e 100644 --- a/src/backend/langflow/components/models/OllamaModel.py +++ b/src/backend/langflow/components/models/OllamaModel.py @@ -13,7 +13,7 @@ from langflow.field_typing import Text class ChatOllamaComponent(LCModelComponent): - display_name = "ChatOllamaModel" + display_name = "ChatOllama" description = "Generate text using Local LLM for chat with Ollama." icon = "Ollama" diff --git a/src/backend/langflow/components/models/OpenAIModel.py b/src/backend/langflow/components/models/OpenAIModel.py index f595255ce..2a8314fd2 100644 --- a/src/backend/langflow/components/models/OpenAIModel.py +++ b/src/backend/langflow/components/models/OpenAIModel.py @@ -8,7 +8,7 @@ from langflow.field_typing import NestedDict, Text class OpenAIModelComponent(LCModelComponent): - display_name = "OpenAI Model" + display_name = "OpenAI" description = "Generates text using OpenAI's models." icon = "OpenAI" diff --git a/src/backend/langflow/components/models/VertexAiModel.py b/src/backend/langflow/components/models/VertexAiModel.py index c4354d999..11c49c7b4 100644 --- a/src/backend/langflow/components/models/VertexAiModel.py +++ b/src/backend/langflow/components/models/VertexAiModel.py @@ -7,7 +7,7 @@ from langflow.field_typing import Text class ChatVertexAIComponent(LCModelComponent): - display_name = "ChatVertexAIModel" + display_name = "ChatVertexAI" description = "Generate text using Vertex AI Chat large language models API." icon = "VertexAI" diff --git a/src/backend/langflow/components/prompts/__init_.py b/src/backend/langflow/components/prompts/__init_.py deleted file mode 100644 index e69de29bb..000000000 From c863621987b9a2d185c45082bcf7bb77664f40d9 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Thu, 7 Mar 2024 10:08:57 +0100 Subject: [PATCH 03/17] Fixed name and classes --- .../components/extraSidebarComponent/index.tsx | 10 +++++----- src/frontend/src/style/applies.css | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx index 38c9f020d..bd483f78c 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx @@ -267,8 +267,8 @@ export default function ExtraSidebar(): JSX.Element { <> {index === 0 && (
-
- Native +
+ Native Components
)} @@ -283,7 +283,7 @@ export default function ExtraSidebar(): JSX.Element { {/* BUG ON THIS ICON */} @@ -296,8 +296,8 @@ export default function ExtraSidebar(): JSX.Element {
-
- Legacy +
+ Legacy Components
)} diff --git a/src/frontend/src/style/applies.css b/src/frontend/src/style/applies.css index 356544b4f..4bb0dd1c4 100644 --- a/src/frontend/src/style/applies.css +++ b/src/frontend/src/style/applies.css @@ -69,7 +69,7 @@ @apply flex h-full w-[14.5rem] flex-col overflow-hidden border-r scrollbar-hide; } .side-bar-search-div-placement { - @apply relative mx-auto flex items-center py-5; + @apply relative mx-auto flex items-center py-3; } .side-bar-components-icon { @apply h-6 w-4 text-ring; From 07e1d2a45979b5b8b77dd2454a96dd216b89630b Mon Sep 17 00:00:00 2001 From: cristhianzl Date: Thu, 7 Mar 2024 09:07:48 -0300 Subject: [PATCH 04/17] =?UTF-8?q?=F0=9F=90=9B=20fix(genericIconComponent):?= =?UTF-8?q?=20wrap=20ForwardedIconComponent=20with=20memo=20to=20improve?= =?UTF-8?q?=20performance=20by=20preventing=20unnecessary=20re-renders=20?= =?UTF-8?q?=F0=9F=94=A8=20refactor(genericIconComponent):=20improve=20code?= =?UTF-8?q?=20readability=20by=20adding=20line=20breaks=20and=20indentatio?= =?UTF-8?q?n=20for=20better=20code=20organization=20and=20readability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/genericIconComponent/index.tsx | 84 ++++++++++--------- 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/src/frontend/src/components/genericIconComponent/index.tsx b/src/frontend/src/components/genericIconComponent/index.tsx index af3fe061b..9c3ea4402 100644 --- a/src/frontend/src/components/genericIconComponent/index.tsx +++ b/src/frontend/src/components/genericIconComponent/index.tsx @@ -1,51 +1,53 @@ import dynamicIconImports from "lucide-react/dynamicIconImports"; -import { Suspense, forwardRef, lazy } from "react"; +import { Suspense, forwardRef, lazy, memo } from "react"; import { IconComponentProps } from "../../types/components"; import { nodeIconsLucide } from "../../utils/styleUtils"; -const ForwardedIconComponent = forwardRef( - ( - { - name, - className, - iconColor, - stroke, - strokeWidth, - id = "", - }: IconComponentProps, - ref - ) => { - let TargetIcon = nodeIconsLucide[name]; - if (!TargetIcon) { - // check if name exists in dynamicIconImports - if (!dynamicIconImports[name]) { - TargetIcon = nodeIconsLucide["unknown"]; - } else TargetIcon = lazy(dynamicIconImports[name]); - } +const ForwardedIconComponent = memo( + forwardRef( + ( + { + name, + className, + iconColor, + stroke, + strokeWidth, + id = "", + }: IconComponentProps, + ref + ) => { + 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, - ...(stroke && { stroke: stroke }), - ...(iconColor && { color: iconColor, stroke: stroke }), - }; + const style = { + strokeWidth: strokeWidth ?? 1.5, + ...(stroke && { stroke: stroke }), + ...(iconColor && { color: iconColor, stroke: stroke }), + }; - if (!TargetIcon) { - return null; // Render nothing until the icon is loaded + if (!TargetIcon) { + return null; // Render nothing until the icon is loaded + } + const fallback = ( +
+ ); + return ( + + + + ); } - const fallback = ( -
- ); - return ( - - - - ); - } + ) ); export default ForwardedIconComponent; From fdd5ecc87d190ce9070e9df38c084662558edd30 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 7 Mar 2024 09:57:04 -0300 Subject: [PATCH 05/17] Update project components with latest versions --- src/backend/langflow/initial_setup/setup.py | 25 ++++++++++++++++++++- src/backend/langflow/interface/types.py | 14 ++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/backend/langflow/initial_setup/setup.py b/src/backend/langflow/initial_setup/setup.py index f1e3fd9ce..040ae2594 100644 --- a/src/backend/langflow/initial_setup/setup.py +++ b/src/backend/langflow/initial_setup/setup.py @@ -6,8 +6,9 @@ from emoji import demojize, purely_emoji from loguru import logger from sqlmodel import select +from langflow.interface.types import get_all_components from langflow.services.database.models.flow.model import Flow, FlowCreate -from langflow.services.deps import session_scope +from langflow.services.deps import get_settings_service, session_scope STARTER_FOLDER_NAME = "Starter Projects" @@ -17,6 +18,23 @@ STARTER_FOLDER_NAME = "Starter Projects" # can use them as a starting point for their own projects. +def update_projects_components_with_latest_component_versions( + project_data, all_types_dict +): + + # project data has a nodes key, which is a list of nodes + # we want to run through each node and see if it exists in the all_types_dict + # if so, we go into the template key and also get the template from all_types_dict + # and update it all + for node in project_data.get("nodes", []): + node_data = node.get("data").get("node") + if node_data.get("display_name") in all_types_dict: + latest_node = all_types_dict.get(node_data.get("display_name")) + latest_template = latest_node.get("template") + node_data["template"]["code"] = latest_template["code"] + return project_data + + def load_starter_projects(): starter_projects = [] folder = Path(__file__).parent / "starter_projects" @@ -115,6 +133,8 @@ def delete_start_projects(session): def create_or_update_starter_projects(): + components_paths = get_settings_service().settings.COMPONENTS_PATH + all_types_dict = get_all_components(components_paths, as_dict=True) with session_scope() as session: starter_projects = load_starter_projects() delete_start_projects(session) @@ -128,6 +148,9 @@ def create_or_update_starter_projects(): project_icon, project_icon_bg_color, ) = get_project_data(project) + project_data = update_projects_components_with_latest_component_versions( + project_data, all_types_dict + ) if project_name and project_data: for existing_project in get_all_flows_similar_to_project( session, project_name diff --git a/src/backend/langflow/interface/types.py b/src/backend/langflow/interface/types.py index 32e8c2987..4908a60e3 100644 --- a/src/backend/langflow/interface/types.py +++ b/src/backend/langflow/interface/types.py @@ -72,3 +72,17 @@ def get_all_types_dict(components_paths): return merge_nested_dicts_with_renaming( native_components, custom_components_from_file ) + + +def get_all_components(components_paths, as_dict=False): + """Get all components names combining native and custom components.""" + all_types_dict = get_all_types_dict(components_paths) + components = [] if not as_dict else {} + for category in all_types_dict.values(): + for component in category.values(): + component["name"] = component["display_name"] + if as_dict: + components[component["name"]] = component + else: + components.append(component) + return components From 544f4e82653e2348ffe76c31afa83874df066b92 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 7 Mar 2024 09:57:10 -0300 Subject: [PATCH 06/17] Add initial setup tests --- tests/test_initial_setup.py | 84 +++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tests/test_initial_setup.py diff --git a/tests/test_initial_setup.py b/tests/test_initial_setup.py new file mode 100644 index 000000000..9b34f844d --- /dev/null +++ b/tests/test_initial_setup.py @@ -0,0 +1,84 @@ +from itertools import chain + +import pytest +from sqlalchemy import func +from sqlmodel import select + +from langflow.graph.graph.base import Graph +from langflow.graph.schema import ResultData +from langflow.initial_setup.setup import ( + STARTER_FOLDER_NAME, + create_or_update_starter_projects, + get_project_data, + load_starter_projects, +) +from langflow.services.database.models.flow.model import Flow +from langflow.services.deps import session_scope + + +def test_load_starter_projects(): + projects = load_starter_projects() + assert isinstance(projects, list) + assert all(isinstance(project, dict) for project in projects) + + +def test_get_project_data(): + projects = load_starter_projects() + for project in projects: + data = get_project_data(project) + assert all(d is not None for d in data) + + +def test_create_or_update_starter_projects(client): + with session_scope() as session: + # Run the function to create or update projects + create_or_update_starter_projects() + + # Get the number of projects returned by load_starter_projects + num_projects = len(load_starter_projects()) + + # Get the number of projects in the database + num_db_projects = session.exec( + select(func.count(Flow.id)).where(Flow.folder == STARTER_FOLDER_NAME) + ).one() + + # Check that the number of projects in the database is the same as the number of projects returned by load_starter_projects + assert num_db_projects == num_projects + + +@pytest.mark.asyncio +async def test_starter_project_can_run_successfully(client): + with session_scope() as session: + # Run the function to create or update projects + create_or_update_starter_projects() + + # Get the number of projects returned by load_starter_projects + num_projects = len(load_starter_projects()) + + # Get the number of projects in the database + num_db_projects = session.exec( + select(func.count(Flow.id)).where(Flow.folder == STARTER_FOLDER_NAME) + ).one() + + # Check that the number of projects in the database is the same as the number of projects returned by load_starter_projects + assert num_db_projects == num_projects + + # Get all the starter projects + projects = session.exec( + select(Flow).where(Flow.folder == STARTER_FOLDER_NAME) + ).all() + + graphs: list[Graph] = [ + (project.name, Graph.from_payload(project.data, flow_id=project.id)) + for project in projects + ] + assert len(graphs) == len(projects) + for name, graph in graphs: + outputs = await graph.run( + inputs={}, + outputs=[], + session_id="test", + ) + assert all( + isinstance(output, ResultData) for output in chain.from_iterable(outputs) + ), f"Project {name} error: {outputs}" From 6c415d08654ace4c2067cb22c19686363718df15 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 7 Mar 2024 09:57:23 -0300 Subject: [PATCH 07/17] Refactor code to improve performance and readability --- src/backend/langflow/graph/graph/base.py | 108 +++++++++++++++++------ 1 file changed, 82 insertions(+), 26 deletions(-) diff --git a/src/backend/langflow/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py index a7d5a1d42..edb053f4d 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -76,7 +76,9 @@ class Graph: """Returns the state of the graph.""" return self.state_manager.get_state(name, run_id=self._run_id) - def update_state(self, name: str, record: Union[str, Record], caller: Optional[str] = None) -> None: + def update_state( + self, name: str, record: Union[str, Record], caller: Optional[str] = None + ) -> None: """Updates the state of the graph.""" if caller: # If there is a caller which is a vertex_id, I want to activate @@ -108,7 +110,9 @@ class Graph: def reset_activated_vertices(self): self.activated_vertices = [] - def append_state(self, name: str, record: Union[str, Record], caller: Optional[str] = None) -> None: + def append_state( + self, name: str, record: Union[str, Record], caller: Optional[str] = None + ) -> None: """Appends the state of the graph.""" if caller: self.activate_state_vertices(name, caller) @@ -156,7 +160,10 @@ class Graph: """Runs the graph with the given inputs.""" for vertex_id in self._is_input_vertices: vertex = self.get_vertex(vertex_id) - if input_components and (vertex_id not in input_components or vertex.display_name not in input_components): + if input_components and ( + vertex_id not in input_components + or vertex.display_name not in input_components + ): continue if vertex is None: raise ValueError(f"Vertex {vertex_id} not found") @@ -179,9 +186,13 @@ class Graph: if vertex is None: raise ValueError(f"Vertex {vertex_id} not found") - if not vertex.result and not stream and hasattr(vertex, "consume_async_generator"): + if ( + not vertex.result + and not stream + and hasattr(vertex, "consume_async_generator") + ): await vertex.consume_async_generator() - if vertex.display_name in outputs or vertex.id in outputs: + if not outputs or (vertex.display_name in outputs or vertex.id in outputs): vertex_outputs.append(vertex.result) return vertex_outputs @@ -189,8 +200,8 @@ class Graph: self, inputs: Dict[str, Union[str, list[str]]], outputs: list[str], - stream: bool, session_id: str, + stream: Optional[bool] = False, ) -> List[Optional["ResultData"]]: """Runs the graph with the given inputs.""" @@ -257,7 +268,9 @@ class Graph: def build_parent_child_map(self): parent_child_map = defaultdict(list) for vertex in self.vertices: - parent_child_map[vertex.id] = [child.id for child in self.get_successors(vertex)] + parent_child_map[vertex.id] = [ + child.id for child in self.get_successors(vertex) + ] return parent_child_map def increment_run_count(self): @@ -442,7 +455,11 @@ class Graph: """Updates the edges of a vertex.""" # Vertex has edges, so we need to update the edges for edge in vertex.edges: - if edge not in self.edges and edge.source_id in self.vertex_map and edge.target_id in self.vertex_map: + if ( + edge not in self.edges + and edge.source_id in self.vertex_map + and edge.target_id in self.vertex_map + ): self.edges.append(edge) def _build_graph(self) -> None: @@ -467,7 +484,11 @@ class Graph: return self.vertices.remove(vertex) self.vertex_map.pop(vertex_id) - self.edges = [edge for edge in self.edges if edge.source_id != vertex_id and edge.target_id != vertex_id] + self.edges = [ + edge + for edge in self.edges + if edge.source_id != vertex_id and edge.target_id != vertex_id + ] def _build_vertex_params(self) -> None: """Identifies and handles the LLM vertex within the graph.""" @@ -488,7 +509,9 @@ class Graph: return for vertex in self.vertices: if not self._validate_vertex(vertex): - raise ValueError(f"{vertex.display_name} is not connected to any other components") + raise ValueError( + f"{vertex.display_name} is not connected to any other components" + ) def _validate_vertex(self, vertex: Vertex) -> bool: """Validates a vertex.""" @@ -550,7 +573,9 @@ class Graph: name=f"{vertex.display_name} Run {vertex_task_run_count.get(vertex_id, 0)}", ) tasks.append(task) - vertex_task_run_count[vertex_id] = vertex_task_run_count.get(vertex_id, 0) + 1 + vertex_task_run_count[vertex_id] = ( + vertex_task_run_count.get(vertex_id, 0) + 1 + ) logger.debug(f"Running layer {layer_index} with {len(tasks)} tasks") await self._execute_tasks(tasks) logger.debug("Graph processing complete") @@ -592,7 +617,9 @@ class Graph: def dfs(vertex): if state[vertex] == 1: # We have a cycle - raise ValueError("Graph contains a cycle, cannot perform topological sort") + raise ValueError( + "Graph contains a cycle, cannot perform topological sort" + ) if state[vertex] == 0: state[vertex] = 1 for edge in vertex.edges: @@ -616,7 +643,10 @@ class Graph: def get_predecessors(self, vertex): """Returns the predecessors of a vertex.""" - return [self.get_vertex(source_id) for source_id in self.predecessor_map.get(vertex.id, [])] + return [ + self.get_vertex(source_id) + for source_id in self.predecessor_map.get(vertex.id, []) + ] def get_all_successors(self, vertex, recursive=True, flat=True): # Recursively get the successors of the current vertex @@ -657,7 +687,10 @@ class Graph: def get_successors(self, vertex): """Returns the successors of a vertex.""" - return [self.get_vertex(target_id) for target_id in self.successor_map.get(vertex.id, [])] + return [ + self.get_vertex(target_id) + for target_id in self.successor_map.get(vertex.id, []) + ] def get_vertex_neighbors(self, vertex: Vertex) -> Dict[Vertex, int]: """Returns the neighbors of a vertex.""" @@ -703,7 +736,9 @@ class Graph: edges_added.add((source.id, target.id)) return edges - def _get_vertex_class(self, node_type: str, node_base_type: str, node_id: str) -> Type[Vertex]: + def _get_vertex_class( + self, node_type: str, node_base_type: str, node_id: str + ) -> Type[Vertex]: """Returns the node class based on the node type.""" # First we check for the node_base_type node_name = node_id.split("-")[0] @@ -736,14 +771,18 @@ class Graph: vertex_type: str = vertex_data["type"] # type: ignore vertex_base_type: str = vertex_data["node"]["template"]["_type"] # type: ignore - VertexClass = self._get_vertex_class(vertex_type, vertex_base_type, vertex_data["id"]) + VertexClass = self._get_vertex_class( + vertex_type, vertex_base_type, vertex_data["id"] + ) vertex_instance = VertexClass(vertex, graph=self) vertex_instance.set_top_level(self.top_level_vertices) vertices.append(vertex_instance) return vertices - def get_children_by_vertex_type(self, vertex: Vertex, vertex_type: str) -> List[Vertex]: + def get_children_by_vertex_type( + self, vertex: Vertex, vertex_type: str + ) -> List[Vertex]: """Returns the children of a vertex based on the vertex type.""" children = [] vertex_types = [vertex.data["type"]] @@ -755,7 +794,9 @@ class Graph: def __repr__(self): vertex_ids = [vertex.id for vertex in self.vertices] - edges_repr = "\n".join([f"{edge.source_id} --> {edge.target_id}" for edge in self.edges]) + edges_repr = "\n".join( + [f"{edge.source_id} --> {edge.target_id}" for edge in self.edges] + ) return f"Graph:\nNodes: {vertex_ids}\nConnections:\n{edges_repr}" def sort_up_to_vertex(self, vertex_id: str, is_start: bool = False) -> List[Vertex]: @@ -823,7 +864,8 @@ class Graph: vertex.id for vertex in vertices # if filter_graphs then only vertex.is_input will be considered - if self.in_degree_map[vertex.id] == 0 and (not filter_graphs or vertex.is_input) + if self.in_degree_map[vertex.id] == 0 + and (not filter_graphs or vertex.is_input) ) layers: List[List[str]] = [] visited = set(queue) @@ -897,7 +939,9 @@ class Graph: return refined_layers - def sort_chat_inputs_first(self, vertices_layers: List[List[str]]) -> List[List[str]]: + def sort_chat_inputs_first( + self, vertices_layers: List[List[str]] + ) -> List[List[str]]: chat_inputs_first = [] for layer in vertices_layers: for vertex_id in layer: @@ -938,7 +982,9 @@ class Graph: first_layer = vertices_layers[0] # save the only the rest self.vertices_layers = vertices_layers[1:] - self.vertices_to_run = {vertex_id for vertex_id in chain.from_iterable(vertices_layers)} + self.vertices_to_run = { + vertex_id for vertex_id in chain.from_iterable(vertices_layers) + } # Return just the first layer return first_layer @@ -949,11 +995,15 @@ class Graph: self.vertices_to_run.remove(vertex_id) return should_run - def sort_interface_components_first(self, vertices_layers: List[List[str]]) -> List[List[str]]: + def sort_interface_components_first( + self, vertices_layers: List[List[str]] + ) -> List[List[str]]: """Sorts the vertices in the graph so that vertices containing ChatInput or ChatOutput come first.""" def contains_interface_component(vertex): - return any(component.value in vertex for component in InterfaceComponentTypes) + return any( + component.value in vertex for component in InterfaceComponentTypes + ) # Sort each inner list so that vertices containing ChatInput or ChatOutput come first sorted_vertices = [ @@ -965,16 +1015,22 @@ class Graph: ] return sorted_vertices - def sort_by_avg_build_time(self, vertices_layers: List[List[str]]) -> List[List[str]]: + def sort_by_avg_build_time( + self, vertices_layers: List[List[str]] + ) -> List[List[str]]: """Sorts the vertices in the graph so that vertices with the lowest average build time come first.""" def sort_layer_by_avg_build_time(vertices_ids: List[str]) -> List[str]: """Sorts the vertices in the graph so that vertices with the lowest average build time come first.""" if len(vertices_ids) == 1: return vertices_ids - vertices_ids.sort(key=lambda vertex_id: self.get_vertex(vertex_id).avg_build_time) + vertices_ids.sort( + key=lambda vertex_id: self.get_vertex(vertex_id).avg_build_time + ) return vertices_ids - sorted_vertices = [sort_layer_by_avg_build_time(layer) for layer in vertices_layers] + sorted_vertices = [ + sort_layer_by_avg_build_time(layer) for layer in vertices_layers + ] return sorted_vertices From 091d80cd5b154b77a891c0b6ead88b2e1c9bb6d6 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 7 Mar 2024 12:26:43 -0300 Subject: [PATCH 08/17] Add dotdict class for accessing dictionary elements using dot notation --- src/backend/langflow/schema/__init__.py | 3 +- src/backend/langflow/schema/dotdict.py | 71 +++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/backend/langflow/schema/dotdict.py diff --git a/src/backend/langflow/schema/__init__.py b/src/backend/langflow/schema/__init__.py index 8cd0af848..14230578c 100644 --- a/src/backend/langflow/schema/__init__.py +++ b/src/backend/langflow/schema/__init__.py @@ -1,3 +1,4 @@ +from .dotdict import dotdict from .schema import Record -__all__ = ["Record"] +__all__ = ["Record", "dotdict"] diff --git a/src/backend/langflow/schema/dotdict.py b/src/backend/langflow/schema/dotdict.py new file mode 100644 index 000000000..f85c928bb --- /dev/null +++ b/src/backend/langflow/schema/dotdict.py @@ -0,0 +1,71 @@ +class dotdict(dict): + """ + dotdict allows accessing dictionary elements using dot notation (e.g., dict.key instead of dict['key']). + It automatically converts nested dictionaries into dotdict instances, enabling dot notation on them as well. + + Note: + - Only keys that are valid attribute names (e.g., strings that could be variable names) are accessible via dot notation. + - Keys which are not valid Python attribute names or collide with the dict method names (like 'items', 'keys') + should be accessed using the traditional dict['key'] notation. + """ + + def __getattr__(self, attr): + """ + Override dot access to behave like dictionary lookup. Automatically convert nested dicts to dotdicts. + + Args: + attr (str): Attribute to access. + + Returns: + The value associated with 'attr' in the dictionary, converted to dotdict if it is a dict. + + Raises: + AttributeError: If the attribute is not found in the dictionary. + """ + try: + value = self[attr] + if isinstance(value, dict) and not isinstance(value, dotdict): + value = dotdict(value) + self[attr] = value # Update self to nest dotdict for future accesses + return value + except KeyError: + raise AttributeError(f"'dotdict' object has no attribute '{attr}'") + + def __setattr__(self, key, value): + """ + Override attribute setting to work as dictionary item assignment. + + Args: + key (str): The key under which to store the value. + value: The value to store in the dictionary. + """ + if isinstance(value, dict) and not isinstance(value, dotdict): + value = dotdict(value) + self[key] = value + + def __delattr__(self, key): + """ + Override attribute deletion to work as dictionary item deletion. + + Args: + key (str): The key of the item to delete from the dictionary. + + Raises: + AttributeError: If the key is not found in the dictionary. + """ + try: + del self[key] + except KeyError: + raise AttributeError(f"'dotdict' object has no attribute '{key}'") + + def __missing__(self, key): + """ + Handle missing keys by returning an empty dotdict. This allows chaining access without raising KeyError. + + Args: + key: The missing key. + + Returns: + An empty dotdict instance for the given missing key. + """ + return dotdict() From 2930b4ed1aa4a2c2da15ff61434e06278fd5e5b6 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 7 Mar 2024 12:26:57 -0300 Subject: [PATCH 09/17] Update CustomComponent class methods to use dotdict --- .../custom_component/custom_component.py | 49 ++++++++++++++----- 1 file changed, 38 insertions(+), 11 deletions(-) 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 f0c3bfa80..7c2b6b873 100644 --- a/src/backend/langflow/interface/custom/custom_component/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component/custom_component.py @@ -24,6 +24,7 @@ from langflow.interface.custom.code_parser.utils import ( ) from langflow.interface.custom.custom_component.component import Component from langflow.schema import Record +from langflow.schema.dotdict import dotdict from langflow.services.database.models.flow import Flow from langflow.services.database.utils import session_getter from langflow.services.deps import ( @@ -77,13 +78,17 @@ class CustomComponent(Component): def update_state(self, name: str, value: Any): try: - self.vertex.graph.update_state(name=name, record=value, caller=self.vertex.id) + self.vertex.graph.update_state( + name=name, record=value, caller=self.vertex.id + ) except Exception as e: raise ValueError(f"Error updating state: {e}") def append_state(self, name: str, value: Any): try: - self.vertex.graph.append_state(name=name, record=value, caller=self.vertex.id) + self.vertex.graph.append_state( + name=name, record=value, caller=self.vertex.id + ) except Exception as e: raise ValueError(f"Error appending state: {e}") @@ -134,7 +139,9 @@ class CustomComponent(Component): def build_config(self): return self.field_config - def update_build_config(self, build_config: dict, field_name: str, field_value: Any): + def update_build_config( + self, build_config: dotdict, field_name: str, field_value: Any + ): build_config[field_name] = field_value return build_config @@ -142,7 +149,9 @@ class CustomComponent(Component): def tree(self): return self.get_code_tree(self.code or "") - def to_records(self, data: Any, keys: Optional[List[str]] = None, silent_errors: bool = False) -> List[Record]: + def to_records( + self, data: Any, keys: Optional[List[str]] = None, silent_errors: bool = False + ) -> List[Record]: """ Converts input data into a list of Record objects. @@ -191,7 +200,9 @@ class CustomComponent(Component): return records - def create_references_from_records(self, records: List[Record], include_data: bool = False) -> str: + def create_references_from_records( + self, records: List[Record], include_data: bool = False + ) -> str: """ Create references from a list of records. @@ -230,14 +241,20 @@ class CustomComponent(Component): if not self.code: return {} - component_classes = [cls for cls in self.tree["classes"] if self.code_class_base_inheritance in cls["bases"]] + component_classes = [ + cls + for cls in self.tree["classes"] + if self.code_class_base_inheritance in cls["bases"] + ] if not component_classes: return {} # Assume the first Component class is the one we're interested in component_class = component_classes[0] build_methods = [ - method for method in component_class["methods"] if method["name"] == self.function_entrypoint_name + method + for method in component_class["methods"] + if method["name"] == self.function_entrypoint_name ] return build_methods[0] if build_methods else {} @@ -294,7 +311,9 @@ class CustomComponent(Component): # Retrieve and decrypt the credential by name for the current user db_service = get_db_service() with session_getter(db_service) as session: - return credential_service.get_credential(user_id=self._user_id or "", name=name, session=session) + return credential_service.get_credential( + user_id=self._user_id or "", name=name, session=session + ) return get_credential @@ -304,7 +323,9 @@ class CustomComponent(Component): credential_service = get_credential_service() db_service = get_db_service() with session_getter(db_service) as session: - return credential_service.list_credentials(user_id=self._user_id, session=session) + return credential_service.list_credentials( + user_id=self._user_id, session=session + ) def index(self, value: int = 0): """Returns a function that returns the value at the given index in the iterable.""" @@ -343,7 +364,11 @@ class CustomComponent(Component): 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] + 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: @@ -365,7 +390,9 @@ 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).where(Flow.is_component == False) # noqa + select(Flow) + .where(Flow.user_id == self._user_id) + .where(Flow.is_component == False) # noqa ).all() flows_records = [flow.to_record() for flow in flows] From 716b6cf4b7b3571de79d147d7d208ad579883ad3 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 7 Mar 2024 12:27:14 -0300 Subject: [PATCH 10/17] Fix custom component build error and import statement --- .../langflow/interface/custom/utils.py | 26 ++++++++++++------- src/backend/langflow/schema/schema.py | 2 +- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/backend/langflow/interface/custom/utils.py b/src/backend/langflow/interface/custom/utils.py index 266a044f5..68951f3f8 100644 --- a/src/backend/langflow/interface/custom/utils.py +++ b/src/backend/langflow/interface/custom/utils.py @@ -20,6 +20,7 @@ from langflow.interface.custom.directory_reader.utils import ( merge_nested_dicts_with_renaming, ) from langflow.interface.custom.eval import eval_custom_component_code +from langflow.schema import dotdict from langflow.template.field.base import TemplateField from langflow.template.frontend_node.custom_components import ( CustomComponentFrontendNode, @@ -245,7 +246,7 @@ def add_extra_fields(frontend_node, field_config, function_args): def get_field_dict(field: Union[TemplateField, dict]): """Get the field dictionary from a TemplateField or a dict""" if isinstance(field, TemplateField): - return field.model_dump(by_alias=True, exclude_none=True) + return dotdict(field.model_dump(by_alias=True, exclude_none=True)) return field @@ -284,6 +285,7 @@ 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) + build_config[field_name] = field_dict # This has to be done to set refresh if options or value are callable if update_field is not None and field_name != update_field: build_config = update_field_dict( @@ -320,7 +322,11 @@ def run_build_config( return build_config, custom_instance except Exception as exc: + logger.error(f"Error while building field config: {str(exc)}") + if hasattr(exc, "detail") and "traceback" in exc.detail: + logger.error(exc.detail["traceback"]) + raise exc @@ -345,6 +351,7 @@ def build_frontend_node(template_config): def add_code_field(frontend_node: CustomComponentFrontendNode, raw_code, field_config): + code_field = TemplateField( dynamic=True, required=True, @@ -353,7 +360,7 @@ def add_code_field(frontend_node: CustomComponentFrontendNode, raw_code, field_c value=raw_code, password=False, name="code", - advanced=field_config.pop("advanced", False), + advanced=True, field_type="code", is_list=False, ) @@ -404,7 +411,7 @@ def build_custom_component_template( status_code=400, detail={ "error": ( - "Invalid type convertion. Please check your code and try again." + f"Something went wrong while building the custom component. Hints: {str(exc)}" ), "traceback": traceback.format_exc(), }, @@ -415,7 +422,6 @@ def create_component_template(component): """Create a template for a component.""" component_code = component["code"] component_output_types = component["output_types"] - # remove component_extractor = CustomComponent(code=component_code) @@ -431,9 +437,7 @@ def build_custom_components(components_paths: List[str]): if not components_paths: return {} - logger.info( - f"Building custom components from {components_paths}" - ) + logger.info(f"Building custom components from {components_paths}") custom_components_from_file = {} processed_paths = set() for path in components_paths: @@ -467,9 +471,11 @@ def update_field_dict( if "refresh" in field_dict: if call: try: + dd_build_config = dotdict(build_config) custom_component_instance.update_build_config( - build_config, update_field, update_field_value + dd_build_config, update_field, update_field_value ) + build_config = dd_build_config except Exception as exc: logger.error(f"Error while running update_build_config: {str(exc)}") raise UpdateBuildConfigError( @@ -483,8 +489,10 @@ def update_field_dict( return build_config -def sanitize_field_config(field_config: Dict): +def sanitize_field_config(field_config: Union[Dict, TemplateField]): # If any of the already existing keys are in field_config, remove them + if isinstance(field_config, TemplateField): + field_config = field_config.to_dict() for key in [ "name", "field_type", diff --git a/src/backend/langflow/schema/schema.py b/src/backend/langflow/schema/schema.py index 1b3e6b10e..077c20cf5 100644 --- a/src/backend/langflow/schema/schema.py +++ b/src/backend/langflow/schema/schema.py @@ -1,6 +1,6 @@ import copy -from langchain_core.documents import Document # Assumed import +from langchain_core.documents import Document from pydantic import BaseModel From 224f5b436e0c6a573ac7ffe90cfb88c922899b2e Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 7 Mar 2024 12:27:43 -0300 Subject: [PATCH 11/17] Refactor prompt validation and variable handling --- src/backend/langflow/api/v1/base.py | 38 ++++------ src/backend/langflow/api/v1/validate.py | 88 ++++++---------------- src/backend/langflow/base/prompts/utils.py | 86 +++++++++++++++++++++ 3 files changed, 126 insertions(+), 86 deletions(-) diff --git a/src/backend/langflow/api/v1/base.py b/src/backend/langflow/api/v1/base.py index cc16c6d1b..bad43c437 100644 --- a/src/backend/langflow/api/v1/base.py +++ b/src/backend/langflow/api/v1/base.py @@ -1,9 +1,7 @@ from typing import Optional -from langchain.prompts import PromptTemplate from pydantic import BaseModel, field_validator, model_serializer -from langflow.interface.utils import extract_input_variables_from_prompt from langflow.template.frontend_node.base import FrontendNode @@ -80,22 +78,6 @@ INVALID_NAMES = { } -def validate_prompt(template: str): - input_variables = extract_input_variables_from_prompt(template) - - # Check if there are invalid characters in the input_variables - input_variables = check_input_variables(input_variables) - if any(var in INVALID_NAMES for var in input_variables): - raise ValueError(f"Invalid input variables. None of the variables can be named {', '.join(input_variables)}. ") - - try: - PromptTemplate(template=template, input_variables=input_variables) - except Exception as exc: - raise ValueError(f"Invalid prompt: {exc}") from exc - - return input_variables - - def is_json_like(var): if var.startswith("{{") and var.endswith("}}"): # If it is a double brance variable @@ -121,7 +103,9 @@ def fix_variable(var, invalid_chars, wrong_variables): # Handle variables starting with a number if var[0].isdigit(): invalid_chars.append(var[0]) - new_var, invalid_chars, wrong_variables = fix_variable(var[1:], invalid_chars, wrong_variables) + new_var, invalid_chars, wrong_variables = fix_variable( + var[1:], invalid_chars, wrong_variables + ) # Temporarily replace {{ and }} to avoid treating them as invalid new_var = new_var.replace("{{", "ᴛᴇᴍᴘᴏᴘᴇɴ").replace("}}", "ᴛᴇᴍᴘᴄʟᴏsᴇ") @@ -148,7 +132,9 @@ def check_variable(var, invalid_chars, wrong_variables, empty_variables): return wrong_variables, empty_variables -def check_for_errors(input_variables, fixed_variables, wrong_variables, empty_variables): +def check_for_errors( + input_variables, fixed_variables, wrong_variables, empty_variables +): if any(var for var in input_variables if var not in fixed_variables): error_message = ( f"Error: Input variables contain invalid characters or formats. \n" @@ -173,11 +159,17 @@ def check_input_variables(input_variables): if is_json_like(var): continue - new_var, wrong_variables, empty_variables = fix_variable(var, invalid_chars, wrong_variables) - wrong_variables, empty_variables = check_variable(var, INVALID_CHARACTERS, wrong_variables, empty_variables) + new_var, wrong_variables, empty_variables = fix_variable( + var, invalid_chars, wrong_variables + ) + wrong_variables, empty_variables = check_variable( + var, INVALID_CHARACTERS, wrong_variables, empty_variables + ) fixed_variables.append(new_var) variables_to_check.append(var) - check_for_errors(variables_to_check, fixed_variables, wrong_variables, empty_variables) + check_for_errors( + variables_to_check, fixed_variables, wrong_variables, empty_variables + ) return fixed_variables diff --git a/src/backend/langflow/api/v1/validate.py b/src/backend/langflow/api/v1/validate.py index 02c17686b..b7b43c376 100644 --- a/src/backend/langflow/api/v1/validate.py +++ b/src/backend/langflow/api/v1/validate.py @@ -6,9 +6,14 @@ from langflow.api.v1.base import ( CodeValidationResponse, PromptValidationResponse, ValidatePromptRequest, +) +from langflow.base.prompts.utils import ( + add_new_variables_to_template, + get_old_custom_fields, + remove_old_variables_from_template, + update_input_variables_field, validate_prompt, ) -from langflow.template.field.prompt import DefaultPromptField from langflow.utils.validate import validate_code # build router @@ -37,13 +42,28 @@ def post_validate_prompt(prompt_request: ValidatePromptRequest): input_variables=input_variables, frontend_node=None, ) - old_custom_fields = get_old_custom_fields(prompt_request) + old_custom_fields = get_old_custom_fields( + prompt_request.custom_fields, prompt_request.name + ) - add_new_variables_to_template(input_variables, prompt_request) + add_new_variables_to_template( + input_variables, + prompt_request.custom_fields, + prompt_request.frontend_node.template, + prompt_request.name, + ) - remove_old_variables_from_template(old_custom_fields, input_variables, prompt_request) + remove_old_variables_from_template( + old_custom_fields, + input_variables, + prompt_request.custom_fields, + prompt_request.frontend_node.template, + prompt_request.name, + ) - update_input_variables_field(input_variables, prompt_request) + update_input_variables_field( + input_variables, prompt_request.frontend_node.template + ) return PromptValidationResponse( input_variables=input_variables, @@ -52,61 +72,3 @@ def post_validate_prompt(prompt_request: ValidatePromptRequest): except Exception as e: logger.exception(e) raise HTTPException(status_code=500, detail=str(e)) from e - - -def get_old_custom_fields(prompt_request): - try: - if len(prompt_request.frontend_node.custom_fields) == 1 and prompt_request.name == "": - # If there is only one custom field and the name is empty string - # then we are dealing with the first prompt request after the node was created - prompt_request.name = list(prompt_request.frontend_node.custom_fields.keys())[0] - - old_custom_fields = prompt_request.frontend_node.custom_fields[prompt_request.name] - if old_custom_fields is None: - old_custom_fields = [] - - old_custom_fields = old_custom_fields.copy() - except KeyError: - old_custom_fields = [] - prompt_request.frontend_node.custom_fields[prompt_request.name] = [] - return old_custom_fields - - -def add_new_variables_to_template(input_variables, prompt_request): - for variable in input_variables: - try: - template_field = DefaultPromptField(name=variable, display_name=variable) - if variable in prompt_request.frontend_node.template: - # Set the new field with the old value - template_field.value = prompt_request.frontend_node.template[variable]["value"] - - prompt_request.frontend_node.template[variable] = template_field.to_dict() - - # Check if variable is not already in the list before appending - if variable not in prompt_request.frontend_node.custom_fields[prompt_request.name]: - prompt_request.frontend_node.custom_fields[prompt_request.name].append(variable) - - except Exception as exc: - logger.exception(exc) - raise HTTPException(status_code=500, detail=str(exc)) from exc - - -def remove_old_variables_from_template(old_custom_fields, input_variables, prompt_request): - for variable in old_custom_fields: - if variable not in input_variables: - try: - # Remove the variable from custom_fields associated with the given name - if variable in prompt_request.frontend_node.custom_fields[prompt_request.name]: - prompt_request.frontend_node.custom_fields[prompt_request.name].remove(variable) - - # Remove the variable from the template - prompt_request.frontend_node.template.pop(variable, None) - - except Exception as exc: - logger.exception(exc) - raise HTTPException(status_code=500, detail=str(exc)) from exc - - -def update_input_variables_field(input_variables, prompt_request): - if "input_variables" in prompt_request.frontend_node.template: - prompt_request.frontend_node.template["input_variables"]["value"] = input_variables diff --git a/src/backend/langflow/base/prompts/utils.py b/src/backend/langflow/base/prompts/utils.py index 1f41ebda1..c30d2d6a2 100644 --- a/src/backend/langflow/base/prompts/utils.py +++ b/src/backend/langflow/base/prompts/utils.py @@ -1,6 +1,12 @@ +from fastapi import HTTPException +from langchain.prompts import PromptTemplate from langchain_core.documents import Document +from loguru import logger +from langflow.api.v1.base import INVALID_NAMES, check_input_variables +from langflow.interface.utils import extract_input_variables_from_prompt from langflow.schema import Record +from langflow.template.field.prompt import DefaultPromptField def dict_values_to_string(d: dict) -> dict: @@ -53,3 +59,83 @@ def document_to_string(document: Document) -> str: str: The document as a string. """ return document.page_content + + +def validate_prompt(prompt_template: str, silent_errors: bool = False) -> list[str]: + input_variables = extract_input_variables_from_prompt(prompt_template) + + # Check if there are invalid characters in the input_variables + input_variables = check_input_variables(input_variables) + if any(var in INVALID_NAMES for var in input_variables): + raise ValueError( + f"Invalid input variables. None of the variables can be named {', '.join(input_variables)}. " + ) + + try: + PromptTemplate(template=prompt_template, input_variables=input_variables) + except Exception as exc: + logger.error(f"Invalid prompt: {exc}") + if not silent_errors: + raise ValueError(f"Invalid prompt: {exc}") from exc + + return input_variables + + +def get_old_custom_fields(custom_fields, name): + try: + if len(custom_fields) == 1 and name == "": + # If there is only one custom field and the name is empty string + # then we are dealing with the first prompt request after the node was created + name = list(custom_fields.keys())[0] + + old_custom_fields = custom_fields[name] + if not old_custom_fields: + old_custom_fields = [] + + old_custom_fields = old_custom_fields.copy() + except KeyError: + old_custom_fields = [] + custom_fields[name] = [] + return old_custom_fields + + +def add_new_variables_to_template(input_variables, custom_fields, template, name): + for variable in input_variables: + try: + template_field = DefaultPromptField(name=variable, display_name=variable) + if variable in template: + # Set the new field with the old value + template_field.value = template[variable]["value"] + + template[variable] = template_field.to_dict() + + # Check if variable is not already in the list before appending + if variable not in custom_fields[name]: + custom_fields[name].append(variable) + + except Exception as exc: + logger.exception(exc) + raise HTTPException(status_code=500, detail=str(exc)) from exc + + +def remove_old_variables_from_template( + old_custom_fields, input_variables, custom_fields, template, name +): + for variable in old_custom_fields: + if variable not in input_variables: + try: + # Remove the variable from custom_fields associated with the given name + if variable in custom_fields[name]: + custom_fields[name].remove(variable) + + # Remove the variable from the template + template.pop(variable, None) + + except Exception as exc: + logger.exception(exc) + raise HTTPException(status_code=500, detail=str(exc)) from exc + + +def update_input_variables_field(input_variables, template): + if "input_variables" in template: + template["input_variables"]["value"] = input_variables From 24a2e640ff0dd22f9aa5e60a3374aacd938cb765 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 7 Mar 2024 14:36:34 -0300 Subject: [PATCH 12/17] Update refresh functionality in API and UI components --- docs/docs/components/custom.mdx | 3 +- .../langflow/interface/custom/utils.py | 5 +- src/backend/langflow/template/field/base.py | 19 +++- .../components/parameterComponent/index.tsx | 91 +++++++++++++++---- .../src/components/ui/refreshButton.tsx | 3 + src/frontend/src/types/api/index.ts | 4 +- 6 files changed, 99 insertions(+), 26 deletions(-) diff --git a/docs/docs/components/custom.mdx b/docs/docs/components/custom.mdx index 9fb0fe689..43eb336fc 100644 --- a/docs/docs/components/custom.mdx +++ b/docs/docs/components/custom.mdx @@ -83,7 +83,8 @@ The CustomComponent class serves as the foundation for creating custom component | _`file_types: List[str]`_ | This is a requirement if the _`field_type`_ is _file_. Defines which file types will be accepted. For example, _json_, _yaml_ or _yml_. | | _`range_spec: langflow.field_typing.RangeSpec`_ | This is a requirement if the _`field_type`_ is _`float`_. Defines the range of values accepted and the step size. If none is defined, the default is _`[-1, 1, 0.1]`_. | | _`title_case: bool`_ | Formats the name of the field when _`display_name`_ is not defined. Set it to False to keep the name as you set it in the _`build`_ method. | - | _`refresh: bool`_ | If set to True a button will appear to the right of the field, and when clicked, it will call the _`update_build_config`_ method which takes in the _`build_config`_, the name of the field (_`field_name`_) and the latest value of the field (_`field_value`_). This is useful when you want to update the _`build_config`_ based on the value of the field. | + | _`refresh_button: bool`_ | If set to True a button will appear to the right of the field, and when clicked, it will call the _`update_build_config`_ method which takes in the _`build_config`_, the name of the field (_`field_name`_) and the latest value of the field (_`field_value`_). This is useful when you want to update the _`build_config`_ based on the value of the field. | + | _`real_time_refresh: bool`_ | If set to True, the _`update_build_config`_ method will be called every time the field value changes. | diff --git a/src/backend/langflow/interface/custom/utils.py b/src/backend/langflow/interface/custom/utils.py index 68951f3f8..bfc8c18fe 100644 --- a/src/backend/langflow/interface/custom/utils.py +++ b/src/backend/langflow/interface/custom/utils.py @@ -468,7 +468,9 @@ def update_field_dict( call: bool = False, ): """Update the field dictionary by calling options() or value() if they are callable""" - if "refresh" in field_dict: + if ("real_time_refresh" in field_dict or "refresh_button" in field_dict) and any( + (field_dict["real_time_refresh"], field_dict["refresh_button"]) + ): if call: try: dd_build_config = dotdict(build_config) @@ -481,7 +483,6 @@ def update_field_dict( raise UpdateBuildConfigError( f"Error while running update_build_config: {str(exc)}" ) from exc - field_dict["refresh"] = True # Let's check if "range_spec" is a RangeSpec object if "rangeSpec" in field_dict and isinstance(field_dict["rangeSpec"], RangeSpec): diff --git a/src/backend/langflow/template/field/base.py b/src/backend/langflow/template/field/base.py index bf6d461f7..76e5a13d0 100644 --- a/src/backend/langflow/template/field/base.py +++ b/src/backend/langflow/template/field/base.py @@ -65,10 +65,17 @@ class TemplateField(BaseModel): info: Optional[str] = "" """Additional information about the field to be shown in the tooltip. Defaults to an empty string.""" - refresh: Optional[bool] = None - """Specifies if the field should be refreshed. Defaults to False.""" + real_time_refresh: Optional[bool] = None + """Specifies if the field should have real time refresh. `refresh_button` must be False. Defaults to None.""" - range_spec: Optional[RangeSpec] = Field(default=None, serialization_alias="rangeSpec") + refresh_button: Optional[bool] = None + """Specifies if the field should have a refresh button. Defaults to False.""" + refresh_button_text: Optional[str] = None + """Specifies the text for the refresh button. Defaults to None.""" + + range_spec: Optional[RangeSpec] = Field( + default=None, serialization_alias="rangeSpec" + ) """Range specification for the field. Defaults to None.""" title_case: bool = False @@ -117,6 +124,10 @@ class TemplateField(BaseModel): if not isinstance(value, list): raise ValueError("file_types must be a list") return [ - (f".{file_type}" if isinstance(file_type, str) and not file_type.startswith(".") else file_type) + ( + f".{file_type}" + if isinstance(file_type, str) and not file_type.startswith(".") + else file_type + ) for file_type in value ] diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 075ded804..b0fd6804d 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -88,10 +88,36 @@ export default function ParameterComponent({ const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot); + const handleRefreshButtonPress = async (name, data) => { + setIsLoading(true); + try { + let newTemplate = await handleUpdateValues(name, data); + if (newTemplate) { + setNode(data.id, (oldNode) => { + let newNode = cloneDeep(oldNode); + newNode.data = { + ...newNode.data, + }; + newNode.data.node.template = newTemplate; + return newNode; + }); + } + } catch (error) { + let responseError = error as ResponseErrorTypeAPI; + setErrorData({ + title: "Error while updating the Component", + list: [responseError.response.data.detail.error ?? "Unknown error"], + }); + } + setIsLoading(false); + renderTooltips(); + }; + useEffect(() => { async function fetchData() { if ( - data.node?.template[name]?.refresh && + (data.node?.template[name]?.real_time_refresh || + data.node?.template[name]?.refresh_button) && // options can be undefined but not an empty array (data.node?.template[name]?.options?.length ?? 0) === 0 ) { @@ -128,7 +154,8 @@ export default function ParameterComponent({ takeSnapshot(); } const shouldUpdate = - data.node?.template[name].refresh && + data.node?.template[name].real_time_refresh && + !data.node?.template[name].refresh_button && data.node!.template[name].value !== newValue; data.node!.template[name].value = newValue; // necessary to enable ctrl+z inside the input @@ -154,7 +181,7 @@ export default function ParameterComponent({ ...newNode.data, }; - if (data.node?.template[name].refresh && newTemplate) { + if (data.node?.template[name].real_time_refresh && newTemplate) { newNode.data.node.template = newTemplate; } else newNode.data.node.template[name].value = newValue; @@ -458,7 +485,7 @@ export default function ParameterComponent({ } onChange={handleOnNewValue} /> - {/* {data.node?.template[name].refresh && ( + {/* {data.node?.template[name].refresh_button && (
)} */}
) : data.node?.template[name].multiline ? ( - +
+
+ +
+ {data.node?.template[name].refresh_button && ( +
+ +
+ )} +
) : (
- {data.node?.template[name].refresh && ( + {data.node?.template[name].refresh_button && (
@@ -535,7 +587,7 @@ export default function ParameterComponent({ ) : left === true && type === "str" && (data.node?.template[name].options || - data.node?.template[name]?.refresh) ? ( + data.node?.template[name]?.real_time_refresh) ? ( // TODO: Improve CSS
@@ -548,15 +600,18 @@ export default function ParameterComponent({ id={"dropdown-" + name} />
- {data.node?.template[name].refresh && ( + {data.node?.template[name].refresh_button && (
diff --git a/src/frontend/src/components/ui/refreshButton.tsx b/src/frontend/src/components/ui/refreshButton.tsx index bf41ba114..791b0ca05 100644 --- a/src/frontend/src/components/ui/refreshButton.tsx +++ b/src/frontend/src/components/ui/refreshButton.tsx @@ -7,6 +7,7 @@ function RefreshButton({ isLoading, disabled, name, + button_text, data, handleUpdateValues, className, @@ -15,6 +16,7 @@ function RefreshButton({ isLoading: boolean; disabled: boolean; name: string; + button_text: string; data: NodeDataType; className?: string; handleUpdateValues: (name: string, data: NodeDataType) => void; @@ -43,6 +45,7 @@ function RefreshButton({ onClick={handleClick} id={id} > + {button_text} ; display_name?: string; name?: string; - refresh?: boolean; + real_time_refresh?: boolean; + refresh_button?: boolean; + refresh_button_text?: string; [key: string]: any; }; export type sendAllProps = { From f8636f13991213946c695ae0a78ed0e643fcf2ce Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Thu, 7 Mar 2024 15:36:31 -0300 Subject: [PATCH 13/17] Add EXAMPLES_MOCK constant to constants.ts --- src/frontend/src/constants/constants.ts | 72 +++++++++++++++++++++++ src/frontend/src/pages/MainPage/index.tsx | 3 +- 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/src/frontend/src/constants/constants.ts b/src/frontend/src/constants/constants.ts index fc89b9890..4927e9d81 100644 --- a/src/frontend/src/constants/constants.ts +++ b/src/frontend/src/constants/constants.ts @@ -1,6 +1,7 @@ // src/constants/constants.ts import { languageMap } from "../types/components"; +import { FlowType } from "../types/flow"; /** * invalid characters for flow name @@ -739,4 +740,75 @@ export const PRIORITY_SIDEBAR_ORDER = [ "helpers", "experimental", ]; +/* +Data ingestion +Basic Prompting +Chat com memória +Working with data (file/website) +API requests +Vector Store +Assistant +*/ +export const EXAMPLES_MOCK:FlowType[] = [ + { + name: "Working with data", + id: "Working with data Description", + data: { + nodes: [], + edges: [], + viewport: { zoom: 1, x: 1, y: 1 } + }, + description: "This flow represents the first process in our application.", + folder: STARTER_FOLDER_NAME, + user_id: undefined, + }, + { + name: "Basic Prompting", + id: "Basic Prompting Description", + data: { + nodes: [], + edges: [], + viewport: { zoom: 1, x: 1, y: 1 } + }, + description: "This flow represents the first process in our application.", + folder: STARTER_FOLDER_NAME, + user_id: undefined, + }, + { + name: "Chat with memory", + id: "Chat with memory Description", + data: { + nodes: [], + edges: [], + viewport: { zoom: 1, x: 1, y: 1 } + }, + description: "This flow represents the first process in our application.", + folder: STARTER_FOLDER_NAME, + user_id: undefined, + }, + { + name: "API requests", + id: "API requests Description", + data: { + nodes: [], + edges: [], + viewport: { zoom: 1, x: 1, y: 1 } + }, + description: "This flow represents the first process in our application.", + folder: STARTER_FOLDER_NAME, + user_id: undefined, + }, + { + name: "Assistant", + id: "Assistant Description", + data: { + nodes: [], + edges: [], + viewport: { zoom: 1, x: 1, y: 1 } + }, + description: "This flow represents the first process in our application.", + folder: STARTER_FOLDER_NAME, + user_id: undefined, + }, +]; diff --git a/src/frontend/src/pages/MainPage/index.tsx b/src/frontend/src/pages/MainPage/index.tsx index e2dfe124b..91ed4066d 100644 --- a/src/frontend/src/pages/MainPage/index.tsx +++ b/src/frontend/src/pages/MainPage/index.tsx @@ -10,6 +10,7 @@ import SidebarNav from "../../components/sidebarComponent"; import { Button } from "../../components/ui/button"; import { CONSOLE_ERROR_MSG } from "../../constants/alerts_constants"; import { + EXAMPLES_MOCK, MY_COLLECTION_DESC, USER_PROJECTS_HEADER, } from "../../constants/constants"; @@ -133,7 +134,7 @@ export default function HomePage(): JSX.Element {
- {examples.map((example, idx) => { + {EXAMPLES_MOCK.map((example, idx) => { return ; })} From a01a22ce3e71dd41727580a9b44b2d7bb1c7cf88 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 7 Mar 2024 15:59:13 -0300 Subject: [PATCH 14/17] Refactor APIRequest and TextToRecord components --- .../langflow/components/data/APIRequest.py | 44 +++++++++++------ .../components/experimental/TextToRecord.py | 2 +- .../components/helpers/IDGenerator.py | 6 ++- .../components/helpers/RecordComponent1.py | 47 ------------------- .../custom_component/custom_component.py | 5 +- 5 files changed, 39 insertions(+), 65 deletions(-) delete mode 100644 src/backend/langflow/components/helpers/RecordComponent1.py diff --git a/src/backend/langflow/components/data/APIRequest.py b/src/backend/langflow/components/data/APIRequest.py index abdede217..1b8655369 100644 --- a/src/backend/langflow/components/data/APIRequest.py +++ b/src/backend/langflow/components/data/APIRequest.py @@ -1,12 +1,11 @@ import asyncio +import json from typing import List, Optional import httpx -import json from langflow import CustomComponent from langflow.schema import Record -from langflow.services.database.models.base import orjson_dumps class APIRequest(CustomComponent): @@ -52,35 +51,44 @@ class APIRequest(CustomComponent): timeout: int = 5, ) -> Record: method = method.upper() - if method not in ["GET", "POST", "PATCH", "PUT"]: + if method not in ["GET", "POST", "PATCH", "PUT", "DELETE"]: raise ValueError(f"Unsupported method: {method}") data = body if body else None data = json.dumps(data) try: - response = await client.request(method, url, headers=headers, content=data, timeout=timeout) + 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) + result = response.json() except Exception: result = response.text return Record( - text=result, data={ "source": url, "headers": headers, "status_code": response.status_code, + "result": result, }, ) except httpx.TimeoutException: return Record( - text="Request Timed Out", - data={"source": url, "headers": headers, "status_code": 408}, + data={ + "source": url, + "headers": headers, + "status_code": 408, + "error": "Request timed out", + }, ) except Exception as exc: return Record( - text=str(exc), - data={"source": url, "headers": headers, "status_code": 500}, + data={ + "source": url, + "headers": headers, + "status_code": 500, + "error": str(exc), + }, ) async def build( @@ -88,15 +96,23 @@ class APIRequest(CustomComponent): method: str, url: List[str], headers: Optional[dict] = None, - body: Optional[dict] = None, + body: Optional[List[Record]] = None, timeout: int = 5, ) -> List[Record]: if headers is None: headers = {} urls = url if isinstance(url, list) else [url] - bodies = body if isinstance(body, list) else [body] if body else [None] * len(urls) + bodies = [] + if body: + if isinstance(body, list): + bodies = [b.data for b in body] + else: + bodies = [body.data] async with httpx.AsyncClient() as client: results = await asyncio.gather( - *[self.make_request(client, method, u, headers, rec, timeout) for u, rec in zip(urls, bodies)] + *[ + self.make_request(client, method, u, headers, rec, timeout) + for u, rec in zip(urls, bodies) + ] ) return results diff --git a/src/backend/langflow/components/experimental/TextToRecord.py b/src/backend/langflow/components/experimental/TextToRecord.py index 9b601dc42..fcb91fdee 100644 --- a/src/backend/langflow/components/experimental/TextToRecord.py +++ b/src/backend/langflow/components/experimental/TextToRecord.py @@ -80,7 +80,7 @@ class TextToRecordComponent(CustomComponent): "display_name": "Mode", "options": ["Text", "Number"], "info": "The mode to use for creating the record.", - "refresh": True, + "real_time_refresh": True, "input_types": [], }, } diff --git a/src/backend/langflow/components/helpers/IDGenerator.py b/src/backend/langflow/components/helpers/IDGenerator.py index 35b9a3d42..73c56eb7b 100644 --- a/src/backend/langflow/components/helpers/IDGenerator.py +++ b/src/backend/langflow/components/helpers/IDGenerator.py @@ -9,7 +9,9 @@ class UUIDGeneratorComponent(CustomComponent): display_name = "Unique ID Generator" description = "Generates a unique ID." - def update_build_config(self, build_config: dict, field_name: Text, field_value: Any): + def update_build_config( + self, build_config: dict, field_name: Text, field_value: Any + ): if field_name == "unique_id": build_config[field_name]["value"] = str(uuid.uuid4()) return build_config @@ -18,7 +20,7 @@ class UUIDGeneratorComponent(CustomComponent): return { "unique_id": { "display_name": "Value", - "refresh": True, + "real_time_refresh": True, } } diff --git a/src/backend/langflow/components/helpers/RecordComponent1.py b/src/backend/langflow/components/helpers/RecordComponent1.py deleted file mode 100644 index dce557826..000000000 --- a/src/backend/langflow/components/helpers/RecordComponent1.py +++ /dev/null @@ -1,47 +0,0 @@ -from typing import Any - -from langflow import CustomComponent -from langflow.schema import Record -from langflow.template.field.base import TemplateField - - -class RecordComponent(CustomComponent): - display_name = "Record Numbers" - description = "A component to create a record from key-value pairs." - field_order = ["n_keys"] - - def update_build_config(self, build_config: dict, field_name: str, field_value: Any): - if field_value is None: - return - elif int(field_value) == 0: - keep = ["n_keys", "code"] - for key in build_config.copy(): - if key in keep: - continue - del build_config[key] - build_config[field_name]["value"] = int(field_value) - - # Add new fields depending on the field value - for i in range(int(field_value)): - field = TemplateField( - name=f"Key and Value {i}", - field_type="dict", - display_name="", - info="The key for the record.", - input_types=["Text"], - ) - build_config[field.name] = field.to_dict() - - def build_config(self): - return { - "n_keys": { - "display_name": "Number of Fields", - "refresh": True, - "info": "The number of keys to create in the record.", - }, - } - - def build(self, n_keys: int, **kwargs) -> Record: - data = {k: v for d in kwargs.values() for k, v in d.items()} - record = Record(data=data) - return record 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 7c2b6b873..56febbf48 100644 --- a/src/backend/langflow/interface/custom/custom_component/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component/custom_component.py @@ -140,7 +140,10 @@ class CustomComponent(Component): return self.field_config def update_build_config( - self, build_config: dotdict, field_name: str, field_value: Any + self, + build_config: dotdict, + field_name: str, + field_value: Any, ): build_config[field_name] = field_value return build_config From 17d1841e28d09f67c1ae231dcb66f6ea007fadb2 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 7 Mar 2024 16:24:08 -0300 Subject: [PATCH 15/17] Fix code formatting and add MissingDefault class --- .../custom/code_parser/code_parser.py | 46 ++++++++++++++----- .../langflow/interface/custom/schema.py | 9 ++++ .../langflow/interface/custom/utils.py | 16 +++++-- 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/src/backend/langflow/interface/custom/code_parser/code_parser.py b/src/backend/langflow/interface/custom/code_parser/code_parser.py index eaff42fab..1b4bfaf39 100644 --- a/src/backend/langflow/interface/custom/code_parser/code_parser.py +++ b/src/backend/langflow/interface/custom/code_parser/code_parser.py @@ -9,7 +9,11 @@ from fastapi import HTTPException from loguru import logger from langflow.interface.custom.eval import eval_custom_component_code -from langflow.interface.custom.schema import CallableCodeDetails, ClassCodeDetails +from langflow.interface.custom.schema import ( + CallableCodeDetails, + ClassCodeDetails, + MissingDefault, +) class CodeSyntaxError(HTTPException): @@ -95,7 +99,9 @@ class CodeParser: elif isinstance(node, ast.ImportFrom): for alias in node.names: if alias.asname: - self.data["imports"].append((node.module, f"{alias.name} as {alias.asname}")) + self.data["imports"].append( + (node.module, f"{alias.name} as {alias.asname}") + ) else: self.data["imports"].append((node.module, alias.name)) @@ -144,7 +150,9 @@ class CodeParser: return_type = None if node.returns: return_type_str = ast.unparse(node.returns) - eval_env = self.construct_eval_env(return_type_str, tuple(self.data["imports"])) + eval_env = self.construct_eval_env( + return_type_str, tuple(self.data["imports"]) + ) try: return_type = eval(return_type_str, eval_env) @@ -185,15 +193,23 @@ class CodeParser: num_args = len(node.args.args) num_defaults = len(node.args.defaults) num_missing_defaults = num_args - num_defaults - missing_defaults = [None] * num_missing_defaults - default_values = [ast.unparse(default).strip("'") if default else None for default in node.args.defaults] + missing_defaults = [MissingDefault()] * num_missing_defaults + default_values = [ + ast.unparse(default).strip("'") if default else None + for default in node.args.defaults + ] # Now check all default values to see if there # are any "None" values in the middle - default_values = [None if value == "None" else value for value in default_values] + default_values = [ + None if value == "None" else value for value in default_values + ] defaults = missing_defaults + default_values - args = [self.parse_arg(arg, default) for arg, default in zip(node.args.args, defaults)] + args = [ + self.parse_arg(arg, default) + for arg, default in zip(node.args.args, defaults) + ] return args def parse_varargs(self, node: ast.FunctionDef) -> List[Dict[str, Any]]: @@ -211,11 +227,17 @@ class CodeParser: """ Parses the keyword-only arguments of a function or method node. """ - kw_defaults = [None] * (len(node.args.kwonlyargs) - len(node.args.kw_defaults)) + [ - ast.unparse(default) if default else None for default in node.args.kw_defaults + kw_defaults = [None] * ( + len(node.args.kwonlyargs) - len(node.args.kw_defaults) + ) + [ + ast.unparse(default) if default else None + for default in node.args.kw_defaults ] - args = [self.parse_arg(arg, default) for arg, default in zip(node.args.kwonlyargs, kw_defaults)] + args = [ + self.parse_arg(arg, default) + for arg, default in zip(node.args.kwonlyargs, kw_defaults) + ] return args def parse_kwargs(self, node: ast.FunctionDef) -> List[Dict[str, Any]]: @@ -319,7 +341,9 @@ class CodeParser: Extracts global variables from the code. """ global_var = { - "targets": [t.id if hasattr(t, "id") else ast.dump(t) for t in node.targets], + "targets": [ + t.id if hasattr(t, "id") else ast.dump(t) for t in node.targets + ], "value": ast.unparse(node.value), } self.data["global_vars"].append(global_var) diff --git a/src/backend/langflow/interface/custom/schema.py b/src/backend/langflow/interface/custom/schema.py index 7c5975150..1636882ef 100644 --- a/src/backend/langflow/interface/custom/schema.py +++ b/src/backend/langflow/interface/custom/schema.py @@ -27,3 +27,12 @@ class CallableCodeDetails(BaseModel): body: list return_type: Optional[Any] = None has_return: bool = False + + +class MissingDefault: + """ + A class to represent a missing default value. + """ + + def __repr__(self): + return "MISSING" diff --git a/src/backend/langflow/interface/custom/utils.py b/src/backend/langflow/interface/custom/utils.py index bfc8c18fe..3a4df9f56 100644 --- a/src/backend/langflow/interface/custom/utils.py +++ b/src/backend/langflow/interface/custom/utils.py @@ -20,6 +20,7 @@ from langflow.interface.custom.directory_reader.utils import ( merge_nested_dicts_with_renaming, ) from langflow.interface.custom.eval import eval_custom_component_code +from langflow.interface.custom.schema import MissingDefault from langflow.schema import dotdict from langflow.template.field.base import TemplateField from langflow.template.frontend_node.custom_components import ( @@ -111,7 +112,7 @@ def extract_type_from_optional(field_type): str: The extracted type, or an empty string if no type was found. """ match = re.search(r"\[(.*?)\]$", field_type) - return match[1] if match else None + return match[1] if match else field_type def get_field_properties(extra_field): @@ -119,7 +120,13 @@ def get_field_properties(extra_field): field_name = extra_field["name"] field_type = extra_field.get("type", "str") field_value = extra_field.get("default", "") - field_required = "optional" not in field_type.lower() + # a required field is a field that does not contain + # optional in field_type + # and a field that does not have a default value + field_required = "optional" not in field_type.lower() and isinstance( + field_value, MissingDefault + ) + field_value = field_value if not isinstance(field_value, MissingDefault) else None if not field_required: field_type = extract_type_from_optional(field_type) @@ -469,7 +476,10 @@ def update_field_dict( ): """Update the field dictionary by calling options() or value() if they are callable""" if ("real_time_refresh" in field_dict or "refresh_button" in field_dict) and any( - (field_dict["real_time_refresh"], field_dict["refresh_button"]) + ( + field_dict.get("real_time_refresh", False), + field_dict.get("refresh_button", False), + ) ): if call: try: From 0eacc33f82c75da29cd317e2321233a33b903514 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 7 Mar 2024 16:55:21 -0300 Subject: [PATCH 16/17] Update input handling in Vertex class --- src/backend/langflow/graph/vertex/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/backend/langflow/graph/vertex/base.py b/src/backend/langflow/graph/vertex/base.py index 711b01bad..08c8d8f99 100644 --- a/src/backend/langflow/graph/vertex/base.py +++ b/src/backend/langflow/graph/vertex/base.py @@ -18,6 +18,7 @@ from loguru import logger from langflow.graph.schema import ( INPUT_COMPONENTS, + INPUT_FIELD_NAME, OUTPUT_COMPONENTS, InterfaceComponentTypes, ResultData, @@ -709,7 +710,8 @@ class Vertex: self._reset() if self._is_chat_input() and inputs is not None: - self.update_raw_params(inputs) + inputs = {"input_value": inputs.get(INPUT_FIELD_NAME, "")} + self.update_raw_params(inputs, overwrite=True) # Run steps for step in self.steps: From 34d578b813b7ef9822f125e7d1163aa96227c23a Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 7 Mar 2024 16:55:29 -0300 Subject: [PATCH 17/17] Refactor build_vertex function and improve code readability --- src/backend/langflow/api/v1/chat.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/backend/langflow/api/v1/chat.py b/src/backend/langflow/api/v1/chat.py index ecec8f200..208007394 100644 --- a/src/backend/langflow/api/v1/chat.py +++ b/src/backend/langflow/api/v1/chat.py @@ -93,7 +93,7 @@ async def build_vertex( current_user=Depends(get_current_active_user), ): """Build a vertex instead of the entire graph.""" - {"inputs": {"input_value": "some value"}} + start_time = time.perf_counter() next_vertices_ids = [] try: @@ -101,8 +101,12 @@ async def build_vertex( cache = chat_service.get_cache(flow_id) if not cache: # If there's no cache - logger.warning(f"No cache found for {flow_id}. Building graph starting at {vertex_id}") - graph = build_and_cache_graph(flow_id=flow_id, session=next(get_session()), chat_service=chat_service) + logger.warning( + f"No cache found for {flow_id}. Building graph starting at {vertex_id}" + ) + graph = build_and_cache_graph( + flow_id=flow_id, session=next(get_session()), chat_service=chat_service + ) else: graph = cache.get("result") result_data_response = ResultDataResponse(results={}) @@ -122,7 +126,9 @@ async def build_vertex( else: raise ValueError(f"No result found for vertex {vertex_id}") next_vertices_ids = vertex.successors_ids - next_vertices_ids = [v for v in next_vertices_ids if graph.should_run_vertex(v)] + next_vertices_ids = [ + v for v in next_vertices_ids if graph.should_run_vertex(v) + ] result_data_response = ResultDataResponse(**result_dict.model_dump()) @@ -205,7 +211,9 @@ async def build_vertex_stream( else: graph = cache.get("result") else: - session_data = await session_service.load_session(session_id, flow_id=flow_id) + session_data = await session_service.load_session( + session_id, flow_id=flow_id + ) graph, artifacts = session_data if session_data else (None, None) if not graph: raise ValueError(f"No graph found for {flow_id}.")