Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support importing and overwriting workflow DSL #5511

Merged
merged 7 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading