From 7328664edf3cdd639476720091668a37ee1f0786 Mon Sep 17 00:00:00 2001 From: Vaayne Date: Thu, 31 Jul 2025 00:00:18 +0800 Subject: [PATCH] feat: add per-assistant memory configuration - Add memory configuration options per assistant - Update UI for memory settings management - Add toggle for assistant-specific memory usage - Update translation files for memory settings - Enhance MemoryService for per-assistant functionality Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-7ea2038f-9b07-485f-b489-e29d3953323f --- src/renderer/src/i18n/locales/en-us.json | 1 + src/renderer/src/i18n/locales/ja-jp.json | 1 + src/renderer/src/i18n/locales/ru-ru.json | 1 + src/renderer/src/i18n/locales/zh-cn.json | 1 + src/renderer/src/i18n/locales/zh-tw.json | 1 + src/renderer/src/pages/memory/index.tsx | 86 ++++++- .../AssistantMemorySettings.tsx | 65 ++++- src/renderer/src/services/ApiService.ts | 17 +- src/renderer/src/services/MemoryService.ts | 242 ++++++++++++++++-- src/renderer/src/types/index.ts | 1 + 10 files changed, 377 insertions(+), 39 deletions(-) diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index fc34851baa..b8662380ba 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -987,6 +987,7 @@ }, "memory": { "actions": "Actions", + "active_memory_user": "Active Memory User", "add_failed": "Failed to add memory", "add_first_memory": "Add Your First Memory", "add_memory": "Add Memory", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 31be073d7f..5dd5785eb2 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -987,6 +987,7 @@ }, "memory": { "actions": "アクション", + "active_memory_user": "アクティブメモリユーザー", "add_failed": "メモリーの追加に失敗しました", "add_first_memory": "最初のメモリを追加", "add_memory": "メモリーを追加", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index a368a910f5..54696d6819 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -987,6 +987,7 @@ }, "memory": { "actions": "Действия", + "active_memory_user": "Активный пользователь памяти", "add_failed": "Не удалось добавить память", "add_first_memory": "Добавить первое воспоминание", "add_memory": "Добавить память", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 1a7d633a33..0ab8d36066 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -987,6 +987,7 @@ }, "memory": { "actions": "操作", + "active_memory_user": "活跃记忆用户", "add_failed": "添加记忆失败", "add_first_memory": "添加您的第一条记忆", "add_memory": "添加记忆", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 13a6508873..203f82be7d 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -987,6 +987,7 @@ }, "memory": { "actions": "操作", + "active_memory_user": "活躍記憶使用者", "add_failed": "新增記憶失敗", "add_first_memory": "新增您的第一個記憶", "add_memory": "新增記憶", diff --git a/src/renderer/src/pages/memory/index.tsx b/src/renderer/src/pages/memory/index.tsx index 72a7bf70ac..acc8b00b1a 100644 --- a/src/renderer/src/pages/memory/index.tsx +++ b/src/renderer/src/pages/memory/index.tsx @@ -299,6 +299,7 @@ const MemoriesPage = () => { const dispatch = useDispatch() const currentUser = useSelector(selectCurrentUserId) const globalMemoryEnabled = useSelector(selectGlobalMemoryEnabled) + const allAssistants = useSelector((state: any) => state.assistants.assistants) const [allMemories, setAllMemories] = useState([]) const [loading, setLoading] = useState(false) @@ -323,6 +324,16 @@ const MemoriesPage = () => { return user === DEFAULT_USER_ID ? user.slice(0, 1).toUpperCase() : user.slice(0, 2).toUpperCase() } + // Get assistants linked to a specific memory user + const getAssistantsForUser = (userId: string) => { + return allAssistants.filter((assistant: any) => { + // Assistant uses this user if either: + // 1. memoryUserId explicitly matches + // 2. memoryUserId is undefined and this is the current global user + return assistant.memoryUserId === userId || (!assistant.memoryUserId && userId === currentUser) + }) + } + // Load unique users from database const loadUniqueUsers = useCallback(async () => { try { @@ -630,23 +641,61 @@ const MemoriesPage = () => { )}> {uniqueUsers .filter((user) => user !== DEFAULT_USER_ID) .map((user) => ( ))} @@ -673,6 +722,19 @@ const MemoriesPage = () => { {uniqueUsers.length} {t('memory.users', 'Users')} + {(() => { + const linkedAssistants = getAssistantsForUser(currentUser) + return ( + linkedAssistants.length > 0 && ( + + + Linked Assistants ({linkedAssistants.length}):{' '} + {linkedAssistants.map((a: any) => a.name).join(', ')} + + + ) + ) + })()} diff --git a/src/renderer/src/pages/settings/AssistantSettings/AssistantMemorySettings.tsx b/src/renderer/src/pages/settings/AssistantSettings/AssistantMemorySettings.tsx index b7ab156ca7..5eee33be39 100644 --- a/src/renderer/src/pages/settings/AssistantSettings/AssistantMemorySettings.tsx +++ b/src/renderer/src/pages/settings/AssistantSettings/AssistantMemorySettings.tsx @@ -3,9 +3,9 @@ import { loggerService } from '@logger' import { Box } from '@renderer/components/Layout' import MemoriesSettingsModal from '@renderer/pages/memory/settings-modal' import MemoryService from '@renderer/services/MemoryService' -import { selectGlobalMemoryEnabled, selectMemoryConfig } from '@renderer/store/memory' +import { selectCurrentUserId, selectGlobalMemoryEnabled, selectMemoryConfig } from '@renderer/store/memory' import { Assistant, AssistantSettings } from '@renderer/types' -import { Alert, Button, Card, Space, Switch, Tooltip, Typography } from 'antd' +import { Alert, Button, Card, Select, Space, Switch, Tooltip, Typography } from 'antd' import { useForm } from 'antd/es/form/Form' import { Settings2 } from 'lucide-react' import { useCallback, useEffect, useState } from 'react' @@ -28,19 +28,36 @@ const AssistantMemorySettings: React.FC = ({ assistant, updateAssistant, const { t } = useTranslation() const memoryConfig = useSelector(selectMemoryConfig) const globalMemoryEnabled = useSelector(selectGlobalMemoryEnabled) + const currentUserId = useSelector(selectCurrentUserId) const [memoryStats, setMemoryStats] = useState<{ count: number; loading: boolean }>({ count: 0, loading: true }) + const [availableUsers, setAvailableUsers] = useState< + { userId: string; memoryCount: number; lastMemoryDate: string }[] + >([]) const [settingsModalVisible, setSettingsModalVisible] = useState(false) const memoryService = MemoryService.getInstance() const form = useForm() + // Load available memory users + const loadUsers = useCallback(async () => { + try { + const users = await memoryService.getUsersList() + setAvailableUsers(users) + } catch (error) { + logger.error('Failed to load memory users:', error as Error) + setAvailableUsers([]) + } + }, [memoryService]) + // Load memory statistics for this assistant const loadMemoryStats = useCallback(async () => { setMemoryStats((prev) => ({ ...prev, loading: true })) try { + const effectiveUserId = memoryService.getEffectiveUserId(assistant, currentUserId) const result = await memoryService.list({ + userId: effectiveUserId, agentId: assistant.id, limit: 1000 }) @@ -49,16 +66,25 @@ const AssistantMemorySettings: React.FC = ({ assistant, updateAssistant, logger.error('Failed to load memory stats:', error as Error) setMemoryStats({ count: 0, loading: false }) } - }, [assistant.id, memoryService]) + }, [assistant, currentUserId, memoryService]) useEffect(() => { + loadUsers() loadMemoryStats() - }, [loadMemoryStats]) + }, [loadUsers, loadMemoryStats]) const handleMemoryToggle = (enabled: boolean) => { updateAssistant({ ...assistant, enableMemory: enabled }) } + const handleMemoryUserChange = (value: string) => { + // 'global' means use global default (undefined) + const memoryUserId = value === 'global' ? undefined : value + updateAssistant({ ...assistant, memoryUserId }) + // Reload stats after changing user + setTimeout(() => loadMemoryStats(), 100) + } + const handleNavigateToMemory = () => { // Close current modal/page first if (onClose) { @@ -70,6 +96,8 @@ const AssistantMemorySettings: React.FC = ({ assistant, updateAssistant, const isMemoryConfigured = memoryConfig.embedderApiClient && memoryConfig.llmApiClient const isMemoryEnabled = globalMemoryEnabled && isMemoryConfigured + const effectiveUserId = memoryService.getEffectiveUserId(assistant, currentUserId) + const currentMemoryUser = assistant.memoryUserId || 'global' return ( @@ -124,12 +152,41 @@ const AssistantMemorySettings: React.FC = ({ assistant, updateAssistant, /> )} + {/* Memory User Selection */} + {assistant.enableMemory && isMemoryEnabled && ( + + +
+ {t('memory.active_memory_user')}: + +
+
+
+ )} +
{t('memory.stored_memories')}: {memoryStats.loading ? t('common.loading') : memoryStats.count}
+
+ {t('memory.active_memory_user')}: + {effectiveUserId} +
{memoryConfig.embedderApiClient && (
{t('memory.embedding_model')}: diff --git a/src/renderer/src/services/ApiService.ts b/src/renderer/src/services/ApiService.ts index 841d9de934..f4b7a5f4a5 100644 --- a/src/renderer/src/services/ApiService.ts +++ b/src/renderer/src/services/ApiService.ts @@ -56,6 +56,7 @@ import { } from './AssistantService' import { processKnowledgeSearch } from './KnowledgeService' import { MemoryProcessor } from './MemoryProcessor' +import MemoryService from './MemoryService' import { filterContextMessages, filterEmptyMessages, @@ -221,10 +222,12 @@ async function fetchExternalTool( } if (memoryConfig.llmApiClient && memoryConfig.embedderApiClient) { - const currentUserId = selectCurrentUserId(store.getState()) - // Search for relevant memories - const processorConfig = MemoryProcessor.getProcessorConfig(memoryConfig, assistant.id, currentUserId) - logger.info(`Searching for relevant memories with content: ${content}`) + const globalUserId = selectCurrentUserId(store.getState()) + const memoryService = MemoryService.getInstance() + const effectiveUserId = memoryService.getEffectiveUserId(assistant, globalUserId) + // Search for relevant memories using effective user ID + const processorConfig = MemoryProcessor.getProcessorConfig(memoryConfig, assistant.id, effectiveUserId) + logger.info(`Searching for relevant memories with content: ${content} for effective user: ${effectiveUserId}`) const memoryProcessor = new MemoryProcessor() const relevantMemories = await memoryProcessor.searchRelevantMemories( content, @@ -540,7 +543,9 @@ async function processConversationMemory(messages: Message[], assistant: Assista // return // } - const currentUserId = selectCurrentUserId(store.getState()) + const globalUserId = selectCurrentUserId(store.getState()) + const memoryService = MemoryService.getInstance() + const effectiveUserId = memoryService.getEffectiveUserId(assistant, globalUserId) // Create updated memory config with resolved models const updatedMemoryConfig = { @@ -565,7 +570,7 @@ async function processConversationMemory(messages: Message[], assistant: Assista const processorConfig = MemoryProcessor.getProcessorConfig( updatedMemoryConfig, assistant.id, - currentUserId, + effectiveUserId, lastUserMessage?.id ) diff --git a/src/renderer/src/services/MemoryService.ts b/src/renderer/src/services/MemoryService.ts index 4307060aa6..cf6cbbc41a 100644 --- a/src/renderer/src/services/MemoryService.ts +++ b/src/renderer/src/services/MemoryService.ts @@ -1,8 +1,10 @@ import { loggerService } from '@logger' +import type { RootState } from '@renderer/store' import store from '@renderer/store' -import { selectMemoryConfig } from '@renderer/store/memory' +import { selectCurrentUserId, selectMemoryConfig } from '@renderer/store/memory' import { AddMemoryOptions, + Assistant, AssistantMessage, MemoryHistoryItem, MemoryListOptions, @@ -26,8 +28,10 @@ interface SearchResult { class MemoryService { private static instance: MemoryService | null = null private currentUserId: string = 'default-user' + private getStateFunction: () => RootState - constructor() { + constructor(getStateFunction: () => RootState = () => store.getState()) { + this.getStateFunction = getStateFunction this.init() } @@ -68,6 +72,37 @@ class MemoryService { return this.currentUserId } + /** + * Gets the effective memory user ID for an assistant using dependency injection + * Falls back to global currentUserId when assistant has no specific memoryUserId + * @param assistant - The assistant object containing memoryUserId + * @param globalUserId - The global user ID to fall back to + * @returns The effective user ID to use for memory operations + */ + public getEffectiveUserId(assistant: Assistant, globalUserId: string): string { + return assistant.memoryUserId || globalUserId + } + + /** + * Private helper to resolve user ID for context operations + * @param assistant - Optional assistant object to determine effective user ID + * @returns The resolved user ID to use for memory operations + */ + private resolveUserId(assistant?: Assistant): string { + let globalUserId = this.currentUserId + + if (this.getStateFunction) { + try { + globalUserId = selectCurrentUserId(this.getStateFunction()) + } catch (error) { + logger.warn('Failed to get state, falling back to internal currentUserId:', error as Error) + globalUserId = this.currentUserId + } + } + + return assistant ? this.getEffectiveUserId(assistant, globalUserId) : globalUserId + } + /** * Lists all stored memories * @param config - Optional configuration for filtering memories @@ -110,12 +145,32 @@ class MemoryService { * @returns Promise resolving to search results of added memories */ public async add(messages: string | AssistantMessage[], options: AddMemoryOptions): Promise { - options.userId = this.currentUserId - const result: SearchResult = await window.api.memory.add(messages, options) - // Convert SearchResult to MemorySearchResult for consistency - return { - results: result.memories, - relations: [] + const optionsWithUser = { + ...options, + userId: this.currentUserId + } + + try { + const result: SearchResult = await window.api.memory.add(messages, optionsWithUser) + + // Handle error responses from main process + if (result.error) { + logger.error(`Memory service error: ${result.error}`) + throw new Error(result.error) + } + + // Convert SearchResult to MemorySearchResult for consistency + return { + results: result.memories || [], + relations: [] + } + } catch (error) { + logger.error('Failed to add memories:', error as Error) + // Return empty result on error to prevent UI crashes + return { + results: [], + relations: [] + } } } @@ -126,12 +181,32 @@ class MemoryService { * @returns Promise resolving to search results matching the query */ public async search(query: string, options: MemorySearchOptions): Promise { - options.userId = this.currentUserId - const result: SearchResult = await window.api.memory.search(query, options) - // Convert SearchResult to MemorySearchResult for consistency - return { - results: result.memories, - relations: [] + const optionsWithUser = { + ...options, + userId: this.currentUserId + } + + try { + const result: SearchResult = await window.api.memory.search(query, optionsWithUser) + + // Handle error responses from main process + if (result.error) { + logger.error(`Memory service error: ${result.error}`) + throw new Error(result.error) + } + + // Convert SearchResult to MemorySearchResult for consistency + return { + results: result.memories || [], + relations: [] + } + } catch (error) { + logger.error('Failed to search memories:', error as Error) + // Return empty result on error to prevent UI crashes + return { + results: [], + relations: [] + } } } @@ -197,12 +272,13 @@ class MemoryService { */ public async updateConfig(): Promise { try { - if (!store || !store.getState) { - logger.warn('Store not available, skipping memory config update') + if (!this.getStateFunction) { + logger.warn('State function not available, skipping memory config update') return } - const memoryConfig = selectMemoryConfig(store.getState()) + const state = this.getStateFunction() + const memoryConfig = selectMemoryConfig(state) const embedderApiClient = memoryConfig.embedderApiClient const llmApiClient = memoryConfig.llmApiClient @@ -218,6 +294,138 @@ class MemoryService { return } } + + // Enhanced methods with assistant context support + + /** + * Lists stored memories with assistant context support + * Automatically resolves the effective user ID based on assistant's memoryUserId + * @param config - Configuration for filtering memories + * @param assistant - Optional assistant object to determine effective user ID + * @returns Promise resolving to search results containing filtered memories + */ + public async listWithContext( + config?: Omit, + assistant?: Assistant + ): Promise { + const effectiveUserId = this.resolveUserId(assistant) + + const configWithUser = { + ...config, + userId: effectiveUserId + } + + try { + const result: SearchResult = await window.api.memory.list(configWithUser) + + // Handle error responses from main process + if (result.error) { + logger.error(`Memory service error: ${result.error}`) + throw new Error(result.error) + } + + // Convert SearchResult to MemorySearchResult for consistency + return { + results: result.memories || [], + relations: [] + } + } catch (error) { + logger.error('Failed to list memories with context:', error as Error) + // Return empty result on error to prevent UI crashes + return { + results: [], + relations: [] + } + } + } + + /** + * Adds new memory entries with assistant context support + * Automatically resolves the effective user ID based on assistant's memoryUserId + * @param messages - String content or array of assistant messages to store as memory + * @param options - Configuration options for adding memory (without userId) + * @param assistant - Optional assistant object to determine effective user ID + * @returns Promise resolving to search results of added memories + */ + public async addWithContext( + messages: string | AssistantMessage[], + options: Omit, + assistant?: Assistant + ): Promise { + const effectiveUserId = this.resolveUserId(assistant) + + const optionsWithUser = { + ...options, + userId: effectiveUserId + } + + try { + const result: SearchResult = await window.api.memory.add(messages, optionsWithUser) + + // Handle error responses from main process + if (result.error) { + logger.error(`Memory service error: ${result.error}`) + throw new Error(result.error) + } + + // Convert SearchResult to MemorySearchResult for consistency + return { + results: result.memories || [], + relations: [] + } + } catch (error) { + logger.error('Failed to add memories with context:', error as Error) + // Return empty result on error to prevent UI crashes + return { + results: [], + relations: [] + } + } + } + + /** + * Searches stored memories with assistant context support + * Automatically resolves the effective user ID based on assistant's memoryUserId + * @param query - Search query string to find relevant memories + * @param options - Configuration options for memory search (without userId) + * @param assistant - Optional assistant object to determine effective user ID + * @returns Promise resolving to search results matching the query + */ + public async searchWithContext( + query: string, + options: Omit, + assistant?: Assistant + ): Promise { + const effectiveUserId = this.resolveUserId(assistant) + + const optionsWithUser = { + ...options, + userId: effectiveUserId + } + + try { + const result: SearchResult = await window.api.memory.search(query, optionsWithUser) + + // Handle error responses from main process + if (result.error) { + logger.error(`Memory service error: ${result.error}`) + throw new Error(result.error) + } + + // Convert SearchResult to MemorySearchResult for consistency + return { + results: result.memories || [], + relations: [] + } + } catch (error) { + logger.error('Failed to search memories with context:', error as Error) + // Return empty result on error to prevent UI crashes + return { + results: [], + relations: [] + } + } + } } export default MemoryService diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 9eff1e5c6a..530cbcaa76 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -32,6 +32,7 @@ export type Assistant = { regularPhrases?: QuickPhrase[] // Added for regular phrase tags?: string[] // 助手标签 enableMemory?: boolean + memoryUserId?: string // 绑定的记忆用户ID,当未指定时使用全局记忆用户 } export type TranslateAssistant = Assistant & {