diff --git a/src/backend/langflow/api/v1/endpoints.py b/src/backend/langflow/api/v1/endpoints.py index 14a160c8f..9f51ee5bc 100644 --- a/src/backend/langflow/api/v1/endpoints.py +++ b/src/backend/langflow/api/v1/endpoints.py @@ -15,7 +15,7 @@ from langflow.api.v1.schemas import ( ) from langflow.interface.custom.custom_component import CustomComponent from langflow.interface.custom.directory_reader import DirectoryReader -from langflow.interface.types import build_langchain_template_custom_component, create_and_validate_component +from langflow.interface.types import build_custom_component_template, create_and_validate_component from langflow.processing.process import process_graph_cached, process_tweaks from langflow.services.auth.utils import api_key_security, get_current_active_user from langflow.services.cache.utils import save_uploaded_file @@ -216,7 +216,7 @@ async def custom_component( ): component = create_and_validate_component(raw_code.code) - built_frontend_node = build_langchain_template_custom_component(component, user_id=user.id) + built_frontend_node = build_custom_component_template(component, user_id=user.id) built_frontend_node = update_frontend_node_with_template_values(built_frontend_node, raw_code) return built_frontend_node @@ -224,7 +224,7 @@ async def custom_component( @router.post("/custom_component/reload", status_code=HTTPStatus.OK) async def reload_custom_component(path: str, user: User = Depends(get_current_active_user)): - from langflow.interface.types import build_langchain_template_custom_component + from langflow.interface.types import build_custom_component_template try: reader = DirectoryReader("") @@ -234,7 +234,7 @@ async def reload_custom_component(path: str, user: User = Depends(get_current_ac extractor = CustomComponent(code=content) extractor.validate() - return build_langchain_template_custom_component(extractor, user_id=user.id) + return build_custom_component_template(extractor, user_id=user.id) except Exception as exc: raise HTTPException(status_code=400, detail=str(exc)) @@ -246,6 +246,6 @@ async def custom_component_update( ): component = create_and_validate_component(raw_code.code) - component_node = build_langchain_template_custom_component(component, user_id=user.id, update_field=raw_code.field) + component_node = build_custom_component_template(component, user_id=user.id, update_field=raw_code.field) # Update the field return component_node diff --git a/src/backend/langflow/interface/types.py b/src/backend/langflow/interface/types.py index 3e1c0386a..c508816a0 100644 --- a/src/backend/langflow/interface/types.py +++ b/src/backend/langflow/interface/types.py @@ -8,6 +8,8 @@ from uuid import UUID from cachetools import LRUCache, cached from fastapi import HTTPException +from loguru import logger + from langflow.interface.agents.base import agent_creator from langflow.interface.chains.base import chain_creator from langflow.interface.custom.custom_component import CustomComponent @@ -31,7 +33,6 @@ from langflow.template.field.base import TemplateField from langflow.template.frontend_node.constants import CLASSES_TO_REMOVE from langflow.template.frontend_node.custom_components import CustomComponentFrontendNode from langflow.utils.util import get_base_classes -from loguru import logger # Used to get the base_classes list @@ -335,7 +336,7 @@ def add_output_types(frontend_node, return_types: List[str]): frontend_node.get("output_types").append(return_type) -def build_langchain_template_custom_component( +def build_custom_component_template( custom_component: CustomComponent, user_id: Optional[Union[str, UUID]] = None, update_field: Optional[str] = None, @@ -394,85 +395,118 @@ def build_and_validate_all_files(reader: DirectoryReader, file_list): def build_valid_menu(valid_components): - """Build the valid menu""" + """Build the valid menu.""" valid_menu = {} logger.debug("------------------- VALID COMPONENTS -------------------") for menu_item in valid_components["menu"]: menu_name = menu_item["name"] - valid_menu[menu_name] = {} - # menu_path = menu_item["path"] - for component in menu_item["components"]: - logger.debug(f"Building component: {component.get('name'), component.get('output_types')}") - try: - component_name = component["name"] - component_code = component["code"] - component_output_types = component["output_types"] - - component_extractor = CustomComponent(code=component_code) - component_extractor.validate() - - component_template = build_langchain_template_custom_component(component_extractor) - component_template["output_types"] = component_output_types - # full_path = f"{menu_path}/{component.get('file')}" - # component_template["full_path"] = full_path - if len(component_output_types) == 1: - component_name = component_output_types[0] - else: - file_name = component.get("file").split(".")[0] - if "_" in file_name: - # turn .py file into camelcase - component_name = "".join([word.capitalize() for word in file_name.split("_")]) - else: - component_name = file_name - - valid_menu[menu_name][component_name] = component_template - logger.debug(f"Added {component_name} to valid menu to {menu_name}") - - except Exception as exc: - logger.error(f"Error loading Component: {component['output_types']}") - logger.exception(f"Error while building custom component {component_output_types}: {exc}") - + valid_menu[menu_name] = build_menu_items(menu_item) return valid_menu +def build_menu_items(menu_item): + """Build menu items for a given menu.""" + menu_items = {} + for component in menu_item["components"]: + try: + component_name, component_template = build_component(component) + menu_items[component_name] = component_template + logger.debug(f"Added {component_name} to valid menu.") + except Exception as exc: + logger.error(f"Error loading Component: {component['output_types']}") + logger.exception(f"Error while building custom component {component['output_types']}: {exc}") + return menu_items + + +def build_component(component): + """Build a single component.""" + logger.debug(f"Building component: {component.get('name'), component.get('output_types')}") + component_name = determine_component_name(component) + component_template = create_component_template(component) + return component_name, component_template + + +def determine_component_name(component): + """Determine the name of the component.""" + component_output_types = component["output_types"] + if len(component_output_types) == 1: + return component_output_types[0] + else: + file_name = component.get("file").split(".")[0] + return "".join(word.capitalize() for word in file_name.split("_")) if "_" in file_name else file_name + + +def create_component_template(component): + """Create a template for a component.""" + component_code = component["code"] + component_output_types = component["output_types"] + + component_extractor = CustomComponent(code=component_code) + component_extractor.validate() + + component_template = build_custom_component_template(component_extractor) + component_template["output_types"] = component_output_types + return component_template + + def build_invalid_menu(invalid_components): - """Build the invalid menu""" - if invalid_components.get("menu"): - logger.debug("------------------- INVALID COMPONENTS -------------------") + """Build the invalid menu.""" + if not invalid_components.get("menu"): + return {} + + logger.debug("------------------- INVALID COMPONENTS -------------------") invalid_menu = {} for menu_item in invalid_components["menu"]: menu_name = menu_item["name"] - invalid_menu[menu_name] = {} - - for component in menu_item["components"]: - try: - component_name = component["name"] - component_code = component["code"] - - component_template = ( - CustomComponentFrontendNode( - description="ERROR - Check your Python Code", - display_name=f"ERROR - {component_name}", - ) - .to_dict() - .get(type(CustomComponent()).__name__) - ) - - component_template["error"] = component.get("error", None) - logger.debug(component) - logger.debug(f"Component Path: {component.get('path', None)}") - logger.debug(f"Component Error: {component.get('error', None)}") - component_template.get("template").get("code")["value"] = component_code - - invalid_menu[menu_name][component_name] = component_template - logger.debug(f"Added {component_name} to invalid menu to {menu_name}") - - except Exception as exc: - logger.exception(f"Error while creating custom component [{component_name}]: {str(exc)}") - + invalid_menu[menu_name] = build_invalid_menu_items(menu_item) return invalid_menu +def build_invalid_menu_items(menu_item): + """Build invalid menu items for a given menu.""" + menu_items = {} + for component in menu_item["components"]: + try: + component_name, component_template = build_invalid_component(component) + menu_items[component_name] = component_template + logger.debug(f"Added {component_name} to invalid menu.") + except Exception as exc: + logger.exception(f"Error while creating custom component [{component_name}]: {str(exc)}") + return menu_items + + +def build_invalid_component(component): + """Build a single invalid component.""" + component_name = component["name"] + component_template = create_invalid_component_template(component, component_name) + log_invalid_component_details(component) + return component_name, component_template + + +def create_invalid_component_template(component, component_name): + """Create a template for an invalid component.""" + component_code = component["code"] + component_template = ( + CustomComponentFrontendNode( + description="ERROR - Check your Python Code", + display_name=f"ERROR - {component_name}", + ) + .to_dict() + .get(type(CustomComponent()).__name__) + ) + + component_template["error"] = component.get("error", None) + component_template.get("template").get("code")["value"] = component_code + return component_template + + +def log_invalid_component_details(component): + """Log details of an invalid component.""" + logger.debug(component) + logger.debug(f"Component Path: {component.get('path', None)}") + logger.debug(f"Component Error: {component.get('error', None)}") + + def get_new_key(dictionary, original_key): counter = 1 new_key = original_key + " (" + str(counter) + ")" @@ -496,7 +530,7 @@ def merge_nested_dicts_with_renaming(dict1, dict2): return dict1 -def build_langchain_custom_component_list_from_path(path: str): +def build_custom_component_list_from_path(path: str): """Build a list of custom components for the langchain from a given path""" file_list = load_files_from_path(path) reader = DirectoryReader(path, False) @@ -510,34 +544,35 @@ def build_langchain_custom_component_list_from_path(path: str): def get_all_types_dict(settings_service): + """Get all types dictionary combining native and custom components.""" native_components = build_langchain_types_dict() - # custom_components is a list of dicts - # need to merge all the keys into one dict - custom_components_from_file: dict[str, Any] = {} - if settings_service.settings.COMPONENTS_PATH: - logger.info(f"Building custom components from {settings_service.settings.COMPONENTS_PATH}") + custom_components_from_file = build_custom_components(settings_service) + return merge_nested_dicts_with_renaming(native_components, custom_components_from_file) - custom_component_dicts = [] - processed_paths = [] - for path in settings_service.settings.COMPONENTS_PATH: - if str(path) in processed_paths: - continue - custom_component_dict = build_langchain_custom_component_list_from_path(str(path)) - custom_component_dicts.append(custom_component_dict) - processed_paths.append(str(path)) - logger.info(f"Loading {len(custom_component_dicts)} category(ies)") - for custom_component_dict in custom_component_dicts: - # custom_component_dict is a dict of dicts - if not custom_component_dict: - continue - category = list(custom_component_dict.keys())[0] +def build_custom_components(settings_service): + """Build custom components from the specified paths.""" + if not settings_service.settings.COMPONENTS_PATH: + return {} + + logger.info(f"Building custom components from {settings_service.settings.COMPONENTS_PATH}") + custom_components_from_file = {} + processed_paths = set() + for path in settings_service.settings.COMPONENTS_PATH: + path_str = str(path) + if path_str in processed_paths: + continue + + custom_component_dict = build_custom_component_list_from_path(path_str) + if custom_component_dict: + category = next(iter(custom_component_dict)) logger.info(f"Loading {len(custom_component_dict[category])} component(s) from category {category}") custom_components_from_file = merge_nested_dicts_with_renaming( custom_components_from_file, custom_component_dict ) + processed_paths.add(path_str) - return merge_nested_dicts_with_renaming(native_components, custom_components_from_file) + return custom_components_from_file def merge_nested_dicts(dict1, dict2): diff --git a/src/backend/langflow/services/store/service.py b/src/backend/langflow/services/store/service.py index 94a89f974..52ee1c861 100644 --- a/src/backend/langflow/services/store/service.py +++ b/src/backend/langflow/services/store/service.py @@ -498,11 +498,13 @@ class StoreService(Service): comp_count = metadata.get("filter_count", 0) except HTTPStatusError as exc: if exc.response.status_code == 403: - raise ForbiddenError("You are not authorized to access this public resource") + raise ForbiddenError("You are not authorized to access this public resource") from exc elif exc.response.status_code == 401: - raise APIKeyError("You are not authorized to access this resource. Please check your API key.") + raise APIKeyError( + "You are not authorized to access this resource. Please check your API key." + ) from exc except Exception as exc: - raise ValueError(f"Unexpected error: {exc}") + raise ValueError(f"Unexpected error: {exc}") from exc try: if result and not metadata: if len(result) >= limit: diff --git a/src/frontend/src/modals/DeleteConfirmationModal/index.tsx b/src/frontend/src/modals/DeleteConfirmationModal/index.tsx index 0800e3984..89158c29c 100644 --- a/src/frontend/src/modals/DeleteConfirmationModal/index.tsx +++ b/src/frontend/src/modals/DeleteConfirmationModal/index.tsx @@ -21,7 +21,7 @@ export default function DeleteConfirmationModal({ }) { return ( - {children} + {children} diff --git a/src/frontend/src/modals/EditNodeModal/index.tsx b/src/frontend/src/modals/EditNodeModal/index.tsx index a571b51cd..1e9888d96 100644 --- a/src/frontend/src/modals/EditNodeModal/index.tsx +++ b/src/frontend/src/modals/EditNodeModal/index.tsx @@ -37,6 +37,7 @@ import { convertObjToArray, convertValuesToNumbers, hasDuplicateKeys, + scapedJSONStringfy, } from "../../utils/reactflowUtils"; import { classNames } from "../../utils/utils"; import BaseModal from "../baseModal"; @@ -62,10 +63,6 @@ const EditNodeModal = forwardRef( const { setTabsState, tabId } = useContext(FlowsContext); const { reactFlowInstance } = useContext(typesContext); - let disabled = - reactFlowInstance - ?.getEdges() - .some((edge) => edge.targetHandle === data.id) ?? false; function changeAdvanced(n) { setMyData((old) => { @@ -150,52 +147,329 @@ const EditNodeModal = forwardRef( myData.node.template[templateParam].type ) ) - .map((templateParam, index) => ( - - - - - {myData.node?.template[templateParam] - .display_name - ? myData.node.template[templateParam] - .display_name - : myData.node?.template[templateParam].name} - - - - - {myData.node?.template[templateParam].type === - "str" && - !myData.node.template[templateParam].options ? ( -
- {myData.node.template[templateParam].list ? ( - { + let id = { + inputTypes: + data.node!.template[templateParam].input_types, + type: data.node!.template[templateParam].type, + id: data.id, + fieldName: templateParam, + }; + let disabled = + reactFlowInstance?.getEdges().some( + (edge) => + edge.targetHandle === + scapedJSONStringfy( + data.node!.template[templateParam].proxy + ? { + ...id, + proxy: + data.node?.template[templateParam] + .proxy, + } + : id + ) + ) ?? false; + return ( + + + + + {myData.node?.template[templateParam] + .display_name + ? myData.node.template[templateParam] + .display_name + : myData.node?.template[templateParam] + .name} + + + + + {myData.node?.template[templateParam].type === + "str" && + !myData.node.template[templateParam].options ? ( +
+ {myData.node.template[templateParam] + .list ? ( + { + handleOnNewValue( + value, + templateParam + ); + }} + /> + ) : myData.node.template[templateParam] + .multiline ? ( + { + handleOnNewValue( + value, + templateParam + ); + }} + /> + ) : ( + { + handleOnNewValue( + value, + templateParam + ); + }} + /> + )} +
+ ) : myData.node?.template[templateParam] + .type === "NestedDict" ? ( +
+ { + onChange={(newValue) => { + myData.node!.template[ + templateParam + ].value = newValue; + handleOnNewValue( + newValue, + templateParam + ); + }} + id="editnode-div-dict-input" + /> +
+ ) : myData.node?.template[templateParam] + .type === "dict" ? ( +
1 + ? "my-3" + : "" + )} + > + { + const valueToNumbers = + convertValuesToNumbers(newValue); + myData.node!.template[ + templateParam + ].value = valueToNumbers; + setErrorDuplicateKey( + hasDuplicateKeys(valueToNumbers) + ); + handleOnNewValue( + valueToNumbers, + templateParam + ); + }} + /> +
+ ) : myData.node?.template[templateParam] + .type === "bool" ? ( +
+ {" "} + { + handleOnNewValue( + isEnabled, + templateParam + ); + }} + size="small" + /> +
+ ) : myData.node?.template[templateParam] + .type === "float" ? ( +
+ { handleOnNewValue(value, templateParam); }} /> - ) : myData.node.template[templateParam] - .multiline ? ( - + ) : myData.node?.template[templateParam] + .type === "str" && + myData.node.template[templateParam] + .options ? ( +
+ + handleOnNewValue(value, templateParam) + } + value={ + myData.node.template[templateParam] + .value ?? "Choose an option" + } + id={"dropdown-edit-" + index} + > +
+ ) : myData.node?.template[templateParam] + .type === "int" ? ( +
+ { + handleOnNewValue(value, templateParam); + }} + /> +
+ ) : myData.node?.template[templateParam] + .type === "file" ? ( +
+ { + handleOnNewValue(value, templateParam); + }} + fileTypes={ + myData.node.template[templateParam] + .fileTypes + } + onFileChange={(filePath: string) => { + data.node!.template[ + templateParam + ].file_path = filePath; + }} + > +
+ ) : myData.node?.template[templateParam] + .type === "prompt" ? ( +
+ { + myData.node = nodeClass; + }} + value={ + myData.node.template[templateParam] + .value ?? "" + } + onChange={(value: string | string[]) => { + handleOnNewValue(value, templateParam); + }} + id={"prompt-area-edit" + index} + /> +
+ ) : myData.node?.template[templateParam] + .type === "code" ? ( +
+ { + data.node = nodeClass; + }} + nodeClass={data.node} disabled={disabled} editNode={true} value={ @@ -205,269 +479,38 @@ const EditNodeModal = forwardRef( onChange={(value: string | string[]) => { handleOnNewValue(value, templateParam); }} + id={"code-area-edit" + index} /> - ) : ( - { - handleOnNewValue(value, templateParam); - }} - /> - )} -
- ) : myData.node?.template[templateParam].type === - "NestedDict" ? ( -
- { - myData.node!.template[ - templateParam - ].value = newValue; - handleOnNewValue(newValue, templateParam); - }} - id="editnode-div-dict-input" - /> -
- ) : myData.node?.template[templateParam].type === - "dict" ? ( -
1 - ? "my-3" - : "" - )} - > - { - const valueToNumbers = - convertValuesToNumbers(newValue); - myData.node!.template[ - templateParam - ].value = valueToNumbers; - setErrorDuplicateKey( - hasDuplicateKeys(valueToNumbers) - ); - handleOnNewValue( - valueToNumbers, - templateParam - ); - }} - /> -
- ) : myData.node?.template[templateParam].type === - "bool" ? ( -
- {" "} +
+ ) : myData.node?.template[templateParam] + .type === "Any" ? ( + "-" + ) : ( +
+ )} + + +
{ - handleOnNewValue( - isEnabled, - templateParam - ); + enabled={ + !myData.node?.template[templateParam] + .advanced + } + setEnabled={(e) => { + changeAdvanced(templateParam); }} + disabled={disabled} size="small" />
- ) : myData.node?.template[templateParam].type === - "float" ? ( -
- { - handleOnNewValue(value, templateParam); - }} - /> -
- ) : myData.node?.template[templateParam].type === - "str" && - myData.node.template[templateParam].options ? ( -
- - handleOnNewValue(value, templateParam) - } - value={ - myData.node.template[templateParam] - .value ?? "Choose an option" - } - id={"dropdown-edit-" + index} - > -
- ) : myData.node?.template[templateParam].type === - "int" ? ( -
- { - handleOnNewValue(value, templateParam); - }} - /> -
- ) : myData.node?.template[templateParam].type === - "file" ? ( -
- { - handleOnNewValue(value, templateParam); - }} - fileTypes={ - myData.node.template[templateParam] - .fileTypes - } - onFileChange={(filePath: string) => { - data.node!.template[ - templateParam - ].file_path = filePath; - }} - > -
- ) : myData.node?.template[templateParam].type === - "prompt" ? ( -
- { - myData.node = nodeClass; - }} - value={ - myData.node.template[templateParam] - .value ?? "" - } - onChange={(value: string | string[]) => { - handleOnNewValue(value, templateParam); - }} - id={"prompt-area-edit" + index} - /> -
- ) : myData.node?.template[templateParam].type === - "code" ? ( -
- { - data.node = nodeClass; - }} - nodeClass={data.node} - disabled={disabled} - editNode={true} - value={ - myData.node.template[templateParam] - .value ?? "" - } - onChange={(value: string | string[]) => { - handleOnNewValue(value, templateParam); - }} - id={"code-area-edit" + index} - /> -
- ) : myData.node?.template[templateParam].type === - "Any" ? ( - "-" - ) : ( -
- )} -
- -
- { - changeAdvanced(templateParam); - }} - disabled={disabled} - size="small" - /> -
-
- - ))} + + + ); + })}
diff --git a/src/frontend/src/pages/MainPage/components/components/index.tsx b/src/frontend/src/pages/MainPage/components/components/index.tsx index 0997e117c..8cada7571 100644 --- a/src/frontend/src/pages/MainPage/components/components/index.tsx +++ b/src/frontend/src/pages/MainPage/components/components/index.tsx @@ -1,5 +1,5 @@ import { useContext, useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import PaginatorComponent from "../../../../components/PaginatorComponent"; import CollectionCardComponent from "../../../../components/cardComponent"; import CardsWrapComponent from "../../../../components/cardsWrapComponent"; @@ -142,20 +142,20 @@ export default function ComponentsComponent({ disabled={isLoading} button={ !is_component ? ( - + + + ) : ( <> ) diff --git a/tests/conftest.py b/tests/conftest.py index 006714303..7461b1f0f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -305,7 +305,7 @@ def added_vector_store(client, json_vector_store, logged_in_headers): @pytest.fixture def test_component_code(): - path = Path(__file__).parent.absolute() / "data" / "test_component.py" + path = Path(__file__).parent.absolute() / "data" / "component_test.py" # load the content as a string with open(path, "r") as f: return f.read() diff --git a/tests/data/test_component.py b/tests/data/component_test.py similarity index 100% rename from tests/data/test_component.py rename to tests/data/component_test.py diff --git a/tests/test_custom_component.py b/tests/test_custom_component.py index 4e0566010..3be85ddd2 100644 --- a/tests/test_custom_component.py +++ b/tests/test_custom_component.py @@ -8,7 +8,7 @@ from fastapi import HTTPException from langflow.interface.custom.base import CustomComponent from langflow.interface.custom.code_parser import CodeParser, CodeSyntaxError from langflow.interface.custom.component import Component, ComponentCodeNullError -from langflow.interface.types import build_langchain_template_custom_component, create_and_validate_component +from langflow.interface.types import build_custom_component_template, create_and_validate_component from langflow.services.database.models.flow import Flow, FlowCreate code_default = """ @@ -539,13 +539,13 @@ def test_create_and_validate_component_valid_code(test_component_code): def test_build_langchain_template_custom_component_valid_code(test_component_code): component = create_and_validate_component(test_component_code) - frontend_node = build_langchain_template_custom_component(component) + frontend_node = build_custom_component_template(component) assert isinstance(frontend_node, dict) template = frontend_node["template"] assert isinstance(template, dict) assert "param" in template param_options = template["param"]["options"] # Now run it again with an update field - frontend_node = build_langchain_template_custom_component(component, update_field="param") + frontend_node = build_custom_component_template(component, update_field="param") new_param_options = frontend_node["template"]["param"]["options"] assert param_options != new_param_options