From 9802322ae93c0088dac7cd4c68f6774c4f36707f Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 11 Mar 2024 14:27:15 -0300 Subject: [PATCH] Make update_build_config use always the latest version --- src/backend/langflow/api/v1/endpoints.py | 78 +++++++++--- src/backend/langflow/api/v1/schemas.py | 21 +++- .../langflow/interface/custom/utils.py | 116 +++++++++--------- src/frontend/src/controllers/API/index.ts | 3 + src/frontend/src/utils/parameterUtils.ts | 6 + 5 files changed, 141 insertions(+), 83 deletions(-) diff --git a/src/backend/langflow/api/v1/endpoints.py b/src/backend/langflow/api/v1/endpoints.py index 1d9950585..ce20860c3 100644 --- a/src/backend/langflow/api/v1/endpoints.py +++ b/src/backend/langflow/api/v1/endpoints.py @@ -8,7 +8,7 @@ from sqlmodel import Session, select from langflow.api.utils import update_frontend_node_with_template_values from langflow.api.v1.schemas import ( - CustomComponentCode, + CustomComponentRequest, InputValueRequest, ProcessResponse, RunResponse, @@ -52,7 +52,9 @@ def get_all( raise HTTPException(status_code=500, detail=str(exc)) from exc -@router.post("/run/{flow_id}", response_model=RunResponse, response_model_exclude_none=True) +@router.post( + "/run/{flow_id}", response_model=RunResponse, response_model_exclude_none=True +) async def run_flow_with_caching( session: Annotated[Session, Depends(get_session)], flow_id: str, @@ -103,7 +105,9 @@ async def run_flow_with_caching( """ try: if inputs is not None: - input_values: list[dict[str, Union[str, list[str]]]] = [_input.model_dump() for _input in inputs] + input_values: list[dict[str, Union[str, list[str]]]] = [ + _input.model_dump() for _input in inputs + ] else: input_values = [{}] @@ -111,7 +115,9 @@ async def run_flow_with_caching( outputs = [] if session_id: - session_data = await session_service.load_session(session_id, flow_id=flow_id) + session_data = await session_service.load_session( + session_id, flow_id=flow_id + ) graph, artifacts = session_data if session_data else (None, None) task_result: Any = None if not graph: @@ -130,7 +136,11 @@ async def run_flow_with_caching( else: # Get the flow that matches the flow_id and belongs to the user # flow = session.query(Flow).filter(Flow.id == flow_id).filter(Flow.user_id == api_key_user.id).first() - flow = session.exec(select(Flow).where(Flow.id == flow_id).where(Flow.user_id == api_key_user.id)).first() + flow = session.exec( + select(Flow) + .where(Flow.id == flow_id) + .where(Flow.user_id == api_key_user.id) + ).first() if flow is None: raise ValueError(f"Flow {flow_id} not found") @@ -154,12 +164,18 @@ async def run_flow_with_caching( # StatementError('(builtins.ValueError) badly formed hexadecimal UUID string') if "badly formed hexadecimal UUID string" in str(exc): # This means the Flow ID is not a valid UUID which means it can't find the flow - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=str(exc) + ) from exc except ValueError as exc: if f"Flow {flow_id} not found" in str(exc): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=str(exc) + ) from exc else: - raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)) from exc + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc) + ) from exc @router.post( @@ -188,7 +204,8 @@ async def process( """ # Raise a depreciation warning logger.warning( - "The /process endpoint is deprecated and will be removed in a future version. " "Please use /run instead." + "The /process endpoint is deprecated and will be removed in a future version. " + "Please use /run instead." ) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -253,19 +270,23 @@ def get_version(): @router.post("/custom_component", status_code=HTTPStatus.OK) async def custom_component( - raw_code: CustomComponentCode, + raw_code: CustomComponentRequest, user: User = Depends(get_current_active_user), ): component = CustomComponent(code=raw_code.code) - built_frontend_node = build_custom_component_template(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.frontend_node) + built_frontend_node = update_frontend_node_with_template_values( + built_frontend_node, raw_code.frontend_node + ) return built_frontend_node @router.post("/custom_component/reload", status_code=HTTPStatus.OK) -async def reload_custom_component(path: str, user: User = Depends(get_current_active_user)): +async def reload_custom_component( + path: str, user: User = Depends(get_current_active_user) +): from langflow.interface.custom.utils import build_custom_component_template try: @@ -275,23 +296,40 @@ async def reload_custom_component(path: str, user: User = Depends(get_current_ac raise ValueError(content) extractor = CustomComponent(code=content) - return build_custom_component_template(extractor, user_id=user.id) + frontend_node, _ = build_custom_component_template(extractor, user_id=user.id) + return frontend_node except Exception as exc: raise HTTPException(status_code=400, detail=str(exc)) @router.post("/custom_component/update", status_code=HTTPStatus.OK) async def custom_component_update( - raw_code: CustomComponentCode, + code_request: CustomComponentRequest, user: User = Depends(get_current_active_user), ): - component = CustomComponent(code=raw_code.code) + """ + Update a custom component with the provided code request. - component_node = build_custom_component_template( + This endpoint generates the CustomComponentFrontendNode normally but then runs the `update_build_config` method + on the latest version of the template. This ensures that every time it runs, it has the latest version of the template. + + Args: + code_request (CustomComponentRequest): The code request containing the updated code for the custom component. + user (User, optional): The user making the request. Defaults to the current active user. + + Returns: + dict: The updated custom component node. + + """ + component = CustomComponent(code=code_request.code) + + component_node, cc_instance = build_custom_component_template( component, user_id=user.id, - update_field=raw_code.field, - update_field_value=raw_code.field_value, ) - # Update the field + updated_build_config = cc_instance.update_build_config( + code_request.template, code_request.field_value, code_request.field_name + ) + component_node["template"] = updated_build_config + return component_node diff --git a/src/backend/langflow/api/v1/schemas.py b/src/backend/langflow/api/v1/schemas.py index 70a60de5b..334db7b91 100644 --- a/src/backend/langflow/api/v1/schemas.py +++ b/src/backend/langflow/api/v1/schemas.py @@ -4,8 +4,16 @@ from pathlib import Path from typing import Any, Dict, List, Optional, Union from uuid import UUID -from pydantic import BaseModel, Field, RootModel, field_validator, model_serializer +from pydantic import ( + BaseModel, + Field, + RootModel, + field_serializer, + field_validator, + model_serializer, +) +from langflow.schema import dotdict from langflow.services.database.models.api_key.model import ApiKeyRead from langflow.services.database.models.base import orjson_dumps from langflow.services.database.models.flow import FlowCreate, FlowRead @@ -158,15 +166,22 @@ class StreamData(BaseModel): data: dict def __str__(self) -> str: - return f"event: {self.event}\ndata: {orjson_dumps(self.data, indent_2=False)}\n\n" + return ( + f"event: {self.event}\ndata: {orjson_dumps(self.data, indent_2=False)}\n\n" + ) -class CustomComponentCode(BaseModel): +class CustomComponentRequest(BaseModel): code: str field: Optional[str] = None field_value: Optional[Any] = None + template: Optional[Union[dict, dotdict]] = None frontend_node: Optional[dict] = None + @field_serializer("template") + def template_into_dotdict(v): + return dotdict(v) + class CustomComponentResponseError(BaseModel): detail: str diff --git a/src/backend/langflow/interface/custom/utils.py b/src/backend/langflow/interface/custom/utils.py index 05f8de842..43ee7db7c 100644 --- a/src/backend/langflow/interface/custom/utils.py +++ b/src/backend/langflow/interface/custom/utils.py @@ -3,7 +3,7 @@ import contextlib import re import traceback import warnings -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Tuple, Union from uuid import UUID from fastapi import HTTPException @@ -34,14 +34,18 @@ class UpdateBuildConfigError(Exception): pass -def add_output_types(frontend_node: CustomComponentFrontendNode, return_types: List[str]): +def add_output_types( + frontend_node: CustomComponentFrontendNode, return_types: List[str] +): """Add output types to the frontend node""" for return_type in return_types: if return_type is None: raise HTTPException( status_code=400, detail={ - "error": ("Invalid return type. Please check your code and try again."), + "error": ( + "Invalid return type. Please check your code and try again." + ), "traceback": traceback.format_exc(), }, ) @@ -73,14 +77,18 @@ def reorder_fields(frontend_node: CustomComponentFrontendNode, field_order: List frontend_node.field_order = field_order -def add_base_classes(frontend_node: CustomComponentFrontendNode, return_types: List[str]): +def add_base_classes( + frontend_node: CustomComponentFrontendNode, return_types: List[str] +): """Add base classes to the frontend node""" for return_type_instance in return_types: if return_type_instance is None: raise HTTPException( status_code=400, detail={ - "error": ("Invalid return type. Please check your code and try again."), + "error": ( + "Invalid return type. Please check your code and try again." + ), "traceback": traceback.format_exc(), }, ) @@ -115,7 +123,9 @@ def get_field_properties(extra_field): # a required field is a field that does not contain # optional in field_type # and a field that does not have a default value - field_required = "optional" not in field_type.lower() and isinstance(field_value, MissingDefault) + field_required = "optional" not in field_type.lower() and isinstance( + field_value, MissingDefault + ) field_value = field_value if not isinstance(field_value, MissingDefault) else None if not field_required: @@ -164,10 +174,14 @@ def add_new_custom_field( # If options is a list, then it's a dropdown # If options is None, then it's a list of strings is_list = isinstance(field_config.get("options"), list) - field_config["is_list"] = is_list or field_config.get("list", False) or field_contains_list + field_config["is_list"] = ( + is_list or field_config.get("list", False) or field_contains_list + ) if "name" in field_config: - warnings.warn("The 'name' key in field_config is used to build the object and can't be changed.") + warnings.warn( + "The 'name' key in field_config is used to build the object and can't be changed." + ) required = field_config.pop("required", field_required) placeholder = field_config.pop("placeholder", "") @@ -206,7 +220,9 @@ def add_extra_fields(frontend_node, field_config, function_args): ]: continue - field_name, field_type, field_value, field_required = get_field_properties(extra_field) + field_name, field_type, field_value, field_required = get_field_properties( + extra_field + ) config = _field_config.pop(field_name, {}) frontend_node = add_new_custom_field( frontend_node, @@ -216,13 +232,17 @@ def add_extra_fields(frontend_node, field_config, function_args): field_required, config, ) - if "kwargs" in function_args_names and not all(key in function_args_names for key in field_config.keys()): + if "kwargs" in function_args_names and not all( + key in function_args_names for key in field_config.keys() + ): for field_name, field_config in _field_config.copy().items(): if "name" not in field_config or field_name == "code": continue config = _field_config.get(field_name, {}) config = config.model_dump() if isinstance(config, BaseModel) else config - field_name, field_type, field_value, field_required = get_field_properties(extra_field=config) + field_name, field_type, field_value, field_required = get_field_properties( + extra_field=config + ) frontend_node = add_new_custom_field( frontend_node, field_name, @@ -243,9 +263,7 @@ def get_field_dict(field: Union[TemplateField, dict]): def run_build_config( custom_component: CustomComponent, user_id: Optional[Union[str, UUID]] = None, - update_field=None, - update_field_value=None, -): +) -> Tuple[dict, CustomComponent]: """Build the field configuration for a custom component""" try: @@ -260,7 +278,9 @@ def run_build_config( raise HTTPException( status_code=400, detail={ - "error": ("Invalid type convertion. Please check your code and try again."), + "error": ( + "Invalid type convertion. Please check your code and try again." + ), "traceback": traceback.format_exc(), }, ) from exc @@ -274,38 +294,6 @@ def run_build_config( # as a dict with the same keys as TemplateField field_dict = get_field_dict(field) build_config[field_name] = field_dict - # This has to be done to set refresh if options or value are callable - if update_field is not None and field_name != update_field: - build_config = update_field_dict( - custom_component_instance=custom_instance, - field_dict=field_dict, - build_config=build_config, - call=False, - ) - continue - try: - build_config = update_field_dict( - custom_component_instance=custom_instance, - field_dict=field_dict, - build_config=build_config, - update_field=update_field, - update_field_value=update_field_value, - call=True, - ) - build_config[field_name] = field_dict - except Exception as exc: - logger.error(f"Error while getting build_config: {str(exc)}") - if isinstance(exc, UpdateBuildConfigError): - message = str(exc) - else: - message = f"Error while getting build_config: {str(exc)}" - raise HTTPException( - status_code=400, - detail={ - "error": message, - "traceback": traceback.format_exc(), - }, - ) from exc return build_config, custom_instance @@ -358,9 +346,7 @@ def add_code_field(frontend_node: CustomComponentFrontendNode, raw_code, field_c def build_custom_component_template( custom_component: CustomComponent, user_id: Optional[Union[str, UUID]] = None, - update_field: Optional[str] = None, - update_field_value: Optional[str] = None, -) -> Optional[Dict[str, Any]]: +) -> Tuple[Dict[str, Any], CustomComponent]: """Build a custom component template for the langchain""" try: frontend_node = build_frontend_node(custom_component.template_config) @@ -368,29 +354,35 @@ def build_custom_component_template( field_config, custom_instance = run_build_config( custom_component, user_id=user_id, - update_field=update_field, - update_field_value=update_field_value, ) entrypoint_args = custom_component.get_function_entrypoint_args add_extra_fields(frontend_node, field_config, entrypoint_args) - frontend_node = add_code_field(frontend_node, custom_component.code, field_config.get("code", {})) + frontend_node = add_code_field( + frontend_node, custom_component.code, field_config.get("code", {}) + ) - add_base_classes(frontend_node, custom_component.get_function_entrypoint_return_type) - add_output_types(frontend_node, custom_component.get_function_entrypoint_return_type) + add_base_classes( + frontend_node, custom_component.get_function_entrypoint_return_type + ) + add_output_types( + frontend_node, custom_component.get_function_entrypoint_return_type + ) reorder_fields(frontend_node, custom_instance._get_field_order()) - return frontend_node.to_dict(add_name=False) + return frontend_node.to_dict(add_name=False), custom_instance except Exception as exc: if isinstance(exc, HTTPException): raise exc raise HTTPException( status_code=400, detail={ - "error": (f"Something went wrong while building the custom component. Hints: {str(exc)}"), + "error": ( + f"Something went wrong while building the custom component. Hints: {str(exc)}" + ), "traceback": traceback.format_exc(), }, ) from exc @@ -403,7 +395,7 @@ def create_component_template(component): component_extractor = CustomComponent(code=component_code) - component_template = build_custom_component_template(component_extractor) + component_template, _ = build_custom_component_template(component_extractor) if not component_template["output_types"] and component_output_types: component_template["output_types"] = component_output_types @@ -426,7 +418,9 @@ def build_custom_components(components_paths: List[str]): custom_component_dict = build_custom_component_list_from_path(path_str) if custom_component_dict: category = next(iter(custom_component_dict)) - logger.info(f"Loading {len(custom_component_dict[category])} component(s) from category {category}") + logger.info( + f"Loading {len(custom_component_dict[category])} component(s) from category {category}" + ) custom_components_from_file = merge_nested_dicts_with_renaming( custom_components_from_file, custom_component_dict ) @@ -461,7 +455,9 @@ def update_field_dict( build_config = dd_build_config except Exception as exc: logger.error(f"Error while running update_build_config: {str(exc)}") - raise UpdateBuildConfigError(f"Error while running update_build_config: {str(exc)}") from exc + raise UpdateBuildConfigError( + f"Error while running update_build_config: {str(exc)}" + ) from exc # Let's check if "range_spec" is a RangeSpec object if "rangeSpec" in field_dict and isinstance(field_dict["rangeSpec"], RangeSpec): diff --git a/src/frontend/src/controllers/API/index.ts b/src/frontend/src/controllers/API/index.ts index a8ee9c6e7..694428a22 100644 --- a/src/frontend/src/controllers/API/index.ts +++ b/src/frontend/src/controllers/API/index.ts @@ -4,6 +4,7 @@ import { BASE_URL_API } from "../../constants/constants"; import { api } from "../../controllers/API/api"; import { APIObjectType, + APITemplateType, Component, LoginType, Users, @@ -369,11 +370,13 @@ export async function postCustomComponent( export async function postCustomComponentUpdate( code: string, + template: APITemplateType, field: string, field_value: any ): Promise> { return await api.post(`${BASE_URL_API}custom_component/update`, { code, + template, field, field_value, }); diff --git a/src/frontend/src/utils/parameterUtils.ts b/src/frontend/src/utils/parameterUtils.ts index 9635ce96d..d950ac0c1 100644 --- a/src/frontend/src/utils/parameterUtils.ts +++ b/src/frontend/src/utils/parameterUtils.ts @@ -9,9 +9,15 @@ export const handleUpdateValues = async (name: string, data: NodeDataType) => { console.error("Code not found in the template"); return; } + const template = data.node?.template; + if (!template) { + console.error("No template found in the node."); + return; + } try { let newTemplate = await postCustomComponentUpdate( code, + template, name, data.node?.template[name]?.value )