fix(memory): fix retrieval issues and enable database backup (#12073)

* fix(memory): fix retrieval issues and enable database backup

- Fix memory retrieval by storing model references instead of API client configs
  (baseURL was missing v1 suffix causing retrieval failures)
- Move memory database to DATA_PATH/Memory for proper backup support
- Add migration to convert legacy embedderApiClient/llmApiClient to model references
- Simplify IPC handlers by removing unnecessary async/await wrappers
- Rename and relocate MemorySettingsModal for better organization

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(UserSelector): simplify user label rendering and remove unused dependencies

- Update UserSelector component to directly use user IDs as labels instead of rendering them through a function.
- Remove unnecessary dependency on the renderLabel function to streamline the code.

* refactor(UserSelector): remove unused dependencies and simplify user avatar logic

- Eliminate the getUserAvatar function and directly use user IDs for rendering.
- Remove the HStack and Avatar components from the renderLabel function to streamline the UserSelector component.

* refactor(ipc): simplify IPC handler for deleting all memories for a user and streamline error logging

- Remove unnecessary async/await from the Memory_DeleteAllMemoriesForUser handler.
- Simplify error logging in useAppInit hook for memory service configuration updates.
- Update persisted reducer version from 191 to 189 in the store configuration.

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
亢奋猫 2025-12-23 13:21:29 +08:00 committed by GitHub
parent 6bdaba8a15
commit 6815ab65d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 170 additions and 176 deletions

View File

@ -318,6 +318,7 @@ export enum IpcChannel {
Memory_DeleteUser = 'memory:delete-user',
Memory_DeleteAllMemoriesForUser = 'memory:delete-all-memories-for-user',
Memory_GetUsersList = 'memory:get-users-list',
Memory_MigrateMemoryDb = 'memory:migrate-memory-db',
// TRACE
TRACE_SAVE_DATA = 'trace:saveData',

View File

@ -686,36 +686,19 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle(IpcChannel.KnowledgeBase_Check_Quota, KnowledgeService.checkQuota.bind(KnowledgeService))
// memory
ipcMain.handle(IpcChannel.Memory_Add, async (_, messages, config) => {
return await memoryService.add(messages, config)
})
ipcMain.handle(IpcChannel.Memory_Search, async (_, query, config) => {
return await memoryService.search(query, config)
})
ipcMain.handle(IpcChannel.Memory_List, async (_, config) => {
return await memoryService.list(config)
})
ipcMain.handle(IpcChannel.Memory_Delete, async (_, id) => {
return await memoryService.delete(id)
})
ipcMain.handle(IpcChannel.Memory_Update, async (_, id, memory, metadata) => {
return await memoryService.update(id, memory, metadata)
})
ipcMain.handle(IpcChannel.Memory_Get, async (_, memoryId) => {
return await memoryService.get(memoryId)
})
ipcMain.handle(IpcChannel.Memory_SetConfig, async (_, config) => {
memoryService.setConfig(config)
})
ipcMain.handle(IpcChannel.Memory_DeleteUser, async (_, userId) => {
return await memoryService.deleteUser(userId)
})
ipcMain.handle(IpcChannel.Memory_DeleteAllMemoriesForUser, async (_, userId) => {
return await memoryService.deleteAllMemoriesForUser(userId)
})
ipcMain.handle(IpcChannel.Memory_GetUsersList, async () => {
return await memoryService.getUsersList()
})
ipcMain.handle(IpcChannel.Memory_Add, (_, messages, config) => memoryService.add(messages, config))
ipcMain.handle(IpcChannel.Memory_Search, (_, query, config) => memoryService.search(query, config))
ipcMain.handle(IpcChannel.Memory_List, (_, config) => memoryService.list(config))
ipcMain.handle(IpcChannel.Memory_Delete, (_, id) => memoryService.delete(id))
ipcMain.handle(IpcChannel.Memory_Update, (_, id, memory, metadata) => memoryService.update(id, memory, metadata))
ipcMain.handle(IpcChannel.Memory_Get, (_, memoryId) => memoryService.get(memoryId))
ipcMain.handle(IpcChannel.Memory_SetConfig, (_, config) => memoryService.setConfig(config))
ipcMain.handle(IpcChannel.Memory_DeleteUser, (_, userId) => memoryService.deleteUser(userId))
ipcMain.handle(IpcChannel.Memory_DeleteAllMemoriesForUser, (_, userId) =>
memoryService.deleteAllMemoriesForUser(userId)
)
ipcMain.handle(IpcChannel.Memory_GetUsersList, () => memoryService.getUsersList())
ipcMain.handle(IpcChannel.Memory_MigrateMemoryDb, () => memoryService.migrateMemoryDb())
// window
ipcMain.handle(IpcChannel.Windows_SetMinimumSize, (_, width: number, height: number) => {

View File

@ -1,7 +1,9 @@
import type { Client } from '@libsql/client'
import { createClient } from '@libsql/client'
import { loggerService } from '@logger'
import { DATA_PATH } from '@main/config'
import Embeddings from '@main/knowledge/embedjs/embeddings/Embeddings'
import { makeSureDirExists } from '@main/utils'
import type {
AddMemoryOptions,
AssistantMessage,
@ -13,6 +15,7 @@ import type {
} from '@types'
import crypto from 'crypto'
import { app } from 'electron'
import fs from 'fs'
import path from 'path'
import { MemoryQueries } from './queries'
@ -71,6 +74,21 @@ export class MemoryService {
return MemoryService.instance
}
/**
* Migrate the memory database from the old path to the new path
* If the old memory database exists, rename it to the new path
*/
public migrateMemoryDb(): void {
const oldMemoryDbPath = path.join(app.getPath('userData'), 'memories.db')
const memoryDbPath = path.join(DATA_PATH, 'Memory', 'memories.db')
makeSureDirExists(path.dirname(memoryDbPath))
if (fs.existsSync(oldMemoryDbPath)) {
fs.renameSync(oldMemoryDbPath, memoryDbPath)
}
}
/**
* Initialize the database connection and create tables
*/
@ -80,11 +98,12 @@ export class MemoryService {
}
try {
const userDataPath = app.getPath('userData')
const dbPath = path.join(userDataPath, 'memories.db')
const memoryDbPath = path.join(DATA_PATH, 'Memory', 'memories.db')
makeSureDirExists(path.dirname(memoryDbPath))
this.db = createClient({
url: `file:${dbPath}`,
url: `file:${memoryDbPath}`,
intMode: 'number'
})
@ -168,12 +187,13 @@ export class MemoryService {
// Generate embedding if model is configured
let embedding: number[] | null = null
const embedderApiClient = this.config?.embedderApiClient
if (embedderApiClient) {
const embeddingModel = this.config?.embeddingModel
if (embeddingModel) {
try {
embedding = await this.generateEmbedding(trimmedMemory)
logger.debug(
`Generated embedding for restored memory with dimension: ${embedding.length} (target: ${this.config?.embedderDimensions || MemoryService.UNIFIED_DIMENSION})`
`Generated embedding for restored memory with dimension: ${embedding.length} (target: ${this.config?.embeddingDimensions || MemoryService.UNIFIED_DIMENSION})`
)
} catch (error) {
logger.error('Failed to generate embedding for restored memory:', error as Error)
@ -211,11 +231,11 @@ export class MemoryService {
// Generate embedding if model is configured
let embedding: number[] | null = null
if (this.config?.embedderApiClient) {
if (this.config?.embeddingModel) {
try {
embedding = await this.generateEmbedding(trimmedMemory)
logger.debug(
`Generated embedding with dimension: ${embedding.length} (target: ${this.config?.embedderDimensions || MemoryService.UNIFIED_DIMENSION})`
`Generated embedding with dimension: ${embedding.length} (target: ${this.config?.embeddingDimensions || MemoryService.UNIFIED_DIMENSION})`
)
// Check for similar memories using vector similarity
@ -300,7 +320,7 @@ export class MemoryService {
try {
// If we have an embedder model configured, use vector search
if (this.config?.embedderApiClient) {
if (this.config?.embeddingModel) {
try {
const queryEmbedding = await this.generateEmbedding(query)
return await this.hybridSearch(query, queryEmbedding, { limit, userId, agentId, filters })
@ -497,11 +517,11 @@ export class MemoryService {
// Generate new embedding if model is configured
let embedding: number[] | null = null
if (this.config?.embedderApiClient) {
if (this.config?.embeddingModel) {
try {
embedding = await this.generateEmbedding(memory)
logger.debug(
`Updated embedding with dimension: ${embedding.length} (target: ${this.config?.embedderDimensions || MemoryService.UNIFIED_DIMENSION})`
`Updated embedding with dimension: ${embedding.length} (target: ${this.config?.embeddingDimensions || MemoryService.UNIFIED_DIMENSION})`
)
} catch (error) {
logger.error('Failed to generate embedding for update:', error as Error)
@ -710,21 +730,22 @@ export class MemoryService {
* Generate embedding for text
*/
private async generateEmbedding(text: string): Promise<number[]> {
if (!this.config?.embedderApiClient) {
if (!this.config?.embeddingModel) {
throw new Error('Embedder model not configured')
}
try {
// Initialize embeddings instance if needed
if (!this.embeddings) {
if (!this.config.embedderApiClient) {
if (!this.config.embeddingApiClient) {
throw new Error('Embedder provider not configured')
}
this.embeddings = new Embeddings({
embedApiClient: this.config.embedderApiClient,
dimensions: this.config.embedderDimensions
embedApiClient: this.config.embeddingApiClient,
dimensions: this.config.embeddingDimensions
})
await this.embeddings.init()
}

View File

@ -310,7 +310,8 @@ const api = {
deleteUser: (userId: string) => ipcRenderer.invoke(IpcChannel.Memory_DeleteUser, userId),
deleteAllMemoriesForUser: (userId: string) =>
ipcRenderer.invoke(IpcChannel.Memory_DeleteAllMemoriesForUser, userId),
getUsersList: () => ipcRenderer.invoke(IpcChannel.Memory_GetUsersList)
getUsersList: () => ipcRenderer.invoke(IpcChannel.Memory_GetUsersList),
migrateMemoryDb: () => ipcRenderer.invoke(IpcChannel.Memory_MigrateMemoryDb)
},
window: {
setMinimumSize: (width: number, height: number) =>

View File

@ -24,7 +24,8 @@ export const memorySearchTool = () => {
}
const memoryConfig = selectMemoryConfig(store.getState())
if (!memoryConfig.llmApiClient || !memoryConfig.embedderApiClient) {
if (!memoryConfig.llmModel || !memoryConfig.embeddingModel) {
return []
}

View File

@ -268,9 +268,7 @@ export function useAppInit() {
// Update memory service configuration when it changes
useEffect(() => {
const memoryService = MemoryService.getInstance()
memoryService.updateConfig().catch((error) => {
logger.error('Failed to update memory config:', error)
})
memoryService.updateConfig().catch((error) => logger.error('Failed to update memory config:', error))
}, [memoryConfig])
useEffect(() => {

View File

@ -1,7 +1,7 @@
import { InfoCircleOutlined } from '@ant-design/icons'
import { loggerService } from '@logger'
import { Box } from '@renderer/components/Layout'
import MemoriesSettingsModal from '@renderer/pages/memory/settings-modal'
import MemoriesSettingsModal from '@renderer/pages/settings/MemorySettings/MemorySettingsModal'
import MemoryService from '@renderer/services/MemoryService'
import { selectGlobalMemoryEnabled, selectMemoryConfig } from '@renderer/store/memory'
import type { Assistant, AssistantSettings } from '@renderer/types'
@ -68,7 +68,7 @@ const AssistantMemorySettings: React.FC<Props> = ({ assistant, updateAssistant,
window.location.hash = '#/settings/memory'
}
const isMemoryConfigured = memoryConfig.embedderApiClient && memoryConfig.llmApiClient
const isMemoryConfigured = memoryConfig.embeddingModel && memoryConfig.llmModel
const isMemoryEnabled = globalMemoryEnabled && isMemoryConfigured
return (
@ -130,16 +130,16 @@ const AssistantMemorySettings: React.FC<Props> = ({ assistant, updateAssistant,
<Text strong>{t('memory.stored_memories')}: </Text>
<Text>{memoryStats.loading ? t('common.loading') : memoryStats.count}</Text>
</div>
{memoryConfig.embedderApiClient && (
{memoryConfig.embeddingModel && (
<div>
<Text strong>{t('memory.embedding_model')}: </Text>
<Text code>{memoryConfig.embedderApiClient.model}</Text>
<Text code>{memoryConfig.embeddingModel.id}</Text>
</div>
)}
{memoryConfig.llmApiClient && (
{memoryConfig.llmModel && (
<div>
<Text strong>{t('memory.llm_model')}: </Text>
<Text code>{memoryConfig.llmApiClient.model}</Text>
<Text code>{memoryConfig.llmModel.id}</Text>
</div>
)}
</Space>

View File

@ -5,7 +5,6 @@ import { HStack } from '@renderer/components/Layout'
import TextBadge from '@renderer/components/TextBadge'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useModel } from '@renderer/hooks/useModel'
import MemoriesSettingsModal from '@renderer/pages/memory/settings-modal'
import MemoryService from '@renderer/services/MemoryService'
import {
selectCurrentUserId,
@ -34,6 +33,7 @@ import {
SettingTitle
} from '../index'
import { DEFAULT_USER_ID } from './constants'
import MemorySettingsModal from './MemorySettingsModal'
import UserSelector from './UserSelector'
const logger = loggerService.withContext('MemorySettings')
@ -154,23 +154,17 @@ const EditMemoryModal: React.FC<EditMemoryModalProps> = ({ visible, memory, onCa
open={visible}
onCancel={onCancel}
width={600}
centered
transitionName="animation-move-down"
okButtonProps={{ loading: loading, title: t('common.save'), onClick: () => form.submit() }}
styles={{
header: {
borderBottom: '0.5px solid var(--color-border)',
paddingBottom: 16
},
body: {
paddingTop: 24
paddingBottom: 16,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0
}
}}
footer={[
<Button key="cancel" size="large" onClick={onCancel}>
{t('common.cancel')}
</Button>,
<Button key="submit" type="primary" size="large" loading={loading} onClick={() => form.submit()}>
{t('common.save')}
</Button>
]}>
}}>
<Form form={form} layout="vertical" onFinish={handleSubmit}>
<Form.Item
label={t('memory.memory_content')}
@ -548,10 +542,10 @@ const MemorySettings = () => {
}
const memoryConfig = useSelector(selectMemoryConfig)
const embedderModel = useModel(memoryConfig.embedderApiClient?.model, memoryConfig.embedderApiClient?.provider)
const embeddingModel = useModel(memoryConfig.embeddingModel?.id, memoryConfig.embeddingModel?.provider)
const handleGlobalMemoryToggle = async (enabled: boolean) => {
if (enabled && !embedderModel) {
if (enabled && !embeddingModel) {
window.keyv.set('memory.wait.settings', true)
return setSettingsModalVisible(true)
}
@ -799,7 +793,7 @@ const MemorySettings = () => {
existingUsers={[...uniqueUsers, DEFAULT_USER_ID]}
/>
<MemoriesSettingsModal
<MemorySettingsModal
visible={settingsModalVisible}
onSubmit={async () => await handleSettingsSubmit()}
onCancel={handleSettingsCancel}

View File

@ -1,10 +1,9 @@
import { loggerService } from '@logger'
import AiProvider from '@renderer/aiCore'
import InputEmbeddingDimension from '@renderer/components/InputEmbeddingDimension'
import ModelSelector from '@renderer/components/ModelSelector'
import { InfoTooltip } from '@renderer/components/TooltipIcons'
import { isEmbeddingModel, isRerankModel } from '@renderer/config/models'
import { useModel } from '@renderer/hooks/useModel'
import { getModel, useModel } from '@renderer/hooks/useModel'
import { useProviders } from '@renderer/hooks/useProvider'
import { getModelUniqId } from '@renderer/services/ModelService'
import { selectMemoryConfig, updateMemoryConfig } from '@renderer/store/memory'
@ -12,12 +11,12 @@ import type { Model } from '@renderer/types'
import { Flex, Form, Modal } from 'antd'
import { t } from 'i18next'
import type { FC } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
const logger = loggerService.withContext('MemoriesSettingsModal')
const logger = loggerService.withContext('MemorySettingsModal')
interface MemoriesSettingsModalProps {
interface MemorySettingsModalProps {
visible: boolean
onSubmit: (values: any) => void
onCancel: () => void
@ -26,78 +25,57 @@ interface MemoriesSettingsModalProps {
type formValue = {
llmModel: string
embedderModel: string
embedderDimensions: number
embeddingModel: string
embeddingDimensions: number
}
const MemoriesSettingsModal: FC<MemoriesSettingsModalProps> = ({ visible, onSubmit, onCancel, form }) => {
const MemorySettingsModal: FC<MemorySettingsModalProps> = ({ visible, onSubmit, onCancel, form }) => {
const { providers } = useProviders()
const dispatch = useDispatch()
const memoryConfig = useSelector(selectMemoryConfig)
const [loading, setLoading] = useState(false)
// Get all models for lookup
const allModels = useMemo(() => providers.flatMap((p) => p.models), [providers])
const llmModel = useModel(memoryConfig.llmApiClient?.model, memoryConfig.llmApiClient?.provider)
const embedderModel = useModel(memoryConfig.embedderApiClient?.model, memoryConfig.embedderApiClient?.provider)
const findModelById = useCallback(
(id: string | undefined) => (id ? allModels.find((m) => getModelUniqId(m) === id) : undefined),
[allModels]
)
const llmModel = useModel(memoryConfig.llmModel?.id, memoryConfig.llmModel?.provider)
const embeddingModel = useModel(memoryConfig.embeddingModel?.id, memoryConfig.embeddingModel?.provider)
// Initialize form with current memory config when modal opens
useEffect(() => {
if (visible && memoryConfig) {
form.setFieldsValue({
llmModel: getModelUniqId(llmModel),
embedderModel: getModelUniqId(embedderModel),
embedderDimensions: memoryConfig.embedderDimensions
embeddingModel: getModelUniqId(embeddingModel),
embeddingDimensions: memoryConfig.embeddingDimensions
// customFactExtractionPrompt: memoryConfig.customFactExtractionPrompt,
// customUpdateMemoryPrompt: memoryConfig.customUpdateMemoryPrompt
})
}
}, [visible, memoryConfig, form, llmModel, embedderModel])
}, [embeddingModel, form, llmModel, memoryConfig, visible])
const handleFormSubmit = async (values: formValue) => {
try {
// Convert model IDs back to Model objects
const llmModel = findModelById(values.llmModel)
const llmProvider = providers.find((p) => p.id === llmModel?.provider)
const aiLlmProvider = new AiProvider(llmProvider!)
const embedderModel = findModelById(values.embedderModel)
const embedderProvider = providers.find((p) => p.id === embedderModel?.provider)
const aiEmbedderProvider = new AiProvider(embedderProvider!)
if (embedderModel) {
const llmModel = getModel(values.llmModel)
const embeddingModel = getModel(values.embeddingModel)
if (embeddingModel) {
setLoading(true)
const provider = providers.find((p) => p.id === embedderModel.provider)
const provider = providers.find((p) => p.id === embeddingModel.provider)
if (!provider) {
return
}
const finalDimensions =
typeof values.embedderDimensions === 'string'
? parseInt(values.embedderDimensions)
: values.embedderDimensions
typeof values.embeddingDimensions === 'string'
? parseInt(values.embeddingDimensions)
: values.embeddingDimensions
const updatedConfig = {
...memoryConfig,
llmApiClient: {
model: llmModel?.id ?? '',
provider: llmProvider?.id ?? '',
apiKey: aiLlmProvider.getApiKey(),
baseURL: aiLlmProvider.getBaseURL(),
apiVersion: llmProvider?.apiVersion
},
embedderApiClient: {
model: embedderModel?.id ?? '',
provider: embedderProvider?.id ?? '',
apiKey: aiEmbedderProvider.getApiKey(),
baseURL: aiEmbedderProvider.getBaseURL(),
apiVersion: embedderProvider?.apiVersion
},
embedderDimensions: finalDimensions
llmModel,
embeddingModel,
embeddingDimensions: finalDimensions
// customFactExtractionPrompt: values.customFactExtractionPrompt,
// customUpdateMemoryPrompt: values.customUpdateMemoryPrompt
}
@ -150,7 +128,7 @@ const MemoriesSettingsModal: FC<MemoriesSettingsModalProps> = ({ visible, onSubm
</Form.Item>
<Form.Item
label={t('memory.embedding_model')}
name="embedderModel"
name="embeddingModel"
rules={[{ required: true, message: t('memory.please_select_embedding_model') }]}>
<ModelSelector
providers={providers}
@ -160,10 +138,10 @@ const MemoriesSettingsModal: FC<MemoriesSettingsModalProps> = ({ visible, onSubm
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) => prevValues.embedderModel !== currentValues.embedderModel}>
shouldUpdate={(prevValues, currentValues) => prevValues.embeddingModel !== currentValues.embeddingModel}>
{({ getFieldValue }) => {
const embedderModelId = getFieldValue('embedderModel')
const embedderModel = findModelById(embedderModelId)
const embeddingModelId = getFieldValue('embeddingModel')
const embeddingModel = getModel(embeddingModelId)
return (
<Form.Item
label={
@ -172,7 +150,7 @@ const MemoriesSettingsModal: FC<MemoriesSettingsModalProps> = ({ visible, onSubm
<InfoTooltip title={t('knowledge.dimensions_size_tooltip')} />
</Flex>
}
name="embedderDimensions"
name="embeddingDimensions"
rules={[
{
validator(_, value) {
@ -183,7 +161,7 @@ const MemoriesSettingsModal: FC<MemoriesSettingsModalProps> = ({ visible, onSubm
}
}
]}>
<InputEmbeddingDimension model={embedderModel} disabled={!embedderModel} />
<InputEmbeddingDimension model={embeddingModel} disabled={!embeddingModel} />
</Form.Item>
)
}}
@ -199,4 +177,4 @@ const MemoriesSettingsModal: FC<MemoriesSettingsModalProps> = ({ visible, onSubm
)
}
export default MemoriesSettingsModal
export default MemorySettingsModal

View File

@ -1,7 +1,6 @@
import { HStack } from '@renderer/components/Layout'
import { Avatar, Button, Select, Space, Tooltip } from 'antd'
import { Button, Select, Space, Tooltip } from 'antd'
import { UserRoundPlus } from 'lucide-react'
import { useCallback, useMemo } from 'react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { DEFAULT_USER_ID } from './constants'
@ -16,39 +15,18 @@ interface UserSelectorProps {
const UserSelector: React.FC<UserSelectorProps> = ({ currentUser, uniqueUsers, onUserSwitch, onAddUser }) => {
const { t } = useTranslation()
const getUserAvatar = useCallback((user: string) => {
return user === DEFAULT_USER_ID ? user.slice(0, 1).toUpperCase() : user.slice(0, 2).toUpperCase()
}, [])
const renderLabel = useCallback(
(userId: string, userName: string) => {
return (
<HStack alignItems="center" gap={10}>
<Avatar size={20} style={{ background: 'var(--color-primary)' }}>
{getUserAvatar(userId)}
</Avatar>
<span>{userName}</span>
</HStack>
)
},
[getUserAvatar]
)
const options = useMemo(() => {
const defaultOption = {
value: DEFAULT_USER_ID,
label: renderLabel(DEFAULT_USER_ID, t('memory.default_user'))
label: t('memory.default_user')
}
const userOptions = uniqueUsers
.filter((user) => user !== DEFAULT_USER_ID)
.map((user) => ({
value: user,
label: renderLabel(user, user)
}))
.map((user) => ({ value: user, label: user }))
return [defaultOption, ...userOptions]
}, [renderLabel, t, uniqueUsers])
}, [t, uniqueUsers])
return (
<Space.Compact>

View File

@ -40,7 +40,7 @@ export class MemoryProcessor {
try {
const { memoryConfig } = config
if (!memoryConfig.llmApiClient) {
if (!memoryConfig.llmModel) {
throw new Error('No LLM model configured for memory processing')
}
@ -53,8 +53,9 @@ export class MemoryProcessor {
const responseContent = await fetchGenerate({
prompt: systemPrompt,
content: userPrompt,
model: getModel(memoryConfig.llmApiClient.model, memoryConfig.llmApiClient.provider)
model: getModel(memoryConfig.llmModel.id, memoryConfig.llmModel.provider)
})
if (!responseContent || responseContent.trim() === '') {
return []
}
@ -100,9 +101,10 @@ export class MemoryProcessor {
const { memoryConfig, assistantId, userId, lastMessageId } = config
if (!memoryConfig.llmApiClient) {
if (!memoryConfig.llmModel) {
throw new Error('No LLM model configured for memory processing')
}
const existingMemoriesResult = (window.keyv.get(`memory-search-${lastMessageId}`) as MemoryItem[]) || []
const existingMemories = existingMemoriesResult.map((memory) => ({
@ -123,7 +125,7 @@ export class MemoryProcessor {
const responseContent = await fetchGenerate({
prompt: updateMemorySystemPrompt,
content: updateMemoryUserPrompt,
model: getModel(memoryConfig.llmApiClient.model, memoryConfig.llmApiClient.provider)
model: getModel(memoryConfig.llmModel.id, memoryConfig.llmModel.provider)
})
if (!responseContent || responseContent.trim() === '') {
return []

View File

@ -1,14 +1,19 @@
import { loggerService } from '@logger'
import { getModel } from '@renderer/hooks/useModel'
import store from '@renderer/store'
import { selectMemoryConfig } from '@renderer/store/memory'
import type {
AddMemoryOptions,
AssistantMessage,
KnowledgeBase,
MemoryHistoryItem,
MemoryListOptions,
MemorySearchOptions,
MemorySearchResult
} from '@types'
import { now } from 'lodash'
import { getKnowledgeBaseParams } from './KnowledgeService'
const logger = loggerService.withContext('MemoryService')
@ -203,16 +208,24 @@ class MemoryService {
}
const memoryConfig = selectMemoryConfig(store.getState())
const embedderApiClient = memoryConfig.embedderApiClient
const llmApiClient = memoryConfig.llmApiClient
const embeddingModel = memoryConfig.embeddingModel
const configWithProviders = {
// Get knowledge base params for memory
const { embedApiClient: embeddingApiClient } = getKnowledgeBaseParams({
id: 'memory',
name: 'Memory',
model: getModel(embeddingModel?.id, embeddingModel?.provider),
dimensions: memoryConfig.embeddingDimensions,
items: [],
created_at: now(),
updated_at: now(),
version: 1
} as KnowledgeBase)
return window.api.memory.setConfig({
...memoryConfig,
embedderApiClient,
llmApiClient
}
return window.api.memory.setConfig(configWithProviders)
embeddingApiClient
})
} catch (error) {
logger.warn('Failed to update memory config:', error as Error)
return

View File

@ -67,7 +67,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 188,
version: 189,
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'],
migrate
},

View File

@ -17,7 +17,7 @@ export interface MemoryState {
// Default memory configuration to avoid undefined errors
const defaultMemoryConfig: MemoryConfig = {
embedderDimensions: 1536,
embeddingDimensions: undefined,
isAutoDimensions: true,
customFactExtractionPrompt: factExtractionPrompt,
customUpdateMemoryPrompt: updateMemorySystemPrompt

View File

@ -18,6 +18,7 @@ import { TRANSLATE_PROMPT } from '@renderer/config/prompts'
import { SYSTEM_PROVIDERS } from '@renderer/config/providers'
import { DEFAULT_SIDEBAR_ICONS } from '@renderer/config/sidebar'
import db from '@renderer/databases'
import { getModel } from '@renderer/hooks/useModel'
import i18n from '@renderer/i18n'
import { DEFAULT_ASSISTANT_SETTINGS } from '@renderer/services/AssistantService'
import { defaultPreprocessProviders } from '@renderer/store/preprocess'
@ -3068,6 +3069,35 @@ const migrateConfig = {
logger.error('migrate 188 error', error as Error)
return state
}
},
// 1.7.7
'189': (state: RootState) => {
try {
window.api.memory.migrateMemoryDb()
// @ts-ignore
const memoryLlmApiClient = state?.memory?.memoryConfig?.llmApiClient
// @ts-ignore
const memoryEmbeddingApiClient = state?.memory?.memoryConfig?.embedderApiClient
if (memoryLlmApiClient) {
state.memory.memoryConfig.llmModel = getModel(memoryLlmApiClient.model, memoryLlmApiClient.provider)
// @ts-ignore
delete state.memory.memoryConfig.llmApiClient
}
if (memoryEmbeddingApiClient) {
state.memory.memoryConfig.embeddingModel = getModel(
memoryEmbeddingApiClient.model,
memoryEmbeddingApiClient.provider
)
// @ts-ignore
delete state.memory.memoryConfig.embedderApiClient
}
return state
} catch (error) {
logger.error('migrate 189 error', error as Error)
return state
}
}
}

View File

@ -915,17 +915,11 @@ export * from './tool'
// Memory Service Types
// ========================================================================
export interface MemoryConfig {
/**
* @deprecated use embedderApiClient instead
*/
embedderModel?: Model
embedderDimensions?: number
/**
* @deprecated use llmApiClient instead
*/
embeddingDimensions?: number
embeddingModel?: Model
llmModel?: Model
embedderApiClient?: ApiClient
llmApiClient?: ApiClient
// Dynamically retrieved, not persistently stored
embeddingApiClient?: ApiClient
customFactExtractionPrompt?: string
customUpdateMemoryPrompt?: string
/** Indicates whether embedding dimensions are automatically detected */