Compare commits

...

4 Commits

Author SHA1 Message Date
one
630c8e4d38 refactor: improve UserSelector with tag 2025-08-05 11:32:47 +08:00
one
3fd53572dd Merge branch 'main' into feature/per-assistant-memory-config 2025-08-05 11:23:01 +08:00
Vaayne
3aec08d650 feat: improve memory service logging and per-assistant configuration
- Enhance memory search logging with structured context in MemoryProcessor
- Add per-assistant memory configuration support in MemoryService
- Reduce log verbosity in StreamProcessingService (debug to silly)
- Fix memory search to use assistant's memoryUserId when available

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-02 11:42:48 +08:00
Vaayne
7328664edf 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@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-7ea2038f-9b07-485f-b489-e29d3953323f
2025-07-31 00:00:18 +08:00
13 changed files with 354 additions and 39 deletions

View File

@@ -998,6 +998,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",

View File

@@ -998,6 +998,7 @@
},
"memory": {
"actions": "アクション",
"active_memory_user": "アクティブメモリユーザー",
"add_failed": "メモリーの追加に失敗しました",
"add_first_memory": "最初のメモリを追加",
"add_memory": "メモリーを追加",

View File

@@ -998,6 +998,7 @@
},
"memory": {
"actions": "Действия",
"active_memory_user": "Активный пользователь памяти",
"add_failed": "Не удалось добавить память",
"add_first_memory": "Добавить первое воспоминание",
"add_memory": "Добавить память",

View File

@@ -998,6 +998,7 @@
},
"memory": {
"actions": "操作",
"active_memory_user": "活跃记忆用户",
"add_failed": "添加记忆失败",
"add_first_memory": "添加您的第一条记忆",
"add_memory": "添加记忆",

View File

@@ -998,6 +998,7 @@
},
"memory": {
"actions": "操作",
"active_memory_user": "活躍記憶使用者",
"add_failed": "新增記憶失敗",
"add_first_memory": "新增您的第一個記憶",
"add_memory": "新增記憶",

View File

@@ -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<Props> = ({ 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<Props> = ({ 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<Props> = ({ assistant, updateAssistant,
const isMemoryConfigured = memoryConfig.embedderApiClient && memoryConfig.llmApiClient
const isMemoryEnabled = globalMemoryEnabled && isMemoryConfigured
const effectiveUserId = memoryService.getEffectiveUserId(assistant, currentUserId)
const currentMemoryUser = assistant.memoryUserId || 'global'
return (
<Container>
@@ -124,12 +152,41 @@ const AssistantMemorySettings: React.FC<Props> = ({ assistant, updateAssistant,
/>
)}
{/* Memory User Selection */}
{assistant.enableMemory && isMemoryEnabled && (
<Card size="small" style={{ marginBottom: 16 }}>
<Space direction="vertical" style={{ width: '100%' }}>
<div>
<Text strong>{t('memory.active_memory_user')}: </Text>
<Select
value={currentMemoryUser}
onChange={handleMemoryUserChange}
style={{ width: 200, marginLeft: 8 }}
disabled={!assistant.enableMemory}>
<Select.Option value="global">
{t('memory.default_user')} ({currentUserId})
</Select.Option>
{availableUsers.map((user) => (
<Select.Option key={user.userId} value={user.userId}>
{user.userId} ({user.memoryCount} memories)
</Select.Option>
))}
</Select>
</div>
</Space>
</Card>
)}
<Card size="small" style={{ marginBottom: 16 }}>
<Space direction="vertical" style={{ width: '100%' }}>
<div>
<Text strong>{t('memory.stored_memories')}: </Text>
<Text>{memoryStats.loading ? t('common.loading') : memoryStats.count}</Text>
</div>
<div>
<Text strong>{t('memory.active_memory_user')}: </Text>
<Text code>{effectiveUserId}</Text>
</div>
{memoryConfig.embedderApiClient && (
<div>
<Text strong>{t('memory.embedding_model')}: </Text>

View File

@@ -458,7 +458,7 @@ const MemorySettings = () => {
try {
// Create the user by adding an initial memory with the userId
// This implicitly creates the user in the system
await memoryService.setCurrentUser(userId)
memoryService.setCurrentUser(userId)
await memoryService.add(t('memory.initial_memory_content'), { userId })
// Refresh the users list from the database to persist the new user

View File

@@ -1,4 +1,6 @@
import CustomTag from '@renderer/components/CustomTag'
import { HStack } from '@renderer/components/Layout'
import { useAssistants } from '@renderer/hooks/useAssistant'
import { Avatar, Button, Select, Space, Tooltip } from 'antd'
import { UserRoundPlus } from 'lucide-react'
import { useCallback, useMemo } from 'react'
@@ -15,23 +17,50 @@ interface UserSelectorProps {
const UserSelector: React.FC<UserSelectorProps> = ({ currentUser, uniqueUsers, onUserSwitch, onAddUser }) => {
const { t } = useTranslation()
const { assistants } = useAssistants()
const getUserAvatar = useCallback((user: string) => {
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 = useCallback(
(userId: string) => {
return assistants.filter(
(assistant) =>
// Assistant uses this user if either:
// 1. memoryUserId explicitly matches
// 2. memoryUserId is undefined and this is the current global user
assistant.memoryUserId === userId || (!assistant.memoryUserId && userId === currentUser)
)
},
[assistants, currentUser]
)
const renderLabel = useCallback(
(userId: string, userName: string) => {
const linkedAssistants = getAssistantsForUser(userId)
return (
<HStack alignItems="center" gap={10}>
<Avatar size={20} style={{ background: 'var(--color-primary)' }}>
{getUserAvatar(userId)}
</Avatar>
<span>{userName}</span>
<HStack alignItems="center" justifyContent="space-between" style={{ width: '100%' }}>
<HStack alignItems="center" gap={8} style={{ minWidth: 0 }}>
<Avatar size={20} style={{ background: 'var(--color-primary)' }}>
{getUserAvatar(userId)}
</Avatar>
<span style={{ whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }}>{userName}</span>
</HStack>
{linkedAssistants.length > 0 && (
<CustomTag
color="#8c8c8c"
size={10}
tooltip={`Linked Assistants: ${linkedAssistants.map((a) => a.name).join(', ')}`}>
{linkedAssistants.length}
</CustomTag>
)}
</HStack>
)
},
[getUserAvatar]
[getUserAvatar, getAssistantsForUser]
)
const options = useMemo(() => {

View File

@@ -67,6 +67,7 @@ import {
} from './AssistantService'
import { processKnowledgeSearch } from './KnowledgeService'
import { MemoryProcessor } from './MemoryProcessor'
import MemoryService from './MemoryService'
import {
filterContextMessages,
filterEmptyMessages,
@@ -232,10 +233,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,
@@ -557,7 +560,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 = {
@@ -582,7 +587,7 @@ async function processConversationMemory(messages: Message[], assistant: Assista
const processorConfig = MemoryProcessor.getProcessorConfig(
updatedMemoryConfig,
assistant.id,
currentUserId,
effectiveUserId,
lastUserMessage?.id
)

View File

@@ -238,12 +238,10 @@ export class MemoryProcessor {
limit
})
logger.debug(
`Searching memories with query: ${query} for user: ${userId} and assistant: ${assistantId} result: ${result}`
)
logger.debug('Searching memories successful', { query, userId, assistantId, result })
return result.results
} catch (error) {
logger.error('Error searching memories:', error as Error)
logger.error('Searching memories error:', { error })
return []
}
}

View File

@@ -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,
@@ -10,6 +12,8 @@ import {
MemorySearchResult
} from '@types'
import { getAssistantById } from './AssistantService'
const logger = loggerService.withContext('MemoryService')
// Main process SearchResult type (matches what the IPC actually returns)
@@ -26,8 +30,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 +74,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 +147,32 @@ class MemoryService {
* @returns Promise resolving to search results of added memories
*/
public async add(messages: string | AssistantMessage[], options: AddMemoryOptions): Promise<MemorySearchResult> {
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 +183,42 @@ class MemoryService {
* @returns Promise resolving to search results matching the query
*/
public async search(query: string, options: MemorySearchOptions): Promise<MemorySearchResult> {
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
}
// If agentId is provided, resolve userId from assistant's memoryUserId
if (optionsWithUser.agentId) {
const assistant = getAssistantById(optionsWithUser.agentId)
if (assistant) {
optionsWithUser.userId = assistant.memoryUserId || this.currentUserId
}
}
logger.debug('Searching memories start with options', { query: query, options: optionsWithUser })
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 +284,13 @@ class MemoryService {
*/
public async updateConfig(): Promise<void> {
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 +306,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<MemoryListOptions, 'userId'>,
assistant?: Assistant
): Promise<MemorySearchResult> {
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<AddMemoryOptions, 'userId'>,
assistant?: Assistant
): Promise<MemorySearchResult> {
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<MemorySearchOptions, 'userId'>,
assistant?: Assistant
): Promise<MemorySearchResult> {
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

View File

@@ -51,7 +51,7 @@ export function createStreamProcessor(callbacks: StreamProcessorCallbacks = {})
return (chunk: Chunk) => {
try {
const data = chunk
logger.debug('data: ', data)
logger.silly('data: ', data)
switch (data.type) {
case ChunkType.BLOCK_COMPLETE: {
if (callbacks.onComplete) callbacks.onComplete(AssistantMessageStatus.SUCCESS, data?.response)

View File

@@ -32,6 +32,7 @@ export type Assistant = {
regularPhrases?: QuickPhrase[] // Added for regular phrase
tags?: string[] // 助手标签
enableMemory?: boolean
memoryUserId?: string // 绑定的记忆用户ID当未指定时使用全局记忆用户
}
export type TranslateAssistant = Assistant & {