Files
MuyueWorkspace/internal/memory/recall.go
Augustin 4523bbd42c
All checks were successful
Stable Release / stable (push) Successful in 1m34s
feat: RAG, memory, plugins, lessons, file editor, split panes, Markdown rendering, PWA + UI overhaul
Major additions:
- RAG pipeline (indexing, chunking, search) with sidebar upload button
- Memory system with CRUD API
- Plugins and lessons modules
- MCP discovery and MCP server
- Advanced skills (auto-create, conditional, improver)
- Agent browser/image support, delegate, sessions
- File editor with CodeMirror in split panes
- Markdown rendering via react-markdown + KaTeX + highlight.js
- Raw markdown toggle
- PWA manifest + service worker
- Extension UI redesign with new design tokens and studio-style chat
- Pipeline API for chat streaming
- Mobile responsive layout

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-27 21:04:11 +02:00

216 lines
4.8 KiB
Go

package memory
import (
"database/sql"
"strings"
"time"
)
type SearchResult struct {
Memory
Score float64 `json:"score"`
}
func (s *Store) Search(query string, limit int) ([]SearchResult, error) {
s.mu.RLock()
defer s.mu.RUnlock()
if limit <= 0 {
limit = 10
}
if limit > 50 {
limit = 50
}
normalizedQuery := normalizeQuery(query)
rows, err := s.db.Query(`
SELECT m.id, m.type, m.key, m.content, m.tags, m.source, m.confidence,
m.access_count, m.created_at, m.updated_at,
bm25(memories_fts) as score
FROM memories_fts f
JOIN memories m ON m.rowid = f.rowid
WHERE memories_fts MATCH ?
ORDER BY score
LIMIT ?
`, normalizedQuery, limit)
if err != nil {
return fallbackSearch(s.db, query, limit)
}
defer rows.Close()
return scanSearchResults(rows)
}
func (s *Store) Recall(query string, limit int) ([]Memory, error) {
results, err := s.Search(query, limit)
if err != nil {
return nil, err
}
memories := make([]Memory, len(results))
for i, r := range results {
memories[i] = r.Memory
}
return memories, nil
}
func (s *Store) RecallByType(memType MemoryType, limit int) ([]Memory, error) {
s.mu.RLock()
defer s.mu.RUnlock()
if limit <= 0 {
limit = 20
}
rows, err := s.db.Query(`
SELECT id, type, key, content, tags, source, confidence, access_count, created_at, updated_at
FROM memories WHERE type = ?
ORDER BY access_count DESC, updated_at DESC
LIMIT ?
`, string(memType), limit)
if err != nil {
return nil, err
}
defer rows.Close()
return scanMemories(rows)
}
func (s *Store) RecallRecent(since time.Time, limit int) ([]Memory, error) {
s.mu.RLock()
defer s.mu.RUnlock()
if limit <= 0 {
limit = 20
}
rows, err := s.db.Query(`
SELECT id, type, key, content, tags, source, confidence, access_count, created_at, updated_at
FROM memories WHERE updated_at >= ?
ORDER BY updated_at DESC
LIMIT ?
`, since, limit)
if err != nil {
return nil, err
}
defer rows.Close()
return scanMemories(rows)
}
func (s *Store) RecallPreferences() ([]Memory, error) {
return s.RecallByType(TypePreference, 50)
}
func (s *Store) RecallFacts() ([]Memory, error) {
return s.RecallByType(TypeFact, 50)
}
func (s *Store) StorePreference(key, content string) error {
return s.Store(&Memory{
Type: TypePreference,
Key: key,
Content: content,
Source: "user",
Confidence: 0.9,
})
}
func (s *Store) StoreContext(key, content string) error {
return s.Store(&Memory{
Type: TypeContext,
Key: key,
Content: content,
Source: "conversation",
Confidence: 0.7,
})
}
func (s *Store) StoreSummary(sessionID, summary string) error {
return s.Store(&Memory{
Type: TypeSummary,
Key: "session:" + sessionID,
Content: summary,
Source: "auto",
Confidence: 0.8,
})
}
func (s *Store) StoreFact(key, content string) error {
return s.Store(&Memory{
Type: TypeFact,
Key: key,
Content: content,
Source: "auto",
Confidence: 0.85,
})
}
func normalizeQuery(query string) string {
words := strings.Fields(strings.ToLower(query))
var escaped []string
for _, w := range words {
if len(w) > 0 {
escaped = append(escaped, w+"*")
}
}
return strings.Join(escaped, " OR ")
}
func fallbackSearch(db *sql.DB, query string, limit int) ([]SearchResult, error) {
likePattern := "%" + strings.ToLower(query) + "%"
rows, err := db.Query(`
SELECT id, type, key, content, tags, source, confidence, access_count, created_at, updated_at
FROM memories
WHERE LOWER(key) LIKE ? OR LOWER(content) LIKE ? OR LOWER(tags) LIKE ?
ORDER BY updated_at DESC
LIMIT ?
`, likePattern, likePattern, likePattern, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var results []SearchResult
for rows.Next() {
var m Memory
err := rows.Scan(&m.ID, &m.Type, &m.Key, &m.Content, &m.Tags, &m.Source, &m.Confidence, &m.AccessCount, &m.CreatedAt, &m.UpdatedAt)
if err != nil {
return results, err
}
score := computeFallbackScore(m, query)
results = append(results, SearchResult{Memory: m, Score: score})
}
return results, nil
}
func computeFallbackScore(m Memory, query string) float64 {
score := m.Confidence * 0.5
lower := strings.ToLower(query)
if strings.Contains(strings.ToLower(m.Key), lower) {
score += 0.3
}
if strings.Contains(strings.ToLower(m.Content), lower) {
score += 0.2
}
score += float64(m.AccessCount) * 0.01
return score
}
func scanSearchResults(rows *sql.Rows) ([]SearchResult, error) {
var results []SearchResult
for rows.Next() {
var m Memory
var score float64
err := rows.Scan(&m.ID, &m.Type, &m.Key, &m.Content, &m.Tags, &m.Source,
&m.Confidence, &m.AccessCount, &m.CreatedAt, &m.UpdatedAt, &score)
if err != nil {
return results, err
}
results = append(results, SearchResult{Memory: m, Score: score})
}
return results, nil
}