From 589ac9b22c8506a86f04eb30cfff8b8276ee3d69 Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 29 Mar 2024 17:24:30 +0800 Subject: [PATCH 1/9] feat: http key value inputs --- .../nodes/http/components/api-input.tsx | 7 +- .../nodes/http/components/key-value/index.tsx | 79 ++++++++----------- .../key-value/key-value-edit/index.tsx | 12 +-- .../key-value/key-value-edit/input-item.tsx | 61 ++++++++------ .../key-value/key-value-edit/item.tsx | 4 +- .../components/workflow/nodes/http/panel.tsx | 8 -- 6 files changed, 78 insertions(+), 93 deletions(-) diff --git a/web/app/components/workflow/nodes/http/components/api-input.tsx b/web/app/components/workflow/nodes/http/components/api-input.tsx index 9f1136527f..70a750db5b 100644 --- a/web/app/components/workflow/nodes/http/components/api-input.tsx +++ b/web/app/components/workflow/nodes/http/components/api-input.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import React, { useEffect, useRef, useState } from 'react' +import React, { useState } from 'react' import cn from 'classnames' import { useTranslation } from 'react-i18next' import { Method } from '../types' @@ -38,7 +38,6 @@ const ApiInput: FC = ({ }) => { const { t } = useTranslation() - const inputRef = useRef(null) const [isFocus, setIsFocus] = useState(false) const availableVarList = useAvailableVarList(nodeId, { onlyLeafNodeVar: false, @@ -47,10 +46,6 @@ const ApiInput: FC = ({ }, }) - useEffect(() => { - if (isFocus) - inputRef.current?.focus() - }, [isFocus]) return (
void onAdd: () => void - isKeyValueEdit: boolean - toggleKeyValueEdit: () => void + // toggleKeyValueEdit: () => void } const KeyValueList: FC = ({ @@ -19,47 +17,40 @@ const KeyValueList: FC = ({ list, onChange, onAdd, - isKeyValueEdit, - toggleKeyValueEdit, + // toggleKeyValueEdit, }) => { - const handleBulkValueChange = useCallback((value: string) => { - const newList = value.split('\n').map((item) => { - const [key, value] = item.split(':') - return { - key: key ? key.trim() : '', - value: value ? value.trim() : '', - } - }) - onChange(newList) - }, [onChange]) + // const handleBulkValueChange = useCallback((value: string) => { + // const newList = value.split('\n').map((item) => { + // const [key, value] = item.split(':') + // return { + // key: key ? key.trim() : '', + // value: value ? value.trim() : '', + // } + // }) + // onChange(newList) + // }, [onChange]) - const bulkList = (() => { - const res = list.map((item) => { - if (!item.key && !item.value) - return '' - if (!item.value) - return item.key - return `${item.key}:${item.value}` - }).join('\n') - return res - })() - return ( - <> - {isKeyValueEdit - ? - : - } - - ) + // const bulkList = (() => { + // const res = list.map((item) => { + // if (!item.key && !item.value) + // return '' + // if (!item.value) + // return item.key + // return `${item.key}:${item.value}` + // }).join('\n') + // return res + // })() + return + // : } export default React.memo(KeyValueList) diff --git a/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/index.tsx b/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/index.tsx index 528579f3d2..53c70b0c6a 100644 --- a/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/index.tsx +++ b/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/index.tsx @@ -5,8 +5,8 @@ import produce from 'immer' import { useTranslation } from 'react-i18next' import type { KeyValue } from '../../../types' import KeyValueItem from './item' -import TooltipPlus from '@/app/components/base/tooltip-plus' -import { EditList } from '@/app/components/base/icons/src/vender/solid/communication' +// import TooltipPlus from '@/app/components/base/tooltip-plus' +// import { EditList } from '@/app/components/base/icons/src/vender/solid/communication' const i18nPrefix = 'workflow.nodes.http' @@ -15,7 +15,7 @@ type Props = { list: KeyValue[] onChange: (newList: KeyValue[]) => void onAdd: () => void - onSwitchToBulkEdit: () => void + // onSwitchToBulkEdit: () => void } const KeyValueList: FC = ({ @@ -23,7 +23,7 @@ const KeyValueList: FC = ({ list, onChange, onAdd, - onSwitchToBulkEdit, + // onSwitchToBulkEdit, }) => { const { t } = useTranslation() @@ -51,7 +51,7 @@ const KeyValueList: FC = ({
{t(`${i18nPrefix}.key`)}
{t(`${i18nPrefix}.value`)}
- {!readonly && ( + {/* {!readonly && ( @@ -61,7 +61,7 @@ const KeyValueList: FC = ({ >
- )} + )} */}
{ diff --git a/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/input-item.tsx b/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/input-item.tsx index 6c30a6e284..239b5ff5c3 100644 --- a/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/input-item.tsx +++ b/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/input-item.tsx @@ -1,10 +1,10 @@ 'use client' import type { FC } from 'react' -import React, { useCallback } from 'react' -import { useBoolean } from 'ahooks' +import React, { useCallback, useState } from 'react' import cn from 'classnames' +import { useTranslation } from 'react-i18next' import RemoveButton from '@/app/components/workflow/nodes/_base/components/remove-button' -import SupportVarInput from '@/app/components/workflow/nodes/_base/components/support-var-input' +import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var' type Props = { className?: string @@ -25,15 +25,11 @@ const InputItem: FC = ({ placeholder, readOnly, }) => { - const hasValue = !!value - const [isEdit, { - setTrue: setIsEditTrue, - setFalse: setIsEditFalse, - }] = useBoolean(false) + const { t } = useTranslation() - const handleChange = useCallback((e: React.ChangeEvent) => { - onChange(e.target.value) - }, [onChange]) + const hasValue = !!value + + const [isFocus, setIsFocus] = useState(false) const handleRemove = useCallback((e: React.MouseEvent) => { e.stopPropagation() @@ -41,34 +37,47 @@ const InputItem: FC = ({ }, [onRemove]) return ( -
- {(isEdit && !readOnly) +
+ {(!readOnly) ? ( - + ) :
{!hasValue &&
{placeholder}
} {hasValue && ( - )} - {hasRemove && !isEdit && ( + {hasRemove && !isFocus && ( = ({ return ( // group class name is for hover row show remove button -
+
= ({
> = ({ headers, setHeaders, addHeader, - isHeaderKeyValueEdit, - toggleIsHeaderKeyValueEdit, params, setParams, addParam, - isParamKeyValueEdit, - toggleIsParamKeyValueEdit, setBody, isShowAuthorization, showAuthorization, @@ -110,8 +106,6 @@ const Panel: FC> = ({ onChange={setHeaders} onAdd={addHeader} readonly={readOnly} - isKeyValueEdit={isHeaderKeyValueEdit} - toggleKeyValueEdit={toggleIsHeaderKeyValueEdit} /> > = ({ onChange={setParams} onAdd={addParam} readonly={readOnly} - isKeyValueEdit={isParamKeyValueEdit} - toggleKeyValueEdit={toggleIsParamKeyValueEdit} /> Date: Fri, 29 Mar 2024 17:55:59 +0800 Subject: [PATCH 2/9] feat: http attr support selct keys --- .../nodes/http/components/key-value/index.tsx | 3 +++ .../key-value/key-value-edit/index.tsx | 3 +++ .../key-value/key-value-edit/input-item.tsx | 20 ++++++++++++++----- .../key-value/key-value-edit/item.tsx | 6 +++++- .../components/workflow/nodes/http/panel.tsx | 2 ++ web/i18n/en-US/workflow.ts | 1 + web/i18n/zh-Hans/workflow.ts | 1 + 7 files changed, 30 insertions(+), 6 deletions(-) diff --git a/web/app/components/workflow/nodes/http/components/key-value/index.tsx b/web/app/components/workflow/nodes/http/components/key-value/index.tsx index a400de521b..65fab16cac 100644 --- a/web/app/components/workflow/nodes/http/components/key-value/index.tsx +++ b/web/app/components/workflow/nodes/http/components/key-value/index.tsx @@ -6,6 +6,7 @@ import KeyValueEdit from './key-value-edit' type Props = { readonly: boolean + nodeId: string list: KeyValue[] onChange: (newList: KeyValue[]) => void onAdd: () => void @@ -14,6 +15,7 @@ type Props = { const KeyValueList: FC = ({ readonly, + nodeId, list, onChange, onAdd, @@ -42,6 +44,7 @@ const KeyValueList: FC = ({ // })() return void onAdd: () => void @@ -20,6 +21,7 @@ type Props = { const KeyValueList: FC = ({ readonly, + nodeId, list, onChange, onAdd, @@ -68,6 +70,7 @@ const KeyValueList: FC = ({ list.map((item, index) => ( void hasRemove: boolean @@ -18,6 +21,7 @@ type Props = { const InputItem: FC = ({ className, + nodeId, value, onChange, hasRemove, @@ -30,6 +34,12 @@ const InputItem: FC = ({ const hasValue = !!value const [isFocus, setIsFocus] = useState(false) + const availableVarList = useAvailableVarList(nodeId, { + onlyLeafNodeVar: false, + filterVar: (varPayload: Var) => { + return [VarType.string, VarType.number].includes(varPayload.type) + }, + }) const handleRemove = useCallback((e: React.MouseEvent) => { e.stopPropagation() @@ -55,9 +65,9 @@ const InputItem: FC = ({ value={value} onChange={onChange} readOnly={readOnly} - nodesOutputVars={[]} + nodesOutputVars={availableVarList} onFocusChange={setIsFocus} - placeholder={t('workflow.nodes.http.apiPlaceholder')!} + placeholder={t('workflow.nodes.http.insertVarPlaceholder')!} placeholderClassName='!leading-[21px]' /> ) @@ -71,9 +81,9 @@ const InputItem: FC = ({ value={value} onChange={onChange} readOnly={readOnly} - nodesOutputVars={[]} + nodesOutputVars={availableVarList} onFocusChange={setIsFocus} - placeholder={t('workflow.nodes.http.apiPlaceholder')!} + placeholder={t('workflow.nodes.http.insertVarPlaceholder')!} placeholderClassName='!leading-[21px]' /> )} diff --git a/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/item.tsx b/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/item.tsx index 5469cc6c12..27abed7f33 100644 --- a/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/item.tsx +++ b/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/item.tsx @@ -11,6 +11,7 @@ const i18nPrefix = 'workflow.nodes.http' type Props = { className?: string + nodeId: string readonly: boolean canRemove: boolean payload: KeyValue @@ -22,6 +23,7 @@ type Props = { const KeyValueItem: FC = ({ className, + nodeId, readonly, canRemove, payload, @@ -45,9 +47,10 @@ const KeyValueItem: FC = ({ return ( // group class name is for hover row show remove button -
+
= ({
> = ({ title={t(`${i18nPrefix}.headers`)} > > = ({ title={t(`${i18nPrefix}.params`)} > Date: Fri, 29 Mar 2024 18:02:06 +0800 Subject: [PATCH 3/9] chore: remove input vars --- .../components/workflow/nodes/http/panel.tsx | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/web/app/components/workflow/nodes/http/panel.tsx b/web/app/components/workflow/nodes/http/panel.tsx index 5d679f96a6..d2649962e4 100644 --- a/web/app/components/workflow/nodes/http/panel.tsx +++ b/web/app/components/workflow/nodes/http/panel.tsx @@ -8,9 +8,7 @@ import KeyValue from './components/key-value' import EditBody from './components/edit-body' import AuthorizationModal from './components/authorization' import type { HttpNodeType } from './types' -import VarList from '@/app/components/workflow/nodes/_base/components/variable/var-list' import Field from '@/app/components/workflow/nodes/_base/components/field' -import AddButton from '@/app/components/base/button/add-button' import Split from '@/app/components/workflow/nodes/_base/components/split' import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' import { Settings01 } from '@/app/components/base/icons/src/vender/line/general' @@ -29,9 +27,6 @@ const Panel: FC> = ({ const { readOnly, inputs, - handleVarListChange, - handleAddVariable, - filterVar, handleMethodChange, handleUrlChange, headers, @@ -60,20 +55,6 @@ const Panel: FC> = ({ return (
- : undefined - } - > - - Date: Fri, 29 Mar 2024 18:07:54 +0800 Subject: [PATCH 4/9] checklist --- ...kflow-variable-block-replacement-block.tsx | 2 +- .../components/workflow/header/checklist.tsx | 124 ++++++++++++++- web/app/components/workflow/header/index.tsx | 10 +- web/app/components/workflow/panel/index.tsx | 8 - .../workflow/panel/workflow-info.tsx | 143 ------------------ 5 files changed, 130 insertions(+), 157 deletions(-) delete mode 100644 web/app/components/workflow/panel/workflow-info.tsx diff --git a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/workflow-variable-block-replacement-block.tsx b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/workflow-variable-block-replacement-block.tsx index d0589a9485..a07d3cd98a 100644 --- a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/workflow-variable-block-replacement-block.tsx +++ b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/workflow-variable-block-replacement-block.tsx @@ -13,7 +13,7 @@ import { CustomTextNode } from '../custom-text/node' import { $createWorkflowVariableBlockNode } from './node' import { WorkflowVariableBlockNode } from './index' -const REGEX = /\{\{#(\d+|sys)(\.[a-zA-Z_][a-zA-Z0-9_]{0,29})+#\}\}/gi +const REGEX = /\{\{(#[a-zA-Z0-9_]{1,50}(\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10}#)\}\}/gi const WorkflowVariableBlockReplacementBlock = ({ getWorkflowNode = () => undefined, diff --git a/web/app/components/workflow/header/checklist.tsx b/web/app/components/workflow/header/checklist.tsx index 02ae026c31..8fe99180f6 100644 --- a/web/app/components/workflow/header/checklist.tsx +++ b/web/app/components/workflow/header/checklist.tsx @@ -1,16 +1,76 @@ import { memo, + useMemo, useState, } from 'react' +import { useTranslation } from 'react-i18next' +import { + getIncomers, + getOutgoers, + useEdges, + useNodes, +} from 'reactflow' +import BlockIcon from '../block-icon' +import { + useNodesExtraData, + useNodesInteractions, +} from '../hooks' +import type { CommonNodeType } from '../types' +import { BlockEnum } from '../types' +import { useStore } from '../store' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' -import { Checklist } from '@/app/components/base/icons/src/vender/line/general' +import { + Checklist, + XClose, +} from '@/app/components/base/icons/src/vender/line/general' +import { AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback' const WorkflowChecklist = () => { + const { t } = useTranslation() const [open, setOpen] = useState(false) + const nodes = useNodes() + const edges = useEdges() + const nodesExtraData = useNodesExtraData() + const { handleNodeSelect } = useNodesInteractions() + const buildInTools = useStore(s => s.buildInTools) + const customTools = useStore(s => s.customTools) + + const needWarningNodes = useMemo(() => { + const list = [] + + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i] + const incomers = getIncomers(node, nodes, edges) + const outgoers = getOutgoers(node, nodes, edges) + const { errorMessage } = nodesExtraData[node.data.type].checkValid(node.data, t) + let toolIcon + + if (node.data.type === BlockEnum.Tool) { + if (node.data.provider_type === 'builtin') + toolIcon = buildInTools.find(tool => tool.id === node.data.provider_id)?.icon + + if (node.data.provider_type === 'custom') + toolIcon = customTools.find(tool => tool.id === node.data.provider_id)?.icon + } + + if (errorMessage || ((!incomers.length && !outgoers.length))) { + list.push({ + id: node.id, + type: node.data.type, + title: node.data.title, + toolIcon, + unConnected: !incomers.length && !outgoers.length, + errorMessage, + }) + } + } + + return list + }, [t, nodes, edges, nodesExtraData, buildInTools, customTools]) return ( { onOpenChange={setOpen} > setOpen(v => !v)}> -
+
{ } />
+
+ {needWarningNodes.length} +
- + +
+
+
{t('workflow.panel.checklist')}({needWarningNodes.length})
+
setOpen(false)} + > + +
+
+
+
{t('workflow.panel.checklistTip')}
+
+ { + needWarningNodes.map(node => ( +
handleNodeSelect(node.id)} + > +
+ + {node.title} +
+ { + node.unConnected && ( +
+
+ + {t('workflow.common.needConnecttip')} +
+
+ ) + } + { + node.errorMessage && ( +
+
+ + {node.errorMessage} +
+
+ ) + } +
+ )) + } +
+
+
+
) } diff --git a/web/app/components/workflow/header/index.tsx b/web/app/components/workflow/header/index.tsx index 4cd829fe67..27c54f71c0 100644 --- a/web/app/components/workflow/header/index.tsx +++ b/web/app/components/workflow/header/index.tsx @@ -119,7 +119,14 @@ const Header: FC = () => { {t('workflow.common.features')} - + { + !nodesReadOnly && ( + <> +
+ + + ) + }
) } @@ -150,7 +157,6 @@ const Header: FC = () => { > {t('workflow.common.restore')} -
) } diff --git a/web/app/components/workflow/panel/index.tsx b/web/app/components/workflow/panel/index.tsx index 618feddace..88426a3a0b 100644 --- a/web/app/components/workflow/panel/index.tsx +++ b/web/app/components/workflow/panel/index.tsx @@ -8,7 +8,6 @@ import type { CommonNodeType } from '../types' import { Panel as NodePanel } from '../nodes' import { useStore } from '../store' import { useIsChatMode } from '../hooks' -import WorkflowInfo from './workflow-info' import DebugAndPreview from './debug-and-preview' import RunHistory from './run-history' import Record from './record' @@ -28,13 +27,11 @@ const Panel: FC = () => { const historyWorkflowData = useStore(s => s.historyWorkflowData) const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal } = useAppStore() const { - showWorkflowInfoPanel, showNodePanel, showDebugAndPreviewPanel, showWorkflowPreview, } = useMemo(() => { return { - showWorkflowInfoPanel: !selectedNode && !workflowRunningData && !historyWorkflowData, showNodePanel: !!selectedNode && !workflowRunningData && !historyWorkflowData, showDebugAndPreviewPanel: isChatMode && workflowRunningData && !historyWorkflowData, showWorkflowPreview: !isChatMode && workflowRunningData && !historyWorkflowData, @@ -91,11 +88,6 @@ const Panel: FC = () => { ) } - { - showWorkflowInfoPanel && ( - - ) - } { showRunHistory && ( diff --git a/web/app/components/workflow/panel/workflow-info.tsx b/web/app/components/workflow/panel/workflow-info.tsx deleted file mode 100644 index a4e720a631..0000000000 --- a/web/app/components/workflow/panel/workflow-info.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { - memo, - useMemo, -} from 'react' -import { useTranslation } from 'react-i18next' -import { - getIncomers, - getOutgoers, - useEdges, - useNodes, -} from 'reactflow' -import BlockIcon from '../block-icon' -import { useNodesExtraData } from '../hooks' -import type { CommonNodeType } from '../types' -import { BlockEnum } from '../types' -import { useStore } from '../store' -import { AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback' -import { FileCheck02 } from '@/app/components/base/icons/src/vender/line/files' -import { useStore as useAppStore } from '@/app/components/app/store' -import AppIcon from '@/app/components/base/app-icon' - -const WorkflowInfo = () => { - const { t } = useTranslation() - const appDetail = useAppStore(state => state.appDetail) - const nodes = useNodes() - const edges = useEdges() - const nodesExtraData = useNodesExtraData() - const buildInTools = useStore(s => s.buildInTools) - const customTools = useStore(s => s.customTools) - const needConnectNodes = nodes.filter((node) => { - const incomers = getIncomers(node, nodes, edges) - const outgoers = getOutgoers(node, nodes, edges) - - return !incomers.length && !outgoers.length - }) - - const needWarningNodes = useMemo(() => { - const list = [] - - for (let i = 0; i < nodes.length; i++) { - const node = nodes[i] - const incomers = getIncomers(node, nodes, edges) - const outgoers = getOutgoers(node, nodes, edges) - const { errorMessage } = nodesExtraData[node.data.type].checkValid(node.data, t) - let toolIcon - - if (node.data.type === BlockEnum.Tool) { - if (node.data.provider_type === 'builtin') - toolIcon = buildInTools.find(tool => tool.id === node.data.provider_id)?.icon - - if (node.data.provider_type === 'custom') - toolIcon = customTools.find(tool => tool.id === node.data.provider_id)?.icon - } - - if (errorMessage || ((!incomers.length && !outgoers.length))) { - list.push({ - id: node.id, - type: node.data.type, - title: node.data.title, - toolIcon, - unConnected: !incomers.length && !outgoers.length, - errorMessage, - }) - } - } - - return list - }, [t, nodes, edges, nodesExtraData, buildInTools, customTools]) - - if (!appDetail) - return null - - return ( -
-
-
- -
- {appDetail.name} -
-
-
- {appDetail.description} -
-
- - {t('workflow.panel.checklist')}({needConnectNodes.length}) -
-
-
-
- {t('workflow.panel.checklistTip')} -
-
- { - needWarningNodes.map(node => ( -
-
- - {node.title} -
- { - node.unConnected && ( -
-
- - {t('workflow.common.needConnecttip')} -
-
- ) - } - { - node.errorMessage && ( -
-
- - {node.errorMessage} -
-
- ) - } -
- )) - } -
-
-
- ) -} - -export default memo(WorkflowInfo) From a8236a270ab79e83cd19ca5839b689e751080bad Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 29 Mar 2024 18:19:37 +0800 Subject: [PATCH 5/9] feat: body to json editor --- .../nodes/_base/components/prompt/editor.tsx | 1 - .../nodes/http/components/edit-body/index.tsx | 34 ++++++++++++++----- .../components/workflow/nodes/http/panel.tsx | 1 + 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx b/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx index c2276e10ed..497b62fb7d 100644 --- a/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx +++ b/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx @@ -19,7 +19,6 @@ import TooltipPlus from '@/app/components/base/tooltip-plus' type Props = { title: string | JSX.Element value: string - variables: string[] onChange: (value: string) => void readOnly?: boolean showRemove?: boolean diff --git a/web/app/components/workflow/nodes/http/components/edit-body/index.tsx b/web/app/components/workflow/nodes/http/components/edit-body/index.tsx index 06b9feca13..34753218b4 100644 --- a/web/app/components/workflow/nodes/http/components/edit-body/index.tsx +++ b/web/app/components/workflow/nodes/http/components/edit-body/index.tsx @@ -8,11 +8,14 @@ import { BodyType } from '../../types' import useKeyValueList from '../../hooks/use-key-value-list' import KeyValue from '../key-value' import TextEditor from '../../../_base/components/editor/text-editor' -import CodeEditor from '../../../_base/components/editor/code-editor' -import { CodeLanguage } from '../../../code/types' +import useAvailableVarList from '../../../_base/hooks/use-available-var-list' +import InputWithVar from '@/app/components/workflow/nodes/_base/components/prompt/editor' +import type { Var } from '@/app/components/workflow/types' +import { VarType } from '@/app/components/workflow/types' type Props = { readonly: boolean + nodeId: string payload: Body onChange: (payload: Body) => void } @@ -34,10 +37,17 @@ const bodyTextMap = { const EditBody: FC = ({ readonly, + nodeId, payload, onChange, }) => { const { type } = payload + const availableVarList = useAvailableVarList(nodeId, { + onlyLeafNodeVar: false, + filterVar: (varPayload: Var) => { + return [VarType.string, VarType.number].includes(varPayload.type) + }, + }) const handleTypeChange = useCallback((e: React.ChangeEvent) => { const newType = e.target.value as BodyType @@ -111,11 +121,10 @@ const EditBody: FC = ({ {(type === BodyType.formData || type === BodyType.xWwwFormUrlencoded) && ( )} @@ -130,11 +139,18 @@ const EditBody: FC = ({ )} {type === BodyType.json && ( - JSON
} - value={payload.data} onChange={handleBodyValueChange} - language={CodeLanguage.json} + // JSON
} + // value={payload.data} onChange={handleBodyValueChange} + // language={CodeLanguage.json} + // /> + )}
diff --git a/web/app/components/workflow/nodes/http/panel.tsx b/web/app/components/workflow/nodes/http/panel.tsx index d2649962e4..91ea68cd2c 100644 --- a/web/app/components/workflow/nodes/http/panel.tsx +++ b/web/app/components/workflow/nodes/http/panel.tsx @@ -105,6 +105,7 @@ const Panel: FC> = ({ title={t(`${i18nPrefix}.body`)} > Date: Fri, 29 Mar 2024 18:24:46 +0800 Subject: [PATCH 6/9] fix style of app card --- web/app/(commonLayout)/apps/AppCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/(commonLayout)/apps/AppCard.tsx b/web/app/(commonLayout)/apps/AppCard.tsx index 754f1ffdc2..6f28372267 100644 --- a/web/app/(commonLayout)/apps/AppCard.tsx +++ b/web/app/(commonLayout)/apps/AppCard.tsx @@ -215,7 +215,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { e.preventDefault() getRedirection(isCurrentWorkspaceManager, app, push) }} - className='group flex col-span-1 bg-white border-2 border-solid border-transparent rounded-lg shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg' + className='group flex col-span-1 bg-white border-2 border-solid border-transparent rounded-xl shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg' >
From 7c45f369d19a46eb13111cb54c7c201b5262ebb0 Mon Sep 17 00:00:00 2001 From: StyleZhang Date: Fri, 29 Mar 2024 18:27:17 +0800 Subject: [PATCH 7/9] checklist --- .../vender/line/general/checklist-square.svg | 5 + .../vender/line/general/ChecklistSquare.json | 36 ++++++ .../vender/line/general/ChecklistSquare.tsx | 16 +++ .../icons/src/vender/line/general/index.ts | 1 + .../components/workflow/header/checklist.tsx | 108 +++++++++++------- web/i18n/en-US/workflow.ts | 1 + web/i18n/zh-Hans/workflow.ts | 1 + 7 files changed, 126 insertions(+), 42 deletions(-) create mode 100644 web/app/components/base/icons/assets/vender/line/general/checklist-square.svg create mode 100644 web/app/components/base/icons/src/vender/line/general/ChecklistSquare.json create mode 100644 web/app/components/base/icons/src/vender/line/general/ChecklistSquare.tsx diff --git a/web/app/components/base/icons/assets/vender/line/general/checklist-square.svg b/web/app/components/base/icons/assets/vender/line/general/checklist-square.svg new file mode 100644 index 0000000000..8fdddfab67 --- /dev/null +++ b/web/app/components/base/icons/assets/vender/line/general/checklist-square.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/app/components/base/icons/src/vender/line/general/ChecklistSquare.json b/web/app/components/base/icons/src/vender/line/general/ChecklistSquare.json new file mode 100644 index 0000000000..737c69623d --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/general/ChecklistSquare.json @@ -0,0 +1,36 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "32", + "height": "32", + "viewBox": "0 0 32 32", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "checklist-square" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector", + "d": "M9.7823 11.9146C9.32278 11.6082 8.70191 11.7324 8.39554 12.1919C8.08918 12.6514 8.21333 13.2723 8.67285 13.5787L9.7823 11.9146ZM10.9151 13.8717L10.3603 14.7037C10.8019 14.9982 11.3966 14.8963 11.7151 14.4717L10.9151 13.8717ZM14.5226 10.7284C14.8539 10.2865 14.7644 9.65973 14.3225 9.32836C13.8807 8.99699 13.2539 9.08653 12.9225 9.52836L14.5226 10.7284ZM19.3333 11C18.781 11 18.3333 11.4477 18.3333 12C18.3333 12.5523 18.781 13 19.3333 13V11ZM22 13C22.5523 13 23 12.5523 23 12C23 11.4477 22.5523 11 22 11V13ZM19.3333 19C18.781 19 18.3333 19.4477 18.3333 20C18.3333 20.5523 18.781 21 19.3333 21V19ZM22 21C22.5523 21 23 20.5523 23 20C23 19.4477 22.5523 19 22 19V21ZM9.86913 19.9163C9.4096 19.6099 8.78873 19.7341 8.48238 20.1937C8.17602 20.6532 8.3002 21.274 8.75973 21.5804L9.86913 19.9163ZM11.0019 21.8734L10.4472 22.7054C10.8888 22.9998 11.4835 22.8979 11.8019 22.4734L11.0019 21.8734ZM14.6094 18.7301C14.9408 18.2883 14.8512 17.6615 14.4094 17.3301C13.9676 16.9987 13.3408 17.0883 13.0094 17.5301L14.6094 18.7301ZM6.18404 27.564L5.73005 28.455H5.73005L6.18404 27.564ZM4.43597 25.816L3.54497 26.27H3.54497L4.43597 25.816ZM27.564 25.816L28.455 26.27L27.564 25.816ZM25.816 27.564L26.27 28.455L25.816 27.564ZM25.816 4.43597L26.27 3.54497V3.54497L25.816 4.43597ZM27.564 6.18404L28.455 5.73005V5.73005L27.564 6.18404ZM6.18404 4.43597L5.73005 3.54497L6.18404 4.43597ZM4.43597 6.18404L3.54497 5.73005L4.43597 6.18404ZM8.67285 13.5787L10.3603 14.7037L11.4698 13.0397L9.7823 11.9146L8.67285 13.5787ZM11.7151 14.4717L14.5226 10.7284L12.9225 9.52836L10.1151 13.2717L11.7151 14.4717ZM19.3333 13H22V11H19.3333V13ZM19.3333 21H22V19H19.3333V21ZM8.75973 21.5804L10.4472 22.7054L11.5566 21.0413L9.86913 19.9163L8.75973 21.5804ZM11.8019 22.4734L14.6094 18.7301L13.0094 17.5301L10.2019 21.2733L11.8019 22.4734ZM10.4 5H21.6V3H10.4V5ZM27 10.4V21.6H29V10.4H27ZM21.6 27H10.4V29H21.6V27ZM5 21.6V10.4H3V21.6H5ZM10.4 27C9.26339 27 8.47108 26.9992 7.85424 26.9488C7.24907 26.8994 6.90138 26.8072 6.63803 26.673L5.73005 28.455C6.32234 28.7568 6.96253 28.8826 7.69138 28.9422C8.40855 29.0008 9.2964 29 10.4 29V27ZM3 21.6C3 22.7036 2.99922 23.5914 3.05782 24.3086C3.11737 25.0375 3.24318 25.6777 3.54497 26.27L5.32698 25.362C5.19279 25.0986 5.10062 24.7509 5.05118 24.1458C5.00078 23.5289 5 22.7366 5 21.6H3ZM6.63803 26.673C6.07354 26.3854 5.6146 25.9265 5.32698 25.362L3.54497 26.27C4.02433 27.2108 4.78924 27.9757 5.73005 28.455L6.63803 26.673ZM27 21.6C27 22.7366 26.9992 23.5289 26.9488 24.1458C26.8994 24.7509 26.8072 25.0986 26.673 25.362L28.455 26.27C28.7568 25.6777 28.8826 25.0375 28.9422 24.3086C29.0008 23.5914 29 22.7036 29 21.6H27ZM21.6 29C22.7036 29 23.5914 29.0008 24.3086 28.9422C25.0375 28.8826 25.6777 28.7568 26.27 28.455L25.362 26.673C25.0986 26.8072 24.7509 26.8994 24.1458 26.9488C23.5289 26.9992 22.7366 27 21.6 27V29ZM26.673 25.362C26.3854 25.9265 25.9265 26.3854 25.362 26.673L26.27 28.455C27.2108 27.9757 27.9757 27.2108 28.455 26.27L26.673 25.362ZM21.6 5C22.7366 5 23.5289 5.00078 24.1458 5.05118C24.7509 5.10062 25.0986 5.19279 25.362 5.32698L26.27 3.54497C25.6777 3.24318 25.0375 3.11737 24.3086 3.05782C23.5914 2.99922 22.7036 3 21.6 3V5ZM29 10.4C29 9.2964 29.0008 8.40855 28.9422 7.69138C28.8826 6.96253 28.7568 6.32234 28.455 5.73005L26.673 6.63803C26.8072 6.90138 26.8994 7.24907 26.9488 7.85424C26.9992 8.47108 27 9.26339 27 10.4H29ZM25.362 5.32698C25.9265 5.6146 26.3854 6.07354 26.673 6.63803L28.455 5.73005C27.9757 4.78924 27.2108 4.02433 26.27 3.54497L25.362 5.32698ZM10.4 3C9.2964 3 8.40855 2.99922 7.69138 3.05782C6.96253 3.11737 6.32234 3.24318 5.73005 3.54497L6.63803 5.32698C6.90138 5.19279 7.24907 5.10062 7.85424 5.05118C8.47108 5.00078 9.26339 5 10.4 5V3ZM5 10.4C5 9.26339 5.00078 8.47108 5.05118 7.85424C5.10062 7.24907 5.19279 6.90138 5.32698 6.63803L3.54497 5.73005C3.24318 6.32234 3.11737 6.96253 3.05782 7.69138C2.99922 8.40855 3 9.2964 3 10.4H5ZM5.73005 3.54497C4.78924 4.02433 4.02433 4.78924 3.54497 5.73005L5.32698 6.63803C5.6146 6.07354 6.07354 5.6146 6.63803 5.32698L5.73005 3.54497Z", + "fill": "currentColor" + }, + "children": [] + } + ] + } + ] + }, + "name": "ChecklistSquare" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/line/general/ChecklistSquare.tsx b/web/app/components/base/icons/src/vender/line/general/ChecklistSquare.tsx new file mode 100644 index 0000000000..6dc7c67950 --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/general/ChecklistSquare.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './ChecklistSquare.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' + +const Icon = React.forwardRef, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'ChecklistSquare' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/line/general/index.ts b/web/app/components/base/icons/src/vender/line/general/index.ts index 7460282d72..8369117eed 100644 --- a/web/app/components/base/icons/src/vender/line/general/index.ts +++ b/web/app/components/base/icons/src/vender/line/general/index.ts @@ -3,6 +3,7 @@ export { default as Bookmark } from './Bookmark' export { default as CheckCircle } from './CheckCircle' export { default as CheckDone01 } from './CheckDone01' export { default as Check } from './Check' +export { default as ChecklistSquare } from './ChecklistSquare' export { default as Checklist } from './Checklist' export { default as DotsGrid } from './DotsGrid' export { default as DotsHorizontal } from './DotsHorizontal' diff --git a/web/app/components/workflow/header/checklist.tsx b/web/app/components/workflow/header/checklist.tsx index 8fe99180f6..4c3d3c6a81 100644 --- a/web/app/components/workflow/header/checklist.tsx +++ b/web/app/components/workflow/header/checklist.tsx @@ -25,6 +25,7 @@ import { } from '@/app/components/base/portal-to-follow-elem' import { Checklist, + ChecklistSquare, XClose, } from '@/app/components/base/icons/src/vender/line/general' import { AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback' @@ -98,15 +99,24 @@ const WorkflowChecklist = () => { } />
-
- {needWarningNodes.length} -
+ { + !!needWarningNodes.length && ( +
+ {needWarningNodes.length} +
+ ) + }
-
-
-
{t('workflow.panel.checklist')}({needWarningNodes.length})
+
+
+
{t('workflow.panel.checklist')}{needWarningNodes.length ? `(${needWarningNodes.length})` : ''}
setOpen(false)} @@ -115,47 +125,61 @@ const WorkflowChecklist = () => {
-
{t('workflow.panel.checklistTip')}
-
- { - needWarningNodes.map(node => ( -
handleNodeSelect(node.id)} - > -
- - {node.title} -
+ { + !!needWarningNodes.length && ( + <> +
{t('workflow.panel.checklistTip')}
+
{ - node.unConnected && ( -
-
- - {t('workflow.common.needConnecttip')} + needWarningNodes.map(node => ( +
handleNodeSelect(node.id)} + > +
+ + {node.title}
+ { + node.unConnected && ( +
+
+ + {t('workflow.common.needConnecttip')} +
+
+ ) + } + { + node.errorMessage && ( +
+
+ + {node.errorMessage} +
+
+ ) + }
- ) - } - { - node.errorMessage && ( -
-
- - {node.errorMessage} -
-
- ) + )) }
- )) - } -
+ + ) + } + { + !needWarningNodes.length && ( +
+ + {t('workflow.panel.checklistResolved')} +
+ ) + }
diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index d5477582a9..5d6c02db82 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -110,6 +110,7 @@ const translation = { runThisStep: 'Run this step', checklist: 'Checklist', checklistTip: 'Make sure all issues are resolved before publishing', + checklistResolved: 'All issues are resolved', organizeBlocks: 'Organize blocks', change: 'Change', }, diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 2762908788..d6c64ccdbf 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -110,6 +110,7 @@ const translation = { runThisStep: '运行此步骤', checklist: '检查清单', checklistTip: '发布前确保所有问题均已解决', + checklistResolved: '所有问题均已解决', organizeBlocks: '整理节点', change: '更改', }, From 8a2d04b305c651062d2a22633f25827643578364 Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 29 Mar 2024 18:32:49 +0800 Subject: [PATCH 8/9] chore: llm editor bg and not flash --- .../workflow/nodes/_base/components/prompt/editor.tsx | 4 ++-- .../workflow/nodes/http/components/edit-body/index.tsx | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx b/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx index 497b62fb7d..e5e10f724b 100644 --- a/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx +++ b/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx @@ -77,8 +77,8 @@ const Editor: FC = ({ return (
-
-
+
+
{title}
diff --git a/web/app/components/workflow/nodes/http/components/edit-body/index.tsx b/web/app/components/workflow/nodes/http/components/edit-body/index.tsx index 34753218b4..daeac6e28f 100644 --- a/web/app/components/workflow/nodes/http/components/edit-body/index.tsx +++ b/web/app/components/workflow/nodes/http/components/edit-body/index.tsx @@ -64,8 +64,6 @@ const EditBody: FC = ({ list: body, setList: setBody, addItem: addBody, - isKeyValueEdit: isBodyKeyValueEdit, - toggleIsKeyValueEdit: toggleIsBodyKeyValueEdit, } = useKeyValueList(payload.data, (value) => { const newBody = produce(payload, (draft: Body) => { draft.data = value @@ -151,6 +149,7 @@ const EditBody: FC = ({ onChange={handleBodyValueChange} justVar nodesOutputVars={availableVarList} + readOnly={readonly} /> )}
From 971436d935ccc87bc05b8c431f1acca2817f7880 Mon Sep 17 00:00:00 2001 From: takatost Date: Fri, 29 Mar 2024 18:44:21 +0800 Subject: [PATCH 9/9] llm and answer node support inner variable template --- api/core/prompt/advanced_prompt_transform.py | 8 ++- .../prompt/utils/prompt_template_parser.py | 17 +++-- api/core/workflow/nodes/answer/answer_node.py | 37 +++-------- api/core/workflow/nodes/answer/entities.py | 2 - api/core/workflow/nodes/llm/entities.py | 2 - api/core/workflow/nodes/llm/llm_node.py | 27 +++++--- .../question_classifier_node.py | 2 +- api/core/workflow/utils/__init__.py | 0 .../utils/variable_template_parser.py | 58 +++++++++++++++++ api/services/workflow/workflow_converter.py | 58 ++++++++++------- .../workflow/nodes/test_llm.py | 21 +------ .../core/workflow/nodes/test_answer.py | 13 +--- .../workflow/test_workflow_converter.py | 62 +++++++++---------- 13 files changed, 172 insertions(+), 135 deletions(-) create mode 100644 api/core/workflow/utils/__init__.py create mode 100644 api/core/workflow/utils/variable_template_parser.py diff --git a/api/core/prompt/advanced_prompt_transform.py b/api/core/prompt/advanced_prompt_transform.py index e50ce8ab06..674ba29b6e 100644 --- a/api/core/prompt/advanced_prompt_transform.py +++ b/api/core/prompt/advanced_prompt_transform.py @@ -21,6 +21,8 @@ class AdvancedPromptTransform(PromptTransform): """ Advanced Prompt Transform for Workflow LLM Node. """ + def __init__(self, with_variable_tmpl: bool = False) -> None: + self.with_variable_tmpl = with_variable_tmpl def get_prompt(self, prompt_template: Union[list[ChatModelMessage], CompletionModelPromptTemplate], inputs: dict, @@ -74,7 +76,7 @@ class AdvancedPromptTransform(PromptTransform): prompt_messages = [] - prompt_template = PromptTemplateParser(template=raw_prompt) + prompt_template = PromptTemplateParser(template=raw_prompt, with_variable_tmpl=self.with_variable_tmpl) prompt_inputs = {k: inputs[k] for k in prompt_template.variable_keys if k in inputs} prompt_inputs = self._set_context_variable(context, prompt_template, prompt_inputs) @@ -128,7 +130,7 @@ class AdvancedPromptTransform(PromptTransform): for prompt_item in raw_prompt_list: raw_prompt = prompt_item.text - prompt_template = PromptTemplateParser(template=raw_prompt) + prompt_template = PromptTemplateParser(template=raw_prompt, with_variable_tmpl=self.with_variable_tmpl) prompt_inputs = {k: inputs[k] for k in prompt_template.variable_keys if k in inputs} prompt_inputs = self._set_context_variable(context, prompt_template, prompt_inputs) @@ -211,7 +213,7 @@ class AdvancedPromptTransform(PromptTransform): if '#histories#' in prompt_template.variable_keys: if memory: inputs = {'#histories#': '', **prompt_inputs} - prompt_template = PromptTemplateParser(raw_prompt) + prompt_template = PromptTemplateParser(template=raw_prompt, with_variable_tmpl=self.with_variable_tmpl) prompt_inputs = {k: inputs[k] for k in prompt_template.variable_keys if k in inputs} tmp_human_message = UserPromptMessage( content=prompt_template.format(prompt_inputs) diff --git a/api/core/prompt/utils/prompt_template_parser.py b/api/core/prompt/utils/prompt_template_parser.py index 454f92e3b7..3e68492df2 100644 --- a/api/core/prompt/utils/prompt_template_parser.py +++ b/api/core/prompt/utils/prompt_template_parser.py @@ -1,6 +1,9 @@ import re REGEX = re.compile(r"\{\{([a-zA-Z_][a-zA-Z0-9_]{0,29}|#histories#|#query#|#context#)\}\}") +WITH_VARIABLE_TMPL_REGEX = re.compile( + r"\{\{([a-zA-Z_][a-zA-Z0-9_]{0,29}|#[a-zA-Z0-9_]{1,50}\.[a-zA-Z0-9_\.]{1,100}#|#histories#|#query#|#context#)\}\}" +) class PromptTemplateParser: @@ -15,13 +18,15 @@ class PromptTemplateParser: `{{#histories#}}` `{{#query#}}` `{{#context#}}`. No other `{{##}}` template variables are allowed. """ - def __init__(self, template: str): + def __init__(self, template: str, with_variable_tmpl: bool = False): self.template = template + self.with_variable_tmpl = with_variable_tmpl + self.regex = WITH_VARIABLE_TMPL_REGEX if with_variable_tmpl else REGEX self.variable_keys = self.extract() def extract(self) -> list: # Regular expression to match the template rules - return re.findall(REGEX, self.template) + return re.findall(self.regex, self.template) def format(self, inputs: dict, remove_template_variables: bool = True) -> str: def replacer(match): @@ -29,12 +34,12 @@ class PromptTemplateParser: value = inputs.get(key, match.group(0)) # return original matched string if key not found if remove_template_variables: - return PromptTemplateParser.remove_template_variables(value) + return PromptTemplateParser.remove_template_variables(value, self.with_variable_tmpl) return value - prompt = re.sub(REGEX, replacer, self.template) + prompt = re.sub(self.regex, replacer, self.template) return re.sub(r'<\|.*?\|>', '', prompt) @classmethod - def remove_template_variables(cls, text: str): - return re.sub(REGEX, r'{\1}', text) + def remove_template_variables(cls, text: str, with_variable_tmpl: bool = False): + return re.sub(WITH_VARIABLE_TMPL_REGEX if with_variable_tmpl else REGEX, r'{\1}', text) diff --git a/api/core/workflow/nodes/answer/answer_node.py b/api/core/workflow/nodes/answer/answer_node.py index 7a98150aab..9194d3fef7 100644 --- a/api/core/workflow/nodes/answer/answer_node.py +++ b/api/core/workflow/nodes/answer/answer_node.py @@ -13,6 +13,7 @@ from core.workflow.nodes.answer.entities import ( VarGenerateRouteChunk, ) from core.workflow.nodes.base_node import BaseNode +from core.workflow.utils.variable_template_parser import VariableTemplateParser from models.workflow import WorkflowNodeExecutionStatus @@ -66,32 +67,8 @@ class AnswerNode(BaseNode): part = cast(TextGenerateRouteChunk, part) answer += part.text - # re-fetch variable values - variable_values = {} - for variable_selector in node_data.variables: - value = variable_pool.get_variable_value( - variable_selector=variable_selector.value_selector - ) - - if isinstance(value, str | int | float): - value = str(value) - elif isinstance(value, FileVar): - value = value.to_dict() - elif isinstance(value, list): - new_value = [] - for item in value: - if isinstance(item, FileVar): - new_value.append(item.to_dict()) - else: - new_value.append(item) - - value = new_value - - variable_values[variable_selector.variable] = value - return NodeRunResult( status=WorkflowNodeExecutionStatus.SUCCEEDED, - inputs=variable_values, outputs={ "answer": answer } @@ -116,15 +93,18 @@ class AnswerNode(BaseNode): :param node_data: node data object :return: """ + variable_template_parser = VariableTemplateParser(template=node_data.answer) + variable_selectors = variable_template_parser.extract_variable_selectors() + value_selector_mapping = { variable_selector.variable: variable_selector.value_selector - for variable_selector in node_data.variables + for variable_selector in variable_selectors } variable_keys = list(value_selector_mapping.keys()) # format answer template - template_parser = PromptTemplateParser(node_data.answer) + template_parser = PromptTemplateParser(template=node_data.answer, with_variable_tmpl=True) template_variable_keys = template_parser.variable_keys # Take the intersection of variable_keys and template_variable_keys @@ -164,8 +144,11 @@ class AnswerNode(BaseNode): """ node_data = cast(cls._node_data_cls, node_data) + variable_template_parser = VariableTemplateParser(template=node_data.answer) + variable_selectors = variable_template_parser.extract_variable_selectors() + variable_mapping = {} - for variable_selector in node_data.variables: + for variable_selector in variable_selectors: variable_mapping[variable_selector.variable] = variable_selector.value_selector return variable_mapping diff --git a/api/core/workflow/nodes/answer/entities.py b/api/core/workflow/nodes/answer/entities.py index 8aed752ccb..9effbbbe67 100644 --- a/api/core/workflow/nodes/answer/entities.py +++ b/api/core/workflow/nodes/answer/entities.py @@ -2,14 +2,12 @@ from pydantic import BaseModel from core.workflow.entities.base_node_data_entities import BaseNodeData -from core.workflow.entities.variable_entities import VariableSelector class AnswerNodeData(BaseNodeData): """ Answer Node Data. """ - variables: list[VariableSelector] = [] answer: str diff --git a/api/core/workflow/nodes/llm/entities.py b/api/core/workflow/nodes/llm/entities.py index 67163c93cd..c390aaf8c9 100644 --- a/api/core/workflow/nodes/llm/entities.py +++ b/api/core/workflow/nodes/llm/entities.py @@ -4,7 +4,6 @@ from pydantic import BaseModel from core.prompt.entities.advanced_prompt_entities import ChatModelMessage, CompletionModelPromptTemplate, MemoryConfig from core.workflow.entities.base_node_data_entities import BaseNodeData -from core.workflow.entities.variable_entities import VariableSelector class ModelConfig(BaseModel): @@ -44,7 +43,6 @@ class LLMNodeData(BaseNodeData): LLM Node Data. """ model: ModelConfig - variables: list[VariableSelector] = [] prompt_template: Union[list[ChatModelMessage], CompletionModelPromptTemplate] memory: Optional[MemoryConfig] = None context: ContextConfig diff --git a/api/core/workflow/nodes/llm/llm_node.py b/api/core/workflow/nodes/llm/llm_node.py index cc49a22020..c0049c5bb3 100644 --- a/api/core/workflow/nodes/llm/llm_node.py +++ b/api/core/workflow/nodes/llm/llm_node.py @@ -15,13 +15,14 @@ from core.model_runtime.entities.model_entities import ModelType from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel from core.model_runtime.utils.encoders import jsonable_encoder from core.prompt.advanced_prompt_transform import AdvancedPromptTransform -from core.prompt.entities.advanced_prompt_entities import MemoryConfig +from core.prompt.entities.advanced_prompt_entities import CompletionModelPromptTemplate, MemoryConfig from core.prompt.utils.prompt_message_util import PromptMessageUtil from core.workflow.entities.base_node_data_entities import BaseNodeData from core.workflow.entities.node_entities import NodeRunMetadataKey, NodeRunResult, NodeType, SystemVariable from core.workflow.entities.variable_pool import VariablePool from core.workflow.nodes.base_node import BaseNode from core.workflow.nodes.llm.entities import LLMNodeData, ModelConfig +from core.workflow.utils.variable_template_parser import VariableTemplateParser from extensions.ext_database import db from models.model import Conversation from models.provider import Provider, ProviderType @@ -48,9 +49,7 @@ class LLMNode(BaseNode): # fetch variables and fetch values from variable pool inputs = self._fetch_inputs(node_data, variable_pool) - node_inputs = { - **inputs - } + node_inputs = {} # fetch files files: list[FileVar] = self._fetch_files(node_data, variable_pool) @@ -192,10 +191,21 @@ class LLMNode(BaseNode): :return: """ inputs = {} - for variable_selector in node_data.variables: + prompt_template = node_data.prompt_template + + variable_selectors = [] + if isinstance(prompt_template, list): + for prompt in prompt_template: + variable_template_parser = VariableTemplateParser(template=prompt.text) + variable_selectors.extend(variable_template_parser.extract_variable_selectors()) + elif isinstance(prompt_template, CompletionModelPromptTemplate): + variable_template_parser = VariableTemplateParser(template=prompt_template.text) + variable_selectors = variable_template_parser.extract_variable_selectors() + + for variable_selector in variable_selectors: variable_value = variable_pool.get_variable_value(variable_selector.value_selector) if variable_value is None: - raise ValueError(f'Variable {variable_selector.value_selector} not found') + raise ValueError(f'Variable {variable_selector.variable} not found') inputs[variable_selector.variable] = variable_value @@ -411,7 +421,7 @@ class LLMNode(BaseNode): :param model_config: model config :return: """ - prompt_transform = AdvancedPromptTransform() + prompt_transform = AdvancedPromptTransform(with_variable_tmpl=True) prompt_messages = prompt_transform.get_prompt( prompt_template=node_data.prompt_template, inputs=inputs, @@ -486,9 +496,6 @@ class LLMNode(BaseNode): node_data = cast(cls._node_data_cls, node_data) variable_mapping = {} - for variable_selector in node_data.variables: - variable_mapping[variable_selector.variable] = variable_selector.value_selector - if node_data.context.enabled: variable_mapping['#context#'] = node_data.context.variable_selector diff --git a/api/core/workflow/nodes/question_classifier/question_classifier_node.py b/api/core/workflow/nodes/question_classifier/question_classifier_node.py index 01b2908f85..4cc698a840 100644 --- a/api/core/workflow/nodes/question_classifier/question_classifier_node.py +++ b/api/core/workflow/nodes/question_classifier/question_classifier_node.py @@ -128,7 +128,7 @@ class QuestionClassifierNode(LLMNode): :param model_config: model config :return: """ - prompt_transform = AdvancedPromptTransform() + prompt_transform = AdvancedPromptTransform(with_variable_tmpl=True) prompt_template = self._get_prompt_template(node_data, query) prompt_messages = prompt_transform.get_prompt( prompt_template=prompt_template, diff --git a/api/core/workflow/utils/__init__.py b/api/core/workflow/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/workflow/utils/variable_template_parser.py b/api/core/workflow/utils/variable_template_parser.py new file mode 100644 index 0000000000..23b8ce2974 --- /dev/null +++ b/api/core/workflow/utils/variable_template_parser.py @@ -0,0 +1,58 @@ +import re + +from core.workflow.entities.variable_entities import VariableSelector + +REGEX = re.compile(r"\{\{(#[a-zA-Z0-9_]{1,50}(\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10}#)\}\}") + + +class VariableTemplateParser: + """ + Rules: + + 1. Template variables must be enclosed in `{{}}`. + 2. The template variable Key can only be: #node_id.var1.var2#. + 3. The template variable Key cannot contain new lines or spaces, and must comply with rule 2. + """ + + def __init__(self, template: str): + self.template = template + self.variable_keys = self.extract() + + def extract(self) -> list: + # Regular expression to match the template rules + matches = re.findall(REGEX, self.template) + + first_group_matches = [match[0] for match in matches] + + return list(set(first_group_matches)) + + def extract_variable_selectors(self) -> list[VariableSelector]: + variable_selectors = [] + for variable_key in self.variable_keys: + remove_hash = variable_key.replace('#', '') + split_result = remove_hash.split('.') + if len(split_result) < 2: + continue + + variable_selectors.append(VariableSelector( + variable=variable_key, + value_selector=split_result + )) + + return variable_selectors + + def format(self, inputs: dict, remove_template_variables: bool = True) -> str: + def replacer(match): + key = match.group(1) + value = inputs.get(key, match.group(0)) # return original matched string if key not found + + if remove_template_variables: + return VariableTemplateParser.remove_template_variables(value) + return value + + prompt = re.sub(REGEX, replacer, self.template) + return re.sub(r'<\|.*?\|>', '', prompt) + + @classmethod + def remove_template_variables(cls, text: str): + return re.sub(REGEX, r'{\1}', text) diff --git a/api/services/workflow/workflow_converter.py b/api/services/workflow/workflow_converter.py index c7424f3f95..d597941ef6 100644 --- a/api/services/workflow/workflow_converter.py +++ b/api/services/workflow/workflow_converter.py @@ -291,7 +291,7 @@ class WorkflowConverter: if app_model.mode == AppMode.CHAT.value: http_request_variables.append({ "variable": "_query", - "value_selector": ["start", "sys.query"] + "value_selector": ["sys", ".query"] }) request_body = { @@ -375,7 +375,7 @@ class WorkflowConverter: """ retrieve_config = dataset_config.retrieve_config if new_app_mode == AppMode.ADVANCED_CHAT: - query_variable_selector = ["start", "sys.query"] + query_variable_selector = ["sys", "query"] elif retrieve_config.query_variable: # fetch query variable query_variable_selector = ["start", retrieve_config.query_variable] @@ -449,19 +449,31 @@ class WorkflowConverter: has_context=knowledge_retrieval_node is not None, query_in_prompt=False ) + + template = prompt_template_config['prompt_template'].template + for v in start_node['data']['variables']: + template = template.replace('{{' + v['variable'] + '}}', '{{#start.' + v['variable'] + '#}}') + prompts = [ { "role": 'user', - "text": prompt_template_config['prompt_template'].template + "text": template } ] else: advanced_chat_prompt_template = prompt_template.advanced_chat_prompt_template - prompts = [{ - "role": m.role.value, - "text": m.text - } for m in advanced_chat_prompt_template.messages] \ - if advanced_chat_prompt_template else [] + + prompts = [] + for m in advanced_chat_prompt_template.messages: + if advanced_chat_prompt_template: + text = m.text + for v in start_node['data']['variables']: + text = text.replace('{{' + v['variable'] + '}}', '{{#start.' + v['variable'] + '#}}') + + prompts.append({ + "role": m.role.value, + "text": text + }) # Completion Model else: if prompt_template.prompt_type == PromptTemplateEntity.PromptType.SIMPLE: @@ -475,8 +487,13 @@ class WorkflowConverter: has_context=knowledge_retrieval_node is not None, query_in_prompt=False ) + + template = prompt_template_config['prompt_template'].template + for v in start_node['data']['variables']: + template = template.replace('{{' + v['variable'] + '}}', '{{#start.' + v['variable'] + '#}}') + prompts = { - "text": prompt_template_config['prompt_template'].template + "text": template } prompt_rules = prompt_template_config['prompt_rules'] @@ -486,9 +503,16 @@ class WorkflowConverter: } else: advanced_completion_prompt_template = prompt_template.advanced_completion_prompt_template + if advanced_completion_prompt_template: + text = advanced_completion_prompt_template.prompt + for v in start_node['data']['variables']: + text = text.replace('{{' + v['variable'] + '}}', '{{#start.' + v['variable'] + '#}}') + else: + text = "" + prompts = { - "text": advanced_completion_prompt_template.prompt, - } if advanced_completion_prompt_template else {"text": ""} + "text": text, + } if advanced_completion_prompt_template.role_prefix: role_prefix = { @@ -519,10 +543,6 @@ class WorkflowConverter: "mode": model_config.mode, "completion_params": completion_params }, - "variables": [{ - "variable": v['variable'], - "value_selector": ["start", v['variable']] - } for v in start_node['data']['variables']], "prompt_template": prompts, "memory": memory, "context": { @@ -532,7 +552,7 @@ class WorkflowConverter: }, "vision": { "enabled": file_upload is not None, - "variable_selector": ["start", "sys.files"] if file_upload is not None else None, + "variable_selector": ["sys", "files"] if file_upload is not None else None, "configs": { "detail": file_upload.image_config['detail'] } if file_upload is not None else None @@ -571,11 +591,7 @@ class WorkflowConverter: "data": { "title": "ANSWER", "type": NodeType.ANSWER.value, - "variables": [{ - "variable": "text", - "value_selector": ["llm", "text"] - }], - "answer": "{{text}}" + "answer": "{{#llm.text#}}" } } diff --git a/api/tests/integration_tests/workflow/nodes/test_llm.py b/api/tests/integration_tests/workflow/nodes/test_llm.py index 999ebf7734..73794336c2 100644 --- a/api/tests/integration_tests/workflow/nodes/test_llm.py +++ b/api/tests/integration_tests/workflow/nodes/test_llm.py @@ -40,32 +40,17 @@ def test_execute_llm(setup_openai_mock): 'mode': 'chat', 'completion_params': {} }, - 'variables': [ - { - 'variable': 'weather', - 'value_selector': ['abc', 'output'], - }, - { - 'variable': 'query', - 'value_selector': ['sys', 'query'] - } - ], 'prompt_template': [ { 'role': 'system', - 'text': 'you are a helpful assistant.\ntoday\'s weather is {{weather}}.' + 'text': 'you are a helpful assistant.\ntoday\'s weather is {{#abc.output#}}.' }, { 'role': 'user', - 'text': '{{query}}' + 'text': '{{#sys.query#}}' } ], - 'memory': { - 'window': { - 'enabled': True, - 'size': 2 - } - }, + 'memory': None, 'context': { 'enabled': False }, diff --git a/api/tests/unit_tests/core/workflow/nodes/test_answer.py b/api/tests/unit_tests/core/workflow/nodes/test_answer.py index 038fda9dac..e2d5be769c 100644 --- a/api/tests/unit_tests/core/workflow/nodes/test_answer.py +++ b/api/tests/unit_tests/core/workflow/nodes/test_answer.py @@ -4,7 +4,6 @@ from core.workflow.entities.node_entities import SystemVariable from core.workflow.entities.variable_pool import VariablePool from core.workflow.nodes.answer.answer_node import AnswerNode from core.workflow.nodes.base_node import UserFrom -from core.workflow.nodes.if_else.if_else_node import IfElseNode from extensions.ext_database import db from models.workflow import WorkflowNodeExecutionStatus @@ -21,17 +20,7 @@ def test_execute_answer(): 'data': { 'title': '123', 'type': 'answer', - 'variables': [ - { - 'value_selector': ['llm', 'text'], - 'variable': 'text' - }, - { - 'value_selector': ['start', 'weather'], - 'variable': 'weather' - }, - ], - 'answer': 'Today\'s weather is {{weather}}\n{{text}}\n{{img}}\nFin.' + 'answer': 'Today\'s weather is {{#start.weather#}}\n{{#llm.text#}}\n{{img}}\nFin.' } } ) diff --git a/api/tests/unit_tests/services/workflow/test_workflow_converter.py b/api/tests/unit_tests/services/workflow/test_workflow_converter.py index b4a4d6707a..6c1402a518 100644 --- a/api/tests/unit_tests/services/workflow/test_workflow_converter.py +++ b/api/tests/unit_tests/services/workflow/test_workflow_converter.py @@ -19,7 +19,7 @@ from services.workflow.workflow_converter import WorkflowConverter def default_variables(): return [ VariableEntity( - variable="text-input", + variable="text_input", label="text-input", type=VariableEntity.Type.TEXT_INPUT ), @@ -43,7 +43,7 @@ def test__convert_to_start_node(default_variables): # assert assert isinstance(result["data"]["variables"][0]["type"], str) assert result["data"]["variables"][0]["type"] == "text-input" - assert result["data"]["variables"][0]["variable"] == "text-input" + assert result["data"]["variables"][0]["variable"] == "text_input" assert result["data"]["variables"][1]["variable"] == "paragraph" assert result["data"]["variables"][2]["variable"] == "select" @@ -191,7 +191,7 @@ def test__convert_to_http_request_node_for_workflow_app(default_variables): def test__convert_to_knowledge_retrieval_node_for_chatbot(): - new_app_mode = AppMode.CHAT + new_app_mode = AppMode.ADVANCED_CHAT dataset_config = DatasetEntity( dataset_ids=["dataset_id_1", "dataset_id_2"], @@ -221,7 +221,7 @@ def test__convert_to_knowledge_retrieval_node_for_chatbot(): ) assert node["data"]["type"] == "knowledge-retrieval" - assert node["data"]["query_variable_selector"] == ["start", "sys.query"] + assert node["data"]["query_variable_selector"] == ["sys", "query"] assert node["data"]["dataset_ids"] == dataset_config.dataset_ids assert (node["data"]["retrieval_mode"] == dataset_config.retrieve_config.retrieve_strategy.value) @@ -276,7 +276,7 @@ def test__convert_to_knowledge_retrieval_node_for_workflow_app(): def test__convert_to_llm_node_for_chatbot_simple_chat_model(default_variables): - new_app_mode = AppMode.CHAT + new_app_mode = AppMode.ADVANCED_CHAT model = "gpt-4" model_mode = LLMMode.CHAT @@ -298,7 +298,7 @@ def test__convert_to_llm_node_for_chatbot_simple_chat_model(default_variables): prompt_template = PromptTemplateEntity( prompt_type=PromptTemplateEntity.PromptType.SIMPLE, - simple_prompt_template="You are a helpful assistant {{text-input}}, {{paragraph}}, {{select}}." + simple_prompt_template="You are a helpful assistant {{text_input}}, {{paragraph}}, {{select}}." ) llm_node = workflow_converter._convert_to_llm_node( @@ -311,16 +311,15 @@ def test__convert_to_llm_node_for_chatbot_simple_chat_model(default_variables): assert llm_node["data"]["type"] == "llm" assert llm_node["data"]["model"]['name'] == model assert llm_node["data"]['model']["mode"] == model_mode.value - assert llm_node["data"]["variables"] == [{ - "variable": v.variable, - "value_selector": ["start", v.variable] - } for v in default_variables] - assert llm_node["data"]["prompts"][0]['text'] == prompt_template.simple_prompt_template + '\n' + template = prompt_template.simple_prompt_template + for v in default_variables: + template = template.replace('{{' + v.variable + '}}', '{{#start.' + v.variable + '#}}') + assert llm_node["data"]["prompt_template"][0]['text'] == template + '\n' assert llm_node["data"]['context']['enabled'] is False def test__convert_to_llm_node_for_chatbot_simple_completion_model(default_variables): - new_app_mode = AppMode.CHAT + new_app_mode = AppMode.ADVANCED_CHAT model = "gpt-3.5-turbo-instruct" model_mode = LLMMode.COMPLETION @@ -342,7 +341,7 @@ def test__convert_to_llm_node_for_chatbot_simple_completion_model(default_variab prompt_template = PromptTemplateEntity( prompt_type=PromptTemplateEntity.PromptType.SIMPLE, - simple_prompt_template="You are a helpful assistant {{text-input}}, {{paragraph}}, {{select}}." + simple_prompt_template="You are a helpful assistant {{text_input}}, {{paragraph}}, {{select}}." ) llm_node = workflow_converter._convert_to_llm_node( @@ -355,16 +354,15 @@ def test__convert_to_llm_node_for_chatbot_simple_completion_model(default_variab assert llm_node["data"]["type"] == "llm" assert llm_node["data"]["model"]['name'] == model assert llm_node["data"]['model']["mode"] == model_mode.value - assert llm_node["data"]["variables"] == [{ - "variable": v.variable, - "value_selector": ["start", v.variable] - } for v in default_variables] - assert llm_node["data"]["prompts"]['text'] == prompt_template.simple_prompt_template + '\n' + template = prompt_template.simple_prompt_template + for v in default_variables: + template = template.replace('{{' + v.variable + '}}', '{{#start.' + v.variable + '#}}') + assert llm_node["data"]["prompt_template"]['text'] == template + '\n' assert llm_node["data"]['context']['enabled'] is False def test__convert_to_llm_node_for_chatbot_advanced_chat_model(default_variables): - new_app_mode = AppMode.CHAT + new_app_mode = AppMode.ADVANCED_CHAT model = "gpt-4" model_mode = LLMMode.CHAT @@ -404,17 +402,16 @@ def test__convert_to_llm_node_for_chatbot_advanced_chat_model(default_variables) assert llm_node["data"]["type"] == "llm" assert llm_node["data"]["model"]['name'] == model assert llm_node["data"]['model']["mode"] == model_mode.value - assert llm_node["data"]["variables"] == [{ - "variable": v.variable, - "value_selector": ["start", v.variable] - } for v in default_variables] - assert isinstance(llm_node["data"]["prompts"], list) - assert len(llm_node["data"]["prompts"]) == len(prompt_template.advanced_chat_prompt_template.messages) - assert llm_node["data"]["prompts"][0]['text'] == prompt_template.advanced_chat_prompt_template.messages[0].text + assert isinstance(llm_node["data"]["prompt_template"], list) + assert len(llm_node["data"]["prompt_template"]) == len(prompt_template.advanced_chat_prompt_template.messages) + template = prompt_template.advanced_chat_prompt_template.messages[0].text + for v in default_variables: + template = template.replace('{{' + v.variable + '}}', '{{#start.' + v.variable + '#}}') + assert llm_node["data"]["prompt_template"][0]['text'] == template def test__convert_to_llm_node_for_workflow_advanced_completion_model(default_variables): - new_app_mode = AppMode.CHAT + new_app_mode = AppMode.ADVANCED_CHAT model = "gpt-3.5-turbo-instruct" model_mode = LLMMode.COMPLETION @@ -456,9 +453,8 @@ def test__convert_to_llm_node_for_workflow_advanced_completion_model(default_var assert llm_node["data"]["type"] == "llm" assert llm_node["data"]["model"]['name'] == model assert llm_node["data"]['model']["mode"] == model_mode.value - assert llm_node["data"]["variables"] == [{ - "variable": v.variable, - "value_selector": ["start", v.variable] - } for v in default_variables] - assert isinstance(llm_node["data"]["prompts"], dict) - assert llm_node["data"]["prompts"]['text'] == prompt_template.advanced_completion_prompt_template.prompt + assert isinstance(llm_node["data"]["prompt_template"], dict) + template = prompt_template.advanced_completion_prompt_template.prompt + for v in default_variables: + template = template.replace('{{' + v.variable + '}}', '{{#start.' + v.variable + '#}}') + assert llm_node["data"]["prompt_template"]['text'] == template