216 lines
7.4 KiB
Go
216 lines
7.4 KiB
Go
/*
|
|
* Copyright 2026 CloudWeGo Authors
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package adk
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/cloudwego/eino/components/model"
|
|
"github.com/cloudwego/eino/schema"
|
|
)
|
|
|
|
func TestBuildModelWrappers_FailoverProxyInner(t *testing.T) {
|
|
base := &fakeChatModel{
|
|
callbacksEnabled: true,
|
|
generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) {
|
|
return schema.AssistantMessage("ok", nil), nil
|
|
},
|
|
stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) {
|
|
return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("ok", nil)}), nil
|
|
},
|
|
}
|
|
|
|
failoverCfg := &ModelFailoverConfig[*schema.Message]{
|
|
MaxRetries: 0,
|
|
ShouldFailover: func(context.Context, *schema.Message, error) bool { return false },
|
|
GetFailoverModel: func(_ context.Context, _ *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) {
|
|
return base, nil, nil
|
|
},
|
|
}
|
|
|
|
wrapped := buildModelWrappers[*schema.Message](base, &modelWrapperConfig{
|
|
failoverConfig: failoverCfg,
|
|
})
|
|
|
|
smw, ok := wrapped.(*stateModelWrapper)
|
|
require.True(t, ok)
|
|
_, ok = smw.inner.(*failoverProxyModel)
|
|
require.True(t, ok)
|
|
require.Same(t, base, smw.original)
|
|
require.Same(t, failoverCfg, smw.modelFailoverConfig)
|
|
}
|
|
|
|
func TestStateModelWrapper_Generate_WithFailover(t *testing.T) {
|
|
wantErr := errors.New("first failed")
|
|
var shouldCalls int32
|
|
var m1Calls int32
|
|
var m2Calls int32
|
|
|
|
m1 := &fakeChatModel{
|
|
callbacksEnabled: true,
|
|
generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) {
|
|
atomic.AddInt32(&m1Calls, 1)
|
|
return schema.AssistantMessage("partial", nil), wantErr
|
|
},
|
|
stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) {
|
|
return nil, errors.New("unused")
|
|
},
|
|
}
|
|
m2 := &fakeChatModel{
|
|
callbacksEnabled: true,
|
|
generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) {
|
|
atomic.AddInt32(&m2Calls, 1)
|
|
return schema.AssistantMessage("ok", nil), nil
|
|
},
|
|
stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) {
|
|
return nil, errors.New("unused")
|
|
},
|
|
}
|
|
|
|
failoverCfg := &ModelFailoverConfig[*schema.Message]{
|
|
MaxRetries: 1,
|
|
ShouldFailover: func(_ context.Context, out *schema.Message, err error) bool {
|
|
atomic.AddInt32(&shouldCalls, 1)
|
|
require.ErrorIs(t, err, wantErr)
|
|
require.NotNil(t, out)
|
|
require.Equal(t, "partial", out.Content)
|
|
return true
|
|
},
|
|
GetFailoverModel: func(_ context.Context, failoverCtx *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) {
|
|
require.Equal(t, uint(1), failoverCtx.FailoverAttempt)
|
|
return m2, nil, nil
|
|
},
|
|
}
|
|
|
|
wrapped := buildModelWrappers[*schema.Message](m1, &modelWrapperConfig{
|
|
failoverConfig: failoverCfg,
|
|
})
|
|
|
|
ctx := withTypedChatModelAgentExecCtx(context.Background(), &chatModelAgentExecCtx{
|
|
failoverLastSuccessModel: m1,
|
|
})
|
|
got, err := wrapped.Generate(ctx, []*schema.Message{schema.UserMessage("hi")})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, got)
|
|
require.Equal(t, "ok", got.Content)
|
|
require.Equal(t, int32(1), atomic.LoadInt32(&m1Calls))
|
|
require.Equal(t, int32(1), atomic.LoadInt32(&m2Calls))
|
|
require.Equal(t, int32(1), atomic.LoadInt32(&shouldCalls))
|
|
}
|
|
|
|
func TestStateModelWrapper_Stream_WithFailover(t *testing.T) {
|
|
streamErr := errors.New("mid error")
|
|
var shouldCalls int32
|
|
var m1Calls int32
|
|
var m2Calls int32
|
|
|
|
m1 := &fakeChatModel{
|
|
callbacksEnabled: true,
|
|
generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) {
|
|
return nil, errors.New("unused")
|
|
},
|
|
stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) {
|
|
atomic.AddInt32(&m1Calls, 1)
|
|
return streamWithMidError([]*schema.Message{
|
|
schema.AssistantMessage("p1", nil),
|
|
schema.AssistantMessage("p2", nil),
|
|
}, streamErr), nil
|
|
},
|
|
}
|
|
m2 := &fakeChatModel{
|
|
callbacksEnabled: true,
|
|
generate: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) {
|
|
return nil, errors.New("unused")
|
|
},
|
|
stream: func(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) {
|
|
atomic.AddInt32(&m2Calls, 1)
|
|
return schema.StreamReaderFromArray([]*schema.Message{schema.AssistantMessage("final", nil)}), nil
|
|
},
|
|
}
|
|
|
|
failoverCfg := &ModelFailoverConfig[*schema.Message]{
|
|
MaxRetries: 1,
|
|
ShouldFailover: func(_ context.Context, out *schema.Message, err error) bool {
|
|
atomic.AddInt32(&shouldCalls, 1)
|
|
require.ErrorIs(t, err, streamErr)
|
|
require.NotNil(t, out)
|
|
require.Equal(t, "p1p2", out.Content)
|
|
return true
|
|
},
|
|
GetFailoverModel: func(_ context.Context, failoverCtx *FailoverContext[*schema.Message]) (model.BaseChatModel, []*schema.Message, error) {
|
|
require.Equal(t, uint(1), failoverCtx.FailoverAttempt)
|
|
return m2, nil, nil
|
|
},
|
|
}
|
|
|
|
wrapped := buildModelWrappers[*schema.Message](m1, &modelWrapperConfig{
|
|
failoverConfig: failoverCfg,
|
|
})
|
|
|
|
ctx := withTypedChatModelAgentExecCtx(context.Background(), &chatModelAgentExecCtx{
|
|
failoverLastSuccessModel: m1,
|
|
})
|
|
sr, err := wrapped.Stream(ctx, []*schema.Message{schema.UserMessage("hi")})
|
|
require.NoError(t, err)
|
|
msgs, err := drainMessageStream(sr)
|
|
require.NoError(t, err)
|
|
require.Len(t, msgs, 1)
|
|
require.Equal(t, "final", msgs[0].Content)
|
|
require.Equal(t, int32(1), atomic.LoadInt32(&m1Calls))
|
|
require.Equal(t, int32(1), atomic.LoadInt32(&m2Calls))
|
|
require.Equal(t, int32(1), atomic.LoadInt32(&shouldCalls))
|
|
}
|
|
|
|
func TestFailoverAcceptsAgenticAgent(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
m := &mockAgenticModel{
|
|
generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) {
|
|
return agenticMsg("ok"), nil
|
|
},
|
|
}
|
|
|
|
fallbackModel := &mockAgenticModel{
|
|
generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) {
|
|
return agenticMsg("fallback"), nil
|
|
},
|
|
}
|
|
|
|
agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{
|
|
Name: "FailoverAgent",
|
|
Description: "Agent with failover config",
|
|
Model: m,
|
|
ModelFailoverConfig: &ModelFailoverConfig[*schema.AgenticMessage]{
|
|
MaxRetries: 1,
|
|
ShouldFailover: func(ctx context.Context, outputMessage *schema.AgenticMessage, outputErr error) bool {
|
|
return true
|
|
},
|
|
GetFailoverModel: func(ctx context.Context, failoverCtx *FailoverContext[*schema.AgenticMessage]) (model.BaseModel[*schema.AgenticMessage], []*schema.AgenticMessage, error) {
|
|
return fallbackModel, nil, nil
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, agent)
|
|
}
|