Merge branch 'feat/sprint-web-flow' into feat-sprint-web-flow-2.0

This commit is contained in:
云锋
2024-08-16 15:59:38 +08:00
committed by GitHub
20 changed files with 239 additions and 149 deletions

View File

@@ -11,6 +11,7 @@ _UI_TYPE = Literal[
"select",
"cascader",
"checkbox",
"radio",
"date_picker",
"input",
"text_area",
@@ -175,6 +176,12 @@ class UICheckbox(UIComponent):
self._check_options(parameter_dict.get("options", {}))
class UIRadio(UICheckbox):
"""Radio component."""
ui_type: Literal["radio"] = Field("radio", frozen=True) # type: ignore
class UIDatePicker(UIComponent):
"""Date picker component."""
@@ -232,23 +239,31 @@ class UIInput(UIComponent):
class UITextArea(PanelEditorMixin, UIInput):
"""Text area component."""
class AutoSize(BaseModel):
"""Auto size configuration."""
class UIAttribute(UIInput.UIAttribute):
"""Text area attribute."""
min_rows: Optional[int] = Field(
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",
)
auto_size: Optional[Union[bool, AutoSize]] = Field(
None,
description="The minimum number of rows",
)
max_rows: Optional[int] = Field(
None,
description="The maximum number of rows",
description="Whether the height of the textarea automatically adjusts "
"based on the content",
)
ui_type: Literal["text_area"] = Field("text_area", frozen=True) # type: ignore
autosize: Optional[Union[bool, AutoSize]] = Field(
attr: Optional[UIAttribute] = Field(
None,
description="Whether the height of the textarea automatically adjusts based "
"on the content",
description="The attributes of the component",
)
@@ -430,8 +445,9 @@ class UICodeEditor(UITextArea):
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",
attr: Optional[UITextArea.UIAttribute] = Field(
default_factory=lambda: UITextArea.UIAttribute(
auto_size=UITextArea.UIAttribute.AutoSize(min_rows=2, max_rows=40)
),
description="The attributes of the component",
)

View File

@@ -1,20 +1,12 @@
import json
from functools import cache
from typing import Dict, List, Literal, Optional, Union
from typing import Dict, List, Optional, Union
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from fastapi.security.http import HTTPAuthorizationCredentials, HTTPBearer
from dbgpt.component import SystemApp
from dbgpt.core.awel.flow import ResourceMetadata, ViewMetadata
from dbgpt.core.interface.variables import (
BUILTIN_VARIABLES_CORE_FLOW_NODES,
BUILTIN_VARIABLES_CORE_FLOWS,
BUILTIN_VARIABLES_CORE_SECRETS,
BUILTIN_VARIABLES_CORE_VARIABLES,
BuiltinVariablesProvider,
StorageVariables,
)
from dbgpt.serve.core import Result, blocking_func_to_async
from dbgpt.util import PaginationResult
@@ -330,6 +322,12 @@ async def update_variables(
return Result.succ(res)
@router.post("/flow/debug")
async def debug():
"""Debug the flow."""
# TODO: Implement the debug endpoint
def init_endpoints(system_app: SystemApp) -> None:
"""Initialize the endpoints"""
from .variables_provider import (

View File

@@ -1,6 +1,7 @@
from typing import Any, List, Literal, Optional
from typing import Any, Dict, List, Literal, Optional, Union
from dbgpt._private.pydantic import BaseModel, ConfigDict, Field
from dbgpt.core.awel import CommonLLMHttpRequestBody
from dbgpt.core.awel.flow.flow_factory import FlowPanel
from dbgpt.core.awel.util.parameter_util import RefreshOptionRequest
@@ -113,3 +114,26 @@ class RefreshNodeRequest(BaseModel):
title="The refresh options",
description="The refresh options",
)
class FlowDebugRequest(BaseModel):
"""Flow response model"""
model_config = ConfigDict(title=f"FlowDebugRequest")
flow: ServeRequest = Field(
...,
title="The flow to debug",
description="The flow to debug",
)
request: Union[CommonLLMHttpRequestBody, Dict[str, Any]] = Field(
...,
title="The request to debug",
description="The request to debug",
)
variables: Optional[Dict[str, Any]] = Field(
None,
title="The variables to debug",
description="The variables to debug",
)
user_name: Optional[str] = Field(None, description="User name")
sys_code: Optional[str] = Field(None, description="System code")

View File

@@ -206,7 +206,7 @@ class ExampleFlowCheckboxOperator(MapOperator[str, str]):
OptionValue(label="Orange", name="orange", value="orange"),
OptionValue(label="Pear", name="pear", value="pear"),
],
ui=ui.UICheckbox(attr=ui.UICheckbox.UIAttribute(show_search=True)),
ui=ui.UICheckbox(),
)
],
inputs=[
@@ -236,6 +236,59 @@ class ExampleFlowCheckboxOperator(MapOperator[str, str]):
return "Your name is %s, and you like %s." % (user_name, ", ".join(self.fruits))
class ExampleFlowRadioOperator(MapOperator[str, str]):
"""An example flow operator that includes a radio as parameter."""
metadata = ViewMetadata(
label="Example Flow Radio",
name="example_flow_radio",
category=OperatorCategory.EXAMPLE,
description="An example flow operator that includes a radio 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.UIRadio(),
)
],
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 ExampleFlowDatePickerOperator(MapOperator[str, str]):
"""An example flow operator that includes a date picker as parameter."""
@@ -348,8 +401,13 @@ class ExampleFlowTextAreaOperator(MapOperator[str, 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),
attr=ui.UITextArea.UIAttribute(
show_count=True,
maxlength=1000,
auto_size=ui.UITextArea.UIAttribute.AutoSize(
min_rows=2, max_rows=6
),
),
),
)
],

