feat: implement memory management system with SQLite persistence, including conversation windows and episodic facts
This commit is contained in:
+199
@@ -0,0 +1,199 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/enmanuel/agents/pkg/memory"
|
||||
)
|
||||
|
||||
// MemoryStore is the subset of memory.Store needed by memory tools.
|
||||
type MemoryStore interface {
|
||||
SaveFact(ctx context.Context, fact memory.Fact) error
|
||||
RecallFacts(ctx context.Context, agentID, subject string, key *string) ([]memory.Fact, error)
|
||||
DeleteFacts(ctx context.Context, agentID, subject string, key *string) error
|
||||
}
|
||||
|
||||
// WindowClearer allows tools to clear the conversation window for a room.
|
||||
type WindowClearer interface {
|
||||
ClearWindow(roomID string)
|
||||
}
|
||||
|
||||
// RoomContext is a thread-safe holder for the current room ID.
|
||||
// Set by the runtime before each event handling; read by memory_clear_context.
|
||||
type RoomContext struct {
|
||||
mu sync.RWMutex
|
||||
roomID string
|
||||
}
|
||||
|
||||
// Set updates the current room ID.
|
||||
func (rc *RoomContext) Set(roomID string) {
|
||||
rc.mu.Lock()
|
||||
rc.roomID = roomID
|
||||
rc.mu.Unlock()
|
||||
}
|
||||
|
||||
// Get returns the current room ID.
|
||||
func (rc *RoomContext) Get() string {
|
||||
rc.mu.RLock()
|
||||
defer rc.mu.RUnlock()
|
||||
return rc.roomID
|
||||
}
|
||||
|
||||
// NewMemorySave creates a tool that saves a fact to long-term memory.
|
||||
func NewMemorySave(agentID string, store MemoryStore) Tool {
|
||||
return Tool{
|
||||
Def: Def{
|
||||
Name: "memory_save",
|
||||
Description: "Save a fact to long-term memory. Use this to remember important information about users, topics, or preferences.",
|
||||
Parameters: []Param{
|
||||
{Name: "subject", Type: "string", Description: "The subject this fact is about (e.g. a username, a topic)", Required: true},
|
||||
{Name: "key", Type: "string", Description: "The fact key (e.g. 'favorite_language', 'timezone')", Required: true},
|
||||
{Name: "value", Type: "string", Description: "The fact value to store", Required: true},
|
||||
},
|
||||
},
|
||||
Exec: func(ctx context.Context, args map[string]any) Result {
|
||||
subject := getString(args, "subject")
|
||||
key := getString(args, "key")
|
||||
value := getString(args, "value")
|
||||
if subject == "" || key == "" || value == "" {
|
||||
return Result{Err: fmt.Errorf("memory_save: subject, key, and value are required")}
|
||||
}
|
||||
err := store.SaveFact(ctx, memory.Fact{
|
||||
AgentID: agentID,
|
||||
Subject: subject,
|
||||
Key: key,
|
||||
Value: value,
|
||||
})
|
||||
if err != nil {
|
||||
return Result{Err: fmt.Errorf("memory_save: %w", err)}
|
||||
}
|
||||
return Result{Output: fmt.Sprintf("saved: %s.%s = %s", subject, key, value)}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewMemoryRecall creates a tool that retrieves facts from long-term memory.
|
||||
func NewMemoryRecall(agentID string, store MemoryStore) Tool {
|
||||
return Tool{
|
||||
Def: Def{
|
||||
Name: "memory_recall",
|
||||
Description: "Recall facts from long-term memory about a subject. Omit key to get all facts for the subject.",
|
||||
Parameters: []Param{
|
||||
{Name: "subject", Type: "string", Description: "The subject to recall facts about", Required: true},
|
||||
{Name: "key", Type: "string", Description: "Optional specific fact key to recall", Required: false},
|
||||
},
|
||||
},
|
||||
Exec: func(ctx context.Context, args map[string]any) Result {
|
||||
subject := getString(args, "subject")
|
||||
if subject == "" {
|
||||
return Result{Err: fmt.Errorf("memory_recall: subject is required")}
|
||||
}
|
||||
var keyPtr *string
|
||||
if k := getString(args, "key"); k != "" {
|
||||
keyPtr = &k
|
||||
}
|
||||
facts, err := store.RecallFacts(ctx, agentID, subject, keyPtr)
|
||||
if err != nil {
|
||||
return Result{Err: fmt.Errorf("memory_recall: %w", err)}
|
||||
}
|
||||
if len(facts) == 0 {
|
||||
return Result{Output: fmt.Sprintf("no facts found for subject %q", subject)}
|
||||
}
|
||||
var sb strings.Builder
|
||||
for _, f := range facts {
|
||||
fmt.Fprintf(&sb, "%s.%s = %s\n", f.Subject, f.Key, f.Value)
|
||||
}
|
||||
return Result{Output: sb.String()}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewMemoryForget creates a tool that deletes facts from long-term memory.
|
||||
func NewMemoryForget(agentID string, store MemoryStore) Tool {
|
||||
return Tool{
|
||||
Def: Def{
|
||||
Name: "memory_forget",
|
||||
Description: "Delete facts from long-term memory. Omit key to delete all facts for the subject.",
|
||||
Parameters: []Param{
|
||||
{Name: "subject", Type: "string", Description: "The subject whose facts to delete", Required: true},
|
||||
{Name: "key", Type: "string", Description: "Optional specific fact key to delete; omit to delete all", Required: false},
|
||||
},
|
||||
},
|
||||
Exec: func(ctx context.Context, args map[string]any) Result {
|
||||
subject := getString(args, "subject")
|
||||
if subject == "" {
|
||||
return Result{Err: fmt.Errorf("memory_forget: subject is required")}
|
||||
}
|
||||
var keyPtr *string
|
||||
if k := getString(args, "key"); k != "" {
|
||||
keyPtr = &k
|
||||
}
|
||||
err := store.DeleteFacts(ctx, agentID, subject, keyPtr)
|
||||
if err != nil {
|
||||
return Result{Err: fmt.Errorf("memory_forget: %w", err)}
|
||||
}
|
||||
if keyPtr != nil {
|
||||
return Result{Output: fmt.Sprintf("forgot %s.%s", subject, *keyPtr)}
|
||||
}
|
||||
return Result{Output: fmt.Sprintf("forgot all facts about %s", subject)}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewMemoryClearContext creates a tool that clears the conversation window.
|
||||
func NewMemoryClearContext(clearer WindowClearer, roomCtx *RoomContext) Tool {
|
||||
return Tool{
|
||||
Def: Def{
|
||||
Name: "memory_clear_context",
|
||||
Description: "Clear the conversation context window. Useful to start fresh. Omit room_id to clear the current room.",
|
||||
Parameters: []Param{
|
||||
{Name: "room_id", Type: "string", Description: "Optional room ID to clear; defaults to current room", Required: false},
|
||||
},
|
||||
},
|
||||
Exec: func(ctx context.Context, args map[string]any) Result {
|
||||
roomID := getString(args, "room_id")
|
||||
if roomID == "" {
|
||||
roomID = roomCtx.Get()
|
||||
}
|
||||
if roomID == "" {
|
||||
return Result{Err: fmt.Errorf("memory_clear_context: no room_id provided and no current room")}
|
||||
}
|
||||
clearer.ClearWindow(roomID)
|
||||
return Result{Output: fmt.Sprintf("conversation context cleared for room %s", roomID)}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewMemorySummary creates a tool that saves an important summary to long-term memory.
|
||||
func NewMemorySummary(agentID string, store MemoryStore) Tool {
|
||||
return Tool{
|
||||
Def: Def{
|
||||
Name: "memory_summary",
|
||||
Description: "Save an important summary or takeaway from the current conversation to long-term memory.",
|
||||
Parameters: []Param{
|
||||
{Name: "text", Type: "string", Description: "The summary text to save", Required: true},
|
||||
},
|
||||
},
|
||||
Exec: func(ctx context.Context, args map[string]any) Result {
|
||||
text := getString(args, "text")
|
||||
if text == "" {
|
||||
return Result{Err: fmt.Errorf("memory_summary: text is required")}
|
||||
}
|
||||
key := time.Now().UTC().Format("2006-01-02T15:04:05")
|
||||
err := store.SaveFact(ctx, memory.Fact{
|
||||
AgentID: agentID,
|
||||
Subject: "_summary",
|
||||
Key: key,
|
||||
Value: text,
|
||||
})
|
||||
if err != nil {
|
||||
return Result{Err: fmt.Errorf("memory_summary: %w", err)}
|
||||
}
|
||||
return Result{Output: "summary saved"}
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user