From 9e3d59d701b245ada3e65c60bc77800431fc6a08 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sun, 3 Mar 2024 23:38:50 -0300 Subject: [PATCH 01/40] Fix issue with returning string in Vertex class --- src/backend/langflow/graph/vertex/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/backend/langflow/graph/vertex/base.py b/src/backend/langflow/graph/vertex/base.py index 2a534adbd..4a00f74ea 100644 --- a/src/backend/langflow/graph/vertex/base.py +++ b/src/backend/langflow/graph/vertex/base.py @@ -143,6 +143,8 @@ class Vertex: if not isinstance(result, (dict, str)) and hasattr(result, "content"): return result.content return result + if isinstance(self._built_object, str): + self._built_result = self._built_object if isinstance(self._built_result, UnbuiltResult): return {} From 5b85b04edbb8487cbc1f8b4b7162913db9321816 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 00:05:19 -0300 Subject: [PATCH 02/40] Update GenericNode and flowStore --- .../src/CustomNodes/GenericNode/index.tsx | 46 +++++++++++-------- src/frontend/src/stores/flowStore.ts | 5 +- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index a7126fff7..094791a26 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -9,6 +9,7 @@ import Loading from "../../components/ui/loading"; import { Textarea } from "../../components/ui/textarea"; import Xmark from "../../components/ui/xmark"; import { + RUN_TIMESTAMP_PREFIX, STATUS_BUILD, STATUS_BUILDING, priorityFields, @@ -105,7 +106,7 @@ export default function GenericNode({ if (duration === undefined) { return ""; } else { - return `Duration: ${duration}`; + return `${duration}`; } }; const durationString = getDurationString(validationStatus?.data.duration); @@ -493,14 +494,32 @@ export default function GenericNode({ ) : !validationStatus ? ( {STATUS_BUILD} ) : ( -
- {typeof validationStatus.params === "string" - ? `${durationString}\n${validationStatus.params}` - .split("\n") - .map((line, index) => ( -
{line}
- )) - : durationString} +
+
+ {lastRunTime && ( +
+
{RUN_TIMESTAMP_PREFIX}
+
+ {lastRunTime} +
+
+ )} +
+
+
Duration:
+
+ {validationStatus?.data.duration} +
+
+
+ + Output + + {validationStatus?.params + .split("\n") + .map((line, index) => ( +
{line}
+ ))}
) } @@ -725,15 +744,6 @@ export default function GenericNode({ showNode={showNode} /> )} -
- {lastRunTime && ( -
- {lastRunTime.split("\n").map((line, index) => ( -
{line}
- ))} -
- )} -
)} diff --git a/src/frontend/src/stores/flowStore.ts b/src/frontend/src/stores/flowStore.ts index f27acbcdc..44aade7bd 100644 --- a/src/frontend/src/stores/flowStore.ts +++ b/src/frontend/src/stores/flowStore.ts @@ -13,7 +13,6 @@ import { FLOW_BUILD_SUCCESS_ALERT, MISSED_ERROR_ALERT, } from "../constants/alerts_constants"; -import { RUN_TIMESTAMP_PREFIX } from "../constants/constants"; import { BuildStatus } from "../constants/enums"; import { getFlowPool } from "../controllers/API"; import { VertexBuildTypeAPI } from "../types/api"; @@ -558,9 +557,7 @@ const useFlowStore = create((set, get) => ({ }; if (status == BuildStatus.BUILT) { const timestamp_string = new Date(Date.now()).toLocaleString(); - newFlowBuildStatus[ - id - ].timestamp = `${RUN_TIMESTAMP_PREFIX} ${timestamp_string}`; + newFlowBuildStatus[id].timestamp = timestamp_string; } console.log("updateBuildStatus", newFlowBuildStatus); }); From 0788a7217922878790edade3d9bb608532ffbb86 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 00:08:10 -0300 Subject: [PATCH 03/40] Refactor GenericNode component to improve output display --- .../src/CustomNodes/GenericNode/index.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index 094791a26..042757832 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -494,7 +494,7 @@ export default function GenericNode({ ) : !validationStatus ? ( {STATUS_BUILD} ) : ( -
+
{lastRunTime && (
@@ -512,14 +512,16 @@ export default function GenericNode({

- + Output - {validationStatus?.params - .split("\n") - .map((line, index) => ( -
{line}
- ))} +
+ {validationStatus?.params + .split("\n") + .map((line, index) => ( +
{line}
+ ))} +
) } From 6b4f3a446b9c9a3c00143b763d5d442aae6382a0 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 00:12:21 -0300 Subject: [PATCH 04/40] Update graph vertices and edges --- src/backend/langflow/graph/graph/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/backend/langflow/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py index 8b47ebe85..8f544fc0c 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -296,6 +296,8 @@ class Graph: for vertex_id in new_vertex_ids: new_vertex = other.get_vertex(vertex_id) self._update_edges(new_vertex) + # Graph is set at the end because the edges come from the graph + # and the other graph is where the new edges and vertices come from new_vertex.graph = self # Update existing vertices that have changed From f1a1fcbdaa28ae29e28dcaf79c99faa8c5e776f0 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 00:14:13 -0300 Subject: [PATCH 05/40] Document add vertices logic --- src/backend/langflow/graph/graph/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/backend/langflow/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py index 8f544fc0c..43d32c176 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -287,6 +287,11 @@ class Graph: for vertex_id in removed_vertex_ids: self.remove_vertex(vertex_id) + # The order here matters because adding the vertex is required + # if any of them have edges that point to any of the new vertices + # By adding them first, them adding the edges we ensure that the + # edges have valid vertices to point to + # Add new vertices for vertex_id in new_vertex_ids: new_vertex = other.get_vertex(vertex_id) From 40ee9648592615a3614516dde80da365a0f5882c Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 09:55:48 -0300 Subject: [PATCH 06/40] Add RefreshButton component to parameterComponent --- .../components/parameterComponent/index.tsx | 28 ++++++-------- .../src/components/ui/refreshButton.tsx | 38 +++++++++++++++++++ 2 files changed, 50 insertions(+), 16 deletions(-) create mode 100644 src/frontend/src/components/ui/refreshButton.tsx diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 385d441d3..942d62135 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -16,6 +16,7 @@ import PromptAreaComponent from "../../../../components/promptComponent"; import TextAreaComponent from "../../../../components/textAreaComponent"; import ToggleShadComponent from "../../../../components/toggleShadComponent"; import { Button } from "../../../../components/ui/button"; +import { RefreshButton } from "../../../../components/ui/refreshButton"; import { INPUT_HANDLER_HOVER, LANGFLOW_SUPPORTED_TYPES, @@ -68,6 +69,7 @@ export default function ParameterComponent({ const nodes = useFlowStore((state) => state.nodes); const edges = useFlowStore((state) => state.edges); const setNode = useFlowStore((state) => state.setNode); + const [isLoading, setIsLoading] = useState(false); const flow = currentFlow?.data?.nodes ?? null; @@ -420,14 +422,11 @@ export default function ParameterComponent({ />
{data.node?.template[name].refresh && ( - + )} )} @@ -466,14 +465,11 @@ export default function ParameterComponent({ /> {data.node?.template[name].refresh && ( - + )} ) : left === true && type === "code" ? ( diff --git a/src/frontend/src/components/ui/refreshButton.tsx b/src/frontend/src/components/ui/refreshButton.tsx new file mode 100644 index 000000000..7374686f2 --- /dev/null +++ b/src/frontend/src/components/ui/refreshButton.tsx @@ -0,0 +1,38 @@ +import { useState } from "react"; +import IconComponent from "../../components/genericIconComponent"; +import { NodeDataType } from "../../types/flow"; +import { cn } from "../../utils/utils"; +function RefreshButton({ + name, + data, + handleUpdateValues, +}: { + name: string; + data: NodeDataType; + handleUpdateValues: (name: string, data: NodeDataType) => void; +}) { + const [isLoading, setIsLoading] = useState(false); + + const handleClick = () => { + setIsLoading(true); + console.log("refreshing"); + handleUpdateValues(name, data); + setInterval(() => { + setIsLoading(false); + }, 500); + }; + + return ( + + ); +} + +export { RefreshButton }; From 8416fb25a7afb790c10dd9f7974ad2613adad498 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 09:56:06 -0300 Subject: [PATCH 07/40] Refactor component and custom_component classes, and add to_record method to Flow model --- .../custom/custom_component/component.py | 7 +++++-- .../custom_component/custom_component.py | 12 ++++++++---- .../services/database/models/flow/model.py | 18 +++++++++++++++++- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/backend/langflow/interface/custom/custom_component/component.py b/src/backend/langflow/interface/custom/custom_component/component.py index 13f185ed9..a889fa7b9 100644 --- a/src/backend/langflow/interface/custom/custom_component/component.py +++ b/src/backend/langflow/interface/custom/custom_component/component.py @@ -21,7 +21,9 @@ class ComponentFunctionEntrypointNameNullError(HTTPException): class Component: ERROR_CODE_NULL: ClassVar[str] = "Python code must be provided." - ERROR_FUNCTION_ENTRYPOINT_NAME_NULL: ClassVar[str] = "The name of the entrypoint function must be provided." + ERROR_FUNCTION_ENTRYPOINT_NAME_NULL: ClassVar[str] = ( + "The name of the entrypoint function must be provided." + ) code: Optional[str] = None _function_entrypoint_name: str = "build" @@ -39,7 +41,8 @@ class Component: def __setattr__(self, key, value): if key == "_user_id" and hasattr(self, "_user_id"): warnings.warn("user_id is immutable and cannot be changed.") - super().__setattr__(key, value) + else: + super().__setattr__(key, value) @cachedmethod(cache=operator.attrgetter("cache")) def get_code_tree(self, code: str): diff --git a/src/backend/langflow/interface/custom/custom_component/custom_component.py b/src/backend/langflow/interface/custom/custom_component/custom_component.py index 9364265c2..3c483eb25 100644 --- a/src/backend/langflow/interface/custom/custom_component/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component/custom_component.py @@ -341,7 +341,7 @@ class CustomComponent(Component): input_value_dict = {"input_value": input_value} return await graph.run(input_value_dict, stream=False) - def list_flows(self, *, get_session: Optional[Callable] = None) -> List[Flow]: + def list_flows(self, *, get_session: Optional[Callable] = None) -> List[Record]: if not self._user_id: raise ValueError("Session is invalid") try: @@ -349,11 +349,15 @@ class CustomComponent(Component): db_service = get_db_service() with get_session(db_service) as session: flows = session.exec( - select(Flow).where(Flow.user_id == self._user_id) + select(Flow) + .where(Flow.user_id == self._user_id) + .where(Flow.is_component == False) ).all() - return flows + + flows_records = [flow.to_record() for flow in flows] + return flows_records except Exception as e: - raise ValueError("Session is invalid") from e + raise ValueError(f"Error listing flows: {e}") def build(self, *args: Any, **kwargs: Any) -> Any: raise NotImplementedError diff --git a/src/backend/langflow/services/database/models/flow/model.py b/src/backend/langflow/services/database/models/flow/model.py index d942fa93c..1211c40ed 100644 --- a/src/backend/langflow/services/database/models/flow/model.py +++ b/src/backend/langflow/services/database/models/flow/model.py @@ -7,6 +7,8 @@ from uuid import UUID, uuid4 from pydantic import field_serializer, field_validator from sqlmodel import JSON, Column, Field, Relationship, SQLModel +from langflow.schema.schema import Record + if TYPE_CHECKING: from langflow.services.database.models.user import User @@ -16,7 +18,9 @@ class FlowBase(SQLModel): description: Optional[str] = Field(index=True, nullable=True, default=None) data: Optional[Dict] = Field(default=None, nullable=True) is_component: Optional[bool] = Field(default=False, nullable=True) - updated_at: Optional[datetime] = Field(default_factory=datetime.utcnow, nullable=True) + updated_at: Optional[datetime] = Field( + default_factory=datetime.utcnow, nullable=True + ) folder: Optional[str] = Field(default=None, nullable=True) @field_validator("data") @@ -57,6 +61,18 @@ class Flow(FlowBase, table=True): user_id: UUID = Field(index=True, foreign_key="user.id", nullable=True) user: "User" = Relationship(back_populates="flows") + def to_record(self): + serialized = self.model_dump() + data = { + "id": serialized.pop("id"), + "data": serialized.pop("data"), + "name": serialized.pop("name"), + "description": serialized.pop("description"), + "updated_at": serialized.pop("updated_at"), + } + record = Record(text=data.get("name"), data=data) + return record + class FlowCreate(FlowBase): user_id: Optional[UUID] = None From 18fd6c18144213e84047bf0b3788215fda61a4db Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 09:56:12 -0300 Subject: [PATCH 08/40] Remove ShouldRunNext component --- .../components/routing/ShouldRunNext.py | 48 ------------------- .../langflow/components/routing/__init__.py | 0 2 files changed, 48 deletions(-) delete mode 100644 src/backend/langflow/components/routing/ShouldRunNext.py delete mode 100644 src/backend/langflow/components/routing/__init__.py diff --git a/src/backend/langflow/components/routing/ShouldRunNext.py b/src/backend/langflow/components/routing/ShouldRunNext.py deleted file mode 100644 index 3cfde3c22..000000000 --- a/src/backend/langflow/components/routing/ShouldRunNext.py +++ /dev/null @@ -1,48 +0,0 @@ -# Implement ShouldRunNext component -from langchain_core.prompts import PromptTemplate - -from langflow import CustomComponent -from langflow.field_typing import BaseLanguageModel, Prompt - - -class ShouldRunNext(CustomComponent): - display_name = "Should Run Next" - description = "Decides whether to run the next component." - - def build_config(self): - return { - "prompt": { - "display_name": "Prompt", - "info": "The prompt to use for the decision. It should generate a boolean response (True or False).", - }, - "llm": { - "display_name": "LLM", - "info": "The language model to use for the decision.", - }, - } - - def build(self, template: Prompt, llm: BaseLanguageModel, **kwargs) -> bool: - # This is a simple component that always returns True - prompt_template = PromptTemplate.from_template(template) - - attributes_to_check = ["text", "page_content"] - for key, value in kwargs.items(): - for attribute in attributes_to_check: - if hasattr(value, attribute): - kwargs[key] = getattr(value, attribute) - - chain = prompt_template | llm - result = chain.invoke(kwargs) - if hasattr(result, "content") and isinstance(result.content, str): - result = result.content - elif isinstance(result, str): - result = result - else: - result = result.get("response") - - if result.lower() not in ["true", "false"]: - raise ValueError("The prompt should generate a boolean response (True or False).") - # The string should be the words true or false - # if not raise an error - bool_result = result.lower() == "true" - return bool_result diff --git a/src/backend/langflow/components/routing/__init__.py b/src/backend/langflow/components/routing/__init__.py deleted file mode 100644 index e69de29bb..000000000 From 4421c8154d0e9fb258e022caae4c4fdd32aed2f1 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 10:11:47 -0300 Subject: [PATCH 09/40] Fix isLoading state initialization and add delay before refreshing --- .../components/parameterComponent/index.tsx | 1 - src/frontend/src/components/ui/refreshButton.tsx | 13 ++++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 942d62135..d71632ce5 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -69,7 +69,6 @@ export default function ParameterComponent({ const nodes = useFlowStore((state) => state.nodes); const edges = useFlowStore((state) => state.edges); const setNode = useFlowStore((state) => state.setNode); - const [isLoading, setIsLoading] = useState(false); const flow = currentFlow?.data?.nodes ?? null; diff --git a/src/frontend/src/components/ui/refreshButton.tsx b/src/frontend/src/components/ui/refreshButton.tsx index 7374686f2..3ac90be77 100644 --- a/src/frontend/src/components/ui/refreshButton.tsx +++ b/src/frontend/src/components/ui/refreshButton.tsx @@ -13,13 +13,20 @@ function RefreshButton({ }) { const [isLoading, setIsLoading] = useState(false); - const handleClick = () => { + const handleClick = async () => { setIsLoading(true); console.log("refreshing"); handleUpdateValues(name, data); - setInterval(() => { + try { + // Wait for at least 500 milliseconds + await new Promise((resolve) => setTimeout(resolve, 500)); + // Continue with the request + // If the request takes longer than 500 milliseconds, it will not wait an additional 500 milliseconds + } catch (error) { + console.error("Error occurred while waiting for refresh:", error); + } finally { setIsLoading(false); - }, 500); + } }; return ( From 82bb7dc4151b0289253f75e0dc359a1e64f9cfc7 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 10:11:54 -0300 Subject: [PATCH 10/40] Add ListFlows and RunFlow components --- .../components/utilities/ListFlows.py | 19 ++++++++ .../langflow/components/utilities/RunFlow.py | 43 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 src/backend/langflow/components/utilities/ListFlows.py create mode 100644 src/backend/langflow/components/utilities/RunFlow.py diff --git a/src/backend/langflow/components/utilities/ListFlows.py b/src/backend/langflow/components/utilities/ListFlows.py new file mode 100644 index 000000000..724cd9a82 --- /dev/null +++ b/src/backend/langflow/components/utilities/ListFlows.py @@ -0,0 +1,19 @@ +from typing import List + +from langflow import CustomComponent +from langflow.schema import Record + + +class ListFlowsComponent(CustomComponent): + display_name = "List Flows" + description = "A component to list all available flows." + + def build_config(self): + return {} + + def build( + self, + ) -> List[Record]: + flows = self.list_flows() + self.status = flows + return flows diff --git a/src/backend/langflow/components/utilities/RunFlow.py b/src/backend/langflow/components/utilities/RunFlow.py new file mode 100644 index 000000000..16a681dec --- /dev/null +++ b/src/backend/langflow/components/utilities/RunFlow.py @@ -0,0 +1,43 @@ +from typing import List, Optional + +from langflow import CustomComponent +from langflow.field_typing import NestedDict, Text +from langflow.graph.schema import ResultData +from langflow.schema import Record + + +class RunFlowComponent(CustomComponent): + display_name = "Run Flow" + description = "A component to run a flow." + + def get_flow_names(self) -> List[str]: + flow_records = self.list_flows() + return [flow_record.data["name"] for flow_record in flow_records] + + def build_config(self): + return { + "input_value": { + "display_name": "Input Value", + "multiline": True, + }, + "flow_id": { + "display_name": "Flow ID", + "info": "The ID of the flow to run.", + "options": self.get_flow_names, + }, + "tweaks": { + "display_name": "Tweaks", + "info": "Tweaks to apply to the flow.", + }, + } + + async def build( + self, input_value: Text, flow_id: str, tweaks: NestedDict + ) -> Record: + input_dict = {"input_value": input_value} + result: List[Optional[ResultData]] = await self.run_flow( + input_value=input_dict, flow_id=flow_id, tweaks=tweaks + ) + record = Record(data=result) + self.status = record + return record From 46520e84a32c940b6b3631f7d3fd69022a071e5c Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 10:26:02 -0300 Subject: [PATCH 11/40] Add flow_records attribute and update build method in RunFlowComponent --- .../langflow/components/utilities/RunFlow.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/backend/langflow/components/utilities/RunFlow.py b/src/backend/langflow/components/utilities/RunFlow.py index 16a681dec..3255fc875 100644 --- a/src/backend/langflow/components/utilities/RunFlow.py +++ b/src/backend/langflow/components/utilities/RunFlow.py @@ -12,6 +12,7 @@ class RunFlowComponent(CustomComponent): def get_flow_names(self) -> List[str]: flow_records = self.list_flows() + self.flow_records = flow_records return [flow_record.data["name"] for flow_record in flow_records] def build_config(self): @@ -20,7 +21,7 @@ class RunFlowComponent(CustomComponent): "display_name": "Input Value", "multiline": True, }, - "flow_id": { + "flow": { "display_name": "Flow ID", "info": "The ID of the flow to run.", "options": self.get_flow_names, @@ -31,10 +32,17 @@ class RunFlowComponent(CustomComponent): }, } - async def build( - self, input_value: Text, flow_id: str, tweaks: NestedDict - ) -> Record: + async def build(self, input_value: Text, flow: str, tweaks: NestedDict) -> Record: input_dict = {"input_value": input_value} + flow_ids = [ + flow_record.data["id"] + for flow_record in self.flow_records + if flow_record.data["name"] == flow + ] + if not flow_ids: + raise ValueError(f"Flow {flow} not found.") + flow_id = flow_ids[0] + result: List[Optional[ResultData]] = await self.run_flow( input_value=input_dict, flow_id=flow_id, tweaks=tweaks ) From 0094732218bb09d85d25836e4703561dc8bd8c4d Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 10:30:41 -0300 Subject: [PATCH 12/40] Refactor RunFlowComponent and CustomComponent classes --- .../langflow/components/utilities/RunFlow.py | 17 ++++---------- .../custom_component/custom_component.py | 23 ++++++++++++++++++- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/backend/langflow/components/utilities/RunFlow.py b/src/backend/langflow/components/utilities/RunFlow.py index 3255fc875..70e92f380 100644 --- a/src/backend/langflow/components/utilities/RunFlow.py +++ b/src/backend/langflow/components/utilities/RunFlow.py @@ -12,7 +12,6 @@ class RunFlowComponent(CustomComponent): def get_flow_names(self) -> List[str]: flow_records = self.list_flows() - self.flow_records = flow_records return [flow_record.data["name"] for flow_record in flow_records] def build_config(self): @@ -21,7 +20,7 @@ class RunFlowComponent(CustomComponent): "display_name": "Input Value", "multiline": True, }, - "flow": { + "flow_name": { "display_name": "Flow ID", "info": "The ID of the flow to run.", "options": self.get_flow_names, @@ -32,19 +31,13 @@ class RunFlowComponent(CustomComponent): }, } - async def build(self, input_value: Text, flow: str, tweaks: NestedDict) -> Record: + async def build( + self, input_value: Text, flow_name: str, tweaks: NestedDict + ) -> Record: input_dict = {"input_value": input_value} - flow_ids = [ - flow_record.data["id"] - for flow_record in self.flow_records - if flow_record.data["name"] == flow - ] - if not flow_ids: - raise ValueError(f"Flow {flow} not found.") - flow_id = flow_ids[0] result: List[Optional[ResultData]] = await self.run_flow( - input_value=input_dict, flow_id=flow_id, tweaks=tweaks + input_value=input_dict, flow_name=flow_name, tweaks=tweaks ) record = Record(data=result) self.status = 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 3c483eb25..1f7b58eb4 100644 --- a/src/backend/langflow/interface/custom/custom_component/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component/custom_component.py @@ -74,6 +74,7 @@ class CustomComponent(Component): user_id: Optional[Union[UUID, str]] = None status: Optional[Any] = None """The status of the component. This is displayed on the frontend. Defaults to None.""" + _flows_records: Optional[List[Record]] = None _tree: Optional[dict] = None @@ -334,9 +335,28 @@ class CustomComponent(Component): async def run_flow( self, input_value: Union[str, list[str]], - flow_id: str, + flow_id: Optional[str] = None, + flow_name: Optional[str] = None, tweaks: Optional[dict] = None, ) -> Any: + if not flow_id and not flow_name: + raise ValueError("Flow ID or Flow Name is required") + + if not flow_id and self._flows_records: + flow_ids = [ + flow.data["id"] + for flow in self._flows_records + if flow.data["name"] == flow_name + ] + if not flow_ids: + raise ValueError(f"Flow {flow_name} not found") + elif len(flow_ids) > 1: + raise ValueError(f"Multiple flows found with the name {flow_name}") + flow_id = flow_ids[0] + + if not flow_id: + raise ValueError(f"Flow {flow_name} not found") + graph = await self.load_flow(flow_id, tweaks) input_value_dict = {"input_value": input_value} return await graph.run(input_value_dict, stream=False) @@ -355,6 +375,7 @@ class CustomComponent(Component): ).all() flows_records = [flow.to_record() for flow in flows] + self._flows_records = flows_records return flows_records except Exception as e: raise ValueError(f"Error listing flows: {e}") From ce8c274721928aa35a07beafb1f6ae1ce75e7d4d Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 10:31:33 -0300 Subject: [PATCH 13/40] Refactor RefreshButton component to handle disabled state and loading state --- .../src/components/ui/refreshButton.tsx | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/frontend/src/components/ui/refreshButton.tsx b/src/frontend/src/components/ui/refreshButton.tsx index 3ac90be77..5ff581faf 100644 --- a/src/frontend/src/components/ui/refreshButton.tsx +++ b/src/frontend/src/components/ui/refreshButton.tsx @@ -3,10 +3,12 @@ import IconComponent from "../../components/genericIconComponent"; import { NodeDataType } from "../../types/flow"; import { cn } from "../../utils/utils"; function RefreshButton({ + disabled, name, data, handleUpdateValues, }: { + disabled: boolean; name: string; data: NodeDataType; handleUpdateValues: (name: string, data: NodeDataType) => void; @@ -14,6 +16,7 @@ function RefreshButton({ const [isLoading, setIsLoading] = useState(false); const handleClick = async () => { + if (disabled) return; setIsLoading(true); console.log("refreshing"); handleUpdateValues(name, data); @@ -28,15 +31,23 @@ function RefreshButton({ setIsLoading(false); } }; + const className = cn( + "extra-side-bar-buttons ml-2 mt-1 w-1/6", + disabled ? "cursor-not-allowed" : "cursor-pointer" + ); + // icon class name should take into account the disabled state and the loading state + const disabledIconTextClass = disabled ? "text-muted-foreground" : ""; + const iconClassName = cn( + "h-4 w-4", + isLoading ? "animate-spin" : "animate-wiggle", + disabledIconTextClass + ); return ( - ); From f0073b55dfe8ba0bccbb97855937b927f325d958 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 10:32:05 -0300 Subject: [PATCH 14/40] Add disabled prop to RefreshButton component --- .../GenericNode/components/parameterComponent/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index d71632ce5..67de484f3 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -422,6 +422,7 @@ export default function ParameterComponent({ {data.node?.template[name].refresh && ( Date: Mon, 4 Mar 2024 10:37:13 -0300 Subject: [PATCH 15/40] Update RunFlow and RefreshButton components --- src/backend/langflow/components/utilities/RunFlow.py | 4 ++-- src/frontend/src/components/ui/refreshButton.tsx | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/backend/langflow/components/utilities/RunFlow.py b/src/backend/langflow/components/utilities/RunFlow.py index 70e92f380..035476ba8 100644 --- a/src/backend/langflow/components/utilities/RunFlow.py +++ b/src/backend/langflow/components/utilities/RunFlow.py @@ -21,8 +21,8 @@ class RunFlowComponent(CustomComponent): "multiline": True, }, "flow_name": { - "display_name": "Flow ID", - "info": "The ID of the flow to run.", + "display_name": "Flow Name", + "info": "The name of the flow to run.", "options": self.get_flow_names, }, "tweaks": { diff --git a/src/frontend/src/components/ui/refreshButton.tsx b/src/frontend/src/components/ui/refreshButton.tsx index 5ff581faf..1c2a0c17f 100644 --- a/src/frontend/src/components/ui/refreshButton.tsx +++ b/src/frontend/src/components/ui/refreshButton.tsx @@ -1,7 +1,8 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import IconComponent from "../../components/genericIconComponent"; import { NodeDataType } from "../../types/flow"; import { cn } from "../../utils/utils"; + function RefreshButton({ disabled, name, @@ -15,6 +16,10 @@ function RefreshButton({ }) { const [isLoading, setIsLoading] = useState(false); + useEffect(() => { + handleUpdateValues(name, data); + }, []); // Empty dependency array to run only once + const handleClick = async () => { if (disabled) return; setIsLoading(true); @@ -31,10 +36,12 @@ function RefreshButton({ setIsLoading(false); } }; + const className = cn( "extra-side-bar-buttons ml-2 mt-1 w-1/6", disabled ? "cursor-not-allowed" : "cursor-pointer" ); + // icon class name should take into account the disabled state and the loading state const disabledIconTextClass = disabled ? "text-muted-foreground" : ""; const iconClassName = cn( From 4487a839e6e764cfd4ea571e3543b11f4ef1d7ed Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 10:50:17 -0300 Subject: [PATCH 16/40] Refactor RefreshButton component to accept className and id props --- src/frontend/src/components/ui/refreshButton.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/frontend/src/components/ui/refreshButton.tsx b/src/frontend/src/components/ui/refreshButton.tsx index 1c2a0c17f..f19a44524 100644 --- a/src/frontend/src/components/ui/refreshButton.tsx +++ b/src/frontend/src/components/ui/refreshButton.tsx @@ -8,11 +8,15 @@ function RefreshButton({ name, data, handleUpdateValues, + className, + id, }: { disabled: boolean; name: string; data: NodeDataType; + className?: string; handleUpdateValues: (name: string, data: NodeDataType) => void; + id: string; }) { const [isLoading, setIsLoading] = useState(false); @@ -37,8 +41,8 @@ function RefreshButton({ } }; - const className = cn( - "extra-side-bar-buttons ml-2 mt-1 w-1/6", + const classNames = cn( + className, disabled ? "cursor-not-allowed" : "cursor-pointer" ); @@ -51,10 +55,11 @@ function RefreshButton({ ); return ( - ); From f1faf3c4282223b4e395c6bcea813d56d814c91e Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 10:50:23 -0300 Subject: [PATCH 17/40] Refactor parameter component to include refresh button --- .../components/parameterComponent/index.tsx | 68 +++++++++++++------ 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 67de484f3..79431ff72 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -391,16 +391,30 @@ export default function ParameterComponent({ !data.node?.template[name].options ? (
{data.node?.template[name].list ? ( - +
+ + {data.node?.template[name].refresh && ( +
+ +
+ )} +
) : data.node?.template[name].multiline ? (
{data.node?.template[name].refresh && ( - +
+ +
)} )} @@ -453,7 +471,8 @@ export default function ParameterComponent({ ) : left === true && type === "str" && - data.node?.template[name].options ? ( + (data.node?.template[name].options || + data.node?.template[name]?.refresh) ? ( // TODO: Improve CSS
@@ -465,11 +484,16 @@ export default function ParameterComponent({ />
{data.node?.template[name].refresh && ( - +
+ +
)}
) : left === true && type === "code" ? ( From 12459dbdb9a0cefc8630fd4f57582e69d7ef703d Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 10:55:32 -0300 Subject: [PATCH 18/40] Refactor validateNodes to use nodes from useFlowStore --- src/frontend/src/utils/buildUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/frontend/src/utils/buildUtils.ts b/src/frontend/src/utils/buildUtils.ts index e631c91de..ba3327959 100644 --- a/src/frontend/src/utils/buildUtils.ts +++ b/src/frontend/src/utils/buildUtils.ts @@ -125,7 +125,8 @@ export async function buildVertices({ if (validateNodes) { try { - validateNodes(verticesIds); + const nodes = useFlowStore.getState().nodes; + validateNodes(nodes.map((node) => node.id)); } catch (e) { return; } From ad0b0e5d5bb6b12b132e6b3e65b094311efc7ec5 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 11:29:11 -0300 Subject: [PATCH 19/40] Add isLoading state to ParameterComponent and DropdownComponent --- .../components/parameterComponent/index.tsx | 42 ++++++++++++++++++- .../components/dropdownComponent/index.tsx | 17 +++++--- .../src/components/ui/refreshButton.tsx | 21 +--------- 3 files changed, 54 insertions(+), 26 deletions(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 79431ff72..57694cb75 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -69,7 +69,7 @@ export default function ParameterComponent({ const nodes = useFlowStore((state) => state.nodes); const edges = useFlowStore((state) => state.edges); const setNode = useFlowStore((state) => state.setNode); - + const [isLoading, setIsLoading] = useState(false); const flow = currentFlow?.data?.nodes ?? null; const groupedEdge = useRef(null); @@ -87,6 +87,7 @@ export default function ParameterComponent({ const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot); const handleUpdateValues = async (name: string, data: NodeDataType) => { + setIsLoading(true); const code = data.node?.template["code"]?.value; if (!code) { console.error("Code not found in the template"); @@ -96,13 +97,46 @@ export default function ParameterComponent({ try { const res = await postCustomComponentUpdate(code, name); if (res.status === 200 && data.node?.template) { - data.node!.template[name] = res.data.template[name]; + setNode(data.id, (oldNode) => { + let newNode = cloneDeep(oldNode); + + newNode.data = { + ...newNode.data, + }; + + newNode.data.node.template[name] = res.data.template[name]; + + return newNode; + }); } } catch (err) { setErrorData(err as { title: string; list?: Array }); } + + renderTooltips(); + try { + // Wait for at least 500 milliseconds + await new Promise((resolve) => setTimeout(resolve, 500)); + // Continue with the request + // If the request takes longer than 500 milliseconds, it will not wait an additional 500 milliseconds + } catch (error) { + console.error("Error occurred while waiting for refresh:", error); + } finally { + setIsLoading(false); + } }; + useEffect(() => { + function fetchData() { + if (data.node?.template[name]?.refresh) { + handleUpdateValues(name, data); + } + } + fetchData(); + // I want this to run as soon as the component mounts + // but it is not updating the data + // the .refresh does not change + }, [data.node?.template[name]?.refresh]); const handleOnNewValue = ( newValue: string | string[] | boolean | Object[] ): void => { @@ -405,6 +439,7 @@ export default function ParameterComponent({ {data.node?.template[name].refresh && (
) : ( <> -
- - No parameters are available for display. - -
+ {(!isLoading && ( +
+ + No parameters are available for display. + +
+ )) || ( +
+ Loading... +
+ )} )} diff --git a/src/frontend/src/components/ui/refreshButton.tsx b/src/frontend/src/components/ui/refreshButton.tsx index f19a44524..3c446ff30 100644 --- a/src/frontend/src/components/ui/refreshButton.tsx +++ b/src/frontend/src/components/ui/refreshButton.tsx @@ -1,9 +1,9 @@ -import { useEffect, useState } from "react"; import IconComponent from "../../components/genericIconComponent"; import { NodeDataType } from "../../types/flow"; import { cn } from "../../utils/utils"; function RefreshButton({ + isLoading, disabled, name, data, @@ -11,6 +11,7 @@ function RefreshButton({ className, id, }: { + isLoading: boolean; disabled: boolean; name: string; data: NodeDataType; @@ -18,27 +19,9 @@ function RefreshButton({ handleUpdateValues: (name: string, data: NodeDataType) => void; id: string; }) { - const [isLoading, setIsLoading] = useState(false); - - useEffect(() => { - handleUpdateValues(name, data); - }, []); // Empty dependency array to run only once - const handleClick = async () => { if (disabled) return; - setIsLoading(true); - console.log("refreshing"); handleUpdateValues(name, data); - try { - // Wait for at least 500 milliseconds - await new Promise((resolve) => setTimeout(resolve, 500)); - // Continue with the request - // If the request takes longer than 500 milliseconds, it will not wait an additional 500 milliseconds - } catch (error) { - console.error("Error occurred while waiting for refresh:", error); - } finally { - setIsLoading(false); - } }; const classNames = cn( From c84af7d1ea6ef0720e1646ee405054d0bacee614 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 11:29:26 -0300 Subject: [PATCH 20/40] Add refresh and isLoading properties to TemplateVariableType and DropDownComponentType --- src/frontend/src/types/api/index.ts | 1 + src/frontend/src/types/components/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/frontend/src/types/api/index.ts b/src/frontend/src/types/api/index.ts index e10c01835..7b33b9ffb 100644 --- a/src/frontend/src/types/api/index.ts +++ b/src/frontend/src/types/api/index.ts @@ -54,6 +54,7 @@ export type TemplateVariableType = { input_types?: Array; display_name?: string; name?: string; + refresh?: boolean; [key: string]: any; }; export type sendAllProps = { diff --git a/src/frontend/src/types/components/index.ts b/src/frontend/src/types/components/index.ts index cd23ae31b..67b3f7b9e 100644 --- a/src/frontend/src/types/components/index.ts +++ b/src/frontend/src/types/components/index.ts @@ -30,6 +30,7 @@ export type ToggleComponentType = { editNode?: boolean; }; export type DropDownComponentType = { + isLoading?: boolean; value: string; options: string[]; onSelect: (value: string) => void; From 09c154aba8044c2207016f31175196285717fd1e Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 11:30:00 -0300 Subject: [PATCH 21/40] Update field_dict by calling options() and value() if they are callable --- src/backend/langflow/interface/custom/utils.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/backend/langflow/interface/custom/utils.py b/src/backend/langflow/interface/custom/utils.py index 2f7257417..85f636811 100644 --- a/src/backend/langflow/interface/custom/utils.py +++ b/src/backend/langflow/interface/custom/utils.py @@ -7,6 +7,8 @@ from typing import Any, Dict, List, Optional, Union from uuid import UUID from fastapi import HTTPException +from loguru import logger + from langflow.field_typing.range_spec import RangeSpec from langflow.interface.custom.attributes import ATTR_FUNC_MAPPING from langflow.interface.custom.code_parser.utils import extract_inner_type @@ -23,7 +25,6 @@ from langflow.template.frontend_node.custom_components import ( ) from langflow.utils import validate from langflow.utils.util import get_base_classes -from loguru import logger def add_output_types( @@ -249,10 +250,12 @@ def run_build_config( # Allow user to build TemplateField as well # as a dict with the same keys as TemplateField field_dict = get_field_dict(field) + # This has to be done to set refresh if options or value are callable + update_field_dict(field_dict) if update_field is not None and field_name != update_field: continue try: - update_field_dict(field_dict) + update_field_dict(field_dict, call=True) build_config[field_name] = field_dict except Exception as exc: logger.error(f"Error while getting build_config: {str(exc)}") @@ -399,15 +402,17 @@ def build_custom_components(settings_service): return custom_components_from_file -def update_field_dict(field_dict): +def update_field_dict(field_dict, call=False): """Update the field dictionary by calling options() or value() if they are callable""" if "options" in field_dict and callable(field_dict["options"]): - field_dict["options"] = field_dict["options"]() + if call: + field_dict["options"] = field_dict["options"]() # Also update the "refresh" key field_dict["refresh"] = True if "value" in field_dict and callable(field_dict["value"]): - field_dict["value"] = field_dict["value"]() + if call: + field_dict["value"] = field_dict["value"]() field_dict["refresh"] = True # Let's check if "range_spec" is a RangeSpec object From 780ce08604109e69b544b5e1160e3aa6059d0db5 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 11:34:04 -0300 Subject: [PATCH 22/40] Refactor handleUpdateValues function to include an optional delayAnimation parameter --- .../components/parameterComponent/index.tsx | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 57694cb75..9d9d747af 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -86,7 +86,11 @@ export default function ParameterComponent({ const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot); - const handleUpdateValues = async (name: string, data: NodeDataType) => { + const handleUpdateValues = async ( + name: string, + data: NodeDataType, + delayAnimation: boolean = true + ) => { setIsLoading(true); const code = data.node?.template["code"]?.value; if (!code) { @@ -114,29 +118,31 @@ export default function ParameterComponent({ } renderTooltips(); - try { - // Wait for at least 500 milliseconds - await new Promise((resolve) => setTimeout(resolve, 500)); - // Continue with the request - // If the request takes longer than 500 milliseconds, it will not wait an additional 500 milliseconds - } catch (error) { - console.error("Error occurred while waiting for refresh:", error); - } finally { - setIsLoading(false); - } + if (delayAnimation) { + try { + // Wait for at least 500 milliseconds + await new Promise((resolve) => setTimeout(resolve, 500)); + // Continue with the request + // If the request takes longer than 500 milliseconds, it will not wait an additional 500 milliseconds + } catch (error) { + console.error("Error occurred while waiting for refresh:", error); + } finally { + setIsLoading(false); + } + } else setIsLoading(false); }; useEffect(() => { function fetchData() { if (data.node?.template[name]?.refresh) { - handleUpdateValues(name, data); + handleUpdateValues(name, data, false); } } fetchData(); // I want this to run as soon as the component mounts // but it is not updating the data // the .refresh does not change - }, [data.node?.template[name]?.refresh]); + }, []); const handleOnNewValue = ( newValue: string | string[] | boolean | Object[] ): void => { From 5e73b0b9ab5d67fde8063dc88e2498b393c0153f Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Mon, 4 Mar 2024 11:43:23 -0300 Subject: [PATCH 23/40] Add scapeJSONParse function to update edge data --- .../src/pages/FlowPage/components/PageComponent/index.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index cac8b8faa..155c7bc61 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -31,6 +31,7 @@ import { getNodeId, isValidConnection, reconnectEdges, + scapeJSONParse, validateSelection, } from "../../../../utils/reactflowUtils"; import { getRandomName, isWrappedWithClass } from "../../../../utils/utils"; @@ -320,6 +321,8 @@ export default function Page({ (oldEdge: Edge, newConnection: Connection) => { if (isValidConnection(newConnection, nodes, edges)) { edgeUpdateSuccessful.current = true; + oldEdge.data.targetHandle = scapeJSONParse(newConnection.targetHandle!); + oldEdge.data.sourceHandle = scapeJSONParse(newConnection.sourceHandle!); setEdges((els) => updateEdge(oldEdge, newConnection, els)); } }, From edb1e4137f9e57a14d87ba6363b931ddff3bfc67 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 11:56:04 -0300 Subject: [PATCH 24/40] Fix data not updating when component mounts --- .../GenericNode/components/parameterComponent/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 9d9d747af..6011ebc3e 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -134,14 +134,14 @@ export default function ParameterComponent({ useEffect(() => { function fetchData() { - if (data.node?.template[name]?.refresh) { + if ( + data.node?.template[name]?.refresh && + Object.keys(data.node?.template[name]?.options ?? {}).length === 0 + ) { handleUpdateValues(name, data, false); } } fetchData(); - // I want this to run as soon as the component mounts - // but it is not updating the data - // the .refresh does not change }, []); const handleOnNewValue = ( newValue: string | string[] | boolean | Object[] From a5f4299cd772842e88469f7545346b2e1ffdf840 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 11:56:47 -0300 Subject: [PATCH 25/40] Fix issue with missing flow records --- .../interface/custom/custom_component/custom_component.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 1f7b58eb4..a616b8a88 100644 --- a/src/backend/langflow/interface/custom/custom_component/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component/custom_component.py @@ -341,7 +341,8 @@ class CustomComponent(Component): ) -> Any: if not flow_id and not flow_name: raise ValueError("Flow ID or Flow Name is required") - + if not self._flows_records: + self.list_flows() if not flow_id and self._flows_records: flow_ids = [ flow.data["id"] From 537337ed7fe65e16ff551d84c428b8fcfa083b19 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Mon, 4 Mar 2024 12:58:12 -0300 Subject: [PATCH 26/40] Refactor code for parameterComponent and nodeToolbarComponent --- .../components/parameterComponent/index.tsx | 54 ++++++++++--------- .../components/nodeToolbarComponent/index.tsx | 7 +-- src/frontend/src/stores/flowStore.ts | 4 +- src/frontend/src/utils/styleUtils.ts | 2 + 4 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 9d9d747af..43181ae24 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -253,15 +253,15 @@ export default function ParameterComponent({ {item.display_name === "" ? "" : " - "} {item.display_name.split(", ").length > 2 ? item.display_name.split(", ").map((el, index) => ( - - - {index === + + + {index === item.display_name.split(", ").length - 1 - ? el - : (el += `, `)} - - - )) + ? el + : (el += `, `)} + + + )) : item.display_name} ) : ( @@ -270,14 +270,14 @@ export default function ParameterComponent({ {item.type === "" ? "" : " - "} {item.type.split(", ").length > 2 ? item.type.split(", ").map((el, index) => ( - - - {index === item.type.split(", ").length - 1 - ? el - : (el += `, `)} - - - )) + + + {index === item.type.split(", ").length - 1 + ? el + : (el += `, `)} + + + )) : item.type} )} @@ -346,7 +346,7 @@ export default function ParameterComponent({ className={ "relative mt-1 flex w-full flex-wrap items-center justify-between bg-muted px-5 py-2" + ((name === "code" && type === "code") || - (name.includes("code") && proxy) + (name.includes("code") && proxy) ? " hidden " : "") } @@ -355,10 +355,14 @@ export default function ParameterComponent({
+ {!left && data.node?.pinned && +
+ +
} {proxy ? ( {proxy.id}}> {title} @@ -427,8 +431,8 @@ export default function ParameterComponent({ )} {left === true && - type === "str" && - !data.node?.template[name].options ? ( + type === "str" && + !data.node?.template[name].options ? (
{data.node?.template[name].list ? (
@@ -436,7 +440,7 @@ export default function ParameterComponent({ disabled={disabled} value={ !data.node.template[name].value || - data.node.template[name].value === "" + data.node.template[name].value === "" ? [""] : data.node.template[name].value } @@ -599,10 +603,10 @@ export default function ParameterComponent({ editNode={false} value={ !data.node!.template[name].value || - data.node!.template[name].value?.toString() === "{}" + data.node!.template[name].value?.toString() === "{}" ? { - yourkey: "value", - } + yourkey: "value", + } : data.node!.template[name].value } onChange={handleOnNewValue} @@ -616,7 +620,7 @@ export default function ParameterComponent({ editNode={false} value={ data.node!.template[name].value?.length === 0 || - !data.node!.template[name].value + !data.node!.template[name].value ? [{ "": "" }] : convertObjToArray(data.node!.template[name].value) } diff --git a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx index 789de30a0..c2040696d 100644 --- a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx @@ -267,7 +267,7 @@ export default function NodeToolbarComponent({ - + diff --git a/src/frontend/src/stores/flowStore.ts b/src/frontend/src/stores/flowStore.ts index 44aade7bd..5ec2ee3a2 100644 --- a/src/frontend/src/stores/flowStore.ts +++ b/src/frontend/src/stores/flowStore.ts @@ -187,10 +187,12 @@ const useFlowStore = create((set, get) => ({ typeof change === "function" ? change(get().nodes.find((node) => node.id === id)!) : change; - get().setNodes((oldNodes) => oldNodes.map((node) => { if (node.id === id) { + if((node.data as NodeDataType).node?.pinned){ + (newChange.data as NodeDataType).node!.pinned = false; + } return newChange; } return node; diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index 7d84772b8..969173513 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -6,6 +6,7 @@ import { BookMarked, BookmarkPlus, Bot, + Snowflake, Boxes, Braces, Cable, @@ -383,6 +384,7 @@ export const nodeIconsLucide: iconsType = { Clipboard, Code2, Variable, + Snowflake, Store, Download, Eraser, From a471cbfc4204881bb8fa832f34f78dcad88c6d5e Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Mon, 4 Mar 2024 13:41:35 -0300 Subject: [PATCH 27/40] Update styling colors to use variable names --- .../GenericNode/components/parameterComponent/index.tsx | 8 ++++---- .../FlowPage/components/nodeToolbarComponent/index.tsx | 2 +- src/frontend/src/style/index.css | 2 ++ src/frontend/tailwind.config.js | 1 + 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 1ff9ea3d9..3ce16df5a 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -361,15 +361,15 @@ export default function ParameterComponent({ > {!left && data.node?.pinned &&
- +
} {proxy ? ( {proxy.id}}> - {title} + {title} ) : ( - title - )} + {title} + )} {required ? " *" : ""} diff --git a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx index c2040696d..64f004333 100644 --- a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx @@ -291,7 +291,7 @@ export default function NodeToolbarComponent({ className={cn( "h-4 w-4 transition-all", // TODO UPDATE THIS COLOR TO BE A VARIABLE - pinned ? "animate-wiggle text-blue-400" : "" + pinned ? "animate-wiggle text-ice" : "" )} /> diff --git a/src/frontend/src/style/index.css b/src/frontend/src/style/index.css index ba0496071..fb4242661 100644 --- a/src/frontend/src/style/index.css +++ b/src/frontend/src/style/index.css @@ -27,6 +27,7 @@ --radius: 0.5rem; --ring: 215 20.2% 65.1%; /* hsl(215 20% 65%) */ --round-btn-shadow: #00000063; + --ice: #31a3cc; --error-background: #fef2f2; --error-foreground: #991b1b; @@ -67,6 +68,7 @@ .dark { --background: 224 35% 7.5%; /* hsl(224 40% 10%) */ --foreground: 213 31% 80%; /* hsl(213 31% 91%) */ + --ice: #60A5FA; --muted: 223 27% 11%; /* hsl(223 27% 11%) */ --muted-foreground: 215.4 16.3% 56.9%; /* hsl(215 16% 56%) */ diff --git a/src/frontend/tailwind.config.js b/src/frontend/tailwind.config.js index ba37b1b62..80a5416c3 100644 --- a/src/frontend/tailwind.config.js +++ b/src/frontend/tailwind.config.js @@ -87,6 +87,7 @@ module.exports = { "beta-foreground": "var(--beta-foreground)", "chat-bot-icon": "var(--chat-bot-icon)", "chat-user-icon": "var(--chat-user-icon)", + "ice": "var(--ice)", white: "var(--white)", border: "hsl(var(--border))", From 2502b53504c316fa6b910cac1763a268c874a5bf Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 15:18:50 -0300 Subject: [PATCH 28/40] Refactor RunFlowComponent to handle multiple results --- .../langflow/components/utilities/RunFlow.py | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/backend/langflow/components/utilities/RunFlow.py b/src/backend/langflow/components/utilities/RunFlow.py index 035476ba8..d0e49ac90 100644 --- a/src/backend/langflow/components/utilities/RunFlow.py +++ b/src/backend/langflow/components/utilities/RunFlow.py @@ -31,14 +31,28 @@ class RunFlowComponent(CustomComponent): }, } + def build_records_from_result_data(self, result_data: ResultData) -> Record: + messages = result_data.messages + records = [] + for message in messages: + record = Record(text=message.get("text", ""), data={"result": result_data}) + records.append(record) + return records + async def build( self, input_value: Text, flow_name: str, tweaks: NestedDict ) -> Record: - input_dict = {"input_value": input_value} - result: List[Optional[ResultData]] = await self.run_flow( - input_value=input_dict, flow_name=flow_name, tweaks=tweaks + results: List[Optional[ResultData]] = await self.run_flow( + input_value=input_value, flow_name=flow_name, tweaks=tweaks ) - record = Record(data=result) - self.status = record - return record + if isinstance(results, list): + records = [] + for result in results: + if result: + records.extend(self.build_records_from_result_data(result)) + else: + records = self.build_records_from_result_data(results) + + self.status = records + return records From 3f3ac0400f4506849bd805c4547ade03fabad892 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 15:19:17 -0300 Subject: [PATCH 29/40] Update schemas and vertex classes --- src/backend/langflow/api/v1/schemas.py | 2 +- src/backend/langflow/graph/schema.py | 3 ++- src/backend/langflow/graph/vertex/base.py | 29 ++++++++++++++++++++++ src/backend/langflow/graph/vertex/types.py | 15 +++++------ 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/backend/langflow/api/v1/schemas.py b/src/backend/langflow/api/v1/schemas.py index 94f60d822..7a91473e6 100644 --- a/src/backend/langflow/api/v1/schemas.py +++ b/src/backend/langflow/api/v1/schemas.py @@ -232,7 +232,7 @@ class VertexBuildResponse(BaseModel): next_vertices_ids: Optional[List[str]] = None inactive_vertices: Optional[List[str]] = None valid: bool - params: Optional[str] + params: Optional[Any] = Field(default_factory=dict) """JSON string of the params.""" data: ResultDataResponse """Mapping of vertex ids to result dict containing the param name and result value.""" diff --git a/src/backend/langflow/graph/schema.py b/src/backend/langflow/graph/schema.py index f53a0833f..a9f06ac1e 100644 --- a/src/backend/langflow/graph/schema.py +++ b/src/backend/langflow/graph/schema.py @@ -4,12 +4,13 @@ from typing import Any, Optional from pydantic import BaseModel, Field, field_serializer from langflow.graph.utils import serialize_field -from langflow.utils.schemas import ContainsEnumMeta +from langflow.utils.schemas import ChatOutputResponse, ContainsEnumMeta class ResultData(BaseModel): results: Optional[Any] = Field(default_factory=dict) artifacts: Optional[Any] = Field(default_factory=dict) + messages: Optional[list[ChatOutputResponse]] = Field(default_factory=list) timedelta: Optional[float] = None duration: Optional[str] = None diff --git a/src/backend/langflow/graph/vertex/base.py b/src/backend/langflow/graph/vertex/base.py index 4a00f74ea..739748f7c 100644 --- a/src/backend/langflow/graph/vertex/base.py +++ b/src/backend/langflow/graph/vertex/base.py @@ -28,6 +28,7 @@ from langflow.interface.initialize import loading from langflow.interface.listing import lazy_load_dict from langflow.services.deps import get_storage_service from langflow.utils.constants import DIRECT_TYPES +from langflow.utils.schemas import ChatOutputResponse from langflow.utils.util import sync_to_async if TYPE_CHECKING: @@ -413,15 +414,43 @@ class Vertex: self._built = True + def extract_messages_from_artifacts(self, artifacts: Dict[str, Any]) -> List[str]: + """ + Extracts messages from the artifacts. + + Args: + artifacts (Dict[str, Any]): The artifacts to extract messages from. + + Returns: + List[str]: The extracted messages. + """ + messages = [] + for key, artifact in artifacts.items(): + if not isinstance(artifact, dict): + continue + if "message" in artifact: + chat_output_response = ChatOutputResponse( + message=artifact["message"], + sender=artifact.get("sender"), + sender_name=artifact.get("sender_name"), + session_id=artifact.get("session_id"), + component_id=self.id, + ) + messages.append(chat_output_response.model_dump(exclude_none=True)) + + return messages + def _finalize_build(self): result_dict = self.get_built_result() # We need to set the artifacts to pass information # to the frontend self.set_artifacts() artifacts = self.artifacts + messages = self.extract_messages_from_artifacts(artifacts) result_dict = ResultData( results=result_dict, artifacts=artifacts, + messages=messages, ) self.set_result(result_dict) diff --git a/src/backend/langflow/graph/vertex/types.py b/src/backend/langflow/graph/vertex/types.py index b01443fac..1c93729f1 100644 --- a/src/backend/langflow/graph/vertex/types.py +++ b/src/backend/langflow/graph/vertex/types.py @@ -1,6 +1,7 @@ import ast import json -from typing import AsyncIterator, Callable, Dict, Iterator, List, Optional, Union +from typing import (AsyncIterator, Callable, Dict, Iterator, List, Optional, + Union) import yaml from langchain_core.messages import AIMessage @@ -124,13 +125,13 @@ class DocumentLoaderVertex(StatefulVertex): if not isinstance(self._built_object, UnbuiltObject): avg_length = sum( - len(doc.page_content) - for doc in self._built_object - if hasattr(doc, "page_content") + len(record.text) + for record in self._built_object + if hasattr(record, "text") ) / len(self._built_object) - return f"""{self.display_name}({len(self._built_object)} documents) - \nAvg. Document Length (characters): {int(avg_length)} - Documents: {self._built_object[:3]}...""" + return f"""{self.display_name}({len(self._built_object)} records) + \nAvg. Record Length (characters): {int(avg_length)} + Records: {self._built_object[:3]}...""" return f"{self.vertex_type}()" From 563356515637633cc90ae10e6435a766fd7ee73d Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 15:19:22 -0300 Subject: [PATCH 30/40] Refactor graph processing and error handling --- src/backend/langflow/graph/graph/base.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/backend/langflow/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py index 43d32c176..daff60b6d 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -111,7 +111,12 @@ class Graph: vertex = self.get_vertex(vertex_id) if vertex is None: raise ValueError(f"Vertex {vertex_id} not found") - if 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() outputs.append(vertex.result) return outputs @@ -472,15 +477,19 @@ class Graph: async def process(self) -> "Graph": """Processes the graph with vertices in each layer run in parallel.""" vertices_layers = self.sorted_vertices_layers - + vertex_task_run_count = {} for layer_index, layer in enumerate(vertices_layers): tasks = [] for vertex_id in layer: vertex = self.get_vertex(vertex_id) task = asyncio.create_task( - vertex.build(), name=f"layer-{layer_index}-vertex-{vertex_id}" + vertex.build(), + 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 + ) logger.debug(f"Running layer {layer_index} with {len(tasks)} tasks") await self._execute_tasks(tasks) logger.debug("Graph processing complete") @@ -499,6 +508,10 @@ class Graph: # coroutine has not attribute get_name task_name = tasks[i].get_name() logger.error(f"Task {task_name} failed with exception: {e}") + # Cancel all remaining tasks + for t in tasks[i:]: + t.cancel() + raise e return results def topological_sort(self) -> List[Vertex]: @@ -815,6 +828,7 @@ class Graph: vertices_layers = self.sort_by_avg_build_time(vertices_layers) # vertices_layers = self.sort_chat_inputs_first(vertices_layers) self.increment_run_count() + self._sorted_vertices_layers = vertices_layers first_layer = vertices_layers[0] # save the only the rest self.vertices_layers = vertices_layers[1:] From 2d1900d3dcc642252b6beb0ae85e281387888c55 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 15:19:32 -0300 Subject: [PATCH 31/40] Refactor code to handle optional fields in Record and add new fields to ChatOutputResponse --- .../custom/custom_component/custom_component.py | 2 +- src/backend/langflow/schema/schema.py | 10 ++++------ src/backend/langflow/utils/schemas.py | 2 ++ 3 files changed, 7 insertions(+), 7 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 a616b8a88..d2087dd2e 100644 --- a/src/backend/langflow/interface/custom/custom_component/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component/custom_component.py @@ -112,7 +112,7 @@ class CustomComponent(Component): return yaml.dump(self.repr_value) if isinstance(self.repr_value, str): return self.repr_value - return str(self.repr_value) + return self.repr_value def build_config(self): return self.field_config diff --git a/src/backend/langflow/schema/schema.py b/src/backend/langflow/schema/schema.py index c99aaf127..639c9da96 100644 --- a/src/backend/langflow/schema/schema.py +++ b/src/backend/langflow/schema/schema.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Optional from langchain_core.documents import Document from pydantic import BaseModel @@ -13,7 +13,7 @@ class Record(BaseModel): data (dict, optional): Additional data associated with the record. """ - text: str + text: Optional[str] = "" data: dict = {} @classmethod @@ -52,8 +52,6 @@ class Record(BaseModel): Returns the text of the record. Returns: - str: The text of the record. + str: The text and data of the record. """ - return self.text - - + return self.model_dump_json(indent=2) diff --git a/src/backend/langflow/utils/schemas.py b/src/backend/langflow/utils/schemas.py index 354cb5949..3e6f17a5a 100644 --- a/src/backend/langflow/utils/schemas.py +++ b/src/backend/langflow/utils/schemas.py @@ -11,7 +11,9 @@ class ChatOutputResponse(BaseModel): message: Union[str, List[Union[str, Dict]]] sender: Optional[str] = "Machine" sender_name: Optional[str] = "AI" + session_id: Optional[str] = None stream_url: Optional[str] = None + component_id: Optional[str] = None @classmethod def from_message( From 8f545ef9e93c0223d530b5b87bac931c1e14e896 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 15:19:37 -0300 Subject: [PATCH 32/40] Add validationString state and update validationStatus rendering --- .../src/CustomNodes/GenericNode/index.tsx | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index 042757832..80e5ad0f7 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -60,6 +60,8 @@ export default function GenericNode({ useState(null); const [handles, setHandles] = useState(0); + const [validationString, setValidationString] = useState(""); + const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot); function countHandles(): void { @@ -132,6 +134,18 @@ export default function GenericNode({ } }, [flowPool[data.id], data.id]); + useEffect(() => { + if (validationStatus?.params) { + // if it is not a string turn it into a string + let newValidationString = validationStatus.params; + if (typeof newValidationString !== "string") { + newValidationString = JSON.stringify(validationStatus.params); + } + + setValidationString(newValidationString); + } + }, [validationStatus, validationStatus?.params]); + const showNode = data.showNode ?? true; const nameEditable = true; @@ -516,11 +530,9 @@ export default function GenericNode({ Output
- {validationStatus?.params - .split("\n") - .map((line, index) => ( -
{line}
- ))} + {validationString.split("\n").map((line, index) => ( +
{line}
+ ))}
) From 5e0a8534f5c4e5eb3e2ff900f875494ad69b5cb9 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 16:19:35 -0300 Subject: [PATCH 33/40] Add disabled property to DropDownComponentType, fix styleUtils import, and update RefreshButton component --- .../components/parameterComponent/index.tsx | 68 ++++++++++--------- .../components/dropdownComponent/index.tsx | 2 + src/frontend/src/components/ui/button.tsx | 2 +- .../src/components/ui/refreshButton.tsx | 16 +++-- src/frontend/src/style/applies.css | 16 +++-- src/frontend/src/types/components/index.ts | 1 + src/frontend/src/utils/styleUtils.ts | 3 +- 7 files changed, 65 insertions(+), 43 deletions(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 3ce16df5a..52d8448a9 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -253,15 +253,15 @@ export default function ParameterComponent({ {item.display_name === "" ? "" : " - "} {item.display_name.split(", ").length > 2 ? item.display_name.split(", ").map((el, index) => ( - - - {index === + + + {index === item.display_name.split(", ").length - 1 - ? el - : (el += `, `)} - - - )) + ? el + : (el += `, `)} + + + )) : item.display_name} ) : ( @@ -270,14 +270,14 @@ export default function ParameterComponent({ {item.type === "" ? "" : " - "} {item.type.split(", ").length > 2 ? item.type.split(", ").map((el, index) => ( - - - {index === item.type.split(", ").length - 1 - ? el - : (el += `, `)} - - - )) + + + {index === item.type.split(", ").length - 1 + ? el + : (el += `, `)} + + + )) : item.type} )} @@ -346,7 +346,7 @@ export default function ParameterComponent({ className={ "relative mt-1 flex w-full flex-wrap items-center justify-between bg-muted px-5 py-2" + ((name === "code" && type === "code") || - (name.includes("code") && proxy) + (name.includes("code") && proxy) ? " hidden " : "") } @@ -355,21 +355,26 @@ export default function ParameterComponent({
- {!left && data.node?.pinned && + {!left && data.node?.pinned && (
- -
} + +
+ )} {proxy ? ( {proxy.id}}> - {title} + + {title} + ) : ( - {title} - )} + + {title} + + )} {required ? " *" : ""} @@ -431,8 +436,8 @@ export default function ParameterComponent({ )} {left === true && - type === "str" && - !data.node?.template[name].options ? ( + type === "str" && + !data.node?.template[name].options ? (
{data.node?.template[name].list ? (
@@ -440,7 +445,7 @@ export default function ParameterComponent({ disabled={disabled} value={ !data.node.template[name].value || - data.node.template[name].value === "" + data.node.template[name].value === "" ? [""] : data.node.template[name].value } @@ -523,6 +528,7 @@ export default function ParameterComponent({
{ setInternalValue(value); onSelect(value); diff --git a/src/frontend/src/components/ui/button.tsx b/src/frontend/src/components/ui/button.tsx index a43ce3359..262de49cc 100644 --- a/src/frontend/src/components/ui/button.tsx +++ b/src/frontend/src/components/ui/button.tsx @@ -4,7 +4,7 @@ import * as React from "react"; import { cn } from "../../utils/utils"; const buttonVariants = cva( - "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background", + "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background", { variants: { variant: { diff --git a/src/frontend/src/components/ui/refreshButton.tsx b/src/frontend/src/components/ui/refreshButton.tsx index 3c446ff30..bf41ba114 100644 --- a/src/frontend/src/components/ui/refreshButton.tsx +++ b/src/frontend/src/components/ui/refreshButton.tsx @@ -1,6 +1,7 @@ import IconComponent from "../../components/genericIconComponent"; import { NodeDataType } from "../../types/flow"; import { cn } from "../../utils/utils"; +import { Button } from "./button"; function RefreshButton({ isLoading, @@ -24,10 +25,7 @@ function RefreshButton({ handleUpdateValues(name, data); }; - const classNames = cn( - className, - disabled ? "cursor-not-allowed" : "cursor-pointer" - ); + const classNames = cn(className, disabled ? "cursor-not-allowed" : ""); // icon class name should take into account the disabled state and the loading state const disabledIconTextClass = disabled ? "text-muted-foreground" : ""; @@ -38,13 +36,19 @@ function RefreshButton({ ); return ( - + ); } diff --git a/src/frontend/src/style/applies.css b/src/frontend/src/style/applies.css index 77a760255..cca804fc7 100644 --- a/src/frontend/src/style/applies.css +++ b/src/frontend/src/style/applies.css @@ -316,7 +316,7 @@ @apply border-none ring ring-[#FF9090]; } .built-invalid-status-dark { - @apply border-none ring ring-[#751C1C] + @apply border-none ring ring-[#751C1C]; } .building-status { @apply border-none ring; @@ -431,7 +431,9 @@ .code-area-external-link:hover { @apply hover:text-accent-foreground; } - + .dropdown-component-disabled { + @apply pointer-events-none cursor-not-allowed; + } .dropdown-component-outline { @apply input-edit-node relative pr-8; } @@ -441,11 +443,17 @@ .dropdown-component-display { @apply block w-full truncate bg-background; } + .dropdown-component-display-disabled { + @apply text-muted-foreground; + } .dropdown-component-arrow { @apply pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2; } .dropdown-component-arrow-color { - @apply extra-side-bar-save-disable h-5 w-5; + @apply h-5 w-5 text-accent-foreground; + } + .dropdown-component-arrow-color-disable { + @apply h-5 w-5 text-muted-foreground; } .dropdown-component-options { @apply z-10 mt-1 max-h-60 overflow-auto rounded-md bg-background py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm; @@ -902,7 +910,7 @@ @apply flex-max-width px-2 py-6 pl-4 pr-9; } .form-modal-chatbot-icon { - @apply flex flex-col mb-3 ml-3 mr-6 mt-1; + @apply mb-3 ml-3 mr-6 mt-1 flex flex-col; } .form-modal-chat-image { @apply flex flex-col items-center gap-1; diff --git a/src/frontend/src/types/components/index.ts b/src/frontend/src/types/components/index.ts index 67b3f7b9e..aef92b1a4 100644 --- a/src/frontend/src/types/components/index.ts +++ b/src/frontend/src/types/components/index.ts @@ -30,6 +30,7 @@ export type ToggleComponentType = { editNode?: boolean; }; export type DropDownComponentType = { + disabled?: boolean; isLoading?: boolean; value: string; options: string[]; diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index 969173513..9274c59d8 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -6,7 +6,6 @@ import { BookMarked, BookmarkPlus, Bot, - Snowflake, Boxes, Braces, Cable, @@ -100,6 +99,7 @@ import { Share2, Shield, Sliders, + Snowflake, Sparkles, Square, Store, @@ -443,6 +443,7 @@ export const nodeIconsLucide: iconsType = { Link, ToyBrick, RefreshCcw, + ListRestart, Combine, TerminalIcon, TerminalSquare, From 73b3013763a19431404a39d2746ff1d0b54bde18 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 17:40:32 -0300 Subject: [PATCH 34/40] Update categories and components --- .../custom_components => base}/__init__.py | 0 .../io/base => base/io}/__init__.py | 0 .../{components/io/base => base/io}/chat.py | 0 .../{components/io/base => base/io}/text.py | 0 .../{documentloaders => data}/FileLoader.py | 0 .../GatherRecords.py | 0 .../{documentloaders => data}/UrlLoader.py | 4 +- .../langflow/components/data/__init__.py | 0 .../components/{io => inputs}/ChatInput.py | 2 +- .../components/{io => inputs}/TextInput.py | 2 +- .../langflow/components/inputs/__init__.py | 0 .../BingSearchAPIWrapper.py | 0 .../DocumentToRecord.py | 0 .../GoogleSearchAPIWrapper.py | 0 .../GoogleSerperAPIWrapper.py | 0 .../JSONDocumentBuilder.py | 0 .../SQLDatabase.py | 0 .../SearxSearchWrapper.py | 0 .../SerpAPIWrapper.py | 0 .../WikipediaAPIWrapper.py | 0 .../WolframAlphaAPIWrapper.py | 0 .../components/{io => outputs}/ChatOutput.py | 2 +- .../components/{io => outputs}/TextOutput.py | 2 +- .../langflow/components/outputs/__init__.py | 0 .../components/utilities/APIRequest.py | 109 ++++++++++++++++++ .../CustomComponent.py | 1 + .../components/utilities/GetRequest.py | 75 ------------ .../components/utilities/PostRequest.py | 78 ------------- .../components/utilities/UpdateRequest.py | 89 -------------- .../langflow/components/utilities/__init__.py | 0 .../extraSidebarComponent/index.tsx | 15 +-- .../extraSidebarComponent/utils.tsx | 31 +++++ src/frontend/src/utils/styleUtils.ts | 18 ++- 33 files changed, 163 insertions(+), 265 deletions(-) rename src/backend/langflow/{components/custom_components => base}/__init__.py (100%) rename src/backend/langflow/{components/io/base => base/io}/__init__.py (100%) rename src/backend/langflow/{components/io/base => base/io}/chat.py (100%) rename src/backend/langflow/{components/io/base => base/io}/text.py (100%) rename src/backend/langflow/components/{documentloaders => data}/FileLoader.py (100%) rename src/backend/langflow/components/{documentloaders => data}/GatherRecords.py (100%) rename src/backend/langflow/components/{documentloaders => data}/UrlLoader.py (91%) create mode 100644 src/backend/langflow/components/data/__init__.py rename src/backend/langflow/components/{io => inputs}/ChatInput.py (92%) rename src/backend/langflow/components/{io => inputs}/TextInput.py (84%) create mode 100644 src/backend/langflow/components/inputs/__init__.py rename src/backend/langflow/components/{utilities => langchain_utilities}/BingSearchAPIWrapper.py (100%) rename src/backend/langflow/components/{utilities => langchain_utilities}/DocumentToRecord.py (100%) rename src/backend/langflow/components/{utilities => langchain_utilities}/GoogleSearchAPIWrapper.py (100%) rename src/backend/langflow/components/{utilities => langchain_utilities}/GoogleSerperAPIWrapper.py (100%) rename src/backend/langflow/components/{utilities => langchain_utilities}/JSONDocumentBuilder.py (100%) rename src/backend/langflow/components/{utilities => langchain_utilities}/SQLDatabase.py (100%) rename src/backend/langflow/components/{utilities => langchain_utilities}/SearxSearchWrapper.py (100%) rename src/backend/langflow/components/{utilities => langchain_utilities}/SerpAPIWrapper.py (100%) rename src/backend/langflow/components/{utilities => langchain_utilities}/WikipediaAPIWrapper.py (100%) rename src/backend/langflow/components/{utilities => langchain_utilities}/WolframAlphaAPIWrapper.py (100%) rename src/backend/langflow/components/{io => outputs}/ChatOutput.py (92%) rename src/backend/langflow/components/{io => outputs}/TextOutput.py (87%) create mode 100644 src/backend/langflow/components/outputs/__init__.py create mode 100644 src/backend/langflow/components/utilities/APIRequest.py rename src/backend/langflow/components/{custom_components => utilities}/CustomComponent.py (91%) delete mode 100644 src/backend/langflow/components/utilities/GetRequest.py delete mode 100644 src/backend/langflow/components/utilities/PostRequest.py delete mode 100644 src/backend/langflow/components/utilities/UpdateRequest.py create mode 100644 src/backend/langflow/components/utilities/__init__.py create mode 100644 src/frontend/src/pages/FlowPage/components/extraSidebarComponent/utils.tsx diff --git a/src/backend/langflow/components/custom_components/__init__.py b/src/backend/langflow/base/__init__.py similarity index 100% rename from src/backend/langflow/components/custom_components/__init__.py rename to src/backend/langflow/base/__init__.py diff --git a/src/backend/langflow/components/io/base/__init__.py b/src/backend/langflow/base/io/__init__.py similarity index 100% rename from src/backend/langflow/components/io/base/__init__.py rename to src/backend/langflow/base/io/__init__.py diff --git a/src/backend/langflow/components/io/base/chat.py b/src/backend/langflow/base/io/chat.py similarity index 100% rename from src/backend/langflow/components/io/base/chat.py rename to src/backend/langflow/base/io/chat.py diff --git a/src/backend/langflow/components/io/base/text.py b/src/backend/langflow/base/io/text.py similarity index 100% rename from src/backend/langflow/components/io/base/text.py rename to src/backend/langflow/base/io/text.py diff --git a/src/backend/langflow/components/documentloaders/FileLoader.py b/src/backend/langflow/components/data/FileLoader.py similarity index 100% rename from src/backend/langflow/components/documentloaders/FileLoader.py rename to src/backend/langflow/components/data/FileLoader.py diff --git a/src/backend/langflow/components/documentloaders/GatherRecords.py b/src/backend/langflow/components/data/GatherRecords.py similarity index 100% rename from src/backend/langflow/components/documentloaders/GatherRecords.py rename to src/backend/langflow/components/data/GatherRecords.py diff --git a/src/backend/langflow/components/documentloaders/UrlLoader.py b/src/backend/langflow/components/data/UrlLoader.py similarity index 91% rename from src/backend/langflow/components/documentloaders/UrlLoader.py rename to src/backend/langflow/components/data/UrlLoader.py index eb60ac572..c1346f142 100644 --- a/src/backend/langflow/components/documentloaders/UrlLoader.py +++ b/src/backend/langflow/components/data/UrlLoader.py @@ -40,7 +40,9 @@ class UrlLoaderComponent(CustomComponent): except Exception as e: raise ValueError(f"No loader found for: {web_path}") from e docs = loader_instance.load() - avg_length = sum(len(doc.page_content) for doc in docs if hasattr(doc, "page_content")) / len(docs) + avg_length = sum( + len(doc.page_content) for doc in docs if hasattr(doc, "page_content") + ) / len(docs) self.status = f"""{len(docs)} documents) \nAvg. Document Length (characters): {int(avg_length)} Documents: {docs[:3]}...""" diff --git a/src/backend/langflow/components/data/__init__.py b/src/backend/langflow/components/data/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/langflow/components/io/ChatInput.py b/src/backend/langflow/components/inputs/ChatInput.py similarity index 92% rename from src/backend/langflow/components/io/ChatInput.py rename to src/backend/langflow/components/inputs/ChatInput.py index de8ce14cb..e5867751c 100644 --- a/src/backend/langflow/components/io/ChatInput.py +++ b/src/backend/langflow/components/inputs/ChatInput.py @@ -1,6 +1,6 @@ from typing import Optional, Union -from langflow.components.io.base.chat import ChatComponent +from langflow.base.io.chat import ChatComponent from langflow.field_typing import Text from langflow.schema import Record diff --git a/src/backend/langflow/components/io/TextInput.py b/src/backend/langflow/components/inputs/TextInput.py similarity index 84% rename from src/backend/langflow/components/io/TextInput.py rename to src/backend/langflow/components/inputs/TextInput.py index dc7ff1a73..034cf527b 100644 --- a/src/backend/langflow/components/io/TextInput.py +++ b/src/backend/langflow/components/inputs/TextInput.py @@ -1,6 +1,6 @@ from typing import Optional -from langflow.components.io.base.text import TextComponent +from langflow.base.io.text import TextComponent from langflow.field_typing import Text diff --git a/src/backend/langflow/components/inputs/__init__.py b/src/backend/langflow/components/inputs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/langflow/components/utilities/BingSearchAPIWrapper.py b/src/backend/langflow/components/langchain_utilities/BingSearchAPIWrapper.py similarity index 100% rename from src/backend/langflow/components/utilities/BingSearchAPIWrapper.py rename to src/backend/langflow/components/langchain_utilities/BingSearchAPIWrapper.py diff --git a/src/backend/langflow/components/utilities/DocumentToRecord.py b/src/backend/langflow/components/langchain_utilities/DocumentToRecord.py similarity index 100% rename from src/backend/langflow/components/utilities/DocumentToRecord.py rename to src/backend/langflow/components/langchain_utilities/DocumentToRecord.py diff --git a/src/backend/langflow/components/utilities/GoogleSearchAPIWrapper.py b/src/backend/langflow/components/langchain_utilities/GoogleSearchAPIWrapper.py similarity index 100% rename from src/backend/langflow/components/utilities/GoogleSearchAPIWrapper.py rename to src/backend/langflow/components/langchain_utilities/GoogleSearchAPIWrapper.py diff --git a/src/backend/langflow/components/utilities/GoogleSerperAPIWrapper.py b/src/backend/langflow/components/langchain_utilities/GoogleSerperAPIWrapper.py similarity index 100% rename from src/backend/langflow/components/utilities/GoogleSerperAPIWrapper.py rename to src/backend/langflow/components/langchain_utilities/GoogleSerperAPIWrapper.py diff --git a/src/backend/langflow/components/utilities/JSONDocumentBuilder.py b/src/backend/langflow/components/langchain_utilities/JSONDocumentBuilder.py similarity index 100% rename from src/backend/langflow/components/utilities/JSONDocumentBuilder.py rename to src/backend/langflow/components/langchain_utilities/JSONDocumentBuilder.py diff --git a/src/backend/langflow/components/utilities/SQLDatabase.py b/src/backend/langflow/components/langchain_utilities/SQLDatabase.py similarity index 100% rename from src/backend/langflow/components/utilities/SQLDatabase.py rename to src/backend/langflow/components/langchain_utilities/SQLDatabase.py diff --git a/src/backend/langflow/components/utilities/SearxSearchWrapper.py b/src/backend/langflow/components/langchain_utilities/SearxSearchWrapper.py similarity index 100% rename from src/backend/langflow/components/utilities/SearxSearchWrapper.py rename to src/backend/langflow/components/langchain_utilities/SearxSearchWrapper.py diff --git a/src/backend/langflow/components/utilities/SerpAPIWrapper.py b/src/backend/langflow/components/langchain_utilities/SerpAPIWrapper.py similarity index 100% rename from src/backend/langflow/components/utilities/SerpAPIWrapper.py rename to src/backend/langflow/components/langchain_utilities/SerpAPIWrapper.py diff --git a/src/backend/langflow/components/utilities/WikipediaAPIWrapper.py b/src/backend/langflow/components/langchain_utilities/WikipediaAPIWrapper.py similarity index 100% rename from src/backend/langflow/components/utilities/WikipediaAPIWrapper.py rename to src/backend/langflow/components/langchain_utilities/WikipediaAPIWrapper.py diff --git a/src/backend/langflow/components/utilities/WolframAlphaAPIWrapper.py b/src/backend/langflow/components/langchain_utilities/WolframAlphaAPIWrapper.py similarity index 100% rename from src/backend/langflow/components/utilities/WolframAlphaAPIWrapper.py rename to src/backend/langflow/components/langchain_utilities/WolframAlphaAPIWrapper.py diff --git a/src/backend/langflow/components/io/ChatOutput.py b/src/backend/langflow/components/outputs/ChatOutput.py similarity index 92% rename from src/backend/langflow/components/io/ChatOutput.py rename to src/backend/langflow/components/outputs/ChatOutput.py index a528b65b8..aa61159c9 100644 --- a/src/backend/langflow/components/io/ChatOutput.py +++ b/src/backend/langflow/components/outputs/ChatOutput.py @@ -1,6 +1,6 @@ from typing import Optional, Union -from langflow.components.io.base.chat import ChatComponent +from langflow.base.io.chat import ChatComponent from langflow.field_typing import Text from langflow.schema import Record diff --git a/src/backend/langflow/components/io/TextOutput.py b/src/backend/langflow/components/outputs/TextOutput.py similarity index 87% rename from src/backend/langflow/components/io/TextOutput.py rename to src/backend/langflow/components/outputs/TextOutput.py index f92e09146..c971a9699 100644 --- a/src/backend/langflow/components/io/TextOutput.py +++ b/src/backend/langflow/components/outputs/TextOutput.py @@ -1,6 +1,6 @@ from typing import Optional -from langflow.components.io.base.text import TextComponent +from langflow.base.io.text import TextComponent from langflow.field_typing import Text diff --git a/src/backend/langflow/components/outputs/__init__.py b/src/backend/langflow/components/outputs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/langflow/components/utilities/APIRequest.py b/src/backend/langflow/components/utilities/APIRequest.py new file mode 100644 index 000000000..2e73979ff --- /dev/null +++ b/src/backend/langflow/components/utilities/APIRequest.py @@ -0,0 +1,109 @@ +import asyncio +from typing import List, Optional, Union +import httpx + +import requests + +from langflow import CustomComponent +from langflow.schema import Record +from langflow.services.database.models.base import orjson_dumps + + +class APIRequest(CustomComponent): + display_name: str = "API Request" + description: str = "Make an HTTP request to the given URL." + output_types: list[str] = ["Record"] + documentation: str = "https://docs.langflow.org/components/utilities#api-request" + beta: bool = True + field_config = { + "url": {"display_name": "URL", "info": "The URL to make the request to."}, + "method": { + "display_name": "Method", + "info": "The HTTP method to use.", + "field_type": "str", + "options": ["GET", "POST", "PATCH", "PUT"], + "value": "GET", + }, + "headers": { + "display_name": "Headers", + "info": "The headers to send with the request.", + }, + "record": { + "display_name": "Record", + "info": "The record to send with the request (for POST, PATCH, PUT).", + }, + "timeout": { + "display_name": "Timeout", + "field_type": "int", + "info": "The timeout to use for the request.", + "value": 5, + }, + } + + async def make_request( + self, + session: requests.Session, + method: str, + url: str, + headers: Optional[dict] = None, + record: Optional[Record] = None, + timeout: int = 5, + ) -> Record: + method = method.upper() + if method not in ["GET", "POST", "PATCH", "PUT"]: + raise ValueError(f"Unsupported method: {method}") + + data = record.text if record else None + try: + async with httpx.AsyncClient() as client: + response = await client.request( + method, url, headers=headers, content=data, timeout=timeout + ) + try: + response_json = response.json() + result = orjson_dumps(response_json, indent_2=False) + except Exception: + result = response.text + return Record( + text=result, + data={ + "source": url, + "headers": headers, + "status_code": response.status_code, + }, + ) + except httpx.TimeoutException: + return Record( + text="Request Timed Out", + data={"source": url, "headers": headers, "status_code": 408}, + ) + except Exception as exc: + return Record( + text=str(exc), + data={"source": url, "headers": headers, "status_code": 500}, + ) + + async def build( + self, + method: str, + url: List[str], + headers: Optional[dict] = None, + record: Optional[Union[Record, List[Record]]] = None, + timeout: int = 5, + ) -> List[Record]: + if headers is None: + headers = {} + urls = url if isinstance(url, list) else [url] + records = ( + record + if isinstance(record, list) + else [record] if record else [None] * len(urls) + ) + + results = await asyncio.gather( + *[ + self.make_request(method, u, headers, doc, timeout) + for u, doc in zip(urls, records) + ] + ) + return results diff --git a/src/backend/langflow/components/custom_components/CustomComponent.py b/src/backend/langflow/components/utilities/CustomComponent.py similarity index 91% rename from src/backend/langflow/components/custom_components/CustomComponent.py rename to src/backend/langflow/components/utilities/CustomComponent.py index 533ccb727..c45b5effd 100644 --- a/src/backend/langflow/components/custom_components/CustomComponent.py +++ b/src/backend/langflow/components/utilities/CustomComponent.py @@ -4,6 +4,7 @@ from langflow.field_typing import Data class Component(CustomComponent): documentation: str = "http://docs.langflow.org/components/custom" + icon = "custom_components" def build_config(self): return {"param": {"display_name": "Parameter"}} diff --git a/src/backend/langflow/components/utilities/GetRequest.py b/src/backend/langflow/components/utilities/GetRequest.py deleted file mode 100644 index d6ee5a44f..000000000 --- a/src/backend/langflow/components/utilities/GetRequest.py +++ /dev/null @@ -1,75 +0,0 @@ -from typing import Optional, Text - -import requests -from langchain_core.documents import Document - -from langflow import CustomComponent -from langflow.services.database.models.base import orjson_dumps - - -class GetRequest(CustomComponent): - display_name: str = "GET Request" - description: str = "Make a GET request to the given URL." - output_types: list[str] = ["Document"] - documentation: str = "https://docs.langflow.org/components/utilities#get-request" - beta: bool = True - field_config = { - "url": { - "display_name": "URL", - "info": "The URL to make the request to", - "is_list": True, - }, - "headers": { - "display_name": "Headers", - "info": "The headers to send with the request.", - }, - "code": {"show": False}, - "timeout": { - "display_name": "Timeout", - "field_type": "int", - "info": "The timeout to use for the request.", - "value": 5, - }, - } - - def get_document(self, session: requests.Session, url: str, headers: Optional[dict], timeout: int) -> Document: - try: - response = session.get(url, headers=headers, timeout=int(timeout)) - try: - response_json = response.json() - result = orjson_dumps(response_json, indent_2=False) - except Exception: - result = response.text - self.repr_value = result - return Document( - page_content=result, - metadata={ - "source": url, - "headers": headers, - "status_code": response.status_code, - }, - ) - except requests.Timeout: - return Document( - page_content="Request Timed Out", - metadata={"source": url, "headers": headers, "status_code": 408}, - ) - except Exception as exc: - return Document( - page_content=Text(exc), - metadata={"source": url, "headers": headers, "status_code": 500}, - ) - - def build( - self, - url: str, - headers: Optional[dict] = None, - timeout: int = 5, - ) -> list[Document]: - if headers is None: - headers = {} - urls = url if isinstance(url, list) else [url] - with requests.Session() as session: - documents = [self.get_document(session, u, headers, timeout) for u in urls] - self.repr_value = documents - return documents diff --git a/src/backend/langflow/components/utilities/PostRequest.py b/src/backend/langflow/components/utilities/PostRequest.py deleted file mode 100644 index befc006c8..000000000 --- a/src/backend/langflow/components/utilities/PostRequest.py +++ /dev/null @@ -1,78 +0,0 @@ -from typing import Optional, Text - -import requests -from langchain_core.documents import Document - -from langflow import CustomComponent -from langflow.services.database.models.base import orjson_dumps - - -class PostRequest(CustomComponent): - display_name: str = "POST Request" - description: str = "Make a POST request to the given URL." - output_types: list[str] = ["Document"] - documentation: str = "https://docs.langflow.org/components/utilities#post-request" - beta: bool = True - field_config = { - "url": {"display_name": "URL", "info": "The URL to make the request to."}, - "headers": { - "display_name": "Headers", - "info": "The headers to send with the request.", - }, - "code": {"show": False}, - "document": {"display_name": "Document"}, - } - - def post_document( - self, - session: requests.Session, - document: Document, - url: str, - headers: Optional[dict] = None, - ) -> Document: - try: - response = session.post(url, headers=headers, data=document.page_content) - try: - response_json = response.json() - result = orjson_dumps(response_json, indent_2=False) - except Exception: - result = response.text - self.repr_value = result - return Document( - page_content=result, - metadata={ - "source": url, - "headers": headers, - "status_code": response, - }, - ) - except Exception as exc: - return Document( - page_content=Text(exc), - metadata={ - "source": url, - "headers": headers, - "status_code": 500, - }, - ) - - def build( - self, - document: Document, - url: str, - headers: Optional[dict] = None, - ) -> list[Document]: - if headers is None: - headers = {} - - if not isinstance(document, list) and isinstance(document, Document): - documents: list[Document] = [document] - elif isinstance(document, list) and all(isinstance(doc, Document) for doc in document): - documents = document - else: - raise ValueError("document must be a Document or a list of Documents") - - with requests.Session() as session: - documents = [self.post_document(session, doc, url, headers) for doc in documents] - self.repr_value = documents - return documents diff --git a/src/backend/langflow/components/utilities/UpdateRequest.py b/src/backend/langflow/components/utilities/UpdateRequest.py deleted file mode 100644 index 41a57eda6..000000000 --- a/src/backend/langflow/components/utilities/UpdateRequest.py +++ /dev/null @@ -1,89 +0,0 @@ -from typing import List, Optional, Text - -import requests -from langchain_core.documents import Document - -from langflow import CustomComponent -from langflow.services.database.models.base import orjson_dumps - - -class UpdateRequest(CustomComponent): - display_name: str = "Update Request" - description: str = "Make a PATCH request to the given URL." - output_types: list[str] = ["Document"] - documentation: str = "https://docs.langflow.org/components/utilities#update-request" - beta: bool = True - field_config = { - "url": {"display_name": "URL", "info": "The URL to make the request to."}, - "headers": { - "display_name": "Headers", - "field_type": "NestedDict", - "info": "The headers to send with the request.", - }, - "code": {"show": False}, - "document": {"display_name": "Document"}, - "method": { - "display_name": "Method", - "field_type": "str", - "info": "The HTTP method to use.", - "options": ["PATCH", "PUT"], - "value": "PATCH", - }, - } - - def update_document( - self, - session: requests.Session, - document: Document, - url: str, - headers: Optional[dict] = None, - method: str = "PATCH", - ) -> Document: - try: - if method == "PATCH": - response = session.patch(url, headers=headers, data=document.page_content) - elif method == "PUT": - response = session.put(url, headers=headers, data=document.page_content) - else: - raise ValueError(f"Unsupported method: {method}") - try: - response_json = response.json() - result = orjson_dumps(response_json, indent_2=False) - except Exception: - result = response.text - self.repr_value = result - return Document( - page_content=result, - metadata={ - "source": url, - "headers": headers, - "status_code": response.status_code, - }, - ) - except Exception as exc: - return Document( - page_content=Text(exc), - metadata={"source": url, "headers": headers, "status_code": 500}, - ) - - def build( - self, - method: str, - document: Document, - url: str, - headers: Optional[dict] = None, - ) -> List[Document]: - if headers is None: - headers = {} - - if not isinstance(document, list) and isinstance(document, Document): - documents: list[Document] = [document] - elif isinstance(document, list) and all(isinstance(doc, Document) for doc in document): - documents = document - else: - raise ValueError("document must be a Document or a list of Documents") - - with requests.Session() as session: - documents = [self.update_document(session, doc, url, headers, method) for doc in documents] - self.repr_value = documents - return documents diff --git a/src/backend/langflow/components/utilities/__init__.py b/src/backend/langflow/components/utilities/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx index fe78895c0..299507bbd 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx @@ -26,6 +26,7 @@ import { } from "../../../../utils/utils"; import DisclosureComponent from "../DisclosureComponent"; import SidebarDraggableComponent from "./sideBarDraggableComponent"; +import { sortKeys } from "./utils"; export default function ExtraSidebar(): JSX.Element { const data = useTypesStore((state) => state.data); @@ -320,19 +321,7 @@ export default function ExtraSidebar(): JSX.Element {
{Object.keys(dataFilter) - .sort((a, b) => { - if (a.toLowerCase() === "saved_components") { - return -1; - } else if (b.toLowerCase() === "saved_components") { - return 1; - } else if (a.toLowerCase() === "custom_components") { - return -2; - } else if (b.toLowerCase() === "custom_components") { - return 2; - } else { - return a.localeCompare(b); - } - }) + .sort(sortKeys) .map((SBSectionName: keyof APIObjectType, index) => Object.keys(dataFilter[SBSectionName]).length > 0 ? ( Date: Mon, 4 Mar 2024 17:40:39 -0300 Subject: [PATCH 35/40] Add dynamic icon loading and suspense fallback --- .../components/genericIconComponent/index.tsx | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/frontend/src/components/genericIconComponent/index.tsx b/src/frontend/src/components/genericIconComponent/index.tsx index 62299bdf6..af3fe061b 100644 --- a/src/frontend/src/components/genericIconComponent/index.tsx +++ b/src/frontend/src/components/genericIconComponent/index.tsx @@ -1,4 +1,5 @@ -import { forwardRef } from "react"; +import dynamicIconImports from "lucide-react/dynamicIconImports"; +import { Suspense, forwardRef, lazy } from "react"; import { IconComponentProps } from "../../types/components"; import { nodeIconsLucide } from "../../utils/styleUtils"; @@ -14,7 +15,13 @@ const ForwardedIconComponent = forwardRef( }: IconComponentProps, ref ) => { - const TargetIcon = nodeIconsLucide[name] ?? nodeIconsLucide["unknown"]; + let TargetIcon = nodeIconsLucide[name]; + if (!TargetIcon) { + // check if name exists in dynamicIconImports + if (!dynamicIconImports[name]) { + TargetIcon = nodeIconsLucide["unknown"]; + } else TargetIcon = lazy(dynamicIconImports[name]); + } const style = { strokeWidth: strokeWidth ?? 1.5, @@ -22,13 +29,21 @@ const ForwardedIconComponent = forwardRef( ...(iconColor && { color: iconColor, stroke: stroke }), }; + if (!TargetIcon) { + return null; // Render nothing until the icon is loaded + } + const fallback = ( +
+ ); return ( - + + + ); } ); From cacd42b4b7029b12810c5031b70eb47a878d3ef5 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 18:09:56 -0300 Subject: [PATCH 36/40] Make output become repr if no status is set --- .../langflow/interface/initialize/loading.py | 65 ++++++++++++++----- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/src/backend/langflow/interface/initialize/loading.py b/src/backend/langflow/interface/initialize/loading.py index d9b678639..7bd8e34f4 100644 --- a/src/backend/langflow/interface/initialize/loading.py +++ b/src/backend/langflow/interface/initialize/loading.py @@ -30,6 +30,7 @@ from langflow.interface.retrievers.base import retriever_creator from langflow.interface.toolkits.base import toolkits_creator from langflow.interface.utils import load_file_into_dict from langflow.interface.wrappers.base import wrapper_creator +from langflow.schema.schema import Record from langflow.utils import validate if TYPE_CHECKING: @@ -143,9 +144,13 @@ async def instantiate_based_on_type( return class_object(**params) -async def instantiate_custom_component(node_type, class_object, params, user_id, vertex): +async def instantiate_custom_component( + node_type, class_object, params, user_id, vertex +): params_copy = params.copy() - class_object: Type["CustomComponent"] = eval_custom_component_code(params_copy.pop("code")) + class_object: Type["CustomComponent"] = eval_custom_component_code( + params_copy.pop("code") + ) custom_component: "CustomComponent" = class_object( user_id=user_id, parameters=params_copy, @@ -165,8 +170,10 @@ async def instantiate_custom_component(node_type, class_object, params, user_id, else: # Call the build method directly if it's sync build_result = custom_component.build(**params_copy) - - return custom_component, build_result, {"repr": custom_component.custom_repr()} + custom_repr = custom_component.custom_repr() + if not custom_repr and isinstance(build_result, (dict, Record, str)): + custom_repr = build_result + return custom_component, build_result, {"repr": custom_repr} def instantiate_wrapper(node_type, class_object, params): @@ -219,7 +226,9 @@ def instantiate_memory(node_type, class_object, params): # I want to catch a specific attribute error that happens # when the object does not have a cursor attribute except Exception as exc: - if "object has no attribute 'cursor'" in str(exc) or 'object has no field "conn"' in str(exc): + if "object has no attribute 'cursor'" in str( + exc + ) or 'object has no field "conn"' in str(exc): raise AttributeError( ( "Failed to build connection to database." @@ -262,7 +271,9 @@ def instantiate_agent(node_type, class_object: Type[agent_module.Agent], params: if class_method := getattr(class_object, method, None): agent = class_method(**params) tools = params.get("tools", []) - return AgentExecutor.from_agent_and_tools(agent=agent, tools=tools, handle_parsing_errors=True) + return AgentExecutor.from_agent_and_tools( + agent=agent, tools=tools, handle_parsing_errors=True + ) return load_agent_executor(class_object, params) @@ -318,7 +329,11 @@ def instantiate_embedding(node_type, class_object, params: Dict): try: return class_object(**params) except ValidationError: - params = {key: value for key, value in params.items() if key in class_object.model_fields} + params = { + key: value + for key, value in params.items() + if key in class_object.model_fields + } return class_object(**params) @@ -330,7 +345,9 @@ def instantiate_vectorstore(class_object: Type[VectorStore], params: Dict): if "texts" in params: params["documents"] = params.pop("texts") if "documents" in params: - params["documents"] = [doc for doc in params["documents"] if isinstance(doc, Document)] + params["documents"] = [ + doc for doc in params["documents"] if isinstance(doc, Document) + ] if initializer := vecstore_initializer.get(class_object.__name__): vecstore = initializer(class_object, params) else: @@ -345,7 +362,9 @@ def instantiate_vectorstore(class_object: Type[VectorStore], params: Dict): return vecstore -def instantiate_documentloader(node_type: str, class_object: Type[BaseLoader], params: Dict): +def instantiate_documentloader( + node_type: str, class_object: Type[BaseLoader], params: Dict +): if "file_filter" in params: # file_filter will be a string but we need a function # that will be used to filter the files using file_filter @@ -354,13 +373,17 @@ def instantiate_documentloader(node_type: str, class_object: Type[BaseLoader], p # in x and if it is, we will return True file_filter = params.pop("file_filter") extensions = file_filter.split(",") - params["file_filter"] = lambda x: any(extension.strip() in x for extension in extensions) + params["file_filter"] = lambda x: any( + extension.strip() in x for extension in extensions + ) metadata = params.pop("metadata", None) if metadata and isinstance(metadata, str): try: metadata = orjson.loads(metadata) except json.JSONDecodeError as exc: - raise ValueError("The metadata you provided is not a valid JSON string.") from exc + raise ValueError( + "The metadata you provided is not a valid JSON string." + ) from exc if node_type == "WebBaseLoader": if web_path := params.pop("web_path", None): @@ -393,12 +416,16 @@ def instantiate_textsplitter( "Try changing the chunk_size of the Text Splitter." ) from exc - if ("separator_type" in params and params["separator_type"] == "Text") or "separator_type" not in params: + if ( + "separator_type" in params and params["separator_type"] == "Text" + ) or "separator_type" not in params: params.pop("separator_type", None) # separators might come in as an escaped string like \\n # so we need to convert it to a string if "separators" in params: - params["separators"] = params["separators"].encode().decode("unicode-escape") + params["separators"] = ( + params["separators"].encode().decode("unicode-escape") + ) text_splitter = class_object(**params) else: from langchain.text_splitter import Language @@ -425,7 +452,8 @@ def replace_zero_shot_prompt_with_prompt_template(nodes): tools = [ tool for tool in nodes - if tool["type"] != "chatOutputNode" and "Tool" in tool["data"]["node"]["base_classes"] + if tool["type"] != "chatOutputNode" + and "Tool" in tool["data"]["node"]["base_classes"] ] node["data"] = build_prompt_template(prompt=node["data"], tools=tools) break @@ -439,7 +467,9 @@ def load_agent_executor(agent_class: type[agent_module.Agent], params, **kwargs) # agent has hidden args for memory. might need to be support # memory = params["memory"] # if allowed_tools is not a list or set, make it a list - if not isinstance(allowed_tools, (list, set)) and isinstance(allowed_tools, BaseTool): + if not isinstance(allowed_tools, (list, set)) and isinstance( + allowed_tools, BaseTool + ): allowed_tools = [allowed_tools] tool_names = [tool.name for tool in allowed_tools] # Agent class requires an output_parser but Agent classes @@ -467,7 +497,10 @@ def build_prompt_template(prompt, tools): format_instructions = prompt["node"]["template"]["format_instructions"]["value"] tool_strings = "\n".join( - [f"{tool['data']['node']['name']}: {tool['data']['node']['description']}" for tool in tools] + [ + f"{tool['data']['node']['name']}: {tool['data']['node']['description']}" + for tool in tools + ] ) tool_names = ", ".join([tool["data"]["node"]["name"] for tool in tools]) format_instructions = format_instructions.format(tool_names=tool_names) From 81f0bdd50b0fe236addd88f13fe40d295095f618 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 18:12:45 -0300 Subject: [PATCH 37/40] Add DirectoryComponent and related utility functions --- src/backend/langflow/base/data/__init__.py | 0 src/backend/langflow/base/data/utils.py | 89 ++++++++++ .../langflow/components/data/Directory.py | 76 +++++++++ .../langflow/components/data/GatherRecords.py | 161 ------------------ 4 files changed, 165 insertions(+), 161 deletions(-) create mode 100644 src/backend/langflow/base/data/__init__.py create mode 100644 src/backend/langflow/base/data/utils.py create mode 100644 src/backend/langflow/components/data/Directory.py delete mode 100644 src/backend/langflow/components/data/GatherRecords.py diff --git a/src/backend/langflow/base/data/__init__.py b/src/backend/langflow/base/data/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/langflow/base/data/utils.py b/src/backend/langflow/base/data/utils.py new file mode 100644 index 000000000..2219310ef --- /dev/null +++ b/src/backend/langflow/base/data/utils.py @@ -0,0 +1,89 @@ +from concurrent import futures +from pathlib import Path +from typing import List, Optional, Text + +from langflow.schema.schema import Record + + +def is_hidden(path: Path) -> bool: + return path.name.startswith(".") + + +def retrieve_file_paths( + path: str, + types: List[str], + load_hidden: bool, + recursive: bool, + depth: int, +) -> List[str]: + path_obj = Path(path) + if not path_obj.exists() or not path_obj.is_dir(): + raise ValueError(f"Path {path} must exist and be a directory.") + + def match_types(p: Path) -> bool: + return any(p.suffix == f".{t}" for t in types) if types else True + + def is_not_hidden(p: Path) -> bool: + return not is_hidden(p) or load_hidden + + def walk_level(directory: Path, max_depth: int): + directory = directory.resolve() + prefix_length = len(directory.parts) + for p in directory.rglob("*" if recursive else "[!.]*"): + if len(p.parts) - prefix_length <= max_depth: + yield p + + glob = "**/*" if recursive else "*" + paths = walk_level(path_obj, depth) if depth else path_obj.glob(glob) + file_paths = [ + Text(p) for p in paths if p.is_file() and match_types(p) and is_not_hidden(p) + ] + + return file_paths + + +def parse_file_to_record(file_path: str, silent_errors: bool) -> Optional[Record]: + # Use the partition function to load the file + from unstructured.partition.auto import partition # type: ignore + + try: + elements = partition(file_path) + except Exception as e: + if not silent_errors: + raise ValueError(f"Error loading file {file_path}: {e}") from e + return None + + # Create a Record + text = "\n\n".join([Text(el) for el in elements]) + metadata = elements.metadata if hasattr(elements, "metadata") else {} + metadata["file_path"] = file_path + record = Record(text=text, data=metadata) + return record + + +def get_elements( + file_paths: List[str], + silent_errors: bool, + max_concurrency: int, + use_multithreading: bool, +) -> List[Optional[Record]]: + if use_multithreading: + records = parallel_load_records(file_paths, silent_errors, max_concurrency) + else: + records = [ + parse_file_to_record(file_path, silent_errors) for file_path in file_paths + ] + records = list(filter(None, records)) + return records + + +def parallel_load_records( + file_paths: List[str], silent_errors: bool, max_concurrency: int +) -> List[Optional[Record]]: + with futures.ThreadPoolExecutor(max_workers=max_concurrency) as executor: + loaded_files = executor.map( + lambda file_path: parse_file_to_record(file_path, silent_errors), + file_paths, + ) + # loaded_files is an iterator, so we need to convert it to a list + return list(loaded_files) diff --git a/src/backend/langflow/components/data/Directory.py b/src/backend/langflow/components/data/Directory.py new file mode 100644 index 000000000..327f270cc --- /dev/null +++ b/src/backend/langflow/components/data/Directory.py @@ -0,0 +1,76 @@ +from typing import Any, Dict, List, Optional + +from langflow import CustomComponent +from langflow.base.data.utils import ( + parallel_load_records, + parse_file_to_record, + retrieve_file_paths, +) +from langflow.schema import Record + + +class DirectoryComponent(CustomComponent): + display_name = "Directory" + description = "Load files from a directory." + + def build_config(self) -> Dict[str, Any]: + return { + "path": {"display_name": "Path"}, + "types": { + "display_name": "Types", + "info": "File types to load. Leave empty to load all types.", + }, + "depth": {"display_name": "Depth", "info": "Depth to search for files."}, + "max_concurrency": {"display_name": "Max Concurrency", "advanced": True}, + "load_hidden": { + "display_name": "Load Hidden", + "advanced": True, + "info": "If true, hidden files will be loaded.", + }, + "recursive": { + "display_name": "Recursive", + "advanced": True, + "info": "If true, the search will be recursive.", + }, + "silent_errors": { + "display_name": "Silent Errors", + "advanced": True, + "info": "If true, errors will not raise an exception.", + }, + "use_multithreading": { + "display_name": "Use Multithreading", + "advanced": True, + }, + } + + def build( + self, + path: str, + types: Optional[List[str]] = None, + depth: int = 0, + max_concurrency: int = 2, + load_hidden: bool = False, + recursive: bool = True, + silent_errors: bool = False, + use_multithreading: bool = True, + ) -> List[Optional[Record]]: + if types is None: + types = [] + resolved_path = self.resolve_path(path) + file_paths = retrieve_file_paths( + resolved_path, types, load_hidden, recursive, depth + ) + loaded_records = [] + + if use_multithreading: + loaded_records = parallel_load_records( + file_paths, silent_errors, max_concurrency + ) + else: + loaded_records = [ + parse_file_to_record(file_path, silent_errors) + for file_path in file_paths + ] + loaded_records = list(filter(None, loaded_records)) + self.status = loaded_records + return loaded_records diff --git a/src/backend/langflow/components/data/GatherRecords.py b/src/backend/langflow/components/data/GatherRecords.py deleted file mode 100644 index ac298c092..000000000 --- a/src/backend/langflow/components/data/GatherRecords.py +++ /dev/null @@ -1,161 +0,0 @@ -from concurrent import futures -from pathlib import Path -from typing import Any, Dict, List, Optional, Text - -from langflow import CustomComponent -from langflow.schema import Record - - -class GatherRecordsComponent(CustomComponent): - display_name = "Gather Records" - description = "Gather records from a directory." - - def build_config(self) -> Dict[str, Any]: - return { - "path": {"display_name": "Path"}, - "types": { - "display_name": "Types", - "info": "File types to load. Leave empty to load all types.", - }, - "depth": {"display_name": "Depth", "info": "Depth to search for files."}, - "max_concurrency": {"display_name": "Max Concurrency", "advanced": True}, - "load_hidden": { - "display_name": "Load Hidden", - "advanced": True, - "info": "If true, hidden files will be loaded.", - }, - "recursive": { - "display_name": "Recursive", - "advanced": True, - "info": "If true, the search will be recursive.", - }, - "silent_errors": { - "display_name": "Silent Errors", - "advanced": True, - "info": "If true, errors will not raise an exception.", - }, - "use_multithreading": { - "display_name": "Use Multithreading", - "advanced": True, - }, - } - - def is_hidden(self, path: Path) -> bool: - return path.name.startswith(".") - - def retrieve_file_paths( - self, - path: str, - types: List[str], - load_hidden: bool, - recursive: bool, - depth: int, - ) -> List[str]: - path_obj = Path(path) - if not path_obj.exists() or not path_obj.is_dir(): - raise ValueError(f"Path {path} must exist and be a directory.") - - def match_types(p: Path) -> bool: - return any(p.suffix == f".{t}" for t in types) if types else True - - def is_not_hidden(p: Path) -> bool: - return not self.is_hidden(p) or load_hidden - - def walk_level(directory: Path, max_depth: int): - directory = directory.resolve() - prefix_length = len(directory.parts) - for p in directory.rglob("*" if recursive else "[!.]*"): - if len(p.parts) - prefix_length <= max_depth: - yield p - - glob = "**/*" if recursive else "*" - paths = walk_level(path_obj, depth) if depth else path_obj.glob(glob) - file_paths = [ - Text(p) - for p in paths - if p.is_file() and match_types(p) and is_not_hidden(p) - ] - - return file_paths - - def parse_file_to_record( - self, file_path: str, silent_errors: bool - ) -> Optional[Record]: - # Use the partition function to load the file - from unstructured.partition.auto import partition # type: ignore - - try: - elements = partition(file_path) - except Exception as e: - if not silent_errors: - raise ValueError(f"Error loading file {file_path}: {e}") from e - return None - - # Create a Record - text = "\n\n".join([Text(el) for el in elements]) - metadata = elements.metadata if hasattr(elements, "metadata") else {} - metadata["file_path"] = file_path - record = Record(text=text, data=metadata) - return record - - def get_elements( - self, - file_paths: List[str], - silent_errors: bool, - max_concurrency: int, - use_multithreading: bool, - ) -> List[Optional[Record]]: - if use_multithreading: - records = self.parallel_load_records( - file_paths, silent_errors, max_concurrency - ) - else: - records = [ - self.parse_file_to_record(file_path, silent_errors) - for file_path in file_paths - ] - records = list(filter(None, records)) - return records - - def parallel_load_records( - self, file_paths: List[str], silent_errors: bool, max_concurrency: int - ) -> List[Optional[Record]]: - with futures.ThreadPoolExecutor(max_workers=max_concurrency) as executor: - loaded_files = executor.map( - lambda file_path: self.parse_file_to_record(file_path, silent_errors), - file_paths, - ) - # loaded_files is an iterator, so we need to convert it to a list - return list(loaded_files) - - def build( - self, - path: str, - types: Optional[List[str]] = None, - depth: int = 0, - max_concurrency: int = 2, - load_hidden: bool = False, - recursive: bool = True, - silent_errors: bool = False, - use_multithreading: bool = True, - ) -> List[Optional[Record]]: - if types is None: - types = [] - resolved_path = self.resolve_path(path) - file_paths = self.retrieve_file_paths( - resolved_path, types, load_hidden, recursive, depth - ) - loaded_records = [] - - if use_multithreading: - loaded_records = self.parallel_load_records( - file_paths, silent_errors, max_concurrency - ) - else: - loaded_records = [ - self.parse_file_to_record(file_path, silent_errors) - for file_path in file_paths - ] - loaded_records = list(filter(None, loaded_records)) - self.status = loaded_records - return loaded_records From 56af9280b516d93f5a19f594ee4813f37ad39f85 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 18:13:09 -0300 Subject: [PATCH 38/40] Add FileComponent to load a file --- src/backend/langflow/components/data/File.py | 28 ++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/backend/langflow/components/data/File.py diff --git a/src/backend/langflow/components/data/File.py b/src/backend/langflow/components/data/File.py new file mode 100644 index 000000000..dbc14abc4 --- /dev/null +++ b/src/backend/langflow/components/data/File.py @@ -0,0 +1,28 @@ +from typing import Any, Dict, Optional + +from langflow import CustomComponent +from langflow.base.data.utils import parse_file_to_record +from langflow.schema import Record + + +class FileComponent(CustomComponent): + display_name = "File" + description = "Load a file." + + def build_config(self) -> Dict[str, Any]: + return { + "path": {"display_name": "Path"}, + "silent_errors": { + "display_name": "Silent Errors", + "advanced": True, + "info": "If true, errors will not raise an exception.", + }, + } + + def build( + self, + path: str, + silent_errors: bool = False, + ) -> Optional[Record]: + resolved_path = self.resolve_path(path) + return parse_file_to_record(resolved_path, silent_errors) From 698fa6bf90da658b064e76837aa9dcf636a28c67 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 18:14:47 -0300 Subject: [PATCH 39/40] Add URLComponent to load a URL --- src/backend/langflow/components/data/URL.py | 26 +++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/backend/langflow/components/data/URL.py diff --git a/src/backend/langflow/components/data/URL.py b/src/backend/langflow/components/data/URL.py new file mode 100644 index 000000000..08eafeaa3 --- /dev/null +++ b/src/backend/langflow/components/data/URL.py @@ -0,0 +1,26 @@ +from typing import Any, Dict, Optional + +from langchain_community.document_loaders.url import UnstructuredURLLoader + +from langflow import CustomComponent +from langflow.schema import Record + + +class URLComponent(CustomComponent): + display_name = "URL" + description = "Load a URL." + + def build_config(self) -> Dict[str, Any]: + return { + "urls": {"display_name": "URL"}, + } + + async def build( + self, + urls: list[str], + ) -> Optional[Record]: + + loader = UnstructuredURLLoader(urls=urls) + docs = loader.load() + records = self.to_records(docs) + return records From 3d708493419321ebd2279f7c7fe1b72a18b3d2de Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 18:21:17 -0300 Subject: [PATCH 40/40] Update variable names and properties related to pinning to use "frozen" instead. --- src/backend/langflow/api/v1/chat.py | 4 +-- src/backend/langflow/graph/graph/base.py | 6 ++-- src/backend/langflow/graph/vertex/base.py | 6 ++-- src/backend/langflow/graph/vertex/types.py | 5 ++- .../langflow/interface/custom/attributes.py | 2 +- .../custom_component/custom_component.py | 4 +-- .../langflow/template/frontend_node/base.py | 31 ++++++++++++++----- .../template/frontend_node/memories.py | 6 ++-- .../components/parameterComponent/index.tsx | 6 ++-- .../components/PageComponent/index.tsx | 4 +-- .../components/nodeToolbarComponent/index.tsx | 6 ++-- src/frontend/src/stores/flowStore.ts | 4 +-- src/frontend/src/types/api/index.ts | 2 +- 13 files changed, 51 insertions(+), 35 deletions(-) diff --git a/src/backend/langflow/api/v1/chat.py b/src/backend/langflow/api/v1/chat.py index d71e4a324..98dd0dfef 100644 --- a/src/backend/langflow/api/v1/chat.py +++ b/src/backend/langflow/api/v1/chat.py @@ -114,7 +114,7 @@ async def build_vertex( vertex = graph.get_vertex(vertex_id) try: - if not vertex.pinned or not vertex._built: + if not vertex.frozen or not vertex._built: inputs_dict = inputs.model_dump() if inputs else {} await vertex.build(user_id=current_user.id, inputs=inputs_dict) @@ -233,7 +233,7 @@ async def build_vertex_stream( ) yield str(stream_data) - elif not vertex.pinned or not vertex._built: + elif not vertex.frozen or not vertex._built: logger.debug(f"Streaming vertex {vertex_id}") stream_data = StreamData( event="message", diff --git a/src/backend/langflow/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py index daff60b6d..b7ed754d4 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -337,9 +337,9 @@ class Graph: vertex.params = {} vertex._build_params() vertex.graph = self - # If the vertex is pinned, we don't want + # If the vertex is frozen, we don't want # to reset the results nor the _built attribute - if not vertex.pinned: + if not vertex.frozen: vertex._built = False vertex.result = None vertex.artifacts = {} @@ -352,7 +352,7 @@ class Graph: for vid in [edge.source_id, edge.target_id]: if vid in self.vertex_map: _vertex = self.vertex_map[vid] - if not _vertex.pinned: + if not _vertex.frozen: _vertex._build_params() def _add_vertex(self, vertex: Vertex) -> None: diff --git a/src/backend/langflow/graph/vertex/base.py b/src/backend/langflow/graph/vertex/base.py index 739748f7c..1df6b9637 100644 --- a/src/backend/langflow/graph/vertex/base.py +++ b/src/backend/langflow/graph/vertex/base.py @@ -193,7 +193,7 @@ class Vertex: self.base_type = state["base_type"] self.is_task = state["is_task"] self.id = state["id"] - self.pinned = state.get("pinned", False) + self.frozen = state.get("frozen", False) self._parse_data() if "_built_object" in state: self._built_object = state["_built_object"] @@ -219,7 +219,7 @@ class Vertex: self.data = self._data["data"] self.output = self.data["node"]["base_classes"] self.display_name = self.data["node"].get("display_name", self.id.split("-")[0]) - self.pinned = self.data["node"].get("pinned", False) + self.frozen = self.data["node"].get("frozen", False) self.selected_output_type = self.data["node"].get("selected_output_type") self.is_input = self.data["node"].get("is_input") or self.is_input self.is_output = self.data["node"].get("is_output") or self.is_output @@ -670,7 +670,7 @@ class Vertex: self.build_inactive() return - if self.pinned and self._built: + if self.frozen and self._built: return self.get_requester_result(requester) self._reset() diff --git a/src/backend/langflow/graph/vertex/types.py b/src/backend/langflow/graph/vertex/types.py index 1c93729f1..adbdafd6e 100644 --- a/src/backend/langflow/graph/vertex/types.py +++ b/src/backend/langflow/graph/vertex/types.py @@ -1,7 +1,6 @@ import ast import json -from typing import (AsyncIterator, Callable, Dict, Iterator, List, Optional, - Union) +from typing import AsyncIterator, Callable, Dict, Iterator, List, Optional, Union import yaml from langchain_core.messages import AIMessage @@ -224,7 +223,7 @@ class ChainVertex(StatelessVertex): if isinstance(value, PromptVertex): # Build the PromptVertex, passing the tools if available tools = kwargs.get("tools", None) - self.params[key] = value.build(tools=tools, pinned=force) + self.params[key] = value.build(tools=tools, frozen=force) await self._build(user_id=user_id) diff --git a/src/backend/langflow/interface/custom/attributes.py b/src/backend/langflow/interface/custom/attributes.py index 9b91af43c..7bcfb5f4b 100644 --- a/src/backend/langflow/interface/custom/attributes.py +++ b/src/backend/langflow/interface/custom/attributes.py @@ -37,7 +37,7 @@ ATTR_FUNC_MAPPING: dict[str, Callable] = { "beta": getattr_return_bool, "documentation": getattr_return_str, "icon": validate_icon, - "pinned": getattr_return_bool, + "frozen": getattr_return_bool, "is_input": getattr_return_bool, "is_output": getattr_return_bool, } diff --git a/src/backend/langflow/interface/custom/custom_component/custom_component.py b/src/backend/langflow/interface/custom/custom_component/custom_component.py index d2087dd2e..a7eaa2c05 100644 --- a/src/backend/langflow/interface/custom/custom_component/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component/custom_component.py @@ -59,8 +59,8 @@ class CustomComponent(Component): """The field configuration of the component. Defaults to an empty dictionary.""" field_order: Optional[List[str]] = None """The field order of the component. Defaults to an empty list.""" - pinned: Optional[bool] = False - """The default pinned state of the component. Defaults to False.""" + frozen: Optional[bool] = False + """The default frozen state of the component. Defaults to False.""" build_parameters: Optional[dict] = None """The build parameters of the component. Defaults to None.""" selected_output_type: Optional[str] = None diff --git a/src/backend/langflow/template/frontend_node/base.py b/src/backend/langflow/template/frontend_node/base.py index 2a19ec9c9..bcbbb36c1 100644 --- a/src/backend/langflow/template/frontend_node/base.py +++ b/src/backend/langflow/template/frontend_node/base.py @@ -71,8 +71,8 @@ class FrontendNode(BaseModel): """Full path of the frontend node.""" field_formatters: FieldFormatters = Field(default_factory=FieldFormatters) """Field formatters for the frontend node.""" - pinned: bool = False - """Whether the frontend node is pinned.""" + frozen: bool = False + """Whether the frontend node is frozen.""" beta: bool = False error: Optional[str] = None @@ -171,7 +171,9 @@ class FrontendNode(BaseModel): return _type @staticmethod - def handle_special_field(field, key: str, _type: str, SPECIAL_FIELD_HANDLERS) -> str: + def handle_special_field( + field, key: str, _type: str, SPECIAL_FIELD_HANDLERS + ) -> str: """Handles special field by using the respective handler if present.""" handler = SPECIAL_FIELD_HANDLERS.get(key) return handler(field) if handler else _type @@ -182,7 +184,11 @@ class FrontendNode(BaseModel): if "dict" in _type.lower() and field.name == "dict_": field.field_type = "file" field.file_types = [".json", ".yaml", ".yml"] - elif _type.startswith("Dict") or _type.startswith("Mapping") or _type.startswith("dict"): + elif ( + _type.startswith("Dict") + or _type.startswith("Mapping") + or _type.startswith("dict") + ): field.field_type = "dict" return _type @@ -193,7 +199,9 @@ class FrontendNode(BaseModel): field.value = value["default"] @staticmethod - def handle_specific_field_values(field: TemplateField, key: str, name: Optional[str] = None) -> None: + def handle_specific_field_values( + field: TemplateField, key: str, name: Optional[str] = None + ) -> None: """Handles specific field values for certain fields.""" if key == "headers": field.value = """{"Authorization": "Bearer "}""" @@ -201,7 +209,9 @@ class FrontendNode(BaseModel): FrontendNode._handle_api_key_specific_field_values(field, key, name) @staticmethod - def _handle_model_specific_field_values(field: TemplateField, key: str, name: Optional[str] = None) -> None: + def _handle_model_specific_field_values( + field: TemplateField, key: str, name: Optional[str] = None + ) -> None: """Handles specific field values related to models.""" model_dict = { "OpenAI": constants.OPENAI_MODELS, @@ -214,7 +224,9 @@ class FrontendNode(BaseModel): field.is_list = True @staticmethod - def _handle_api_key_specific_field_values(field: TemplateField, key: str, name: Optional[str] = None) -> None: + def _handle_api_key_specific_field_values( + field: TemplateField, key: str, name: Optional[str] = None + ) -> None: """Handles specific field values related to API keys.""" if "api_key" in key and "OpenAI" in str(name): field.display_name = "OpenAI API Key" @@ -254,7 +266,10 @@ class FrontendNode(BaseModel): @staticmethod def should_be_password(key: str, show: bool) -> bool: """Determines whether the field should be a password field.""" - return any(text in key.lower() for text in {"password", "token", "api", "key"}) and show + return ( + any(text in key.lower() for text in {"password", "token", "api", "key"}) + and show + ) @staticmethod def should_be_multiline(key: str) -> bool: diff --git a/src/backend/langflow/template/frontend_node/memories.py b/src/backend/langflow/template/frontend_node/memories.py index 588bcc2c0..f1c326810 100644 --- a/src/backend/langflow/template/frontend_node/memories.py +++ b/src/backend/langflow/template/frontend_node/memories.py @@ -15,7 +15,7 @@ from langflow.template.template.base import Template class MemoryFrontendNode(FrontendNode): - pinned: bool = True + frozen: bool = True def add_extra_fields(self) -> None: # chat history should have another way to add common field? @@ -80,7 +80,9 @@ class MemoryFrontendNode(FrontendNode): field.show = True field.advanced = False field.value = "" - field.info = INPUT_KEY_INFO if field.name == "input_key" else OUTPUT_KEY_INFO + field.info = ( + INPUT_KEY_INFO if field.name == "input_key" else OUTPUT_KEY_INFO + ) if field.name == "memory_key": field.value = "chat_history" diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 52d8448a9..83d916a9d 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -359,19 +359,19 @@ export default function ParameterComponent({ (info !== "" ? " flex items-center" : "") } > - {!left && data.node?.pinned && ( + {!left && data.node?.frozen && (
)} {proxy ? ( {proxy.id}}> - + {title} ) : ( - + {title} )} diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index 155c7bc61..d740129e4 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -109,7 +109,7 @@ export default function Page({ ...old.data, node: { ...old.data.node, - pinned: old.data?.node?.pinned ? false : true, + frozen: old.data?.node?.frozen ? false : true, }, }, })); @@ -491,4 +491,4 @@ export default function Page({
); -} \ No newline at end of file +} diff --git a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx index 64f004333..0dc36eac7 100644 --- a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx @@ -62,7 +62,7 @@ export default function NodeToolbarComponent({ const isMinimal = numberOfHandles <= 1; const isGroup = data.node?.flow ? true : false; - const pinned = data.node?.pinned ?? false; + const frozen = data.node?.frozen ?? false; const paste = useFlowStore((state) => state.paste); const nodes = useFlowStore((state) => state.nodes); const edges = useFlowStore((state) => state.edges); @@ -280,7 +280,7 @@ export default function NodeToolbarComponent({ ...old.data, node: { ...old.data.node, - pinned: old.data?.node?.pinned ? false : true, + frozen: old.data?.node?.frozen ? false : true, }, }, })); @@ -291,7 +291,7 @@ export default function NodeToolbarComponent({ className={cn( "h-4 w-4 transition-all", // TODO UPDATE THIS COLOR TO BE A VARIABLE - pinned ? "animate-wiggle text-ice" : "" + frozen ? "animate-wiggle text-ice" : "" )} /> diff --git a/src/frontend/src/stores/flowStore.ts b/src/frontend/src/stores/flowStore.ts index 5ec2ee3a2..782a01400 100644 --- a/src/frontend/src/stores/flowStore.ts +++ b/src/frontend/src/stores/flowStore.ts @@ -190,8 +190,8 @@ const useFlowStore = create((set, get) => ({ get().setNodes((oldNodes) => oldNodes.map((node) => { if (node.id === id) { - if((node.data as NodeDataType).node?.pinned){ - (newChange.data as NodeDataType).node!.pinned = false; + if ((node.data as NodeDataType).node?.frozen) { + (newChange.data as NodeDataType).node!.frozen = false; } return newChange; } diff --git a/src/frontend/src/types/api/index.ts b/src/frontend/src/types/api/index.ts index 7b33b9ffb..c277c916f 100644 --- a/src/frontend/src/types/api/index.ts +++ b/src/frontend/src/types/api/index.ts @@ -27,7 +27,7 @@ export type APIClassType = { documentation: string; error?: string; official?: boolean; - pinned?: boolean; + frozen?: boolean; flow?: FlowType; [key: string]: | Array