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.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

View file

@ -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

View file

@ -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):

View file

@ -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<AxiosResponse<APIClassType>> {
return await api.post(`${BASE_URL_API}custom_component/update`, {
code,
template,
field,
field_value,
});

View file

@ -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
)