diff --git a/dbgpt/_private/config.py b/dbgpt/_private/config.py index cd7936a60..10b3f1684 100644 --- a/dbgpt/_private/config.py +++ b/dbgpt/_private/config.py @@ -332,6 +332,17 @@ class Config(metaclass=Singleton): os.getenv("MULTI_INSTANCE", "False").lower() == "true" ) + # file server configuration + # The host of the current file server, if None, get the host automatically + self.FILE_SERVER_HOST = os.getenv("FILE_SERVER_HOST") + self.FILE_SERVER_LOCAL_STORAGE_PATH = os.getenv( + "FILE_SERVER_LOCAL_STORAGE_PATH" + ) + # multi-instance flag + self.WEBSERVER_MULTI_INSTANCE = ( + os.getenv("MULTI_INSTANCE", "False").lower() == "true" + ) + @property def local_db_manager(self) -> "ConnectorManager": from dbgpt.datasource.manages import ConnectorManager diff --git a/dbgpt/core/awel/flow/flow_factory.py b/dbgpt/core/awel/flow/flow_factory.py index 87b828971..1665e280c 100644 --- a/dbgpt/core/awel/flow/flow_factory.py +++ b/dbgpt/core/awel/flow/flow_factory.py @@ -173,7 +173,6 @@ class _VariablesRequestBase(BaseModel): description="The key of the variable to create", examples=["dbgpt.model.openai.api_key"], ) - label: str = Field( ..., description="The label of the variable to create", diff --git a/web/README.md b/web/README.md index b074b8120..f1db52c60 100644 --- a/web/README.md +++ b/web/README.md @@ -50,7 +50,7 @@ yarn install ### Usage ```sh -cp .env.example .env +cp .env.template .env ``` edit the `API_BASE_URL` to the real address diff --git a/web/app/i18n.ts b/web/app/i18n.ts index b9bf268e1..da5d80bdc 100644 --- a/web/app/i18n.ts +++ b/web/app/i18n.ts @@ -1,3 +1,4 @@ +import { Domain } from '@mui/icons-material'; import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import en from '@/locales/en'; diff --git a/web/client/api/request.ts b/web/client/api/request.ts index 706b9862b..d449cabdd 100644 --- a/web/client/api/request.ts +++ b/web/client/api/request.ts @@ -45,7 +45,19 @@ import { import { BaseModelParams, IModelData, StartModelParams, SupportModel } from '@/types/model'; import { AxiosRequestConfig } from 'axios'; import { DELETE, GET, POST, PUT } from '.'; - +import { UpdatePromptParams, IPrompt, PromptParams } from '@/types/prompt'; +import { + IFlow, + IFlowNode, + IFlowResponse, + IFlowUpdateParam, + IFlowRefreshParams, + IFlowExportParams, + IFlowImportParams, + IUploadFileRequestParams, + IUploadFileResponse, +} from '@/types/flow'; +import { IAgent, IApp, IAppData } from '@/types/app'; /** App */ export const postScenes = () => { @@ -273,23 +285,63 @@ export const postChatFeedBackForm = ({ data, config }: { data: ChatFeedBackSchem /** AWEL Flow */ export const addFlow = (data: IFlowUpdateParam) => { - return POST('/api/v1/serve/awel/flows', data); + return POST('/api/v2/serve/awel/flows', data); +}; + +export const getFlows = (page?: number, page_size?: number) => { + return GET('/api/v2/serve/awel/flows', { + page, + page_size, + }); }; export const getFlowById = (id: string) => { - return GET(`/api/v1/serve/awel/flows/${id}`); + return GET(`/api/v2/serve/awel/flows/${id}`); }; export const updateFlowById = (id: string, data: IFlowUpdateParam) => { - return PUT(`/api/v1/serve/awel/flows/${id}`, data); + return PUT(`/api/v2/serve/awel/flows/${id}`, data); }; export const deleteFlowById = (id: string) => { - return DELETE(`/api/v1/serve/awel/flows/${id}`); + return DELETE(`/api/v2/serve/awel/flows/${id}`); }; export const getFlowNodes = () => { - return GET>(`/api/v1/serve/awel/nodes`); + return GET>(`/api/v2/serve/awel/nodes`); +}; + +export const refreshFlowNodeById = (data: IFlowRefreshParams) => { + return POST('/api/v2/serve/awel/nodes/refresh', data); +}; + +export const debugFlow = (data: any) => { + return POST('/api/v2/serve/awel/flow/debug', data); +}; + +export const exportFlow = (data: IFlowExportParams) => { + return GET(`/api/v2/serve/awel/flow/export/${data.uid}`, data); +}; + +export const importFlow = (data: IFlowImportParams) => { + return POST('/api/v2/serve/awel/flow/import', data); +}; + +export const uploadFile = (data: IUploadFileRequestParams) => { + return POST>('/api/v2/serve/file/files/dbgpt', data); +}; + +export const downloadFile = (fileId: string) => { + return GET(`/api/v2/serve/file/files/dbgpt/${fileId}`); +}; + +// TODO:wait for interface update +export const getFlowTemplateList = () => { + return GET>('/api/v2/serve/awel/flow/templates'); +}; + +export const getFlowTemplateById = (id: string) => { + return GET(`/api/v2/serve/awel/flow/templates/${id}`); }; /** app */ diff --git a/web/components/common/gpt-card.tsx b/web/components/common/gpt-card.tsx index 97424a4f1..2de92304a 100644 --- a/web/components/common/gpt-card.tsx +++ b/web/components/common/gpt-card.tsx @@ -61,6 +61,7 @@ function GPTCard({ return icon; }, [icon]); + // TODO: 算子资源标签 const tagNode = useMemo(() => { if (!tags || !tags.length) return null; return ( diff --git a/web/components/flow/add-nodes.tsx b/web/components/flow/add-nodes.tsx index e12db16fc..2f557c6aa 100644 --- a/web/components/flow/add-nodes.tsx +++ b/web/components/flow/add-nodes.tsx @@ -122,7 +122,7 @@ const AddNodes: React.FC = () => { className="flex items-center justify-center rounded-full left-4 top-4" style={{ zIndex: 1050 }} icon={} - > + /> ); }; diff --git a/web/components/flow/canvas-modal/export-flow-modal.tsx b/web/components/flow/canvas-modal/export-flow-modal.tsx new file mode 100644 index 000000000..486ebb987 --- /dev/null +++ b/web/components/flow/canvas-modal/export-flow-modal.tsx @@ -0,0 +1,90 @@ +import { Modal, Form, Input, Button, Space, Radio, message } from 'antd'; +import { IFlowData, IFlowUpdateParam } from '@/types/flow'; +import { apiInterceptors, exportFlow } from '@/client/api'; +import { ReactFlowInstance } from 'reactflow'; +import { useTranslation } from 'react-i18next'; + +type Props = { + reactFlow: ReactFlowInstance; + flowInfo?: IFlowUpdateParam; + isExportFlowModalOpen: boolean; + setIsExportFlowModalOpen: (value: boolean) => void; +}; + +export const ExportFlowModal: React.FC = ({ reactFlow, flowInfo, isExportFlowModalOpen, setIsExportFlowModalOpen }) => { + const { t } = useTranslation(); + const [form] = Form.useForm(); + const [messageApi, contextHolder] = message.useMessage(); + + const onFlowExport = async (values: any) => { + const flowData = reactFlow.toObject() as IFlowData; + const blob = new Blob([JSON.stringify(flowData)], { type: 'text/plain;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = values.file_name || 'flow.json'; + a.click(); + + const [, , res] = await apiInterceptors(exportFlow(values)); + + if (res?.success) { + messageApi.success(t('export_flow_success')); + } else if (res?.err_msg) { + messageApi.error(res?.err_msg); + } + + setIsExportFlowModalOpen(false); + }; + + return ( + <> + setIsExportFlowModalOpen(false)} footer={null}> +
+ + + + + + + JSON + DBGPTS + + + + + + File + JSON + + + + + + + + + + + +
+
+ + {contextHolder} + + ); +}; diff --git a/web/components/flow/canvas-modal/import-flow-modal.tsx b/web/components/flow/canvas-modal/import-flow-modal.tsx new file mode 100644 index 000000000..fe2855aee --- /dev/null +++ b/web/components/flow/canvas-modal/import-flow-modal.tsx @@ -0,0 +1,82 @@ +import { Modal, Form, Button, Space, message, Checkbox, Upload } from 'antd'; +import { apiInterceptors, importFlow } from '@/client/api'; +import { Node, Edge } from 'reactflow'; +import { UploadOutlined } from '@mui/icons-material'; +import { t } from 'i18next'; + +type Props = { + isImportModalOpen: boolean; + setNodes: React.Dispatch[]>>; + setEdges: React.Dispatch[]>>; + setIsImportFlowModalOpen: (value: boolean) => void; +}; + +export const ImportFlowModal: React.FC = ({ setNodes, setEdges, isImportModalOpen, setIsImportFlowModalOpen }) => { + const [form] = Form.useForm(); + const [messageApi, contextHolder] = message.useMessage(); + + // TODO: Implement onFlowImport + const onFlowImport = async (values: any) => { + // const input = document.createElement('input'); + // input.type = 'file'; + // input.accept = '.json'; + // input.onchange = async (e: any) => { + // const file = e.target.files[0]; + // const reader = new FileReader(); + // reader.onload = async (event) => { + // const flowData = JSON.parse(event.target?.result as string) as IFlowData; + // setNodes(flowData.nodes); + // setEdges(flowData.edges); + // }; + // reader.readAsText(file); + // }; + // input.click; + console.log(values); + values.file = values.file?.[0]; + + const [, , res] = await apiInterceptors(importFlow(values)); + + if (res?.success) { + messageApi.success(t('export_flow_success')); + } else if (res?.err_msg) { + messageApi.error(res?.err_msg); + } + + setIsImportFlowModalOpen(false); + }; + + return ( + <> + setIsImportFlowModalOpen(false)} footer={null}> +
+ (Array.isArray(e) ? e : e && e.fileList)} + rules={[{ required: true, message: 'Please upload a file' }]} + > + false} maxCount={1}> + + + + + + + + + + + + + + +
+
+ + {contextHolder} + + ); +}; diff --git a/web/components/flow/canvas-modal/index.ts b/web/components/flow/canvas-modal/index.ts new file mode 100644 index 000000000..e131e7b65 --- /dev/null +++ b/web/components/flow/canvas-modal/index.ts @@ -0,0 +1,3 @@ +export * from './save-flow-modal'; +export * from './export-flow-modal'; +export * from './import-flow-modal'; \ No newline at end of file diff --git a/web/components/flow/canvas-modal/save-flow-modal.tsx b/web/components/flow/canvas-modal/save-flow-modal.tsx new file mode 100644 index 000000000..e03e036d0 --- /dev/null +++ b/web/components/flow/canvas-modal/save-flow-modal.tsx @@ -0,0 +1,152 @@ +import { useState } from 'react'; +import { Modal, Form, Input, Button, Space, message, Checkbox } from 'antd'; +import { IFlowData, IFlowUpdateParam } from '@/types/flow'; +import { apiInterceptors, addFlow, updateFlowById } from '@/client/api'; +import { mapHumpToUnderline } from '@/utils/flow'; +import { useTranslation } from 'react-i18next'; +import { ReactFlowInstance } from 'reactflow'; +import { useSearchParams } from 'next/navigation'; + +const { TextArea } = Input; + +type Props = { + reactFlow: ReactFlowInstance; + flowInfo?: IFlowUpdateParam; + isSaveFlowModalOpen: boolean; + setIsSaveFlowModalOpen: (value: boolean) => void; +}; + +export const SaveFlowModal: React.FC = ({ reactFlow, isSaveFlowModalOpen, flowInfo, setIsSaveFlowModalOpen }) => { + const [deploy, setDeploy] = useState(true); + const { t } = useTranslation(); + const searchParams = useSearchParams(); + const id = searchParams?.get('id') || ''; + const [form] = Form.useForm(); + const [messageApi, contextHolder] = message.useMessage(); + + function onLabelChange(e: React.ChangeEvent) { + const label = e.target.value; + // replace spaces with underscores, convert uppercase letters to lowercase, remove characters other than digits, letters, _, and -. + let result = label + .replace(/\s+/g, '_') + .replace(/[^a-z0-9_-]/g, '') + .toLowerCase(); + result = result; + form.setFieldsValue({ name: result }); + } + + async function onSaveFlow() { + const { name, label, description = '', editable = false, state = 'deployed' } = form.getFieldsValue(); + console.log(form.getFieldsValue()); + const reactFlowObject = mapHumpToUnderline(reactFlow.toObject() as IFlowData); + + if (id) { + const [, , res] = await apiInterceptors(updateFlowById(id, { name, label, description, editable, uid: id, flow_data: reactFlowObject, state })); + + if (res?.success) { + messageApi.success(t('save_flow_success')); + } else if (res?.err_msg) { + messageApi.error(res?.err_msg); + } + } else { + const [_, res] = await apiInterceptors(addFlow({ name, label, description, editable, flow_data: reactFlowObject, state })); + if (res?.uid) { + messageApi.success(t('save_flow_success')); + const history = window.history; + history.pushState(null, '', `/flow/canvas?id=${res.uid}`); + } + } + setIsSaveFlowModalOpen(false); + } + + return ( + <> + { + setIsSaveFlowModalOpen(false); + }} + cancelButtonProps={{ className: 'hidden' }} + okButtonProps={{ className: 'hidden' }} + > +
+ + + + + ({ + validator(_, value) { + const regex = /^[a-zA-Z0-9_\-]+$/; + if (!regex.test(value)) { + return Promise.reject('Can only contain numbers, letters, underscores, and dashes'); + } + return Promise.resolve(); + }, + }), + ]} + > + + + + +