Skip to content

Commit 3265691

Browse files
committed
update autonomous cron example, adding file store
1 parent e449bbd commit 3265691

File tree

2 files changed

+245
-1
lines changed

2 files changed

+245
-1
lines changed
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"log"
8+
"os"
9+
"sort"
10+
"sync"
11+
"time"
12+
13+
kit "github.com/Protocol-Lattice/go-agent/src/adk"
14+
"github.com/Protocol-Lattice/go-agent/src/adk/modules"
15+
"github.com/Protocol-Lattice/go-agent/src/memory"
16+
"github.com/Protocol-Lattice/go-agent/src/memory/model"
17+
)
18+
19+
// FileBackedStore is a simple persistent VectorStore that writes to a local JSON file.
20+
// It is ideal for examples and local testing where you want continuity without running a database.
21+
type FileBackedStore struct {
22+
filePath string
23+
mu sync.RWMutex
24+
nextID int64
25+
records map[int64]model.MemoryRecord
26+
}
27+
28+
func NewFileBackedStore(filePath string) *FileBackedStore {
29+
store := &FileBackedStore{
30+
filePath: filePath,
31+
records: make(map[int64]model.MemoryRecord),
32+
}
33+
store.load()
34+
return store
35+
}
36+
37+
func (s *FileBackedStore) load() {
38+
s.mu.Lock()
39+
defer s.mu.Unlock()
40+
41+
data, err := os.ReadFile(s.filePath)
42+
if err != nil {
43+
if !os.IsNotExist(err) {
44+
println("Warning: failed to read memory file:", err.Error())
45+
}
46+
return
47+
}
48+
49+
var records []model.MemoryRecord
50+
if err := json.Unmarshal(data, &records); err != nil {
51+
println("Warning: failed to unmarshal memory file:", err.Error())
52+
return
53+
}
54+
55+
for _, rec := range records {
56+
s.records[rec.ID] = rec
57+
if rec.ID > s.nextID {
58+
s.nextID = rec.ID
59+
}
60+
}
61+
}
62+
63+
func (s *FileBackedStore) save() error {
64+
var records []model.MemoryRecord
65+
for _, rec := range s.records {
66+
records = append(records, rec)
67+
}
68+
69+
data, err := json.MarshalIndent(records, "", " ")
70+
if err != nil {
71+
return err
72+
}
73+
74+
return os.WriteFile(s.filePath, data, 0644)
75+
}
76+
77+
func (s *FileBackedStore) StoreMemory(ctx context.Context, sessionID, content string, metadata map[string]any, embedding []float32) error {
78+
s.mu.Lock()
79+
defer s.mu.Unlock()
80+
81+
if s.records == nil {
82+
s.records = make(map[int64]model.MemoryRecord)
83+
}
84+
now := time.Now().UTC()
85+
importance, source, summary, lastEmbedded, metadataJSON := model.NormalizeMetadata(metadata, now)
86+
meta := model.DecodeMetadata(metadataJSON)
87+
space := model.StringFromAny(meta["space"])
88+
if space == "" {
89+
space = sessionID
90+
}
91+
matrix := model.ValidEmbeddingMatrix(meta)
92+
storedEmbedding := append([]float32(nil), embedding...)
93+
if len(storedEmbedding) == 0 {
94+
for _, vec := range matrix {
95+
if len(vec) == 0 {
96+
continue
97+
}
98+
storedEmbedding = append([]float32(nil), vec...)
99+
break
100+
}
101+
}
102+
103+
s.nextID++
104+
record := model.MemoryRecord{
105+
ID: s.nextID,
106+
SessionID: sessionID,
107+
Space: space,
108+
Content: content,
109+
Metadata: metadataJSON,
110+
Embedding: storedEmbedding,
111+
Importance: importance,
112+
Source: source,
113+
Summary: summary,
114+
CreatedAt: now,
115+
LastEmbedded: lastEmbedded,
116+
GraphEdges: model.ValidGraphEdges(meta),
117+
EmbeddingMatrix: matrix,
118+
}
119+
120+
s.records[record.ID] = record
121+
return s.save()
122+
}
123+
124+
func (s *FileBackedStore) SearchMemory(ctx context.Context, sessionID string, queryEmbedding []float32, limit int) ([]model.MemoryRecord, error) {
125+
s.mu.RLock()
126+
defer s.mu.RUnlock()
127+
128+
if limit <= 0 {
129+
return nil, nil
130+
}
131+
132+
type scored struct {
133+
rec model.MemoryRecord
134+
score float64
135+
}
136+
scoredRecords := make([]scored, 0, len(s.records))
137+
138+
for _, rec := range s.records {
139+
if sessionID != "" && rec.SessionID != sessionID {
140+
continue
141+
}
142+
score := model.MaxCosineSimilarity(queryEmbedding, rec)
143+
rec.Score = score
144+
scoredRecords = append(scoredRecords, scored{rec: rec, score: score})
145+
}
146+
147+
sort.Slice(scoredRecords, func(i, j int) bool {
148+
return scoredRecords[i].score > scoredRecords[j].score
149+
})
150+
151+
if len(scoredRecords) > limit {
152+
scoredRecords = scoredRecords[:limit]
153+
}
154+
155+
result := make([]model.MemoryRecord, len(scoredRecords))
156+
for i, sc := range scoredRecords {
157+
result[i] = sc.rec
158+
}
159+
160+
return result, nil
161+
}
162+
163+
func (s *FileBackedStore) UpdateEmbedding(ctx context.Context, id int64, embedding []float32, lastEmbedded time.Time) error {
164+
s.mu.Lock()
165+
defer s.mu.Unlock()
166+
167+
rec, ok := s.records[id]
168+
if !ok {
169+
return errors.New("memory not found")
170+
}
171+
172+
rec.Embedding = append([]float32(nil), embedding...)
173+
rec.LastEmbedded = lastEmbedded
174+
s.records[id] = rec
175+
176+
return s.save()
177+
}
178+
179+
func (s *FileBackedStore) DeleteMemory(ctx context.Context, ids []int64) error {
180+
s.mu.Lock()
181+
defer s.mu.Unlock()
182+
183+
for _, id := range ids {
184+
delete(s.records, id)
185+
}
186+
187+
return s.save()
188+
}
189+
190+
func (s *FileBackedStore) Iterate(ctx context.Context, fn func(model.MemoryRecord) bool) error {
191+
s.mu.RLock()
192+
defer s.mu.RUnlock()
193+
194+
ids := make([]int64, 0, len(s.records))
195+
for id := range s.records {
196+
ids = append(ids, id)
197+
}
198+
199+
sort.Slice(ids, func(i, j int) bool { return s.records[ids[i]].CreatedAt.Before(s.records[ids[j]].CreatedAt) })
200+
201+
for _, id := range ids {
202+
if !fn(s.records[id]) {
203+
break
204+
}
205+
}
206+
207+
return nil
208+
}
209+
210+
func (s *FileBackedStore) Count(ctx context.Context) (int, error) {
211+
s.mu.RLock()
212+
defer s.mu.RUnlock()
213+
return len(s.records), nil
214+
}
215+
216+
func FileBackedMemoryModule(filePath string, window int, embedder memory.Embedder, opts *memory.Options) *modules.MemoryModule {
217+
size := window
218+
if size <= 0 {
219+
size = 8
220+
}
221+
bank := memory.NewMemoryBankWithStore(NewFileBackedStore(filePath))
222+
mem := memory.NewSessionMemory(bank, size)
223+
mem.WithEmbedder(embedder)
224+
225+
engineOpts := memory.DefaultOptions()
226+
if opts != nil {
227+
engineOpts = *opts
228+
}
229+
230+
memoryEngine := memory.NewEngine(bank.Store, engineOpts)
231+
mem.WithEngine(memoryEngine)
232+
233+
engineLogger := log.New(os.Stderr, "memory-engine: ", log.LstdFlags)
234+
mem.Engine.WithLogger(engineLogger)
235+
236+
provider := func(context.Context) (kit.MemoryBundle, error) {
237+
shared := func(local string, spaces ...string) *memory.SharedSession {
238+
return memory.NewSharedSession(mem, local, spaces...)
239+
}
240+
return kit.MemoryBundle{Session: mem, Shared: shared}, nil
241+
}
242+
243+
return modules.NewMemoryModule("file_memory", provider)
244+
}

cmd/example/autonomous_cron/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func main() {
6363
modules.NewModelModule("model", func(_ context.Context) (models.Agent, error) {
6464
return orchestratorModel, nil
6565
}),
66-
modules.InMemoryMemoryModule(10000, memory.AutoEmbedder(), &memOpts),
66+
FileBackedMemoryModule("agent_memory.json", 10000, memory.AutoEmbedder(), &memOpts),
6767
),
6868
adk.WithCodeModeUtcp(client, orchestratorModel),
6969
)

0 commit comments

Comments
 (0)