feat: memory

This commit is contained in:
zxhlyh 2025-10-28 18:27:59 +08:00
parent 38d27100ac
commit 10f5270320
8 changed files with 148 additions and 92 deletions

View File

@ -341,7 +341,7 @@ const Editor: FC<Props> = ({
</div>
</div>
</Wrap>
{isMemorySupported && <MemoryCreateButton instanceId={instanceId} hideTrigger />}
{isMemorySupported && <MemoryCreateButton nodeId={nodeId || ''} instanceId={instanceId} hideTrigger />}
</>
)

View File

@ -13,6 +13,7 @@ import ActionButton from '@/app/components/base/action-button'
import { useMemoryVariables } from './hooks/use-memory-variables'
import Confirm from '@/app/components/base/confirm'
import { Memory as MemoryIcon } from '@/app/components/base/icons/src/vender/line/others'
import VariableModal from '@/app/components/workflow/panel/chat-variable-panel/components/variable-modal'
type BlockMemoryProps = {
id: string
@ -20,17 +21,18 @@ type BlockMemoryProps = {
}
const BlockMemory = ({ id }: BlockMemoryProps) => {
const { t } = useTranslation()
const { memoryVariablesInUsed } = useMemoryVariables(id)
const {
memoryVariablesInUsed,
editMemoryVariable,
handleSetEditMemoryVariable,
handleEdit,
} = useMemoryVariables(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'),
@ -67,7 +69,7 @@ const BlockMemory = ({ id }: BlockMemoryProps) => {
<ActionButton
className='hidden shrink-0 group-hover:block'
size='m'
onClick={() => handleEdit(memoryVariable.id)}
onClick={() => handleSetEditMemoryVariable(memoryVariable.id)}
>
<RiEditLine className='h-4 w-4 text-text-tertiary' />
</ActionButton>
@ -93,6 +95,16 @@ const BlockMemory = ({ id }: BlockMemoryProps) => {
/>
)
}
{
!!editMemoryVariable && (
<VariableModal
chatVar={editMemoryVariable}
onClose={() => handleSetEditMemoryVariable(undefined)}
onSave={handleEdit}
nodeScopeMemoryVariable={{ nodeId: id }}
/>
)
}
</>
)
}

View File

@ -1,8 +1,20 @@
import { useMemo } from 'react'
import { useStore } from '@/app/components/workflow/store'
import {
useCallback,
useMemo,
useState,
} from 'react'
import {
useStore,
useWorkflowStore,
} from '@/app/components/workflow/store'
import type { MemoryVariable } from '@/app/components/workflow/types'
import { useNodesSyncDraft } from '@/app/components/workflow/hooks/use-nodes-sync-draft'
export const useMemoryVariables = (nodeId: string) => {
const workflowStore = useWorkflowStore()
const memoryVariables = useStore(s => s.memoryVariables)
const [editMemoryVariable, setEditMemoryVariable] = useState<MemoryVariable | undefined>(undefined)
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const memoryVariablesInUsed = useMemo(() => {
return memoryVariables.filter(variable => variable.node_id === nodeId)
@ -11,8 +23,36 @@ export const useMemoryVariables = (nodeId: string) => {
const handleDelete = (blockId: string) => {
console.log('delete', blockId)
}
const handleSave = useCallback((newMemoryVar: MemoryVariable) => {
const { memoryVariables, setMemoryVariables } = workflowStore.getState()
const newList = [newMemoryVar, ...memoryVariables]
setMemoryVariables(newList)
handleSyncWorkflowDraft()
}, [handleSyncWorkflowDraft, workflowStore])
const handleSetEditMemoryVariable = (memoryVariableId?: string) => {
if (!memoryVariableId) {
setEditMemoryVariable(undefined)
return
}
const memoryVariable = memoryVariables.find(variable => variable.id === memoryVariableId)
setEditMemoryVariable(memoryVariable)
}
const handleEdit = (memoryVariable: MemoryVariable) => {
const { memoryVariables, setMemoryVariables } = workflowStore.getState()
const newList = memoryVariables.map(variable => variable.id === memoryVariable.id ? memoryVariable : variable)
setMemoryVariables(newList)
handleSyncWorkflowDraft()
}
return {
memoryVariablesInUsed,
handleDelete,
handleSave,
handleSetEditMemoryVariable,
handleEdit,
editMemoryVariable,
}
}

View File

@ -1,7 +1,9 @@
import {
memo,
useCallback,
} from 'react'
import { useTranslation } from 'react-i18next'
import { RiAddLine } from '@remixicon/react'
import Collapse from '@/app/components/workflow/nodes/_base/components/collapse'
import type {
Node,
@ -17,6 +19,8 @@ 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'
import { ActionButton } from '@/app/components/base/action-button'
import cn from '@/utils/classnames'
type MemoryProps = Pick<Node, 'id' | 'data'> & {
readonly?: boolean
@ -38,6 +42,14 @@ const MemorySystem = ({
handleUpdateMemory,
} = useMemory(id, data as LLMNodeType)
const renderTrigger = useCallback((open?: boolean) => {
return (
<ActionButton className={cn('shrink-0', open && 'bg-state-base-hover')}>
<RiAddLine className='h-4 w-4' />
</ActionButton>
)
}, [])
return (
<>
<div className=''>
@ -64,7 +76,10 @@ const MemorySystem = ({
<>
<Divider type='vertical' className='!ml-1.5 !mr-1 h-3 !w-px bg-divider-regular' />
<div onClick={e => e.stopPropagation()}>
<MemoryCreateButton />
<MemoryCreateButton
nodeId={id}
renderTrigger={renderTrigger}
/>
</div>
</>
)

View File

@ -1,5 +1,4 @@
import { useCallback, useState } from 'react'
import { RiAddLine } from '@remixicon/react'
import VariableModal from '@/app/components/workflow/panel/chat-variable-panel/components/variable-modal'
import type { OffsetOptions, Placement } from '@floating-ui/react'
import {
@ -7,52 +6,36 @@ import {
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import ActionButton from '@/app/components/base/action-button'
import type { MemoryVariable } from '@/app/components/workflow/types'
import { useStore } from '@/app/components/workflow/store'
import { useNodesSyncDraft } from '@/app/components/workflow/hooks/use-nodes-sync-draft'
import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { MEMORY_VAR_CREATED_BY_MODAL_BY_EVENT_EMITTER, MEMORY_VAR_MODAL_SHOW_BY_EVENT_EMITTER } from '@/app/components/workflow/nodes/_base/components/prompt/type'
import { useMemoryVariables } from './hooks/use-memory-variables'
type Props = {
placement?: Placement
offset?: number | OffsetOptions
hideTrigger?: boolean
instanceId?: string
nodeId: string
renderTrigger?: (open?: boolean) => React.ReactNode
}
const MemoryCreateButton = ({
placement,
offset,
hideTrigger,
instanceId,
nodeId,
renderTrigger = () => <div></div>,
}: Props) => {
const { eventEmitter } = useEventEmitterContextContext()
const [open, setOpen] = useState(false)
const varList = useStore(s => s.memoryVariables) as MemoryVariable[]
const updateMemoryVarList = useStore(s => s.setMemoryVariables)
const { doSyncWorkflowDraft } = useNodesSyncDraft()
const {
invalidateConversationVarValues,
} = useInspectVarsCrud()
const handleVarChanged = useCallback(() => {
doSyncWorkflowDraft(false, {
onSuccess() {
invalidateConversationVarValues()
},
})
}, [doSyncWorkflowDraft, invalidateConversationVarValues])
const { handleSave: handleSaveMemoryVariables } = useMemoryVariables(nodeId)
const handleSave = useCallback((newMemoryVar: MemoryVariable) => {
const newList = [newMemoryVar, ...varList]
updateMemoryVarList(newList)
handleVarChanged()
setOpen(false)
handleSaveMemoryVariables(newMemoryVar)
if (instanceId)
eventEmitter?.emit({ type: MEMORY_VAR_CREATED_BY_MODAL_BY_EVENT_EMITTER, instanceId, variable: ['memory', newMemoryVar.name] } as any)
}, [varList, updateMemoryVarList, handleVarChanged, setOpen, eventEmitter, instanceId])
}, [handleSaveMemoryVariables, eventEmitter, instanceId])
eventEmitter?.useSubscription((v: any) => {
if (v.type === MEMORY_VAR_MODAL_SHOW_BY_EVENT_EMITTER && v.instanceId === instanceId)
@ -68,12 +51,7 @@ const MemoryCreateButton = ({
offset={offset}
>
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
{hideTrigger && <div></div>}
{!hideTrigger && (
<ActionButton className='shrink-0'>
<RiAddLine className='h-4 w-4' />
</ActionButton>
)}
{renderTrigger?.(open)}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[11]'>
<VariableModal
@ -81,6 +59,7 @@ const MemoryCreateButton = ({
onClose={() => {
setOpen(false)
}}
nodeScopeMemoryVariable={nodeId ? { nodeId } : undefined}
/>
</PortalToFollowElemContent>
</PortalToFollowElem>

View File

@ -19,15 +19,20 @@ import VariableForm from '@/app/components/base/form/form-scenarios/variable'
import { useForm } from '../hooks'
export type ModalPropsType = {
className?: string
chatVar?: ConversationVariable | MemoryVariable
onClose: () => void
onSave: (chatVar: ConversationVariable | MemoryVariable) => void
nodeScopeMemoryVariable?: {
nodeId: string
}
}
const ChatVariableModal = ({
chatVar,
onClose,
onSave,
nodeScopeMemoryVariable,
}: ModalPropsType) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
@ -36,7 +41,7 @@ const ChatVariableModal = ({
const {
formSchemas,
defaultValues,
} = useForm(chatVar)
} = useForm(chatVar, nodeScopeMemoryVariable)
const formRef = useRef<FormRefObject>(null)
const form = useTanstackForm({
defaultValues,

View File

@ -10,14 +10,14 @@ import {
} from './use-memory-schema'
import type { ConversationVariable, MemoryVariable } from '@/app/components/workflow/types'
export const useForm = (chatVar?: ConversationVariable | MemoryVariable) => {
export const useForm = (chatVar?: ConversationVariable | MemoryVariable, nodeScopeMemoryVariable?: { nodeId: string }) => {
const { t } = useTranslation()
const typeSchema = useTypeSchema()
const valueSchema = useValueSchema()
const editInJSONSchema = useEditInJSONSchema()
const memorySchema = useMemorySchema()
const memoryDefaultValues = useMemoryDefaultValues()
const memorySchema = useMemorySchema(nodeScopeMemoryVariable)
const memoryDefaultValues = useMemoryDefaultValues(nodeScopeMemoryVariable)
const formSchemas = useMemo(() => {
return [

View File

@ -3,9 +3,56 @@ import { FormTypeEnum } from '@/app/components/base/form/types'
import type { FormSchema } from '@/app/components/base/form/types'
import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type'
export const useMemorySchema = () => {
export const useMemorySchema = (nodeScopeMemoryVariable?: { nodeId: string }) => {
const { t } = useTranslation()
const scopeSchema = [
{
name: 'scope',
label: t('workflow.chatVariable.modal.scope'),
type: FormTypeEnum.radio,
default: 'app',
fieldClassName: 'flex justify-between',
inputClassName: 'w-[102px]',
options: [
{
label: 'App',
value: 'app',
},
{
label: 'Node',
value: 'node',
},
],
show_on: [
{
variable: 'value_type',
value: [ChatVarType.Memory],
},
],
selfFormProps: {
withTopDivider: true,
},
},
{
name: 'node_id',
label: 'Node',
type: FormTypeEnum.nodeSelector,
fieldClassName: 'flex justify-between',
default: '',
show_on: [
{
variable: 'value_type',
value: [ChatVarType.Memory],
},
{
variable: 'scope',
value: 'node',
},
],
},
]
return [
{
name: 'template',
@ -90,50 +137,7 @@ export const useMemorySchema = () => {
inputWrapperClassName: 'w-[102px]',
},
},
{
name: 'scope',
label: t('workflow.chatVariable.modal.scope'),
type: FormTypeEnum.radio,
default: 'app',
fieldClassName: 'flex justify-between',
inputClassName: 'w-[102px]',
options: [
{
label: 'App',
value: 'app',
},
{
label: 'Node',
value: 'node',
},
],
show_on: [
{
variable: 'value_type',
value: [ChatVarType.Memory],
},
],
selfFormProps: {
withTopDivider: true,
},
},
{
name: 'node_id',
label: 'Node',
type: FormTypeEnum.nodeSelector,
fieldClassName: 'flex justify-between',
default: '',
show_on: [
{
variable: 'value_type',
value: [ChatVarType.Memory],
},
{
variable: 'scope',
value: 'node',
},
],
},
...(!nodeScopeMemoryVariable ? scopeSchema : []),
{
name: 'term',
label: t('workflow.chatVariable.modal.term'),
@ -238,14 +242,15 @@ export const useMemorySchema = () => {
] as FormSchema[]
}
export const useMemoryDefaultValues = () => {
export const useMemoryDefaultValues = (nodeScopeMemoryVariable?: { nodeId: string }) => {
return {
template: '',
instruction: '',
strategy: 'on_turns',
update_turns: 500,
preserved_turns: 10,
scope: 'app',
scope: nodeScopeMemoryVariable ? 'node' : 'app',
node_id: nodeScopeMemoryVariable?.nodeId || '',
term: 'session',
more: false,
model: '',