78e3f450c2
- Fix: Session history flash (race condition + WS guard) - Fix: Chat background overlay + sidebar transparency - Fix: IoT device control (Chinese action names, status field) - Feat: Independent memory-service (port 8091, 13 endpoints) - Feat: Independent tool-engine service (port 8092, 13 tools) - Feat: Tool call logs with paginated DevTools panel - Feat: Thinking log records with DevTools panel - Feat: Future development roadmap document - Chore: Updated .gitignore, go.work, DevTools config - Chore: 5-service health check, project review docs
317 lines
8.2 KiB
Go
317 lines
8.2 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
|
|
"github.com/yourname/cyrene-ai/memory-service/internal/model"
|
|
"github.com/yourname/cyrene-ai/memory-service/internal/store"
|
|
)
|
|
|
|
// deDupThreshold 去重相似度阈值
|
|
const deDupThreshold = 0.75
|
|
|
|
// MemoryService 记忆业务逻辑
|
|
type MemoryService struct {
|
|
store *store.Store
|
|
}
|
|
|
|
// NewMemoryService 创建记忆服务
|
|
func NewMemoryService(s *store.Store) *MemoryService {
|
|
return &MemoryService{store: s}
|
|
}
|
|
|
|
// CreateMemory 创建/保存记忆
|
|
func (svc *MemoryService) CreateMemory(ctx context.Context, entry *model.MemoryEntry) error {
|
|
if entry.UserID == "" {
|
|
return fmt.Errorf("user_id 不能为空")
|
|
}
|
|
if entry.Content == "" {
|
|
return fmt.Errorf("content 不能为空")
|
|
}
|
|
if entry.Category == "" {
|
|
entry.Category = model.CategoryKnowledge
|
|
}
|
|
if entry.Importance < 1 {
|
|
entry.Importance = 5
|
|
}
|
|
if entry.Priority < 0 || entry.Priority > 3 {
|
|
entry.Priority = model.MemoryNormal
|
|
}
|
|
if entry.Source == "" {
|
|
entry.Source = "manual"
|
|
}
|
|
|
|
// 去重检查
|
|
similar, err := svc.findSimilar(ctx, entry.UserID, entry)
|
|
if err == nil && similar != nil {
|
|
// 合并到已有记忆
|
|
return svc.mergeMemory(ctx, similar, entry)
|
|
}
|
|
|
|
return svc.store.Save(ctx, entry)
|
|
}
|
|
|
|
// GetMemory 获取单个记忆
|
|
func (svc *MemoryService) GetMemory(ctx context.Context, id string) (*model.MemoryEntry, error) {
|
|
return svc.store.GetByID(ctx, id)
|
|
}
|
|
|
|
// ListMemories 列出用户所有记忆
|
|
func (svc *MemoryService) ListMemories(ctx context.Context, userID string, category string, minImportance int, limit int) ([]model.MemoryEntry, error) {
|
|
if limit <= 0 {
|
|
limit = 50
|
|
}
|
|
|
|
q := model.MemoryQuery{
|
|
UserID: userID,
|
|
MinImportance: minImportance,
|
|
Limit: limit,
|
|
}
|
|
if category != "" {
|
|
q.Category = model.MemoryCategory(category)
|
|
}
|
|
|
|
return svc.store.Query(ctx, q)
|
|
}
|
|
|
|
// UpdateMemory 更新记忆
|
|
func (svc *MemoryService) UpdateMemory(ctx context.Context, entry *model.MemoryEntry) error {
|
|
if entry.ID == "" {
|
|
return fmt.Errorf("id 不能为空")
|
|
}
|
|
return svc.store.Update(ctx, entry)
|
|
}
|
|
|
|
// DeleteMemory 删除记忆
|
|
func (svc *MemoryService) DeleteMemory(ctx context.Context, id string) error {
|
|
return svc.store.Delete(ctx, id)
|
|
}
|
|
|
|
// QueryMemories 语义查询 + 关键词匹配
|
|
func (svc *MemoryService) QueryMemories(ctx context.Context, userID string, queryText string, category string, minImportance int, limit int) ([]model.MemoryEntry, error) {
|
|
if limit <= 0 {
|
|
limit = 10
|
|
}
|
|
|
|
var allEntries []model.MemoryEntry
|
|
seen := make(map[string]bool)
|
|
|
|
// 1. 关键词匹配检索
|
|
keywordEntries, err := svc.store.SearchByKeyword(ctx, userID, queryText, limit*2)
|
|
if err == nil {
|
|
for _, e := range keywordEntries {
|
|
if !seen[e.ID] {
|
|
seen[e.ID] = true
|
|
allEntries = append(allEntries, e)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. 补充按分类/重要性查询
|
|
q := model.MemoryQuery{
|
|
UserID: userID,
|
|
MinImportance: minImportance,
|
|
Limit: limit,
|
|
}
|
|
if category != "" {
|
|
q.Category = model.MemoryCategory(category)
|
|
}
|
|
categoryEntries, err := svc.store.Query(ctx, q)
|
|
if err == nil {
|
|
for _, e := range categoryEntries {
|
|
if !seen[e.ID] {
|
|
seen[e.ID] = true
|
|
allEntries = append(allEntries, e)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. 在应用层做内容匹配过滤
|
|
queryLower := strings.ToLower(queryText)
|
|
var matched []model.MemoryEntry
|
|
for _, entry := range allEntries {
|
|
contentLower := strings.ToLower(entry.Content)
|
|
summaryLower := strings.ToLower(entry.Summary)
|
|
|
|
if strings.Contains(contentLower, queryLower) || strings.Contains(summaryLower, queryLower) {
|
|
matched = append(matched, entry)
|
|
continue
|
|
}
|
|
|
|
for _, kw := range entry.Keywords {
|
|
if strings.Contains(queryLower, strings.ToLower(kw)) ||
|
|
strings.Contains(strings.ToLower(kw), queryLower) {
|
|
matched = append(matched, entry)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(matched) == 0 {
|
|
matched = allEntries
|
|
}
|
|
|
|
// 4. 去重合并
|
|
matched = svc.deduplicate(matched)
|
|
|
|
// 5. 按重要性降序
|
|
sortByImportance(matched)
|
|
|
|
if len(matched) > limit {
|
|
matched = matched[:limit]
|
|
}
|
|
|
|
return matched, nil
|
|
}
|
|
|
|
// ConsolidateMemories 合并相似记忆
|
|
func (svc *MemoryService) ConsolidateMemories(ctx context.Context, userID string) (int, error) {
|
|
return svc.store.ConsolidateMemories(ctx, userID)
|
|
}
|
|
|
|
// DecayMemories 衰减旧记忆
|
|
func (svc *MemoryService) DecayMemories(ctx context.Context, userID string) (int, int, error) {
|
|
return svc.store.DecayMemories(ctx, userID)
|
|
}
|
|
|
|
// GetCategories 获取用户分类统计
|
|
func (svc *MemoryService) GetCategories(ctx context.Context, userID string) (map[string]int, error) {
|
|
return svc.store.GetCategories(ctx, userID)
|
|
}
|
|
|
|
// findSimilar 查找相似记忆
|
|
func (svc *MemoryService) findSimilar(ctx context.Context, userID string, newMem *model.MemoryEntry) (*model.MemoryEntry, error) {
|
|
existing, err := svc.store.Query(ctx, model.MemoryQuery{
|
|
UserID: userID,
|
|
Limit: 100,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for i := range existing {
|
|
score := existing[i].SimilarityScore(newMem)
|
|
if score >= deDupThreshold {
|
|
return &existing[i], nil
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// mergeMemory 合并新记忆到已有记忆
|
|
func (svc *MemoryService) mergeMemory(ctx context.Context, existing *model.MemoryEntry, newMem *model.MemoryEntry) error {
|
|
// 更新内容(如果新内容更有价值)
|
|
if newMem.Importance > existing.Importance || len(newMem.Content) > len(existing.Content) {
|
|
existing.Content = newMem.Content
|
|
existing.Summary = newMem.Summary
|
|
}
|
|
|
|
// 合并关键词
|
|
keywordSet := make(map[string]bool)
|
|
for _, k := range existing.Keywords {
|
|
keywordSet[k] = true
|
|
}
|
|
for _, k := range newMem.Keywords {
|
|
keywordSet[k] = true
|
|
}
|
|
mergedKeywords := make([]string, 0, len(keywordSet))
|
|
for k := range keywordSet {
|
|
mergedKeywords = append(mergedKeywords, k)
|
|
}
|
|
existing.Keywords = mergedKeywords
|
|
|
|
// 取最高重要性
|
|
if newMem.Importance > existing.Importance {
|
|
existing.Importance = newMem.Importance
|
|
}
|
|
|
|
// 取最高优先级
|
|
if newMem.Priority > existing.Priority {
|
|
existing.Priority = newMem.Priority
|
|
}
|
|
|
|
existing.AccessCount++
|
|
existing.Source = "merged"
|
|
|
|
log.Printf("[memory-service] 合并记忆 [%s|%d★]: %s (去重)", existing.Category, existing.Importance, existing.Summary)
|
|
return svc.store.Update(ctx, existing)
|
|
}
|
|
|
|
// deduplicate 去重合并
|
|
func (svc *MemoryService) deduplicate(entries []model.MemoryEntry) []model.MemoryEntry {
|
|
if len(entries) < 2 {
|
|
return entries
|
|
}
|
|
|
|
result := make([]model.MemoryEntry, 0, len(entries))
|
|
discarded := make(map[int]bool)
|
|
|
|
for i := 0; i < len(entries); i++ {
|
|
if discarded[i] {
|
|
continue
|
|
}
|
|
for j := i + 1; j < len(entries); j++ {
|
|
if discarded[j] {
|
|
continue
|
|
}
|
|
score := entries[i].SimilarityScore(&entries[j])
|
|
if score >= deDupThreshold {
|
|
if entries[j].Importance > entries[i].Importance ||
|
|
(entries[j].Importance == entries[i].Importance && entries[j].Priority > entries[i].Priority) {
|
|
discarded[i] = true
|
|
break
|
|
} else {
|
|
discarded[j] = true
|
|
}
|
|
}
|
|
}
|
|
if !discarded[i] {
|
|
result = append(result, entries[i])
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// sortByImportance 按 Importance 降序, Priority 降序排列
|
|
func sortByImportance(entries []model.MemoryEntry) {
|
|
for i := 0; i < len(entries); i++ {
|
|
for j := i + 1; j < len(entries); j++ {
|
|
if entries[j].Importance > entries[i].Importance ||
|
|
(entries[j].Importance == entries[i].Importance && entries[j].Priority > entries[i].Priority) {
|
|
entries[i], entries[j] = entries[j], entries[i]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// SaveThinkingLog 保存自主思考日志
|
|
func (svc *MemoryService) SaveThinkingLog(ctx context.Context, tl *model.ThinkingLog) error {
|
|
if tl.Content == "" {
|
|
return fmt.Errorf("content 不能为空")
|
|
}
|
|
if tl.UserID == "" {
|
|
tl.UserID = "admin_admin"
|
|
}
|
|
return svc.store.SaveThinkingLog(ctx, tl)
|
|
}
|
|
|
|
// QueryThinkingLogs 分页查询思考日志
|
|
func (svc *MemoryService) QueryThinkingLogs(ctx context.Context, q model.ThinkingQuery) ([]model.ThinkingLog, error) {
|
|
return svc.store.QueryThinkingLogs(ctx, q)
|
|
}
|
|
|
|
// GetThinkingLogByID 获取单条思考日志
|
|
func (svc *MemoryService) GetThinkingLogByID(ctx context.Context, id string) (*model.ThinkingLog, error) {
|
|
return svc.store.GetThinkingLogByID(ctx, id)
|
|
}
|
|
|
|
// GetThinkingStats 获取思考日志统计
|
|
func (svc *MemoryService) GetThinkingStats(ctx context.Context) (*model.ThinkingStats, error) {
|
|
return svc.store.GetThinkingStats(ctx)
|
|
}
|