All checks were successful
Beta Release / beta (push) Successful in 5m9s
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>
141 lines
3.5 KiB
Go
141 lines
3.5 KiB
Go
package memory
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type MemoryInjector struct {
|
|
store *Store
|
|
}
|
|
|
|
func NewInjector(store *Store) *MemoryInjector {
|
|
return &MemoryInjector{store: store}
|
|
}
|
|
|
|
func (mi *MemoryInjector) BuildContextBlock(query string) (string, error) {
|
|
var contextParts []string
|
|
|
|
preferences, err := mi.store.RecallPreferences()
|
|
if err == nil && len(preferences) > 0 {
|
|
var prefLines []string
|
|
for _, p := range preferences {
|
|
prefLines = append(prefLines, fmt.Sprintf("- %s: %s", p.Key, p.Content))
|
|
}
|
|
contextParts = append(contextParts,
|
|
"[User Preferences]\n"+strings.Join(prefLines, "\n"))
|
|
}
|
|
|
|
facts, err := mi.store.RecallFacts()
|
|
if err == nil && len(facts) > 0 {
|
|
var factLines []string
|
|
for _, f := range facts {
|
|
factLines = append(factLines, fmt.Sprintf("- %s: %s", f.Key, f.Content))
|
|
}
|
|
contextParts = append(contextParts,
|
|
"[Known Facts]\n"+strings.Join(factLines, "\n"))
|
|
}
|
|
|
|
if query != "" {
|
|
relevant, err := mi.store.Recall(query, 5)
|
|
if err == nil && len(relevant) > 0 {
|
|
var relLines []string
|
|
for _, r := range relevant {
|
|
relLines = append(relLines, fmt.Sprintf("- [%s] %s: %s", r.Type, r.Key, truncate(r.Content, 150)))
|
|
}
|
|
contextParts = append(contextParts,
|
|
"[Relevant Memories]\n"+strings.Join(relLines, "\n"))
|
|
}
|
|
}
|
|
|
|
recentCutoff := time.Now().Add(-24 * time.Hour)
|
|
recent, err := mi.store.RecallRecent(recentCutoff, 5)
|
|
if err == nil && len(recent) > 0 {
|
|
var recentLines []string
|
|
for _, r := range recent {
|
|
recentLines = append(recentLines, fmt.Sprintf("- [%s] %s", r.Type, truncate(r.Content, 100)))
|
|
}
|
|
contextParts = append(contextParts,
|
|
"[Recent Context]\n"+strings.Join(recentLines, "\n"))
|
|
}
|
|
|
|
if len(contextParts) == 0 {
|
|
return "", nil
|
|
}
|
|
|
|
return fmt.Sprintf("<memory-context>\n[System note: NOT new user input — recalled context]\n%s\n</memory-context>",
|
|
strings.Join(contextParts, "\n\n")), nil
|
|
}
|
|
|
|
func (mi *MemoryInjector) BuildSystemPromptBlock() (string, error) {
|
|
preferences, err := mi.store.RecallPreferences()
|
|
if err != nil || len(preferences) == 0 {
|
|
return "", nil
|
|
}
|
|
|
|
var lines []string
|
|
lines = append(lines, "Known user preferences:")
|
|
for _, p := range preferences {
|
|
lines = append(lines, fmt.Sprintf("- %s: %s", p.Key, p.Content))
|
|
}
|
|
|
|
return strings.Join(lines, "\n"), nil
|
|
}
|
|
|
|
func (mi *MemoryInjector) ExtractAndStore(userMessage, assistantMessage string) error {
|
|
pref := extractPreference(userMessage)
|
|
if pref != "" {
|
|
if err := mi.store.StorePreference("detected", pref); err != nil {
|
|
return fmt.Errorf("store preference: %w", err)
|
|
}
|
|
}
|
|
|
|
if assistantMessage != "" {
|
|
ctx := extractContext(assistantMessage)
|
|
if ctx != "" {
|
|
if err := mi.store.StoreContext("conversation", ctx); err != nil {
|
|
return fmt.Errorf("store context: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func extractPreference(message string) string {
|
|
indicators := []string{
|
|
"i prefer", "i like", "i always", "i never", "my favorite",
|
|
"i use", "je préfère", "j'aime", "toujours", "jamais",
|
|
}
|
|
lower := strings.ToLower(message)
|
|
for _, ind := range indicators {
|
|
if strings.Contains(lower, ind) {
|
|
idx := strings.Index(lower, ind)
|
|
end := idx + len(ind) + 100
|
|
if end > len(message) {
|
|
end = len(message)
|
|
}
|
|
return truncate(message[idx:end], 200)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func extractContext(message string) string {
|
|
if len(message) < 50 {
|
|
return ""
|
|
}
|
|
if len(message) > 500 {
|
|
return truncate(message, 500)
|
|
}
|
|
return message
|
|
}
|
|
|
|
func truncate(s string, maxLen int) string {
|
|
if len(s) <= maxLen {
|
|
return s
|
|
}
|
|
return s[:maxLen] + "..."
|
|
}
|