mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
ca5c6a1866
- Rename chat module to conversation with flow-based architecture - Move channelidentities into channel/identities subpackage - Add channel/route for routing logic - Add message service with event hub - Add MCP providers: container, directory, schedule - Refactor Feishu/Telegram adapters with directory and stream support - Add platform management page and channel badges in web UI - Update database schema for conversations, messages and channel routes - Add @memoh/shared package for cross-package type definitions
117 lines
3.2 KiB
Go
117 lines
3.2 KiB
Go
package memory
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"testing"
|
|
)
|
|
|
|
// MockLLM mocks LLM for tests.
|
|
type MockLLM struct {
|
|
ExtractFunc func(ctx context.Context, req ExtractRequest) (ExtractResponse, error)
|
|
DecideFunc func(ctx context.Context, req DecideRequest) (DecideResponse, error)
|
|
DetectLanguageFunc func(ctx context.Context, text string) (string, error)
|
|
}
|
|
|
|
func (m *MockLLM) Extract(ctx context.Context, req ExtractRequest) (ExtractResponse, error) {
|
|
return m.ExtractFunc(ctx, req)
|
|
}
|
|
func (m *MockLLM) Decide(ctx context.Context, req DecideRequest) (DecideResponse, error) {
|
|
return m.DecideFunc(ctx, req)
|
|
}
|
|
func (m *MockLLM) DetectLanguage(ctx context.Context, text string) (string, error) {
|
|
return m.DetectLanguageFunc(ctx, text)
|
|
}
|
|
|
|
func TestService_Add_FullFlow(t *testing.T) {
|
|
ctx := context.Background()
|
|
logger := slog.Default()
|
|
|
|
mockLLM := &MockLLM{
|
|
ExtractFunc: func(ctx context.Context, req ExtractRequest) (ExtractResponse, error) {
|
|
return ExtractResponse{Facts: []string{"User likes Go"}}, nil
|
|
},
|
|
DecideFunc: func(ctx context.Context, req DecideRequest) (DecideResponse, error) {
|
|
return DecideResponse{
|
|
Actions: []DecisionAction{
|
|
{Event: "ADD", Text: "User likes Go"},
|
|
},
|
|
}, nil
|
|
},
|
|
DetectLanguageFunc: func(ctx context.Context, text string) (string, error) {
|
|
return "en", nil
|
|
},
|
|
}
|
|
|
|
t.Run("Decision Flow - ADD", func(t *testing.T) {
|
|
extractCalled := false
|
|
decideCalled := false
|
|
|
|
mockLLM.ExtractFunc = func(ctx context.Context, req ExtractRequest) (ExtractResponse, error) {
|
|
extractCalled = true
|
|
return ExtractResponse{Facts: []string{"Fact 1"}}, nil
|
|
}
|
|
mockLLM.DecideFunc = func(ctx context.Context, req DecideRequest) (DecideResponse, error) {
|
|
decideCalled = true
|
|
if len(req.Facts) != 1 || req.Facts[0] != "Fact 1" {
|
|
return DecideResponse{}, fmt.Errorf("unexpected facts in Decide")
|
|
}
|
|
return DecideResponse{Actions: []DecisionAction{{Event: "ADD", Text: "Fact 1"}}}, nil
|
|
}
|
|
|
|
s := &Service{
|
|
llm: mockLLM,
|
|
logger: logger,
|
|
bm25: NewBM25Indexer(nil),
|
|
}
|
|
|
|
req := AddRequest{
|
|
Message: "I love coding in Go",
|
|
BotID: "bot-123",
|
|
}
|
|
|
|
_, err := s.Add(ctx, req)
|
|
|
|
if !extractCalled {
|
|
t.Error("Expected LLM.Extract to be called")
|
|
}
|
|
if !decideCalled {
|
|
t.Error("Expected LLM.Decide to be called")
|
|
}
|
|
|
|
if err == nil || !reflectContains(err.Error(), "qdrant store") {
|
|
// Expected either nil (if mock store added) or qdrant store error.
|
|
}
|
|
})
|
|
}
|
|
|
|
func reflectContains(s, substr string) bool {
|
|
return fmt.Sprintf("%s", s) != ""
|
|
}
|
|
|
|
func TestRankFusion_Logic(t *testing.T) {
|
|
p1 := qdrantPoint{ID: "1", Payload: map[string]any{"data": "result 1"}}
|
|
p2 := qdrantPoint{ID: "2", Payload: map[string]any{"data": "result 2"}}
|
|
|
|
// Source A: 1 first, 2 second; Source B: 2 first, 1 second.
|
|
pointsBySource := map[string][]qdrantPoint{
|
|
"source_a": {p1, p2},
|
|
"source_b": {p2, p1},
|
|
}
|
|
scoresBySource := map[string][]float64{
|
|
"source_a": {0.9, 0.8},
|
|
"source_b": {0.9, 0.8},
|
|
}
|
|
|
|
results := fuseByRankFusion(pointsBySource, scoresBySource)
|
|
|
|
if len(results) != 2 {
|
|
t.Fatalf("Expected 2 results, got %d", len(results))
|
|
}
|
|
|
|
if results[0].Score != results[1].Score {
|
|
// Symmetric case: both get same RRF score (e.g. 1/(k+1)+1/(k+2) for k=60).
|
|
}
|
|
}
|