mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-15 13:56:53 +08:00
FEAT: NEW WORKFLOW ENGINE (#3160)
Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: Yeuoly <admin@srmxy.cn> Co-authored-by: JzoNg <jzongcode@gmail.com> Co-authored-by: StyleZhang <jasonapring2015@outlook.com> Co-authored-by: jyong <jyong@dify.ai> Co-authored-by: nite-knite <nkCoding@gmail.com> Co-authored-by: jyong <718720800@qq.com>
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import type { PromptItem, ValueSelector, Var } from '../../../types'
|
||||
import { PromptRole } from '../../../types'
|
||||
import useAvailableVarList from '../../_base/hooks/use-available-var-list'
|
||||
import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor'
|
||||
import AddButton from '@/app/components/workflow/nodes/_base/components/add-button'
|
||||
import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
|
||||
const i18nPrefix = 'workflow.nodes.llm'
|
||||
|
||||
type Props = {
|
||||
readOnly: boolean
|
||||
nodeId: string
|
||||
filterVar: (payload: Var, selector: ValueSelector) => boolean
|
||||
isChatModel: boolean
|
||||
isChatApp: boolean
|
||||
payload: PromptItem | PromptItem[]
|
||||
onChange: (payload: PromptItem | PromptItem[]) => void
|
||||
isShowContext: boolean
|
||||
hasSetBlockStatus: {
|
||||
context: boolean
|
||||
history: boolean
|
||||
query: boolean
|
||||
}
|
||||
}
|
||||
|
||||
const ConfigPrompt: FC<Props> = ({
|
||||
readOnly,
|
||||
nodeId,
|
||||
filterVar,
|
||||
isChatModel,
|
||||
isChatApp,
|
||||
payload,
|
||||
onChange,
|
||||
isShowContext,
|
||||
hasSetBlockStatus,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
availableVars,
|
||||
availableNodes,
|
||||
} = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar,
|
||||
})
|
||||
|
||||
const handleChatModePromptChange = useCallback((index: number) => {
|
||||
return (prompt: string) => {
|
||||
const newPrompt = produce(payload as PromptItem[], (draft) => {
|
||||
draft[index].text = prompt
|
||||
})
|
||||
onChange(newPrompt)
|
||||
}
|
||||
}, [onChange, payload])
|
||||
|
||||
const roleOptions = [
|
||||
{
|
||||
label: 'system',
|
||||
value: PromptRole.system,
|
||||
},
|
||||
{
|
||||
label: 'user',
|
||||
value: PromptRole.user,
|
||||
},
|
||||
{
|
||||
label: 'assistant',
|
||||
value: PromptRole.assistant,
|
||||
},
|
||||
]
|
||||
|
||||
const handleChatModeMessageRoleChange = useCallback((index: number) => {
|
||||
return (role: PromptRole) => {
|
||||
const newPrompt = produce(payload as PromptItem[], (draft) => {
|
||||
draft[index].role = role
|
||||
})
|
||||
onChange(newPrompt)
|
||||
}
|
||||
}, [onChange, payload])
|
||||
|
||||
const handleAddPrompt = useCallback(() => {
|
||||
const newPrompt = produce(payload as PromptItem[], (draft) => {
|
||||
const isLastItemUser = draft[draft.length - 1].role === PromptRole.user
|
||||
draft.push({ role: isLastItemUser ? PromptRole.assistant : PromptRole.user, text: '' })
|
||||
})
|
||||
onChange(newPrompt)
|
||||
}, [onChange, payload])
|
||||
|
||||
const handleRemove = useCallback((index: number) => {
|
||||
return () => {
|
||||
const newPrompt = produce(payload as PromptItem[], (draft) => {
|
||||
draft.splice(index, 1)
|
||||
})
|
||||
onChange(newPrompt)
|
||||
}
|
||||
}, [onChange, payload])
|
||||
|
||||
const handleCompletionPromptChange = useCallback((prompt: string) => {
|
||||
const newPrompt = produce(payload as PromptItem, (draft) => {
|
||||
draft.text = prompt
|
||||
})
|
||||
onChange(newPrompt)
|
||||
}, [onChange, payload])
|
||||
|
||||
// console.log(getInputVars((payload as PromptItem).text))
|
||||
|
||||
return (
|
||||
<div>
|
||||
{(isChatModel && Array.isArray(payload))
|
||||
? (
|
||||
<div>
|
||||
<div className='space-y-2'>
|
||||
{
|
||||
(payload as PromptItem[]).map((item, index) => {
|
||||
return (
|
||||
<Editor
|
||||
instanceId={`${nodeId}-chat-workflow-llm-prompt-editor-${item.role}-${index}`}
|
||||
key={index}
|
||||
title={
|
||||
<div className='relative left-1 flex items-center'>
|
||||
<TypeSelector
|
||||
value={item.role as string}
|
||||
options={roleOptions}
|
||||
onChange={handleChatModeMessageRoleChange(index)}
|
||||
triggerClassName='text-xs font-semibold text-gray-700 uppercase'
|
||||
itemClassName='text-[13px] font-medium text-gray-700'
|
||||
/>
|
||||
<TooltipPlus
|
||||
popupContent={
|
||||
<div className='max-w-[180px]'>{t(`${i18nPrefix}.roleDescription.${item.role}`)}</div>
|
||||
}
|
||||
>
|
||||
<HelpCircle className='w-3.5 h-3.5 text-gray-400' />
|
||||
</TooltipPlus>
|
||||
</div>
|
||||
}
|
||||
value={item.text}
|
||||
onChange={handleChatModePromptChange(index)}
|
||||
readOnly={readOnly}
|
||||
showRemove={(payload as PromptItem[]).length > 1}
|
||||
onRemove={handleRemove(index)}
|
||||
isChatModel={isChatModel}
|
||||
isChatApp={isChatApp}
|
||||
isShowContext={isShowContext}
|
||||
hasSetBlockStatus={hasSetBlockStatus}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
</div>
|
||||
<AddButton
|
||||
className='mt-2'
|
||||
text={t(`${i18nPrefix}.addMessage`)}
|
||||
onClick={handleAddPrompt}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div>
|
||||
<Editor
|
||||
instanceId={`${nodeId}-chat-workflow-llm-prompt-editor`}
|
||||
title={<span className='capitalize'>{t(`${i18nPrefix}.prompt`)}</span>}
|
||||
value={(payload as PromptItem).text}
|
||||
onChange={handleCompletionPromptChange}
|
||||
readOnly={readOnly}
|
||||
isChatModel={isChatModel}
|
||||
isChatApp={isChatApp}
|
||||
isShowContext={isShowContext}
|
||||
hasSetBlockStatus={hasSetBlockStatus}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ConfigPrompt)
|
||||
@@ -0,0 +1,65 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import { Resolution } from '@/types/app'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.llm'
|
||||
|
||||
type ItemProps = {
|
||||
title: string
|
||||
value: Resolution
|
||||
onSelect: (value: Resolution) => void
|
||||
isSelected: boolean
|
||||
}
|
||||
|
||||
const Item: FC<ItemProps> = ({ title, value, onSelect, isSelected }) => {
|
||||
const handleSelect = useCallback(() => {
|
||||
if (isSelected)
|
||||
return
|
||||
onSelect(value)
|
||||
}, [value, onSelect, isSelected])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(isSelected ? 'bg-white border-[2px] border-primary-400 shadow-xs' : 'bg-gray-25 border border-gray-100', 'flex items-center h-8 px-3 rounded-xl text-[13px] font-normal text-gray-900 cursor-pointer')}
|
||||
onClick={handleSelect}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type Props = {
|
||||
value: Resolution
|
||||
onChange: (value: Resolution) => void
|
||||
}
|
||||
|
||||
const ResolutionPicker: FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='flex items-center'>
|
||||
<div className='mr-2 text-xs font-medium text-gray-500 uppercase'>{t(`${i18nPrefix}.resolution.name`)}</div>
|
||||
<div className='flex items-center space-x-1'>
|
||||
<Item
|
||||
title={t(`${i18nPrefix}.resolution.high`)}
|
||||
value={Resolution.high}
|
||||
onSelect={onChange}
|
||||
isSelected={value === Resolution.high}
|
||||
/>
|
||||
<Item
|
||||
title={t(`${i18nPrefix}.resolution.low`)}
|
||||
value={Resolution.low}
|
||||
onSelect={onChange}
|
||||
isSelected={value === Resolution.low}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ResolutionPicker)
|
||||
60
web/app/components/workflow/nodes/llm/default.ts
Normal file
60
web/app/components/workflow/nodes/llm/default.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { BlockEnum } from '../../types'
|
||||
import { type NodeDefault, PromptRole } from '../../types'
|
||||
import type { LLMNodeType } from './types'
|
||||
import type { PromptItem } from '@/models/debug'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
|
||||
|
||||
const i18nPrefix = 'workflow.errorMsg'
|
||||
|
||||
const nodeDefault: NodeDefault<LLMNodeType> = {
|
||||
defaultValue: {
|
||||
model: {
|
||||
provider: '',
|
||||
name: '',
|
||||
mode: 'chat',
|
||||
completion_params: {
|
||||
temperature: 0.7,
|
||||
},
|
||||
},
|
||||
variables: [],
|
||||
prompt_template: [{
|
||||
role: PromptRole.system,
|
||||
text: '',
|
||||
}],
|
||||
context: {
|
||||
enabled: false,
|
||||
variable_selector: [],
|
||||
},
|
||||
vision: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: LLMNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
if (!errorMessages && !payload.model.provider)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.model`) })
|
||||
|
||||
if (!errorMessages && !payload.memory) {
|
||||
const isChatModel = payload.model.mode === 'chat'
|
||||
const isPromptyEmpty = isChatModel ? !(payload.prompt_template as PromptItem[]).some(t => t.text !== '') : (payload.prompt_template as PromptItem).text === ''
|
||||
if (isPromptyEmpty)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.llm.prompt') })
|
||||
}
|
||||
return {
|
||||
isValid: !errorMessages,
|
||||
errorMessage: errorMessages,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default nodeDefault
|
||||
35
web/app/components/workflow/nodes/llm/node.tsx
Normal file
35
web/app/components/workflow/nodes/llm/node.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { LLMNodeType } from './types'
|
||||
import {
|
||||
useTextGenerationCurrentProviderAndModelAndModelList,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
|
||||
import type { NodeProps } from '@/app/components/workflow/types'
|
||||
|
||||
const Node: FC<NodeProps<LLMNodeType>> = ({
|
||||
data,
|
||||
}) => {
|
||||
const { provider, name: modelId } = data.model || {}
|
||||
const {
|
||||
textGenerationModelList,
|
||||
} = useTextGenerationCurrentProviderAndModelAndModelList()
|
||||
const hasSetModel = provider && modelId
|
||||
|
||||
if (!hasSetModel)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className='mb-1 px-3 py-1'>
|
||||
{hasSetModel && (
|
||||
<ModelSelector
|
||||
defaultModel={{ provider, model: modelId }}
|
||||
modelList={textGenerationModelList}
|
||||
readonly
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Node)
|
||||
279
web/app/components/workflow/nodes/llm/panel.tsx
Normal file
279
web/app/components/workflow/nodes/llm/panel.tsx
Normal file
@@ -0,0 +1,279 @@
|
||||
import type { FC } from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BlockEnum } from '../../types'
|
||||
import MemoryConfig from '../_base/components/memory-config'
|
||||
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
|
||||
import useConfig from './use-config'
|
||||
import ResolutionPicker from './components/resolution-picker'
|
||||
import type { LLMNodeType } from './types'
|
||||
import ConfigPrompt from './components/config-prompt'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
||||
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
|
||||
import { Resolution } from '@/types/app'
|
||||
import { InputVarType, type NodePanelProps } from '@/app/components/workflow/types'
|
||||
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
|
||||
import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form'
|
||||
import ResultPanel from '@/app/components/workflow/run/result-panel'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor'
|
||||
const i18nPrefix = 'workflow.nodes.llm'
|
||||
|
||||
const Panel: FC<NodePanelProps<LLMNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
|
||||
const startNode = useMemo(() => {
|
||||
const nodes = store.getState().getNodes()
|
||||
return nodes.find(node => node.data.type === BlockEnum.Start)
|
||||
}, [store])
|
||||
|
||||
const {
|
||||
readOnly,
|
||||
inputs,
|
||||
isChatModel,
|
||||
isChatMode,
|
||||
isCompletionModel,
|
||||
shouldShowContextTip,
|
||||
isShowVisionConfig,
|
||||
handleModelChanged,
|
||||
hasSetBlockStatus,
|
||||
handleCompletionParamsChange,
|
||||
handleContextVarChange,
|
||||
filterInputVar,
|
||||
filterVar,
|
||||
handlePromptChange,
|
||||
handleMemoryChange,
|
||||
handleVisionResolutionChange,
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
inputVarValues,
|
||||
setInputVarValues,
|
||||
visionFiles,
|
||||
setVisionFiles,
|
||||
contexts,
|
||||
setContexts,
|
||||
runningStatus,
|
||||
handleRun,
|
||||
handleStop,
|
||||
varInputs,
|
||||
runResult,
|
||||
} = useConfig(id, data)
|
||||
|
||||
const model = inputs.model
|
||||
|
||||
const singleRunForms = (() => {
|
||||
const forms: FormProps[] = []
|
||||
|
||||
if (varInputs.length > 0) {
|
||||
forms.push(
|
||||
{
|
||||
label: t(`${i18nPrefix}.singleRun.variable`)!,
|
||||
inputs: varInputs,
|
||||
values: inputVarValues,
|
||||
onChange: setInputVarValues,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (inputs.context?.variable_selector && inputs.context?.variable_selector.length > 0) {
|
||||
forms.push(
|
||||
{
|
||||
label: t(`${i18nPrefix}.context`)!,
|
||||
inputs: [{
|
||||
label: '',
|
||||
variable: '#context#',
|
||||
type: InputVarType.contexts,
|
||||
required: false,
|
||||
}],
|
||||
values: { '#context#': contexts },
|
||||
onChange: keyValue => setContexts((keyValue as any)['#context#']),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (isShowVisionConfig) {
|
||||
forms.push(
|
||||
{
|
||||
label: t(`${i18nPrefix}.vision`)!,
|
||||
inputs: [{
|
||||
label: t(`${i18nPrefix}.files`)!,
|
||||
variable: '#files#',
|
||||
type: InputVarType.files,
|
||||
required: false,
|
||||
}],
|
||||
values: { '#files#': visionFiles },
|
||||
onChange: keyValue => setVisionFiles((keyValue as any)['#files#']),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return forms
|
||||
})()
|
||||
|
||||
return (
|
||||
<div className='mt-2'>
|
||||
<div className='px-4 pb-4 space-y-4'>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.model`)}
|
||||
>
|
||||
<ModelParameterModal
|
||||
popupClassName='!w-[387px]'
|
||||
isInWorkflow
|
||||
isAdvancedMode={true}
|
||||
mode={model?.mode}
|
||||
provider={model?.provider}
|
||||
completionParams={model?.completion_params}
|
||||
modelId={model?.name}
|
||||
setModel={handleModelChanged}
|
||||
onCompletionParamsChange={handleCompletionParamsChange}
|
||||
hideDebugWithMultipleModel
|
||||
debugWithMultipleModel={false}
|
||||
readonly={readOnly}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{/* knowledge */}
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.context`)}
|
||||
tooltip={t(`${i18nPrefix}.contextTooltip`)!}
|
||||
>
|
||||
<>
|
||||
<VarReferencePicker
|
||||
readonly={readOnly}
|
||||
nodeId={id}
|
||||
isShowNodeName
|
||||
value={inputs.context?.variable_selector || []}
|
||||
onChange={handleContextVarChange}
|
||||
filterVar={filterVar}
|
||||
/>
|
||||
{shouldShowContextTip && (
|
||||
<div className='leading-[18px] text-xs font-normal text-[#DC6803]'>{t(`${i18nPrefix}.notSetContextInPromptTip`)}</div>
|
||||
)}
|
||||
</>
|
||||
</Field>
|
||||
|
||||
{/* Prompt */}
|
||||
{model.name && (
|
||||
<ConfigPrompt
|
||||
readOnly={readOnly}
|
||||
nodeId={id}
|
||||
filterVar={filterInputVar}
|
||||
isChatModel={isChatModel}
|
||||
isChatApp={isChatMode}
|
||||
isShowContext
|
||||
payload={inputs.prompt_template}
|
||||
onChange={handlePromptChange}
|
||||
hasSetBlockStatus={hasSetBlockStatus}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Memory put place examples. */}
|
||||
{isChatMode && isChatModel && !!inputs.memory && (
|
||||
<div className='mt-4'>
|
||||
<div className='flex justify-between items-center h-8 pl-3 pr-2 rounded-lg bg-gray-100'>
|
||||
<div className='flex items-center space-x-1'>
|
||||
<div className='text-xs font-semibold text-gray-700 uppercase'>{t('workflow.nodes.common.memories.title')}</div>
|
||||
<TooltipPlus
|
||||
popupContent={t('workflow.nodes.common.memories.tip')}
|
||||
>
|
||||
<HelpCircle className='w-3.5 h-3.5 text-gray-400' />
|
||||
</TooltipPlus>
|
||||
</div>
|
||||
<div className='flex items-center h-[18px] px-1 rounded-[5px] border border-black/8 text-xs font-semibold text-gray-500 uppercase'>{t('workflow.nodes.common.memories.builtIn')}</div>
|
||||
</div>
|
||||
{/* Readonly User Query */}
|
||||
<div className='mt-4'>
|
||||
<Editor
|
||||
title={<div className='flex items-center space-x-1'>
|
||||
<div className='text-xs font-semibold text-gray-700 uppercase'>user</div>
|
||||
<TooltipPlus
|
||||
popupContent={
|
||||
<div className='max-w-[180px]'>{t('workflow.nodes.llm.roleDescription.user')}</div>
|
||||
}
|
||||
>
|
||||
<HelpCircle className='w-3.5 h-3.5 text-gray-400' />
|
||||
</TooltipPlus>
|
||||
</div>}
|
||||
value={'{{#sys.query#}}'}
|
||||
onChange={() => { }}
|
||||
readOnly
|
||||
isShowContext={false}
|
||||
isChatApp
|
||||
isChatModel={false}
|
||||
hasSetBlockStatus={{
|
||||
query: false,
|
||||
history: true,
|
||||
context: true,
|
||||
}}
|
||||
availableNodes={[startNode!]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Memory */}
|
||||
{isChatMode && (
|
||||
<>
|
||||
<Split />
|
||||
<MemoryConfig
|
||||
readonly={readOnly}
|
||||
config={{ data: inputs.memory }}
|
||||
onChange={handleMemoryChange}
|
||||
canSetRoleName={isCompletionModel}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Vision: GPT4-vision and so on */}
|
||||
{isShowVisionConfig && (
|
||||
<>
|
||||
<Split />
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.vision`)}
|
||||
tooltip={t('appDebug.vision.description')!}
|
||||
operations={
|
||||
<ResolutionPicker
|
||||
value={inputs.vision.configs?.detail || Resolution.high}
|
||||
onChange={handleVisionResolutionChange}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Split />
|
||||
<div className='px-4 pt-4 pb-2'>
|
||||
<OutputVars>
|
||||
<>
|
||||
<VarItem
|
||||
name='text'
|
||||
type='string'
|
||||
description={t(`${i18nPrefix}.outputVars.output`)}
|
||||
/>
|
||||
</>
|
||||
</OutputVars>
|
||||
</div>
|
||||
{isShowSingleRun && (
|
||||
<BeforeRunForm
|
||||
nodeName={inputs.title}
|
||||
onHide={hideSingleRun}
|
||||
forms={singleRunForms}
|
||||
runningStatus={runningStatus}
|
||||
onRun={handleRun}
|
||||
onStop={handleStop}
|
||||
result={<ResultPanel {...runResult} showSteps={false} />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Panel)
|
||||
19
web/app/components/workflow/nodes/llm/types.ts
Normal file
19
web/app/components/workflow/nodes/llm/types.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { Resolution } from '@/types/app'
|
||||
import type { CommonNodeType, Memory, ModelConfig, PromptItem, ValueSelector, Variable } from '@/app/components/workflow/types'
|
||||
|
||||
export type LLMNodeType = CommonNodeType & {
|
||||
model: ModelConfig
|
||||
variables: Variable[]
|
||||
prompt_template: PromptItem[] | PromptItem
|
||||
memory?: Memory
|
||||
context: {
|
||||
enabled: boolean
|
||||
variable_selector: ValueSelector
|
||||
}
|
||||
vision: {
|
||||
enabled: boolean
|
||||
configs?: {
|
||||
detail: Resolution
|
||||
}
|
||||
}
|
||||
}
|
||||
316
web/app/components/workflow/nodes/llm/use-config.ts
Normal file
316
web/app/components/workflow/nodes/llm/use-config.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import produce from 'immer'
|
||||
import useVarList from '../_base/hooks/use-var-list'
|
||||
import { VarType } from '../../types'
|
||||
import type { Memory, ValueSelector, Var } from '../../types'
|
||||
import { useStore } from '../../store'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodesReadOnly,
|
||||
} from '../../hooks'
|
||||
import type { LLMNodeType } from './types'
|
||||
import { Resolution } from '@/types/app'
|
||||
import { useModelListAndDefaultModelAndCurrentProviderAndModel, useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import {
|
||||
ModelFeatureEnum,
|
||||
ModelTypeEnum,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run'
|
||||
import type { PromptItem } from '@/models/debug'
|
||||
import { RETRIEVAL_OUTPUT_STRUCT } from '@/app/components/workflow/constants'
|
||||
import { checkHasContextBlock, checkHasHistoryBlock, checkHasQueryBlock } from '@/app/components/base/prompt-editor/constants'
|
||||
|
||||
const useConfig = (id: string, payload: LLMNodeType) => {
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const defaultConfig = useStore(s => s.nodesDefaultConfigs)[payload.type]
|
||||
const [defaultRolePrefix, setDefaultRolePrefix] = useState<{ user: string; assistant: string }>({ user: '', assistant: '' })
|
||||
const { inputs, setInputs: doSetInputs } = useNodeCrud<LLMNodeType>(id, payload)
|
||||
const setInputs = useCallback((newInputs: LLMNodeType) => {
|
||||
if (newInputs.memory && !newInputs.memory.role_prefix) {
|
||||
const newPayload = produce(newInputs, (draft) => {
|
||||
draft.memory!.role_prefix = defaultRolePrefix
|
||||
})
|
||||
doSetInputs(newPayload)
|
||||
return
|
||||
}
|
||||
doSetInputs(newInputs)
|
||||
}, [doSetInputs, defaultRolePrefix])
|
||||
const inputRef = useRef(inputs)
|
||||
useEffect(() => {
|
||||
inputRef.current = inputs
|
||||
}, [inputs])
|
||||
// model
|
||||
const model = inputs.model
|
||||
const modelMode = inputs.model?.mode
|
||||
const isChatModel = modelMode === 'chat'
|
||||
|
||||
const isCompletionModel = !isChatModel
|
||||
|
||||
const hasSetBlockStatus = (() => {
|
||||
const promptTemplate = inputs.prompt_template
|
||||
const hasSetContext = isChatModel ? (promptTemplate as PromptItem[]).some(item => checkHasContextBlock(item.text)) : checkHasContextBlock((promptTemplate as PromptItem).text)
|
||||
if (!isChatMode) {
|
||||
return {
|
||||
history: false,
|
||||
query: false,
|
||||
context: hasSetContext,
|
||||
}
|
||||
}
|
||||
if (isChatModel) {
|
||||
return {
|
||||
history: false,
|
||||
query: (promptTemplate as PromptItem[]).some(item => checkHasQueryBlock(item.text)),
|
||||
context: hasSetContext,
|
||||
}
|
||||
}
|
||||
else {
|
||||
return {
|
||||
history: checkHasHistoryBlock((promptTemplate as PromptItem).text),
|
||||
query: checkHasQueryBlock((promptTemplate as PromptItem).text),
|
||||
context: hasSetContext,
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
const shouldShowContextTip = !hasSetBlockStatus.context && inputs.context.enabled
|
||||
|
||||
const appendDefaultPromptConfig = useCallback((draft: LLMNodeType, defaultConfig: any, passInIsChatMode?: boolean) => {
|
||||
const promptTemplates = defaultConfig.prompt_templates
|
||||
if (passInIsChatMode === undefined ? isChatModel : passInIsChatMode) {
|
||||
draft.prompt_template = promptTemplates.chat_model.prompts
|
||||
}
|
||||
else {
|
||||
draft.prompt_template = promptTemplates.completion_model.prompt
|
||||
|
||||
setDefaultRolePrefix({
|
||||
user: promptTemplates.completion_model.conversation_histories_role.user_prefix,
|
||||
assistant: promptTemplates.completion_model.conversation_histories_role.assistant_prefix,
|
||||
})
|
||||
}
|
||||
}, [isChatModel])
|
||||
useEffect(() => {
|
||||
const isReady = defaultConfig && Object.keys(defaultConfig).length > 0
|
||||
|
||||
if (isReady && !inputs.prompt_template) {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
appendDefaultPromptConfig(draft, defaultConfig)
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [defaultConfig, isChatModel])
|
||||
|
||||
const {
|
||||
currentProvider,
|
||||
currentModel,
|
||||
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textGeneration)
|
||||
|
||||
const handleModelChanged = useCallback((model: { provider: string; modelId: string; mode?: string }) => {
|
||||
const newInputs = produce(inputRef.current, (draft) => {
|
||||
draft.model.provider = model.provider
|
||||
draft.model.name = model.modelId
|
||||
draft.model.mode = model.mode!
|
||||
const isModeChange = model.mode !== inputRef.current.model.mode
|
||||
if (isModeChange && defaultConfig && Object.keys(defaultConfig).length > 0)
|
||||
appendDefaultPromptConfig(draft, defaultConfig, model.mode === 'chat')
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [setInputs, defaultConfig, appendDefaultPromptConfig])
|
||||
|
||||
useEffect(() => {
|
||||
if (currentProvider?.provider && currentModel?.model && !model.provider) {
|
||||
handleModelChanged({
|
||||
provider: currentProvider?.provider,
|
||||
modelId: currentModel?.model,
|
||||
mode: currentModel?.model_properties?.mode as string,
|
||||
})
|
||||
}
|
||||
}, [model.provider, currentProvider, currentModel, handleModelChanged])
|
||||
|
||||
const handleCompletionParamsChange = useCallback((newParams: Record<string, any>) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.model.completion_params = newParams
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const {
|
||||
currentModel: currModel,
|
||||
} = useTextGenerationCurrentProviderAndModelAndModelList(
|
||||
{
|
||||
provider: model.provider,
|
||||
model: model.name,
|
||||
},
|
||||
)
|
||||
const isShowVisionConfig = !!currModel?.features?.includes(ModelFeatureEnum.vision)
|
||||
|
||||
// variables
|
||||
const { handleVarListChange, handleAddVariable } = useVarList<LLMNodeType>({
|
||||
inputs,
|
||||
setInputs,
|
||||
})
|
||||
|
||||
// context
|
||||
const handleContextVarChange = useCallback((newVar: ValueSelector | string) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.context.variable_selector = newVar as ValueSelector || []
|
||||
draft.context.enabled = !!(newVar && newVar.length > 0)
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handlePromptChange = useCallback((newPrompt: PromptItem[] | PromptItem) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.prompt_template = newPrompt
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleMemoryChange = useCallback((newMemory?: Memory) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.memory = newMemory
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleVisionResolutionChange = useCallback((newResolution: Resolution) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
if (!draft.vision.configs) {
|
||||
draft.vision.configs = {
|
||||
detail: Resolution.high,
|
||||
}
|
||||
}
|
||||
draft.vision.configs.detail = newResolution
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const filterInputVar = useCallback((varPayload: Var) => {
|
||||
return [VarType.number, VarType.string].includes(varPayload.type)
|
||||
}, [])
|
||||
|
||||
const filterVar = useCallback((varPayload: Var) => {
|
||||
return [VarType.arrayObject, VarType.array, VarType.string].includes(varPayload.type)
|
||||
}, [])
|
||||
|
||||
// single run
|
||||
const {
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
getInputVars,
|
||||
runningStatus,
|
||||
handleRun,
|
||||
handleStop,
|
||||
runInputData,
|
||||
setRunInputData,
|
||||
runResult,
|
||||
} = useOneStepRun<LLMNodeType>({
|
||||
id,
|
||||
data: inputs,
|
||||
defaultRunInputData: {
|
||||
'#context#': [RETRIEVAL_OUTPUT_STRUCT],
|
||||
'#files#': [],
|
||||
},
|
||||
})
|
||||
|
||||
// const handleRun = (submitData: Record<string, any>) => {
|
||||
// console.log(submitData)
|
||||
// const res = produce(submitData, (draft) => {
|
||||
// debugger
|
||||
// if (draft.contexts) {
|
||||
// draft['#context#'] = draft.contexts
|
||||
// delete draft.contexts
|
||||
// }
|
||||
// if (draft.visionFiles) {
|
||||
// draft['#files#'] = draft.visionFiles
|
||||
// delete draft.visionFiles
|
||||
// }
|
||||
// })
|
||||
|
||||
// doHandleRun(res)
|
||||
// }
|
||||
|
||||
const inputVarValues = (() => {
|
||||
const vars: Record<string, any> = {}
|
||||
Object.keys(runInputData)
|
||||
.filter(key => !['#context#', '#files#'].includes(key))
|
||||
.forEach((key) => {
|
||||
vars[key] = runInputData[key]
|
||||
})
|
||||
return vars
|
||||
})()
|
||||
|
||||
const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
|
||||
const newVars = {
|
||||
...newPayload,
|
||||
'#context#': runInputData['#context#'],
|
||||
'#files#': runInputData['#files#'],
|
||||
}
|
||||
setRunInputData(newVars)
|
||||
}, [runInputData, setRunInputData])
|
||||
|
||||
const contexts = runInputData['#context#']
|
||||
const setContexts = useCallback((newContexts: string[]) => {
|
||||
setRunInputData({
|
||||
...runInputData,
|
||||
'#context#': newContexts,
|
||||
})
|
||||
}, [runInputData, setRunInputData])
|
||||
|
||||
const visionFiles = runInputData['#files#']
|
||||
const setVisionFiles = useCallback((newFiles: any[]) => {
|
||||
setRunInputData({
|
||||
...runInputData,
|
||||
'#files#': newFiles,
|
||||
})
|
||||
}, [runInputData, setRunInputData])
|
||||
|
||||
const allVarStrArr = (() => {
|
||||
const arr = isChatModel ? (inputs.prompt_template as PromptItem[]).map(item => item.text) : [(inputs.prompt_template as PromptItem).text]
|
||||
if (isChatMode && isChatModel && !!inputs.memory)
|
||||
arr.push('{{#sys.query#}}')
|
||||
|
||||
return arr
|
||||
})()
|
||||
|
||||
const varInputs = getInputVars(allVarStrArr)
|
||||
|
||||
return {
|
||||
readOnly,
|
||||
isChatMode,
|
||||
inputs,
|
||||
isChatModel,
|
||||
isCompletionModel,
|
||||
hasSetBlockStatus,
|
||||
shouldShowContextTip,
|
||||
isShowVisionConfig,
|
||||
handleModelChanged,
|
||||
handleCompletionParamsChange,
|
||||
handleVarListChange,
|
||||
handleAddVariable,
|
||||
handleContextVarChange,
|
||||
filterInputVar,
|
||||
filterVar,
|
||||
handlePromptChange,
|
||||
handleMemoryChange,
|
||||
handleVisionResolutionChange,
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
inputVarValues,
|
||||
setInputVarValues,
|
||||
visionFiles,
|
||||
setVisionFiles,
|
||||
contexts,
|
||||
setContexts,
|
||||
varInputs,
|
||||
runningStatus,
|
||||
handleRun,
|
||||
handleStop,
|
||||
runResult,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfig
|
||||
5
web/app/components/workflow/nodes/llm/utils.ts
Normal file
5
web/app/components/workflow/nodes/llm/utils.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { LLMNodeType } from './types'
|
||||
|
||||
export const checkNodeValid = (payload: LLMNodeType) => {
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user