diff --git a/src/backend/langflow/api/v1/endpoints.py b/src/backend/langflow/api/v1/endpoints.py index b5dc8dfa9..79eb90ac3 100644 --- a/src/backend/langflow/api/v1/endpoints.py +++ b/src/backend/langflow/api/v1/endpoints.py @@ -5,6 +5,7 @@ from langflow.cache.utils import save_uploaded_file from langflow.database.models.flow import Flow from langflow.processing.process import process_graph_cached, process_tweaks from langflow.utils.logger import logger +from langflow.settings import settings from fastapi import APIRouter, Depends, HTTPException, UploadFile @@ -35,7 +36,16 @@ router = APIRouter(tags=["Base"]) @router.get("/all") def get_all(): - return build_langchain_types_dict() + native_components = build_langchain_types_dict() + + if settings.component_path: + custom_components_from_file = build_langchain_custom_component_list_from_path( + str(settings.component_path[0]) + ) + else: + custom_components_from_file = {} + + return {**native_components, **custom_components_from_file} @router.get("/load_custom_component_from_path") @@ -59,8 +69,9 @@ def get_load_custom_component_from_path_test(path: str): reader = DirectoryReader(path, False) file_list = reader.get_files() + data = reader.build_component_menu_list(file_list) - return reader.build_component_menu_list(file_list) + return reader.filter_loaded_components(data, True) # For backwards compatibility we will keep the old endpoint diff --git a/src/backend/langflow/interface/custom/component.py b/src/backend/langflow/interface/custom/component.py index 5e84c235e..3db2a2516 100644 --- a/src/backend/langflow/interface/custom/component.py +++ b/src/backend/langflow/interface/custom/component.py @@ -56,13 +56,13 @@ class Component(BaseModel): item_name = item.get("name") if item_value := item.get("value"): - if "langflow_display_name" in item_name: + if "display_name" in item_name: template_config["display_name"] = ast.literal_eval(item_value) - elif "langflow_description" in item_name: + elif "description" in item_name: template_config["description"] = ast.literal_eval(item_value) - elif "langflow_field_config" in item_name: + elif "field_config" in item_name: template_config["field_config"] = ast.literal_eval(item_value) return template_config diff --git a/src/backend/langflow/interface/custom/constants.py b/src/backend/langflow/interface/custom/constants.py index 6bf4f4651..a001e9091 100644 --- a/src/backend/langflow/interface/custom/constants.py +++ b/src/backend/langflow/interface/custom/constants.py @@ -35,9 +35,9 @@ from langchain.schema import Document import requests class YourComponent(CustomComponent): - langflow_display_name: str = "Your Component" - langflow_description: str = "Your description" - langflow_field_config = { "url": { "multiline": True, "required": True } } + display_name: str = "Your Component" + description: str = "Your description" + field_config = { "url": { "multiline": True, "required": True } } def build(self, url: str, llm: BaseLLM, template: Prompt) -> Document: response = requests.get(url) diff --git a/src/backend/langflow/interface/custom/load_custom_component_from_path.py b/src/backend/langflow/interface/custom/load_custom_component_from_path.py index 16c846f48..6842dbee4 100644 --- a/src/backend/langflow/interface/custom/load_custom_component_from_path.py +++ b/src/backend/langflow/interface/custom/load_custom_component_from_path.py @@ -34,7 +34,9 @@ class StringCompressor: class DirectoryReader: - base_path = "/custom_component_files" + # Ensure the base path to read the files that contain + # the custom components from this directory. + base_path = "" def __init__(self, directory_path, compress_code_field=False): """ @@ -59,6 +61,22 @@ class DirectoryReader: """ return len(file_content.strip()) == 0 + def filter_loaded_components(self, data: dict, with_errors: bool) -> dict: + items = [ + { + "name": menu["name"], + "path": menu["path"], + "components": [ + component + for component in menu["components"] + if (component["error"] if with_errors else not component["error"]) + ], + } + for menu in data["menu"] + ] + filtred = [menu for menu in items if menu["components"]] + return {"menu": filtred} + def validate_code(self, file_content): """ Validate the Python code by trying to parse it with ast.parse. @@ -92,6 +110,7 @@ class DirectoryReader: raise CustomComponentPathValueError( f"The path needs to start with '{self.base_path}'." ) + file_list = [] for root, _, files in os.walk(safe_path): file_list.extend( diff --git a/src/backend/langflow/interface/types.py b/src/backend/langflow/interface/types.py index b33e2a397..ba645bb15 100644 --- a/src/backend/langflow/interface/types.py +++ b/src/backend/langflow/interface/types.py @@ -244,10 +244,26 @@ def build_langchain_custom_component_list_from_path(path: str): # Build and validate all files data = reader.build_component_menu_list(file_list) + valid_components = reader.filter_loaded_components(data, False) - raw_code = data.get("menu")[0].get("components")[0].get("code") + # TODO: Handle those invalid components + reader.filter_loaded_components(data, True) - extractor = CustomComponent(code=raw_code) - extractor.is_check_valid() + menu = {} + for menu_item in valid_components["menu"]: + menu_name = menu_item["name"] + menu[menu_name] = {} - return build_langchain_template_custom_component(extractor) + for component in menu_item["components"]: + component_name = component["name"] + component_code = component["code"] + + component_extractor = CustomComponent(code=component_code) + component_extractor.is_check_valid() + component_template = build_langchain_template_custom_component( + component_extractor + ) + + menu[menu_name][component_name] = component_template + + return menu diff --git a/src/backend/langflow/settings.py b/src/backend/langflow/settings.py index 5966d2711..06f925a3c 100644 --- a/src/backend/langflow/settings.py +++ b/src/backend/langflow/settings.py @@ -1,5 +1,6 @@ import os -from typing import Optional +from typing import Optional, List +from pathlib import Path import yaml from pydantic import BaseSettings, root_validator @@ -28,9 +29,10 @@ class Settings(BaseSettings): database_url: Optional[str] = None cache: str = "InMemoryCache" remove_api_keys: bool = False + component_path: List[Path] @root_validator(pre=True) - def set_database_url(cls, values): + def set_env_variables(cls, values): if "database_url" not in values: logger.debug( "No database_url provided, trying LANGFLOW_DATABASE_URL env variable" @@ -40,6 +42,12 @@ class Settings(BaseSettings): else: logger.debug("No DATABASE_URL env variable, using sqlite database") values["database_url"] = "sqlite:///./langflow.db" + + values["component_path"] = [Path(__file__).parent / "components"] + + if os.getenv("LANGFLOW_COMPONENT_PATH"): + values["component_path"].append(Path(os.getenv("LANGFLOW_COMPONENT_PATH"))) + return values class Config: @@ -71,6 +79,7 @@ class Settings(BaseSettings): self.retrievers = new_settings.retrievers or {} self.output_parsers = new_settings.output_parsers or {} self.custom_components = new_settings.custom_components or {} + self.component_path = new_settings.component_path or [] self.dev = dev def update_settings(self, **kwargs): diff --git a/tests/test_custom_component.py b/tests/test_custom_component.py index e60b392ea..f4e57d10d 100644 --- a/tests/test_custom_component.py +++ b/tests/test_custom_component.py @@ -24,9 +24,9 @@ from langchain.schema import Document import requests class YourComponent(CustomComponent): - langflow_display_name: str = "Your Component" - langflow_description: str = "Your description" - langflow_field_config = { "url": { "multiline": True, "required": True } } + display_name: str = "Your Component" + description: str = "Your description" + field_config = { "url": { "multiline": True, "required": True } } def build(self, url: str, llm: BaseLLM, template: Prompt) -> Document: response = requests.get(url)