View File

@@ -59,6 +59,7 @@ function GPTCard({
return icon;
}, [icon]);
// TODO: 算子资源标签
const tagNode = useMemo(() => {
if (!tags || !tags.length) return null;
return (

View File

@@ -18,7 +18,7 @@ type CanvasNodeProps = {
const ICON_PATH_PREFIX = '/icons/node/';
function TypeLabel({ label }: { label: string }) {
return <div className="w-full h-8 bg-stone-100 dark:bg-zinc-700 px-2 flex items-center justify-center">{label}</div>;
return <div className="w-full h-8 align-middle font-semibold">{label}</div>;
}
const CanvasNode: React.FC<CanvasNodeProps> = ({ data }) => {
@@ -74,20 +74,22 @@ const CanvasNode: React.FC<CanvasNodeProps> = ({ data }) => {
function renderOutput(data: IFlowNode) {
if (flowType === 'operator' && outputs?.length > 0) {
return (
<>
<div className="bg-zinc-100 dark:bg-zinc-700 rounded p-2">
<TypeLabel label="Outputs" />
{(outputs || []).map((output, index) => (
<NodeHandler key={`${data.id}_input_${index}`} node={data} data={output} type="source" label="outputs" index={index} />
))}
</>
<div className="flex flex-col space-y-3">
{outputs?.map((output, index) => (
<NodeHandler key={`${data.id}_input_${index}`} node={data} data={output} type="source" label="outputs" index={index} />
))}
</div>
</div>
);
} else if (flowType === 'resource') {
// resource nodes show output default
return (
<>
<div className="bg-zinc-100 dark:bg-zinc-700 rounded p-2">
<TypeLabel label="Outputs" />
<NodeHandler key={`${data.id}_input_0`} node={data} data={data} type="source" label="outputs" index={0} />
</>
</div>
);
}
}
@@ -105,7 +107,15 @@ const CanvasNode: React.FC<CanvasNodeProps> = ({ data }) => {
<DeleteOutlined className="h-full text-lg cursor-pointer" onClick={deleteNode} />
</IconWrapper>
<IconWrapper className="mt-2">
<Tooltip title={<><p className="font-bold">{node.label}</p><p>{node.description}</p></>} placement="right">
<Tooltip
title={
<>
<p className="font-bold">{node.label}</p>
<p>{node.description}</p>
</>
}
placement="right"
>
<InfoCircleOutlined className="h-full text-lg cursor-pointer" />
</Tooltip>
</IconWrapper>
@@ -113,36 +123,46 @@ const CanvasNode: React.FC<CanvasNodeProps> = ({ data }) => {
}
>
<div
className={classNames('w-72 h-auto rounded-xl shadow-md p-0 border bg-white dark:bg-zinc-800 cursor-grab', {
'border-blue-500': node.selected || isHovered,
'border-stone-400 dark:border-white': !node.selected && !isHovered,
'border-dashed': flowType !== 'operator',
'border-red-600': node.invalid,
})}
className={classNames(
'w-80 h-auto rounded-xl shadow-md px-2 py-4 border bg-white dark:bg-zinc-800 cursor-grab flex flex-col space-y-2 text-sm',
{
'border-blue-500': node.selected || isHovered,
'border-stone-400 dark:border-white': !node.selected && !isHovered,
'border-dashed': flowType !== 'operator',
'border-red-600': node.invalid,
},
)}
onMouseEnter={onHover}
onMouseLeave={onLeave}
>
{/* icon and label */}
<div className="flex flex-row items-center p-2">
<div className="flex flex-row items-center">
<Image src={'/icons/node/vis.png'} width={24} height={24} alt="" />
<p className="ml-2 text-lg font-bold text-ellipsis overflow-hidden whitespace-nowrap">{node.label}</p>
</div>
{inputs && inputs.length > 0 && (
<>
{inputs?.length > 0 && (
<div className="bg-zinc-100 dark:bg-zinc-700 rounded p-2">
<TypeLabel label="Inputs" />
{(inputs || []).map((input, index) => (
<NodeHandler key={`${node.id}_input_${index}`} node={node} data={input} type="target" label="inputs" index={index} />
))}
</>
<div className="flex flex-col space-y-2">
{inputs?.map((input, index) => (
<NodeHandler key={`${node.id}_input_${index}`} node={node} data={input} type="target" label="inputs" index={index} />
))}
</div>
</div>
)}
{parameters && parameters.length > 0 && (
<>
{parameters?.length > 0 && (
<div className="bg-zinc-100 dark:bg-zinc-700 rounded p-2">
<TypeLabel label="Parameters" />
{(parameters || []).map((parameter, index) => (
<NodeParamHandler key={`${node.id}_param_${index}`} node={node} data={parameter} label="parameters" index={index} />
))}
</>
<div className="flex flex-col space-y-3 text-neutral-500">
{parameters?.map((parameter, index) => (
<NodeParamHandler key={`${node.id}_param_${index}`} node={node} data={parameter} label="parameters" index={index} />
))}
</div>
</div>
)}
{renderOutput(node)}
</div>
</Popover>

View File

@@ -94,15 +94,15 @@ const NodeHandler: React.FC<NodeHandlerProps> = ({ node, data, type, label, inde
})}
>
<Handle
className="w-2 h-2"
className={classNames('w-2 h-2', type === 'source' ? '-mr-4' : '-ml-4')}
type={type}
position={type === 'source' ? Position.Right : Position.Left}
id={`${node.id}|${label}|${index}`}
isValidConnection={(connection) => isValidConnection(connection)}
/>
<Typography
className={classNames('p-2', {
'pr-4': label === 'outputs',
className={classNames('bg-white dark:bg-[#232734] w-full px-2 py-1 rounded text-neutral-500',{
'text-right': label === 'outputs',
})}
>
<Popconfirm

View File

@@ -17,6 +17,7 @@ import {
RenderTextArea,
RenderUpload,
RenderCodeEditor,
RenderPassword,
} from './node-renderer';
interface NodeParamHandlerProps {
@@ -40,7 +41,7 @@ const NodeParamHandler: React.FC<NodeParamHandlerProps> = ({ node, data, label,
case 'int':
case 'float':
return (
<div className="p-2 text-sm">
<div className="text-sm">
<p>
{data.label}:<RequiredIcon optional={data.optional} />
{data.description && (
@@ -60,7 +61,7 @@ const NodeParamHandler: React.FC<NodeParamHandlerProps> = ({ node, data, label,
);
case 'str':
return (
<div className="p-2 text-sm">
<div className="text-sm">
<p>
{data.label}:<RequiredIcon optional={data.optional} />
{data.description && (
@@ -87,11 +88,11 @@ const NodeParamHandler: React.FC<NodeParamHandlerProps> = ({ node, data, label,
)}
</div>
);
case 'checkbox':
case 'bool':
defaultValue = defaultValue === 'False' ? false : defaultValue;
defaultValue = defaultValue === 'True' ? true : defaultValue;
return (
<div className="p-2 text-sm">
<div className="text-sm">
<p>
{data.label}:<RequiredIcon optional={data.optional} />
{data.description && (
@@ -139,6 +140,8 @@ const NodeParamHandler: React.FC<NodeParamHandlerProps> = ({ node, data, label,
case 'time_picker':
return <RenderTimePicker {...props} />;
case 'tree_select':
return <RenderPassword {...props} />;
case 'password':
return <RenderTreeSelect {...props} />;
case 'upload':
return <RenderUpload {...props} />;

View File

@@ -13,15 +13,13 @@ export const RenderCascader = (params: Props) => {
const attr = convertKeysToCamelCase(data.ui?.attr || {});
return (
<div className="p-2 text-sm">
<Cascader
{...attr}
options={data.options}
defaultValue={defaultValue}
placeholder="please select"
className="w-full nodrag"
onChange={onChange}
/>
</div>
<Cascader
{...attr}
options={data.options}
defaultValue={defaultValue}
placeholder="please select"
className="w-full nodrag"
onChange={onChange}
/>
);
};

View File

@@ -14,7 +14,7 @@ export const RenderCheckbox = (params: Props) => {
return (
data.options?.length > 0 && (
<div className="p-2 text-sm">
<div className="bg-white p-2 rounded">
<Checkbox.Group {...attr} options={data.options} defaultValue={defaultValue} onChange={onChange} />
</div>
)

View File

@@ -11,6 +11,8 @@ type Props = {
export const RenderDatePicker = (params: Props) => {
const { data, defaultValue, onChange } = params;
console.log('data', data);
const attr = convertKeysToCamelCase(data.ui?.attr || {});
const onChangeDate: DatePickerProps['onChange'] = (date, dateString) => {
@@ -18,8 +20,12 @@ export const RenderDatePicker = (params: Props) => {
};
return (
<div className="p-2 text-sm">
<DatePicker {...attr} className="w-full" defaultValue={dayjs(defaultValue)} onChange={onChangeDate} />
</div>
<DatePicker
{...attr}
className="w-full"
placeholder="please select a date"
defaultValue={defaultValue ? dayjs(defaultValue) : null}
onChange={onChangeDate}
/>
);
};

View File

@@ -10,3 +10,4 @@ export * from './time-picker';
export * from './tree-select';
export * from './codeEditor';
export * from './upload';
export * from './password';

View File

@@ -13,16 +13,15 @@ export const RenderInput = (params: Props) => {
const attr = convertKeysToCamelCase(data.ui?.attr || {});
return (
<div className="p-2 text-sm">
<Input
{...attr}
className="w-full"
placeholder="please input"
defaultValue={defaultValue}
onChange={(e) => {
onChange(e.target.value);
}}
/>
</div>
<Input
{...attr}
className="w-full"
placeholder="please input"
defaultValue={defaultValue}
allowClear
onChange={(e) => {
onChange(e.target.value);
}}
/>
);
};

View File

@@ -0,0 +1,18 @@
import { IFlowNodeParameter } from '@/types/flow';
import { Input } from 'antd';
import { convertKeysToCamelCase } from '@/utils/flow';
const { Password } = Input;
type TextAreaProps = {
data: IFlowNodeParameter;
defaultValue: any;
onChange: (value: any) => void;
};
export const RenderPassword = (params: TextAreaProps) => {
const { data, defaultValue, onChange } = params;
const attr = convertKeysToCamelCase(data.ui?.attr || {});
return <Password {...attr} placeholder="input password" defaultValue={defaultValue} onChange={onChange} />;
};

View File

@@ -13,7 +13,7 @@ export const RenderRadio = (params: Props) => {
const attr = convertKeysToCamelCase(data.ui?.attr || {});
return (
<div className="p-2 text-sm">
<div className="bg-white p-2 rounded">
<Radio.Group
{...attr}
options={data.options}

View File

@@ -13,15 +13,6 @@ export const RenderSelect = (params: Props) => {
const attr = convertKeysToCamelCase(data.ui?.attr || {});
return (
<div className="p-2 text-sm">
<Select
{...attr}
className="w-full nodrag"
placeholder="please select"
defaultValue={defaultValue}
options={data.options}
onChange={onChange}
/>
</div>
<Select {...attr} className="w-full nodrag" placeholder="please select" defaultValue={defaultValue} options={data.options} onChange={onChange} />
);
};

View File

@@ -1,40 +0,0 @@
import { IFlowNodeParameter } from '@/types/flow';
import { convertKeysToCamelCase } from '@/utils/flow';
import { Col, InputNumber, Row, Slider, Space } from 'antd';
import type { InputNumberProps } from 'antd';
import React, { useState } from 'react';
type TextAreaProps = {
data: IFlowNodeParameter;
defaultValue: any;
onChange: (value: any) => void;
};
export const RenderSlider = (params: TextAreaProps) => {
const { data, defaultValue, onChange } = params;
const attr = convertKeysToCamelCase(data.ui?.attr || {});
const [inputValue, setInputValue] = useState(defaultValue);
const onChangeSlider: InputNumberProps['onChange'] = (newValue) => {
setInputValue(newValue as number);
onChange(newValue as number);
};
return (
<div className="p-2 text-sm">
{data?.ui?.show_input ? (
<Row>
<Col span={12}>
<Slider {...attr} onChange={onChangeSlider} value={typeof inputValue === 'number' ? inputValue : 0} />
</Col>
<Col span={4}>
<InputNumber {...attr} style={{ margin: '0 16px' }} value={inputValue} onChange={onChangeSlider} />
</Col>
</Row>
) : (
<Slider {...attr} onChange={onChangeSlider} value={typeof inputValue === 'number' ? inputValue : 0} />
)}
</div>
);
};

View File

@@ -21,7 +21,7 @@ export const RenderSlider = (params: TextAreaProps) => {
};
return (
<div className="p-2 text-sm">
<>
{data?.ui?.show_input ? (
<Row>
<Col span={12}>
@@ -34,6 +34,6 @@ export const RenderSlider = (params: TextAreaProps) => {
) : (
<Slider {...attr} onChange={onChangeSlider} value={typeof inputValue === 'number' ? inputValue : 0} />
)}
</div>
</>
);
};

View File

@@ -1,6 +1,7 @@
import { IFlowNodeParameter } from '@/types/flow';
import { Input } from 'antd';
import { convertKeysToCamelCase } from '@/utils/flow';
import classNames from 'classnames';
const { TextArea } = Input;
@@ -12,12 +13,12 @@ type TextAreaProps = {
export const RenderTextArea = (params: TextAreaProps) => {
const { data, defaultValue, onChange } = params;
convertKeysToCamelCase(data?.ui?.attr?.autosize || {});
const attr = convertKeysToCamelCase(data.ui?.attr || {});
return (
<div className="p-2 text-sm">
<TextArea {...attr} defaultValue={defaultValue} onChange={(e) => onChange(e.target.value)} {...data.ui.attr.autosize} rows={4} />
<div className={classNames({ 'mb-3': attr.showCount === true })}>
<TextArea {...attr} defaultValue={defaultValue} onChange={onChange} />
</div>
);
};

View File

@@ -17,9 +17,5 @@ export const RenderTimePicker = (params: TextAreaProps) => {
onChange(timeString);
};
return (
<div className="p-2 text-sm">
<TimePicker {...attr} className="w-full" defaultValue={defaultValue} onChange={onChangeTime} />
</div>
);
return <TimePicker {...attr} className="w-full" defaultValue={defaultValue} onChange={onChangeTime} placeholder="please select a moment" />;
};