From 51ff21be0190a1ad3db486a30bee26ab8473184a Mon Sep 17 00:00:00 2001 From: Fangyin Cheng Date: Tue, 30 Jul 2024 09:11:59 +0800 Subject: [PATCH 01/89] feat(core): Add UI component for AWEL flow --- dbgpt/core/awel/flow/exceptions.py | 11 + dbgpt/core/awel/flow/ui.py | 348 +++++++++++++++++++++++++++++ 2 files changed, 359 insertions(+) create mode 100644 dbgpt/core/awel/flow/ui.py diff --git a/dbgpt/core/awel/flow/exceptions.py b/dbgpt/core/awel/flow/exceptions.py index 0c3dc667d..68c02f8ac 100644 --- a/dbgpt/core/awel/flow/exceptions.py +++ b/dbgpt/core/awel/flow/exceptions.py @@ -44,3 +44,14 @@ class FlowDAGMetadataException(FlowMetadataException): def __init__(self, message: str, error_type="build_dag_metadata_error"): """Create a new FlowDAGMetadataException.""" super().__init__(message, error_type) + + +class FlowUIComponentException(FlowException): + """The exception for UI parameter failed.""" + + def __init__( + self, message: str, component_name: str, error_type="build_ui_component_error" + ): + """Create a new FlowUIParameterException.""" + new_message = f"{component_name}: {message}" + super().__init__(new_message, error_type) diff --git a/dbgpt/core/awel/flow/ui.py b/dbgpt/core/awel/flow/ui.py new file mode 100644 index 000000000..a9f220961 --- /dev/null +++ b/dbgpt/core/awel/flow/ui.py @@ -0,0 +1,348 @@ +"""UI components for AWEL flow.""" + +from typing import Any, Dict, List, Literal, Optional + +from dbgpt._private.pydantic import BaseModel, Field + +from .exceptions import FlowUIComponentException + +_UI_TYPE = Literal[ + "cascader", + "checkbox", + "date_picker", + "input", + "text_area", + "auto_complete", + "slider", + "time_picker", + "tree_select", + "upload", + "variable", + "password", + "code_editor", +] + + +class RefreshableMixin(BaseModel): + """Refreshable mixin.""" + + refresh: Optional[bool] = Field( + False, + description="Whether to enable the refresh", + ) + refresh_depends: Optional[List[str]] = Field( + None, + description="The dependencies of the refresh", + ) + + +class UIComponent(RefreshableMixin, BaseModel): + """UI component.""" + + class UIRange(BaseModel): + """UI range.""" + + min: int | float | str | None = Field(None, description="Minimum value") + max: int | float | str | None = Field(None, description="Maximum value") + step: int | float | str | None = Field(None, description="Step value") + format: str | None = Field(None, description="Format") + + ui_type: _UI_TYPE = Field(..., description="UI component type") + + disabled: bool = Field( + False, + description="Whether the component is disabled", + ) + + def check_parameter(self, parameter_dict: Dict[str, Any]): + """Check parameter. + + Raises: + FlowUIParameterException: If the parameter is invalid. + """ + + def _check_options(self, options: Dict[str, Any]): + """Check options.""" + if not options: + raise FlowUIComponentException("options is required", self.ui_type) + + +class StatusMixin(BaseModel): + """Status mixin.""" + + status: Optional[Literal["error", "warning"]] = Field( + None, + description="Status of the input", + ) + + +class RangeMixin(BaseModel): + """Range mixin.""" + + ui_range: Optional[UIComponent.UIRange] = Field( + None, + description="Range for the component", + ) + + +class InputMixin(BaseModel): + """Input mixin.""" + + class Count(BaseModel): + """Count.""" + + show: Optional[bool] = Field( + None, + description="Whether to show count", + ) + max: Optional[int] = Field( + None, + description="The maximum count", + ) + exceed_strategy: Optional[Literal["cut", "warning"]] = Field( + None, + description="The strategy when the count exceeds", + ) + + count: Optional[Count] = Field( + None, + description="Count configuration", + ) + + +class PanelEditorMixin(BaseModel): + """Edit the content in the panel.""" + + class Editor(BaseModel): + """Editor configuration.""" + + width: Optional[int] = Field( + None, + description="The width of the panel", + ) + height: Optional[int] = Field( + None, + description="The height of the panel", + ) + + editor: Optional[Editor] = Field( + None, + description="The editor configuration", + ) + + +class UICascader(StatusMixin, UIComponent): + """Cascader component.""" + + ui_type: Literal["cascader"] = Field("cascader", frozen=True) + + show_search: bool = Field( + False, + description="Whether to show search input", + ) + + def check_parameter(self, parameter_dict: Dict[str, Any]): + """Check parameter.""" + options = parameter_dict.get("options") + if not options: + raise FlowUIComponentException("options is required", self.ui_type) + first_level = options[0] + if "children" not in first_level: + raise FlowUIComponentException( + "children is required in options", self.ui_type + ) + + +class UICheckbox(UIComponent): + """Checkbox component.""" + + ui_type: Literal["checkbox"] = Field("checkbox", frozen=True) + + def check_parameter(self, parameter_dict: Dict[str, Any]): + """Check parameter.""" + self._check_options(parameter_dict.get("options", {})) + + +class UIDatePicker(StatusMixin, RangeMixin, UIComponent): + """Date picker component.""" + + ui_type: Literal["date_picker"] = Field("date_picker", frozen=True) + + placement: Optional[ + Literal["topLeft", "topRight", "bottomLeft", "bottomRight"] + ] = Field( + None, + description="The position of the picker panel, None means bottomLeft", + ) + + +class UIInput(StatusMixin, InputMixin, UIComponent): + """Input component.""" + + ui_type: Literal["input"] = Field("input", frozen=True) + + prefix: Optional[str] = Field( + None, + description="The prefix, icon or text", + examples=["$", "icon:UserOutlined"], + ) + suffix: Optional[str] = Field( + None, + description="The suffix, icon or text", + examples=["$", "icon:SearchOutlined"], + ) + + +class UITextArea(PanelEditorMixin, UIInput): + """Text area component.""" + + ui_type: Literal["text_area"] = Field("text_area", frozen=True) # type: ignore + auto_size: Optional[bool] = Field( + None, + description="Whether the height of the textarea automatically adjusts based " + "on the content", + ) + min_rows: Optional[int] = Field( + None, + description="The minimum number of rows", + ) + max_rows: Optional[int] = Field( + None, + description="The maximum number of rows", + ) + + +class UIAutoComplete(UIInput): + """Auto complete component.""" + + ui_type: Literal["auto_complete"] = Field( # type: ignore + "auto_complete", frozen=True + ) + + +class UISlider(RangeMixin, UIComponent): + """Slider component.""" + + ui_type: Literal["slider"] = Field("slider", frozen=True) + + show_input: bool = Field( + False, description="Whether to display the value in a input component" + ) + + +class UITimePicker(StatusMixin, UIComponent): + """Time picker component.""" + + ui_type: Literal["time_picker"] = Field("time_picker", frozen=True) + + format: Optional[str] = Field( + None, + description="The format of the time", + examples=["HH:mm:ss", "HH:mm"], + ) + hour_step: Optional[int] = Field( + None, + description="The step of the hour input", + ) + minute_step: Optional[int] = Field( + None, + description="The step of the minute input", + ) + second_step: Optional[int] = Field( + None, + description="The step of the second input", + ) + + +class UITreeSelect(StatusMixin, UIComponent): + """Tree select component.""" + + ui_type: Literal["tree_select"] = Field("tree_select", frozen=True) + + def check_parameter(self, parameter_dict: Dict[str, Any]): + """Check parameter.""" + options = parameter_dict.get("options") + if not options: + raise FlowUIComponentException("options is required", self.ui_type) + first_level = options[0] + if "children" not in first_level: + raise FlowUIComponentException( + "children is required in options", self.ui_type + ) + + +class UIUpload(StatusMixin, UIComponent): + """Upload component.""" + + ui_type: Literal["upload"] = Field("upload", frozen=True) + + max_file_size: Optional[int] = Field( + None, + description="The maximum size of the file, in bytes", + ) + max_count: Optional[int] = Field( + None, + description="The maximum number of files that can be uploaded", + ) + file_types: Optional[List[str]] = Field( + None, + description="The file types that can be accepted", + examples=[[".png", ".jpg"]], + ) + up_event: Optional[Literal["after_select", "button_click"]] = Field( + None, + description="The event that triggers the upload", + ) + drag: bool = Field( + False, + description="Whether to support drag and drop upload", + ) + action: Optional[str] = Field( + None, + description="The URL for the file upload", + ) + + +class UIVariableInput(UIInput): + """Variable input component.""" + + ui_type: Literal["variable"] = Field("variable", frozen=True) # type: ignore + key: str = Field(..., description="The key of the variable") + key_type: Literal["common", "secret"] = Field( + "common", + description="The type of the key", + ) + refresh: Optional[bool] = Field( + True, + description="Whether to enable the refresh", + ) + + def check_parameter(self, parameter_dict: Dict[str, Any]): + """Check parameter.""" + self._check_options(parameter_dict.get("options", {})) + + +class UIPasswordInput(UIVariableInput): + """Password input component.""" + + ui_type: Literal["password"] = Field("password", frozen=True) # type: ignore + + key_type: Literal["secret"] = Field( + "secret", + description="The type of the key", + ) + + def check_parameter(self, parameter_dict: Dict[str, Any]): + """Check parameter.""" + self._check_options(parameter_dict.get("options", {})) + + +class UICodeEditor(UITextArea): + """Code editor component.""" + + ui_type: Literal["code_editor"] = Field("code_editor", frozen=True) # type: ignore + + language: Optional[str] = Field( + "python", + description="The language of the code", + ) From 8465726dc8e3ee0109d836bbf952bd9793bf8622 Mon Sep 17 00:00:00 2001 From: Fangyin Cheng Date: Mon, 5 Aug 2024 18:08:02 +0800 Subject: [PATCH 02/89] feat: Add flow2.0 examples --- dbgpt/core/awel/flow/base.py | 18 +- dbgpt/core/awel/flow/ui.py | 289 +++++---- dbgpt/core/awel/util/parameter_util.py | 5 +- .../core/interface/operators/llm_operator.py | 5 + .../interface/operators/prompt_operator.py | 4 + dbgpt/serve/flow/api/endpoints.py | 5 +- examples/awel/awel_flow_ui_components.py | 583 ++++++++++++++++++ 7 files changed, 786 insertions(+), 123 deletions(-) create mode 100644 examples/awel/awel_flow_ui_components.py diff --git a/dbgpt/core/awel/flow/base.py b/dbgpt/core/awel/flow/base.py index fb60538ba..da0b2c378 100644 --- a/dbgpt/core/awel/flow/base.py +++ b/dbgpt/core/awel/flow/base.py @@ -19,6 +19,7 @@ from dbgpt.core.interface.serialization import Serializable from .exceptions import FlowMetadataException, FlowParameterMetadataException +from .ui import UIComponent _TYPE_REGISTRY: Dict[str, Type] = {} @@ -136,6 +137,7 @@ def __init__(self, label: str, description: str): "agent": _CategoryDetail("Agent", "The agent operator"), "rag": _CategoryDetail("RAG", "The RAG operator"), "experimental": _CategoryDetail("EXPERIMENTAL", "EXPERIMENTAL operator"), + "example": _CategoryDetail("Example", "Example operator"), } @@ -151,6 +153,7 @@ class OperatorCategory(str, Enum): AGENT = "agent" RAG = "rag" EXPERIMENTAL = "experimental" + EXAMPLE = "example" def label(self) -> str: """Get the label of the category.""" @@ -193,6 +196,7 @@ class OperatorType(str, Enum): "embeddings": _CategoryDetail("Embeddings", "The embeddings resource"), "rag": _CategoryDetail("RAG", "The resource"), "vector_store": _CategoryDetail("Vector Store", "The vector store resource"), + "example": _CategoryDetail("Example", "The example resource"), } @@ -209,6 +213,7 @@ class ResourceCategory(str, Enum): EMBEDDINGS = "embeddings" RAG = "rag" VECTOR_STORE = "vector_store" + EXAMPLE = "example" def label(self) -> str: """Get the label of the category.""" @@ -343,6 +348,9 @@ class Parameter(TypeMetadata, Serializable): alias: Optional[List[str]] = Field( None, description="The alias of the parameter(Compatible with old version)" ) + ui: Optional[UIComponent] = Field( + None, description="The UI component of the parameter" + ) @model_validator(mode="before") @classmethod @@ -398,6 +406,7 @@ def build_from( label: str, name: str, type: Type, + is_list: bool = False, optional: bool = False, default: Optional[Union[DefaultParameterType, _MISSING_TYPE]] = _MISSING_VALUE, placeholder: Optional[DefaultParameterType] = None, @@ -405,6 +414,7 @@ def build_from( options: Optional[Union[BaseDynamicOptions, List[OptionValue]]] = None, resource_type: ResourceType = ResourceType.INSTANCE, alias: Optional[List[str]] = None, + ui: Optional[UIComponent] = None, ): """Build the parameter from the type.""" type_name = type.__qualname__ @@ -419,6 +429,7 @@ def build_from( name=name, type_name=type_name, type_cls=type_cls, + is_list=is_list, category=category.value, resource_type=resource_type, optional=optional, @@ -427,6 +438,7 @@ def build_from( description=description or label, options=options, alias=alias, + ui=ui, ) @classmethod @@ -456,11 +468,12 @@ def build_from_ui(cls, data: Dict) -> "Parameter": description=data["description"], options=data["options"], value=data["value"], + ui=data.get("ui"), ) def to_dict(self) -> Dict: """Convert current metadata to json dict.""" - dict_value = model_to_dict(self, exclude={"options", "alias"}) + dict_value = model_to_dict(self, exclude={"options", "alias", "ui"}) if not self.options: dict_value["options"] = None elif isinstance(self.options, BaseDynamicOptions): @@ -468,6 +481,9 @@ def to_dict(self) -> Dict: dict_value["options"] = [value.to_dict() for value in values] else: dict_value["options"] = [value.to_dict() for value in self.options] + + if self.ui: + dict_value["ui"] = self.ui.to_dict() return dict_value def get_dict_options(self) -> Optional[List[Dict]]: diff --git a/dbgpt/core/awel/flow/ui.py b/dbgpt/core/awel/flow/ui.py index a9f220961..ca4361276 100644 --- a/dbgpt/core/awel/flow/ui.py +++ b/dbgpt/core/awel/flow/ui.py @@ -1,8 +1,9 @@ """UI components for AWEL flow.""" -from typing import Any, Dict, List, Literal, Optional +from typing import Any, Dict, List, Literal, Optional, Union -from dbgpt._private.pydantic import BaseModel, Field +from dbgpt._private.pydantic import BaseModel, Field, model_to_dict +from dbgpt.core.interface.serialization import Serializable from .exceptions import FlowUIComponentException @@ -36,37 +37,6 @@ class RefreshableMixin(BaseModel): ) -class UIComponent(RefreshableMixin, BaseModel): - """UI component.""" - - class UIRange(BaseModel): - """UI range.""" - - min: int | float | str | None = Field(None, description="Minimum value") - max: int | float | str | None = Field(None, description="Maximum value") - step: int | float | str | None = Field(None, description="Step value") - format: str | None = Field(None, description="Format") - - ui_type: _UI_TYPE = Field(..., description="UI component type") - - disabled: bool = Field( - False, - description="Whether the component is disabled", - ) - - def check_parameter(self, parameter_dict: Dict[str, Any]): - """Check parameter. - - Raises: - FlowUIParameterException: If the parameter is invalid. - """ - - def _check_options(self, options: Dict[str, Any]): - """Check options.""" - if not options: - raise FlowUIComponentException("options is required", self.ui_type) - - class StatusMixin(BaseModel): """Status mixin.""" @@ -76,40 +46,6 @@ class StatusMixin(BaseModel): ) -class RangeMixin(BaseModel): - """Range mixin.""" - - ui_range: Optional[UIComponent.UIRange] = Field( - None, - description="Range for the component", - ) - - -class InputMixin(BaseModel): - """Input mixin.""" - - class Count(BaseModel): - """Count.""" - - show: Optional[bool] = Field( - None, - description="Whether to show count", - ) - max: Optional[int] = Field( - None, - description="The maximum count", - ) - exceed_strategy: Optional[Literal["cut", "warning"]] = Field( - None, - description="The strategy when the count exceeds", - ) - - count: Optional[Count] = Field( - None, - description="Count configuration", - ) - - class PanelEditorMixin(BaseModel): """Edit the content in the panel.""" @@ -126,19 +62,62 @@ class Editor(BaseModel): ) editor: Optional[Editor] = Field( - None, + default_factory=lambda: PanelEditorMixin.Editor(width=800, height=400), description="The editor configuration", ) -class UICascader(StatusMixin, UIComponent): +class UIComponent(RefreshableMixin, Serializable, BaseModel): + """UI component.""" + + class UIAttribute(StatusMixin, BaseModel): + """Base UI attribute.""" + + disabled: bool = Field( + False, + description="Whether the component is disabled", + ) + + ui_type: _UI_TYPE = Field(..., description="UI component type") + + attr: Optional[UIAttribute] = Field( + None, + description="The attributes of the component", + ) + + def check_parameter(self, parameter_dict: Dict[str, Any]): + """Check parameter. + + Raises: + FlowUIParameterException: If the parameter is invalid. + """ + + def _check_options(self, options: Dict[str, Any]): + """Check options.""" + if not options: + raise FlowUIComponentException("options is required", self.ui_type) + + def to_dict(self) -> Dict: + """Convert current metadata to json dict.""" + return model_to_dict(self) + + +class UICascader(UIComponent): """Cascader component.""" + class UIAttribute(UIComponent.UIAttribute): + """Cascader attribute.""" + + show_search: bool = Field( + False, + description="Whether to show search input", + ) + ui_type: Literal["cascader"] = Field("cascader", frozen=True) - show_search: bool = Field( - False, - description="Whether to show search input", + attr: Optional[UIAttribute] = Field( + None, + description="The attributes of the component", ) def check_parameter(self, parameter_dict: Dict[str, Any]): @@ -163,53 +142,81 @@ def check_parameter(self, parameter_dict: Dict[str, Any]): self._check_options(parameter_dict.get("options", {})) -class UIDatePicker(StatusMixin, RangeMixin, UIComponent): +class UIDatePicker(UIComponent): """Date picker component.""" + class UIAttribute(UIComponent.UIAttribute): + """Date picker attribute.""" + + placement: Optional[ + Literal["topLeft", "topRight", "bottomLeft", "bottomRight"] + ] = Field( + None, + description="The position of the picker panel, None means bottomLeft", + ) + ui_type: Literal["date_picker"] = Field("date_picker", frozen=True) - placement: Optional[ - Literal["topLeft", "topRight", "bottomLeft", "bottomRight"] - ] = Field( + attr: Optional[UIAttribute] = Field( None, - description="The position of the picker panel, None means bottomLeft", + description="The attributes of the component", ) -class UIInput(StatusMixin, InputMixin, UIComponent): +class UIInput(UIComponent): """Input component.""" + class UIAttribute(UIComponent.UIAttribute): + """Input attribute.""" + + prefix: Optional[str] = Field( + None, + description="The prefix, icon or text", + examples=["$", "icon:UserOutlined"], + ) + suffix: Optional[str] = Field( + None, + description="The suffix, icon or text", + examples=["$", "icon:SearchOutlined"], + ) + show_count: Optional[bool] = Field( + None, + description="Whether to show count", + ) + maxlength: Optional[int] = Field( + None, + description="The maximum length of the input", + ) + ui_type: Literal["input"] = Field("input", frozen=True) - prefix: Optional[str] = Field( + attr: Optional[UIAttribute] = Field( None, - description="The prefix, icon or text", - examples=["$", "icon:UserOutlined"], - ) - suffix: Optional[str] = Field( - None, - description="The suffix, icon or text", - examples=["$", "icon:SearchOutlined"], + description="The attributes of the component", ) class UITextArea(PanelEditorMixin, UIInput): """Text area component.""" + class AutoSize(BaseModel): + """Auto size configuration.""" + + min_rows: Optional[int] = Field( + None, + description="The minimum number of rows", + ) + max_rows: Optional[int] = Field( + None, + description="The maximum number of rows", + ) + ui_type: Literal["text_area"] = Field("text_area", frozen=True) # type: ignore - auto_size: Optional[bool] = Field( + autosize: Optional[Union[bool, AutoSize]] = Field( None, description="Whether the height of the textarea automatically adjusts based " "on the content", ) - min_rows: Optional[int] = Field( - None, - description="The minimum number of rows", - ) - max_rows: Optional[int] = Field( - None, - description="The maximum number of rows", - ) class UIAutoComplete(UIInput): @@ -220,44 +227,73 @@ class UIAutoComplete(UIInput): ) -class UISlider(RangeMixin, UIComponent): +class UISlider(UIComponent): """Slider component.""" + class UIAttribute(UIComponent.UIAttribute): + """Slider attribute.""" + + min: Optional[int | float] = Field( + None, + description="The minimum value", + ) + max: Optional[int | float] = Field( + None, + description="The maximum value", + ) + step: Optional[int | float] = Field( + None, + description="The step of the slider", + ) + ui_type: Literal["slider"] = Field("slider", frozen=True) show_input: bool = Field( False, description="Whether to display the value in a input component" ) + attr: Optional[UIAttribute] = Field( + None, + description="The attributes of the component", + ) + -class UITimePicker(StatusMixin, UIComponent): +class UITimePicker(UIComponent): """Time picker component.""" + class UIAttribute(UIComponent.UIAttribute): + """Time picker attribute.""" + + format: Optional[str] = Field( + None, + description="The format of the time", + examples=["HH:mm:ss", "HH:mm"], + ) + hour_step: Optional[int] = Field( + None, + description="The step of the hour input", + ) + minute_step: Optional[int] = Field( + None, + description="The step of the minute input", + ) + second_step: Optional[int] = Field( + None, + description="The step of the second input", + ) + ui_type: Literal["time_picker"] = Field("time_picker", frozen=True) - format: Optional[str] = Field( - None, - description="The format of the time", - examples=["HH:mm:ss", "HH:mm"], - ) - hour_step: Optional[int] = Field( + attr: Optional[UIAttribute] = Field( None, - description="The step of the hour input", - ) - minute_step: Optional[int] = Field( - None, - description="The step of the minute input", - ) - second_step: Optional[int] = Field( - None, - description="The step of the second input", + description="The attributes of the component", ) -class UITreeSelect(StatusMixin, UIComponent): +class UITreeSelect(UICascader): """Tree select component.""" - ui_type: Literal["tree_select"] = Field("tree_select", frozen=True) + ui_type: Literal["tree_select"] = Field("tree_select", frozen=True) # type: ignore def check_parameter(self, parameter_dict: Dict[str, Any]): """Check parameter.""" @@ -271,19 +307,24 @@ def check_parameter(self, parameter_dict: Dict[str, Any]): ) -class UIUpload(StatusMixin, UIComponent): +class UIUpload(UIComponent): """Upload component.""" + class UIAttribute(UIComponent.UIAttribute): + """Upload attribute.""" + + max_count: Optional[int] = Field( + None, + description="The maximum number of files that can be uploaded", + ) + ui_type: Literal["upload"] = Field("upload", frozen=True) max_file_size: Optional[int] = Field( None, description="The maximum size of the file, in bytes", ) - max_count: Optional[int] = Field( - None, - description="The maximum number of files that can be uploaded", - ) + file_types: Optional[List[str]] = Field( None, description="The file types that can be accepted", @@ -346,3 +387,13 @@ class UICodeEditor(UITextArea): "python", description="The language of the code", ) + + +class DefaultUITextArea(UITextArea): + """Default text area component.""" + + autosize: Union[bool, UITextArea.AutoSize] = Field( + default_factory=lambda: UITextArea.AutoSize(min_rows=2, max_rows=40), + description="Whether the height of the textarea automatically adjusts based " + "on the content", + ) diff --git a/dbgpt/core/awel/util/parameter_util.py b/dbgpt/core/awel/util/parameter_util.py index defd99a3b..70015c9ba 100644 --- a/dbgpt/core/awel/util/parameter_util.py +++ b/dbgpt/core/awel/util/parameter_util.py @@ -2,7 +2,7 @@ import inspect from abc import ABC, abstractmethod -from typing import Any, Callable, Dict, List +from typing import Any, Callable, Dict, List, Optional from dbgpt._private.pydantic import BaseModel, Field, model_validator from dbgpt.core.interface.serialization import Serializable @@ -16,6 +16,9 @@ class OptionValue(Serializable, BaseModel): label: str = Field(..., description="The label of the option") name: str = Field(..., description="The name of the option") value: Any = Field(..., description="The value of the option") + children: Optional[List["OptionValue"]] = Field( + None, description="The children of the option" + ) def to_dict(self) -> Dict: """Convert current metadata to json dict.""" diff --git a/dbgpt/core/interface/operators/llm_operator.py b/dbgpt/core/interface/operators/llm_operator.py index 53e34ffe5..45863d0a9 100644 --- a/dbgpt/core/interface/operators/llm_operator.py +++ b/dbgpt/core/interface/operators/llm_operator.py @@ -24,6 +24,7 @@ OperatorType, Parameter, ViewMetadata, + ui, ) from dbgpt.core.interface.llm import ( LLMClient, @@ -69,6 +70,10 @@ class RequestBuilderOperator(MapOperator[RequestInput, ModelRequest]): optional=True, default=None, description=_("The temperature of the model request."), + ui=ui.UISlider( + show_input=True, + attr=ui.UISlider.UIAttribute(min=0.0, max=2.0, step=0.1), + ), ), Parameter.build_from( _("Max New Tokens"), diff --git a/dbgpt/core/interface/operators/prompt_operator.py b/dbgpt/core/interface/operators/prompt_operator.py index c3765aa67..7d97230ac 100644 --- a/dbgpt/core/interface/operators/prompt_operator.py +++ b/dbgpt/core/interface/operators/prompt_operator.py @@ -1,4 +1,5 @@ """The prompt operator.""" + from abc import ABC from typing import Any, Dict, List, Optional, Union @@ -18,6 +19,7 @@ ResourceCategory, ViewMetadata, register_resource, + ui, ) from dbgpt.core.interface.message import BaseMessage from dbgpt.core.interface.operators.llm_operator import BaseLLM @@ -48,6 +50,7 @@ optional=True, default="You are a helpful AI Assistant.", description=_("The system message."), + ui=ui.DefaultUITextArea(), ), Parameter.build_from( label=_("Message placeholder"), @@ -65,6 +68,7 @@ default="{user_input}", placeholder="{user_input}", description=_("The human message."), + ui=ui.DefaultUITextArea(), ), ], ) diff --git a/dbgpt/serve/flow/api/endpoints.py b/dbgpt/serve/flow/api/endpoints.py index 6cb5ef879..98ff81d2f 100644 --- a/dbgpt/serve/flow/api/endpoints.py +++ b/dbgpt/serve/flow/api/endpoints.py @@ -209,7 +209,7 @@ async def query_page( @router.get("/nodes", dependencies=[Depends(check_api_key)]) -async def get_nodes() -> Result[List[Union[ViewMetadata, ResourceMetadata]]]: +async def get_nodes(): """Get the operator or resource nodes Returns: @@ -218,7 +218,8 @@ async def get_nodes() -> Result[List[Union[ViewMetadata, ResourceMetadata]]]: """ from dbgpt.core.awel.flow.base import _OPERATOR_REGISTRY - return Result.succ(_OPERATOR_REGISTRY.metadata_list()) + metadata_list = _OPERATOR_REGISTRY.metadata_list() + return Result.succ(metadata_list) def init_endpoints(system_app: SystemApp) -> None: diff --git a/examples/awel/awel_flow_ui_components.py b/examples/awel/awel_flow_ui_components.py new file mode 100644 index 000000000..2af3e2bf3 --- /dev/null +++ b/examples/awel/awel_flow_ui_components.py @@ -0,0 +1,583 @@ +"""Some UI components for the AWEL flow.""" + +import logging +from typing import List, Optional + +from dbgpt.core.awel import MapOperator +from dbgpt.core.awel.flow import ( + IOField, + OperatorCategory, + OptionValue, + Parameter, + ViewMetadata, + ui, +) + +logger = logging.getLogger(__name__) + + +class ExampleFlowCascaderOperator(MapOperator[str, str]): + """An example flow operator that includes a cascader as parameter.""" + + metadata = ViewMetadata( + label="Example Flow Cascader", + name="example_flow_cascader", + category=OperatorCategory.EXAMPLE, + description="An example flow operator that includes a cascader as parameter.", + parameters=[ + Parameter.build_from( + "Address Selector", + "address", + type=str, + is_list=True, + optional=True, + default=None, + placeholder="Select the address", + description="The address of the location.", + options=[ + OptionValue( + label="Zhejiang", + name="zhejiang", + value="zhejiang", + children=[ + OptionValue( + label="Hangzhou", + name="hangzhou", + value="hangzhou", + children=[ + OptionValue( + label="Xihu", + name="xihu", + value="xihu", + ), + OptionValue( + label="Feilaifeng", + name="feilaifeng", + value="feilaifeng", + ), + ], + ), + ], + ), + OptionValue( + label="Jiangsu", + name="jiangsu", + value="jiangsu", + children=[ + OptionValue( + label="Nanjing", + name="nanjing", + value="nanjing", + children=[ + OptionValue( + label="Zhonghua Gate", + name="zhonghuamen", + value="zhonghuamen", + ), + OptionValue( + label="Zhongshanling", + name="zhongshanling", + value="zhongshanling", + ), + ], + ), + ], + ), + ], + ui=ui.UICascader(attr=ui.UICascader.UIAttribute(show_search=True)), + ) + ], + inputs=[ + IOField.build_from( + "User Name", + "user_name", + str, + description="The name of the user.", + ) + ], + outputs=[ + IOField.build_from( + "Address", + "address", + str, + description="User's address.", + ) + ], + ) + + def __int__(self, address: Optional[List[str]] = None, **kwargs): + super().__init__(**kwargs) + self.address = address or [] + + async def map(self, user_name: str) -> str: + """Map the user name to the address.""" + full_address_str = " ".join(self.address) + return "Your name is %s, and your address is %s." % ( + user_name, + full_address_str, + ) + + +class ExampleFlowCheckboxOperator(MapOperator[str, str]): + """An example flow operator that includes a checkbox as parameter.""" + + metadata = ViewMetadata( + label="Example Flow Checkbox", + name="example_flow_checkbox", + category=OperatorCategory.EXAMPLE, + description="An example flow operator that includes a checkbox as parameter.", + parameters=[ + Parameter.build_from( + "Fruits Selector", + "fruits", + type=str, + is_list=True, + optional=True, + default=None, + placeholder="Select the fruits", + description="The fruits you like.", + options=[ + OptionValue(label="Apple", name="apple", value="apple"), + OptionValue(label="Banana", name="banana", value="banana"), + OptionValue(label="Orange", name="orange", value="orange"), + OptionValue(label="Pear", name="pear", value="pear"), + ], + ui=ui.UICheckbox(attr=ui.UICheckbox.UIAttribute(show_search=True)), + ) + ], + inputs=[ + IOField.build_from( + "User Name", + "user_name", + str, + description="The name of the user.", + ) + ], + outputs=[ + IOField.build_from( + "Fruits", + "fruits", + str, + description="User's favorite fruits.", + ) + ], + ) + + def __init__(self, fruits: Optional[List[str]] = None, **kwargs): + super().__init__(**kwargs) + self.fruits = fruits or [] + + async def map(self, user_name: str) -> str: + """Map the user name to the fruits.""" + return "Your name is %s, and you like %s." % (user_name, ", ".join(self.fruits)) + + +class ExampleFlowDatePickerOperator(MapOperator[str, str]): + """An example flow operator that includes a date picker as parameter.""" + + metadata = ViewMetadata( + label="Example Flow Date Picker", + name="example_flow_date_picker", + category=OperatorCategory.EXAMPLE, + description="An example flow operator that includes a date picker as parameter.", + parameters=[ + Parameter.build_from( + "Date Selector", + "date", + type=str, + placeholder="Select the date", + description="The date you choose.", + ui=ui.UIDatePicker( + attr=ui.UIDatePicker.UIAttribute(placement="bottomLeft") + ), + ) + ], + inputs=[ + IOField.build_from( + "User Name", + "user_name", + str, + description="The name of the user.", + ) + ], + outputs=[ + IOField.build_from( + "Date", + "date", + str, + description="User's selected date.", + ) + ], + ) + + def __init__(self, date: str, **kwargs): + super().__init__(**kwargs) + self.date = date + + async def map(self, user_name: str) -> str: + """Map the user name to the date.""" + return "Your name is %s, and you choose the date %s." % (user_name, self.date) + + +class ExampleFlowInputOperator(MapOperator[str, str]): + """An example flow operator that includes an input as parameter.""" + + metadata = ViewMetadata( + label="Example Flow Input", + name="example_flow_input", + category=OperatorCategory.EXAMPLE, + description="An example flow operator that includes a input as parameter.", + parameters=[ + Parameter.build_from( + "Your hobby", + "hobby", + type=str, + placeholder="Please input your hobby", + description="The hobby you like.", + ui=ui.UIInput( + attr=ui.UIInput.UIAttribute( + prefix="icon:UserOutlined", show_count=True, maxlength=200 + ) + ), + ) + ], + inputs=[ + IOField.build_from( + "User Name", + "user_name", + str, + description="The name of the user.", + ) + ], + outputs=[ + IOField.build_from( + "User Hobby", + "hobby", + str, + description="User's hobby.", + ) + ], + ) + + def __init__(self, hobby: str, **kwargs): + super().__init__(**kwargs) + self.hobby = hobby + + async def map(self, user_name: str) -> str: + """Map the user name to the input.""" + return "Your name is %s, and your hobby is %s." % (user_name, self.hobby) + + +class ExampleFlowTextAreaOperator(MapOperator[str, str]): + """An example flow operator that includes a text area as parameter.""" + + metadata = ViewMetadata( + label="Example Flow Text Area", + name="example_flow_text_area", + category=OperatorCategory.EXAMPLE, + description="An example flow operator that includes a text area as parameter.", + parameters=[ + Parameter.build_from( + "Your comment", + "comment", + type=str, + placeholder="Please input your comment", + description="The comment you want to say.", + ui=ui.UITextArea( + attr=ui.UITextArea.UIAttribute(show_count=True, maxlength=1000), + autosize=ui.UITextArea.AutoSize(min_rows=2, max_rows=6), + ), + ) + ], + inputs=[ + IOField.build_from( + "User Name", + "user_name", + str, + description="The name of the user.", + ) + ], + outputs=[ + IOField.build_from( + "User Comment", + "comment", + str, + description="User's comment.", + ) + ], + ) + + def __init__(self, comment: str, **kwargs): + super().__init__(**kwargs) + self.comment = comment + + async def map(self, user_name: str) -> str: + """Map the user name to the text area.""" + return "Your name is %s, and your comment is %s." % (user_name, self.comment) + + +class ExampleFlowSliderOperator(MapOperator[float, float]): + + metadata = ViewMetadata( + label="Example Flow Slider", + name="example_flow_slider", + category=OperatorCategory.EXAMPLE, + description="An example flow operator that includes a slider as parameter.", + parameters=[ + Parameter.build_from( + "Default Temperature", + "default_temperature", + type=float, + optional=True, + default=0.7, + placeholder="Set the default temperature, e.g., 0.7", + description="The default temperature to pass to the LLM.", + ui=ui.UISlider( + show_input=True, + attr=ui.UISlider.UIAttribute(min=0.0, max=2.0, step=0.1), + ), + ) + ], + inputs=[ + IOField.build_from( + "Temperature", + "temperature", + float, + description="The temperature.", + ) + ], + outputs=[ + IOField.build_from( + "Temperature", + "temperature", + float, + description="The temperature to pass to the LLM.", + ) + ], + ) + + def __init__(self, default_temperature: float = 0.7, **kwargs): + super().__init__(**kwargs) + self.default_temperature = default_temperature + + async def map(self, temperature: float) -> float: + """Map the temperature to the result.""" + if temperature < 0.0 or temperature > 2.0: + logger.warning("Temperature out of range: %s", temperature) + return self.default_temperature + else: + return temperature + + +class ExampleFlowSliderListOperator(MapOperator[float, float]): + """An example flow operator that includes a slider list as parameter.""" + + metadata = ViewMetadata( + label="Example Flow Slider List", + name="example_flow_slider_list", + category=OperatorCategory.EXAMPLE, + description="An example flow operator that includes a slider list as parameter.", + parameters=[ + Parameter.build_from( + "Temperature Selector", + "temperature_range", + type=float, + is_list=True, + optional=True, + default=None, + placeholder="Set the temperature, e.g., [0.1, 0.9]", + description="The temperature range to pass to the LLM.", + ui=ui.UISlider( + show_input=True, + attr=ui.UISlider.UIAttribute(min=0.0, max=2.0, step=0.1), + ), + ) + ], + inputs=[ + IOField.build_from( + "Temperature", + "temperature", + float, + description="The temperature.", + ) + ], + outputs=[ + IOField.build_from( + "Temperature", + "temperature", + float, + description="The temperature to pass to the LLM.", + ) + ], + ) + + def __init__(self, temperature_range: Optional[List[float]] = None, **kwargs): + super().__init__(**kwargs) + temperature_range = temperature_range or [0.1, 0.9] + if temperature_range and len(temperature_range) != 2: + raise ValueError("The length of temperature range must be 2.") + self.temperature_range = temperature_range + + async def map(self, temperature: float) -> float: + """Map the temperature to the result.""" + min_temperature, max_temperature = self.temperature_range + if temperature < min_temperature or temperature > max_temperature: + logger.warning( + "Temperature out of range: %s, min: %s, max: %s", + temperature, + min_temperature, + max_temperature, + ) + return min_temperature + return temperature + + +class ExampleFlowTimePickerOperator(MapOperator[str, str]): + """An example flow operator that includes a time picker as parameter.""" + + metadata = ViewMetadata( + label="Example Flow Time Picker", + name="example_flow_time_picker", + category=OperatorCategory.EXAMPLE, + description="An example flow operator that includes a time picker as parameter.", + parameters=[ + Parameter.build_from( + "Time Selector", + "time", + type=str, + placeholder="Select the time", + description="The time you choose.", + ui=ui.UITimePicker( + attr=ui.UITimePicker.UIAttribute( + format="HH:mm:ss", hour_step=2, minute_step=10, second_step=10 + ), + ), + ) + ], + inputs=[ + IOField.build_from( + "User Name", + "user_name", + str, + description="The name of the user.", + ) + ], + outputs=[ + IOField.build_from( + "Time", + "time", + str, + description="User's selected time.", + ) + ], + ) + + def __init__(self, time: str, **kwargs): + super().__init__(**kwargs) + self.time = time + + async def map(self, user_name: str) -> str: + """Map the user name to the time.""" + return "Your name is %s, and you choose the time %s." % (user_name, self.time) + + +class ExampleFlowTreeSelectOperator(MapOperator[str, str]): + """An example flow operator that includes a tree select as parameter.""" + + metadata = ViewMetadata( + label="Example Flow Tree Select", + name="example_flow_tree_select", + category=OperatorCategory.EXAMPLE, + description="An example flow operator that includes a tree select as parameter.", + parameters=[ + Parameter.build_from( + "Address Selector", + "address", + type=str, + is_list=True, + optional=True, + default=None, + placeholder="Select the address", + description="The address of the location.", + options=[ + OptionValue( + label="Zhejiang", + name="zhejiang", + value="zhejiang", + children=[ + OptionValue( + label="Hangzhou", + name="hangzhou", + value="hangzhou", + children=[ + OptionValue( + label="Xihu", + name="xihu", + value="xihu", + ), + OptionValue( + label="Feilaifeng", + name="feilaifeng", + value="feilaifeng", + ), + ], + ), + ], + ), + OptionValue( + label="Jiangsu", + name="jiangsu", + value="jiangsu", + children=[ + OptionValue( + label="Nanjing", + name="nanjing", + value="nanjing", + children=[ + OptionValue( + label="Zhonghua Gate", + name="zhonghuamen", + value="zhonghuamen", + ), + OptionValue( + label="Zhongshanling", + name="zhongshanling", + value="zhongshanling", + ), + ], + ), + ], + ), + ], + ui=ui.UITreeSelect(attr=ui.UITreeSelect.UIAttribute(show_search=True)), + ) + ], + inputs=[ + IOField.build_from( + "User Name", + "user_name", + str, + description="The name of the user.", + ) + ], + outputs=[ + IOField.build_from( + "Address", + "address", + str, + description="User's address.", + ) + ], + ) + + def __int__(self, address: Optional[List[str]] = None, **kwargs): + super().__init__(**kwargs) + self.address = address or [] + + async def map(self, user_name: str) -> str: + """Map the user name to the address.""" + full_address_str = " ".join(self.address) + return "Your name is %s, and your address is %s." % ( + user_name, + full_address_str, + ) From db44f2b3a2d83dc7a56eb68c04902f82ad1588e2 Mon Sep 17 00:00:00 2001 From: Fangyin Cheng Date: Tue, 6 Aug 2024 10:17:58 +0800 Subject: [PATCH 03/89] feat(core): Support refresh for AWEL flow --- dbgpt/core/awel/flow/base.py | 52 ++++++++- dbgpt/core/awel/flow/ui.py | 33 ++++++ dbgpt/core/awel/util/parameter_util.py | 38 ++++++- dbgpt/serve/flow/api/endpoints.py | 19 +++- dbgpt/serve/flow/api/schemas.py | 40 ++++++- examples/awel/awel_flow_ui_components.py | 136 +++++++++++++++++++++++ 6 files changed, 307 insertions(+), 11 deletions(-) diff --git a/dbgpt/core/awel/flow/base.py b/dbgpt/core/awel/flow/base.py index da0b2c378..846b18baf 100644 --- a/dbgpt/core/awel/flow/base.py +++ b/dbgpt/core/awel/flow/base.py @@ -15,7 +15,11 @@ model_to_dict, model_validator, ) -from dbgpt.core.awel.util.parameter_util import BaseDynamicOptions, OptionValue +from dbgpt.core.awel.util.parameter_util import ( + BaseDynamicOptions, + OptionValue, + RefreshOptionRequest, +) from dbgpt.core.interface.serialization import Serializable from .exceptions import FlowMetadataException, FlowParameterMetadataException @@ -486,6 +490,25 @@ def to_dict(self) -> Dict: dict_value["ui"] = self.ui.to_dict() return dict_value + def refresh(self, request: Optional[RefreshOptionRequest] = None) -> Dict: + """Refresh the options of the parameter. + + Args: + request (RefreshOptionRequest): The request to refresh the options. + + Returns: + Dict: The response. + """ + dict_value = self.to_dict() + if not self.options: + dict_value["options"] = None + elif isinstance(self.options, BaseDynamicOptions): + values = self.options.refresh(request) + dict_value["options"] = [value.to_dict() for value in values] + else: + dict_value["options"] = [value.to_dict() for value in self.options] + return dict_value + def get_dict_options(self) -> Optional[List[Dict]]: """Get the options of the parameter.""" if not self.options: @@ -655,10 +678,10 @@ class BaseMetadata(BaseResource): ], ) - tags: Optional[List[str]] = Field( + tags: Optional[Dict[str, str]] = Field( default=None, description="The tags of the operator", - examples=[["llm", "openai", "gpt3"]], + examples=[{"order": "higher-order"}, {"order": "first-order"}], ) parameters: List[Parameter] = Field( @@ -768,6 +791,20 @@ def to_dict(self) -> Dict: ] return dict_value + def refresh(self, request: List[RefreshOptionRequest]) -> Dict: + """Refresh the metadata.""" + name_to_request = {req.name: req for req in request} + parameter_requests = { + parameter.name: name_to_request.get(parameter.name) + for parameter in self.parameters + } + dict_value = self.to_dict() + dict_value["parameters"] = [ + parameter.refresh(parameter_requests.get(parameter.name)) + for parameter in self.parameters + ] + return dict_value + class ResourceMetadata(BaseMetadata, TypeMetadata): """The metadata of the resource.""" @@ -1051,6 +1088,15 @@ def metadata_list(self): """Get the metadata list.""" return [item.metadata.to_dict() for item in self._registry.values()] + def refresh( + self, key: str, is_operator: bool, request: List[RefreshOptionRequest] + ) -> Dict: + """Refresh the metadata.""" + if is_operator: + return _get_operator_class(key).metadata.refresh(request) # type: ignore + else: + return _get_resource_class(key).metadata.refresh(request) + _OPERATOR_REGISTRY: FlowRegistry = FlowRegistry() diff --git a/dbgpt/core/awel/flow/ui.py b/dbgpt/core/awel/flow/ui.py index ca4361276..91008269e 100644 --- a/dbgpt/core/awel/flow/ui.py +++ b/dbgpt/core/awel/flow/ui.py @@ -8,6 +8,7 @@ from .exceptions import FlowUIComponentException _UI_TYPE = Literal[ + "select", "cascader", "checkbox", "date_picker", @@ -102,6 +103,38 @@ def to_dict(self) -> Dict: return model_to_dict(self) +class UISelect(UIComponent): + """Select component.""" + + class UIAttribute(UIComponent.UIAttribute): + """Select attribute.""" + + show_search: bool = Field( + False, + description="Whether to show search input", + ) + mode: Optional[Literal["tags"]] = Field( + None, + description="The mode of the select", + ) + placement: Optional[ + Literal["topLeft", "topRight", "bottomLeft", "bottomRight"] + ] = Field( + None, + description="The position of the picker panel, None means bottomLeft", + ) + + ui_type: Literal["select"] = Field("select", frozen=True) + attr: Optional[UIAttribute] = Field( + None, + description="The attributes of the component", + ) + + def check_parameter(self, parameter_dict: Dict[str, Any]): + """Check parameter.""" + self._check_options(parameter_dict.get("options", {})) + + class UICascader(UIComponent): """Cascader component.""" diff --git a/dbgpt/core/awel/util/parameter_util.py b/dbgpt/core/awel/util/parameter_util.py index 70015c9ba..2393aed89 100644 --- a/dbgpt/core/awel/util/parameter_util.py +++ b/dbgpt/core/awel/util/parameter_util.py @@ -10,6 +10,27 @@ _DEFAULT_DYNAMIC_REGISTRY = {} +class RefreshOptionDependency(BaseModel): + """The refresh dependency.""" + + name: str = Field(..., description="The name of the refresh dependency") + value: Optional[Any] = Field( + None, description="The value of the refresh dependency" + ) + has_value: bool = Field( + False, description="Whether the refresh dependency has value" + ) + + +class RefreshOptionRequest(BaseModel): + """The refresh option request.""" + + name: str = Field(..., description="The name of parameter to refresh") + depends: Optional[List[RefreshOptionDependency]] = Field( + None, description="The depends of the refresh config" + ) + + class OptionValue(Serializable, BaseModel): """The option value of the parameter.""" @@ -28,24 +49,31 @@ def to_dict(self) -> Dict: class BaseDynamicOptions(Serializable, BaseModel, ABC): """The base dynamic options.""" - @abstractmethod def option_values(self) -> List[OptionValue]: """Return the option values of the parameter.""" + return self.refresh(None) + + @abstractmethod + def refresh(self, request: Optional[RefreshOptionRequest]) -> List[OptionValue]: + """Refresh the dynamic options.""" class FunctionDynamicOptions(BaseDynamicOptions): """The function dynamic options.""" - func: Callable[[], List[OptionValue]] = Field( + func: Callable[..., List[OptionValue]] = Field( ..., description="The function to generate the dynamic options" ) func_id: str = Field( ..., description="The unique id of the function to generate the dynamic options" ) - def option_values(self) -> List[OptionValue]: - """Return the option values of the parameter.""" - return self.func() + def refresh(self, request: Optional[RefreshOptionRequest]) -> List[OptionValue]: + """Refresh the dynamic options.""" + if not request or not request.depends: + return self.func() + kwargs = {dep.name: dep.value for dep in request.depends if dep.has_value} + return self.func(**kwargs) @model_validator(mode="before") @classmethod diff --git a/dbgpt/serve/flow/api/endpoints.py b/dbgpt/serve/flow/api/endpoints.py index 98ff81d2f..99852271a 100644 --- a/dbgpt/serve/flow/api/endpoints.py +++ b/dbgpt/serve/flow/api/endpoints.py @@ -11,7 +11,7 @@ from ..config import APP_NAME, SERVE_SERVICE_COMPONENT_NAME, ServeConfig from ..service.service import Service -from .schemas import ServeRequest, ServerResponse +from .schemas import RefreshNodeRequest, ServeRequest, ServerResponse router = APIRouter() @@ -222,6 +222,23 @@ async def get_nodes(): return Result.succ(metadata_list) +@router.post("/nodes/refresh", dependencies=[Depends(check_api_key)]) +async def refresh_nodes(refresh_request: RefreshNodeRequest): + """Refresh the operator or resource nodes + + Returns: + Result[None]: The response + """ + from dbgpt.core.awel.flow.base import _OPERATOR_REGISTRY + + new_metadata = _OPERATOR_REGISTRY.refresh( + key=refresh_request.id, + is_operator=refresh_request.flow_type == "operator", + request=refresh_request.refresh, + ) + return Result.succ(new_metadata) + + def init_endpoints(system_app: SystemApp) -> None: """Initialize the endpoints""" global global_system_app diff --git a/dbgpt/serve/flow/api/schemas.py b/dbgpt/serve/flow/api/schemas.py index 6fb8c1924..2daa8f581 100644 --- a/dbgpt/serve/flow/api/schemas.py +++ b/dbgpt/serve/flow/api/schemas.py @@ -1,7 +1,8 @@ -from dbgpt._private.pydantic import ConfigDict +from typing import List, Literal -# Define your Pydantic schemas here +from dbgpt._private.pydantic import BaseModel, ConfigDict, Field from dbgpt.core.awel.flow.flow_factory import FlowPanel +from dbgpt.core.awel.util.parameter_util import RefreshOptionRequest from ..config import SERVE_APP_NAME_HUMP @@ -14,3 +15,38 @@ class ServerResponse(FlowPanel): # TODO define your own fields here model_config = ConfigDict(title=f"ServerResponse for {SERVE_APP_NAME_HUMP}") + + +class RefreshNodeRequest(BaseModel): + """Flow response model""" + + model_config = ConfigDict(title=f"RefreshNodeRequest") + id: str = Field( + ..., + title="The id of the node", + description="The id of the node to refresh", + examples=["operator_llm_operator___$$___llm___$$___v1"], + ) + flow_type: Literal["operator", "resource"] = Field( + "operator", + title="The type of the node", + description="The type of the node to refresh", + examples=["operator", "resource"], + ) + type_name: str = Field( + ..., + title="The type of the node", + description="The type of the node to refresh", + examples=["LLMOperator"], + ) + type_cls: str = Field( + ..., + title="The class of the node", + description="The class of the node to refresh", + examples=["dbgpt.core.operator.llm.LLMOperator"], + ) + refresh: List[RefreshOptionRequest] = Field( + ..., + title="The refresh options", + description="The refresh options", + ) diff --git a/examples/awel/awel_flow_ui_components.py b/examples/awel/awel_flow_ui_components.py index 2af3e2bf3..fc8d9a5c4 100644 --- a/examples/awel/awel_flow_ui_components.py +++ b/examples/awel/awel_flow_ui_components.py @@ -5,6 +5,7 @@ from dbgpt.core.awel import MapOperator from dbgpt.core.awel.flow import ( + FunctionDynamicOptions, IOField, OperatorCategory, OptionValue, @@ -16,6 +17,59 @@ logger = logging.getLogger(__name__) +class ExampleFlowSelectOperator(MapOperator[str, str]): + """An example flow operator that includes a select as parameter.""" + + metadata = ViewMetadata( + label="Example Flow Select", + name="example_flow_select", + category=OperatorCategory.EXAMPLE, + description="An example flow operator that includes a select as parameter.", + parameters=[ + Parameter.build_from( + "Fruits Selector", + "fruits", + type=str, + optional=True, + default=None, + placeholder="Select the fruits", + description="The fruits you like.", + options=[ + OptionValue(label="Apple", name="apple", value="apple"), + OptionValue(label="Banana", name="banana", value="banana"), + OptionValue(label="Orange", name="orange", value="orange"), + OptionValue(label="Pear", name="pear", value="pear"), + ], + ui=ui.UISelect(attr=ui.UISelect.UIAttribute(show_search=True)), + ) + ], + inputs=[ + IOField.build_from( + "User Name", + "user_name", + str, + description="The name of the user.", + ) + ], + outputs=[ + IOField.build_from( + "Fruits", + "fruits", + str, + description="User's favorite fruits.", + ) + ], + ) + + def __init__(self, fruits: Optional[str] = None, **kwargs): + super().__init__(**kwargs) + self.fruits = fruits + + async def map(self, user_name: str) -> str: + """Map the user name to the fruits.""" + return "Your name is %s, and you like %s." % (user_name, self.fruits) + + class ExampleFlowCascaderOperator(MapOperator[str, str]): """An example flow operator that includes a cascader as parameter.""" @@ -581,3 +635,85 @@ async def map(self, user_name: str) -> str: user_name, full_address_str, ) + + +def get_recent_3_times(time_interval: int = 1) -> List[OptionValue]: + """Get the recent times.""" + from datetime import datetime, timedelta + + now = datetime.now() + recent_times = [now - timedelta(hours=time_interval * i) for i in range(3)] + formatted_times = [time.strftime("%Y-%m-%d %H:%M:%S") for time in recent_times] + option_values = [ + OptionValue(label=formatted_time, name=f"time_{i + 1}", value=formatted_time) + for i, formatted_time in enumerate(formatted_times) + ] + + return option_values + + +class ExampleFlowRefreshOperator(MapOperator[str, str]): + """An example flow operator that includes a refresh option.""" + + metadata = ViewMetadata( + label="Example Refresh Operator", + name="example_refresh_operator", + category=OperatorCategory.EXAMPLE, + description="An example flow operator that includes a refresh option.", + parameters=[ + Parameter.build_from( + "Time Interval", + "time_interval", + type=int, + optional=True, + default=1, + placeholder="Set the time interval", + description="The time interval to fetch the times", + ), + Parameter.build_from( + "Recent Time", + "recent_time", + type=str, + optional=True, + default=None, + placeholder="Select the recent time", + description="The recent time to choose.", + options=FunctionDynamicOptions(func=get_recent_3_times), + ui=ui.UISelect( + refresh=True, + refresh_depends=["time_interval"], + attr=ui.UISelect.UIAttribute(show_search=True), + ), + ), + ], + inputs=[ + IOField.build_from( + "User Name", + "user_name", + str, + description="The name of the user.", + ) + ], + outputs=[ + IOField.build_from( + "Time", + "time", + str, + description="User's selected time.", + ) + ], + ) + + def __init__( + self, time_interval: int = 1, recent_time: Optional[str] = None, **kwargs + ): + super().__init__(**kwargs) + self.time_interval = time_interval + self.recent_time = recent_time + + async def map(self, user_name: str) -> str: + """Map the user name to the time.""" + return "Your name is %s, and you choose the time %s." % ( + user_name, + self.recent_time, + ) From e97b63a4d4f347fa34b8e791397018c336a4b597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A8=E6=AC=A3?= Date: Thu, 8 Aug 2024 19:27:52 +0800 Subject: [PATCH 04/89] =?UTF-8?q?feat:=20=E7=BC=96=E6=8E=92=E7=94=BB?= =?UTF-8?q?=E5=B8=83=E6=96=B0=E5=A2=9E=E5=9F=BA=E4=BA=8EAWEL2.0=E7=9A=84Se?= =?UTF-8?q?lect=E7=B1=BB=E5=9E=8B=E8=8A=82=E7=82=B9=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/components/flow/node-param-handler.tsx | 36 ++++++++++++++----- .../flow/node-renderer/cascader.tsx | 3 ++ web/components/flow/node-renderer/index.ts | 2 ++ web/components/flow/node-renderer/select.tsx | 21 +++++++++++ web/types/flow.ts | 9 +++++ 5 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 web/components/flow/node-renderer/cascader.tsx create mode 100644 web/components/flow/node-renderer/index.ts create mode 100644 web/components/flow/node-renderer/select.tsx diff --git a/web/components/flow/node-param-handler.tsx b/web/components/flow/node-param-handler.tsx index c617369cf..d86da6653 100644 --- a/web/components/flow/node-param-handler.tsx +++ b/web/components/flow/node-param-handler.tsx @@ -4,6 +4,7 @@ import React from 'react'; import RequiredIcon from './required-icon'; import NodeHandler from './node-handler'; import { InfoCircleOutlined } from '@ant-design/icons'; +import { RenderSelect, RenderCascader } from './node-renderer'; interface NodeParamHandlerProps { node: IFlowNode; @@ -14,14 +15,14 @@ interface NodeParamHandlerProps { // render node parameters item const NodeParamHandler: React.FC = ({ node, data, label, index }) => { - function handleChange(value: any) { + function onChange(value: any) { data.value = value; } - if (data.category === 'resource') { - return ; - } else if (data.category === 'common') { + // 基于AWEL1.0的流程设计器,对节点参数的渲染 + function renderNodeWithoutUiParam(data: IFlowNodeParameter) { let defaultValue = data.value !== null && data.value !== undefined ? data.value : data.default; + switch (data.type_name) { case 'int': case 'float': @@ -39,7 +40,7 @@ const NodeParamHandler: React.FC = ({ node, data, label, className="w-full" defaultValue={defaultValue} onChange={(value: number | null) => { - handleChange(value); + onChange(value); }} /> @@ -60,20 +61,20 @@ const NodeParamHandler: React.FC = ({ node, data, label, className="w-full nodrag" defaultValue={defaultValue} options={data.options.map((item: any) => ({ label: item.label, value: item.value }))} - onChange={handleChange} + onChange={onChange} /> ) : ( { - handleChange(e.target.value); + onChange(e.target.value); }} /> )} ); - case 'bool': + case 'checkbox': defaultValue = defaultValue === 'False' ? false : defaultValue; defaultValue = defaultValue === 'True' ? true : defaultValue; return ( @@ -89,7 +90,7 @@ const NodeParamHandler: React.FC = ({ node, data, label, className="ml-2" defaultChecked={defaultValue} onChange={(e) => { - handleChange(e.target.checked); + onChange(e.target.checked); }} />

