feat: add dump and dumps methods to Graph (#3202)

* feat(utils.py): add escape_json_dump function to escape JSON strings for Edge dictionaries

* refactor(Output): streamline add_types method to prevent duplicate entries in types list for improved type management

* feat(data.py): add classmethod decorator to validate_data for enhanced validation logic when checking data types

* feat(setup.py): implement retry logic for loading starter projects to enhance robustness against JSON decode errors

* fix(input_mixin.py): improve model_config formatting and update field_type alias for clarity and consistency in field definitions

* feat(types.py): refactor vertex constructors to use NodeData and add input/output methods for better component interaction

* feat(schema.py): add NodeData and Position TypedDicts for improved type safety and structure in vertex data handling

* feat(base.py): update Vertex to use NodeData type and add to_data method for better data management and access

* refactor(schema.py): update TargetHandle and SourceHandle models to include model_config attribute

* Add TypedDict classes for graph schema serialization in `schema.py`

* Refactor `Edge` class to improve handle validation and data handling

- Consolidated imports and removed redundant `BaseModel` definitions for `SourceHandle` and `TargetHandle`.
- Added `valid_handles`, `target_param`, and `_target_handle` attributes to `Edge` class.
- Enhanced handle validation logic to distinguish between dictionary and string types.
- Introduced `to_data` method to return edge data.
- Updated attribute names to follow consistent naming conventions (`base_classes`, `input_types`, `field_name`).

* Refactor `Edge` class to improve handle validation and data handling

* Refactor: Standardize attribute naming and add `to_data` method in Edge class

- Renamed attributes to use snake_case consistently (`baseClasses` to `base_classes`, `inputTypes` to `input_types`, `fieldName` to `field_name`).
- Added `to_data` method to return `_data` attribute.
- Updated validation methods to use new attribute names.

* Refactor: Update Edge class to consistently use snake_case for attributes and improve validation logic for handles

* Refactor: Change node argument type in add_node and _create_vertex methods to NodeData for better type safety and clarity

* Refactor: Implement JSON serialization for graph data with `dumps` and `dump` methods, enhancing data export capabilities

* Refactor: Add pytest fixtures for ingestion and RAG graphs, enhance test structure for better clarity and organization

* Refactor: Add pytest fixtures for memory_chatbot_graph tests and improve test structure

* Refactor: Remove unused methods in ComponentVertex class to streamline code and improve readability

* Refactor: Remove unnecessary line in ComponentVertex class to enhance code clarity and maintainability

* Refactor: Update import path for DefaultPromptField to improve code organization and maintainability in api_utils.py

* Refactor: Update import path for DefaultPromptField to enhance code organization and maintainability in prompt.py

* fix: Remove  fixture in test_memory_chatbot.py that blocked db setup

* Refactor: Add durations path for unit tests to improve test reporting

* Refactor: Add splitting algorithm option for unit tests

* Add async option to Makefile for unit tests and update GitHub Actions workflow

- Introduced `async` variable in Makefile to conditionally run unit tests with or without parallel execution.
- Updated `unit_tests` target in Makefile to handle `async` flag.
- Modified GitHub Actions workflow to set `async=false` for unit tests.
This commit is contained in:
Gabriel Luiz Freitas Almeida 2024-08-05 18:00:46 -03:00 • committed by GitHub
commit bb1bc5c2df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 797 additions and 107 deletions

View file

@ -50,7 +50,7 @@ jobs:
with: with:
timeout_minutes: 12 timeout_minutes: 12
max_attempts: 2 max_attempts: 2
command: make unit_tests args="--splits ${{ matrix.splitCount }} --group ${{ matrix.group }}" command: make unit_tests async=false args="--splits ${{ matrix.splitCount }} --group ${{ matrix.group }}"
test-cli: test-cli:
name: Test CLI - Python ${{ matrix.python-version }} name: Test CLI - Python ${{ matrix.python-version }}

View file

@ -18,7 +18,7 @@ env ?= .env
open_browser ?= true open_browser ?= true
path = src/backend/base/langflow/frontend path = src/backend/base/langflow/frontend
workers ?= 1 workers ?= 1
async ?= true
all: help all: help
###################### ######################
@ -130,14 +130,25 @@ coverage: ## run the tests and generate a coverage report
@poetry run coverage erase @poetry run coverage erase
unit_tests: ## run unit tests unit_tests: ## run unit tests
ifeq ($(async), true)
poetry run pytest src/backend/tests \ poetry run pytest src/backend/tests \
--ignore=src/backend/tests/integration \ --ignore=src/backend/tests/integration \
--instafail -ra -n auto -m "not api_key_required" \ --instafail -n auto -ra -m "not api_key_required" \
--durations-path src/backend/tests/.test_durations \
--splitting-algorithm least_duration \
$(args) $(args)
else
poetry run pytest src/backend/tests \
--ignore=src/backend/tests/integration \
--instafail -ra -m "not api_key_required" \
--durations-path src/backend/tests/.test_durations \
--splitting-algorithm least_duration \
$(args)
endif
integration_tests: ## run integration tests integration_tests: ## run integration tests
poetry run pytest src/backend/tests/integration \ poetry run pytest src/backend/tests/integration \
--instafail -ra -n auto \ --instafail -ra \
$(args) $(args)
tests: ## run unit, integration, coverage tests tests: ## run unit, integration, coverage tests

View file

@ -6,7 +6,7 @@ from langchain_core.prompts import PromptTemplate
from loguru import logger from loguru import logger
from langflow.interface.utils import extract_input_variables_from_prompt from langflow.interface.utils import extract_input_variables_from_prompt
from langflow.template.field.prompt import DefaultPromptField from langflow.inputs.inputs import DefaultPromptField
_INVALID_CHARACTERS = { _INVALID_CHARACTERS = {

View file

@ -1,51 +1,31 @@
from typing import TYPE_CHECKING, Any, List, Optional, cast from typing import TYPE_CHECKING, Any, cast
from loguru import logger from loguru import logger
from pydantic import BaseModel, Field, field_validator
from langflow.graph.edge.schema import EdgeData from langflow.graph.edge.schema import EdgeData, SourceHandle, TargetHandle, TargetHandleDict
from langflow.schema.schema import INPUT_FIELD_NAME from langflow.schema.schema import INPUT_FIELD_NAME
if TYPE_CHECKING: if TYPE_CHECKING:
from langflow.graph.vertex.base import Vertex from langflow.graph.vertex.base import Vertex
class SourceHandle(BaseModel):
baseClasses: list[str] = Field(default_factory=list, 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.")
name: Optional[str] = Field(None, description="Name of the source handle.")
output_types: List[str] = Field(default_factory=list, description="List of output types for the source handle.")
@field_validator("name", mode="before")
@classmethod
def validate_name(cls, v, _info):
if _info.data["dataType"] == "GroupNode":
# 'OpenAIModel-u4iGV_text_output'
splits = v.split("_", 1)
if len(splits) != 2:
raise ValueError(f"Invalid source handle name {v}")
v = splits[1]
return v
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.")
type: str = Field(..., description="Type of the target handle.")
class Edge: class Edge:
def __init__(self, source: "Vertex", target: "Vertex", edge: EdgeData): def __init__(self, source: "Vertex", target: "Vertex", edge: EdgeData):
self.source_id: str = source.id if source else "" self.source_id: str = source.id if source else ""
self.target_id: str = target.id if target else "" self.target_id: str = target.id if target else ""
self.valid_handles: bool = False
self.target_param: str | None = None
self._target_handle: TargetHandleDict | str | None = None
self._data = edge.copy()
if data := edge.get("data", {}): if data := edge.get("data", {}):
self._source_handle = data.get("sourceHandle", {}) self._source_handle = data.get("sourceHandle", {})
self._target_handle = data.get("targetHandle", {}) self._target_handle = cast(TargetHandleDict, data.get("targetHandle", {}))
self.source_handle: SourceHandle = SourceHandle(**self._source_handle) self.source_handle: SourceHandle = SourceHandle(**self._source_handle)
if isinstance(self._target_handle, dict):
self.target_handle: TargetHandle = TargetHandle(**self._target_handle) self.target_handle: TargetHandle = TargetHandle(**self._target_handle)
self.target_param = self.target_handle.fieldName else:
raise ValueError("Target handle is not a dictionary")
self.target_param = self.target_handle.field_name
# validate handles # validate handles
self.validate_handles(source, target) self.validate_handles(source, target)
else: else:
@ -55,23 +35,31 @@ class Edge:
self._target_handle = edge.get("targetHandle", "") # type: ignore self._target_handle = edge.get("targetHandle", "") # type: ignore
# 'BaseLoader;BaseOutputParser|documents|PromptTemplate-zmTlD' # 'BaseLoader;BaseOutputParser|documents|PromptTemplate-zmTlD'
# target_param is documents # target_param is documents
self.target_param = cast(str, self._target_handle.split("|")[1]) # type: ignore if isinstance(self._target_handle, str):
self.target_param = self._target_handle.split("|")[1]
self.source_handle = None
self.target_handle = None
else:
raise ValueError("Target handle is not a string")
# Validate in __init__ to fail fast # Validate in __init__ to fail fast
self.validate_edge(source, target) self.validate_edge(source, target)
def to_data(self):
return self._data
def validate_handles(self, source, target) -> None: def validate_handles(self, source, target) -> None:
if isinstance(self._source_handle, str) or self.source_handle.baseClasses: if isinstance(self._source_handle, str) or self.source_handle.base_classes:
self._legacy_validate_handles(source, target) self._legacy_validate_handles(source, target)
else: else:
self._validate_handles(source, target) self._validate_handles(source, target)
def _validate_handles(self, source, target) -> None: def _validate_handles(self, source, target) -> None:
if self.target_handle.inputTypes is None: if self.target_handle.input_types is None:
self.valid_handles = self.target_handle.type in self.source_handle.output_types self.valid_handles = self.target_handle.type in self.source_handle.output_types
elif self.source_handle.output_types is not None: elif self.source_handle.output_types is not None:
self.valid_handles = ( self.valid_handles = (
any(output_type in self.target_handle.inputTypes for output_type in self.source_handle.output_types) any(output_type in self.target_handle.input_types for output_type in self.source_handle.output_types)
or self.target_handle.type in self.source_handle.output_types or self.target_handle.type in self.source_handle.output_types
) )
@ -81,12 +69,12 @@ class Edge:
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 _legacy_validate_handles(self, source, target) -> None: def _legacy_validate_handles(self, source, target) -> None:
if self.target_handle.inputTypes is None: if self.target_handle.input_types 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.base_classes
else: else:
self.valid_handles = ( self.valid_handles = (
any(baseClass in self.target_handle.inputTypes for baseClass in self.source_handle.baseClasses) any(baseClass in self.target_handle.input_types for baseClass in self.source_handle.base_classes)
or self.target_handle.type in self.source_handle.baseClasses or self.target_handle.type in self.source_handle.base_classes
) )
if not self.valid_handles: if not self.valid_handles:
logger.debug(self.source_handle) logger.debug(self.source_handle)
@ -101,9 +89,9 @@ class Edge:
self.target_handle = state.get("target_handle") self.target_handle = state.get("target_handle")
def validate_edge(self, source, target) -> None: def validate_edge(self, source, target) -> None:
# If the self.source_handle has baseClasses, then we are using the legacy # If the self.source_handle has base_classes, then we are using the legacy
# way of defining the source and target handles # way of defining the source and target handles
if isinstance(self._source_handle, str) or self.source_handle.baseClasses: if isinstance(self._source_handle, str) or self.source_handle.base_classes:
self._legacy_validate_edge(source, target) self._legacy_validate_edge(source, target)
else: else:
self._validate_edge(source, target) self._validate_edge(source, target)
@ -230,5 +218,5 @@ class ContractEdge(Edge):
if (hasattr(self, "source_handle") and self.source_handle) and ( if (hasattr(self, "source_handle") and self.source_handle) and (
hasattr(self, "target_handle") and self.target_handle hasattr(self, "target_handle") and self.target_handle
): ):
return f"{self.source_id} -[{self.source_handle.name}->{self.target_handle.fieldName}]-> {self.target_id}" return f"{self.source_id} -[{self.source_handle.name}->{self.target_handle.field_name}]-> {self.target_id}"
return f"{self.source_id} -[{self.target_param}]-> {self.target_id}" return f"{self.source_id} -[{self.target_param}]-> {self.target_id}"

View file

@ -1,6 +1,6 @@
from typing import Any, List, Optional from typing import Any, List, Optional
from pydantic import Field, field_validator from pydantic import ConfigDict, Field, field_validator
from typing_extensions import TypedDict from typing_extensions import TypedDict
from langflow.helpers.base_model import BaseModel from langflow.helpers.base_model import BaseModel
@ -39,7 +39,8 @@ class Payload(BaseModel):
class TargetHandle(BaseModel): class TargetHandle(BaseModel):
fieldName: str = Field(..., alias="fieldName", description="Field name for the target handle.") model_config = ConfigDict(populate_by_name=True)
field_name: str = Field(..., alias="fieldName", description="Field name for the target handle.")
id: str = Field(..., description="Unique identifier for the target handle.") id: str = Field(..., description="Unique identifier for the target handle.")
input_types: List[str] = Field( input_types: List[str] = Field(
default_factory=list, alias="inputTypes", description="List of input types for the target handle." default_factory=list, alias="inputTypes", description="List of input types for the target handle."
@ -48,6 +49,7 @@ class TargetHandle(BaseModel):
class SourceHandle(BaseModel): class SourceHandle(BaseModel):
model_config = ConfigDict(populate_by_name=True)
base_classes: list[str] = Field( base_classes: list[str] = Field(
default_factory=list, alias="baseClasses", description="List of base classes for the source handle." default_factory=list, alias="baseClasses", description="List of base classes for the source handle."
) )

View file

@ -1,4 +1,5 @@
import asyncio import asyncio
import json
import uuid import uuid
from collections import defaultdict, deque from collections import defaultdict, deque
from datetime import datetime, timezone from datetime import datetime, timezone
@ -14,11 +15,12 @@ from langflow.graph.edge.base import ContractEdge
from langflow.graph.edge.schema import EdgeData from langflow.graph.edge.schema import EdgeData
from langflow.graph.graph.constants import Finish, lazy_load_vertex_dict from langflow.graph.graph.constants import Finish, lazy_load_vertex_dict
from langflow.graph.graph.runnable_vertices_manager import RunnableVerticesManager from langflow.graph.graph.runnable_vertices_manager import RunnableVerticesManager
from langflow.graph.graph.schema import VertexBuildResult from langflow.graph.graph.schema import GraphData, GraphDump, VertexBuildResult
from langflow.graph.graph.state_manager import GraphStateManager from langflow.graph.graph.state_manager import GraphStateManager
from langflow.graph.graph.utils import find_start_component_id, process_flow, sort_up_to_vertex from langflow.graph.graph.utils import find_start_component_id, process_flow, sort_up_to_vertex
from langflow.graph.schema import InterfaceComponentTypes, RunOutputs from langflow.graph.schema import InterfaceComponentTypes, RunOutputs
from langflow.graph.vertex.base import Vertex, VertexStates from langflow.graph.vertex.base import Vertex, VertexStates
from langflow.graph.vertex.schema import NodeData
from langflow.graph.vertex.types import ComponentVertex, InterfaceVertex, StateVertex from langflow.graph.vertex.types import ComponentVertex, InterfaceVertex, StateVertex
from langflow.schema import Data from langflow.schema import Data
from langflow.schema.schema import INPUT_FIELD_NAME, InputType from langflow.schema.schema import INPUT_FIELD_NAME, InputType
@ -75,7 +77,7 @@ class Graph:
self.vertices: List[Vertex] = [] self.vertices: List[Vertex] = []
self.run_manager = RunnableVerticesManager() self.run_manager = RunnableVerticesManager()
self.state_manager = GraphStateManager() self.state_manager = GraphStateManager()
self._vertices: List[dict] = [] self._vertices: List[NodeData] = []
self._edges: List[EdgeData] = [] self._edges: List[EdgeData] = []
self.top_level_vertices: List[str] = [] self.top_level_vertices: List[str] = []
self.vertex_map: Dict[str, Vertex] = {} self.vertex_map: Dict[str, Vertex] = {}
@ -86,6 +88,7 @@ class Graph:
self._run_queue: deque[str] = deque() self._run_queue: deque[str] = deque()
self._first_layer: List[str] = [] self._first_layer: List[str] = []
self._lock = asyncio.Lock() self._lock = asyncio.Lock()
self.raw_graph_data: GraphData = {"nodes": [], "edges": []}
try: try:
self.tracing_service: "TracingService" | None = get_tracing_service() self.tracing_service: "TracingService" | None = get_tracing_service()
except Exception as exc: except Exception as exc:
@ -97,7 +100,39 @@ class Graph:
if (start is not None and end is None) or (start is None and end is not None): if (start is not None and end is None) or (start is None and end is not None):
raise ValueError("You must provide both input and output components") raise ValueError("You must provide both input and output components")
def add_nodes_and_edges(self, nodes: List[Dict], edges: List[EdgeData]): def dumps(
self,
name: Optional[str] = None,
description: Optional[str] = None,
endpoint_name: Optional[str] = None,
) -> str:
graph_dict = self.dump(name, description, endpoint_name)
return json.dumps(graph_dict, indent=4, sort_keys=True)
def dump(
self, name: Optional[str] = None, description: Optional[str] = None, endpoint_name: Optional[str] = None
) -> GraphDump:
if self.raw_graph_data != {"nodes": [], "edges": []}:
data_dict = self.raw_graph_data
else:
# we need to convert the vertices and edges to json
nodes = [node.to_data() for node in self.vertices]
edges = [edge.to_data() for edge in self.edges]
self.raw_graph_data = {"nodes": nodes, "edges": edges}
data_dict = self.raw_graph_data
graph_dict: GraphDump = {
"data": data_dict,
"is_component": len(data_dict.get("nodes", [])) == 1 and data_dict["edges"] == [],
}
if name:
graph_dict["name"] = name
if description:
graph_dict["description"] = description
if endpoint_name:
graph_dict["endpoint_name"] = endpoint_name
return graph_dict
def add_nodes_and_edges(self, nodes: List[NodeData], edges: List[EdgeData]):
self._vertices = nodes self._vertices = nodes
self._edges = edges self._edges = edges
self.raw_graph_data = {"nodes": nodes, "edges": edges} self.raw_graph_data = {"nodes": nodes, "edges": edges}
@ -183,7 +218,7 @@ class Graph:
return return
def start(self, inputs: Optional[List[dict]] = None) -> Generator: def start(self, inputs: Optional[List[dict]] = None) -> Generator:
#! Change this soon #! Change this ASAP
nest_asyncio.apply() nest_asyncio.apply()
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
async_gen = self.async_start(inputs) async_gen = self.async_start(inputs)
@ -208,8 +243,7 @@ class Graph:
self.in_degree_map[target_id] += 1 self.in_degree_map[target_id] += 1
self.parent_child_map[source_id].append(target_id) self.parent_child_map[source_id].append(target_id)
# TODO: Create a TypedDict to represente the node def add_node(self, node: NodeData):
def add_node(self, node: dict):
self._vertices.append(node) self._vertices.append(node)
def add_edge(self, edge: EdgeData): def add_edge(self, edge: EdgeData):
@ -1400,7 +1434,7 @@ class Graph:
return vertices return vertices
def _create_vertex(self, frontend_data: dict): def _create_vertex(self, frontend_data: NodeData):
vertex_data = frontend_data["data"] vertex_data = frontend_data["data"]
vertex_type: str = vertex_data["type"] # type: ignore vertex_type: str = vertex_data["type"] # type: ignore
vertex_base_type: str = vertex_data["node"]["template"]["_type"] # type: ignore vertex_base_type: str = vertex_data["node"]["template"]["_type"] # type: ignore

View file

@ -1,10 +1,35 @@
from typing import TYPE_CHECKING, NamedTuple from typing import TYPE_CHECKING, NamedTuple
from typing_extensions import NotRequired, TypedDict
from langflow.graph.edge.schema import EdgeData
from langflow.graph.vertex.schema import NodeData
if TYPE_CHECKING: if TYPE_CHECKING:
from langflow.graph.schema import ResultData from langflow.graph.schema import ResultData
from langflow.graph.vertex.base import Vertex from langflow.graph.vertex.base import Vertex
class ViewPort(TypedDict):
x: float
y: float
zoom: float
class GraphData(TypedDict):
nodes: list[NodeData]
edges: list[EdgeData]
viewport: NotRequired[ViewPort]
class GraphDump(TypedDict, total=False):
data: GraphData
is_component: bool
name: str
description: str
endpoint_name: str
class VertexBuildResult(NamedTuple): class VertexBuildResult(NamedTuple):
result_dict: "ResultData" result_dict: "ResultData"
params: str params: str

View file

@ -13,6 +13,7 @@ from loguru import logger
from langflow.exceptions.component import ComponentBuildException from langflow.exceptions.component import ComponentBuildException
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, log_transaction from langflow.graph.utils import UnbuiltObject, UnbuiltResult, log_transaction
from langflow.graph.vertex.schema import NodeData
from langflow.interface.initialize import loading from langflow.interface.initialize import loading
from langflow.interface.listing import lazy_load_dict from langflow.interface.listing import lazy_load_dict
from langflow.schema.artifact import ArtifactType from langflow.schema.artifact import ArtifactType
@ -42,7 +43,7 @@ class VertexStates(str, Enum):
class Vertex: class Vertex:
def __init__( def __init__(
self, self,
data: Dict, data: NodeData,
graph: "Graph", graph: "Graph",
base_type: Optional[str] = None, base_type: Optional[str] = None,
is_task: bool = False, is_task: bool = False,
@ -63,7 +64,7 @@ class Vertex:
self.has_external_input = False self.has_external_input = False
self.has_external_output = False self.has_external_output = False
self.graph = graph self.graph = graph
self._data = data self._data = data.copy()
self.base_type: Optional[str] = base_type self.base_type: Optional[str] = base_type
self.outputs: List[Dict] = [] self.outputs: List[Dict] = []
self._parse_data() self._parse_data()
@ -101,6 +102,9 @@ class Vertex:
raise ValueError(f"Vertex {self.id} does not have a component instance.") raise ValueError(f"Vertex {self.id} does not have a component instance.")
self._custom_component._set_input_value(name, value) self._custom_component._set_input_value(name, value)
def to_data(self):
return self._data
def add_component_instance(self, component_instance: "Component"): def add_component_instance(self, component_instance: "Component"):
component_instance.set_vertex(self) component_instance.set_vertex(self)
self._custom_component = component_instance self._custom_component = component_instance

View file

@ -0,0 +1,21 @@
from typing import Dict
from typing_extensions import NotRequired, TypedDict
class Position(TypedDict):
x: float
y: float
class NodeData(TypedDict):
id: str
data: Dict
dragging: NotRequired[bool]
height: NotRequired[int]
width: NotRequired[int]
position: NotRequired[Position]
positionAbsolute: NotRequired[Position]
selected: NotRequired[bool]
parent_node_id: NotRequired[str]
type: str

View file

@ -9,6 +9,7 @@ from loguru import logger
from langflow.graph.schema import CHAT_COMPONENTS, RECORDS_COMPONENTS, InterfaceComponentTypes, ResultData from langflow.graph.schema import CHAT_COMPONENTS, RECORDS_COMPONENTS, InterfaceComponentTypes, ResultData
from langflow.graph.utils import UnbuiltObject, log_transaction, log_vertex_build, serialize_field from langflow.graph.utils import UnbuiltObject, log_transaction, log_vertex_build, serialize_field
from langflow.graph.vertex.base import Vertex from langflow.graph.vertex.base import Vertex
from langflow.graph.vertex.schema import NodeData
from langflow.inputs.inputs import InputTypes from langflow.inputs.inputs import InputTypes
from langflow.schema import Data from langflow.schema import Data
from langflow.schema.artifact import ArtifactType from langflow.schema.artifact import ArtifactType
@ -23,7 +24,7 @@ if TYPE_CHECKING:
class CustomComponentVertex(Vertex): class CustomComponentVertex(Vertex):
def __init__(self, data: Dict, graph): def __init__(self, data: NodeData, graph):
super().__init__(data, graph=graph, base_type="custom_components") super().__init__(data, graph=graph, base_type="custom_components")
def _built_object_repr(self): def _built_object_repr(self):
@ -32,9 +33,19 @@ class CustomComponentVertex(Vertex):
class ComponentVertex(Vertex): class ComponentVertex(Vertex):
def __init__(self, data: Dict, graph): def __init__(self, data: NodeData, graph):
super().__init__(data, graph=graph, base_type="component") super().__init__(data, graph=graph, base_type="component")
def get_input(self, name: str) -> InputTypes:
if self._custom_component is None:
raise ValueError(f"Vertex {self.id} does not have a component instance.")
return self._custom_component.get_input(name)
def get_output(self, name: str) -> Output:
if self._custom_component is None:
raise ValueError(f"Vertex {self.id} does not have a component instance.")
return self._custom_component.get_output(name)
def _built_object_repr(self): def _built_object_repr(self):
if self.artifacts and "repr" in self.artifacts: if self.artifacts and "repr" in self.artifacts:
return self.artifacts["repr"] or super()._built_object_repr() return self.artifacts["repr"] or super()._built_object_repr()
@ -58,16 +69,6 @@ class ComponentVertex(Vertex):
for key, value in self._built_object.items(): for key, value in self._built_object.items():
self.add_result(key, value) self.add_result(key, value)
def get_input(self, name: str) -> InputTypes:
if self._custom_component is None:
raise ValueError(f"Vertex {self.id} does not have a component instance.")
return self._custom_component.get_input(name)
def get_output(self, name: str) -> Output:
if self._custom_component is None:
raise ValueError(f"Vertex {self.id} does not have a component instance.")
return self._custom_component.get_output(name)
def get_edge_with_target(self, target_id: str) -> Generator["ContractEdge", None, None]: def get_edge_with_target(self, target_id: str) -> Generator["ContractEdge", None, None]:
""" """
Get the edge with the target id. Get the edge with the target id.
@ -174,7 +175,7 @@ class ComponentVertex(Vertex):
class InterfaceVertex(ComponentVertex): class InterfaceVertex(ComponentVertex):
def __init__(self, data: Dict, graph): def __init__(self, data: NodeData, graph):
super().__init__(data, graph=graph) super().__init__(data, graph=graph)
self.steps = [self._build, self._run] self.steps = [self._build, self._run]
@ -424,7 +425,7 @@ class InterfaceVertex(ComponentVertex):
class StateVertex(ComponentVertex): class StateVertex(ComponentVertex):
def __init__(self, data: Dict, graph): def __init__(self, data: NodeData, graph):
super().__init__(data, graph=graph) super().__init__(data, graph=graph)
self.steps = [self._build] self.steps = [self._build]
self.is_state = False self.is_state = False

View file

@ -2,6 +2,7 @@ import copy
import json import json
import os import os
import shutil import shutil
import time
from collections import defaultdict from collections import defaultdict
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timezone from datetime import datetime, timezone
@ -23,6 +24,7 @@ from langflow.services.database.models.folder.utils import create_default_folder
from langflow.services.database.models.user.crud import get_user_by_username from langflow.services.database.models.user.crud import get_user_by_username
from langflow.services.deps import get_settings_service, get_storage_service, get_variable_service, session_scope from langflow.services.deps import get_settings_service, get_storage_service, get_variable_service, session_scope
from langflow.template.field.prompt import DEFAULT_PROMPT_INTUT_TYPES from langflow.template.field.prompt import DEFAULT_PROMPT_INTUT_TYPES
from langflow.utils.util import escape_json_dump
STARTER_FOLDER_NAME = "Starter Projects" STARTER_FOLDER_NAME = "Starter Projects"
STARTER_FOLDER_DESCRIPTION = "Starter projects to help you get started in Langflow." STARTER_FOLDER_DESCRIPTION = "Starter projects to help you get started in Langflow."
@ -319,10 +321,6 @@ def update_edges_with_latest_component_versions(project_data):
return project_data_copy return project_data_copy
def escape_json_dump(edge_dict):
return json.dumps(edge_dict).replace('"', "œ")
def log_node_changes(node_changes_log): def log_node_changes(node_changes_log):
# The idea here is to log the changes that were made to the nodes in debug # The idea here is to log the changes that were made to the nodes in debug
# Something like: # Something like:
@ -339,17 +337,23 @@ def log_node_changes(node_changes_log):
logger.debug("\n".join(formatted_messages)) logger.debug("\n".join(formatted_messages))
def load_starter_projects() -> list[tuple[Path, dict]]: def load_starter_projects(retries=3, delay=1) -> list[tuple[Path, dict]]:
starter_projects = [] starter_projects = []
folder = Path(__file__).parent / "starter_projects" folder = Path(__file__).parent / "starter_projects"
for file in folder.glob("*.json"): for file in folder.glob("*.json"):
attempt = 0
while attempt < retries:
with open(file, "r", encoding="utf-8") as f: with open(file, "r", encoding="utf-8") as f:
try: try:
project = orjson.loads(f.read()) project = orjson.loads(f.read())
starter_projects.append((file, project)) starter_projects.append((file, project))
logger.info(f"Loaded starter project {file}") logger.info(f"Loaded starter project {file}")
break # Break if load is successful
except orjson.JSONDecodeError as e: except orjson.JSONDecodeError as e:
attempt += 1
if attempt >= retries:
raise ValueError(f"Error loading starter project {file}: {e}") raise ValueError(f"Error loading starter project {file}: {e}")
time.sleep(delay) # Wait before retrying
return starter_projects return starter_projects

View file

@ -27,9 +27,13 @@ SerializableFieldTypes = Annotated[FieldTypes, PlainSerializer(lambda v: v.value
# Base mixin for common input field attributes and methods # Base mixin for common input field attributes and methods
class BaseInputMixin(BaseModel, validate_assignment=True): # type: ignore class BaseInputMixin(BaseModel, validate_assignment=True): # type: ignore
model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid", populate_by_name=True) model_config = ConfigDict(
arbitrary_types_allowed=True,
extra="forbid",
populate_by_name=True,
)
field_type: SerializableFieldTypes = Field(default=FieldTypes.TEXT) field_type: SerializableFieldTypes = Field(default=FieldTypes.TEXT, alias="type")
required: bool = False required: bool = False
"""Specifies if the field is required. Defaults to False.""" """Specifies if the field is required. Defaults to False."""

View file

@ -24,6 +24,7 @@ class Data(BaseModel):
default_value: Optional[str] = "" default_value: Optional[str] = ""
@model_validator(mode="before") @model_validator(mode="before")
@classmethod
def validate_data(cls, values): def validate_data(cls, values):
if not isinstance(values, dict): if not isinstance(values, dict):
raise ValueError("Data must be a dictionary") raise ValueError("Data must be a dictionary")

View file

@ -184,12 +184,9 @@ class Output(BaseModel):
return self.model_dump(by_alias=True, exclude_none=True) return self.model_dump(by_alias=True, exclude_none=True)
def add_types(self, _type: list[Any]): def add_types(self, _type: list[Any]):
for type_ in _type:
if self.types and type_ in self.types:
continue
if self.types is None: if self.types is None:
self.types = [] self.types = []
self.types.append(type_) self.types.extend([t for t in _type if t not in self.types])
def set_selected(self): def set_selected(self):
if not self.selected and self.types: if not self.selected and self.types:

View file

@ -1,3 +1,3 @@
# This file is for backwards compatibility # This file is for backwards compatibility
from langflow.inputs.inputs import DEFAULT_PROMPT_INTUT_TYPES # noqa from langflow.inputs.inputs import DEFAULT_PROMPT_INTUT_TYPES # noqa
from langflow.inputs import DefaultPromptField # noqa from langflow.inputs.inputs import DefaultPromptField # noqa

View file

@ -1,5 +1,6 @@
import importlib import importlib
import inspect import inspect
import json
import re import re
from functools import wraps from functools import wraps
from pathlib import Path from pathlib import Path
@ -456,3 +457,7 @@ def is_class_method(func, cls):
Check if a function is a class method. Check if a function is a class method.
""" """
return inspect.ismethod(func) and func.__self__ is cls.__class__ return inspect.ismethod(func) and func.__self__ is cls.__class__
def escape_json_dump(edge_dict):
return json.dumps(edge_dict).replace('"', "œ")

View file

@ -0,0 +1,354 @@
{
"src/backend/tests/test_endpoints.py::test_build_vertex_invalid_flow_id": 3.1494096249807626,
"src/backend/tests/test_endpoints.py::test_build_vertex_invalid_vertex_id": 3.0606157919974066,
"src/backend/tests/test_endpoints.py::test_get_all": 10.10167008501594,
"src/backend/tests/test_endpoints.py::test_get_vertices": 4.5017141660209745,
"src/backend/tests/test_endpoints.py::test_get_vertices_flow_not_found": 3.7886676250200253,
"src/backend/tests/test_endpoints.py::test_invalid_flow_id": 4.073716707964195,
"src/backend/tests/test_endpoints.py::test_invalid_prompt": 2.7002592499775346,
"src/backend/tests/test_endpoints.py::test_invalid_run_with_input_type_chat": 2.987766916019609,
"src/backend/tests/test_endpoints.py::test_post_validate_code": 3.0467621669813525,
"src/backend/tests/test_endpoints.py::test_successful_run_with_input_type_any": 14.8548604179814,
"src/backend/tests/test_endpoints.py::test_successful_run_with_input_type_chat": 6.242352208995726,
"src/backend/tests/test_endpoints.py::test_successful_run_with_input_type_text": 5.7594154170074034,
"src/backend/tests/test_endpoints.py::test_successful_run_with_output_type_any": 7.347130999987712,
"src/backend/tests/test_endpoints.py::test_successful_run_with_output_type_debug": 6.291947416990297,
"src/backend/tests/test_endpoints.py::test_successful_run_with_output_type_text": 14.872085083043203,
"src/backend/tests/test_endpoints.py::test_valid_prompt": 2.7850471249839757,
"src/backend/tests/test_endpoints.py::test_various_prompts[The weather is {weather} today.-expected_input_variables1]": 2.535564499994507,
"src/backend/tests/test_endpoints.py::test_various_prompts[This prompt has no variables.-expected_input_variables2]": 9.15231529099401,
"src/backend/tests/test_endpoints.py::test_various_prompts[{a}, {b}, and {c} are variables.-expected_input_variables3]": 2.640623040992068,
"src/backend/tests/test_endpoints.py::test_various_prompts[{color} is my favorite color.-expected_input_variables0]": 2.079908042011084,
"src/backend/tests/test_messages_endpoints.py::test_delete_messages": 2.515260499989381,
"src/backend/tests/test_messages_endpoints.py::test_delete_messages_session": 2.3651068749895785,
"src/backend/tests/test_messages_endpoints.py::test_update_message": 2.5627032090269495,
"src/backend/tests/test_messages_endpoints.py::test_update_message_not_found": 3.504595792008331,
"src/backend/tests/test_schema.py::TestInput::test_field_type_str": 0.0005162910092622042,
"src/backend/tests/test_schema.py::TestInput::test_field_type_type": 0.0002682080084923655,
"src/backend/tests/test_schema.py::TestInput::test_input_to_dict": 0.0003857500269077718,
"src/backend/tests/test_schema.py::TestInput::test_invalid_field_type": 0.00031291699269786477,
"src/backend/tests/test_schema.py::TestInput::test_post_process_type_function": 0.0005505419976543635,
"src/backend/tests/test_schema.py::TestInput::test_serialize_field_type": 0.0002683750062715262,
"src/backend/tests/test_schema.py::TestInput::test_validate_type_class": 0.0003414590028114617,
"src/backend/tests/test_schema.py::TestInput::test_validate_type_string": 0.0002427089784760028,
"src/backend/tests/test_schema.py::TestOutput::test_output_add_types": 0.000245749979512766,
"src/backend/tests/test_schema.py::TestOutput::test_output_default": 0.00026183397858403623,
"src/backend/tests/test_schema.py::TestOutput::test_output_set_selected": 0.0003107920056208968,
"src/backend/tests/test_schema.py::TestOutput::test_output_to_dict": 0.0004964589898008853,
"src/backend/tests/test_schema.py::TestOutput::test_output_validate_display_name": 0.0005334159650374204,
"src/backend/tests/test_schema.py::TestOutput::test_output_validate_model": 0.00029370796983130276,
"src/backend/tests/test_schema.py::TestPostProcessType::test_custom_type": 0.001362041017273441,
"src/backend/tests/test_schema.py::TestPostProcessType::test_int_type": 0.00023837501066736877,
"src/backend/tests/test_schema.py::TestPostProcessType::test_list_custom_type": 0.004543458024272695,
"src/backend/tests/test_schema.py::TestPostProcessType::test_list_int_type": 0.0002362079976592213,
"src/backend/tests/test_schema.py::TestPostProcessType::test_union_custom_type": 0.0005842499958816916,
"src/backend/tests/test_schema.py::TestPostProcessType::test_union_type": 0.003973040962591767,
"src/backend/tests/test_user.py::test_add_user": 3.298028166987933,
"src/backend/tests/test_user.py::test_data_consistency_after_delete": 10.030325876054121,
"src/backend/tests/test_user.py::test_data_consistency_after_update": 2.9754588740179315,
"src/backend/tests/test_user.py::test_deactivated_user_cannot_access": 3.544328290998237,
"src/backend/tests/test_user.py::test_deactivated_user_cannot_login": 3.9071091239748057,
"src/backend/tests/test_user.py::test_delete_user": 4.161238500004401,
"src/backend/tests/test_user.py::test_delete_user_wrong_id": 2.7632550839625765,
"src/backend/tests/test_user.py::test_inactive_user": 3.334006417018827,
"src/backend/tests/test_user.py::test_normal_user_cant_delete_user": 2.9729639159922954,
"src/backend/tests/test_user.py::test_normal_user_cant_read_all_users": 2.6966073329967912,
"src/backend/tests/test_user.py::test_patch_reset_password": 11.245606623997446,
"src/backend/tests/test_user.py::test_patch_user": 3.2588992070232052,
"src/backend/tests/test_user.py::test_patch_user_wrong_id": 3.3168086239602417,
"src/backend/tests/test_user.py::test_read_all_users": 2.440687207999872,
"src/backend/tests/test_user.py::test_user_waiting_for_approval": 9.475323291990208,
"src/backend/tests/test_webhook.py::test_webhook_endpoint": 12.348436542029958,
"src/backend/tests/test_webhook.py::test_webhook_flow_on_run_endpoint": 15.205204916041112,
"src/backend/tests/test_webhook.py::test_webhook_with_random_payload": 8.713364291994367,
"src/backend/tests/unit/components/prompts/test_prompt_component.py::TestPromptComponent::test_post_code_processing": 0.008164541970472783,
"src/backend/tests/unit/custom/custom_component/test_component.py::test_set_invalid_output": 0.000454624998383224,
"src/backend/tests/unit/graph/graph/test_base.py::test_graph": 0.007537708996096626,
"src/backend/tests/unit/graph/graph/test_base.py::test_graph_functional": 0.020996668026782572,
"src/backend/tests/unit/graph/graph/test_base.py::test_graph_functional_async_start": 0.009653333021560684,
"src/backend/tests/unit/graph/graph/test_base.py::test_graph_functional_start": 0.008162209036527202,
"src/backend/tests/unit/graph/graph/test_base.py::test_graph_functional_start_end": 0.06379004201153293,
"src/backend/tests/unit/graph/graph/test_base.py::test_graph_not_prepared": 0.01988037399132736,
"src/backend/tests/unit/graph/graph/test_runnable_vertices_manager.py::test_add_to_vertices_being_run": 2.476791499997489,
"src/backend/tests/unit/graph/graph/test_runnable_vertices_manager.py::test_are_all_predecessors_fulfilled": 2.3258769580570515,
"src/backend/tests/unit/graph/graph/test_runnable_vertices_manager.py::test_are_all_predecessors_fulfilled__wrong": 2.4165378749894444,
"src/backend/tests/unit/graph/graph/test_runnable_vertices_manager.py::test_build_run_map": 2.5142138760129455,
"src/backend/tests/unit/graph/graph/test_runnable_vertices_manager.py::test_from_dict": 2.629594833997544,
"src/backend/tests/unit/graph/graph/test_runnable_vertices_manager.py::test_from_dict_without_run_map__bad_case": 8.874073583021527,
"src/backend/tests/unit/graph/graph/test_runnable_vertices_manager.py::test_from_dict_without_run_predecessors__bad_case": 2.743527958955383,
"src/backend/tests/unit/graph/graph/test_runnable_vertices_manager.py::test_from_dict_without_vertices_being_run__bad_case": 2.8369890000030864,
"src/backend/tests/unit/graph/graph/test_runnable_vertices_manager.py::test_from_dict_without_vertices_to_run__bad_case": 2.9151457909611054,
"src/backend/tests/unit/graph/graph/test_runnable_vertices_manager.py::test_is_vertex_runnable": 8.908991582982708,
"src/backend/tests/unit/graph/graph/test_runnable_vertices_manager.py::test_is_vertex_runnable__wrong_is_active": 2.637443292012904,
"src/backend/tests/unit/graph/graph/test_runnable_vertices_manager.py::test_is_vertex_runnable__wrong_run_predecessors": 2.747438082966255,
"src/backend/tests/unit/graph/graph/test_runnable_vertices_manager.py::test_is_vertex_runnable__wrong_vertices_to_run": 2.8337462919880636,
"src/backend/tests/unit/graph/graph/test_runnable_vertices_manager.py::test_pickle": 2.065233791974606,
"src/backend/tests/unit/graph/graph/test_runnable_vertices_manager.py::test_remove_from_predecessors": 8.867784041009145,
"src/backend/tests/unit/graph/graph/test_runnable_vertices_manager.py::test_remove_vertex_from_runnables": 2.2803797090018634,
"src/backend/tests/unit/graph/graph/test_runnable_vertices_manager.py::test_to_dict": 9.405242958950112,
"src/backend/tests/unit/graph/graph/test_runnable_vertices_manager.py::test_update_run_state": 2.4422846660017967,
"src/backend/tests/unit/graph/graph/test_runnable_vertices_manager.py::test_update_vertex_run_state": 2.9607737089972943,
"src/backend/tests/unit/graph/graph/test_runnable_vertices_manager.py::test_update_vertex_run_state__bad_case": 2.485696541989455,
"src/backend/tests/unit/graph/graph/test_utils.py::test_get_successors_a": 8.826332042983267,
"src/backend/tests/unit/graph/graph/test_utils.py::test_get_successors_z": 2.949440208991291,
"src/backend/tests/unit/graph/graph/test_utils.py::test_sort_up_to_vertex_a": 2.8428027920017485,
"src/backend/tests/unit/graph/graph/test_utils.py::test_sort_up_to_vertex_g": 3.315444208041299,
"src/backend/tests/unit/graph/graph/test_utils.py::test_sort_up_to_vertex_h": 2.983557416999247,
"src/backend/tests/unit/graph/graph/test_utils.py::test_sort_up_to_vertex_invalid_vertex": 1.9296646670263726,
"src/backend/tests/unit/graph/graph/test_utils.py::test_sort_up_to_vertex_m": 8.736605707992567,
"src/backend/tests/unit/graph/graph/test_utils.py::test_sort_up_to_vertex_n_is_start": 2.5265350410190877,
"src/backend/tests/unit/graph/graph/test_utils.py::test_sort_up_to_vertex_t": 2.3543146679585334,
"src/backend/tests/unit/graph/graph/test_utils.py::test_sort_up_to_vertex_x": 2.094447916984791,
"src/backend/tests/unit/graph/graph/test_utils.py::test_sort_up_to_vertex_z": 2.7899541249789763,
"src/backend/tests/unit/graph/test_graph.py::test_build_edges": 2.053889958973741,
"src/backend/tests/unit/graph/test_graph.py::test_build_nodes": 3.264545250014635,
"src/backend/tests/unit/graph/test_graph.py::test_build_params": 1.6351483759935945,
"src/backend/tests/unit/graph/test_graph.py::test_circular_dependencies": 4.829830207978375,
"src/backend/tests/unit/graph/test_graph.py::test_find_last_node": 1.8075883749988861,
"src/backend/tests/unit/graph/test_graph.py::test_get_node": 2.2939607900043484,
"src/backend/tests/unit/graph/test_graph.py::test_get_node_neighbors_basic": 2.5666640420095064,
"src/backend/tests/unit/graph/test_graph.py::test_get_root_vertex": 2.33814408298349,
"src/backend/tests/unit/graph/test_graph.py::test_get_vertices_with_target": 2.0869384160032496,
"src/backend/tests/unit/graph/test_graph.py::test_graph_structure": 3.1825925829762127,
"src/backend/tests/unit/graph/test_graph.py::test_invalid_node_types": 2.1994956269918475,
"src/backend/tests/unit/graph/test_graph.py::test_matched_type": 2.3932184999866877,
"src/backend/tests/unit/graph/test_graph.py::test_pickle_graph": 2.184392209019279,
"src/backend/tests/unit/graph/test_graph.py::test_process_flow": 2.1272420000168495,
"src/backend/tests/unit/graph/test_graph.py::test_process_flow_one_group": 1.9646992909838445,
"src/backend/tests/unit/graph/test_graph.py::test_process_flow_vector_store_grouped": 2.415951082977699,
"src/backend/tests/unit/graph/test_graph.py::test_set_new_target_handle": 1.7951639160164632,
"src/backend/tests/unit/graph/test_graph.py::test_ungroup_node": 2.0279515830043238,
"src/backend/tests/unit/graph/test_graph.py::test_update_source_handle": 2.0684266670432407,
"src/backend/tests/unit/graph/test_graph.py::test_update_target_handle_proxy": 1.5892521249479614,
"src/backend/tests/unit/graph/test_graph.py::test_update_template": 1.7316221670189407,
"src/backend/tests/unit/graph/test_graph.py::test_validate_edges": 1.920275832992047,
"src/backend/tests/unit/initial_setup/starter_projects/test_memory_chatbot.py::test_memory_chatbot": 2.1822710419946816,
"src/backend/tests/unit/initial_setup/starter_projects/test_memory_chatbot.py::test_memory_chatbot_dump_components_and_edges": 1.6609269159962423,
"src/backend/tests/unit/initial_setup/starter_projects/test_memory_chatbot.py::test_memory_chatbot_dump_structure": 2.5239112499984913,
"src/backend/tests/unit/initial_setup/starter_projects/test_vector_store_rag.py::test_vector_store_rag": 0.08740387603756972,
"src/backend/tests/unit/initial_setup/starter_projects/test_vector_store_rag.py::test_vector_store_rag_dump": 0.027446749998489395,
"src/backend/tests/unit/initial_setup/starter_projects/test_vector_store_rag.py::test_vector_store_rag_dump_components_and_edges": 0.026998917979653925,
"src/backend/tests/unit/inputs/test_inputs.py::test_bool_input_invalid": 0.0006447900377679616,
"src/backend/tests/unit/inputs/test_inputs.py::test_bool_input_valid": 0.00023025000700727105,
"src/backend/tests/unit/inputs/test_inputs.py::test_data_input_valid": 0.0005409169825725257,
"src/backend/tests/unit/inputs/test_inputs.py::test_dict_input_invalid": 0.00025333400117233396,
"src/backend/tests/unit/inputs/test_inputs.py::test_dict_input_valid": 0.0007362900068983436,
"src/backend/tests/unit/inputs/test_inputs.py::test_dropdown_input_invalid": 0.0009356669906992465,
"src/backend/tests/unit/inputs/test_inputs.py::test_dropdown_input_valid": 0.0004829160461667925,
"src/backend/tests/unit/inputs/test_inputs.py::test_file_input_valid": 0.00024799996754154563,
"src/backend/tests/unit/inputs/test_inputs.py::test_float_input_invalid": 0.00025445802020840347,
"src/backend/tests/unit/inputs/test_inputs.py::test_float_input_valid": 0.00024087497149594128,
"src/backend/tests/unit/inputs/test_inputs.py::test_handle_input_invalid": 0.0003797499812208116,
"src/backend/tests/unit/inputs/test_inputs.py::test_handle_input_valid": 0.0002122489968314767,
"src/backend/tests/unit/inputs/test_inputs.py::test_instantiate_input_comprehensive": 0.001500750018749386,
"src/backend/tests/unit/inputs/test_inputs.py::test_instantiate_input_invalid": 0.00027112496900372207,
"src/backend/tests/unit/inputs/test_inputs.py::test_instantiate_input_valid": 0.0005940830160398036,
"src/backend/tests/unit/inputs/test_inputs.py::test_int_input_invalid": 0.00029999902471899986,
"src/backend/tests/unit/inputs/test_inputs.py::test_int_input_valid": 0.0004179589741397649,
"src/backend/tests/unit/inputs/test_inputs.py::test_message_text_input_invalid": 0.0004274170205462724,
"src/backend/tests/unit/inputs/test_inputs.py::test_message_text_input_valid": 0.0003730839816853404,
"src/backend/tests/unit/inputs/test_inputs.py::test_multiline_input_invalid": 0.00033208398963324726,
"src/backend/tests/unit/inputs/test_inputs.py::test_multiline_input_valid": 0.0003288750012870878,
"src/backend/tests/unit/inputs/test_inputs.py::test_multiline_secret_input_invalid": 0.00023066697758622468,
"src/backend/tests/unit/inputs/test_inputs.py::test_multiline_secret_input_valid": 0.00022683301358483732,
"src/backend/tests/unit/inputs/test_inputs.py::test_multiselect_input_invalid": 0.0002583339810371399,
"src/backend/tests/unit/inputs/test_inputs.py::test_multiselect_input_valid": 0.00027691599098034203,
"src/backend/tests/unit/inputs/test_inputs.py::test_nested_dict_input_invalid": 0.00041799998143687844,
"src/backend/tests/unit/inputs/test_inputs.py::test_nested_dict_input_valid": 0.004873417055932805,
"src/backend/tests/unit/inputs/test_inputs.py::test_prompt_input_valid": 0.0007432089769281447,
"src/backend/tests/unit/inputs/test_inputs.py::test_secret_str_input_invalid": 0.00033837597584351897,
"src/backend/tests/unit/inputs/test_inputs.py::test_secret_str_input_valid": 0.00022083398653194308,
"src/backend/tests/unit/inputs/test_inputs.py::test_str_input_invalid": 0.0004425400111358613,
"src/backend/tests/unit/inputs/test_inputs.py::test_str_input_valid": 0.0003231659939046949,
"src/backend/tests/unit/inputs/test_inputs.py::test_table_input_invalid": 0.010438332974445075,
"src/backend/tests/unit/inputs/test_inputs.py::test_table_input_valid": 0.0003158329927828163,
"src/backend/tests/unit/schema/test_schema_message.py::test_message_async_prompt_serialization": 0.4811764149926603,
"src/backend/tests/unit/schema/test_schema_message.py::test_message_prompt_serialization": 0.0009208739793393761,
"src/backend/tests/unit/test_api_key.py::test_create_api_key": 2.661131207976723,
"src/backend/tests/unit/test_api_key.py::test_delete_api_key": 2.671240749012213,
"src/backend/tests/unit/test_api_key.py::test_get_api_keys": 2.5765499170229305,
"src/backend/tests/unit/test_cache.py::test_build_graph": 3.0347800820018165,
"src/backend/tests/unit/test_chat_endpoint.py::test_build_flow": 8.144330751005327,
"src/backend/tests/unit/test_chat_endpoint.py::test_build_flow_from_request_data": 5.783452290983405,
"src/backend/tests/unit/test_chat_endpoint.py::test_build_flow_with_frozen_path": 7.61259591698763,
"src/backend/tests/unit/test_cli.py::test_components_path": 2.2392006250447594,
"src/backend/tests/unit/test_cli.py::test_superuser": 2.476592207007343,
"src/backend/tests/unit/test_custom_component.py::test_build_config_field_keys": 2.0540905419911724,
"src/backend/tests/unit/test_custom_component.py::test_build_config_field_value_keys": 1.9443145420227665,
"src/backend/tests/unit/test_custom_component.py::test_build_config_field_values_dict": 3.319497832970228,
"src/backend/tests/unit/test_custom_component.py::test_build_config_fields_dict": 1.8987905430258252,
"src/backend/tests/unit/test_custom_component.py::test_build_config_has_fields": 2.7343585419876035,
"src/backend/tests/unit/test_custom_component.py::test_build_config_no_code": 1.792709333007224,
"src/backend/tests/unit/test_custom_component.py::test_build_config_return_type": 2.3822355829761364,
"src/backend/tests/unit/test_custom_component.py::test_code_parser_get_tree": 1.952277623990085,
"src/backend/tests/unit/test_custom_component.py::test_code_parser_init": 2.0454807080095634,
"src/backend/tests/unit/test_custom_component.py::test_code_parser_parse_ann_assign": 2.451507583988132,
"src/backend/tests/unit/test_custom_component.py::test_code_parser_parse_arg_no_annotation": 2.353039957990404,
"src/backend/tests/unit/test_custom_component.py::test_code_parser_parse_arg_with_annotation": 1.7640878760430496,
"src/backend/tests/unit/test_custom_component.py::test_code_parser_parse_assign": 2.0938420000020415,
"src/backend/tests/unit/test_custom_component.py::test_code_parser_parse_callable_details_no_args": 1.7373172499937937,
"src/backend/tests/unit/test_custom_component.py::test_code_parser_parse_classes": 1.7848410429724026,
"src/backend/tests/unit/test_custom_component.py::test_code_parser_parse_function_def_init": 2.2325071259983815,
"src/backend/tests/unit/test_custom_component.py::test_code_parser_parse_function_def_not_init": 2.4196665409835987,
"src/backend/tests/unit/test_custom_component.py::test_code_parser_parse_functions": 2.178845665999688,
"src/backend/tests/unit/test_custom_component.py::test_code_parser_parse_global_vars": 2.4417894990183413,
"src/backend/tests/unit/test_custom_component.py::test_code_parser_parse_imports_import": 1.707990164984949,
"src/backend/tests/unit/test_custom_component.py::test_code_parser_parse_imports_importfrom": 2.1835803739959374,
"src/backend/tests/unit/test_custom_component.py::test_code_parser_syntax_error": 2.277529457001947,
"src/backend/tests/unit/test_custom_component.py::test_component_code_null_error": 1.9170069170068018,
"src/backend/tests/unit/test_custom_component.py::test_component_get_code_tree": 1.8569252080051228,
"src/backend/tests/unit/test_custom_component.py::test_component_get_code_tree_syntax_error": 2.174606874003075,
"src/backend/tests/unit/test_custom_component.py::test_component_get_function_valid": 1.9588903749827296,
"src/backend/tests/unit/test_custom_component.py::test_component_init": 2.5707569160149433,
"src/backend/tests/unit/test_custom_component.py::test_custom_component_build_not_implemented": 2.3832541239680722,
"src/backend/tests/unit/test_custom_component.py::test_custom_component_build_template_config": 1.767908540990902,
"src/backend/tests/unit/test_custom_component.py::test_custom_component_class_template_validation_no_code": 1.9784962920530234,
"src/backend/tests/unit/test_custom_component.py::test_custom_component_get_code_tree_syntax_error": 2.0324612080003135,
"src/backend/tests/unit/test_custom_component.py::test_custom_component_get_function": 1.6494328340049833,
"src/backend/tests/unit/test_custom_component.py::test_custom_component_get_function_entrypoint_args": 1.9188839579874184,
"src/backend/tests/unit/test_custom_component.py::test_custom_component_get_function_entrypoint_args_no_args": 2.0736736260005273,
"src/backend/tests/unit/test_custom_component.py::test_custom_component_get_function_entrypoint_return_type": 2.199567042000126,
"src/backend/tests/unit/test_custom_component.py::test_custom_component_get_function_entrypoint_return_type_no_return_type": 1.7597685409709811,
"src/backend/tests/unit/test_custom_component.py::test_custom_component_get_function_valid": 2.1681128749914933,
"src/backend/tests/unit/test_custom_component.py::test_custom_component_get_main_class_name": 1.658911792008439,
"src/backend/tests/unit/test_custom_component.py::test_custom_component_get_main_class_name_no_main_class": 1.4603167499881238,
"src/backend/tests/unit/test_custom_component.py::test_custom_component_init": 2.329375083994819,
"src/backend/tests/unit/test_custom_component.py::test_custom_component_multiple_outputs": 1.8187729580095038,
"src/backend/tests/unit/test_custom_component.py::test_list_flows_flow_objects": 1.981454541994026,
"src/backend/tests/unit/test_custom_component.py::test_list_flows_return_type": 1.8733046670095064,
"src/backend/tests/unit/test_data_class.py::test_add_method_for_integers": 1.7596916669863276,
"src/backend/tests/unit/test_data_class.py::test_add_method_for_strings": 2.099744750012178,
"src/backend/tests/unit/test_data_class.py::test_add_method_with_non_overlapping_keys": 2.0075557490054052,
"src/backend/tests/unit/test_data_class.py::test_conversion_from_document": 1.9583345409773756,
"src/backend/tests/unit/test_data_class.py::test_conversion_to_document": 1.953191417036578,
"src/backend/tests/unit/test_data_class.py::test_custom_attribute_get_set_del": 2.8074895000027027,
"src/backend/tests/unit/test_data_class.py::test_custom_attribute_setting_and_getting": 1.744376168033341,
"src/backend/tests/unit/test_data_class.py::test_data_initialization": 2.3415857510408387,
"src/backend/tests/unit/test_data_class.py::test_deep_copy": 1.5598407920042519,
"src/backend/tests/unit/test_data_class.py::test_dir_includes_data_keys": 2.4137807070219424,
"src/backend/tests/unit/test_data_class.py::test_dir_reflects_attribute_deletion": 1.8897194170276634,
"src/backend/tests/unit/test_data_class.py::test_get_text_with_empty_data": 2.669506582984468,
"src/backend/tests/unit/test_data_class.py::test_get_text_with_none_data": 1.8896955420204904,
"src/backend/tests/unit/test_data_class.py::test_get_text_with_text_key": 1.953111374983564,
"src/backend/tests/unit/test_data_class.py::test_get_text_without_text_key": 1.9460047910106368,
"src/backend/tests/unit/test_data_class.py::test_str_and_dir_methods": 2.6938894579943735,
"src/backend/tests/unit/test_data_class.py::test_validate_data_with_extra_keys": 1.9336464170191903,
"src/backend/tests/unit/test_data_components.py::test_build_with_multiple_urls": 0.026251333008985966,
"src/backend/tests/unit/test_data_components.py::test_directory_component_build_with_multithreading": 0.0020231239905115217,
"src/backend/tests/unit/test_data_components.py::test_directory_without_mocks": 0.38643029099330306,
"src/backend/tests/unit/test_data_components.py::test_failed_request": 0.011844915978144854,
"src/backend/tests/unit/test_data_components.py::test_parse_curl": 0.0004114170151297003,
"src/backend/tests/unit/test_data_components.py::test_successful_get_request": 0.015994457993656397,
"src/backend/tests/unit/test_data_components.py::test_timeout": 0.01364304099115543,
"src/backend/tests/unit/test_data_components.py::test_url_component": 0.5631265829724725,
"src/backend/tests/unit/test_database.py::test_create_flow": 2.5350006250082515,
"src/backend/tests/unit/test_database.py::test_create_flow_with_invalid_data": 2.6853410840267316,
"src/backend/tests/unit/test_database.py::test_create_flows": 3.4552309999999125,
"src/backend/tests/unit/test_database.py::test_delete_flow": 4.201302792993374,
"src/backend/tests/unit/test_database.py::test_delete_flows": 2.73355954195722,
"src/backend/tests/unit/test_database.py::test_delete_flows_with_transaction_and_build": 3.3879740410193335,
"src/backend/tests/unit/test_database.py::test_delete_nonexistent_flow": 2.906195083982311,
"src/backend/tests/unit/test_database.py::test_download_file": 2.6433084169693757,
"src/backend/tests/unit/test_database.py::test_get_nonexistent_flow": 2.9141747919784393,
"src/backend/tests/unit/test_database.py::test_load_flows": 2.3472657920210622,
"src/backend/tests/unit/test_database.py::test_migrate_transactions": 2.4188965820358135,
"src/backend/tests/unit/test_database.py::test_migrate_transactions_no_duckdb": 2.4176759159890935,
"src/backend/tests/unit/test_database.py::test_read_flow": 2.524181623972254,
"src/backend/tests/unit/test_database.py::test_read_flows": 3.3437811249750666,
"src/backend/tests/unit/test_database.py::test_read_only_starter_projects": 2.8177391680073924,
"src/backend/tests/unit/test_database.py::test_sqlite_pragmas": 2.2383368749869987,
"src/backend/tests/unit/test_database.py::test_update_flow": 3.1579460009816103,
"src/backend/tests/unit/test_database.py::test_update_flow_idempotency": 2.9125417500035837,
"src/backend/tests/unit/test_database.py::test_update_nonexistent_flow": 2.838372750993585,
"src/backend/tests/unit/test_database.py::test_upload_file": 2.6103912079997826,
"src/backend/tests/unit/test_experimental_components.py::test_python_function_component": 2.076999415992759,
"src/backend/tests/unit/test_files.py::test_delete_file": 2.799217874009628,
"src/backend/tests/unit/test_files.py::test_download_file": 2.51829199999338,
"src/backend/tests/unit/test_files.py::test_file_operations": 3.3802113739948254,
"src/backend/tests/unit/test_files.py::test_list_files": 2.7689662509947084,
"src/backend/tests/unit/test_files.py::test_upload_file": 3.3243832079751883,
"src/backend/tests/unit/test_frontend_nodes.py::test_frontend_node_to_dict": 2.1432127919979393,
"src/backend/tests/unit/test_frontend_nodes.py::test_template_field_defaults": 2.2584460410289466,
"src/backend/tests/unit/test_frontend_nodes.py::test_template_to_dict": 2.8126436240272596,
"src/backend/tests/unit/test_helper_components.py::test_data_as_text_component": 2.214979208976729,
"src/backend/tests/unit/test_helper_components.py::test_uuid_generator_component": 3.2323666680022143,
"src/backend/tests/unit/test_initial_setup.py::test_create_or_update_starter_projects": 2.2390285000146832,
"src/backend/tests/unit/test_initial_setup.py::test_get_project_data": 2.6407637080119457,
"src/backend/tests/unit/test_initial_setup.py::test_load_starter_projects": 2.2846807509777136,
"src/backend/tests/unit/test_initial_setup.py::test_refresh_starter_projects": 5.397776041994803,
"src/backend/tests/unit/test_kubernetes_secrets.py::test_create_secret": 2.1846052090113517,
"src/backend/tests/unit/test_kubernetes_secrets.py::test_delete_secret": 2.4931142500427086,
"src/backend/tests/unit/test_kubernetes_secrets.py::test_email_address": 3.9143964989925735,
"src/backend/tests/unit/test_kubernetes_secrets.py::test_encode_string": 2.658771957969293,
"src/backend/tests/unit/test_kubernetes_secrets.py::test_encode_uuid": 3.536810541001614,
"src/backend/tests/unit/test_kubernetes_secrets.py::test_ends_with_non_alphanumeric": 2.6400313759804703,
"src/backend/tests/unit/test_kubernetes_secrets.py::test_get_secret": 2.2426519600267056,
"src/backend/tests/unit/test_kubernetes_secrets.py::test_long_string": 2.5769125409715343,
"src/backend/tests/unit/test_kubernetes_secrets.py::test_starts_with_non_alphanumeric": 2.9591099170211237,
"src/backend/tests/unit/test_kubernetes_secrets.py::test_uuid_case_insensitivity": 2.8027220430085436,
"src/backend/tests/unit/test_loading.py::test_load_flow_from_json": 2.0592095839965623,
"src/backend/tests/unit/test_loading.py::test_load_flow_from_json_object": 0.05121591599890962,
"src/backend/tests/unit/test_loading.py::test_load_flow_from_json_with_tweaks": 2.3594004999904428,
"src/backend/tests/unit/test_logger.py::test_enabled": 2.366979167010868,
"src/backend/tests/unit/test_logger.py::test_get_after_timestamp": 2.843343543005176,
"src/backend/tests/unit/test_logger.py::test_get_before_timestamp": 2.0495081240078434,
"src/backend/tests/unit/test_logger.py::test_get_last_n": 3.3493437920114957,
"src/backend/tests/unit/test_logger.py::test_init_default": 4.3632337910239585,
"src/backend/tests/unit/test_logger.py::test_init_with_env_variable": 2.747672124998644,
"src/backend/tests/unit/test_logger.py::test_len": 3.0128796670178417,
"src/backend/tests/unit/test_logger.py::test_max_size": 2.5830446239560843,
"src/backend/tests/unit/test_logger.py::test_write": 3.656159915990429,
"src/backend/tests/unit/test_logger.py::test_write_overflow": 4.817402709042653,
"src/backend/tests/unit/test_login.py::test_login_successful": 3.081307165994076,
"src/backend/tests/unit/test_login.py::test_login_unsuccessful_wrong_password": 3.23898391702096,
"src/backend/tests/unit/test_login.py::test_login_unsuccessful_wrong_username": 2.315841832984006,
"src/backend/tests/unit/test_messages.py::test_add_messages": 2.261571748997085,
"src/backend/tests/unit/test_messages.py::test_add_messagetables": 2.3893967490294017,
"src/backend/tests/unit/test_messages.py::test_convert_to_langchain[convert_to_langchain_type]": 3.2416470000171103,
"src/backend/tests/unit/test_messages.py::test_convert_to_langchain[message]": 2.004590749013005,
"src/backend/tests/unit/test_messages.py::test_delete_messages": 2.3871561660198495,
"src/backend/tests/unit/test_messages.py::test_get_messages": 2.0157879999896977,
"src/backend/tests/unit/test_messages.py::test_store_message": 1.9027003319642972,
"src/backend/tests/unit/test_process.py::test_load_langchain_object_with_cached_session": 2.2040907500195317,
"src/backend/tests/unit/test_process.py::test_load_langchain_object_with_no_cached_session": 2.883888084004866,
"src/backend/tests/unit/test_process.py::test_load_langchain_object_without_session_id": 2.329415375017561,
"src/backend/tests/unit/test_process.py::test_multiple_tweaks": 2.047054874972673,
"src/backend/tests/unit/test_process.py::test_no_tweaks": 1.952550498972414,
"src/backend/tests/unit/test_process.py::test_single_tweak": 2.148759791016346,
"src/backend/tests/unit/test_process.py::test_tweak_no_node_id": 3.1278445839998312,
"src/backend/tests/unit/test_process.py::test_tweak_not_in_template": 2.113566374988295,
"src/backend/tests/unit/test_setup_superuser.py::test_teardown_superuser_default_superuser": 2.256663625012152,
"src/backend/tests/unit/test_setup_superuser.py::test_teardown_superuser_no_default_superuser": 2.211974083009409,
"src/backend/tests/unit/test_telemetry.py::test_gauge": 2.4124685839633457,
"src/backend/tests/unit/test_telemetry.py::test_gauge_with_counter_method": 2.186464792001061,
"src/backend/tests/unit/test_telemetry.py::test_gauge_with_historgram_method": 2.6113254159863573,
"src/backend/tests/unit/test_telemetry.py::test_gauge_with_up_down_counter_method": 2.225708084035432,
"src/backend/tests/unit/test_telemetry.py::test_increment_counter": 2.130592000001343,
"src/backend/tests/unit/test_telemetry.py::test_increment_counter_empty_label": 2.2976541249954607,
"src/backend/tests/unit/test_telemetry.py::test_increment_counter_missing_mandatory_label": 2.443581625993829,
"src/backend/tests/unit/test_telemetry.py::test_increment_counter_unregisted_metric": 2.630037208989961,
"src/backend/tests/unit/test_telemetry.py::test_init": 2.1476573330292013,
"src/backend/tests/unit/test_telemetry.py::test_missing_labels": 2.3570764580217656,
"src/backend/tests/unit/test_telemetry.py::test_multithreaded_singleton": 2.390594750002492,
"src/backend/tests/unit/test_telemetry.py::test_multithreaded_singleton_race_condition": 2.437567832006607,
"src/backend/tests/unit/test_telemetry.py::test_opentelementry_singleton": 3.0913529580284376,
"src/backend/tests/unit/test_template.py::test_build_template_from_function": 2.9347434579976834,
"src/backend/tests/unit/test_template.py::test_get_base_classes": 2.118878333014436,
"src/backend/tests/unit/test_template.py::test_get_default_factory": 2.143760042003123,
"src/backend/tests/unit/test_validate_code.py::test_create_function": 2.2330028339929413,
"src/backend/tests/unit/test_validate_code.py::test_execute_function_missing_function": 1.7509510009840596,
"src/backend/tests/unit/test_validate_code.py::test_execute_function_missing_module": 1.850503541965736,
"src/backend/tests/unit/test_validate_code.py::test_execute_function_missing_schema": 1.81301379200886,
"src/backend/tests/unit/test_validate_code.py::test_execute_function_success": 2.074162457982311,
"src/backend/tests/unit/test_validate_code.py::test_validate_code": 2.2336132919881493,
"src/backend/tests/unit/test_version.py::test_compute_main": 2.139949625969166,
"src/backend/tests/unit/test_version.py::test_version": 1.6260940420324914,
"src/backend/tests/unit/utils/test_connection_string_parser.py::test_transform_connection_string[protocol::password@host-protocol::password@host]": 0.0012022080190945417,
"src/backend/tests/unit/utils/test_connection_string_parser.py::test_transform_connection_string[protocol:user:pa:ss:word@host-protocol:user:pa:ss:word@host]": 0.000848833005875349,
"src/backend/tests/unit/utils/test_connection_string_parser.py::test_transform_connection_string[protocol:user:pa@ss@word@host-protocol:user:pa%40ss%40word@host]": 0.001619456976186484,
"src/backend/tests/unit/utils/test_connection_string_parser.py::test_transform_connection_string[protocol:user:pass@word@host-protocol:user:pass%40word@host]": 0.000586748996283859,
"src/backend/tests/unit/utils/test_connection_string_parser.py::test_transform_connection_string[protocol:user:password@-protocol:user:password@]": 0.0025060399784706533,
"src/backend/tests/unit/utils/test_connection_string_parser.py::test_transform_connection_string[protocol:user:password@host-protocol:user:password@host]": 0.0007859579636715353,
"src/backend/tests/unit/utils/test_connection_string_parser.py::test_transform_connection_string[protocol:user@host-protocol:user@host]": 0.0004099990183021873,
"src/backend/tests/unit/utils/test_connection_string_parser.py::test_transform_connection_string[user:password@host-user:password@host]": 0.0005190849769860506
}

View file

@ -1,5 +1,7 @@
from collections import deque from collections import deque
import pytest
from langflow.components.helpers.Memory import MemoryComponent from langflow.components.helpers.Memory import MemoryComponent
from langflow.components.inputs.ChatInput import ChatInput from langflow.components.inputs.ChatInput import ChatInput
from langflow.components.models.OpenAIModel import OpenAIModelComponent from langflow.components.models.OpenAIModel import OpenAIModelComponent
@ -7,9 +9,11 @@ from langflow.components.outputs.ChatOutput import ChatOutput
from langflow.components.prompts.Prompt import PromptComponent from langflow.components.prompts.Prompt import PromptComponent
from langflow.graph import Graph from langflow.graph import Graph
from langflow.graph.graph.constants import Finish from langflow.graph.graph.constants import Finish
from langflow.graph.graph.schema import GraphDump
def test_memory_chatbot(): @pytest.fixture
def memory_chatbot_graph():
session_id = "test_session_id" session_id = "test_session_id"
template = """{context} template = """{context}
@ -32,10 +36,87 @@ AI: """
chat_output.set(input_value=openai_component.text_response) chat_output.set(input_value=openai_component.text_response)
graph = Graph(chat_input, chat_output) graph = Graph(chat_input, chat_output)
return graph
def test_memory_chatbot(memory_chatbot_graph):
# Now we run step by step # Now we run step by step
expected_order = deque(["chat_input", "chat_memory", "prompt", "openai", "chat_output"]) expected_order = deque(["chat_input", "chat_memory", "prompt", "openai", "chat_output"])
for step in expected_order: for step in expected_order:
result = graph.step() result = memory_chatbot_graph.step()
if isinstance(result, Finish): if isinstance(result, Finish):
break break
assert step == result.vertex.id assert step == result.vertex.id
def test_memory_chatbot_dump_structure(memory_chatbot_graph: Graph):
# Now we run step by step
graph_dict = memory_chatbot_graph.dump(
name="Memory Chatbot", description="A memory chatbot", endpoint_name="membot"
)
assert isinstance(graph_dict, dict)
# Test structure
assert "data" in graph_dict
assert "is_component" in graph_dict
data_dict = graph_dict["data"]
assert "nodes" in data_dict
assert "edges" in data_dict
assert "description" in graph_dict
assert "endpoint_name" in graph_dict
# Test data
nodes = data_dict["nodes"]
edges = data_dict["edges"]
description = graph_dict["description"]
endpoint_name = graph_dict["endpoint_name"]
assert len(nodes) == 5
assert len(edges) == 4
assert description is not None
assert endpoint_name is not None
def test_memory_chatbot_dump_components_and_edges(memory_chatbot_graph: Graph):
# Check all components and edges were dumped correctly
graph_dict: GraphDump = memory_chatbot_graph.dump(
name="Memory Chatbot", description="A memory chatbot", endpoint_name="membot"
)
data_dict = graph_dict["data"]
nodes = data_dict["nodes"]
edges = data_dict["edges"]
# sort the nodes by id
nodes = sorted(nodes, key=lambda x: x["id"])
# Check each node
assert nodes[0]["data"]["type"] == "ChatInput"
assert nodes[0]["id"] == "chat_input"
assert nodes[1]["data"]["type"] == "MemoryComponent"
assert nodes[1]["id"] == "chat_memory"
assert nodes[2]["data"]["type"] == "ChatOutput"
assert nodes[2]["id"] == "chat_output"
assert nodes[3]["data"]["type"] == "OpenAIModelComponent"
assert nodes[3]["id"] == "openai"
assert nodes[4]["data"]["type"] == "PromptComponent"
assert nodes[4]["id"] == "prompt"
# Check edges
expected_edges = [
("chat_input", "prompt"),
("chat_memory", "prompt"),
("prompt", "openai"),
("openai", "chat_output"),
]
assert len(edges) == len(expected_edges)
for edge in edges:
source = edge["source"]
target = edge["target"]
assert (source, target) in expected_edges, edge

View file

@ -1,5 +1,7 @@
from textwrap import dedent from textwrap import dedent
import pytest
from langflow.components.data.File import FileComponent from langflow.components.data.File import FileComponent
from langflow.components.embeddings.OpenAIEmbeddings import OpenAIEmbeddingsComponent from langflow.components.embeddings.OpenAIEmbeddings import OpenAIEmbeddingsComponent
from langflow.components.helpers.ParseData import ParseDataComponent from langflow.components.helpers.ParseData import ParseDataComponent
@ -14,10 +16,17 @@ from langflow.graph.graph.constants import Finish
from langflow.schema.data import Data from langflow.schema.data import Data
def test_vector_store_rag(): @pytest.fixture
def client():
pass
@pytest.fixture
def ingestion_graph():
# Ingestion Graph # Ingestion Graph
file_component = FileComponent(_id="file-123") file_component = FileComponent(_id="file-123")
file_component.set(path="test.txt") file_component.set(path="test.txt")
file_component.set_output_value("data", Data(text="This is a test file."))
text_splitter = SplitTextComponent(_id="text-splitter-123") text_splitter = SplitTextComponent(_id="text-splitter-123")
text_splitter.set(data_inputs=file_component.load_file) text_splitter.set(data_inputs=file_component.load_file)
openai_embeddings = OpenAIEmbeddingsComponent(_id="openai-embeddings-123") openai_embeddings = OpenAIEmbeddingsComponent(_id="openai-embeddings-123")
@ -31,8 +40,18 @@ def test_vector_store_rag():
api_endpoint="https://astra.example.com", api_endpoint="https://astra.example.com",
token="token", token="token",
) )
vector_store.set_output_value("vector_store", "mock_vector_store")
vector_store.set_output_value("base_retriever", "mock_retriever")
vector_store.set_output_value("search_results", [Data(text="This is a test file.")])
ingestion_graph = Graph(file_component, vector_store)
return ingestion_graph
@pytest.fixture
def rag_graph():
# RAG Graph # RAG Graph
openai_embeddings = OpenAIEmbeddingsComponent(_id="openai-embeddings-124")
chat_input = ChatInput(_id="chatinput-123") chat_input = ChatInput(_id="chatinput-123")
chat_input.get_output("message").value = "What is the meaning of life?" chat_input.get_output("message").value = "What is the meaning of life?"
rag_vector_store = AstraVectorStoreComponent(_id="rag-vector-store-123") rag_vector_store = AstraVectorStoreComponent(_id="rag-vector-store-123")
@ -69,21 +88,160 @@ def test_vector_store_rag():
chat_output.set(input_value=openai_component.text_response) chat_output.set(input_value=openai_component.text_response)
graph = Graph(start=chat_input, end=chat_output) graph = Graph(start=chat_input, end=chat_output)
assert graph is not None return graph
ids = [
def test_vector_store_rag(ingestion_graph, rag_graph):
assert ingestion_graph is not None
ingestion_ids = [
"file-123",
"text-splitter-123",
"openai-embeddings-123",
"vector-store-123",
]
assert rag_graph is not None
rag_ids = [
"chatinput-123", "chatinput-123",
"chatoutput-123", "chatoutput-123",
"openai-123", "openai-123",
"parse-data-123", "parse-data-123",
"prompt-123", "prompt-123",
"rag-vector-store-123", "rag-vector-store-123",
"openai-embeddings-123", "openai-embeddings-124",
] ]
for ids, graph, len_results in zip([ingestion_ids, rag_ids], [ingestion_graph, rag_graph], [5, 8]):
results = [] results = []
for result in graph.start(): for result in graph.start():
results.append(result) results.append(result)
assert len(results) == 8 assert len(results) == len_results
vids = [result.vertex.id for result in results if hasattr(result, "vertex")] vids = [result.vertex.id for result in results if hasattr(result, "vertex")]
assert all(vid in ids for vid in vids), f"Diff: {set(vids) - set(ids)}" assert all(vid in ids for vid in vids), f"Diff: {set(vids) - set(ids)}"
assert results[-1] == Finish() assert results[-1] == Finish()
def test_vector_store_rag_dump_components_and_edges(ingestion_graph, rag_graph):
# Test ingestion graph components and edges
ingestion_graph_dump = ingestion_graph.dump(
name="Ingestion Graph", description="Graph for data ingestion", endpoint_name="ingestion"
)
ingestion_data = ingestion_graph_dump["data"]
ingestion_nodes = ingestion_data["nodes"]
ingestion_edges = ingestion_data["edges"]
# Sort nodes by id to check components
ingestion_nodes = sorted(ingestion_nodes, key=lambda x: x["id"])
# Check components in the ingestion graph
assert ingestion_nodes[0]["data"]["type"] == "FileComponent"
assert ingestion_nodes[0]["id"] == "file-123"
assert ingestion_nodes[1]["data"]["type"] == "OpenAIEmbeddingsComponent"
assert ingestion_nodes[1]["id"] == "openai-embeddings-123"
assert ingestion_nodes[2]["data"]["type"] == "SplitTextComponent"
assert ingestion_nodes[2]["id"] == "text-splitter-123"
assert ingestion_nodes[3]["data"]["type"] == "AstraVectorStoreComponent"
assert ingestion_nodes[3]["id"] == "vector-store-123"
# Check edges in the ingestion graph
expected_ingestion_edges = [
("file-123", "text-splitter-123"),
("text-splitter-123", "vector-store-123"),
("openai-embeddings-123", "vector-store-123"),
]
assert len(ingestion_edges) == len(expected_ingestion_edges)
for edge in ingestion_edges:
source = edge["source"]
target = edge["target"]
assert (source, target) in expected_ingestion_edges, edge
# Test RAG graph components and edges
rag_graph_dump = rag_graph.dump(
name="RAG Graph", description="Graph for Retrieval-Augmented Generation", endpoint_name="rag"
)
rag_data = rag_graph_dump["data"]
rag_nodes = rag_data["nodes"]
rag_edges = rag_data["edges"]
# Sort nodes by id to check components
rag_nodes = sorted(rag_nodes, key=lambda x: x["id"])
# Check components in the RAG graph
assert rag_nodes[0]["data"]["type"] == "ChatInput"
assert rag_nodes[0]["id"] == "chatinput-123"
assert rag_nodes[1]["data"]["type"] == "ChatOutput"
assert rag_nodes[1]["id"] == "chatoutput-123"
assert rag_nodes[2]["data"]["type"] == "OpenAIModelComponent"
assert rag_nodes[2]["id"] == "openai-123"
assert rag_nodes[3]["data"]["type"] == "OpenAIEmbeddingsComponent"
assert rag_nodes[3]["id"] == "openai-embeddings-124"
assert rag_nodes[4]["data"]["type"] == "ParseDataComponent"
assert rag_nodes[4]["id"] == "parse-data-123"
assert rag_nodes[5]["data"]["type"] == "PromptComponent"
assert rag_nodes[5]["id"] == "prompt-123"
assert rag_nodes[6]["data"]["type"] == "AstraVectorStoreComponent"
assert rag_nodes[6]["id"] == "rag-vector-store-123"
# Check edges in the RAG graph
expected_rag_edges = [
("chatinput-123", "rag-vector-store-123"),
("openai-embeddings-124", "rag-vector-store-123"),
("chatinput-123", "prompt-123"),
("rag-vector-store-123", "parse-data-123"),
("parse-data-123", "prompt-123"),
("prompt-123", "openai-123"),
("openai-123", "chatoutput-123"),
]
assert len(rag_edges) == len(expected_rag_edges), rag_edges
for edge in rag_edges:
source = edge["source"]
target = edge["target"]
assert (source, target) in expected_rag_edges, f"Edge {source} -> {target} not found"
def test_vector_store_rag_dump(ingestion_graph, rag_graph):
# Test ingestion graph dump
ingestion_graph_dump = ingestion_graph.dump(
name="Ingestion Graph", description="Graph for data ingestion", endpoint_name="ingestion"
)
assert isinstance(ingestion_graph_dump, dict)
ingestion_data = ingestion_graph_dump["data"]
assert "nodes" in ingestion_data
assert "edges" in ingestion_data
assert "description" in ingestion_graph_dump
assert "endpoint_name" in ingestion_graph_dump
ingestion_nodes = ingestion_data["nodes"]
ingestion_edges = ingestion_data["edges"]
assert len(ingestion_nodes) == 4 # There are 4 components in the ingestion graph
assert len(ingestion_edges) == 3 # There are 3 connections between components
# Test RAG graph dump
rag_graph_dump = rag_graph.dump(
name="RAG Graph", description="Graph for Retrieval-Augmented Generation", endpoint_name="rag"
)
assert isinstance(rag_graph_dump, dict)
rag_data = rag_graph_dump["data"]
assert "nodes" in rag_data
assert "edges" in rag_data
assert "description" in rag_graph_dump
assert "endpoint_name" in rag_graph_dump
rag_nodes = rag_data["nodes"]
rag_edges = rag_data["edges"]
assert len(rag_nodes) == 7 # There are 7 components in the RAG graph
assert len(rag_edges) == 7 # There are 7 connections between components