package memory import ( "context" "fmt" "math" "strings" "time" ) // Service handles business logic for the memory system. type Service struct { repo *Repository } // NewService creates a new memory service. func NewService(repo *Repository) *Service { return &Service{repo: repo} } // Add stores a new memory with deduplication. func (s *Service) Add(ctx context.Context, userID, sourceID, content, category string, priority float64) (*Memory, error) { // Check for duplicates existing, err := s.repo.SearchByKeywords(ctx, userID, content, 5) if err == nil { for _, e := range existing { if jaccardSimilarity(content, e.Content) > 0.8 { // Merge: update priority and keywords e.Priority = math.Max(e.Priority, priority) e.Content = mergeContent(e.Content, content) e.AccessCnt++ e.LastAcc = time.Now() if err := s.repo.Update(ctx, &e); err != nil { return nil, fmt.Errorf("merge memory: %w", err) } return &e, nil } } } keywords := extractKeywords(content) if priority <= 0 { priority = 0.5 } m := &Memory{ UserID: userID, SourceID: sourceID, Content: content, Summary: truncate(content, 200), Keywords: keywords, Category: category, Priority: priority, Decay: 1.0, } if _, err := s.repo.Create(ctx, m); err != nil { return nil, fmt.Errorf("add memory: %w", err) } return m, nil } // Search performs combined keyword + semantic search. func (s *Service) Search(ctx context.Context, q SearchQuery) ([]Memory, error) { if q.Limit <= 0 { q.Limit = 10 } memories, err := s.repo.SearchByKeywords(ctx, q.UserID, q.Query, q.Limit) if err != nil { return nil, fmt.Errorf("search memory: %w", err) } // Record access for found memories for _, m := range memories { go s.repo.RecordAccess(context.Background(), m.ID) } // Filter by source if specified if q.SourceID != "" { filtered := make([]Memory, 0) for _, m := range memories { if m.SourceID == q.SourceID { filtered = append(filtered, m) } } return filtered, nil } return memories, nil } // ApplyDecay reduces the decay factor based on time elapsed since last access. func (s *Service) ApplyDecay(ctx context.Context, userID string) (int, error) { memories, err := s.repo.ListByUser(ctx, userID, 100) if err != nil { return 0, fmt.Errorf("list for decay: %w", err) } updated := 0 for _, m := range memories { elapsed := time.Since(m.LastAcc) days := elapsed.Hours() / 24 newDecay := math.Exp(-0.01 * days) if newDecay != m.Decay { m.Decay = newDecay if err := s.repo.Update(ctx, &m); err == nil { updated++ } } } // Purge forgotten memories if _, err := s.repo.PurgeDecayed(ctx, userID); err != nil { return updated, err } return updated, nil } // Delete removes a memory (admin use). func (s *Service) Delete(ctx context.Context, id string) error { return s.repo.Delete(ctx, id) } // List returns recent memories for a user. func (s *Service) List(ctx context.Context, userID string, limit int) ([]Memory, error) { return s.repo.ListByUser(ctx, userID, limit) } // GetByID returns a single memory. func (s *Service) GetByID(ctx context.Context, id string) (*Memory, error) { return s.repo.GetByID(ctx, id) } // --- Internal helpers --- // jaccardSimilarity computes the Jaccard similarity between two strings. func jaccardSimilarity(a, b string) float64 { setA := tokenSet(a) setB := tokenSet(b) intersection := 0 for token := range setA { if setB[token] { intersection++ } } union := len(setA) + len(setB) - intersection if union == 0 { return 0 } return float64(intersection) / float64(union) } func tokenSet(s string) map[string]bool { // Use 2-gram tokens for Chinese and word tokens for English tokens := make(map[string]bool) runes := []rune(s) for i := 0; i < len(runes)-1; i++ { tokens[string(runes[i:i+2])] = true } for _, word := range strings.Fields(s) { if len(word) > 1 { tokens[word] = true } } return tokens } // extractKeywords extracts simple keywords from text. func extractKeywords(content string) []string { // Simple extraction: split by common delimiters, filter short words words := strings.FieldsFunc(content, func(r rune) bool { return r == ',' || r == '。' || r == '、' || r == ' ' || r == '\n' || r == '!' || r == '?' }) seen := make(map[string]bool) var keywords []string for _, w := range words { runes := []rune(w) if len(runes) >= 2 && len(runes) <= 8 && !seen[w] { keywords = append(keywords, w) seen[w] = true } if len(keywords) >= 10 { break } } return keywords } func mergeContent(old, new string) string { if len(new) > len(old) { return new } return old } func truncate(s string, maxLen int) string { runes := []rune(s) if len(runes) <= maxLen { return s } return string(runes[:maxLen]) + "..." }