782 lines
26 KiB
Go
782 lines
26 KiB
Go
package semanticcache
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
bifrost "github.com/maximhq/bifrost/core"
|
|
"github.com/maximhq/bifrost/core/schemas"
|
|
"github.com/maximhq/bifrost/framework/vectorstore"
|
|
mocker "github.com/maximhq/bifrost/plugins/mocker"
|
|
)
|
|
|
|
// getWeaviateConfigFromEnv retrieves Weaviate configuration from environment variables
|
|
func getWeaviateConfigFromEnv() vectorstore.WeaviateConfig {
|
|
scheme := os.Getenv("WEAVIATE_SCHEME")
|
|
if scheme == "" {
|
|
scheme = "http"
|
|
}
|
|
host := schemas.NewEnvVar("env.WEAVIATE_HOST")
|
|
if host.GetValue() == "" {
|
|
host = schemas.NewEnvVar("localhost:9000")
|
|
}
|
|
|
|
apiKey := schemas.NewEnvVar("env.WEAVIATE_API_KEY")
|
|
|
|
timeoutStr := os.Getenv("WEAVIATE_TIMEOUT")
|
|
timeout := 30 // default
|
|
if timeoutStr != "" {
|
|
if t, err := strconv.Atoi(timeoutStr); err == nil {
|
|
timeout = t
|
|
}
|
|
}
|
|
|
|
return vectorstore.WeaviateConfig{
|
|
Scheme: scheme,
|
|
Host: host,
|
|
APIKey: apiKey,
|
|
Timeout: time.Duration(timeout) * time.Second,
|
|
}
|
|
}
|
|
|
|
// getRedisConfigFromEnv retrieves Redis configuration from environment variables
|
|
func getRedisConfigFromEnv() vectorstore.RedisConfig {
|
|
addr := schemas.NewEnvVar("env.REDIS_ADDR")
|
|
if addr.GetValue() == "" {
|
|
addr = schemas.NewEnvVar("localhost:6379")
|
|
}
|
|
username := schemas.NewEnvVar("env.REDIS_USERNAME")
|
|
password := schemas.NewEnvVar("env.REDIS_PASSWORD")
|
|
db := schemas.NewEnvVar("env.REDIS_DB")
|
|
|
|
timeoutStr := os.Getenv("REDIS_TIMEOUT")
|
|
if timeoutStr == "" {
|
|
timeoutStr = "10s"
|
|
}
|
|
timeout, err := time.ParseDuration(timeoutStr)
|
|
if err != nil {
|
|
timeout = 10 * time.Second
|
|
}
|
|
|
|
return vectorstore.RedisConfig{
|
|
Addr: addr,
|
|
Username: username,
|
|
Password: password,
|
|
DB: db,
|
|
ContextTimeout: timeout,
|
|
}
|
|
}
|
|
|
|
// getQdrantConfigFromEnv retrieves Qdrant configuration from environment variables
|
|
func getQdrantConfigFromEnv() vectorstore.QdrantConfig {
|
|
host := schemas.NewEnvVar("env.QDRANT_HOST")
|
|
if host.GetValue() == "" {
|
|
host = schemas.NewEnvVar("localhost")
|
|
}
|
|
port := schemas.NewEnvVar("env.QDRANT_PORT")
|
|
if port.GetValue() == "" {
|
|
port = schemas.NewEnvVar("6334")
|
|
}
|
|
apiKey := schemas.NewEnvVar("env.QDRANT_API_KEY")
|
|
useTLS := schemas.NewEnvVar("env.QDRANT_USE_TLS")
|
|
if useTLS.GetValue() == "" {
|
|
useTLS = schemas.NewEnvVar("false")
|
|
}
|
|
|
|
return vectorstore.QdrantConfig{
|
|
Host: *host,
|
|
Port: *port,
|
|
APIKey: *apiKey,
|
|
UseTLS: *useTLS,
|
|
}
|
|
}
|
|
|
|
// getPineconeConfigFromEnv retrieves Pinecone configuration from environment variables
|
|
func getPineconeConfigFromEnv() vectorstore.PineconeConfig {
|
|
apiKey := schemas.NewEnvVar("env.PINECONE_API_KEY")
|
|
if apiKey.GetValue() == "" {
|
|
apiKey = schemas.NewEnvVar("pclocal") // Pinecone Local doesn't validate API keys
|
|
}
|
|
indexHost := schemas.NewEnvVar("env.PINECONE_INDEX_HOST")
|
|
if indexHost.GetValue() == "" {
|
|
indexHost = schemas.NewEnvVar("localhost:5081") // Pinecone Local default port
|
|
}
|
|
|
|
return vectorstore.PineconeConfig{
|
|
APIKey: *apiKey,
|
|
IndexHost: *indexHost,
|
|
}
|
|
}
|
|
|
|
// BaseAccount implements the schemas.Account interface for testing purposes.
|
|
type BaseAccount struct{}
|
|
|
|
func (baseAccount *BaseAccount) GetConfiguredProviders() ([]schemas.ModelProvider, error) {
|
|
return []schemas.ModelProvider{schemas.OpenAI}, nil
|
|
}
|
|
|
|
func (baseAccount *BaseAccount) GetKeysForProvider(ctx context.Context, providerKey schemas.ModelProvider) ([]schemas.Key, error) {
|
|
return []schemas.Key{
|
|
{
|
|
Value: *schemas.NewEnvVar("env.OPENAI_API_KEY"),
|
|
Models: schemas.WhiteList{"*"}, // "*" means allow all models
|
|
Weight: 1.0,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (baseAccount *BaseAccount) GetConfigForProvider(providerKey schemas.ModelProvider) (*schemas.ProviderConfig, error) {
|
|
return &schemas.ProviderConfig{
|
|
NetworkConfig: schemas.NetworkConfig{
|
|
DefaultRequestTimeoutInSeconds: 60,
|
|
MaxRetries: 5,
|
|
RetryBackoffInitial: 100 * time.Millisecond,
|
|
RetryBackoffMax: 30 * time.Second,
|
|
},
|
|
ConcurrencyAndBufferSize: schemas.ConcurrencyAndBufferSize{
|
|
Concurrency: 10,
|
|
BufferSize: 10,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// getMockRules returns a list of mock rules for the semantic cache tests
|
|
func getMockRules() []mocker.MockRule {
|
|
return []mocker.MockRule{
|
|
// Core test prompts
|
|
{
|
|
Name: "bifrost-definition",
|
|
Enabled: true,
|
|
Conditions: mocker.Conditions{MessageRegex: bifrost.Ptr("(?i)What is Bifrost.*")},
|
|
Probability: 1.0,
|
|
Responses: []mocker.Response{
|
|
{Type: mocker.ResponseTypeSuccess, Content: &mocker.SuccessResponse{Message: "Bifrost is a unified API for interacting with multiple AI providers."}},
|
|
},
|
|
},
|
|
{
|
|
Name: "machine-learning-explanation",
|
|
Enabled: true,
|
|
Conditions: mocker.Conditions{MessageRegex: bifrost.Ptr("(?i)what is machine learning\\?|explain machine learning|machine learning concepts|can you explain machine learning|explain the basics of machine learning")},
|
|
Probability: 1.0,
|
|
Responses: []mocker.Response{
|
|
{Type: mocker.ResponseTypeSuccess, Content: &mocker.SuccessResponse{Message: "Machine learning is a field of AI that uses statistical techniques to give computer systems the ability to learn from data."}},
|
|
},
|
|
},
|
|
{
|
|
Name: "ai-explanation",
|
|
Enabled: true,
|
|
Conditions: mocker.Conditions{MessageRegex: bifrost.Ptr("(?i)what is artificial intelligence\\?|can you explain what ai is\\?|define artificial intelligence")},
|
|
Probability: 1.0,
|
|
Responses: []mocker.Response{
|
|
{Type: mocker.ResponseTypeSuccess, Content: &mocker.SuccessResponse{Message: "Artificial intelligence is the simulation of human intelligence in machines."}},
|
|
},
|
|
},
|
|
{
|
|
Name: "capital-of-france",
|
|
Enabled: true,
|
|
Conditions: mocker.Conditions{MessageRegex: bifrost.Ptr("What is the capital of France\\?")},
|
|
Probability: 1.0,
|
|
Responses: []mocker.Response{
|
|
{Type: mocker.ResponseTypeSuccess, Content: &mocker.SuccessResponse{Message: "The capital of France is Paris."}},
|
|
},
|
|
},
|
|
{
|
|
Name: "newton-laws",
|
|
Enabled: true,
|
|
Conditions: mocker.Conditions{MessageRegex: bifrost.Ptr("(?i)describe.*newton.*three laws|describe.*three laws.*newton")},
|
|
Probability: 1.0,
|
|
Responses: []mocker.Response{
|
|
{Type: mocker.ResponseTypeSuccess, Content: &mocker.SuccessResponse{Message: "Newton's three laws of motion are: 1. An object at rest stays at rest and an object in motion stays in motion with the same speed and in the same direction unless acted upon by an unbalanced force. 2. The acceleration of an object as produced by a net force is directly proportional to the magnitude of the net force, in the same direction as the net force, and inversely proportional to the mass of the object. 3. For every action, there is an equal and opposite reaction."}},
|
|
},
|
|
},
|
|
// Weather-related prompts
|
|
{
|
|
Name: "weather-question",
|
|
Enabled: true,
|
|
Conditions: mocker.Conditions{MessageRegex: bifrost.Ptr("(?i)what.*weather|weather.*like")},
|
|
Probability: 1.0,
|
|
Responses: []mocker.Response{
|
|
{Type: mocker.ResponseTypeSuccess, Content: &mocker.SuccessResponse{Message: "It's sunny today with a temperature of 72°F."}},
|
|
},
|
|
},
|
|
// Blockchain and deep learning
|
|
{
|
|
Name: "blockchain-definition",
|
|
Enabled: true,
|
|
Conditions: mocker.Conditions{MessageRegex: bifrost.Ptr("(?i)define blockchain|blockchain technology")},
|
|
Probability: 1.0,
|
|
Responses: []mocker.Response{
|
|
{Type: mocker.ResponseTypeSuccess, Content: &mocker.SuccessResponse{Message: "Blockchain is a distributed ledger technology that maintains a continuously growing list of records."}},
|
|
},
|
|
},
|
|
{
|
|
Name: "deep-learning",
|
|
Enabled: true,
|
|
Conditions: mocker.Conditions{MessageRegex: bifrost.Ptr("(?i)what is deep learning")},
|
|
Probability: 1.0,
|
|
Responses: []mocker.Response{
|
|
{Type: mocker.ResponseTypeSuccess, Content: &mocker.SuccessResponse{Message: "Deep learning is a subset of machine learning that uses neural networks with multiple layers."}},
|
|
},
|
|
},
|
|
// Quantum computing
|
|
{
|
|
Name: "quantum-computing",
|
|
Enabled: true,
|
|
Conditions: mocker.Conditions{MessageRegex: bifrost.Ptr("(?i)quantum computing|explain quantum")},
|
|
Probability: 1.0,
|
|
Responses: []mocker.Response{
|
|
{Type: mocker.ResponseTypeSuccess, Content: &mocker.SuccessResponse{Message: "Quantum computing uses quantum mechanical phenomena to process information in ways that classical computers cannot."}},
|
|
},
|
|
},
|
|
// Conversation prompts
|
|
{
|
|
Name: "hello-greeting",
|
|
Enabled: true,
|
|
Conditions: mocker.Conditions{MessageRegex: bifrost.Ptr("(?i)^hello$|^hi$|hello.*world")},
|
|
Probability: 1.0,
|
|
Responses: []mocker.Response{
|
|
{Type: mocker.ResponseTypeSuccess, Content: &mocker.SuccessResponse{Message: "Hello! How can I help you today?"}},
|
|
},
|
|
},
|
|
{
|
|
Name: "how-are-you",
|
|
Enabled: true,
|
|
Conditions: mocker.Conditions{MessageRegex: bifrost.Ptr("(?i)how are you")},
|
|
Probability: 1.0,
|
|
Responses: []mocker.Response{
|
|
{Type: mocker.ResponseTypeSuccess, Content: &mocker.SuccessResponse{Message: "I'm doing well, thank you for asking!"}},
|
|
},
|
|
},
|
|
{
|
|
Name: "meaning-of-life",
|
|
Enabled: true,
|
|
Conditions: mocker.Conditions{MessageRegex: bifrost.Ptr("(?i)meaning of life")},
|
|
Probability: 1.0,
|
|
Responses: []mocker.Response{
|
|
{Type: mocker.ResponseTypeSuccess, Content: &mocker.SuccessResponse{Message: "The meaning of life is a philosophical question that has been pondered for centuries. Some say it's 42!"}},
|
|
},
|
|
},
|
|
{
|
|
Name: "short-story",
|
|
Enabled: true,
|
|
Conditions: mocker.Conditions{MessageRegex: bifrost.Ptr("(?i)tell me.*short story")},
|
|
Probability: 1.0,
|
|
Responses: []mocker.Response{
|
|
{Type: mocker.ResponseTypeSuccess, Content: &mocker.SuccessResponse{Message: "Once upon a time, there was a brave knight who saved the day."}},
|
|
},
|
|
},
|
|
// Test-specific prompts
|
|
{
|
|
Name: "test-configuration",
|
|
Enabled: true,
|
|
Conditions: mocker.Conditions{MessageRegex: bifrost.Ptr("(?i)test configuration")},
|
|
Probability: 1.0,
|
|
Responses: []mocker.Response{
|
|
{Type: mocker.ResponseTypeSuccess, Content: &mocker.SuccessResponse{Message: "This is a test configuration response."}},
|
|
},
|
|
},
|
|
{
|
|
Name: "test-messages",
|
|
Enabled: true,
|
|
Conditions: mocker.Conditions{MessageRegex: bifrost.Ptr("(?i)test.*message|test.*no-store|test.*cache|test.*error|ttl test|threshold test|provider.*test|edge case test")},
|
|
Probability: 1.0,
|
|
Responses: []mocker.Response{
|
|
{Type: mocker.ResponseTypeSuccess, Content: &mocker.SuccessResponse{Message: "This is a test response for various test scenarios."}},
|
|
},
|
|
},
|
|
{
|
|
Name: "long-prompt",
|
|
Enabled: true,
|
|
Conditions: mocker.Conditions{MessageRegex: bifrost.Ptr("(?i)very long prompt")},
|
|
Probability: 1.0,
|
|
Responses: []mocker.Response{
|
|
{Type: mocker.ResponseTypeSuccess, Content: &mocker.SuccessResponse{Message: "This is a response to a very long prompt."}},
|
|
},
|
|
},
|
|
{
|
|
Name: "parameter-tests",
|
|
Enabled: true,
|
|
Conditions: mocker.Conditions{MessageRegex: bifrost.Ptr("(?i)test.*parameters|performance test")},
|
|
Probability: 1.0,
|
|
Responses: []mocker.Response{
|
|
{Type: mocker.ResponseTypeSuccess, Content: &mocker.SuccessResponse{Message: "Parameter test response with various settings."}},
|
|
},
|
|
},
|
|
// Dynamic message patterns (for conversation tests)
|
|
{
|
|
Name: "message-pattern",
|
|
Enabled: true,
|
|
Conditions: mocker.Conditions{MessageRegex: bifrost.Ptr("(?i)message \\d+")},
|
|
Probability: 1.0,
|
|
Responses: []mocker.Response{
|
|
{Type: mocker.ResponseTypeSuccess, Content: &mocker.SuccessResponse{Message: "Response to numbered message."}},
|
|
},
|
|
},
|
|
// Default catch-all rule (lowest priority)
|
|
{
|
|
Name: "default-mock",
|
|
Enabled: true,
|
|
Priority: -1, // Lower priority
|
|
Conditions: mocker.Conditions{},
|
|
Probability: 1.0,
|
|
Responses: []mocker.Response{
|
|
{Type: mocker.ResponseTypeSuccess, Content: &mocker.SuccessResponse{Message: "This is a generic mocked response."}},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// getMockedBifrostClient creates a Bifrost client with a mocker plugin for testing
|
|
func getMockedBifrostClient(t *testing.T, ctx *schemas.BifrostContext, logger schemas.Logger, semanticCachePlugin schemas.LLMPlugin) *bifrost.Bifrost {
|
|
mockerCfg := mocker.MockerConfig{
|
|
Enabled: true,
|
|
Rules: getMockRules(),
|
|
}
|
|
|
|
mockerPlugin, err := mocker.Init(mockerCfg)
|
|
if err != nil {
|
|
t.Fatalf("Failed to initialize mocker plugin: %v", err)
|
|
}
|
|
|
|
account := &BaseAccount{}
|
|
client, err := bifrost.Init(ctx, schemas.BifrostConfig{
|
|
Account: account,
|
|
LLMPlugins: []schemas.LLMPlugin{semanticCachePlugin, mockerPlugin},
|
|
Logger: logger,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Error initializing Bifrost with mocker: %v", err)
|
|
}
|
|
|
|
return client
|
|
}
|
|
|
|
// TestSetup contains common test setup components
|
|
type TestSetup struct {
|
|
Logger schemas.Logger
|
|
Store vectorstore.VectorStore
|
|
Plugin schemas.LLMPlugin
|
|
Client *bifrost.Bifrost
|
|
Config *Config
|
|
}
|
|
|
|
// NewTestSetup creates a new test setup with default configuration
|
|
func NewTestSetup(t *testing.T) *TestSetup {
|
|
return NewTestSetupWithConfig(t, &Config{
|
|
Provider: schemas.OpenAI,
|
|
EmbeddingModel: "text-embedding-3-small",
|
|
Dimension: 1536,
|
|
Threshold: 0.8,
|
|
CleanUpOnShutdown: true,
|
|
Keys: []schemas.Key{
|
|
{
|
|
Value: *schemas.NewEnvVar("env.OPENAI_API_KEY"),
|
|
Models: schemas.WhiteList{"*"},
|
|
Weight: 1.0,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
// NewTestSetupWithConfig creates a new test setup with custom configuration
|
|
func NewTestSetupWithConfig(t *testing.T, config *Config) *TestSetup {
|
|
return NewTestSetupWithVectorStore(t, config, vectorstore.VectorStoreTypeWeaviate)
|
|
}
|
|
|
|
// NewTestSetupWithVectorStore creates a new test setup with custom configuration and vector store type
|
|
func NewTestSetupWithVectorStore(t *testing.T, config *Config, storeType vectorstore.VectorStoreType) *TestSetup {
|
|
ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline)
|
|
logger := bifrost.NewDefaultLogger(schemas.LogLevelDebug)
|
|
|
|
// Get the appropriate config for the vector store type
|
|
var storeConfig interface{}
|
|
switch storeType {
|
|
case vectorstore.VectorStoreTypeWeaviate:
|
|
storeConfig = getWeaviateConfigFromEnv()
|
|
case vectorstore.VectorStoreTypeRedis:
|
|
storeConfig = getRedisConfigFromEnv()
|
|
case vectorstore.VectorStoreTypeQdrant:
|
|
storeConfig = getQdrantConfigFromEnv()
|
|
case vectorstore.VectorStoreTypePinecone:
|
|
storeConfig = getPineconeConfigFromEnv()
|
|
default:
|
|
t.Fatalf("Unsupported vector store type: %s", storeType)
|
|
}
|
|
|
|
store, err := vectorstore.NewVectorStore(context.Background(), &vectorstore.Config{
|
|
Type: storeType,
|
|
Config: storeConfig,
|
|
Enabled: true,
|
|
}, logger)
|
|
if err != nil {
|
|
t.Skipf("Vector store %s not available or failed to connect: %v", storeType, err)
|
|
}
|
|
|
|
plugin, err := Init(schemas.NewBifrostContext(context.Background(), schemas.NoDeadline), config, logger, store)
|
|
if err != nil {
|
|
t.Fatalf("Failed to initialize plugin: %v", err)
|
|
}
|
|
|
|
// Clear test keys
|
|
pluginImpl := plugin.(*Plugin)
|
|
clearTestKeysWithStore(t, pluginImpl.store)
|
|
|
|
// Get a mocked Bifrost client
|
|
client := getMockedBifrostClient(t, ctx, logger, plugin)
|
|
|
|
return &TestSetup{
|
|
Logger: logger,
|
|
Store: store,
|
|
Plugin: plugin,
|
|
Client: client,
|
|
Config: config,
|
|
}
|
|
}
|
|
|
|
// Cleanup cleans up test resources
|
|
func (ts *TestSetup) Cleanup() {
|
|
if ts.Client != nil {
|
|
ts.Client.Shutdown()
|
|
}
|
|
}
|
|
|
|
// clearTestKeysWithStore removes all keys matching the test prefix using the store interface
|
|
func clearTestKeysWithStore(t *testing.T, store vectorstore.VectorStore) {
|
|
// With the new unified VectorStore interface, cleanup is typically handled
|
|
// by the vector store implementation (e.g., dropping entire classes)
|
|
t.Logf("Test cleanup delegated to vector store implementation")
|
|
}
|
|
|
|
// CreateBasicChatRequest creates a basic chat completion request for testing
|
|
func CreateBasicChatRequest(content string, temperature float64, maxTokens int) *schemas.BifrostChatRequest {
|
|
return &schemas.BifrostChatRequest{
|
|
Provider: schemas.OpenAI,
|
|
Model: "gpt-4o-mini",
|
|
Input: []schemas.ChatMessage{
|
|
{
|
|
Role: "user",
|
|
Content: &schemas.ChatMessageContent{
|
|
ContentStr: &content,
|
|
},
|
|
},
|
|
},
|
|
Params: &schemas.ChatParameters{
|
|
Temperature: &temperature,
|
|
MaxCompletionTokens: &maxTokens,
|
|
},
|
|
}
|
|
}
|
|
|
|
// CreateStreamingChatRequest creates a streaming chat completion request for testing
|
|
func CreateStreamingChatRequest(content string, temperature float64, maxTokens int) *schemas.BifrostChatRequest {
|
|
return CreateBasicChatRequest(content, temperature, maxTokens)
|
|
}
|
|
|
|
// CreateSpeechRequest creates a speech synthesis request for testing
|
|
func CreateSpeechRequest(input string, voice string) *schemas.BifrostSpeechRequest {
|
|
return &schemas.BifrostSpeechRequest{
|
|
Provider: schemas.OpenAI,
|
|
Model: "tts-1",
|
|
Input: &schemas.SpeechInput{
|
|
Input: input,
|
|
},
|
|
Params: &schemas.SpeechParameters{
|
|
VoiceConfig: &schemas.SpeechVoiceInput{
|
|
Voice: &voice,
|
|
},
|
|
ResponseFormat: "mp3",
|
|
},
|
|
}
|
|
}
|
|
|
|
// AssertCacheHit verifies that a response was served from cache
|
|
func AssertCacheHit(t *testing.T, response *schemas.BifrostResponse, expectedCacheType string) {
|
|
extraFields := response.GetExtraFields()
|
|
|
|
if extraFields.CacheDebug == nil {
|
|
t.Error("Cache metadata missing 'cache_debug'")
|
|
return
|
|
}
|
|
|
|
// Check that it's actually a cache hit
|
|
if !extraFields.CacheDebug.CacheHit {
|
|
t.Error("❌ Expected cache hit but response was not cached")
|
|
return
|
|
}
|
|
|
|
if expectedCacheType != "" {
|
|
cacheType := extraFields.CacheDebug.HitType
|
|
if cacheType != nil && *cacheType != expectedCacheType {
|
|
t.Errorf("Expected cache type '%s', got '%s'", expectedCacheType, *cacheType)
|
|
return
|
|
}
|
|
|
|
t.Log("✅ Response correctly served from cache")
|
|
}
|
|
|
|
t.Log("✅ Response correctly served from cache")
|
|
}
|
|
|
|
// AssertNoCacheHit verifies that a response was NOT served from cache
|
|
func AssertNoCacheHit(t *testing.T, response *schemas.BifrostResponse) {
|
|
extraFields := response.GetExtraFields()
|
|
|
|
if extraFields.CacheDebug == nil {
|
|
t.Log("✅ Response correctly not served from cache (no 'cache_debug' flag)")
|
|
return
|
|
}
|
|
|
|
// Check the actual CacheHit field instead of just checking if CacheDebug exists
|
|
if extraFields.CacheDebug.CacheHit {
|
|
t.Error("❌ Response was cached when it shouldn't be")
|
|
return
|
|
}
|
|
|
|
t.Log("✅ Response correctly not served from cache (cache_debug present but CacheHit=false)")
|
|
}
|
|
|
|
// WaitForCache waits for async cache operations to complete
|
|
func WaitForCache(plugin schemas.LLMPlugin) {
|
|
if p, ok := plugin.(*Plugin); ok {
|
|
p.WaitForPendingOperations()
|
|
}
|
|
// Small buffer for Weaviate index consistency
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
|
|
// CreateEmbeddingRequest creates an embedding request for testing
|
|
func CreateEmbeddingRequest(texts []string) *schemas.BifrostEmbeddingRequest {
|
|
return &schemas.BifrostEmbeddingRequest{
|
|
Provider: schemas.OpenAI,
|
|
Model: "text-embedding-3-small",
|
|
Input: &schemas.EmbeddingInput{
|
|
Texts: texts,
|
|
},
|
|
}
|
|
}
|
|
|
|
// CreateBasicResponsesRequest creates a basic Responses API request for testing
|
|
func CreateBasicResponsesRequest(content string, temperature float64, maxTokens int) *schemas.BifrostResponsesRequest {
|
|
userRole := schemas.ResponsesInputMessageRoleUser
|
|
return &schemas.BifrostResponsesRequest{
|
|
Provider: schemas.OpenAI,
|
|
Model: "gpt-4o",
|
|
Input: []schemas.ResponsesMessage{
|
|
{
|
|
Role: &userRole,
|
|
Content: &schemas.ResponsesMessageContent{
|
|
ContentStr: &content,
|
|
},
|
|
},
|
|
},
|
|
Params: &schemas.ResponsesParameters{
|
|
Temperature: &temperature,
|
|
MaxOutputTokens: &maxTokens,
|
|
},
|
|
}
|
|
}
|
|
|
|
// CreateResponsesRequestWithTools creates a Responses API request with tools for testing
|
|
func CreateResponsesRequestWithTools(content string, temperature float64, maxTokens int, tools []schemas.ResponsesTool) *schemas.BifrostResponsesRequest {
|
|
req := CreateBasicResponsesRequest(content, temperature, maxTokens)
|
|
req.Params.Tools = tools
|
|
return req
|
|
}
|
|
|
|
// CreateResponsesRequestWithInstructions creates a Responses API request with system instructions
|
|
func CreateResponsesRequestWithInstructions(content string, instructions string, temperature float64, maxTokens int) *schemas.BifrostResponsesRequest {
|
|
req := CreateBasicResponsesRequest(content, temperature, maxTokens)
|
|
req.Params.Instructions = &instructions
|
|
return req
|
|
}
|
|
|
|
// CreateStreamingResponsesRequest creates a streaming Responses API request for testing
|
|
func CreateStreamingResponsesRequest(content string, temperature float64, maxTokens int) *schemas.BifrostResponsesRequest {
|
|
return CreateBasicResponsesRequest(content, temperature, maxTokens)
|
|
}
|
|
|
|
// CreateImageGenerationRequest creates an image generation request for testing
|
|
func CreateImageGenerationRequest(prompt string, size string, quality string) *schemas.BifrostImageGenerationRequest {
|
|
return &schemas.BifrostImageGenerationRequest{
|
|
Provider: schemas.OpenAI,
|
|
Model: "gpt-image-1",
|
|
Input: &schemas.ImageGenerationInput{
|
|
Prompt: prompt,
|
|
},
|
|
Params: &schemas.ImageGenerationParameters{
|
|
Size: bifrost.Ptr(size),
|
|
Quality: bifrost.Ptr(quality),
|
|
N: bifrost.Ptr(1),
|
|
},
|
|
}
|
|
}
|
|
|
|
// CreateContextWithCacheKey creates a context with the test cache key
|
|
func CreateContextWithCacheKey(value string) *schemas.BifrostContext {
|
|
return schemas.NewBifrostContextWithValue(context.Background(), schemas.NoDeadline, CacheKey, value)
|
|
}
|
|
|
|
// CreateContextWithCacheKeyAndType creates a context with cache key and cache type
|
|
func CreateContextWithCacheKeyAndType(value string, cacheType CacheType) *schemas.BifrostContext {
|
|
return schemas.NewBifrostContextWithValue(context.Background(), schemas.NoDeadline, CacheKey, value).WithValue(CacheTypeKey, cacheType)
|
|
}
|
|
|
|
// CreateContextWithCacheKeyAndTTL creates a context with cache key and custom TTL
|
|
func CreateContextWithCacheKeyAndTTL(value string, ttl time.Duration) *schemas.BifrostContext {
|
|
return schemas.NewBifrostContextWithValue(context.Background(), schemas.NoDeadline, CacheKey, value).WithValue(CacheTTLKey, ttl)
|
|
}
|
|
|
|
// CreateContextWithCacheKeyAndThreshold creates a context with cache key and custom threshold
|
|
func CreateContextWithCacheKeyAndThreshold(value string, threshold float64) *schemas.BifrostContext {
|
|
return schemas.NewBifrostContext(context.Background(), schemas.NoDeadline).WithValue(CacheKey, value).WithValue(CacheThresholdKey, threshold)
|
|
}
|
|
|
|
// CreateContextWithCacheKeyAndNoStore creates a context with cache key and no-store flag
|
|
func CreateContextWithCacheKeyAndNoStore(value string, noStore bool) *schemas.BifrostContext {
|
|
return schemas.NewBifrostContext(context.Background(), schemas.NoDeadline).WithValue(CacheKey, value).WithValue(CacheNoStoreKey, noStore)
|
|
}
|
|
|
|
// CreateTestSetupWithConversationThreshold creates a test setup with custom conversation history threshold
|
|
func CreateTestSetupWithConversationThreshold(t *testing.T, threshold int) *TestSetup {
|
|
config := &Config{
|
|
Provider: schemas.OpenAI,
|
|
EmbeddingModel: "text-embedding-3-small",
|
|
Dimension: 1536,
|
|
CleanUpOnShutdown: true,
|
|
Threshold: 0.8,
|
|
ConversationHistoryThreshold: threshold,
|
|
Keys: []schemas.Key{
|
|
{
|
|
Value: *schemas.NewEnvVar("env.OPENAI_API_KEY"),
|
|
Models: []string{"*"},
|
|
Weight: 1.0,
|
|
},
|
|
},
|
|
}
|
|
|
|
return NewTestSetupWithConfig(t, config)
|
|
}
|
|
|
|
// CreateTestSetupWithExcludeSystemPrompt creates a test setup with ExcludeSystemPrompt setting
|
|
func CreateTestSetupWithExcludeSystemPrompt(t *testing.T, excludeSystem bool) *TestSetup {
|
|
config := &Config{
|
|
Provider: schemas.OpenAI,
|
|
EmbeddingModel: "text-embedding-3-small",
|
|
Dimension: 1536,
|
|
CleanUpOnShutdown: true,
|
|
Threshold: 0.8,
|
|
ExcludeSystemPrompt: &excludeSystem,
|
|
Keys: []schemas.Key{
|
|
{
|
|
Value: *schemas.NewEnvVar("env.OPENAI_API_KEY"),
|
|
Models: []string{"*"},
|
|
Weight: 1.0,
|
|
},
|
|
},
|
|
}
|
|
|
|
return NewTestSetupWithConfig(t, config)
|
|
}
|
|
|
|
// CreateTestSetupWithThresholdAndExcludeSystem creates a test setup with both conversation threshold and exclude system prompt settings
|
|
func CreateTestSetupWithThresholdAndExcludeSystem(t *testing.T, threshold int, excludeSystem bool) *TestSetup {
|
|
config := &Config{
|
|
Provider: schemas.OpenAI,
|
|
EmbeddingModel: "text-embedding-3-small",
|
|
Dimension: 1536,
|
|
CleanUpOnShutdown: true,
|
|
Threshold: 0.8,
|
|
ConversationHistoryThreshold: threshold,
|
|
ExcludeSystemPrompt: &excludeSystem,
|
|
Keys: []schemas.Key{
|
|
{
|
|
Value: *schemas.NewEnvVar("env.OPENAI_API_KEY"),
|
|
Models: []string{"*"},
|
|
Weight: 1.0,
|
|
},
|
|
},
|
|
}
|
|
|
|
return NewTestSetupWithConfig(t, config)
|
|
}
|
|
|
|
// CreateConversationRequest creates a chat request with conversation history
|
|
func CreateConversationRequest(messages []schemas.ChatMessage, temperature float64, maxTokens int) *schemas.BifrostChatRequest {
|
|
return &schemas.BifrostChatRequest{
|
|
Provider: schemas.OpenAI,
|
|
Model: "gpt-4o-mini",
|
|
Input: messages,
|
|
Params: &schemas.ChatParameters{
|
|
Temperature: &temperature,
|
|
MaxCompletionTokens: &maxTokens,
|
|
},
|
|
}
|
|
}
|
|
|
|
// BuildConversationHistory creates a conversation history from pairs of user/assistant messages
|
|
func BuildConversationHistory(systemPrompt string, userAssistantPairs ...[]string) []schemas.ChatMessage {
|
|
messages := []schemas.ChatMessage{}
|
|
|
|
// Add system prompt if provided
|
|
if systemPrompt != "" {
|
|
messages = append(messages, schemas.ChatMessage{
|
|
Role: schemas.ChatMessageRoleSystem,
|
|
Content: &schemas.ChatMessageContent{
|
|
ContentStr: &systemPrompt,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Add user/assistant pairs
|
|
for _, pair := range userAssistantPairs {
|
|
if len(pair) >= 1 && pair[0] != "" {
|
|
userMsg := pair[0]
|
|
messages = append(messages, schemas.ChatMessage{
|
|
Role: schemas.ChatMessageRoleUser,
|
|
Content: &schemas.ChatMessageContent{
|
|
ContentStr: &userMsg,
|
|
},
|
|
})
|
|
}
|
|
if len(pair) >= 2 && pair[1] != "" {
|
|
assistantMsg := pair[1]
|
|
messages = append(messages, schemas.ChatMessage{
|
|
Role: schemas.ChatMessageRoleAssistant,
|
|
Content: &schemas.ChatMessageContent{
|
|
ContentStr: &assistantMsg,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
return messages
|
|
}
|
|
|
|
// AddUserMessage adds a user message to existing conversation
|
|
func AddUserMessage(messages []schemas.ChatMessage, userMessage string) []schemas.ChatMessage {
|
|
newMessage := schemas.ChatMessage{
|
|
Role: schemas.ChatMessageRoleUser,
|
|
Content: &schemas.ChatMessageContent{
|
|
ContentStr: &userMessage,
|
|
},
|
|
}
|
|
return append(messages, newMessage)
|
|
}
|
|
|
|
// RetryConfig defines retry configuration for API requests
|
|
type RetryConfig struct {
|
|
MaxRetries int
|
|
BaseDelay time.Duration
|
|
}
|
|
|
|
// DefaultRetryConfig returns the default retry configuration
|
|
func DefaultRetryConfig() RetryConfig {
|
|
return RetryConfig{
|
|
MaxRetries: 2,
|
|
BaseDelay: 5 * time.Millisecond,
|
|
}
|
|
}
|