269 lines
8.2 KiB
Go
269 lines
8.2 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"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/cloudwego/eino/callbacks"
|
|
"github.com/cloudwego/eino/components/model"
|
|
"github.com/cloudwego/eino/schema"
|
|
)
|
|
|
|
type agenticCallbackRecorder struct {
|
|
mu sync.Mutex
|
|
onStartCalled bool
|
|
onEndCalled bool
|
|
runInfo *callbacks.RunInfo
|
|
inputReceived *TypedAgentCallbackInput[*schema.AgenticMessage]
|
|
eventsReceived []*TypedAgentEvent[*schema.AgenticMessage]
|
|
eventsDone chan struct{}
|
|
closeOnce sync.Once
|
|
}
|
|
|
|
func (r *agenticCallbackRecorder) getOnStartCalled() bool {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
return r.onStartCalled
|
|
}
|
|
|
|
func (r *agenticCallbackRecorder) getOnEndCalled() bool {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
return r.onEndCalled
|
|
}
|
|
|
|
func (r *agenticCallbackRecorder) getEventsReceived() []*TypedAgentEvent[*schema.AgenticMessage] {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
result := make([]*TypedAgentEvent[*schema.AgenticMessage], len(r.eventsReceived))
|
|
copy(result, r.eventsReceived)
|
|
return result
|
|
}
|
|
|
|
func newAgenticRecordingHandler(recorder *agenticCallbackRecorder) callbacks.Handler {
|
|
recorder.eventsDone = make(chan struct{})
|
|
return callbacks.NewHandlerBuilder().
|
|
OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context {
|
|
if info.Component != ComponentOfAgenticAgent {
|
|
return ctx
|
|
}
|
|
recorder.mu.Lock()
|
|
defer recorder.mu.Unlock()
|
|
recorder.onStartCalled = true
|
|
recorder.runInfo = info
|
|
if agentInput := ConvTypedCallbackInput[*schema.AgenticMessage](input); agentInput != nil {
|
|
recorder.inputReceived = agentInput
|
|
}
|
|
return ctx
|
|
}).
|
|
OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context {
|
|
if info.Component != ComponentOfAgenticAgent {
|
|
return ctx
|
|
}
|
|
recorder.mu.Lock()
|
|
recorder.onEndCalled = true
|
|
recorder.runInfo = info
|
|
recorder.mu.Unlock()
|
|
|
|
if agentOutput := ConvTypedCallbackOutput[*schema.AgenticMessage](output); agentOutput != nil {
|
|
if agentOutput.Events != nil {
|
|
go func() {
|
|
defer recorder.closeOnce.Do(func() { close(recorder.eventsDone) })
|
|
for {
|
|
event, ok := agentOutput.Events.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
recorder.mu.Lock()
|
|
recorder.eventsReceived = append(recorder.eventsReceived, event)
|
|
recorder.mu.Unlock()
|
|
}
|
|
}()
|
|
return ctx
|
|
}
|
|
}
|
|
recorder.closeOnce.Do(func() { close(recorder.eventsDone) })
|
|
return ctx
|
|
}).
|
|
Build()
|
|
}
|
|
|
|
func TestAgenticCallback(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
expectedContent := "This is the test response content"
|
|
m := &mockAgenticModel{
|
|
generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) {
|
|
return agenticMsg(expectedContent), nil
|
|
},
|
|
}
|
|
|
|
agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{
|
|
Name: "TestChatAgent",
|
|
Description: "Test chat agent",
|
|
Instruction: "You are a test agent",
|
|
Model: m,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
recorder := &agenticCallbackRecorder{}
|
|
handler := newAgenticRecordingHandler(recorder)
|
|
|
|
var agentEvents []*TypedAgentEvent[*schema.AgenticMessage]
|
|
runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{Agent: agent})
|
|
iter := runner.Query(ctx, "hello", WithCallbacks(handler))
|
|
for {
|
|
event, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
agentEvents = append(agentEvents, event)
|
|
}
|
|
|
|
<-recorder.eventsDone
|
|
assertAgenticEventRoleFields(t, agentEvents)
|
|
|
|
t.Run("OnStart_Invocation", func(t *testing.T) {
|
|
assert.True(t, recorder.getOnStartCalled(), "OnStart should be called")
|
|
require.NotNil(t, recorder.inputReceived, "Input should be received")
|
|
require.NotNil(t, recorder.inputReceived.Input, "AgentInput should be set")
|
|
assert.Len(t, recorder.inputReceived.Input.Messages, 1)
|
|
})
|
|
|
|
t.Run("OnEnd_Invocation", func(t *testing.T) {
|
|
assert.True(t, recorder.getOnEndCalled(), "OnEnd should be called")
|
|
assert.Len(t, recorder.getEventsReceived(), 1)
|
|
})
|
|
|
|
t.Run("RunInfo_Fields", func(t *testing.T) {
|
|
require.NotNil(t, recorder.runInfo)
|
|
assert.Equal(t, "TestChatAgent", recorder.runInfo.Name)
|
|
assert.Equal(t, ComponentOfAgenticAgent, recorder.runInfo.Component)
|
|
})
|
|
|
|
t.Run("Events_MatchAgentOutput", func(t *testing.T) {
|
|
require.NotEmpty(t, agentEvents, "Agent should emit events")
|
|
received := recorder.getEventsReceived()
|
|
require.NotEmpty(t, received, "Callback should receive events")
|
|
|
|
require.Len(t, received, 1, "Callback should receive exactly 1 event")
|
|
require.NotNil(t, received[0].Output)
|
|
require.NotNil(t, received[0].Output.MessageOutput)
|
|
require.NotNil(t, received[0].Output.MessageOutput.Message)
|
|
assert.Equal(t, expectedContent, agenticTextContent(received[0].Output.MessageOutput.Message))
|
|
})
|
|
}
|
|
|
|
func TestAgenticCallbackMultipleHandlers(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
m := &mockAgenticModel{
|
|
generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) {
|
|
return agenticMsg("test response"), nil
|
|
},
|
|
}
|
|
|
|
agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{
|
|
Name: "TestAgent",
|
|
Description: "Test agent",
|
|
Instruction: "You are a test agent",
|
|
Model: m,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
recorder1 := &agenticCallbackRecorder{}
|
|
recorder2 := &agenticCallbackRecorder{}
|
|
handler1 := newAgenticRecordingHandler(recorder1)
|
|
handler2 := newAgenticRecordingHandler(recorder2)
|
|
|
|
runner := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{Agent: agent})
|
|
iter := runner.Query(ctx, "hello", WithCallbacks(handler1, handler2))
|
|
for {
|
|
_, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
}
|
|
|
|
<-recorder1.eventsDone
|
|
<-recorder2.eventsDone
|
|
|
|
assert.True(t, recorder1.getOnStartCalled(), "Handler1 OnStart should be called")
|
|
assert.True(t, recorder2.getOnStartCalled(), "Handler2 OnStart should be called")
|
|
assert.True(t, recorder1.getOnEndCalled(), "Handler1 OnEnd should be called")
|
|
assert.True(t, recorder2.getOnEndCalled(), "Handler2 OnEnd should be called")
|
|
|
|
assert.NotEmpty(t, recorder1.getEventsReceived(), "Handler1 should receive events")
|
|
assert.NotEmpty(t, recorder2.getEventsReceived(), "Handler2 should receive events")
|
|
}
|
|
|
|
func TestCoverage_WrapAgenticIterWithOnEnd(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
var onEndCalled bool
|
|
handler := callbacks.NewHandlerBuilder().
|
|
OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context {
|
|
return ctx
|
|
}).
|
|
OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context {
|
|
if info.Component == ComponentOfAgenticAgent {
|
|
onEndCalled = true
|
|
}
|
|
return ctx
|
|
}).
|
|
Build()
|
|
|
|
ctx = initAgenticCallbacks(ctx, "test-agent", "ChatModel",
|
|
WithCallbacks(handler))
|
|
|
|
cbInput := &TypedAgentCallbackInput[*schema.AgenticMessage]{
|
|
Input: &TypedAgentInput[*schema.AgenticMessage]{
|
|
Messages: []*schema.AgenticMessage{schema.UserAgenticMessage("Hi")},
|
|
},
|
|
}
|
|
ctx = callbacks.OnStart(ctx, cbInput)
|
|
|
|
origIter, origGen := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]()
|
|
go func() {
|
|
defer origGen.Close()
|
|
origGen.Send(&TypedAgentEvent[*schema.AgenticMessage]{
|
|
Output: &TypedAgentOutput[*schema.AgenticMessage]{
|
|
MessageOutput: &TypedMessageVariant[*schema.AgenticMessage]{
|
|
Message: agenticMsg("done"),
|
|
},
|
|
},
|
|
})
|
|
}()
|
|
|
|
wrappedIter := wrapAgenticIterWithOnEnd(ctx, origIter)
|
|
|
|
for {
|
|
_, ok := wrappedIter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
}
|
|
|
|
assert.True(t, onEndCalled, "OnEnd callback should have been called")
|
|
}
|