add memory variable

This commit is contained in:
zxhlyh 2025-10-15 16:15:16 +08:00
parent 6b6ab5e034
commit 2a27d81553
21 changed files with 240 additions and 37 deletions

View File

@ -34,6 +34,7 @@ const WorkflowMain = ({
features,
conversation_variables,
environment_variables,
memory_blocks,
} = payload
if (features && featuresStore) {
const { setFeatures } = featuresStore.getState()
@ -48,6 +49,10 @@ const WorkflowMain = ({
const { setEnvironmentVariables } = workflowStore.getState()
setEnvironmentVariables(environment_variables)
}
if (memory_blocks) {
const { setMemoryVariables } = workflowStore.getState()
setMemoryVariables(memory_blocks)
}
}, [featuresStore, workflowStore])
const {

View File

@ -33,6 +33,7 @@ export const useNodesSyncDraft = () => {
const {
appId,
conversationVariables,
memoryVariables,
environmentVariables,
syncWorkflowDraftHash,
} = workflowStore.getState()
@ -84,6 +85,7 @@ export const useNodesSyncDraft = () => {
},
environment_variables: environmentVariables,
conversation_variables: conversationVariables,
memory_blocks: memoryVariables,
hash: syncWorkflowDraftHash,
},
}

View File

@ -53,6 +53,7 @@ export const useWorkflowInit = () => {
}, {} as Record<string, string>),
environmentVariables: res.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [],
conversationVariables: res.conversation_variables || [],
memoryVariables: res.memory_blocks || [],
})
setSyncWorkflowDraftHash(res.hash)
setIsLoading(false)

View File

@ -16,6 +16,7 @@ export const useWorkflowRefreshDraft = () => {
setEnvironmentVariables,
setEnvSecrets,
setConversationVariables,
setMemoryVariables,
} = workflowStore.getState()
setIsSyncingWorkflowDraft(true)
fetchWorkflowDraft(`/apps/${appId}/workflows/draft`).then((response) => {
@ -27,6 +28,7 @@ export const useWorkflowRefreshDraft = () => {
}, {} as Record<string, string>))
setEnvironmentVariables(response.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [])
setConversationVariables(response.conversation_variables || [])
setMemoryVariables(response.memory_blocks || [])
}).finally(() => setIsSyncingWorkflowDraft(false))
}, [handleUpdateWorkflowCanvas, workflowStore])

View File

@ -0,0 +1,99 @@
import {
memo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
RiDeleteBinLine,
RiEditLine,
} from '@remixicon/react'
import type { Memory } from '@/app/components/workflow/types'
import Badge from '@/app/components/base/badge'
import ActionButton from '@/app/components/base/action-button'
import { useMemoryVariables } from './hooks/use-memory-variables'
import Confirm from '@/app/components/base/confirm'
type BlockMemoryProps = {
payload: Memory
}
const BlockMemory = ({ payload }: BlockMemoryProps) => {
const { t } = useTranslation()
const { block_id } = payload
const { memoryVariablesInUsed } = useMemoryVariables(block_id || [])
const [showConfirm, setShowConfirm] = useState<{
title: string
desc: string
onConfirm: () => void
} | undefined>(undefined)
const handleEdit = (blockId: string) => {
console.log('edit', blockId)
}
const handleDelete = (blockId: string) => {
setShowConfirm({
title: t('workflow.nodes.common.memory.block.deleteConfirmTitle'),
desc: t('workflow.nodes.common.memory.block.deleteConfirmDesc'),
onConfirm: () => handleDelete(blockId),
})
}
if (!block_id?.length) {
return (
<div className='system-xs-regular mt-2 flex items-center justify-center rounded-[10px] bg-background-section p-3 text-text-tertiary'>
{t('workflow.nodes.common.memory.block.empty')}
</div>
)
}
return (
<>
<div>
{
memoryVariablesInUsed.map(memoryVariable => (
<div
key={memoryVariable.id}
className='group flex h-8 items-center space-x-1 rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg pl-2 pr-1 shadow-xs hover:border hover:border-state-destructive-solid hover:bg-state-destructive-hover'>
<div className='h-4 w-4'></div>
<div
title={memoryVariable.name}
className='system-sm-medium grow truncate text-text-secondary'
>
{memoryVariable.name}
</div>
<Badge className='shrink-0'>
{memoryVariable.term}
</Badge>
<ActionButton
className='hidden shrink-0 group-hover:block'
size='m'
onClick={() => handleEdit(memoryVariable.id)}
>
<RiEditLine className='h-4 w-4 text-text-tertiary' />
</ActionButton>
<ActionButton
className='hidden shrink-0 group-hover:block'
size='m'
onClick={() => handleDelete(memoryVariable.id)}
>
<RiDeleteBinLine className='h-4 w-4 text-text-destructive' />
</ActionButton>
</div>
))
}
</div>
{
!!showConfirm && (
<Confirm
isShow
onCancel={() => setShowConfirm(undefined)}
onConfirm={showConfirm.onConfirm}
title={showConfirm.title}
content={showConfirm.desc}
/>
)
}
</>
)
}
export default memo(BlockMemory)