@@ -97,6 +98,23 @@ const NodeParamHandler: React.FC = ({ node, data, label, ); } } + + // 基于AWEL2.0的流程设计器,对节点参数的渲染 + function renderNodeWithUiParam(data: IFlowNodeParameter) { + let defaultValue = data.value !== null && data.value !== undefined ? data.value : data.default; + + // TODO: 根据ui_type渲染不同的组件 + switch (data?.ui?.ui_type) { + case 'select': + return ; + } + } + + if (data.category === 'resource') { + return ; + } else if (data.category === 'common') { + return data?.ui ? renderNodeWithUiParam(data) : renderNodeWithoutUiParam(data); + } }; export default NodeParamHandler; diff --git a/web/components/flow/node-renderer/cascader.tsx b/web/components/flow/node-renderer/cascader.tsx new file mode 100644 index 000000000..605105a93 --- /dev/null +++ b/web/components/flow/node-renderer/cascader.tsx @@ -0,0 +1,3 @@ +import { IFlowNodeParameter } from '@/types/flow'; + +export const RenderCascader = (props: IFlowNodeParameter) => {}; diff --git a/web/components/flow/node-renderer/index.ts b/web/components/flow/node-renderer/index.ts new file mode 100644 index 000000000..d9d116497 --- /dev/null +++ b/web/components/flow/node-renderer/index.ts @@ -0,0 +1,2 @@ +export * from "./select"; +export * from "./cascader"; \ No newline at end of file diff --git a/web/components/flow/node-renderer/select.tsx b/web/components/flow/node-renderer/select.tsx new file mode 100644 index 000000000..897d18522 --- /dev/null +++ b/web/components/flow/node-renderer/select.tsx @@ -0,0 +1,21 @@ +import { IFlowNodeParameter } from "@/types/flow"; +import { Select } from "antd"; + +type SelectProps = { + data: IFlowNodeParameter; + defaultValue: any; + onChange: (value: any) => void; +} + +export const RenderSelect = (params: SelectProps) => { + const { data, defaultValue, onChange } = params; + + return data.options?.length > 0 && ( + { + onChange(e.target.value); + }} + /> + ); +}; diff --git a/web/components/flow/node-renderer/radio.tsx b/web/components/flow/node-renderer/radio.tsx new file mode 100644 index 000000000..4b5a1a194 --- /dev/null +++ b/web/components/flow/node-renderer/radio.tsx @@ -0,0 +1,25 @@ +import { IFlowNodeParameter } from '@/types/flow'; +import { convertKeysToCamelCase } from '@/utils/flow'; +import { Radio } from 'antd'; + +type Props = { + data: IFlowNodeParameter; + defaultValue: any; + onChange: (value: any) => void; +}; + +export const RenderRadio = (params: Props) => { + const { data, defaultValue, onChange } = params; + const attr = convertKeysToCamelCase(data.ui?.attr || {}); + + return ( + { + onChange(e.target.checked); + }} + defaultValue={defaultValue} + /> + ); +}; diff --git a/web/components/flow/node-renderer/select.tsx b/web/components/flow/node-renderer/select.tsx index 897d18522..0752d6959 100644 --- a/web/components/flow/node-renderer/select.tsx +++ b/web/components/flow/node-renderer/select.tsx @@ -1,21 +1,25 @@ -import { IFlowNodeParameter } from "@/types/flow"; -import { Select } from "antd"; +import { IFlowNodeParameter } from '@/types/flow'; +import { Select } from 'antd'; +import { convertKeysToCamelCase } from '@/utils/flow'; -type SelectProps = { +type Props = { data: IFlowNodeParameter; defaultValue: any; onChange: (value: any) => void; -} +}; -export const RenderSelect = (params: SelectProps) => { +export const RenderSelect = (params: Props) => { const { data, defaultValue, onChange } = params; + const attr = convertKeysToCamelCase(data.ui?.attr || {}); - return data.options?.length > 0 && ( + return (