From 62a7dc77d6071a375a23b49cafd0dd8c56a53589 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 29 Feb 2024 23:39:36 -0300 Subject: [PATCH 001/199] Add state management methods to CustomComponent class --- .../custom_component/custom_component.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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 c3b06f90a..b8ec433f3 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,24 @@ class CustomComponent(Component): status: Optional[Any] = None """The status of the component. This is displayed on the frontend. Defaults to None.""" + def update_state(self, name: str, value: Any): + try: + self.vertex.graph.state_manager.update_state(key=name, new_state=value) + except Exception as e: + raise ValueError(f"Error updating state: {e}") + + def append_state(self, name: str, value: Any): + try: + self.vertex.graph.state_manager.append_state(key=name, new_state=value) + except Exception as e: + raise ValueError(f"Error appending state: {e}") + + def get_state(self, name: str): + try: + return self.vertex.graph.state_manager.get_state(key=name) + except Exception as e: + raise ValueError(f"Error getting state: {e}") + _tree: Optional[dict] = None def __init__(self, **data): From f3a7570aaddfad29cfbf4834250927fca0c4ebdd Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 29 Feb 2024 23:39:42 -0300 Subject: [PATCH 002/199] Add SharedState component for state sharing --- .../components/utilities/SharedState.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/backend/langflow/components/utilities/SharedState.py diff --git a/src/backend/langflow/components/utilities/SharedState.py b/src/backend/langflow/components/utilities/SharedState.py new file mode 100644 index 000000000..7d29da9bb --- /dev/null +++ b/src/backend/langflow/components/utilities/SharedState.py @@ -0,0 +1,38 @@ +from typing import Union + +from langflow import CustomComponent +from langflow.field_typing import Text +from langflow.schema import Record + + +class SharedState(CustomComponent): + display_name = "Shared State" + description = "A component to share state between components." + + def build_config(self): + return { + "name": {"display_name": "Name", "info": "The name of the state."}, + "record": {"display_name": "Record", "info": "The record to store."}, + "append": { + "display_name": "Append", + "info": "If True, the record will be appended to the state.", + }, + } + + def build( + self, name: str, record: Union[Text, Record], append: bool = False + ) -> Record: + if append: + self.append_state(name, record) + else: + self.update_state(name, record) + + state = self.get_state(name) + if not isinstance(state, Record): + if isinstance(state, str): + state = Record(text=state) + elif isinstance(state, dict): + state = Record(data=state) + else: + state = Record(text=str(state)) + return state From 1ce317c252246f9d1ede2755a4ae5ba2ce5785ae Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 1 Mar 2024 07:35:46 -0300 Subject: [PATCH 003/199] Fix duplicate edges in Graph class --- src/backend/langflow/graph/graph/base.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/backend/langflow/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py index 453076e1b..245a8a4ae 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -534,14 +534,21 @@ class Graph: # if we can't find a vertex, we raise an error edges: List[ContractEdge] = [] + edges_added = set() for edge in self._edges: source = self.get_vertex(edge["source"]) target = self.get_vertex(edge["target"]) + if source is None: raise ValueError(f"Source vertex {edge['source']} not found") if target is None: raise ValueError(f"Target vertex {edge['target']} not found") + + if (source.id, target.id) in edges_added: + continue + edges.append(ContractEdge(source, target, edge)) + edges_added.add((source.id, target.id)) return edges def _get_vertex_class( From 74b723c4da2c748ae251bc87db5a879d494a3699 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 1 Mar 2024 17:13:20 -0300 Subject: [PATCH 004/199] Refactor variable names and fix typo in code --- src/backend/langflow/api/v1/chat.py | 10 +++++----- src/backend/langflow/api/v1/schemas.py | 6 ++++-- src/backend/langflow/graph/graph/base.py | 8 ++++---- src/backend/langflow/graph/vertex/base.py | 6 +++--- src/frontend/src/stores/flowStore.ts | 16 ++++++++++++---- src/frontend/src/types/api/index.ts | 2 +- src/frontend/src/utils/buildUtils.ts | 16 +++++++--------- 7 files changed, 36 insertions(+), 28 deletions(-) diff --git a/src/backend/langflow/api/v1/chat.py b/src/backend/langflow/api/v1/chat.py index 258277bca..7126c19e3 100644 --- a/src/backend/langflow/api/v1/chat.py +++ b/src/backend/langflow/api/v1/chat.py @@ -153,14 +153,14 @@ async def build_vertex( result_data_response.duration = duration result_data_response.timedelta = timedelta vertex.add_build_time(timedelta) - inactive_vertices = None - if graph.inactive_vertices: - inactive_vertices = list(graph.inactive_vertices) - graph.reset_inactive_vertices() + inactivated_vertices = None + if graph.inactivated_vertices: + inactivated_vertices = list(graph.inactivated_vertices) + graph.reset_inactivated_vertices() chat_service.set_cache(flow_id, graph) build_response = VertexBuildResponse( - inactive_vertices=inactive_vertices, + inactivated_vertices=inactivated_vertices, valid=valid, params=params, id=vertex.id, diff --git a/src/backend/langflow/api/v1/schemas.py b/src/backend/langflow/api/v1/schemas.py index 80d40007b..044d98507 100644 --- a/src/backend/langflow/api/v1/schemas.py +++ b/src/backend/langflow/api/v1/schemas.py @@ -158,7 +158,9 @@ class StreamData(BaseModel): data: dict def __str__(self) -> str: - return f"event: {self.event}\ndata: {orjson_dumps(self.data, indent_2=False)}\n\n" + return ( + f"event: {self.event}\ndata: {orjson_dumps(self.data, indent_2=False)}\n\n" + ) class CustomComponentCode(BaseModel): @@ -227,7 +229,7 @@ class ResultDataResponse(BaseModel): class VertexBuildResponse(BaseModel): id: Optional[str] = None - inactive_vertices: Optional[List[str]] = None + inactivated_vertices: Optional[List[str]] = None valid: bool params: Optional[str] """JSON string of the params.""" diff --git a/src/backend/langflow/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py index 245a8a4ae..0b5af21f6 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -54,7 +54,7 @@ class Graph: self._vertices = self._graph_data["nodes"] self._edges = self._graph_data["edges"] - self.inactive_vertices: set = set() + self.inactivated_vertices: set = set() self.edges: List[ContractEdge] = [] self.vertices: List[Vertex] = [] self._build_graph() @@ -137,7 +137,7 @@ class Graph: return { "runs": self._runs, "updates": self._updates, - "inactive_vertices": self.inactive_vertices, + "inactivated_vertices": self.inactivated_vertices, } def build_graph_maps(self): @@ -145,8 +145,8 @@ class Graph: self.in_degree_map = self.build_in_degree() self.parent_child_map = self.build_parent_child_map() - def reset_inactive_vertices(self): - self.inactive_vertices = set() + def reset_inactivated_vertices(self): + self.inactivated_vertices = set() def mark_all_vertices(self, state: str): """Marks all vertices in the graph.""" diff --git a/src/backend/langflow/graph/vertex/base.py b/src/backend/langflow/graph/vertex/base.py index 6425f5e08..a550b5b0f 100644 --- a/src/backend/langflow/graph/vertex/base.py +++ b/src/backend/langflow/graph/vertex/base.py @@ -103,12 +103,12 @@ class Vertex: ): # If the vertex is inactive and has only one in degree # it means that it is not a merge point in the graph - self.graph.inactive_vertices.add(self.id) + self.graph.inactivated_vertices.add(self.id) elif ( self.state == VertexStates.ACTIVE - and self.id in self.graph.inactive_vertices + and self.id in self.graph.inactivated_vertices ): - self.graph.inactive_vertices.remove(self.id) + self.graph.inactivated_vertices.remove(self.id) @property def avg_build_time(self): diff --git a/src/frontend/src/stores/flowStore.ts b/src/frontend/src/stores/flowStore.ts index 1725de2a0..9ee25623b 100644 --- a/src/frontend/src/stores/flowStore.ts +++ b/src/frontend/src/stores/flowStore.ts @@ -219,8 +219,16 @@ const useFlowStore = create((set, get) => ({ ); }, paste: (selection, position) => { - if(selection.nodes.some((node) => node.data.type === "ChatInput") && checkChatInput(get().nodes)){ - useAlertStore.getState().setErrorData({title: "Error pasting components", list: ["You can only have one ChatInput component in the flow"]}); + if ( + selection.nodes.some((node) => node.data.type === "ChatInput") && + checkChatInput(get().nodes) + ) { + useAlertStore + .getState() + .setErrorData({ + title: "Error pasting components", + list: ["You can only have one ChatInput component in the flow"], + }); return; } let minimumX = Infinity; @@ -440,8 +448,8 @@ const useFlowStore = create((set, get) => ({ status: BuildStatus, buildId: string ) { - if (vertexBuildData && vertexBuildData.inactive_vertices) { - get().removeFromVerticesBuild(vertexBuildData.inactive_vertices); + if (vertexBuildData && vertexBuildData.inactivated_vertices) { + get().removeFromVerticesBuild(vertexBuildData.inactivated_vertices); } get().addDataToFlowPool( { ...vertexBuildData, buildId }, diff --git a/src/frontend/src/types/api/index.ts b/src/frontend/src/types/api/index.ts index 6663692dc..27e5946b3 100644 --- a/src/frontend/src/types/api/index.ts +++ b/src/frontend/src/types/api/index.ts @@ -140,7 +140,7 @@ export type VerticesOrderTypeAPI = { export type VertexBuildTypeAPI = { id: string; - inactive_vertices: Array | null; + inactivated_vertices: Array | null; valid: boolean; params: string; data: VertexDataTypeAPI; diff --git a/src/frontend/src/utils/buildUtils.ts b/src/frontend/src/utils/buildUtils.ts index e5003caf2..1ce675eec 100644 --- a/src/frontend/src/utils/buildUtils.ts +++ b/src/frontend/src/utils/buildUtils.ts @@ -31,7 +31,7 @@ function getInactiveVertexData(vertexId: string): VertexBuildTypeAPI { id: vertexId, data: inactiveData, params: "Inactive", - inactive_vertices: null, + inactivated_vertices: null, valid: false, timestamp: new Date().toISOString(), }; @@ -85,14 +85,12 @@ export async function updateVerticesOrder( } const verticesIds = verticesLayers.flat(); - useFlowStore - .getState() - .updateVerticesBuild({ - verticesLayers, - verticesIds, - verticesOrder, - runId, - }); + useFlowStore.getState().updateVerticesBuild({ + verticesLayers, + verticesIds, + verticesOrder, + runId, + }); resolve({ verticesLayers, verticesIds, verticesOrder, runId }); }); } From 94a00c0c5ecb3ddb3b406f7c57cf1541d1e980bc Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 1 Mar 2024 23:56:35 -0300 Subject: [PATCH 005/199] Refactor build_vertex function to reset both inactivated and activated vertices --- src/backend/langflow/api/v1/chat.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/backend/langflow/api/v1/chat.py b/src/backend/langflow/api/v1/chat.py index 7126c19e3..399098d47 100644 --- a/src/backend/langflow/api/v1/chat.py +++ b/src/backend/langflow/api/v1/chat.py @@ -154,13 +154,15 @@ async def build_vertex( result_data_response.timedelta = timedelta vertex.add_build_time(timedelta) inactivated_vertices = None - if graph.inactivated_vertices: - inactivated_vertices = list(graph.inactivated_vertices) - graph.reset_inactivated_vertices() + inactivated_vertices = list(graph.inactivated_vertices) + graph.reset_inactivated_vertices() + activated_vertices = list(graph.activated_vertices) + graph.reset_activated_vertices() chat_service.set_cache(flow_id, graph) build_response = VertexBuildResponse( inactivated_vertices=inactivated_vertices, + activated_vertices=activated_vertices, valid=valid, params=params, id=vertex.id, From 3d4ab248588712240ce6653afbf0607623bc3f6a Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 1 Mar 2024 23:57:01 -0300 Subject: [PATCH 006/199] Add activated_vertices to VertexBuildResponse and update state management in Graph --- src/backend/langflow/api/v1/schemas.py | 1 + src/backend/langflow/graph/graph/base.py | 50 ++++++++++++ src/backend/langflow/graph/vertex/base.py | 26 +++--- src/backend/langflow/graph/vertex/types.py | 92 +++++++++++++++------- 4 files changed, 125 insertions(+), 44 deletions(-) diff --git a/src/backend/langflow/api/v1/schemas.py b/src/backend/langflow/api/v1/schemas.py index 044d98507..88610d637 100644 --- a/src/backend/langflow/api/v1/schemas.py +++ b/src/backend/langflow/api/v1/schemas.py @@ -230,6 +230,7 @@ class ResultDataResponse(BaseModel): class VertexBuildResponse(BaseModel): id: Optional[str] = None inactivated_vertices: Optional[List[str]] = None + activated_vertices: Optional[List[str]] = None valid: bool params: Optional[str] """JSON string of the params.""" diff --git a/src/backend/langflow/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py index 0b5af21f6..558fac152 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -16,9 +16,11 @@ from langflow.graph.vertex.types import ( FileToolVertex, LLMVertex, RoutingVertex, + StateVertex, ToolkitVertex, ) from langflow.interface.tools.constants import FILE_TOOLS +from langflow.schema import Record from langflow.utils import payload if TYPE_CHECKING: @@ -55,6 +57,7 @@ class Graph: self._vertices = self._graph_data["nodes"] self._edges = self._graph_data["edges"] self.inactivated_vertices: set = set() + self.activated_vertices: set = set() self.edges: List[ContractEdge] = [] self.vertices: List[Vertex] = [] self._build_graph() @@ -62,6 +65,37 @@ class Graph: self.define_vertices_lists() self.state_manager = GraphStateManager() + def update_state( + self, name: str, record: Union[str, Record], caller: Optional[str] = None + ) -> None: + """Updates the state of the graph.""" + if caller: + # If there is a caller which is a vertex_id, I want to activate + # all StateVertex in self.vertices that are not the caller + # essentially notifying all the other vertices that the state has changed + # This also has to activate their successors + caller_vertex = self.get_vertex(caller) + for vertex in self.vertices: + if vertex.id != caller and isinstance(vertex, StateVertex): + successors = self.get_all_successors(vertex) + self.activated_vertices.add(vertex.id) + for successor in successors: + self.activated_vertices.add(successor.id) + + self.state_manager.update_state(name, record) + + def reset_activated_vertices(self): + self.activated_vertices = set() + + def append_state( + self, name: str, record: Union[str, Record], caller: Optional[str] = None + ) -> None: + """Appends the state of the graph.""" + if caller: + self.state_manager.subscribe(name, caller) + + self.state_manager.append_state(name, record) + def set_run_id(self, run_id: str): for vertex in self.vertices: self.state_manager.subscribe(run_id, vertex.update_graph_state) @@ -500,6 +534,20 @@ class Graph: for source_id in self.predecessor_map.get(vertex.id, []) ] + def get_all_successors(self, vertex, recursive=True): + # Recursively get the successors of the current vertex + successors = vertex.successors + if not successors: + return [] + successors_result = [] + for successor in successors: + # Just return a list of successors + if recursive: + next_successors = self.get_all_successors(successor) + successors_result.extend(next_successors) + successors_result.append(successor) + return successors_result + def get_successors(self, vertex): """Returns the successors of a vertex.""" return [ @@ -561,6 +609,8 @@ class Graph: return ChatVertex elif node_name in ["ShouldRunNext"]: return RoutingVertex + elif node_name in ["SharedState"]: + return StateVertex elif node_base_type in lazy_load_vertex_dict.VERTEX_TYPE_MAP: return lazy_load_vertex_dict.VERTEX_TYPE_MAP[node_base_type] elif node_name in lazy_load_vertex_dict.VERTEX_TYPE_MAP: diff --git a/src/backend/langflow/graph/vertex/base.py b/src/backend/langflow/graph/vertex/base.py index a550b5b0f..406652388 100644 --- a/src/backend/langflow/graph/vertex/base.py +++ b/src/backend/langflow/graph/vertex/base.py @@ -88,12 +88,9 @@ class Vertex: def update_graph_state(self, key, new_state, append: bool): if append: - if key in self.graph_state: - self.graph_state[key].append(new_state) - else: - self.graph_state[key] = [new_state] + self.graph.append_state(key, new_state, caller=self.id) else: - self.graph_state[key] = new_state + self.graph.update_state(key, new_state, caller=self.id) def set_state(self, state: str): self.state = VertexStates[state] @@ -511,7 +508,16 @@ class Vertex: self.params[key] = [] self.params[key].extend(built) else: - self.params[key].append(built) + try: + if self.params[key] == built: + continue + + self.params[key].append(built) + except AttributeError as e: + logger.exception(e) + raise ValueError( + f"Params {key} ({self.params[key]}) is not a list and cannot be extended with {built}" + ) from e def _handle_func(self, key, result): """ @@ -670,11 +676,3 @@ class Vertex: if self._built_object is not None else "Failed to build 😵‍💫" ) - - -class StatefulVertex(Vertex): - pass - - -class StatelessVertex(Vertex): - pass diff --git a/src/backend/langflow/graph/vertex/types.py b/src/backend/langflow/graph/vertex/types.py index c4f33df40..ba1cd2998 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 @@ -8,14 +9,14 @@ from loguru import logger from langflow.graph.schema import INPUT_FIELD_NAME from langflow.graph.utils import UnbuiltObject, flatten_list, serialize_field -from langflow.graph.vertex.base import StatefulVertex, StatelessVertex +from langflow.graph.vertex.base import Vertex from langflow.interface.utils import extract_input_variables_from_prompt from langflow.schema import Record from langflow.services.monitor.utils import log_vertex_build from langflow.utils.schemas import ChatOutputResponse -class AgentVertex(StatelessVertex): +class AgentVertex(Vertex): def __init__(self, data: Dict, graph, params: Optional[Dict] = None): super().__init__(data, graph=graph, base_type="agents", params=params) @@ -58,12 +59,12 @@ class AgentVertex(StatelessVertex): await self._build(user_id=user_id) -class ToolVertex(StatelessVertex): +class ToolVertex(Vertex): def __init__(self, data: Dict, graph, params: Optional[Dict] = None): super().__init__(data, graph=graph, base_type="tools", params=params) -class LLMVertex(StatelessVertex): +class LLMVertex(Vertex): built_node_type = None class_built_object = None @@ -86,7 +87,7 @@ class LLMVertex(StatelessVertex): self.class_built_object = self._built_object -class ToolkitVertex(StatelessVertex): +class ToolkitVertex(Vertex): def __init__(self, data: Dict, graph, params=None): super().__init__(data, graph=graph, base_type="toolkits", params=params) @@ -100,7 +101,7 @@ class FileToolVertex(ToolVertex): ) -class WrapperVertex(StatelessVertex): +class WrapperVertex(Vertex): def __init__(self, data: Dict, graph, params=None): super().__init__(data, graph=graph, base_type="wrappers") self.steps: List[Callable] = [self._custom_build] @@ -114,7 +115,7 @@ class WrapperVertex(StatelessVertex): await self._build(user_id=user_id) -class DocumentLoaderVertex(StatefulVertex): +class DocumentLoaderVertex(Vertex): def __init__(self, data: Dict, graph, params: Optional[Dict] = None): super().__init__(data, graph=graph, base_type="documentloaders", params=params) @@ -123,21 +124,23 @@ class DocumentLoaderVertex(StatefulVertex): # show how many documents are in the list? 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( - self._built_object - ) + avg_length = sum( + len(doc.page_content) + for doc in self._built_object + if hasattr(doc, "page_content") + ) / 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.vertex_type}()" -class EmbeddingVertex(StatefulVertex): +class EmbeddingVertex(Vertex): def __init__(self, data: Dict, graph, params: Optional[Dict] = None): super().__init__(data, graph=graph, base_type="embeddings", params=params) -class VectorStoreVertex(StatefulVertex): +class VectorStoreVertex(Vertex): def __init__(self, data: Dict, graph, params=None): super().__init__(data, graph=graph, base_type="vectorstores") @@ -179,17 +182,17 @@ class VectorStoreVertex(StatefulVertex): self.remove_docs_and_texts_from_params() -class MemoryVertex(StatefulVertex): +class MemoryVertex(Vertex): def __init__(self, data: Dict, graph): super().__init__(data, graph=graph, base_type="memory") -class RetrieverVertex(StatefulVertex): +class RetrieverVertex(Vertex): def __init__(self, data: Dict, graph): super().__init__(data, graph=graph, base_type="retrievers") -class TextSplitterVertex(StatefulVertex): +class TextSplitterVertex(Vertex): def __init__(self, data: Dict, graph, params: Optional[Dict] = None): super().__init__(data, graph=graph, base_type="textsplitters", params=params) @@ -198,14 +201,16 @@ class TextSplitterVertex(StatefulVertex): # show how many documents are in the list? if not isinstance(self._built_object, UnbuiltObject): - avg_length = sum(len(doc.page_content) for doc in self._built_object) / len(self._built_object) + avg_length = sum(len(doc.page_content) for doc in self._built_object) / len( + self._built_object + ) return f"""{self.vertex_type}({len(self._built_object)} documents) \nAvg. Document Length (characters): {int(avg_length)} \nDocuments: {self._built_object[:3]}...""" return f"{self.vertex_type}()" -class ChainVertex(StatelessVertex): +class ChainVertex(Vertex): def __init__(self, data: Dict, graph): super().__init__(data, graph=graph, base_type="chains") self.steps = [self._custom_build] @@ -235,7 +240,7 @@ class ChainVertex(StatelessVertex): return super()._built_object_repr() -class PromptVertex(StatelessVertex): +class PromptVertex(Vertex): def __init__(self, data: Dict, graph): super().__init__(data, graph=graph, base_type="prompts") self.steps: List[Callable] = [self._custom_build] @@ -245,18 +250,27 @@ class PromptVertex(StatelessVertex): user_id = kwargs.get("user_id", None) tools = kwargs.get("tools", []) if not self._built or force: - if "input_variables" not in self.params or self.params["input_variables"] is None: + if ( + "input_variables" not in self.params + or self.params["input_variables"] is None + ): self.params["input_variables"] = [] # Check if it is a ZeroShotPrompt and needs a tool if "ShotPrompt" in self.vertex_type: - tools = [tool_node.build(user_id=user_id) for tool_node in tools] if tools is not None else [] + tools = ( + [tool_node.build(user_id=user_id) for tool_node in tools] + if tools is not None + else [] + ) # flatten the list of tools if it is a list of lists # first check if it is a list if tools and isinstance(tools, list) and isinstance(tools[0], list): tools = flatten_list(tools) self.params["tools"] = tools prompt_params = [ - key for key, value in self.params.items() if isinstance(value, str) and key != "format_instructions" + key + for key, value in self.params.items() + if isinstance(value, str) and key != "format_instructions" ] else: prompt_params = ["template"] @@ -266,14 +280,20 @@ class PromptVertex(StatelessVertex): prompt_text = self.params[param] variables = extract_input_variables_from_prompt(prompt_text) self.params["input_variables"].extend(variables) - self.params["input_variables"] = list(set(self.params["input_variables"])) + self.params["input_variables"] = list( + set(self.params["input_variables"]) + ) elif isinstance(self.params, dict): self.params.pop("input_variables", None) await self._build(user_id=user_id) def _built_object_repr(self): - if not self.artifacts or self._built_object is None or not hasattr(self._built_object, "format"): + if ( + not self.artifacts + or self._built_object is None + or not hasattr(self._built_object, "format") + ): return super()._built_object_repr() elif isinstance(self._built_object, UnbuiltObject): return super()._built_object_repr() @@ -285,7 +305,9 @@ class PromptVertex(StatelessVertex): # so the prompt format doesn't break artifacts.pop("handle_keys", None) try: - if not hasattr(self._built_object, "template") and hasattr(self._built_object, "prompt"): + if not hasattr(self._built_object, "template") and hasattr( + self._built_object, "prompt" + ): template = self._built_object.prompt.template else: template = self._built_object.template @@ -293,17 +315,21 @@ class PromptVertex(StatelessVertex): if value: replace_key = "{" + key + "}" template = template.replace(replace_key, value) - return template if isinstance(template, str) else f"{self.vertex_type}({template})" + return ( + template + if isinstance(template, str) + else f"{self.vertex_type}({template})" + ) except KeyError: return str(self._built_object) -class OutputParserVertex(StatelessVertex): +class OutputParserVertex(Vertex): def __init__(self, data: Dict, graph): super().__init__(data, graph=graph, base_type="output_parsers") -class CustomComponentVertex(StatelessVertex): +class CustomComponentVertex(Vertex): def __init__(self, data: Dict, graph): super().__init__(data, graph=graph, base_type="custom_components") @@ -312,7 +338,7 @@ class CustomComponentVertex(StatelessVertex): return self.artifacts["repr"] or super()._built_object_repr() -class ChatVertex(StatelessVertex): +class ChatVertex(Vertex): def __init__(self, data: Dict, graph): super().__init__(data, graph=graph, base_type="custom_components", is_task=True) self.steps = [self._build, self._run] @@ -431,7 +457,7 @@ class ChatVertex(StatelessVertex): pass -class RoutingVertex(StatelessVertex): +class RoutingVertex(Vertex): def __init__(self, data: Dict, graph): super().__init__(data, graph=graph, base_type="custom_components") self.use_result = True @@ -457,6 +483,12 @@ class RoutingVertex(StatelessVertex): self._built_result = None +class StateVertex(Vertex): + def __init__(self, data: Dict, graph): + super().__init__(data, graph=graph, base_type="custom_components") + self.steps = [self._build] + + def dict_to_codeblock(d: dict) -> str: serialized = {key: serialize_field(val) for key, val in d.items()} json_str = json.dumps(serialized, indent=4) From 88b2ab328ff87f1ffde993aa2bbdd0a5f30ae3a8 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 1 Mar 2024 23:57:28 -0300 Subject: [PATCH 007/199] Update state and append state methods in SharedState and CustomComponent --- src/backend/langflow/components/utilities/SharedState.py | 2 +- .../interface/custom/custom_component/custom_component.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/backend/langflow/components/utilities/SharedState.py b/src/backend/langflow/components/utilities/SharedState.py index 7d29da9bb..845252dab 100644 --- a/src/backend/langflow/components/utilities/SharedState.py +++ b/src/backend/langflow/components/utilities/SharedState.py @@ -28,7 +28,7 @@ class SharedState(CustomComponent): self.update_state(name, record) state = self.get_state(name) - if not isinstance(state, Record): + if state and not isinstance(state, Record): if isinstance(state, str): state = Record(text=state) elif isinstance(state, dict): 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 b8ec433f3..fd8cd7cd2 100644 --- a/src/backend/langflow/interface/custom/custom_component/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component/custom_component.py @@ -76,13 +76,17 @@ class CustomComponent(Component): def update_state(self, name: str, value: Any): try: - self.vertex.graph.state_manager.update_state(key=name, new_state=value) + self.vertex.graph.update_state( + name=name, record=value, caller=self.vertex.id + ) except Exception as e: raise ValueError(f"Error updating state: {e}") def append_state(self, name: str, value: Any): try: - self.vertex.graph.state_manager.append_state(key=name, new_state=value) + self.vertex.graph.append_state( + key=name, record=value, caller=self.vertex.id + ) except Exception as e: raise ValueError(f"Error appending state: {e}") From 9dac7e7635b25f4de3c1ff64932e7634fd5d76d3 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 1 Mar 2024 23:57:40 -0300 Subject: [PATCH 008/199] Update vertex class in socket utils.py --- src/backend/langflow/services/socket/utils.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/backend/langflow/services/socket/utils.py b/src/backend/langflow/services/socket/utils.py index 4176d081c..a45b85cd6 100644 --- a/src/backend/langflow/services/socket/utils.py +++ b/src/backend/langflow/services/socket/utils.py @@ -7,7 +7,7 @@ from sqlmodel import select from langflow.api.utils import format_elapsed_time from langflow.api.v1.schemas import ResultDataResponse, VertexBuildResponse from langflow.graph.graph.base import Graph -from langflow.graph.vertex.base import StatelessVertex +from langflow.graph.vertex.base import Vertex from langflow.services.database.models.flow.model import Flow from langflow.services.deps import get_session from langflow.services.monitor.utils import log_vertex_build @@ -63,7 +63,7 @@ async def build_vertex( return start_time = time.perf_counter() try: - if isinstance(vertex, StatelessVertex) or not vertex._built: + if isinstance(vertex, Vertex) or not vertex._built: await vertex.build(user_id=None, session_id=sid) params = vertex._built_object_repr() valid = True @@ -96,7 +96,9 @@ async def build_vertex( ) # Emit the vertex build response - response = VertexBuildResponse(valid=valid, params=params, id=vertex.id, data=result_dict) + response = VertexBuildResponse( + valid=valid, params=params, id=vertex.id, data=result_dict + ) await sio.emit("vertex_build", data=response.model_dump(), to=sid) except Exception as exc: From 569be9e0802c14852cde8877d8c26a87137bff15 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 1 Mar 2024 23:57:59 -0300 Subject: [PATCH 009/199] Refactor buildUtils to handle activated vertices --- src/frontend/src/stores/flowStore.ts | 23 ++++++-- src/frontend/src/types/api/index.ts | 1 + src/frontend/src/types/zustand/flow/index.ts | 1 + src/frontend/src/utils/buildUtils.ts | 62 ++++++++++++++++++-- 4 files changed, 75 insertions(+), 12 deletions(-) diff --git a/src/frontend/src/stores/flowStore.ts b/src/frontend/src/stores/flowStore.ts index 9ee25623b..fb995273b 100644 --- a/src/frontend/src/stores/flowStore.ts +++ b/src/frontend/src/stores/flowStore.ts @@ -223,12 +223,10 @@ const useFlowStore = create((set, get) => ({ selection.nodes.some((node) => node.data.type === "ChatInput") && checkChatInput(get().nodes) ) { - useAlertStore - .getState() - .setErrorData({ - title: "Error pasting components", - list: ["You can only have one ChatInput component in the flow"], - }); + useAlertStore.getState().setErrorData({ + title: "Error pasting components", + list: ["You can only have one ChatInput component in the flow"], + }); return; } let minimumX = Infinity; @@ -451,6 +449,9 @@ const useFlowStore = create((set, get) => ({ if (vertexBuildData && vertexBuildData.inactivated_vertices) { get().removeFromVerticesBuild(vertexBuildData.inactivated_vertices); } + if (vertexBuildData && vertexBuildData.activated_vertices) { + get().addToVerticesBuild(vertexBuildData.activated_vertices); + } get().addDataToFlowPool( { ...vertexBuildData, buildId }, vertexBuildData.id @@ -507,6 +508,16 @@ const useFlowStore = create((set, get) => ({ set({ verticesBuild: vertices }); }, verticesBuild: null, + addToVerticesBuild: (vertices: string[]) => { + const verticesBuild = get().verticesBuild; + if (!verticesBuild) return; + set({ + verticesBuild: { + ...verticesBuild, + verticesIds: [...verticesBuild.verticesIds, ...vertices], + }, + }); + }, removeFromVerticesBuild: (vertices: string[]) => { const verticesBuild = get().verticesBuild; if (!verticesBuild) return; diff --git a/src/frontend/src/types/api/index.ts b/src/frontend/src/types/api/index.ts index 27e5946b3..076ecbf45 100644 --- a/src/frontend/src/types/api/index.ts +++ b/src/frontend/src/types/api/index.ts @@ -141,6 +141,7 @@ export type VerticesOrderTypeAPI = { export type VertexBuildTypeAPI = { id: string; inactivated_vertices: Array | null; + activated_vertices: Array | null; valid: boolean; params: string; data: VertexDataTypeAPI; diff --git a/src/frontend/src/types/zustand/flow/index.ts b/src/frontend/src/types/zustand/flow/index.ts index c969d8d6b..5b19cee88 100644 --- a/src/frontend/src/types/zustand/flow/index.ts +++ b/src/frontend/src/types/zustand/flow/index.ts @@ -105,6 +105,7 @@ export type FlowStoreType = { runId: string; } | null ) => void; + addToVerticesBuild: (vertices: string[]) => void; removeFromVerticesBuild: (vertices: string[]) => void; verticesBuild: { verticesIds: string[]; diff --git a/src/frontend/src/utils/buildUtils.ts b/src/frontend/src/utils/buildUtils.ts index 1ce675eec..1e09d5398 100644 --- a/src/frontend/src/utils/buildUtils.ts +++ b/src/frontend/src/utils/buildUtils.ts @@ -32,6 +32,7 @@ function getInactiveVertexData(vertexId: string): VertexBuildTypeAPI { data: inactiveData, params: "Inactive", inactivated_vertices: null, + activated_vertices: null, valid: false, timestamp: new Date().toISOString(), }; @@ -110,7 +111,7 @@ export async function buildVertices({ if (!verticesBuild || nodeId) { verticesBuild = await updateVerticesOrder(flowId, nodeId); } - const verticesIds = verticesBuild?.verticesIds!; + let verticesIds = verticesBuild?.verticesIds!; const verticesLayers = verticesBuild?.verticesLayers!; const verticesOrder = verticesBuild?.verticesOrder!; const runId = verticesBuild?.runId!; @@ -128,26 +129,75 @@ export async function buildVertices({ useFlowStore.getState().updateBuildStatus(verticesIds, BuildStatus.TO_BUILD); useFlowStore.getState().setIsBuilding(true); + let dynamicVerticesLayers: Array> = [...verticesLayers]; + + const handleBuildUpdate = (data: VertexBuildTypeAPI, status: BuildStatus) => { + // Handle activated vertices + console.log("handleBuildUpdate", data, status); + if (data.activated_vertices && data.activated_vertices.length > 0) { + // Logic to determine the correct placement for activated vertices in dynamicVerticesLayers + // For simplicity, this example adds them to the next layer + // const nextLayerIndex = i + 1; i doesnt exist in this scope + // we don't want to add the activated vertices to the last layer + // because these vertices should be built right away + const thisVertexLayer = dynamicVerticesLayers.findIndex((layer) => + layer.includes(data.id) + ); + const nextLayerIndex = thisVertexLayer + 1; + console.log("nextLayerIndex", nextLayerIndex); + console.log("dynamicVerticesLayers", dynamicVerticesLayers); + if (dynamicVerticesLayers[nextLayerIndex]) { + // If the next layer exists, add the activated vertices to it + // dynamicVerticesLayers[nextLayerIndex] = dynamicVerticesLayers[ + // nextLayerIndex + // ].concat(data.activated_vertices); + // instead of adding them all at once, add them one by one + // add one per layer and if the next layer doesn't exist, create it + + for (const vertex of data.activated_vertices) { + console.log("vertex", vertex); + if (dynamicVerticesLayers[nextLayerIndex].includes(vertex)) { + continue; + } else if (dynamicVerticesLayers[nextLayerIndex].length > 0) { + dynamicVerticesLayers[nextLayerIndex].push(vertex); + } else { + dynamicVerticesLayers[nextLayerIndex] = [vertex]; + } + console.log("dynamicVerticesLayers", dynamicVerticesLayers); + } + } else { + dynamicVerticesLayers.push(data.activated_vertices); + console.log(dynamicVerticesLayers); + } + } + if (onBuildUpdate) onBuildUpdate(data, status, runId); + }; // Set each vertex state to building const buildResults: Array = []; - for (const layer of verticesLayers) { + for (let i = 0; i < dynamicVerticesLayers.length; i++) { + console.log(dynamicVerticesLayers); + const layer = dynamicVerticesLayers[i]; if (onBuildStart) onBuildStart(layer); for (const id of layer) { // Check if id is in the list of inactive nodes - if (!verticesIds.includes(id) && onBuildUpdate) { + // useFlowStore because it gets updated constantly + if ( + !useFlowStore.getState().verticesBuild?.verticesIds.includes(id) && + onBuildUpdate + ) { // If it is, skip building and set the state to inactive + console.log("inactive", id); onBuildUpdate(getInactiveVertexData(id), BuildStatus.INACTIVE, runId); buildResults.push(false); continue; } + await buildVertex({ flowId, id, input_value, - onBuildUpdate: (data: VertexBuildTypeAPI, status: BuildStatus) => { - if (onBuildUpdate) onBuildUpdate(data, status, runId); - }, + onBuildUpdate: handleBuildUpdate, onBuildError, verticesIds, buildResults, From 5ebcdf5d527c76437364dfd8ba00b954aa00df0b Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 2 Mar 2024 00:47:04 -0300 Subject: [PATCH 010/199] Refactor SharedState build method to accept optional record parameter --- .../langflow/components/utilities/SharedState.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/backend/langflow/components/utilities/SharedState.py b/src/backend/langflow/components/utilities/SharedState.py index 845252dab..c3f0c0634 100644 --- a/src/backend/langflow/components/utilities/SharedState.py +++ b/src/backend/langflow/components/utilities/SharedState.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Optional from langflow import CustomComponent from langflow.field_typing import Text @@ -20,12 +20,13 @@ class SharedState(CustomComponent): } def build( - self, name: str, record: Union[Text, Record], append: bool = False + self, name: str, record: Optional[Record] = None, append: bool = False ) -> Record: - if append: - self.append_state(name, record) - else: - self.update_state(name, record) + if record: + if append: + self.append_state(name, record) + else: + self.update_state(name, record) state = self.get_state(name) if state and not isinstance(state, Record): From 801c5462fc56f8d4f3f1d75f6a3bd949fa29565e Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 2 Mar 2024 00:47:15 -0300 Subject: [PATCH 011/199] Fix method argument type in CustomComponent class --- .../custom/custom_component/custom_component.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 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 fd8cd7cd2..80e28efb8 100644 --- a/src/backend/langflow/interface/custom/custom_component/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component/custom_component.py @@ -14,7 +14,6 @@ from uuid import UUID import yaml from cachetools import TTLCache, cachedmethod -from fastapi import HTTPException from langchain_core.documents import Document from sqlmodel import select @@ -85,7 +84,7 @@ class CustomComponent(Component): def append_state(self, name: str, value: Any): try: self.vertex.graph.append_state( - key=name, record=value, caller=self.vertex.id + name=name, record=value, caller=self.vertex.id ) except Exception as e: raise ValueError(f"Error appending state: {e}") @@ -201,18 +200,7 @@ class CustomComponent(Component): args = build_method["args"] for arg in args: - if arg.get("type") == "prompt": - raise HTTPException( - status_code=400, - detail={ - "error": "Type hint Error", - "traceback": ( - "Prompt type is not supported in the build method." - " Try using PromptTemplate instead." - ), - }, - ) - elif not arg.get("type") and arg.get("name") != "self": + if not arg.get("type") and arg.get("name") != "self": # Set the type to Data arg["type"] = "Data" return args From d846d806ccd6c904927967afcc92f2c53e04fe5f Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 2 Mar 2024 01:18:43 -0300 Subject: [PATCH 012/199] Add TextToRecordComponent to convert text to a Record --- .../components/utilities/TextToRecord.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/backend/langflow/components/utilities/TextToRecord.py diff --git a/src/backend/langflow/components/utilities/TextToRecord.py b/src/backend/langflow/components/utilities/TextToRecord.py new file mode 100644 index 000000000..e176e15f5 --- /dev/null +++ b/src/backend/langflow/components/utilities/TextToRecord.py @@ -0,0 +1,29 @@ +from typing import Optional + +from langflow import CustomComponent +from langflow.field_typing import Text +from langflow.schema import Record + + +class TextToRecordComponent(CustomComponent): + display_name = "Text to Record" + description = "Converts text to a Record." + + def build_config(self): + return { + "text": { + "display_name": "Text", + "info": "The text to convert to a record.", + }, + "data": { + "display_name": "Data", + "info": "The optional data to include in the record.", + }, + } + + def build( + self, + text: Text, + data: Optional[dict] = {}, + ) -> Record: + return Record(text=text, data=data) From 22884aeb7ea586648504f809394067998c087033 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 2 Mar 2024 01:18:51 -0300 Subject: [PATCH 013/199] Add state activation for specific vertices --- src/backend/langflow/graph/graph/base.py | 30 ++++++++++++++++-------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/backend/langflow/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py index d82ccfeb8..90a6e4f9a 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -3,6 +3,8 @@ from collections import defaultdict, deque from typing import TYPE_CHECKING, Dict, Generator, List, Optional, Type, Union from langchain.chains.base import Chain +from loguru import logger + from langflow.graph.edge.base import ContractEdge from langflow.graph.graph.constants import lazy_load_vertex_dict from langflow.graph.graph.state_manager import GraphStateManager @@ -20,7 +22,6 @@ from langflow.graph.vertex.types import ( from langflow.interface.tools.constants import FILE_TOOLS from langflow.schema import Record from langflow.utils import payload -from loguru import logger if TYPE_CHECKING: from langflow.graph.schema import ResultData @@ -43,6 +44,7 @@ class Graph: self.flow_id = flow_id self._is_input_vertices: List[str] = [] self._is_output_vertices: List[str] = [] + self._is_state_vertices: List[str] = [] self._has_session_id_vertices: List[str] = [] self._sorted_vertices_layers: List[List[str]] = [] self.run_id = None @@ -73,16 +75,23 @@ class Graph: # all StateVertex in self.vertices that are not the caller # essentially notifying all the other vertices that the state has changed # This also has to activate their successors - caller_vertex = self.get_vertex(caller) - for vertex in self.vertices: - if vertex.id != caller and isinstance(vertex, StateVertex): - successors = self.get_all_successors(vertex) - self.activated_vertices.add(vertex.id) - for successor in successors: - self.activated_vertices.add(successor.id) + self.activate_state_vertices(name, caller) self.state_manager.update_state(name, record) + def activate_state_vertices(self, name: str, caller: str): + for vertex_id in self._is_state_vertices: + vertex = self.get_vertex(vertex_id) + if ( + name in vertex._raw_params["name"] + and vertex_id != caller + and isinstance(vertex, StateVertex) + ): + successors = self.get_all_successors(vertex) + self.activated_vertices.add(vertex_id) + for successor in successors: + self.activated_vertices.add(successor.id) + def reset_activated_vertices(self): self.activated_vertices = set() @@ -91,7 +100,8 @@ class Graph: ) -> None: """Appends the state of the graph.""" if caller: - self.state_manager.subscribe(name, caller) + + self.activate_state_vertices(name, caller) self.state_manager.append_state(name, record) @@ -113,7 +123,7 @@ class Graph: """ Defines the lists of vertices that are inputs, outputs, and have session_id. """ - attributes = ["is_input", "is_output", "has_session_id"] + attributes = ["is_input", "is_output", "has_session_id", "is_state"] for vertex in self.vertices: for attribute in attributes: if getattr(vertex, attribute): From 2b215e66f4ccfb340125af887b0bcff0499ff86e Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 2 Mar 2024 01:18:58 -0300 Subject: [PATCH 014/199] Add is_state attribute to Vertex class and StateVertex subclass --- src/backend/langflow/graph/vertex/base.py | 1 + src/backend/langflow/graph/vertex/types.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/backend/langflow/graph/vertex/base.py b/src/backend/langflow/graph/vertex/base.py index 2f11e0ed3..097b99fbc 100644 --- a/src/backend/langflow/graph/vertex/base.py +++ b/src/backend/langflow/graph/vertex/base.py @@ -57,6 +57,7 @@ class Vertex: self.will_stream = False self.updated_raw_params = False self.id: str = data["id"] + self.is_state = False self.is_input = any( input_component_name in self.id for input_component_name in INPUT_COMPONENTS ) diff --git a/src/backend/langflow/graph/vertex/types.py b/src/backend/langflow/graph/vertex/types.py index ba1cd2998..516b06dee 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 @@ -487,6 +486,7 @@ class StateVertex(Vertex): def __init__(self, data: Dict, graph): super().__init__(data, graph=graph, base_type="custom_components") self.steps = [self._build] + self.is_state = True def dict_to_codeblock(d: dict) -> str: From 6aec697c2a23cf542314612b96c181147a810c80 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 2 Mar 2024 01:19:03 -0300 Subject: [PATCH 015/199] Add error handling for observer callbacks in GraphStateManager --- src/backend/langflow/graph/graph/state_manager.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/backend/langflow/graph/graph/state_manager.py b/src/backend/langflow/graph/graph/state_manager.py index 64476011b..46477d220 100644 --- a/src/backend/langflow/graph/graph/state_manager.py +++ b/src/backend/langflow/graph/graph/state_manager.py @@ -2,6 +2,8 @@ from collections import defaultdict from threading import Lock from typing import Callable +from loguru import logger + class GraphStateManager: def __init__(self): @@ -13,6 +15,8 @@ class GraphStateManager: with self.lock: if key not in self.states: self.states[key] = [] + elif not isinstance(self.states[key], list): + self.states[key] = [self.states[key]] self.states[key].append(new_state) self.notify_append_observers(key, new_state) @@ -36,4 +40,8 @@ class GraphStateManager: def notify_append_observers(self, key, new_state): for callback in self.observers[key]: - callback(key, new_state, append=True) + try: + callback(key, new_state, append=True) + except Exception as e: + logger.error(f"Error in observer {callback} for key {key}: {e}") + logger.warning("Callbacks not implemented yet") From d4cad8130208adef86a9e6de793eaa542a629bc8 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 2 Mar 2024 01:19:16 -0300 Subject: [PATCH 016/199] Remove unused import and update timer duration --- src/frontend/src/App.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index 22fd6c5bb..b57dad461 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -18,7 +18,6 @@ import { getHealth } from "./controllers/API"; import Router from "./routes"; import useAlertStore from "./stores/alertStore"; import { useDarkStore } from "./stores/darkStore"; -import useFlowStore from "./stores/flowStore"; import useFlowsManagerStore from "./stores/flowsManagerStore"; import { useStoreStore } from "./stores/storeStore"; import { useTypesStore } from "./stores/typesStore"; @@ -70,7 +69,7 @@ export default function App() { .catch(() => { setFetchError(true); }); - }, 20000); + }, 20000); // 20 seconds // Clean up the timer on component unmount return () => { From 48ec2d2fa8a8ed8128aba789a9ed6196767129a3 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 2 Mar 2024 15:18:58 -0300 Subject: [PATCH 017/199] Fix empty records bug and return empty string if state is not found --- src/backend/langflow/components/utilities/RecordsAsText.py | 2 ++ src/backend/langflow/graph/graph/state_manager.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/backend/langflow/components/utilities/RecordsAsText.py b/src/backend/langflow/components/utilities/RecordsAsText.py index debf3eed2..18bf8be8c 100644 --- a/src/backend/langflow/components/utilities/RecordsAsText.py +++ b/src/backend/langflow/components/utilities/RecordsAsText.py @@ -25,6 +25,8 @@ class RecordsAsTextComponent(CustomComponent): records: list[Record], template: str = "Text: {text}\nData: {data}", ) -> Text: + if not records: + return "" if isinstance(records, Record): records = [records] diff --git a/src/backend/langflow/graph/graph/state_manager.py b/src/backend/langflow/graph/graph/state_manager.py index 46477d220..3fcbb68a3 100644 --- a/src/backend/langflow/graph/graph/state_manager.py +++ b/src/backend/langflow/graph/graph/state_manager.py @@ -27,7 +27,7 @@ class GraphStateManager: def get_state(self, key): with self.lock: - return self.states.get(key, None) + return self.states.get(key, "") def subscribe(self, key, observer: Callable): with self.lock: From 8ac3f4579e7eee2710f6356b7812d9407561c998 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 2 Mar 2024 15:19:34 -0300 Subject: [PATCH 018/199] Refactor activated_vertices to activated_layers --- src/backend/langflow/api/v1/chat.py | 4 +- src/backend/langflow/api/v1/schemas.py | 2 +- src/backend/langflow/graph/graph/base.py | 51 +++++++++++++++++++----- src/frontend/src/stores/flowStore.ts | 14 +++---- src/frontend/src/types/api/index.ts | 2 +- src/frontend/src/utils/buildUtils.ts | 42 ++++++------------- 6 files changed, 63 insertions(+), 52 deletions(-) diff --git a/src/backend/langflow/api/v1/chat.py b/src/backend/langflow/api/v1/chat.py index 399098d47..8782efb29 100644 --- a/src/backend/langflow/api/v1/chat.py +++ b/src/backend/langflow/api/v1/chat.py @@ -156,13 +156,13 @@ async def build_vertex( inactivated_vertices = None inactivated_vertices = list(graph.inactivated_vertices) graph.reset_inactivated_vertices() - activated_vertices = list(graph.activated_vertices) + activated_layers = graph.activated_layers graph.reset_activated_vertices() chat_service.set_cache(flow_id, graph) build_response = VertexBuildResponse( inactivated_vertices=inactivated_vertices, - activated_vertices=activated_vertices, + activated_layers=activated_layers, valid=valid, params=params, id=vertex.id, diff --git a/src/backend/langflow/api/v1/schemas.py b/src/backend/langflow/api/v1/schemas.py index 88610d637..f570923e1 100644 --- a/src/backend/langflow/api/v1/schemas.py +++ b/src/backend/langflow/api/v1/schemas.py @@ -230,7 +230,7 @@ class ResultDataResponse(BaseModel): class VertexBuildResponse(BaseModel): id: Optional[str] = None inactivated_vertices: Optional[List[str]] = None - activated_vertices: Optional[List[str]] = None + activated_layers: Optional[List[List[str]]] = None valid: bool params: Optional[str] """JSON string of the params.""" diff --git a/src/backend/langflow/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py index 90a6e4f9a..95ab6db6b 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -58,7 +58,7 @@ class Graph: self._vertices = self._graph_data["nodes"] self._edges = self._graph_data["edges"] self.inactivated_vertices: set = set() - self.activated_vertices: set = set() + self.activated_layers: List[List[str]] = [] self.edges: List[ContractEdge] = [] self.vertices: List[Vertex] = [] self._build_graph() @@ -80,20 +80,26 @@ class Graph: self.state_manager.update_state(name, record) def activate_state_vertices(self, name: str, caller: str): + layers = [] for vertex_id in self._is_state_vertices: + if vertex_id == caller: + continue vertex = self.get_vertex(vertex_id) if ( - name in vertex._raw_params["name"] + isinstance(vertex._raw_params["name"], str) + and name in vertex._raw_params["name"] and vertex_id != caller and isinstance(vertex, StateVertex) ): - successors = self.get_all_successors(vertex) - self.activated_vertices.add(vertex_id) - for successor in successors: - self.activated_vertices.add(successor.id) + layers.append([vertex_id]) + successors = self.get_all_successors(vertex, flat=False) + for layer in successors: + + layers.append([v.id for v in layer]) + self.activated_layers = layers def reset_activated_vertices(self): - self.activated_vertices = set() + self.activated_layers = [] def append_state( self, name: str, record: Union[str, Record], caller: Optional[str] = None @@ -555,18 +561,41 @@ class Graph: for source_id in self.predecessor_map.get(vertex.id, []) ] - def get_all_successors(self, vertex, recursive=True): + def get_all_successors(self, vertex, recursive=True, flat=True): # Recursively get the successors of the current vertex + # successors = vertex.successors + # if not successors: + # return [] + # successors_result = [] + # for successor in successors: + # # Just return a list of successors + # if recursive: + # next_successors = self.get_all_successors(successor) + # successors_result.extend(next_successors) + # successors_result.append(successor) + # return successors_result + # The above is the version without the flat parameter + # The below is the version with the flat parameter + # the flat parameter will define if each layer of successors + # becomes one list or if the result is a list of lists + # if flat is True, the result will be a list of vertices + # if flat is False, the result will be a list of lists of vertices + # each list will represent a layer of successors successors = vertex.successors if not successors: return [] successors_result = [] for successor in successors: - # Just return a list of successors if recursive: next_successors = self.get_all_successors(successor) - successors_result.extend(next_successors) - successors_result.append(successor) + if flat: + successors_result.extend(next_successors) + else: + successors_result.append(next_successors) + if flat: + successors_result.append(successor) + else: + successors_result.append([successor]) return successors_result def get_successors(self, vertex): diff --git a/src/frontend/src/stores/flowStore.ts b/src/frontend/src/stores/flowStore.ts index f93b72858..d7780c857 100644 --- a/src/frontend/src/stores/flowStore.ts +++ b/src/frontend/src/stores/flowStore.ts @@ -223,12 +223,10 @@ const useFlowStore = create((set, get) => ({ selection.nodes.some((node) => node.data.type === "ChatInput") && checkChatInput(get().nodes) ) { - useAlertStore - .getState() - .setErrorData({ - title: "Error pasting components", - list: ["You can only have one ChatInput component in the flow"], - }); + useAlertStore.getState().setErrorData({ + title: "Error pasting components", + list: ["You can only have one ChatInput component in the flow"], + }); return; } let minimumX = Infinity; @@ -451,8 +449,8 @@ const useFlowStore = create((set, get) => ({ if (vertexBuildData && vertexBuildData.inactivated_vertices) { get().removeFromVerticesBuild(vertexBuildData.inactivated_vertices); } - if (vertexBuildData && vertexBuildData.activated_vertices) { - get().addToVerticesBuild(vertexBuildData.activated_vertices); + if (vertexBuildData && vertexBuildData.activated_layers) { + get().addToVerticesBuild(vertexBuildData.activated_layers.flat()); } get().addDataToFlowPool( { ...vertexBuildData, buildId }, diff --git a/src/frontend/src/types/api/index.ts b/src/frontend/src/types/api/index.ts index 076ecbf45..469ca3d9f 100644 --- a/src/frontend/src/types/api/index.ts +++ b/src/frontend/src/types/api/index.ts @@ -141,7 +141,7 @@ export type VerticesOrderTypeAPI = { export type VertexBuildTypeAPI = { id: string; inactivated_vertices: Array | null; - activated_vertices: Array | null; + activated_layers: Array> | null; valid: boolean; params: string; data: VertexDataTypeAPI; diff --git a/src/frontend/src/utils/buildUtils.ts b/src/frontend/src/utils/buildUtils.ts index 63da6b7e8..b89fc6a5a 100644 --- a/src/frontend/src/utils/buildUtils.ts +++ b/src/frontend/src/utils/buildUtils.ts @@ -32,7 +32,7 @@ function getInactiveVertexData(vertexId: string): VertexBuildTypeAPI { data: inactiveData, params: "Inactive", inactivated_vertices: null, - activated_vertices: null, + activated_layers: null, valid: false, timestamp: new Date().toISOString(), }; @@ -131,41 +131,25 @@ export async function buildVertices({ const handleBuildUpdate = (data: VertexBuildTypeAPI, status: BuildStatus) => { // Handle activated vertices console.log("handleBuildUpdate", data, status); - if (data.activated_vertices && data.activated_vertices.length > 0) { - // Logic to determine the correct placement for activated vertices in dynamicVerticesLayers - // For simplicity, this example adds them to the next layer - // const nextLayerIndex = i + 1; i doesnt exist in this scope - // we don't want to add the activated vertices to the last layer - // because these vertices should be built right away + if (data.activated_layers && data.activated_layers.length > 0) { const thisVertexLayer = dynamicVerticesLayers.findIndex((layer) => layer.includes(data.id) ); - const nextLayerIndex = thisVertexLayer + 1; + let nextLayerIndex = thisVertexLayer + 1; + console.log("nextLayerIndex", nextLayerIndex); console.log("dynamicVerticesLayers", dynamicVerticesLayers); - if (dynamicVerticesLayers[nextLayerIndex]) { - // If the next layer exists, add the activated vertices to it - // dynamicVerticesLayers[nextLayerIndex] = dynamicVerticesLayers[ - // nextLayerIndex - // ].concat(data.activated_vertices); - // instead of adding them all at once, add them one by one - // add one per layer and if the next layer doesn't exist, create it - for (const vertex of data.activated_vertices) { - console.log("vertex", vertex); - if (dynamicVerticesLayers[nextLayerIndex].includes(vertex)) { - continue; - } else if (dynamicVerticesLayers[nextLayerIndex].length > 0) { - dynamicVerticesLayers[nextLayerIndex].push(vertex); - } else { - dynamicVerticesLayers[nextLayerIndex] = [vertex]; - } - console.log("dynamicVerticesLayers", dynamicVerticesLayers); + data.activated_layers.forEach((newLayer) => { + if (!dynamicVerticesLayers[nextLayerIndex]) { + dynamicVerticesLayers[nextLayerIndex] = []; } - } else { - dynamicVerticesLayers.push(data.activated_vertices); - console.log(dynamicVerticesLayers); - } + dynamicVerticesLayers[nextLayerIndex] = [ + ...dynamicVerticesLayers[nextLayerIndex], + ...newLayer, + ]; + nextLayerIndex += 1; + }); } if (onBuildUpdate) onBuildUpdate(data, status, runId); }; From 8e65ba5f17a8fde900d46f3fa0ced45c0ca1ad02 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 2 Mar 2024 15:20:21 -0300 Subject: [PATCH 019/199] Fix handling of empty state in SharedState.py --- src/backend/langflow/components/utilities/SharedState.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/backend/langflow/components/utilities/SharedState.py b/src/backend/langflow/components/utilities/SharedState.py index 3e715cf1a..d3c5e20fb 100644 --- a/src/backend/langflow/components/utilities/SharedState.py +++ b/src/backend/langflow/components/utilities/SharedState.py @@ -35,4 +35,6 @@ class SharedState(CustomComponent): state = Record(data=state) else: state = Record(text=str(state)) + elif not state: + state = Record(text="") return state From 85576d420418a36b7c70e5eb340d7a6d826b56d8 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 2 Mar 2024 17:37:12 -0300 Subject: [PATCH 020/199] Add status attribute to SharedState class --- src/backend/langflow/components/utilities/SharedState.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backend/langflow/components/utilities/SharedState.py b/src/backend/langflow/components/utilities/SharedState.py index d3c5e20fb..95de17774 100644 --- a/src/backend/langflow/components/utilities/SharedState.py +++ b/src/backend/langflow/components/utilities/SharedState.py @@ -37,4 +37,5 @@ class SharedState(CustomComponent): state = Record(text=str(state)) elif not state: state = Record(text="") + self.status = state return state From 7207c4c0a2c9b9b9171b9316a4a3d6b5fd413153 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 2 Mar 2024 17:38:01 -0300 Subject: [PATCH 021/199] Add layer to dynamicVerticesLayers array --- src/frontend/src/utils/buildUtils.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/utils/buildUtils.ts b/src/frontend/src/utils/buildUtils.ts index b89fc6a5a..1fe55d209 100644 --- a/src/frontend/src/utils/buildUtils.ts +++ b/src/frontend/src/utils/buildUtils.ts @@ -139,7 +139,8 @@ export async function buildVertices({ console.log("nextLayerIndex", nextLayerIndex); console.log("dynamicVerticesLayers", dynamicVerticesLayers); - + // This adds layers to the dynamicVerticesLayers array + // starting from the index of the current layer + 1 data.activated_layers.forEach((newLayer) => { if (!dynamicVerticesLayers[nextLayerIndex]) { dynamicVerticesLayers[nextLayerIndex] = []; @@ -150,6 +151,21 @@ export async function buildVertices({ ]; nextLayerIndex += 1; }); + // Let's implement one that just adds all layers to the end of the array + // data.activated_layers.forEach((newLayer) => { + // // filter the newLayer to remove any vertices that are already in the dynamicVerticesLayers + // // after thisVertexLayer + // newLayer = newLayer.filter((vertex) => { + // return !dynamicVerticesLayers + // .slice(thisVertexLayer) + // .flat() + // .includes(vertex); + // }); + // if (newLayer.length > 0) { + // console.log("newLayer after filter", newLayer); + // dynamicVerticesLayers.push(newLayer); + // } + // }); } if (onBuildUpdate) onBuildUpdate(data, status, runId); }; @@ -157,9 +173,9 @@ export async function buildVertices({ // Set each vertex state to building const buildResults: Array = []; for (let i = 0; i < dynamicVerticesLayers.length; i++) { - console.log(dynamicVerticesLayers); const layer = dynamicVerticesLayers[i]; if (onBuildStart) onBuildStart(layer); + for (const id of layer) { // Check if id is in the list of inactive nodes // useFlowStore because it gets updated constantly From 0fe8d1116de1bc9318810cf3d37e6c1aeb878086 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 2 Mar 2024 17:38:08 -0300 Subject: [PATCH 022/199] Add console log for onBuildStart event --- src/frontend/src/stores/flowStore.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/frontend/src/stores/flowStore.ts b/src/frontend/src/stores/flowStore.ts index d7780c857..8bd3df4d8 100644 --- a/src/frontend/src/stores/flowStore.ts +++ b/src/frontend/src/stores/flowStore.ts @@ -484,6 +484,7 @@ const useFlowStore = create((set, get) => ({ setErrorData({ list, title }); }, onBuildStart: (idList) => { + console.log("onBuildStart", idList); useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILDING); }, validateNodes: validateSubgraph, From e9772d06fc630654d4ba75e5675afb74781501c6 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 2 Mar 2024 17:38:21 -0300 Subject: [PATCH 023/199] Refactor graph state management and add get_state method --- src/backend/langflow/graph/graph/base.py | 49 +++++++++++++++---- .../langflow/graph/graph/state_manager.py | 24 +++++---- .../custom_component/custom_component.py | 2 +- 3 files changed, 56 insertions(+), 19 deletions(-) diff --git a/src/backend/langflow/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py index 95ab6db6b..df270a201 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -47,7 +47,7 @@ class Graph: self._is_state_vertices: List[str] = [] self._has_session_id_vertices: List[str] = [] self._sorted_vertices_layers: List[List[str]] = [] - self.run_id = None + self._run_id = None self.top_level_vertices = [] for vertex in self._vertices: @@ -66,6 +66,10 @@ class Graph: self.define_vertices_lists() self.state_manager = GraphStateManager() + def get_state(self, name: str) -> Optional[Record]: + """Returns the state of the graph.""" + return self.state_manager.get_state(name, run_id=self._run_id) + def update_state( self, name: str, record: Union[str, Record], caller: Optional[str] = None ) -> None: @@ -77,7 +81,7 @@ class Graph: # This also has to activate their successors self.activate_state_vertices(name, caller) - self.state_manager.update_state(name, record) + self.state_manager.update_state(name, record, run_id=self._run_id) def activate_state_vertices(self, name: str, caller: str): layers = [] @@ -109,15 +113,21 @@ class Graph: self.activate_state_vertices(name, caller) - self.state_manager.append_state(name, record) + self.state_manager.append_state(name, record, run_id=self._run_id) + + @property + def run_id(self): + if not self._run_id: + raise ValueError("Run ID not set") + return self._run_id def set_run_id(self, run_id: str): for vertex in self.vertices: self.state_manager.subscribe(run_id, vertex.update_graph_state) - self.run_id = run_id + self._run_id = run_id def add_state(self, state: str): - self.state_manager.append_state(self.run_id, state) + self.state_manager.append_state(self._run_id, state) @property def sorted_vertices_layers(self) -> List[List[str]]: @@ -760,21 +770,28 @@ class Graph: def layered_topological_sort( self, vertices: List[Vertex], + filter_graphs: bool = False, ) -> List[List[str]]: """Performs a layered topological sort of the vertices in the graph.""" vertices_ids = {vertex.id for vertex in vertices} # Queue for vertices with no incoming edges queue = deque( - vertex.id for vertex in vertices if self.in_degree_map[vertex.id] == 0 + vertex.id + for vertex in vertices + # if filter_graphs then only vertex.is_input will be considered + if self.in_degree_map[vertex.id] == 0 + and (not filter_graphs or vertex.is_input) ) layers: List[List[str]] = [] - + visited = set(queue) current_layer = 0 while queue: layers.append([]) # Start a new layer layer_size = len(queue) for _ in range(layer_size): vertex_id = queue.popleft() + visited.add(vertex_id) + layers[current_layer].append(vertex_id) for neighbor in self.successor_map[vertex_id]: # only vertices in `vertices_ids` should be considered @@ -785,8 +802,16 @@ class Graph: continue self.in_degree_map[neighbor] -= 1 # 'remove' edge - if self.in_degree_map[neighbor] == 0: + if self.in_degree_map[neighbor] == 0 and neighbor not in visited: queue.append(neighbor) + + # if > 0 it might mean not all predecessors have added to the queue + # so we should process the neighbors predecessors + elif self.in_degree_map[neighbor] > 0: + for predecessor in self.predecessor_map[neighbor]: + if predecessor not in queue and predecessor not in visited: + queue.append(predecessor) + current_layer += 1 # Next layer new_layers = self.refine_layers(layers) return new_layers @@ -851,9 +876,15 @@ class Graph: self.mark_all_vertices("ACTIVE") if component_id: vertices = self.sort_up_to_vertex(component_id) + vertices_layers = self.layered_topological_sort(vertices) else: vertices = self.vertices - vertices_layers = self.layered_topological_sort(vertices) + # without component_id we are probably running in the chat + # so we want to pick only graphs that start with ChatInput or + # TextInput + vertices_layers = self.layered_topological_sort( + vertices, filter_graphs=True + ) vertices_layers = self.sort_by_avg_build_time(vertices_layers) vertices_layers = self.sort_chat_inputs_first(vertices_layers) self.increment_run_count() diff --git a/src/backend/langflow/graph/graph/state_manager.py b/src/backend/langflow/graph/graph/state_manager.py index 3fcbb68a3..ed5844d87 100644 --- a/src/backend/langflow/graph/graph/state_manager.py +++ b/src/backend/langflow/graph/graph/state_manager.py @@ -11,23 +11,29 @@ class GraphStateManager: self.observers = defaultdict(list) self.lock = Lock() - def append_state(self, key, new_state): + def append_state(self, key, new_state, run_id: str): with self.lock: - if key not in self.states: - self.states[key] = [] + if run_id not in self.states: + self.states[run_id] = {} + if key not in self.states[run_id]: + self.states[run_id][key] = [] elif not isinstance(self.states[key], list): - self.states[key] = [self.states[key]] - self.states[key].append(new_state) + self.states[run_id][key] = [self.states[key]] + self.states[run_id][key].append(new_state) self.notify_append_observers(key, new_state) - def update_state(self, key, new_state): + def update_state(self, key, new_state, run_id: str): with self.lock: - self.states[key] = new_state + if run_id not in self.states: + self.states[run_id] = {} + if key not in self.states[run_id]: + self.states[run_id][key] = {} + self.states[run_id][key] = new_state self.notify_observers(key, new_state) - def get_state(self, key): + def get_state(self, key, run_id: str): with self.lock: - return self.states.get(key, "") + return self.states.get(run_id, {}).get(key, "") def subscribe(self, key, observer: Callable): with self.lock: 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 11c4c941d..1596533a2 100644 --- a/src/backend/langflow/interface/custom/custom_component/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component/custom_component.py @@ -92,7 +92,7 @@ class CustomComponent(Component): def get_state(self, name: str): try: - return self.vertex.graph.state_manager.get_state(key=name) + return self.vertex.graph.get_state(name=name) except Exception as e: raise ValueError(f"Error getting state: {e}") From 131cebcea6cf7c3b1a5bfad9eec4844ad6cfe7b3 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 2 Mar 2024 17:38:28 -0300 Subject: [PATCH 024/199] Add status update to TextToRecordComponent --- src/backend/langflow/components/utilities/TextToRecord.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/backend/langflow/components/utilities/TextToRecord.py b/src/backend/langflow/components/utilities/TextToRecord.py index e176e15f5..277434da6 100644 --- a/src/backend/langflow/components/utilities/TextToRecord.py +++ b/src/backend/langflow/components/utilities/TextToRecord.py @@ -26,4 +26,6 @@ class TextToRecordComponent(CustomComponent): text: Text, data: Optional[dict] = {}, ) -> Record: - return Record(text=text, data=data) + record = Record(text=text, data=data) + self.status = record + return record From 9f2d67f3bcabb68f92a26f914531d0ef4237aa2c Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 2 Mar 2024 17:38:36 -0300 Subject: [PATCH 025/199] Refactor Vertex class in base.py --- src/backend/langflow/graph/vertex/base.py | 25 ++++++++--------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/src/backend/langflow/graph/vertex/base.py b/src/backend/langflow/graph/vertex/base.py index 097b99fbc..8f754325e 100644 --- a/src/backend/langflow/graph/vertex/base.py +++ b/src/backend/langflow/graph/vertex/base.py @@ -2,26 +2,13 @@ import ast import inspect import types from enum import Enum -from typing import ( - TYPE_CHECKING, - Any, - AsyncIterator, - Callable, - Coroutine, - Dict, - Iterator, - List, - Optional, -) +from typing import (TYPE_CHECKING, Any, AsyncIterator, Callable, Coroutine, + Dict, Iterator, List, Optional) from loguru import logger -from langflow.graph.schema import ( - INPUT_COMPONENTS, - OUTPUT_COMPONENTS, - InterfaceComponentTypes, - ResultData, -) +from langflow.graph.schema import (INPUT_COMPONENTS, OUTPUT_COMPONENTS, + InterfaceComponentTypes, ResultData) from langflow.graph.utils import UnbuiltObject, UnbuiltResult from langflow.graph.vertex.utils import generate_result from langflow.interface.initialize import loading @@ -639,6 +626,10 @@ class Vertex: if self.pinned and self._built: return self.get_requester_result(requester) + elif self._built and requester is not None: + # This means that the vertex has already been built + # and we are just getting the result for the requester + return await self.get_requester_result(requester) self._reset() if self.is_input and inputs is not None: From bc3402586d5451ea815cecca56bf996192f09caf Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 2 Mar 2024 18:11:07 -0300 Subject: [PATCH 026/199] Fix bug in PromptComponent --- src/backend/langflow/components/prompts/Prompt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/langflow/components/prompts/Prompt.py b/src/backend/langflow/components/prompts/Prompt.py index d8afbcb43..975998919 100644 --- a/src/backend/langflow/components/prompts/Prompt.py +++ b/src/backend/langflow/components/prompts/Prompt.py @@ -23,7 +23,7 @@ class PromptComponent(CustomComponent): prompt_template = PromptTemplate.from_template(Text(template)) attributes_to_check = ["text", "page_content"] - for key, value in kwargs.items(): + for key, value in kwargs.copy().items(): for attribute in attributes_to_check: if hasattr(value, attribute): kwargs[key] = getattr(value, attribute) From e54fe60a6b02a9f68ec76e534d6526d836450f47 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 2 Mar 2024 18:11:18 -0300 Subject: [PATCH 027/199] Add GetNotified and Notify components --- .../components/utilities/GetNotifified.py | 20 ++++++++++ .../langflow/components/utilities/Notify.py | 40 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 src/backend/langflow/components/utilities/GetNotifified.py create mode 100644 src/backend/langflow/components/utilities/Notify.py diff --git a/src/backend/langflow/components/utilities/GetNotifified.py b/src/backend/langflow/components/utilities/GetNotifified.py new file mode 100644 index 000000000..d97c30df2 --- /dev/null +++ b/src/backend/langflow/components/utilities/GetNotifified.py @@ -0,0 +1,20 @@ +from langflow import CustomComponent +from langflow.schema import Record + + +class GetNotifiedComponent(CustomComponent): + display_name = "Get Notified" + description = "A component to get notified by Notify component." + + def build_config(self): + return { + "name": { + "display_name": "Name", + "info": "The name of the notification to listen for.", + }, + } + + def build(self, name: str) -> Record: + state = self.get_state(name) + self.status = state + return state diff --git a/src/backend/langflow/components/utilities/Notify.py b/src/backend/langflow/components/utilities/Notify.py new file mode 100644 index 000000000..a43a4bb0c --- /dev/null +++ b/src/backend/langflow/components/utilities/Notify.py @@ -0,0 +1,40 @@ +from typing import Optional + +from langflow import CustomComponent +from langflow.schema import Record + + +class NotifyComponent(CustomComponent): + display_name = "Notify" + description = "A component to generate a notification to Get Notified component." + + def build_config(self): + return { + "name": {"display_name": "Name", "info": "The name of the notification."}, + "record": {"display_name": "Record", "info": "The record to store."}, + "append": { + "display_name": "Append", + "info": "If True, the record will be appended to the notification.", + }, + } + + def build( + self, name: str, record: Optional[Record] = None, append: bool = False + ) -> Record: + if state and not isinstance(state, Record): + if isinstance(state, str): + state = Record(text=state) + elif isinstance(state, dict): + state = Record(data=state) + else: + state = Record(text=str(state)) + elif not state: + state = Record(text="") + if record: + if append: + self.append_state(name, record) + else: + self.update_state(name, record) + else: + state = "No record provided." + self.status = state From 178a98dc8e6534c25c0fb584676b9cd58b1e43ed Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sun, 3 Mar 2024 22:47:34 -0300 Subject: [PATCH 028/199] Refactor graph activation logic and add successors_ids property to StateVertex --- src/backend/langflow/api/v1/chat.py | 2 +- src/backend/langflow/graph/graph/base.py | 25 ++++++++++------------ src/backend/langflow/graph/vertex/types.py | 5 +++++ 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/backend/langflow/api/v1/chat.py b/src/backend/langflow/api/v1/chat.py index 09ac2aac8..edd4d4724 100644 --- a/src/backend/langflow/api/v1/chat.py +++ b/src/backend/langflow/api/v1/chat.py @@ -162,7 +162,7 @@ async def build_vertex( inactivated_vertices = None inactivated_vertices = list(graph.inactivated_vertices) graph.reset_inactivated_vertices() - activated_layers = graph.activated_layers + activated_layers = graph.activated_vertices graph.reset_activated_vertices() chat_service.set_cache(flow_id, graph) diff --git a/src/backend/langflow/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py index ece989651..06d917eb5 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -59,7 +59,7 @@ class Graph: self._vertices = self._graph_data["nodes"] self._edges = self._graph_data["edges"] self.inactivated_vertices: set = set() - self.activated_layers: List[List[str]] = [] + self.activated_vertices: List[str] = [] self.vertices_layers = [] self.vertices_to_run = set() self.stop_vertex = None @@ -90,7 +90,7 @@ class Graph: self.state_manager.update_state(name, record, run_id=self._run_id) def activate_state_vertices(self, name: str, caller: str): - layers = [] + vertices_ids = [] for vertex_id in self._is_state_vertices: if vertex_id == caller: continue @@ -101,15 +101,14 @@ class Graph: and vertex_id != caller and isinstance(vertex, StateVertex) ): - layers.append([vertex_id]) - successors = self.get_all_successors(vertex, flat=False) - for layer in successors: - - layers.append([v.id for v in layer]) - self.activated_layers = layers + vertices_ids.append(vertex_id) + successors = self.get_all_successors(vertex, flat=True) + self.vertices_to_run.update(list(map(lambda x: x.id, successors))) + self.activated_vertices = vertices_ids + self.vertices_to_run.update(vertices_ids) def reset_activated_vertices(self): - self.activated_layers = [] + self.activated_vertices = [] def append_state( self, name: str, record: Union[str, Record], caller: Optional[str] = None @@ -922,15 +921,13 @@ class Graph: vertices = self.sort_up_to_vertex(stop_component_id) elif start_component_id: vertices = self.sort_up_to_vertex(start_component_id, is_start=True) - else: vertices = self.vertices # without component_id we are probably running in the chat # so we want to pick only graphs that start with ChatInput or # TextInput - vertices_layers = self.layered_topological_sort( - vertices, filter_graphs=True - ) + + vertices_layers = self.layered_topological_sort(vertices) vertices_layers = self.sort_by_avg_build_time(vertices_layers) # vertices_layers = self.sort_chat_inputs_first(vertices_layers) self.increment_run_count() @@ -938,7 +935,7 @@ class Graph: # save the only the rest self.vertices_layers = vertices_layers[1:] self.vertices_to_run = { - vertex for vertex in chain.from_iterable(vertices_layers) + vertex_id for vertex_id in chain.from_iterable(vertices_layers) } # Return just the first layer return first_layer diff --git a/src/backend/langflow/graph/vertex/types.py b/src/backend/langflow/graph/vertex/types.py index a759be958..927cbda0f 100644 --- a/src/backend/langflow/graph/vertex/types.py +++ b/src/backend/langflow/graph/vertex/types.py @@ -500,6 +500,11 @@ class StateVertex(Vertex): self.steps = [self._build] self.is_state = True + @property + def successors_ids(self) -> List[str]: + successors = self.graph.successor_map.get(self.id, []) + return successors + self.graph.activated_vertices + def dict_to_codeblock(d: dict) -> str: serialized = {key: serialize_field(val) for key, val in d.items()} From d1557d8c1e1ed25fd218efc63864b9b91bc8486a Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sun, 3 Mar 2024 22:47:44 -0300 Subject: [PATCH 029/199] Remove unused variable and reset activated vertices in build_vertex function --- src/backend/langflow/api/v1/chat.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/backend/langflow/api/v1/chat.py b/src/backend/langflow/api/v1/chat.py index edd4d4724..9f0259771 100644 --- a/src/backend/langflow/api/v1/chat.py +++ b/src/backend/langflow/api/v1/chat.py @@ -162,7 +162,6 @@ async def build_vertex( inactivated_vertices = None inactivated_vertices = list(graph.inactivated_vertices) graph.reset_inactivated_vertices() - activated_layers = graph.activated_vertices graph.reset_activated_vertices() chat_service.set_cache(flow_id, graph) From 9a93e5e417a0e45c837e26e51563c2583f6aa803 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sun, 3 Mar 2024 23:19:23 -0300 Subject: [PATCH 030/199] Add GetNotified and Notify components --- .../{GetNotifified.py => GetNotified.py} | 0 .../langflow/components/utilities/Notify.py | 21 ++++++++++--------- src/backend/langflow/graph/graph/base.py | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) rename src/backend/langflow/components/utilities/{GetNotifified.py => GetNotified.py} (100%) diff --git a/src/backend/langflow/components/utilities/GetNotifified.py b/src/backend/langflow/components/utilities/GetNotified.py similarity index 100% rename from src/backend/langflow/components/utilities/GetNotifified.py rename to src/backend/langflow/components/utilities/GetNotified.py diff --git a/src/backend/langflow/components/utilities/Notify.py b/src/backend/langflow/components/utilities/Notify.py index a43a4bb0c..2155b079e 100644 --- a/src/backend/langflow/components/utilities/Notify.py +++ b/src/backend/langflow/components/utilities/Notify.py @@ -21,20 +21,21 @@ class NotifyComponent(CustomComponent): def build( self, name: str, record: Optional[Record] = None, append: bool = False ) -> Record: - if state and not isinstance(state, Record): - if isinstance(state, str): - state = Record(text=state) - elif isinstance(state, dict): - state = Record(data=state) + if record and not isinstance(record, Record): + if isinstance(record, str): + record = Record(text=record) + elif isinstance(record, dict): + record = Record(data=record) else: - state = Record(text=str(state)) - elif not state: - state = Record(text="") + record = Record(text=str(record)) + elif not record: + record = Record(text="") if record: if append: self.append_state(name, record) else: self.update_state(name, record) else: - state = "No record provided." - self.status = state + self.status = "No record provided." + self.status = record + return record diff --git a/src/backend/langflow/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py index 06d917eb5..06ae4ee8f 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -699,7 +699,7 @@ class Graph: return ChatVertex elif node_name in ["ShouldRunNext"]: return RoutingVertex - elif node_name in ["SharedState"]: + elif node_name in ["SharedState", "Notify", "GetNotified"]: return StateVertex elif node_base_type in lazy_load_vertex_dict.VERTEX_TYPE_MAP: return lazy_load_vertex_dict.VERTEX_TYPE_MAP[node_base_type] From a31f824457eb2d55ea9257a395c8b76945ca2c3d Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sun, 3 Mar 2024 23:37:16 -0300 Subject: [PATCH 031/199] 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 e40eabdaa..186030729 100644 --- a/src/backend/langflow/graph/vertex/base.py +++ b/src/backend/langflow/graph/vertex/base.py @@ -141,6 +141,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 815e9cfb5990abb62988aa4542df13d005ad0745 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 15:17:19 -0300 Subject: [PATCH 032/199] 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 06ae4ee8f..a0b91b440 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -170,7 +170,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 @@ -524,15 +529,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") @@ -551,6 +560,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]: @@ -931,6 +944,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 d403ca7a6c972c6377e47657941287f6e4a2f229 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 15:17:57 -0300 Subject: [PATCH 033/199] Add session_id and component_id to ChatOutputResponse schema --- src/backend/langflow/api/v1/schemas.py | 2 +- .../langflow/components/utilities/RunFlow.py | 58 +++++++++++++++++++ src/backend/langflow/graph/schema.py | 3 +- src/backend/langflow/graph/vertex/base.py | 29 ++++++++++ .../custom_component/custom_component.py | 2 +- src/backend/langflow/schema/schema.py | 10 ++-- src/backend/langflow/utils/schemas.py | 2 + .../src/CustomNodes/GenericNode/index.tsx | 49 +++++++++++++--- 8 files changed, 138 insertions(+), 17 deletions(-) create mode 100644 src/backend/langflow/components/utilities/RunFlow.py diff --git a/src/backend/langflow/api/v1/schemas.py b/src/backend/langflow/api/v1/schemas.py index 9968e839c..433d57a37 100644 --- a/src/backend/langflow/api/v1/schemas.py +++ b/src/backend/langflow/api/v1/schemas.py @@ -232,7 +232,7 @@ class VertexBuildResponse(BaseModel): inactivated_vertices: Optional[List[str]] = None next_vertices_ids: 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/components/utilities/RunFlow.py b/src/backend/langflow/components/utilities/RunFlow.py new file mode 100644 index 000000000..d0e49ac90 --- /dev/null +++ b/src/backend/langflow/components/utilities/RunFlow.py @@ -0,0 +1,58 @@ +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_name": { + "display_name": "Flow Name", + "info": "The name of the flow to run.", + "options": self.get_flow_names, + }, + "tweaks": { + "display_name": "Tweaks", + "info": "Tweaks to apply to the flow.", + }, + } + + 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: + + results: List[Optional[ResultData]] = await self.run_flow( + input_value=input_value, flow_name=flow_name, tweaks=tweaks + ) + 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 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 186030729..635b6f7f3 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: @@ -411,15 +412,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/interface/custom/custom_component/custom_component.py b/src/backend/langflow/interface/custom/custom_component/custom_component.py index 1596533a2..ced2c7a78 100644 --- a/src/backend/langflow/interface/custom/custom_component/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component/custom_component.py @@ -132,7 +132,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( diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index a7126fff7..72bedfcb3 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, @@ -59,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 { @@ -131,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; @@ -493,14 +508,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 + +
+ {validationString.split("\n").map((line, index) => ( +
{line}
+ ))} +
) } From 16c8d5be0c34c713f227c961133d73f7fc4f75e2 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 15:18:04 -0300 Subject: [PATCH 034/199] Refactor DocumentLoaderVertex to use record instead of doc and update display message format --- src/backend/langflow/graph/vertex/types.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/backend/langflow/graph/vertex/types.py b/src/backend/langflow/graph/vertex/types.py index 927cbda0f..ac9e1d0bc 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(Vertex): 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 5e0a8534f5c4e5eb3e2ff900f875494ad69b5cb9 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 16:19:35 -0300 Subject: [PATCH 035/199] 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 036/199] 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 037/199] 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 038/199] 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 039/199] 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 040/199] 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 041/199] 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 042/199] 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 From c6045809f2a16cdf6cee5c1242b0a5adae62d3a2 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 18:52:50 -0300 Subject: [PATCH 043/199] Update vertex raw parameters and add session ID --- src/backend/langflow/graph/graph/base.py | 14 ++++++++++---- src/backend/langflow/graph/vertex/base.py | 6 +++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/backend/langflow/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py index a0b91b440..97246aee1 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -151,14 +151,20 @@ class Graph: getattr(self, f"_{attribute}_vertices").append(vertex.id) async def _run( - self, inputs: Dict[str, str], stream: bool + self, inputs: Dict[str, str], stream: bool, session_id: str ) -> List[Optional["ResultData"]]: """Runs the graph with the given inputs.""" for vertex_id in self._is_input_vertices: vertex = self.get_vertex(vertex_id) if vertex is None: raise ValueError(f"Vertex {vertex_id} not found") - vertex.update_raw_params(inputs) + vertex.update_raw_params(inputs, overwrite=True) + # Update all the vertices with the session_id + for vertex_id in self._has_session_id_vertices: + vertex = self.get_vertex(vertex_id) + if vertex is None: + raise ValueError(f"Vertex {vertex_id} not found") + vertex.update_raw_params({"session_id": session_id}) try: await self.process() self.increment_run_count() @@ -181,7 +187,7 @@ class Graph: return outputs async def run( - self, inputs: Dict[str, Union[str, list[str]]], stream: bool + self, inputs: Dict[str, Union[str, list[str]]], stream: bool, session_id: str ) -> List[Optional["ResultData"]]: """Runs the graph with the given inputs.""" @@ -195,7 +201,7 @@ class Graph: inputs_values = [inputs_values] for input_value in inputs_values: run_outputs = await self._run( - {INPUT_FIELD_NAME: input_value}, stream=stream + {INPUT_FIELD_NAME: input_value}, stream=stream, session_id=session_id ) logger.debug(f"Run outputs: {run_outputs}") outputs.extend(run_outputs) diff --git a/src/backend/langflow/graph/vertex/base.py b/src/backend/langflow/graph/vertex/base.py index 635b6f7f3..f4e4823be 100644 --- a/src/backend/langflow/graph/vertex/base.py +++ b/src/backend/langflow/graph/vertex/base.py @@ -383,7 +383,7 @@ class Vertex: self.params = params self._raw_params = params.copy() - def update_raw_params(self, new_params: Dict[str, str]): + def update_raw_params(self, new_params: Dict[str, str], overwrite: bool = False): """ Update the raw parameters of the vertex with the given new parameters. @@ -398,6 +398,10 @@ class Vertex: return if any(isinstance(self._raw_params.get(key), Vertex) for key in new_params): return + if not overwrite: + for key in new_params.copy(): + if key not in self._raw_params: + new_params.pop(key) self._raw_params.update(new_params) self.updated_raw_params = True From 4b14aef8a73428046651b05f5400298d8987fbc6 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 18:52:59 -0300 Subject: [PATCH 044/199] Refactor process.py for readability and maintainability --- src/backend/langflow/processing/process.py | 42 ++++++++++++++++------ 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/src/backend/langflow/processing/process.py b/src/backend/langflow/processing/process.py index 408db0a53..e2af56d21 100644 --- a/src/backend/langflow/processing/process.py +++ b/src/backend/langflow/processing/process.py @@ -126,7 +126,9 @@ async def process_runnable(runnable: Runnable, inputs: Union[dict, List[dict]]): elif isinstance(inputs, dict) and hasattr(runnable, "ainvoke"): result = await runnable.ainvoke(inputs) else: - raise ValueError(f"Runnable {runnable} does not support inputs of type {type(inputs)}") + raise ValueError( + f"Runnable {runnable} does not support inputs of type {type(inputs)}" + ) # Check if the result is a list of AIMessages if isinstance(result, list) and all(isinstance(r, AIMessage) for r in result): result = [r.content for r in result] @@ -135,7 +137,9 @@ async def process_runnable(runnable: Runnable, inputs: Union[dict, List[dict]]): return result -async def process_inputs_dict(built_object: Union[Chain, VectorStore, Runnable], inputs: dict): +async def process_inputs_dict( + built_object: Union[Chain, VectorStore, Runnable], inputs: dict +): if isinstance(built_object, Chain): if inputs is None: raise ValueError("Inputs must be provided for a Chain") @@ -170,7 +174,9 @@ async def process_inputs_list(built_object: Runnable, inputs: List[dict]): return await process_runnable(built_object, inputs) -async def generate_result(built_object: Union[Chain, VectorStore, Runnable], inputs: Union[dict, List[dict]]): +async def generate_result( + built_object: Union[Chain, VectorStore, Runnable], inputs: Union[dict, List[dict]] +): if isinstance(inputs, dict): result = await process_inputs_dict(built_object, inputs) elif isinstance(inputs, List) and isinstance(built_object, Runnable): @@ -208,24 +214,30 @@ async def run_graph( else: graph_data = graph._graph_data if not session_id and session_service is not None: - session_id = session_service.generate_key(session_id=flow_id, data_graph=graph_data) + session_id = session_service.generate_key( + session_id=flow_id, data_graph=graph_data + ) if inputs is None: inputs = {} - outputs = await graph.run(inputs, stream=stream) + outputs = await graph.run(inputs, stream=stream, session_id=session_id) if session_id and session_service: session_service.update_session(session_id, (graph, artifacts)) return outputs, session_id -def validate_input(graph_data: Dict[str, Any], tweaks: Dict[str, Dict[str, Any]]) -> List[Dict[str, Any]]: +def validate_input( + graph_data: Dict[str, Any], tweaks: Dict[str, Dict[str, Any]] +) -> List[Dict[str, Any]]: if not isinstance(graph_data, dict) or not isinstance(tweaks, dict): raise ValueError("graph_data and tweaks should be dictionaries") nodes = graph_data.get("data", {}).get("nodes") or graph_data.get("nodes") if not isinstance(nodes, list): - raise ValueError("graph_data should contain a list of nodes under 'data' key or directly under 'nodes' key") + raise ValueError( + "graph_data should contain a list of nodes under 'data' key or directly under 'nodes' key" + ) return nodes @@ -234,7 +246,9 @@ def apply_tweaks(node: Dict[str, Any], node_tweaks: Dict[str, Any]) -> None: template_data = node.get("data", {}).get("node", {}).get("template") if not isinstance(template_data, dict): - logger.warning(f"Template data for node {node.get('id')} should be a dictionary") + logger.warning( + f"Template data for node {node.get('id')} should be a dictionary" + ) return for tweak_name, tweak_value in node_tweaks.items(): @@ -249,7 +263,9 @@ def apply_tweaks_on_vertex(vertex: Vertex, node_tweaks: Dict[str, Any]) -> None: vertex.params[tweak_name] = tweak_value -def process_tweaks(graph_data: Dict[str, Any], tweaks: Dict[str, Dict[str, Any]]) -> Dict[str, Any]: +def process_tweaks( + graph_data: Dict[str, Any], tweaks: Dict[str, Dict[str, Any]] +) -> Dict[str, Any]: """ This function is used to tweak the graph data using the node id and the tweaks dict. @@ -270,7 +286,9 @@ def process_tweaks(graph_data: Dict[str, Any], tweaks: Dict[str, Dict[str, Any]] if node_tweaks := tweaks.get(node_id): apply_tweaks(node, node_tweaks) else: - logger.warning("Each node should be a dictionary with an 'id' key of type str") + logger.warning( + "Each node should be a dictionary with an 'id' key of type str" + ) return graph_data @@ -282,6 +300,8 @@ def process_tweaks_on_graph(graph: Graph, tweaks: Dict[str, Dict[str, Any]]): if node_tweaks := tweaks.get(node_id): apply_tweaks_on_vertex(vertex, node_tweaks) else: - logger.warning("Each node should be a Vertex with an 'id' attribute of type str") + logger.warning( + "Each node should be a Vertex with an 'id' attribute of type str" + ) return graph From 76a52c0a766765e869b8d2a77ce9ac3bf9cf7128 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 19:19:44 -0300 Subject: [PATCH 045/199] Remove unnecessary code and delay in flowsManagerStore --- src/frontend/src/stores/flowStore.ts | 4 +--- src/frontend/src/stores/flowsManagerStore.ts | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/frontend/src/stores/flowStore.ts b/src/frontend/src/stores/flowStore.ts index 48431e069..cbf183289 100644 --- a/src/frontend/src/stores/flowStore.ts +++ b/src/frontend/src/stores/flowStore.ts @@ -453,9 +453,7 @@ const useFlowStore = create((set, get) => ({ if (vertexBuildData && vertexBuildData.inactivated_vertices) { get().removeFromVerticesBuild(vertexBuildData.inactivated_vertices); } - if (vertexBuildData && vertexBuildData.activated_layers) { - get().addToVerticesBuild(vertexBuildData.activated_layers.flat()); - } + if (vertexBuildData.next_vertices_ids) { // next_vertices_ids is a list of vertices that are going to be built next // verticesLayers is a list of list of vertices ids, where each list is a layer of vertices diff --git a/src/frontend/src/stores/flowsManagerStore.ts b/src/frontend/src/stores/flowsManagerStore.ts index 49849ba54..fac806c5f 100644 --- a/src/frontend/src/stores/flowsManagerStore.ts +++ b/src/frontend/src/stores/flowsManagerStore.ts @@ -92,7 +92,6 @@ const useFlowsManagerStore = create((set, get) => ({ true ); } - set({ saveLoading: true }); }, 500); // Delay of 500ms because chat message depends on it. }, saveFlow: (flow: FlowType, silent?: boolean) => { From 971b591cd66eb02d4d54fc81125bee07ecd8d8f4 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 23:29:32 -0300 Subject: [PATCH 046/199] Delete UrlLoaderComponent and Add DocumentToRecordComponent --- .../langflow/components/data/UrlLoader.py | 49 ------------------- .../DocumentToRecord.py | 0 2 files changed, 49 deletions(-) delete mode 100644 src/backend/langflow/components/data/UrlLoader.py rename src/backend/langflow/components/{langchain_utilities => utilities}/DocumentToRecord.py (100%) diff --git a/src/backend/langflow/components/data/UrlLoader.py b/src/backend/langflow/components/data/UrlLoader.py deleted file mode 100644 index c1346f142..000000000 --- a/src/backend/langflow/components/data/UrlLoader.py +++ /dev/null @@ -1,49 +0,0 @@ -from typing import List - -from langchain import document_loaders -from langchain_core.documents import Document - -from langflow import CustomComponent - - -class UrlLoaderComponent(CustomComponent): - display_name: str = "Url Loader" - description: str = "Generic Url Loader Component" - - def build_config(self): - return { - "web_path": { - "display_name": "Url", - "required": True, - }, - "loader": { - "display_name": "Loader", - "is_list": True, - "required": True, - "options": [ - "AZLyricsLoader", - "CollegeConfidentialLoader", - "GitbookLoader", - "HNLoader", - "IFixitLoader", - "IMSDbLoader", - "WebBaseLoader", - ], - "value": "WebBaseLoader", - }, - "code": {"show": False}, - } - - def build(self, web_path: str, loader: str) -> List[Document]: - try: - loader_instance = getattr(document_loaders, loader)(web_path=web_path) - 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) - self.status = f"""{len(docs)} documents) - \nAvg. Document Length (characters): {int(avg_length)} - Documents: {docs[:3]}...""" - return docs diff --git a/src/backend/langflow/components/langchain_utilities/DocumentToRecord.py b/src/backend/langflow/components/utilities/DocumentToRecord.py similarity index 100% rename from src/backend/langflow/components/langchain_utilities/DocumentToRecord.py rename to src/backend/langflow/components/utilities/DocumentToRecord.py From fa4b8214bf85cd2fc8180bb24a5b1f0bef8124dd Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 4 Mar 2024 23:31:00 -0300 Subject: [PATCH 047/199] Refactor imports in types.py --- src/backend/langflow/interface/types.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/backend/langflow/interface/types.py b/src/backend/langflow/interface/types.py index 3dcddf99f..9a1375869 100644 --- a/src/backend/langflow/interface/types.py +++ b/src/backend/langflow/interface/types.py @@ -1,8 +1,9 @@ from cachetools import LRUCache, cached - from langflow.interface.agents.base import agent_creator from langflow.interface.chains.base import chain_creator -from langflow.interface.custom.directory_reader.utils import merge_nested_dicts_with_renaming +from langflow.interface.custom.directory_reader.utils import ( + merge_nested_dicts_with_renaming, +) from langflow.interface.custom.utils import build_custom_components from langflow.interface.document_loaders.base import documentloader_creator from langflow.interface.embeddings.base import embedding_creator @@ -13,7 +14,6 @@ from langflow.interface.retrievers.base import retriever_creator from langflow.interface.text_splitters.base import textsplitter_creator from langflow.interface.toolkits.base import toolkits_creator from langflow.interface.tools.base import tool_creator -from langflow.interface.utilities.base import utility_creator from langflow.interface.wrappers.base import wrapper_creator @@ -48,7 +48,7 @@ def build_langchain_types_dict(): # sourcery skip: dict-assign-update-to-union # vectorstore_creator, documentloader_creator, textsplitter_creator, - utility_creator, + # utility_creator, output_parser_creator, retriever_creator, ] @@ -66,4 +66,6 @@ def get_all_types_dict(settings_service): """Get all types dictionary combining native and custom components.""" native_components = build_langchain_types_dict() custom_components_from_file = build_custom_components(settings_service) - return merge_nested_dicts_with_renaming(native_components, custom_components_from_file) + return merge_nested_dicts_with_renaming( + native_components, custom_components_from_file + ) From 78d0c122d370cadf1357f3619a12b2ea3f3928ac Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 11:56:34 -0300 Subject: [PATCH 048/199] Update settings and task service --- .../langflow/services/settings/base.py | 37 ++++++++++++----- src/backend/langflow/services/task/service.py | 40 +++++++++++-------- 2 files changed, 51 insertions(+), 26 deletions(-) diff --git a/src/backend/langflow/services/settings/base.py b/src/backend/langflow/services/settings/base.py index 93f8b3085..3a1ec956e 100644 --- a/src/backend/langflow/services/settings/base.py +++ b/src/backend/langflow/services/settings/base.py @@ -58,13 +58,17 @@ class Settings(BaseSettings): STORE: Optional[bool] = True STORE_URL: Optional[str] = "https://api.langflow.store" - DOWNLOAD_WEBHOOK_URL: Optional[ - str - ] = "https://api.langflow.store/flows/trigger/ec611a61-8460-4438-b187-a4f65e5559d4" - LIKE_WEBHOOK_URL: Optional[str] = "https://api.langflow.store/flows/trigger/64275852-ec00-45c1-984e-3bff814732da" + DOWNLOAD_WEBHOOK_URL: Optional[str] = ( + "https://api.langflow.store/flows/trigger/ec611a61-8460-4438-b187-a4f65e5559d4" + ) + LIKE_WEBHOOK_URL: Optional[str] = ( + "https://api.langflow.store/flows/trigger/64275852-ec00-45c1-984e-3bff814732da" + ) STORAGE_TYPE: str = "local" + CELERY_ENABLED: bool = False + @validator("CONFIG_DIR", pre=True, allow_reuse=True) def set_langflow_dir(cls, value): if not value: @@ -91,7 +95,9 @@ class Settings(BaseSettings): @validator("DATABASE_URL", pre=True) def set_database_url(cls, value, values): if not value: - logger.debug("No database_url provided, trying LANGFLOW_DATABASE_URL env variable") + logger.debug( + "No database_url provided, trying LANGFLOW_DATABASE_URL env variable" + ) if langflow_database_url := os.getenv("LANGFLOW_DATABASE_URL"): value = langflow_database_url logger.debug("Using LANGFLOW_DATABASE_URL env variable.") @@ -101,7 +107,9 @@ class Settings(BaseSettings): # so we need to migrate to the new format # if there is a database in that location if not values["CONFIG_DIR"]: - raise ValueError("CONFIG_DIR not set, please set it or provide a DATABASE_URL") + raise ValueError( + "CONFIG_DIR not set, please set it or provide a DATABASE_URL" + ) new_path = f"{values['CONFIG_DIR']}/langflow.db" if Path("./langflow.db").exists(): @@ -125,15 +133,22 @@ class Settings(BaseSettings): if os.getenv("LANGFLOW_COMPONENTS_PATH"): logger.debug("Adding LANGFLOW_COMPONENTS_PATH to components_path") langflow_component_path = os.getenv("LANGFLOW_COMPONENTS_PATH") - if Path(langflow_component_path).exists() and langflow_component_path not in value: + if ( + Path(langflow_component_path).exists() + and langflow_component_path not in value + ): if isinstance(langflow_component_path, list): for path in langflow_component_path: if path not in value: value.append(path) - logger.debug(f"Extending {langflow_component_path} to components_path") + logger.debug( + f"Extending {langflow_component_path} to components_path" + ) elif langflow_component_path not in value: value.append(langflow_component_path) - logger.debug(f"Appending {langflow_component_path} to components_path") + logger.debug( + f"Appending {langflow_component_path} to components_path" + ) if not value: value = [BASE_COMPONENTS_PATH] @@ -145,7 +160,9 @@ class Settings(BaseSettings): logger.debug(f"Components path: {value}") return value - model_config = SettingsConfigDict(validate_assignment=True, extra="ignore", env_prefix="LANGFLOW_") + model_config = SettingsConfigDict( + validate_assignment=True, extra="ignore", env_prefix="LANGFLOW_" + ) # @model_validator() # @classmethod diff --git a/src/backend/langflow/services/task/service.py b/src/backend/langflow/services/task/service.py index 3f7a81f2c..e7f87d3f9 100644 --- a/src/backend/langflow/services/task/service.py +++ b/src/backend/langflow/services/task/service.py @@ -1,11 +1,14 @@ -from typing import Any, Callable, Coroutine, Union +from typing import TYPE_CHECKING, Any, Callable, Coroutine, Union + +from loguru import logger from langflow.services.base import Service from langflow.services.task.backends.anyio import AnyIOBackend from langflow.services.task.backends.base import TaskBackend from langflow.services.task.utils import get_celery_worker_status -from langflow.utils.logger import configure -from loguru import logger + +if TYPE_CHECKING: + from langflow.services.settings.service import SettingsService def check_celery_availability(): @@ -20,28 +23,31 @@ def check_celery_availability(): return status -try: - configure() - status = check_celery_availability() - - USE_CELERY = status.get("availability") is not None -except ImportError: - USE_CELERY = False - - class TaskService(Service): name = "task_service" - def __init__(self): - self.backend = self.get_backend() + def __init__(self, settings_service: "SettingsService"): + self.settings_service = settings_service + try: + if self.settings_service.settings.CELERY_ENABLED: + USE_CELERY = True + status = check_celery_availability() + + USE_CELERY = status.get("availability") is not None + else: + USE_CELERY = False + except ImportError: + USE_CELERY = False + self.use_celery = USE_CELERY + self.backend = self.get_backend() @property def backend_name(self) -> str: return self.backend.name def get_backend(self) -> TaskBackend: - if USE_CELERY: + if self.use_celery: from langflow.services.task.backends.celery import CeleryBackend logger.debug("Using Celery backend") @@ -68,7 +74,9 @@ class TaskService(Service): result = await result return task.id, result - async def launch_task(self, task_func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any: + async def launch_task( + self, task_func: Callable[..., Any], *args: Any, **kwargs: Any + ) -> Any: logger.debug(f"Launching task {task_func} with args {args} and kwargs {kwargs}") logger.debug(f"Using backend {self.backend}") task = self.backend.launch_task(task_func, *args, **kwargs) From a4f5ff0daf795e98c651ba7adb920adb16099595 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 12:07:28 -0300 Subject: [PATCH 049/199] tweaks now accept passing the name of parameter which will update all nodes --- src/backend/langflow/processing/process.py | 23 +++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/backend/langflow/processing/process.py b/src/backend/langflow/processing/process.py index e2af56d21..5db886282 100644 --- a/src/backend/langflow/processing/process.py +++ b/src/backend/langflow/processing/process.py @@ -271,24 +271,25 @@ def process_tweaks( :param graph_data: The dictionary containing the graph data. It must contain a 'data' key with 'nodes' as its child or directly contain 'nodes' key. Each node should have an 'id' and 'data'. - :param tweaks: A dictionary where the key is the node id and the value is a dictionary of the tweaks. - The inner dictionary contains the name of a certain parameter as the key and the value to be tweaked. - + :param tweaks: The dictionary containing the tweaks. The keys can be the node id or the name of the tweak. + The values can be a dictionary containing the tweaks for the node or the value of the tweak. :return: The modified graph_data dictionary. :raises ValueError: If the input is not in the expected format. """ nodes = validate_input(graph_data, tweaks) + nodes_map = {node.get("id"): node for node in nodes} + + all_nodes_tweaks = {} + for key, value in tweaks.items(): + if isinstance(value, dict): + if node := nodes_map.get(key): + apply_tweaks(node, value) + else: + all_nodes_tweaks[key] = value for node in nodes: - if isinstance(node, dict) and isinstance(node.get("id"), str): - node_id = node["id"] - if node_tweaks := tweaks.get(node_id): - apply_tweaks(node, node_tweaks) - else: - logger.warning( - "Each node should be a dictionary with an 'id' key of type str" - ) + apply_tweaks(node, all_nodes_tweaks) return graph_data From e1ded9c1067b4dc20c643c14c779dd5dca55df91 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 12:24:49 -0300 Subject: [PATCH 050/199] Add support for specifying outputs in run_flow_with_caching and run_graph --- src/backend/langflow/api/v1/endpoints.py | 6 +++++ src/backend/langflow/graph/graph/base.py | 26 ++++++++++++++-------- src/backend/langflow/processing/process.py | 8 ++++++- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/backend/langflow/api/v1/endpoints.py b/src/backend/langflow/api/v1/endpoints.py index bd07d38b2..09114f3ae 100644 --- a/src/backend/langflow/api/v1/endpoints.py +++ b/src/backend/langflow/api/v1/endpoints.py @@ -57,6 +57,7 @@ async def run_flow_with_caching( session: Annotated[Session, Depends(get_session)], flow_id: str, inputs: Optional[InputValueRequest] = None, + outputs: Optional[List[str]] = None, tweaks: Optional[dict] = None, stream: Annotated[bool, Body(embed=True)] = False, # noqa: F821 session_id: Annotated[Union[None, str], Body(embed=True)] = None, # noqa: F821 @@ -69,6 +70,9 @@ async def run_flow_with_caching( else: input_values_dict = {} + if outputs is None: + outputs = [] + if session_id: session_data = await session_service.load_session( session_id, flow_id=flow_id @@ -82,6 +86,7 @@ async def run_flow_with_caching( flow_id=flow_id, session_id=session_id, inputs=input_values_dict, + outputs=outputs, artifacts=artifacts, session_service=session_service, stream=stream, @@ -107,6 +112,7 @@ async def run_flow_with_caching( flow_id=flow_id, session_id=session_id, inputs=input_values_dict, + outputs=outputs, artifacts={}, session_service=session_service, stream=stream, diff --git a/src/backend/langflow/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py index f08cd1193..f8d740f51 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -151,7 +151,7 @@ class Graph: getattr(self, f"_{attribute}_vertices").append(vertex.id) async def _run( - self, inputs: Dict[str, str], stream: bool, session_id: str + self, inputs: Dict[str, str], outputs: list[str], stream: bool, session_id: str ) -> List[Optional["ResultData"]]: """Runs the graph with the given inputs.""" for vertex_id in self._is_input_vertices: @@ -171,7 +171,7 @@ class Graph: except Exception as exc: logger.exception(exc) raise ValueError(f"Error running graph: {exc}") from exc - outputs = [] + vertex_outputs = [] for vertex_id in self._is_output_vertices: vertex = self.get_vertex(vertex_id) if vertex is None: @@ -183,11 +183,16 @@ class Graph: and hasattr(vertex, "consume_async_generator") ): await vertex.consume_async_generator() - outputs.append(vertex.result) - return outputs + if vertex.display_name in outputs or vertex.id in outputs: + vertex_outputs.append(vertex.result) + return vertex_outputs async def run( - self, inputs: Dict[str, Union[str, list[str]]], stream: bool, session_id: str + self, + inputs: Dict[str, Union[str, list[str]]], + outputs: list[str], + stream: bool, + session_id: str, ) -> List[Optional["ResultData"]]: """Runs the graph with the given inputs.""" @@ -195,17 +200,20 @@ class Graph: # we need to go through self.inputs and update the self._raw_params # of the vertices that are inputs # if the value is a list, we need to run multiple times - outputs = [] + vertex_outputs = [] inputs_values = inputs.get(INPUT_FIELD_NAME, "") if not isinstance(inputs_values, list): inputs_values = [inputs_values] for input_value in inputs_values: run_outputs = await self._run( - {INPUT_FIELD_NAME: input_value}, stream=stream, session_id=session_id + {INPUT_FIELD_NAME: input_value}, + outputs, + stream=stream, + session_id=session_id, ) logger.debug(f"Run outputs: {run_outputs}") - outputs.extend(run_outputs) - return outputs + vertex_outputs.append(run_outputs) + return vertex_outputs # vertices_layers is a list of lists ordered by the order the vertices # should be built. diff --git a/src/backend/langflow/processing/process.py b/src/backend/langflow/processing/process.py index 5db886282..8ef4ae50f 100644 --- a/src/backend/langflow/processing/process.py +++ b/src/backend/langflow/processing/process.py @@ -204,6 +204,7 @@ async def run_graph( stream: bool, session_id: Optional[str] = None, inputs: Optional[dict[str, Union[List[str], str]]] = None, + outputs: Optional[List[str]] = None, artifacts: Optional[Dict[str, Any]] = None, session_service: Optional[SessionService] = None, ): @@ -220,7 +221,12 @@ async def run_graph( if inputs is None: inputs = {} - outputs = await graph.run(inputs, stream=stream, session_id=session_id) + outputs = await graph.run( + inputs, + outputs, + stream=stream, + session_id=session_id, + ) if session_id and session_service: session_service.update_session(session_id, (graph, artifacts)) return outputs, session_id From 6b66834990bae39c333289d0d367fa248cbd6b91 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 12:40:56 -0300 Subject: [PATCH 051/199] Refactor API endpoints and schemas --- src/backend/langflow/api/v1/endpoints.py | 24 ++++++++---------------- src/backend/langflow/api/v1/schemas.py | 17 ++++++++++++++++- src/backend/langflow/graph/graph/base.py | 17 ++++++++++++++--- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/src/backend/langflow/api/v1/endpoints.py b/src/backend/langflow/api/v1/endpoints.py index 09114f3ae..f2f0d401a 100644 --- a/src/backend/langflow/api/v1/endpoints.py +++ b/src/backend/langflow/api/v1/endpoints.py @@ -7,28 +7,20 @@ from loguru import logger from sqlmodel import Session, select from langflow.api.utils import update_frontend_node_with_template_values -from langflow.api.v1.schemas import ( - CustomComponentCode, - InputValueRequest, - ProcessResponse, - RunResponse, - TaskStatusResponse, - UploadFileResponse, -) +from langflow.api.v1.schemas import (CustomComponentCode, InputValueRequest, + ProcessResponse, RunResponse, + TaskStatusResponse, UploadFileResponse) from langflow.interface.custom.custom_component import CustomComponent from langflow.interface.custom.directory_reader import DirectoryReader from langflow.interface.custom.utils import build_custom_component_template from langflow.processing.process import process_tweaks, run_graph -from langflow.services.auth.utils import api_key_security, get_current_active_user +from langflow.services.auth.utils import (api_key_security, + get_current_active_user) from langflow.services.cache.utils import save_uploaded_file from langflow.services.database.models.flow import Flow from langflow.services.database.models.user.model import User -from langflow.services.deps import ( - get_session, - get_session_service, - get_settings_service, - get_task_service, -) +from langflow.services.deps import (get_session, get_session_service, + get_settings_service, get_task_service) from langflow.services.session.service import SessionService from langflow.services.task.service import TaskService @@ -56,7 +48,7 @@ def get_all( async def run_flow_with_caching( session: Annotated[Session, Depends(get_session)], flow_id: str, - inputs: Optional[InputValueRequest] = None, + inputs: Optional[List[InputValueRequest]] = None, outputs: Optional[List[str]] = None, tweaks: Optional[dict] = None, stream: Annotated[bool, Body(embed=True)] = False, # noqa: F821 diff --git a/src/backend/langflow/api/v1/schemas.py b/src/backend/langflow/api/v1/schemas.py index 433d57a37..97d4722ec 100644 --- a/src/backend/langflow/api/v1/schemas.py +++ b/src/backend/langflow/api/v1/schemas.py @@ -245,4 +245,19 @@ class VerticesBuiltResponse(BaseModel): class InputValueRequest(BaseModel): - input_value: str + components: Optional[List[str]] = None + input_value: Optional[List[str]] = None + + # add an example + model_config = { + "json_schema_extra": { + "examples": [ + {"components": ["components_id"], "input_value": ["input_value"]}, + {"components": ["Component Name"], "input_value": ["input_value"]}, + {"input_value": ["input_value"]}, + { + "input_value": ["input_value1", "input_value2"], + }, + ] + } + } diff --git a/src/backend/langflow/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py index f8d740f51..32c1369d8 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -151,11 +151,21 @@ class Graph: getattr(self, f"_{attribute}_vertices").append(vertex.id) async def _run( - self, inputs: Dict[str, str], outputs: list[str], stream: bool, session_id: str + self, + inputs: Dict[str, str], + input_components: list[str], + outputs: list[str], + stream: bool, + session_id: str, ) -> List[Optional["ResultData"]]: """Runs the graph with the given inputs.""" for vertex_id in self._is_input_vertices: vertex = self.get_vertex(vertex_id) + if input_components and ( + vertex_id not in input_components + or vertex.display_name not in input_components + ): + continue if vertex is None: raise ValueError(f"Vertex {vertex_id} not found") vertex.update_raw_params(inputs, overwrite=True) @@ -206,8 +216,9 @@ class Graph: inputs_values = [inputs_values] for input_value in inputs_values: run_outputs = await self._run( - {INPUT_FIELD_NAME: input_value}, - outputs, + inputs={INPUT_FIELD_NAME: input_value}, + input_components=inputs.get("components", []), + outputs=outputs, stream=stream, session_id=session_id, ) From b4d859db667a15636b351a89ffb70f548d4dd382 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 12:57:18 -0300 Subject: [PATCH 052/199] Update InputValueRequest schema with additional component name --- src/backend/langflow/api/v1/schemas.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/backend/langflow/api/v1/schemas.py b/src/backend/langflow/api/v1/schemas.py index 97d4722ec..d641d3052 100644 --- a/src/backend/langflow/api/v1/schemas.py +++ b/src/backend/langflow/api/v1/schemas.py @@ -252,7 +252,10 @@ class InputValueRequest(BaseModel): model_config = { "json_schema_extra": { "examples": [ - {"components": ["components_id"], "input_value": ["input_value"]}, + { + "components": ["components_id", "Component Name"], + "input_value": ["input_value"], + }, {"components": ["Component Name"], "input_value": ["input_value"]}, {"input_value": ["input_value"]}, { From 5e1488471d37050f6a8f187d8672a043439218a9 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 14:20:21 -0300 Subject: [PATCH 053/199] Refactor API endpoints and add new schemas --- src/backend/langflow/api/v1/endpoints.py | 62 +++++++++++++++++++++--- src/backend/langflow/api/v1/schemas.py | 43 +++++++++++++--- 2 files changed, 89 insertions(+), 16 deletions(-) diff --git a/src/backend/langflow/api/v1/endpoints.py b/src/backend/langflow/api/v1/endpoints.py index f2f0d401a..c28d06bcd 100644 --- a/src/backend/langflow/api/v1/endpoints.py +++ b/src/backend/langflow/api/v1/endpoints.py @@ -7,20 +7,29 @@ from loguru import logger from sqlmodel import Session, select from langflow.api.utils import update_frontend_node_with_template_values -from langflow.api.v1.schemas import (CustomComponentCode, InputValueRequest, - ProcessResponse, RunResponse, - TaskStatusResponse, UploadFileResponse) +from langflow.api.v1.schemas import ( + CustomComponentCode, + InputValueRequest, + ProcessResponse, + RunResponse, + TaskStatusResponse, + Tweaks, + UploadFileResponse, +) from langflow.interface.custom.custom_component import CustomComponent from langflow.interface.custom.directory_reader import DirectoryReader from langflow.interface.custom.utils import build_custom_component_template from langflow.processing.process import process_tweaks, run_graph -from langflow.services.auth.utils import (api_key_security, - get_current_active_user) +from langflow.services.auth.utils import api_key_security, get_current_active_user from langflow.services.cache.utils import save_uploaded_file from langflow.services.database.models.flow import Flow from langflow.services.database.models.user.model import User -from langflow.services.deps import (get_session, get_session_service, - get_settings_service, get_task_service) +from langflow.services.deps import ( + get_session, + get_session_service, + get_settings_service, + get_task_service, +) from langflow.services.session.service import SessionService from langflow.services.task.service import TaskService @@ -50,12 +59,49 @@ async def run_flow_with_caching( flow_id: str, inputs: Optional[List[InputValueRequest]] = None, outputs: Optional[List[str]] = None, - tweaks: Optional[dict] = None, + tweaks: Annotated[Optional[Tweaks], Body(embed=True)] = None, # noqa: F821 stream: Annotated[bool, Body(embed=True)] = False, # noqa: F821 session_id: Annotated[Union[None, str], Body(embed=True)] = None, # noqa: F821 api_key_user: User = Depends(api_key_security), session_service: SessionService = Depends(get_session_service), ): + """ + Executes a specified flow by ID with optional input values, output selection, tweaks, and streaming capability. + This endpoint supports running flows with caching to enhance performance and efficiency. + + ### Parameters: + - `flow_id` (str): The unique identifier of the flow to be executed. + - `inputs` (List[InputValueRequest], optional): A list of inputs specifying the input values and components for the flow. Each input can target specific components and provide custom values. + - `outputs` (List[str], optional): A list of output names to retrieve from the executed flow. If not provided, all outputs are returned. + - `tweaks` (Optional[Tweaks], optional): A dictionary of tweaks to customize the flow execution. The tweaks can be used to modify the flow's parameters and components. Tweaks can be overridden by the input values. + - `stream` (bool, optional): Specifies whether the results should be streamed. Defaults to False. + - `session_id` (Union[None, str], optional): An optional session ID to utilize existing session data for the flow execution. + - `api_key_user` (User): The user associated with the current API key. Automatically resolved from the API key. + - `session_service` (SessionService): The session service object for managing flow sessions. + + ### Returns: + A `RunResponse` object containing the selected outputs (or all if not specified) of the executed flow and the session ID. The structure of the response accommodates multiple inputs, providing a nested list of outputs for each input. + + ### Raises: + HTTPException: Indicates issues with finding the specified flow, invalid input formats, or internal errors during flow execution. + + ### Example usage: + ```json + POST /run/{flow_id} + Payload: + { + "inputs": [ + {"components": ["component1"], "input_value": "value1"}, + {"components": ["component3"], "input_value": "value2"} + ], + "outputs": ["Component Name", "component_id"], + "tweaks": {"parameter_name": "value", "Component Name": {"parameter_name": "value"}, "component_id": {"parameter_name": "value"}} + "stream": false + } + ``` + + This endpoint facilitates complex flow executions with customized inputs, outputs, and configurations, catering to diverse application requirements. + """ try: if inputs is not None: input_values_dict: dict[str, Union[str, list[str]]] = inputs.model_dump() diff --git a/src/backend/langflow/api/v1/schemas.py b/src/backend/langflow/api/v1/schemas.py index d641d3052..c47082540 100644 --- a/src/backend/langflow/api/v1/schemas.py +++ b/src/backend/langflow/api/v1/schemas.py @@ -4,7 +4,7 @@ from pathlib import Path from typing import Any, Dict, List, Optional, Union from uuid import UUID -from pydantic import BaseModel, Field, field_validator, model_serializer +from pydantic import BaseModel, Field, RootModel, field_validator, model_serializer from langflow.services.database.models.api_key.model import ApiKeyRead from langflow.services.database.models.base import orjson_dumps @@ -246,7 +246,7 @@ class VerticesBuiltResponse(BaseModel): class InputValueRequest(BaseModel): components: Optional[List[str]] = None - input_value: Optional[List[str]] = None + input_value: Optional[str] = None # add an example model_config = { @@ -254,13 +254,40 @@ class InputValueRequest(BaseModel): "examples": [ { "components": ["components_id", "Component Name"], - "input_value": ["input_value"], - }, - {"components": ["Component Name"], "input_value": ["input_value"]}, - {"input_value": ["input_value"]}, - { - "input_value": ["input_value1", "input_value2"], + "input_value": "input_value", }, + {"components": ["Component Name"], "input_value": "input_value"}, + {"input_value": "input_value"}, ] } } + + +class Tweaks(RootModel): + root: dict[str, Union[str, dict[str, str]]] = Field( + description="A dictionary of tweaks to adjust the flow's execution. Allows customizing flow behavior dynamically. All tweaks are overridden by the input values.", + ) + model_config = { + "json_schema_extra": { + "examples": [ + { + "parameter_name": "value", + "Component Name": {"parameter_name": "value"}, + "component_id": {"parameter_name": "value"}, + } + ] + } + } + + # This should behave like a dict + def __getitem__(self, key): + return self.root[key] + + def __setitem__(self, key, value): + self.root[key] = value + + def __delitem__(self, key): + del self.root[key] + + def items(self): + return self.root.items() From 2be300b158c250d892051358153fc50ee14150f1 Mon Sep 17 00:00:00 2001 From: cristhianzl Date: Tue, 5 Mar 2024 14:42:07 -0300 Subject: [PATCH 054/199] =?UTF-8?q?=F0=9F=94=A7=20fix(codeAreaModalCompone?= =?UTF-8?q?nt.spec.ts):=20remove=20unnecessary=20code=20that=20clicks=20an?= =?UTF-8?q?d=20checks=20showcode=20element=20=F0=9F=94=A7=20fix(codeAreaMo?= =?UTF-8?q?dalComponent.spec.ts):=20remove=20unnecessary=20code=20that=20c?= =?UTF-8?q?licks=20and=20checks=20showcode=20element=20=F0=9F=94=A7=20fix(?= =?UTF-8?q?codeAreaModalComponent.spec.ts):=20remove=20unnecessary=20code?= =?UTF-8?q?=20that=20clicks=20and=20checks=20showcode=20element=20?= =?UTF-8?q?=F0=9F=94=A7=20fix(codeAreaModalComponent.spec.ts):=20remove=20?= =?UTF-8?q?unnecessary=20code=20that=20clicks=20and=20checks=20showcode=20?= =?UTF-8?q?element=20=F0=9F=94=A7=20fix(codeAreaModalComponent.spec.ts):?= =?UTF-8?q?=20remove=20unnecessary=20code=20that=20clicks=20and=20checks?= =?UTF-8?q?=20showcode=20element=20=F0=9F=94=A7=20fix(codeAreaModalCompone?= =?UTF-8?q?nt.spec.ts):=20remove=20unnecessary=20code=20that=20clicks=20an?= =?UTF-8?q?d=20checks=20showcode=20element=20=F0=9F=94=A7=20fix(codeAreaMo?= =?UTF-8?q?dalComponent.spec.ts):=20remove=20unnecessary=20code=20that=20c?= =?UTF-8?q?licks=20and=20checks=20showcode=20element=20=F0=9F=94=A7=20fix(?= =?UTF-8?q?codeAreaModalComponent.spec.ts):=20remove=20unnecessary=20code?= =?UTF-8?q?=20that=20clicks=20and=20checks=20showcode=20element=20?= =?UTF-8?q?=F0=9F=94=A7=20fix(codeAreaModalComponent.spec.ts):=20remove=20?= =?UTF-8?q?unnecessary=20code=20that=20clicks=20and=20checks=20showcode=20?= =?UTF-8?q?element=20=F0=9F=94=A7=20fix(codeAreaModalComponent.spec.ts):?= =?UTF-8?q?=20remove=20unnecessary=20code=20that=20clicks=20and=20checks?= =?UTF-8?q?=20showcode=20element=20=F0=9F=94=A7=20fix(codeAreaModalCompone?= =?UTF-8?q?nt.spec.ts):=20remove=20unnecessary=20code=20that=20clicks=20an?= =?UTF-8?q?d=20checks=20showcode=20element=20=F0=9F=94=A7=20fix(codeAreaMo?= =?UTF-8?q?dalComponent.spec.ts):=20remove=20unnecessary=20code=20that=20c?= =?UTF-8?q?licks=20and=20checks=20showcode=20element=20=F0=9F=94=A7=20fix(?= =?UTF-8?q?codeAreaModalComponent.spec.ts):=20remove=20unnecessary=20code?= =?UTF-8?q?=20that=20clicks=20and=20checks=20showcode=20element=20?= =?UTF-8?q?=F0=9F=94=A7=20fix(codeAreaModalComponent.spec.ts):=20remove=20?= =?UTF-8?q?unnecessary=20code=20that=20clicks=20and=20checks=20showcode=20?= =?UTF-8?q?element=20=F0=9F=94=A7=20fix(codeAreaModalComponent.spec.ts):?= =?UTF-8?q?=20remove=20unnecessary=20code=20that=20clicks=20and=20checks?= =?UTF-8?q?=20showcode=20element=20=F0=9F=94=A7=20fix(codeAreaModalCompone?= =?UTF-8?q?nt.spec.ts):=20remove=20unnecessary=20code=20that=20clicks=20an?= =?UTF-8?q?d=20checks=20showcode=20element=20=F0=9F=94=A7=20fix(codeAreaMo?= =?UTF-8?q?dalComponent.spec.ts):=20remove=20unnecessary=20code=20that=20c?= =?UTF-8?q?licks=20and=20checks=20showcode=20element=20=F0=9F=94=A7=20fix(?= =?UTF-8?q?codeAreaModalComponent.spec.ts):=20remove=20unnecessary=20code?= =?UTF-8?q?=20that=20clicks=20and=20checks=20showcode=20element=20?= =?UTF-8?q?=F0=9F=94=A7=20fix(codeAreaModalComponent.spec.ts):=20remove=20?= =?UTF-8?q?unnecessary=20code=20that=20clicks=20and=20checks=20showcode=20?= =?UTF-8?q?element=20=F0=9F=94=A7=20fix(codeAreaModalComponent.spec.ts):?= =?UTF-8?q?=20remove=20unnecessary=20code=20that=20clicks=20and=20checks?= =?UTF-8?q?=20showcode=20element=20=F0=9F=94=A7=20fix(codeAreaModalCompone?= =?UTF-8?q?nt.spec.ts):=20remove=20unnecessary=20code=20that=20clicks=20an?= =?UTF-8?q?d=20checks=20showcode=20element=20=F0=9F=94=A7=20fix(codeAreaMo?= =?UTF-8?q?dalComponent.spec.ts):=20remove=20unnecessary=20code=20that=20c?= =?UTF-8?q?licks=20and=20checks=20showcode=20element=20=F0=9F=94=A7=20fix(?= =?UTF-8?q?codeAreaModalComponent.spec.ts):=20remove=20unnecessary=20code?= =?UTF-8?q?=20that=20clicks=20and=20checks=20showcode=20element=20?= =?UTF-8?q?=F0=9F=94=A7=20fix(codeAreaModalComponent.spec.ts):=20remove=20?= =?UTF-8?q?unnecessary=20code=20that=20clicks=20and=20checks=20showcode=20?= =?UTF-8?q?element=20=F0=9F=94=A7=20fix(codeAreaModalComponent.spec.ts):?= =?UTF-8?q?=20remove=20unnecessary=20code=20that=20clicks=20and=20checks?= =?UTF-8?q?=20showcode=20element=20=F0=9F=94=A7=20fix(codeAreaModalCompone?= =?UTF-8?q?nt.spec.ts):=20remove=20unnecessary=20code=20that=20clicks=20an?= =?UTF-8?q?d=20checks=20showcode=20element=20=F0=9F=94=A7=20fix(codeAreaMo?= =?UTF-8?q?dalComponent.spec.ts):=20remove=20unnecessary=20code=20that=20c?= =?UTF-8?q?licks=20and=20checks=20showcode=20element=20=F0=9F=94=A7=20fix(?= =?UTF-8?q?codeAreaModalComponent.spec.ts):=20remove=20unnecessary=20code?= =?UTF-8?q?=20that=20clicks=20and=20checks=20showcode=20element=20?= =?UTF-8?q?=F0=9F=94=A7=20fix(codeAreaModalComponent.spec.ts):=20remove=20?= =?UTF-8?q?unnecessary=20code=20that=20clicks=20and=20checks=20showcode=20?= =?UTF-8?q?element=20=F0=9F=94=A7=20fix(codeAreaModalComponent.spec.ts):?= =?UTF-8?q?=20remove=20unnecessary=20code=20that=20clicks=20and=20checks?= =?UTF-8?q?=20showcode=20element=20=F0=9F=94=A7=20fix(codeAreaModalCompone?= =?UTF-8?q?nt.spec.ts):=20remove=20unnecessary=20code=20that=20clicks=20an?= =?UTF-8?q?d=20checks=20showcode=20element=20=F0=9F=94=A7=20fix(codeAreaMo?= =?UTF-8?q?dalComponent.spec.ts):=20remove=20unnecessary=20code=20that=20c?= =?UTF-8?q?licks=20and=20checks=20showcode=20element=20=F0=9F=94=A7=20fix(?= =?UTF-8?q?codeAreaModalComponent.spec.ts):=20remove=20unnecessary=20code?= =?UTF-8?q?=20that=20clicks=20and=20checks=20showcode=20element=20?= =?UTF-8?q?=F0=9F=94=A7=20fix(codeAreaModalComponent.spec.ts):=20remove=20?= =?UTF-8?q?unnecessary=20code=20that=20clicks=20and=20checks=20showcode=20?= =?UTF-8?q?element=20=F0=9F=94=A7=20fix(codeAreaModalComponent.spec.ts):?= =?UTF-8?q?=20remove=20unnecessary=20code=20that=20clicks=20and=20checks?= =?UTF-8?q?=20showcode=20element=20=F0=9F=94=A7=20fix(codeAreaModalCompone?= =?UTF-8?q?nt.spec.ts):=20remove=20unnecessary=20code=20that=20clicks=20an?= =?UTF-8?q?d=20checks=20showcode=20element=20=F0=9F=94=A7=20fix(codeAreaMo?= =?UTF-8?q?dalComponent.spec.ts):=20remove=20unnecessary=20code=20that=20c?= =?UTF-8?q?licks=20and=20checks=20showcode=20element=20=F0=9F=94=A7=20fix(?= =?UTF-8?q?codeAreaModalComponent.spec.ts):=20remove=20unnecessary=20code?= =?UTF-8?q?=20that=20clicks=20and=20checks=20showcode=20element=20?= =?UTF-8?q?=F0=9F=94=A7=20fix(codeAreaModalComponent.spec.ts):=20remove=20?= =?UTF-8?q?unnecessary=20code=20that=20clicks=20and=20checks=20showcode=20?= =?UTF-8?q?element=20=F0=9F=94=A7=20fix(codeAreaModalComponent.spec.ts):?= =?UTF-8?q?=20remove=20unnecessary=20code=20that=20clicks=20and=20checks?= =?UTF-8?q?=20showcode=20element=20=F0=9F=94=A7=20fix(codeAreaModalCompone?= =?UTF-8?q?nt.spec.ts):=20remove=20unnecessary=20code=20that=20clicks=20an?= =?UTF-8?q?d=20checks=20showcode=20element=20=F0=9F=94=A7=20fix(codeAreaMo?= =?UTF-8?q?dalComponent.spec.ts):=20remove=20unnecessary=20code=20that=20c?= =?UTF-8?q?licks=20and=20checks=20showcode=20element=20=F0=9F=94=A7=20fix(?= =?UTF-8?q?codeAreaModalComponent.spec.ts):=20remove=20unnecessary=20code?= =?UTF-8?q?=20that=20clicks=20and=20checks=20showcode=20element=20?= =?UTF-8?q?=F0=9F=94=A7=20fix(codeAreaModalComponent.spec.ts):=20remove=20?= =?UTF-8?q?unnecessary=20code=20that=20clicks=20and=20checks=20showcode=20?= =?UTF-8?q?element=20=F0=9F=94=A7=20fix(codeAreaModalComponent.spec.ts):?= =?UTF-8?q?=20remove=20unnecessary=20code=20that=20clicks=20and=20checks?= =?UTF-8?q?=20showcode=20element=20=F0=9F=94=A7=20fix(codeAreaModalCompone?= =?UTF-8?q?nt.spec.ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🐛 fix(intComponent.spec.ts): fix test case for IntComponent to use correct input field and update assertions ✨ feat(intComponent.spec.ts): add additional test cases for IntComponent to cover more scenarios and improve test coverage --- .../end-to-end/codeAreaModalComponent.spec.ts | 31 ---- .../end-to-end/dropdownComponent.spec.ts | 124 +++++++++++++-- .../tests/end-to-end/flowPage.spec.ts | 2 +- .../tests/end-to-end/intComponent.spec.ts | 142 ++++++++++++++---- 4 files changed, 224 insertions(+), 75 deletions(-) diff --git a/src/frontend/tests/end-to-end/codeAreaModalComponent.spec.ts b/src/frontend/tests/end-to-end/codeAreaModalComponent.spec.ts index 1f0c2f4ac..5e509cb01 100644 --- a/src/frontend/tests/end-to-end/codeAreaModalComponent.spec.ts +++ b/src/frontend/tests/end-to-end/codeAreaModalComponent.spec.ts @@ -37,9 +37,6 @@ test("CodeAreaModalComponent", async ({ page }) => { await page.getByTestId("more-options-modal").click(); await page.getByTestId("edit-button-modal").click(); - await page.locator('//*[@id="showcode"]').click(); - expect(await page.locator('//*[@id="showcode"]').isChecked()).toBeFalsy(); - await page.locator('//*[@id="showdescription"]').click(); expect( await page.locator('//*[@id="showdescription"]').isChecked() @@ -53,9 +50,6 @@ test("CodeAreaModalComponent", async ({ page }) => { await page.locator('//*[@id="showreturn_direct"]').isChecked() ).toBeFalsy(); - await page.locator('//*[@id="showcode"]').click(); - expect(await page.locator('//*[@id="showcode"]').isChecked()).toBeTruthy(); - await page.locator('//*[@id="showdescription"]').click(); expect( await page.locator('//*[@id="showdescription"]').isChecked() @@ -69,9 +63,6 @@ test("CodeAreaModalComponent", async ({ page }) => { await page.locator('//*[@id="showreturn_direct"]').isChecked() ).toBeTruthy(); - await page.locator('//*[@id="showcode"]').click(); - expect(await page.locator('//*[@id="showcode"]').isChecked()).toBeFalsy(); - await page.locator('//*[@id="showdescription"]').click(); expect( await page.locator('//*[@id="showdescription"]').isChecked() @@ -85,9 +76,6 @@ test("CodeAreaModalComponent", async ({ page }) => { await page.locator('//*[@id="showreturn_direct"]').isChecked() ).toBeFalsy(); - await page.locator('//*[@id="showcode"]').click(); - expect(await page.locator('//*[@id="showcode"]').isChecked()).toBeTruthy(); - await page.locator('//*[@id="showdescription"]').click(); expect( await page.locator('//*[@id="showdescription"]').isChecked() @@ -101,9 +89,6 @@ test("CodeAreaModalComponent", async ({ page }) => { await page.locator('//*[@id="showreturn_direct"]').isChecked() ).toBeTruthy(); - await page.locator('//*[@id="showcode"]').click(); - expect(await page.locator('//*[@id="showcode"]').isChecked()).toBeFalsy(); - await page.locator('//*[@id="saveChangesBtn"]').click(); const plusButtonLocator = page.locator('//*[@id="code-input-0"]'); @@ -114,22 +99,6 @@ test("CodeAreaModalComponent", async ({ page }) => { await page.getByTestId("more-options-modal").click(); await page.getByTestId("edit-button-modal").click(); - await page.locator('//*[@id="showcode"]').click(); - expect(await page.locator('//*[@id="showcode"]').isChecked()).toBeTruthy(); - - await page.locator('//*[@id="code-area-editcode"]').click(); - - let value = await page.locator('//*[@id="codeValue"]').inputValue(); - - if ( - value != - 'def python_function(text: str) -> str: """This is a default python function that returns the input text""" return text' - ) { - expect(false).toBeTruthy(); - } - - await page.locator('//*[@id="checkAndSaveBtn"]').click(); - await page.locator('//*[@id="saveChangesBtn"]').click(); await page.getByTestId("div-generic-node").click(); diff --git a/src/frontend/tests/end-to-end/dropdownComponent.spec.ts b/src/frontend/tests/end-to-end/dropdownComponent.spec.ts index 3fdb49d54..e8b462266 100644 --- a/src/frontend/tests/end-to-end/dropdownComponent.spec.ts +++ b/src/frontend/tests/end-to-end/dropdownComponent.spec.ts @@ -43,36 +43,132 @@ test("dropDownComponent", async ({ page }) => { expect(false).toBeTruthy(); } - // showcode - await page.locator('//*[@id="showcode"]').click(); - expect(await page.locator('//*[@id="showcode"]').isChecked()).toBeTruthy(); + await page.locator('//*[@id="showcache"]').click(); + expect(await page.locator('//*[@id="showcache"]').isChecked()).toBeFalsy(); + + await page.locator('//*[@id="showcache"]').click(); + expect(await page.locator('//*[@id="showcache"]').isChecked()).toBeTruthy(); + + await page.locator('//*[@id="showcredentials_profile_name"]').click(); + expect( + await page.locator('//*[@id="showcredentials_profile_name"]').isChecked() + ).toBeFalsy(); + + await page.locator('//*[@id="showcredentials_profile_name"]').click(); + expect( + await page.locator('//*[@id="showcredentials_profile_name"]').isChecked() + ).toBeTruthy(); + + await page.locator('//*[@id="showendpoint_url"]').click(); + expect( + await page.locator('//*[@id="showendpoint_url"]').isChecked() + ).toBeFalsy(); + + await page.locator('//*[@id="showendpoint_url"]').click(); + expect( + await page.locator('//*[@id="showendpoint_url"]').isChecked() + ).toBeTruthy(); + + await page.locator('//*[@id="showmodel_kwargs"]').click(); + expect( + await page.locator('//*[@id="showmodel_kwargs"]').isChecked() + ).toBeFalsy(); + + await page.locator('//*[@id="showmodel_kwargs"]').click(); + expect( + await page.locator('//*[@id="showmodel_kwargs"]').isChecked() + ).toBeTruthy(); + + await page.locator('//*[@id="showregion_name"]').click(); + expect( + await page.locator('//*[@id="showregion_name"]').isChecked() + ).toBeFalsy(); + + await page.locator('//*[@id="showregion_name"]').click(); + expect( + await page.locator('//*[@id="showregion_name"]').isChecked() + ).toBeTruthy(); + + await page.locator('//*[@id="showstreaming"]').click(); + expect( + await page.locator('//*[@id="showstreaming"]').isChecked() + ).toBeFalsy(); + + await page.locator('//*[@id="showstreaming"]').click(); + expect( + await page.locator('//*[@id="showstreaming"]').isChecked() + ).toBeTruthy(); // showmodel_id await page.locator('//*[@id="showmodel_id"]').click(); expect(await page.locator('//*[@id="showmodel_id"]').isChecked()).toBeFalsy(); - // showcode - await page.locator('//*[@id="showcode"]').click(); - expect(await page.locator('//*[@id="showcode"]').isChecked()).toBeFalsy(); - // showmodel_id await page.locator('//*[@id="showmodel_id"]').click(); expect( await page.locator('//*[@id="showmodel_id"]').isChecked() ).toBeTruthy(); - // showcode - await page.locator('//*[@id="showcode"]').click(); - expect(await page.locator('//*[@id="showcode"]').isChecked()).toBeTruthy(); + await page.locator('//*[@id="showcache"]').click(); + expect(await page.locator('//*[@id="showcache"]').isChecked()).toBeFalsy(); + + await page.locator('//*[@id="showcache"]').click(); + expect(await page.locator('//*[@id="showcache"]').isChecked()).toBeTruthy(); + + await page.locator('//*[@id="showcredentials_profile_name"]').click(); + expect( + await page.locator('//*[@id="showcredentials_profile_name"]').isChecked() + ).toBeFalsy(); + + await page.locator('//*[@id="showcredentials_profile_name"]').click(); + expect( + await page.locator('//*[@id="showcredentials_profile_name"]').isChecked() + ).toBeTruthy(); + + await page.locator('//*[@id="showendpoint_url"]').click(); + expect( + await page.locator('//*[@id="showendpoint_url"]').isChecked() + ).toBeFalsy(); + + await page.locator('//*[@id="showendpoint_url"]').click(); + expect( + await page.locator('//*[@id="showendpoint_url"]').isChecked() + ).toBeTruthy(); + + await page.locator('//*[@id="showmodel_kwargs"]').click(); + expect( + await page.locator('//*[@id="showmodel_kwargs"]').isChecked() + ).toBeFalsy(); + + await page.locator('//*[@id="showmodel_kwargs"]').click(); + expect( + await page.locator('//*[@id="showmodel_kwargs"]').isChecked() + ).toBeTruthy(); + + await page.locator('//*[@id="showregion_name"]').click(); + expect( + await page.locator('//*[@id="showregion_name"]').isChecked() + ).toBeFalsy(); + + await page.locator('//*[@id="showregion_name"]').click(); + expect( + await page.locator('//*[@id="showregion_name"]').isChecked() + ).toBeTruthy(); + + await page.locator('//*[@id="showstreaming"]').click(); + expect( + await page.locator('//*[@id="showstreaming"]').isChecked() + ).toBeFalsy(); + + await page.locator('//*[@id="showstreaming"]').click(); + expect( + await page.locator('//*[@id="showstreaming"]').isChecked() + ).toBeTruthy(); // showmodel_id await page.locator('//*[@id="showmodel_id"]').click(); expect(await page.locator('//*[@id="showmodel_id"]').isChecked()).toBeFalsy(); - // showcode - await page.locator('//*[@id="showcode"]').click(); - expect(await page.locator('//*[@id="showcode"]').isChecked()).toBeFalsy(); - // showmodel_id await page.locator('//*[@id="showmodel_id"]').click(); expect( diff --git a/src/frontend/tests/end-to-end/flowPage.spec.ts b/src/frontend/tests/end-to-end/flowPage.spec.ts index 7b509864f..78c7b3280 100644 --- a/src/frontend/tests/end-to-end/flowPage.spec.ts +++ b/src/frontend/tests/end-to-end/flowPage.spec.ts @@ -19,7 +19,7 @@ test.describe("Flow Page tests", () => { await page.waitForTimeout(2000); await page - .locator('//*[@id="custom_componentsCustomComponent"]') + .locator('//*[@id="utilitiesCustomComponent"]') .dragTo(page.locator('//*[@id="react-flow-id"]')); await page.mouse.up(); await page.mouse.down(); diff --git a/src/frontend/tests/end-to-end/intComponent.spec.ts b/src/frontend/tests/end-to-end/intComponent.spec.ts index 0cf355832..1acd958a7 100644 --- a/src/frontend/tests/end-to-end/intComponent.spec.ts +++ b/src/frontend/tests/end-to-end/intComponent.spec.ts @@ -8,32 +8,32 @@ test("IntComponent", async ({ page }) => { await page.waitForTimeout(2000); await page.getByPlaceholder("Search").click(); - await page.getByPlaceholder("Search").fill("getrequest"); + await page.getByPlaceholder("Search").fill("openai"); await page.waitForTimeout(2000); await page - .getByTestId("utilitiesGET Request") + .getByTestId("modelsOpenAI Model") .first() .dragTo(page.locator('//*[@id="react-flow-id"]')); await page.mouse.up(); await page.mouse.down(); - await page.getByTestId("int-input-timeout").click(); + await page.getByTestId("int-input-max_tokens").click(); await page - .getByTestId("int-input-timeout") + .getByTestId("int-input-max_tokens") .fill("123456789123456789123456789"); - let value = await page.getByTestId("int-input-timeout").inputValue(); + let value = await page.getByTestId("int-input-max_tokens").inputValue(); if (value != "123456789123456789123456789") { expect(false).toBeTruthy(); } - await page.getByTestId("int-input-timeout").click(); - await page.getByTestId("int-input-timeout").fill("0"); + await page.getByTestId("int-input-max_tokens").click(); + await page.getByTestId("int-input-max_tokens").fill("0"); - value = await page.getByTestId("int-input-timeout").inputValue(); + value = await page.getByTestId("int-input-max_tokens").inputValue(); if (value != "0") { expect(false).toBeTruthy(); @@ -42,35 +42,119 @@ test("IntComponent", async ({ page }) => { await page.getByTestId("more-options-modal").click(); await page.getByTestId("edit-button-modal").click(); - value = await page.getByTestId("edit-int-input-timeout").inputValue(); + value = await page.getByTestId("edit-int-input-max_tokens").inputValue(); if (value != "0") { expect(false).toBeTruthy(); } - await page.getByTestId("edit-int-input-timeout").click(); + await page.getByTestId("edit-int-input-max_tokens").click(); await page - .getByTestId("edit-int-input-timeout") + .getByTestId("edit-int-input-max_tokens") .fill("123456789123456789123456789"); - await page.locator('//*[@id="showheaders"]').click(); - expect(await page.locator('//*[@id="showheaders"]').isChecked()).toBeFalsy(); + await page.locator('//*[@id="showinput_value"]').click(); + expect( + await page.locator('//*[@id="showinput_value"]').isChecked() + ).toBeFalsy(); - await page.locator('//*[@id="showtimeout"]').click(); - expect(await page.locator('//*[@id="showtimeout"]').isChecked()).toBeFalsy(); + await page.locator('//*[@id="showmodel_kwargs"]').click(); + expect( + await page.locator('//*[@id="showmodel_kwargs"]').isChecked() + ).toBeTruthy(); - await page.locator('//*[@id="showurl"]').click(); - expect(await page.locator('//*[@id="showurl"]').isChecked()).toBeFalsy(); + await page.locator('//*[@id="showmodel_name"]').click(); + expect( + await page.locator('//*[@id="showmodel_name"]').isChecked() + ).toBeFalsy(); - await page.locator('//*[@id="showheaders"]').click(); - expect(await page.locator('//*[@id="showheaders"]').isChecked()).toBeTruthy(); + await page.locator('//*[@id="showopenai_api_base"]').click(); + expect( + await page.locator('//*[@id="showopenai_api_base"]').isChecked() + ).toBeFalsy(); - await page.locator('//*[@id="showurl"]').click(); - expect(await page.locator('//*[@id="showurl"]').isChecked()).toBeTruthy(); + await page.locator('//*[@id="showopenai_api_key"]').click(); + expect( + await page.locator('//*[@id="showopenai_api_key"]').isChecked() + ).toBeFalsy(); + + await page.locator('//*[@id="showstream"]').click(); + expect(await page.locator('//*[@id="showstream"]').isChecked()).toBeFalsy(); + + await page.locator('//*[@id="showtemperature"]').click(); + expect( + await page.locator('//*[@id="showtemperature"]').isChecked() + ).toBeFalsy(); + + await page.locator('//*[@id="showinput_value"]').click(); + expect( + await page.locator('//*[@id="showinput_value"]').isChecked() + ).toBeTruthy(); + + await page.locator('//*[@id="showmodel_kwargs"]').click(); + expect( + await page.locator('//*[@id="showmodel_kwargs"]').isChecked() + ).toBeFalsy(); + + await page.locator('//*[@id="showmodel_name"]').click(); + expect( + await page.locator('//*[@id="showmodel_name"]').isChecked() + ).toBeTruthy(); + + await page.locator('//*[@id="showopenai_api_base"]').click(); + expect( + await page.locator('//*[@id="showopenai_api_base"]').isChecked() + ).toBeTruthy(); + + await page.locator('//*[@id="showopenai_api_key"]').click(); + expect( + await page.locator('//*[@id="showopenai_api_key"]').isChecked() + ).toBeTruthy(); + + await page.locator('//*[@id="showstream"]').click(); + expect(await page.locator('//*[@id="showstream"]').isChecked()).toBeTruthy(); + + await page.locator('//*[@id="showtemperature"]').click(); + expect( + await page.locator('//*[@id="showtemperature"]').isChecked() + ).toBeTruthy(); + + await page.locator('//*[@id="showinput_value"]').click(); + expect( + await page.locator('//*[@id="showinput_value"]').isChecked() + ).toBeFalsy(); + + await page.locator('//*[@id="showmodel_kwargs"]').click(); + expect( + await page.locator('//*[@id="showmodel_kwargs"]').isChecked() + ).toBeTruthy(); + + await page.locator('//*[@id="showmodel_name"]').click(); + expect( + await page.locator('//*[@id="showmodel_name"]').isChecked() + ).toBeFalsy(); + + await page.locator('//*[@id="showopenai_api_base"]').click(); + expect( + await page.locator('//*[@id="showopenai_api_base"]').isChecked() + ).toBeFalsy(); + + await page.locator('//*[@id="showopenai_api_key"]').click(); + expect( + await page.locator('//*[@id="showopenai_api_key"]').isChecked() + ).toBeFalsy(); + + await page.locator('//*[@id="showstream"]').click(); + expect(await page.locator('//*[@id="showstream"]').isChecked()).toBeFalsy(); + + await page.locator('//*[@id="showtemperature"]').click(); + expect( + await page.locator('//*[@id="showtemperature"]').isChecked() + ).toBeFalsy(); await page.locator('//*[@id="saveChangesBtn"]').click(); - const plusButtonLocator = page.getByTestId("int-input-timeout"); + const plusButtonLocator = page.getByTestId("int-input-max_tokens"); const elementCount = await plusButtonLocator.count(); if (elementCount === 0) { expect(true).toBeTruthy(); @@ -84,7 +168,7 @@ test("IntComponent", async ({ page }) => { ).toBeTruthy(); const valueEditNode = await page - .getByTestId("edit-int-input-timeout") + .getByTestId("edit-int-input-max_tokens") .inputValue(); if (valueEditNode != "123456789123456789123456789") { @@ -92,19 +176,19 @@ test("IntComponent", async ({ page }) => { } await page.locator('//*[@id="saveChangesBtn"]').click(); - await page.getByTestId("int-input-timeout").click(); - await page.getByTestId("int-input-timeout").fill("3"); + await page.getByTestId("int-input-max_tokens").click(); + await page.getByTestId("int-input-max_tokens").fill("3"); - let value = await page.getByTestId("int-input-timeout").inputValue(); + let value = await page.getByTestId("int-input-max_tokens").inputValue(); if (value != "3") { expect(false).toBeTruthy(); } - await page.getByTestId("int-input-timeout").click(); - await page.getByTestId("int-input-timeout").fill("-3"); + await page.getByTestId("int-input-max_tokens").click(); + await page.getByTestId("int-input-max_tokens").fill("-3"); - value = await page.getByTestId("int-input-timeout").inputValue(); + value = await page.getByTestId("int-input-max_tokens").inputValue(); if (value != "0") { expect(false).toBeTruthy(); From 240b1900b97ae0bd965fdf15055334701ebc3357 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 16:29:03 -0300 Subject: [PATCH 055/199] Add update_build_config method that updates any part of the template --- docs/docs/components/custom.mdx | 10 +- src/backend/langflow/api/v1/endpoints.py | 5 +- src/backend/langflow/api/v1/schemas.py | 1 + src/backend/langflow/api/v1/validate.py | 65 +++++++---- .../custom/code_parser/code_parser.py | 40 +++++-- .../custom_component/custom_component.py | 26 ++--- .../langflow/interface/custom/utils.py | 108 ++++++++++++++---- src/backend/langflow/template/field/prompt.py | 14 +++ src/backend/langflow/utils/validate.py | 60 +++++++--- .../components/parameterComponent/index.tsx | 37 +++--- src/frontend/src/controllers/API/index.ts | 4 +- 11 files changed, 257 insertions(+), 113 deletions(-) create mode 100644 src/backend/langflow/template/field/prompt.py diff --git a/docs/docs/components/custom.mdx b/docs/docs/components/custom.mdx index d8c6ff2f5..9fb0fe689 100644 --- a/docs/docs/components/custom.mdx +++ b/docs/docs/components/custom.mdx @@ -83,15 +83,13 @@ The CustomComponent class serves as the foundation for creating custom component | _`file_types: List[str]`_ | This is a requirement if the _`field_type`_ is _file_. Defines which file types will be accepted. For example, _json_, _yaml_ or _yml_. | | _`range_spec: langflow.field_typing.RangeSpec`_ | This is a requirement if the _`field_type`_ is _`float`_. Defines the range of values accepted and the step size. If none is defined, the default is _`[-1, 1, 0.1]`_. | | _`title_case: bool`_ | Formats the name of the field when _`display_name`_ is not defined. Set it to False to keep the name as you set it in the _`build`_ method. | + | _`refresh: bool`_ | If set to True a button will appear to the right of the field, and when clicked, it will call the _`update_build_config`_ method which takes in the _`build_config`_, the name of the field (_`field_name`_) and the latest value of the field (_`field_value`_). This is useful when you want to update the _`build_config`_ based on the value of the field. | - - - Keys _`options`_ and _`value`_ can receive a method or function that returns a list of strings or a string, respectively. This is useful when you want to dynamically generate the options or the default value of a field. A refresh button will appear next to the field in the component, allowing the user to update the options or the default value. - - - + +By using the _`update_build_config`_ method, you can update the _`build_config`_ in whatever way you want based on the value of the field or not. + - The CustomComponent class also provides helpful methods for specific tasks (e.g., to load and use other flows from the Langflow platform): diff --git a/src/backend/langflow/api/v1/endpoints.py b/src/backend/langflow/api/v1/endpoints.py index c28d06bcd..b77685d65 100644 --- a/src/backend/langflow/api/v1/endpoints.py +++ b/src/backend/langflow/api/v1/endpoints.py @@ -306,7 +306,10 @@ async def custom_component_update( component = CustomComponent(code=raw_code.code) component_node = build_custom_component_template( - component, user_id=user.id, update_field=raw_code.field + component, + user_id=user.id, + update_field=raw_code.field, + update_field_value=raw_code.field_value, ) # Update the field return component_node diff --git a/src/backend/langflow/api/v1/schemas.py b/src/backend/langflow/api/v1/schemas.py index c47082540..17274bcfc 100644 --- a/src/backend/langflow/api/v1/schemas.py +++ b/src/backend/langflow/api/v1/schemas.py @@ -166,6 +166,7 @@ class StreamData(BaseModel): class CustomComponentCode(BaseModel): code: str field: Optional[str] = None + field_value: Optional[str] = None frontend_node: Optional[dict] = None diff --git a/src/backend/langflow/api/v1/validate.py b/src/backend/langflow/api/v1/validate.py index b062c1329..6b64faf20 100644 --- a/src/backend/langflow/api/v1/validate.py +++ b/src/backend/langflow/api/v1/validate.py @@ -1,4 +1,6 @@ from fastapi import APIRouter, HTTPException +from loguru import logger + from langflow.api.v1.base import ( Code, CodeValidationResponse, @@ -6,9 +8,8 @@ from langflow.api.v1.base import ( ValidatePromptRequest, validate_prompt, ) -from langflow.template.field.base import TemplateField -from langflow.utils.validate import PROMPT_INPUT_TYPES, validate_code -from loguru import logger +from langflow.template.field.prompt import DefaultPromptField +from langflow.utils.validate import validate_code # build router router = APIRouter(prefix="/validate", tags=["Validate"]) @@ -40,7 +41,9 @@ def post_validate_prompt(prompt_request: ValidatePromptRequest): add_new_variables_to_template(input_variables, prompt_request) - remove_old_variables_from_template(old_custom_fields, input_variables, prompt_request) + remove_old_variables_from_template( + old_custom_fields, input_variables, prompt_request + ) update_input_variables_field(input_variables, prompt_request) @@ -55,12 +58,19 @@ def post_validate_prompt(prompt_request: ValidatePromptRequest): def get_old_custom_fields(prompt_request): try: - if len(prompt_request.frontend_node.custom_fields) == 1 and prompt_request.name == "": + if ( + len(prompt_request.frontend_node.custom_fields) == 1 + and prompt_request.name == "" + ): # If there is only one custom field and the name is empty string # then we are dealing with the first prompt request after the node was created - prompt_request.name = list(prompt_request.frontend_node.custom_fields.keys())[0] + prompt_request.name = list( + prompt_request.frontend_node.custom_fields.keys() + )[0] - old_custom_fields = prompt_request.frontend_node.custom_fields[prompt_request.name] + old_custom_fields = prompt_request.frontend_node.custom_fields[ + prompt_request.name + ] if old_custom_fields is None: old_custom_fields = [] @@ -74,38 +84,43 @@ def get_old_custom_fields(prompt_request): def add_new_variables_to_template(input_variables, prompt_request): for variable in input_variables: try: - template_field = TemplateField( - name=variable, - display_name=variable, - field_type="str", - show=True, - advanced=False, - multiline=True, - input_types=PROMPT_INPUT_TYPES, - value="", # Set the value to empty string - ) + template_field = DefaultPromptField(name=variable, display_name=variable) if variable in prompt_request.frontend_node.template: # Set the new field with the old value - template_field.value = prompt_request.frontend_node.template[variable]["value"] + template_field.value = prompt_request.frontend_node.template[variable][ + "value" + ] prompt_request.frontend_node.template[variable] = template_field.to_dict() # Check if variable is not already in the list before appending - if variable not in prompt_request.frontend_node.custom_fields[prompt_request.name]: - prompt_request.frontend_node.custom_fields[prompt_request.name].append(variable) + if ( + variable + not in prompt_request.frontend_node.custom_fields[prompt_request.name] + ): + prompt_request.frontend_node.custom_fields[prompt_request.name].append( + variable + ) except Exception as exc: logger.exception(exc) raise HTTPException(status_code=500, detail=str(exc)) from exc -def remove_old_variables_from_template(old_custom_fields, input_variables, prompt_request): +def remove_old_variables_from_template( + old_custom_fields, input_variables, prompt_request +): for variable in old_custom_fields: if variable not in input_variables: try: # Remove the variable from custom_fields associated with the given name - if variable in prompt_request.frontend_node.custom_fields[prompt_request.name]: - prompt_request.frontend_node.custom_fields[prompt_request.name].remove(variable) + if ( + variable + in prompt_request.frontend_node.custom_fields[prompt_request.name] + ): + prompt_request.frontend_node.custom_fields[ + prompt_request.name + ].remove(variable) # Remove the variable from the template prompt_request.frontend_node.template.pop(variable, None) @@ -117,4 +132,6 @@ def remove_old_variables_from_template(old_custom_fields, input_variables, promp def update_input_variables_field(input_variables, prompt_request): if "input_variables" in prompt_request.frontend_node.template: - prompt_request.frontend_node.template["input_variables"]["value"] = input_variables + prompt_request.frontend_node.template["input_variables"][ + "value" + ] = input_variables diff --git a/src/backend/langflow/interface/custom/code_parser/code_parser.py b/src/backend/langflow/interface/custom/code_parser/code_parser.py index 258d2fa6b..846edc716 100644 --- a/src/backend/langflow/interface/custom/code_parser/code_parser.py +++ b/src/backend/langflow/interface/custom/code_parser/code_parser.py @@ -95,7 +95,9 @@ class CodeParser: elif isinstance(node, ast.ImportFrom): for alias in node.names: if alias.asname: - self.data["imports"].append((node.module, f"{alias.name} as {alias.asname}")) + self.data["imports"].append( + (node.module, f"{alias.name} as {alias.asname}") + ) else: self.data["imports"].append((node.module, alias.name)) @@ -144,7 +146,9 @@ class CodeParser: return_type = None if node.returns: return_type_str = ast.unparse(node.returns) - eval_env = self.construct_eval_env(return_type_str, tuple(self.data["imports"])) + eval_env = self.construct_eval_env( + return_type_str, tuple(self.data["imports"]) + ) try: return_type = eval(return_type_str, eval_env) @@ -174,7 +178,7 @@ class CodeParser: args += self.parse_keyword_args(node) # Commented out because we don't want kwargs # showing up as fields in the frontend - # args += self.parse_kwargs(node) + args += self.parse_kwargs(node) return args @@ -186,14 +190,22 @@ class CodeParser: num_defaults = len(node.args.defaults) num_missing_defaults = num_args - num_defaults missing_defaults = [None] * num_missing_defaults - default_values = [ast.unparse(default).strip("'") if default else None for default in node.args.defaults] + default_values = [ + ast.unparse(default).strip("'") if default else None + for default in node.args.defaults + ] # Now check all default values to see if there # are any "None" values in the middle - default_values = [None if value == "None" else value for value in default_values] + default_values = [ + None if value == "None" else value for value in default_values + ] defaults = missing_defaults + default_values - args = [self.parse_arg(arg, default) for arg, default in zip(node.args.args, defaults)] + args = [ + self.parse_arg(arg, default) + for arg, default in zip(node.args.args, defaults) + ] return args def parse_varargs(self, node: ast.FunctionDef) -> List[Dict[str, Any]]: @@ -211,11 +223,17 @@ class CodeParser: """ Parses the keyword-only arguments of a function or method node. """ - kw_defaults = [None] * (len(node.args.kwonlyargs) - len(node.args.kw_defaults)) + [ - ast.unparse(default) if default else None for default in node.args.kw_defaults + kw_defaults = [None] * ( + len(node.args.kwonlyargs) - len(node.args.kw_defaults) + ) + [ + ast.unparse(default) if default else None + for default in node.args.kw_defaults ] - args = [self.parse_arg(arg, default) for arg, default in zip(node.args.kwonlyargs, kw_defaults)] + args = [ + self.parse_arg(arg, default) + for arg, default in zip(node.args.kwonlyargs, kw_defaults) + ] return args def parse_kwargs(self, node: ast.FunctionDef) -> List[Dict[str, Any]]: @@ -319,7 +337,9 @@ class CodeParser: Extracts global variables from the code. """ global_var = { - "targets": [t.id if hasattr(t, "id") else ast.dump(t) for t in node.targets], + "targets": [ + t.id if hasattr(t, "id") else ast.dump(t) for t in node.targets + ], "value": ast.unparse(node.value), } self.data["global_vars"].append(global_var) diff --git a/src/backend/langflow/interface/custom/custom_component/custom_component.py b/src/backend/langflow/interface/custom/custom_component/custom_component.py index d2913be3d..dde147d77 100644 --- a/src/backend/langflow/interface/custom/custom_component/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component/custom_component.py @@ -1,15 +1,7 @@ import operator from pathlib import Path -from typing import ( - TYPE_CHECKING, - Any, - Callable, - ClassVar, - List, - Optional, - Sequence, - Union, -) +from typing import (TYPE_CHECKING, Any, Callable, ClassVar, List, Optional, + Sequence, Union) from uuid import UUID import yaml @@ -20,17 +12,13 @@ from sqlmodel import select from langflow.interface.custom.code_parser.utils import ( extract_inner_type_from_generic_alias, - extract_union_types_from_generic_alias, -) + extract_union_types_from_generic_alias) from langflow.interface.custom.custom_component.component import Component from langflow.schema import Record from langflow.services.database.models.flow import Flow from langflow.services.database.utils import session_getter -from langflow.services.deps import ( - get_credential_service, - get_db_service, - get_storage_service, -) +from langflow.services.deps import (get_credential_service, get_db_service, + get_storage_service) from langflow.services.storage.service import StorageService from langflow.utils import validate @@ -138,6 +126,10 @@ class CustomComponent(Component): def build_config(self): return self.field_config + def update_build_config(self, build_config: dict, field_name: str, field_value: Any): + build_config[field_name] = field_value + return build_config + @property def tree(self): return self.get_code_tree(self.code or "") diff --git a/src/backend/langflow/interface/custom/utils.py b/src/backend/langflow/interface/custom/utils.py index 85f636811..513669f75 100644 --- a/src/backend/langflow/interface/custom/utils.py +++ b/src/backend/langflow/interface/custom/utils.py @@ -8,6 +8,7 @@ from uuid import UUID from fastapi import HTTPException from loguru import logger +from pydantic import BaseModel from langflow.field_typing.range_spec import RangeSpec from langflow.interface.custom.attributes import ATTR_FUNC_MAPPING @@ -27,6 +28,10 @@ from langflow.utils import validate from langflow.utils.util import get_base_classes +class UpdateBuildConfigError(Exception): + pass + + def add_output_types( frontend_node: CustomComponentFrontendNode, return_types: List[str] ): @@ -190,15 +195,23 @@ def add_extra_fields(frontend_node, field_config, function_args): """Add extra fields to the frontend node""" if not function_args: return + _field_config = field_config.copy() + function_args_names = [arg["name"] for arg in function_args] + # If kwargs is in the function_args and not all field_config keys are in function_args + # then we need to add the extra fields for extra_field in function_args: - if "name" not in extra_field or extra_field["name"] == "self": + if "name" not in extra_field or extra_field["name"] in [ + "self", + "kwargs", + "args", + ]: continue field_name, field_type, field_value, field_required = get_field_properties( extra_field ) - config = field_config.get(field_name, {}) + config = _field_config.pop(field_name, {}) frontend_node = add_new_custom_field( frontend_node, field_name, @@ -207,6 +220,23 @@ def add_extra_fields(frontend_node, field_config, function_args): field_required, config, ) + if "kwargs" in function_args_names and not all( + key in function_args_names for key in field_config.keys() + ): + for field_name, field_config in _field_config.copy().items(): + config = _field_config.get(field_name, {}) + config = config.model_dump() if isinstance(config, BaseModel) else config + field_name, field_type, field_value, field_required = get_field_properties( + extra_field=config + ) + frontend_node = add_new_custom_field( + frontend_node, + field_name, + field_type, + field_value, + field_required, + config, + ) def get_field_dict(field: Union[TemplateField, dict]): @@ -220,6 +250,7 @@ def run_build_config( custom_component: CustomComponent, user_id: Optional[Union[str, UUID]] = None, update_field=None, + update_field_value=None, ): """Build the field configuration for a custom component""" @@ -246,33 +277,48 @@ def run_build_config( custom_instance = custom_class(user_id=user_id) build_config: Dict = custom_instance.build_config() - for field_name, field in build_config.items(): + for field_name, field in build_config.copy().items(): # 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: + build_config = update_field_dict( + custom_component_instance=custom_instance, + field_dict=field_dict, + build_config=build_config, + call=False, + ) continue try: - update_field_dict(field_dict, call=True) + build_config = update_field_dict( + custom_component_instance=custom_instance, + field_dict=field_dict, + build_config=build_config, + update_field=update_field, + update_field_value=update_field_value, + call=True, + ) build_config[field_name] = field_dict except Exception as exc: logger.error(f"Error while getting build_config: {str(exc)}") + if isinstance(exc, UpdateBuildConfigError): + message = str(exc) + else: + message = f"Error while getting build_config: {str(exc)}" + raise HTTPException( + status_code=400, + detail={ + "error": message, + "traceback": traceback.format_exc(), + }, + ) from exc return build_config, custom_instance except Exception as exc: logger.error(f"Error while building field config: {str(exc)}") - raise HTTPException( - status_code=400, - detail={ - "error": ( - "Invalid type convertion. Please check your code and try again." - ), - "traceback": traceback.format_exc(), - }, - ) from exc + raise exc def sanitize_template_config(template_config): @@ -317,13 +363,17 @@ def build_custom_component_template( custom_component: CustomComponent, user_id: Optional[Union[str, UUID]] = None, update_field: Optional[str] = None, + update_field_value: Optional[str] = None, ) -> Optional[Dict[str, Any]]: """Build a custom component template for the langchain""" try: frontend_node = build_frontend_node(custom_component.template_config) field_config, custom_instance = run_build_config( - custom_component, user_id=user_id, update_field=update_field + custom_component, + user_id=user_id, + update_field=update_field, + update_field_value=update_field_value, ) entrypoint_args = custom_component.get_function_entrypoint_args @@ -402,22 +452,32 @@ def build_custom_components(settings_service): return custom_components_from_file -def update_field_dict(field_dict, call=False): +def update_field_dict( + custom_component_instance: "CustomComponent", + field_dict: Dict, + build_config: Dict, + update_field: Optional[str] = None, + update_field_value: Optional[Any] = None, + call: bool = False, +): """Update the field dictionary by calling options() or value() if they are callable""" - if "options" in field_dict and callable(field_dict["options"]): + if "refresh" in field_dict: 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"]): - if call: - field_dict["value"] = field_dict["value"]() + try: + custom_component_instance.update_build_config( + build_config, update_field, update_field_value + ) + except Exception as exc: + logger.error(f"Error while running update_build_config: {str(exc)}") + raise UpdateBuildConfigError( + f"Error while running update_build_config: {str(exc)}" + ) from exc field_dict["refresh"] = True # Let's check if "range_spec" is a RangeSpec object if "rangeSpec" in field_dict and isinstance(field_dict["rangeSpec"], RangeSpec): field_dict["rangeSpec"] = field_dict["rangeSpec"].model_dump() + return build_config def sanitize_field_config(field_config: Dict): diff --git a/src/backend/langflow/template/field/prompt.py b/src/backend/langflow/template/field/prompt.py new file mode 100644 index 000000000..89a4bf745 --- /dev/null +++ b/src/backend/langflow/template/field/prompt.py @@ -0,0 +1,14 @@ +from typing import Optional + +from langflow.template.field.base import TemplateField + + +class DefaultPromptField(TemplateField): + name: str + display_name: Optional[str] = None + field_type: str = "str" + + advanced: bool = False + multiline: bool = True + input_types: list[str] = ["Document", "BaseOutputParser", "Text", "Record"] + value: str = "" # Set the value to empty string diff --git a/src/backend/langflow/utils/validate.py b/src/backend/langflow/utils/validate.py index 21821538c..9611d3a56 100644 --- a/src/backend/langflow/utils/validate.py +++ b/src/backend/langflow/utils/validate.py @@ -6,8 +6,6 @@ from typing import Dict, List, Optional, Union from langflow.field_typing.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES -PROMPT_INPUT_TYPES = ["Document", "BaseOutputParser", "Text", "Record"] - def add_type_ignores(): if not hasattr(ast, "TypeIgnore"): @@ -45,7 +43,9 @@ def validate_code(code): # Evaluate the function definition for node in tree.body: if isinstance(node, ast.FunctionDef): - code_obj = compile(ast.Module(body=[node], type_ignores=[]), "", "exec") + code_obj = compile( + ast.Module(body=[node], type_ignores=[]), "", "exec" + ) try: exec(code_obj) except Exception as e: @@ -89,15 +89,23 @@ def execute_function(code, function_name, *args, **kwargs): exec_globals, locals(), ) - exec_globals[alias.asname or alias.name] = importlib.import_module(alias.name) + exec_globals[alias.asname or alias.name] = importlib.import_module( + alias.name + ) except ModuleNotFoundError as e: - raise ModuleNotFoundError(f"Module {alias.name} not found. Please install it and try again.") from e + raise ModuleNotFoundError( + f"Module {alias.name} not found. Please install it and try again." + ) from e function_code = next( - node for node in module.body if isinstance(node, ast.FunctionDef) and node.name == function_name + node + for node in module.body + if isinstance(node, ast.FunctionDef) and node.name == function_name ) function_code.parent = None - code_obj = compile(ast.Module(body=[function_code], type_ignores=[]), "", "exec") + code_obj = compile( + ast.Module(body=[function_code], type_ignores=[]), "", "exec" + ) try: exec(code_obj, exec_globals, locals()) except Exception as exc: @@ -124,15 +132,23 @@ def create_function(code, function_name): if isinstance(node, ast.Import): for alias in node.names: try: - exec_globals[alias.asname or alias.name] = importlib.import_module(alias.name) + exec_globals[alias.asname or alias.name] = importlib.import_module( + alias.name + ) except ModuleNotFoundError as e: - raise ModuleNotFoundError(f"Module {alias.name} not found. Please install it and try again.") from e + raise ModuleNotFoundError( + f"Module {alias.name} not found. Please install it and try again." + ) from e function_code = next( - node for node in module.body if isinstance(node, ast.FunctionDef) and node.name == function_name + node + for node in module.body + if isinstance(node, ast.FunctionDef) and node.name == function_name ) function_code.parent = None - code_obj = compile(ast.Module(body=[function_code], type_ignores=[]), "", "exec") + code_obj = compile( + ast.Module(body=[function_code], type_ignores=[]), "", "exec" + ) with contextlib.suppress(Exception): exec(code_obj, exec_globals, locals()) exec_globals[function_name] = locals()[function_name] @@ -194,9 +210,13 @@ def prepare_global_scope(code, module): if isinstance(node, ast.Import): for alias in node.names: try: - exec_globals[alias.asname or alias.name] = importlib.import_module(alias.name) + exec_globals[alias.asname or alias.name] = importlib.import_module( + alias.name + ) except ModuleNotFoundError as e: - raise ModuleNotFoundError(f"Module {alias.name} not found. Please install it and try again.") from e + raise ModuleNotFoundError( + f"Module {alias.name} not found. Please install it and try again." + ) from e elif isinstance(node, ast.ImportFrom) and node.module is not None: try: imported_module = importlib.import_module(node.module) @@ -217,7 +237,11 @@ def extract_class_code(module, class_name): :param class_name: Name of the class to extract :return: AST node of the specified class """ - class_code = next(node for node in module.body if isinstance(node, ast.ClassDef) and node.name == class_name) + class_code = next( + node + for node in module.body + if isinstance(node, ast.ClassDef) and node.name == class_name + ) class_code.parent = None return class_code @@ -230,7 +254,9 @@ def compile_class_code(class_code): :param class_code: AST node of the class :return: Compiled code object of the class """ - code_obj = compile(ast.Module(body=[class_code], type_ignores=[]), "", "exec") + code_obj = compile( + ast.Module(body=[class_code], type_ignores=[]), "", "exec" + ) return code_obj @@ -274,7 +300,9 @@ def get_default_imports(code_string): langflow_imports = list(CUSTOM_COMPONENT_SUPPORTED_TYPES.keys()) necessary_imports = find_names_in_code(code_string, langflow_imports) langflow_module = importlib.import_module("langflow.field_typing") - default_imports.update({name: getattr(langflow_module, name) for name in necessary_imports}) + default_imports.update( + {name: getattr(langflow_module, name) for name in necessary_imports} + ) return default_imports diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 83d916a9d..7a6c306f1 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -98,24 +98,33 @@ export default function ParameterComponent({ return; } - try { - const res = await postCustomComponentUpdate(code, name); - if (res.status === 200 && data.node?.template) { - setNode(data.id, (oldNode) => { - let newNode = cloneDeep(oldNode); + await postCustomComponentUpdate( + code, + name, + data.node?.template[name]?.value + ) + .then((res) => { + if (res.status === 200 && data.node?.template) { + setNode(data.id, (oldNode) => { + let newNode = cloneDeep(oldNode); - newNode.data = { - ...newNode.data, - }; + newNode.data = { + ...newNode.data, + }; - newNode.data.node.template[name] = res.data.template[name]; + newNode.data.node.template = res.data.template; - return newNode; + return newNode; + }); + } + }) + .catch((error) => { + console.error("Error occurred while updating the node:", error); + setErrorData({ + title: "Error while updating the Component", + list: [error.response.data.detail.error ?? "Unknown error"], }); - } - } catch (err) { - setErrorData(err as { title: string; list?: Array }); - } + }); renderTooltips(); if (delayAnimation) { diff --git a/src/frontend/src/controllers/API/index.ts b/src/frontend/src/controllers/API/index.ts index ff6305cfd..6ee4141a0 100644 --- a/src/frontend/src/controllers/API/index.ts +++ b/src/frontend/src/controllers/API/index.ts @@ -369,11 +369,13 @@ export async function postCustomComponent( export async function postCustomComponentUpdate( code: string, - field: string + field: string, + field_value: string ): Promise> { return await api.post(`${BASE_URL_API}custom_component/update`, { code, field, + field_value, }); } From f8e9df61eac73b734c3abc8161b6f52d815c20a9 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 17:34:11 -0300 Subject: [PATCH 056/199] Implement getting all flows without a user_id as example flows --- src/backend/langflow/api/v1/flows.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/backend/langflow/api/v1/flows.py b/src/backend/langflow/api/v1/flows.py index 517ff33c1..de021d091 100644 --- a/src/backend/langflow/api/v1/flows.py +++ b/src/backend/langflow/api/v1/flows.py @@ -10,7 +10,12 @@ from sqlmodel import Session, select from langflow.api.utils import remove_api_keys, validate_is_component from langflow.api.v1.schemas import FlowListCreate, FlowListRead from langflow.services.auth.utils import get_current_active_user -from langflow.services.database.models.flow import Flow, FlowCreate, FlowRead, FlowUpdate +from langflow.services.database.models.flow import ( + Flow, + FlowCreate, + FlowRead, + FlowUpdate, +) from langflow.services.database.models.user.model import User from langflow.services.deps import get_session, get_settings_service @@ -42,11 +47,15 @@ def create_flow( def read_flows( *, current_user: User = Depends(get_current_active_user), + session: Session = Depends(get_session), ): """Read all flows.""" try: flows = current_user.flows flows = validate_is_component(flows) + # with the session get the flows that DO NOT have a user_id + example_flows = session.exec(select(Flow).where(Flow.user_id == None)).all() + flows.extend(example_flows) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) from e return [jsonable_encoder(flow) for flow in flows] @@ -60,7 +69,11 @@ def read_flow( current_user: User = Depends(get_current_active_user), ): """Read a flow.""" - if user_flow := (session.exec(select(Flow).where(Flow.id == flow_id, Flow.user_id == current_user.id)).first()): + if user_flow := ( + session.exec( + select(Flow).where(Flow.id == flow_id, Flow.user_id == current_user.id) + ).first() + ): return user_flow else: raise HTTPException(status_code=404, detail="Flow not found") @@ -109,9 +122,6 @@ def delete_flow( return {"message": "Flow deleted successfully"} -# Define a new model to handle multiple flows - - @router.post("/batch/", response_model=List[FlowRead], status_code=201) def create_flows( *, From 4875ac57c86ffd4b44a6d00077a315c2584d6615 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 17:36:09 -0300 Subject: [PATCH 057/199] Update Langflow documentation with new component descriptions --- docs/docs/getting-started/creating-flows.mdx | 23 +++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/docs/docs/getting-started/creating-flows.mdx b/docs/docs/getting-started/creating-flows.mdx index aecc3ea16..9c16d225f 100644 --- a/docs/docs/getting-started/creating-flows.mdx +++ b/docs/docs/getting-started/creating-flows.mdx @@ -7,7 +7,8 @@ import ReactPlayer from "react-player"; ## Compose -Creating flows with Langflow is easy. Drag sidebar components onto the canvas and connect them together to create your pipeline. Langflow provides a range of [LangChain components](https://python.langchain.com/docs/modules/) to choose from, including LLMs, prompt serializers, agents, and chains. +Creating flows with Langflow is easy. Drag sidebar components onto the canvas and connect them together to create your pipeline. +Langflow provides a range of Components to choose from, including **Chat Input**, **Chat Output**, **API Request** and **Prompt**. -## Fork +## Starter Flows -The easiest way to start with Langflow is by forking a **community example**. Forking an example stores a copy in your project collection, allowing you to edit and save the modified version as a new flow. +Langflow provides a range of starter flows to help you get started. These flows are pre-built and can be used as a starting point for your own flows.
-## Build +## Defining Inputs and Outputs + +Each flow can have multiple inputs and outputs. These can be defined by placing **Inputs** and **Outputs** components on the canvas. + +The **Inputs** components define the inputs to the flow. +Whenever you place an Input component on the canvas, it will allow you to interactively define change its value +from the Interactive Panel. + +The **Text Input** component allows you to define a text input, and the **Chat Input** component allows you to use the chat input from the Interactive Panel. + +The **Outputs** components define the outputs of the flow and work similarly to the Inputs components. + +Both Inputs and Outputs components can be connected to other components on the canvas and are used to define how the API works too. + -Building a flow means validating if the components have prerequisites fulfilled and are properly instantiated. When a chat message is sent, the flow will run for the first time, executing the pipeline.
Date: Tue, 5 Mar 2024 17:41:39 -0300 Subject: [PATCH 058/199] Refactor flow reading logic and handle exception when retrieving example flows --- src/backend/langflow/api/v1/flows.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/backend/langflow/api/v1/flows.py b/src/backend/langflow/api/v1/flows.py index de021d091..c58168d34 100644 --- a/src/backend/langflow/api/v1/flows.py +++ b/src/backend/langflow/api/v1/flows.py @@ -5,17 +5,14 @@ from uuid import UUID import orjson from fastapi import APIRouter, Depends, File, HTTPException, UploadFile from fastapi.encoders import jsonable_encoder +from loguru import logger from sqlmodel import Session, select from langflow.api.utils import remove_api_keys, validate_is_component from langflow.api.v1.schemas import FlowListCreate, FlowListRead from langflow.services.auth.utils import get_current_active_user -from langflow.services.database.models.flow import ( - Flow, - FlowCreate, - FlowRead, - FlowUpdate, -) +from langflow.services.database.models.flow import (Flow, FlowCreate, FlowRead, + FlowUpdate) from langflow.services.database.models.user.model import User from langflow.services.deps import get_session, get_settings_service @@ -54,8 +51,11 @@ def read_flows( flows = current_user.flows flows = validate_is_component(flows) # with the session get the flows that DO NOT have a user_id - example_flows = session.exec(select(Flow).where(Flow.user_id == None)).all() - flows.extend(example_flows) + try: + example_flows = session.exec(select(Flow).where(Flow.user_id == None)).all() + flows.extend(example_flows) + except Exception as e: + logger.error(e) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) from e return [jsonable_encoder(flow) for flow in flows] From 42313fe1c1094cffcc90f161f9a8805470f875e1 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 17:42:06 -0300 Subject: [PATCH 059/199] Add logging to read_flows function --- src/backend/langflow/api/v1/flows.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/backend/langflow/api/v1/flows.py b/src/backend/langflow/api/v1/flows.py index de021d091..371e320f4 100644 --- a/src/backend/langflow/api/v1/flows.py +++ b/src/backend/langflow/api/v1/flows.py @@ -5,6 +5,7 @@ from uuid import UUID import orjson from fastapi import APIRouter, Depends, File, HTTPException, UploadFile from fastapi.encoders import jsonable_encoder +from loguru import logger from sqlmodel import Session, select from langflow.api.utils import remove_api_keys, validate_is_component @@ -54,8 +55,11 @@ def read_flows( flows = current_user.flows flows = validate_is_component(flows) # with the session get the flows that DO NOT have a user_id - example_flows = session.exec(select(Flow).where(Flow.user_id == None)).all() - flows.extend(example_flows) + try: + example_flows = session.exec(select(Flow).where(Flow.user_id == None)).all() + flows.extend(example_flows) + except Exception as e: + logger.error(e) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) from e return [jsonable_encoder(flow) for flow in flows] From cbf6dfae4d2b4c8d8c34230165e72c655a1f8d03 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 17:44:04 -0300 Subject: [PATCH 060/199] Fix exception variable name in DatabaseService --- src/backend/langflow/services/database/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/langflow/services/database/service.py b/src/backend/langflow/services/database/service.py index 3cecdd340..c4b06b90f 100644 --- a/src/backend/langflow/services/database/service.py +++ b/src/backend/langflow/services/database/service.py @@ -169,7 +169,7 @@ class DatabaseService(Service): try: command.check(alembic_cfg) - except util.exc.AutogenerateDiffsDetected as e: + except util.exc.AutogenerateDiffsDetected as exc: logger.error(f"AutogenerateDiffsDetected: {exc}") if not fix: raise RuntimeError( From 1d2364935823c8c090ea7462d17d47f06a339a0f Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 17:44:48 -0300 Subject: [PATCH 061/199] Fix exception variable name in database service --- src/backend/langflow/services/database/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/langflow/services/database/service.py b/src/backend/langflow/services/database/service.py index c4b06b90f..c1e214700 100644 --- a/src/backend/langflow/services/database/service.py +++ b/src/backend/langflow/services/database/service.py @@ -174,7 +174,7 @@ class DatabaseService(Service): if not fix: raise RuntimeError( "Something went wrong running migrations. Please, run `langflow migration --fix`" - ) from e + ) from exc if fix: self.try_downgrade_upgrade_until_success(alembic_cfg) From ac8f2b7070289af54ad366132428fd22ba883b64 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 17:46:17 -0300 Subject: [PATCH 062/199] Update user_id field to be optional in FlowRead class --- src/backend/langflow/services/database/models/flow/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/langflow/services/database/models/flow/model.py b/src/backend/langflow/services/database/models/flow/model.py index 1211c40ed..c7ee08141 100644 --- a/src/backend/langflow/services/database/models/flow/model.py +++ b/src/backend/langflow/services/database/models/flow/model.py @@ -80,7 +80,7 @@ class FlowCreate(FlowBase): class FlowRead(FlowBase): id: UUID - user_id: UUID = Field() + user_id: Optional[UUID] = Field() class FlowUpdate(SQLModel): From e454624036c14f9a8da673903194fd9e01f79a3c Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 17:46:59 -0300 Subject: [PATCH 063/199] Update user_id field to be optional in FlowRead class --- src/backend/langflow/services/database/models/flow/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/langflow/services/database/models/flow/model.py b/src/backend/langflow/services/database/models/flow/model.py index 1211c40ed..c7ee08141 100644 --- a/src/backend/langflow/services/database/models/flow/model.py +++ b/src/backend/langflow/services/database/models/flow/model.py @@ -80,7 +80,7 @@ class FlowCreate(FlowBase): class FlowRead(FlowBase): id: UUID - user_id: UUID = Field() + user_id: Optional[UUID] = Field() class FlowUpdate(SQLModel): From 245648c8156d00859be750440b5487474c075f92 Mon Sep 17 00:00:00 2001 From: igorrCarvalho Date: Tue, 5 Mar 2024 18:25:48 -0300 Subject: [PATCH 064/199] Refactor: UI changes --- .../src/CustomNodes/GenericNode/index.tsx | 3 +- .../extraSidebarComponent/utils.tsx | 3 +- .../components/nodeToolbarComponent/index.tsx | 2 +- src/frontend/src/utils/styleUtils.ts | 29 ++++++++++++------- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index 80e5ad0f7..ddfc04aef 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -44,6 +44,7 @@ export default function GenericNode({ const buildFlow = useFlowStore((state) => state.buildFlow); const setNode = useFlowStore((state) => state.setNode); const name = nodeIconsLucide[data.type] ? data.type : types[data.type]; + console.log(types[data.type]) const [inputName, setInputName] = useState(false); const [nodeName, setNodeName] = useState(data.node!.display_name); const [inputDescription, setInputDescription] = useState(false); @@ -157,7 +158,7 @@ export default function GenericNode({ const iconElement = data?.node?.icon; const iconColor = nodeColors[types[data.type]]; const iconName = - iconElement || (data.node?.flow ? "group_components" : name); + iconElement || (data.node?.flow ? "group_components" : name); const iconClassName = `generic-node-icon ${ !showNode ? " absolute inset-x-6 h-12 w-12 " : "" }`; diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/utils.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/utils.tsx index 6261e87bb..f8b5a0af9 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/utils.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/utils.tsx @@ -4,9 +4,10 @@ export function sortKeys(a: string, b: string) { "saved_components", "inputs", "outputs", + "prompts", "data", - "utilities", "models", + "utilities", ]; const indexA = order.indexOf(a.toLowerCase()); const indexB = order.indexOf(b.toLowerCase()); diff --git a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx index 0dc36eac7..8733e4528 100644 --- a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx @@ -219,7 +219,7 @@ export default function NodeToolbarComponent({ }} data-testid="code-button-modal" > - + ) : ( diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index 601066407..dc7115838 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -3,11 +3,13 @@ import { ArrowLeft, ArrowUpToLine, Bell, + Binary, BookMarked, BookmarkPlus, Bot, Boxes, Braces, + BrainCircuit, Check, CheckCircle2, ChevronDown, @@ -28,6 +30,7 @@ import { Compass, Copy, Cpu, + Database, Delete, Download, DownloadCloud, @@ -40,6 +43,7 @@ import { FileDown, FileSearch, FileSearch2, + FileSliders, FileText, FileType2, FileUp, @@ -79,12 +83,14 @@ import { MoonIcon, MoreHorizontal, Network, + Package2, Paperclip, Pencil, PencilLine, Pin, Play, Plus, + PocketKnife, Redo, RefreshCcw, Repeat, @@ -120,6 +126,7 @@ import { UserPlus2, Users2, Variable, + Wand, Wand2, Workflow, Wrench, @@ -242,7 +249,7 @@ export const nodeNames: { [char: string]: string } = { outputs: "Outputs", data: "Data", prompts: "Prompts", - models: "Language Models", + models: "Models", model_specs: "Model Specs", chains: "Chains", agents: "Agents", @@ -258,8 +265,8 @@ export const nodeNames: { [char: string]: string } = { wrappers: "Wrappers", textsplitters: "Text Splitters", retrievers: "Retrievers", - utilities: "Utilities", - langchain_utilities: "Langchain Utilities", + utilities: "Helpers", + langchain_utilities: "Utilities", output_parsers: "Output Parsers", custom_components: "Custom", unknown: "Other", @@ -268,7 +275,7 @@ export const nodeNames: { [char: string]: string } = { export const nodeIconsLucide: iconsType = { inputs: Download, outputs: Upload, - data: FolderOpen, + data: Database, AzureChatOpenAi: AzureIcon, Ollama: OllamaIcon, ChatOllama: OllamaIcon, @@ -331,27 +338,27 @@ export const nodeIconsLucide: iconsType = { VertexAIEmbeddings: VertexAIIcon, Share3: ShareIcon, Share4: Share2Icon, - agents: Rocket, + agents: Bot, Workflow, User, WikipediaAPIWrapper: SvgWikipedia, chains: Link, memories: Cpu, - models: Bot, - model_specs: Lightbulb, + models: BrainCircuit, + model_specs: FileSliders, prompts: TerminalSquare, - tools: Wrench, + tools: Hammer, advanced: Laptop2, chat: MessageCircle, - embeddings: Fingerprint, + embeddings: Binary, saved_components: GradientSave, documentloaders: Paperclip, vectorstores: Layers, - toolkits: Hammer, + toolkits: Package2, textsplitters: Scissors, wrappers: Gift, utilities: Wand2, - langchain_utilities: Wand2, + langchain_utilities: PocketKnife, WolframAlphaAPIWrapper: SvgWolfram, output_parsers: Compass, retrievers: FileSearch, From dce9901222bd4dd9ca9f8fa14d492e08648732bf Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 18:46:18 -0300 Subject: [PATCH 065/199] Remove "documents" configuration from vector store components --- .../textsplitters/CharacterTextSplitter.py | 20 +++++++++----- .../LanguageRecursiveTextSplitter.py | 27 +++++++++++-------- .../RecursiveCharacterTextSplitter.py | 27 ++++++++++++------- .../components/vectorstores/Chroma.py | 9 ++++--- .../components/vectorstores/ChromaSearch.py | 1 - .../langflow/components/vectorstores/FAISS.py | 13 ++++++--- .../components/vectorstores/FAISSSearch.py | 1 - .../vectorstores/MongoDBAtlasVector.py | 21 +++++++++++---- .../components/vectorstores/Pinecone.py | 13 ++++++--- .../components/vectorstores/Qdrant.py | 24 +++++++++++++---- .../langflow/components/vectorstores/Redis.py | 19 +++++++++---- .../components/vectorstores/RedisSearch.py | 2 +- .../vectorstores/SupabaseVectorStore.py | 20 ++++++++++---- .../components/vectorstores/Vectara.py | 23 +++++++++++----- .../components/vectorstores/VectaraSearch.py | 4 --- .../components/vectorstores/Weaviate.py | 19 +++++++++---- .../components/vectorstores/WeaviateSearch.py | 1 - .../components/vectorstores/pgvector.py | 17 +++++++++--- 18 files changed, 183 insertions(+), 78 deletions(-) diff --git a/src/backend/langflow/components/textsplitters/CharacterTextSplitter.py b/src/backend/langflow/components/textsplitters/CharacterTextSplitter.py index d165f47fd..96576a4a3 100644 --- a/src/backend/langflow/components/textsplitters/CharacterTextSplitter.py +++ b/src/backend/langflow/components/textsplitters/CharacterTextSplitter.py @@ -1,8 +1,9 @@ from typing import List from langchain.text_splitter import CharacterTextSplitter -from langchain_core.documents.base import Document + from langflow import CustomComponent +from langflow.schema.schema import Record class CharacterTextSplitterComponent(CustomComponent): @@ -11,7 +12,7 @@ class CharacterTextSplitterComponent(CustomComponent): def build_config(self): return { - "documents": {"display_name": "Documents"}, + "inputs": {"display_name": "Input", "input_types": ["Document", "Record"]}, "chunk_overlap": {"display_name": "Chunk Overlap", "default": 200}, "chunk_size": {"display_name": "Chunk Size", "default": 1000}, "separator": {"display_name": "Separator", "default": "\n"}, @@ -19,17 +20,24 @@ class CharacterTextSplitterComponent(CustomComponent): def build( self, - documents: List[Document], + inputs: List[Record], chunk_overlap: int = 200, chunk_size: int = 1000, separator: str = "\n", - ) -> List[Document]: + ) -> List[Record]: # separator may come escaped from the frontend separator = separator.encode().decode("unicode_escape") + documents = [] + for _input in inputs: + if isinstance(_input, Record): + documents.append(_input.to_lc_document()) + else: + documents.append(_input) docs = CharacterTextSplitter( chunk_overlap=chunk_overlap, chunk_size=chunk_size, separator=separator, ).split_documents(documents) - self.status = docs - return docs + records = self.to_records(docs) + self.status = records + return records diff --git a/src/backend/langflow/components/textsplitters/LanguageRecursiveTextSplitter.py b/src/backend/langflow/components/textsplitters/LanguageRecursiveTextSplitter.py index d1494f4d0..b86b834d8 100644 --- a/src/backend/langflow/components/textsplitters/LanguageRecursiveTextSplitter.py +++ b/src/backend/langflow/components/textsplitters/LanguageRecursiveTextSplitter.py @@ -1,23 +1,22 @@ -from typing import Optional +from typing import List, Optional from langchain.text_splitter import Language -from langchain_core.documents import Document from langflow import CustomComponent +from langflow.schema.schema import Record class LanguageRecursiveTextSplitterComponent(CustomComponent): display_name: str = "Language Recursive Text Splitter" description: str = "Split text into chunks of a specified length based on language." - documentation: str = "https://docs.langflow.org/components/text-splitters#languagerecursivetextsplitter" + documentation: str = ( + "https://docs.langflow.org/components/text-splitters#languagerecursivetextsplitter" + ) def build_config(self): options = [x.value for x in Language] return { - "documents": { - "display_name": "Documents", - "info": "The documents to split.", - }, + "inputs": {"display_name": "Input", "input_types": ["Document", "Record"]}, "separator_type": { "display_name": "Separator Type", "info": "The type of separator to use.", @@ -47,11 +46,11 @@ class LanguageRecursiveTextSplitterComponent(CustomComponent): def build( self, - documents: list[Document], + inputs: List[Record], chunk_size: Optional[int] = 1000, chunk_overlap: Optional[int] = 200, separator_type: str = "Python", - ) -> list[Document]: + ) -> list[Record]: """ Split text into chunks of a specified length. @@ -77,6 +76,12 @@ class LanguageRecursiveTextSplitterComponent(CustomComponent): chunk_size=chunk_size, chunk_overlap=chunk_overlap, ) - + documents = [] + for _input in inputs: + if isinstance(_input, Record): + documents.append(_input.to_lc_document()) + else: + documents.append(_input) docs = splitter.split_documents(documents) - return docs + records = self.to_records(docs) + return records diff --git a/src/backend/langflow/components/textsplitters/RecursiveCharacterTextSplitter.py b/src/backend/langflow/components/textsplitters/RecursiveCharacterTextSplitter.py index d07ae3ebe..a15600394 100644 --- a/src/backend/langflow/components/textsplitters/RecursiveCharacterTextSplitter.py +++ b/src/backend/langflow/components/textsplitters/RecursiveCharacterTextSplitter.py @@ -1,22 +1,26 @@ from typing import Optional +from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_core.documents import Document from langflow import CustomComponent +from langflow.schema import Record from langflow.utils.util import build_loader_repr_from_documents -from langchain.text_splitter import RecursiveCharacterTextSplitter class RecursiveCharacterTextSplitterComponent(CustomComponent): display_name: str = "Recursive Character Text Splitter" description: str = "Split text into chunks of a specified length." - documentation: str = "https://docs.langflow.org/components/text-splitters#recursivecharactertextsplitter" + documentation: str = ( + "https://docs.langflow.org/components/text-splitters#recursivecharactertextsplitter" + ) def build_config(self): return { - "documents": { - "display_name": "Documents", - "info": "The documents to split.", + "inputs": { + "display_name": "Input", + "info": "The texts to split.", + "input_types": ["Document", "Record"], }, "separators": { "display_name": "Separators", @@ -40,11 +44,11 @@ class RecursiveCharacterTextSplitterComponent(CustomComponent): def build( self, - documents: list[Document], + inputs: list[Document], separators: Optional[list[str]] = None, chunk_size: Optional[int] = 1000, chunk_overlap: Optional[int] = 200, - ) -> list[Document]: + ) -> list[Record]: """ Split text into chunks of a specified length. @@ -75,7 +79,12 @@ class RecursiveCharacterTextSplitterComponent(CustomComponent): chunk_size=chunk_size, chunk_overlap=chunk_overlap, ) - + documents = [] + for _input in inputs: + if isinstance(_input, Record): + documents.append(_input.to_lc_document()) + else: + documents.append(_input) docs = splitter.split_documents(documents) self.repr_value = build_loader_repr_from_documents(docs) - return docs + return self.to_records(docs) diff --git a/src/backend/langflow/components/vectorstores/Chroma.py b/src/backend/langflow/components/vectorstores/Chroma.py index b1756e777..139a877e9 100644 --- a/src/backend/langflow/components/vectorstores/Chroma.py +++ b/src/backend/langflow/components/vectorstores/Chroma.py @@ -31,7 +31,7 @@ class ChromaComponent(CustomComponent): "collection_name": {"display_name": "Collection Name", "value": "langflow"}, "index_directory": {"display_name": "Persist Directory"}, "code": {"advanced": True, "display_name": "Code"}, - "documents": {"display_name": "Documents", "is_list": True}, + "inputs": {"display_name": "Input", "input_types": ["Document", "Record"]}, "embedding": {"display_name": "Embedding"}, "chroma_server_cors_allow_origins": { "display_name": "Server CORS Allow Origins", @@ -84,7 +84,8 @@ class ChromaComponent(CustomComponent): if chroma_server_host is not None: chroma_settings = chromadb.config.Settings( - chroma_server_cors_allow_origins=chroma_server_cors_allow_origins or None, + chroma_server_cors_allow_origins=chroma_server_cors_allow_origins + or None, chroma_server_host=chroma_server_host, chroma_server_port=chroma_server_port or None, chroma_server_grpc_port=chroma_server_grpc_port or None, @@ -99,7 +100,9 @@ class ChromaComponent(CustomComponent): if documents is not None and embedding is not None: if len(documents) == 0: - raise ValueError("If documents are provided, there must be at least one document.") + raise ValueError( + "If documents are provided, there must be at least one document." + ) chroma = Chroma.from_documents( documents=documents, # type: ignore persist_directory=index_directory, diff --git a/src/backend/langflow/components/vectorstores/ChromaSearch.py b/src/backend/langflow/components/vectorstores/ChromaSearch.py index 3a6d283b3..cf98ee987 100644 --- a/src/backend/langflow/components/vectorstores/ChromaSearch.py +++ b/src/backend/langflow/components/vectorstores/ChromaSearch.py @@ -35,7 +35,6 @@ class ChromaSearchComponent(LCVectorStoreComponent): # "persist": {"display_name": "Persist"}, "index_directory": {"display_name": "Index Directory"}, "code": {"show": False, "display_name": "Code"}, - "documents": {"display_name": "Documents", "is_list": True}, "embedding": { "display_name": "Embedding", "info": "Embedding model to vectorize inputs (make sure to use same as index)", diff --git a/src/backend/langflow/components/vectorstores/FAISS.py b/src/backend/langflow/components/vectorstores/FAISS.py index a0324456e..7cdadccdb 100644 --- a/src/backend/langflow/components/vectorstores/FAISS.py +++ b/src/backend/langflow/components/vectorstores/FAISS.py @@ -5,7 +5,8 @@ from langchain_community.vectorstores import VectorStore from langchain_community.vectorstores.faiss import FAISS from langflow import CustomComponent -from langflow.field_typing import Document, Embeddings +from langflow.field_typing import Embeddings +from langflow.schema.schema import Record class FAISSComponent(CustomComponent): @@ -15,7 +16,7 @@ class FAISSComponent(CustomComponent): def build_config(self): return { - "documents": {"display_name": "Documents"}, + "inputs": {"display_name": "Input", "input_types": ["Document", "Record"]}, "embedding": {"display_name": "Embedding"}, "folder_path": { "display_name": "Folder Path", @@ -27,10 +28,16 @@ class FAISSComponent(CustomComponent): def build( self, embedding: Embeddings, - documents: List[Document], + inputs: List[Record], folder_path: str, index_name: str = "langflow_index", ) -> Union[VectorStore, FAISS, BaseRetriever]: + documents = [] + for _input in inputs: + if isinstance(_input, Record): + documents.append(_input.to_lc_document()) + else: + documents.append(_input) vector_store = FAISS.from_documents(documents=documents, embedding=embedding) if not folder_path: raise ValueError("Folder path is required to save the FAISS index.") diff --git a/src/backend/langflow/components/vectorstores/FAISSSearch.py b/src/backend/langflow/components/vectorstores/FAISSSearch.py index f6ddf4f7a..4544c300b 100644 --- a/src/backend/langflow/components/vectorstores/FAISSSearch.py +++ b/src/backend/langflow/components/vectorstores/FAISSSearch.py @@ -14,7 +14,6 @@ class FAISSSearchComponent(LCVectorStoreComponent): def build_config(self): return { - "documents": {"display_name": "Documents"}, "embedding": {"display_name": "Embedding"}, "folder_path": { "display_name": "Folder Path", diff --git a/src/backend/langflow/components/vectorstores/MongoDBAtlasVector.py b/src/backend/langflow/components/vectorstores/MongoDBAtlasVector.py index e15368f7d..4488e4e92 100644 --- a/src/backend/langflow/components/vectorstores/MongoDBAtlasVector.py +++ b/src/backend/langflow/components/vectorstores/MongoDBAtlasVector.py @@ -3,17 +3,20 @@ from typing import List, Optional from langchain_community.vectorstores.mongodb_atlas import MongoDBAtlasVectorSearch from langflow import CustomComponent -from langflow.field_typing import Document, Embeddings, NestedDict +from langflow.field_typing import Embeddings, NestedDict +from langflow.schema.schema import Record class MongoDBAtlasComponent(CustomComponent): display_name = "MongoDB Atlas" - description = "Construct a `MongoDB Atlas Vector Search` vector store from raw documents." + description = ( + "Construct a `MongoDB Atlas Vector Search` vector store from raw documents." + ) icon = "MongoDB" def build_config(self): return { - "documents": {"display_name": "Documents"}, + "inputs": {"display_name": "Input", "input_types": ["Document", "Record"]}, "embedding": {"display_name": "Embedding"}, "collection_name": {"display_name": "Collection Name"}, "db_name": {"display_name": "Database Name"}, @@ -25,7 +28,7 @@ class MongoDBAtlasComponent(CustomComponent): def build( self, embedding: Embeddings, - documents: List[Document], + inputs: List[Record], collection_name: str = "", db_name: str = "", index_name: str = "", @@ -36,12 +39,20 @@ class MongoDBAtlasComponent(CustomComponent): try: from pymongo import MongoClient except ImportError: - raise ImportError("Please install pymongo to use MongoDB Atlas Vector Store") + raise ImportError( + "Please install pymongo to use MongoDB Atlas Vector Store" + ) try: mongo_client: MongoClient = MongoClient(mongodb_atlas_cluster_uri) collection = mongo_client[db_name][collection_name] except Exception as e: raise ValueError(f"Failed to connect to MongoDB Atlas: {e}") + documents = [] + for _input in inputs: + if isinstance(_input, Record): + documents.append(_input.to_lc_document()) + else: + documents.append(_input) if documents: vector_store = MongoDBAtlasVectorSearch.from_documents( documents=documents, diff --git a/src/backend/langflow/components/vectorstores/Pinecone.py b/src/backend/langflow/components/vectorstores/Pinecone.py index 54222b133..c71048266 100644 --- a/src/backend/langflow/components/vectorstores/Pinecone.py +++ b/src/backend/langflow/components/vectorstores/Pinecone.py @@ -7,7 +7,8 @@ from langchain_community.vectorstores import VectorStore from langchain_community.vectorstores.pinecone import Pinecone from langflow import CustomComponent -from langflow.field_typing import Document, Embeddings +from langflow.field_typing import Embeddings +from langflow.schema.schema import Record class PineconeComponent(CustomComponent): @@ -17,7 +18,7 @@ class PineconeComponent(CustomComponent): def build_config(self): return { - "documents": {"display_name": "Documents"}, + "inputs": {"display_name": "Input", "input_types": ["Document", "Record"]}, "embedding": {"display_name": "Embedding"}, "index_name": {"display_name": "Index Name"}, "namespace": {"display_name": "Namespace"}, @@ -44,7 +45,7 @@ class PineconeComponent(CustomComponent): self, embedding: Embeddings, pinecone_env: str, - documents: List[Document], + inputs: List[Record], text_key: str = "text", pool_threads: int = 4, index_name: Optional[str] = None, @@ -59,6 +60,12 @@ class PineconeComponent(CustomComponent): pinecone.init(api_key=pinecone_api_key, environment=pinecone_env) # type: ignore if not index_name: raise ValueError("Index Name is required.") + documents = [] + for _input in inputs: + if isinstance(_input, Record): + documents.append(_input.to_lc_document()) + else: + documents.append(_input) if documents: return Pinecone.from_documents( documents=documents, diff --git a/src/backend/langflow/components/vectorstores/Qdrant.py b/src/backend/langflow/components/vectorstores/Qdrant.py index 23ee70b11..e1773268b 100644 --- a/src/backend/langflow/components/vectorstores/Qdrant.py +++ b/src/backend/langflow/components/vectorstores/Qdrant.py @@ -3,8 +3,10 @@ from typing import Optional, Union from langchain.schema import BaseRetriever from langchain_community.vectorstores import VectorStore from langchain_community.vectorstores.qdrant import Qdrant + from langflow import CustomComponent -from langflow.field_typing import Document, Embeddings, NestedDict +from langflow.field_typing import Embeddings, NestedDict +from langflow.schema.schema import Record class QdrantComponent(CustomComponent): @@ -14,17 +16,23 @@ class QdrantComponent(CustomComponent): def build_config(self): return { - "documents": {"display_name": "Documents"}, + "inputs": {"display_name": "Input", "input_types": ["Document", "Record"]}, "embedding": {"display_name": "Embedding"}, "api_key": {"display_name": "API Key", "password": True, "advanced": True}, "collection_name": {"display_name": "Collection Name"}, - "content_payload_key": {"display_name": "Content Payload Key", "advanced": True}, + "content_payload_key": { + "display_name": "Content Payload Key", + "advanced": True, + }, "distance_func": {"display_name": "Distance Function", "advanced": True}, "grpc_port": {"display_name": "gRPC Port", "advanced": True}, "host": {"display_name": "Host", "advanced": True}, "https": {"display_name": "HTTPS", "advanced": True}, "location": {"display_name": "Location", "advanced": True}, - "metadata_payload_key": {"display_name": "Metadata Payload Key", "advanced": True}, + "metadata_payload_key": { + "display_name": "Metadata Payload Key", + "advanced": True, + }, "path": {"display_name": "Path", "advanced": True}, "port": {"display_name": "Port", "advanced": True}, "prefer_grpc": {"display_name": "Prefer gRPC", "advanced": True}, @@ -38,7 +46,7 @@ class QdrantComponent(CustomComponent): self, embedding: Embeddings, collection_name: str, - documents: Optional[Document] = None, + inputs: Optional[Record] = None, api_key: Optional[str] = None, content_payload_key: str = "page_content", distance_func: str = "Cosine", @@ -55,6 +63,12 @@ class QdrantComponent(CustomComponent): timeout: Optional[int] = None, url: Optional[str] = None, ) -> Union[VectorStore, Qdrant, BaseRetriever]: + documents = [] + for _input in inputs: + if isinstance(_input, Record): + documents.append(_input.to_lc_document()) + else: + documents.append(_input) if documents is None: from qdrant_client import QdrantClient diff --git a/src/backend/langflow/components/vectorstores/Redis.py b/src/backend/langflow/components/vectorstores/Redis.py index b2d7e4542..bbf04f1a4 100644 --- a/src/backend/langflow/components/vectorstores/Redis.py +++ b/src/backend/langflow/components/vectorstores/Redis.py @@ -3,9 +3,10 @@ from typing import Optional, Union from langchain.embeddings.base import Embeddings from langchain_community.vectorstores import VectorStore from langchain_community.vectorstores.redis import Redis -from langchain_core.documents import Document from langchain_core.retrievers import BaseRetriever + from langflow import CustomComponent +from langflow.schema.schema import Record class RedisComponent(CustomComponent): @@ -28,7 +29,7 @@ class RedisComponent(CustomComponent): return { "index_name": {"display_name": "Index Name", "value": "your_index"}, "code": {"show": False, "display_name": "Code"}, - "documents": {"display_name": "Documents", "is_list": True}, + "inputs": {"display_name": "Input", "input_types": ["Document", "Record"]}, "embedding": {"display_name": "Embedding"}, "schema": {"display_name": "Schema", "file_types": [".yaml"]}, "redis_server_url": { @@ -44,7 +45,7 @@ class RedisComponent(CustomComponent): redis_server_url: str, redis_index_name: str, schema: Optional[str] = None, - documents: Optional[Document] = None, + inputs: Optional[Record] = None, ) -> Union[VectorStore, BaseRetriever]: """ Builds the Vector Store or BaseRetriever object. @@ -58,9 +59,17 @@ class RedisComponent(CustomComponent): Returns: - VectorStore: The Vector Store object. """ - if documents is None: + documents = [] + for _input in inputs: + if isinstance(_input, Record): + documents.append(_input.to_lc_document()) + else: + documents.append(_input) + if not documents: if schema is None: - raise ValueError("If no documents are provided, a schema must be provided.") + raise ValueError( + "If no documents are provided, a schema must be provided." + ) redis_vs = Redis.from_existing_index( embedding=embedding, index_name=redis_index_name, diff --git a/src/backend/langflow/components/vectorstores/RedisSearch.py b/src/backend/langflow/components/vectorstores/RedisSearch.py index 4089d4f47..63fc46ddb 100644 --- a/src/backend/langflow/components/vectorstores/RedisSearch.py +++ b/src/backend/langflow/components/vectorstores/RedisSearch.py @@ -33,7 +33,7 @@ class RedisSearchComponent(RedisComponent, LCVectorStoreComponent): "input_value": {"display_name": "Input"}, "index_name": {"display_name": "Index Name", "value": "your_index"}, "code": {"show": False, "display_name": "Code"}, - "documents": {"display_name": "Documents", "is_list": True}, + "embedding": {"display_name": "Embedding"}, "schema": {"display_name": "Schema", "file_types": [".yaml"]}, "redis_server_url": { diff --git a/src/backend/langflow/components/vectorstores/SupabaseVectorStore.py b/src/backend/langflow/components/vectorstores/SupabaseVectorStore.py index 2ec6dfabc..7f6296c03 100644 --- a/src/backend/langflow/components/vectorstores/SupabaseVectorStore.py +++ b/src/backend/langflow/components/vectorstores/SupabaseVectorStore.py @@ -3,10 +3,12 @@ from typing import List, Union from langchain.schema import BaseRetriever from langchain_community.vectorstores import VectorStore from langchain_community.vectorstores.supabase import SupabaseVectorStore -from langflow import CustomComponent -from langflow.field_typing import Document, Embeddings, NestedDict from supabase.client import Client, create_client +from langflow import CustomComponent +from langflow.field_typing import Embeddings, NestedDict +from langflow.schema.schema import Record + class SupabaseComponent(CustomComponent): display_name = "Supabase" @@ -14,7 +16,7 @@ class SupabaseComponent(CustomComponent): def build_config(self): return { - "documents": {"display_name": "Documents"}, + "inputs": {"display_name": "Input", "input_types": ["Document", "Record"]}, "embedding": {"display_name": "Embedding"}, "query_name": {"display_name": "Query Name"}, "search_kwargs": {"display_name": "Search Kwargs", "advanced": True}, @@ -26,14 +28,22 @@ class SupabaseComponent(CustomComponent): def build( self, embedding: Embeddings, - documents: List[Document], + inputs: List[Record], query_name: str = "", search_kwargs: NestedDict = {}, supabase_service_key: str = "", supabase_url: str = "", table_name: str = "", ) -> Union[VectorStore, SupabaseVectorStore, BaseRetriever]: - supabase: Client = create_client(supabase_url, supabase_key=supabase_service_key) + supabase: Client = create_client( + supabase_url, supabase_key=supabase_service_key + ) + documents = [] + for _input in inputs: + if isinstance(_input, Record): + documents.append(_input.to_lc_document()) + else: + documents.append(_input) return SupabaseVectorStore.from_documents( documents=documents, embedding=embedding, diff --git a/src/backend/langflow/components/vectorstores/Vectara.py b/src/backend/langflow/components/vectorstores/Vectara.py index 0a396918c..8f1f7250e 100644 --- a/src/backend/langflow/components/vectorstores/Vectara.py +++ b/src/backend/langflow/components/vectorstores/Vectara.py @@ -8,13 +8,16 @@ from langchain_community.vectorstores.vectara import Vectara from langchain_core.vectorstores import VectorStore from langflow import CustomComponent -from langflow.field_typing import BaseRetriever, Document +from langflow.field_typing import BaseRetriever +from langflow.schema.schema import Record class VectaraComponent(CustomComponent): display_name: str = "Vectara" description: str = "Implementation of Vector Store using Vectara" - documentation = "https://python.langchain.com/docs/integrations/vectorstores/vectara" + documentation = ( + "https://python.langchain.com/docs/integrations/vectorstores/vectara" + ) beta = True icon = "Vectara" field_config = { @@ -28,8 +31,9 @@ class VectaraComponent(CustomComponent): "display_name": "Vectara API Key", "password": True, }, - "documents": { - "display_name": "Documents", + "inputs": { + "display_name": "Input", + "input_types": ["Document", "Record"], "info": "If provided, will be upserted to corpus (optional)", }, "files_url": { @@ -44,11 +48,18 @@ class VectaraComponent(CustomComponent): vectara_corpus_id: str, vectara_api_key: str, files_url: Optional[List[str]] = None, - documents: Optional[Document] = None, + inputs: Optional[Record] = None, ) -> Union[VectorStore, BaseRetriever]: source = "Langflow" - if documents is not None: + documents = [] + for _input in inputs: + if isinstance(_input, Record): + documents.append(_input.to_lc_document()) + else: + documents.append(_input) + + if documents: return Vectara.from_documents( documents=documents, # type: ignore embedding=FakeEmbeddings(size=768), diff --git a/src/backend/langflow/components/vectorstores/VectaraSearch.py b/src/backend/langflow/components/vectorstores/VectaraSearch.py index ae2d442be..cbc876f86 100644 --- a/src/backend/langflow/components/vectorstores/VectaraSearch.py +++ b/src/backend/langflow/components/vectorstores/VectaraSearch.py @@ -33,10 +33,6 @@ class VectaraSearchComponent(VectaraComponent, LCVectorStoreComponent): "display_name": "Vectara API Key", "password": True, }, - "documents": { - "display_name": "Documents", - "info": "If provided, will be upserted to corpus (optional)", - }, "files_url": { "display_name": "Files Url", "info": "Make vectara object using url of files (optional)", diff --git a/src/backend/langflow/components/vectorstores/Weaviate.py b/src/backend/langflow/components/vectorstores/Weaviate.py index 3d804255a..a85febcd5 100644 --- a/src/backend/langflow/components/vectorstores/Weaviate.py +++ b/src/backend/langflow/components/vectorstores/Weaviate.py @@ -2,16 +2,19 @@ from typing import Optional, Union import weaviate # type: ignore from langchain.embeddings.base import Embeddings -from langchain.schema import BaseRetriever, Document +from langchain.schema import BaseRetriever from langchain_community.vectorstores import VectorStore, Weaviate from langflow import CustomComponent +from langflow.schema.schema import Record class WeaviateVectorStoreComponent(CustomComponent): display_name: str = "Weaviate" description: str = "Implementation of Vector Store using Weaviate" - documentation = "https://python.langchain.com/docs/integrations/vectorstores/weaviate" + documentation = ( + "https://python.langchain.com/docs/integrations/vectorstores/weaviate" + ) beta = True field_config = { "url": {"display_name": "Weaviate URL", "value": "http://localhost:8080"}, @@ -30,7 +33,7 @@ class WeaviateVectorStoreComponent(CustomComponent): "advanced": True, "value": "text", }, - "documents": {"display_name": "Documents", "is_list": True}, + "inputs": {"display_name": "Input", "input_types": ["Document", "Record"]}, "embedding": {"display_name": "Embedding"}, "attributes": { "display_name": "Attributes", @@ -55,7 +58,7 @@ class WeaviateVectorStoreComponent(CustomComponent): index_name: Optional[str] = None, text_key: str = "text", embedding: Optional[Embeddings] = None, - documents: Optional[Document] = None, + inputs: Optional[Record] = None, attributes: Optional[list] = None, ) -> Union[VectorStore, BaseRetriever]: if api_key: @@ -78,8 +81,14 @@ class WeaviateVectorStoreComponent(CustomComponent): return pascal_case_word index_name = _to_pascal_case(index_name) if index_name else None + documents = [] + for _input in inputs: + if isinstance(_input, Record): + documents.append(_input.to_lc_document()) + else: + documents.append(_input) - if documents is not None and embedding is not None: + if documents and embedding is not None: return Weaviate.from_documents( client=client, index_name=index_name, diff --git a/src/backend/langflow/components/vectorstores/WeaviateSearch.py b/src/backend/langflow/components/vectorstores/WeaviateSearch.py index 6eee202c9..9b14ca779 100644 --- a/src/backend/langflow/components/vectorstores/WeaviateSearch.py +++ b/src/backend/langflow/components/vectorstores/WeaviateSearch.py @@ -39,7 +39,6 @@ class WeaviateSearchVectorStore(WeaviateVectorStoreComponent, LCVectorStoreCompo "advanced": True, "value": "text", }, - "documents": {"display_name": "Documents", "is_list": True}, "embedding": {"display_name": "Embedding"}, "attributes": { "display_name": "Attributes", diff --git a/src/backend/langflow/components/vectorstores/pgvector.py b/src/backend/langflow/components/vectorstores/pgvector.py index 2baf6dae6..e3b528313 100644 --- a/src/backend/langflow/components/vectorstores/pgvector.py +++ b/src/backend/langflow/components/vectorstores/pgvector.py @@ -3,9 +3,10 @@ from typing import Optional, Union from langchain.embeddings.base import Embeddings from langchain_community.vectorstores import VectorStore from langchain_community.vectorstores.pgvector import PGVector -from langchain_core.documents import Document from langchain_core.retrievers import BaseRetriever + from langflow import CustomComponent +from langflow.schema.schema import Record class PGVectorComponent(CustomComponent): @@ -15,7 +16,9 @@ class PGVectorComponent(CustomComponent): display_name: str = "PGVector" description: str = "Implementation of Vector Store using PostgreSQL" - documentation = "https://python.langchain.com/docs/integrations/vectorstores/pgvector" + documentation = ( + "https://python.langchain.com/docs/integrations/vectorstores/pgvector" + ) def build_config(self): """ @@ -26,7 +29,7 @@ class PGVectorComponent(CustomComponent): """ return { "code": {"show": False}, - "documents": {"display_name": "Documents", "is_list": True}, + "inputs": {"display_name": "Input", "input_types": ["Document", "Record"]}, "embedding": {"display_name": "Embedding"}, "pg_server_url": { "display_name": "PostgreSQL Server Connection String", @@ -40,7 +43,7 @@ class PGVectorComponent(CustomComponent): embedding: Embeddings, pg_server_url: str, collection_name: str, - documents: Optional[Document] = None, + inputs: Optional[Record] = None, ) -> Union[VectorStore, BaseRetriever]: """ Builds the Vector Store or BaseRetriever object. @@ -55,6 +58,12 @@ class PGVectorComponent(CustomComponent): - VectorStore: The Vector Store object. """ + documents = [] + for _input in inputs: + if isinstance(_input, Record): + documents.append(_input.to_lc_document()) + else: + documents.append(_input) try: if documents is None: vector_store = PGVector.from_existing_index( From b0d4a67863d4c97db98492f180ed82278a704bb7 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 19:16:57 -0300 Subject: [PATCH 066/199] Refactor URLComponent to use WebBaseLoader instead of UnstructuredURLLoader --- src/backend/langflow/components/data/URL.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/backend/langflow/components/data/URL.py b/src/backend/langflow/components/data/URL.py index 08eafeaa3..d3af63b1c 100644 --- a/src/backend/langflow/components/data/URL.py +++ b/src/backend/langflow/components/data/URL.py @@ -1,6 +1,6 @@ -from typing import Any, Dict, Optional +from typing import Any, Dict -from langchain_community.document_loaders.url import UnstructuredURLLoader +from langchain_community.document_loaders.web_base import WebBaseLoader from langflow import CustomComponent from langflow.schema import Record @@ -8,7 +8,7 @@ from langflow.schema import Record class URLComponent(CustomComponent): display_name = "URL" - description = "Load a URL." + description = "Load URLs and convert them to records." def build_config(self) -> Dict[str, Any]: return { @@ -18,9 +18,9 @@ class URLComponent(CustomComponent): async def build( self, urls: list[str], - ) -> Optional[Record]: + ) -> Record: - loader = UnstructuredURLLoader(urls=urls) + loader = WebBaseLoader(web_paths=urls) docs = loader.load() records = self.to_records(docs) return records From 7c91e6900433174dfbe6ba331fb95464ed09bb26 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 19:17:04 -0300 Subject: [PATCH 067/199] Refactor ChromaComponent to accept inputs as Records --- .../langflow/components/vectorstores/Chroma.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/backend/langflow/components/vectorstores/Chroma.py b/src/backend/langflow/components/vectorstores/Chroma.py index 139a877e9..2a247e8d6 100644 --- a/src/backend/langflow/components/vectorstores/Chroma.py +++ b/src/backend/langflow/components/vectorstores/Chroma.py @@ -2,11 +2,12 @@ from typing import List, Optional, Union import chromadb # type: ignore from langchain.embeddings.base import Embeddings -from langchain.schema import BaseRetriever, Document +from langchain.schema import BaseRetriever from langchain_community.vectorstores import VectorStore from langchain_community.vectorstores.chroma import Chroma from langflow import CustomComponent +from langflow.schema.schema import Record class ChromaComponent(CustomComponent): @@ -55,7 +56,7 @@ class ChromaComponent(CustomComponent): embedding: Embeddings, chroma_server_ssl_enabled: bool, index_directory: Optional[str] = None, - documents: Optional[List[Document]] = None, + inputs: Optional[List[Record]] = None, chroma_server_cors_allow_origins: Optional[str] = None, chroma_server_host: Optional[str] = None, chroma_server_port: Optional[int] = None, @@ -98,6 +99,12 @@ class ChromaComponent(CustomComponent): if index_directory is not None: index_directory = self.resolve_path(index_directory) + documents = [] + for _input in inputs: + if isinstance(_input, Record): + documents.append(_input.to_lc_document()) + else: + documents.append(_input) if documents is not None and embedding is not None: if len(documents) == 0: raise ValueError( From afb3cfbc0589117959f705f81808e5c4678199e0 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 19:17:20 -0300 Subject: [PATCH 068/199] Remove console.log statements in updateBuildStatus function --- src/frontend/src/stores/flowStore.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/frontend/src/stores/flowStore.ts b/src/frontend/src/stores/flowStore.ts index cbf183289..e3cfbb20e 100644 --- a/src/frontend/src/stores/flowStore.ts +++ b/src/frontend/src/stores/flowStore.ts @@ -563,7 +563,6 @@ const useFlowStore = create((set, get) => ({ }); }, updateBuildStatus: (nodeIdList: string[], status: BuildStatus) => { - console.log("updateBuildStatus", nodeIdList, status); const newFlowBuildStatus = { ...get().flowBuildStatus }; nodeIdList.forEach((id) => { newFlowBuildStatus[id] = { @@ -573,7 +572,6 @@ const useFlowStore = create((set, get) => ({ const timestamp_string = new Date(Date.now()).toLocaleString(); newFlowBuildStatus[id].timestamp = timestamp_string; } - console.log("updateBuildStatus", newFlowBuildStatus); }); set({ flowBuildStatus: newFlowBuildStatus }); }, From 8d1dfdc2b60c2502c0ad000528fce95d9fa7b049 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 19:17:30 -0300 Subject: [PATCH 069/199] Add initial setup for loading starter projects into the database --- .../langflow/initial_setup/__init__.py | 0 src/backend/langflow/initial_setup/setup.py | 73 +++++++++++++++++++ src/backend/langflow/main.py | 16 +++- 3 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 src/backend/langflow/initial_setup/__init__.py create mode 100644 src/backend/langflow/initial_setup/setup.py diff --git a/src/backend/langflow/initial_setup/__init__.py b/src/backend/langflow/initial_setup/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/langflow/initial_setup/setup.py b/src/backend/langflow/initial_setup/setup.py new file mode 100644 index 000000000..3b9ed8d5e --- /dev/null +++ b/src/backend/langflow/initial_setup/setup.py @@ -0,0 +1,73 @@ +import json +from datetime import datetime +from pathlib import Path + +from loguru import logger +from sqlmodel import select + +from langflow.services.database.models.flow.model import Flow +from langflow.services.deps import get_session + +STARTER_FOLDER_NAME = "Starter Projects" + + +# In the folder ./starter_projects we have a few JSON files that represent +# starter projects. We want to load these into the database so that users +# can use them as a starting point for their own projects. +def load_starter_projects(): + # Load the starter projects from the JSON files + # using Pathlib's glob method + starter_projects = [] + folder = Path(__file__).parent / "starter_projects" + for file in folder.glob("*.json"): + with open(file, "r") as f: + starter_projects.append(json.load(f)) + logger.info(f"Loaded starter project {file}") + return starter_projects + + +# We want to load the starter projects into the database +def create_or_update_starter_projects(): + session = next(get_session()) + starter_projects = load_starter_projects() + for project in starter_projects: + # Check if the project already exists in the database + project_name = project.get("name") + project_description = project.get("description") + project_is_component = project.get("is_component") + project_updated_at = project.get("updated_at") + # 2024-03-05T21:59:59.738081 + updated_at_datetime = datetime.strptime( + project_updated_at, "%Y-%m-%dT%H:%M:%S.%f" + ) + project_data = project.get("data") + if project_name and project_data: + existing_project = session.exec( + select(Flow).where( + Flow.name == project_name, Flow.folder == STARTER_FOLDER_NAME + ) + ).first() + if existing_project: + logger.info(f"Updating starter project {project_name}") + existing_project.data = project_data + existing_project.folder = STARTER_FOLDER_NAME + existing_project.description = project_description + existing_project.is_component = project_is_component + existing_project.updated_at = updated_at_datetime + # Now we need to update the project in the database + session.add(existing_project) + else: + logger.info(f"Creating starter project {project_name}") + session.add( + Flow( + name=project_name, + description=project_description, + is_component=project_is_component, + updated_at=updated_at_datetime, + folder=STARTER_FOLDER_NAME, + data=project_data, + ) + ) + session.commit() + session.close() + logger.info("Starter projects loaded into database") diff --git a/src/backend/langflow/main.py b/src/backend/langflow/main.py index 4b4c19a25..7c34d9b08 100644 --- a/src/backend/langflow/main.py +++ b/src/backend/langflow/main.py @@ -8,7 +8,9 @@ from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles + from langflow.api import router +from langflow.initial_setup.setup import create_or_update_starter_projects from langflow.interface.utils import setup_llm_caching from langflow.services.plugins.langfuse_plugin import LangfuseInstance from langflow.services.utils import initialize_services, teardown_services @@ -18,9 +20,12 @@ from langflow.utils.logger import configure def get_lifespan(fix_migration=False, socketio_server=None): @asynccontextmanager async def lifespan(app: FastAPI): - initialize_services(fix_migration=fix_migration, socketio_server=socketio_server) + initialize_services( + fix_migration=fix_migration, socketio_server=socketio_server + ) setup_llm_caching() LangfuseInstance.update() + create_or_update_starter_projects() yield teardown_services() @@ -31,7 +36,9 @@ def create_app(): """Create the FastAPI app and include the router.""" configure() - socketio_server = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*", logger=True) + socketio_server = socketio.AsyncServer( + async_mode="asgi", cors_allowed_origins="*", logger=True + ) lifespan = get_lifespan(socketio_server=socketio_server) app = FastAPI(lifespan=lifespan) origins = ["*"] @@ -98,7 +105,9 @@ def get_static_files_dir(): return frontend_path / "frontend" -def setup_app(static_files_dir: Optional[Path] = None, backend_only: bool = False) -> FastAPI: +def setup_app( + static_files_dir: Optional[Path] = None, backend_only: bool = False +) -> FastAPI: """Setup the FastAPI app.""" # get the directory of the current file if not static_files_dir: @@ -114,6 +123,7 @@ def setup_app(static_files_dir: Optional[Path] = None, backend_only: bool = Fals if __name__ == "__main__": import uvicorn + from langflow.__main__ import get_number_of_workers configure() From 83c65fd3dfd5023df490f5651d0c2e0245771587 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 19:17:51 -0300 Subject: [PATCH 070/199] Add Data Ingestion Starter Project --- .../Langflow Data Ingestion.json | 1085 +++++++++++++++++ 1 file changed, 1085 insertions(+) create mode 100644 src/backend/langflow/initial_setup/starter_projects/Langflow Data Ingestion.json diff --git a/src/backend/langflow/initial_setup/starter_projects/Langflow Data Ingestion.json b/src/backend/langflow/initial_setup/starter_projects/Langflow Data Ingestion.json new file mode 100644 index 000000000..de8a8c1df --- /dev/null +++ b/src/backend/langflow/initial_setup/starter_projects/Langflow Data Ingestion.json @@ -0,0 +1,1085 @@ +{ + "name": "Data Ingestion", + "description": "This project is the starting point to insert data into a Vector Store. \n\nWe use the Vector Store Chroma but you can replace it with any other Vector Store. \n\nYou start by deciding what type of data you want to load, then you pick a place where you want to store the vectors and run it.\n\nThis will create a vector store in your local environment which you can query using the Chroma Search component.", + "data": { + "nodes": [ + { + "id": "RecursiveCharacterTextSplitter-jwfyG", + "type": "genericNode", + "position": { + "x": 1042.4388767006992, + "y": 633.2204634490822 + }, + "data": { + "type": "RecursiveCharacterTextSplitter", + "node": { + "template": { + "inputs": { + "type": "Document", + "required": true, + "placeholder": "", + "list": true, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "inputs", + "display_name": "Input", + "advanced": false, + "input_types": [ + "Document", + "Record" + ], + "dynamic": false, + "info": "The texts to split.", + "title_case": false + }, + "chunk_overlap": { + "type": "int", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": 200, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "chunk_overlap", + "display_name": "Chunk Overlap", + "advanced": false, + "dynamic": false, + "info": "The amount of overlap between chunks.", + "title_case": false + }, + "chunk_size": { + "type": "int", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": 1000, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "chunk_size", + "display_name": "Chunk Size", + "advanced": false, + "dynamic": false, + "info": "The maximum length of each chunk.", + "title_case": false + }, + "code": { + "type": "code", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "from typing import Optional\n\nfrom langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain_core.documents import Document\n\nfrom langflow import CustomComponent\nfrom langflow.schema import Record\nfrom langflow.utils.util import build_loader_repr_from_documents\n\n\nclass RecursiveCharacterTextSplitterComponent(CustomComponent):\n display_name: str = \"Recursive Character Text Splitter\"\n description: str = \"Split text into chunks of a specified length.\"\n documentation: str = (\n \"https://docs.langflow.org/components/text-splitters#recursivecharactertextsplitter\"\n )\n\n def build_config(self):\n return {\n \"inputs\": {\n \"display_name\": \"Input\",\n \"info\": \"The texts to split.\",\n \"input_types\": [\"Document\", \"Record\"],\n },\n \"separators\": {\n \"display_name\": \"Separators\",\n \"info\": 'The characters to split on.\\nIf left empty defaults to [\"\\\\n\\\\n\", \"\\\\n\", \" \", \"\"].',\n \"is_list\": True,\n },\n \"chunk_size\": {\n \"display_name\": \"Chunk Size\",\n \"info\": \"The maximum length of each chunk.\",\n \"field_type\": \"int\",\n \"value\": 1000,\n },\n \"chunk_overlap\": {\n \"display_name\": \"Chunk Overlap\",\n \"info\": \"The amount of overlap between chunks.\",\n \"field_type\": \"int\",\n \"value\": 200,\n },\n \"code\": {\"show\": False},\n }\n\n def build(\n self,\n inputs: list[Document],\n separators: Optional[list[str]] = None,\n chunk_size: Optional[int] = 1000,\n chunk_overlap: Optional[int] = 200,\n ) -> list[Record]:\n \"\"\"\n Split text into chunks of a specified length.\n\n Args:\n separators (list[str]): The characters to split on.\n chunk_size (int): The maximum length of each chunk.\n chunk_overlap (int): The amount of overlap between chunks.\n length_function (function): The function to use to calculate the length of the text.\n\n Returns:\n list[str]: The chunks of text.\n \"\"\"\n\n if separators == \"\":\n separators = None\n elif separators:\n # check if the separators list has escaped characters\n # if there are escaped characters, unescape them\n separators = [x.encode().decode(\"unicode-escape\") for x in separators]\n\n # Make sure chunk_size and chunk_overlap are ints\n if isinstance(chunk_size, str):\n chunk_size = int(chunk_size)\n if isinstance(chunk_overlap, str):\n chunk_overlap = int(chunk_overlap)\n splitter = RecursiveCharacterTextSplitter(\n separators=separators,\n chunk_size=chunk_size,\n chunk_overlap=chunk_overlap,\n )\n documents = []\n for _input in inputs:\n if isinstance(_input, Record):\n documents.append(_input.to_lc_document())\n else:\n documents.append(_input)\n docs = splitter.split_documents(documents)\n self.repr_value = build_loader_repr_from_documents(docs)\n return self.to_records(docs)\n", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "code", + "advanced": false, + "dynamic": true, + "info": "", + "title_case": false + }, + "separators": { + "type": "str", + "required": false, + "placeholder": "", + "list": true, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "separators", + "display_name": "Separators", + "advanced": false, + "dynamic": false, + "info": "The characters to split on.\nIf left empty defaults to [\"\\n\\n\", \"\\n\", \" \", \"\"].", + "title_case": false, + "input_types": [ + "Text" + ], + "value": [ + "\\n" + ] + }, + "_type": "CustomComponent" + }, + "description": "Split text into chunks of a specified length.", + "base_classes": [ + "Record" + ], + "display_name": "Recursive Character Text Splitter", + "documentation": "https://docs.langflow.org/components/text-splitters#recursivecharactertextsplitter", + "custom_fields": { + "inputs": null, + "separators": null, + "chunk_size": null, + "chunk_overlap": null + }, + "output_types": [ + "Record" + ], + "field_formatters": {}, + "frozen": false, + "beta": true + }, + "id": "RecursiveCharacterTextSplitter-jwfyG" + }, + "selected": false, + "width": 384, + "height": 509, + "positionAbsolute": { + "x": 1042.4388767006992, + "y": 633.2204634490822 + }, + "dragging": false + }, + { + "id": "Chroma-aFGHF", + "type": "genericNode", + "position": { + "x": 1641.280676720732, + "y": 356.94961598422196 + }, + "data": { + "type": "Chroma", + "node": { + "template": { + "embedding": { + "type": "Embeddings", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "embedding", + "display_name": "Embedding", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false + }, + "inputs": { + "type": "Record", + "required": false, + "placeholder": "", + "list": true, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "inputs", + "display_name": "Input", + "advanced": false, + "input_types": [ + "Document", + "Record" + ], + "dynamic": false, + "info": "", + "title_case": false + }, + "chroma_server_cors_allow_origins": { + "type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "chroma_server_cors_allow_origins", + "display_name": "Server CORS Allow Origins", + "advanced": true, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ] + }, + "chroma_server_grpc_port": { + "type": "int", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "chroma_server_grpc_port", + "display_name": "Server gRPC Port", + "advanced": true, + "dynamic": false, + "info": "", + "title_case": false + }, + "chroma_server_host": { + "type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "chroma_server_host", + "display_name": "Server Host", + "advanced": true, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ] + }, + "chroma_server_port": { + "type": "int", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "chroma_server_port", + "display_name": "Server Port", + "advanced": true, + "dynamic": false, + "info": "", + "title_case": false + }, + "chroma_server_ssl_enabled": { + "type": "bool", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "chroma_server_ssl_enabled", + "display_name": "Server SSL Enabled", + "advanced": true, + "dynamic": false, + "info": "", + "title_case": false + }, + "code": { + "type": "code", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "from typing import List, Optional, Union\n\nimport chromadb # type: ignore\nfrom langchain.embeddings.base import Embeddings\nfrom langchain.schema import BaseRetriever\nfrom langchain_community.vectorstores import VectorStore\nfrom langchain_community.vectorstores.chroma import Chroma\n\nfrom langflow import CustomComponent\nfrom langflow.schema.schema import Record\n\n\nclass ChromaComponent(CustomComponent):\n \"\"\"\n A custom component for implementing a Vector Store using Chroma.\n \"\"\"\n\n display_name: str = \"Chroma\"\n description: str = \"Implementation of Vector Store using Chroma\"\n documentation = \"https://python.langchain.com/docs/integrations/vectorstores/chroma\"\n beta: bool = True\n icon = \"Chroma\"\n\n def build_config(self):\n \"\"\"\n Builds the configuration for the component.\n\n Returns:\n - dict: A dictionary containing the configuration options for the component.\n \"\"\"\n return {\n \"collection_name\": {\"display_name\": \"Collection Name\", \"value\": \"langflow\"},\n \"index_directory\": {\"display_name\": \"Persist Directory\"},\n \"code\": {\"advanced\": True, \"display_name\": \"Code\"},\n \"inputs\": {\"display_name\": \"Input\", \"input_types\": [\"Document\", \"Record\"]},\n \"embedding\": {\"display_name\": \"Embedding\"},\n \"chroma_server_cors_allow_origins\": {\n \"display_name\": \"Server CORS Allow Origins\",\n \"advanced\": True,\n },\n \"chroma_server_host\": {\"display_name\": \"Server Host\", \"advanced\": True},\n \"chroma_server_port\": {\"display_name\": \"Server Port\", \"advanced\": True},\n \"chroma_server_grpc_port\": {\n \"display_name\": \"Server gRPC Port\",\n \"advanced\": True,\n },\n \"chroma_server_ssl_enabled\": {\n \"display_name\": \"Server SSL Enabled\",\n \"advanced\": True,\n },\n }\n\n def build(\n self,\n collection_name: str,\n embedding: Embeddings,\n chroma_server_ssl_enabled: bool,\n index_directory: Optional[str] = None,\n inputs: Optional[List[Record]] = None,\n chroma_server_cors_allow_origins: Optional[str] = None,\n chroma_server_host: Optional[str] = None,\n chroma_server_port: Optional[int] = None,\n chroma_server_grpc_port: Optional[int] = None,\n ) -> Union[VectorStore, BaseRetriever]:\n \"\"\"\n Builds the Vector Store or BaseRetriever object.\n\n Args:\n - collection_name (str): The name of the collection.\n - index_directory (Optional[str]): The directory to persist the Vector Store to.\n - chroma_server_ssl_enabled (bool): Whether to enable SSL for the Chroma server.\n - embedding (Optional[Embeddings]): The embeddings to use for the Vector Store.\n - documents (Optional[Document]): The documents to use for the Vector Store.\n - chroma_server_cors_allow_origins (Optional[str]): The CORS allow origins for the Chroma server.\n - chroma_server_host (Optional[str]): The host for the Chroma server.\n - chroma_server_port (Optional[int]): The port for the Chroma server.\n - chroma_server_grpc_port (Optional[int]): The gRPC port for the Chroma server.\n\n Returns:\n - Union[VectorStore, BaseRetriever]: The Vector Store or BaseRetriever object.\n \"\"\"\n\n # Chroma settings\n chroma_settings = None\n\n if chroma_server_host is not None:\n chroma_settings = chromadb.config.Settings(\n chroma_server_cors_allow_origins=chroma_server_cors_allow_origins\n or None,\n chroma_server_host=chroma_server_host,\n chroma_server_port=chroma_server_port or None,\n chroma_server_grpc_port=chroma_server_grpc_port or None,\n chroma_server_ssl_enabled=chroma_server_ssl_enabled,\n )\n\n # If documents, then we need to create a Chroma instance using .from_documents\n\n # Check index_directory and expand it if it is a relative path\n if index_directory is not None:\n index_directory = self.resolve_path(index_directory)\n\n documents = []\n for _input in inputs:\n if isinstance(_input, Record):\n documents.append(_input.to_lc_document())\n else:\n documents.append(_input)\n if documents is not None and embedding is not None:\n if len(documents) == 0:\n raise ValueError(\n \"If documents are provided, there must be at least one document.\"\n )\n chroma = Chroma.from_documents(\n documents=documents, # type: ignore\n persist_directory=index_directory,\n collection_name=collection_name,\n embedding=embedding,\n client_settings=chroma_settings,\n )\n else:\n chroma = Chroma(\n persist_directory=index_directory,\n client_settings=chroma_settings,\n embedding_function=embedding,\n )\n return chroma\n", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "code", + "advanced": true, + "dynamic": true, + "info": "", + "title_case": false + }, + "collection_name": { + "type": "str", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": "langflow_contrib", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "collection_name", + "display_name": "Collection Name", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ] + }, + "index_directory": { + "type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "index_directory", + "display_name": "Persist Directory", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ], + "value": "./chroma_langflow" + }, + "_type": "CustomComponent" + }, + "description": "Implementation of Vector Store using Chroma", + "icon": "Chroma", + "base_classes": [ + "Serializable", + "VectorStore", + "object", + "Runnable", + "BaseRetriever", + "RunnableSerializable", + "Generic" + ], + "display_name": "Chroma", + "documentation": "https://python.langchain.com/docs/integrations/vectorstores/chroma", + "custom_fields": { + "collection_name": null, + "embedding": null, + "chroma_server_ssl_enabled": null, + "index_directory": null, + "inputs": null, + "chroma_server_cors_allow_origins": null, + "chroma_server_host": null, + "chroma_server_port": null, + "chroma_server_grpc_port": null + }, + "output_types": [ + "VectorStore", + "BaseRetriever" + ], + "field_formatters": {}, + "frozen": false, + "beta": true + }, + "id": "Chroma-aFGHF" + }, + "selected": true, + "width": 384, + "height": 495, + "positionAbsolute": { + "x": 1641.280676720732, + "y": 356.94961598422196 + }, + "dragging": false + }, + { + "id": "OpenAIEmbeddings-rbMk3", + "type": "genericNode", + "position": { + "x": 1053.9472627140208, + "y": -2.5921878249999963 + }, + "data": { + "type": "OpenAIEmbeddings", + "node": { + "template": { + "allowed_special": { + "type": "str", + "required": true, + "placeholder": "", + "list": true, + "show": true, + "multiline": false, + "value": [], + "fileTypes": [], + "file_path": "", + "password": false, + "name": "allowed_special", + "display_name": "Allowed Special", + "advanced": true, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ] + }, + "chunk_size": { + "type": "int", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": 1000, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "chunk_size", + "display_name": "Chunk Size", + "advanced": true, + "dynamic": false, + "info": "", + "title_case": false + }, + "client": { + "type": "Any", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "client", + "display_name": "Client", + "advanced": true, + "dynamic": false, + "info": "", + "title_case": false + }, + "code": { + "type": "code", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "from typing import Any, Callable, Dict, List, Optional, Union\n\nfrom langchain_openai.embeddings.base import OpenAIEmbeddings\nfrom langflow import CustomComponent\nfrom langflow.field_typing import NestedDict\nfrom pydantic.v1.types import SecretStr\n\n\nclass OpenAIEmbeddingsComponent(CustomComponent):\n display_name = \"OpenAIEmbeddings\"\n description = \"OpenAI embedding models\"\n\n def build_config(self):\n return {\n \"allowed_special\": {\n \"display_name\": \"Allowed Special\",\n \"advanced\": True,\n \"field_type\": \"str\",\n \"is_list\": True,\n },\n \"default_headers\": {\n \"display_name\": \"Default Headers\",\n \"advanced\": True,\n \"field_type\": \"dict\",\n },\n \"default_query\": {\n \"display_name\": \"Default Query\",\n \"advanced\": True,\n \"field_type\": \"NestedDict\",\n },\n \"disallowed_special\": {\n \"display_name\": \"Disallowed Special\",\n \"advanced\": True,\n \"field_type\": \"str\",\n \"is_list\": True,\n },\n \"chunk_size\": {\"display_name\": \"Chunk Size\", \"advanced\": True},\n \"client\": {\"display_name\": \"Client\", \"advanced\": True},\n \"deployment\": {\"display_name\": \"Deployment\", \"advanced\": True},\n \"embedding_ctx_length\": {\n \"display_name\": \"Embedding Context Length\",\n \"advanced\": True,\n },\n \"max_retries\": {\"display_name\": \"Max Retries\", \"advanced\": True},\n \"model\": {\n \"display_name\": \"Model\",\n \"advanced\": False,\n \"options\": [\"text-embedding-3-small\", \"text-embedding-3-large\", \"text-embedding-ada-002\"],\n },\n \"model_kwargs\": {\"display_name\": \"Model Kwargs\", \"advanced\": True},\n \"openai_api_base\": {\"display_name\": \"OpenAI API Base\", \"password\": True, \"advanced\": True},\n \"openai_api_key\": {\"display_name\": \"OpenAI API Key\", \"password\": True},\n \"openai_api_type\": {\"display_name\": \"OpenAI API Type\", \"advanced\": True, \"password\": True},\n \"openai_api_version\": {\n \"display_name\": \"OpenAI API Version\",\n \"advanced\": True,\n },\n \"openai_organization\": {\n \"display_name\": \"OpenAI Organization\",\n \"advanced\": True,\n },\n \"openai_proxy\": {\"display_name\": \"OpenAI Proxy\", \"advanced\": True},\n \"request_timeout\": {\"display_name\": \"Request Timeout\", \"advanced\": True},\n \"show_progress_bar\": {\n \"display_name\": \"Show Progress Bar\",\n \"advanced\": True,\n },\n \"skip_empty\": {\"display_name\": \"Skip Empty\", \"advanced\": True},\n \"tiktoken_model_name\": {\"display_name\": \"TikToken Model Name\"},\n \"tikToken_enable\": {\"display_name\": \"TikToken Enable\", \"advanced\": True},\n }\n\n def build(\n self,\n default_headers: Optional[Dict[str, str]] = None,\n default_query: Optional[NestedDict] = {},\n allowed_special: List[str] = [],\n disallowed_special: List[str] = [\"all\"],\n chunk_size: int = 1000,\n client: Optional[Any] = None,\n deployment: str = \"text-embedding-3-small\",\n embedding_ctx_length: int = 8191,\n max_retries: int = 6,\n model: str = \"text-embedding-3-small\",\n model_kwargs: NestedDict = {},\n openai_api_base: Optional[str] = None,\n openai_api_key: Optional[str] = \"\",\n openai_api_type: Optional[str] = None,\n openai_api_version: Optional[str] = None,\n openai_organization: Optional[str] = None,\n openai_proxy: Optional[str] = None,\n request_timeout: Optional[float] = None,\n show_progress_bar: bool = False,\n skip_empty: bool = False,\n tiktoken_enable: bool = True,\n tiktoken_model_name: Optional[str] = None,\n ) -> Union[OpenAIEmbeddings, Callable]:\n # This is to avoid errors with Vector Stores (e.g Chroma)\n if disallowed_special == [\"all\"]:\n disallowed_special = \"all\" # type: ignore\n\n api_key = SecretStr(openai_api_key) if openai_api_key else None\n\n return OpenAIEmbeddings(\n tiktoken_enabled=tiktoken_enable,\n default_headers=default_headers,\n default_query=default_query,\n allowed_special=set(allowed_special),\n disallowed_special=\"all\",\n chunk_size=chunk_size,\n client=client,\n deployment=deployment,\n embedding_ctx_length=embedding_ctx_length,\n max_retries=max_retries,\n model=model,\n model_kwargs=model_kwargs,\n base_url=openai_api_base,\n api_key=api_key,\n openai_api_type=openai_api_type,\n api_version=openai_api_version,\n organization=openai_organization,\n openai_proxy=openai_proxy,\n timeout=request_timeout,\n show_progress_bar=show_progress_bar,\n skip_empty=skip_empty,\n tiktoken_model_name=tiktoken_model_name,\n )\n", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "code", + "advanced": false, + "dynamic": true, + "info": "", + "title_case": false + }, + "default_headers": { + "type": "dict", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "default_headers", + "display_name": "Default Headers", + "advanced": true, + "dynamic": false, + "info": "", + "title_case": false + }, + "default_query": { + "type": "NestedDict", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": {}, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "default_query", + "display_name": "Default Query", + "advanced": true, + "dynamic": false, + "info": "", + "title_case": false + }, + "deployment": { + "type": "str", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": "text-embedding-3-small", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "deployment", + "display_name": "Deployment", + "advanced": true, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ] + }, + "disallowed_special": { + "type": "str", + "required": true, + "placeholder": "", + "list": true, + "show": true, + "multiline": false, + "value": [ + "all" + ], + "fileTypes": [], + "file_path": "", + "password": false, + "name": "disallowed_special", + "display_name": "Disallowed Special", + "advanced": true, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ] + }, + "embedding_ctx_length": { + "type": "int", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": 8191, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "embedding_ctx_length", + "display_name": "Embedding Context Length", + "advanced": true, + "dynamic": false, + "info": "", + "title_case": false + }, + "max_retries": { + "type": "int", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": 6, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "max_retries", + "display_name": "Max Retries", + "advanced": true, + "dynamic": false, + "info": "", + "title_case": false + }, + "model": { + "type": "str", + "required": true, + "placeholder": "", + "list": true, + "show": true, + "multiline": false, + "value": "text-embedding-3-small", + "fileTypes": [], + "file_path": "", + "password": false, + "options": [ + "text-embedding-3-small", + "text-embedding-3-large", + "text-embedding-ada-002" + ], + "name": "model", + "display_name": "Model", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ] + }, + "model_kwargs": { + "type": "NestedDict", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": {}, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "model_kwargs", + "display_name": "Model Kwargs", + "advanced": true, + "dynamic": false, + "info": "", + "title_case": false + }, + "openai_api_base": { + "type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": true, + "name": "openai_api_base", + "display_name": "OpenAI API Base", + "advanced": true, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ], + "value": "" + }, + "openai_api_key": { + "type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": "", + "fileTypes": [], + "file_path": "", + "password": true, + "name": "openai_api_key", + "display_name": "OpenAI API Key", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ] + }, + "openai_api_type": { + "type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": true, + "name": "openai_api_type", + "display_name": "OpenAI API Type", + "advanced": true, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ], + "value": "" + }, + "openai_api_version": { + "type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "openai_api_version", + "display_name": "OpenAI API Version", + "advanced": true, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ] + }, + "openai_organization": { + "type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "openai_organization", + "display_name": "OpenAI Organization", + "advanced": true, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ] + }, + "openai_proxy": { + "type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "openai_proxy", + "display_name": "OpenAI Proxy", + "advanced": true, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ] + }, + "request_timeout": { + "type": "float", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "request_timeout", + "display_name": "Request Timeout", + "advanced": true, + "dynamic": false, + "info": "", + "rangeSpec": { + "min": -1, + "max": 1, + "step": 0.1 + }, + "title_case": false + }, + "show_progress_bar": { + "type": "bool", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "show_progress_bar", + "display_name": "Show Progress Bar", + "advanced": true, + "dynamic": false, + "info": "", + "title_case": false + }, + "skip_empty": { + "type": "bool", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "skip_empty", + "display_name": "Skip Empty", + "advanced": true, + "dynamic": false, + "info": "", + "title_case": false + }, + "tiktoken_enable": { + "type": "bool", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": true, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "tiktoken_enable", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false + }, + "tiktoken_model_name": { + "type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "tiktoken_model_name", + "display_name": "TikToken Model Name", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ] + }, + "_type": "CustomComponent" + }, + "description": "OpenAI embedding models", + "base_classes": [ + "Embeddings", + "OpenAIEmbeddings", + "Callable" + ], + "display_name": "OpenAIEmbeddings", + "documentation": "", + "custom_fields": { + "default_headers": null, + "default_query": null, + "allowed_special": null, + "disallowed_special": null, + "chunk_size": null, + "client": null, + "deployment": null, + "embedding_ctx_length": null, + "max_retries": null, + "model": null, + "model_kwargs": null, + "openai_api_base": null, + "openai_api_key": null, + "openai_api_type": null, + "openai_api_version": null, + "openai_organization": null, + "openai_proxy": null, + "request_timeout": null, + "show_progress_bar": null, + "skip_empty": null, + "tiktoken_enable": null, + "tiktoken_model_name": null + }, + "output_types": [ + "OpenAIEmbeddings", + "Callable" + ], + "field_formatters": {}, + "frozen": false, + "beta": true + }, + "id": "OpenAIEmbeddings-rbMk3" + }, + "selected": false, + "width": 384, + "height": 573, + "positionAbsolute": { + "x": 1053.9472627140208, + "y": -2.5921878249999963 + }, + "dragging": false + }, + { + "id": "URL-5zjQH", + "type": "genericNode", + "position": { + "x": 567.0838444398559, + "y": 596.6568151511171 + }, + "data": { + "type": "URL", + "node": { + "template": { + "code": { + "type": "code", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "from typing import Any, Dict\n\nfrom langchain_community.document_loaders.web_base import WebBaseLoader\n\nfrom langflow import CustomComponent\nfrom langflow.schema import Record\n\n\nclass URLComponent(CustomComponent):\n display_name = \"URL\"\n description = \"Load URLs and convert them to records.\"\n\n def build_config(self) -> Dict[str, Any]:\n return {\n \"urls\": {\"display_name\": \"URL\"},\n }\n\n async def build(\n self,\n urls: list[str],\n ) -> Record:\n\n loader = WebBaseLoader(web_paths=urls)\n docs = loader.load()\n records = self.to_records(docs)\n return records\n", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "code", + "advanced": false, + "dynamic": true, + "info": "", + "title_case": false + }, + "urls": { + "type": "str", + "required": true, + "placeholder": "", + "list": true, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "urls", + "display_name": "URL", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ], + "value": [ + "https://raw.githubusercontent.com/logspace-ai/langflow/dev/CONTRIBUTING.md" + ] + }, + "_type": "CustomComponent" + }, + "description": "Load URLs and convert them to records.", + "base_classes": [ + "Record" + ], + "display_name": "URL", + "documentation": "", + "custom_fields": { + "urls": null + }, + "output_types": [ + "Record" + ], + "field_formatters": {}, + "frozen": false, + "beta": true + }, + "id": "URL-5zjQH" + }, + "selected": false, + "width": 384, + "height": 289, + "dragging": false, + "positionAbsolute": { + "x": 567.0838444398559, + "y": 596.6568151511171 + } + } + ], + "edges": [ + { + "source": "RecursiveCharacterTextSplitter-jwfyG", + "sourceHandle": "{œbaseClassesœ:[œRecordœ],œdataTypeœ:œRecursiveCharacterTextSplitterœ,œidœ:œRecursiveCharacterTextSplitter-jwfyGœ}", + "target": "Chroma-aFGHF", + "targetHandle": "{œfieldNameœ:œinputsœ,œidœ:œChroma-aFGHFœ,œinputTypesœ:[œDocumentœ,œRecordœ],œtypeœ:œRecordœ}", + "data": { + "targetHandle": { + "fieldName": "inputs", + "id": "Chroma-aFGHF", + "inputTypes": [ + "Document", + "Record" + ], + "type": "Record" + }, + "sourceHandle": { + "baseClasses": [ + "Record" + ], + "dataType": "RecursiveCharacterTextSplitter", + "id": "RecursiveCharacterTextSplitter-jwfyG" + } + }, + "style": { + "stroke": "#555" + }, + "className": "stroke-gray-900 stroke-connection", + "id": "reactflow__edge-RecursiveCharacterTextSplitter-jwfyG{œbaseClassesœ:[œRecordœ],œdataTypeœ:œRecursiveCharacterTextSplitterœ,œidœ:œRecursiveCharacterTextSplitter-jwfyGœ}-Chroma-aFGHF{œfieldNameœ:œinputsœ,œidœ:œChroma-aFGHFœ,œinputTypesœ:[œDocumentœ,œRecordœ],œtypeœ:œRecordœ}" + }, + { + "source": "OpenAIEmbeddings-rbMk3", + "sourceHandle": "{œbaseClassesœ:[œEmbeddingsœ,œOpenAIEmbeddingsœ,œCallableœ],œdataTypeœ:œOpenAIEmbeddingsœ,œidœ:œOpenAIEmbeddings-rbMk3œ}", + "target": "Chroma-aFGHF", + "targetHandle": "{œfieldNameœ:œembeddingœ,œidœ:œChroma-aFGHFœ,œinputTypesœ:null,œtypeœ:œEmbeddingsœ}", + "data": { + "targetHandle": { + "fieldName": "embedding", + "id": "Chroma-aFGHF", + "inputTypes": null, + "type": "Embeddings" + }, + "sourceHandle": { + "baseClasses": [ + "Embeddings", + "OpenAIEmbeddings", + "Callable" + ], + "dataType": "OpenAIEmbeddings", + "id": "OpenAIEmbeddings-rbMk3" + } + }, + "style": { + "stroke": "#555" + }, + "className": "stroke-gray-900 stroke-connection", + "id": "reactflow__edge-OpenAIEmbeddings-rbMk3{œbaseClassesœ:[œEmbeddingsœ,œOpenAIEmbeddingsœ,œCallableœ],œdataTypeœ:œOpenAIEmbeddingsœ,œidœ:œOpenAIEmbeddings-rbMk3œ}-Chroma-aFGHF{œfieldNameœ:œembeddingœ,œidœ:œChroma-aFGHFœ,œinputTypesœ:null,œtypeœ:œEmbeddingsœ}" + }, + { + "source": "URL-5zjQH", + "sourceHandle": "{œbaseClassesœ:[œRecordœ],œdataTypeœ:œURLœ,œidœ:œURL-5zjQHœ}", + "target": "RecursiveCharacterTextSplitter-jwfyG", + "targetHandle": "{œfieldNameœ:œinputsœ,œidœ:œRecursiveCharacterTextSplitter-jwfyGœ,œinputTypesœ:[œDocumentœ,œRecordœ],œtypeœ:œDocumentœ}", + "data": { + "targetHandle": { + "fieldName": "inputs", + "id": "RecursiveCharacterTextSplitter-jwfyG", + "inputTypes": [ + "Document", + "Record" + ], + "type": "Document" + }, + "sourceHandle": { + "baseClasses": [ + "Record" + ], + "dataType": "URL", + "id": "URL-5zjQH" + } + }, + "style": { + "stroke": "#555" + }, + "className": "stroke-foreground stroke-connection", + "id": "reactflow__edge-URL-5zjQH{œbaseClassesœ:[œRecordœ],œdataTypeœ:œURLœ,œidœ:œURL-5zjQHœ}-RecursiveCharacterTextSplitter-jwfyG{œfieldNameœ:œinputsœ,œidœ:œRecursiveCharacterTextSplitter-jwfyGœ,œinputTypesœ:[œDocumentœ,œRecordœ],œtypeœ:œDocumentœ}" + } + ], + "viewport": { + "x": -160.3219973143573, + "y": 117.63775645863632, + "zoom": 0.48903173672366845 + } + }, + "is_component": false, + "updated_at": "2024-03-05T21:59:59.738081", + "folder": null, + "id": "7f90dc54-717d-49fe-a43f-c4dc055daa4e", + "user_id": "9365dbda-e8cf-4e95-8c84-49f8b6edb44f" +} \ No newline at end of file From 9d61ece65dc3e4245cf9f7c4e1652ccbdf39f229 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Tue, 5 Mar 2024 19:25:39 -0300 Subject: [PATCH 071/199] Add new components and update styleUtils.ts --- .../components/NewFlowCardComponent/index.tsx | 54 +++++++++++++ .../src/components/exampleComponent/index.tsx | 78 +++++++++++++++++++ src/frontend/src/pages/MainPage/index.tsx | 38 ++++++--- src/frontend/src/utils/styleUtils.ts | 6 ++ 4 files changed, 167 insertions(+), 9 deletions(-) create mode 100644 src/frontend/src/components/NewFlowCardComponent/index.tsx create mode 100644 src/frontend/src/components/exampleComponent/index.tsx diff --git a/src/frontend/src/components/NewFlowCardComponent/index.tsx b/src/frontend/src/components/NewFlowCardComponent/index.tsx new file mode 100644 index 000000000..131fb3543 --- /dev/null +++ b/src/frontend/src/components/NewFlowCardComponent/index.tsx @@ -0,0 +1,54 @@ +import { useEffect, useState } from "react"; +import { getComponent, postLikeComponent } from "../../controllers/API"; +import DeleteConfirmationModal from "../../modals/DeleteConfirmationModal"; +import useAlertStore from "../../stores/alertStore"; +import useFlowsManagerStore from "../../stores/flowsManagerStore"; +import { useStoreStore } from "../../stores/storeStore"; +import { storeComponent } from "../../types/store"; +import cloneFLowWithParent from "../../utils/storeUtils"; +import { cn } from "../../utils/utils"; +import ShadTooltip from "../ShadTooltipComponent"; +import IconComponent from "../genericIconComponent"; +import { Badge } from "../ui/badge"; +import { Button } from "../ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "../ui/card"; +import { FlowType } from "../../types/flow"; +import { useNavigate } from "react-router-dom"; + +export default function NewFlowCardComponent({ +}: { + }) { + const addFlow = useFlowsManagerStore((state) => state.addFlow); + const navigate = useNavigate(); + + return ( + + + + + + ); +} diff --git a/src/frontend/src/components/exampleComponent/index.tsx b/src/frontend/src/components/exampleComponent/index.tsx new file mode 100644 index 000000000..af0cf56ea --- /dev/null +++ b/src/frontend/src/components/exampleComponent/index.tsx @@ -0,0 +1,78 @@ +import { useEffect, useState } from "react"; +import { getComponent, postLikeComponent } from "../../controllers/API"; +import DeleteConfirmationModal from "../../modals/DeleteConfirmationModal"; +import useAlertStore from "../../stores/alertStore"; +import useFlowsManagerStore from "../../stores/flowsManagerStore"; +import { useStoreStore } from "../../stores/storeStore"; +import { storeComponent } from "../../types/store"; +import cloneFLowWithParent from "../../utils/storeUtils"; +import { cn } from "../../utils/utils"; +import ShadTooltip from "../ShadTooltipComponent"; +import IconComponent from "../genericIconComponent"; +import { Badge } from "../ui/badge"; +import { Button } from "../ui/button"; +import { + Card, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "../ui/card"; +import { FlowType } from "../../types/flow"; + +export default function CollectionCardComponent({ + data, +}: { + data: FlowType; + authorized?: boolean; +}) { + const addFlow = useFlowsManagerStore((state) => state.addFlow); + + return ( + +
+ +
+ + + +
{data.name}
+
+
+
+ +
{data.description}
+
+
+
+ + +
+
+ +
+
+
+
+ ); +} diff --git a/src/frontend/src/pages/MainPage/index.tsx b/src/frontend/src/pages/MainPage/index.tsx index cc4115e7e..fc8747094 100644 --- a/src/frontend/src/pages/MainPage/index.tsx +++ b/src/frontend/src/pages/MainPage/index.tsx @@ -1,5 +1,5 @@ import { Group, ToyBrick } from "lucide-react"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { Outlet, useLocation, useNavigate } from "react-router-dom"; import DropdownButton from "../../components/DropdownButtonComponent"; import IconComponent from "../../components/genericIconComponent"; @@ -14,6 +14,9 @@ import { import useAlertStore from "../../stores/alertStore"; import useFlowsManagerStore from "../../stores/flowsManagerStore"; import { downloadFlows } from "../../utils/reactflowUtils"; +import BaseModal from "../../modals/baseModal"; +import ExampleCardComponent from "../../components/exampleComponent"; +import NewFlowCardComponent from "../../components/NewFlowCardComponent"; export default function HomePage(): JSX.Element { const addFlow = useFlowsManagerStore((state) => state.addFlow); const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow); @@ -25,6 +28,7 @@ export default function HomePage(): JSX.Element { const setErrorData = useAlertStore((state) => state.setErrorData); const location = useLocation(); const pathname = location.pathname; + const [openModal, setOpenModal] = useState(false); const is_component = pathname === "/components"; const dropdownOptions = [ { @@ -36,9 +40,8 @@ export default function HomePage(): JSX.Element { }) .then((id) => { setSuccessData({ - title: `${ - is_component ? "Component" : "Flow" - } uploaded successfully`, + title: `${is_component ? "Component" : "Flow" + } uploaded successfully`, }); if (!is_component) navigate("/flow/" + id); }) @@ -98,11 +101,7 @@ export default function HomePage(): JSX.Element { { - addFlow(true).then((id) => { - navigate("/flow/" + id); - }); - }} + onFirstBtnClick={() => setOpenModal(true)} options={dropdownOptions} />
@@ -116,6 +115,27 @@ export default function HomePage(): JSX.Element {
+ + + + Create a New Flow + + + +
+ + +
+
+
); } diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index 601066407..96d754e85 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -5,8 +5,11 @@ import { Bell, BookMarked, BookmarkPlus, + PlusCircle, + PlusSquare, Bot, Boxes, + GitCompare, Braces, Check, CheckCircle2, @@ -392,6 +395,8 @@ export const nodeIconsLucide: iconsType = { Circle, CircleDot, Clipboard, + PlusCircle, + PlusSquare, Code2, Variable, Snowflake, @@ -469,3 +474,4 @@ export const nodeIconsLucide: iconsType = { Delete, Command, }; + From 57afaae666a96b1f6e3b8f6615cff160b1e5751d Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Tue, 5 Mar 2024 19:26:47 -0300 Subject: [PATCH 072/199] Add STARTER_FOLDER_NAME constant --- src/frontend/src/constants/constants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/frontend/src/constants/constants.ts b/src/frontend/src/constants/constants.ts index 3f4584102..fb9d38451 100644 --- a/src/frontend/src/constants/constants.ts +++ b/src/frontend/src/constants/constants.ts @@ -727,3 +727,4 @@ export const STATUS_BUILD = "Build to validate status."; export const STATUS_BUILDING = "Building..."; export const SAVED_HOVER = "Last saved at "; export const RUN_TIMESTAMP_PREFIX = "Last Run: "; +export const STARTER_FOLDER_NAME = "Starter Projects"; From 8a1d48336a672c52eb19083644ff9bf884cc654a Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 19:33:25 -0300 Subject: [PATCH 073/199] Refactor starter project loading and database update --- src/backend/langflow/initial_setup/setup.py | 142 +++++++++++++------- src/backend/langflow/services/deps.py | 14 ++ 2 files changed, 106 insertions(+), 50 deletions(-) diff --git a/src/backend/langflow/initial_setup/setup.py b/src/backend/langflow/initial_setup/setup.py index 3b9ed8d5e..1f24ddab1 100644 --- a/src/backend/langflow/initial_setup/setup.py +++ b/src/backend/langflow/initial_setup/setup.py @@ -1,12 +1,12 @@ -import json from datetime import datetime from pathlib import Path +import orjson from loguru import logger from sqlmodel import select from langflow.services.database.models.flow.model import Flow -from langflow.services.deps import get_session +from langflow.services.deps import session_scope STARTER_FOLDER_NAME = "Starter Projects" @@ -14,60 +14,102 @@ STARTER_FOLDER_NAME = "Starter Projects" # In the folder ./starter_projects we have a few JSON files that represent # starter projects. We want to load these into the database so that users # can use them as a starting point for their own projects. + + def load_starter_projects(): - # Load the starter projects from the JSON files - # using Pathlib's glob method starter_projects = [] folder = Path(__file__).parent / "starter_projects" for file in folder.glob("*.json"): - with open(file, "r") as f: - starter_projects.append(json.load(f)) - logger.info(f"Loaded starter project {file}") + project = orjson.loads(file.read_text()) + starter_projects.append(project) + logger.info(f"Loaded starter project {file}") return starter_projects -# We want to load the starter projects into the database +def get_project_data(project): + project_name = project.get("name") + project_description = project.get("description") + project_is_component = project.get("is_component") + project_updated_at = project.get("updated_at") + updated_at_datetime = datetime.strptime(project_updated_at, "%Y-%m-%dT%H:%M:%S.%f") + project_data = project.get("data") + return ( + project_name, + project_description, + project_is_component, + updated_at_datetime, + project_data, + ) + + +def update_existing_project( + existing_project, + project_name, + project_description, + project_is_component, + updated_at_datetime, + project_data, +): + logger.info(f"Updating starter project {project_name}") + existing_project.data = project_data + existing_project.folder = STARTER_FOLDER_NAME + existing_project.description = project_description + existing_project.is_component = project_is_component + existing_project.updated_at = updated_at_datetime + + +def create_new_project( + session, + project_name, + project_description, + project_is_component, + updated_at_datetime, + project_data, +): + logger.info(f"Creating starter project {project_name}") + new_project = Flow( + name=project_name, + description=project_description, + is_component=project_is_component, + updated_at=updated_at_datetime, + folder=STARTER_FOLDER_NAME, + data=project_data, + ) + session.add(new_project) + + def create_or_update_starter_projects(): - session = next(get_session()) - starter_projects = load_starter_projects() - for project in starter_projects: - # Check if the project already exists in the database - project_name = project.get("name") - project_description = project.get("description") - project_is_component = project.get("is_component") - project_updated_at = project.get("updated_at") - # 2024-03-05T21:59:59.738081 - updated_at_datetime = datetime.strptime( - project_updated_at, "%Y-%m-%dT%H:%M:%S.%f" - ) - project_data = project.get("data") - if project_name and project_data: - existing_project = session.exec( - select(Flow).where( - Flow.name == project_name, Flow.folder == STARTER_FOLDER_NAME - ) - ).first() - if existing_project: - logger.info(f"Updating starter project {project_name}") - existing_project.data = project_data - existing_project.folder = STARTER_FOLDER_NAME - existing_project.description = project_description - existing_project.is_component = project_is_component - existing_project.updated_at = updated_at_datetime - # Now we need to update the project in the database - session.add(existing_project) - else: - logger.info(f"Creating starter project {project_name}") - session.add( - Flow( - name=project_name, - description=project_description, - is_component=project_is_component, - updated_at=updated_at_datetime, - folder=STARTER_FOLDER_NAME, - data=project_data, + with session_scope() as session: + starter_projects = load_starter_projects() + for project in starter_projects: + ( + project_name, + project_description, + project_is_component, + updated_at_datetime, + project_data, + ) = get_project_data(project) + if project_name and project_data: + existing_project = session.exec( + select(Flow).where( + Flow.name == project_name, Flow.folder == STARTER_FOLDER_NAME + ) + ).first() + if existing_project: + update_existing_project( + existing_project, + project_name, + project_description, + project_is_component, + updated_at_datetime, + project_data, + ) + else: + create_new_project( + session, + project_name, + project_description, + project_is_component, + updated_at_datetime, + project_data, ) - ) - session.commit() - session.close() - logger.info("Starter projects loaded into database") diff --git a/src/backend/langflow/services/deps.py b/src/backend/langflow/services/deps.py index 19f3dcbf0..7d2338b04 100644 --- a/src/backend/langflow/services/deps.py +++ b/src/backend/langflow/services/deps.py @@ -1,3 +1,4 @@ +from contextlib import contextmanager from typing import TYPE_CHECKING, Generator from langflow.services import ServiceType, service_manager @@ -54,6 +55,19 @@ def get_session() -> Generator["Session", None, None]: yield from db_service.get_session() +@contextmanager +def session_scope(): + session = next(get_session()) + try: + yield session + session.commit() + except: + session.rollback() + raise + finally: + session.close() + + def get_cache_service() -> "BaseCacheService": return service_manager.get(ServiceType.CACHE_SERVICE) # type: ignore From f2395aaee11748c571a03770c19829e917d6f9d1 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Tue, 5 Mar 2024 20:44:11 -0300 Subject: [PATCH 074/199] fix(exampleComponent): add Link component to wrap Button for navigation feat(exampleComponent): add Link to navigate to specific flow based on data id feat(components): add support for setting examples based on STARTER_FOLDER_NAME constant feat(components): filter out examples from all flows and set them separately feat(components): log examples and set them using setExamples function feat(MainPage): add examples state to store and use it to render ExampleCardComponent feat(MainPage): map over examples to render ExampleCardComponent for each example feat(flowsManagerStore): add examples array to store state feat(flowsManagerStore): add setExamples function to set examples in store feat(flow): add folder and user_id fields to FlowType feat(zustand): add examples array and setExamples function to FlowsManagerStoreType --- src/frontend/src/components/exampleComponent/index.tsx | 3 +++ .../src/pages/MainPage/components/components/index.tsx | 8 +++++++- src/frontend/src/pages/MainPage/index.tsx | 9 +++++---- src/frontend/src/stores/flowsManagerStore.ts | 4 ++++ src/frontend/src/types/flow/index.ts | 2 ++ src/frontend/src/types/zustand/flowsManager/index.ts | 2 ++ 6 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/frontend/src/components/exampleComponent/index.tsx b/src/frontend/src/components/exampleComponent/index.tsx index af0cf56ea..7dff31254 100644 --- a/src/frontend/src/components/exampleComponent/index.tsx +++ b/src/frontend/src/components/exampleComponent/index.tsx @@ -19,6 +19,7 @@ import { CardTitle, } from "../ui/card"; import { FlowType } from "../../types/flow"; +import { Link } from "react-router-dom"; export default function CollectionCardComponent({ data, @@ -58,6 +59,7 @@ export default function CollectionCardComponent({
+ +
diff --git a/src/frontend/src/pages/MainPage/components/components/index.tsx b/src/frontend/src/pages/MainPage/components/components/index.tsx index 86ee65d0c..0073447c0 100644 --- a/src/frontend/src/pages/MainPage/components/components/index.tsx +++ b/src/frontend/src/pages/MainPage/components/components/index.tsx @@ -14,6 +14,7 @@ import { import useAlertStore from "../../../../stores/alertStore"; import useFlowsManagerStore from "../../../../stores/flowsManagerStore"; import { FlowType } from "../../../../types/flow"; +import { STARTER_FOLDER_NAME } from "../../../../constants/constants"; export default function ComponentsComponent({ is_component = true, @@ -24,6 +25,7 @@ export default function ComponentsComponent({ const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow); const removeFlow = useFlowsManagerStore((state) => state.removeFlow); const isLoading = useFlowsManagerStore((state) => state.isLoading); + const setExamples = useFlowsManagerStore((state) => state.setExamples); const flows = useFlowsManagerStore((state) => state.flows); const setSuccessData = useAlertStore((state) => state.setSuccessData); const setErrorData = useAlertStore((state) => state.setErrorData); @@ -35,7 +37,7 @@ export default function ComponentsComponent({ useEffect(() => { if (isLoading) return; - const all = flows + let all = flows .filter((f) => (f.is_component ?? false) === is_component) .sort((a, b) => { if (a?.updated_at && b?.updated_at) { @@ -56,6 +58,10 @@ export default function ComponentsComponent({ }); const start = (pageIndex - 1) * pageSize; const end = start + pageSize; + const examples = all.filter(f=>(f.folder===STARTER_FOLDER_NAME && !f.user_id)); + console.log(examples); + setExamples(examples); + all = all.filter(f=>!(f.folder===STARTER_FOLDER_NAME && !f.user_id));; setData(all.slice(start, end)); }, [flows, isLoading, pageIndex, pageSize]); diff --git a/src/frontend/src/pages/MainPage/index.tsx b/src/frontend/src/pages/MainPage/index.tsx index fc8747094..88055ff79 100644 --- a/src/frontend/src/pages/MainPage/index.tsx +++ b/src/frontend/src/pages/MainPage/index.tsx @@ -29,6 +29,7 @@ export default function HomePage(): JSX.Element { const location = useLocation(); const pathname = location.pathname; const [openModal, setOpenModal] = useState(false); + const examples = useFlowsManagerStore((state) => state.examples); const is_component = pathname === "/components"; const dropdownOptions = [ { @@ -128,10 +129,10 @@ export default function HomePage(): JSX.Element {
- + {examples.map((example, idx) => { + return( + ) + })}
diff --git a/src/frontend/src/stores/flowsManagerStore.ts b/src/frontend/src/stores/flowsManagerStore.ts index fac806c5f..96b443152 100644 --- a/src/frontend/src/stores/flowsManagerStore.ts +++ b/src/frontend/src/stores/flowsManagerStore.ts @@ -37,6 +37,10 @@ const past = {}; const future = {}; const useFlowsManagerStore = create((set, get) => ({ + examples:[], + setExamples: (examples: FlowType[]) => { + set({ examples }); + }, currentFlowId: "", setCurrentFlowId: (currentFlowId: string) => { set((state) => ({ diff --git a/src/frontend/src/types/flow/index.ts b/src/frontend/src/types/flow/index.ts index 967d4e424..ffb364d61 100644 --- a/src/frontend/src/types/flow/index.ts +++ b/src/frontend/src/types/flow/index.ts @@ -13,6 +13,8 @@ export type FlowType = { updated_at?: string; date_created?: string; parent?: string; + folder?: string; + user_id?: string; }; export type NodeType = { diff --git a/src/frontend/src/types/zustand/flowsManager/index.ts b/src/frontend/src/types/zustand/flowsManager/index.ts index 471d4cf17..87fbf9a22 100644 --- a/src/frontend/src/types/zustand/flowsManager/index.ts +++ b/src/frontend/src/types/zustand/flowsManager/index.ts @@ -44,6 +44,8 @@ export type FlowsManagerStoreType = { undo: () => void; redo: () => void; takeSnapshot: () => void; + examples: Array; + setExamples: (examples: FlowType[]) => void; }; export type UseUndoRedoOptions = { From 781d2705aa56a60f88a14c2157b4024b3657f1ea Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Tue, 5 Mar 2024 20:54:07 -0300 Subject: [PATCH 075/199] Update exampleComponent and MainPage components --- .../src/components/exampleComponent/index.tsx | 47 +++++++++++-------- src/frontend/src/pages/MainPage/index.tsx | 2 +- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/frontend/src/components/exampleComponent/index.tsx b/src/frontend/src/components/exampleComponent/index.tsx index 7dff31254..fdaa36766 100644 --- a/src/frontend/src/components/exampleComponent/index.tsx +++ b/src/frontend/src/components/exampleComponent/index.tsx @@ -19,15 +19,17 @@ import { CardTitle, } from "../ui/card"; import { FlowType } from "../../types/flow"; -import { Link } from "react-router-dom"; +import { updateIds } from "../../utils/reactflowUtils"; +import { useNavigate } from "react-router-dom"; export default function CollectionCardComponent({ - data, + flow, }: { - data: FlowType; + flow: FlowType; authorized?: boolean; }) { const addFlow = useFlowsManagerStore((state) => state.addFlow); + const navigate = useNavigate(); return ( - -
{data.name}
+ +
{flow.name}
-
{data.description}
+
{flow.description}
@@ -59,20 +61,25 @@ export default function CollectionCardComponent({
- - - +
diff --git a/src/frontend/src/pages/MainPage/index.tsx b/src/frontend/src/pages/MainPage/index.tsx index 88055ff79..de546befa 100644 --- a/src/frontend/src/pages/MainPage/index.tsx +++ b/src/frontend/src/pages/MainPage/index.tsx @@ -131,7 +131,7 @@ export default function HomePage(): JSX.Element {
{examples.map((example, idx) => { return( - ) + ) })}
From 3b7f1928ff8d511cc28489d87e32f1cc3375453d Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Tue, 5 Mar 2024 20:59:15 -0300 Subject: [PATCH 076/199] update examples in MainPage using zustand --- .../src/pages/MainPage/components/components/index.tsx | 4 ---- src/frontend/src/pages/MainPage/index.tsx | 2 +- src/frontend/src/stores/flowsManagerStore.ts | 4 +++- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/frontend/src/pages/MainPage/components/components/index.tsx b/src/frontend/src/pages/MainPage/components/components/index.tsx index 0073447c0..234852388 100644 --- a/src/frontend/src/pages/MainPage/components/components/index.tsx +++ b/src/frontend/src/pages/MainPage/components/components/index.tsx @@ -58,10 +58,6 @@ export default function ComponentsComponent({ }); const start = (pageIndex - 1) * pageSize; const end = start + pageSize; - const examples = all.filter(f=>(f.folder===STARTER_FOLDER_NAME && !f.user_id)); - console.log(examples); - setExamples(examples); - all = all.filter(f=>!(f.folder===STARTER_FOLDER_NAME && !f.user_id));; setData(all.slice(start, end)); }, [flows, isLoading, pageIndex, pageSize]); diff --git a/src/frontend/src/pages/MainPage/index.tsx b/src/frontend/src/pages/MainPage/index.tsx index de546befa..9b35681eb 100644 --- a/src/frontend/src/pages/MainPage/index.tsx +++ b/src/frontend/src/pages/MainPage/index.tsx @@ -131,7 +131,7 @@ export default function HomePage(): JSX.Element {
{examples.map((example, idx) => { return( - ) + ) })}
diff --git a/src/frontend/src/stores/flowsManagerStore.ts b/src/frontend/src/stores/flowsManagerStore.ts index 96b443152..d993f40b0 100644 --- a/src/frontend/src/stores/flowsManagerStore.ts +++ b/src/frontend/src/stores/flowsManagerStore.ts @@ -25,6 +25,7 @@ import useAlertStore from "./alertStore"; import { useDarkStore } from "./darkStore"; import useFlowStore from "./flowStore"; import { useTypesStore } from "./typesStore"; +import { STARTER_FOLDER_NAME } from "../constants/constants"; let saveTimeoutId: NodeJS.Timeout | null = null; @@ -66,7 +67,8 @@ const useFlowsManagerStore = create((set, get) => ({ .then((dbData) => { if (dbData) { const { data, flows } = processFlows(dbData, false); - get().setFlows(flows); + get().setExamples(flows.filter(f=>(f.folder===STARTER_FOLDER_NAME && !f.user_id))); + get().setFlows(flows.filter(f=>!(f.folder===STARTER_FOLDER_NAME && !f.user_id))); useTypesStore.setState((state) => ({ data: { ...state.data, ["saved_components"]: data }, })); From e6672189538cdee2c3b049e71929bfd17ae5c8ae Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 17:36:09 -0300 Subject: [PATCH 077/199] Update Langflow documentation with new component descriptions --- docs/docs/getting-started/creating-flows.mdx | 23 +++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/docs/docs/getting-started/creating-flows.mdx b/docs/docs/getting-started/creating-flows.mdx index aecc3ea16..9c16d225f 100644 --- a/docs/docs/getting-started/creating-flows.mdx +++ b/docs/docs/getting-started/creating-flows.mdx @@ -7,7 +7,8 @@ import ReactPlayer from "react-player"; ## Compose -Creating flows with Langflow is easy. Drag sidebar components onto the canvas and connect them together to create your pipeline. Langflow provides a range of [LangChain components](https://python.langchain.com/docs/modules/) to choose from, including LLMs, prompt serializers, agents, and chains. +Creating flows with Langflow is easy. Drag sidebar components onto the canvas and connect them together to create your pipeline. +Langflow provides a range of Components to choose from, including **Chat Input**, **Chat Output**, **API Request** and **Prompt**. -## Fork +## Starter Flows -The easiest way to start with Langflow is by forking a **community example**. Forking an example stores a copy in your project collection, allowing you to edit and save the modified version as a new flow. +Langflow provides a range of starter flows to help you get started. These flows are pre-built and can be used as a starting point for your own flows.
-## Build +## Defining Inputs and Outputs + +Each flow can have multiple inputs and outputs. These can be defined by placing **Inputs** and **Outputs** components on the canvas. + +The **Inputs** components define the inputs to the flow. +Whenever you place an Input component on the canvas, it will allow you to interactively define change its value +from the Interactive Panel. + +The **Text Input** component allows you to define a text input, and the **Chat Input** component allows you to use the chat input from the Interactive Panel. + +The **Outputs** components define the outputs of the flow and work similarly to the Inputs components. + +Both Inputs and Outputs components can be connected to other components on the canvas and are used to define how the API works too. + -Building a flow means validating if the components have prerequisites fulfilled and are properly instantiated. When a chat message is sent, the flow will run for the first time, executing the pipeline.
Date: Tue, 5 Mar 2024 21:02:15 -0300 Subject: [PATCH 078/199] Update description and info in RecordsAsTextComponent --- src/backend/langflow/components/utilities/RecordsAsText.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/langflow/components/utilities/RecordsAsText.py b/src/backend/langflow/components/utilities/RecordsAsText.py index 18bf8be8c..f7750bdba 100644 --- a/src/backend/langflow/components/utilities/RecordsAsText.py +++ b/src/backend/langflow/components/utilities/RecordsAsText.py @@ -6,7 +6,7 @@ from langflow.schema import Record class RecordsAsTextComponent(CustomComponent): display_name = "Records to Text" - description = "Converts Records a list of Records to text using a template." + description = "Converts Records into single piece of text using a template." def build_config(self): return { @@ -16,7 +16,7 @@ class RecordsAsTextComponent(CustomComponent): }, "template": { "display_name": "Template", - "info": "The template to use for formatting the records. It must contain the keys {text} and {data}.", + "info": "The template to use for formatting the records. It can contain the keys {text}, {data} or any other key in the Record.", }, } From d770a3747b68c87afbee12984cf9d31bb61644f5 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 21:09:47 -0300 Subject: [PATCH 079/199] rename utilities folder to helpers --- .../components/{utilities => helpers}/APIRequest.py | 0 .../components/{utilities => helpers}/CustomComponent.py | 0 .../components/{utilities => helpers}/DocumentToRecord.py | 0 .../components/{utilities => helpers}/GetNotified.py | 0 .../components/{utilities => helpers}/IDGenerator.py | 0 .../components/{utilities => helpers}/ListFlows.py | 0 .../langflow/components/{utilities => helpers}/Notify.py | 0 .../components/{utilities => helpers}/PythonFunction.py | 0 .../components/{utilities => helpers}/RecordsAsText.py | 0 .../langflow/components/{utilities => helpers}/RunFlow.py | 0 .../components/{utilities => helpers}/RunnableExecutor.py | 0 .../components/{utilities => helpers}/SQLExecutor.py | 0 .../components/{utilities => helpers}/SharedState.py | 0 .../components/{utilities => helpers}/ShouldRunNext.py | 0 .../components/{utilities => helpers}/TextToRecord.py | 0 .../components/{utilities => helpers}/__init__.py | 0 src/frontend/src/utils/styleUtils.ts | 8 +------- 17 files changed, 1 insertion(+), 7 deletions(-) rename src/backend/langflow/components/{utilities => helpers}/APIRequest.py (100%) rename src/backend/langflow/components/{utilities => helpers}/CustomComponent.py (100%) rename src/backend/langflow/components/{utilities => helpers}/DocumentToRecord.py (100%) rename src/backend/langflow/components/{utilities => helpers}/GetNotified.py (100%) rename src/backend/langflow/components/{utilities => helpers}/IDGenerator.py (100%) rename src/backend/langflow/components/{utilities => helpers}/ListFlows.py (100%) rename src/backend/langflow/components/{utilities => helpers}/Notify.py (100%) rename src/backend/langflow/components/{utilities => helpers}/PythonFunction.py (100%) rename src/backend/langflow/components/{utilities => helpers}/RecordsAsText.py (100%) rename src/backend/langflow/components/{utilities => helpers}/RunFlow.py (100%) rename src/backend/langflow/components/{utilities => helpers}/RunnableExecutor.py (100%) rename src/backend/langflow/components/{utilities => helpers}/SQLExecutor.py (100%) rename src/backend/langflow/components/{utilities => helpers}/SharedState.py (100%) rename src/backend/langflow/components/{utilities => helpers}/ShouldRunNext.py (100%) rename src/backend/langflow/components/{utilities => helpers}/TextToRecord.py (100%) rename src/backend/langflow/components/{utilities => helpers}/__init__.py (100%) diff --git a/src/backend/langflow/components/utilities/APIRequest.py b/src/backend/langflow/components/helpers/APIRequest.py similarity index 100% rename from src/backend/langflow/components/utilities/APIRequest.py rename to src/backend/langflow/components/helpers/APIRequest.py diff --git a/src/backend/langflow/components/utilities/CustomComponent.py b/src/backend/langflow/components/helpers/CustomComponent.py similarity index 100% rename from src/backend/langflow/components/utilities/CustomComponent.py rename to src/backend/langflow/components/helpers/CustomComponent.py diff --git a/src/backend/langflow/components/utilities/DocumentToRecord.py b/src/backend/langflow/components/helpers/DocumentToRecord.py similarity index 100% rename from src/backend/langflow/components/utilities/DocumentToRecord.py rename to src/backend/langflow/components/helpers/DocumentToRecord.py diff --git a/src/backend/langflow/components/utilities/GetNotified.py b/src/backend/langflow/components/helpers/GetNotified.py similarity index 100% rename from src/backend/langflow/components/utilities/GetNotified.py rename to src/backend/langflow/components/helpers/GetNotified.py diff --git a/src/backend/langflow/components/utilities/IDGenerator.py b/src/backend/langflow/components/helpers/IDGenerator.py similarity index 100% rename from src/backend/langflow/components/utilities/IDGenerator.py rename to src/backend/langflow/components/helpers/IDGenerator.py diff --git a/src/backend/langflow/components/utilities/ListFlows.py b/src/backend/langflow/components/helpers/ListFlows.py similarity index 100% rename from src/backend/langflow/components/utilities/ListFlows.py rename to src/backend/langflow/components/helpers/ListFlows.py diff --git a/src/backend/langflow/components/utilities/Notify.py b/src/backend/langflow/components/helpers/Notify.py similarity index 100% rename from src/backend/langflow/components/utilities/Notify.py rename to src/backend/langflow/components/helpers/Notify.py diff --git a/src/backend/langflow/components/utilities/PythonFunction.py b/src/backend/langflow/components/helpers/PythonFunction.py similarity index 100% rename from src/backend/langflow/components/utilities/PythonFunction.py rename to src/backend/langflow/components/helpers/PythonFunction.py diff --git a/src/backend/langflow/components/utilities/RecordsAsText.py b/src/backend/langflow/components/helpers/RecordsAsText.py similarity index 100% rename from src/backend/langflow/components/utilities/RecordsAsText.py rename to src/backend/langflow/components/helpers/RecordsAsText.py diff --git a/src/backend/langflow/components/utilities/RunFlow.py b/src/backend/langflow/components/helpers/RunFlow.py similarity index 100% rename from src/backend/langflow/components/utilities/RunFlow.py rename to src/backend/langflow/components/helpers/RunFlow.py diff --git a/src/backend/langflow/components/utilities/RunnableExecutor.py b/src/backend/langflow/components/helpers/RunnableExecutor.py similarity index 100% rename from src/backend/langflow/components/utilities/RunnableExecutor.py rename to src/backend/langflow/components/helpers/RunnableExecutor.py diff --git a/src/backend/langflow/components/utilities/SQLExecutor.py b/src/backend/langflow/components/helpers/SQLExecutor.py similarity index 100% rename from src/backend/langflow/components/utilities/SQLExecutor.py rename to src/backend/langflow/components/helpers/SQLExecutor.py diff --git a/src/backend/langflow/components/utilities/SharedState.py b/src/backend/langflow/components/helpers/SharedState.py similarity index 100% rename from src/backend/langflow/components/utilities/SharedState.py rename to src/backend/langflow/components/helpers/SharedState.py diff --git a/src/backend/langflow/components/utilities/ShouldRunNext.py b/src/backend/langflow/components/helpers/ShouldRunNext.py similarity index 100% rename from src/backend/langflow/components/utilities/ShouldRunNext.py rename to src/backend/langflow/components/helpers/ShouldRunNext.py diff --git a/src/backend/langflow/components/utilities/TextToRecord.py b/src/backend/langflow/components/helpers/TextToRecord.py similarity index 100% rename from src/backend/langflow/components/utilities/TextToRecord.py rename to src/backend/langflow/components/helpers/TextToRecord.py diff --git a/src/backend/langflow/components/utilities/__init__.py b/src/backend/langflow/components/helpers/__init__.py similarity index 100% rename from src/backend/langflow/components/utilities/__init__.py rename to src/backend/langflow/components/helpers/__init__.py diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index dc7115838..409e1160f 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -47,9 +47,7 @@ import { FileText, FileType2, FileUp, - Fingerprint, FlaskConical, - FolderOpen, FolderPlus, FormInput, Forward, @@ -66,7 +64,6 @@ import { Key, Laptop2, Layers, - Lightbulb, Link, Loader2, Lock, @@ -94,7 +91,6 @@ import { Redo, RefreshCcw, Repeat, - Rocket, Save, SaveAll, Scissors, @@ -126,10 +122,8 @@ import { UserPlus2, Users2, Variable, - Wand, Wand2, Workflow, - Wrench, X, XCircle, Zap, @@ -265,7 +259,7 @@ export const nodeNames: { [char: string]: string } = { wrappers: "Wrappers", textsplitters: "Text Splitters", retrievers: "Retrievers", - utilities: "Helpers", + helpers: "Helpers", langchain_utilities: "Utilities", output_parsers: "Output Parsers", custom_components: "Custom", From f304f7ef38e7d66fdead09dfe679dfc17420c4d4 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 21:16:34 -0300 Subject: [PATCH 080/199] Delete io/__init__.py file --- src/backend/langflow/components/io/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/backend/langflow/components/io/__init__.py diff --git a/src/backend/langflow/components/io/__init__.py b/src/backend/langflow/components/io/__init__.py deleted file mode 100644 index e69de29bb..000000000 From a2dfadcf4aaa23e3df2b994432c65e3f4f20eb02 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 21:24:53 -0300 Subject: [PATCH 081/199] Update sidebar component and styleUtils --- .../components/extraSidebarComponent/utils.tsx | 3 ++- src/frontend/src/utils/styleUtils.ts | 10 ++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/utils.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/utils.tsx index f8b5a0af9..998ed8cd7 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/utils.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/utils.tsx @@ -6,8 +6,9 @@ export function sortKeys(a: string, b: string) { "outputs", "prompts", "data", + "prompts", "models", - "utilities", + "helpers", ]; const indexA = order.indexOf(a.toLowerCase()); const indexB = order.indexOf(b.toLowerCase()); diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index c9425dbe4..ac88826ca 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -6,11 +6,8 @@ import { Binary, BookMarked, BookmarkPlus, - PlusCircle, - PlusSquare, Bot, Boxes, - GitCompare, Braces, BrainCircuit, Check, @@ -90,6 +87,8 @@ import { Pin, Play, Plus, + PlusCircle, + PlusSquare, PocketKnife, Redo, RefreshCcw, @@ -231,7 +230,7 @@ export const nodeColors: { [char: string]: string } = { textsplitters: "#B47CB5", toolkits: "#DB2C2C", wrappers: "#E6277A", - utilities: "#31A3CC", + helpers: "#31A3CC", langchain_utilities: "#31A3CC", output_parsers: "#E6A627", str: "#31a3cc", @@ -354,7 +353,7 @@ export const nodeIconsLucide: iconsType = { toolkits: Package2, textsplitters: Scissors, wrappers: Gift, - utilities: Wand2, + helpers: Wand2, langchain_utilities: PocketKnife, WolframAlphaAPIWrapper: SvgWolfram, output_parsers: Compass, @@ -475,4 +474,3 @@ export const nodeIconsLucide: iconsType = { Delete, Command, }; - From 2b8164f28d0a97719d57636f95c26934a7cfc7e4 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 21:27:05 -0300 Subject: [PATCH 082/199] Refactor APIRequest class to use httpx.AsyncClient --- .../langflow/components/helpers/APIRequest.py | 56 +++++++++---------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/src/backend/langflow/components/helpers/APIRequest.py b/src/backend/langflow/components/helpers/APIRequest.py index 2e73979ff..dfaf2ed07 100644 --- a/src/backend/langflow/components/helpers/APIRequest.py +++ b/src/backend/langflow/components/helpers/APIRequest.py @@ -1,8 +1,7 @@ import asyncio -from typing import List, Optional, Union -import httpx +from typing import List, Optional -import requests +import httpx from langflow import CustomComponent from langflow.schema import Record @@ -42,7 +41,7 @@ class APIRequest(CustomComponent): async def make_request( self, - session: requests.Session, + client: httpx.AsyncClient, method: str, url: str, headers: Optional[dict] = None, @@ -55,23 +54,22 @@ class APIRequest(CustomComponent): 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, - }, - ) + 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", @@ -88,7 +86,7 @@ class APIRequest(CustomComponent): method: str, url: List[str], headers: Optional[dict] = None, - record: Optional[Union[Record, List[Record]]] = None, + record: Optional[Record] = None, timeout: int = 5, ) -> List[Record]: if headers is None: @@ -99,11 +97,11 @@ class APIRequest(CustomComponent): 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) - ] - ) + async with httpx.AsyncClient() as client: + results = await asyncio.gather( + *[ + self.make_request(client, method, u, headers, rec, timeout) + for u, rec in zip(urls, records) + ] + ) return results From 6af16272fd407ce399a92d75fb569f0426e2d495 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Tue, 5 Mar 2024 21:34:36 -0300 Subject: [PATCH 083/199] Add ShadTooltip component and update styling in exampleComponent and MainPage --- .../src/components/exampleComponent/index.tsx | 40 ++++++++++--------- src/frontend/src/pages/MainPage/index.tsx | 2 +- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/frontend/src/components/exampleComponent/index.tsx b/src/frontend/src/components/exampleComponent/index.tsx index fdaa36766..fb6d570aa 100644 --- a/src/frontend/src/components/exampleComponent/index.tsx +++ b/src/frontend/src/components/exampleComponent/index.tsx @@ -53,7 +53,9 @@ export default function CollectionCardComponent({
-
{flow.description}
+ +
{flow.description}
+
@@ -61,25 +63,25 @@ export default function CollectionCardComponent({
- + navigate("/flow/" + id); + }); + }} + tabIndex={-1} + variant="outline" + size="sm" + className="whitespace-nowrap " + > + + Select Flow +
diff --git a/src/frontend/src/pages/MainPage/index.tsx b/src/frontend/src/pages/MainPage/index.tsx index 9b35681eb..2574f03af 100644 --- a/src/frontend/src/pages/MainPage/index.tsx +++ b/src/frontend/src/pages/MainPage/index.tsx @@ -128,7 +128,7 @@ export default function HomePage(): JSX.Element { /> -
+
{examples.map((example, idx) => { return( ) From 07f3ee59d74e291d8e9d40d02db667ea4046c734 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 21:40:09 -0300 Subject: [PATCH 084/199] Refactor code in utils.py --- src/backend/langflow/interface/custom/utils.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/backend/langflow/interface/custom/utils.py b/src/backend/langflow/interface/custom/utils.py index 513669f75..c7e33b992 100644 --- a/src/backend/langflow/interface/custom/utils.py +++ b/src/backend/langflow/interface/custom/utils.py @@ -15,15 +15,12 @@ from langflow.interface.custom.attributes import ATTR_FUNC_MAPPING from langflow.interface.custom.code_parser.utils import extract_inner_type from langflow.interface.custom.custom_component import CustomComponent from langflow.interface.custom.directory_reader.utils import ( - build_custom_component_list_from_path, - determine_component_name, - merge_nested_dicts_with_renaming, -) + build_custom_component_list_from_path, determine_component_name, + merge_nested_dicts_with_renaming) from langflow.interface.custom.eval import eval_custom_component_code from langflow.template.field.base import TemplateField -from langflow.template.frontend_node.custom_components import ( - CustomComponentFrontendNode, -) +from langflow.template.frontend_node.custom_components import \ + CustomComponentFrontendNode from langflow.utils import validate from langflow.utils.util import get_base_classes @@ -224,6 +221,8 @@ def add_extra_fields(frontend_node, field_config, function_args): key in function_args_names for key in field_config.keys() ): for field_name, field_config in _field_config.copy().items(): + if "name" not in extra_field: + continue config = _field_config.get(field_name, {}) config = config.model_dump() if isinstance(config, BaseModel) else config field_name, field_type, field_value, field_required = get_field_properties( From c56846eeb55c1bc48e07100d3d70657001d72fad Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 21:40:16 -0300 Subject: [PATCH 085/199] Update nodeColors in styleUtils.ts --- src/frontend/src/utils/styleUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index 409e1160f..ce400444f 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -228,7 +228,7 @@ export const nodeColors: { [char: string]: string } = { textsplitters: "#B47CB5", toolkits: "#DB2C2C", wrappers: "#E6277A", - utilities: "#31A3CC", + helpers: "#31A3CC", langchain_utilities: "#31A3CC", output_parsers: "#E6A627", str: "#31a3cc", From 1919621ef89f7d6ad529b2b8ea8a2dcc388bd91b Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 21:42:55 -0300 Subject: [PATCH 086/199] Update styleUtils.ts: changed "utilities" to "helpers" --- src/frontend/src/utils/styleUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index ce400444f..cabae91b6 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -351,7 +351,7 @@ export const nodeIconsLucide: iconsType = { toolkits: Package2, textsplitters: Scissors, wrappers: Gift, - utilities: Wand2, + helpers: Wand2, langchain_utilities: PocketKnife, WolframAlphaAPIWrapper: SvgWolfram, output_parsers: Compass, From 57d6a78d4ee6aa1292a72b91b04fbeb0b6aa07b6 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 21:47:02 -0300 Subject: [PATCH 087/199] Refactor utils.py: Fix formatting and import statements --- src/backend/langflow/interface/custom/utils.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/backend/langflow/interface/custom/utils.py b/src/backend/langflow/interface/custom/utils.py index c7e33b992..49e686b5e 100644 --- a/src/backend/langflow/interface/custom/utils.py +++ b/src/backend/langflow/interface/custom/utils.py @@ -15,12 +15,15 @@ from langflow.interface.custom.attributes import ATTR_FUNC_MAPPING from langflow.interface.custom.code_parser.utils import extract_inner_type from langflow.interface.custom.custom_component import CustomComponent from langflow.interface.custom.directory_reader.utils import ( - build_custom_component_list_from_path, determine_component_name, - merge_nested_dicts_with_renaming) + build_custom_component_list_from_path, + determine_component_name, + merge_nested_dicts_with_renaming, +) from langflow.interface.custom.eval import eval_custom_component_code from langflow.template.field.base import TemplateField -from langflow.template.frontend_node.custom_components import \ - CustomComponentFrontendNode +from langflow.template.frontend_node.custom_components import ( + CustomComponentFrontendNode, +) from langflow.utils import validate from langflow.utils.util import get_base_classes @@ -221,7 +224,7 @@ def add_extra_fields(frontend_node, field_config, function_args): key in function_args_names for key in field_config.keys() ): for field_name, field_config in _field_config.copy().items(): - if "name" not in extra_field: + if "name" not in field_config or field_name == "code": continue config = _field_config.get(field_name, {}) config = config.model_dump() if isinstance(config, BaseModel) else config From 94122cbe444a3415c7772ea8f5ed9dd344575134 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 21:47:09 -0300 Subject: [PATCH 088/199] Add priority sidebar order constant and use it for sorting keys in extraSidebarComponent --- src/frontend/src/constants/constants.ts | 11 +++++++++++ .../components/extraSidebarComponent/utils.tsx | 16 +++++----------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/frontend/src/constants/constants.ts b/src/frontend/src/constants/constants.ts index 3f4584102..10b6e41df 100644 --- a/src/frontend/src/constants/constants.ts +++ b/src/frontend/src/constants/constants.ts @@ -727,3 +727,14 @@ export const STATUS_BUILD = "Build to validate status."; export const STATUS_BUILDING = "Building..."; export const SAVED_HOVER = "Last saved at "; export const RUN_TIMESTAMP_PREFIX = "Last Run: "; + +export const PRIORITY_SIDEBAR_ORDER = [ + "saved_components", + "inputs", + "outputs", + "prompts", + "data", + "prompt", + "models", + "helpers", +]; diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/utils.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/utils.tsx index f8b5a0af9..ee0509cd0 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/utils.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/utils.tsx @@ -1,16 +1,10 @@ +import { PRIORITY_SIDEBAR_ORDER } from "../../../../constants/constants"; + export function sortKeys(a: string, b: string) { // Define the order of specific keys - const order = [ - "saved_components", - "inputs", - "outputs", - "prompts", - "data", - "models", - "utilities", - ]; - const indexA = order.indexOf(a.toLowerCase()); - const indexB = order.indexOf(b.toLowerCase()); + + const indexA = PRIORITY_SIDEBAR_ORDER.indexOf(a.toLowerCase()); + const indexB = PRIORITY_SIDEBAR_ORDER.indexOf(b.toLowerCase()); // Check if both keys are in the predefined order if (indexA !== -1 && indexB !== -1) { From f1eb6ee6730e6fb7ab03e7a2a3ec95f0f5f6010f Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 21:54:48 -0300 Subject: [PATCH 089/199] Add STARTER_FOLDER_NAME constant --- src/frontend/src/constants/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/constants/constants.ts b/src/frontend/src/constants/constants.ts index 10b6e41df..897a1b4b3 100644 --- a/src/frontend/src/constants/constants.ts +++ b/src/frontend/src/constants/constants.ts @@ -727,7 +727,7 @@ export const STATUS_BUILD = "Build to validate status."; export const STATUS_BUILDING = "Building..."; export const SAVED_HOVER = "Last saved at "; export const RUN_TIMESTAMP_PREFIX = "Last Run: "; - +export const STARTER_FOLDER_NAME = "Starter Projects"; export const PRIORITY_SIDEBAR_ORDER = [ "saved_components", "inputs", From abf9bbd4a2c00b600dacd98b9177c23428100b18 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 22:05:57 -0300 Subject: [PATCH 090/199] Update IDGenerator.py to generate a unique ID in the build_config --- .../langflow/components/helpers/IDGenerator.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/backend/langflow/components/helpers/IDGenerator.py b/src/backend/langflow/components/helpers/IDGenerator.py index ceb937a6c..1a8cecbfd 100644 --- a/src/backend/langflow/components/helpers/IDGenerator.py +++ b/src/backend/langflow/components/helpers/IDGenerator.py @@ -1,5 +1,5 @@ import uuid -from typing import Text +from typing import Any, Text from langflow import CustomComponent @@ -9,11 +9,20 @@ class UUIDGeneratorComponent(CustomComponent): display_name = "Unique ID Generator" description = "Generates a unique ID." - def generate(self, *args, **kwargs): - return Text(uuid.uuid4().hex) + def update_build_config( + self, build_config: dict, field_name: Text, field_value: Any + ): + if field_name == "unique_id": + build_config[field_name]["value"] = str(uuid.uuid4()) + return build_config def build_config(self): - return {"unique_id": {"display_name": "Value", "value": self.generate}} + return { + "unique_id": { + "display_name": "Value", + "refresh": True, + } + } def build(self, unique_id: str) -> str: return unique_id From 930a2c01ecd1930547561bb367ebe0ee600c7e95 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 22:06:06 -0300 Subject: [PATCH 091/199] Remove console.log statement and format code --- src/frontend/src/CustomNodes/GenericNode/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index ddfc04aef..80e5ad0f7 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -44,7 +44,6 @@ export default function GenericNode({ const buildFlow = useFlowStore((state) => state.buildFlow); const setNode = useFlowStore((state) => state.setNode); const name = nodeIconsLucide[data.type] ? data.type : types[data.type]; - console.log(types[data.type]) const [inputName, setInputName] = useState(false); const [nodeName, setNodeName] = useState(data.node!.display_name); const [inputDescription, setInputDescription] = useState(false); @@ -158,7 +157,7 @@ export default function GenericNode({ const iconElement = data?.node?.icon; const iconColor = nodeColors[types[data.type]]; const iconName = - iconElement || (data.node?.flow ? "group_components" : name); + iconElement || (data.node?.flow ? "group_components" : name); const iconClassName = `generic-node-icon ${ !showNode ? " absolute inset-x-6 h-12 w-12 " : "" }`; From 1f77b49191ffb6e600d09fbbeea0707f47225128 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 22:24:08 -0300 Subject: [PATCH 092/199] Add MessageHistoryComponent to retrieve stored messages --- .../langflow/components/{memories => helpers}/MessageHistory.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/backend/langflow/components/{memories => helpers}/MessageHistory.py (100%) diff --git a/src/backend/langflow/components/memories/MessageHistory.py b/src/backend/langflow/components/helpers/MessageHistory.py similarity index 100% rename from src/backend/langflow/components/memories/MessageHistory.py rename to src/backend/langflow/components/helpers/MessageHistory.py From bc37bc9982c4ac2e3d105486a2a9fe66bacc9dd3 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 22:24:18 -0300 Subject: [PATCH 093/199] Update APIRequest.py to use record.data instead of record.text --- src/backend/langflow/components/helpers/APIRequest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/langflow/components/helpers/APIRequest.py b/src/backend/langflow/components/helpers/APIRequest.py index dfaf2ed07..4d9f3ce9c 100644 --- a/src/backend/langflow/components/helpers/APIRequest.py +++ b/src/backend/langflow/components/helpers/APIRequest.py @@ -52,7 +52,7 @@ class APIRequest(CustomComponent): if method not in ["GET", "POST", "PATCH", "PUT"]: raise ValueError(f"Unsupported method: {method}") - data = record.text if record else None + data = record.data if record else None try: response = await client.request( method, url, headers=headers, content=data, timeout=timeout From 13a91a2625c5bc334d28f1bcf5287d686de06d7a Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 22:46:24 -0300 Subject: [PATCH 094/199] Add ExtractDataFromRecord and MergeRecords components --- .../helpers/ExtractDataFromRecord.py | 17 +++++++++++++ .../components/helpers/MergeRecords.py | 25 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 src/backend/langflow/components/helpers/ExtractDataFromRecord.py create mode 100644 src/backend/langflow/components/helpers/MergeRecords.py diff --git a/src/backend/langflow/components/helpers/ExtractDataFromRecord.py b/src/backend/langflow/components/helpers/ExtractDataFromRecord.py new file mode 100644 index 000000000..00fda4b40 --- /dev/null +++ b/src/backend/langflow/components/helpers/ExtractDataFromRecord.py @@ -0,0 +1,17 @@ +from langflow import CustomComponent +from langflow.schema import Record + + +class ExtractKeyFromRecordComponent(CustomComponent): + display_name = "Extract Key From Record" + description = "Extracts a key from a record." + + field_config = { + "record": {"display_name": "Record"}, + } + + def build(self, record: Record, key: str, silent_error: bool = True) -> dict: + + data = getattr(record, key) + self.status = data + return data diff --git a/src/backend/langflow/components/helpers/MergeRecords.py b/src/backend/langflow/components/helpers/MergeRecords.py new file mode 100644 index 000000000..9c280d12a --- /dev/null +++ b/src/backend/langflow/components/helpers/MergeRecords.py @@ -0,0 +1,25 @@ +from langflow import CustomComponent +from langflow.schema import Record + + +class MergeRecordsComponent(CustomComponent): + display_name = "Merge Records" + description = "Merges records." + + field_config = { + "records": {"display_name": "Records"}, + } + + def build(self, records: list[Record]) -> Record: + if not records: + return records + if len(records) == 1: + return records[0] + merged_record = None + for record in records: + if merged_record is None: + merged_record = record + else: + merged_record += record + self.status = merged_record + return merged_record From 1b4f0138d267bf63c0864ea3a2c19bd4c3856527 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 22:46:32 -0300 Subject: [PATCH 095/199] Handle KeyError in Graph class constructor --- src/backend/langflow/graph/graph/base.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/backend/langflow/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py index 32c1369d8..ba2f9927a 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -320,9 +320,12 @@ class Graph: return cls(vertices, edges, flow_id) except KeyError as exc: logger.exception(exc) - raise ValueError( - f"Invalid payload. Expected keys 'nodes' and 'edges'. Found {list(payload.keys())}" - ) from exc + if "nodes" not in payload and "edges" not in payload: + logger.exception(exc) + raise ValueError( + f"Invalid payload. Expected keys 'nodes' and 'edges'. Found {list(payload.keys())}" + ) from exc + raise ValueError(f"Error while creating graph from payload: {exc}") from exc def __eq__(self, other: object) -> bool: if not isinstance(other, Graph): From 8990a781b19777d0070a45d532d15fef73220c62 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 22:46:42 -0300 Subject: [PATCH 096/199] Refactor Record class to allow attribute-like access to data dictionary --- src/backend/langflow/schema/schema.py | 83 ++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 15 deletions(-) diff --git a/src/backend/langflow/schema/schema.py b/src/backend/langflow/schema/schema.py index 639c9da96..5a20d941f 100644 --- a/src/backend/langflow/schema/schema.py +++ b/src/backend/langflow/schema/schema.py @@ -1,6 +1,6 @@ -from typing import Any, Optional +import copy -from langchain_core.documents import Document +from langchain_core.documents import Document # Assumed import from pydantic import BaseModel @@ -9,12 +9,11 @@ class Record(BaseModel): Represents a record with text and optional data. Attributes: - text (str): The text of the record. data (dict, optional): Additional data associated with the record. """ - text: Optional[str] = "" data: dict = {} + _default_value = None @classmethod def from_document(cls, document: Document) -> "Record": @@ -27,7 +26,22 @@ class Record(BaseModel): Returns: Record: The converted Record. """ - return cls(text=document.page_content, data=document.metadata) + data = document.metadata + data["text"] = document.page_content + return cls(data=data) + + def __add__(self, other: "Record") -> "Record": + """ + Concatenates the text of two records and combines their data. + + Args: + other (Record): The other record to concatenate with. + + Returns: + Record: The concatenated record. + """ + combined_data = {**self.data, **other.data} + return Record(data=combined_data) def to_lc_document(self) -> Document: """ @@ -38,20 +52,59 @@ class Record(BaseModel): """ return Document(page_content=self.text, metadata=self.data) - def __call__(self, *args: Any, **kwds: Any) -> Any: + def __getattr__(self, key): """ - Returns the text of the record. + Allows attribute-like access to the data dictionary. + """ + try: + if key == "data" or key.startswith("_"): + return super().__getattr__(key) - Returns: - Any: The text of the record. + return self.data.get(key, self._default_value) + except KeyError: + # Fallback to default behavior to raise AttributeError for undefined attributes + raise AttributeError( + f"'{type(self).__name__}' object has no attribute '{key}'" + ) + + def __setattr__(self, key, value): """ - return self.text + Allows attribute-like setting of values in the data dictionary, + while still allowing direct assignment to class attributes. + """ + if key == "data" or key.startswith("_"): + super().__setattr__(key, value) + else: + self.data[key] = value + + def __delattr__(self, key): + """ + Allows attribute-like deletion from the data dictionary. + """ + if key == "data" or key.startswith("_"): + super().__delattr__(key) + else: + del self.data[key] + + def __deepcopy__(self, memo): + """ + Custom deepcopy implementation to handle copying of the Record object. + """ + cls = self.__class__ + result = cls.__new__(cls) + memo[id(self)] = result + for k, v in self.__dict__.items(): + setattr(result, k, copy.deepcopy(v, memo)) + return result def __str__(self) -> str: """ - Returns the text of the record. - - Returns: - str: The text and data of the record. + Returns a string representation of the Record, including text and data. """ - return self.model_dump_json(indent=2) + # Assuming a method to dump model data as JSON string exists. + # If it doesn't, you might need to implement it or use json.dumps() directly. + # build the string considering all keys in the data dictionary + prefix = "Record(" + suffix = ")" + text = ", ".join([f"{k}={v}" for k, v in self.data.items()]) + return prefix + text + suffix From 43fdc7bbba37808c8dca51db4432165c0976d08b Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 23:01:54 -0300 Subject: [PATCH 097/199] Refactor CustomComponent class and update to_records method --- .../custom_component/custom_component.py | 61 ++++++++++++------- 1 file changed, 39 insertions(+), 22 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 dde147d77..35bdd2c5f 100644 --- a/src/backend/langflow/interface/custom/custom_component/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component/custom_component.py @@ -1,7 +1,15 @@ import operator from pathlib import Path -from typing import (TYPE_CHECKING, Any, Callable, ClassVar, List, Optional, - Sequence, Union) +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + List, + Optional, + Sequence, + Union, +) from uuid import UUID import yaml @@ -12,13 +20,17 @@ from sqlmodel import select from langflow.interface.custom.code_parser.utils import ( extract_inner_type_from_generic_alias, - extract_union_types_from_generic_alias) + extract_union_types_from_generic_alias, +) from langflow.interface.custom.custom_component.component import Component from langflow.schema import Record from langflow.services.database.models.flow import Flow from langflow.services.database.utils import session_getter -from langflow.services.deps import (get_credential_service, get_db_service, - get_storage_service) +from langflow.services.deps import ( + get_credential_service, + get_db_service, + get_storage_service, +) from langflow.services.storage.service import StorageService from langflow.utils import validate @@ -126,7 +138,9 @@ class CustomComponent(Component): def build_config(self): return self.field_config - def update_build_config(self, build_config: dict, field_name: str, field_value: Any): + def update_build_config( + self, build_config: dict, field_name: str, field_value: Any + ): build_config[field_name] = field_value return build_config @@ -135,7 +149,7 @@ class CustomComponent(Component): return self.get_code_tree(self.code or "") def to_records( - self, data: Any, text_key: str = "text", data_key: str = "data" + self, data: Any, keys: Optional[List[str]] = None, silent_errors: bool = False ) -> List[Record]: """ Converts input data into a list of Record objects. @@ -144,8 +158,9 @@ class CustomComponent(Component): data (Any): The input data to be converted. It can be a single item or a sequence of items. If the input data is a Langchain Document, text_key and data_key are ignored. - text_key (str, optional): The key to access the text value in each item. Defaults to "text". - data_key (str, optional): The key to access the data value in each item. Defaults to "data". + keys (List[str], optional): The keys to access the text and data values in each item. + It should be a list of strings where the first element is the text key and the second element is the data key. + Defaults to None, in which case the default keys "text" and "data" are used. Returns: List[Record]: A list of Record objects. @@ -158,27 +173,29 @@ class CustomComponent(Component): if not isinstance(data, Sequence): data = [data] for item in data: + data_dict = {} if isinstance(item, Document): - item = {"text": item.page_content, "data": item.metadata} + data_dict = item.metadata + data_dict["text"] = item.page_content elif isinstance(item, BaseModel): model_dump = item.model_dump() - if text_key not in model_dump: - raise ValueError(f"Key '{text_key}' not found in BaseModel item.") - if data_key not in model_dump: - raise ValueError(f"Key '{data_key}' not found in BaseModel item.") - item = {"text": model_dump[text_key], "data": model_dump[data_key]} + for key in keys: + if silent_errors: + data_dict[key] = model_dump.get(key, "") + else: + try: + data_dict[key] = model_dump[key] + except KeyError: + raise ValueError(f"Key {key} not found in {item}") + elif isinstance(item, str): - item = {"text": item, "data": {}} + data_dict = {"text": item} elif isinstance(item, dict): - if text_key not in item: - raise ValueError(f"Key '{text_key}' not found in dictionary item.") - if data_key not in item: - raise ValueError(f"Key '{data_key}' not found in dictionary item.") - item = {"text": item[text_key], "data": item[data_key]} + data_dict = item.copy() else: raise ValueError(f"Invalid data type: {type(item)}") - records.append(Record(**item)) + records.append(Record(data=data_dict)) return records From ee863342868cfc8bb4ec8a6e2dd515bd0004fdaf Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 23:15:36 -0300 Subject: [PATCH 098/199] Refactor TemplateField class in base.py --- src/backend/langflow/template/field/base.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/backend/langflow/template/field/base.py b/src/backend/langflow/template/field/base.py index 455d5779b..858ed00bc 100644 --- a/src/backend/langflow/template/field/base.py +++ b/src/backend/langflow/template/field/base.py @@ -68,7 +68,9 @@ class TemplateField(BaseModel): refresh: Optional[bool] = None """Specifies if the field should be refreshed. Defaults to False.""" - range_spec: Optional[RangeSpec] = Field(default=None, serialization_alias="rangeSpec") + range_spec: Optional[RangeSpec] = Field( + default=None, serialization_alias="rangeSpec" + ) """Range specification for the field. Defaults to None.""" title_case: bool = False @@ -88,6 +90,8 @@ class TemplateField(BaseModel): result["input_types"].append("Text") if self.field_type == "Text": result["type"] = "str" + else: + result["type"] = self.field_type return result @field_serializer("file_path") @@ -117,6 +121,10 @@ class TemplateField(BaseModel): if not isinstance(value, list): raise ValueError("file_types must be a list") return [ - (f".{file_type}" if isinstance(file_type, str) and not file_type.startswith(".") else file_type) + ( + f".{file_type}" + if isinstance(file_type, str) and not file_type.startswith(".") + else file_type + ) for file_type in value ] From 22debde71d31b61748d5b17da54e1f57ae34c421 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 5 Mar 2024 23:19:20 -0300 Subject: [PATCH 099/199] Refactor parameterComponent to conditionally set the width of InputListComponent --- .../GenericNode/components/parameterComponent/index.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 7a6c306f1..c3e04627f 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -449,7 +449,12 @@ export default function ParameterComponent({ !data.node?.template[name].options ? (
{data.node?.template[name].list ? ( -
+
Date: Wed, 6 Mar 2024 00:50:27 -0300 Subject: [PATCH 100/199] Refactor APIRequest class to use 'body' instead of 'record' parameter --- .../langflow/components/helpers/APIRequest.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/backend/langflow/components/helpers/APIRequest.py b/src/backend/langflow/components/helpers/APIRequest.py index 4d9f3ce9c..75abe4e94 100644 --- a/src/backend/langflow/components/helpers/APIRequest.py +++ b/src/backend/langflow/components/helpers/APIRequest.py @@ -27,9 +27,9 @@ class APIRequest(CustomComponent): "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).", + "body": { + "display_name": "Body", + "info": "The body to send with the request (for POST, PATCH, PUT).", }, "timeout": { "display_name": "Timeout", @@ -45,14 +45,14 @@ class APIRequest(CustomComponent): method: str, url: str, headers: Optional[dict] = None, - record: Optional[Record] = None, + body: Optional[dict] = None, timeout: int = 5, ) -> Record: method = method.upper() if method not in ["GET", "POST", "PATCH", "PUT"]: raise ValueError(f"Unsupported method: {method}") - data = record.data if record else None + data = body if body else None try: response = await client.request( method, url, headers=headers, content=data, timeout=timeout @@ -86,22 +86,22 @@ class APIRequest(CustomComponent): method: str, url: List[str], headers: Optional[dict] = None, - record: Optional[Record] = None, + body: Optional[dict] = 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) + bodies = ( + body + if isinstance(body, list) + else [body] if body else [None] * len(urls) ) async with httpx.AsyncClient() as client: results = await asyncio.gather( *[ self.make_request(client, method, u, headers, rec, timeout) - for u, rec in zip(urls, records) + for u, rec in zip(urls, bodies) ] ) return results From a7dddbcb1d75167436bcb2f165bd0b953a58857e Mon Sep 17 00:00:00 2001 From: Rodrigo Nader Date: Wed, 6 Mar 2024 01:23:49 -0300 Subject: [PATCH 101/199] Add APIRequest component for making HTTP requests --- .../langflow/components/{helpers => data}/APIRequest.py | 4 ++++ 1 file changed, 4 insertions(+) rename src/backend/langflow/components/{helpers => data}/APIRequest.py (96%) diff --git a/src/backend/langflow/components/helpers/APIRequest.py b/src/backend/langflow/components/data/APIRequest.py similarity index 96% rename from src/backend/langflow/components/helpers/APIRequest.py rename to src/backend/langflow/components/data/APIRequest.py index 75abe4e94..86674c781 100644 --- a/src/backend/langflow/components/helpers/APIRequest.py +++ b/src/backend/langflow/components/data/APIRequest.py @@ -2,6 +2,7 @@ import asyncio from typing import List, Optional import httpx +import json from langflow import CustomComponent from langflow.schema import Record @@ -26,10 +27,12 @@ class APIRequest(CustomComponent): "headers": { "display_name": "Headers", "info": "The headers to send with the request.", + "input_types": ["dict"] }, "body": { "display_name": "Body", "info": "The body to send with the request (for POST, PATCH, PUT).", + "input_types": ["dict"] }, "timeout": { "display_name": "Timeout", @@ -53,6 +56,7 @@ class APIRequest(CustomComponent): raise ValueError(f"Unsupported method: {method}") data = body if body else None + data = json.dumps(data) try: response = await client.request( method, url, headers=headers, content=data, timeout=timeout From a1c09a5b4dd36ed2c5e6f7b37e37298f432f0c67 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 10:54:51 -0300 Subject: [PATCH 102/199] Refactor database initialization and connection handling --- src/backend/langflow/alembic/env.py | 16 +++++++++++++++- src/backend/langflow/alembic/script.py.mako | 2 ++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/backend/langflow/alembic/env.py b/src/backend/langflow/alembic/env.py index 479db05bb..bc470f05b 100644 --- a/src/backend/langflow/alembic/env.py +++ b/src/backend/langflow/alembic/env.py @@ -62,12 +62,26 @@ def run_migrations_online() -> None: and associate a connection with the context. """ - from langflow.services.deps import get_db_service try: + from langflow.services.database.factory import DatabaseServiceFactory + from langflow.services.deps import get_db_service + from langflow.services.manager import ( + initialize_settings_service, + service_manager, + ) + from langflow.services.schema import ServiceType + + initialize_settings_service() + service_manager.register_factory( + DatabaseServiceFactory(), [ServiceType.SETTINGS_SERVICE] + ) connectable = get_db_service().engine except Exception as e: logger.error(f"Error getting database engine: {e}") + url = os.getenv("LANGFLOW_DATABASE_URL") + url = url or config.get_main_option("sqlalchemy.url") + config.set_main_option("sqlalchemy.url", url) connectable = engine_from_config( config.get_section(config.config_ini_section, {}), prefix="sqlalchemy.", diff --git a/src/backend/langflow/alembic/script.py.mako b/src/backend/langflow/alembic/script.py.mako index 2fbdc930d..bc9bca83a 100644 --- a/src/backend/langflow/alembic/script.py.mako +++ b/src/backend/langflow/alembic/script.py.mako @@ -23,10 +23,12 @@ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} def upgrade() -> None: conn = op.get_bind() inspector = Inspector.from_engine(conn) # type: ignore + table_names = inspector.get_table_names() ${upgrades if upgrades else "pass"} def downgrade() -> None: conn = op.get_bind() inspector = Inspector.from_engine(conn) # type: ignore + table_names = inspector.get_table_names() ${downgrades if downgrades else "pass"} From dc1c092fe7c416fba8b36915fe7c8d9d0dc6a6de Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 10:55:07 -0300 Subject: [PATCH 103/199] Add icon and icon_bg_color fields to Flow model --- ...fd30_add_icon_and_icon_bg_color_to_flow.py | 56 +++++++++++++++++++ .../services/database/models/flow/model.py | 38 +++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 src/backend/langflow/alembic/versions/63b9c451fd30_add_icon_and_icon_bg_color_to_flow.py diff --git a/src/backend/langflow/alembic/versions/63b9c451fd30_add_icon_and_icon_bg_color_to_flow.py b/src/backend/langflow/alembic/versions/63b9c451fd30_add_icon_and_icon_bg_color_to_flow.py new file mode 100644 index 000000000..3deb66346 --- /dev/null +++ b/src/backend/langflow/alembic/versions/63b9c451fd30_add_icon_and_icon_bg_color_to_flow.py @@ -0,0 +1,56 @@ +"""Add icon and icon_bg_color to Flow + +Revision ID: 63b9c451fd30 +Revises: bc2f01c40e4a +Create Date: 2024-03-06 10:53:47.148658 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +import sqlmodel +from alembic import op +from sqlalchemy.engine.reflection import Inspector + +# revision identifiers, used by Alembic. +revision: str = "63b9c451fd30" +down_revision: Union[str, None] = "bc2f01c40e4a" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + conn = op.get_bind() + inspector = Inspector.from_engine(conn) # type: ignore + table_names = inspector.get_table_names() + column_names = [column["name"] for column in inspector.get_columns("flow")] + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("flow", schema=None) as batch_op: + if "icon" not in column_names: + batch_op.add_column( + sa.Column("icon", sqlmodel.sql.sqltypes.AutoString(), nullable=True) + ) + if "icon_bg_color" not in column_names: + batch_op.add_column( + sa.Column( + "icon_bg_color", sqlmodel.sql.sqltypes.AutoString(), nullable=True + ) + ) + + # ### end Alembic commands ### + + +def downgrade() -> None: + conn = op.get_bind() + inspector = Inspector.from_engine(conn) # type: ignore + table_names = inspector.get_table_names() + column_names = [column["name"] for column in inspector.get_columns("flow")] + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("flow", schema=None) as batch_op: + if "icon" in column_names: + batch_op.drop_column("icon") + if "icon_bg_color" in column_names: + batch_op.drop_column("icon_bg_color") + + # ### end Alembic commands ### diff --git a/src/backend/langflow/services/database/models/flow/model.py b/src/backend/langflow/services/database/models/flow/model.py index c7ee08141..b76fd11cf 100644 --- a/src/backend/langflow/services/database/models/flow/model.py +++ b/src/backend/langflow/services/database/models/flow/model.py @@ -1,5 +1,6 @@ # Path: src/backend/langflow/database/models/flow.py +import re from datetime import datetime from typing import TYPE_CHECKING, Dict, Optional from uuid import UUID, uuid4 @@ -7,6 +8,7 @@ from uuid import UUID, uuid4 from pydantic import field_serializer, field_validator from sqlmodel import JSON, Column, Field, Relationship, SQLModel +from langflow.interface.custom.attributes import validate_icon from langflow.schema.schema import Record if TYPE_CHECKING: @@ -16,6 +18,8 @@ if TYPE_CHECKING: class FlowBase(SQLModel): name: str = Field(index=True) description: Optional[str] = Field(index=True, nullable=True, default=None) + icon: Optional[str] = Field(default=None, nullable=True) + icon_bg_color: Optional[str] = Field(default=None, nullable=True) data: Optional[Dict] = Field(default=None, nullable=True) is_component: Optional[bool] = Field(default=False, nullable=True) updated_at: Optional[datetime] = Field( @@ -23,6 +27,40 @@ class FlowBase(SQLModel): ) folder: Optional[str] = Field(default=None, nullable=True) + @field_validator("icon_bg_color") + def validate_icon_bg_color(cls, v): + if v is not None and not isinstance(v, str): + raise ValueError("Icon background color must be a string") + # validate that is is a hex color + if v and not v.startswith("#"): + raise ValueError("Icon background color must start with #") + + # validate that it is a valid hex color + if v and len(v) != 7: + raise ValueError("Icon background color must be 7 characters long") + return v + + @field_validator("icon") + def validate_icon_atr(cls, v): + # const emojiRegex = /\p{Emoji}/u; + # const isEmoji = emojiRegex.test(data?.node?.icon!); + # emoji pattern in Python + pattern = r"\p{Emoji}" + emoji = validate_icon(v) + is_emoji = re.search(pattern, emoji) + if is_emoji: + # this is indeed an emoji + return emoji + # otherwise it should be a valid lucide icon + if v is not None and not isinstance(v, str): + raise ValueError("Icon must be a string") + # is should be lowercase and contain only letters and hyphens + if v and not v.islower(): + raise ValueError("Icon must be lowercase") + if v and not v.replace("-", "").isalpha(): + raise ValueError("Icon must contain only letters and hyphens") + return v + @field_validator("data") def validate_json(v): if not v: From 5f9d3d5c40f128bc53c313399db68ebf2813b1d6 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 10:55:39 -0300 Subject: [PATCH 104/199] Add exception logging in get_all function --- src/backend/langflow/api/v1/endpoints.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backend/langflow/api/v1/endpoints.py b/src/backend/langflow/api/v1/endpoints.py index b77685d65..5eb5e62b7 100644 --- a/src/backend/langflow/api/v1/endpoints.py +++ b/src/backend/langflow/api/v1/endpoints.py @@ -48,6 +48,7 @@ def get_all( all_types_dict = get_all_types_dict(settings_service) return all_types_dict except Exception as exc: + logger.exception(exc) raise HTTPException(status_code=500, detail=str(exc)) from exc From 77ea76d37deefb8b39a85dcec73b0f20c89852b0 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 10:56:42 -0300 Subject: [PATCH 105/199] Refactor build_template_from_function and build_template_from_class functions --- src/backend/langflow/utils/util.py | 61 ++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/src/backend/langflow/utils/util.py b/src/backend/langflow/utils/util.py index 7e1206222..2896ec02e 100644 --- a/src/backend/langflow/utils/util.py +++ b/src/backend/langflow/utils/util.py @@ -15,8 +15,12 @@ def remove_ansi_escape_codes(text): return re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", text) -def build_template_from_function(name: str, type_to_loader_dict: Dict, add_function: bool = False): - classes = [item.__annotations__["return"].__name__ for item in type_to_loader_dict.values()] +def build_template_from_function( + name: str, type_to_loader_dict: Dict, add_function: bool = False +): + classes = [ + item.__annotations__["return"].__name__ for item in type_to_loader_dict.values() + ] # Raise error if name is not in chains if name not in classes: @@ -37,8 +41,10 @@ def build_template_from_function(name: str, type_to_loader_dict: Dict, add_funct for name_, value_ in value.__repr_args__(): if name_ == "default_factory": try: - variables[class_field_items]["default"] = get_default_factory( - module=_class.__base__.__module__, function=value_ + variables[class_field_items]["default"] = ( + get_default_factory( + module=_class.__base__.__module__, function=value_ + ) ) except Exception: variables[class_field_items]["default"] = None @@ -46,7 +52,9 @@ def build_template_from_function(name: str, type_to_loader_dict: Dict, add_funct variables[class_field_items][name_] = value_ variables[class_field_items]["placeholder"] = ( - docs.params[class_field_items] if class_field_items in docs.params else "" + docs.params[class_field_items] + if class_field_items in docs.params + else "" ) # Adding function to base classes to allow # the output to be a function @@ -61,7 +69,9 @@ def build_template_from_function(name: str, type_to_loader_dict: Dict, add_funct } -def build_template_from_class(name: str, type_to_cls_dict: Dict, add_function: bool = False): +def build_template_from_class( + name: str, type_to_cls_dict: Dict, add_function: bool = False +): classes = [item.__name__ for item in type_to_cls_dict.values()] # Raise error if name is not in chains @@ -85,8 +95,11 @@ def build_template_from_class(name: str, type_to_cls_dict: Dict, add_function: b for name_, value_ in value.__repr_args__(): if name_ == "default_factory": try: - variables[class_field_items]["default"] = get_default_factory( - module=_class.__base__.__module__, function=value_ + variables[class_field_items]["default"] = ( + get_default_factory( + module=_class.__base__.__module__, + function=value_, + ) ) except Exception: variables[class_field_items]["default"] = None @@ -94,7 +107,9 @@ def build_template_from_class(name: str, type_to_cls_dict: Dict, add_function: b variables[class_field_items][name_] = value_ variables[class_field_items]["placeholder"] = ( - docs.params[class_field_items] if class_field_items in docs.params else "" + docs.params[class_field_items] + if class_field_items in docs.params + else "" ) base_classes = get_base_classes(_class) # Adding function to base classes to allow @@ -126,7 +141,9 @@ def build_template_from_method( # Check if the method exists in this class if not hasattr(_class, method_name): - raise ValueError(f"Method {method_name} not found in class {class_name}") + raise ValueError( + f"Method {method_name} not found in class {class_name}" + ) # Get the method method = getattr(_class, method_name) @@ -145,8 +162,14 @@ def build_template_from_method( "_type": _type, **{ name: { - "default": param.default if param.default != param.empty else None, - "type": param.annotation if param.annotation != param.empty else None, + "default": ( + param.default if param.default != param.empty else None + ), + "type": ( + param.annotation + if param.annotation != param.empty + else None + ), "required": param.default == param.empty, } for name, param in params.items() @@ -233,7 +256,9 @@ def sync_to_async(func): return async_wrapper -def format_dict(dictionary: Dict[str, Any], class_name: Optional[str] = None) -> Dict[str, Any]: +def format_dict( + dictionary: Dict[str, Any], class_name: Optional[str] = None +) -> Dict[str, Any]: """ Formats a dictionary by removing certain keys and modifying the values of other keys. @@ -243,7 +268,7 @@ def format_dict(dictionary: Dict[str, Any], class_name: Optional[str] = None) -> """ for key, value in dictionary.items(): - if key == "_type": + if key in ["_type"]: continue _type: Union[str, type] = get_type(value) @@ -319,7 +344,9 @@ def check_list_type(_type: str, value: Dict[str, Any]) -> str: The modified type string. """ if any(list_type in _type for list_type in ["List", "Sequence", "Set"]): - _type = _type.replace("List[", "").replace("Sequence[", "").replace("Set[", "")[:-1] + _type = ( + _type.replace("List[", "").replace("Sequence[", "").replace("Set[", "")[:-1] + ) value["list"] = True else: value["list"] = False @@ -422,7 +449,9 @@ def set_headers_value(value: Dict[str, Any]) -> None: value["value"] = """{"Authorization": "Bearer "}""" -def add_options_to_field(value: Dict[str, Any], class_name: Optional[str], key: str) -> None: +def add_options_to_field( + value: Dict[str, Any], class_name: Optional[str], key: str +) -> None: """ Adds options to the field based on the class name and key. """ From 44811308458398dac6e0da32ff9cc00ab2791e74 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 10:56:51 -0300 Subject: [PATCH 106/199] Update field_value type in CustomComponentCode schema --- src/backend/langflow/api/v1/schemas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/langflow/api/v1/schemas.py b/src/backend/langflow/api/v1/schemas.py index 17274bcfc..d500d8278 100644 --- a/src/backend/langflow/api/v1/schemas.py +++ b/src/backend/langflow/api/v1/schemas.py @@ -166,7 +166,7 @@ class StreamData(BaseModel): class CustomComponentCode(BaseModel): code: str field: Optional[str] = None - field_value: Optional[str] = None + field_value: Optional[Any] = None frontend_node: Optional[dict] = None From f497ecdeb9be0f79c30e085c31bc27388d8ff94d Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 10:57:07 -0300 Subject: [PATCH 107/199] Refactor Vertex class to support dictionary of nodes --- src/backend/langflow/graph/vertex/base.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/backend/langflow/graph/vertex/base.py b/src/backend/langflow/graph/vertex/base.py index 66fdd44c6..d160f2db2 100644 --- a/src/backend/langflow/graph/vertex/base.py +++ b/src/backend/langflow/graph/vertex/base.py @@ -313,7 +313,16 @@ class Vertex: params[param_key] = [] params[param_key].append(self.graph.get_vertex(edge.source_id)) elif edge.target_id == self.id: - params[param_key] = self.graph.get_vertex(edge.source_id) + if isinstance(template_dict[param_key]["value"], dict): + # we don't know the key of the dict but we need to set the value + # to the vertex that is the source of the edge + param_dict = template_dict[param_key]["value"] + params[param_key] = { + key: self.graph.get_vertex(edge.source_id) + for key in param_dict.keys() + } + else: + params[param_key] = self.graph.get_vertex(edge.source_id) for key, value in template_dict.items(): if key in params: @@ -493,9 +502,21 @@ class Vertex: await self._build_node_and_update_params(key, value, user_id) elif isinstance(value, list) and self._is_list_of_nodes(value): await self._build_list_of_nodes_and_update_params(key, value, user_id) + elif isinstance(value, dict): + await self._build_dict_of_nodes_and_update_params(key, value, user_id) elif key not in self.params or self.updated_raw_params: self.params[key] = value + async def _build_dict_of_nodes_and_update_params( + self, key, nodes: Dict[str, "Vertex"], user_id=None + ): + """ + Iterates over a dictionary of nodes, builds each and updates the params dictionary. + """ + for sub_key, node in nodes.items(): + built = await node.get_result(requester=self, user_id=user_id) + self.params[key][sub_key] = built + def _is_node(self, value): """ Checks if the provided value is an instance of Vertex. From 067181dc6332bb7d8d9a9ecf8ae4b5ca2420e478 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 11:10:20 -0300 Subject: [PATCH 108/199] Add field_order attribute to frontend_node --- src/backend/langflow/interface/custom/utils.py | 1 + src/backend/langflow/template/frontend_node/base.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/backend/langflow/interface/custom/utils.py b/src/backend/langflow/interface/custom/utils.py index 49e686b5e..f25360c6a 100644 --- a/src/backend/langflow/interface/custom/utils.py +++ b/src/backend/langflow/interface/custom/utils.py @@ -72,6 +72,7 @@ def reorder_fields(frontend_node: CustomComponentFrontendNode, field_order: List if field.name not in field_order: reordered_fields.append(field) frontend_node.template.fields = reordered_fields + frontend_node.field_order = field_order def add_base_classes( diff --git a/src/backend/langflow/template/frontend_node/base.py b/src/backend/langflow/template/frontend_node/base.py index bcbbb36c1..27572cd5b 100644 --- a/src/backend/langflow/template/frontend_node/base.py +++ b/src/backend/langflow/template/frontend_node/base.py @@ -74,6 +74,9 @@ class FrontendNode(BaseModel): frozen: bool = False """Whether the frontend node is frozen.""" + field_order: list[str] = [] + """Order of the fields in the frontend node.""" + beta: bool = False error: Optional[str] = None From 62ce986576d559fbee477e835c1fe05001aa47b0 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 11:10:28 -0300 Subject: [PATCH 109/199] Fix emoji validation in FlowBase model --- src/backend/langflow/services/database/models/flow/model.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/backend/langflow/services/database/models/flow/model.py b/src/backend/langflow/services/database/models/flow/model.py index b76fd11cf..9bf76c340 100644 --- a/src/backend/langflow/services/database/models/flow/model.py +++ b/src/backend/langflow/services/database/models/flow/model.py @@ -45,6 +45,8 @@ class FlowBase(SQLModel): # const emojiRegex = /\p{Emoji}/u; # const isEmoji = emojiRegex.test(data?.node?.icon!); # emoji pattern in Python + if v is None: + return v pattern = r"\p{Emoji}" emoji = validate_icon(v) is_emoji = re.search(pattern, emoji) From fc880709658e4c241cf7ca838fe11ef618a513f8 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 11:10:40 -0300 Subject: [PATCH 110/199] Remove console.log statements in flowStore.ts --- src/frontend/src/stores/flowStore.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/frontend/src/stores/flowStore.ts b/src/frontend/src/stores/flowStore.ts index e3cfbb20e..16f6b75f4 100644 --- a/src/frontend/src/stores/flowStore.ts +++ b/src/frontend/src/stores/flowStore.ts @@ -514,7 +514,6 @@ const useFlowStore = create((set, get) => ({ get().setIsBuilding(false); }, onBuildStart: (idList) => { - console.log("onBuildStart", idList); useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILDING); }, validateNodes: validateSubgraph, @@ -536,7 +535,6 @@ const useFlowStore = create((set, get) => ({ runId: string; } | null ) => { - console.log("updateVerticesBuild", vertices); set({ verticesBuild: vertices }); }, verticesBuild: null, From bcd230f86f741aa8d8f8d1dc691de129e811860c Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 11:10:49 -0300 Subject: [PATCH 111/199] Update field_value type to any in postCustomComponentUpdate function --- src/frontend/src/controllers/API/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/controllers/API/index.ts b/src/frontend/src/controllers/API/index.ts index 6ee4141a0..a8ee9c6e7 100644 --- a/src/frontend/src/controllers/API/index.ts +++ b/src/frontend/src/controllers/API/index.ts @@ -370,7 +370,7 @@ export async function postCustomComponent( export async function postCustomComponentUpdate( code: string, field: string, - field_value: string + field_value: any ): Promise> { return await api.post(`${BASE_URL_API}custom_component/update`, { code, From c52371e53e76b0868069f0ab26d48076b5d8ae4d Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 11:10:58 -0300 Subject: [PATCH 112/199] Add field_order property to APIClassType --- src/frontend/src/types/api/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/frontend/src/types/api/index.ts b/src/frontend/src/types/api/index.ts index bcbb31730..5e7682307 100644 --- a/src/frontend/src/types/api/index.ts +++ b/src/frontend/src/types/api/index.ts @@ -29,6 +29,7 @@ export type APIClassType = { official?: boolean; frozen?: boolean; flow?: FlowType; + field_order?: string[]; [key: string]: | Array | string From 2a3dc80304d233eb2097a5f21bbdb5dfa1431215 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 11:16:55 -0300 Subject: [PATCH 113/199] Add icon and icon background color to Data Ingestion project --- .../initial_setup/starter_projects/Langflow Data Ingestion.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/backend/langflow/initial_setup/starter_projects/Langflow Data Ingestion.json b/src/backend/langflow/initial_setup/starter_projects/Langflow Data Ingestion.json index de8a8c1df..e40ffda57 100644 --- a/src/backend/langflow/initial_setup/starter_projects/Langflow Data Ingestion.json +++ b/src/backend/langflow/initial_setup/starter_projects/Langflow Data Ingestion.json @@ -1,5 +1,7 @@ { "name": "Data Ingestion", + "icon": ":inbox_tray:", + "icon_bg_color": "#FFD700", "description": "This project is the starting point to insert data into a Vector Store. \n\nWe use the Vector Store Chroma but you can replace it with any other Vector Store. \n\nYou start by deciding what type of data you want to load, then you pick a place where you want to store the vectors and run it.\n\nThis will create a vector store in your local environment which you can query using the Chroma Search component.", "data": { "nodes": [ From bd86d078814d7dae428558b42faa139cc39d81be Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Wed, 6 Mar 2024 11:39:25 -0300 Subject: [PATCH 114/199] Add flow icon and icon background color to FlowType --- src/frontend/src/components/exampleComponent/index.tsx | 10 ++++++++-- src/frontend/src/types/flow/index.ts | 2 ++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/components/exampleComponent/index.tsx b/src/frontend/src/components/exampleComponent/index.tsx index fb6d570aa..fa021b299 100644 --- a/src/frontend/src/components/exampleComponent/index.tsx +++ b/src/frontend/src/components/exampleComponent/index.tsx @@ -41,12 +41,18 @@ export default function CollectionCardComponent({
- + {flow.icon} +
+ + )} + {!flow.icon && + />}
{flow.name}
diff --git a/src/frontend/src/types/flow/index.ts b/src/frontend/src/types/flow/index.ts index ffb364d61..6c7c941ad 100644 --- a/src/frontend/src/types/flow/index.ts +++ b/src/frontend/src/types/flow/index.ts @@ -15,6 +15,8 @@ export type FlowType = { parent?: string; folder?: string; user_id?: string; + icon?:string; + icon_bg_color?:string; }; export type NodeType = { From ad5110554640c19c994ecbc73505753dd69c0b13 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 11:21:44 -0300 Subject: [PATCH 115/199] Fix error in GenericNode component --- .../src/CustomNodes/GenericNode/index.tsx | 27 +++++++++++-------- src/frontend/src/types/components/index.ts | 1 + 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index 80e5ad0f7..c8d338956 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -12,10 +12,10 @@ import { RUN_TIMESTAMP_PREFIX, STATUS_BUILD, STATUS_BUILDING, - priorityFields, } from "../../constants/constants"; import { BuildStatus } from "../../constants/enums"; import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent"; +import useAlertStore from "../../stores/alertStore"; import { useDarkStore } from "../../stores/darkStore"; import useFlowStore from "../../stores/flowStore"; import useFlowsManagerStore from "../../stores/flowsManagerStore"; @@ -24,7 +24,7 @@ import { validationStatusType } from "../../types/components"; import { NodeDataType } from "../../types/flow"; import { handleKeyDown, scapedJSONStringfy } from "../../utils/reactflowUtils"; import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils"; -import { classNames, cn, getFieldTitle } from "../../utils/utils"; +import { classNames, cn, getFieldTitle, sortFields } from "../../utils/utils"; import ParameterComponent from "./components/parameterComponent"; export default function GenericNode({ @@ -43,6 +43,7 @@ export default function GenericNode({ const flowPool = useFlowStore((state) => state.flowPool); const buildFlow = useFlowStore((state) => state.buildFlow); const setNode = useFlowStore((state) => state.setNode); + const setErrorData = useAlertStore((state) => state.setErrorData); const name = nodeIconsLucide[data.type] ? data.type : types[data.type]; const [inputName, setInputName] = useState(false); const [nodeName, setNodeName] = useState(data.node!.display_name); @@ -64,6 +65,18 @@ export default function GenericNode({ const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot); + if (!data.node!.template) { + setErrorData({ + title: `Error in component ${data.node!.display_name}`, + list: [ + `The component ${data.node!.display_name} has no template.`, + `Please contact the developer of the component to fix this issue.`, + ], + }); + takeSnapshot(); + deleteNode(data.id); + } + function countHandles(): void { let count = Object.keys(data.node!.template) .filter((templateField) => templateField.charAt(0) !== "_") @@ -636,15 +649,7 @@ export default function GenericNode({ <> {Object.keys(data.node!.template) .filter((templateField) => templateField.charAt(0) !== "_") - .sort((a, b) => { - if (priorityFields.has(a.toLowerCase())) { - return -1; - } else if (priorityFields.has(b.toLowerCase())) { - return 1; - } else { - return a.localeCompare(b); - } - }) + .sort((a, b) => sortFields(a, b, data.node?.field_order ?? [])) .map((templateField: string, idx) => (
{data.node!.template[templateField].show && diff --git a/src/frontend/src/types/components/index.ts b/src/frontend/src/types/components/index.ts index aef92b1a4..14fb959ff 100644 --- a/src/frontend/src/types/components/index.ts +++ b/src/frontend/src/types/components/index.ts @@ -71,6 +71,7 @@ export type KeyPairListComponentType = { editNode?: boolean; duplicateKey?: boolean; editNodeModal?: boolean; + isList?: boolean; }; export type DictComponentType = { From aa1515a96d3d8824351c59c4fc2bf3eb2b2eed7b Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 11:22:17 -0300 Subject: [PATCH 116/199] Add sortFields function to utils.ts --- src/frontend/src/utils/utils.ts | 42 ++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/frontend/src/utils/utils.ts b/src/frontend/src/utils/utils.ts index 22fe93185..9651169eb 100644 --- a/src/frontend/src/utils/utils.ts +++ b/src/frontend/src/utils/utils.ts @@ -1,5 +1,6 @@ import clsx, { ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; +import { priorityFields } from "../constants/constants"; import { ADJECTIVES, DESCRIPTIONS, NOUNS } from "../flow_constants"; import { APIDataType, @@ -143,7 +144,7 @@ export function groupByFamily( // se existir o flow for (const node of flow) { // para cada node do flow - if (node!.data!.node!.flow) break; // não faz nada se o node for um group + if (node!.data!.node!.flow || !node!.data!.node!.template) break; // não faz nada se o node for um group const nodeData = node.data; const foundNode = checkedNodes.get(nodeData.type); // verifica se o tipo do node já foi checado @@ -642,3 +643,42 @@ export function getFieldTitle( ? template[templateField].display_name! : template[templateField].name ?? templateField; } + +export function sortFields(a, b, fieldOrder) { + // Early return for empty fields + if (!a && !b) return 0; + if (!a) return 1; + if (!b) return -1; + + // Normalize the case to ensure case-insensitive comparison + const normalizedFieldA = a.toLowerCase(); + const normalizedFieldB = b.toLowerCase(); + + const aIsPriority = priorityFields.has(normalizedFieldA); + const bIsPriority = priorityFields.has(normalizedFieldB); + + // Sort by priority + if (aIsPriority && !bIsPriority) return -1; + if (!aIsPriority && bIsPriority) return 1; + + // Check if either field is in the fieldOrder array + const indexOfA = fieldOrder.indexOf(normalizedFieldA); + const indexOfB = fieldOrder.indexOf(normalizedFieldB); + + // If both fields are in fieldOrder, sort by their order in the array + if (indexOfA !== -1 && indexOfB !== -1) { + return indexOfA - indexOfB; + } + + // If only one of the fields is in fieldOrder, that field comes first + if (indexOfA !== -1) { + return -1; + } + if (indexOfB !== -1) { + return 1; + } + + // Default case for fields not in priorityFields and not found in fieldOrder + // You might want to sort them alphabetically or in another specific manner + return a.localeCompare(b); +} From e9db0bffc048dbe064d4e974c4d948e74df9ee75 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 11:22:30 -0300 Subject: [PATCH 117/199] Fix conditional check in ApiModal --- src/frontend/src/modals/ApiModal/index.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/frontend/src/modals/ApiModal/index.tsx b/src/frontend/src/modals/ApiModal/index.tsx index 81d6defee..9c451beeb 100644 --- a/src/frontend/src/modals/ApiModal/index.tsx +++ b/src/frontend/src/modals/ApiModal/index.tsx @@ -98,6 +98,9 @@ const ApiModal = forwardRef( let arrNodesWithValues: string[] = []; flow["data"]!["nodes"].forEach((node) => { + if (!node["data"]["node"]["template"]) { + return; + } Object.keys(node["data"]["node"]["template"]) .filter( (templateField) => From 91a98ae8774f172219275e0ab9a88189130a34d5 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 11:22:59 -0300 Subject: [PATCH 118/199] Format flows.py --- src/backend/langflow/api/v1/flows.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/backend/langflow/api/v1/flows.py b/src/backend/langflow/api/v1/flows.py index c58168d34..6af3286ae 100644 --- a/src/backend/langflow/api/v1/flows.py +++ b/src/backend/langflow/api/v1/flows.py @@ -11,8 +11,12 @@ from sqlmodel import Session, select from langflow.api.utils import remove_api_keys, validate_is_component from langflow.api.v1.schemas import FlowListCreate, FlowListRead from langflow.services.auth.utils import get_current_active_user -from langflow.services.database.models.flow import (Flow, FlowCreate, FlowRead, - FlowUpdate) +from langflow.services.database.models.flow import ( + Flow, + FlowCreate, + FlowRead, + FlowUpdate, +) from langflow.services.database.models.user.model import User from langflow.services.deps import get_session, get_settings_service @@ -52,7 +56,9 @@ def read_flows( flows = validate_is_component(flows) # with the session get the flows that DO NOT have a user_id try: - example_flows = session.exec(select(Flow).where(Flow.user_id == None)).all() + example_flows = session.exec( + select(Flow).where(Flow.user_id == None) + ).all() # noqa flows.extend(example_flows) except Exception as e: logger.error(e) From 308c27c3813cc203cda21eadac9597eef1f5a356 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 11:37:19 -0300 Subject: [PATCH 119/199] Add STARTER_FOLDER_NAME filter to read_flows function --- src/backend/langflow/api/v1/flows.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/backend/langflow/api/v1/flows.py b/src/backend/langflow/api/v1/flows.py index 6af3286ae..3f5b902d2 100644 --- a/src/backend/langflow/api/v1/flows.py +++ b/src/backend/langflow/api/v1/flows.py @@ -10,6 +10,7 @@ from sqlmodel import Session, select from langflow.api.utils import remove_api_keys, validate_is_component from langflow.api.v1.schemas import FlowListCreate, FlowListRead +from langflow.initial_setup.setup import STARTER_FOLDER_NAME from langflow.services.auth.utils import get_current_active_user from langflow.services.database.models.flow import ( Flow, @@ -57,7 +58,9 @@ def read_flows( # with the session get the flows that DO NOT have a user_id try: example_flows = session.exec( - select(Flow).where(Flow.user_id == None) + select(Flow).where( + Flow.user_id == None, Flow.folder == STARTER_FOLDER_NAME + ) ).all() # noqa flows.extend(example_flows) except Exception as e: From d540b0b5c005c1001360d970a1d4f13581a0b196 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 11:45:27 -0300 Subject: [PATCH 120/199] Add support for auto login in read_flows and read_flow --- src/backend/langflow/api/v1/flows.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/backend/langflow/api/v1/flows.py b/src/backend/langflow/api/v1/flows.py index 3f5b902d2..885426bff 100644 --- a/src/backend/langflow/api/v1/flows.py +++ b/src/backend/langflow/api/v1/flows.py @@ -20,6 +20,7 @@ from langflow.services.database.models.flow import ( ) from langflow.services.database.models.user.model import User from langflow.services.deps import get_session, get_settings_service +from langflow.services.settings.service import SettingsService # build router router = APIRouter(prefix="/flows", tags=["Flows"]) @@ -50,10 +51,16 @@ def read_flows( *, current_user: User = Depends(get_current_active_user), session: Session = Depends(get_session), + settings_service: "SettingsService" = Depends(get_settings_service), ): """Read all flows.""" try: - flows = current_user.flows + auth_settings = settings_service.auth_settings + if auth_settings.AUTO_LOGIN: + flows = session.exec(select(Flow).where(Flow.user_id == None)).all() + else: + flows = current_user.flows + flows = validate_is_component(flows) # with the session get the flows that DO NOT have a user_id try: @@ -76,11 +83,17 @@ def read_flow( session: Session = Depends(get_session), flow_id: UUID, current_user: User = Depends(get_current_active_user), + settings_service: "SettingsService" = Depends(get_settings_service), ): """Read a flow.""" + auth_settings = settings_service.auth_settings + if auth_settings.AUTO_LOGIN: + user_id = None + else: + user_id = current_user.id if user_flow := ( session.exec( - select(Flow).where(Flow.id == flow_id, Flow.user_id == current_user.id) + select(Flow).where(Flow.id == flow_id, Flow.user_id == user_id) ).first() ): return user_flow @@ -99,7 +112,12 @@ def update_flow( ): """Update a flow.""" - db_flow = read_flow(session=session, flow_id=flow_id, current_user=current_user) + db_flow = read_flow( + session=session, + flow_id=flow_id, + current_user=current_user, + settings_service=settings_service, + ) if not db_flow: raise HTTPException(status_code=404, detail="Flow not found") flow_data = flow.model_dump(exclude_unset=True) From b74c5816116053d2b72cac49dc026472f763430c Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 11:50:30 -0300 Subject: [PATCH 121/199] Add project icon and icon background color --- src/backend/langflow/initial_setup/setup.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/backend/langflow/initial_setup/setup.py b/src/backend/langflow/initial_setup/setup.py index 1f24ddab1..91bf4f82f 100644 --- a/src/backend/langflow/initial_setup/setup.py +++ b/src/backend/langflow/initial_setup/setup.py @@ -33,12 +33,16 @@ def get_project_data(project): project_updated_at = project.get("updated_at") updated_at_datetime = datetime.strptime(project_updated_at, "%Y-%m-%dT%H:%M:%S.%f") project_data = project.get("data") + project_icon = project.get("icon") + project_icon_bg_color = project.get("icon_bg_color") return ( project_name, project_description, project_is_component, updated_at_datetime, project_data, + project_icon, + project_icon_bg_color, ) @@ -49,6 +53,8 @@ def update_existing_project( project_is_component, updated_at_datetime, project_data, + project_icon, + project_icon_bg_color, ): logger.info(f"Updating starter project {project_name}") existing_project.data = project_data @@ -56,6 +62,8 @@ def update_existing_project( existing_project.description = project_description existing_project.is_component = project_is_component existing_project.updated_at = updated_at_datetime + existing_project.icon = project_icon + existing_project.icon_bg_color = project_icon_bg_color def create_new_project( @@ -65,6 +73,8 @@ def create_new_project( project_is_component, updated_at_datetime, project_data, + project_icon, + project_icon_bg_color, ): logger.info(f"Creating starter project {project_name}") new_project = Flow( @@ -74,6 +84,8 @@ def create_new_project( updated_at=updated_at_datetime, folder=STARTER_FOLDER_NAME, data=project_data, + icon=project_icon, + icon_bg_color=project_icon_bg_color, ) session.add(new_project) @@ -88,6 +100,8 @@ def create_or_update_starter_projects(): project_is_component, updated_at_datetime, project_data, + project_icon, + project_icon_bg_color, ) = get_project_data(project) if project_name and project_data: existing_project = session.exec( @@ -103,6 +117,8 @@ def create_or_update_starter_projects(): project_is_component, updated_at_datetime, project_data, + project_icon, + project_icon_bg_color, ) else: create_new_project( @@ -112,4 +128,6 @@ def create_or_update_starter_projects(): project_is_component, updated_at_datetime, project_data, + project_icon, + project_icon_bg_color, ) From 54708946e1be57d64ffebf8f33bae659ea90da9c Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 12:02:03 -0300 Subject: [PATCH 122/199] Add emoji module and update emoji validation logic --- .../langflow/services/database/models/flow/model.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/backend/langflow/services/database/models/flow/model.py b/src/backend/langflow/services/database/models/flow/model.py index 9bf76c340..d9fe5538e 100644 --- a/src/backend/langflow/services/database/models/flow/model.py +++ b/src/backend/langflow/services/database/models/flow/model.py @@ -1,10 +1,10 @@ # Path: src/backend/langflow/database/models/flow.py -import re from datetime import datetime from typing import TYPE_CHECKING, Dict, Optional from uuid import UUID, uuid4 +from emoji import purely_emoji from pydantic import field_serializer, field_validator from sqlmodel import JSON, Column, Field, Relationship, SQLModel @@ -47,10 +47,10 @@ class FlowBase(SQLModel): # emoji pattern in Python if v is None: return v - pattern = r"\p{Emoji}" + emoji = validate_icon(v) - is_emoji = re.search(pattern, emoji) - if is_emoji: + + if purely_emoji(emoji): # this is indeed an emoji return emoji # otherwise it should be a valid lucide icon From 3a89586ea9e61e9bf6f4ea0cde560d30d6cf69ed Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 12:42:29 -0300 Subject: [PATCH 123/199] Fix flow duplication issue and add settings service dependency --- src/backend/langflow/api/v1/flows.py | 12 ++++++++++-- .../langflow/services/database/models/flow/model.py | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/backend/langflow/api/v1/flows.py b/src/backend/langflow/api/v1/flows.py index 885426bff..06741ba1d 100644 --- a/src/backend/langflow/api/v1/flows.py +++ b/src/backend/langflow/api/v1/flows.py @@ -69,7 +69,9 @@ def read_flows( Flow.user_id == None, Flow.folder == STARTER_FOLDER_NAME ) ).all() # noqa - flows.extend(example_flows) + for example_flow in example_flows: + if example_flow not in flows: + flows.append(example_flow) except Exception as e: logger.error(e) except Exception as e: @@ -139,9 +141,15 @@ def delete_flow( session: Session = Depends(get_session), flow_id: UUID, current_user: User = Depends(get_current_active_user), + settings_service=Depends(get_settings_service), ): """Delete a flow.""" - flow = read_flow(session=session, flow_id=flow_id, current_user=current_user) + flow = read_flow( + session=session, + flow_id=flow_id, + current_user=current_user, + settings_service=settings_service, + ) if not flow: raise HTTPException(status_code=404, detail="Flow not found") session.delete(flow) diff --git a/src/backend/langflow/services/database/models/flow/model.py b/src/backend/langflow/services/database/models/flow/model.py index d9fe5538e..4810da557 100644 --- a/src/backend/langflow/services/database/models/flow/model.py +++ b/src/backend/langflow/services/database/models/flow/model.py @@ -98,7 +98,7 @@ class FlowBase(SQLModel): class Flow(FlowBase, table=True): id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True) data: Optional[Dict] = Field(default=None, sa_column=Column(JSON)) - user_id: UUID = Field(index=True, foreign_key="user.id", nullable=True) + user_id: Optional[UUID] = Field(index=True, foreign_key="user.id", nullable=True) user: "User" = Relationship(back_populates="flows") def to_record(self): From 239db30934f06ecce8d3ef7a7a4b4dfb1d99d53b Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 12:45:09 -0300 Subject: [PATCH 124/199] Refactor project creation and update logic --- src/backend/langflow/initial_setup/setup.py | 82 ++++++++++++--------- 1 file changed, 49 insertions(+), 33 deletions(-) diff --git a/src/backend/langflow/initial_setup/setup.py b/src/backend/langflow/initial_setup/setup.py index 91bf4f82f..d01a9a788 100644 --- a/src/backend/langflow/initial_setup/setup.py +++ b/src/backend/langflow/initial_setup/setup.py @@ -5,7 +5,7 @@ import orjson from loguru import logger from sqlmodel import select -from langflow.services.database.models.flow.model import Flow +from langflow.services.database.models.flow.model import Flow, FlowCreate from langflow.services.deps import session_scope STARTER_FOLDER_NAME = "Starter Projects" @@ -77,22 +77,50 @@ def create_new_project( project_icon_bg_color, ): logger.info(f"Creating starter project {project_name}") - new_project = Flow( + new_project = FlowCreate( name=project_name, description=project_description, + icon=project_icon, + icon_bg_color=project_icon_bg_color, + data=project_data, is_component=project_is_component, updated_at=updated_at_datetime, folder=STARTER_FOLDER_NAME, - data=project_data, - icon=project_icon, - icon_bg_color=project_icon_bg_color, ) - session.add(new_project) + db_flow = Flow.model_validate(new_project, from_attributes=True) + session.add(db_flow) + flows = session.exec( + select(Flow).where( + Flow.name == project_name, + ) + ).all() + + +def get_all_flows_similar_to_project(session, project_name): + flows = session.exec( + select(Flow).where( + Flow.name == project_name, + Flow.folder == STARTER_FOLDER_NAME, + ) + ).all() + return flows + + +def delete_start_projects(session): + flows = session.exec( + select(Flow).where( + Flow.folder == STARTER_FOLDER_NAME, + ) + ).all() + for flow in flows: + session.delete(flow) def create_or_update_starter_projects(): + with session_scope() as session: starter_projects = load_starter_projects() + delete_start_projects(session) for project in starter_projects: ( project_name, @@ -104,30 +132,18 @@ def create_or_update_starter_projects(): project_icon_bg_color, ) = get_project_data(project) if project_name and project_data: - existing_project = session.exec( - select(Flow).where( - Flow.name == project_name, Flow.folder == STARTER_FOLDER_NAME - ) - ).first() - if existing_project: - update_existing_project( - existing_project, - project_name, - project_description, - project_is_component, - updated_at_datetime, - project_data, - project_icon, - project_icon_bg_color, - ) - else: - create_new_project( - session, - project_name, - project_description, - project_is_component, - updated_at_datetime, - project_data, - project_icon, - project_icon_bg_color, - ) + for existing_project in get_all_flows_similar_to_project( + session, project_name + ): + session.delete(existing_project) + + create_new_project( + session, + project_name, + project_description, + project_is_component, + updated_at_datetime, + project_data, + project_icon, + project_icon_bg_color, + ) From 4d989ee51ec4daf75029b7b809370f482ea7596f Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 12:55:55 -0300 Subject: [PATCH 125/199] If input_types is an empty list it should override the defaults --- src/backend/langflow/template/field/base.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/backend/langflow/template/field/base.py b/src/backend/langflow/template/field/base.py index 858ed00bc..11b540e77 100644 --- a/src/backend/langflow/template/field/base.py +++ b/src/backend/langflow/template/field/base.py @@ -86,8 +86,6 @@ class TemplateField(BaseModel): if self.field_type in ["str", "Text"]: if "input_types" not in result: result["input_types"] = ["Text"] - else: - result["input_types"].append("Text") if self.field_type == "Text": result["type"] = "str" else: From 199b656f1972b7226c4a1f9dce04b6795874a365 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 12:56:32 -0300 Subject: [PATCH 126/199] Remove field_order from template --- src/backend/langflow/template/template/base.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/backend/langflow/template/template/base.py b/src/backend/langflow/template/template/base.py index 9bc375b0f..e268313ea 100644 --- a/src/backend/langflow/template/template/base.py +++ b/src/backend/langflow/template/template/base.py @@ -1,14 +1,14 @@ from typing import Callable, Union +from pydantic import BaseModel, model_serializer + from langflow.template.field.base import TemplateField from langflow.utils.constants import DIRECT_TYPES -from pydantic import BaseModel, model_serializer class Template(BaseModel): type_name: str fields: list[TemplateField] - field_order: list[str] = [] def process_fields( self, @@ -30,7 +30,6 @@ class Template(BaseModel): for field in self.fields: result[field.name] = field.model_dump(by_alias=True, exclude_none=True) result["_type"] = result.pop("type_name") - result.pop("field_order", None) return result # For backwards compatibility @@ -46,7 +45,9 @@ class Template(BaseModel): """Returns the field with the given name.""" field = next((field for field in self.fields if field.name == field_name), None) if field is None: - raise ValueError(f"Field {field_name} not found in template {self.type_name}") + raise ValueError( + f"Field {field_name} not found in template {self.type_name}" + ) return field def update_field(self, field_name: str, field: TemplateField) -> None: From 971b0ca3665aa5cc1a6f337a72a801a8719c5bb8 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 13:27:32 -0300 Subject: [PATCH 127/199] Fix bug in Vertex class for handling dictionary values --- src/backend/langflow/graph/vertex/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/langflow/graph/vertex/base.py b/src/backend/langflow/graph/vertex/base.py index d160f2db2..ed46389e9 100644 --- a/src/backend/langflow/graph/vertex/base.py +++ b/src/backend/langflow/graph/vertex/base.py @@ -313,7 +313,7 @@ class Vertex: params[param_key] = [] params[param_key].append(self.graph.get_vertex(edge.source_id)) elif edge.target_id == self.id: - if isinstance(template_dict[param_key]["value"], dict): + if isinstance(template_dict[param_key].get("value"), dict): # we don't know the key of the dict but we need to set the value # to the vertex that is the source of the edge param_dict = template_dict[param_key]["value"] From c7d0444adfbaf770450080313b7fbda7aad00a74 Mon Sep 17 00:00:00 2001 From: igorrCarvalho Date: Wed, 6 Mar 2024 14:15:59 -0300 Subject: [PATCH 128/199] Feat: Remove buttons from sideBar and create flow toolbar (run/share/api) --- src/frontend/src/App.css | 8 ++ .../src/CustomNodes/GenericNode/index.tsx | 14 +-- .../src/components/chatComponent/index.tsx | 106 +++++++++++++++++- .../components/menuBar/index.tsx | 51 ++++++++- src/frontend/src/modals/exportModal/index.tsx | 6 +- .../extraSidebarComponent/index.tsx | 62 +--------- .../sideBarDraggableComponent/index.tsx | 1 + src/frontend/src/style/applies.css | 2 +- src/frontend/src/style/index.css | 4 + src/frontend/src/utils/styleUtils.ts | 2 + src/frontend/tailwind.config.js | 2 +- 11 files changed, 179 insertions(+), 79 deletions(-) diff --git a/src/frontend/src/App.css b/src/frontend/src/App.css index 095c63bd7..c2427fb0f 100644 --- a/src/frontend/src/App.css +++ b/src/frontend/src/App.css @@ -90,3 +90,11 @@ body { .jv-indent::-webkit-scrollbar-thumb:hover { background-color: #bbb !important; } + +.custom-hover { + transition: background-color 0.5s ease; +} + +.custom-hover:hover { + background-color: rgba(99, 102, 241, 0.1); /* Medium indigo color with 20% opacity */ +} \ No newline at end of file diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index ddfc04aef..a580684ac 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -509,10 +509,10 @@ export default function GenericNode({ ) : !validationStatus ? ( {STATUS_BUILD} ) : ( -
+
{lastRunTime && ( -
+
{RUN_TIMESTAMP_PREFIX}
{lastRunTime} @@ -520,19 +520,19 @@ export default function GenericNode({
)}
-
+
Duration:
-
+
{validationStatus?.data.duration}

- + Output -
+
{validationString.split("\n").map((line, index) => ( -
{line}
+
{line}
))}
diff --git a/src/frontend/src/components/chatComponent/index.tsx b/src/frontend/src/components/chatComponent/index.tsx index da612340a..15e13fc15 100644 --- a/src/frontend/src/components/chatComponent/index.tsx +++ b/src/frontend/src/components/chatComponent/index.tsx @@ -1,14 +1,26 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import useFlowStore from "../../stores/flowStore"; import { ChatType } from "../../types/chat"; import IOView from "../IOview"; import ChatTrigger from "../ViewTriggers/chat"; +import { Transition } from "@headlessui/react"; +import ForwardedIconComponent from "../genericIconComponent"; +import { Separator } from "../ui/separator"; +import ShareModal from "../../modals/shareModal"; +import { useStoreStore } from "../../stores/storeStore"; +import useFlowsManagerStore from "../../stores/flowsManagerStore"; +import { classNames } from "../../utils/utils"; +import ApiModal from "../../modals/ApiModal"; export default function Chat({ flow }: ChatType): JSX.Element { const [open, setOpen] = useState(false); const flowState = useFlowStore((state) => state.flowState); const nodes = useFlowStore((state) => state.nodes); const hasIO = useFlowStore((state) => state.hasIO); + const hasStore = useStoreStore((state) => state.hasStore); + const validApiKey = useStoreStore((state) => state.validApiKey); + const currentFlow = useFlowsManagerStore((state) => state.currentFlow); + const hasApiKey = useStoreStore((state) => state.hasApiKey); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { @@ -29,16 +41,100 @@ export default function Chat({ flow }: ChatType): JSX.Element { const prevNodesRef = useRef(); + const ModalMemo = useMemo( + () => ( + + + + ), + [hasApiKey, validApiKey, currentFlow, hasStore] + ); + return ( <> -
- {/* */} - {hasIO && ( + +
+
+
+ {hasIO && ( - +
+ + Run +
)} +
+ {hasIO && ( +
+ +
+ )} +
+ {currentFlow && currentFlow.data && ( + +
+ + API +
+
+ )} +
+ {hasStore && validApiKey && ( +
+ +
+ )} +
+ {hasStore && validApiKey && ( +
{ModalMemo}
+ )} +
+
+
); } diff --git a/src/frontend/src/components/headerComponent/components/menuBar/index.tsx b/src/frontend/src/components/headerComponent/components/menuBar/index.tsx index 9bff70924..bc5202c87 100644 --- a/src/frontend/src/components/headerComponent/components/menuBar/index.tsx +++ b/src/frontend/src/components/headerComponent/components/menuBar/index.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { DropdownMenu, DropdownMenuContent, @@ -14,10 +14,13 @@ import FlowSettingsModal from "../../../../modals/flowSettingsModal"; import useAlertStore from "../../../../stores/alertStore"; import useFlowStore from "../../../../stores/flowStore"; import useFlowsManagerStore from "../../../../stores/flowsManagerStore"; -import { cn } from "../../../../utils/utils"; +import { classNames, cn } from "../../../../utils/utils"; import ShadTooltip from "../../../ShadTooltipComponent"; import IconComponent from "../../../genericIconComponent"; import { Button } from "../../../ui/button"; +import { UPLOAD_ERROR_ALERT } from "../../../../constants/alerts_constants"; +import ExportModal from "../../../../modals/exportModal"; +import { useStoreStore } from "../../../../stores/storeStore"; export const MenuBar = ({ removeFunction, @@ -32,9 +35,27 @@ export const MenuBar = ({ const saveLoading = useFlowsManagerStore((state) => state.saveLoading); const [openSettings, setOpenSettings] = useState(false); const n = useFlowStore((state) => state.nodes); - + const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow); + const hasApiKey = useStoreStore((state) => state.hasApiKey); + const validApiKey = useStoreStore((state) => state.validApiKey); const navigate = useNavigate(); const isBuilding = useFlowStore((state) => state.isBuilding); + const [myOpen, mySetOpen] = useState(false) + + const ExportMemo = useMemo( + () => ( + +
+ + Export +
+
+ ), + [] + ); function handleAddFlow() { try { @@ -100,6 +121,30 @@ export const MenuBar = ({ /> Settings + { + uploadFlow({ newProject: false, isComponent: false }).catch( + (error) => { + setErrorData({ + title: UPLOAD_ERROR_ALERT, + list: [error], + }); + } + ); + }} + > + + Import + + {(!hasApiKey || !validApiKey) && ( + +
+ + Export +
+
+ )} { diff --git a/src/frontend/src/modals/exportModal/index.tsx b/src/frontend/src/modals/exportModal/index.tsx index 9fac4d0a8..e10dd6ff6 100644 --- a/src/frontend/src/modals/exportModal/index.tsx +++ b/src/frontend/src/modals/exportModal/index.tsx @@ -16,7 +16,7 @@ import { downloadFlow, removeApiKeys } from "../../utils/reactflowUtils"; import BaseModal from "../baseModal"; const ExportModal = forwardRef( - (props: { children: ReactNode }, ref): JSX.Element => { + (props: { children: ReactNode, myOpen?: boolean, mySetOpen?: (open: boolean) => void }, ref): JSX.Element => { const version = useDarkStore((state) => state.version); const setNoticeData = useAlertStore((state) => state.setNoticeData); const [checked, setChecked] = useState(true); @@ -29,6 +29,10 @@ const ExportModal = forwardRef( const [description, setDescription] = useState(currentFlow!.description); const [open, setOpen] = useState(false); + useEffect(() => { + console.log(open) + }, [open]) + return ( {props.children} diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx index 299507bbd..565192709 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx @@ -235,66 +235,6 @@ export default function ExtraSidebar(): JSX.Element { return (
-
- {hasStore && validApiKey && ( - -
{ModalMemo}
-
- )} -
- - - -
- {(!hasApiKey || !validApiKey) && ( - -
{ExportMemo}
-
- )} - -
- {currentFlow && currentFlow.data && ( - - - - )} -
-
-
-
handleBlur()} @@ -318,7 +258,7 @@ export default function ExtraSidebar(): JSX.Element { />
- +
{Object.keys(dataFilter) .sort(sortKeys) diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/sideBarDraggableComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/sideBarDraggableComponent/index.tsx index f33e97a1d..35f646d62 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/sideBarDraggableComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/sideBarDraggableComponent/index.tsx @@ -43,6 +43,7 @@ export const SidebarDraggableComponent = forwardRef( const deleteComponent = useFlowsManagerStore( (state) => state.deleteComponent ); + console.log(display_name) const version = useDarkStore((state) => state.version); const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 }); const popoverRef = useRef(null); diff --git a/src/frontend/src/style/applies.css b/src/frontend/src/style/applies.css index cca804fc7..348b58f9a 100644 --- a/src/frontend/src/style/applies.css +++ b/src/frontend/src/style/applies.css @@ -114,7 +114,7 @@ @apply pointer-events-none; } .extra-side-bar-buttons { - @apply relative inline-flex w-full items-center justify-center rounded-md bg-background px-2 py-2 text-foreground shadow-sm ring-1 ring-inset ring-input transition-all duration-500 ease-in-out; + @apply relative inline-flex w-full items-center justify-center rounded-md bg-background px-2 py-2 text-foreground transition-all duration-500 ease-in-out; } .extra-side-bar-buttons:hover { @apply hover:bg-muted; diff --git a/src/frontend/src/style/index.css b/src/frontend/src/style/index.css index fb4242661..0c58cb1df 100644 --- a/src/frontend/src/style/index.css +++ b/src/frontend/src/style/index.css @@ -50,6 +50,9 @@ --component-icon: #d8598a; --flow-icon: #2f67d0; + --hover: #F2F4F5; + + /* Colors that are shared in dark and light mode */ --blur-shared: #151923de; @@ -69,6 +72,7 @@ --background: 224 35% 7.5%; /* hsl(224 40% 10%) */ --foreground: 213 31% 80%; /* hsl(213 31% 91%) */ --ice: #60A5FA; + --hover: #1A202E; --muted: 223 27% 11%; /* hsl(223 27% 11%) */ --muted-foreground: 215.4 16.3% 56.9%; /* hsl(215 16% 56%) */ diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index dc7115838..340d30ffd 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -32,6 +32,7 @@ import { Cpu, Database, Delete, + Dot, Download, DownloadCloud, Edit, @@ -475,4 +476,5 @@ export const nodeIconsLucide: iconsType = { Bot, Delete, Command, + Dot, }; diff --git a/src/frontend/tailwind.config.js b/src/frontend/tailwind.config.js index 80a5416c3..540289fc8 100644 --- a/src/frontend/tailwind.config.js +++ b/src/frontend/tailwind.config.js @@ -88,7 +88,7 @@ module.exports = { "chat-bot-icon": "var(--chat-bot-icon)", "chat-user-icon": "var(--chat-user-icon)", "ice": "var(--ice)", - + hover: "var(--hover)", white: "var(--white)", border: "hsl(var(--border))", input: "hsl(var(--input))", From 8695d9058398f3de2074ae1c6e06c3e5f652f052 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 14:21:22 -0300 Subject: [PATCH 129/199] Add example flows to user flows --- src/backend/langflow/api/v1/flows.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/backend/langflow/api/v1/flows.py b/src/backend/langflow/api/v1/flows.py index 06741ba1d..60fc0070c 100644 --- a/src/backend/langflow/api/v1/flows.py +++ b/src/backend/langflow/api/v1/flows.py @@ -62,6 +62,7 @@ def read_flows( flows = current_user.flows flows = validate_is_component(flows) + flow_ids = [flow.id for flow in flows] # with the session get the flows that DO NOT have a user_id try: example_flows = session.exec( @@ -70,7 +71,7 @@ def read_flows( ) ).all() # noqa for example_flow in example_flows: - if example_flow not in flows: + if example_flow.id not in flow_ids: flows.append(example_flow) except Exception as e: logger.error(e) From 2d3c341abeccd11112eb4c9dd55866245596a44c Mon Sep 17 00:00:00 2001 From: igorrCarvalho Date: Wed, 6 Mar 2024 14:21:33 -0300 Subject: [PATCH 130/199] Refactor: Remove dead code --- .../components/menuBar/index.tsx | 20 ++----------------- src/frontend/src/modals/exportModal/index.tsx | 6 +----- .../sideBarDraggableComponent/index.tsx | 2 +- 3 files changed, 4 insertions(+), 24 deletions(-) diff --git a/src/frontend/src/components/headerComponent/components/menuBar/index.tsx b/src/frontend/src/components/headerComponent/components/menuBar/index.tsx index bc5202c87..d4090cd19 100644 --- a/src/frontend/src/components/headerComponent/components/menuBar/index.tsx +++ b/src/frontend/src/components/headerComponent/components/menuBar/index.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from "react"; +import { useState } from "react"; import { DropdownMenu, DropdownMenuContent, @@ -14,7 +14,7 @@ import FlowSettingsModal from "../../../../modals/flowSettingsModal"; import useAlertStore from "../../../../stores/alertStore"; import useFlowStore from "../../../../stores/flowStore"; import useFlowsManagerStore from "../../../../stores/flowsManagerStore"; -import { classNames, cn } from "../../../../utils/utils"; +import { cn } from "../../../../utils/utils"; import ShadTooltip from "../../../ShadTooltipComponent"; import IconComponent from "../../../genericIconComponent"; import { Button } from "../../../ui/button"; @@ -40,22 +40,6 @@ export const MenuBar = ({ const validApiKey = useStoreStore((state) => state.validApiKey); const navigate = useNavigate(); const isBuilding = useFlowStore((state) => state.isBuilding); - const [myOpen, mySetOpen] = useState(false) - - const ExportMemo = useMemo( - () => ( - -
- - Export -
-
- ), - [] - ); function handleAddFlow() { try { diff --git a/src/frontend/src/modals/exportModal/index.tsx b/src/frontend/src/modals/exportModal/index.tsx index e10dd6ff6..9fac4d0a8 100644 --- a/src/frontend/src/modals/exportModal/index.tsx +++ b/src/frontend/src/modals/exportModal/index.tsx @@ -16,7 +16,7 @@ import { downloadFlow, removeApiKeys } from "../../utils/reactflowUtils"; import BaseModal from "../baseModal"; const ExportModal = forwardRef( - (props: { children: ReactNode, myOpen?: boolean, mySetOpen?: (open: boolean) => void }, ref): JSX.Element => { + (props: { children: ReactNode }, ref): JSX.Element => { const version = useDarkStore((state) => state.version); const setNoticeData = useAlertStore((state) => state.setNoticeData); const [checked, setChecked] = useState(true); @@ -29,10 +29,6 @@ const ExportModal = forwardRef( const [description, setDescription] = useState(currentFlow!.description); const [open, setOpen] = useState(false); - useEffect(() => { - console.log(open) - }, [open]) - return ( {props.children} diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/sideBarDraggableComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/sideBarDraggableComponent/index.tsx index 35f646d62..7d468b4ba 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/sideBarDraggableComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/sideBarDraggableComponent/index.tsx @@ -43,7 +43,7 @@ export const SidebarDraggableComponent = forwardRef( const deleteComponent = useFlowsManagerStore( (state) => state.deleteComponent ); - console.log(display_name) + const version = useDarkStore((state) => state.version); const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 }); const popoverRef = useRef(null); From aa85b572c367bdc03d31d9dbb7b16edee6b4659d Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 14:24:02 -0300 Subject: [PATCH 131/199] Add experimental node color and name --- src/frontend/src/constants/constants.ts | 1 + src/frontend/src/utils/styleUtils.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/frontend/src/constants/constants.ts b/src/frontend/src/constants/constants.ts index 897a1b4b3..9e40d56ca 100644 --- a/src/frontend/src/constants/constants.ts +++ b/src/frontend/src/constants/constants.ts @@ -737,4 +737,5 @@ export const PRIORITY_SIDEBAR_ORDER = [ "prompt", "models", "helpers", + "experimental", ]; diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index ac88826ca..dcc0ae6b3 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -231,6 +231,7 @@ export const nodeColors: { [char: string]: string } = { toolkits: "#DB2C2C", wrappers: "#E6277A", helpers: "#31A3CC", + experimental: "#E6277A", langchain_utilities: "#31A3CC", output_parsers: "#E6A627", str: "#31a3cc", @@ -262,6 +263,7 @@ export const nodeNames: { [char: string]: string } = { textsplitters: "Text Splitters", retrievers: "Retrievers", helpers: "Helpers", + experimental: "Experimental", langchain_utilities: "Utilities", output_parsers: "Output Parsers", custom_components: "Custom", @@ -354,6 +356,7 @@ export const nodeIconsLucide: iconsType = { textsplitters: Scissors, wrappers: Gift, helpers: Wand2, + experimental: FlaskConical, langchain_utilities: PocketKnife, WolframAlphaAPIWrapper: SvgWolfram, output_parsers: Compass, From 535f559bed756c20d4ff9410bbaa9b0e94a9aff7 Mon Sep 17 00:00:00 2001 From: igorrCarvalho Date: Wed, 6 Mar 2024 14:27:31 -0300 Subject: [PATCH 132/199] Refactor: remove console.log --- src/frontend/src/CustomNodes/GenericNode/index.tsx | 1 - .../components/headerComponent/components/menuBar/index.tsx | 2 +- src/frontend/src/style/applies.css | 3 +++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index a580684ac..a78362f16 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -44,7 +44,6 @@ export default function GenericNode({ const buildFlow = useFlowStore((state) => state.buildFlow); const setNode = useFlowStore((state) => state.setNode); const name = nodeIconsLucide[data.type] ? data.type : types[data.type]; - console.log(types[data.type]) const [inputName, setInputName] = useState(false); const [nodeName, setNodeName] = useState(data.node!.display_name); const [inputDescription, setInputDescription] = useState(false); diff --git a/src/frontend/src/components/headerComponent/components/menuBar/index.tsx b/src/frontend/src/components/headerComponent/components/menuBar/index.tsx index d4090cd19..81463bf12 100644 --- a/src/frontend/src/components/headerComponent/components/menuBar/index.tsx +++ b/src/frontend/src/components/headerComponent/components/menuBar/index.tsx @@ -123,7 +123,7 @@ export const MenuBar = ({ {(!hasApiKey || !validApiKey) && ( -
+
Export
diff --git a/src/frontend/src/style/applies.css b/src/frontend/src/style/applies.css index 348b58f9a..579e347b2 100644 --- a/src/frontend/src/style/applies.css +++ b/src/frontend/src/style/applies.css @@ -116,6 +116,9 @@ .extra-side-bar-buttons { @apply relative inline-flex w-full items-center justify-center rounded-md bg-background px-2 py-2 text-foreground transition-all duration-500 ease-in-out; } + .header-menubar-item { + @apply relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 cursor-pointer; + } .extra-side-bar-buttons:hover { @apply hover:bg-muted; } From c5671f132f49f4d1a84c4eb9403307a9feb91de1 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 14:32:04 -0300 Subject: [PATCH 133/199] Add experimental components for listing flows, getting notified, and executing runnables --- .../{helpers => experimental}/GetNotified.py | 0 .../{helpers => experimental}/ListFlows.py | 0 .../{helpers => experimental}/Notify.py | 0 .../{helpers => experimental}/RunFlow.py | 0 .../RunnableExecutor.py | 0 .../{helpers => experimental}/SQLExecutor.py | 16 +++++- .../components/experimental/__init__.py | 0 .../components/helpers/SharedState.py | 41 ---------------- .../components/helpers/ShouldRunNext.py | 49 ------------------- 9 files changed, 14 insertions(+), 92 deletions(-) rename src/backend/langflow/components/{helpers => experimental}/GetNotified.py (100%) rename src/backend/langflow/components/{helpers => experimental}/ListFlows.py (100%) rename src/backend/langflow/components/{helpers => experimental}/Notify.py (100%) rename src/backend/langflow/components/{helpers => experimental}/RunFlow.py (100%) rename src/backend/langflow/components/{helpers => experimental}/RunnableExecutor.py (100%) rename src/backend/langflow/components/{helpers => experimental}/SQLExecutor.py (76%) create mode 100644 src/backend/langflow/components/experimental/__init__.py delete mode 100644 src/backend/langflow/components/helpers/SharedState.py delete mode 100644 src/backend/langflow/components/helpers/ShouldRunNext.py diff --git a/src/backend/langflow/components/helpers/GetNotified.py b/src/backend/langflow/components/experimental/GetNotified.py similarity index 100% rename from src/backend/langflow/components/helpers/GetNotified.py rename to src/backend/langflow/components/experimental/GetNotified.py diff --git a/src/backend/langflow/components/helpers/ListFlows.py b/src/backend/langflow/components/experimental/ListFlows.py similarity index 100% rename from src/backend/langflow/components/helpers/ListFlows.py rename to src/backend/langflow/components/experimental/ListFlows.py diff --git a/src/backend/langflow/components/helpers/Notify.py b/src/backend/langflow/components/experimental/Notify.py similarity index 100% rename from src/backend/langflow/components/helpers/Notify.py rename to src/backend/langflow/components/experimental/Notify.py diff --git a/src/backend/langflow/components/helpers/RunFlow.py b/src/backend/langflow/components/experimental/RunFlow.py similarity index 100% rename from src/backend/langflow/components/helpers/RunFlow.py rename to src/backend/langflow/components/experimental/RunFlow.py diff --git a/src/backend/langflow/components/helpers/RunnableExecutor.py b/src/backend/langflow/components/experimental/RunnableExecutor.py similarity index 100% rename from src/backend/langflow/components/helpers/RunnableExecutor.py rename to src/backend/langflow/components/experimental/RunnableExecutor.py diff --git a/src/backend/langflow/components/helpers/SQLExecutor.py b/src/backend/langflow/components/experimental/SQLExecutor.py similarity index 76% rename from src/backend/langflow/components/helpers/SQLExecutor.py rename to src/backend/langflow/components/experimental/SQLExecutor.py index 530391c31..e1b4e699f 100644 --- a/src/backend/langflow/components/helpers/SQLExecutor.py +++ b/src/backend/langflow/components/experimental/SQLExecutor.py @@ -11,7 +11,10 @@ class SQLExecutorComponent(CustomComponent): def build_config(self): return { - "database": {"display_name": "Database"}, + "database_url": { + "display_name": "Database URL", + "info": "The URL of the database.", + }, "include_columns": { "display_name": "Include Columns", "info": "Include columns in the result.", @@ -26,15 +29,24 @@ class SQLExecutorComponent(CustomComponent): }, } + def clean_up_uri(self, uri: str) -> str: + if uri.startswith("postgresql://"): + uri = uri.replace("postgresql://", "postgres://") + return uri.strip() + def build( self, query: str, - database: SQLDatabase, + database_url: str, include_columns: bool = False, passthrough: bool = False, add_error: bool = False, ) -> Text: error = None + try: + database = SQLDatabase.from_uri(database_url) + except Exception as e: + raise ValueError(f"An error occurred while connecting to the database: {e}") try: tool = QuerySQLDataBaseTool(db=database) result = tool.run(query, include_columns=include_columns) diff --git a/src/backend/langflow/components/experimental/__init__.py b/src/backend/langflow/components/experimental/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/langflow/components/helpers/SharedState.py b/src/backend/langflow/components/helpers/SharedState.py deleted file mode 100644 index 95de17774..000000000 --- a/src/backend/langflow/components/helpers/SharedState.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import Optional - -from langflow import CustomComponent -from langflow.schema import Record - - -class SharedState(CustomComponent): - display_name = "Shared State" - description = "A component to share state between components." - - def build_config(self): - return { - "name": {"display_name": "Name", "info": "The name of the state."}, - "record": {"display_name": "Record", "info": "The record to store."}, - "append": { - "display_name": "Append", - "info": "If True, the record will be appended to the state.", - }, - } - - def build( - self, name: str, record: Optional[Record] = None, append: bool = False - ) -> Record: - if record: - if append: - self.append_state(name, record) - else: - self.update_state(name, record) - - state = self.get_state(name) - if state and not isinstance(state, Record): - if isinstance(state, str): - state = Record(text=state) - elif isinstance(state, dict): - state = Record(data=state) - else: - state = Record(text=str(state)) - elif not state: - state = Record(text="") - self.status = state - return state diff --git a/src/backend/langflow/components/helpers/ShouldRunNext.py b/src/backend/langflow/components/helpers/ShouldRunNext.py deleted file mode 100644 index b9ae3b048..000000000 --- a/src/backend/langflow/components/helpers/ShouldRunNext.py +++ /dev/null @@ -1,49 +0,0 @@ -# Implement ShouldRunNext component -from typing import Text -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) -> dict: - # This is a simple component that always returns True - prompt_template = PromptTemplate.from_template(Text(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 {"condition": bool_result, "result": kwargs} From 1dec624c61d647fbc29d49f5f10365c550c0c9c5 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 14:34:40 -0300 Subject: [PATCH 134/199] Commented out unused code in read_flow function --- src/backend/langflow/api/v1/flows.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/backend/langflow/api/v1/flows.py b/src/backend/langflow/api/v1/flows.py index 60fc0070c..062c24426 100644 --- a/src/backend/langflow/api/v1/flows.py +++ b/src/backend/langflow/api/v1/flows.py @@ -89,14 +89,14 @@ def read_flow( settings_service: "SettingsService" = Depends(get_settings_service), ): """Read a flow.""" - auth_settings = settings_service.auth_settings - if auth_settings.AUTO_LOGIN: - user_id = None - else: - user_id = current_user.id + # auth_settings = settings_service.auth_settings + # if auth_settings.AUTO_LOGIN: + # user_id = None + # else: + # user_id = current_user.id if user_flow := ( session.exec( - select(Flow).where(Flow.id == flow_id, Flow.user_id == user_id) + select(Flow).where(Flow.id == flow_id, Flow.user_id == current_user.id) ).first() ): return user_flow From d354ae9975a88c2707aca2d6a846cb4c8045f86b Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 14:46:36 -0300 Subject: [PATCH 135/199] Refactor read_flow function to handle auto login --- src/backend/langflow/api/v1/flows.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/backend/langflow/api/v1/flows.py b/src/backend/langflow/api/v1/flows.py index 062c24426..a4a84a1aa 100644 --- a/src/backend/langflow/api/v1/flows.py +++ b/src/backend/langflow/api/v1/flows.py @@ -89,16 +89,13 @@ def read_flow( settings_service: "SettingsService" = Depends(get_settings_service), ): """Read a flow.""" - # auth_settings = settings_service.auth_settings - # if auth_settings.AUTO_LOGIN: - # user_id = None - # else: - # user_id = current_user.id - if user_flow := ( - session.exec( - select(Flow).where(Flow.id == flow_id, Flow.user_id == current_user.id) - ).first() - ): + auth_settings = settings_service.auth_settings + stmt = select(Flow).where(Flow.id == flow_id) + if auth_settings.AUTO_LOGIN: + # If auto login is enable user_id can be current_user.id or None + # so write an OR + stmt = stmt.where((Flow.user_id == current_user.id) | (Flow.user_id == None)) + if user_flow := session.exec(stmt).first(): return user_flow else: raise HTTPException(status_code=404, detail="Flow not found") From 1753b17053740cbc4d85a1d38276a0cec68f52f8 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 14:46:55 -0300 Subject: [PATCH 136/199] Add isList prop and update onChange function --- .../src/components/keypairListComponent/index.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/frontend/src/components/keypairListComponent/index.tsx b/src/frontend/src/components/keypairListComponent/index.tsx index 034628643..9f452c988 100644 --- a/src/frontend/src/components/keypairListComponent/index.tsx +++ b/src/frontend/src/components/keypairListComponent/index.tsx @@ -12,10 +12,11 @@ export default function KeypairListComponent({ disabled, editNode = false, duplicateKey, + isList = true, }: KeyPairListComponentType): JSX.Element { useEffect(() => { if (disabled && value.length > 0 && value[0] !== "") { - onChange([""]); + onChange([{ "": "" }]); } }, [disabled]); @@ -79,6 +80,7 @@ export default function KeypairListComponent({ : "keypair" + (index + 100).toString() } type="text" + disabled={disabled} value={obj[key]} className={editNode ? "input-edit-node" : ""} placeholder="Type a value..." @@ -87,7 +89,7 @@ export default function KeypairListComponent({ } /> - {index === ref.current.length - 1 ? ( + {isList && index === ref.current.length - 1 ? ( - ) : ( + ) : isList ? ( + ) : ( + "" )}
); From 1d9aed286ae2c050362d8fb3e6cbafaf2159f937 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 14:47:25 -0300 Subject: [PATCH 137/199] Add CodeErrorDataTypeAPI and ResponseErrorTypeAPI to types/api --- src/frontend/src/modals/codeAreaModal/index.tsx | 3 ++- src/frontend/src/types/api/index.ts | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/frontend/src/modals/codeAreaModal/index.tsx b/src/frontend/src/modals/codeAreaModal/index.tsx index b45e17b56..47f91d4b2 100644 --- a/src/frontend/src/modals/codeAreaModal/index.tsx +++ b/src/frontend/src/modals/codeAreaModal/index.tsx @@ -26,6 +26,7 @@ import { useDarkStore } from "../../stores/darkStore"; import useFlowStore from "../../stores/flowStore"; import { codeAreaModalPropsType } from "../../types/components"; import BaseModal from "../baseModal"; +import { CodeErrorDataTypeAPI } from "../../types/api"; export default function CodeAreaModal({ value, @@ -50,7 +51,7 @@ export default function CodeAreaModal({ const setSuccessData = useAlertStore((state) => state.setSuccessData); const setErrorData = useAlertStore((state) => state.setErrorData); const [error, setError] = useState<{ - detail: { error: string | undefined; traceback: string | undefined }; + detail: CodeErrorDataTypeAPI; } | null>(null); useEffect(() => { diff --git a/src/frontend/src/types/api/index.ts b/src/frontend/src/types/api/index.ts index 5e7682307..fc8346a5b 100644 --- a/src/frontend/src/types/api/index.ts +++ b/src/frontend/src/types/api/index.ts @@ -159,3 +159,17 @@ export type VertexDataTypeAPI = { timedelta?: number; duration?: string; }; + +export type CodeErrorDataTypeAPI = { + error: string | undefined; + traceback: string | undefined; +}; + +// the error above is inside this error.response.data.detail.error +// which comes from a request to the API +// to type the error we need to know the structure of the object + +// error that has a response, that has a data, that has a detail, that has an error +export type ResponseErrorTypeAPI = { + response: { data: { detail: CodeErrorDataTypeAPI } }; +}; From 35889692a216e1a53baa2e459664a4755c5da97a Mon Sep 17 00:00:00 2001 From: igorrCarvalho Date: Wed, 6 Mar 2024 14:49:11 -0300 Subject: [PATCH 138/199] Refactor: Rename JSX function to improve code understanding --- src/frontend/src/components/chatComponent/index.tsx | 2 +- .../src/pages/FlowPage/components/PageComponent/index.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/frontend/src/components/chatComponent/index.tsx b/src/frontend/src/components/chatComponent/index.tsx index 15e13fc15..a53184856 100644 --- a/src/frontend/src/components/chatComponent/index.tsx +++ b/src/frontend/src/components/chatComponent/index.tsx @@ -12,7 +12,7 @@ import useFlowsManagerStore from "../../stores/flowsManagerStore"; import { classNames } from "../../utils/utils"; import ApiModal from "../../modals/ApiModal"; -export default function Chat({ flow }: ChatType): JSX.Element { +export default function FlowToolbar({ flow }: ChatType): JSX.Element { const [open, setOpen] = useState(false); const flowState = useFlowStore((state) => state.flowState); const nodes = useFlowStore((state) => state.nodes); diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index d740129e4..91299ec6f 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -12,7 +12,6 @@ import ReactFlow, { updateEdge, } from "reactflow"; import GenericNode from "../../../../CustomNodes/GenericNode"; -import Chat from "../../../../components/chatComponent"; import { INVALID_SELECTION_ERROR_ALERT, UPLOAD_ALERT_LIST, @@ -38,6 +37,7 @@ import { getRandomName, isWrappedWithClass } from "../../../../utils/utils"; import ConnectionLineComponent from "../ConnectionLineComponent"; import SelectionMenu from "../SelectionMenuComponent"; import ExtraSidebar from "../extraSidebarComponent"; +import FlowToolbar from "../../../../components/chatComponent"; const nodeTypes = { genericNode: GenericNode, @@ -481,7 +481,7 @@ export default function Page({ }} /> - {!view && } + {!view && }
) : ( <> From a060b8de1839f0311f0566dcbaa8f419cdb90327 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 14:51:30 -0300 Subject: [PATCH 139/199] Refactor read_flows function to include current user's flows --- src/backend/langflow/api/v1/flows.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/backend/langflow/api/v1/flows.py b/src/backend/langflow/api/v1/flows.py index a4a84a1aa..d370458a9 100644 --- a/src/backend/langflow/api/v1/flows.py +++ b/src/backend/langflow/api/v1/flows.py @@ -57,7 +57,11 @@ def read_flows( try: auth_settings = settings_service.auth_settings if auth_settings.AUTO_LOGIN: - flows = session.exec(select(Flow).where(Flow.user_id == None)).all() + flows = session.exec( + select(Flow).where( + Flow.user_id == None | Flow.user_id == current_user.id + ) + ).all() # noqa else: flows = current_user.flows @@ -67,7 +71,7 @@ def read_flows( try: example_flows = session.exec( select(Flow).where( - Flow.user_id == None, Flow.folder == STARTER_FOLDER_NAME + Flow.user_id == None, Flow.folder == STARTER_FOLDER_NAME # noqa ) ).all() # noqa for example_flow in example_flows: From be32d57434f5ae849a8aefdbfcc3e7d637605bc4 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 14:53:10 -0300 Subject: [PATCH 140/199] Update chain.invoke() argument to use a dictionary for input value --- src/backend/langflow/components/chains/ConversationChain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/langflow/components/chains/ConversationChain.py b/src/backend/langflow/components/chains/ConversationChain.py index 6e1e319d6..774632412 100644 --- a/src/backend/langflow/components/chains/ConversationChain.py +++ b/src/backend/langflow/components/chains/ConversationChain.py @@ -31,7 +31,7 @@ class ConversationChainComponent(CustomComponent): chain = ConversationChain(llm=llm) else: chain = ConversationChain(llm=llm, memory=memory) - result = chain.invoke(inputs) + result = chain.invoke({"input": input_value}) if hasattr(result, "content") and isinstance(result.content, str): result = result.content elif isinstance(result, str): From 2450161fc16249aea594ad07258eddcbc8fcc64a Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 14:54:14 -0300 Subject: [PATCH 141/199] Format py --- src/backend/langflow/api/v1/base.py | 24 +--- src/backend/langflow/api/v1/callback.py | 16 +-- src/backend/langflow/api/v1/chat.py | 16 +-- src/backend/langflow/api/v1/endpoints.py | 37 ++---- src/backend/langflow/api/v1/flows.py | 13 +-- src/backend/langflow/api/v1/schemas.py | 4 +- src/backend/langflow/api/v1/validate.py | 47 ++------ src/backend/langflow/base/data/utils.py | 12 +- src/backend/langflow/base/io/chat.py | 4 +- .../langflow/components/data/APIRequest.py | 19 +-- .../langflow/components/data/Directory.py | 13 +-- .../langflow/components/data/FileLoader.py | 12 +- src/backend/langflow/components/data/URL.py | 1 - .../components/experimental/Notify.py | 4 +- .../components/experimental/RunFlow.py | 5 +- .../helpers/ExtractDataFromRecord.py | 1 - .../components/helpers/IDGenerator.py | 4 +- .../components/helpers/RecordComponent1.py | 47 ++++++++ .../components/helpers/RecordComponent2.py | 51 ++++++++ .../LanguageRecursiveTextSplitter.py | 4 +- .../RecursiveCharacterTextSplitter.py | 4 +- .../components/vectorstores/Chroma.py | 7 +- .../components/vectorstores/ChromaSearch.py | 3 +- .../components/vectorstores/FAISSSearch.py | 4 +- .../vectorstores/MongoDBAtlasVector.py | 8 +- .../langflow/components/vectorstores/Redis.py | 4 +- .../components/vectorstores/RedisSearch.py | 1 - .../vectorstores/SupabaseVectorStore.py | 4 +- .../vectorstores/SupabaseVectorStoreSearch.py | 4 +- .../components/vectorstores/Vectara.py | 4 +- .../components/vectorstores/VectaraSearch.py | 4 +- .../components/vectorstores/Weaviate.py | 4 +- .../components/vectorstores/WeaviateSearch.py | 4 +- .../components/vectorstores/base/model.py | 10 +- .../components/vectorstores/pgvector.py | 4 +- .../components/vectorstores/pgvectorSearch.py | 4 +- src/backend/langflow/graph/edge/base.py | 42 ++----- src/backend/langflow/graph/graph/base.py | 105 ++++------------- src/backend/langflow/graph/graph/constants.py | 5 +- src/backend/langflow/graph/vertex/base.py | 110 ++++-------------- src/backend/langflow/graph/vertex/types.py | 50 ++------ src/backend/langflow/helpers/record.py | 5 +- src/backend/langflow/initial_setup/setup.py | 10 +- .../custom/code_parser/code_parser.py | 38 ++---- .../custom/custom_component/component.py | 4 +- .../custom_component/custom_component.py | 48 ++------ .../directory_reader/directory_reader.py | 54 +++------ .../langflow/interface/custom/utils.py | 72 +++--------- .../langflow/interface/initialize/loading.py | 58 +++------ src/backend/langflow/interface/types.py | 4 +- src/backend/langflow/interface/utils.py | 12 +- src/backend/langflow/main.py | 12 +- src/backend/langflow/processing/process.py | 36 ++---- src/backend/langflow/schema/schema.py | 4 +- src/backend/langflow/services/chat/utils.py | 8 +- .../services/database/models/flow/model.py | 4 +- .../langflow/services/database/service.py | 38 ++---- .../langflow/services/monitor/schema.py | 12 +- .../langflow/services/settings/base.py | 35 ++---- src/backend/langflow/services/socket/utils.py | 4 +- src/backend/langflow/services/task/service.py | 4 +- src/backend/langflow/services/utils.py | 36 ++---- src/backend/langflow/template/field/base.py | 10 +- .../langflow/template/frontend_node/base.py | 27 +---- .../template/frontend_node/memories.py | 4 +- .../langflow/template/template/base.py | 4 +- src/backend/langflow/utils/util.py | 60 +++------- src/backend/langflow/utils/validate.py | 58 +++------ 68 files changed, 408 insertions(+), 972 deletions(-) create mode 100644 src/backend/langflow/components/helpers/RecordComponent1.py create mode 100644 src/backend/langflow/components/helpers/RecordComponent2.py diff --git a/src/backend/langflow/api/v1/base.py b/src/backend/langflow/api/v1/base.py index 2380b019e..cc16c6d1b 100644 --- a/src/backend/langflow/api/v1/base.py +++ b/src/backend/langflow/api/v1/base.py @@ -86,9 +86,7 @@ def validate_prompt(template: str): # Check if there are invalid characters in the input_variables input_variables = check_input_variables(input_variables) if any(var in INVALID_NAMES for var in input_variables): - raise ValueError( - f"Invalid input variables. None of the variables can be named {', '.join(input_variables)}. " - ) + raise ValueError(f"Invalid input variables. None of the variables can be named {', '.join(input_variables)}. ") try: PromptTemplate(template=template, input_variables=input_variables) @@ -123,9 +121,7 @@ def fix_variable(var, invalid_chars, wrong_variables): # Handle variables starting with a number if var[0].isdigit(): invalid_chars.append(var[0]) - new_var, invalid_chars, wrong_variables = fix_variable( - var[1:], invalid_chars, wrong_variables - ) + new_var, invalid_chars, wrong_variables = fix_variable(var[1:], invalid_chars, wrong_variables) # Temporarily replace {{ and }} to avoid treating them as invalid new_var = new_var.replace("{{", "ᴛᴇᴍᴘᴏᴘᴇɴ").replace("}}", "ᴛᴇᴍᴘᴄʟᴏsᴇ") @@ -152,9 +148,7 @@ def check_variable(var, invalid_chars, wrong_variables, empty_variables): return wrong_variables, empty_variables -def check_for_errors( - input_variables, fixed_variables, wrong_variables, empty_variables -): +def check_for_errors(input_variables, fixed_variables, wrong_variables, empty_variables): if any(var for var in input_variables if var not in fixed_variables): error_message = ( f"Error: Input variables contain invalid characters or formats. \n" @@ -179,17 +173,11 @@ def check_input_variables(input_variables): if is_json_like(var): continue - new_var, wrong_variables, empty_variables = fix_variable( - var, invalid_chars, wrong_variables - ) - wrong_variables, empty_variables = check_variable( - var, INVALID_CHARACTERS, wrong_variables, empty_variables - ) + new_var, wrong_variables, empty_variables = fix_variable(var, invalid_chars, wrong_variables) + wrong_variables, empty_variables = check_variable(var, INVALID_CHARACTERS, wrong_variables, empty_variables) fixed_variables.append(new_var) variables_to_check.append(var) - check_for_errors( - variables_to_check, fixed_variables, wrong_variables, empty_variables - ) + check_for_errors(variables_to_check, fixed_variables, wrong_variables, empty_variables) return fixed_variables diff --git a/src/backend/langflow/api/v1/callback.py b/src/backend/langflow/api/v1/callback.py index 38737623c..b326311ac 100644 --- a/src/backend/langflow/api/v1/callback.py +++ b/src/backend/langflow/api/v1/callback.py @@ -33,9 +33,7 @@ class AsyncStreamingLLMCallbackHandleSIO(AsyncCallbackHandler): resp = ChatResponse(message=token, type="stream", intermediate_steps="") await self.socketio_service.emit_token(to=self.sid, data=resp.model_dump()) - async def on_tool_start( - self, serialized: Dict[str, Any], input_str: str, **kwargs: Any - ) -> Any: + async def on_tool_start(self, serialized: Dict[str, Any], input_str: str, **kwargs: Any) -> Any: """Run when tool starts running.""" resp = ChatResponse( message="", @@ -73,9 +71,7 @@ class AsyncStreamingLLMCallbackHandleSIO(AsyncCallbackHandler): try: # This is to emulate the stream of tokens for resp in resps: - await self.socketio_service.emit_token( - to=self.sid, data=resp.model_dump() - ) + await self.socketio_service.emit_token(to=self.sid, data=resp.model_dump()) except Exception as exc: logger.error(f"Error sending response: {exc}") @@ -101,9 +97,7 @@ class AsyncStreamingLLMCallbackHandleSIO(AsyncCallbackHandler): resp = PromptResponse( prompt=text, ) - await self.socketio_service.emit_message( - to=self.sid, data=resp.model_dump() - ) + await self.socketio_service.emit_message(to=self.sid, data=resp.model_dump()) async def on_agent_action(self, action: AgentAction, **kwargs: Any): log = f"Thought: {action.log}" @@ -113,9 +107,7 @@ class AsyncStreamingLLMCallbackHandleSIO(AsyncCallbackHandler): logs = log.split("\n") for log in logs: resp = ChatResponse(message="", type="stream", intermediate_steps=log) - await self.socketio_service.emit_token( - to=self.sid, data=resp.model_dump() - ) + await self.socketio_service.emit_token(to=self.sid, data=resp.model_dump()) else: resp = ChatResponse(message="", type="stream", intermediate_steps=log) await self.socketio_service.emit_token(to=self.sid, data=resp.model_dump()) diff --git a/src/backend/langflow/api/v1/chat.py b/src/backend/langflow/api/v1/chat.py index b72504da6..ecec8f200 100644 --- a/src/backend/langflow/api/v1/chat.py +++ b/src/backend/langflow/api/v1/chat.py @@ -101,12 +101,8 @@ async def build_vertex( cache = chat_service.get_cache(flow_id) if not cache: # If there's no cache - logger.warning( - f"No cache found for {flow_id}. Building graph starting at {vertex_id}" - ) - graph = build_and_cache_graph( - flow_id=flow_id, session=next(get_session()), chat_service=chat_service - ) + logger.warning(f"No cache found for {flow_id}. Building graph starting at {vertex_id}") + graph = build_and_cache_graph(flow_id=flow_id, session=next(get_session()), chat_service=chat_service) else: graph = cache.get("result") result_data_response = ResultDataResponse(results={}) @@ -126,9 +122,7 @@ async def build_vertex( else: raise ValueError(f"No result found for vertex {vertex_id}") next_vertices_ids = vertex.successors_ids - next_vertices_ids = [ - v for v in next_vertices_ids if graph.should_run_vertex(v) - ] + next_vertices_ids = [v for v in next_vertices_ids if graph.should_run_vertex(v)] result_data_response = ResultDataResponse(**result_dict.model_dump()) @@ -211,9 +205,7 @@ async def build_vertex_stream( else: graph = cache.get("result") else: - session_data = await session_service.load_session( - session_id, flow_id=flow_id - ) + session_data = await session_service.load_session(session_id, flow_id=flow_id) graph, artifacts = session_data if session_data else (None, None) if not graph: raise ValueError(f"No graph found for {flow_id}.") diff --git a/src/backend/langflow/api/v1/endpoints.py b/src/backend/langflow/api/v1/endpoints.py index 5eb5e62b7..127a51627 100644 --- a/src/backend/langflow/api/v1/endpoints.py +++ b/src/backend/langflow/api/v1/endpoints.py @@ -52,9 +52,7 @@ def get_all( raise HTTPException(status_code=500, detail=str(exc)) from exc -@router.post( - "/run/{flow_id}", response_model=RunResponse, response_model_exclude_none=True -) +@router.post("/run/{flow_id}", response_model=RunResponse, response_model_exclude_none=True) async def run_flow_with_caching( session: Annotated[Session, Depends(get_session)], flow_id: str, @@ -113,9 +111,7 @@ async def run_flow_with_caching( outputs = [] if session_id: - session_data = await session_service.load_session( - session_id, flow_id=flow_id - ) + session_data = await session_service.load_session(session_id, flow_id=flow_id) graph, artifacts = session_data if session_data else (None, None) task_result: Any = None if not graph: @@ -134,11 +130,7 @@ async def run_flow_with_caching( else: # Get the flow that matches the flow_id and belongs to the user # flow = session.query(Flow).filter(Flow.id == flow_id).filter(Flow.user_id == api_key_user.id).first() - flow = session.exec( - select(Flow) - .where(Flow.id == flow_id) - .where(Flow.user_id == api_key_user.id) - ).first() + flow = session.exec(select(Flow).where(Flow.id == flow_id).where(Flow.user_id == api_key_user.id)).first() if flow is None: raise ValueError(f"Flow {flow_id} not found") @@ -162,18 +154,12 @@ async def run_flow_with_caching( # StatementError('(builtins.ValueError) badly formed hexadecimal UUID string') if "badly formed hexadecimal UUID string" in str(exc): # This means the Flow ID is not a valid UUID which means it can't find the flow - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=str(exc) - ) from exc + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc except ValueError as exc: if f"Flow {flow_id} not found" in str(exc): - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=str(exc) - ) from exc + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc else: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc) - ) from exc + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)) from exc @router.post( @@ -202,8 +188,7 @@ async def process( """ # Raise a depreciation warning logger.warning( - "The /process endpoint is deprecated and will be removed in a future version. " - "Please use /run instead." + "The /process endpoint is deprecated and will be removed in a future version. " "Please use /run instead." ) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -275,16 +260,12 @@ async def custom_component( built_frontend_node = build_custom_component_template(component, user_id=user.id) - built_frontend_node = update_frontend_node_with_template_values( - built_frontend_node, raw_code.frontend_node - ) + built_frontend_node = update_frontend_node_with_template_values(built_frontend_node, raw_code.frontend_node) return built_frontend_node @router.post("/custom_component/reload", status_code=HTTPStatus.OK) -async def reload_custom_component( - path: str, user: User = Depends(get_current_active_user) -): +async def reload_custom_component(path: str, user: User = Depends(get_current_active_user)): from langflow.interface.custom.utils import build_custom_component_template try: diff --git a/src/backend/langflow/api/v1/flows.py b/src/backend/langflow/api/v1/flows.py index d370458a9..069d98a9e 100644 --- a/src/backend/langflow/api/v1/flows.py +++ b/src/backend/langflow/api/v1/flows.py @@ -57,11 +57,7 @@ def read_flows( try: auth_settings = settings_service.auth_settings if auth_settings.AUTO_LOGIN: - flows = session.exec( - select(Flow).where( - Flow.user_id == None | Flow.user_id == current_user.id - ) - ).all() # noqa + flows = session.exec(select(Flow).where(Flow.user_id == None | Flow.user_id == current_user.id)).all() # noqa else: flows = current_user.flows @@ -71,7 +67,8 @@ def read_flows( try: example_flows = session.exec( select(Flow).where( - Flow.user_id == None, Flow.folder == STARTER_FOLDER_NAME # noqa + Flow.user_id == None, + Flow.folder == STARTER_FOLDER_NAME, # noqa ) ).all() # noqa for example_flow in example_flows: @@ -98,7 +95,9 @@ def read_flow( if auth_settings.AUTO_LOGIN: # If auto login is enable user_id can be current_user.id or None # so write an OR - stmt = stmt.where((Flow.user_id == current_user.id) | (Flow.user_id == None)) + stmt = stmt.where( + (Flow.user_id == current_user.id) | (Flow.user_id == None) # noqa + ) # noqa if user_flow := session.exec(stmt).first(): return user_flow else: diff --git a/src/backend/langflow/api/v1/schemas.py b/src/backend/langflow/api/v1/schemas.py index d500d8278..70a60de5b 100644 --- a/src/backend/langflow/api/v1/schemas.py +++ b/src/backend/langflow/api/v1/schemas.py @@ -158,9 +158,7 @@ class StreamData(BaseModel): data: dict def __str__(self) -> str: - return ( - f"event: {self.event}\ndata: {orjson_dumps(self.data, indent_2=False)}\n\n" - ) + return f"event: {self.event}\ndata: {orjson_dumps(self.data, indent_2=False)}\n\n" class CustomComponentCode(BaseModel): diff --git a/src/backend/langflow/api/v1/validate.py b/src/backend/langflow/api/v1/validate.py index 6b64faf20..02c17686b 100644 --- a/src/backend/langflow/api/v1/validate.py +++ b/src/backend/langflow/api/v1/validate.py @@ -41,9 +41,7 @@ def post_validate_prompt(prompt_request: ValidatePromptRequest): add_new_variables_to_template(input_variables, prompt_request) - remove_old_variables_from_template( - old_custom_fields, input_variables, prompt_request - ) + remove_old_variables_from_template(old_custom_fields, input_variables, prompt_request) update_input_variables_field(input_variables, prompt_request) @@ -58,19 +56,12 @@ def post_validate_prompt(prompt_request: ValidatePromptRequest): def get_old_custom_fields(prompt_request): try: - if ( - len(prompt_request.frontend_node.custom_fields) == 1 - and prompt_request.name == "" - ): + if len(prompt_request.frontend_node.custom_fields) == 1 and prompt_request.name == "": # If there is only one custom field and the name is empty string # then we are dealing with the first prompt request after the node was created - prompt_request.name = list( - prompt_request.frontend_node.custom_fields.keys() - )[0] + prompt_request.name = list(prompt_request.frontend_node.custom_fields.keys())[0] - old_custom_fields = prompt_request.frontend_node.custom_fields[ - prompt_request.name - ] + old_custom_fields = prompt_request.frontend_node.custom_fields[prompt_request.name] if old_custom_fields is None: old_custom_fields = [] @@ -87,40 +78,26 @@ def add_new_variables_to_template(input_variables, prompt_request): template_field = DefaultPromptField(name=variable, display_name=variable) if variable in prompt_request.frontend_node.template: # Set the new field with the old value - template_field.value = prompt_request.frontend_node.template[variable][ - "value" - ] + template_field.value = prompt_request.frontend_node.template[variable]["value"] prompt_request.frontend_node.template[variable] = template_field.to_dict() # Check if variable is not already in the list before appending - if ( - variable - not in prompt_request.frontend_node.custom_fields[prompt_request.name] - ): - prompt_request.frontend_node.custom_fields[prompt_request.name].append( - variable - ) + if variable not in prompt_request.frontend_node.custom_fields[prompt_request.name]: + prompt_request.frontend_node.custom_fields[prompt_request.name].append(variable) except Exception as exc: logger.exception(exc) raise HTTPException(status_code=500, detail=str(exc)) from exc -def remove_old_variables_from_template( - old_custom_fields, input_variables, prompt_request -): +def remove_old_variables_from_template(old_custom_fields, input_variables, prompt_request): for variable in old_custom_fields: if variable not in input_variables: try: # Remove the variable from custom_fields associated with the given name - if ( - variable - in prompt_request.frontend_node.custom_fields[prompt_request.name] - ): - prompt_request.frontend_node.custom_fields[ - prompt_request.name - ].remove(variable) + if variable in prompt_request.frontend_node.custom_fields[prompt_request.name]: + prompt_request.frontend_node.custom_fields[prompt_request.name].remove(variable) # Remove the variable from the template prompt_request.frontend_node.template.pop(variable, None) @@ -132,6 +109,4 @@ def remove_old_variables_from_template( def update_input_variables_field(input_variables, prompt_request): if "input_variables" in prompt_request.frontend_node.template: - prompt_request.frontend_node.template["input_variables"][ - "value" - ] = input_variables + prompt_request.frontend_node.template["input_variables"]["value"] = input_variables diff --git a/src/backend/langflow/base/data/utils.py b/src/backend/langflow/base/data/utils.py index 2219310ef..03f6de046 100644 --- a/src/backend/langflow/base/data/utils.py +++ b/src/backend/langflow/base/data/utils.py @@ -35,9 +35,7 @@ def retrieve_file_paths( 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) - ] + file_paths = [Text(p) for p in paths if p.is_file() and match_types(p) and is_not_hidden(p)] return file_paths @@ -70,16 +68,12 @@ def get_elements( 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 = [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]]: +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), diff --git a/src/backend/langflow/base/io/chat.py b/src/backend/langflow/base/io/chat.py index 8695dbcbc..db15223ee 100644 --- a/src/backend/langflow/base/io/chat.py +++ b/src/backend/langflow/base/io/chat.py @@ -45,9 +45,7 @@ class ChatComponent(CustomComponent): return [] if not session_id or not sender or not sender_name: - raise ValueError( - "All of session_id, sender, and sender_name must be provided." - ) + raise ValueError("All of session_id, sender, and sender_name must be provided.") if isinstance(message, Record): record = message record.data.update( diff --git a/src/backend/langflow/components/data/APIRequest.py b/src/backend/langflow/components/data/APIRequest.py index 86674c781..abdede217 100644 --- a/src/backend/langflow/components/data/APIRequest.py +++ b/src/backend/langflow/components/data/APIRequest.py @@ -27,12 +27,12 @@ class APIRequest(CustomComponent): "headers": { "display_name": "Headers", "info": "The headers to send with the request.", - "input_types": ["dict"] + "input_types": ["dict"], }, "body": { "display_name": "Body", "info": "The body to send with the request (for POST, PATCH, PUT).", - "input_types": ["dict"] + "input_types": ["dict"], }, "timeout": { "display_name": "Timeout", @@ -58,9 +58,7 @@ class APIRequest(CustomComponent): data = body if body else None data = json.dumps(data) try: - response = await client.request( - method, url, headers=headers, content=data, timeout=timeout - ) + response = await client.request(method, url, headers=headers, content=data, timeout=timeout) try: response_json = response.json() result = orjson_dumps(response_json, indent_2=False) @@ -96,16 +94,9 @@ class APIRequest(CustomComponent): if headers is None: headers = {} urls = url if isinstance(url, list) else [url] - bodies = ( - body - if isinstance(body, list) - else [body] if body else [None] * len(urls) - ) + bodies = body if isinstance(body, list) else [body] if body else [None] * len(urls) async with httpx.AsyncClient() as client: results = await asyncio.gather( - *[ - self.make_request(client, method, u, headers, rec, timeout) - for u, rec in zip(urls, bodies) - ] + *[self.make_request(client, method, u, headers, rec, timeout) for u, rec in zip(urls, bodies)] ) return results diff --git a/src/backend/langflow/components/data/Directory.py b/src/backend/langflow/components/data/Directory.py index 327f270cc..f05b11e2c 100644 --- a/src/backend/langflow/components/data/Directory.py +++ b/src/backend/langflow/components/data/Directory.py @@ -57,20 +57,13 @@ class DirectoryComponent(CustomComponent): if types is None: types = [] resolved_path = self.resolve_path(path) - file_paths = retrieve_file_paths( - resolved_path, types, load_hidden, recursive, depth - ) + 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 - ) + 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 = [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/FileLoader.py b/src/backend/langflow/components/data/FileLoader.py index 2f74d9d04..d513298c6 100644 --- a/src/backend/langflow/components/data/FileLoader.py +++ b/src/backend/langflow/components/data/FileLoader.py @@ -11,9 +11,7 @@ class FileLoaderComponent(CustomComponent): beta = True def build_config(self): - loader_options = ["Automatic"] + [ - loader_info["name"] for loader_info in LOADERS_INFO - ] + loader_options = ["Automatic"] + [loader_info["name"] for loader_info in LOADERS_INFO] file_types = [] suffixes = [] @@ -105,9 +103,7 @@ class FileLoaderComponent(CustomComponent): if isinstance(selected_loader_info, dict): loader_import: str = selected_loader_info["import"] else: - raise ValueError( - f"Loader info for {loader} is not a dict\nLoader info:\n{selected_loader_info}" - ) + raise ValueError(f"Loader info for {loader} is not a dict\nLoader info:\n{selected_loader_info}") module_name, class_name = loader_import.rsplit(".", 1) try: @@ -115,9 +111,7 @@ class FileLoaderComponent(CustomComponent): loader_module = __import__(module_name, fromlist=[class_name]) loader_instance = getattr(loader_module, class_name) except ImportError as e: - raise ValueError( - f"Loader {loader} could not be imported\nLoader info:\n{selected_loader_info}" - ) from e + raise ValueError(f"Loader {loader} could not be imported\nLoader info:\n{selected_loader_info}") from e result = loader_instance(file_path=file_path) docs = result.load() diff --git a/src/backend/langflow/components/data/URL.py b/src/backend/langflow/components/data/URL.py index d3af63b1c..8368e72be 100644 --- a/src/backend/langflow/components/data/URL.py +++ b/src/backend/langflow/components/data/URL.py @@ -19,7 +19,6 @@ class URLComponent(CustomComponent): self, urls: list[str], ) -> Record: - loader = WebBaseLoader(web_paths=urls) docs = loader.load() records = self.to_records(docs) diff --git a/src/backend/langflow/components/experimental/Notify.py b/src/backend/langflow/components/experimental/Notify.py index 2155b079e..3b5662355 100644 --- a/src/backend/langflow/components/experimental/Notify.py +++ b/src/backend/langflow/components/experimental/Notify.py @@ -18,9 +18,7 @@ class NotifyComponent(CustomComponent): }, } - def build( - self, name: str, record: Optional[Record] = None, append: bool = False - ) -> Record: + def build(self, name: str, record: Optional[Record] = None, append: bool = False) -> Record: if record and not isinstance(record, Record): if isinstance(record, str): record = Record(text=record) diff --git a/src/backend/langflow/components/experimental/RunFlow.py b/src/backend/langflow/components/experimental/RunFlow.py index d0e49ac90..94ba88044 100644 --- a/src/backend/langflow/components/experimental/RunFlow.py +++ b/src/backend/langflow/components/experimental/RunFlow.py @@ -39,10 +39,7 @@ class RunFlowComponent(CustomComponent): records.append(record) return records - async def build( - self, input_value: Text, flow_name: str, tweaks: NestedDict - ) -> Record: - + async def build(self, input_value: Text, flow_name: str, tweaks: NestedDict) -> Record: results: List[Optional[ResultData]] = await self.run_flow( input_value=input_value, flow_name=flow_name, tweaks=tweaks ) diff --git a/src/backend/langflow/components/helpers/ExtractDataFromRecord.py b/src/backend/langflow/components/helpers/ExtractDataFromRecord.py index 00fda4b40..2b28545b5 100644 --- a/src/backend/langflow/components/helpers/ExtractDataFromRecord.py +++ b/src/backend/langflow/components/helpers/ExtractDataFromRecord.py @@ -11,7 +11,6 @@ class ExtractKeyFromRecordComponent(CustomComponent): } def build(self, record: Record, key: str, silent_error: bool = True) -> dict: - data = getattr(record, key) self.status = data return data diff --git a/src/backend/langflow/components/helpers/IDGenerator.py b/src/backend/langflow/components/helpers/IDGenerator.py index 1a8cecbfd..35b9a3d42 100644 --- a/src/backend/langflow/components/helpers/IDGenerator.py +++ b/src/backend/langflow/components/helpers/IDGenerator.py @@ -9,9 +9,7 @@ class UUIDGeneratorComponent(CustomComponent): display_name = "Unique ID Generator" description = "Generates a unique ID." - def update_build_config( - self, build_config: dict, field_name: Text, field_value: Any - ): + def update_build_config(self, build_config: dict, field_name: Text, field_value: Any): if field_name == "unique_id": build_config[field_name]["value"] = str(uuid.uuid4()) return build_config diff --git a/src/backend/langflow/components/helpers/RecordComponent1.py b/src/backend/langflow/components/helpers/RecordComponent1.py new file mode 100644 index 000000000..dce557826 --- /dev/null +++ b/src/backend/langflow/components/helpers/RecordComponent1.py @@ -0,0 +1,47 @@ +from typing import Any + +from langflow import CustomComponent +from langflow.schema import Record +from langflow.template.field.base import TemplateField + + +class RecordComponent(CustomComponent): + display_name = "Record Numbers" + description = "A component to create a record from key-value pairs." + field_order = ["n_keys"] + + def update_build_config(self, build_config: dict, field_name: str, field_value: Any): + if field_value is None: + return + elif int(field_value) == 0: + keep = ["n_keys", "code"] + for key in build_config.copy(): + if key in keep: + continue + del build_config[key] + build_config[field_name]["value"] = int(field_value) + + # Add new fields depending on the field value + for i in range(int(field_value)): + field = TemplateField( + name=f"Key and Value {i}", + field_type="dict", + display_name="", + info="The key for the record.", + input_types=["Text"], + ) + build_config[field.name] = field.to_dict() + + def build_config(self): + return { + "n_keys": { + "display_name": "Number of Fields", + "refresh": True, + "info": "The number of keys to create in the record.", + }, + } + + def build(self, n_keys: int, **kwargs) -> Record: + data = {k: v for d in kwargs.values() for k, v in d.items()} + record = Record(data=data) + return record diff --git a/src/backend/langflow/components/helpers/RecordComponent2.py b/src/backend/langflow/components/helpers/RecordComponent2.py new file mode 100644 index 000000000..b476efb4d --- /dev/null +++ b/src/backend/langflow/components/helpers/RecordComponent2.py @@ -0,0 +1,51 @@ +from typing import Any, List + +from langflow import CustomComponent +from langflow.schema import Record +from langflow.template.field.base import TemplateField + + +class RecordComponent2(CustomComponent): + display_name = "Record Text" + description = "A component to create a record from key-value pairs." + field_order = ["keys"] + + def update_build_config(self, build_config: dict, field_name: str, field_value: Any): + if field_value is None: + field_value = [] + if field_name is None: + return build_config + elif len(field_value) == 0: + keep = ["keys", "code"] + for key in build_config.copy(): + if key in keep: + continue + del build_config[key] + build_config[field_name]["value"] = field_value + + # Add new fields depending on the field value + for val in field_value: + if not isinstance(val, str) or val == "": + continue + field = TemplateField( + name=val, + field_type="str", + display_name="", + info="The key for the record.", + ) + build_config[field.name] = field.to_dict() + + def build_config(self): + return { + "keys": { + "display_name": "Keys", + "refresh": True, + "info": "The number of keys to create in the record.", + "input_types": [], + }, + } + + def build(self, keys: List[str], **kwargs) -> Record: + record = Record(data=kwargs) + self.status = record + return record diff --git a/src/backend/langflow/components/textsplitters/LanguageRecursiveTextSplitter.py b/src/backend/langflow/components/textsplitters/LanguageRecursiveTextSplitter.py index b86b834d8..3f521e0ba 100644 --- a/src/backend/langflow/components/textsplitters/LanguageRecursiveTextSplitter.py +++ b/src/backend/langflow/components/textsplitters/LanguageRecursiveTextSplitter.py @@ -9,9 +9,7 @@ from langflow.schema.schema import Record class LanguageRecursiveTextSplitterComponent(CustomComponent): display_name: str = "Language Recursive Text Splitter" description: str = "Split text into chunks of a specified length based on language." - documentation: str = ( - "https://docs.langflow.org/components/text-splitters#languagerecursivetextsplitter" - ) + documentation: str = "https://docs.langflow.org/components/text-splitters#languagerecursivetextsplitter" def build_config(self): options = [x.value for x in Language] diff --git a/src/backend/langflow/components/textsplitters/RecursiveCharacterTextSplitter.py b/src/backend/langflow/components/textsplitters/RecursiveCharacterTextSplitter.py index a15600394..c523a2921 100644 --- a/src/backend/langflow/components/textsplitters/RecursiveCharacterTextSplitter.py +++ b/src/backend/langflow/components/textsplitters/RecursiveCharacterTextSplitter.py @@ -11,9 +11,7 @@ from langflow.utils.util import build_loader_repr_from_documents class RecursiveCharacterTextSplitterComponent(CustomComponent): display_name: str = "Recursive Character Text Splitter" description: str = "Split text into chunks of a specified length." - documentation: str = ( - "https://docs.langflow.org/components/text-splitters#recursivecharactertextsplitter" - ) + documentation: str = "https://docs.langflow.org/components/text-splitters#recursivecharactertextsplitter" def build_config(self): return { diff --git a/src/backend/langflow/components/vectorstores/Chroma.py b/src/backend/langflow/components/vectorstores/Chroma.py index 2a247e8d6..063ff7cf3 100644 --- a/src/backend/langflow/components/vectorstores/Chroma.py +++ b/src/backend/langflow/components/vectorstores/Chroma.py @@ -85,8 +85,7 @@ class ChromaComponent(CustomComponent): if chroma_server_host is not None: chroma_settings = chromadb.config.Settings( - chroma_server_cors_allow_origins=chroma_server_cors_allow_origins - or None, + chroma_server_cors_allow_origins=chroma_server_cors_allow_origins or None, chroma_server_host=chroma_server_host, chroma_server_port=chroma_server_port or None, chroma_server_grpc_port=chroma_server_grpc_port or None, @@ -107,9 +106,7 @@ class ChromaComponent(CustomComponent): documents.append(_input) if documents is not None and embedding is not None: if len(documents) == 0: - raise ValueError( - "If documents are provided, there must be at least one document." - ) + raise ValueError("If documents are provided, there must be at least one document.") chroma = Chroma.from_documents( documents=documents, # type: ignore persist_directory=index_directory, diff --git a/src/backend/langflow/components/vectorstores/ChromaSearch.py b/src/backend/langflow/components/vectorstores/ChromaSearch.py index cf98ee987..baa550472 100644 --- a/src/backend/langflow/components/vectorstores/ChromaSearch.py +++ b/src/backend/langflow/components/vectorstores/ChromaSearch.py @@ -92,8 +92,7 @@ class ChromaSearchComponent(LCVectorStoreComponent): if chroma_server_host is not None: chroma_settings = chromadb.config.Settings( - chroma_server_cors_allow_origins=chroma_server_cors_allow_origins - or None, + chroma_server_cors_allow_origins=chroma_server_cors_allow_origins or None, chroma_server_host=chroma_server_host, chroma_server_port=chroma_server_port or None, chroma_server_grpc_port=chroma_server_grpc_port or None, diff --git a/src/backend/langflow/components/vectorstores/FAISSSearch.py b/src/backend/langflow/components/vectorstores/FAISSSearch.py index 4544c300b..27cdc606c 100644 --- a/src/backend/langflow/components/vectorstores/FAISSSearch.py +++ b/src/backend/langflow/components/vectorstores/FAISSSearch.py @@ -33,9 +33,7 @@ class FAISSSearchComponent(LCVectorStoreComponent): if not folder_path: raise ValueError("Folder path is required to save the FAISS index.") path = self.resolve_path(folder_path) - vector_store = FAISS.load_local( - folder_path=Text(path), embeddings=embedding, index_name=index_name - ) + vector_store = FAISS.load_local(folder_path=Text(path), embeddings=embedding, index_name=index_name) if not vector_store: raise ValueError("Failed to load the FAISS index.") diff --git a/src/backend/langflow/components/vectorstores/MongoDBAtlasVector.py b/src/backend/langflow/components/vectorstores/MongoDBAtlasVector.py index 4488e4e92..f45d55584 100644 --- a/src/backend/langflow/components/vectorstores/MongoDBAtlasVector.py +++ b/src/backend/langflow/components/vectorstores/MongoDBAtlasVector.py @@ -9,9 +9,7 @@ from langflow.schema.schema import Record class MongoDBAtlasComponent(CustomComponent): display_name = "MongoDB Atlas" - description = ( - "Construct a `MongoDB Atlas Vector Search` vector store from raw documents." - ) + description = "Construct a `MongoDB Atlas Vector Search` vector store from raw documents." icon = "MongoDB" def build_config(self): @@ -39,9 +37,7 @@ class MongoDBAtlasComponent(CustomComponent): try: from pymongo import MongoClient except ImportError: - raise ImportError( - "Please install pymongo to use MongoDB Atlas Vector Store" - ) + raise ImportError("Please install pymongo to use MongoDB Atlas Vector Store") try: mongo_client: MongoClient = MongoClient(mongodb_atlas_cluster_uri) collection = mongo_client[db_name][collection_name] diff --git a/src/backend/langflow/components/vectorstores/Redis.py b/src/backend/langflow/components/vectorstores/Redis.py index bbf04f1a4..599a697a0 100644 --- a/src/backend/langflow/components/vectorstores/Redis.py +++ b/src/backend/langflow/components/vectorstores/Redis.py @@ -67,9 +67,7 @@ class RedisComponent(CustomComponent): documents.append(_input) if not documents: if schema is None: - raise ValueError( - "If no documents are provided, a schema must be provided." - ) + raise ValueError("If no documents are provided, a schema must be provided.") redis_vs = Redis.from_existing_index( embedding=embedding, index_name=redis_index_name, diff --git a/src/backend/langflow/components/vectorstores/RedisSearch.py b/src/backend/langflow/components/vectorstores/RedisSearch.py index 63fc46ddb..b2b420d3e 100644 --- a/src/backend/langflow/components/vectorstores/RedisSearch.py +++ b/src/backend/langflow/components/vectorstores/RedisSearch.py @@ -33,7 +33,6 @@ class RedisSearchComponent(RedisComponent, LCVectorStoreComponent): "input_value": {"display_name": "Input"}, "index_name": {"display_name": "Index Name", "value": "your_index"}, "code": {"show": False, "display_name": "Code"}, - "embedding": {"display_name": "Embedding"}, "schema": {"display_name": "Schema", "file_types": [".yaml"]}, "redis_server_url": { diff --git a/src/backend/langflow/components/vectorstores/SupabaseVectorStore.py b/src/backend/langflow/components/vectorstores/SupabaseVectorStore.py index 7f6296c03..5d32388d9 100644 --- a/src/backend/langflow/components/vectorstores/SupabaseVectorStore.py +++ b/src/backend/langflow/components/vectorstores/SupabaseVectorStore.py @@ -35,9 +35,7 @@ class SupabaseComponent(CustomComponent): supabase_url: str = "", table_name: str = "", ) -> Union[VectorStore, SupabaseVectorStore, BaseRetriever]: - supabase: Client = create_client( - supabase_url, supabase_key=supabase_service_key - ) + supabase: Client = create_client(supabase_url, supabase_key=supabase_service_key) documents = [] for _input in inputs: if isinstance(_input, Record): diff --git a/src/backend/langflow/components/vectorstores/SupabaseVectorStoreSearch.py b/src/backend/langflow/components/vectorstores/SupabaseVectorStoreSearch.py index 5fd4dbd18..ca8113c56 100644 --- a/src/backend/langflow/components/vectorstores/SupabaseVectorStoreSearch.py +++ b/src/backend/langflow/components/vectorstores/SupabaseVectorStoreSearch.py @@ -38,9 +38,7 @@ class SupabaseSearchComponent(LCVectorStoreComponent): supabase_url: str = "", table_name: str = "", ) -> List[Record]: - supabase: Client = create_client( - supabase_url, supabase_key=supabase_service_key - ) + supabase: Client = create_client(supabase_url, supabase_key=supabase_service_key) vector_store = SupabaseVectorStore( client=supabase, embedding=embedding, diff --git a/src/backend/langflow/components/vectorstores/Vectara.py b/src/backend/langflow/components/vectorstores/Vectara.py index 8f1f7250e..cd25b2dd9 100644 --- a/src/backend/langflow/components/vectorstores/Vectara.py +++ b/src/backend/langflow/components/vectorstores/Vectara.py @@ -15,9 +15,7 @@ from langflow.schema.schema import Record class VectaraComponent(CustomComponent): display_name: str = "Vectara" description: str = "Implementation of Vector Store using Vectara" - documentation = ( - "https://python.langchain.com/docs/integrations/vectorstores/vectara" - ) + documentation = "https://python.langchain.com/docs/integrations/vectorstores/vectara" beta = True icon = "Vectara" field_config = { diff --git a/src/backend/langflow/components/vectorstores/VectaraSearch.py b/src/backend/langflow/components/vectorstores/VectaraSearch.py index cbc876f86..3220d1561 100644 --- a/src/backend/langflow/components/vectorstores/VectaraSearch.py +++ b/src/backend/langflow/components/vectorstores/VectaraSearch.py @@ -11,9 +11,7 @@ from langflow.schema import Record class VectaraSearchComponent(VectaraComponent, LCVectorStoreComponent): display_name: str = "Vectara Search" description: str = "Search a Vectara Vector Store for similar documents." - documentation = ( - "https://python.langchain.com/docs/integrations/vectorstores/vectara" - ) + documentation = "https://python.langchain.com/docs/integrations/vectorstores/vectara" beta = True icon = "Vectara" diff --git a/src/backend/langflow/components/vectorstores/Weaviate.py b/src/backend/langflow/components/vectorstores/Weaviate.py index a85febcd5..8bc46d17b 100644 --- a/src/backend/langflow/components/vectorstores/Weaviate.py +++ b/src/backend/langflow/components/vectorstores/Weaviate.py @@ -12,9 +12,7 @@ from langflow.schema.schema import Record class WeaviateVectorStoreComponent(CustomComponent): display_name: str = "Weaviate" description: str = "Implementation of Vector Store using Weaviate" - documentation = ( - "https://python.langchain.com/docs/integrations/vectorstores/weaviate" - ) + documentation = "https://python.langchain.com/docs/integrations/vectorstores/weaviate" beta = True field_config = { "url": {"display_name": "Weaviate URL", "value": "http://localhost:8080"}, diff --git a/src/backend/langflow/components/vectorstores/WeaviateSearch.py b/src/backend/langflow/components/vectorstores/WeaviateSearch.py index 9b14ca779..5713ca26f 100644 --- a/src/backend/langflow/components/vectorstores/WeaviateSearch.py +++ b/src/backend/langflow/components/vectorstores/WeaviateSearch.py @@ -11,9 +11,7 @@ from langflow.schema import Record class WeaviateSearchVectorStore(WeaviateVectorStoreComponent, LCVectorStoreComponent): display_name: str = "Weaviate Search" description: str = "Search a Weaviate Vector Store for similar documents." - documentation = ( - "https://python.langchain.com/docs/integrations/vectorstores/weaviate" - ) + documentation = "https://python.langchain.com/docs/integrations/vectorstores/weaviate" beta = True icon = "Weaviate" diff --git a/src/backend/langflow/components/vectorstores/base/model.py b/src/backend/langflow/components/vectorstores/base/model.py index 6c2c7d453..de5b5840f 100644 --- a/src/backend/langflow/components/vectorstores/base/model.py +++ b/src/backend/langflow/components/vectorstores/base/model.py @@ -37,14 +37,8 @@ class LCVectorStoreComponent(CustomComponent): """ docs: List[Document] = [] - if ( - input_value - and isinstance(input_value, str) - and hasattr(vector_store, "search") - ): - docs = vector_store.search( - query=input_value, search_type=search_type.lower() - ) + if input_value and isinstance(input_value, str) and hasattr(vector_store, "search"): + docs = vector_store.search(query=input_value, search_type=search_type.lower()) else: raise ValueError("Invalid inputs provided.") return docs_to_records(docs) diff --git a/src/backend/langflow/components/vectorstores/pgvector.py b/src/backend/langflow/components/vectorstores/pgvector.py index e3b528313..7ab20b8df 100644 --- a/src/backend/langflow/components/vectorstores/pgvector.py +++ b/src/backend/langflow/components/vectorstores/pgvector.py @@ -16,9 +16,7 @@ class PGVectorComponent(CustomComponent): display_name: str = "PGVector" description: str = "Implementation of Vector Store using PostgreSQL" - documentation = ( - "https://python.langchain.com/docs/integrations/vectorstores/pgvector" - ) + documentation = "https://python.langchain.com/docs/integrations/vectorstores/pgvector" def build_config(self): """ diff --git a/src/backend/langflow/components/vectorstores/pgvectorSearch.py b/src/backend/langflow/components/vectorstores/pgvectorSearch.py index f40e5ed26..04666fe74 100644 --- a/src/backend/langflow/components/vectorstores/pgvectorSearch.py +++ b/src/backend/langflow/components/vectorstores/pgvectorSearch.py @@ -15,9 +15,7 @@ class PGVectorSearchComponent(PGVectorComponent, LCVectorStoreComponent): display_name: str = "PGVector Search" description: str = "Search a PGVector Store for similar documents." - documentation = ( - "https://python.langchain.com/docs/integrations/vectorstores/pgvector" - ) + documentation = "https://python.langchain.com/docs/integrations/vectorstores/pgvector" def build_config(self): """ diff --git a/src/backend/langflow/graph/edge/base.py b/src/backend/langflow/graph/edge/base.py index c49ec714c..53c4892f5 100644 --- a/src/backend/langflow/graph/edge/base.py +++ b/src/backend/langflow/graph/edge/base.py @@ -12,9 +12,7 @@ if TYPE_CHECKING: class SourceHandle(BaseModel): - baseClasses: List[str] = Field( - ..., description="List of base classes for the source handle." - ) + baseClasses: List[str] = Field(..., description="List of base classes for the source handle.") dataType: str = Field(..., description="Data type for the source handle.") id: str = Field(..., description="Unique identifier for the source handle.") @@ -22,9 +20,7 @@ class SourceHandle(BaseModel): class TargetHandle(BaseModel): fieldName: str = Field(..., description="Field name for the target handle.") id: str = Field(..., description="Unique identifier for the target handle.") - inputTypes: Optional[List[str]] = Field( - None, description="List of input types for the target handle." - ) + inputTypes: Optional[List[str]] = Field(None, description="List of input types for the target handle.") type: str = Field(..., description="Type of the target handle.") @@ -53,24 +49,16 @@ class Edge: def validate_handles(self, source, target) -> None: if self.target_handle.inputTypes is None: - self.valid_handles = ( - self.target_handle.type in self.source_handle.baseClasses - ) + self.valid_handles = self.target_handle.type in self.source_handle.baseClasses else: self.valid_handles = ( - any( - baseClass in self.target_handle.inputTypes - for baseClass in self.source_handle.baseClasses - ) + any(baseClass in self.target_handle.inputTypes for baseClass in self.source_handle.baseClasses) or self.target_handle.type in self.source_handle.baseClasses ) if not self.valid_handles: logger.debug(self.source_handle) logger.debug(self.target_handle) - raise ValueError( - f"Edge between {source.vertex_type} and {target.vertex_type} " - f"has invalid handles" - ) + raise ValueError(f"Edge between {source.vertex_type} and {target.vertex_type} " f"has invalid handles") def __setstate__(self, state): self.source_id = state["source_id"] @@ -87,11 +75,7 @@ class Edge: # Both lists contain strings and sometimes a string contains the value we are # looking for e.g. comgin_out=["Chain"] and target_reqs=["LLMChain"] # so we need to check if any of the strings in source_types is in target_reqs - self.valid = any( - output in target_req - for output in self.source_types - for target_req in self.target_reqs - ) + self.valid = any(output in target_req for output in self.source_types for target_req in self.target_reqs) # Get what type of input the target node is expecting self.matched_type = next( @@ -102,10 +86,7 @@ class Edge: if no_matched_type: logger.debug(self.source_types) logger.debug(self.target_reqs) - raise ValueError( - f"Edge between {source.vertex_type} and {target.vertex_type} " - f"has no matched type" - ) + raise ValueError(f"Edge between {source.vertex_type} and {target.vertex_type} " f"has no matched type") def __repr__(self) -> str: return ( @@ -118,10 +99,7 @@ class Edge: def __eq__(self, __o: object) -> bool: # Create a better way to compare edges - return ( - self._source_handle == __o._source_handle - and self._target_handle == __o._target_handle - ) + return self._source_handle == __o._source_handle and self._target_handle == __o._target_handle class ContractEdge(Edge): @@ -178,9 +156,7 @@ class ContractEdge(Edge): return f"{self.source_id} -[{self.target_param}]-> {self.target_id}" -def log_transaction( - edge: ContractEdge, source: "Vertex", target: "Vertex", status, error=None -): +def log_transaction(edge: ContractEdge, source: "Vertex", target: "Vertex", status, error=None): try: monitor_service = get_monitor_service() clean_params = build_clean_params(target) diff --git a/src/backend/langflow/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py index ba2f9927a..a7d5a1d42 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -76,9 +76,7 @@ class Graph: """Returns the state of the graph.""" return self.state_manager.get_state(name, run_id=self._run_id) - def update_state( - self, name: str, record: Union[str, Record], caller: Optional[str] = None - ) -> None: + def update_state(self, name: str, record: Union[str, Record], caller: Optional[str] = None) -> None: """Updates the state of the graph.""" if caller: # If there is a caller which is a vertex_id, I want to activate @@ -110,12 +108,9 @@ class Graph: def reset_activated_vertices(self): self.activated_vertices = [] - def append_state( - self, name: str, record: Union[str, Record], caller: Optional[str] = None - ) -> None: + def append_state(self, name: str, record: Union[str, Record], caller: Optional[str] = None) -> None: """Appends the state of the graph.""" if caller: - self.activate_state_vertices(name, caller) self.state_manager.append_state(name, record, run_id=self._run_id) @@ -161,10 +156,7 @@ class Graph: """Runs the graph with the given inputs.""" for vertex_id in self._is_input_vertices: vertex = self.get_vertex(vertex_id) - if input_components and ( - vertex_id not in input_components - or vertex.display_name not in input_components - ): + if input_components and (vertex_id not in input_components or vertex.display_name not in input_components): continue if vertex is None: raise ValueError(f"Vertex {vertex_id} not found") @@ -187,11 +179,7 @@ class Graph: if vertex is None: raise ValueError(f"Vertex {vertex_id} not found") - if ( - not vertex.result - and not stream - and hasattr(vertex, "consume_async_generator") - ): + if not vertex.result and not stream and hasattr(vertex, "consume_async_generator"): await vertex.consume_async_generator() if vertex.display_name in outputs or vertex.id in outputs: vertex_outputs.append(vertex.result) @@ -269,9 +257,7 @@ class Graph: def build_parent_child_map(self): parent_child_map = defaultdict(list) for vertex in self.vertices: - parent_child_map[vertex.id] = [ - child.id for child in self.get_successors(vertex) - ] + parent_child_map[vertex.id] = [child.id for child in self.get_successors(vertex)] return parent_child_map def increment_run_count(self): @@ -456,11 +442,7 @@ class Graph: """Updates the edges of a vertex.""" # Vertex has edges, so we need to update the edges for edge in vertex.edges: - if ( - edge not in self.edges - and edge.source_id in self.vertex_map - and edge.target_id in self.vertex_map - ): + if edge not in self.edges and edge.source_id in self.vertex_map and edge.target_id in self.vertex_map: self.edges.append(edge) def _build_graph(self) -> None: @@ -485,11 +467,7 @@ class Graph: return self.vertices.remove(vertex) self.vertex_map.pop(vertex_id) - self.edges = [ - edge - for edge in self.edges - if edge.source_id != vertex_id and edge.target_id != vertex_id - ] + self.edges = [edge for edge in self.edges if edge.source_id != vertex_id and edge.target_id != vertex_id] def _build_vertex_params(self) -> None: """Identifies and handles the LLM vertex within the graph.""" @@ -510,9 +488,7 @@ class Graph: return for vertex in self.vertices: if not self._validate_vertex(vertex): - raise ValueError( - f"{vertex.display_name} is not connected to any other components" - ) + raise ValueError(f"{vertex.display_name} is not connected to any other components") def _validate_vertex(self, vertex: Vertex) -> bool: """Validates a vertex.""" @@ -574,9 +550,7 @@ class Graph: name=f"{vertex.display_name} Run {vertex_task_run_count.get(vertex_id, 0)}", ) tasks.append(task) - vertex_task_run_count[vertex_id] = ( - vertex_task_run_count.get(vertex_id, 0) + 1 - ) + vertex_task_run_count[vertex_id] = vertex_task_run_count.get(vertex_id, 0) + 1 logger.debug(f"Running layer {layer_index} with {len(tasks)} tasks") await self._execute_tasks(tasks) logger.debug("Graph processing complete") @@ -618,9 +592,7 @@ class Graph: def dfs(vertex): if state[vertex] == 1: # We have a cycle - raise ValueError( - "Graph contains a cycle, cannot perform topological sort" - ) + raise ValueError("Graph contains a cycle, cannot perform topological sort") if state[vertex] == 0: state[vertex] = 1 for edge in vertex.edges: @@ -644,10 +616,7 @@ class Graph: def get_predecessors(self, vertex): """Returns the predecessors of a vertex.""" - return [ - self.get_vertex(source_id) - for source_id in self.predecessor_map.get(vertex.id, []) - ] + return [self.get_vertex(source_id) for source_id in self.predecessor_map.get(vertex.id, [])] def get_all_successors(self, vertex, recursive=True, flat=True): # Recursively get the successors of the current vertex @@ -688,10 +657,7 @@ class Graph: def get_successors(self, vertex): """Returns the successors of a vertex.""" - return [ - self.get_vertex(target_id) - for target_id in self.successor_map.get(vertex.id, []) - ] + return [self.get_vertex(target_id) for target_id in self.successor_map.get(vertex.id, [])] def get_vertex_neighbors(self, vertex: Vertex) -> Dict[Vertex, int]: """Returns the neighbors of a vertex.""" @@ -737,9 +703,7 @@ class Graph: edges_added.add((source.id, target.id)) return edges - def _get_vertex_class( - self, node_type: str, node_base_type: str, node_id: str - ) -> Type[Vertex]: + def _get_vertex_class(self, node_type: str, node_base_type: str, node_id: str) -> Type[Vertex]: """Returns the node class based on the node type.""" # First we check for the node_base_type node_name = node_id.split("-")[0] @@ -772,18 +736,14 @@ class Graph: vertex_type: str = vertex_data["type"] # type: ignore vertex_base_type: str = vertex_data["node"]["template"]["_type"] # type: ignore - VertexClass = self._get_vertex_class( - vertex_type, vertex_base_type, vertex_data["id"] - ) + VertexClass = self._get_vertex_class(vertex_type, vertex_base_type, vertex_data["id"]) vertex_instance = VertexClass(vertex, graph=self) vertex_instance.set_top_level(self.top_level_vertices) vertices.append(vertex_instance) return vertices - def get_children_by_vertex_type( - self, vertex: Vertex, vertex_type: str - ) -> List[Vertex]: + def get_children_by_vertex_type(self, vertex: Vertex, vertex_type: str) -> List[Vertex]: """Returns the children of a vertex based on the vertex type.""" children = [] vertex_types = [vertex.data["type"]] @@ -795,9 +755,7 @@ class Graph: def __repr__(self): vertex_ids = [vertex.id for vertex in self.vertices] - edges_repr = "\n".join( - [f"{edge.source_id} --> {edge.target_id}" for edge in self.edges] - ) + edges_repr = "\n".join([f"{edge.source_id} --> {edge.target_id}" for edge in self.edges]) return f"Graph:\nNodes: {vertex_ids}\nConnections:\n{edges_repr}" def sort_up_to_vertex(self, vertex_id: str, is_start: bool = False) -> List[Vertex]: @@ -865,8 +823,7 @@ class Graph: vertex.id for vertex in vertices # if filter_graphs then only vertex.is_input will be considered - if self.in_degree_map[vertex.id] == 0 - and (not filter_graphs or vertex.is_input) + if self.in_degree_map[vertex.id] == 0 and (not filter_graphs or vertex.is_input) ) layers: List[List[str]] = [] visited = set(queue) @@ -940,9 +897,7 @@ class Graph: return refined_layers - def sort_chat_inputs_first( - self, vertices_layers: List[List[str]] - ) -> List[List[str]]: + def sort_chat_inputs_first(self, vertices_layers: List[List[str]]) -> List[List[str]]: chat_inputs_first = [] for layer in vertices_layers: for vertex_id in layer: @@ -983,9 +938,7 @@ class Graph: first_layer = vertices_layers[0] # save the only the rest self.vertices_layers = vertices_layers[1:] - self.vertices_to_run = { - vertex_id for vertex_id in chain.from_iterable(vertices_layers) - } + self.vertices_to_run = {vertex_id for vertex_id in chain.from_iterable(vertices_layers)} # Return just the first layer return first_layer @@ -996,15 +949,11 @@ class Graph: self.vertices_to_run.remove(vertex_id) return should_run - def sort_interface_components_first( - self, vertices_layers: List[List[str]] - ) -> List[List[str]]: + def sort_interface_components_first(self, vertices_layers: List[List[str]]) -> List[List[str]]: """Sorts the vertices in the graph so that vertices containing ChatInput or ChatOutput come first.""" def contains_interface_component(vertex): - return any( - component.value in vertex for component in InterfaceComponentTypes - ) + return any(component.value in vertex for component in InterfaceComponentTypes) # Sort each inner list so that vertices containing ChatInput or ChatOutput come first sorted_vertices = [ @@ -1016,22 +965,16 @@ class Graph: ] return sorted_vertices - def sort_by_avg_build_time( - self, vertices_layers: List[List[str]] - ) -> List[List[str]]: + def sort_by_avg_build_time(self, vertices_layers: List[List[str]]) -> List[List[str]]: """Sorts the vertices in the graph so that vertices with the lowest average build time come first.""" def sort_layer_by_avg_build_time(vertices_ids: List[str]) -> List[str]: """Sorts the vertices in the graph so that vertices with the lowest average build time come first.""" if len(vertices_ids) == 1: return vertices_ids - vertices_ids.sort( - key=lambda vertex_id: self.get_vertex(vertex_id).avg_build_time - ) + vertices_ids.sort(key=lambda vertex_id: self.get_vertex(vertex_id).avg_build_time) return vertices_ids - sorted_vertices = [ - sort_layer_by_avg_build_time(layer) for layer in vertices_layers - ] + sorted_vertices = [sort_layer_by_avg_build_time(layer) for layer in vertices_layers] return sorted_vertices diff --git a/src/backend/langflow/graph/graph/constants.py b/src/backend/langflow/graph/graph/constants.py index 2badbf0eb..0d0e69c77 100644 --- a/src/backend/langflow/graph/graph/constants.py +++ b/src/backend/langflow/graph/graph/constants.py @@ -47,10 +47,7 @@ class VertexTypesDict(LazyLoadDictBase): **{t: types.DocumentLoaderVertex for t in documentloader_creator.to_list()}, **{t: types.TextSplitterVertex for t in textsplitter_creator.to_list()}, **{t: types.OutputParserVertex for t in output_parser_creator.to_list()}, - **{ - t: types.CustomComponentVertex - for t in custom_component_creator.to_list() - }, + **{t: types.CustomComponentVertex for t in custom_component_creator.to_list()}, **{t: types.RetrieverVertex for t in retriever_creator.to_list()}, **{t: types.ChatVertex for t in CHAT_COMPONENTS}, **{t: types.RoutingVertex for t in ROUTING_COMPONENTS}, diff --git a/src/backend/langflow/graph/vertex/base.py b/src/backend/langflow/graph/vertex/base.py index ed46389e9..494eed388 100644 --- a/src/backend/langflow/graph/vertex/base.py +++ b/src/backend/langflow/graph/vertex/base.py @@ -59,13 +59,8 @@ class Vertex: self.updated_raw_params = False self.id: str = data["id"] self.is_state = False - self.is_input = any( - input_component_name in self.id for input_component_name in INPUT_COMPONENTS - ) - self.is_output = any( - output_component_name in self.id - for output_component_name in OUTPUT_COMPONENTS - ) + self.is_input = any(input_component_name in self.id for input_component_name in INPUT_COMPONENTS) + self.is_output = any(output_component_name in self.id for output_component_name in OUTPUT_COMPONENTS) self.has_session_id = None self._custom_component = None self.has_external_input = False @@ -106,17 +101,11 @@ class Vertex: def set_state(self, state: str): self.state = VertexStates[state] - if ( - self.state == VertexStates.INACTIVE - and self.graph.in_degree_map[self.id] < 2 - ): + if self.state == VertexStates.INACTIVE and self.graph.in_degree_map[self.id] < 2: # If the vertex is inactive and has only one in degree # it means that it is not a merge point in the graph self.graph.inactivated_vertices.add(self.id) - elif ( - self.state == VertexStates.ACTIVE - and self.id in self.graph.inactivated_vertices - ): + elif self.state == VertexStates.ACTIVE and self.id in self.graph.inactivated_vertices: self.graph.inactivated_vertices.remove(self.id) @property @@ -133,9 +122,7 @@ class Vertex: # If the Vertex.type is a power component # then we need to return the built object # instead of the result dict - if self.is_interface_component and not isinstance( - self._built_object, UnbuiltObject - ): + if self.is_interface_component and not isinstance(self._built_object, UnbuiltObject): result = self._built_object # if it is not a dict or a string and hasattr model_dump then # return the model_dump @@ -147,11 +134,7 @@ class Vertex: if isinstance(self._built_result, UnbuiltResult): return {} - return ( - self._built_result - if isinstance(self._built_result, dict) - else {"result": self._built_result} - ) + return self._built_result if isinstance(self._built_result, dict) else {"result": self._built_result} def set_artifacts(self) -> None: pass @@ -221,31 +204,19 @@ class Vertex: 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 - template_dicts = { - key: value - for key, value in self.data["node"]["template"].items() - if isinstance(value, dict) - } + template_dicts = {key: value for key, value in self.data["node"]["template"].items() if isinstance(value, dict)} self.has_session_id = "session_id" in template_dicts self.required_inputs = [ - template_dicts[key]["type"] - for key, value in template_dicts.items() - if value["required"] + template_dicts[key]["type"] for key, value in template_dicts.items() if value["required"] ] self.optional_inputs = [ - template_dicts[key]["type"] - for key, value in template_dicts.items() - if not value["required"] + template_dicts[key]["type"] for key, value in template_dicts.items() if not value["required"] ] # Add the template_dicts[key]["input_types"] to the optional_inputs self.optional_inputs.extend( - [ - input_type - for value in template_dicts.values() - for input_type in value.get("input_types", []) - ] + [input_type for value in template_dicts.values() for input_type in value.get("input_types", [])] ) template_dict = self.data["node"]["template"] @@ -292,11 +263,7 @@ class Vertex: self.updated_raw_params = False return - template_dict = { - key: value - for key, value in self.data["node"]["template"].items() - if isinstance(value, dict) - } + template_dict = {key: value for key, value in self.data["node"]["template"].items() if isinstance(value, dict)} params = {} for edge in self.edges: @@ -317,10 +284,7 @@ class Vertex: # we don't know the key of the dict but we need to set the value # to the vertex that is the source of the edge param_dict = template_dict[param_key]["value"] - params[param_key] = { - key: self.graph.get_vertex(edge.source_id) - for key in param_dict.keys() - } + params[param_key] = {key: self.graph.get_vertex(edge.source_id) for key in param_dict.keys()} else: params[param_key] = self.graph.get_vertex(edge.source_id) @@ -356,11 +320,7 @@ class Vertex: # list of dicts, so we need to convert it to a dict # before passing it to the build method if isinstance(val, list): - params[key] = { - k: v - for item in value.get("value", []) - for k, v in item.items() - } + params[key] = {k: v for item in value.get("value", []) for k, v in item.items()} elif isinstance(val, dict): params[key] = val elif value.get("type") == "int" and val is not None: @@ -485,9 +445,7 @@ class Vertex: if isinstance(self._built_object, str): self._built_result = self._built_object - result = await generate_result( - self._built_object, inputs, self.has_external_output, session_id - ) + result = await generate_result(self._built_object, inputs, self.has_external_output, session_id) self._built_result = result async def _build_each_node_in_params_dict(self, user_id=None): @@ -507,9 +465,7 @@ class Vertex: elif key not in self.params or self.updated_raw_params: self.params[key] = value - async def _build_dict_of_nodes_and_update_params( - self, key, nodes: Dict[str, "Vertex"], user_id=None - ): + async def _build_dict_of_nodes_and_update_params(self, key, nodes: Dict[str, "Vertex"], user_id=None): """ Iterates over a dictionary of nodes, builds each and updates the params dictionary. """ @@ -529,9 +485,7 @@ class Vertex: """ return all(self._is_node(node) for node in value) - async def get_result( - self, requester: Optional["Vertex"] = None, user_id=None, timeout=None - ) -> Any: + async def get_result(self, requester: Optional["Vertex"] = None, user_id=None, timeout=None) -> Any: # PLEASE REVIEW THIS IF STATEMENT # Check if the Vertex was built already if self._built: @@ -565,9 +519,7 @@ class Vertex: self._extend_params_list_with_result(key, result) self.params[key] = result - async def _build_list_of_nodes_and_update_params( - self, key, nodes: List["Vertex"], user_id=None - ): + async def _build_list_of_nodes_and_update_params(self, key, nodes: List["Vertex"], user_id=None): """ Iterates over a list of nodes, builds each and updates the params dictionary. """ @@ -634,9 +586,7 @@ class Vertex: except Exception as exc: logger.exception(exc) - raise ValueError( - f"Error building node {self.display_name}: {str(exc)}" - ) from exc + raise ValueError(f"Error building node {self.display_name}: {str(exc)}") from exc def _update_built_object_and_artifacts(self, result): """ @@ -664,9 +614,7 @@ class Vertex: logger.warning(message) elif isinstance(self._built_object, (Iterator, AsyncIterator)): if self.display_name in ["Text Output"]: - raise ValueError( - f"You are trying to stream to a {self.display_name}. Try using a Chat Output instead." - ) + raise ValueError(f"You are trying to stream to a {self.display_name}. Try using a Chat Output instead.") def _reset(self, params_update: Optional[Dict[str, Any]] = None): self._built = False @@ -728,24 +676,16 @@ class Vertex: return self._built_object # Get the requester edge - requester_edge = next( - (edge for edge in self.edges if edge.target_id == requester.id), None - ) + requester_edge = next((edge for edge in self.edges if edge.target_id == requester.id), None) # Return the result of the requester edge - return ( - None - if requester_edge is None - else await requester_edge.get_result(source=self, target=requester) - ) + return None if requester_edge is None else await requester_edge.get_result(source=self, target=requester) def add_edge(self, edge: "ContractEdge") -> None: if edge not in self.edges: self.edges.append(edge) def __repr__(self) -> str: - return ( - f"Vertex(display_name={self.display_name}, id={self.id}, data={self.data})" - ) + return f"Vertex(display_name={self.display_name}, id={self.id}, data={self.data})" def __eq__(self, __o: object) -> bool: try: @@ -766,8 +706,4 @@ class Vertex: def _built_object_repr(self): # Add a message with an emoji, stars for sucess, - return ( - "Built sucessfully ✨" - if self._built_object is not None - else "Failed to build 😵‍💫" - ) + return "Built sucessfully ✨" if self._built_object is not None else "Failed to build 😵‍💫" diff --git a/src/backend/langflow/graph/vertex/types.py b/src/backend/langflow/graph/vertex/types.py index 1bfd7cacf..37f1f1657 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 @@ -124,11 +123,9 @@ class DocumentLoaderVertex(Vertex): # show how many documents are in the list? if not isinstance(self._built_object, UnbuiltObject): - avg_length = sum( - len(record.text) - for record in self._built_object - if hasattr(record, "text") - ) / len(self._built_object) + avg_length = sum(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)} records) \nAvg. Record Length (characters): {int(avg_length)} Records: {self._built_object[:3]}...""" @@ -201,9 +198,7 @@ class TextSplitterVertex(Vertex): # show how many documents are in the list? if not isinstance(self._built_object, UnbuiltObject): - avg_length = sum(len(doc.page_content) for doc in self._built_object) / len( - self._built_object - ) + avg_length = sum(len(doc.page_content) for doc in self._built_object) / len(self._built_object) return f"""{self.vertex_type}({len(self._built_object)} documents) \nAvg. Document Length (characters): {int(avg_length)} \nDocuments: {self._built_object[:3]}...""" @@ -250,27 +245,18 @@ class PromptVertex(Vertex): user_id = kwargs.get("user_id", None) tools = kwargs.get("tools", []) if not self._built or force: - if ( - "input_variables" not in self.params - or self.params["input_variables"] is None - ): + if "input_variables" not in self.params or self.params["input_variables"] is None: self.params["input_variables"] = [] # Check if it is a ZeroShotPrompt and needs a tool if "ShotPrompt" in self.vertex_type: - tools = ( - [tool_node.build(user_id=user_id) for tool_node in tools] - if tools is not None - else [] - ) + tools = [tool_node.build(user_id=user_id) for tool_node in tools] if tools is not None else [] # flatten the list of tools if it is a list of lists # first check if it is a list if tools and isinstance(tools, list) and isinstance(tools[0], list): tools = flatten_list(tools) self.params["tools"] = tools prompt_params = [ - key - for key, value in self.params.items() - if isinstance(value, str) and key != "format_instructions" + key for key, value in self.params.items() if isinstance(value, str) and key != "format_instructions" ] else: prompt_params = ["template"] @@ -280,20 +266,14 @@ class PromptVertex(Vertex): prompt_text = self.params[param] variables = extract_input_variables_from_prompt(prompt_text) self.params["input_variables"].extend(variables) - self.params["input_variables"] = list( - set(self.params["input_variables"]) - ) + self.params["input_variables"] = list(set(self.params["input_variables"])) elif isinstance(self.params, dict): self.params.pop("input_variables", None) await self._build(user_id=user_id) def _built_object_repr(self): - if ( - not self.artifacts - or self._built_object is None - or not hasattr(self._built_object, "format") - ): + if not self.artifacts or self._built_object is None or not hasattr(self._built_object, "format"): return super()._built_object_repr() elif isinstance(self._built_object, UnbuiltObject): return super()._built_object_repr() @@ -305,9 +285,7 @@ class PromptVertex(Vertex): # so the prompt format doesn't break artifacts.pop("handle_keys", None) try: - if not hasattr(self._built_object, "template") and hasattr( - self._built_object, "prompt" - ): + if not hasattr(self._built_object, "template") and hasattr(self._built_object, "prompt"): template = self._built_object.prompt.template else: template = self._built_object.template @@ -315,11 +293,7 @@ class PromptVertex(Vertex): if value: replace_key = "{" + key + "}" template = template.replace(replace_key, value) - return ( - template - if isinstance(template, str) - else f"{self.vertex_type}({template})" - ) + return template if isinstance(template, str) else f"{self.vertex_type}({template})" except KeyError: return str(self._built_object) diff --git a/src/backend/langflow/helpers/record.py b/src/backend/langflow/helpers/record.py index 9e1f2eb34..9ae3764bd 100644 --- a/src/backend/langflow/helpers/record.py +++ b/src/backend/langflow/helpers/record.py @@ -30,8 +30,5 @@ def records_to_text(template: str, records: list[Record]) -> list[str]: records = [records] # Check if there are any format strings in the template - formated_records = [ - template.format(text=record.text, data=record.data, **record.data) - for record in records - ] + formated_records = [template.format(text=record.text, data=record.data, **record.data) for record in records] return "\n".join(formated_records) diff --git a/src/backend/langflow/initial_setup/setup.py b/src/backend/langflow/initial_setup/setup.py index d01a9a788..476f43410 100644 --- a/src/backend/langflow/initial_setup/setup.py +++ b/src/backend/langflow/initial_setup/setup.py @@ -89,11 +89,6 @@ def create_new_project( ) db_flow = Flow.model_validate(new_project, from_attributes=True) session.add(db_flow) - flows = session.exec( - select(Flow).where( - Flow.name == project_name, - ) - ).all() def get_all_flows_similar_to_project(session, project_name): @@ -117,7 +112,6 @@ def delete_start_projects(session): def create_or_update_starter_projects(): - with session_scope() as session: starter_projects = load_starter_projects() delete_start_projects(session) @@ -132,9 +126,7 @@ def create_or_update_starter_projects(): project_icon_bg_color, ) = get_project_data(project) if project_name and project_data: - for existing_project in get_all_flows_similar_to_project( - session, project_name - ): + for existing_project in get_all_flows_similar_to_project(session, project_name): session.delete(existing_project) create_new_project( diff --git a/src/backend/langflow/interface/custom/code_parser/code_parser.py b/src/backend/langflow/interface/custom/code_parser/code_parser.py index 846edc716..eaff42fab 100644 --- a/src/backend/langflow/interface/custom/code_parser/code_parser.py +++ b/src/backend/langflow/interface/custom/code_parser/code_parser.py @@ -95,9 +95,7 @@ class CodeParser: elif isinstance(node, ast.ImportFrom): for alias in node.names: if alias.asname: - self.data["imports"].append( - (node.module, f"{alias.name} as {alias.asname}") - ) + self.data["imports"].append((node.module, f"{alias.name} as {alias.asname}")) else: self.data["imports"].append((node.module, alias.name)) @@ -146,9 +144,7 @@ class CodeParser: return_type = None if node.returns: return_type_str = ast.unparse(node.returns) - eval_env = self.construct_eval_env( - return_type_str, tuple(self.data["imports"]) - ) + eval_env = self.construct_eval_env(return_type_str, tuple(self.data["imports"])) try: return_type = eval(return_type_str, eval_env) @@ -190,22 +186,14 @@ class CodeParser: num_defaults = len(node.args.defaults) num_missing_defaults = num_args - num_defaults missing_defaults = [None] * num_missing_defaults - default_values = [ - ast.unparse(default).strip("'") if default else None - for default in node.args.defaults - ] + default_values = [ast.unparse(default).strip("'") if default else None for default in node.args.defaults] # Now check all default values to see if there # are any "None" values in the middle - default_values = [ - None if value == "None" else value for value in default_values - ] + default_values = [None if value == "None" else value for value in default_values] defaults = missing_defaults + default_values - args = [ - self.parse_arg(arg, default) - for arg, default in zip(node.args.args, defaults) - ] + args = [self.parse_arg(arg, default) for arg, default in zip(node.args.args, defaults)] return args def parse_varargs(self, node: ast.FunctionDef) -> List[Dict[str, Any]]: @@ -223,17 +211,11 @@ class CodeParser: """ Parses the keyword-only arguments of a function or method node. """ - kw_defaults = [None] * ( - len(node.args.kwonlyargs) - len(node.args.kw_defaults) - ) + [ - ast.unparse(default) if default else None - for default in node.args.kw_defaults + kw_defaults = [None] * (len(node.args.kwonlyargs) - len(node.args.kw_defaults)) + [ + ast.unparse(default) if default else None for default in node.args.kw_defaults ] - args = [ - self.parse_arg(arg, default) - for arg, default in zip(node.args.kwonlyargs, kw_defaults) - ] + args = [self.parse_arg(arg, default) for arg, default in zip(node.args.kwonlyargs, kw_defaults)] return args def parse_kwargs(self, node: ast.FunctionDef) -> List[Dict[str, Any]]: @@ -337,9 +319,7 @@ class CodeParser: Extracts global variables from the code. """ global_var = { - "targets": [ - t.id if hasattr(t, "id") else ast.dump(t) for t in node.targets - ], + "targets": [t.id if hasattr(t, "id") else ast.dump(t) for t in node.targets], "value": ast.unparse(node.value), } self.data["global_vars"].append(global_var) diff --git a/src/backend/langflow/interface/custom/custom_component/component.py b/src/backend/langflow/interface/custom/custom_component/component.py index a889fa7b9..ce40b0f74 100644 --- a/src/backend/langflow/interface/custom/custom_component/component.py +++ b/src/backend/langflow/interface/custom/custom_component/component.py @@ -21,9 +21,7 @@ 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" 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 35bdd2c5f..f0c3bfa80 100644 --- a/src/backend/langflow/interface/custom/custom_component/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component/custom_component.py @@ -77,17 +77,13 @@ class CustomComponent(Component): def update_state(self, name: str, value: Any): try: - self.vertex.graph.update_state( - name=name, record=value, caller=self.vertex.id - ) + self.vertex.graph.update_state(name=name, record=value, caller=self.vertex.id) except Exception as e: raise ValueError(f"Error updating state: {e}") def append_state(self, name: str, value: Any): try: - self.vertex.graph.append_state( - name=name, record=value, caller=self.vertex.id - ) + self.vertex.graph.append_state(name=name, record=value, caller=self.vertex.id) except Exception as e: raise ValueError(f"Error appending state: {e}") @@ -138,9 +134,7 @@ class CustomComponent(Component): def build_config(self): return self.field_config - def update_build_config( - self, build_config: dict, field_name: str, field_value: Any - ): + def update_build_config(self, build_config: dict, field_name: str, field_value: Any): build_config[field_name] = field_value return build_config @@ -148,9 +142,7 @@ class CustomComponent(Component): def tree(self): return self.get_code_tree(self.code or "") - def to_records( - self, data: Any, keys: Optional[List[str]] = None, silent_errors: bool = False - ) -> List[Record]: + def to_records(self, data: Any, keys: Optional[List[str]] = None, silent_errors: bool = False) -> List[Record]: """ Converts input data into a list of Record objects. @@ -199,9 +191,7 @@ class CustomComponent(Component): return records - def create_references_from_records( - self, records: List[Record], include_data: bool = False - ) -> str: + def create_references_from_records(self, records: List[Record], include_data: bool = False) -> str: """ Create references from a list of records. @@ -240,20 +230,14 @@ class CustomComponent(Component): if not self.code: return {} - component_classes = [ - cls - for cls in self.tree["classes"] - if self.code_class_base_inheritance in cls["bases"] - ] + component_classes = [cls for cls in self.tree["classes"] if self.code_class_base_inheritance in cls["bases"]] if not component_classes: return {} # Assume the first Component class is the one we're interested in component_class = component_classes[0] build_methods = [ - method - for method in component_class["methods"] - if method["name"] == self.function_entrypoint_name + method for method in component_class["methods"] if method["name"] == self.function_entrypoint_name ] return build_methods[0] if build_methods else {} @@ -310,9 +294,7 @@ class CustomComponent(Component): # Retrieve and decrypt the credential by name for the current user db_service = get_db_service() with session_getter(db_service) as session: - return credential_service.get_credential( - user_id=self._user_id or "", name=name, session=session - ) + return credential_service.get_credential(user_id=self._user_id or "", name=name, session=session) return get_credential @@ -322,9 +304,7 @@ class CustomComponent(Component): credential_service = get_credential_service() db_service = get_db_service() with session_getter(db_service) as session: - return credential_service.list_credentials( - user_id=self._user_id, session=session - ) + return credential_service.list_credentials(user_id=self._user_id, session=session) def index(self, value: int = 0): """Returns a function that returns the value at the given index in the iterable.""" @@ -363,11 +343,7 @@ class CustomComponent(Component): if not self._flows_records: self.list_flows() if not flow_id and self._flows_records: - flow_ids = [ - flow.data["id"] - for flow in self._flows_records - if flow.data["name"] == flow_name - ] + flow_ids = [flow.data["id"] for flow in self._flows_records if flow.data["name"] == flow_name] if not flow_ids: raise ValueError(f"Flow {flow_name} not found") elif len(flow_ids) > 1: @@ -389,9 +365,7 @@ class CustomComponent(Component): db_service = get_db_service() with get_session(db_service) as session: flows = session.exec( - select(Flow) - .where(Flow.user_id == self._user_id) - .where(Flow.is_component == False) + select(Flow).where(Flow.user_id == self._user_id).where(Flow.is_component == False) # noqa ).all() flows_records = [flow.to_record() for flow in flows] diff --git a/src/backend/langflow/interface/custom/directory_reader/directory_reader.py b/src/backend/langflow/interface/custom/directory_reader/directory_reader.py index 448e3c485..5acc15131 100644 --- a/src/backend/langflow/interface/custom/directory_reader/directory_reader.py +++ b/src/backend/langflow/interface/custom/directory_reader/directory_reader.py @@ -80,13 +80,9 @@ class DirectoryReader: except Exception as e: logger.error(f"Error while loading component: {e}") continue - items.append( - {"name": menu["name"], "path": menu["path"], "components": components} - ) + items.append({"name": menu["name"], "path": menu["path"], "components": components}) filtered = [menu for menu in items if menu["components"]] - logger.debug( - f'Filtered components {"with errors" if with_errors else ""}: {len(filtered)}' - ) + logger.debug(f'Filtered components {"with errors" if with_errors else ""}: {len(filtered)}') return {"menu": filtered} def validate_code(self, file_content): @@ -119,9 +115,7 @@ class DirectoryReader: Walk through the directory path and return a list of all .py files. """ if not (safe_path := self.get_safe_path()): - raise CustomComponentPathValueError( - f"The path needs to start with '{self.base_path}'." - ) + raise CustomComponentPathValueError(f"The path needs to start with '{self.base_path}'.") file_list = [] safe_path_obj = Path(safe_path) @@ -131,11 +125,7 @@ class DirectoryReader: # any folders below [folder] will be ignored # basically the parent folder of the file should be a # folder in the safe_path - if ( - file_path.is_file() - and file_path.parent.parent == safe_path_obj - and not file_path.name.startswith("__") - ): + if file_path.is_file() and file_path.parent.parent == safe_path_obj and not file_path.name.startswith("__"): file_list.append(str(file_path)) return file_list @@ -173,9 +163,7 @@ class DirectoryReader: for node in ast.walk(module): if isinstance(node, ast.FunctionDef): for arg in node.args.args: - if self._is_type_hint_in_arg_annotation( - arg.annotation, type_hint_name - ): + if self._is_type_hint_in_arg_annotation(arg.annotation, type_hint_name): return True except SyntaxError: # Returns False if the code is not valid Python @@ -193,16 +181,14 @@ class DirectoryReader: and annotation.value.id == type_hint_name ) - def is_type_hint_used_but_not_imported( - self, type_hint_name: str, code: str - ) -> bool: + def is_type_hint_used_but_not_imported(self, type_hint_name: str, code: str) -> bool: """ Check if a type hint is used but not imported in the given code. """ try: - return self._is_type_hint_used_in_args( + return self._is_type_hint_used_in_args(type_hint_name, code) and not self._is_type_hint_imported( type_hint_name, code - ) and not self._is_type_hint_imported(type_hint_name, code) + ) except SyntaxError: # Returns True if there's something wrong with the code # TODO : Find a better way to handle this @@ -223,9 +209,9 @@ class DirectoryReader: return False, "Syntax error" elif not self.validate_build(file_content): return False, "Missing build function" - elif self._is_type_hint_used_in_args( + elif self._is_type_hint_used_in_args("Optional", file_content) and not self._is_type_hint_imported( "Optional", file_content - ) and not self._is_type_hint_imported("Optional", file_content): + ): return ( False, "Type hint 'Optional' is used but not imported in the code.", @@ -241,18 +227,14 @@ class DirectoryReader: from the .py files in the directory. """ response = {"menu": []} - logger.debug( - "-------------------- Building component menu list --------------------" - ) + logger.debug("-------------------- Building component menu list --------------------") for file_path in file_paths: menu_name = os.path.basename(os.path.dirname(file_path)) filename = os.path.basename(file_path) validation_result, result_content = self.process_file(file_path) if not validation_result: - logger.error( - f"Error while processing file {file_path}: {result_content}" - ) + logger.error(f"Error while processing file {file_path}: {result_content}") menu_result = self.find_menu(response, menu_name) or { "name": menu_name, @@ -265,9 +247,7 @@ class DirectoryReader: # first check if it's already CamelCase if "_" in component_name: - component_name_camelcase = " ".join( - word.title() for word in component_name.split("_") - ) + component_name_camelcase = " ".join(word.title() for word in component_name.split("_")) else: component_name_camelcase = component_name @@ -275,9 +255,7 @@ class DirectoryReader: try: output_types = self.get_output_types_from_code(result_content) except Exception as exc: - logger.exception( - f"Error while getting output types from code: {str(exc)}" - ) + logger.exception(f"Error while getting output types from code: {str(exc)}") output_types = [component_name_camelcase] else: output_types = [component_name_camelcase] @@ -293,9 +271,7 @@ class DirectoryReader: if menu_result not in response["menu"]: response["menu"].append(menu_result) - logger.debug( - "-------------------- Component menu list built --------------------" - ) + logger.debug("-------------------- Component menu list built --------------------") return response @staticmethod diff --git a/src/backend/langflow/interface/custom/utils.py b/src/backend/langflow/interface/custom/utils.py index f25360c6a..389f24ffd 100644 --- a/src/backend/langflow/interface/custom/utils.py +++ b/src/backend/langflow/interface/custom/utils.py @@ -32,18 +32,14 @@ class UpdateBuildConfigError(Exception): pass -def add_output_types( - frontend_node: CustomComponentFrontendNode, return_types: List[str] -): +def add_output_types(frontend_node: CustomComponentFrontendNode, return_types: List[str]): """Add output types to the frontend node""" for return_type in return_types: if return_type is None: raise HTTPException( status_code=400, detail={ - "error": ( - "Invalid return type. Please check your code and try again." - ), + "error": ("Invalid return type. Please check your code and try again."), "traceback": traceback.format_exc(), }, ) @@ -75,18 +71,14 @@ def reorder_fields(frontend_node: CustomComponentFrontendNode, field_order: List frontend_node.field_order = field_order -def add_base_classes( - frontend_node: CustomComponentFrontendNode, return_types: List[str] -): +def add_base_classes(frontend_node: CustomComponentFrontendNode, return_types: List[str]): """Add base classes to the frontend node""" for return_type_instance in return_types: if return_type_instance is None: raise HTTPException( status_code=400, detail={ - "error": ( - "Invalid return type. Please check your code and try again." - ), + "error": ("Invalid return type. Please check your code and try again."), "traceback": traceback.format_exc(), }, ) @@ -163,14 +155,10 @@ def add_new_custom_field( # If options is a list, then it's a dropdown # If options is None, then it's a list of strings is_list = isinstance(field_config.get("options"), list) - field_config["is_list"] = ( - is_list or field_config.get("is_list", False) or field_contains_list - ) + field_config["is_list"] = is_list or field_config.get("is_list", False) or field_contains_list if "name" in field_config: - warnings.warn( - "The 'name' key in field_config is used to build the object and can't be changed." - ) + warnings.warn("The 'name' key in field_config is used to build the object and can't be changed.") required = field_config.pop("required", field_required) placeholder = field_config.pop("placeholder", "") @@ -209,9 +197,7 @@ def add_extra_fields(frontend_node, field_config, function_args): ]: continue - field_name, field_type, field_value, field_required = get_field_properties( - extra_field - ) + field_name, field_type, field_value, field_required = get_field_properties(extra_field) config = _field_config.pop(field_name, {}) frontend_node = add_new_custom_field( frontend_node, @@ -221,17 +207,13 @@ def add_extra_fields(frontend_node, field_config, function_args): field_required, config, ) - if "kwargs" in function_args_names and not all( - key in function_args_names for key in field_config.keys() - ): + if "kwargs" in function_args_names and not all(key in function_args_names for key in field_config.keys()): for field_name, field_config in _field_config.copy().items(): if "name" not in field_config or field_name == "code": continue config = _field_config.get(field_name, {}) config = config.model_dump() if isinstance(config, BaseModel) else config - field_name, field_type, field_value, field_required = get_field_properties( - extra_field=config - ) + field_name, field_type, field_value, field_required = get_field_properties(extra_field=config) frontend_node = add_new_custom_field( frontend_node, field_name, @@ -269,9 +251,7 @@ def run_build_config( raise HTTPException( status_code=400, detail={ - "error": ( - "Invalid type convertion. Please check your code and try again." - ), + "error": ("Invalid type convertion. Please check your code and try again."), "traceback": traceback.format_exc(), }, ) from exc @@ -383,16 +363,10 @@ def build_custom_component_template( add_extra_fields(frontend_node, field_config, entrypoint_args) - frontend_node = add_code_field( - frontend_node, custom_component.code, field_config.get("code", {}) - ) + frontend_node = add_code_field(frontend_node, custom_component.code, field_config.get("code", {})) - add_base_classes( - frontend_node, custom_component.get_function_entrypoint_return_type - ) - add_output_types( - frontend_node, custom_component.get_function_entrypoint_return_type - ) + add_base_classes(frontend_node, custom_component.get_function_entrypoint_return_type) + add_output_types(frontend_node, custom_component.get_function_entrypoint_return_type) reorder_fields(frontend_node, custom_instance._get_field_order()) @@ -403,9 +377,7 @@ def build_custom_component_template( raise HTTPException( status_code=400, detail={ - "error": ( - "Invalid type convertion. Please check your code and try again." - ), + "error": ("Invalid type convertion. Please check your code and try again."), "traceback": traceback.format_exc(), }, ) from exc @@ -431,9 +403,7 @@ def build_custom_components(settings_service): if not settings_service.settings.COMPONENTS_PATH: return {} - logger.info( - f"Building custom components from {settings_service.settings.COMPONENTS_PATH}" - ) + logger.info(f"Building custom components from {settings_service.settings.COMPONENTS_PATH}") custom_components_from_file = {} processed_paths = set() for path in settings_service.settings.COMPONENTS_PATH: @@ -444,9 +414,7 @@ def build_custom_components(settings_service): custom_component_dict = build_custom_component_list_from_path(path_str) if custom_component_dict: category = next(iter(custom_component_dict)) - logger.info( - f"Loading {len(custom_component_dict[category])} component(s) from category {category}" - ) + logger.info(f"Loading {len(custom_component_dict[category])} component(s) from category {category}") custom_components_from_file = merge_nested_dicts_with_renaming( custom_components_from_file, custom_component_dict ) @@ -467,14 +435,10 @@ def update_field_dict( if "refresh" in field_dict: if call: try: - custom_component_instance.update_build_config( - build_config, update_field, update_field_value - ) + custom_component_instance.update_build_config(build_config, update_field, update_field_value) except Exception as exc: logger.error(f"Error while running update_build_config: {str(exc)}") - raise UpdateBuildConfigError( - f"Error while running update_build_config: {str(exc)}" - ) from exc + raise UpdateBuildConfigError(f"Error while running update_build_config: {str(exc)}") from exc field_dict["refresh"] = True # Let's check if "range_spec" is a RangeSpec object diff --git a/src/backend/langflow/interface/initialize/loading.py b/src/backend/langflow/interface/initialize/loading.py index 7bd8e34f4..fd6f27db6 100644 --- a/src/backend/langflow/interface/initialize/loading.py +++ b/src/backend/langflow/interface/initialize/loading.py @@ -144,13 +144,9 @@ 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, @@ -226,9 +222,7 @@ 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." @@ -271,9 +265,7 @@ 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) @@ -329,11 +321,7 @@ 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) @@ -345,9 +333,7 @@ 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: @@ -362,9 +348,7 @@ 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 @@ -373,17 +357,13 @@ def instantiate_documentloader( # 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): @@ -416,16 +396,12 @@ 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 @@ -452,8 +428,7 @@ 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 @@ -467,9 +442,7 @@ 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 @@ -497,10 +470,7 @@ def build_prompt_template(prompt, tools): format_instructions = prompt["node"]["template"]["format_instructions"]["value"] tool_strings = "\n".join( - [ - f"{tool['data']['node']['name']}: {tool['data']['node']['description']}" - for tool in tools - ] + [f"{tool['data']['node']['name']}: {tool['data']['node']['description']}" for tool in tools] ) tool_names = ", ".join([tool["data"]["node"]["name"] for tool in tools]) format_instructions = format_instructions.format(tool_names=tool_names) diff --git a/src/backend/langflow/interface/types.py b/src/backend/langflow/interface/types.py index 9a1375869..e0e7c6c59 100644 --- a/src/backend/langflow/interface/types.py +++ b/src/backend/langflow/interface/types.py @@ -66,6 +66,4 @@ def get_all_types_dict(settings_service): """Get all types dictionary combining native and custom components.""" native_components = build_langchain_types_dict() custom_components_from_file = build_custom_components(settings_service) - return merge_nested_dicts_with_renaming( - native_components, custom_components_from_file - ) + return merge_nested_dicts_with_renaming(native_components, custom_components_from_file) diff --git a/src/backend/langflow/interface/utils.py b/src/backend/langflow/interface/utils.py index 30c55f1ef..8e7f476f5 100644 --- a/src/backend/langflow/interface/utils.py +++ b/src/backend/langflow/interface/utils.py @@ -43,9 +43,7 @@ def try_setting_streaming_options(langchain_object): llm = None if hasattr(langchain_object, "llm"): llm = langchain_object.llm - elif hasattr(langchain_object, "llm_chain") and hasattr( - langchain_object.llm_chain, "llm" - ): + elif hasattr(langchain_object, "llm_chain") and hasattr(langchain_object.llm_chain, "llm"): llm = langchain_object.llm_chain.llm if isinstance(llm, BaseLanguageModel): @@ -71,9 +69,7 @@ def extract_input_variables_from_prompt(prompt: str) -> list[str]: # Extract the variable name from either the single or double brace match if match.group(1): # Match found in double braces - variable_name = ( - "{{" + match.group(1) + "}}" - ) # Re-add single braces for JSON strings + variable_name = "{{" + match.group(1) + "}}" # Re-add single braces for JSON strings else: # Match found in single braces variable_name = match.group(2) if variable_name is not None: @@ -109,9 +105,7 @@ def set_langchain_cache(settings): if cache_type := os.getenv("LANGFLOW_LANGCHAIN_CACHE"): try: - cache_class = import_class( - f"langchain.cache.{cache_type or settings.LANGCHAIN_CACHE}" - ) + cache_class = import_class(f"langchain.cache.{cache_type or settings.LANGCHAIN_CACHE}") logger.debug(f"Setting up LLM caching with {cache_class.__name__}") set_llm_cache(cache_class()) diff --git a/src/backend/langflow/main.py b/src/backend/langflow/main.py index 7c34d9b08..76724521f 100644 --- a/src/backend/langflow/main.py +++ b/src/backend/langflow/main.py @@ -20,9 +20,7 @@ from langflow.utils.logger import configure def get_lifespan(fix_migration=False, socketio_server=None): @asynccontextmanager async def lifespan(app: FastAPI): - initialize_services( - fix_migration=fix_migration, socketio_server=socketio_server - ) + initialize_services(fix_migration=fix_migration, socketio_server=socketio_server) setup_llm_caching() LangfuseInstance.update() create_or_update_starter_projects() @@ -36,9 +34,7 @@ def create_app(): """Create the FastAPI app and include the router.""" configure() - socketio_server = socketio.AsyncServer( - async_mode="asgi", cors_allowed_origins="*", logger=True - ) + socketio_server = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*", logger=True) lifespan = get_lifespan(socketio_server=socketio_server) app = FastAPI(lifespan=lifespan) origins = ["*"] @@ -105,9 +101,7 @@ def get_static_files_dir(): return frontend_path / "frontend" -def setup_app( - static_files_dir: Optional[Path] = None, backend_only: bool = False -) -> FastAPI: +def setup_app(static_files_dir: Optional[Path] = None, backend_only: bool = False) -> FastAPI: """Setup the FastAPI app.""" # get the directory of the current file if not static_files_dir: diff --git a/src/backend/langflow/processing/process.py b/src/backend/langflow/processing/process.py index 8ef4ae50f..f034656be 100644 --- a/src/backend/langflow/processing/process.py +++ b/src/backend/langflow/processing/process.py @@ -126,9 +126,7 @@ async def process_runnable(runnable: Runnable, inputs: Union[dict, List[dict]]): elif isinstance(inputs, dict) and hasattr(runnable, "ainvoke"): result = await runnable.ainvoke(inputs) else: - raise ValueError( - f"Runnable {runnable} does not support inputs of type {type(inputs)}" - ) + raise ValueError(f"Runnable {runnable} does not support inputs of type {type(inputs)}") # Check if the result is a list of AIMessages if isinstance(result, list) and all(isinstance(r, AIMessage) for r in result): result = [r.content for r in result] @@ -137,9 +135,7 @@ async def process_runnable(runnable: Runnable, inputs: Union[dict, List[dict]]): return result -async def process_inputs_dict( - built_object: Union[Chain, VectorStore, Runnable], inputs: dict -): +async def process_inputs_dict(built_object: Union[Chain, VectorStore, Runnable], inputs: dict): if isinstance(built_object, Chain): if inputs is None: raise ValueError("Inputs must be provided for a Chain") @@ -174,9 +170,7 @@ async def process_inputs_list(built_object: Runnable, inputs: List[dict]): return await process_runnable(built_object, inputs) -async def generate_result( - built_object: Union[Chain, VectorStore, Runnable], inputs: Union[dict, List[dict]] -): +async def generate_result(built_object: Union[Chain, VectorStore, Runnable], inputs: Union[dict, List[dict]]): if isinstance(inputs, dict): result = await process_inputs_dict(built_object, inputs) elif isinstance(inputs, List) and isinstance(built_object, Runnable): @@ -215,9 +209,7 @@ async def run_graph( else: graph_data = graph._graph_data if not session_id and session_service is not None: - session_id = session_service.generate_key( - session_id=flow_id, data_graph=graph_data - ) + session_id = session_service.generate_key(session_id=flow_id, data_graph=graph_data) if inputs is None: inputs = {} @@ -232,18 +224,14 @@ async def run_graph( return outputs, session_id -def validate_input( - graph_data: Dict[str, Any], tweaks: Dict[str, Dict[str, Any]] -) -> List[Dict[str, Any]]: +def validate_input(graph_data: Dict[str, Any], tweaks: Dict[str, Dict[str, Any]]) -> List[Dict[str, Any]]: if not isinstance(graph_data, dict) or not isinstance(tweaks, dict): raise ValueError("graph_data and tweaks should be dictionaries") nodes = graph_data.get("data", {}).get("nodes") or graph_data.get("nodes") if not isinstance(nodes, list): - raise ValueError( - "graph_data should contain a list of nodes under 'data' key or directly under 'nodes' key" - ) + raise ValueError("graph_data should contain a list of nodes under 'data' key or directly under 'nodes' key") return nodes @@ -252,9 +240,7 @@ def apply_tweaks(node: Dict[str, Any], node_tweaks: Dict[str, Any]) -> None: template_data = node.get("data", {}).get("node", {}).get("template") if not isinstance(template_data, dict): - logger.warning( - f"Template data for node {node.get('id')} should be a dictionary" - ) + logger.warning(f"Template data for node {node.get('id')} should be a dictionary") return for tweak_name, tweak_value in node_tweaks.items(): @@ -269,9 +255,7 @@ def apply_tweaks_on_vertex(vertex: Vertex, node_tweaks: Dict[str, Any]) -> None: vertex.params[tweak_name] = tweak_value -def process_tweaks( - graph_data: Dict[str, Any], tweaks: Dict[str, Dict[str, Any]] -) -> Dict[str, Any]: +def process_tweaks(graph_data: Dict[str, Any], tweaks: Dict[str, Dict[str, Any]]) -> Dict[str, Any]: """ This function is used to tweak the graph data using the node id and the tweaks dict. @@ -307,8 +291,6 @@ def process_tweaks_on_graph(graph: Graph, tweaks: Dict[str, Dict[str, Any]]): if node_tweaks := tweaks.get(node_id): apply_tweaks_on_vertex(vertex, node_tweaks) else: - logger.warning( - "Each node should be a Vertex with an 'id' attribute of type str" - ) + logger.warning("Each node should be a Vertex with an 'id' attribute of type str") return graph diff --git a/src/backend/langflow/schema/schema.py b/src/backend/langflow/schema/schema.py index 5a20d941f..fd3bd4ea0 100644 --- a/src/backend/langflow/schema/schema.py +++ b/src/backend/langflow/schema/schema.py @@ -63,9 +63,7 @@ class Record(BaseModel): return self.data.get(key, self._default_value) except KeyError: # Fallback to default behavior to raise AttributeError for undefined attributes - raise AttributeError( - f"'{type(self).__name__}' object has no attribute '{key}'" - ) + raise AttributeError(f"'{type(self).__name__}' object has no attribute '{key}'") def __setattr__(self, key, value): """ diff --git a/src/backend/langflow/services/chat/utils.py b/src/backend/langflow/services/chat/utils.py index f0e584f4c..0562b9df2 100644 --- a/src/backend/langflow/services/chat/utils.py +++ b/src/backend/langflow/services/chat/utils.py @@ -22,9 +22,7 @@ async def process_graph( if build_result is None: # Raise user facing error - raise ValueError( - "There was an error loading the langchain_object. Please, check all the nodes and try again." - ) + raise ValueError("There was an error loading the langchain_object. Please, check all the nodes and try again.") # Generate result and thought try: @@ -50,7 +48,5 @@ async def process_graph( raise e -async def run_build_result( - build_result: Any, chat_inputs: ChatMessage, client_id: str, session_id: str -): +async def run_build_result(build_result: Any, chat_inputs: ChatMessage, client_id: str, session_id: str): return build_result(inputs=chat_inputs.message) diff --git a/src/backend/langflow/services/database/models/flow/model.py b/src/backend/langflow/services/database/models/flow/model.py index 4810da557..b91cc409d 100644 --- a/src/backend/langflow/services/database/models/flow/model.py +++ b/src/backend/langflow/services/database/models/flow/model.py @@ -22,9 +22,7 @@ class FlowBase(SQLModel): icon_bg_color: Optional[str] = Field(default=None, nullable=True) 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("icon_bg_color") diff --git a/src/backend/langflow/services/database/service.py b/src/backend/langflow/services/database/service.py index c1e214700..ebaccf600 100644 --- a/src/backend/langflow/services/database/service.py +++ b/src/backend/langflow/services/database/service.py @@ -36,10 +36,7 @@ class DatabaseService(Service): def _create_engine(self) -> "Engine": """Create the engine for the database.""" settings_service = get_settings_service() - if ( - settings_service.settings.DATABASE_URL - and settings_service.settings.DATABASE_URL.startswith("sqlite") - ): + if settings_service.settings.DATABASE_URL and settings_service.settings.DATABASE_URL.startswith("sqlite"): connect_args = {"check_same_thread": False} else: connect_args = {} @@ -51,9 +48,7 @@ class DatabaseService(Service): def __exit__(self, exc_type, exc_value, traceback): if exc_type is not None: # If an exception has been raised - logger.error( - f"Session rollback because of exception: {exc_type.__name__} {exc_value}" - ) + logger.error(f"Session rollback because of exception: {exc_type.__name__} {exc_value}") self._session.rollback() else: self._session.commit() @@ -70,9 +65,7 @@ class DatabaseService(Service): settings_service = get_settings_service() if settings_service.auth_settings.AUTO_LOGIN: with Session(self.engine) as session: - flows = session.exec( - select(models.Flow).where(models.Flow.user_id is None) - ).all() + flows = session.exec(select(models.Flow).where(models.Flow.user_id is None)).all() if flows: logger.debug("Migrating flows to default superuser") username = settings_service.auth_settings.SUPERUSER @@ -102,9 +95,7 @@ class DatabaseService(Service): expected_columns = list(model.model_fields.keys()) try: - available_columns = [ - col["name"] for col in inspector.get_columns(table) - ] + available_columns = [col["name"] for col in inspector.get_columns(table)] except sa.exc.NoSuchTableError: logger.error(f"Missing table: {table}") return False @@ -161,9 +152,7 @@ class DatabaseService(Service): try: command.check(alembic_cfg) except Exception as exc: - if isinstance( - exc, (util.exc.CommandError, util.exc.AutogenerateDiffsDetected) - ): + if isinstance(exc, (util.exc.CommandError, util.exc.AutogenerateDiffsDetected)): command.upgrade(alembic_cfg, "head") time.sleep(3) @@ -199,10 +188,7 @@ class DatabaseService(Service): # We will check that all models are in the database # and that the database is up to date with all columns sql_models = [models.Flow, models.User, models.ApiKey] - return [ - TableResults(sql_model.__tablename__, self.check_table(sql_model)) - for sql_model in sql_models - ] + return [TableResults(sql_model.__tablename__, self.check_table(sql_model)) for sql_model in sql_models] def check_table(self, model): results = [] @@ -211,9 +197,7 @@ class DatabaseService(Service): expected_columns = list(model.__fields__.keys()) available_columns = [] try: - available_columns = [ - col["name"] for col in inspector.get_columns(table_name) - ] + available_columns = [col["name"] for col in inspector.get_columns(table_name)] results.append(Result(name=table_name, type="table", success=True)) except sa.exc.NoSuchTableError: logger.error(f"Missing table: {table_name}") @@ -244,9 +228,7 @@ class DatabaseService(Service): try: table.create(self.engine, checkfirst=True) except OperationalError as oe: - logger.warning( - f"Table {table} already exists, skipping. Exception: {oe}" - ) + logger.warning(f"Table {table} already exists, skipping. Exception: {oe}") except Exception as exc: logger.error(f"Error creating table {table}: {exc}") raise RuntimeError(f"Error creating table {table}") from exc @@ -258,9 +240,7 @@ class DatabaseService(Service): if table not in table_names: logger.error("Something went wrong creating the database and tables.") logger.error("Please check your database settings.") - raise RuntimeError( - "Something went wrong creating the database and tables." - ) + raise RuntimeError("Something went wrong creating the database and tables.") logger.debug("Database and tables created successfully") diff --git a/src/backend/langflow/services/monitor/schema.py b/src/backend/langflow/services/monitor/schema.py index 2c1e34cd5..d4293ecbf 100644 --- a/src/backend/langflow/services/monitor/schema.py +++ b/src/backend/langflow/services/monitor/schema.py @@ -10,9 +10,7 @@ if TYPE_CHECKING: class TransactionModel(BaseModel): id: Optional[int] = Field(default=None, alias="id") - timestamp: Optional[datetime] = Field( - default_factory=datetime.now, alias="timestamp" - ) + timestamp: Optional[datetime] = Field(default_factory=datetime.now, alias="timestamp") source: str target: str target_args: dict @@ -53,12 +51,8 @@ class MessageModel(BaseModel): @classmethod def from_record(cls, record: "Record"): # first check if the record has all the required fields - if not record.data or ( - "sender" not in record.data and "sender_name" not in record.data - ): - raise ValueError( - "The record does not have the required fields 'sender' and 'sender_name' in the data." - ) + if not record.data or ("sender" not in record.data and "sender_name" not in record.data): + raise ValueError("The record does not have the required fields 'sender' and 'sender_name' in the data.") return cls( sender=record.data["sender"], sender_name=record.data["sender_name"], diff --git a/src/backend/langflow/services/settings/base.py b/src/backend/langflow/services/settings/base.py index 3a1ec956e..f2e78b8cb 100644 --- a/src/backend/langflow/services/settings/base.py +++ b/src/backend/langflow/services/settings/base.py @@ -58,12 +58,10 @@ class Settings(BaseSettings): STORE: Optional[bool] = True STORE_URL: Optional[str] = "https://api.langflow.store" - DOWNLOAD_WEBHOOK_URL: Optional[str] = ( - "https://api.langflow.store/flows/trigger/ec611a61-8460-4438-b187-a4f65e5559d4" - ) - LIKE_WEBHOOK_URL: Optional[str] = ( - "https://api.langflow.store/flows/trigger/64275852-ec00-45c1-984e-3bff814732da" - ) + DOWNLOAD_WEBHOOK_URL: Optional[ + str + ] = "https://api.langflow.store/flows/trigger/ec611a61-8460-4438-b187-a4f65e5559d4" + LIKE_WEBHOOK_URL: Optional[str] = "https://api.langflow.store/flows/trigger/64275852-ec00-45c1-984e-3bff814732da" STORAGE_TYPE: str = "local" @@ -95,9 +93,7 @@ class Settings(BaseSettings): @validator("DATABASE_URL", pre=True) def set_database_url(cls, value, values): if not value: - logger.debug( - "No database_url provided, trying LANGFLOW_DATABASE_URL env variable" - ) + logger.debug("No database_url provided, trying LANGFLOW_DATABASE_URL env variable") if langflow_database_url := os.getenv("LANGFLOW_DATABASE_URL"): value = langflow_database_url logger.debug("Using LANGFLOW_DATABASE_URL env variable.") @@ -107,9 +103,7 @@ class Settings(BaseSettings): # so we need to migrate to the new format # if there is a database in that location if not values["CONFIG_DIR"]: - raise ValueError( - "CONFIG_DIR not set, please set it or provide a DATABASE_URL" - ) + raise ValueError("CONFIG_DIR not set, please set it or provide a DATABASE_URL") new_path = f"{values['CONFIG_DIR']}/langflow.db" if Path("./langflow.db").exists(): @@ -133,22 +127,15 @@ class Settings(BaseSettings): if os.getenv("LANGFLOW_COMPONENTS_PATH"): logger.debug("Adding LANGFLOW_COMPONENTS_PATH to components_path") langflow_component_path = os.getenv("LANGFLOW_COMPONENTS_PATH") - if ( - Path(langflow_component_path).exists() - and langflow_component_path not in value - ): + if Path(langflow_component_path).exists() and langflow_component_path not in value: if isinstance(langflow_component_path, list): for path in langflow_component_path: if path not in value: value.append(path) - logger.debug( - f"Extending {langflow_component_path} to components_path" - ) + logger.debug(f"Extending {langflow_component_path} to components_path") elif langflow_component_path not in value: value.append(langflow_component_path) - logger.debug( - f"Appending {langflow_component_path} to components_path" - ) + logger.debug(f"Appending {langflow_component_path} to components_path") if not value: value = [BASE_COMPONENTS_PATH] @@ -160,9 +147,7 @@ class Settings(BaseSettings): logger.debug(f"Components path: {value}") return value - model_config = SettingsConfigDict( - validate_assignment=True, extra="ignore", env_prefix="LANGFLOW_" - ) + model_config = SettingsConfigDict(validate_assignment=True, extra="ignore", env_prefix="LANGFLOW_") # @model_validator() # @classmethod diff --git a/src/backend/langflow/services/socket/utils.py b/src/backend/langflow/services/socket/utils.py index a45b85cd6..c1f012e18 100644 --- a/src/backend/langflow/services/socket/utils.py +++ b/src/backend/langflow/services/socket/utils.py @@ -96,9 +96,7 @@ async def build_vertex( ) # Emit the vertex build response - response = VertexBuildResponse( - valid=valid, params=params, id=vertex.id, data=result_dict - ) + response = VertexBuildResponse(valid=valid, params=params, id=vertex.id, data=result_dict) await sio.emit("vertex_build", data=response.model_dump(), to=sid) except Exception as exc: diff --git a/src/backend/langflow/services/task/service.py b/src/backend/langflow/services/task/service.py index e7f87d3f9..4d9a4412f 100644 --- a/src/backend/langflow/services/task/service.py +++ b/src/backend/langflow/services/task/service.py @@ -74,9 +74,7 @@ class TaskService(Service): result = await result return task.id, result - async def launch_task( - self, task_func: Callable[..., Any], *args: Any, **kwargs: Any - ) -> Any: + async def launch_task(self, task_func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any: logger.debug(f"Launching task {task_func} with args {args} and kwargs {kwargs}") logger.debug(f"Using backend {self.backend}") task = self.backend.launch_task(task_func, *args, **kwargs) diff --git a/src/backend/langflow/services/utils.py b/src/backend/langflow/services/utils.py index 34f1a042d..4c3533e97 100644 --- a/src/backend/langflow/services/utils.py +++ b/src/backend/langflow/services/utils.py @@ -92,16 +92,12 @@ def get_or_create_super_user(session: Session, username, password, is_default): ) return None else: - logger.debug( - "User with superuser credentials exists but is not a superuser." - ) + logger.debug("User with superuser credentials exists but is not a superuser.") return None if user: if verify_password(password, user.password): - raise ValueError( - "User with superuser credentials exists but is not a superuser." - ) + raise ValueError("User with superuser credentials exists but is not a superuser.") else: raise ValueError("Incorrect superuser credentials") @@ -130,21 +126,15 @@ def setup_superuser(settings_service, session: Session): username = settings_service.auth_settings.SUPERUSER password = settings_service.auth_settings.SUPERUSER_PASSWORD - is_default = (username == DEFAULT_SUPERUSER) and ( - password == DEFAULT_SUPERUSER_PASSWORD - ) + is_default = (username == DEFAULT_SUPERUSER) and (password == DEFAULT_SUPERUSER_PASSWORD) try: - user = get_or_create_super_user( - session=session, username=username, password=password, is_default=is_default - ) + user = get_or_create_super_user(session=session, username=username, password=password, is_default=is_default) if user is not None: logger.debug("Superuser created successfully.") except Exception as exc: logger.exception(exc) - raise RuntimeError( - "Could not create superuser. Please create a superuser manually." - ) from exc + raise RuntimeError("Could not create superuser. Please create a superuser manually.") from exc finally: settings_service.auth_settings.reset_credentials() @@ -158,9 +148,7 @@ def teardown_superuser(settings_service, session): if not settings_service.auth_settings.AUTO_LOGIN: try: - logger.debug( - "AUTO_LOGIN is set to False. Removing default superuser if exists." - ) + logger.debug("AUTO_LOGIN is set to False. Removing default superuser if exists.") username = DEFAULT_SUPERUSER from langflow.services.database.models.user.model import User @@ -210,9 +198,7 @@ def initialize_session_service(): initialize_settings_service() - service_manager.register_factory( - cache_factory.CacheServiceFactory(), dependencies=[ServiceType.SETTINGS_SERVICE] - ) + service_manager.register_factory(cache_factory.CacheServiceFactory(), dependencies=[ServiceType.SETTINGS_SERVICE]) service_manager.register_factory( session_service_factory.SessionServiceFactory(), @@ -229,9 +215,7 @@ def initialize_services(fix_migration: bool = False, socketio_server=None): service_manager.register_factory(factory, dependencies=dependencies) except Exception as exc: logger.exception(exc) - raise RuntimeError( - "Could not initialize services. Please check your settings." - ) from exc + raise RuntimeError("Could not initialize services. Please check your settings.") from exc # Test cache connection service_manager.get(ServiceType.CACHE_SERVICE) @@ -241,9 +225,7 @@ def initialize_services(fix_migration: bool = False, socketio_server=None): except Exception as exc: logger.error(exc) raise exc - setup_superuser( - service_manager.get(ServiceType.SETTINGS_SERVICE), next(get_session()) - ) + setup_superuser(service_manager.get(ServiceType.SETTINGS_SERVICE), next(get_session())) try: get_db_service().migrate_flows_if_auto_login() except Exception as exc: diff --git a/src/backend/langflow/template/field/base.py b/src/backend/langflow/template/field/base.py index 11b540e77..bf6d461f7 100644 --- a/src/backend/langflow/template/field/base.py +++ b/src/backend/langflow/template/field/base.py @@ -68,9 +68,7 @@ class TemplateField(BaseModel): refresh: Optional[bool] = None """Specifies if the field should be refreshed. Defaults to False.""" - range_spec: Optional[RangeSpec] = Field( - default=None, serialization_alias="rangeSpec" - ) + range_spec: Optional[RangeSpec] = Field(default=None, serialization_alias="rangeSpec") """Range specification for the field. Defaults to None.""" title_case: bool = False @@ -119,10 +117,6 @@ class TemplateField(BaseModel): if not isinstance(value, list): raise ValueError("file_types must be a list") return [ - ( - f".{file_type}" - if isinstance(file_type, str) and not file_type.startswith(".") - else file_type - ) + (f".{file_type}" if isinstance(file_type, str) and not file_type.startswith(".") else file_type) for file_type in value ] diff --git a/src/backend/langflow/template/frontend_node/base.py b/src/backend/langflow/template/frontend_node/base.py index 27572cd5b..7bd68ddf9 100644 --- a/src/backend/langflow/template/frontend_node/base.py +++ b/src/backend/langflow/template/frontend_node/base.py @@ -174,9 +174,7 @@ 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 @@ -187,11 +185,7 @@ 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 @@ -202,9 +196,7 @@ 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 "}""" @@ -212,9 +204,7 @@ 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, @@ -227,9 +217,7 @@ 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" @@ -269,10 +257,7 @@ 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 f1c326810..93ea561dd 100644 --- a/src/backend/langflow/template/frontend_node/memories.py +++ b/src/backend/langflow/template/frontend_node/memories.py @@ -80,9 +80,7 @@ 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/backend/langflow/template/template/base.py b/src/backend/langflow/template/template/base.py index e268313ea..d7632e239 100644 --- a/src/backend/langflow/template/template/base.py +++ b/src/backend/langflow/template/template/base.py @@ -45,9 +45,7 @@ class Template(BaseModel): """Returns the field with the given name.""" field = next((field for field in self.fields if field.name == field_name), None) if field is None: - raise ValueError( - f"Field {field_name} not found in template {self.type_name}" - ) + raise ValueError(f"Field {field_name} not found in template {self.type_name}") return field def update_field(self, field_name: str, field: TemplateField) -> None: diff --git a/src/backend/langflow/utils/util.py b/src/backend/langflow/utils/util.py index 2896ec02e..814a9d4ab 100644 --- a/src/backend/langflow/utils/util.py +++ b/src/backend/langflow/utils/util.py @@ -15,12 +15,8 @@ def remove_ansi_escape_codes(text): return re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", text) -def build_template_from_function( - name: str, type_to_loader_dict: Dict, add_function: bool = False -): - classes = [ - item.__annotations__["return"].__name__ for item in type_to_loader_dict.values() - ] +def build_template_from_function(name: str, type_to_loader_dict: Dict, add_function: bool = False): + classes = [item.__annotations__["return"].__name__ for item in type_to_loader_dict.values()] # Raise error if name is not in chains if name not in classes: @@ -41,10 +37,8 @@ def build_template_from_function( for name_, value_ in value.__repr_args__(): if name_ == "default_factory": try: - variables[class_field_items]["default"] = ( - get_default_factory( - module=_class.__base__.__module__, function=value_ - ) + variables[class_field_items]["default"] = get_default_factory( + module=_class.__base__.__module__, function=value_ ) except Exception: variables[class_field_items]["default"] = None @@ -52,9 +46,7 @@ def build_template_from_function( variables[class_field_items][name_] = value_ variables[class_field_items]["placeholder"] = ( - docs.params[class_field_items] - if class_field_items in docs.params - else "" + docs.params[class_field_items] if class_field_items in docs.params else "" ) # Adding function to base classes to allow # the output to be a function @@ -69,9 +61,7 @@ def build_template_from_function( } -def build_template_from_class( - name: str, type_to_cls_dict: Dict, add_function: bool = False -): +def build_template_from_class(name: str, type_to_cls_dict: Dict, add_function: bool = False): classes = [item.__name__ for item in type_to_cls_dict.values()] # Raise error if name is not in chains @@ -95,11 +85,9 @@ def build_template_from_class( for name_, value_ in value.__repr_args__(): if name_ == "default_factory": try: - variables[class_field_items]["default"] = ( - get_default_factory( - module=_class.__base__.__module__, - function=value_, - ) + variables[class_field_items]["default"] = get_default_factory( + module=_class.__base__.__module__, + function=value_, ) except Exception: variables[class_field_items]["default"] = None @@ -107,9 +95,7 @@ def build_template_from_class( variables[class_field_items][name_] = value_ variables[class_field_items]["placeholder"] = ( - docs.params[class_field_items] - if class_field_items in docs.params - else "" + docs.params[class_field_items] if class_field_items in docs.params else "" ) base_classes = get_base_classes(_class) # Adding function to base classes to allow @@ -141,9 +127,7 @@ def build_template_from_method( # Check if the method exists in this class if not hasattr(_class, method_name): - raise ValueError( - f"Method {method_name} not found in class {class_name}" - ) + raise ValueError(f"Method {method_name} not found in class {class_name}") # Get the method method = getattr(_class, method_name) @@ -162,14 +146,8 @@ def build_template_from_method( "_type": _type, **{ name: { - "default": ( - param.default if param.default != param.empty else None - ), - "type": ( - param.annotation - if param.annotation != param.empty - else None - ), + "default": (param.default if param.default != param.empty else None), + "type": (param.annotation if param.annotation != param.empty else None), "required": param.default == param.empty, } for name, param in params.items() @@ -256,9 +234,7 @@ def sync_to_async(func): return async_wrapper -def format_dict( - dictionary: Dict[str, Any], class_name: Optional[str] = None -) -> Dict[str, Any]: +def format_dict(dictionary: Dict[str, Any], class_name: Optional[str] = None) -> Dict[str, Any]: """ Formats a dictionary by removing certain keys and modifying the values of other keys. @@ -344,9 +320,7 @@ def check_list_type(_type: str, value: Dict[str, Any]) -> str: The modified type string. """ if any(list_type in _type for list_type in ["List", "Sequence", "Set"]): - _type = ( - _type.replace("List[", "").replace("Sequence[", "").replace("Set[", "")[:-1] - ) + _type = _type.replace("List[", "").replace("Sequence[", "").replace("Set[", "")[:-1] value["list"] = True else: value["list"] = False @@ -449,9 +423,7 @@ def set_headers_value(value: Dict[str, Any]) -> None: value["value"] = """{"Authorization": "Bearer "}""" -def add_options_to_field( - value: Dict[str, Any], class_name: Optional[str], key: str -) -> None: +def add_options_to_field(value: Dict[str, Any], class_name: Optional[str], key: str) -> None: """ Adds options to the field based on the class name and key. """ diff --git a/src/backend/langflow/utils/validate.py b/src/backend/langflow/utils/validate.py index 9611d3a56..e7bd4ae05 100644 --- a/src/backend/langflow/utils/validate.py +++ b/src/backend/langflow/utils/validate.py @@ -43,9 +43,7 @@ def validate_code(code): # Evaluate the function definition for node in tree.body: if isinstance(node, ast.FunctionDef): - code_obj = compile( - ast.Module(body=[node], type_ignores=[]), "", "exec" - ) + code_obj = compile(ast.Module(body=[node], type_ignores=[]), "", "exec") try: exec(code_obj) except Exception as e: @@ -89,23 +87,15 @@ def execute_function(code, function_name, *args, **kwargs): exec_globals, locals(), ) - exec_globals[alias.asname or alias.name] = importlib.import_module( - alias.name - ) + exec_globals[alias.asname or alias.name] = importlib.import_module(alias.name) except ModuleNotFoundError as e: - raise ModuleNotFoundError( - f"Module {alias.name} not found. Please install it and try again." - ) from e + raise ModuleNotFoundError(f"Module {alias.name} not found. Please install it and try again.") from e function_code = next( - node - for node in module.body - if isinstance(node, ast.FunctionDef) and node.name == function_name + node for node in module.body if isinstance(node, ast.FunctionDef) and node.name == function_name ) function_code.parent = None - code_obj = compile( - ast.Module(body=[function_code], type_ignores=[]), "", "exec" - ) + code_obj = compile(ast.Module(body=[function_code], type_ignores=[]), "", "exec") try: exec(code_obj, exec_globals, locals()) except Exception as exc: @@ -132,23 +122,15 @@ def create_function(code, function_name): if isinstance(node, ast.Import): for alias in node.names: try: - exec_globals[alias.asname or alias.name] = importlib.import_module( - alias.name - ) + exec_globals[alias.asname or alias.name] = importlib.import_module(alias.name) except ModuleNotFoundError as e: - raise ModuleNotFoundError( - f"Module {alias.name} not found. Please install it and try again." - ) from e + raise ModuleNotFoundError(f"Module {alias.name} not found. Please install it and try again.") from e function_code = next( - node - for node in module.body - if isinstance(node, ast.FunctionDef) and node.name == function_name + node for node in module.body if isinstance(node, ast.FunctionDef) and node.name == function_name ) function_code.parent = None - code_obj = compile( - ast.Module(body=[function_code], type_ignores=[]), "", "exec" - ) + code_obj = compile(ast.Module(body=[function_code], type_ignores=[]), "", "exec") with contextlib.suppress(Exception): exec(code_obj, exec_globals, locals()) exec_globals[function_name] = locals()[function_name] @@ -210,13 +192,9 @@ def prepare_global_scope(code, module): if isinstance(node, ast.Import): for alias in node.names: try: - exec_globals[alias.asname or alias.name] = importlib.import_module( - alias.name - ) + exec_globals[alias.asname or alias.name] = importlib.import_module(alias.name) except ModuleNotFoundError as e: - raise ModuleNotFoundError( - f"Module {alias.name} not found. Please install it and try again." - ) from e + raise ModuleNotFoundError(f"Module {alias.name} not found. Please install it and try again.") from e elif isinstance(node, ast.ImportFrom) and node.module is not None: try: imported_module = importlib.import_module(node.module) @@ -237,11 +215,7 @@ def extract_class_code(module, class_name): :param class_name: Name of the class to extract :return: AST node of the specified class """ - class_code = next( - node - for node in module.body - if isinstance(node, ast.ClassDef) and node.name == class_name - ) + class_code = next(node for node in module.body if isinstance(node, ast.ClassDef) and node.name == class_name) class_code.parent = None return class_code @@ -254,9 +228,7 @@ def compile_class_code(class_code): :param class_code: AST node of the class :return: Compiled code object of the class """ - code_obj = compile( - ast.Module(body=[class_code], type_ignores=[]), "", "exec" - ) + code_obj = compile(ast.Module(body=[class_code], type_ignores=[]), "", "exec") return code_obj @@ -300,9 +272,7 @@ def get_default_imports(code_string): langflow_imports = list(CUSTOM_COMPONENT_SUPPORTED_TYPES.keys()) necessary_imports = find_names_in_code(code_string, langflow_imports) langflow_module = importlib.import_module("langflow.field_typing") - default_imports.update( - {name: getattr(langflow_module, name) for name in necessary_imports} - ) + default_imports.update({name: getattr(langflow_module, name) for name in necessary_imports}) return default_imports From 0044622d3ae689bbe2b0027f39fd20066e9a3029 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 14:54:39 -0300 Subject: [PATCH 142/199] Fix formatting and remove unused code --- .../components/AccordionComponent/index.tsx | 4 +- .../components/CrashErrorComponent/index.tsx | 4 +- .../components/NewFlowCardComponent/index.tsx | 77 ++++----- .../components/codeTabsComponent/index.tsx | 4 +- .../src/components/exampleComponent/index.tsx | 156 +++++++++--------- .../newChatView/chatMessage/index.tsx | 10 +- src/frontend/src/components/ui/dialog.tsx | 2 +- .../src/constants/alerts_constants.tsx | 1 - .../modals/DeleteConfirmationModal/index.tsx | 3 +- .../src/modals/EditNodeModal/index.tsx | 15 +- .../src/modals/codeAreaModal/index.tsx | 2 +- src/frontend/src/modals/shareModal/index.tsx | 9 +- .../MainPage/components/components/index.tsx | 1 - src/frontend/src/pages/MainPage/index.tsx | 26 +-- src/frontend/src/stores/flowsManagerStore.ts | 16 +- src/frontend/src/types/flow/index.ts | 4 +- 16 files changed, 162 insertions(+), 172 deletions(-) diff --git a/src/frontend/src/components/AccordionComponent/index.tsx b/src/frontend/src/components/AccordionComponent/index.tsx index 9a3c4a212..3d9aac314 100644 --- a/src/frontend/src/components/AccordionComponent/index.tsx +++ b/src/frontend/src/components/AccordionComponent/index.tsx @@ -50,9 +50,7 @@ export default function AccordionComponent({ {trigger} -
- {children} -
+
{children}
diff --git a/src/frontend/src/components/CrashErrorComponent/index.tsx b/src/frontend/src/components/CrashErrorComponent/index.tsx index 0cf376195..57dc83d83 100644 --- a/src/frontend/src/components/CrashErrorComponent/index.tsx +++ b/src/frontend/src/components/CrashErrorComponent/index.tsx @@ -26,7 +26,7 @@ export default function CrashErrorComponent({

- Please report errors with detailed tracebacks on the{" "} + Please report errors with detailed tracebacks on the{" "} {" "} page.

- Thank you! + Thank you!

diff --git a/src/frontend/src/components/NewFlowCardComponent/index.tsx b/src/frontend/src/components/NewFlowCardComponent/index.tsx index 131fb3543..1fe5b4b46 100644 --- a/src/frontend/src/components/NewFlowCardComponent/index.tsx +++ b/src/frontend/src/components/NewFlowCardComponent/index.tsx @@ -1,54 +1,33 @@ -import { useEffect, useState } from "react"; -import { getComponent, postLikeComponent } from "../../controllers/API"; -import DeleteConfirmationModal from "../../modals/DeleteConfirmationModal"; -import useAlertStore from "../../stores/alertStore"; -import useFlowsManagerStore from "../../stores/flowsManagerStore"; -import { useStoreStore } from "../../stores/storeStore"; -import { storeComponent } from "../../types/store"; -import cloneFLowWithParent from "../../utils/storeUtils"; -import { cn } from "../../utils/utils"; -import ShadTooltip from "../ShadTooltipComponent"; -import IconComponent from "../genericIconComponent"; -import { Badge } from "../ui/badge"; -import { Button } from "../ui/button"; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "../ui/card"; -import { FlowType } from "../../types/flow"; import { useNavigate } from "react-router-dom"; +import useFlowsManagerStore from "../../stores/flowsManagerStore"; +import { cn } from "../../utils/utils"; +import IconComponent from "../genericIconComponent"; +import { Card, CardContent } from "../ui/card"; -export default function NewFlowCardComponent({ -}: { - }) { - const addFlow = useFlowsManagerStore((state) => state.addFlow); - const navigate = useNavigate(); +export default function NewFlowCardComponent({}: {}) { + const addFlow = useFlowsManagerStore((state) => state.addFlow); + const navigate = useNavigate(); - return ( - + + - - - ); + + + + + ); } diff --git a/src/frontend/src/components/codeTabsComponent/index.tsx b/src/frontend/src/components/codeTabsComponent/index.tsx index 8ddb09552..a83a36fc2 100644 --- a/src/frontend/src/components/codeTabsComponent/index.tsx +++ b/src/frontend/src/components/codeTabsComponent/index.tsx @@ -126,7 +126,7 @@ export default function CodeTabsComponent({ {tab.code} diff --git a/src/frontend/src/components/exampleComponent/index.tsx b/src/frontend/src/components/exampleComponent/index.tsx index fa021b299..87f69a52b 100644 --- a/src/frontend/src/components/exampleComponent/index.tsx +++ b/src/frontend/src/components/exampleComponent/index.tsx @@ -1,96 +1,88 @@ -import { useEffect, useState } from "react"; -import { getComponent, postLikeComponent } from "../../controllers/API"; -import DeleteConfirmationModal from "../../modals/DeleteConfirmationModal"; -import useAlertStore from "../../stores/alertStore"; +import { useNavigate } from "react-router-dom"; import useFlowsManagerStore from "../../stores/flowsManagerStore"; -import { useStoreStore } from "../../stores/storeStore"; -import { storeComponent } from "../../types/store"; -import cloneFLowWithParent from "../../utils/storeUtils"; +import { FlowType } from "../../types/flow"; +import { updateIds } from "../../utils/reactflowUtils"; import { cn } from "../../utils/utils"; import ShadTooltip from "../ShadTooltipComponent"; import IconComponent from "../genericIconComponent"; -import { Badge } from "../ui/badge"; import { Button } from "../ui/button"; import { - Card, - CardDescription, - CardFooter, - CardHeader, - CardTitle, + Card, + CardDescription, + CardFooter, + CardHeader, + CardTitle, } from "../ui/card"; -import { FlowType } from "../../types/flow"; -import { updateIds } from "../../utils/reactflowUtils"; -import { useNavigate } from "react-router-dom"; export default function CollectionCardComponent({ - flow, + flow, }: { - flow: FlowType; - authorized?: boolean; + flow: FlowType; + authorized?: boolean; }) { - const addFlow = useFlowsManagerStore((state) => state.addFlow); - const navigate = useNavigate(); + const addFlow = useFlowsManagerStore((state) => state.addFlow); + const navigate = useNavigate(); - return ( - -
- -
- - {flow.icon && ( -
- {flow.icon} -
- - )} - {!flow.icon && } - -
{flow.name}
-
-
-
- - -
{flow.description}
-
-
-
-
+ return ( + +
+ +
+ + {flow.icon && ( +
{flow.icon}
+ )} + {!flow.icon && ( + + )} + +
{flow.name}
+
+
+
+ + +
{flow.description}
+
+
+
+
- -
-
- -
-
-
-
- ); + +
+
+ +
+
+
+
+ ); } diff --git a/src/frontend/src/components/newChatView/chatMessage/index.tsx b/src/frontend/src/components/newChatView/chatMessage/index.tsx index 075d5e418..760a9d140 100644 --- a/src/frontend/src/components/newChatView/chatMessage/index.tsx +++ b/src/frontend/src/components/newChatView/chatMessage/index.tsx @@ -134,7 +134,7 @@ export default function ChatMessage({ )}
{!chat.isSend ? ( -
+
{hidden && chat.thought && chat.thought !== "" && (
)} {chat.thought && chat.thought !== "" && !hidden &&

} -
-
-
+
+
+
{useMemo( () => chatMessage === "" && lockChat ? ( @@ -169,7 +169,7 @@ export default function ChatMessage({ - Confirm deletion of {description ?? "component"}? -

+ Confirm deletion of {description ?? "component"}?

Note: This action is irreversible.
diff --git a/src/frontend/src/modals/EditNodeModal/index.tsx b/src/frontend/src/modals/EditNodeModal/index.tsx index e789e1ab8..619a6f7e2 100644 --- a/src/frontend/src/modals/EditNodeModal/index.tsx +++ b/src/frontend/src/modals/EditNodeModal/index.tsx @@ -165,8 +165,19 @@ const EditNodeModal = forwardRef( ) ) ?? false; return ( -
- Attention: API keys in specified fields are automatically removed upon sharing. + Attention: API keys in specified fields are automatically + removed upon sharing. diff --git a/src/frontend/src/pages/MainPage/components/components/index.tsx b/src/frontend/src/pages/MainPage/components/components/index.tsx index 234852388..65189fa8a 100644 --- a/src/frontend/src/pages/MainPage/components/components/index.tsx +++ b/src/frontend/src/pages/MainPage/components/components/index.tsx @@ -14,7 +14,6 @@ import { import useAlertStore from "../../../../stores/alertStore"; import useFlowsManagerStore from "../../../../stores/flowsManagerStore"; import { FlowType } from "../../../../types/flow"; -import { STARTER_FOLDER_NAME } from "../../../../constants/constants"; export default function ComponentsComponent({ is_component = true, diff --git a/src/frontend/src/pages/MainPage/index.tsx b/src/frontend/src/pages/MainPage/index.tsx index 2574f03af..c6e233521 100644 --- a/src/frontend/src/pages/MainPage/index.tsx +++ b/src/frontend/src/pages/MainPage/index.tsx @@ -2,6 +2,8 @@ import { Group, ToyBrick } from "lucide-react"; import { useEffect, useState } from "react"; import { Outlet, useLocation, useNavigate } from "react-router-dom"; import DropdownButton from "../../components/DropdownButtonComponent"; +import NewFlowCardComponent from "../../components/NewFlowCardComponent"; +import ExampleCardComponent from "../../components/exampleComponent"; import IconComponent from "../../components/genericIconComponent"; import PageLayout from "../../components/pageLayout"; import SidebarNav from "../../components/sidebarComponent"; @@ -11,12 +13,10 @@ import { MY_COLLECTION_DESC, USER_PROJECTS_HEADER, } from "../../constants/constants"; +import BaseModal from "../../modals/baseModal"; import useAlertStore from "../../stores/alertStore"; import useFlowsManagerStore from "../../stores/flowsManagerStore"; import { downloadFlows } from "../../utils/reactflowUtils"; -import BaseModal from "../../modals/baseModal"; -import ExampleCardComponent from "../../components/exampleComponent"; -import NewFlowCardComponent from "../../components/NewFlowCardComponent"; export default function HomePage(): JSX.Element { const addFlow = useFlowsManagerStore((state) => state.addFlow); const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow); @@ -41,8 +41,9 @@ export default function HomePage(): JSX.Element { }) .then((id) => { setSuccessData({ - title: `${is_component ? "Component" : "Flow" - } uploaded successfully`, + title: `${ + is_component ? "Component" : "Flow" + } uploaded successfully`, }); if (!is_component) navigate("/flow/" + id); }) @@ -117,24 +118,25 @@ export default function HomePage(): JSX.Element {
- + Create a New Flow -
+
{examples.map((example, idx) => { - return( - ) + return ; })} - -
+ +
diff --git a/src/frontend/src/stores/flowsManagerStore.ts b/src/frontend/src/stores/flowsManagerStore.ts index d993f40b0..2c9fca428 100644 --- a/src/frontend/src/stores/flowsManagerStore.ts +++ b/src/frontend/src/stores/flowsManagerStore.ts @@ -2,6 +2,7 @@ import { AxiosError } from "axios"; import { cloneDeep } from "lodash"; import { Edge, Node, Viewport, XYPosition } from "reactflow"; import { create } from "zustand"; +import { STARTER_FOLDER_NAME } from "../constants/constants"; import { deleteFlowFromDatabase, readFlowsFromDatabase, @@ -25,7 +26,6 @@ import useAlertStore from "./alertStore"; import { useDarkStore } from "./darkStore"; import useFlowStore from "./flowStore"; import { useTypesStore } from "./typesStore"; -import { STARTER_FOLDER_NAME } from "../constants/constants"; let saveTimeoutId: NodeJS.Timeout | null = null; @@ -38,7 +38,7 @@ const past = {}; const future = {}; const useFlowsManagerStore = create((set, get) => ({ - examples:[], + examples: [], setExamples: (examples: FlowType[]) => { set({ examples }); }, @@ -67,8 +67,16 @@ const useFlowsManagerStore = create((set, get) => ({ .then((dbData) => { if (dbData) { const { data, flows } = processFlows(dbData, false); - get().setExamples(flows.filter(f=>(f.folder===STARTER_FOLDER_NAME && !f.user_id))); - get().setFlows(flows.filter(f=>!(f.folder===STARTER_FOLDER_NAME && !f.user_id))); + get().setExamples( + flows.filter( + (f) => f.folder === STARTER_FOLDER_NAME && !f.user_id + ) + ); + get().setFlows( + flows.filter( + (f) => !(f.folder === STARTER_FOLDER_NAME && !f.user_id) + ) + ); useTypesStore.setState((state) => ({ data: { ...state.data, ["saved_components"]: data }, })); diff --git a/src/frontend/src/types/flow/index.ts b/src/frontend/src/types/flow/index.ts index 6c7c941ad..8b259186a 100644 --- a/src/frontend/src/types/flow/index.ts +++ b/src/frontend/src/types/flow/index.ts @@ -15,8 +15,8 @@ export type FlowType = { parent?: string; folder?: string; user_id?: string; - icon?:string; - icon_bg_color?:string; + icon?: string; + icon_bg_color?: string; }; export type NodeType = { From 1a8891134c3f0a5c80904846c9ddd0f7b0e95f66 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 14:56:19 -0300 Subject: [PATCH 143/199] Add parameterUtils.ts file with handleUpdateValues and throttledHandleUpdateValues functions --- .../components/parameterComponent/index.tsx | 153 +++++++++--------- src/frontend/src/utils/parameterUtils.ts | 35 ++++ 2 files changed, 116 insertions(+), 72 deletions(-) create mode 100644 src/frontend/src/utils/parameterUtils.ts diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index c3e04627f..dcb458b0f 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -23,14 +23,16 @@ import { OUTPUT_HANDLER_HOVER, TOOLTIP_EMPTY, } from "../../../../constants/constants"; -import { postCustomComponentUpdate } from "../../../../controllers/API"; import useAlertStore from "../../../../stores/alertStore"; import useFlowStore from "../../../../stores/flowStore"; import useFlowsManagerStore from "../../../../stores/flowsManagerStore"; import { useTypesStore } from "../../../../stores/typesStore"; -import { APIClassType } from "../../../../types/api"; +import { APIClassType, ResponseErrorTypeAPI } from "../../../../types/api"; import { ParameterComponentType } from "../../../../types/components"; -import { NodeDataType } from "../../../../types/flow"; +import { + handleUpdateValues, + throttledHandleUpdateValues, +} from "../../../../utils/parameterUtils"; import { convertObjToArray, convertValuesToNumbers, @@ -86,81 +88,70 @@ export default function ParameterComponent({ const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot); - const handleUpdateValues = async ( - name: string, - data: NodeDataType, - delayAnimation: boolean = true - ) => { - setIsLoading(true); - const code = data.node?.template["code"]?.value; - if (!code) { - console.error("Code not found in the template"); - return; - } - - await postCustomComponentUpdate( - code, - name, - data.node?.template[name]?.value - ) - .then((res) => { - if (res.status === 200 && data.node?.template) { - setNode(data.id, (oldNode) => { - let newNode = cloneDeep(oldNode); - - newNode.data = { - ...newNode.data, - }; - - newNode.data.node.template = res.data.template; - - return newNode; - }); - } - }) - .catch((error) => { - console.error("Error occurred while updating the node:", error); - setErrorData({ - title: "Error while updating the Component", - list: [error.response.data.detail.error ?? "Unknown error"], - }); - }); - - renderTooltips(); - if (delayAnimation) { - try { - // Wait for at least 500 milliseconds - await new Promise((resolve) => setTimeout(resolve, 500)); - // Continue with the request - // If the request takes longer than 500 milliseconds, it will not wait an additional 500 milliseconds - } catch (error) { - console.error("Error occurred while waiting for refresh:", error); - } finally { - setIsLoading(false); - } - } else setIsLoading(false); - }; - useEffect(() => { - function fetchData() { + async function fetchData() { if ( data.node?.template[name]?.refresh && - Object.keys(data.node?.template[name]?.options ?? {}).length === 0 + // options can be undefined but not an empty array + (data.node?.template[name]?.options?.length ?? 0) === 0 ) { - handleUpdateValues(name, data, false); + setIsLoading(true); + try { + let newTemplate = await handleUpdateValues(name, data); + if (newTemplate) { + setNode(data.id, (oldNode) => { + let newNode = cloneDeep(oldNode); + newNode.data = { + ...newNode.data, + }; + newNode.data.node.template = newTemplate; + return newNode; + }); + } + } catch (error) { + let responseError = error as ResponseErrorTypeAPI; + setErrorData({ + title: "Error while updating the Component", + list: [responseError.response.data.detail.error ?? "Unknown error"], + }); + } + setIsLoading(false); + renderTooltips(); } } fetchData(); }, []); - const handleOnNewValue = ( + const handleOnNewValue = async ( newValue: string | string[] | boolean | Object[] - ): void => { + ): Promise => { if (data.node!.template[name].value !== newValue) { takeSnapshot(); } - + const shouldUpdate = + data.node?.template[name].refresh && + data.node!.template[name].value !== newValue; + console.log("shouldUpdate", shouldUpdate); + console.log( + "data.node!.template[name].value", + data.node!.template[name].value + ); + console.log("newValue", newValue); data.node!.template[name].value = newValue; // necessary to enable ctrl+z inside the input - + let newTemplate; + if (shouldUpdate) { + setIsLoading(true); + try { + newTemplate = await throttledHandleUpdateValues(name, data); + } catch (error) { + let responseError = error as ResponseErrorTypeAPI; + setErrorData({ + title: "Error while updating the Component", + list: [responseError.response.data.detail.error ?? "Unknown error"], + }); + } + setIsLoading(false); + // this de + } setNode(data.id, (oldNode) => { let newNode = cloneDeep(oldNode); @@ -168,7 +159,9 @@ export default function ParameterComponent({ ...newNode.data, }; - newNode.data.node.template[name].value = newValue; + if (data.node?.template[name].refresh && newTemplate) { + newNode.data.node.template = newTemplate; + } else newNode.data.node.template[name].value = newValue; return newNode; }); @@ -300,11 +293,14 @@ export default function ParameterComponent({ refHtml.current = {TOOLTIP_EMPTY}; } } + // If optionalHandle is an empty list, then it is not an optional handle + if (optionalHandle && optionalHandle.length === 0) { + optionalHandle = null; + } useEffect(() => { renderTooltips(); }, [tooltipTitle, flow]); - return !showNode ? ( left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? ( <> @@ -451,7 +447,9 @@ export default function ParameterComponent({ {data.node?.template[name].list ? (
@@ -465,7 +463,7 @@ export default function ParameterComponent({ } onChange={handleOnNewValue} /> - {data.node?.template[name].refresh && ( + {/* {data.node?.template[name].refresh && (
- )} + )} */}
) : data.node?.template[name].multiline ? ( ) : (
-
+
{ const valueToNumbers = convertValuesToNumbers(newValue); setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers)); - handleOnNewValue(valueToNumbers); + // if data.node?.template[name].list is true, then the value is an array of objects + // else we need to get the first object of the array + + if (data.node?.template[name].list) { + handleOnNewValue(valueToNumbers); + } else handleOnNewValue(valueToNumbers[0]); }} + isList={data.node?.template[name].list ?? false} />
) : ( diff --git a/src/frontend/src/utils/parameterUtils.ts b/src/frontend/src/utils/parameterUtils.ts new file mode 100644 index 000000000..9635ce96d --- /dev/null +++ b/src/frontend/src/utils/parameterUtils.ts @@ -0,0 +1,35 @@ +import { throttle } from "lodash"; +import { postCustomComponentUpdate } from "../controllers/API"; +import { ResponseErrorTypeAPI } from "../types/api"; +import { NodeDataType } from "../types/flow"; + +export const handleUpdateValues = async (name: string, data: NodeDataType) => { + const code = data.node?.template["code"]?.value; + if (!code) { + console.error("Code not found in the template"); + return; + } + try { + let newTemplate = await postCustomComponentUpdate( + code, + name, + data.node?.template[name]?.value + ) + .then((res) => { + console.log("res", res); + if (res.status === 200 && data.node?.template) { + return res.data.template; + } + }) + .catch((error) => { + throw error; + }); + return newTemplate; + } catch (error) { + console.error("Error occurred while updating the node:", error); + let errorType = error as ResponseErrorTypeAPI; + throw errorType; + } +}; + +export const throttledHandleUpdateValues = throttle(handleUpdateValues, 10); From 4daf4ffc81c321beabd94c18bcf2d9e8899e3f05 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 14:56:36 -0300 Subject: [PATCH 144/199] Fix variable name and simplify if statements --- src/frontend/tailwind.config.js | 2 +- .../end-to-end/assets/flow_group_test.json | 510 +++++++++++++++++- tests/test_endpoints.py | 41 +- 3 files changed, 519 insertions(+), 34 deletions(-) diff --git a/src/frontend/tailwind.config.js b/src/frontend/tailwind.config.js index 80a5416c3..2e2781df7 100644 --- a/src/frontend/tailwind.config.js +++ b/src/frontend/tailwind.config.js @@ -87,7 +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)", + ice: "var(--ice)", white: "var(--white)", border: "hsl(var(--border))", diff --git a/src/frontend/tests/end-to-end/assets/flow_group_test.json b/src/frontend/tests/end-to-end/assets/flow_group_test.json index d8d9a2a87..9df38cfb3 100644 --- a/src/frontend/tests/end-to-end/assets/flow_group_test.json +++ b/src/frontend/tests/end-to-end/assets/flow_group_test.json @@ -1 +1,509 @@ -{"id":"8404c1fc-1bce-43b4-a8bc-3febea587fc8","data":{"nodes":[{"id":"PythonFunctionTool-RfJui","type":"genericNode","position":{"x":117.54690105175428,"y":-84.2465475108354},"data":{"type":"PythonFunctionTool","node":{"template":{"code":{"type":"code","required":true,"placeholder":"","list":false,"show":true,"multiline":true,"value":"\ndef python_function(text: str) -> str:\n \"\"\"This is a default python function that returns the input text\"\"\"\n return text\n","fileTypes":[],"file_path":"","password":false,"name":"code","advanced":false,"dynamic":false,"info":"","title_case":false},"description":{"type":"str","required":true,"placeholder":"","list":false,"show":true,"multiline":true,"value":"Returns the Text you send. This is a testing tool.","fileTypes":[],"file_path":"","password":false,"name":"description","advanced":false,"dynamic":false,"info":"","title_case":false,"input_types":["Text"]},"name":{"type":"str","required":true,"placeholder":"","list":false,"show":true,"multiline":false,"value":"PythonFunction","fileTypes":[],"file_path":"","password":false,"name":"name","advanced":false,"dynamic":false,"info":"","title_case":false,"input_types":["Text"]},"return_direct":{"type":"bool","required":true,"placeholder":"","list":false,"show":true,"multiline":false,"value":false,"fileTypes":[],"file_path":"","password":false,"name":"return_direct","advanced":false,"dynamic":false,"info":"","title_case":false},"_type":"PythonFunctionTool"},"description":"Python function to be executed.","base_classes":["BaseTool","Tool"],"display_name":"PythonFunctionTool","documentation":"","custom_fields":{},"output_types":[],"field_formatters":{},"pinned":false,"beta":false},"id":"PythonFunctionTool-RfJui"},"selected":true,"width":384,"height":466,"positionAbsolute":{"x":117.54690105175428,"y":-84.2465475108354},"dragging":false},{"id":"AgentInitializer-tPdJw","type":"genericNode","position":{"x":677.68677055088,"y":127.19859565276168},"data":{"type":"AgentInitializer","node":{"template":{"llm":{"type":"BaseLanguageModel","required":true,"placeholder":"","list":false,"show":true,"multiline":false,"fileTypes":[],"file_path":"","password":false,"name":"llm","display_name":"Language Model","advanced":false,"dynamic":false,"info":"","title_case":false},"memory":{"type":"BaseChatMemory","required":false,"placeholder":"","list":false,"show":true,"multiline":false,"fileTypes":[],"file_path":"","password":false,"name":"memory","display_name":"Memory","advanced":false,"dynamic":false,"info":"","title_case":false},"tools":{"type":"Tool","required":true,"placeholder":"","list":true,"show":true,"multiline":false,"fileTypes":[],"file_path":"","password":false,"name":"tools","display_name":"Tools","advanced":false,"dynamic":false,"info":"","title_case":false},"agent":{"type":"str","required":true,"placeholder":"","list":true,"show":true,"multiline":false,"value":"zero-shot-react-description","fileTypes":[],"file_path":"","password":false,"options":["zero-shot-react-description","react-docstore","self-ask-with-search","conversational-react-description","chat-zero-shot-react-description","chat-conversational-react-description","structured-chat-zero-shot-react-description","openai-functions","openai-multi-functions","JsonAgent","CSVAgent","VectorStoreAgent","VectorStoreRouterAgent","SQLAgent"],"name":"agent","display_name":"Agent Type","advanced":false,"dynamic":false,"info":"","title_case":false,"input_types":["Text"]},"code":{"type":"code","required":true,"placeholder":"","list":false,"show":true,"multiline":true,"value":"from typing import Callable, List, Optional, Union\n\nfrom langchain.agents import AgentExecutor, AgentType, initialize_agent, types\nfrom langflow import CustomComponent\nfrom langflow.field_typing import BaseChatMemory, BaseLanguageModel, Tool\n\n\nclass AgentInitializerComponent(CustomComponent):\n display_name: str = \"Agent Initializer\"\n description: str = \"Initialize a Langchain Agent.\"\n documentation: str = \"https://python.langchain.com/docs/modules/agents/agent_types/\"\n\n def build_config(self):\n agents = list(types.AGENT_TO_CLASS.keys())\n # field_type and required are optional\n return {\n \"agent\": {\"options\": agents, \"value\": agents[0], \"display_name\": \"Agent Type\"},\n \"max_iterations\": {\"display_name\": \"Max Iterations\", \"value\": 10},\n \"memory\": {\"display_name\": \"Memory\"},\n \"tools\": {\"display_name\": \"Tools\"},\n \"llm\": {\"display_name\": \"Language Model\"},\n \"code\": {\"advanced\": True},\n }\n\n def build(\n self,\n agent: str,\n llm: BaseLanguageModel,\n tools: List[Tool],\n max_iterations: int,\n memory: Optional[BaseChatMemory] = None,\n ) -> Union[AgentExecutor, Callable]:\n agent = AgentType(agent)\n if memory:\n return initialize_agent(\n tools=tools,\n llm=llm,\n agent=agent,\n memory=memory,\n return_intermediate_steps=True,\n handle_parsing_errors=True,\n max_iterations=max_iterations,\n )\n return initialize_agent(\n tools=tools,\n llm=llm,\n agent=agent,\n return_intermediate_steps=True,\n handle_parsing_errors=True,\n max_iterations=max_iterations,\n )\n","fileTypes":[],"file_path":"","password":false,"name":"code","advanced":true,"dynamic":true,"info":"","title_case":false},"max_iterations":{"type":"int","required":true,"placeholder":"","list":false,"show":true,"multiline":false,"value":10,"fileTypes":[],"file_path":"","password":false,"name":"max_iterations","display_name":"Max Iterations","advanced":false,"dynamic":false,"info":"","title_case":false},"_type":"CustomComponent"},"description":"Initialize a Langchain Agent.","base_classes":["Runnable","Chain","Serializable","object","AgentExecutor","Generic","RunnableSerializable","Callable"],"display_name":"Agent Initializer","documentation":"https://python.langchain.com/docs/modules/agents/agent_types/","custom_fields":{"agent":null,"llm":null,"tools":null,"max_iterations":null,"memory":null},"output_types":["AgentExecutor","Callable"],"field_formatters":{},"pinned":false,"beta":true},"id":"AgentInitializer-tPdJw"},"selected":false,"width":384,"height":522},{"id":"ChatOpenAISpecs-stxRM","type":"genericNode","position":{"x":18.226716205350385,"y":432.6122491402193},"data":{"type":"ChatOpenAISpecs","node":{"template":{"code":{"type":"code","required":true,"placeholder":"","list":false,"show":true,"multiline":true,"value":"from typing import Optional, Union\n\nfrom langchain.llms import BaseLLM\nfrom langchain_community.chat_models.openai import ChatOpenAI\nfrom langflow import CustomComponent\nfrom langflow.field_typing import BaseLanguageModel, NestedDict\n\n\nclass ChatOpenAIComponent(CustomComponent):\n display_name = \"ChatOpenAI\"\n description = \"`OpenAI` Chat large language models API.\"\n icon = \"OpenAI\"\n\n def build_config(self):\n return {\n \"max_tokens\": {\n \"display_name\": \"Max Tokens\",\n \"advanced\": False,\n \"required\": False,\n },\n \"model_kwargs\": {\n \"display_name\": \"Model Kwargs\",\n \"advanced\": True,\n \"required\": False,\n },\n \"model_name\": {\n \"display_name\": \"Model Name\",\n \"advanced\": False,\n \"required\": False,\n \"options\": [\n \"gpt-4-turbo-preview\",\n \"gpt-4-0125-preview\",\n \"gpt-4-1106-preview\",\n \"gpt-4-vision-preview\",\n \"gpt-3.5-turbo-0125\",\n \"gpt-3.5-turbo-1106\",\n ],\n },\n \"openai_api_base\": {\n \"display_name\": \"OpenAI API Base\",\n \"advanced\": False,\n \"required\": False,\n \"info\": (\n \"The base URL of the OpenAI API. Defaults to https://api.openai.com/v1.\\n\\n\"\n \"You can change this to use other APIs like JinaChat, LocalAI and Prem.\"\n ),\n },\n \"openai_api_key\": {\n \"display_name\": \"OpenAI API Key\",\n \"advanced\": False,\n \"required\": False,\n \"password\": True,\n },\n \"temperature\": {\n \"display_name\": \"Temperature\",\n \"advanced\": False,\n \"required\": False,\n \"value\": 0.7,\n },\n }\n\n def build(\n self,\n max_tokens: Optional[int] = 256,\n model_kwargs: NestedDict = {},\n model_name: str = \"gpt-4-1106-preview\",\n openai_api_base: Optional[str] = None,\n openai_api_key: Optional[str] = None,\n temperature: float = 0.7,\n ) -> Union[BaseLanguageModel, BaseLLM]:\n if not openai_api_base:\n openai_api_base = \"https://api.openai.com/v1\"\n return ChatOpenAI(\n max_tokens=max_tokens,\n model_kwargs=model_kwargs,\n model=model_name,\n base_url=openai_api_base,\n api_key=openai_api_key,\n temperature=temperature,\n )\n","fileTypes":[],"file_path":"","password":false,"name":"code","advanced":false,"dynamic":true,"info":"","title_case":false},"max_tokens":{"type":"int","required":false,"placeholder":"","list":false,"show":true,"multiline":false,"value":256,"fileTypes":[],"file_path":"","password":false,"name":"max_tokens","display_name":"Max Tokens","advanced":false,"dynamic":false,"info":"","title_case":false},"model_kwargs":{"type":"NestedDict","required":false,"placeholder":"","list":false,"show":true,"multiline":false,"value":{},"fileTypes":[],"file_path":"","password":false,"name":"model_kwargs","display_name":"Model Kwargs","advanced":true,"dynamic":false,"info":"","title_case":false},"model_name":{"type":"str","required":false,"placeholder":"","list":true,"show":true,"multiline":false,"value":"gpt-4-1106-preview","fileTypes":[],"file_path":"","password":false,"options":["gpt-4-turbo-preview","gpt-4-0125-preview","gpt-4-1106-preview","gpt-4-vision-preview","gpt-3.5-turbo-0125","gpt-3.5-turbo-1106"],"name":"model_name","display_name":"Model Name","advanced":false,"dynamic":false,"info":"","title_case":false,"input_types":["Text"]},"openai_api_base":{"type":"str","required":false,"placeholder":"","list":false,"show":true,"multiline":false,"fileTypes":[],"file_path":"","password":false,"name":"openai_api_base","display_name":"OpenAI API Base","advanced":false,"dynamic":false,"info":"The base URL of the OpenAI API. Defaults to https://api.openai.com/v1.\n\nYou can change this to use other APIs like JinaChat, LocalAI and Prem.","title_case":false,"input_types":["Text"]},"openai_api_key":{"type":"str","required":false,"placeholder":"","list":false,"show":true,"multiline":false,"fileTypes":[],"file_path":"","password":true,"name":"openai_api_key","display_name":"OpenAI API Key","advanced":false,"dynamic":false,"info":"","title_case":false,"input_types":["Text"]},"temperature":{"type":"float","required":false,"placeholder":"","list":false,"show":true,"multiline":false,"value":0.7,"fileTypes":[],"file_path":"","password":false,"name":"temperature","display_name":"Temperature","advanced":false,"dynamic":false,"info":"","rangeSpec":{"min":-1,"max":1,"step":0.1},"title_case":false},"_type":"CustomComponent"},"description":"`OpenAI` Chat large language models API.","icon":"OpenAI","base_classes":["Runnable","BaseLLM","Serializable","BaseLanguageModel","object","Generic","RunnableSerializable"],"display_name":"ChatOpenAI","documentation":"","custom_fields":{"max_tokens":null,"model_kwargs":null,"model_name":null,"openai_api_base":null,"openai_api_key":null,"temperature":null},"output_types":["BaseLanguageModel","BaseLLM"],"field_formatters":{},"pinned":false,"beta":true},"id":"ChatOpenAISpecs-stxRM"},"selected":false,"width":384,"height":666,"positionAbsolute":{"x":18.226716205350385,"y":432.6122491402193},"dragging":false}],"edges":[{"source":"ChatOpenAISpecs-stxRM","sourceHandle":"{œbaseClassesœ:[œRunnableœ,œBaseLLMœ,œSerializableœ,œBaseLanguageModelœ,œobjectœ,œGenericœ,œRunnableSerializableœ],œdataTypeœ:œChatOpenAISpecsœ,œidœ:œChatOpenAISpecs-stxRMœ}","target":"AgentInitializer-tPdJw","targetHandle":"{œfieldNameœ:œllmœ,œidœ:œAgentInitializer-tPdJwœ,œinputTypesœ:null,œtypeœ:œBaseLanguageModelœ}","data":{"targetHandle":{"fieldName":"llm","id":"AgentInitializer-tPdJw","inputTypes":null,"type":"BaseLanguageModel"},"sourceHandle":{"baseClasses":["Runnable","BaseLLM","Serializable","BaseLanguageModel","object","Generic","RunnableSerializable"],"dataType":"ChatOpenAISpecs","id":"ChatOpenAISpecs-stxRM"}},"style":{"stroke":"#555"},"className":"stroke-foreground stroke-connection","id":"reactflow__edge-ChatOpenAISpecs-stxRM{œbaseClassesœ:[œRunnableœ,œBaseLLMœ,œSerializableœ,œBaseLanguageModelœ,œobjectœ,œGenericœ,œRunnableSerializableœ],œdataTypeœ:œChatOpenAISpecsœ,œidœ:œChatOpenAISpecs-stxRMœ}-AgentInitializer-tPdJw{œfieldNameœ:œllmœ,œidœ:œAgentInitializer-tPdJwœ,œinputTypesœ:null,œtypeœ:œBaseLanguageModelœ}"},{"source":"PythonFunctionTool-RfJui","sourceHandle":"{œbaseClassesœ:[œBaseToolœ,œToolœ],œdataTypeœ:œPythonFunctionToolœ,œidœ:œPythonFunctionTool-RfJuiœ}","target":"AgentInitializer-tPdJw","targetHandle":"{œfieldNameœ:œtoolsœ,œidœ:œAgentInitializer-tPdJwœ,œinputTypesœ:null,œtypeœ:œToolœ}","data":{"targetHandle":{"fieldName":"tools","id":"AgentInitializer-tPdJw","inputTypes":null,"type":"Tool"},"sourceHandle":{"baseClasses":["BaseTool","Tool"],"dataType":"PythonFunctionTool","id":"PythonFunctionTool-RfJui"}},"style":{"stroke":"#555"},"className":"stroke-foreground stroke-connection","id":"reactflow__edge-PythonFunctionTool-RfJui{œbaseClassesœ:[œBaseToolœ,œToolœ],œdataTypeœ:œPythonFunctionToolœ,œidœ:œPythonFunctionTool-RfJuiœ}-AgentInitializer-tPdJw{œfieldNameœ:œtoolsœ,œidœ:œAgentInitializer-tPdJwœ,œinputTypesœ:null,œtypeœ:œToolœ}"}],"viewport":{"x":37.63043052737157,"y":71.47518177614131,"zoom":0.5140569133280332}},"description":"Uncover Business Opportunities with NLP.","name":"Untitled document (20)","last_tested_version":"0.7.0a0","is_component":false} \ No newline at end of file +{ + "id": "8404c1fc-1bce-43b4-a8bc-3febea587fc8", + "data": { + "nodes": [ + { + "id": "PythonFunctionTool-RfJui", + "type": "genericNode", + "position": { "x": 117.54690105175428, "y": -84.2465475108354 }, + "data": { + "type": "PythonFunctionTool", + "node": { + "template": { + "code": { + "type": "code", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "\ndef python_function(text: str) -> str:\n \"\"\"This is a default python function that returns the input text\"\"\"\n return text\n", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "code", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false + }, + "description": { + "type": "str", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "Returns the Text you send. This is a testing tool.", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "description", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": ["Text"] + }, + "name": { + "type": "str", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": "PythonFunction", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "name", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": ["Text"] + }, + "return_direct": { + "type": "bool", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "return_direct", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false + }, + "_type": "PythonFunctionTool" + }, + "description": "Python function to be executed.", + "base_classes": ["BaseTool", "Tool"], + "display_name": "PythonFunctionTool", + "documentation": "", + "custom_fields": {}, + "output_types": [], + "field_formatters": {}, + "pinned": false, + "beta": false + }, + "id": "PythonFunctionTool-RfJui" + }, + "selected": true, + "width": 384, + "height": 466, + "positionAbsolute": { "x": 117.54690105175428, "y": -84.2465475108354 }, + "dragging": false + }, + { + "id": "AgentInitializer-tPdJw", + "type": "genericNode", + "position": { "x": 677.68677055088, "y": 127.19859565276168 }, + "data": { + "type": "AgentInitializer", + "node": { + "template": { + "llm": { + "type": "BaseLanguageModel", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "llm", + "display_name": "Language Model", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false + }, + "memory": { + "type": "BaseChatMemory", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "memory", + "display_name": "Memory", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false + }, + "tools": { + "type": "Tool", + "required": true, + "placeholder": "", + "list": true, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "tools", + "display_name": "Tools", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false + }, + "agent": { + "type": "str", + "required": true, + "placeholder": "", + "list": true, + "show": true, + "multiline": false, + "value": "zero-shot-react-description", + "fileTypes": [], + "file_path": "", + "password": false, + "options": [ + "zero-shot-react-description", + "react-docstore", + "self-ask-with-search", + "conversational-react-description", + "chat-zero-shot-react-description", + "chat-conversational-react-description", + "structured-chat-zero-shot-react-description", + "openai-functions", + "openai-multi-functions", + "JsonAgent", + "CSVAgent", + "VectorStoreAgent", + "VectorStoreRouterAgent", + "SQLAgent" + ], + "name": "agent", + "display_name": "Agent Type", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": ["Text"] + }, + "code": { + "type": "code", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "from typing import Callable, List, Optional, Union\n\nfrom langchain.agents import AgentExecutor, AgentType, initialize_agent, types\nfrom langflow import CustomComponent\nfrom langflow.field_typing import BaseChatMemory, BaseLanguageModel, Tool\n\n\nclass AgentInitializerComponent(CustomComponent):\n display_name: str = \"Agent Initializer\"\n description: str = \"Initialize a Langchain Agent.\"\n documentation: str = \"https://python.langchain.com/docs/modules/agents/agent_types/\"\n\n def build_config(self):\n agents = list(types.AGENT_TO_CLASS.keys())\n # field_type and required are optional\n return {\n \"agent\": {\"options\": agents, \"value\": agents[0], \"display_name\": \"Agent Type\"},\n \"max_iterations\": {\"display_name\": \"Max Iterations\", \"value\": 10},\n \"memory\": {\"display_name\": \"Memory\"},\n \"tools\": {\"display_name\": \"Tools\"},\n \"llm\": {\"display_name\": \"Language Model\"},\n \"code\": {\"advanced\": True},\n }\n\n def build(\n self,\n agent: str,\n llm: BaseLanguageModel,\n tools: List[Tool],\n max_iterations: int,\n memory: Optional[BaseChatMemory] = None,\n ) -> Union[AgentExecutor, Callable]:\n agent = AgentType(agent)\n if memory:\n return initialize_agent(\n tools=tools,\n llm=llm,\n agent=agent,\n memory=memory,\n return_intermediate_steps=True,\n handle_parsing_errors=True,\n max_iterations=max_iterations,\n )\n return initialize_agent(\n tools=tools,\n llm=llm,\n agent=agent,\n return_intermediate_steps=True,\n handle_parsing_errors=True,\n max_iterations=max_iterations,\n )\n", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "code", + "advanced": true, + "dynamic": true, + "info": "", + "title_case": false + }, + "max_iterations": { + "type": "int", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": 10, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "max_iterations", + "display_name": "Max Iterations", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false + }, + "_type": "CustomComponent" + }, + "description": "Initialize a Langchain Agent.", + "base_classes": [ + "Runnable", + "Chain", + "Serializable", + "object", + "AgentExecutor", + "Generic", + "RunnableSerializable", + "Callable" + ], + "display_name": "Agent Initializer", + "documentation": "https://python.langchain.com/docs/modules/agents/agent_types/", + "custom_fields": { + "agent": null, + "llm": null, + "tools": null, + "max_iterations": null, + "memory": null + }, + "output_types": ["AgentExecutor", "Callable"], + "field_formatters": {}, + "pinned": false, + "beta": true + }, + "id": "AgentInitializer-tPdJw" + }, + "selected": false, + "width": 384, + "height": 522 + }, + { + "id": "ChatOpenAISpecs-stxRM", + "type": "genericNode", + "position": { "x": 18.226716205350385, "y": 432.6122491402193 }, + "data": { + "type": "ChatOpenAISpecs", + "node": { + "template": { + "code": { + "type": "code", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "from typing import Optional, Union\n\nfrom langchain.llms import BaseLLM\nfrom langchain_community.chat_models.openai import ChatOpenAI\nfrom langflow import CustomComponent\nfrom langflow.field_typing import BaseLanguageModel, NestedDict\n\n\nclass ChatOpenAIComponent(CustomComponent):\n display_name = \"ChatOpenAI\"\n description = \"`OpenAI` Chat large language models API.\"\n icon = \"OpenAI\"\n\n def build_config(self):\n return {\n \"max_tokens\": {\n \"display_name\": \"Max Tokens\",\n \"advanced\": False,\n \"required\": False,\n },\n \"model_kwargs\": {\n \"display_name\": \"Model Kwargs\",\n \"advanced\": True,\n \"required\": False,\n },\n \"model_name\": {\n \"display_name\": \"Model Name\",\n \"advanced\": False,\n \"required\": False,\n \"options\": [\n \"gpt-4-turbo-preview\",\n \"gpt-4-0125-preview\",\n \"gpt-4-1106-preview\",\n \"gpt-4-vision-preview\",\n \"gpt-3.5-turbo-0125\",\n \"gpt-3.5-turbo-1106\",\n ],\n },\n \"openai_api_base\": {\n \"display_name\": \"OpenAI API Base\",\n \"advanced\": False,\n \"required\": False,\n \"info\": (\n \"The base URL of the OpenAI API. Defaults to https://api.openai.com/v1.\\n\\n\"\n \"You can change this to use other APIs like JinaChat, LocalAI and Prem.\"\n ),\n },\n \"openai_api_key\": {\n \"display_name\": \"OpenAI API Key\",\n \"advanced\": False,\n \"required\": False,\n \"password\": True,\n },\n \"temperature\": {\n \"display_name\": \"Temperature\",\n \"advanced\": False,\n \"required\": False,\n \"value\": 0.7,\n },\n }\n\n def build(\n self,\n max_tokens: Optional[int] = 256,\n model_kwargs: NestedDict = {},\n model_name: str = \"gpt-4-1106-preview\",\n openai_api_base: Optional[str] = None,\n openai_api_key: Optional[str] = None,\n temperature: float = 0.7,\n ) -> Union[BaseLanguageModel, BaseLLM]:\n if not openai_api_base:\n openai_api_base = \"https://api.openai.com/v1\"\n return ChatOpenAI(\n max_tokens=max_tokens,\n model_kwargs=model_kwargs,\n model=model_name,\n base_url=openai_api_base,\n api_key=openai_api_key,\n temperature=temperature,\n )\n", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "code", + "advanced": false, + "dynamic": true, + "info": "", + "title_case": false + }, + "max_tokens": { + "type": "int", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": 256, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "max_tokens", + "display_name": "Max Tokens", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false + }, + "model_kwargs": { + "type": "NestedDict", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": {}, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "model_kwargs", + "display_name": "Model Kwargs", + "advanced": true, + "dynamic": false, + "info": "", + "title_case": false + }, + "model_name": { + "type": "str", + "required": false, + "placeholder": "", + "list": true, + "show": true, + "multiline": false, + "value": "gpt-4-1106-preview", + "fileTypes": [], + "file_path": "", + "password": false, + "options": [ + "gpt-4-turbo-preview", + "gpt-4-0125-preview", + "gpt-4-1106-preview", + "gpt-4-vision-preview", + "gpt-3.5-turbo-0125", + "gpt-3.5-turbo-1106" + ], + "name": "model_name", + "display_name": "Model Name", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": ["Text"] + }, + "openai_api_base": { + "type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "openai_api_base", + "display_name": "OpenAI API Base", + "advanced": false, + "dynamic": false, + "info": "The base URL of the OpenAI API. Defaults to https://api.openai.com/v1.\n\nYou can change this to use other APIs like JinaChat, LocalAI and Prem.", + "title_case": false, + "input_types": ["Text"] + }, + "openai_api_key": { + "type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": true, + "name": "openai_api_key", + "display_name": "OpenAI API Key", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": ["Text"] + }, + "temperature": { + "type": "float", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": 0.7, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "temperature", + "display_name": "Temperature", + "advanced": false, + "dynamic": false, + "info": "", + "rangeSpec": { "min": -1, "max": 1, "step": 0.1 }, + "title_case": false + }, + "_type": "CustomComponent" + }, + "description": "`OpenAI` Chat large language models API.", + "icon": "OpenAI", + "base_classes": [ + "Runnable", + "BaseLLM", + "Serializable", + "BaseLanguageModel", + "object", + "Generic", + "RunnableSerializable" + ], + "display_name": "ChatOpenAI", + "documentation": "", + "custom_fields": { + "max_tokens": null, + "model_kwargs": null, + "model_name": null, + "openai_api_base": null, + "openai_api_key": null, + "temperature": null + }, + "output_types": ["BaseLanguageModel", "BaseLLM"], + "field_formatters": {}, + "pinned": false, + "beta": true + }, + "id": "ChatOpenAISpecs-stxRM" + }, + "selected": false, + "width": 384, + "height": 666, + "positionAbsolute": { "x": 18.226716205350385, "y": 432.6122491402193 }, + "dragging": false + } + ], + "edges": [ + { + "source": "ChatOpenAISpecs-stxRM", + "sourceHandle": "{œbaseClassesœ:[œRunnableœ,œBaseLLMœ,œSerializableœ,œBaseLanguageModelœ,œobjectœ,œGenericœ,œRunnableSerializableœ],œdataTypeœ:œChatOpenAISpecsœ,œidœ:œChatOpenAISpecs-stxRMœ}", + "target": "AgentInitializer-tPdJw", + "targetHandle": "{œfieldNameœ:œllmœ,œidœ:œAgentInitializer-tPdJwœ,œinputTypesœ:null,œtypeœ:œBaseLanguageModelœ}", + "data": { + "targetHandle": { + "fieldName": "llm", + "id": "AgentInitializer-tPdJw", + "inputTypes": null, + "type": "BaseLanguageModel" + }, + "sourceHandle": { + "baseClasses": [ + "Runnable", + "BaseLLM", + "Serializable", + "BaseLanguageModel", + "object", + "Generic", + "RunnableSerializable" + ], + "dataType": "ChatOpenAISpecs", + "id": "ChatOpenAISpecs-stxRM" + } + }, + "style": { "stroke": "#555" }, + "className": "stroke-foreground stroke-connection", + "id": "reactflow__edge-ChatOpenAISpecs-stxRM{œbaseClassesœ:[œRunnableœ,œBaseLLMœ,œSerializableœ,œBaseLanguageModelœ,œobjectœ,œGenericœ,œRunnableSerializableœ],œdataTypeœ:œChatOpenAISpecsœ,œidœ:œChatOpenAISpecs-stxRMœ}-AgentInitializer-tPdJw{œfieldNameœ:œllmœ,œidœ:œAgentInitializer-tPdJwœ,œinputTypesœ:null,œtypeœ:œBaseLanguageModelœ}" + }, + { + "source": "PythonFunctionTool-RfJui", + "sourceHandle": "{œbaseClassesœ:[œBaseToolœ,œToolœ],œdataTypeœ:œPythonFunctionToolœ,œidœ:œPythonFunctionTool-RfJuiœ}", + "target": "AgentInitializer-tPdJw", + "targetHandle": "{œfieldNameœ:œtoolsœ,œidœ:œAgentInitializer-tPdJwœ,œinputTypesœ:null,œtypeœ:œToolœ}", + "data": { + "targetHandle": { + "fieldName": "tools", + "id": "AgentInitializer-tPdJw", + "inputTypes": null, + "type": "Tool" + }, + "sourceHandle": { + "baseClasses": ["BaseTool", "Tool"], + "dataType": "PythonFunctionTool", + "id": "PythonFunctionTool-RfJui" + } + }, + "style": { "stroke": "#555" }, + "className": "stroke-foreground stroke-connection", + "id": "reactflow__edge-PythonFunctionTool-RfJui{œbaseClassesœ:[œBaseToolœ,œToolœ],œdataTypeœ:œPythonFunctionToolœ,œidœ:œPythonFunctionTool-RfJuiœ}-AgentInitializer-tPdJw{œfieldNameœ:œtoolsœ,œidœ:œAgentInitializer-tPdJwœ,œinputTypesœ:null,œtypeœ:œToolœ}" + } + ], + "viewport": { + "x": 37.63043052737157, + "y": 71.47518177614131, + "zoom": 0.5140569133280332 + } + }, + "description": "Uncover Business Opportunities with NLP.", + "name": "Untitled document (20)", + "last_tested_version": "0.7.0a0", + "is_component": false +} diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index e133844d2..6a2f9cff4 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -29,10 +29,7 @@ def poll_task_status(client, headers, href, max_attempts=20, sleep_time=1): href, headers=headers, ) - if ( - task_status_response.status_code == 200 - and task_status_response.json()["status"] == "SUCCESS" - ): + if task_status_response.status_code == 200 and task_status_response.json()["status"] == "SUCCESS": return task_status_response.json() time.sleep(sleep_time) return None # Return None if task did not complete in time @@ -126,11 +123,7 @@ def created_api_key(active_user): ) db_manager = get_db_service() with session_getter(db_manager) as session: - if ( - existing_api_key := session.query(ApiKey) - .filter(ApiKey.api_key == api_key.api_key) - .first() - ): + if existing_api_key := session.query(ApiKey).filter(ApiKey.api_key == api_key.api_key).first(): return existing_api_key session.add(api_key) session.commit() @@ -296,11 +289,7 @@ def test_get_all(client: TestClient, logged_in_headers): dir_reader = DirectoryReader(settings.COMPONENTS_PATH[0]) files = dir_reader.get_files() # json_response is a dict of dicts - all_names = [ - component_name - for _, components in response.json().items() - for component_name in components - ] + all_names = [component_name for _, components in response.json().items() for component_name in components] json_response = response.json() # We need to test the custom nodes assert len(all_names) > len(files) @@ -425,19 +414,13 @@ def test_various_prompts(client, prompt, expected_input_variables): def test_get_vertices_flow_not_found(client, logged_in_headers): - response = client.get( - "/api/v1/build/nonexistent_id/vertices", headers=logged_in_headers - ) - assert ( - response.status_code == 500 - ) # Or whatever status code you've set for invalid ID + response = client.get("/api/v1/build/nonexistent_id/vertices", headers=logged_in_headers) + assert response.status_code == 500 # Or whatever status code you've set for invalid ID def test_get_vertices(client, added_flow_with_prompt_and_history, logged_in_headers): flow_id = added_flow_with_prompt_and_history["id"] - response = client.get( - f"/api/v1/build/{flow_id}/vertices", headers=logged_in_headers - ) + response = client.get(f"/api/v1/build/{flow_id}/vertices", headers=logged_in_headers) assert response.status_code == 200 assert "ids" in response.json() # The response should contain the list in this order @@ -453,19 +436,13 @@ def test_get_vertices(client, added_flow_with_prompt_and_history, logged_in_head def test_build_vertex_invalid_flow_id(client, logged_in_headers): - response = client.post( - "/api/v1/build/nonexistent_id/vertices/vertex_id", headers=logged_in_headers - ) + response = client.post("/api/v1/build/nonexistent_id/vertices/vertex_id", headers=logged_in_headers) assert response.status_code == 500 -def test_build_vertex_invalid_vertex_id( - client, added_flow_with_prompt_and_history, logged_in_headers -): +def test_build_vertex_invalid_vertex_id(client, added_flow_with_prompt_and_history, logged_in_headers): flow_id = added_flow_with_prompt_and_history["id"] - response = client.post( - f"/api/v1/build/{flow_id}/vertices/invalid_vertex_id", headers=logged_in_headers - ) + response = client.post(f"/api/v1/build/{flow_id}/vertices/invalid_vertex_id", headers=logged_in_headers) assert response.status_code == 500 From 9f1d2878df7c6c54ff9282ebec6ede0cb8a6a851 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Wed, 6 Mar 2024 14:56:56 -0300 Subject: [PATCH 145/199] Add emoji support to CollectionCardComponent --- .../src/components/exampleComponent/index.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/frontend/src/components/exampleComponent/index.tsx b/src/frontend/src/components/exampleComponent/index.tsx index fa021b299..5618ff996 100644 --- a/src/frontend/src/components/exampleComponent/index.tsx +++ b/src/frontend/src/components/exampleComponent/index.tsx @@ -30,6 +30,8 @@ export default function CollectionCardComponent({ }) { const addFlow = useFlowsManagerStore((state) => state.addFlow); const navigate = useNavigate(); + const emojiRegex = /\p{Emoji}/u; + const isEmoji= (str:string)=> emojiRegex.test(str); return (
- {flow.icon && ( -
- {flow.icon} + {flow.icon && isEmoji(flow.icon) && ( +
+ +
+ {flow.icon} +
+
- )} - {!flow.icon && }
{flow.name}
From f44776934d3ba4d85050627bd92882ace4371354 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Wed, 6 Mar 2024 15:05:53 -0300 Subject: [PATCH 146/199] Refactor CollectionCardComponent to handle flow icons and backgrounds --- .../src/components/exampleComponent/index.tsx | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/frontend/src/components/exampleComponent/index.tsx b/src/frontend/src/components/exampleComponent/index.tsx index 95a602db1..0473a234e 100644 --- a/src/frontend/src/components/exampleComponent/index.tsx +++ b/src/frontend/src/components/exampleComponent/index.tsx @@ -15,10 +15,10 @@ import { } from "../ui/card"; export default function CollectionCardComponent({ - flow, + flow, }: { - flow: FlowType; - authorized?: boolean; + flow: FlowType; + authorized?: boolean; }) { const addFlow = useFlowsManagerStore((state) => state.addFlow); const navigate = useNavigate(); @@ -35,18 +35,25 @@ export default function CollectionCardComponent({
- {flow.icon && ( -
- {flow.icon} + {flow.icon && isEmoji(flow.icon) && ( +
+ +
+ {flow.icon} +
)} - {!flow.icon && } + {(!flow.icon || !isEmoji(flow.icon)) && +
+ +
+ }
{flow.name}
From 4286b6a93e9758c57c18fd823f99462291f89a2d Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 15:19:36 -0300 Subject: [PATCH 147/199] Refactor flow retrieval logic in read_flows function --- src/backend/langflow/api/v1/flows.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/backend/langflow/api/v1/flows.py b/src/backend/langflow/api/v1/flows.py index 069d98a9e..19bd86555 100644 --- a/src/backend/langflow/api/v1/flows.py +++ b/src/backend/langflow/api/v1/flows.py @@ -57,7 +57,11 @@ def read_flows( try: auth_settings = settings_service.auth_settings if auth_settings.AUTO_LOGIN: - flows = session.exec(select(Flow).where(Flow.user_id == None | Flow.user_id == current_user.id)).all() # noqa + flows = session.exec( + select(Flow).where( + (Flow.user_id == None) | (Flow.user_id == current_user.id) + ) + ).all() # noqa else: flows = current_user.flows From f735e50fd25e1e05064073440519b534618f155a Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 15:32:50 -0300 Subject: [PATCH 148/199] Refactor RecursiveCharacterTextSplitterComponent to use build_loader_repr_from_records --- .../RecursiveCharacterTextSplitter.py | 11 ++- src/backend/langflow/utils/util.py | 76 +++++++++++++------ 2 files changed, 59 insertions(+), 28 deletions(-) diff --git a/src/backend/langflow/components/textsplitters/RecursiveCharacterTextSplitter.py b/src/backend/langflow/components/textsplitters/RecursiveCharacterTextSplitter.py index c523a2921..508cb0023 100644 --- a/src/backend/langflow/components/textsplitters/RecursiveCharacterTextSplitter.py +++ b/src/backend/langflow/components/textsplitters/RecursiveCharacterTextSplitter.py @@ -5,13 +5,15 @@ from langchain_core.documents import Document from langflow import CustomComponent from langflow.schema import Record -from langflow.utils.util import build_loader_repr_from_documents +from langflow.utils.util import build_loader_repr_from_records class RecursiveCharacterTextSplitterComponent(CustomComponent): display_name: str = "Recursive Character Text Splitter" description: str = "Split text into chunks of a specified length." - documentation: str = "https://docs.langflow.org/components/text-splitters#recursivecharactertextsplitter" + documentation: str = ( + "https://docs.langflow.org/components/text-splitters#recursivecharactertextsplitter" + ) def build_config(self): return { @@ -84,5 +86,6 @@ class RecursiveCharacterTextSplitterComponent(CustomComponent): else: documents.append(_input) docs = splitter.split_documents(documents) - self.repr_value = build_loader_repr_from_documents(docs) - return self.to_records(docs) + records = self.to_records(docs) + self.repr_value = build_loader_repr_from_records(records) + return records diff --git a/src/backend/langflow/utils/util.py b/src/backend/langflow/utils/util.py index 814a9d4ab..af4f02b59 100644 --- a/src/backend/langflow/utils/util.py +++ b/src/backend/langflow/utils/util.py @@ -5,8 +5,8 @@ from functools import wraps from typing import Any, Dict, List, Optional, Union from docstring_parser import parse -from langchain_core.documents import Document +from langflow.schema.schema import Record from langflow.template.frontend_node.constants import FORCE_SHOW_FIELDS from langflow.utils import constants @@ -15,8 +15,12 @@ def remove_ansi_escape_codes(text): return re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", text) -def build_template_from_function(name: str, type_to_loader_dict: Dict, add_function: bool = False): - classes = [item.__annotations__["return"].__name__ for item in type_to_loader_dict.values()] +def build_template_from_function( + name: str, type_to_loader_dict: Dict, add_function: bool = False +): + classes = [ + item.__annotations__["return"].__name__ for item in type_to_loader_dict.values() + ] # Raise error if name is not in chains if name not in classes: @@ -37,8 +41,10 @@ def build_template_from_function(name: str, type_to_loader_dict: Dict, add_funct for name_, value_ in value.__repr_args__(): if name_ == "default_factory": try: - variables[class_field_items]["default"] = get_default_factory( - module=_class.__base__.__module__, function=value_ + variables[class_field_items]["default"] = ( + get_default_factory( + module=_class.__base__.__module__, function=value_ + ) ) except Exception: variables[class_field_items]["default"] = None @@ -46,7 +52,9 @@ def build_template_from_function(name: str, type_to_loader_dict: Dict, add_funct variables[class_field_items][name_] = value_ variables[class_field_items]["placeholder"] = ( - docs.params[class_field_items] if class_field_items in docs.params else "" + docs.params[class_field_items] + if class_field_items in docs.params + else "" ) # Adding function to base classes to allow # the output to be a function @@ -61,7 +69,9 @@ def build_template_from_function(name: str, type_to_loader_dict: Dict, add_funct } -def build_template_from_class(name: str, type_to_cls_dict: Dict, add_function: bool = False): +def build_template_from_class( + name: str, type_to_cls_dict: Dict, add_function: bool = False +): classes = [item.__name__ for item in type_to_cls_dict.values()] # Raise error if name is not in chains @@ -85,9 +95,11 @@ def build_template_from_class(name: str, type_to_cls_dict: Dict, add_function: b for name_, value_ in value.__repr_args__(): if name_ == "default_factory": try: - variables[class_field_items]["default"] = get_default_factory( - module=_class.__base__.__module__, - function=value_, + variables[class_field_items]["default"] = ( + get_default_factory( + module=_class.__base__.__module__, + function=value_, + ) ) except Exception: variables[class_field_items]["default"] = None @@ -95,7 +107,9 @@ def build_template_from_class(name: str, type_to_cls_dict: Dict, add_function: b variables[class_field_items][name_] = value_ variables[class_field_items]["placeholder"] = ( - docs.params[class_field_items] if class_field_items in docs.params else "" + docs.params[class_field_items] + if class_field_items in docs.params + else "" ) base_classes = get_base_classes(_class) # Adding function to base classes to allow @@ -127,7 +141,9 @@ def build_template_from_method( # Check if the method exists in this class if not hasattr(_class, method_name): - raise ValueError(f"Method {method_name} not found in class {class_name}") + raise ValueError( + f"Method {method_name} not found in class {class_name}" + ) # Get the method method = getattr(_class, method_name) @@ -146,8 +162,14 @@ def build_template_from_method( "_type": _type, **{ name: { - "default": (param.default if param.default != param.empty else None), - "type": (param.annotation if param.annotation != param.empty else None), + "default": ( + param.default if param.default != param.empty else None + ), + "type": ( + param.annotation + if param.annotation != param.empty + else None + ), "required": param.default == param.empty, } for name, param in params.items() @@ -234,7 +256,9 @@ def sync_to_async(func): return async_wrapper -def format_dict(dictionary: Dict[str, Any], class_name: Optional[str] = None) -> Dict[str, Any]: +def format_dict( + dictionary: Dict[str, Any], class_name: Optional[str] = None +) -> Dict[str, Any]: """ Formats a dictionary by removing certain keys and modifying the values of other keys. @@ -320,7 +344,9 @@ def check_list_type(_type: str, value: Dict[str, Any]) -> str: The modified type string. """ if any(list_type in _type for list_type in ["List", "Sequence", "Set"]): - _type = _type.replace("List[", "").replace("Sequence[", "").replace("Set[", "")[:-1] + _type = ( + _type.replace("List[", "").replace("Sequence[", "").replace("Set[", "")[:-1] + ) value["list"] = True else: value["list"] = False @@ -423,7 +449,9 @@ def set_headers_value(value: Dict[str, Any]) -> None: value["value"] = """{"Authorization": "Bearer "}""" -def add_options_to_field(value: Dict[str, Any], class_name: Optional[str], key: str) -> None: +def add_options_to_field( + value: Dict[str, Any], class_name: Optional[str], key: str +) -> None: """ Adds options to the field based on the class name and key. """ @@ -440,10 +468,10 @@ def add_options_to_field(value: Dict[str, Any], class_name: Optional[str], key: value["value"] = options_map[class_name][0] -def build_loader_repr_from_documents(documents: List[Document]) -> str: - if documents: - avg_length = sum(len(doc.page_content) for doc in documents) / len(documents) - return f"""{len(documents)} documents - \nAvg. Document Length (characters): {int(avg_length)} - Documents: {documents[:3]}...""" - return "0 documents" +def build_loader_repr_from_records(records: List[Record]) -> str: + if records: + avg_length = sum(len(doc.page_content) for doc in records) / len(records) + return f"""{len(records)} records + \nAvg. Record Length (characters): {int(avg_length)} + Records: {records[:3]}...""" + return "0 records" From eef5043045d0495087f1035f38b2e3d173a74a2e Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 15:33:42 -0300 Subject: [PATCH 149/199] Refactor code and update documentation links --- src/backend/langflow/api/v1/flows.py | 10 ++-- .../RecursiveCharacterTextSplitter.py | 4 +- .../custom_component/custom_component.py | 48 +++++++++++---- src/backend/langflow/utils/util.py | 60 +++++-------------- 4 files changed, 59 insertions(+), 63 deletions(-) diff --git a/src/backend/langflow/api/v1/flows.py b/src/backend/langflow/api/v1/flows.py index 19bd86555..dd60d5fed 100644 --- a/src/backend/langflow/api/v1/flows.py +++ b/src/backend/langflow/api/v1/flows.py @@ -59,9 +59,9 @@ def read_flows( if auth_settings.AUTO_LOGIN: flows = session.exec( select(Flow).where( - (Flow.user_id == None) | (Flow.user_id == current_user.id) + (Flow.user_id == None) | (Flow.user_id == current_user.id) # noqa ) - ).all() # noqa + ).all() else: flows = current_user.flows @@ -71,10 +71,10 @@ def read_flows( try: example_flows = session.exec( select(Flow).where( - Flow.user_id == None, - Flow.folder == STARTER_FOLDER_NAME, # noqa + Flow.user_id == None, # noqa + Flow.folder == STARTER_FOLDER_NAME, ) - ).all() # noqa + ).all() for example_flow in example_flows: if example_flow.id not in flow_ids: flows.append(example_flow) diff --git a/src/backend/langflow/components/textsplitters/RecursiveCharacterTextSplitter.py b/src/backend/langflow/components/textsplitters/RecursiveCharacterTextSplitter.py index 508cb0023..6b9cb865b 100644 --- a/src/backend/langflow/components/textsplitters/RecursiveCharacterTextSplitter.py +++ b/src/backend/langflow/components/textsplitters/RecursiveCharacterTextSplitter.py @@ -11,9 +11,7 @@ from langflow.utils.util import build_loader_repr_from_records class RecursiveCharacterTextSplitterComponent(CustomComponent): display_name: str = "Recursive Character Text Splitter" description: str = "Split text into chunks of a specified length." - documentation: str = ( - "https://docs.langflow.org/components/text-splitters#recursivecharactertextsplitter" - ) + documentation: str = "https://docs.langflow.org/components/text-splitters#recursivecharactertextsplitter" def build_config(self): return { diff --git a/src/backend/langflow/interface/custom/custom_component/custom_component.py b/src/backend/langflow/interface/custom/custom_component/custom_component.py index f0c3bfa80..11ec59bf3 100644 --- a/src/backend/langflow/interface/custom/custom_component/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component/custom_component.py @@ -77,13 +77,17 @@ class CustomComponent(Component): def update_state(self, name: str, value: Any): try: - self.vertex.graph.update_state(name=name, record=value, caller=self.vertex.id) + self.vertex.graph.update_state( + name=name, record=value, caller=self.vertex.id + ) except Exception as e: raise ValueError(f"Error updating state: {e}") def append_state(self, name: str, value: Any): try: - self.vertex.graph.append_state(name=name, record=value, caller=self.vertex.id) + self.vertex.graph.append_state( + name=name, record=value, caller=self.vertex.id + ) except Exception as e: raise ValueError(f"Error appending state: {e}") @@ -134,7 +138,9 @@ class CustomComponent(Component): def build_config(self): return self.field_config - def update_build_config(self, build_config: dict, field_name: str, field_value: Any): + def update_build_config( + self, build_config: dict, field_name: str, field_value: Any + ): build_config[field_name] = field_value return build_config @@ -142,7 +148,9 @@ class CustomComponent(Component): def tree(self): return self.get_code_tree(self.code or "") - def to_records(self, data: Any, keys: Optional[List[str]] = None, silent_errors: bool = False) -> List[Record]: + def to_records( + self, data: Any, keys: Optional[List[str]] = None, silent_errors: bool = False + ) -> List[Record]: """ Converts input data into a list of Record objects. @@ -191,7 +199,9 @@ class CustomComponent(Component): return records - def create_references_from_records(self, records: List[Record], include_data: bool = False) -> str: + def create_references_from_records( + self, records: List[Record], include_data: bool = False + ) -> str: """ Create references from a list of records. @@ -230,14 +240,20 @@ class CustomComponent(Component): if not self.code: return {} - component_classes = [cls for cls in self.tree["classes"] if self.code_class_base_inheritance in cls["bases"]] + component_classes = [ + cls + for cls in self.tree["classes"] + if self.code_class_base_inheritance in cls["bases"] + ] if not component_classes: return {} # Assume the first Component class is the one we're interested in component_class = component_classes[0] build_methods = [ - method for method in component_class["methods"] if method["name"] == self.function_entrypoint_name + method + for method in component_class["methods"] + if method["name"] == self.function_entrypoint_name ] return build_methods[0] if build_methods else {} @@ -294,7 +310,9 @@ class CustomComponent(Component): # Retrieve and decrypt the credential by name for the current user db_service = get_db_service() with session_getter(db_service) as session: - return credential_service.get_credential(user_id=self._user_id or "", name=name, session=session) + return credential_service.get_credential( + user_id=self._user_id or "", name=name, session=session + ) return get_credential @@ -304,7 +322,9 @@ class CustomComponent(Component): credential_service = get_credential_service() db_service = get_db_service() with session_getter(db_service) as session: - return credential_service.list_credentials(user_id=self._user_id, session=session) + return credential_service.list_credentials( + user_id=self._user_id, session=session + ) def index(self, value: int = 0): """Returns a function that returns the value at the given index in the iterable.""" @@ -343,7 +363,11 @@ class CustomComponent(Component): if not self._flows_records: self.list_flows() if not flow_id and self._flows_records: - flow_ids = [flow.data["id"] for flow in self._flows_records if flow.data["name"] == flow_name] + flow_ids = [ + flow.data["id"] + for flow in self._flows_records + if flow.data["name"] == flow_name + ] if not flow_ids: raise ValueError(f"Flow {flow_name} not found") elif len(flow_ids) > 1: @@ -365,7 +389,9 @@ class CustomComponent(Component): db_service = get_db_service() with get_session(db_service) as session: flows = session.exec( - select(Flow).where(Flow.user_id == self._user_id).where(Flow.is_component == False) # noqa + select(Flow) + .where(Flow.user_id == self._user_id) + .where(Flow.is_component == False) # noqa ).all() flows_records = [flow.to_record() for flow in flows] diff --git a/src/backend/langflow/utils/util.py b/src/backend/langflow/utils/util.py index af4f02b59..825b5471c 100644 --- a/src/backend/langflow/utils/util.py +++ b/src/backend/langflow/utils/util.py @@ -15,12 +15,8 @@ def remove_ansi_escape_codes(text): return re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", text) -def build_template_from_function( - name: str, type_to_loader_dict: Dict, add_function: bool = False -): - classes = [ - item.__annotations__["return"].__name__ for item in type_to_loader_dict.values() - ] +def build_template_from_function(name: str, type_to_loader_dict: Dict, add_function: bool = False): + classes = [item.__annotations__["return"].__name__ for item in type_to_loader_dict.values()] # Raise error if name is not in chains if name not in classes: @@ -41,10 +37,8 @@ def build_template_from_function( for name_, value_ in value.__repr_args__(): if name_ == "default_factory": try: - variables[class_field_items]["default"] = ( - get_default_factory( - module=_class.__base__.__module__, function=value_ - ) + variables[class_field_items]["default"] = get_default_factory( + module=_class.__base__.__module__, function=value_ ) except Exception: variables[class_field_items]["default"] = None @@ -52,9 +46,7 @@ def build_template_from_function( variables[class_field_items][name_] = value_ variables[class_field_items]["placeholder"] = ( - docs.params[class_field_items] - if class_field_items in docs.params - else "" + docs.params[class_field_items] if class_field_items in docs.params else "" ) # Adding function to base classes to allow # the output to be a function @@ -69,9 +61,7 @@ def build_template_from_function( } -def build_template_from_class( - name: str, type_to_cls_dict: Dict, add_function: bool = False -): +def build_template_from_class(name: str, type_to_cls_dict: Dict, add_function: bool = False): classes = [item.__name__ for item in type_to_cls_dict.values()] # Raise error if name is not in chains @@ -95,11 +85,9 @@ def build_template_from_class( for name_, value_ in value.__repr_args__(): if name_ == "default_factory": try: - variables[class_field_items]["default"] = ( - get_default_factory( - module=_class.__base__.__module__, - function=value_, - ) + variables[class_field_items]["default"] = get_default_factory( + module=_class.__base__.__module__, + function=value_, ) except Exception: variables[class_field_items]["default"] = None @@ -107,9 +95,7 @@ def build_template_from_class( variables[class_field_items][name_] = value_ variables[class_field_items]["placeholder"] = ( - docs.params[class_field_items] - if class_field_items in docs.params - else "" + docs.params[class_field_items] if class_field_items in docs.params else "" ) base_classes = get_base_classes(_class) # Adding function to base classes to allow @@ -141,9 +127,7 @@ def build_template_from_method( # Check if the method exists in this class if not hasattr(_class, method_name): - raise ValueError( - f"Method {method_name} not found in class {class_name}" - ) + raise ValueError(f"Method {method_name} not found in class {class_name}") # Get the method method = getattr(_class, method_name) @@ -162,14 +146,8 @@ def build_template_from_method( "_type": _type, **{ name: { - "default": ( - param.default if param.default != param.empty else None - ), - "type": ( - param.annotation - if param.annotation != param.empty - else None - ), + "default": (param.default if param.default != param.empty else None), + "type": (param.annotation if param.annotation != param.empty else None), "required": param.default == param.empty, } for name, param in params.items() @@ -256,9 +234,7 @@ def sync_to_async(func): return async_wrapper -def format_dict( - dictionary: Dict[str, Any], class_name: Optional[str] = None -) -> Dict[str, Any]: +def format_dict(dictionary: Dict[str, Any], class_name: Optional[str] = None) -> Dict[str, Any]: """ Formats a dictionary by removing certain keys and modifying the values of other keys. @@ -344,9 +320,7 @@ def check_list_type(_type: str, value: Dict[str, Any]) -> str: The modified type string. """ if any(list_type in _type for list_type in ["List", "Sequence", "Set"]): - _type = ( - _type.replace("List[", "").replace("Sequence[", "").replace("Set[", "")[:-1] - ) + _type = _type.replace("List[", "").replace("Sequence[", "").replace("Set[", "")[:-1] value["list"] = True else: value["list"] = False @@ -449,9 +423,7 @@ def set_headers_value(value: Dict[str, Any]) -> None: value["value"] = """{"Authorization": "Bearer "}""" -def add_options_to_field( - value: Dict[str, Any], class_name: Optional[str], key: str -) -> None: +def add_options_to_field(value: Dict[str, Any], class_name: Optional[str], key: str) -> None: """ Adds options to the field based on the class name and key. """ From e07eae6a8d7d89c7e52d94c35bac776a3fa37a32 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 15:35:36 -0300 Subject: [PATCH 150/199] Fix dict setup --- src/backend/langflow/graph/vertex/base.py | 121 +++++++++++++++++----- 1 file changed, 94 insertions(+), 27 deletions(-) diff --git a/src/backend/langflow/graph/vertex/base.py b/src/backend/langflow/graph/vertex/base.py index 494eed388..711b01bad 100644 --- a/src/backend/langflow/graph/vertex/base.py +++ b/src/backend/langflow/graph/vertex/base.py @@ -59,8 +59,13 @@ class Vertex: self.updated_raw_params = False self.id: str = data["id"] self.is_state = False - self.is_input = any(input_component_name in self.id for input_component_name in INPUT_COMPONENTS) - self.is_output = any(output_component_name in self.id for output_component_name in OUTPUT_COMPONENTS) + self.is_input = any( + input_component_name in self.id for input_component_name in INPUT_COMPONENTS + ) + self.is_output = any( + output_component_name in self.id + for output_component_name in OUTPUT_COMPONENTS + ) self.has_session_id = None self._custom_component = None self.has_external_input = False @@ -101,11 +106,17 @@ class Vertex: def set_state(self, state: str): self.state = VertexStates[state] - if self.state == VertexStates.INACTIVE and self.graph.in_degree_map[self.id] < 2: + if ( + self.state == VertexStates.INACTIVE + and self.graph.in_degree_map[self.id] < 2 + ): # If the vertex is inactive and has only one in degree # it means that it is not a merge point in the graph self.graph.inactivated_vertices.add(self.id) - elif self.state == VertexStates.ACTIVE and self.id in self.graph.inactivated_vertices: + elif ( + self.state == VertexStates.ACTIVE + and self.id in self.graph.inactivated_vertices + ): self.graph.inactivated_vertices.remove(self.id) @property @@ -122,7 +133,9 @@ class Vertex: # If the Vertex.type is a power component # then we need to return the built object # instead of the result dict - if self.is_interface_component and not isinstance(self._built_object, UnbuiltObject): + if self.is_interface_component and not isinstance( + self._built_object, UnbuiltObject + ): result = self._built_object # if it is not a dict or a string and hasattr model_dump then # return the model_dump @@ -134,7 +147,11 @@ class Vertex: if isinstance(self._built_result, UnbuiltResult): return {} - return self._built_result if isinstance(self._built_result, dict) else {"result": self._built_result} + return ( + self._built_result + if isinstance(self._built_result, dict) + else {"result": self._built_result} + ) def set_artifacts(self) -> None: pass @@ -204,19 +221,31 @@ class Vertex: 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 - template_dicts = {key: value for key, value in self.data["node"]["template"].items() if isinstance(value, dict)} + template_dicts = { + key: value + for key, value in self.data["node"]["template"].items() + if isinstance(value, dict) + } self.has_session_id = "session_id" in template_dicts self.required_inputs = [ - template_dicts[key]["type"] for key, value in template_dicts.items() if value["required"] + template_dicts[key]["type"] + for key, value in template_dicts.items() + if value["required"] ] self.optional_inputs = [ - template_dicts[key]["type"] for key, value in template_dicts.items() if not value["required"] + template_dicts[key]["type"] + for key, value in template_dicts.items() + if not value["required"] ] # Add the template_dicts[key]["input_types"] to the optional_inputs self.optional_inputs.extend( - [input_type for value in template_dicts.values() for input_type in value.get("input_types", [])] + [ + input_type + for value in template_dicts.values() + for input_type in value.get("input_types", []) + ] ) template_dict = self.data["node"]["template"] @@ -263,7 +292,11 @@ class Vertex: self.updated_raw_params = False return - template_dict = {key: value for key, value in self.data["node"]["template"].items() if isinstance(value, dict)} + template_dict = { + key: value + for key, value in self.data["node"]["template"].items() + if isinstance(value, dict) + } params = {} for edge in self.edges: @@ -284,7 +317,10 @@ class Vertex: # we don't know the key of the dict but we need to set the value # to the vertex that is the source of the edge param_dict = template_dict[param_key]["value"] - params[param_key] = {key: self.graph.get_vertex(edge.source_id) for key in param_dict.keys()} + params[param_key] = { + key: self.graph.get_vertex(edge.source_id) + for key in param_dict.keys() + } else: params[param_key] = self.graph.get_vertex(edge.source_id) @@ -320,7 +356,11 @@ class Vertex: # list of dicts, so we need to convert it to a dict # before passing it to the build method if isinstance(val, list): - params[key] = {k: v for item in value.get("value", []) for k, v in item.items()} + params[key] = { + k: v + for item in value.get("value", []) + for k, v in item.items() + } elif isinstance(val, dict): params[key] = val elif value.get("type") == "int" and val is not None: @@ -445,7 +485,9 @@ class Vertex: if isinstance(self._built_object, str): self._built_result = self._built_object - result = await generate_result(self._built_object, inputs, self.has_external_output, session_id) + result = await generate_result( + self._built_object, inputs, self.has_external_output, session_id + ) self._built_result = result async def _build_each_node_in_params_dict(self, user_id=None): @@ -461,17 +503,22 @@ class Vertex: elif isinstance(value, list) and self._is_list_of_nodes(value): await self._build_list_of_nodes_and_update_params(key, value, user_id) elif isinstance(value, dict): - await self._build_dict_of_nodes_and_update_params(key, value, user_id) + await self._build_dict_and_update_params(key, value, user_id) elif key not in self.params or self.updated_raw_params: self.params[key] = value - async def _build_dict_of_nodes_and_update_params(self, key, nodes: Dict[str, "Vertex"], user_id=None): + async def _build_dict_and_update_params( + self, key, nodes_dict: Dict[str, "Vertex"], user_id=None + ): """ Iterates over a dictionary of nodes, builds each and updates the params dictionary. """ - for sub_key, node in nodes.items(): - built = await node.get_result(requester=self, user_id=user_id) - self.params[key][sub_key] = built + for sub_key, value in nodes_dict.items(): + if not self._is_node(value): + self.params[key][sub_key] = value + else: + built = await value.get_result(requester=self, user_id=user_id) + self.params[key][sub_key] = built def _is_node(self, value): """ @@ -485,7 +532,9 @@ class Vertex: """ return all(self._is_node(node) for node in value) - async def get_result(self, requester: Optional["Vertex"] = None, user_id=None, timeout=None) -> Any: + async def get_result( + self, requester: Optional["Vertex"] = None, user_id=None, timeout=None + ) -> Any: # PLEASE REVIEW THIS IF STATEMENT # Check if the Vertex was built already if self._built: @@ -519,7 +568,9 @@ class Vertex: self._extend_params_list_with_result(key, result) self.params[key] = result - async def _build_list_of_nodes_and_update_params(self, key, nodes: List["Vertex"], user_id=None): + async def _build_list_of_nodes_and_update_params( + self, key, nodes: List["Vertex"], user_id=None + ): """ Iterates over a list of nodes, builds each and updates the params dictionary. """ @@ -586,7 +637,9 @@ class Vertex: except Exception as exc: logger.exception(exc) - raise ValueError(f"Error building node {self.display_name}: {str(exc)}") from exc + raise ValueError( + f"Error building node {self.display_name}: {str(exc)}" + ) from exc def _update_built_object_and_artifacts(self, result): """ @@ -614,7 +667,9 @@ class Vertex: logger.warning(message) elif isinstance(self._built_object, (Iterator, AsyncIterator)): if self.display_name in ["Text Output"]: - raise ValueError(f"You are trying to stream to a {self.display_name}. Try using a Chat Output instead.") + raise ValueError( + f"You are trying to stream to a {self.display_name}. Try using a Chat Output instead." + ) def _reset(self, params_update: Optional[Dict[str, Any]] = None): self._built = False @@ -676,16 +731,24 @@ class Vertex: return self._built_object # Get the requester edge - requester_edge = next((edge for edge in self.edges if edge.target_id == requester.id), None) + requester_edge = next( + (edge for edge in self.edges if edge.target_id == requester.id), None + ) # Return the result of the requester edge - return None if requester_edge is None else await requester_edge.get_result(source=self, target=requester) + return ( + None + if requester_edge is None + else await requester_edge.get_result(source=self, target=requester) + ) def add_edge(self, edge: "ContractEdge") -> None: if edge not in self.edges: self.edges.append(edge) def __repr__(self) -> str: - return f"Vertex(display_name={self.display_name}, id={self.id}, data={self.data})" + return ( + f"Vertex(display_name={self.display_name}, id={self.id}, data={self.data})" + ) def __eq__(self, __o: object) -> bool: try: @@ -706,4 +769,8 @@ class Vertex: def _built_object_repr(self): # Add a message with an emoji, stars for sucess, - return "Built sucessfully ✨" if self._built_object is not None else "Failed to build 😵‍💫" + return ( + "Built sucessfully ✨" + if self._built_object is not None + else "Failed to build 😵‍💫" + ) From cff2563ee16b8f8ec22d69571674e9c4de205fc4 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 15:35:54 -0300 Subject: [PATCH 151/199] Format --- .../custom_component/custom_component.py | 48 ++---- .../src/components/exampleComponent/index.tsx | 162 +++++++++--------- 2 files changed, 94 insertions(+), 116 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 11ec59bf3..f0c3bfa80 100644 --- a/src/backend/langflow/interface/custom/custom_component/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component/custom_component.py @@ -77,17 +77,13 @@ class CustomComponent(Component): def update_state(self, name: str, value: Any): try: - self.vertex.graph.update_state( - name=name, record=value, caller=self.vertex.id - ) + self.vertex.graph.update_state(name=name, record=value, caller=self.vertex.id) except Exception as e: raise ValueError(f"Error updating state: {e}") def append_state(self, name: str, value: Any): try: - self.vertex.graph.append_state( - name=name, record=value, caller=self.vertex.id - ) + self.vertex.graph.append_state(name=name, record=value, caller=self.vertex.id) except Exception as e: raise ValueError(f"Error appending state: {e}") @@ -138,9 +134,7 @@ class CustomComponent(Component): def build_config(self): return self.field_config - def update_build_config( - self, build_config: dict, field_name: str, field_value: Any - ): + def update_build_config(self, build_config: dict, field_name: str, field_value: Any): build_config[field_name] = field_value return build_config @@ -148,9 +142,7 @@ class CustomComponent(Component): def tree(self): return self.get_code_tree(self.code or "") - def to_records( - self, data: Any, keys: Optional[List[str]] = None, silent_errors: bool = False - ) -> List[Record]: + def to_records(self, data: Any, keys: Optional[List[str]] = None, silent_errors: bool = False) -> List[Record]: """ Converts input data into a list of Record objects. @@ -199,9 +191,7 @@ class CustomComponent(Component): return records - def create_references_from_records( - self, records: List[Record], include_data: bool = False - ) -> str: + def create_references_from_records(self, records: List[Record], include_data: bool = False) -> str: """ Create references from a list of records. @@ -240,20 +230,14 @@ class CustomComponent(Component): if not self.code: return {} - component_classes = [ - cls - for cls in self.tree["classes"] - if self.code_class_base_inheritance in cls["bases"] - ] + component_classes = [cls for cls in self.tree["classes"] if self.code_class_base_inheritance in cls["bases"]] if not component_classes: return {} # Assume the first Component class is the one we're interested in component_class = component_classes[0] build_methods = [ - method - for method in component_class["methods"] - if method["name"] == self.function_entrypoint_name + method for method in component_class["methods"] if method["name"] == self.function_entrypoint_name ] return build_methods[0] if build_methods else {} @@ -310,9 +294,7 @@ class CustomComponent(Component): # Retrieve and decrypt the credential by name for the current user db_service = get_db_service() with session_getter(db_service) as session: - return credential_service.get_credential( - user_id=self._user_id or "", name=name, session=session - ) + return credential_service.get_credential(user_id=self._user_id or "", name=name, session=session) return get_credential @@ -322,9 +304,7 @@ class CustomComponent(Component): credential_service = get_credential_service() db_service = get_db_service() with session_getter(db_service) as session: - return credential_service.list_credentials( - user_id=self._user_id, session=session - ) + return credential_service.list_credentials(user_id=self._user_id, session=session) def index(self, value: int = 0): """Returns a function that returns the value at the given index in the iterable.""" @@ -363,11 +343,7 @@ class CustomComponent(Component): if not self._flows_records: self.list_flows() if not flow_id and self._flows_records: - flow_ids = [ - flow.data["id"] - for flow in self._flows_records - if flow.data["name"] == flow_name - ] + flow_ids = [flow.data["id"] for flow in self._flows_records if flow.data["name"] == flow_name] if not flow_ids: raise ValueError(f"Flow {flow_name} not found") elif len(flow_ids) > 1: @@ -389,9 +365,7 @@ class CustomComponent(Component): db_service = get_db_service() with get_session(db_service) as session: flows = session.exec( - select(Flow) - .where(Flow.user_id == self._user_id) - .where(Flow.is_component == False) # noqa + select(Flow).where(Flow.user_id == self._user_id).where(Flow.is_component == False) # noqa ).all() flows_records = [flow.to_record() for flow in flows] diff --git a/src/frontend/src/components/exampleComponent/index.tsx b/src/frontend/src/components/exampleComponent/index.tsx index 0473a234e..347797c47 100644 --- a/src/frontend/src/components/exampleComponent/index.tsx +++ b/src/frontend/src/components/exampleComponent/index.tsx @@ -7,90 +7,94 @@ import ShadTooltip from "../ShadTooltipComponent"; import IconComponent from "../genericIconComponent"; import { Button } from "../ui/button"; import { - Card, - CardDescription, - CardFooter, - CardHeader, - CardTitle, + Card, + CardDescription, + CardFooter, + CardHeader, + CardTitle, } from "../ui/card"; export default function CollectionCardComponent({ - flow, + flow, }: { - flow: FlowType; - authorized?: boolean; + flow: FlowType; + authorized?: boolean; }) { - const addFlow = useFlowsManagerStore((state) => state.addFlow); - const navigate = useNavigate(); - const emojiRegex = /\p{Emoji}/u; - const isEmoji = (str: string) => emojiRegex.test(str); + const addFlow = useFlowsManagerStore((state) => state.addFlow); + const navigate = useNavigate(); + const emojiRegex = /\p{Emoji}/u; + const isEmoji = (str: string) => emojiRegex.test(str); - return ( - -
- -
- - {flow.icon && isEmoji(flow.icon) && ( -
- -
- {flow.icon} -
-
- - )} - {(!flow.icon || !isEmoji(flow.icon)) && -
- -
- } - -
{flow.name}
-
-
-
- - -
{flow.description}
-
-
-
-
- - -
-
- -
+ return ( + +
+ +
+ + {flow.icon && isEmoji(flow.icon) && ( +
+
{flow.icon}
- - - ); + )} + {(!flow.icon || !isEmoji(flow.icon)) && ( +
+ +
+ )} + +
{flow.name}
+
+
+
+ + +
{flow.description}
+
+
+
+
+ + +
+
+ +
+
+
+
+ ); } From 33cca64f322f48ea713cda3ac362f28d657b55cb Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 15:48:43 -0300 Subject: [PATCH 152/199] Update schema and utils modules --- src/backend/langflow/schema/schema.py | 6 ++- src/backend/langflow/utils/util.py | 62 +++++++++++++++++++-------- 2 files changed, 49 insertions(+), 19 deletions(-) diff --git a/src/backend/langflow/schema/schema.py b/src/backend/langflow/schema/schema.py index fd3bd4ea0..18cf9e13f 100644 --- a/src/backend/langflow/schema/schema.py +++ b/src/backend/langflow/schema/schema.py @@ -13,7 +13,7 @@ class Record(BaseModel): """ data: dict = {} - _default_value = None + _default_value: str = "" @classmethod def from_document(cls, document: Document) -> "Record": @@ -63,7 +63,9 @@ class Record(BaseModel): return self.data.get(key, self._default_value) except KeyError: # Fallback to default behavior to raise AttributeError for undefined attributes - raise AttributeError(f"'{type(self).__name__}' object has no attribute '{key}'") + raise AttributeError( + f"'{type(self).__name__}' object has no attribute '{key}'" + ) def __setattr__(self, key, value): """ diff --git a/src/backend/langflow/utils/util.py b/src/backend/langflow/utils/util.py index 825b5471c..fb704b2bf 100644 --- a/src/backend/langflow/utils/util.py +++ b/src/backend/langflow/utils/util.py @@ -15,8 +15,12 @@ def remove_ansi_escape_codes(text): return re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", text) -def build_template_from_function(name: str, type_to_loader_dict: Dict, add_function: bool = False): - classes = [item.__annotations__["return"].__name__ for item in type_to_loader_dict.values()] +def build_template_from_function( + name: str, type_to_loader_dict: Dict, add_function: bool = False +): + classes = [ + item.__annotations__["return"].__name__ for item in type_to_loader_dict.values() + ] # Raise error if name is not in chains if name not in classes: @@ -37,8 +41,10 @@ def build_template_from_function(name: str, type_to_loader_dict: Dict, add_funct for name_, value_ in value.__repr_args__(): if name_ == "default_factory": try: - variables[class_field_items]["default"] = get_default_factory( - module=_class.__base__.__module__, function=value_ + variables[class_field_items]["default"] = ( + get_default_factory( + module=_class.__base__.__module__, function=value_ + ) ) except Exception: variables[class_field_items]["default"] = None @@ -46,7 +52,9 @@ def build_template_from_function(name: str, type_to_loader_dict: Dict, add_funct variables[class_field_items][name_] = value_ variables[class_field_items]["placeholder"] = ( - docs.params[class_field_items] if class_field_items in docs.params else "" + docs.params[class_field_items] + if class_field_items in docs.params + else "" ) # Adding function to base classes to allow # the output to be a function @@ -61,7 +69,9 @@ def build_template_from_function(name: str, type_to_loader_dict: Dict, add_funct } -def build_template_from_class(name: str, type_to_cls_dict: Dict, add_function: bool = False): +def build_template_from_class( + name: str, type_to_cls_dict: Dict, add_function: bool = False +): classes = [item.__name__ for item in type_to_cls_dict.values()] # Raise error if name is not in chains @@ -85,9 +95,11 @@ def build_template_from_class(name: str, type_to_cls_dict: Dict, add_function: b for name_, value_ in value.__repr_args__(): if name_ == "default_factory": try: - variables[class_field_items]["default"] = get_default_factory( - module=_class.__base__.__module__, - function=value_, + variables[class_field_items]["default"] = ( + get_default_factory( + module=_class.__base__.__module__, + function=value_, + ) ) except Exception: variables[class_field_items]["default"] = None @@ -95,7 +107,9 @@ def build_template_from_class(name: str, type_to_cls_dict: Dict, add_function: b variables[class_field_items][name_] = value_ variables[class_field_items]["placeholder"] = ( - docs.params[class_field_items] if class_field_items in docs.params else "" + docs.params[class_field_items] + if class_field_items in docs.params + else "" ) base_classes = get_base_classes(_class) # Adding function to base classes to allow @@ -127,7 +141,9 @@ def build_template_from_method( # Check if the method exists in this class if not hasattr(_class, method_name): - raise ValueError(f"Method {method_name} not found in class {class_name}") + raise ValueError( + f"Method {method_name} not found in class {class_name}" + ) # Get the method method = getattr(_class, method_name) @@ -146,8 +162,14 @@ def build_template_from_method( "_type": _type, **{ name: { - "default": (param.default if param.default != param.empty else None), - "type": (param.annotation if param.annotation != param.empty else None), + "default": ( + param.default if param.default != param.empty else None + ), + "type": ( + param.annotation + if param.annotation != param.empty + else None + ), "required": param.default == param.empty, } for name, param in params.items() @@ -234,7 +256,9 @@ def sync_to_async(func): return async_wrapper -def format_dict(dictionary: Dict[str, Any], class_name: Optional[str] = None) -> Dict[str, Any]: +def format_dict( + dictionary: Dict[str, Any], class_name: Optional[str] = None +) -> Dict[str, Any]: """ Formats a dictionary by removing certain keys and modifying the values of other keys. @@ -320,7 +344,9 @@ def check_list_type(_type: str, value: Dict[str, Any]) -> str: The modified type string. """ if any(list_type in _type for list_type in ["List", "Sequence", "Set"]): - _type = _type.replace("List[", "").replace("Sequence[", "").replace("Set[", "")[:-1] + _type = ( + _type.replace("List[", "").replace("Sequence[", "").replace("Set[", "")[:-1] + ) value["list"] = True else: value["list"] = False @@ -423,7 +449,9 @@ def set_headers_value(value: Dict[str, Any]) -> None: value["value"] = """{"Authorization": "Bearer "}""" -def add_options_to_field(value: Dict[str, Any], class_name: Optional[str], key: str) -> None: +def add_options_to_field( + value: Dict[str, Any], class_name: Optional[str], key: str +) -> None: """ Adds options to the field based on the class name and key. """ @@ -442,7 +470,7 @@ def add_options_to_field(value: Dict[str, Any], class_name: Optional[str], key: def build_loader_repr_from_records(records: List[Record]) -> str: if records: - avg_length = sum(len(doc.page_content) for doc in records) / len(records) + avg_length = sum(len(doc.text) for doc in records) / len(records) return f"""{len(records)} records \nAvg. Record Length (characters): {int(avg_length)} Records: {records[:3]}...""" From df142844b62a9b71c9adc1ca812bdaa8dd15c2a6 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 16:21:40 -0300 Subject: [PATCH 153/199] Add Basic Prompting example --- .../Langflow Basic Prompting.json | 1199 +++++++++++++++++ 1 file changed, 1199 insertions(+) create mode 100644 src/backend/langflow/initial_setup/starter_projects/Langflow Basic Prompting.json diff --git a/src/backend/langflow/initial_setup/starter_projects/Langflow Basic Prompting.json b/src/backend/langflow/initial_setup/starter_projects/Langflow Basic Prompting.json new file mode 100644 index 000000000..879cb1468 --- /dev/null +++ b/src/backend/langflow/initial_setup/starter_projects/Langflow Basic Prompting.json @@ -0,0 +1,1199 @@ +{ + "id": "4ac1ae80-b818-4fdf-b72c-f22dace784a5", + "icon": "📝", + "icon_bg_color": "#FFD700", + "data": { + "nodes": [ + { + "id": "ChatInput-WcFzs", + "type": "genericNode", + "position": { + "x": 86.66131544226482, + "y": 69.51987428063671 + }, + "data": { + "type": "ChatInput", + "node": { + "template": { + "code": { + "type": "code", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "from typing import Optional, Union\n\nfrom langflow.base.io.chat import ChatComponent\nfrom langflow.field_typing import Text\nfrom langflow.schema import Record\n\n\nclass ChatInput(ChatComponent):\n display_name = \"Chat Input\"\n description = \"Used to get user input from the chat.\"\n\n def build(\n self,\n sender: Optional[str] = \"User\",\n sender_name: Optional[str] = \"User\",\n input_value: Optional[str] = None,\n session_id: Optional[str] = None,\n return_record: Optional[bool] = False,\n ) -> Union[Text, Record]:\n return super().build(\n sender=sender,\n sender_name=sender_name,\n input_value=input_value,\n session_id=session_id,\n return_record=return_record,\n )\n", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "code", + "advanced": false, + "dynamic": true, + "info": "", + "title_case": false + }, + "input_value": { + "type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "input_value", + "display_name": "Message", + "advanced": false, + "input_types": [ + "Text" + ], + "dynamic": false, + "info": "", + "title_case": false, + "value": "Write a press release " + }, + "return_record": { + "type": "bool", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "return_record", + "display_name": "Return Record", + "advanced": false, + "dynamic": false, + "info": "Return the message as a record containing the sender, sender_name, and session_id.", + "title_case": false + }, + "sender": { + "type": "str", + "required": false, + "placeholder": "", + "list": true, + "show": true, + "multiline": false, + "value": "User", + "fileTypes": [], + "file_path": "", + "password": false, + "options": [ + "Machine", + "User" + ], + "name": "sender", + "display_name": "Sender Type", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ] + }, + "sender_name": { + "type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": "User", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "sender_name", + "display_name": "Sender Name", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ] + }, + "session_id": { + "type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "session_id", + "display_name": "Session ID", + "advanced": false, + "dynamic": false, + "info": "If provided, the message will be stored in the memory.", + "title_case": false, + "input_types": [ + "Text" + ] + }, + "_type": "CustomComponent" + }, + "description": "Used to get user input from the chat.", + "base_classes": [ + "object", + "Text", + "Record", + "str" + ], + "display_name": "Chat Input", + "documentation": "", + "custom_fields": { + "sender": null, + "sender_name": null, + "input_value": null, + "session_id": null, + "return_record": null + }, + "output_types": [ + "Text", + "Record" + ], + "field_formatters": {}, + "frozen": false, + "field_order": [], + "beta": true + }, + "id": "ChatInput-WcFzs" + }, + "selected": false, + "width": 384, + "height": 667, + "positionAbsolute": { + "x": 86.66131544226482, + "y": 69.51987428063671 + }, + "dragging": false + }, + { + "id": "Prompt-QtWOn", + "type": "genericNode", + "position": { + "x": 731.5380376186406, + "y": 273.5294585628963 + }, + "data": { + "type": "Prompt", + "node": { + "template": { + "code": { + "type": "code", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "from langchain_core.prompts import PromptTemplate\n\nfrom langflow import CustomComponent\nfrom langflow.field_typing import Prompt, TemplateField, Text\n\n\nclass PromptComponent(CustomComponent):\n display_name: str = \"Prompt\"\n description: str = \"A component for creating prompts using templates\"\n beta = True\n\n def build_config(self):\n return {\n \"template\": TemplateField(display_name=\"Template\"),\n \"code\": TemplateField(advanced=True),\n }\n\n def build(\n self,\n template: Prompt,\n **kwargs,\n ) -> Text:\n prompt_template = PromptTemplate.from_template(Text(template))\n\n attributes_to_check = [\"text\", \"page_content\"]\n for key, value in kwargs.copy().items():\n for attribute in attributes_to_check:\n if hasattr(value, attribute):\n kwargs[key] = getattr(value, attribute)\n\n try:\n formated_prompt = prompt_template.format(**kwargs)\n except Exception as exc:\n raise ValueError(f\"Error formatting prompt: {exc}\") from exc\n self.status = f'Prompt: \"{formated_prompt}\"'\n return formated_prompt\n", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "code", + "advanced": true, + "dynamic": true, + "info": "", + "title_case": false + }, + "template": { + "type": "prompt", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": "{request}\n\n- {topic_1}\n- {topic_2}\n\n\nAnswer:\n\n", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "template", + "display_name": "Template", + "advanced": false, + "input_types": [ + "Text" + ], + "dynamic": false, + "info": "", + "title_case": false + }, + "_type": "CustomComponent", + "request": { + "field_type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "request", + "display_name": "request", + "advanced": false, + "input_types": [ + "Document", + "BaseOutputParser", + "Text", + "Record" + ], + "dynamic": false, + "info": "", + "title_case": false, + "type": "str" + }, + "topic_1": { + "field_type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "topic_1", + "display_name": "topic_1", + "advanced": false, + "input_types": [ + "Document", + "BaseOutputParser", + "Text", + "Record" + ], + "dynamic": false, + "info": "", + "title_case": false, + "type": "str" + }, + "topic_2": { + "field_type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "topic_2", + "display_name": "topic_2", + "advanced": false, + "input_types": [ + "Document", + "BaseOutputParser", + "Text", + "Record" + ], + "dynamic": false, + "info": "", + "title_case": false, + "type": "str" + } + }, + "description": "A component for creating prompts using templates", + "icon": null, + "is_input": null, + "is_output": null, + "is_composition": null, + "base_classes": [ + "object", + "Text", + "str" + ], + "name": "", + "display_name": "Prompt", + "documentation": "", + "custom_fields": { + "template": [ + "request", + "topic_1", + "topic_2" + ] + }, + "output_types": [ + "Text" + ], + "full_path": null, + "field_formatters": {}, + "frozen": false, + "field_order": [], + "beta": true, + "error": null + }, + "id": "Prompt-QtWOn", + "description": "A component for creating prompts using templates", + "display_name": "Prompt" + }, + "selected": false, + "width": 384, + "height": 571, + "dragging": false + }, + { + "id": "TextInput-xUQ9w", + "type": "genericNode", + "position": { + "x": 91.73477837172948, + "y": 787.6263883143245 + }, + "data": { + "type": "TextInput", + "node": { + "template": { + "code": { + "type": "code", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "from typing import Optional\n\nfrom langflow.base.io.text import TextComponent\nfrom langflow.field_typing import Text\n\n\nclass TextInput(TextComponent):\n display_name = \"Text Input\"\n description = \"Used to pass text input to the next component.\"\n\n def build(self, input_value: Optional[str] = \"\") -> Text:\n return super().build(input_value=input_value)\n", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "code", + "advanced": false, + "dynamic": true, + "info": "", + "title_case": false + }, + "input_value": { + "type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "Cars", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "input_value", + "display_name": "Value", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ] + }, + "_type": "CustomComponent" + }, + "description": "Used to pass text input to the next component.", + "base_classes": [ + "object", + "Text", + "str" + ], + "display_name": "Topic 1", + "documentation": "", + "custom_fields": { + "input_value": null + }, + "output_types": [ + "Text" + ], + "field_formatters": {}, + "frozen": false, + "field_order": [ + "input_value" + ], + "beta": true + }, + "id": "TextInput-xUQ9w" + }, + "selected": false, + "width": 384, + "height": 289, + "positionAbsolute": { + "x": 91.73477837172948, + "y": 787.6263883143245 + }, + "dragging": false + }, + { + "id": "TextInput-l4zQt", + "type": "genericNode", + "position": { + "x": 93.56470545178581, + "y": 1125.2986229040628 + }, + "data": { + "type": "TextInput", + "node": { + "template": { + "code": { + "type": "code", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "from typing import Optional\n\nfrom langflow.base.io.text import TextComponent\nfrom langflow.field_typing import Text\n\n\nclass TextInput(TextComponent):\n display_name = \"Text Input\"\n description = \"Used to pass text input to the next component.\"\n\n def build(self, input_value: Optional[str] = \"\") -> Text:\n return super().build(input_value=input_value)\n", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "code", + "advanced": false, + "dynamic": true, + "info": "", + "title_case": false + }, + "input_value": { + "type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "Bottle", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "input_value", + "display_name": "Value", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ] + }, + "_type": "CustomComponent" + }, + "description": "Used to pass text input to the next component.", + "base_classes": [ + "object", + "Text", + "str" + ], + "display_name": "Topic 2", + "documentation": "", + "custom_fields": { + "input_value": null + }, + "output_types": [ + "Text" + ], + "field_formatters": {}, + "frozen": false, + "field_order": [ + "input_value" + ], + "beta": true + }, + "id": "TextInput-l4zQt" + }, + "selected": false, + "width": 384, + "height": 289, + "positionAbsolute": { + "x": 93.56470545178581, + "y": 1125.2986229040628 + }, + "dragging": false + }, + { + "id": "TextOutput-fTp5e", + "type": "genericNode", + "position": { + "x": 1242.6494961686594, + "y": 100.3023112016921 + }, + "data": { + "type": "TextOutput", + "node": { + "template": { + "input_value": { + "type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": "", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "input_value", + "display_name": "Value", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ] + }, + "code": { + "type": "code", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "from typing import Optional\n\nfrom langflow.base.io.text import TextComponent\nfrom langflow.field_typing import Text\n\n\nclass TextOutput(TextComponent):\n display_name = \"Text Output\"\n description = \"Used to pass text output to the next component.\"\n\n field_config = {\n \"input_value\": {\"display_name\": \"Value\"},\n }\n\n def build(self, input_value: Optional[Text] = \"\") -> Text:\n return super().build(input_value=input_value)\n", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "code", + "advanced": false, + "dynamic": true, + "info": "", + "title_case": false + }, + "_type": "CustomComponent" + }, + "description": "Used to pass text output to the next component.", + "base_classes": [ + "object", + "Text", + "str" + ], + "display_name": "Prompt Output", + "documentation": "", + "custom_fields": { + "input_value": null + }, + "output_types": [ + "Text" + ], + "field_formatters": {}, + "frozen": false, + "field_order": [ + "input_value" + ], + "beta": true + }, + "id": "TextOutput-fTp5e" + }, + "selected": false, + "width": 384, + "height": 297, + "positionAbsolute": { + "x": 1242.6494961686594, + "y": 100.3023112016921 + }, + "dragging": false + }, + { + "id": "ChatOutput-AVN8s", + "type": "genericNode", + "position": { + "x": 2299.2806014585203, + "y": 449.2461295937437 + }, + "data": { + "type": "ChatOutput", + "node": { + "template": { + "code": { + "type": "code", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "from typing import Optional, Union\n\nfrom langflow.base.io.chat import ChatComponent\nfrom langflow.field_typing import Text\nfrom langflow.schema import Record\n\n\nclass ChatOutput(ChatComponent):\n display_name = \"Chat Output\"\n description = \"Used to send a message to the chat.\"\n\n def build(\n self,\n sender: Optional[str] = \"Machine\",\n sender_name: Optional[str] = \"AI\",\n input_value: Optional[str] = None,\n session_id: Optional[str] = None,\n return_record: Optional[bool] = False,\n ) -> Union[Text, Record]:\n return super().build(\n sender=sender,\n sender_name=sender_name,\n input_value=input_value,\n session_id=session_id,\n return_record=return_record,\n )\n", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "code", + "advanced": false, + "dynamic": true, + "info": "", + "title_case": false + }, + "input_value": { + "type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "input_value", + "display_name": "Message", + "advanced": false, + "input_types": [ + "Text" + ], + "dynamic": false, + "info": "", + "title_case": false + }, + "return_record": { + "type": "bool", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "return_record", + "display_name": "Return Record", + "advanced": false, + "dynamic": false, + "info": "Return the message as a record containing the sender, sender_name, and session_id.", + "title_case": false + }, + "sender": { + "type": "str", + "required": false, + "placeholder": "", + "list": true, + "show": true, + "multiline": false, + "value": "Machine", + "fileTypes": [], + "file_path": "", + "password": false, + "options": [ + "Machine", + "User" + ], + "name": "sender", + "display_name": "Sender Type", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ] + }, + "sender_name": { + "type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": "AI", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "sender_name", + "display_name": "Sender Name", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ] + }, + "session_id": { + "type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "session_id", + "display_name": "Session ID", + "advanced": false, + "dynamic": false, + "info": "If provided, the message will be stored in the memory.", + "title_case": false, + "input_types": [ + "Text" + ] + }, + "_type": "CustomComponent" + }, + "description": "Used to send a message to the chat.", + "base_classes": [ + "object", + "Text", + "Record", + "str" + ], + "display_name": "Chat Output", + "documentation": "", + "custom_fields": { + "sender": null, + "sender_name": null, + "input_value": null, + "session_id": null, + "return_record": null + }, + "output_types": [ + "Text", + "Record" + ], + "field_formatters": {}, + "frozen": false, + "field_order": [], + "beta": true + }, + "id": "ChatOutput-AVN8s" + }, + "selected": false, + "width": 384, + "height": 667, + "positionAbsolute": { + "x": 2299.2806014585203, + "y": 449.2461295937437 + }, + "dragging": false + }, + { + "id": "OpenAIModel-IRzsd", + "type": "genericNode", + "position": { + "x": 1735.1051821296949, + "y": 246.4955882724468 + }, + "data": { + "type": "OpenAIModel", + "node": { + "template": { + "input_value": { + "type": "str", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "input_value", + "display_name": "Input", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ] + }, + "code": { + "type": "code", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "from typing import Optional\n\nfrom langchain_openai import ChatOpenAI\nfrom pydantic.v1 import SecretStr\n\nfrom langflow.components.models.base.model import LCModelComponent\nfrom langflow.field_typing import NestedDict, Text\n\n\nclass OpenAIModelComponent(LCModelComponent):\n display_name = \"OpenAI Model\"\n description = \"Generates text using OpenAI's models.\"\n icon = \"OpenAI\"\n\n def build_config(self):\n return {\n \"input_value\": {\"display_name\": \"Input\"},\n \"max_tokens\": {\n \"display_name\": \"Max Tokens\",\n \"advanced\": False,\n \"required\": False,\n },\n \"model_kwargs\": {\n \"display_name\": \"Model Kwargs\",\n \"advanced\": True,\n \"required\": False,\n },\n \"model_name\": {\n \"display_name\": \"Model Name\",\n \"advanced\": False,\n \"required\": False,\n \"options\": [\n \"gpt-4-turbo-preview\",\n \"gpt-4-0125-preview\",\n \"gpt-4-1106-preview\",\n \"gpt-4-vision-preview\",\n \"gpt-3.5-turbo-0125\",\n \"gpt-3.5-turbo-1106\",\n ],\n },\n \"openai_api_base\": {\n \"display_name\": \"OpenAI API Base\",\n \"advanced\": False,\n \"required\": False,\n \"info\": (\n \"The base URL of the OpenAI API. Defaults to https://api.openai.com/v1.\\n\\n\"\n \"You can change this to use other APIs like JinaChat, LocalAI and Prem.\"\n ),\n },\n \"openai_api_key\": {\n \"display_name\": \"OpenAI API Key\",\n \"advanced\": False,\n \"required\": False,\n \"password\": True,\n },\n \"temperature\": {\n \"display_name\": \"Temperature\",\n \"advanced\": False,\n \"required\": False,\n \"value\": 0.7,\n },\n \"stream\": {\n \"display_name\": \"Stream\",\n \"info\": \"Stream the response from the model.\",\n },\n }\n\n def build(\n self,\n input_value: Text,\n max_tokens: Optional[int] = 256,\n model_kwargs: NestedDict = {},\n model_name: str = \"gpt-4-1106-preview\",\n openai_api_base: Optional[str] = None,\n openai_api_key: Optional[str] = None,\n temperature: float = 0.7,\n stream: bool = False,\n ) -> Text:\n if not openai_api_base:\n openai_api_base = \"https://api.openai.com/v1\"\n if openai_api_key:\n secret_key = SecretStr(openai_api_key)\n else:\n secret_key = None\n output = ChatOpenAI(\n max_tokens=max_tokens,\n model_kwargs=model_kwargs,\n model=model_name,\n base_url=openai_api_base,\n api_key=secret_key,\n temperature=temperature,\n )\n\n return self.get_result(output=output, stream=stream, input_value=input_value)\n", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "code", + "advanced": false, + "dynamic": true, + "info": "", + "title_case": false + }, + "max_tokens": { + "type": "int", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": 256, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "max_tokens", + "display_name": "Max Tokens", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false + }, + "model_kwargs": { + "type": "NestedDict", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": {}, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "model_kwargs", + "display_name": "Model Kwargs", + "advanced": true, + "dynamic": false, + "info": "", + "title_case": false + }, + "model_name": { + "type": "str", + "required": false, + "placeholder": "", + "list": true, + "show": true, + "multiline": false, + "value": "gpt-4-1106-preview", + "fileTypes": [], + "file_path": "", + "password": false, + "options": [ + "gpt-4-turbo-preview", + "gpt-4-0125-preview", + "gpt-4-1106-preview", + "gpt-4-vision-preview", + "gpt-3.5-turbo-0125", + "gpt-3.5-turbo-1106" + ], + "name": "model_name", + "display_name": "Model Name", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ] + }, + "openai_api_base": { + "type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "openai_api_base", + "display_name": "OpenAI API Base", + "advanced": false, + "dynamic": false, + "info": "The base URL of the OpenAI API. Defaults to https://api.openai.com/v1.\n\nYou can change this to use other APIs like JinaChat, LocalAI and Prem.", + "title_case": false, + "input_types": [ + "Text" + ] + }, + "openai_api_key": { + "type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "fileTypes": [], + "file_path": "", + "password": true, + "name": "openai_api_key", + "display_name": "OpenAI API Key", + "advanced": false, + "dynamic": false, + "info": "", + "title_case": false, + "input_types": [ + "Text" + ] + }, + "stream": { + "type": "bool", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": true, + "fileTypes": [], + "file_path": "", + "password": false, + "name": "stream", + "display_name": "Stream", + "advanced": false, + "dynamic": false, + "info": "Stream the response from the model.", + "title_case": false + }, + "temperature": { + "type": "float", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": false, + "value": "0.2", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "temperature", + "display_name": "Temperature", + "advanced": false, + "dynamic": false, + "info": "", + "rangeSpec": { + "min": -1, + "max": 1, + "step": 0.1 + }, + "title_case": false + }, + "_type": "CustomComponent" + }, + "description": "Generates text using OpenAI's models.", + "icon": "OpenAI", + "base_classes": [ + "object", + "Text", + "str" + ], + "display_name": "OpenAI Model", + "documentation": "", + "custom_fields": { + "input_value": null, + "max_tokens": null, + "model_kwargs": null, + "model_name": null, + "openai_api_base": null, + "openai_api_key": null, + "temperature": null, + "stream": null + }, + "output_types": [ + "Text" + ], + "field_formatters": {}, + "frozen": false, + "field_order": [], + "beta": true + }, + "id": "OpenAIModel-IRzsd" + }, + "selected": false, + "width": 384, + "height": 847, + "positionAbsolute": { + "x": 1735.1051821296949, + "y": 246.4955882724468 + }, + "dragging": false + } + ], + "edges": [ + { + "source": "ChatInput-WcFzs", + "sourceHandle": "{œbaseClassesœ:[œobjectœ,œTextœ,œRecordœ,œstrœ],œdataTypeœ:œChatInputœ,œidœ:œChatInput-WcFzsœ}", + "target": "Prompt-QtWOn", + "targetHandle": "{œfieldNameœ:œrequestœ,œidœ:œPrompt-QtWOnœ,œinputTypesœ:[œDocumentœ,œBaseOutputParserœ,œTextœ,œRecordœ],œtypeœ:œstrœ}", + "data": { + "targetHandle": { + "fieldName": "request", + "id": "Prompt-QtWOn", + "inputTypes": [ + "Document", + "BaseOutputParser", + "Text", + "Record" + ], + "type": "str" + }, + "sourceHandle": { + "baseClasses": [ + "object", + "Text", + "Record", + "str" + ], + "dataType": "ChatInput", + "id": "ChatInput-WcFzs" + } + }, + "style": { + "stroke": "#555" + }, + "className": "stroke-foreground stroke-connection", + "id": "reactflow__edge-ChatInput-WcFzs{œbaseClassesœ:[œobjectœ,œTextœ,œRecordœ,œstrœ],œdataTypeœ:œChatInputœ,œidœ:œChatInput-WcFzsœ}-Prompt-QtWOn{œfieldNameœ:œrequestœ,œidœ:œPrompt-QtWOnœ,œinputTypesœ:[œDocumentœ,œBaseOutputParserœ,œTextœ,œRecordœ],œtypeœ:œstrœ}" + }, + { + "source": "Prompt-QtWOn", + "sourceHandle": "{œbaseClassesœ:[œobjectœ,œTextœ,œstrœ],œdataTypeœ:œPromptœ,œidœ:œPrompt-QtWOnœ}", + "target": "TextOutput-fTp5e", + "targetHandle": "{œfieldNameœ:œinput_valueœ,œidœ:œTextOutput-fTp5eœ,œinputTypesœ:[œTextœ],œtypeœ:œstrœ}", + "data": { + "targetHandle": { + "fieldName": "input_value", + "id": "TextOutput-fTp5e", + "inputTypes": [ + "Text" + ], + "type": "str" + }, + "sourceHandle": { + "baseClasses": [ + "object", + "Text", + "str" + ], + "dataType": "Prompt", + "id": "Prompt-QtWOn" + } + }, + "style": { + "stroke": "#555" + }, + "className": "stroke-foreground stroke-connection", + "id": "reactflow__edge-Prompt-QtWOn{œbaseClassesœ:[œobjectœ,œTextœ,œstrœ],œdataTypeœ:œPromptœ,œidœ:œPrompt-QtWOnœ}-TextOutput-fTp5e{œfieldNameœ:œinput_valueœ,œidœ:œTextOutput-fTp5eœ,œinputTypesœ:[œTextœ],œtypeœ:œstrœ}" + }, + { + "source": "TextOutput-fTp5e", + "sourceHandle": "{œbaseClassesœ:[œobjectœ,œTextœ,œstrœ],œdataTypeœ:œTextOutputœ,œidœ:œTextOutput-fTp5eœ}", + "target": "OpenAIModel-IRzsd", + "targetHandle": "{œfieldNameœ:œinput_valueœ,œidœ:œOpenAIModel-IRzsdœ,œinputTypesœ:[œTextœ],œtypeœ:œstrœ}", + "data": { + "targetHandle": { + "fieldName": "input_value", + "id": "OpenAIModel-IRzsd", + "inputTypes": [ + "Text" + ], + "type": "str" + }, + "sourceHandle": { + "baseClasses": [ + "object", + "Text", + "str" + ], + "dataType": "TextOutput", + "id": "TextOutput-fTp5e" + } + }, + "style": { + "stroke": "#555" + }, + "className": "stroke-foreground stroke-connection", + "id": "reactflow__edge-TextOutput-fTp5e{œbaseClassesœ:[œobjectœ,œTextœ,œstrœ],œdataTypeœ:œTextOutputœ,œidœ:œTextOutput-fTp5eœ}-OpenAIModel-IRzsd{œfieldNameœ:œinput_valueœ,œidœ:œOpenAIModel-IRzsdœ,œinputTypesœ:[œTextœ],œtypeœ:œstrœ}" + }, + { + "source": "OpenAIModel-IRzsd", + "sourceHandle": "{œbaseClassesœ:[œobjectœ,œTextœ,œstrœ],œdataTypeœ:œOpenAIModelœ,œidœ:œOpenAIModel-IRzsdœ}", + "target": "ChatOutput-AVN8s", + "targetHandle": "{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-AVN8sœ,œinputTypesœ:[œTextœ],œtypeœ:œstrœ}", + "data": { + "targetHandle": { + "fieldName": "input_value", + "id": "ChatOutput-AVN8s", + "inputTypes": [ + "Text" + ], + "type": "str" + }, + "sourceHandle": { + "baseClasses": [ + "object", + "Text", + "str" + ], + "dataType": "OpenAIModel", + "id": "OpenAIModel-IRzsd" + } + }, + "style": { + "stroke": "#555" + }, + "className": "stroke-foreground stroke-connection", + "id": "reactflow__edge-OpenAIModel-IRzsd{œbaseClassesœ:[œobjectœ,œTextœ,œstrœ],œdataTypeœ:œOpenAIModelœ,œidœ:œOpenAIModel-IRzsdœ}-ChatOutput-AVN8s{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-AVN8sœ,œinputTypesœ:[œTextœ],œtypeœ:œstrœ}" + }, + { + "source": "TextInput-l4zQt", + "sourceHandle": "{œbaseClassesœ:[œobjectœ,œTextœ,œstrœ],œdataTypeœ:œTextInputœ,œidœ:œTextInput-l4zQtœ}", + "target": "Prompt-QtWOn", + "targetHandle": "{œfieldNameœ:œtopic_2œ,œidœ:œPrompt-QtWOnœ,œinputTypesœ:[œDocumentœ,œBaseOutputParserœ,œTextœ,œRecordœ],œtypeœ:œstrœ}", + "data": { + "targetHandle": { + "fieldName": "topic_2", + "id": "Prompt-QtWOn", + "inputTypes": [ + "Document", + "BaseOutputParser", + "Text", + "Record" + ], + "type": "str" + }, + "sourceHandle": { + "baseClasses": [ + "object", + "Text", + "str" + ], + "dataType": "TextInput", + "id": "TextInput-l4zQt" + } + }, + "style": { + "stroke": "#555" + }, + "className": "stroke-foreground stroke-connection", + "id": "reactflow__edge-TextInput-l4zQt{œbaseClassesœ:[œobjectœ,œTextœ,œstrœ],œdataTypeœ:œTextInputœ,œidœ:œTextInput-l4zQtœ}-Prompt-QtWOn{œfieldNameœ:œtopic_2œ,œidœ:œPrompt-QtWOnœ,œinputTypesœ:[œDocumentœ,œBaseOutputParserœ,œTextœ,œRecordœ],œtypeœ:œstrœ}" + }, + { + "source": "TextInput-xUQ9w", + "sourceHandle": "{œbaseClassesœ:[œobjectœ,œTextœ,œstrœ],œdataTypeœ:œTextInputœ,œidœ:œTextInput-xUQ9wœ}", + "target": "Prompt-QtWOn", + "targetHandle": "{œfieldNameœ:œtopic_1œ,œidœ:œPrompt-QtWOnœ,œinputTypesœ:[œDocumentœ,œBaseOutputParserœ,œTextœ,œRecordœ],œtypeœ:œstrœ}", + "data": { + "targetHandle": { + "fieldName": "topic_1", + "id": "Prompt-QtWOn", + "inputTypes": [ + "Document", + "BaseOutputParser", + "Text", + "Record" + ], + "type": "str" + }, + "sourceHandle": { + "baseClasses": [ + "object", + "Text", + "str" + ], + "dataType": "TextInput", + "id": "TextInput-xUQ9w" + } + }, + "style": { + "stroke": "#555" + }, + "className": "stroke-foreground stroke-connection", + "id": "reactflow__edge-TextInput-xUQ9w{œbaseClassesœ:[œobjectœ,œTextœ,œstrœ],œdataTypeœ:œTextInputœ,œidœ:œTextInput-xUQ9wœ}-Prompt-QtWOn{œfieldNameœ:œtopic_1œ,œidœ:œPrompt-QtWOnœ,œinputTypesœ:[œDocumentœ,œBaseOutputParserœ,œTextœ,œRecordœ],œtypeœ:œstrœ}" + } + ], + "viewport": { + "x": 81.87154098468557, + "y": 266.8627952720353, + "zoom": 0.315125847895746 + } + }, + "description": "Use a language model to generate text based on a prompt. \n\nIn this project, you'll be able to generate text based on a request and some topics.\n\nThe Topic 1 and Topic 2 components are actually Text Input, while the Prompt Output component is a Text Output. Changing the name of the component makes them easier to identify when interacting with them.", + "name": "Basic Prompting", + "last_tested_version": "0.6.8", + "is_component": false +} \ No newline at end of file From 7a3dd1a79ca10064efee6226052cab934297a524 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 16:27:53 -0300 Subject: [PATCH 154/199] Add emoji support to project icons --- src/backend/langflow/initial_setup/setup.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/backend/langflow/initial_setup/setup.py b/src/backend/langflow/initial_setup/setup.py index 476f43410..c15c34aea 100644 --- a/src/backend/langflow/initial_setup/setup.py +++ b/src/backend/langflow/initial_setup/setup.py @@ -2,6 +2,7 @@ from datetime import datetime from pathlib import Path import orjson +from emoji import demojize, purely_emoji from loguru import logger from sqlmodel import select @@ -80,7 +81,7 @@ def create_new_project( new_project = FlowCreate( name=project_name, description=project_description, - icon=project_icon, + icon=project_icon if not purely_emoji(project_icon) else demojize(project_icon), icon_bg_color=project_icon_bg_color, data=project_data, is_component=project_is_component, @@ -126,7 +127,9 @@ def create_or_update_starter_projects(): project_icon_bg_color, ) = get_project_data(project) if project_name and project_data: - for existing_project in get_all_flows_similar_to_project(session, project_name): + for existing_project in get_all_flows_similar_to_project( + session, project_name + ): session.delete(existing_project) create_new_project( From dd00a3705d589b5bf75bef9ed6239c4205366966 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 16:30:19 -0300 Subject: [PATCH 155/199] Fix datetime parsing in setup.py --- src/backend/langflow/initial_setup/setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/backend/langflow/initial_setup/setup.py b/src/backend/langflow/initial_setup/setup.py index c15c34aea..e24eda8a2 100644 --- a/src/backend/langflow/initial_setup/setup.py +++ b/src/backend/langflow/initial_setup/setup.py @@ -32,7 +32,9 @@ def get_project_data(project): project_description = project.get("description") project_is_component = project.get("is_component") project_updated_at = project.get("updated_at") - updated_at_datetime = datetime.strptime(project_updated_at, "%Y-%m-%dT%H:%M:%S.%f") + updated_at_datetime = datetime.strptime( + project_updated_at or datetime.now(), "%Y-%m-%dT%H:%M:%S.%f" + ) project_data = project.get("data") project_icon = project.get("icon") project_icon_bg_color = project.get("icon_bg_color") From 7f2a352b9a7efd41c29d79558619878c29b1362b Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 16:30:55 -0300 Subject: [PATCH 156/199] Fix project_updated_at datetime parsing --- src/backend/langflow/initial_setup/setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backend/langflow/initial_setup/setup.py b/src/backend/langflow/initial_setup/setup.py index e24eda8a2..f1e3fd9ce 100644 --- a/src/backend/langflow/initial_setup/setup.py +++ b/src/backend/langflow/initial_setup/setup.py @@ -32,9 +32,9 @@ def get_project_data(project): project_description = project.get("description") project_is_component = project.get("is_component") project_updated_at = project.get("updated_at") - updated_at_datetime = datetime.strptime( - project_updated_at or datetime.now(), "%Y-%m-%dT%H:%M:%S.%f" - ) + if not project_updated_at: + project_updated_at = datetime.utcnow().isoformat() + updated_at_datetime = datetime.strptime(project_updated_at, "%Y-%m-%dT%H:%M:%S.%f") project_data = project.get("data") project_icon = project.get("icon") project_icon_bg_color = project.get("icon_bg_color") From 134a6d15b6af291e57b5bea2858898288a9d25ee Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 6 Mar 2024 16:48:32 -0300 Subject: [PATCH 157/199] Document build_loader_repr_from_records function to util.py --- src/backend/langflow/utils/util.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/backend/langflow/utils/util.py b/src/backend/langflow/utils/util.py index fb704b2bf..ad6660cc5 100644 --- a/src/backend/langflow/utils/util.py +++ b/src/backend/langflow/utils/util.py @@ -469,6 +469,16 @@ def add_options_to_field( def build_loader_repr_from_records(records: List[Record]) -> str: + """ + Builds a string representation of the loader based on the given records. + + Args: + records (List[Record]): A list of records. + + Returns: + str: A string representation of the loader. + + """ if records: avg_length = sum(len(doc.text) for doc in records) / len(records) return f"""{len(records)} records From 66139eebf00447dcb2ab0253feebda75070bec7e Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Wed, 6 Mar 2024 17:09:59 -0300 Subject: [PATCH 158/199] Add NewFlowCard2 component and UndrawCardComponent --- .../assets/undraw_transfer_files_re_a2a9.svg | 1 + .../src/components/NewFLowCard2/index.tsx | 26 ++++++++++++++ .../src/components/undrawCards/index.tsx | 36 +++++++++++++++++++ src/frontend/src/pages/MainPage/index.tsx | 13 +++---- src/frontend/src/utils/styleUtils.ts | 2 ++ 5 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 src/frontend/src/assets/undraw_transfer_files_re_a2a9.svg create mode 100644 src/frontend/src/components/NewFLowCard2/index.tsx create mode 100644 src/frontend/src/components/undrawCards/index.tsx diff --git a/src/frontend/src/assets/undraw_transfer_files_re_a2a9.svg b/src/frontend/src/assets/undraw_transfer_files_re_a2a9.svg new file mode 100644 index 000000000..c5930b9ea --- /dev/null +++ b/src/frontend/src/assets/undraw_transfer_files_re_a2a9.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/frontend/src/components/NewFLowCard2/index.tsx b/src/frontend/src/components/NewFLowCard2/index.tsx new file mode 100644 index 000000000..096cd2314 --- /dev/null +++ b/src/frontend/src/components/NewFLowCard2/index.tsx @@ -0,0 +1,26 @@ +import { Card, CardContent } from "../ui/card"; +import useFlowsManagerStore from "../../stores/flowsManagerStore"; +import { useNavigate } from "react-router-dom"; +import IconComponent from "../genericIconComponent"; +import { cn } from "../../utils/utils"; + +export default function NewFlowCardComponent() { + const addFlow = useFlowsManagerStore((state) => state.addFlow); + const navigate = useNavigate(); + return ( + { + addFlow(true).then((id) => { + navigate("/flow/" + id); + }); + }} className="pt-4 w-80 h-72 cursor-pointer"> + + + +
Create from scratch
+
+
+ ) +} \ No newline at end of file diff --git a/src/frontend/src/components/undrawCards/index.tsx b/src/frontend/src/components/undrawCards/index.tsx new file mode 100644 index 000000000..c12334c56 --- /dev/null +++ b/src/frontend/src/components/undrawCards/index.tsx @@ -0,0 +1,36 @@ + +import { useNavigate } from "react-router-dom"; +/// +//@ts-ignore +import { ReactComponent as TransferFiles } from "../../assets/undraw_transfer_files_re_a2a9.svg" +import useFlowsManagerStore from "../../stores/flowsManagerStore"; +import { FlowType } from "../../types/flow" +import { updateIds } from "../../utils/reactflowUtils"; +import ShadTooltip from "../ShadTooltipComponent" +import { Card, CardContent, CardDescription, CardFooter, CardTitle } from "../ui/card" + +export default function UndrawCardComponent({ + flow +}: { flow: FlowType }) { + const addFlow = useFlowsManagerStore((state) => state.addFlow); + const navigate = useNavigate(); + return ( + { + updateIds(flow.data!); + addFlow(true, flow).then((id) => { + navigate("/flow/" + id); + }); + }} className="pt-4 w-80 h-72 cursor-pointer"> + +
+
+
+ + {flow.name} + +
{flow.description}
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/frontend/src/pages/MainPage/index.tsx b/src/frontend/src/pages/MainPage/index.tsx index c6e233521..e2d30fe4f 100644 --- a/src/frontend/src/pages/MainPage/index.tsx +++ b/src/frontend/src/pages/MainPage/index.tsx @@ -2,7 +2,7 @@ import { Group, ToyBrick } from "lucide-react"; import { useEffect, useState } from "react"; import { Outlet, useLocation, useNavigate } from "react-router-dom"; import DropdownButton from "../../components/DropdownButtonComponent"; -import NewFlowCardComponent from "../../components/NewFlowCardComponent"; +import NewFlowCardComponent from "../../components/NewFLowCard2";; import ExampleCardComponent from "../../components/exampleComponent"; import IconComponent from "../../components/genericIconComponent"; import PageLayout from "../../components/pageLayout"; @@ -17,6 +17,7 @@ import BaseModal from "../../modals/baseModal"; import useAlertStore from "../../stores/alertStore"; import useFlowsManagerStore from "../../stores/flowsManagerStore"; import { downloadFlows } from "../../utils/reactflowUtils"; +import UndrawCardComponent from "../../components/undrawCards"; export default function HomePage(): JSX.Element { const addFlow = useFlowsManagerStore((state) => state.addFlow); const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow); @@ -119,21 +120,21 @@ export default function HomePage(): JSX.Element {
- Create a New Flow + Get Started -
{examples.map((example, idx) => { - return ; + return ; })}
diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index dcc0ae6b3..4751a05d6 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -41,6 +41,7 @@ import { EyeOff, File, FileDown, + SquarePen, FileSearch, FileSearch2, FileSliders, @@ -374,6 +375,7 @@ export const nodeIconsLucide: iconsType = { XCircle, Info, CheckCircle2, + SquarePen, Zap, MessagesSquare, ExternalLink, From 0f45a09494afa9ef4151bc884e08d42d3b24a99f Mon Sep 17 00:00:00 2001 From: igorrCarvalho Date: Wed, 6 Mar 2024 18:06:52 -0300 Subject: [PATCH 159/199] Refactor: flow toolbar buttons disable intead of dissapear --- src/frontend/src/App.css | 3 +- src/frontend/src/components/IOview/index.tsx | 8 +++- .../src/components/chatComponent/index.tsx | 42 ++++++++++--------- src/frontend/src/style/index.css | 3 +- 4 files changed, 33 insertions(+), 23 deletions(-) diff --git a/src/frontend/src/App.css b/src/frontend/src/App.css index c2427fb0f..6aa681415 100644 --- a/src/frontend/src/App.css +++ b/src/frontend/src/App.css @@ -97,4 +97,5 @@ body { .custom-hover:hover { background-color: rgba(99, 102, 241, 0.1); /* Medium indigo color with 20% opacity */ -} \ No newline at end of file +} + diff --git a/src/frontend/src/components/IOview/index.tsx b/src/frontend/src/components/IOview/index.tsx index 73f0a068e..ced31a177 100644 --- a/src/frontend/src/components/IOview/index.tsx +++ b/src/frontend/src/components/IOview/index.tsx @@ -20,7 +20,12 @@ import { Badge } from "../ui/badge"; import { Button } from "../ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; -export default function IOView({ children, open, setOpen }): JSX.Element { +export default function IOView({ children, open, setOpen, disable }: { + children: JSX.Element; + open: boolean; + setOpen: (open: boolean) => void; + disable?: boolean; +}): JSX.Element { const inputs = useFlowStore((state) => state.inputs).filter( (input) => input.type !== "ChatInput" ); @@ -98,6 +103,7 @@ export default function IOView({ children, open, setOpen }): JSX.Element { size={haveChat ? (selectedTab === 0 ? "large-thin" : "large") : "small"} open={open} setOpen={setOpen} + disable={disable} > {children} {/* TODO ADAPT TO ALL TYPES OF INPUTS AND OUTPUTS */} diff --git a/src/frontend/src/components/chatComponent/index.tsx b/src/frontend/src/components/chatComponent/index.tsx index a53184856..3d3a01562 100644 --- a/src/frontend/src/components/chatComponent/index.tsx +++ b/src/frontend/src/components/chatComponent/index.tsx @@ -51,9 +51,9 @@ export default function FlowToolbar({ flow }: ChatType): JSX.Element {
- ) : ( - - - )} - + @@ -326,31 +320,13 @@ export default function NodeToolbarComponent({ Edit{" "}
{" "} - )} - - {isSaved ? ( - -
- {" "} - Save{" "} -
{" "} -
- ) : ( - hasCode && ( - -
- {" "} - Save{" "} -
{" "} -
- ) - )} + )} + +
+ + Duplicate +
{" "} +