memory var sort

This commit is contained in:
JzoNg 2025-09-20 18:37:41 +08:00
parent 77b7bf9d47
commit f6623423dd
10 changed files with 186 additions and 98 deletions

View File

@ -31,6 +31,8 @@ export const useWorkflowVariables = () => {
filterVar,
hideEnv,
hideChatVar,
conversationVariablesFirst,
memoryVarSortFn,
}: {
parentNode?: Node | null
beforeNodes: Node[]
@ -38,6 +40,8 @@ export const useWorkflowVariables = () => {
filterVar: (payload: Var, selector: ValueSelector) => boolean
hideEnv?: boolean
hideChatVar?: boolean
conversationVariablesFirst?: boolean
memoryVarSortFn?: (a: string, b: string) => number
}): NodeOutPutVar[] => {
const {
conversationVariables,
@ -61,6 +65,8 @@ export const useWorkflowVariables = () => {
dataSourceList: dataSourceList ?? [],
},
schemaTypeDefinitions,
conversationVariablesFirst,
memoryVarSortFn,
})
}, [t, workflowStore, schemaTypeDefinitions, buildInTools])

View File

@ -36,6 +36,8 @@ import Switch from '@/app/components/base/switch'
import { Jinja } from '@/app/components/base/icons/src/vender/workflow'
import { useStore } from '@/app/components/workflow/store'
import { useWorkflowVariableType } from '@/app/components/workflow/hooks'
import Button from '@/app/components/base/button'
import { Memory } from '@/app/components/base/icons/src/vender/line/others'
type Props = {
className?: string
@ -75,10 +77,11 @@ type Props = {
titleTooltip?: ReactNode
inputClassName?: string
editorContainerClassName?: string
placeholder?: string
placeholder?: string | React.JSX.Element
placeholderClassName?: string
titleClassName?: string
required?: boolean
isMemorySupported?: boolean
}
const Editor: FC<Props> = ({
@ -118,6 +121,7 @@ const Editor: FC<Props> = ({
titleClassName,
editorContainerClassName,
required,
isMemorySupported,
}) => {
const { t } = useTranslation()
const { eventEmitter } = useEventEmitterContextContext()
@ -233,68 +237,78 @@ const Editor: FC<Props> = ({
</div>
{/* Min: 80 Max: 560. Header: 24 */}
<div className={cn('pb-2', isExpand && 'flex grow flex-col')}>
<div className={cn('pb-2', isExpand && 'flex grow flex-col', isMemorySupported && isFocus && 'pb-1.5')}>
{!(isSupportJinja && editionType === EditionType.jinja2)
? (
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative min-h-[56px] overflow-y-auto px-3', editorContainerClassName)}>
<PromptEditor
key={controlPromptEditorRerenderKey}
placeholder={placeholder}
placeholderClassName={placeholderClassName}
instanceId={instanceId}
compact
className={cn('min-h-[56px]', inputClassName)}
style={isExpand ? { height: editorExpandHeight - 5 } : {}}
value={value}
contextBlock={{
show: justVar ? false : isShowContext,
selectable: !hasSetBlockStatus?.context,
canNotAddContext: true,
}}
historyBlock={{
show: justVar ? false : isShowHistory,
selectable: !hasSetBlockStatus?.history,
history: {
user: 'Human',
assistant: 'Assistant',
},
}}
queryBlock={{
show: false, // use [sys.query] instead of query block
selectable: false,
}}
workflowVariableBlock={{
show: true,
variables: nodesOutputVars || [],
getVarType: getVarType as any,
workflowNodesMap: availableNodes.reduce((acc, node) => {
acc[node.id] = {
title: node.data.title,
type: node.data.type,
width: node.width,
height: node.height,
position: node.position,
}
if (node.data.type === BlockEnum.Start) {
acc.sys = {
title: t('workflow.blocks.start'),
type: BlockEnum.Start,
<>
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative min-h-[56px] overflow-y-auto px-3', editorContainerClassName)}>
<PromptEditor
key={controlPromptEditorRerenderKey}
placeholder={placeholder}
placeholderClassName={placeholderClassName}
instanceId={instanceId}
compact
className={cn('min-h-[56px]', inputClassName)}
style={isExpand ? { height: editorExpandHeight - 5 } : {}}
value={value}
contextBlock={{
show: justVar ? false : isShowContext,
selectable: !hasSetBlockStatus?.context,
canNotAddContext: true,
}}
historyBlock={{
show: justVar ? false : isShowHistory,
selectable: !hasSetBlockStatus?.history,
history: {
user: 'Human',
assistant: 'Assistant',
},
}}
queryBlock={{
show: false, // use [sys.query] instead of query block
selectable: false,
}}
workflowVariableBlock={{
show: true,
variables: nodesOutputVars || [],
getVarType,
workflowNodesMap: availableNodes.reduce((acc, node) => {
acc[node.id] = {
title: node.data.title,
type: node.data.type,
width: node.width,
height: node.height,
position: node.position,
}
}
return acc
}, {} as any),
showManageInputField: !!pipelineId,
onManageInputField: () => setShowInputFieldPanel?.(true),
}}
onChange={onChange}
onBlur={setBlur}
onFocus={setFocus}
editable={!readOnly}
isSupportFileVar={isSupportFileVar}
/>
{/* to patch Editor not support dynamic change editable status */}
{readOnly && <div className='absolute inset-0 z-10'></div>}
</div>
if (node.data.type === BlockEnum.Start) {
acc.sys = {
title: t('workflow.blocks.start'),
type: BlockEnum.Start,
}
}
return acc
}, {} as any),
showManageInputField: !!pipelineId,
onManageInputField: () => setShowInputFieldPanel?.(true),
}}
onChange={onChange}
onBlur={setBlur}
onFocus={setFocus}
editable={!readOnly}
isSupportFileVar={isSupportFileVar}
/>
{/* to patch Editor not support dynamic change editable status */}
{readOnly && <div className='absolute inset-0 z-10'></div>}
</div>
{isMemorySupported && isFocus && (
<div className='pl-1.5'>
<Button variant='ghost' size='small' className='text-text-tertiary'>
<Memory className='h-3.5 w-3.5' />
<span className='ml-1'>{t('workflow.nodes.llm.memory.addButton')}</span>
</Button>
</div>
)}
</>
)
: (
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative min-h-[56px] overflow-y-auto px-3', editorContainerClassName)}>

View File

@ -313,6 +313,7 @@ const formatItem = (
allPluginInfoList: Record<string, ToolWithProvider[]>,
ragVars?: Var[],
schemaTypeDefinitions: SchemaTypeDefinition[] = [],
memoryVarSortFn?: (a: string, b: string) => number,
): NodeOutPutVar => {
const { id, data } = item
@ -630,6 +631,8 @@ const formatItem = (
}
case 'conversation': {
if (memoryVarSortFn)
data.chatVarList.sort(memoryVarSortFn)
res.vars = data.chatVarList.map((chatVar: ConversationVariable) => {
return {
variable: `conversation.${chatVar.name}`,
@ -759,6 +762,8 @@ export const toNodeOutputVars = (
ragVariables: RAGPipelineVariable[] = [],
allPluginInfoList: Record<string, ToolWithProvider[]>,
schemaTypeDefinitions?: SchemaTypeDefinition[],
conversationVariablesFirst: boolean = false,
memoryVarSortFn?: (a: string, b: string) => number,
): NodeOutPutVar[] => {
// ENV_NODE data format
const ENV_NODE = {
@ -801,44 +806,56 @@ export const toNodeOutputVars = (
return (b.position?.x || 0) - (a.position?.x || 0)
})
const res = [
...sortedNodes.filter(node =>
SUPPORT_OUTPUT_VARS_NODE.includes(node?.data?.type),
),
...(environmentVariables.length > 0 ? [ENV_NODE] : []),
...(isChatMode && conversationVariables.length > 0 ? [CHAT_VAR_NODE] : []),
...(RAG_PIPELINE_NODE.data.ragVariables.length > 0
? [RAG_PIPELINE_NODE]
: []),
]
.map((node) => {
let ragVariablesInDataSource: RAGPipelineVariable[] = []
if (node.data.type === BlockEnum.DataSource) {
ragVariablesInDataSource = ragVariables.filter(
ragVariable => ragVariable.belong_to_node_id === node.id,
)
}
return {
...formatItem(
node,
isChatMode,
filterVar,
allPluginInfoList,
ragVariablesInDataSource.map(
(ragVariable: RAGPipelineVariable) =>
({
variable: `rag.${node.id}.${ragVariable.variable}`,
type: inputVarTypeToVarType(ragVariable.type as any),
description: ragVariable.label,
isRagVariable: true,
} as Var),
),
schemaTypeDefinitions,
let nodeList = []
if (conversationVariablesFirst) {
nodeList = [
...((isChatMode && conversationVariables.length > 0) ? [CHAT_VAR_NODE] : []),
...(environmentVariables.length > 0 ? [ENV_NODE] : []),
...sortedNodes.filter(node => SUPPORT_OUTPUT_VARS_NODE.includes(node?.data?.type)),
...(RAG_PIPELINE_NODE.data.ragVariables.length > 0
? [RAG_PIPELINE_NODE]
: []),
]
}
else {
nodeList = [
...sortedNodes.filter(node => SUPPORT_OUTPUT_VARS_NODE.includes(node?.data?.type)),
...(environmentVariables.length > 0 ? [ENV_NODE] : []),
...((isChatMode && conversationVariables.length > 0) ? [CHAT_VAR_NODE] : []),
...(RAG_PIPELINE_NODE.data.ragVariables.length > 0
? [RAG_PIPELINE_NODE]
: []),
]
}
const res = nodeList.map((node) => {
let ragVariablesInDataSource: RAGPipelineVariable[] = []
if (node.data.type === BlockEnum.DataSource) {
ragVariablesInDataSource = ragVariables.filter(
ragVariable => ragVariable.belong_to_node_id === node.id,
)
}
return {
...formatItem(
node,
isChatMode,
filterVar,
allPluginInfoList,
ragVariablesInDataSource.map(
(ragVariable: RAGPipelineVariable) =>
({
variable: `rag.${node.id}.${ragVariable.variable}`,
type: inputVarTypeToVarType(ragVariable.type as any),
description: ragVariable.label,
isRagVariable: true,
} as Var),
),
isStartNode: node.data.type === BlockEnum.Start,
}
})
.filter(item => item.vars.length > 0)
schemaTypeDefinitions,
memoryVarSortFn,
),
isStartNode: node.data.type === BlockEnum.Start,
}
}).filter(item => item.vars.length > 0)
return res
}
@ -1119,6 +1136,8 @@ export const toNodeAvailableVars = ({
filterVar,
allPluginInfoList,
schemaTypeDefinitions,
conversationVariablesFirst,
memoryVarSortFn,
}: {
parentNode?: Node | null;
t?: any;
@ -1134,6 +1153,8 @@ export const toNodeAvailableVars = ({
filterVar: (payload: Var, selector: ValueSelector) => boolean;
allPluginInfoList: Record<string, ToolWithProvider[]>;
schemaTypeDefinitions?: SchemaTypeDefinition[];
conversationVariablesFirst?: boolean
memoryVarSortFn?: (a: string, b: string) => number
}): NodeOutPutVar[] => {
const beforeNodesOutputVars = toNodeOutputVars(
beforeNodes,
@ -1144,6 +1165,8 @@ export const toNodeAvailableVars = ({
ragVariables,
allPluginInfoList,
schemaTypeDefinitions,
conversationVariablesFirst,
memoryVarSortFn,
)
const isInIteration = parentNode?.data.type === BlockEnum.Iteration
if (isInIteration) {

View File

@ -15,6 +15,8 @@ type Params = {
hideChatVar?: boolean
filterVar: (payload: Var, selector: ValueSelector) => boolean
passedInAvailableNodes?: Node[]
conversationVariablesFirst?: boolean
memoryVarSortFn?: (a: string, b: string) => number
}
// TODO: loop type?
@ -24,6 +26,8 @@ const useAvailableVarList = (nodeId: string, {
hideEnv,
hideChatVar,
passedInAvailableNodes,
conversationVariablesFirst,
memoryVarSortFn,
}: Params = {
onlyLeafNodeVar: false,
filterVar: () => true,
@ -70,6 +74,8 @@ const useAvailableVarList = (nodeId: string, {
filterVar,
hideEnv,
hideChatVar,
conversationVariablesFirst,
memoryVarSortFn,
}), ...dataSourceRagVars]
return {

View File

@ -146,6 +146,17 @@ const ConfigPromptItem: FC<Props> = ({
varList={varList}
handleAddVariable={handleAddVariable}
isSupportFileVar
placeholder={
<>
<div>
{t(`${i18nPrefix}.promptEditorPlaceholder1`)}
</div>
<div>
{t(`${i18nPrefix}.promptEditorPlaceholder2`)}
</div>
</>
}
isMemorySupported
/>
)
}

View File

@ -34,6 +34,7 @@ type Props = {
varList?: Variable[]
handleAddVariable: (payload: any) => void
modelConfig: ModelConfig
memoryVarSortFn?: (a: string, b: string) => number
}
const ConfigPrompt: FC<Props> = ({
@ -49,6 +50,7 @@ const ConfigPrompt: FC<Props> = ({
varList = [],
handleAddVariable,
modelConfig,
memoryVarSortFn,
}) => {
const { t } = useTranslation()
const workflowStore = useWorkflowStore()
@ -73,6 +75,8 @@ const ConfigPrompt: FC<Props> = ({
} = useAvailableVarList(nodeId, {
onlyLeafNodeVar: false,
filterVar,
conversationVariablesFirst: true,
memoryVarSortFn,
})
const handleChatModePromptChange = useCallback((index: number) => {

View File

@ -62,6 +62,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
handleStructureOutputChange,
filterJinja2InputVar,
handleReasoningFormatChange,
memoryVarSortFn,
} = useConfig(id, data)
const model = inputs.model
@ -152,6 +153,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
varList={inputs.prompt_config?.jinja2_variables || []}
handleAddVariable={handleAddVariable}
modelConfig={model}
memoryVarSortFn={memoryVarSortFn}
/>
)}

View File

@ -23,7 +23,7 @@ const useConfig = (id: string, payload: LLMNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const isChatMode = useIsChatMode()
const defaultConfig = useStore(s => s.nodesDefaultConfigs)[payload.type]
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 inputRef = useRef(inputs)
@ -331,6 +331,17 @@ const useConfig = (id: string, payload: LLMNodeType) => {
filterVar: filterMemoryPromptVar,
})
const memoryVarSortFn = useCallback((a: string, b: string) => {
const idsInNode = inputs.memory?.block_id || []
const aInNode = idsInNode.includes(a)
const bInNode = idsInNode.includes(b)
if (aInNode && !bInNode) return -1
if (!aInNode && bInNode) return 1
return a.localeCompare(b)
}, [inputs.memory?.block_id])
return {
readOnly,
isChatMode,
@ -364,6 +375,7 @@ const useConfig = (id: string, payload: LLMNodeType) => {
handleStructureOutputEnableChange,
filterJinja2InputVar,
handleReasoningFormatChange,
memoryVarSortFn,
}
}

View File

@ -522,6 +522,11 @@ const translation = {
saveSchema: 'Please finish editing the current field before saving the schema',
},
},
promptEditorPlaceholder1: 'Click here to edit Prompt',
promptEditorPlaceholder2: 'Type \'/\' to insert variable',
memory: {
addButton: 'Add Memory',
},
},
knowledgeRetrieval: {
queryVariable: 'Query Variable',

View File

@ -522,6 +522,11 @@ const translation = {
tagged: '保持思考标签',
separated: '分开思考标签',
},
promptEditorPlaceholder1: '点击此处编辑提示词',
promptEditorPlaceholder2: '输入 \'/\' 插入变量',
memory: {
addButton: '添加记忆',
},
},
knowledgeRetrieval: {
queryVariable: '查询变量',