From 09320824a9cdf29bd474ee757a1bc54d534f766d Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Mon, 24 Apr 2023 10:32:15 -0300 Subject: [PATCH 1/4] refactor(validate.py): extract build_graph function to langflow.interface.run module feat(validate.py): add post_validate_node endpoint to validate a single node in the graph by its id --- src/backend/langflow/api/validate.py | 18 ++++++++++++++++++ src/backend/langflow/interface/run.py | 14 +++++++------- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/backend/langflow/api/validate.py b/src/backend/langflow/api/validate.py index a60bcc506..07f113a8b 100644 --- a/src/backend/langflow/api/validate.py +++ b/src/backend/langflow/api/validate.py @@ -7,6 +7,7 @@ from langflow.api.base import ( PromptValidationResponse, validate_prompt, ) +from langflow.interface.run import build_graph from langflow.utils.logger import logger from langflow.utils.validate import validate_code @@ -33,3 +34,20 @@ def post_validate_prompt(prompt: Prompt): except Exception as e: logger.exception(e) raise HTTPException(status_code=500, detail=str(e)) from e + + +# validate node +@router.post("/node/{node_id}", status_code=200) +def post_validate_node(node_id: str, data: dict): + try: + # build graph + graph = build_graph(data) + # validate node + node = graph.get_node(node_id) + if node is not None: + _ = node.build() + return node.params + raise + except Exception as e: + logger.exception(e) + raise HTTPException(status_code=500, detail=str(e)) from e diff --git a/src/backend/langflow/interface/run.py b/src/backend/langflow/interface/run.py index deba28586..9fb9b82fe 100644 --- a/src/backend/langflow/interface/run.py +++ b/src/backend/langflow/interface/run.py @@ -39,16 +39,16 @@ def build_langchain_object_with_caching(data_graph): """ logger.debug("Building langchain object") - nodes = data_graph["nodes"] - # Add input variables - # nodes = payload.extract_input_variables(nodes) - # Nodes, edges and root node - edges = data_graph["edges"] - graph = Graph(nodes, edges) - + graph = build_graph(data_graph) return graph.build() +def build_graph(data_graph): + nodes = data_graph["nodes"] + edges = data_graph["edges"] + return Graph(nodes, edges) + + def build_langchain_object(data_graph): """ Build langchain object from data_graph. From f122151b12f388d7fa64f222fc1cbfee1c13f4b5 Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Mon, 24 Apr 2023 13:43:52 -0300 Subject: [PATCH 2/4] fix(validate.py): raise an exception with a message when node is not found feat(GenericNode): add node validation with outline color feedback feat(tailwind.config.js): add styles for outline colors and animations --- src/backend/langflow/api/validate.py | 2 +- .../src/CustomNodes/GenericNode/index.tsx | 56 +++++++++++++- src/frontend/tailwind.config.js | 75 ++++++++++++------- 3 files changed, 100 insertions(+), 33 deletions(-) diff --git a/src/backend/langflow/api/validate.py b/src/backend/langflow/api/validate.py index 07f113a8b..d80010be4 100644 --- a/src/backend/langflow/api/validate.py +++ b/src/backend/langflow/api/validate.py @@ -47,7 +47,7 @@ def post_validate_node(node_id: str, data: dict): if node is not None: _ = node.build() return node.params - raise + raise Exception(f"Node {node_id} not found") except Exception as e: logger.exception(e) raise HTTPException(status_code=500, detail=str(e)) from e diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index ff13af901..c96591cc9 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -7,9 +7,10 @@ import { } from "../../utils"; import ParameterComponent from "./components/parameterComponent"; import { typesContext } from "../../contexts/typesContext"; -import { useContext, useRef } from "react"; +import { useContext, useState, useEffect, useRef } from "react"; import { NodeDataType } from "../../types/flow"; import { alertContext } from "../../contexts/alertContext"; +import { useCallback } from 'react'; export default function GenericNode({ data, @@ -22,6 +23,53 @@ export default function GenericNode({ const showError = useRef(true); const { types, deleteNode } = useContext(typesContext); const Icon = nodeIcons[types[data.type]]; + + // State for outline color + const [isGreenOutline, setIsGreenOutline] = useState(false); + const [isRedOutline, setIsRedOutline] = useState(false); + const { reactFlowInstance } = useContext(typesContext); + + const validateNode = useCallback(async () => { + try { + const response = await fetch(`/validate/node/${data.id}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(reactFlowInstance.toObject()), + }); + + if (response.status === 200) { + setIsGreenOutline(true); + setIsRedOutline(false); + } else if (response.status === 500) { + setIsRedOutline(true); + setIsGreenOutline(false); + } + } catch (error) { + console.error('Error validating node:', error); + setIsRedOutline(true); + } + }, [data.id, reactFlowInstance]); + + useEffect(() => { + validateNode(); + }, [ + validateNode, + ...Object.values(data.node.template).flatMap((t) => Object.values(t)), + + ]); + + useEffect(() => { + if (isGreenOutline) { + setTimeout(() => { + setIsGreenOutline(false); + }, 1000); + } + }, [isGreenOutline]); + + const outlineColor = isGreenOutline ? 'animate-pulse-green' : isRedOutline ? 'border-red-outline' : ''; + if (!Icon) { if (showError.current) { setErrorData({ @@ -34,9 +82,11 @@ export default function GenericNode({ deleteNode(data.id); return; } + return (
Date: Tue, 25 Apr 2023 14:31:57 -0300 Subject: [PATCH 3/4] feat(validate): add debounced validation for nodes in GenericNode component fix(validate): set validation status to "success" or "error" based on response status feat(validate): add state for validation status and outline color in GenericNode component refactor(validate): use useDebouncedCallback hook for debouncing validation function refactor(validate): simplify useEffect dependencies in GenericNode component --- src/backend/langflow/api/validate.py | 2 +- .../src/CustomNodes/GenericNode/index.tsx | 341 +++++++++--------- 2 files changed, 180 insertions(+), 163 deletions(-) diff --git a/src/backend/langflow/api/validate.py b/src/backend/langflow/api/validate.py index d80010be4..e1b5a3a1a 100644 --- a/src/backend/langflow/api/validate.py +++ b/src/backend/langflow/api/validate.py @@ -46,7 +46,7 @@ def post_validate_node(node_id: str, data: dict): node = graph.get_node(node_id) if node is not None: _ = node.build() - return node.params + return str(node.params) raise Exception(f"Node {node_id} not found") except Exception as e: logger.exception(e) diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index c96591cc9..93497c0fc 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -1,186 +1,203 @@ import { TrashIcon } from "@heroicons/react/24/outline"; +import { useDebouncedCallback } from "use-debounce"; import { - classNames, - nodeColors, - nodeIcons, - snakeToNormalCase, + classNames, + nodeColors, + nodeIcons, + snakeToNormalCase, } from "../../utils"; import ParameterComponent from "./components/parameterComponent"; import { typesContext } from "../../contexts/typesContext"; import { useContext, useState, useEffect, useRef } from "react"; import { NodeDataType } from "../../types/flow"; import { alertContext } from "../../contexts/alertContext"; -import { useCallback } from 'react'; +import { useCallback } from "react"; export default function GenericNode({ - data, - selected, + data, + selected, }: { - data: NodeDataType; - selected: boolean; + data: NodeDataType; + selected: boolean; }) { - const { setErrorData } = useContext(alertContext); - const showError = useRef(true); - const { types, deleteNode } = useContext(typesContext); - const Icon = nodeIcons[types[data.type]]; + const { setErrorData } = useContext(alertContext); + const showError = useRef(true); + const { types, deleteNode } = useContext(typesContext); + const Icon = nodeIcons[types[data.type]]; + const [validationStatus, setValidationStatus] = useState("idle"); + // State for outline color + const [isGreenOutline, setIsGreenOutline] = useState(false); + const [isRedOutline, setIsRedOutline] = useState(false); + const { reactFlowInstance } = useContext(typesContext); - // State for outline color - const [isGreenOutline, setIsGreenOutline] = useState(false); - const [isRedOutline, setIsRedOutline] = useState(false); - const { reactFlowInstance } = useContext(typesContext); + const debouncedValidateNode = useDebouncedCallback(async () => { + // Check if the validationStatus is "success" + if (validationStatus === "success") return; - const validateNode = useCallback(async () => { - try { - const response = await fetch(`/validate/node/${data.id}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(reactFlowInstance.toObject()), - }); + try { + const response = await fetch(`/validate/node/${data.id}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(reactFlowInstance.toObject()), + }); - if (response.status === 200) { - setIsGreenOutline(true); - setIsRedOutline(false); - } else if (response.status === 500) { - setIsRedOutline(true); - setIsGreenOutline(false); - } - } catch (error) { - console.error('Error validating node:', error); - setIsRedOutline(true); - } - }, [data.id, reactFlowInstance]); + if (response.status === 200) { + setValidationStatus("success"); + } else if (response.status === 500) { + setValidationStatus("error"); + } + } catch (error) { + console.error("Error validating node:", error); + setValidationStatus("error"); + } + }, 1000); - useEffect(() => { - validateNode(); - }, [ - validateNode, - ...Object.values(data.node.template).flatMap((t) => Object.values(t)), + const validateNode = useCallback(() => { + debouncedValidateNode(); + }, [debouncedValidateNode]); - ]); + useEffect(() => { + validateNode(); + }, [ + validateNode, + ...Object.values(data.node.template).flatMap((t) => Object.values(t)), + ]); - useEffect(() => { - if (isGreenOutline) { - setTimeout(() => { - setIsGreenOutline(false); - }, 1000); - } - }, [isGreenOutline]); + useEffect(() => { + if (validationStatus === "success") { + setIsGreenOutline(true); + setIsRedOutline(false); + setTimeout(() => { + setIsGreenOutline(false); + }, 1000); + } else if (validationStatus === "error") { + setIsRedOutline(true); + setIsGreenOutline(false); + } else { + setIsGreenOutline(false); + setIsRedOutline(false); + } + }, [validationStatus]); - const outlineColor = isGreenOutline ? 'animate-pulse-green' : isRedOutline ? 'border-red-outline' : ''; + const outlineColor = isGreenOutline + ? "animate-pulse-green" + : isRedOutline + ? "border-red-outline" + : ""; - if (!Icon) { - if (showError.current) { - setErrorData({ - title: data.type - ? `The ${data.type} node could not be rendered, please review your json file` - : "There was a node that can't be rendered, please review your json file", - }); - showError.current = false; - } - deleteNode(data.id); - return; - } + if (!Icon) { + if (showError.current) { + setErrorData({ + title: data.type + ? `The ${data.type} node could not be rendered, please review your json file` + : "There was a node that can't be rendered, please review your json file", + }); + showError.current = false; + } + deleteNode(data.id); + return; + } - return ( -
-
-
- -
{data.type}
-
- -
+ return ( +
+
+
+ +
{data.type}
+
+ +
-
-
- {data.node.description} -
+
+
+ {data.node.description} +
- <> - {Object.keys(data.node.template) - .filter((t) => t.charAt(0) !== "_") - .map((t: string, idx) => ( -
- {idx === 0 ? ( -
- !key.startsWith("_") && data.node.template[key].show - ).length === 0 - ? "hidden" - : "" - )} - > - Inputs -
- ) : ( - <> - )} - {data.node.template[t].show ? ( - - ) : ( - <> - )} -
- ))} -
- Output -
- - -
-
- ); + <> + {Object.keys(data.node.template) + .filter((t) => t.charAt(0) !== "_") + .map((t: string, idx) => ( +
+ {idx === 0 ? ( +
+ !key.startsWith("_") && data.node.template[key].show + ).length === 0 + ? "hidden" + : "" + )} + > + Inputs +
+ ) : ( + <> + )} + {data.node.template[t].show ? ( + + ) : ( + <> + )} +
+ ))} +
+ Output +
+ + +
+
+ ); } From ff9a2e66637be9bec990ec5e419d8e9ce0c7e889 Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Tue, 25 Apr 2023 14:39:34 -0300 Subject: [PATCH 4/4] feat(frontend): add use-debounce package to dependencies in package.json --- src/frontend/package-lock.json | 12 ++++++++++++ src/frontend/package.json | 1 + 2 files changed, 13 insertions(+) diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index df61239c5..8e7c578ec 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -38,6 +38,7 @@ "reactflow": "^11.5.5", "tailwindcss": "^3.2.6", "typescript": "^4.9.5", + "use-debounce": "^9.0.4", "web-vitals": "^2.1.4" } }, @@ -17015,6 +17016,17 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-debounce": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-9.0.4.tgz", + "integrity": "sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ==", + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", diff --git a/src/frontend/package.json b/src/frontend/package.json index b669569e3..bdc24b488 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -33,6 +33,7 @@ "reactflow": "^11.5.5", "tailwindcss": "^3.2.6", "typescript": "^4.9.5", + "use-debounce": "^9.0.4", "web-vitals": "^2.1.4" }, "scripts": {