diff --git a/src/backend/base/langflow/api/v1/login.py b/src/backend/base/langflow/api/v1/login.py index cde6bd28b..2637cc865 100644 --- a/src/backend/base/langflow/api/v1/login.py +++ b/src/backend/base/langflow/api/v1/login.py @@ -71,9 +71,7 @@ async def login_to_get_access_token( @router.get("/auto_login") async def auto_login( - response: Response, - db: Session = Depends(get_session), - settings_service=Depends(get_settings_service) + response: Response, db: Session = Depends(get_session), settings_service=Depends(get_settings_service) ): auth_settings = settings_service.auth_settings if settings_service.auth_settings.AUTO_LOGIN: diff --git a/src/backend/base/langflow/components/inputs/TextInput.py b/src/backend/base/langflow/components/inputs/TextInput.py index 2e3a65ee1..596edf0ec 100644 --- a/src/backend/base/langflow/components/inputs/TextInput.py +++ b/src/backend/base/langflow/components/inputs/TextInput.py @@ -30,4 +30,4 @@ class TextInput(TextComponent): ] def text_response(self) -> Text: - return self.input_value if self.input_value else "" + return self.build(input_value=self.input_value, record_template=self.record_template) diff --git a/src/backend/base/langflow/components/outputs/TextOutput.py b/src/backend/base/langflow/components/outputs/TextOutput.py index 2459192be..bc71b3a27 100644 --- a/src/backend/base/langflow/components/outputs/TextOutput.py +++ b/src/backend/base/langflow/components/outputs/TextOutput.py @@ -30,4 +30,4 @@ class TextOutput(TextComponent): ] def text_response(self) -> Text: - return self.input_value if self.input_value else "" + return self.build(input_value=self.input_value, record_template=self.record_template) diff --git a/src/backend/base/langflow/custom/custom_component/custom_component.py b/src/backend/base/langflow/custom/custom_component/custom_component.py index d51b4d5d8..e398e6850 100644 --- a/src/backend/base/langflow/custom/custom_component/custom_component.py +++ b/src/backend/base/langflow/custom/custom_component/custom_component.py @@ -65,8 +65,6 @@ class CustomComponent(BaseComponent): """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 - """The selected output type of the component. Defaults to None.""" vertex: Optional["Vertex"] = None """The edge target parameter of the component. Defaults to None.""" code_class_base_inheritance: ClassVar[str] = "CustomComponent" diff --git a/src/backend/base/langflow/custom/directory_reader/directory_reader.py b/src/backend/base/langflow/custom/directory_reader/directory_reader.py index dc9dfa51c..679ecdf94 100644 --- a/src/backend/base/langflow/custom/directory_reader/directory_reader.py +++ b/src/backend/base/langflow/custom/directory_reader/directory_reader.py @@ -220,8 +220,6 @@ class DirectoryReader: return False, "Empty file" elif not self.validate_code(file_content): return False, "Syntax error" - elif not self.validate_build(file_content): - return False, "Missing build function" elif self._is_type_hint_used_in_args("Optional", file_content) and not self._is_type_hint_imported( "Optional", file_content ): diff --git a/src/backend/base/langflow/graph/edge/base.py b/src/backend/base/langflow/graph/edge/base.py index ac026449d..f4889bfcc 100644 --- a/src/backend/base/langflow/graph/edge/base.py +++ b/src/backend/base/langflow/graph/edge/base.py @@ -14,8 +14,10 @@ class SourceHandle(BaseModel): baseClasses: Optional[List[str]] = Field(None, 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: str = Field(..., description="Name of the source handle.") - output_types: List[str] = Field(..., description="List of output types for the source handle.") + name: Optional[str] = Field(None, description="Name of the source handle.") + output_types: Optional[List[str]] = Field( + default_factory=list, description="List of output types for the source handle." + ) class TargetHandle(BaseModel): @@ -49,6 +51,12 @@ class Edge: self.validate_edge(source, target) def validate_handles(self, source, target) -> None: + if isinstance(self._source_handle, str) or self.source_handle.baseClasses: + self._legacy_validate_handles(source, target) + else: + self._validate_handles(source, target) + + 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.output_types else: @@ -61,6 +69,19 @@ class Edge: logger.debug(self.target_handle) raise ValueError(f"Edge between {source.vertex_type} and {target.vertex_type} " f"has invalid handles") + def _legacy_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 + else: + self.valid_handles = ( + 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") + def __setstate__(self, state): self.source_id = state["source_id"] self.target_id = state["target_id"] @@ -69,6 +90,14 @@ class Edge: self.target_handle = state.get("target_handle") def validate_edge(self, source, target) -> None: + # If the self.source_handle has baseClasses, then we are using the legacy + # way of defining the source and target handles + if isinstance(self._source_handle, str) or self.source_handle.baseClasses: + self._legacy_validate_edge(source, target) + else: + self._validate_edge(source, target) + + def _validate_edge(self, source, target) -> None: # Validate that the outputs of the source node are valid inputs # for the target node # .outputs is a list of Output objects as dictionaries @@ -97,6 +126,27 @@ class Edge: None, ) no_matched_type = self.matched_type is None + 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. ") + + def _legacy_validate_edge(self, source, target) -> None: + # Validate that the outputs of the source node are valid inputs + # for the target node + self.source_types = source.output + self.target_reqs = target.required_inputs + target.optional_inputs + # 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) + # Get what type of input the target node is expecting + + self.matched_type = next( + (output for output in self.source_types if output in self.target_reqs), + None, + ) + no_matched_type = self.matched_type is None if no_matched_type: logger.debug(self.source_types) logger.debug(self.target_reqs) diff --git a/src/backend/base/langflow/graph/graph/base.py b/src/backend/base/langflow/graph/graph/base.py index f4e71bd7c..7f43a1a66 100644 --- a/src/backend/base/langflow/graph/graph/base.py +++ b/src/backend/base/langflow/graph/graph/base.py @@ -20,7 +20,6 @@ from langflow.schema.schema import INPUT_FIELD_NAME, InputType from langflow.services.cache.utils import CacheMiss from langflow.services.chat.service import ChatService from langflow.services.deps import get_chat_service -from langflow.services.monitor.utils import log_transaction if TYPE_CHECKING: from langflow.graph.schema import ResultData @@ -526,6 +525,7 @@ class Graph: 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: @@ -764,11 +764,9 @@ class Graph: next_runnable_vertices, top_level_vertices = await self.get_next_and_top_level_vertices( lock, set_cache_coro, vertex ) - log_transaction(vertex, status="success") return next_runnable_vertices, top_level_vertices, result_dict, params, valid, artifacts, vertex except Exception as exc: logger.exception(f"Error building vertex: {exc}") - log_transaction(vertex, status="failure", error=str(exc)) raise exc async def get_next_and_top_level_vertices( diff --git a/src/backend/base/langflow/graph/vertex/base.py b/src/backend/base/langflow/graph/vertex/base.py index 8863bd5aa..bde56383f 100644 --- a/src/backend/base/langflow/graph/vertex/base.py +++ b/src/backend/base/langflow/graph/vertex/base.py @@ -212,14 +212,18 @@ class Vertex: def _parse_data(self) -> None: self.data = self._data["data"] - self.outputs = self.data["node"]["outputs"] + if self.data["node"]["template"]["_type"] == "Component": + if "outputs" not in self.data["node"]: + raise ValueError(f"Outputs not found for {self.display_name}") + self.outputs = self.data["node"]["outputs"] + else: + self.outputs = self.data["node"]["outputs"] + self.output = self.data["node"]["base_classes"] self.display_name = self.data["node"].get("display_name", self.id.split("-")[0]) self.description = self.data["node"].get("description", "") self.frozen = self.data["node"].get("frozen", False) - self.selected_output_type = ( - str(self.data.get("selected_output_type")).strip() if self.data.get("selected_output_type") else None - ) + 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)} diff --git a/src/backend/base/langflow/graph/vertex/types.py b/src/backend/base/langflow/graph/vertex/types.py index e1180fef9..5320d5e77 100644 --- a/src/backend/base/langflow/graph/vertex/types.py +++ b/src/backend/base/langflow/graph/vertex/types.py @@ -86,7 +86,8 @@ class ComponentVertex(Vertex): edge = self.get_edge_with_target(requester.id) if edge is None: raise ValueError(f"Edge not found between {self.display_name} and {requester.display_name}") - + if edge.source_handle.name not in self.results: + raise ValueError(f"Result not found for {edge.source_handle.name}. Results: {self.results}") result = self.results[edge.source_handle.name] log_transaction(source=self, target=requester, flow_id=self.graph.flow_id, status="success") diff --git a/src/backend/base/langflow/initial_setup/setup.py b/src/backend/base/langflow/initial_setup/setup.py index 62997fbb1..6194c3973 100644 --- a/src/backend/base/langflow/initial_setup/setup.py +++ b/src/backend/base/langflow/initial_setup/setup.py @@ -1,3 +1,5 @@ +import copy +import json import logging import os from collections import defaultdict @@ -42,39 +44,129 @@ def update_projects_components_with_latest_component_versions(project_data, all_ latest_template = latest_node.get("template") node_data["template"]["code"] = latest_template["code"] - for attr in NODE_FORMAT_ATTRIBUTES: - if attr in latest_node: - # Check if it needs to be updated - if latest_node[attr] != node_data.get(attr): - node_changes_log[node_data["display_name"]].append( - { - "attr": attr, - "old_value": node_data.get(attr), - "new_value": latest_node[attr], - } - ) - node_data[attr] = latest_node[attr] - - for field_name, field_dict in latest_template.items(): - if field_name not in node_data["template"]: - continue - # The idea here is to update some attributes of the field - for attr in FIELD_FORMAT_ATTRIBUTES: - if attr in field_dict and attr in node_data["template"].get(field_name): + if "outputs" in latest_node: + node_data["outputs"] = latest_node["outputs"] + if node_data["template"]["_type"] != latest_template["_type"]: + node_data["template"] = latest_template + else: + for attr in NODE_FORMAT_ATTRIBUTES: + if attr in latest_node: # Check if it needs to be updated - if field_dict[attr] != node_data["template"][field_name][attr]: + if latest_node[attr] != node_data.get(attr): node_changes_log[node_data["display_name"]].append( { - "attr": f"{field_name}.{attr}", - "old_value": node_data["template"][field_name][attr], - "new_value": field_dict[attr], + "attr": attr, + "old_value": node_data.get(attr), + "new_value": latest_node[attr], } ) - node_data["template"][field_name][attr] = field_dict[attr] + node_data[attr] = latest_node[attr] + + for field_name, field_dict in latest_template.items(): + if field_name not in node_data["template"]: + continue + # The idea here is to update some attributes of the field + for attr in FIELD_FORMAT_ATTRIBUTES: + if attr in field_dict and attr in node_data["template"].get(field_name): + # Check if it needs to be updated + if field_dict[attr] != node_data["template"][field_name][attr]: + node_changes_log[node_data["display_name"]].append( + { + "attr": f"{field_name}.{attr}", + "old_value": node_data["template"][field_name][attr], + "new_value": field_dict[attr], + } + ) + node_data["template"][field_name][attr] = field_dict[attr] + project_data_copy = update_new_output(project_data_copy) log_node_changes(node_changes_log) return project_data_copy +def scape_json_parse(json_string: str) -> dict: + parsed_string = json_string.replace("œ", '"') + return json.loads(parsed_string) + + +def update_new_output(data): + nodes = copy.deepcopy(data["nodes"]) + edges = copy.deepcopy(data["edges"]) + + for edge in edges: + if "sourceHandle" in edge and "targetHandle" in edge: + new_source_handle = scape_json_parse(edge["sourceHandle"]) + new_target_handle = scape_json_parse(edge["targetHandle"]) + _id = new_source_handle["id"] + source_node_index = next((index for (index, d) in enumerate(nodes) if d["id"] == _id), -1) + source_node = nodes[source_node_index] if source_node_index != -1 else None + + if "baseClasses" in new_source_handle: + if "output_types" not in new_source_handle: + if source_node and "node" in source_node["data"] and "output_types" in source_node["data"]["node"]: + new_source_handle["output_types"] = source_node["data"]["node"]["output_types"] + else: + new_source_handle["output_types"] = new_source_handle["baseClasses"] + del new_source_handle["baseClasses"] + + if "inputTypes" in new_target_handle and new_target_handle["inputTypes"]: + intersection = [ + type_ for type_ in new_source_handle["output_types"] if type_ in new_target_handle["inputTypes"] + ] + else: + intersection = [ + type_ for type_ in new_source_handle["output_types"] if type_ == new_target_handle["type"] + ] + + selected = intersection[0] if intersection else None + if "name" not in new_source_handle: + new_source_handle["name"] = " | ".join(new_source_handle["output_types"]) + new_source_handle["output_types"] = [selected] if selected else [] + + if source_node and not source_node["data"]["node"].get("outputs"): + if "outputs" not in source_node["data"]["node"]: + source_node["data"]["node"]["outputs"] = [] + types = source_node["data"]["node"].get( + "output_types", source_node["data"]["node"].get("base_classes", []) + ) + if not any(output.get("selected") == selected for output in source_node["data"]["node"]["outputs"]): + source_node["data"]["node"]["outputs"].append( + { + "types": types, + "selected": selected, + "name": " | ".join(types), + } + ) + deduplicated_outputs = [] + for output in source_node["data"]["node"]["outputs"]: + if output["name"] not in [d["name"] for d in deduplicated_outputs]: + deduplicated_outputs.append(output) + source_node["data"]["node"]["outputs"] = deduplicated_outputs + + edge["sourceHandle"] = json.dumps(new_source_handle) + edge["data"]["sourceHandle"] = new_source_handle + edge["data"]["targetHandle"] = new_target_handle + # The above sets the edges but some of the sourceHandles do not have valid name + # which can be found in the nodes. We need to update the sourceHandle with the + # name from node['data']['node']['outputs'] + for node in nodes: + if "outputs" in node["data"]["node"]: + for output in node["data"]["node"]["outputs"]: + for edge in edges: + if node["id"] != edge["source"] or output.get("method") is None: + continue + source_handle = scape_json_parse(edge["sourceHandle"]) + if source_handle["output_types"] == output.get("types") and source_handle["name"] != output["name"]: + source_handle["name"] = output["name"] + + edge["sourceHandle"] = json.dumps(source_handle) + edge["data"]["sourceHandle"] = source_handle + + data_copy = copy.deepcopy(data) + data_copy["nodes"] = nodes + data_copy["edges"] = edges + return data_copy + + def log_node_changes(node_changes_log): # The idea here is to log the changes that were made to the nodes in debug # Something like: diff --git a/src/backend/base/langflow/interface/initialize/loading.py b/src/backend/base/langflow/interface/initialize/loading.py index c5cec3b55..ad59be14e 100644 --- a/src/backend/base/langflow/interface/initialize/loading.py +++ b/src/backend/base/langflow/interface/initialize/loading.py @@ -40,7 +40,6 @@ async def instantiate_class( user_id=user_id, parameters=params_copy, vertex=vertex, - selected_output_type=vertex.selected_output_type, ) params_copy = update_params_with_load_from_db_fields( custom_component, params_copy, vertex.load_from_db_fields, fallback_to_env_vars diff --git a/src/backend/base/langflow/services/auth/utils.py b/src/backend/base/langflow/services/auth/utils.py index 0e0aead88..f62d3ce4f 100644 --- a/src/backend/base/langflow/services/auth/utils.py +++ b/src/backend/base/langflow/services/auth/utils.py @@ -215,10 +215,7 @@ def create_user_longterm_token(db: Session = Depends(get_session)) -> tuple[UUID username = settings_service.auth_settings.SUPERUSER super_user = get_user_by_username(db, username) if not super_user: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Super user hasn't been created" - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Super user hasn't been created") access_token_expires_longterm = timedelta(days=365) access_token = create_token( data={"sub": str(super_user.id)}, diff --git a/src/backend/base/langflow/services/settings/service.py b/src/backend/base/langflow/services/settings/service.py index 95088e829..3ecdb683d 100644 --- a/src/backend/base/langflow/services/settings/service.py +++ b/src/backend/base/langflow/services/settings/service.py @@ -1,5 +1,4 @@ import os -from typing import Optional import yaml from loguru import logger @@ -8,6 +7,7 @@ from langflow.services.base import Service from langflow.services.settings.auth import AuthSettings from langflow.services.settings.base import Settings + class SettingsService(Service): name = "settings_service" diff --git a/src/backend/base/langflow/template/frontend_node/base.py b/src/backend/base/langflow/template/frontend_node/base.py index 310c7149c..be4efc92a 100644 --- a/src/backend/base/langflow/template/frontend_node/base.py +++ b/src/backend/base/langflow/template/frontend_node/base.py @@ -72,8 +72,7 @@ class FrontendNode(BaseModel): def serialize_model(self, handler): result = handler(self) if hasattr(self, "template") and hasattr(self.template, "to_dict"): - format_func = self.format_field if self._format_template else None - result["template"] = self.template.to_dict(format_func) + result["template"] = self.template.to_dict() name = result.pop("name") # Migrate base classes to outputs diff --git a/src/backend/base/langflow/utils/validate.py b/src/backend/base/langflow/utils/validate.py index bd7827199..8c84a4615 100644 --- a/src/backend/base/langflow/utils/validate.py +++ b/src/backend/base/langflow/utils/validate.py @@ -253,9 +253,7 @@ def build_class_constructor(compiled_class, exec_globals, class_name): globals()[module_name] = module instance = exec_globals[class_name](*args, **kwargs) - # Get selected type from global scope - if instance.selected_output_type in exec_globals: - instance.selected_output_type = exec_globals[instance.selected_output_type] + return instance build_custom_class.__globals__.update(exec_globals) diff --git a/tests/conftest.py b/tests/conftest.py index 6d371ac51..6e12c56f2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -227,8 +227,9 @@ def client_fixture(session: Session, monkeypatch, request, load_flows_dir): monkeypatch.setenv("LANGFLOW_DATABASE_URL", f"sqlite:///{db_path}") monkeypatch.setenv("LANGFLOW_AUTO_LOGIN", "false") if "load_flows" in request.keywords: - shutil.copyfile(pytest.BASIC_EXAMPLE_PATH, - os.path.join(load_flows_dir, "c54f9130-f2fa-4a3e-b22a-3856d946351b.json")) + shutil.copyfile( + pytest.BASIC_EXAMPLE_PATH, os.path.join(load_flows_dir, "c54f9130-f2fa-4a3e-b22a-3856d946351b.json") + ) monkeypatch.setenv("LANGFLOW_LOAD_FLOWS_PATH", load_flows_dir) monkeypatch.setenv("LANGFLOW_AUTO_LOGIN", "true") diff --git a/tests/test_database.py b/tests/test_database.py index bc5bc3e7a..fd3291634 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -1,5 +1,3 @@ -import os -from typing import Optional, List from uuid import UUID, uuid4 import orjson @@ -13,7 +11,6 @@ from langflow.services.database.models.base import orjson_dumps from langflow.services.database.models.flow import Flow, FlowCreate, FlowUpdate from langflow.services.database.utils import session_getter from langflow.services.deps import get_db_service -from langflow.services.settings.base import Settings @pytest.fixture(scope="module") @@ -263,5 +260,3 @@ def test_load_flows(client: TestClient, load_flows_dir): response = client.get("api/v1/flows/c54f9130-f2fa-4a3e-b22a-3856d946351b") assert response.status_code == 200 assert response.json()["name"] == "BasicExample" - -