- 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
100 lines
2.7 KiB
Go
100 lines
2.7 KiB
Go
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))
|
|
}
|
|
}
|