Skip to content

Commit

Permalink
feat: support importing and overwriting workflow DSL (#5511)
Browse files Browse the repository at this point in the history
Co-authored-by: StyleZhang <jasonapring2015@outlook.com>
  • Loading branch information
takatost and zxhlyh authored Jun 25, 2024
1 parent cdc2a6f commit ec1d3dd
Show file tree
Hide file tree
Showing 12 changed files with 361 additions and 26 deletions.
29 changes: 29 additions & 0 deletions api/controllers/console/app/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,34 @@ def post(self, app_model: App):
}


class DraftWorkflowImportApi(Resource):
@setup_required
@login_required
@account_initialization_required
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
@marshal_with(workflow_fields)
def post(self, app_model: App):
"""
Import draft workflow
"""
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden()

parser = reqparse.RequestParser()
parser.add_argument('data', type=str, required=True, nullable=False, location='json')
args = parser.parse_args()

workflow_service = WorkflowService()
workflow = workflow_service.import_draft_workflow(
app_model=app_model,
data=args['data'],
account=current_user
)

return workflow


class AdvancedChatDraftWorkflowRunApi(Resource):
@setup_required
@login_required
Expand Down Expand Up @@ -439,6 +467,7 @@ def post(self, app_model: App):


api.add_resource(DraftWorkflowApi, '/apps/<uuid:app_id>/workflows/draft')
api.add_resource(DraftWorkflowImportApi, '/apps/<uuid:app_id>/workflows/draft/import')
api.add_resource(AdvancedChatDraftWorkflowRunApi, '/apps/<uuid:app_id>/advanced-chat/workflows/draft/run')
api.add_resource(DraftWorkflowRunApi, '/apps/<uuid:app_id>/workflows/draft/run')
api.add_resource(WorkflowTaskStopApi, '/apps/<uuid:app_id>/workflow-runs/tasks/<string:task_id>/stop')
Expand Down
52 changes: 52 additions & 0 deletions api/services/workflow_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from datetime import datetime, timezone
from typing import Optional

import yaml

