diff --git a/src/backend/langflow/api/v1/endpoints.py b/src/backend/langflow/api/v1/endpoints.py index 1d9950585..44a12a0d5 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, @@ -253,12 +253,12 @@ 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) return built_frontend_node @@ -275,23 +275,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..9d8314d48 100644 --- a/src/backend/langflow/api/v1/schemas.py +++ b/src/backend/langflow/api/v1/schemas.py @@ -4,8 +4,17 @@ 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, + ConfigDict, + 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 @@ -161,12 +170,18 @@ class StreamData(BaseModel): return f"event: {self.event}\ndata: {orjson_dumps(self.data, indent_2=False)}\n\n" -class CustomComponentCode(BaseModel): +class CustomComponentRequest(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) code: str field: Optional[str] = None field_value: Optional[Any] = None + template: Optional[dict] = 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/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py index fa4a0e5eb..96da64eda 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -162,14 +162,16 @@ class Graph: if vertex is None: raise ValueError(f"Vertex {vertex_id} not found") vertex.update_raw_params({"session_id": session_id}) + # Process the graph try: await self.process() self.increment_run_count() except Exception as exc: logger.exception(exc) raise ValueError(f"Error running graph: {exc}") from exc + # Get the outputs vertex_outputs = [] - for vertex_id in self._is_output_vertices: + for vertex_id in self.vertices: vertex = self.get_vertex(vertex_id) if vertex is None: raise ValueError(f"Vertex {vertex_id} not found") @@ -178,6 +180,7 @@ class Graph: await vertex.consume_async_generator() if not outputs or (vertex.display_name in outputs or vertex.id in outputs): vertex_outputs.append(vertex.result) + return vertex_outputs async def run( diff --git a/src/backend/langflow/interface/custom/utils.py b/src/backend/langflow/interface/custom/utils.py index 05f8de842..c9d1cfedd 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 @@ -243,9 +243,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: @@ -274,38 +272,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 +324,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,8 +332,6 @@ 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 @@ -383,7 +345,7 @@ def build_custom_component_template( 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 @@ -403,7 +365,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 diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index 7775e7f94..122ea7d3f 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -85,8 +85,8 @@ export default function GenericNode({ // return if (!thisNodeTemplate.code) return; - const currentCode = thisNodeTemplate.code.value; - const thisNodesCode = data.node!.template.code.value; + const currentCode = thisNodeTemplate.code?.value; + const thisNodesCode = data.node!.template?.code?.value; if (currentCode !== thisNodesCode) { addToOutdatedNodes(data.id); setIsOutdated(true); @@ -96,7 +96,7 @@ export default function GenericNode({ setIsOutdated(false); } // template.code can be undefined - }, [data.node?.template.code.value]); + }, [data.node?.template?.code?.value]); const updateNodeCode = useCallback( (newNodeClass: APIClassType, code: string, name: string) => { 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 ) diff --git a/tests/test_helper_components.py b/tests/test_helper_components.py index 28b84a42e..82515b699 100644 --- a/tests/test_helper_components.py +++ b/tests/test_helper_components.py @@ -24,7 +24,9 @@ def test_document_to_record_component(): # Act # Replace with your actual test data - document = Document(page_content="key: value", metadata={"url": "https://example.com"}) + document = Document( + page_content="key: value", metadata={"url": "https://example.com"} + ) result = document_to_record_component.build(document) # Assert @@ -37,12 +39,14 @@ def test_uuid_generator_component(): uuid_generator_component = helpers.UUIDGeneratorComponent() uuid_generator_component.code = open(helpers.IDGenerator.__file__, "r").read() - frontend_node = build_custom_component_template(uuid_generator_component) + frontend_node, _ = build_custom_component_template(uuid_generator_component) # Act build_config = frontend_node.get("template") field_name = "unique_id" - build_config = uuid_generator_component.update_build_config(build_config, None, field_name) + build_config = uuid_generator_component.update_build_config( + build_config, None, field_name + ) unique_id = build_config["unique_id"]["value"] result = uuid_generator_component.build(unique_id)