feat(memory): add LLM-driven memory extraction from conversations
- memory/extractor: async LLM-based memory extraction from chat history - Parses JSON lines from LLM response as memory entries - Wire into orchestrator pipeline via PostProcess hook - Non-blocking: runs in goroutine with 30s timeout - Automatic dedup via existing keyword overlap logic
This commit is contained in:
1 parent
886fcf2216
commit
47047c903c
5 files changed
+263
-6
No files matched your search
@@ -0,0 +1,99 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/model"
|
||||
)
|
||||
|
||||
func TestParseExtractedMemories(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
minCount int
|
||||
}{
|
||||
{`{"content":"用户喜欢咖啡","category":"preference","priority":0.8}
|
||||
{"content":"用户每天跑步","category":"fact","priority":0.7}`, 2},
|
||||
{`这是提取的记忆:
|
||||
{"content":"用户名叫张三","category":"fact","priority":0.9}
|
||||
{"content":"用户在北京工作","category":"fact","priority":0.85}`, 2},
|
||||
{`no json here`, 0},
|
||||
{``, 0},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
result := parseExtractedMemories(tt.input)
|
||||
if len(result) < tt.minCount {
|
||||
t.Errorf("expected >= %d memories, got %d: %+v", tt.minCount, len(result), result)
|
||||
}
|
||||
for _, m := range result {
|
||||
if m.Content == "" {
|
||||
t.Error("memory content should not be empty")
|
||||
}
|
||||
if m.Category == "" {
|
||||
t.Error("memory category should not be empty")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractorDisabled(t *testing.T) {
|
||||
e := NewExtractor(nil, nil)
|
||||
if e.enabled {
|
||||
t.Error("extractor should be disabled when llm is nil")
|
||||
}
|
||||
|
||||
// Should not panic
|
||||
e.ExtractFromConversation(nil, "user-1", nil)
|
||||
}
|
||||
|
||||
func TestExtractorParseEdgeCases(t *testing.T) {
|
||||
// Test JSON with whitespace
|
||||
result := parseExtractedMemories(` {"content":"test","category":"fact","priority":0.5} `)
|
||||
if len(result) != 1 {
|
||||
t.Errorf("expected 1, got %d", len(result))
|
||||
}
|
||||
|
||||
// Test empty JSON object
|
||||
result = parseExtractedMemories(`{}`)
|
||||
if len(result) != 0 {
|
||||
t.Errorf("expected 0 for empty object, got %d", len(result))
|
||||
}
|
||||
|
||||
// Test invalid JSON line mixed with valid
|
||||
result = parseExtractedMemories(`invalid
|
||||
{"content":"ok","category":"fact","priority":0.5}
|
||||
not json either`)
|
||||
if len(result) != 1 {
|
||||
t.Errorf("expected 1 valid, got %d", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
// mockLLM implements LLMClient for testing.
|
||||
type mockLLM struct {
|
||||
response string
|
||||
err error
|
||||
}
|
||||
|
||||
func (m *mockLLM) Chat(_ context.Context, _ []model.LLMMessage) (*model.LLMResponse, error) {
|
||||
if m.err != nil { return nil, m.err }
|
||||
return &model.LLMResponse{Content: m.response, Model: "mock"}, nil
|
||||
}
|
||||
|
||||
func TestExtractorWithMockLLM(t *testing.T) {
|
||||
mock := &mockLLM{
|
||||
response: `{"content":"用户喜欢喝咖啡","category":"preference","priority":0.8}
|
||||
{"content":"用户在2024年去过日本","category":"event","priority":0.9}`,
|
||||
}
|
||||
|
||||
// We can't easily test the full extraction without a real DB
|
||||
// But verify mock LLM works
|
||||
resp, err := mock.Chat(nil, nil)
|
||||
if err != nil { t.Fatalf("mock LLM: %v", err) }
|
||||
memories := parseExtractedMemories(resp.Content)
|
||||
if len(memories) != 2 {
|
||||
t.Errorf("expected 2 memories, got %d", len(memories))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user