from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfigManager
from core.app.apps.workflow.app_config_manager import WorkflowAppConfigManager
from core.model_runtime.utils.encoders import jsonable_encoder
Expand Down Expand Up @@ -112,6 +114,56 @@ def sync_draft_workflow(self, app_model: App,
# return draft workflow
return workflow

def import_draft_workflow(self, app_model: App,
data: str,
account: Account) -> Workflow:
"""
Import draft workflow
:param app_model: App instance
:param data: import data
:param account: Account instance
:return:
"""
try:
import_data = yaml.safe_load(data)
except yaml.YAMLError as e:
raise ValueError("Invalid YAML format in data argument.")

app_data = import_data.get('app')
workflow = import_data.get('workflow')

if not app_data:
raise ValueError("Missing app in data argument")

app_mode = AppMode.value_of(app_data.get('mode'))
if app_mode not in [AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]:
raise ValueError("Only support import workflow in advanced-chat or workflow app.")

if app_data.get('mode') != app_model.mode:
raise ValueError(f"App mode {app_data.get('mode')} is not matched with current app mode {app_model.mode}")

if not workflow:
raise ValueError("Missing workflow in data argument "
"when app mode is advanced-chat or workflow")

# fetch draft workflow by app_model
current_draft_workflow = self.get_draft_workflow(app_model=app_model)
if current_draft_workflow:
unique_hash = current_draft_workflow.unique_hash
else:
unique_hash = None

# sync draft workflow
draft_workflow = self.sync_draft_workflow(
app_model=app_model,
graph=workflow.get('graph'),
features=workflow.get('features'),
unique_hash=unique_hash,
account=account
)

return draft_workflow

def publish_workflow(self, app_model: App,
account: Account,
draft_workflow: Optional[Workflow] = None) -> Workflow:
Expand Down
29 changes: 26 additions & 3 deletions web/app/components/app-sidebar/app-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTrave
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { getRedirection } from '@/utils/app-redirection'
import UpdateDSLModal from '@/app/components/workflow/update-dsl-modal'

export type IAppInfoProps = {
expand: boolean
Expand All @@ -45,6 +46,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const [showSwitchTip, setShowSwitchTip] = useState<string>('')
const [showSwitchModal, setShowSwitchModal] = useState<boolean>(false)
const [showImportDSLModal, setShowImportDSLModal] = useState<boolean>(false)

const mutateApps = useContextSelector(
AppsContext,
Expand Down Expand Up @@ -295,9 +297,6 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
}}>
<span className='text-gray-700 text-sm leading-5'>{t('app.duplicate')}</span>
</div>
<div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={onExport}>
<span className='text-gray-700 text-sm leading-5'>{t('app.export')}</span>
</div>
{(appDetail.mode === 'completion' || appDetail.mode === 'chat') && (
<>
<Divider className="!my-1" />
Expand All @@ -315,6 +314,22 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
</>
)}
<Divider className="!my-1" />
<div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={onExport}>
<span className='text-gray-700 text-sm leading-5'>{t('app.export')}</span>
</div>
{
(appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') && (
<div
className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer'
onClick={() => {
setOpen(false)
setShowImportDSLModal(true)
}}>
<span className='text-gray-700 text-sm leading-5'>{t('workflow.common.importDSL')}</span>
</div>
)
}
<Divider className="!my-1" />
<div className='group h-9 py-2 px-3 mx-1 flex items-center hover:bg-red-50 rounded-lg cursor-pointer' onClick={() => {
setOpen(false)
setShowConfirmDelete(true)
Expand Down Expand Up @@ -388,6 +403,14 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
onCancel={() => setShowConfirmDelete(false)}
/>
)}
{
showImportDSLModal && (
<UpdateDSLModal
onCancel={() => setShowImportDSLModal(false)}
onBackup={onExport}
/>
)
}
</div>
</PortalToFollowElem>
)
Expand Down
4 changes: 3 additions & 1 deletion web/app/components/app/create-from-dsl-modal/uploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ import Button from '@/app/components/base/button'
export type Props = {
file: File | undefined
updateFile: (file?: File) => void
className?: string
}

const Uploader: FC<Props> = ({
file,
updateFile,
className,
}) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
Expand Down Expand Up @@ -83,7 +85,7 @@ const Uploader: FC<Props> = ({
}, [])

return (
<div className='mt-6'>
<div className={cn('mt-6', className)}>
<input
ref={fileUploader}
style={{ display: 'none' }}
Expand Down
30 changes: 30 additions & 0 deletions web/app/components/workflow/hooks/use-workflow-interactions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useReactFlow } from 'reactflow'
import { useWorkflowStore } from '../store'
import { WORKFLOW_DATA_UPDATE } from '../constants'
Expand All @@ -11,6 +12,9 @@ import { useEdgesInteractions } from './use-edges-interactions'
import { useNodesInteractions } from './use-nodes-interactions'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { fetchWorkflowDraft } from '@/service/workflow'
import { exportAppConfig } from '@/service/apps'
import { useToastContext } from '@/app/components/base/toast'
import { useStore as useAppStore } from '@/app/components/app/store'

export const useWorkflowInteractions = () => {
const workflowStore = useWorkflowStore()
Expand Down Expand Up @@ -71,3 +75,29 @@ export const useWorkflowUpdate = () => {
handleRefreshWorkflowDraft,
}
}

