Make update_build_config use always the latest version

This commit is contained in:
Gabriel Luiz Freitas Almeida 2024-03-11 14:27:15 -03:00
commit 9802322ae9
5 changed files with 141 additions and 83 deletions

View file

@ -8,7 +8,7 @@ from sqlmodel import Session, select
from langflow.api.utils import update_frontend_node_with_template_values from langflow.api.utils import update_frontend_node_with_template_values
from langflow.api.v1.schemas import ( from langflow.api.v1.schemas import (
CustomComponentCode, CustomComponentRequest,
InputValueRequest, InputValueRequest,
ProcessResponse, ProcessResponse,
RunResponse, RunResponse,
@ -52,7 +52,9 @@ def get_all(
raise HTTPException(status_code=500, detail=str(exc)) from exc 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( async def run_flow_with_caching(
session: Annotated[Session, Depends(get_session)], session: Annotated[Session, Depends(get_session)],
flow_id: str, flow_id: str,
@ -103,7 +105,9 @@ async def run_flow_with_caching(
""" """
try: try:
if inputs is not None: 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: else:
input_values = [{}] input_values = [{}]
@ -111,7 +115,9 @@ async def run_flow_with_caching(
outputs = [] outputs = []
if session_id: 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) graph, artifacts = session_data if session_data else (None, None)
task_result: Any = None task_result: Any = None
if not graph: if not graph:
@ -130,7 +136,11 @@ async def run_flow_with_caching(
else: else:
# Get the flow that matches the flow_id and belongs to the user # 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.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: if flow is None:
raise ValueError(f"Flow {flow_id} not found") 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') # StatementError('(builtins.ValueError) badly formed hexadecimal UUID string')
if "badly formed hexadecimal UUID string" in str(exc): 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 # 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: except ValueError as exc:
if f"Flow {flow_id} not found" in str(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: 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( @router.post(
@ -188,7 +204,8 @@ async def process(
""" """
# Raise a depreciation warning # Raise a depreciation warning
logger.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( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
@ -253,19 +270,23 @@ def get_version():
@router.post("/custom_component", status_code=HTTPStatus.OK) @router.post("/custom_component", status_code=HTTPStatus.OK)
async def custom_component( async def custom_component(
raw_code: CustomComponentCode, raw_code: CustomComponentRequest,
user: User = Depends(get_current_active_user), user: User = Depends(get_current_active_user),
): ):
component = CustomComponent(code=raw_code.code) 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 return built_frontend_node
@router.post("/custom_component/reload", status_code=HTTPStatus.OK) @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 from langflow.interface.custom.utils import build_custom_component_template
try: try:
@ -275,23 +296,40 @@ async def reload_custom_component(path: str, user: User = Depends(get_current_ac
raise ValueError(content) raise ValueError(content)
extractor = CustomComponent(code=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: except Exception as exc:
raise HTTPException(status_code=400, detail=str(exc)) raise HTTPException(status_code=400, detail=str(exc))
@router.post("/custom_component/update", status_code=HTTPStatus.OK) @router.post("/custom_component/update", status_code=HTTPStatus.OK)
async def custom_component_update( async def custom_component_update(
raw_code: CustomComponentCode, code_request: CustomComponentRequest,
user: User = Depends(get_current_active_user), 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, component,
user_id=user.id, 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 return component_node

View file

@ -4,8 +4,16 @@ from pathlib import Path
from typing import Any, Dict, List, Optional, Union from typing import Any, Dict, List, Optional, Union
from uuid import UUID 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.api_key.model import ApiKeyRead
from langflow.services.database.models.base import orjson_dumps from langflow.services.database.models.base import orjson_dumps
from langflow.services.database.models.flow import FlowCreate, FlowRead from langflow.services.database.models.flow import FlowCreate, FlowRead
@ -158,15 +166,22 @@ class StreamData(BaseModel):
data: dict data: dict
def __str__(self) -> str: 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 code: str
field: Optional[str] = None field: Optional[str] = None
field_value: Optional[Any] = None field_value: Optional[Any] = None
template: Optional[Union[dict, dotdict]] = None
frontend_node: Optional[dict] = None frontend_node: Optional[dict] = None
@field_serializer("template")
def template_into_dotdict(v):
return dotdict(v)
class CustomComponentResponseError(BaseModel): class CustomComponentResponseError(BaseModel):
detail: str detail: str

View file

@ -3,7 +3,7 @@ import contextlib
import re import re
import traceback import traceback
import warnings import warnings
from typing import Any, Dict, List, Optional, Union from typing import Any, Dict, List, Optional, Tuple, Union
from uuid import UUID from uuid import UUID
from fastapi import HTTPException from fastapi import HTTPException
@ -34,14 +34,18 @@ class UpdateBuildConfigError(Exception):
pass 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""" """Add output types to the frontend node"""
for return_type in return_types: for return_type in return_types:
if return_type is None: if return_type is None:
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail={ 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(), "traceback": traceback.format_exc(),
}, },
) )
@ -73,14 +77,18 @@ def reorder_fields(frontend_node: CustomComponentFrontendNode, field_order: List
frontend_node.field_order = field_order 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""" """Add base classes to the frontend node"""
for return_type_instance in return_types: for return_type_instance in return_types:
if return_type_instance is None: if return_type_instance is None:
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail={ 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(), "traceback": traceback.format_exc(),
}, },
) )
@ -115,7 +123,9 @@ def get_field_properties(extra_field):
# a required field is a field that does not contain # a required field is a field that does not contain
# optional in field_type # optional in field_type
# and a field that does not have a default value # 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 field_value = field_value if not isinstance(field_value, MissingDefault) else None
if not field_required: 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 a list, then it's a dropdown
# If options is None, then it's a list of strings # If options is None, then it's a list of strings
is_list = isinstance(field_config.get("options"), list) 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: 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) required = field_config.pop("required", field_required)
placeholder = field_config.pop("placeholder", "") placeholder = field_config.pop("placeholder", "")
@ -206,7 +220,9 @@ def add_extra_fields(frontend_node, field_config, function_args):
]: ]:
continue 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, {}) config = _field_config.pop(field_name, {})
frontend_node = add_new_custom_field( frontend_node = add_new_custom_field(
frontend_node, frontend_node,
@ -216,13 +232,17 @@ def add_extra_fields(frontend_node, field_config, function_args):
field_required, field_required,
config, 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(): for field_name, field_config in _field_config.copy().items():
if "name" not in field_config or field_name == "code": if "name" not in field_config or field_name == "code":
continue continue
config = _field_config.get(field_name, {}) config = _field_config.get(field_name, {})
config = config.model_dump() if isinstance(config, BaseModel) else config 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 = add_new_custom_field(
frontend_node, frontend_node,
field_name, field_name,
@ -243,9 +263,7 @@ def get_field_dict(field: Union[TemplateField, dict]):
def run_build_config( def run_build_config(
custom_component: CustomComponent, custom_component: CustomComponent,
user_id: Optional[Union[str, UUID]] = None, user_id: Optional[Union[str, UUID]] = None,
update_field=None, ) -> Tuple[dict, CustomComponent]:
update_field_value=None,
):
"""Build the field configuration for a custom component""" """Build the field configuration for a custom component"""
try: try:
@ -260,7 +278,9 @@ def run_build_config(
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail={ 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(), "traceback": traceback.format_exc(),
}, },
) from exc ) from exc
@ -274,38 +294,6 @@ def run_build_config(
# as a dict with the same keys as TemplateField # as a dict with the same keys as TemplateField
field_dict = get_field_dict(field) field_dict = get_field_dict(field)
build_config[field_name] = field_dict 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 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( def build_custom_component_template(
custom_component: CustomComponent, custom_component: CustomComponent,
user_id: Optional[Union[str, UUID]] = None, user_id: Optional[Union[str, UUID]] = None,
update_field: Optional[str] = None, ) -> Tuple[Dict[str, Any], CustomComponent]:
update_field_value: Optional[str] = None,
) -> Optional[Dict[str, Any]]:
"""Build a custom component template for the langchain""" """Build a custom component template for the langchain"""
try: try:
frontend_node = build_frontend_node(custom_component.template_config) 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( field_config, custom_instance = run_build_config(
custom_component, custom_component,
user_id=user_id, user_id=user_id,
update_field=update_field,
update_field_value=update_field_value,
) )
entrypoint_args = custom_component.get_function_entrypoint_args entrypoint_args = custom_component.get_function_entrypoint_args
add_extra_fields(frontend_node, field_config, 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_base_classes(
add_output_types(frontend_node, custom_component.get_function_entrypoint_return_type) 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()) 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: except Exception as exc:
if isinstance(exc, HTTPException): if isinstance(exc, HTTPException):
raise exc raise exc
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail={ 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(), "traceback": traceback.format_exc(),
}, },
) from exc ) from exc
@ -403,7 +395,7 @@ def create_component_template(component):
component_extractor = CustomComponent(code=component_code) 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: if not component_template["output_types"] and component_output_types:
component_template["output_types"] = 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) custom_component_dict = build_custom_component_list_from_path(path_str)
if custom_component_dict: if custom_component_dict:
category = next(iter(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 = merge_nested_dicts_with_renaming(
custom_components_from_file, custom_component_dict custom_components_from_file, custom_component_dict
) )
@ -461,7 +455,9 @@ def update_field_dict(
build_config = dd_build_config build_config = dd_build_config
except Exception as exc: except Exception as exc:
logger.error(f"Error while running update_build_config: {str(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 # Let's check if "range_spec" is a RangeSpec object
if "rangeSpec" in field_dict and isinstance(field_dict["rangeSpec"], RangeSpec): if "rangeSpec" in field_dict and isinstance(field_dict["rangeSpec"], RangeSpec):

View file

@ -4,6 +4,7 @@ import { BASE_URL_API } from "../../constants/constants";
import { api } from "../../controllers/API/api"; import { api } from "../../controllers/API/api";
import { import {
APIObjectType, APIObjectType,
APITemplateType,
Component, Component,
LoginType, LoginType,
Users, Users,
@ -369,11 +370,13 @@ export async function postCustomComponent(
export async function postCustomComponentUpdate( export async function postCustomComponentUpdate(
code: string, code: string,
template: APITemplateType,
field: string, field: string,
field_value: any field_value: any
): Promise<AxiosResponse<APIClassType>> { ): Promise<AxiosResponse<APIClassType>> {
return await api.post(`${BASE_URL_API}custom_component/update`, { return await api.post(`${BASE_URL_API}custom_component/update`, {
code, code,
template,
field, field,
field_value, field_value,
}); });

View file

@ -9,9 +9,15 @@ export const handleUpdateValues = async (name: string, data: NodeDataType) => {
console.error("Code not found in the template"); console.error("Code not found in the template");
return; return;
} }
const template = data.node?.template;
if (!template) {
console.error("No template found in the node.");
return;
}
try { try {
let newTemplate = await postCustomComponentUpdate( let newTemplate = await postCustomComponentUpdate(
code, code,
template,
name, name,
data.node?.template[name]?.value data.node?.template[name]?.value
) )