View File

@ -0,0 +1,18 @@
import { useMemo } from 'react'
import { useStore } from '@/app/components/workflow/store'
export const useMemoryVariables = (blockIds: string[]) => {
const memoryVariables = useStore(s => s.memoryVariables)
const memoryVariablesInUsed = useMemo(() => {
return memoryVariables.filter(variable => blockIds.includes(variable.id))
}, [memoryVariables, blockIds])
const handleDelete = (blockId: string) => {
console.log('delete', blockId)
}
return {
memoryVariablesInUsed,
handleDelete,
}
}

View File

@ -3,11 +3,11 @@ import {
useMemo,
useState,
} from 'react'
import type { LLMNodeType } from '../../types'
import type { LLMNodeType } from '../../../types'
import { useNodeUpdate } from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import {
MEMORY_DEFAULT,
} from './linear-memory'
} from '../linear-memory'
import type { Memory } from '@/app/components/workflow/types'
import { MemoryMode } from '@/app/components/workflow/types'

View File

@ -13,9 +13,10 @@ import MemorySelector from './memory-selector'
import LinearMemory from './linear-memory'
import type { Memory } from '@/app/components/workflow/types'
import type { LLMNodeType } from '../../types'
import { useMemory } from './hooks'
import { useMemory } from './hooks/use-memory'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import { MemoryMode } from '@/app/components/workflow/types'
import BlockMemory from './block-memory'
type MemoryProps = Pick<Node, 'id' | 'data'> & {
readonly?: boolean
@ -89,6 +90,11 @@ const MemorySystem = ({
/>
)
}
{
memoryType === MemoryMode.block && !collapsed && (
<BlockMemory payload={memory as Memory} />
)
}
</>
</Collapse>
<Split className='mt-4' />

View File

@ -22,7 +22,7 @@ import { RiAlertFill, RiQuestionLine } from '@remixicon/react'
import { fetchAndMergeValidCompletionParams } from '@/utils/completion-params'
import Toast from '@/app/components/base/toast'
import MemorySystem from './components/memory-system'
import { useMemory } from './components/memory-system/hooks'
import { useMemory } from './components/memory-system/hooks/use-memory'
import { MemoryMode } from '@/app/components/workflow/types'
const i18nPrefix = 'workflow.nodes.llm'

View File

@ -1,14 +1,18 @@
import { memo, useState } from 'react'
import { capitalize } from 'lodash-es'
import { RiDeleteBinLine, RiEditLine } from '@remixicon/react'
import { BubbleX } from '@/app/components/base/icons/src/vender/line/others'
import type { ConversationVariable } from '@/app/components/workflow/types'
import {
BubbleX,
Memory,
} from '@/app/components/base/icons/src/vender/line/others'
import type { ConversationVariable, MemoryVariable } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
import { ChatVarType } from '../type'
type VariableItemProps = {
item: ConversationVariable
onEdit: (item: ConversationVariable) => void
onDelete: (item: ConversationVariable) => void
item: ConversationVariable | MemoryVariable
onEdit: (item: ConversationVariable | MemoryVariable) => void
onDelete: (item: ConversationVariable | MemoryVariable) => void
}
const VariableItem = ({
@ -24,7 +28,16 @@ const VariableItem = ({
)}>
<div className='flex items-center justify-between'>
<div className='flex grow items-center gap-1'>
<BubbleX className='h-4 w-4 text-util-colors-teal-teal-700' />
{
item.value_type === ChatVarType.Memory && (
<Memory className='h-4 w-4 text-util-colors-teal-teal-700' />
)
}
{
item.value_type !== ChatVarType.Memory && (
<BubbleX className='h-4 w-4 text-util-colors-teal-teal-700' />
)
}
<div className='system-sm-medium text-text-primary'>{item.name}</div>
<div className='system-xs-medium text-text-tertiary'>{capitalize(item.value_type)}</div>
</div>
@ -41,7 +54,11 @@ const VariableItem = ({
</div>
</div>
</div>
<div className='system-xs-regular truncate text-text-tertiary'>{item.description}</div>
{
'description' in item && item.description && (
<div className='system-xs-regular truncate text-text-tertiary'>{item.description}</div>
)
}
</div>
)
}

View File

@ -9,15 +9,15 @@ import {
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import type { ConversationVariable } from '@/app/components/workflow/types'
import type { ConversationVariable, MemoryVariable } from '@/app/components/workflow/types'
type Props = {
open: boolean
setOpen: (value: React.SetStateAction<boolean>) => void
showTip: boolean
chatVar?: ConversationVariable
chatVar?: ConversationVariable | MemoryVariable
onClose: () => void
onSave: (env: ConversationVariable) => void
onSave: (env: ConversationVariable | MemoryVariable) => void
}
const VariableModalTrigger = ({

View File

@ -10,7 +10,7 @@ import { RiCloseLine } from '@remixicon/react'
import Button from '@/app/components/base/button'
import { ToastContext } from '@/app/components/base/toast'
import { useStore } from '@/app/components/workflow/store'
import type { ConversationVariable } from '@/app/components/workflow/types'
import type { ConversationVariable, MemoryVariable } from '@/app/components/workflow/types'
import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type'
import cn from '@/utils/classnames'
import { checkKeys } from '@/utils/var'
@ -19,9 +19,9 @@ import VariableForm from '@/app/components/base/form/form-scenarios/variable'
import { useForm } from '../hooks'
export type ModalPropsType = {
chatVar?: ConversationVariable
chatVar?: ConversationVariable | MemoryVariable
onClose: () => void
onSave: (chatVar: ConversationVariable) => void
onSave: (chatVar: ConversationVariable | MemoryVariable) => void
}
const ChatVariableModal = ({

View File

@ -8,9 +8,9 @@ import {
useMemoryDefaultValues,
useMemorySchema,
} from './use-memory-schema'
import type { ConversationVariable } from '@/app/components/workflow/types'
import type { ConversationVariable, MemoryVariable } from '@/app/components/workflow/types'
export const useForm = (chatVar?: ConversationVariable) => {
export const useForm = (chatVar?: ConversationVariable | MemoryVariable) => {
const { t } = useTranslation()
const typeSchema = useTypeSchema()

View File

@ -8,7 +8,10 @@ import {
} from 'reactflow'
import { RiBookOpenLine, RiCloseLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useStore } from '@/app/components/workflow/store'
import {
useStore,
useWorkflowStore,
} from '@/app/components/workflow/store'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import { BubbleX, LongArrowLeft, LongArrowRight } from '@/app/components/base/icons/src/vender/line/others'
import BlockIcon from '@/app/components/workflow/block-icon'
@ -17,6 +20,7 @@ import VariableItem from '@/app/components/workflow/panel/chat-variable-panel/co
import RemoveEffectVarConfirm from '@/app/components/workflow/nodes/_base/components/remove-effect-var-confirm'
import type {
ConversationVariable,
MemoryVariable,
} from '@/app/components/workflow/types'
import { findUsedVarNodes, updateNodeVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { useNodesSyncDraft } from '@/app/components/workflow/hooks/use-nodes-sync-draft'
@ -24,34 +28,39 @@ import { BlockEnum } from '@/app/components/workflow/types'
import { useDocLink } from '@/context/i18n'
import cn from '@/utils/classnames'
import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud'
import { ChatVarType } from './type'
const ChatVariablePanel = () => {
const { t } = useTranslation()
const docLink = useDocLink()
const store = useStoreApi()
const workflowStore = useWorkflowStore()
const setShowChatVariablePanel = useStore(s => s.setShowChatVariablePanel)
const varList = useStore(s => s.conversationVariables) as ConversationVariable[]
const memoryVariables = useStore(s => s.memoryVariables) as MemoryVariable[]
const updateChatVarList = useStore(s => s.setConversationVariables)
const setMemoryVariables = useStore(s => s.setMemoryVariables)
const { doSyncWorkflowDraft } = useNodesSyncDraft()
const {
invalidateConversationVarValues,
} = useInspectVarsCrud()
const handleVarChanged = useCallback(() => {
const handleVarChanged = useCallback((updateMemoryVariables?: boolean) => {
doSyncWorkflowDraft(false, {
onSuccess() {
invalidateConversationVarValues()
if (!updateMemoryVariables)
invalidateConversationVarValues()
},
})
}, [doSyncWorkflowDraft, invalidateConversationVarValues])
const [showTip, setShowTip] = useState(true)
const [showVariableModal, setShowVariableModal] = useState(false)
const [currentVar, setCurrentVar] = useState<ConversationVariable>()
const [currentVar, setCurrentVar] = useState<ConversationVariable | MemoryVariable>()
const [showRemoveVarConfirm, setShowRemoveConfirm] = useState(false)
const [cacheForDelete, setCacheForDelete] = useState<ConversationVariable>()
const [cacheForDelete, setCacheForDelete] = useState<ConversationVariable | MemoryVariable>()
const getEffectedNodes = useCallback((chatVar: ConversationVariable) => {
const getEffectedNodes = useCallback((chatVar: ConversationVariable | MemoryVariable) => {
const { getNodes } = store.getState()
const allNodes = getNodes()
return findUsedVarNodes(
@ -60,7 +69,7 @@ const ChatVariablePanel = () => {
)
}, [store])
const removeUsedVarInNodes = useCallback((chatVar: ConversationVariable) => {
const removeUsedVarInNodes = useCallback((chatVar: ConversationVariable | MemoryVariable) => {
const { getNodes, setNodes } = store.getState()
const effectedNodes = getEffectedNodes(chatVar)
const newNodes = getNodes().map((node) => {
@ -72,20 +81,21 @@ const ChatVariablePanel = () => {
setNodes(newNodes)
}, [getEffectedNodes, store])
const handleEdit = (chatVar: ConversationVariable) => {
const handleEdit = (chatVar: ConversationVariable | MemoryVariable) => {
setCurrentVar(chatVar)
setShowVariableModal(true)
}
const handleDelete = useCallback((chatVar: ConversationVariable) => {
const handleDelete = useCallback((chatVar: ConversationVariable | MemoryVariable) => {
removeUsedVarInNodes(chatVar)
const varList = workflowStore.getState().conversationVariables
updateChatVarList(varList.filter(v => v.id !== chatVar.id))
setCacheForDelete(undefined)
setShowRemoveConfirm(false)
handleVarChanged()
}, [handleVarChanged, removeUsedVarInNodes, updateChatVarList, varList])
handleVarChanged(chatVar.value_type === ChatVarType.Memory)
}, [handleVarChanged, removeUsedVarInNodes, updateChatVarList])
const deleteCheck = useCallback((chatVar: ConversationVariable) => {
const deleteCheck = useCallback((chatVar: ConversationVariable | MemoryVariable) => {
const effectedNodes = getEffectedNodes(chatVar)
if (effectedNodes.length > 0) {
setCacheForDelete(chatVar)
@ -96,17 +106,24 @@ const ChatVariablePanel = () => {
}
}, [getEffectedNodes, handleDelete])
const handleSave = useCallback(async (chatVar: ConversationVariable) => {
const handleSave = useCallback(async (chatVar: ConversationVariable | MemoryVariable) => {
if (chatVar.value_type === ChatVarType.Memory) {
const memoryVarList = workflowStore.getState().memoryVariables
setMemoryVariables([chatVar, ...memoryVarList])
handleVarChanged(true)
return
}
const varList = workflowStore.getState().conversationVariables
// add chatVar
if (!currentVar) {
const newList = [chatVar, ...varList]
const newList = [chatVar, ...varList] as ConversationVariable[]
updateChatVarList(newList)
handleVarChanged()
return
}
// edit chatVar
const newList = varList.map(v => v.id === currentVar.id ? chatVar : v)
updateChatVarList(newList)
updateChatVarList(newList as ConversationVariable[])
// side effects of rename env
if (currentVar.name !== chatVar.name) {
const { getNodes, setNodes } = store.getState()
@ -120,7 +137,7 @@ const ChatVariablePanel = () => {
setNodes(newNodes)
}
handleVarChanged()
}, [currentVar, getEffectedNodes, handleVarChanged, store, updateChatVarList, varList])
}, [currentVar, getEffectedNodes, handleVarChanged, store, updateChatVarList])
return (
<div
@ -196,6 +213,16 @@ const ChatVariablePanel = () => {
/>
</div>
<div className='grow overflow-y-auto rounded-b-2xl px-4'>
{
memoryVariables.map(memoryVariable => (
<VariableItem
key={memoryVariable.id}
item={memoryVariable}
onEdit={handleEdit}
onDelete={deleteCheck}
/>
))
}
{varList.map(chatVar => (
<VariableItem
key={chatVar.id}

View File

@ -8,6 +8,8 @@ import {
import { createStore } from 'zustand/vanilla'
import type { ChatVariableSliceShape } from './chat-variable-slice'
import { createChatVariableSlice } from './chat-variable-slice'
import type { MemoryVariableSliceShape } from './memory-variable-slice'
import { createMemoryVariableSlice } from './memory-variable-slice'
import type { EnvVariableSliceShape } from './env-variable-slice'
import { createEnvVariableSlice } from './env-variable-slice'
import type { FormSliceShape } from './form-slice'
@ -43,6 +45,7 @@ export type SliceFromInjection
export type Shape
= ChatVariableSliceShape
& MemoryVariableSliceShape
& EnvVariableSliceShape
& FormSliceShape
& HelpLineSliceShape
@ -68,6 +71,7 @@ export const createWorkflowStore = (params: CreateWorkflowStoreParams) => {
return createStore<Shape>((...args) => ({
...createChatVariableSlice(...args),
...createMemoryVariableSlice(...args),
...createEnvVariableSlice(...args),
...createFormSlice(...args),
...createHelpLineSlice(...args),

View File

@ -0,0 +1,14 @@
import type { StateCreator } from 'zustand'
import type { MemoryVariable } from '@/app/components/workflow/types'
export type MemoryVariableSliceShape = {
memoryVariables: MemoryVariable[]
setMemoryVariables: (memoryVariables: MemoryVariable[]) => void
}
export const createMemoryVariableSlice: StateCreator<MemoryVariableSliceShape> = (set) => {
return ({
memoryVariables: [],
setMemoryVariables: memoryVariables => set(() => ({ memoryVariables })),
})
}

View File

@ -167,6 +167,8 @@ export type EnvironmentVariable = {
}
export type MemoryVariable = {
id: string
name: string
template?: string
instruction?: string
schedule_mode?: string
@ -176,6 +178,7 @@ export type MemoryVariable = {
scope?: string
term?: string
end_user_editable?: boolean
value_type: ChatVarType
}
export type ConversationVariable = {
@ -184,7 +187,7 @@ export type ConversationVariable = {
value_type: ChatVarType
value: any
description?: string
} & MemoryVariable
}
export type GlobalVariable = {
name: string

View File

@ -88,6 +88,7 @@ const UpdateDSLModal = ({
hash,
conversation_variables,
environment_variables,
memory_blocks,
} = await fetchWorkflowDraft(`/apps/${app_id}/workflows/draft`)
const { nodes, edges, viewport } = graph
@ -126,6 +127,7 @@ const UpdateDSLModal = ({
hash,
conversation_variables: conversation_variables || [],
environment_variables: environment_variables || [],
memory_blocks: memory_blocks || [],
},
} as any)
}, [eventEmitter])

View File

@ -377,6 +377,7 @@ const translation = {
block: {
title: 'Memory Block',
desc: 'AI remembers specific information you define using custom templates',
empty: 'Please add a memory variable in the Prompt',
},
},
memories: {

View File

@ -377,6 +377,7 @@ const translation = {
block: {
title: '记忆块',
desc: 'AI 会记住你定义的特定信息',
empty: '请在提示词中添加记忆变量以启用记忆块',
},
},
memories: {

View File

@ -1,5 +1,5 @@
import type { Viewport } from 'reactflow'
import type { BlockEnum, CommonNodeType, ConversationVariable, Edge, EnvironmentVariable, InputVar, Node, ValueSelector, VarType, Variable } from '@/app/components/workflow/types'
import type { BlockEnum, CommonNodeType, ConversationVariable, Edge, EnvironmentVariable, InputVar, MemoryVariable, Node, ValueSelector, VarType, Variable } from '@/app/components/workflow/types'
import type { TransferMethod } from '@/types/app'
import type { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
import type { RAGPipelineVariables } from '@/models/pipeline'
@ -130,6 +130,7 @@ export type FetchWorkflowDraftResponse = {
tool_published: boolean
environment_variables?: EnvironmentVariable[]
conversation_variables?: ConversationVariable[]
memory_blocks?: MemoryVariable[]
rag_pipeline_variables?: RAGPipelineVariables
version: string
marked_name: string