export const useDSL = () => {
const { t } = useTranslation()
const { notify } = useToastContext()
const appDetail = useAppStore(s => s.appDetail)

const handleExportDSL = useCallback(async () => {
if (!appDetail)
return
try {
const { data } = await exportAppConfig(appDetail.id)
const a = document.createElement('a')
const file = new Blob([data], { type: 'application/yaml' })
a.href = URL.createObjectURL(file)
a.download = `${appDetail.name}.yml`
a.click()
}
catch (e) {
notify({ type: 'error', message: t('app.exportFailed') })
}
}, [appDetail, notify, t])

return {
handleExportDSL,
}
}
35 changes: 35 additions & 0 deletions web/app/components/workflow/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import ReactFlow, {
useEdgesState,
useNodesState,
useOnViewportChange,
useReactFlow,
} from 'reactflow'
import type {
Viewport,
Expand All @@ -32,6 +33,7 @@ import type {
} from './types'
import { WorkflowContextProvider } from './context'
import {
useDSL,
useEdgesInteractions,
useNodesInteractions,
useNodesReadOnly,
Expand All @@ -58,6 +60,7 @@ import CandidateNode from './candidate-node'
import PanelContextmenu from './panel-contextmenu'
import NodeContextmenu from './node-contextmenu'
import SyncingDataModal from './syncing-data-modal'
import UpdateDSLModal from './update-dsl-modal'
import {
useStore,
useWorkflowStore,
Expand All @@ -76,6 +79,7 @@ import {
import Loading from '@/app/components/base/loading'
import { FeaturesProvider } from '@/app/components/base/features'
import type { Features as FeaturesData } from '@/app/components/base/features/types'
import { useFeaturesStore } from '@/app/components/base/features/hooks'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import Confirm from '@/app/components/base/confirm/common'

Expand All @@ -99,15 +103,20 @@ const Workflow: FC<WorkflowProps> = memo(({
}) => {
const workflowContainerRef = useRef<HTMLDivElement>(null)
const workflowStore = useWorkflowStore()
const reactflow = useReactFlow()
const featuresStore = useFeaturesStore()
const [nodes, setNodes] = useNodesState(originalNodes)
const [edges, setEdges] = useEdgesState(originalEdges)
const showFeaturesPanel = useStore(state => state.showFeaturesPanel)
const controlMode = useStore(s => s.controlMode)
const nodeAnimation = useStore(s => s.nodeAnimation)
const showConfirm = useStore(s => s.showConfirm)
const showImportDSLModal = useStore(s => s.showImportDSLModal)
const {
setShowConfirm,
setControlPromptEditorRerenderKey,
setShowImportDSLModal,
setSyncWorkflowDraftHash,
} = workflowStore.getState()
const {
handleSyncWorkflowDraft,
Expand All @@ -122,6 +131,19 @@ const Workflow: FC<WorkflowProps> = memo(({
if (v.type === WORKFLOW_DATA_UPDATE) {
setNodes(v.payload.nodes)
setEdges(v.payload.edges)

if (v.payload.viewport)
reactflow.setViewport(v.payload.viewport)

if (v.payload.features && featuresStore) {
const { setFeatures } = featuresStore.getState()

setFeatures(v.payload.features)
}

if (v.payload.hash)
setSyncWorkflowDraftHash(v.payload.hash)

setTimeout(() => setControlPromptEditorRerenderKey(Date.now()))
}
})
Expand Down Expand Up @@ -204,11 +226,15 @@ const Workflow: FC<WorkflowProps> = memo(({
} = useSelectionInteractions()
const {
handlePaneContextMenu,
handlePaneContextmenuCancel,
} = usePanelInteractions()
const {
isValidConnection,
} = useWorkflow()
const { handleStartWorkflowRun } = useWorkflowStartRun()
const {
handleExportDSL,
} = useDSL()

useOnViewportChange({
onEnd: () => {
Expand Down Expand Up @@ -266,6 +292,15 @@ const Workflow: FC<WorkflowProps> = memo(({
/>
)
}
{
showImportDSLModal && (
<UpdateDSLModal
onCancel={() => setShowImportDSLModal(false)}
onBackup={handleExportDSL}
onImport={handlePaneContextmenuCancel}
/>
)
}
<ReactFlow
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
Expand Down
Loading

0 comments on commit ec1d3dd

Please sign in to comment.