236 lines
6.7 KiB
Go
236 lines
6.7 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/pgvector/pgvector-go"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/gochat/gochat/internal/llm"
|
|
"github.com/gochat/gochat/internal/model"
|
|
)
|
|
|
|
// ========== Mock Repos for RAG Service Tests ==========
|
|
|
|
// mockAssistantRepo mocks AssistantRepoIface for RAG service tests.
|
|
type mockAssistantRepo struct {
|
|
getByIDResult *model.CaptainAssistant
|
|
getByIDError error
|
|
}
|
|
|
|
func (m *mockAssistantRepo) GetByID(ctx context.Context, id uint) (*model.CaptainAssistant, error) {
|
|
return m.getByIDResult, m.getByIDError
|
|
}
|
|
|
|
// mockResponseRepo mocks ResponseRepoIface for RAG service tests.
|
|
type mockResponseRepo struct {
|
|
searchByEmbeddingResult []model.CaptainAssistantResponse
|
|
searchByEmbeddingError error
|
|
getByIDResult *model.CaptainAssistantResponse
|
|
getByIDError error
|
|
updateError error
|
|
}
|
|
|
|
func (m *mockResponseRepo) SearchByEmbedding(ctx context.Context, assistantID uint, embedding pgvector.Vector, limit int) ([]model.CaptainAssistantResponse, error) {
|
|
return m.searchByEmbeddingResult, m.searchByEmbeddingError
|
|
}
|
|
|
|
func (m *mockResponseRepo) GetByID(ctx context.Context, id uint) (*model.CaptainAssistantResponse, error) {
|
|
return m.getByIDResult, m.getByIDError
|
|
}
|
|
|
|
func (m *mockResponseRepo) Update(ctx context.Context, resp *model.CaptainAssistantResponse) error {
|
|
return m.updateError
|
|
}
|
|
|
|
// ========== RAG Service Tests ==========
|
|
|
|
func setupRAGServiceWithMocks(
|
|
mockProvider *mockLLMProvider,
|
|
mockAssistRepo *mockAssistantRepo,
|
|
mockRespRepo *mockResponseRepo,
|
|
) *RAGService {
|
|
return NewRAGService(mockRespRepo, mockAssistRepo, mockProvider)
|
|
}
|
|
|
|
// --- Query ---
|
|
func TestRAGService_Query(t *testing.T) {
|
|
mockProvider := &mockLLMProvider{}
|
|
assistant := &model.CaptainAssistant{
|
|
AccountID: 1,
|
|
Name: "RAGAssistant",
|
|
Status: model.AssistantStatusActive,
|
|
Config: marshalAssistantConfig(model.DefaultAssistantConfig()),
|
|
}
|
|
assistant.ID = 1
|
|
|
|
mockAssistRepo := &mockAssistantRepo{
|
|
getByIDResult: assistant,
|
|
}
|
|
mockRespRepo := &mockResponseRepo{
|
|
searchByEmbeddingResult: []model.CaptainAssistantResponse{
|
|
{
|
|
AccountID: 1,
|
|
AssistantID: 1,
|
|
Question: "What is GoChat?",
|
|
Answer: "GoChat is an open-source customer engagement platform.",
|
|
Status: model.ResponseStatusApproved,
|
|
},
|
|
},
|
|
}
|
|
// Set the ID on the response after creation (can't set promoted Base.ID in literal)
|
|
mockRespRepo.searchByEmbeddingResult[0].ID = 10
|
|
|
|
// Setup mock LLM: embedding generation returns a valid vector
|
|
mockProvider.embeddingResponse = &llm.EmbeddingResponse{
|
|
Data: []llm.EmbeddingData{
|
|
{Embedding: make([]float64, 1536)},
|
|
},
|
|
}
|
|
// Setup mock LLM: chat completion returns a valid answer
|
|
mockProvider.chatResponse = &llm.ChatResponse{
|
|
Choices: []llm.ChatChoice{
|
|
{Message: llm.ChatMessage{Role: "assistant", Content: "GoChat is an open-source platform for customer engagement."}},
|
|
},
|
|
}
|
|
|
|
svc := setupRAGServiceWithMocks(mockProvider, mockAssistRepo, mockRespRepo)
|
|
|
|
req := &RAGQueryRequest{
|
|
AssistantID: 1,
|
|
Question: "What is GoChat?",
|
|
TopK: 5,
|
|
}
|
|
|
|
result, err := svc.Query(context.Background(), 1, req)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, result.Answer)
|
|
assert.Equal(t, uint(1), result.AssistantID)
|
|
assert.Len(t, result.Sources, 1)
|
|
}
|
|
|
|
func TestRAGService_Query_AssistantNotFound(t *testing.T) {
|
|
mockProvider := &mockLLMProvider{}
|
|
mockAssistRepo := &mockAssistantRepo{
|
|
getByIDError: fmt.Errorf("record not found"),
|
|
}
|
|
mockRespRepo := &mockResponseRepo{}
|
|
|
|
svc := setupRAGServiceWithMocks(mockProvider, mockAssistRepo, mockRespRepo)
|
|
|
|
req := &RAGQueryRequest{
|
|
AssistantID: 9999,
|
|
Question: "What is GoChat?",
|
|
}
|
|
|
|
_, err := svc.Query(context.Background(), 1, req)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "assistant not found")
|
|
}
|
|
|
|
func TestRAGService_Query_EmbeddingGenerationFails(t *testing.T) {
|
|
mockProvider := &mockLLMProvider{
|
|
embeddingError: fmt.Errorf("embedding service unavailable"),
|
|
}
|
|
assistant := &model.CaptainAssistant{
|
|
AccountID: 1,
|
|
Name: "RAGAssistant",
|
|
Status: model.AssistantStatusActive,
|
|
Config: marshalAssistantConfig(model.DefaultAssistantConfig()),
|
|
}
|
|
assistant.ID = 1
|
|
|
|
mockAssistRepo := &mockAssistantRepo{
|
|
getByIDResult: assistant,
|
|
}
|
|
mockRespRepo := &mockResponseRepo{}
|
|
|
|
svc := setupRAGServiceWithMocks(mockProvider, mockAssistRepo, mockRespRepo)
|
|
|
|
req := &RAGQueryRequest{
|
|
AssistantID: 1,
|
|
Question: "What is GoChat?",
|
|
}
|
|
|
|
_, err := svc.Query(context.Background(), 1, req)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "embedding generation failed")
|
|
}
|
|
|
|
func TestRAGService_Query_NoRelevantFAQs(t *testing.T) {
|
|
mockProvider := &mockLLMProvider{}
|
|
assistant := &model.CaptainAssistant{
|
|
AccountID: 1,
|
|
Name: "RAGAssistant",
|
|
Status: model.AssistantStatusActive,
|
|
Config: marshalAssistantConfig(model.DefaultAssistantConfig()),
|
|
}
|
|
assistant.ID = 1
|
|
|
|
mockAssistRepo := &mockAssistantRepo{
|
|
getByIDResult: assistant,
|
|
}
|
|
// No FAQ results found — triggers queryWithoutContext
|
|
mockRespRepo := &mockResponseRepo{
|
|
searchByEmbeddingResult: []model.CaptainAssistantResponse{},
|
|
}
|
|
|
|
// Setup mock LLM: embedding generation returns a valid vector
|
|
mockProvider.embeddingResponse = &llm.EmbeddingResponse{
|
|
Data: []llm.EmbeddingData{
|
|
{Embedding: make([]float64, 1536)},
|
|
},
|
|
}
|
|
// Setup mock LLM: fallback chat completion (no context)
|
|
mockProvider.chatResponse = &llm.ChatResponse{
|
|
Choices: []llm.ChatChoice{
|
|
{Message: llm.ChatMessage{Role: "assistant", Content: "Based on general knowledge, GoChat may be a communication tool."}},
|
|
},
|
|
}
|
|
|
|
svc := setupRAGServiceWithMocks(mockProvider, mockAssistRepo, mockRespRepo)
|
|
|
|
req := &RAGQueryRequest{
|
|
AssistantID: 1,
|
|
Question: "What is GoChat?",
|
|
TopK: 5,
|
|
}
|
|
|
|
result, err := svc.Query(context.Background(), 1, req)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, result.Answer)
|
|
assert.Equal(t, uint(1), result.AssistantID)
|
|
assert.Len(t, result.Sources, 0)
|
|
// Low confidence when no context
|
|
assert.Equal(t, 0.3, result.Confidence)
|
|
}
|
|
|
|
func TestRAGService_Query_DraftAssistant(t *testing.T) {
|
|
mockProvider := &mockLLMProvider{}
|
|
assistant := &model.CaptainAssistant{
|
|
AccountID: 1,
|
|
Name: "RAGAssistant",
|
|
Status: model.AssistantStatusDraft,
|
|
Config: marshalAssistantConfig(model.DefaultAssistantConfig()),
|
|
}
|
|
assistant.ID = 1
|
|
|
|
mockAssistRepo := &mockAssistantRepo{
|
|
getByIDResult: assistant,
|
|
}
|
|
mockRespRepo := &mockResponseRepo{}
|
|
|
|
svc := setupRAGServiceWithMocks(mockProvider, mockAssistRepo, mockRespRepo)
|
|
|
|
req := &RAGQueryRequest{
|
|
AssistantID: 1,
|
|
Question: "What is GoChat?",
|
|
}
|
|
|
|
_, err := svc.Query(context.Background(), 1, req)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "assistant is not active")
|
|
} |