All checks were successful
Stable Release / stable (push) Successful in 1m34s
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>
216 lines
4.8 KiB
Go
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
|
|
}
|