2189 lines
76 KiB
Go
2189 lines
76 KiB
Go
/*
|
|
* Copyright 2025 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"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/cloudwego/eino/components/model"
|
|
"github.com/cloudwego/eino/components/tool"
|
|
"github.com/cloudwego/eino/compose"
|
|
"github.com/cloudwego/eino/schema"
|
|
)
|
|
|
|
type testEnhancedToolWrapperHandler struct {
|
|
*BaseChatModelAgentMiddleware
|
|
wrapEnhancedInvokableFn func(context.Context, EnhancedInvokableToolCallEndpoint, *ToolContext) EnhancedInvokableToolCallEndpoint
|
|
wrapEnhancedStreamableFn func(context.Context, EnhancedStreamableToolCallEndpoint, *ToolContext) EnhancedStreamableToolCallEndpoint
|
|
}
|
|
|
|
func (h *testEnhancedToolWrapperHandler) WrapEnhancedInvokableToolCall(ctx context.Context, endpoint EnhancedInvokableToolCallEndpoint, tCtx *ToolContext) (EnhancedInvokableToolCallEndpoint, error) {
|
|
if h.wrapEnhancedInvokableFn != nil {
|
|
return h.wrapEnhancedInvokableFn(ctx, endpoint, tCtx), nil
|
|
}
|
|
return endpoint, nil
|
|
}
|
|
|
|
func (h *testEnhancedToolWrapperHandler) WrapEnhancedStreamableToolCall(ctx context.Context, endpoint EnhancedStreamableToolCallEndpoint, tCtx *ToolContext) (EnhancedStreamableToolCallEndpoint, error) {
|
|
if h.wrapEnhancedStreamableFn != nil {
|
|
return h.wrapEnhancedStreamableFn(ctx, endpoint, tCtx), nil
|
|
}
|
|
return endpoint, nil
|
|
}
|
|
|
|
func newTestEnhancedInvokableToolCallWrapper(beforeFn, afterFn func()) func(context.Context, EnhancedInvokableToolCallEndpoint, *ToolContext) EnhancedInvokableToolCallEndpoint {
|
|
return func(_ context.Context, endpoint EnhancedInvokableToolCallEndpoint, _ *ToolContext) EnhancedInvokableToolCallEndpoint {
|
|
return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) {
|
|
if beforeFn != nil {
|
|
beforeFn()
|
|
}
|
|
result, err := endpoint(ctx, toolArgument, opts...)
|
|
if afterFn != nil {
|
|
afterFn()
|
|
}
|
|
return result, err
|
|
}
|
|
}
|
|
}
|
|
|
|
func newTestEnhancedStreamableToolCallWrapper(beforeFn, afterFn func()) func(context.Context, EnhancedStreamableToolCallEndpoint, *ToolContext) EnhancedStreamableToolCallEndpoint {
|
|
return func(_ context.Context, endpoint EnhancedStreamableToolCallEndpoint, _ *ToolContext) EnhancedStreamableToolCallEndpoint {
|
|
return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) {
|
|
if beforeFn != nil {
|
|
beforeFn()
|
|
}
|
|
result, err := endpoint(ctx, toolArgument, opts...)
|
|
if afterFn != nil {
|
|
afterFn()
|
|
}
|
|
return result, err
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHandlersToToolMiddlewaresEnhanced(t *testing.T) {
|
|
t.Run("OnlyEnhancedInvokableHandler", func(t *testing.T) {
|
|
var called bool
|
|
handlers := []ChatModelAgentMiddleware{
|
|
&testEnhancedToolWrapperHandler{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
wrapEnhancedInvokableFn: func(_ context.Context, endpoint EnhancedInvokableToolCallEndpoint, _ *ToolContext) EnhancedInvokableToolCallEndpoint {
|
|
return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) {
|
|
called = true
|
|
return endpoint(ctx, toolArgument, opts...)
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
middlewares := handlersToToolMiddlewares(handlers)
|
|
assert.Len(t, middlewares, 1)
|
|
assert.NotNil(t, middlewares[0].EnhancedInvokable)
|
|
assert.NotNil(t, middlewares[0].Invokable)
|
|
assert.NotNil(t, middlewares[0].Streamable)
|
|
assert.NotNil(t, middlewares[0].EnhancedStreamable)
|
|
|
|
mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedInvokableToolOutput, error) {
|
|
return &compose.EnhancedInvokableToolOutput{
|
|
Result: &schema.ToolResult{
|
|
Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "test"}},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
wrapped := middlewares[0].EnhancedInvokable(mockEndpoint)
|
|
_, err := wrapped(context.Background(), &compose.ToolInput{
|
|
Name: "test_tool",
|
|
CallID: "call-1",
|
|
Arguments: `{"input": "test"}`,
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.True(t, called)
|
|
})
|
|
|
|
t.Run("OnlyEnhancedStreamableHandler", func(t *testing.T) {
|
|
var called bool
|
|
handlers := []ChatModelAgentMiddleware{
|
|
&testEnhancedToolWrapperHandler{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
wrapEnhancedStreamableFn: func(_ context.Context, endpoint EnhancedStreamableToolCallEndpoint, _ *ToolContext) EnhancedStreamableToolCallEndpoint {
|
|
return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) {
|
|
called = true
|
|
return endpoint(ctx, toolArgument, opts...)
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
middlewares := handlersToToolMiddlewares(handlers)
|
|
assert.Len(t, middlewares, 1)
|
|
assert.NotNil(t, middlewares[0].EnhancedStreamable)
|
|
assert.NotNil(t, middlewares[0].Invokable)
|
|
assert.NotNil(t, middlewares[0].Streamable)
|
|
assert.NotNil(t, middlewares[0].EnhancedInvokable)
|
|
|
|
mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedStreamableToolOutput, error) {
|
|
return &compose.EnhancedStreamableToolOutput{
|
|
Result: schema.StreamReaderFromArray([]*schema.ToolResult{
|
|
{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "test"}}},
|
|
}),
|
|
}, nil
|
|
}
|
|
|
|
wrapped := middlewares[0].EnhancedStreamable(mockEndpoint)
|
|
_, err := wrapped(context.Background(), &compose.ToolInput{
|
|
Name: "test_tool",
|
|
CallID: "call-1",
|
|
Arguments: `{"input": "test"}`,
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.True(t, called)
|
|
})
|
|
|
|
t.Run("MixedHandlers", func(t *testing.T) {
|
|
var invokableCalled, streamableCalled, enhancedInvokableCalled, enhancedStreamableCalled bool
|
|
|
|
handlers := []ChatModelAgentMiddleware{
|
|
&testToolWrapperHandler{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
wrapInvokableFn: func(_ context.Context, endpoint InvokableToolCallEndpoint, _ *ToolContext) InvokableToolCallEndpoint {
|
|
return func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
|
invokableCalled = true
|
|
return endpoint(ctx, argumentsInJSON, opts...)
|
|
}
|
|
},
|
|
},
|
|
&testToolWrapperHandler{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
wrapStreamableFn: func(_ context.Context, endpoint StreamableToolCallEndpoint, _ *ToolContext) StreamableToolCallEndpoint {
|
|
return func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error) {
|
|
streamableCalled = true
|
|
return endpoint(ctx, argumentsInJSON, opts...)
|
|
}
|
|
},
|
|
},
|
|
&testEnhancedToolWrapperHandler{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
wrapEnhancedInvokableFn: func(_ context.Context, endpoint EnhancedInvokableToolCallEndpoint, _ *ToolContext) EnhancedInvokableToolCallEndpoint {
|
|
return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) {
|
|
enhancedInvokableCalled = true
|
|
return endpoint(ctx, toolArgument, opts...)
|
|
}
|
|
},
|
|
},
|
|
&testEnhancedToolWrapperHandler{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
wrapEnhancedStreamableFn: func(_ context.Context, endpoint EnhancedStreamableToolCallEndpoint, _ *ToolContext) EnhancedStreamableToolCallEndpoint {
|
|
return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) {
|
|
enhancedStreamableCalled = true
|
|
return endpoint(ctx, toolArgument, opts...)
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
middlewares := handlersToToolMiddlewares(handlers)
|
|
assert.Len(t, middlewares, 4)
|
|
|
|
invokableEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) {
|
|
return &compose.ToolOutput{Result: "test"}, nil
|
|
}
|
|
_, _ = middlewares[0].Invokable(invokableEndpoint)(context.Background(), &compose.ToolInput{Name: "test", CallID: "1", Arguments: "{}"})
|
|
|
|
streamableEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.StreamToolOutput, error) {
|
|
return &compose.StreamToolOutput{Result: schema.StreamReaderFromArray([]string{"test"})}, nil
|
|
}
|
|
_, _ = middlewares[1].Streamable(streamableEndpoint)(context.Background(), &compose.ToolInput{Name: "test", CallID: "1", Arguments: "{}"})
|
|
|
|
enhancedInvokableEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedInvokableToolOutput, error) {
|
|
return &compose.EnhancedInvokableToolOutput{Result: &schema.ToolResult{}}, nil
|
|
}
|
|
_, _ = middlewares[2].EnhancedInvokable(enhancedInvokableEndpoint)(context.Background(), &compose.ToolInput{Name: "test", CallID: "1", Arguments: "{}"})
|
|
|
|
enhancedStreamableEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedStreamableToolOutput, error) {
|
|
return &compose.EnhancedStreamableToolOutput{Result: schema.StreamReaderFromArray([]*schema.ToolResult{{}})}, nil
|
|
}
|
|
_, _ = middlewares[3].EnhancedStreamable(enhancedStreamableEndpoint)(context.Background(), &compose.ToolInput{Name: "test", CallID: "1", Arguments: "{}"})
|
|
|
|
assert.True(t, invokableCalled)
|
|
assert.True(t, streamableCalled)
|
|
assert.True(t, enhancedInvokableCalled)
|
|
assert.True(t, enhancedStreamableCalled)
|
|
})
|
|
|
|
t.Run("NoHandlers", func(t *testing.T) {
|
|
handlers := []ChatModelAgentMiddleware{}
|
|
middlewares := handlersToToolMiddlewares(handlers)
|
|
assert.Len(t, middlewares, 0)
|
|
})
|
|
|
|
t.Run("HandlerWithNoToolWrappers", func(t *testing.T) {
|
|
handlers := []ChatModelAgentMiddleware{
|
|
&BaseChatModelAgentMiddleware{},
|
|
}
|
|
middlewares := handlersToToolMiddlewares(handlers)
|
|
assert.Len(t, middlewares, 1)
|
|
})
|
|
|
|
t.Run("EnhancedInvokableToolCallErrorPropagation", func(t *testing.T) {
|
|
expectedErr := errors.New("test error")
|
|
handlers := []ChatModelAgentMiddleware{
|
|
&testEnhancedToolWrapperHandler{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
wrapEnhancedInvokableFn: func(_ context.Context, endpoint EnhancedInvokableToolCallEndpoint, _ *ToolContext) EnhancedInvokableToolCallEndpoint {
|
|
return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) {
|
|
return nil, expectedErr
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
middlewares := handlersToToolMiddlewares(handlers)
|
|
mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedInvokableToolOutput, error) {
|
|
return &compose.EnhancedInvokableToolOutput{Result: &schema.ToolResult{}}, nil
|
|
}
|
|
|
|
wrapped := middlewares[0].EnhancedInvokable(mockEndpoint)
|
|
_, err := wrapped(context.Background(), &compose.ToolInput{Name: "test", CallID: "1", Arguments: "{}"})
|
|
assert.Error(t, err)
|
|
assert.Equal(t, expectedErr, err)
|
|
})
|
|
|
|
t.Run("EnhancedStreamableToolCallErrorPropagation", func(t *testing.T) {
|
|
expectedErr := errors.New("test error")
|
|
handlers := []ChatModelAgentMiddleware{
|
|
&testEnhancedToolWrapperHandler{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
wrapEnhancedStreamableFn: func(_ context.Context, endpoint EnhancedStreamableToolCallEndpoint, _ *ToolContext) EnhancedStreamableToolCallEndpoint {
|
|
return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) {
|
|
return nil, expectedErr
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
middlewares := handlersToToolMiddlewares(handlers)
|
|
mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedStreamableToolOutput, error) {
|
|
return &compose.EnhancedStreamableToolOutput{Result: schema.StreamReaderFromArray([]*schema.ToolResult{})}, nil
|
|
}
|
|
|
|
wrapped := middlewares[0].EnhancedStreamable(mockEndpoint)
|
|
_, err := wrapped(context.Background(), &compose.ToolInput{Name: "test", CallID: "1", Arguments: "{}"})
|
|
assert.Error(t, err)
|
|
assert.Equal(t, expectedErr, err)
|
|
})
|
|
|
|
t.Run("MultipleEnhancedInvokableWrappers", func(t *testing.T) {
|
|
var executionOrder []string
|
|
var mu sync.Mutex
|
|
|
|
handlers := []ChatModelAgentMiddleware{
|
|
&testEnhancedToolWrapperHandler{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
wrapEnhancedInvokableFn: newTestEnhancedInvokableToolCallWrapper(
|
|
func() {
|
|
mu.Lock()
|
|
executionOrder = append(executionOrder, "handler1-before")
|
|
mu.Unlock()
|
|
},
|
|
func() {
|
|
mu.Lock()
|
|
executionOrder = append(executionOrder, "handler1-after")
|
|
mu.Unlock()
|
|
},
|
|
),
|
|
},
|
|
&testEnhancedToolWrapperHandler{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
wrapEnhancedInvokableFn: newTestEnhancedInvokableToolCallWrapper(
|
|
func() {
|
|
mu.Lock()
|
|
executionOrder = append(executionOrder, "handler2-before")
|
|
mu.Unlock()
|
|
},
|
|
func() {
|
|
mu.Lock()
|
|
executionOrder = append(executionOrder, "handler2-after")
|
|
mu.Unlock()
|
|
},
|
|
),
|
|
},
|
|
}
|
|
|
|
middlewares := handlersToToolMiddlewares(handlers)
|
|
assert.Len(t, middlewares, 2)
|
|
|
|
mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedInvokableToolOutput, error) {
|
|
return &compose.EnhancedInvokableToolOutput{Result: &schema.ToolResult{}}, nil
|
|
}
|
|
|
|
wrapped := middlewares[0].EnhancedInvokable(middlewares[1].EnhancedInvokable(mockEndpoint))
|
|
_, err := wrapped(context.Background(), &compose.ToolInput{Name: "test", CallID: "1", Arguments: "{}"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []string{"handler1-before", "handler2-before", "handler2-after", "handler1-after"}, executionOrder)
|
|
})
|
|
|
|
t.Run("MultipleEnhancedStreamableWrappers", func(t *testing.T) {
|
|
var executionOrder []string
|
|
var mu sync.Mutex
|
|
|
|
handlers := []ChatModelAgentMiddleware{
|
|
&testEnhancedToolWrapperHandler{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
wrapEnhancedStreamableFn: newTestEnhancedStreamableToolCallWrapper(
|
|
func() {
|
|
mu.Lock()
|
|
executionOrder = append(executionOrder, "handler1-before")
|
|
mu.Unlock()
|
|
},
|
|
func() {
|
|
mu.Lock()
|
|
executionOrder = append(executionOrder, "handler1-after")
|
|
mu.Unlock()
|
|
},
|
|
),
|
|
},
|
|
&testEnhancedToolWrapperHandler{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
wrapEnhancedStreamableFn: newTestEnhancedStreamableToolCallWrapper(
|
|
func() {
|
|
mu.Lock()
|
|
executionOrder = append(executionOrder, "handler2-before")
|
|
mu.Unlock()
|
|
},
|
|
func() {
|
|
mu.Lock()
|
|
executionOrder = append(executionOrder, "handler2-after")
|
|
mu.Unlock()
|
|
},
|
|
),
|
|
},
|
|
}
|
|
|
|
middlewares := handlersToToolMiddlewares(handlers)
|
|
assert.Len(t, middlewares, 2)
|
|
|
|
mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedStreamableToolOutput, error) {
|
|
return &compose.EnhancedStreamableToolOutput{Result: schema.StreamReaderFromArray([]*schema.ToolResult{{}})}, nil
|
|
}
|
|
|
|
wrapped := middlewares[0].EnhancedStreamable(middlewares[1].EnhancedStreamable(mockEndpoint))
|
|
_, err := wrapped(context.Background(), &compose.ToolInput{Name: "test", CallID: "1", Arguments: "{}"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []string{"handler1-before", "handler2-before", "handler2-after", "handler1-after"}, executionOrder)
|
|
})
|
|
}
|
|
|
|
func TestEnhancedToolContextPropagation(t *testing.T) {
|
|
t.Run("ToolContextContainsCorrectInfo", func(t *testing.T) {
|
|
var capturedCtx *ToolContext
|
|
handlers := []ChatModelAgentMiddleware{
|
|
&testEnhancedToolWrapperHandler{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
wrapEnhancedInvokableFn: func(_ context.Context, endpoint EnhancedInvokableToolCallEndpoint, tCtx *ToolContext) EnhancedInvokableToolCallEndpoint {
|
|
capturedCtx = tCtx
|
|
return endpoint
|
|
},
|
|
},
|
|
}
|
|
|
|
middlewares := handlersToToolMiddlewares(handlers)
|
|
mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedInvokableToolOutput, error) {
|
|
return &compose.EnhancedInvokableToolOutput{Result: &schema.ToolResult{}}, nil
|
|
}
|
|
|
|
wrapped := middlewares[0].EnhancedInvokable(mockEndpoint)
|
|
_, _ = wrapped(context.Background(), &compose.ToolInput{
|
|
Name: "my_tool",
|
|
CallID: "call-123",
|
|
Arguments: `{"key": "value"}`,
|
|
})
|
|
|
|
assert.NotNil(t, capturedCtx)
|
|
assert.Equal(t, "my_tool", capturedCtx.Name)
|
|
assert.Equal(t, "call-123", capturedCtx.CallID)
|
|
})
|
|
|
|
t.Run("StreamableToolContextContainsCorrectInfo", func(t *testing.T) {
|
|
var capturedCtx *ToolContext
|
|
handlers := []ChatModelAgentMiddleware{
|
|
&testEnhancedToolWrapperHandler{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
wrapEnhancedStreamableFn: func(_ context.Context, endpoint EnhancedStreamableToolCallEndpoint, tCtx *ToolContext) EnhancedStreamableToolCallEndpoint {
|
|
capturedCtx = tCtx
|
|
return endpoint
|
|
},
|
|
},
|
|
}
|
|
|
|
middlewares := handlersToToolMiddlewares(handlers)
|
|
mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedStreamableToolOutput, error) {
|
|
return &compose.EnhancedStreamableToolOutput{Result: schema.StreamReaderFromArray([]*schema.ToolResult{{}})}, nil
|
|
}
|
|
|
|
wrapped := middlewares[0].EnhancedStreamable(mockEndpoint)
|
|
_, _ = wrapped(context.Background(), &compose.ToolInput{
|
|
Name: "stream_tool",
|
|
CallID: "call-456",
|
|
Arguments: `{"data": "test"}`,
|
|
})
|
|
|
|
assert.NotNil(t, capturedCtx)
|
|
assert.Equal(t, "stream_tool", capturedCtx.Name)
|
|
assert.Equal(t, "call-456", capturedCtx.CallID)
|
|
})
|
|
}
|
|
|
|
func TestBaseChatModelAgentMiddlewareEnhancedDefaults(t *testing.T) {
|
|
t.Run("DefaultEnhancedInvokableReturnsEndpoint", func(t *testing.T) {
|
|
base := &BaseChatModelAgentMiddleware{}
|
|
|
|
var called bool
|
|
endpoint := func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) {
|
|
called = true
|
|
return &schema.ToolResult{}, nil
|
|
}
|
|
|
|
wrapped, wrapErr := base.WrapEnhancedInvokableToolCall(context.Background(), endpoint, &ToolContext{Name: "test", CallID: "1"})
|
|
assert.NoError(t, wrapErr)
|
|
_, err := wrapped(context.Background(), &schema.ToolArgument{Text: "{}"})
|
|
|
|
assert.NoError(t, err)
|
|
assert.True(t, called)
|
|
})
|
|
|
|
t.Run("DefaultEnhancedStreamableReturnsEndpoint", func(t *testing.T) {
|
|
base := &BaseChatModelAgentMiddleware{}
|
|
|
|
var called bool
|
|
endpoint := func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) {
|
|
called = true
|
|
return schema.StreamReaderFromArray([]*schema.ToolResult{}), nil
|
|
}
|
|
|
|
wrapped, wrapErr := base.WrapEnhancedStreamableToolCall(context.Background(), endpoint, &ToolContext{Name: "test", CallID: "1"})
|
|
assert.NoError(t, wrapErr)
|
|
_, err := wrapped(context.Background(), &schema.ToolArgument{Text: "{}"})
|
|
|
|
assert.NoError(t, err)
|
|
assert.True(t, called)
|
|
})
|
|
}
|
|
|
|
func TestEnhancedToolArgumentsPropagation(t *testing.T) {
|
|
t.Run("ArgumentsPassedCorrectly", func(t *testing.T) {
|
|
var capturedArgs string
|
|
handlers := []ChatModelAgentMiddleware{
|
|
&testEnhancedToolWrapperHandler{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
wrapEnhancedInvokableFn: func(_ context.Context, endpoint EnhancedInvokableToolCallEndpoint, _ *ToolContext) EnhancedInvokableToolCallEndpoint {
|
|
return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) {
|
|
capturedArgs = toolArgument.Text
|
|
return endpoint(ctx, toolArgument, opts...)
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
middlewares := handlersToToolMiddlewares(handlers)
|
|
mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedInvokableToolOutput, error) {
|
|
return &compose.EnhancedInvokableToolOutput{Result: &schema.ToolResult{}}, nil
|
|
}
|
|
|
|
wrapped := middlewares[0].EnhancedInvokable(mockEndpoint)
|
|
_, _ = wrapped(context.Background(), &compose.ToolInput{
|
|
Name: "test_tool",
|
|
CallID: "call-1",
|
|
Arguments: `{"name": "test", "value": 123}`,
|
|
})
|
|
|
|
assert.Equal(t, `{"name": "test", "value": 123}`, capturedArgs)
|
|
})
|
|
}
|
|
|
|
func TestEnhancedToolResultPropagation(t *testing.T) {
|
|
t.Run("ResultPassedThroughMiddleware", func(t *testing.T) {
|
|
expectedResult := &schema.ToolResult{
|
|
Parts: []schema.ToolOutputPart{
|
|
{Type: schema.ToolPartTypeText, Text: "original result"},
|
|
},
|
|
}
|
|
|
|
var capturedResult *schema.ToolResult
|
|
handlers := []ChatModelAgentMiddleware{
|
|
&testEnhancedToolWrapperHandler{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
wrapEnhancedInvokableFn: func(_ context.Context, endpoint EnhancedInvokableToolCallEndpoint, _ *ToolContext) EnhancedInvokableToolCallEndpoint {
|
|
return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) {
|
|
result, err := endpoint(ctx, toolArgument, opts...)
|
|
capturedResult = result
|
|
return result, err
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
middlewares := handlersToToolMiddlewares(handlers)
|
|
mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedInvokableToolOutput, error) {
|
|
return &compose.EnhancedInvokableToolOutput{Result: expectedResult}, nil
|
|
}
|
|
|
|
wrapped := middlewares[0].EnhancedInvokable(mockEndpoint)
|
|
output, err := wrapped(context.Background(), &compose.ToolInput{Name: "test", CallID: "1", Arguments: "{}"})
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, expectedResult, capturedResult)
|
|
assert.Equal(t, expectedResult, output.Result)
|
|
})
|
|
|
|
t.Run("ModifiedResultPropagated", func(t *testing.T) {
|
|
modifiedResult := &schema.ToolResult{
|
|
Parts: []schema.ToolOutputPart{
|
|
{Type: schema.ToolPartTypeText, Text: "modified result"},
|
|
},
|
|
}
|
|
|
|
handlers := []ChatModelAgentMiddleware{
|
|
&testEnhancedToolWrapperHandler{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
wrapEnhancedInvokableFn: func(_ context.Context, endpoint EnhancedInvokableToolCallEndpoint, _ *ToolContext) EnhancedInvokableToolCallEndpoint {
|
|
return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) {
|
|
_, err := endpoint(ctx, toolArgument, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return modifiedResult, nil
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
middlewares := handlersToToolMiddlewares(handlers)
|
|
mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedInvokableToolOutput, error) {
|
|
return &compose.EnhancedInvokableToolOutput{Result: &schema.ToolResult{
|
|
Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "original"}},
|
|
}}, nil
|
|
}
|
|
|
|
wrapped := middlewares[0].EnhancedInvokable(mockEndpoint)
|
|
output, err := wrapped(context.Background(), &compose.ToolInput{Name: "test", CallID: "1", Arguments: "{}"})
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, modifiedResult, output.Result)
|
|
assert.Equal(t, "modified result", output.Result.Parts[0].Text)
|
|
})
|
|
}
|
|
|
|
func TestEnhancedToolEndpointErrorFromNext(t *testing.T) {
|
|
t.Run("EnhancedInvokableNextError", func(t *testing.T) {
|
|
expectedErr := errors.New("next endpoint error")
|
|
handlers := []ChatModelAgentMiddleware{
|
|
&testEnhancedToolWrapperHandler{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
wrapEnhancedInvokableFn: func(_ context.Context, endpoint EnhancedInvokableToolCallEndpoint, _ *ToolContext) EnhancedInvokableToolCallEndpoint {
|
|
return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) {
|
|
return endpoint(ctx, toolArgument, opts...)
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
middlewares := handlersToToolMiddlewares(handlers)
|
|
mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedInvokableToolOutput, error) {
|
|
return nil, expectedErr
|
|
}
|
|
|
|
wrapped := middlewares[0].EnhancedInvokable(mockEndpoint)
|
|
_, err := wrapped(context.Background(), &compose.ToolInput{Name: "test", CallID: "1", Arguments: "{}"})
|
|
|
|
assert.Error(t, err)
|
|
assert.Equal(t, expectedErr, err)
|
|
})
|
|
|
|
t.Run("EnhancedStreamableNextError", func(t *testing.T) {
|
|
expectedErr := errors.New("next endpoint error")
|
|
handlers := []ChatModelAgentMiddleware{
|
|
&testEnhancedToolWrapperHandler{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
wrapEnhancedStreamableFn: func(_ context.Context, endpoint EnhancedStreamableToolCallEndpoint, _ *ToolContext) EnhancedStreamableToolCallEndpoint {
|
|
return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) {
|
|
return endpoint(ctx, toolArgument, opts...)
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
middlewares := handlersToToolMiddlewares(handlers)
|
|
mockEndpoint := func(ctx context.Context, input *compose.ToolInput) (*compose.EnhancedStreamableToolOutput, error) {
|
|
return nil, expectedErr
|
|
}
|
|
|
|
wrapped := middlewares[0].EnhancedStreamable(mockEndpoint)
|
|
_, err := wrapped(context.Background(), &compose.ToolInput{Name: "test", CallID: "1", Arguments: "{}"})
|
|
|
|
assert.Error(t, err)
|
|
assert.Equal(t, expectedErr, err)
|
|
})
|
|
}
|
|
|
|
func TestWrapModelStreamChunksPreserved(t *testing.T) {
|
|
t.Run("AgentEventMessageStreamShouldPreserveChunksWithNoopWrapModel", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
chunk1 := schema.AssistantMessage("Hello ", nil)
|
|
chunk2 := schema.AssistantMessage("World", nil)
|
|
|
|
mockModel := &mockStreamingModel{
|
|
chunks: []*schema.Message{chunk1, chunk2},
|
|
}
|
|
|
|
noopWrapModelHandler := &testModelWrapperHandler{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
fn: func(_ context.Context, m model.BaseChatModel, _ *ModelContext) model.BaseChatModel {
|
|
return m
|
|
},
|
|
}
|
|
|
|
agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
|
|
Name: "TestAgent",
|
|
Description: "Test agent",
|
|
Model: mockModel,
|
|
Handlers: []ChatModelAgentMiddleware{noopWrapModelHandler},
|
|
ModelRetryConfig: &ModelRetryConfig{
|
|
MaxRetries: 3,
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
r := NewRunner(ctx, RunnerConfig{
|
|
Agent: agent,
|
|
EnableStreaming: true,
|
|
})
|
|
iter := r.Run(ctx, []Message{schema.UserMessage("test")})
|
|
|
|
var streamingEvents []*AgentEvent
|
|
for {
|
|
event, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
if event.Output != nil && event.Output.MessageOutput != nil &&
|
|
event.Output.MessageOutput.IsStreaming &&
|
|
event.Output.MessageOutput.Role == schema.Assistant {
|
|
streamingEvents = append(streamingEvents, event)
|
|
}
|
|
}
|
|
|
|
assert.GreaterOrEqual(t, len(streamingEvents), 1, "Should have at least one streaming event")
|
|
|
|
if len(streamingEvents) > 0 {
|
|
event := streamingEvents[0]
|
|
assert.NotNil(t, event.Output.MessageOutput.MessageStream, "Event should have message stream")
|
|
|
|
var receivedChunks []*schema.Message
|
|
for {
|
|
chunk, recvErr := event.Output.MessageOutput.MessageStream.Recv()
|
|
if recvErr != nil {
|
|
break
|
|
}
|
|
receivedChunks = append(receivedChunks, chunk)
|
|
}
|
|
|
|
assert.Equal(t, 2, len(receivedChunks),
|
|
"AgentEvent's MessageStream should contain 2 separate chunks, not 1 concatenated chunk. "+
|
|
"Got %d chunks instead. This indicates the stream is being concatenated before being sent to AgentEvent.",
|
|
len(receivedChunks))
|
|
|
|
if len(receivedChunks) >= 2 {
|
|
assert.Equal(t, "Hello ", receivedChunks[0].Content, "First chunk content should be preserved")
|
|
assert.Equal(t, "World", receivedChunks[1].Content, "Second chunk content should be preserved")
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("AgentEventMessageStreamShouldReflectUserMiddlewareModifications", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
chunk1 := schema.AssistantMessage("Hello ", nil)
|
|
chunk2 := schema.AssistantMessage("World", nil)
|
|
|
|
mockModel := &mockStreamingModel{
|
|
chunks: []*schema.Message{chunk1, chunk2},
|
|
}
|
|
|
|
streamConsumingWrapModelHandler := &testModelWrapperHandler{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
fn: func(_ context.Context, m model.BaseChatModel, _ *ModelContext) model.BaseChatModel {
|
|
return &streamConsumingModelWrapper{inner: m}
|
|
},
|
|
}
|
|
|
|
agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
|
|
Name: "TestAgent",
|
|
Description: "Test agent",
|
|
Model: mockModel,
|
|
Handlers: []ChatModelAgentMiddleware{streamConsumingWrapModelHandler},
|
|
ModelRetryConfig: &ModelRetryConfig{
|
|
MaxRetries: 3,
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
r := NewRunner(ctx, RunnerConfig{
|
|
Agent: agent,
|
|
EnableStreaming: true,
|
|
})
|
|
iter := r.Run(ctx, []Message{schema.UserMessage("test")})
|
|
|
|
var streamingEvents []*AgentEvent
|
|
for {
|
|
event, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
if event.Output != nil && event.Output.MessageOutput != nil &&
|
|
event.Output.MessageOutput.IsStreaming &&
|
|
event.Output.MessageOutput.Role == schema.Assistant {
|
|
streamingEvents = append(streamingEvents, event)
|
|
}
|
|
}
|
|
|
|
assert.GreaterOrEqual(t, len(streamingEvents), 1, "Should have at least one streaming event")
|
|
|
|
if len(streamingEvents) > 0 {
|
|
event := streamingEvents[0]
|
|
assert.NotNil(t, event.Output.MessageOutput.MessageStream, "Event should have message stream")
|
|
|
|
var receivedChunks []*schema.Message
|
|
for {
|
|
chunk, recvErr := event.Output.MessageOutput.MessageStream.Recv()
|
|
if recvErr != nil {
|
|
break
|
|
}
|
|
receivedChunks = append(receivedChunks, chunk)
|
|
}
|
|
|
|
assert.Equal(t, 1, len(receivedChunks),
|
|
"AgentEvent's MessageStream should contain 1 concatenated chunk (modified by user middleware). "+
|
|
"Got %d chunks instead.",
|
|
len(receivedChunks))
|
|
|
|
if len(receivedChunks) >= 1 {
|
|
assert.Equal(t, "Hello World", receivedChunks[0].Content, "Chunk content should be concatenated by user middleware")
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("AgentEventMessageStreamShouldReflectMultipleUserMiddlewareModifications", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
chunk1 := schema.AssistantMessage("Hello ", nil)
|
|
chunk2 := schema.AssistantMessage("World", nil)
|
|
|
|
mockModel := &mockStreamingModel{
|
|
chunks: []*schema.Message{chunk1, chunk2},
|
|
}
|
|
|
|
handler1 := &testModelWrapperHandler{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
fn: func(_ context.Context, m model.BaseChatModel, _ *ModelContext) model.BaseChatModel {
|
|
return &streamConsumingModelWrapper{inner: m}
|
|
},
|
|
}
|
|
|
|
handler2 := &testModelWrapperHandler{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
fn: func(_ context.Context, m model.BaseChatModel, _ *ModelContext) model.BaseChatModel {
|
|
return m
|
|
},
|
|
}
|
|
|
|
agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
|
|
Name: "TestAgent",
|
|
Description: "Test agent",
|
|
Model: mockModel,
|
|
Handlers: []ChatModelAgentMiddleware{handler1, handler2},
|
|
ModelRetryConfig: &ModelRetryConfig{
|
|
MaxRetries: 3,
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
r := NewRunner(ctx, RunnerConfig{
|
|
Agent: agent,
|
|
EnableStreaming: true,
|
|
})
|
|
iter := r.Run(ctx, []Message{schema.UserMessage("test")})
|
|
|
|
var streamingEvents []*AgentEvent
|
|
for {
|
|
event, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
if event.Output != nil && event.Output.MessageOutput != nil &&
|
|
event.Output.MessageOutput.IsStreaming &&
|
|
event.Output.MessageOutput.Role == schema.Assistant {
|
|
streamingEvents = append(streamingEvents, event)
|
|
}
|
|
}
|
|
|
|
assert.GreaterOrEqual(t, len(streamingEvents), 1, "Should have at least one streaming event")
|
|
|
|
if len(streamingEvents) > 0 {
|
|
event := streamingEvents[0]
|
|
assert.NotNil(t, event.Output.MessageOutput.MessageStream, "Event should have message stream")
|
|
|
|
var receivedChunks []*schema.Message
|
|
for {
|
|
chunk, recvErr := event.Output.MessageOutput.MessageStream.Recv()
|
|
if recvErr != nil {
|
|
break
|
|
}
|
|
receivedChunks = append(receivedChunks, chunk)
|
|
}
|
|
|
|
assert.Equal(t, 1, len(receivedChunks),
|
|
"AgentEvent's MessageStream should contain 1 concatenated chunk (modified by user middleware). "+
|
|
"Got %d chunks instead.",
|
|
len(receivedChunks))
|
|
|
|
if len(receivedChunks) >= 1 {
|
|
assert.Equal(t, "Hello World", receivedChunks[0].Content, "Chunk content should be concatenated by user middleware")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
type mockStreamingModel struct {
|
|
chunks []*schema.Message
|
|
}
|
|
|
|
func (m *mockStreamingModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) {
|
|
return schema.ConcatMessages(m.chunks)
|
|
}
|
|
|
|
func (m *mockStreamingModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) {
|
|
sr, sw := schema.Pipe[*schema.Message](len(m.chunks))
|
|
go func() {
|
|
defer sw.Close()
|
|
for _, chunk := range m.chunks {
|
|
sw.Send(chunk, nil)
|
|
}
|
|
}()
|
|
return sr, nil
|
|
}
|
|
|
|
type streamConsumingModelWrapper struct {
|
|
inner model.BaseChatModel
|
|
}
|
|
|
|
func (m *streamConsumingModelWrapper) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) {
|
|
return m.inner.Generate(ctx, input, opts...)
|
|
}
|
|
|
|
func (m *streamConsumingModelWrapper) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) {
|
|
stream, err := m.inner.Stream(ctx, input, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result, err := schema.ConcatMessageStream(stream)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return schema.StreamReaderFromArray([]*schema.Message{result}), nil
|
|
}
|
|
|
|
func TestEventSenderModelWrapperCustomPosition(t *testing.T) {
|
|
t.Run("UserConfiguredEventSenderSkipsDefaultEventSender", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
chunk1 := schema.AssistantMessage("Hello ", nil)
|
|
chunk2 := schema.AssistantMessage("World", nil)
|
|
|
|
mockModel := &mockStreamingModel{
|
|
chunks: []*schema.Message{chunk1, chunk2},
|
|
}
|
|
|
|
agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
|
|
Name: "TestAgent",
|
|
Description: "Test agent",
|
|
Model: mockModel,
|
|
Handlers: []ChatModelAgentMiddleware{NewEventSenderModelWrapper()},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
r := NewRunner(ctx, RunnerConfig{
|
|
Agent: agent,
|
|
EnableStreaming: true,
|
|
})
|
|
iter := r.Run(ctx, []Message{schema.UserMessage("test")})
|
|
|
|
var streamingEvents []*AgentEvent
|
|
for {
|
|
event, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
if event.Output != nil && event.Output.MessageOutput != nil &&
|
|
event.Output.MessageOutput.IsStreaming &&
|
|
event.Output.MessageOutput.Role == schema.Assistant {
|
|
streamingEvents = append(streamingEvents, event)
|
|
}
|
|
}
|
|
|
|
assert.Equal(t, 1, len(streamingEvents), "Should have exactly one streaming event (no duplicate from default event sender)")
|
|
})
|
|
|
|
t.Run("EventSenderAfterUserMiddlewareByDefault", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
mockModel := &mockStreamingModel{
|
|
chunks: []*schema.Message{
|
|
schema.AssistantMessage("Original", nil),
|
|
},
|
|
}
|
|
|
|
modifiedContent := "Modified"
|
|
contentModifyingHandler := &testModelWrapperHandler{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
fn: func(_ context.Context, m model.BaseChatModel, _ *ModelContext) model.BaseChatModel {
|
|
return &contentModifyingModelWrapper{inner: m, newContent: modifiedContent}
|
|
},
|
|
}
|
|
|
|
agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
|
|
Name: "TestAgent",
|
|
Description: "Test agent",
|
|
Model: mockModel,
|
|
Handlers: []ChatModelAgentMiddleware{contentModifyingHandler},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
r := NewRunner(ctx, RunnerConfig{
|
|
Agent: agent,
|
|
EnableStreaming: false,
|
|
})
|
|
iter := r.Run(ctx, []Message{schema.UserMessage("test")})
|
|
|
|
var assistantEvents []*AgentEvent
|
|
for {
|
|
event, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
if event.Output != nil && event.Output.MessageOutput != nil &&
|
|
event.Output.MessageOutput.Role == schema.Assistant {
|
|
assistantEvents = append(assistantEvents, event)
|
|
}
|
|
}
|
|
|
|
assert.GreaterOrEqual(t, len(assistantEvents), 1, "Should have at least one assistant event")
|
|
if len(assistantEvents) > 0 {
|
|
msg := assistantEvents[0].Output.MessageOutput.Message
|
|
assert.Equal(t, modifiedContent, msg.Content, "Event should contain modified content from user middleware")
|
|
}
|
|
})
|
|
|
|
t.Run("EventSenderInnermostGetsOriginalOutput", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
originalContent := "Original"
|
|
mockModel := &mockStreamingModel{
|
|
chunks: []*schema.Message{
|
|
schema.AssistantMessage(originalContent, nil),
|
|
},
|
|
}
|
|
|
|
modifiedContent := "Modified"
|
|
contentModifyingHandler := &testModelWrapperHandler{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
fn: func(_ context.Context, m model.BaseChatModel, _ *ModelContext) model.BaseChatModel {
|
|
return &contentModifyingModelWrapper{inner: m, newContent: modifiedContent}
|
|
},
|
|
}
|
|
|
|
agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
|
|
Name: "TestAgent",
|
|
Description: "Test agent",
|
|
Model: mockModel,
|
|
Handlers: []ChatModelAgentMiddleware{
|
|
contentModifyingHandler,
|
|
NewEventSenderModelWrapper(),
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
r := NewRunner(ctx, RunnerConfig{
|
|
Agent: agent,
|
|
EnableStreaming: false,
|
|
})
|
|
iter := r.Run(ctx, []Message{schema.UserMessage("test")})
|
|
|
|
var assistantEvents []*AgentEvent
|
|
for {
|
|
event, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
if event.Output != nil && event.Output.MessageOutput != nil &&
|
|
event.Output.MessageOutput.Role == schema.Assistant {
|
|
assistantEvents = append(assistantEvents, event)
|
|
}
|
|
}
|
|
|
|
assert.GreaterOrEqual(t, len(assistantEvents), 1, "Should have at least one assistant event")
|
|
if len(assistantEvents) > 0 {
|
|
msg := assistantEvents[0].Output.MessageOutput.Message
|
|
assert.Equal(t, originalContent, msg.Content, "Event should contain original content (EventSenderModelWrapper is innermost)")
|
|
}
|
|
})
|
|
}
|
|
|
|
type contentModifyingModelWrapper struct {
|
|
inner model.BaseChatModel
|
|
newContent string
|
|
}
|
|
|
|
func (m *contentModifyingModelWrapper) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) {
|
|
result, err := m.inner.Generate(ctx, input, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result.Content = m.newContent
|
|
return result, nil
|
|
}
|
|
|
|
func (m *contentModifyingModelWrapper) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) {
|
|
stream, err := m.inner.Stream(ctx, input, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result, err := schema.ConcatMessageStream(stream)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result.Content = m.newContent
|
|
return schema.StreamReaderFromArray([]*schema.Message{result}), nil
|
|
}
|
|
|
|
type mockToolCallingModel struct {
|
|
mu sync.Mutex
|
|
generateCalls int
|
|
toolCallName string
|
|
}
|
|
|
|
func (m *mockToolCallingModel) Generate(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) {
|
|
m.mu.Lock()
|
|
m.generateCalls++
|
|
calls := m.generateCalls
|
|
m.mu.Unlock()
|
|
if calls == 1 {
|
|
return schema.AssistantMessage("calling tool", []schema.ToolCall{
|
|
{ID: "tc-1", Function: schema.FunctionCall{Name: m.toolCallName, Arguments: `{"input":"test"}`}},
|
|
}), nil
|
|
}
|
|
return schema.AssistantMessage("done", nil), nil
|
|
}
|
|
|
|
func (m *mockToolCallingModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) {
|
|
msg, err := m.Generate(ctx, input, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return schema.StreamReaderFromArray([]*schema.Message{msg}), nil
|
|
}
|
|
|
|
func (m *mockToolCallingModel) WithTools(_ []*schema.ToolInfo) (model.ToolCallingChatModel, error) {
|
|
return m, nil
|
|
}
|
|
|
|
type invokableTestTool struct {
|
|
name string
|
|
result string
|
|
}
|
|
|
|
func (t *invokableTestTool) Info(_ context.Context) (*schema.ToolInfo, error) {
|
|
return &schema.ToolInfo{
|
|
Name: t.name,
|
|
Desc: "test tool",
|
|
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
|
"input": {Desc: "input", Required: true, Type: schema.String},
|
|
}),
|
|
}, nil
|
|
}
|
|
|
|
func (t *invokableTestTool) InvokableRun(_ context.Context, _ string, _ ...tool.Option) (string, error) {
|
|
return t.result, nil
|
|
}
|
|
|
|
type streamableTestTool struct {
|
|
name string
|
|
result string
|
|
}
|
|
|
|
func (t *streamableTestTool) Info(_ context.Context) (*schema.ToolInfo, error) {
|
|
return &schema.ToolInfo{
|
|
Name: t.name,
|
|
Desc: "test tool",
|
|
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
|
"input": {Desc: "input", Required: true, Type: schema.String},
|
|
}),
|
|
}, nil
|
|
}
|
|
|
|
func (t *streamableTestTool) StreamableRun(_ context.Context, _ string, _ ...tool.Option) (*schema.StreamReader[string], error) {
|
|
return schema.StreamReaderFromArray([]string{t.result}), nil
|
|
}
|
|
|
|
type enhancedInvokableTestTool struct {
|
|
name string
|
|
result string
|
|
}
|
|
|
|
func (t *enhancedInvokableTestTool) Info(_ context.Context) (*schema.ToolInfo, error) {
|
|
return &schema.ToolInfo{
|
|
Name: t.name,
|
|
Desc: "test tool",
|
|
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
|
"input": {Desc: "input", Required: true, Type: schema.String},
|
|
}),
|
|
}, nil
|
|
}
|
|
|
|
func (t *enhancedInvokableTestTool) InvokableRun(_ context.Context, _ *schema.ToolArgument, _ ...tool.Option) (*schema.ToolResult, error) {
|
|
return &schema.ToolResult{
|
|
Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: t.result}},
|
|
}, nil
|
|
}
|
|
|
|
type enhancedStreamableTestTool struct {
|
|
name string
|
|
result string
|
|
}
|
|
|
|
func (t *enhancedStreamableTestTool) Info(_ context.Context) (*schema.ToolInfo, error) {
|
|
return &schema.ToolInfo{
|
|
Name: t.name,
|
|
Desc: "test tool",
|
|
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
|
"input": {Desc: "input", Required: true, Type: schema.String},
|
|
}),
|
|
}, nil
|
|
}
|
|
|
|
func (t *enhancedStreamableTestTool) StreamableRun(_ context.Context, _ *schema.ToolArgument, _ ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) {
|
|
return schema.StreamReaderFromArray([]*schema.ToolResult{
|
|
{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: t.result}}},
|
|
}), nil
|
|
}
|
|
|
|
type invokableResultModifier struct {
|
|
*BaseChatModelAgentMiddleware
|
|
modifiedResult string
|
|
}
|
|
|
|
func (h *invokableResultModifier) WrapInvokableToolCall(_ context.Context, endpoint InvokableToolCallEndpoint, _ *ToolContext) (InvokableToolCallEndpoint, error) {
|
|
return func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
|
_, err := endpoint(ctx, argumentsInJSON, opts...)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return h.modifiedResult, nil
|
|
}, nil
|
|
}
|
|
|
|
type streamableResultModifier struct {
|
|
*BaseChatModelAgentMiddleware
|
|
modifiedResult string
|
|
}
|
|
|
|
func (h *streamableResultModifier) WrapStreamableToolCall(_ context.Context, endpoint StreamableToolCallEndpoint, _ *ToolContext) (StreamableToolCallEndpoint, error) {
|
|
return func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error) {
|
|
sr, err := endpoint(ctx, argumentsInJSON, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sr.Close()
|
|
return schema.StreamReaderFromArray([]string{h.modifiedResult}), nil
|
|
}, nil
|
|
}
|
|
|
|
type enhancedInvokableResultModifier struct {
|
|
*BaseChatModelAgentMiddleware
|
|
modifiedResult string
|
|
}
|
|
|
|
func (h *enhancedInvokableResultModifier) WrapEnhancedInvokableToolCall(_ context.Context, endpoint EnhancedInvokableToolCallEndpoint, _ *ToolContext) (EnhancedInvokableToolCallEndpoint, error) {
|
|
return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) {
|
|
_, err := endpoint(ctx, toolArgument, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &schema.ToolResult{
|
|
Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: h.modifiedResult}},
|
|
}, nil
|
|
}, nil
|
|
}
|
|
|
|
type enhancedStreamableResultModifier struct {
|
|
*BaseChatModelAgentMiddleware
|
|
modifiedResult string
|
|
}
|
|
|
|
func (h *enhancedStreamableResultModifier) WrapEnhancedStreamableToolCall(_ context.Context, endpoint EnhancedStreamableToolCallEndpoint, _ *ToolContext) (EnhancedStreamableToolCallEndpoint, error) {
|
|
return func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) {
|
|
sr, err := endpoint(ctx, toolArgument, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sr.Close()
|
|
return schema.StreamReaderFromArray([]*schema.ToolResult{
|
|
{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: h.modifiedResult}}},
|
|
}), nil
|
|
}, nil
|
|
}
|
|
|
|
func collectToolEvents(it *AsyncIterator[*AgentEvent]) []*AgentEvent {
|
|
var toolEvents []*AgentEvent
|
|
for {
|
|
ev, ok := it.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
if ev.Output == nil || ev.Output.MessageOutput == nil {
|
|
continue
|
|
}
|
|
mo := ev.Output.MessageOutput
|
|
if mo.Message != nil && mo.Message.Role == schema.Tool {
|
|
toolEvents = append(toolEvents, ev)
|
|
continue
|
|
}
|
|
if mo.IsStreaming && mo.Role == schema.Tool && mo.MessageStream != nil {
|
|
toolEvents = append(toolEvents, ev)
|
|
}
|
|
}
|
|
return toolEvents
|
|
}
|
|
|
|
func collectToolContent(events []*AgentEvent) []string {
|
|
var contents []string
|
|
for _, ev := range events {
|
|
mo := ev.Output.MessageOutput
|
|
if !mo.IsStreaming && mo.Message != nil {
|
|
if mo.Message.Content != "" {
|
|
contents = append(contents, mo.Message.Content)
|
|
} else if len(mo.Message.UserInputMultiContent) > 0 {
|
|
for _, part := range mo.Message.UserInputMultiContent {
|
|
if part.Text != "" {
|
|
contents = append(contents, part.Text)
|
|
}
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
if mo.IsStreaming && mo.MessageStream != nil {
|
|
var msgs []*schema.Message
|
|
for {
|
|
msg, err := mo.MessageStream.Recv()
|
|
if err != nil {
|
|
break
|
|
}
|
|
msgs = append(msgs, msg)
|
|
}
|
|
if len(msgs) > 0 {
|
|
concated, err := schema.ConcatMessages(msgs)
|
|
if err == nil {
|
|
if concated.Content != "" {
|
|
contents = append(contents, concated.Content)
|
|
} else if len(concated.UserInputMultiContent) > 0 {
|
|
for _, part := range concated.UserInputMultiContent {
|
|
if part.Text != "" {
|
|
contents = append(contents, part.Text)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return contents
|
|
}
|
|
|
|
func TestEventSenderToolHandler(t *testing.T) {
|
|
t.Run("Invokable", func(t *testing.T) {
|
|
t.Run("DefaultSendsEvent", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
testTool := &invokableTestTool{name: "test_tool", result: "invokable_output"}
|
|
mockModel := &mockToolCallingModel{toolCallName: "test_tool"}
|
|
|
|
agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
|
|
Name: "TestAgent",
|
|
Description: "Test agent",
|
|
Model: mockModel,
|
|
ToolsConfig: ToolsConfig{
|
|
ToolsNodeConfig: compose.ToolsNodeConfig{
|
|
Tools: []tool.BaseTool{testTool},
|
|
},
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: false})
|
|
it := r.Run(ctx, []Message{schema.UserMessage("test")})
|
|
|
|
toolEvents := collectToolEvents(it)
|
|
assert.Equal(t, 1, len(toolEvents))
|
|
contents := collectToolContent(toolEvents)
|
|
assert.Contains(t, contents, "invokable_output")
|
|
})
|
|
|
|
t.Run("UserConfiguredSkipsDefault", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
testTool := &invokableTestTool{name: "test_tool", result: "invokable_output"}
|
|
mockModel := &mockToolCallingModel{toolCallName: "test_tool"}
|
|
|
|
agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
|
|
Name: "TestAgent",
|
|
Description: "Test agent",
|
|
Model: mockModel,
|
|
ToolsConfig: ToolsConfig{
|
|
ToolsNodeConfig: compose.ToolsNodeConfig{
|
|
Tools: []tool.BaseTool{testTool},
|
|
},
|
|
},
|
|
Handlers: []ChatModelAgentMiddleware{NewEventSenderToolWrapper()},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: false})
|
|
it := r.Run(ctx, []Message{schema.UserMessage("test")})
|
|
|
|
toolEvents := collectToolEvents(it)
|
|
assert.Equal(t, 1, len(toolEvents))
|
|
})
|
|
|
|
t.Run("InnermostGetsOriginalOutput", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
originalResult := "original_invokable_output"
|
|
modifiedResult := "modified_invokable_output"
|
|
testTool := &invokableTestTool{name: "test_tool", result: originalResult}
|
|
mockModel := &mockToolCallingModel{toolCallName: "test_tool"}
|
|
|
|
agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
|
|
Name: "TestAgent",
|
|
Description: "Test agent",
|
|
Model: mockModel,
|
|
ToolsConfig: ToolsConfig{
|
|
ToolsNodeConfig: compose.ToolsNodeConfig{
|
|
Tools: []tool.BaseTool{testTool},
|
|
},
|
|
},
|
|
Handlers: []ChatModelAgentMiddleware{
|
|
&invokableResultModifier{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
modifiedResult: modifiedResult,
|
|
},
|
|
NewEventSenderToolWrapper(),
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: false})
|
|
it := r.Run(ctx, []Message{schema.UserMessage("test")})
|
|
|
|
toolEvents := collectToolEvents(it)
|
|
assert.GreaterOrEqual(t, len(toolEvents), 1)
|
|
contents := collectToolContent(toolEvents)
|
|
assert.Contains(t, contents, originalResult)
|
|
})
|
|
})
|
|
|
|
t.Run("Streamable", func(t *testing.T) {
|
|
t.Run("DefaultSendsEvent", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
testTool := &streamableTestTool{name: "test_tool", result: "streamable_output"}
|
|
mockModel := &mockToolCallingModel{toolCallName: "test_tool"}
|
|
|
|
agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
|
|
Name: "TestAgent",
|
|
Description: "Test agent",
|
|
Model: mockModel,
|
|
ToolsConfig: ToolsConfig{
|
|
ToolsNodeConfig: compose.ToolsNodeConfig{
|
|
Tools: []tool.BaseTool{testTool},
|
|
},
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: true})
|
|
it := r.Run(ctx, []Message{schema.UserMessage("test")})
|
|
|
|
toolEvents := collectToolEvents(it)
|
|
assert.Equal(t, 1, len(toolEvents))
|
|
contents := collectToolContent(toolEvents)
|
|
assert.Contains(t, contents, "streamable_output")
|
|
})
|
|
|
|
t.Run("UserConfiguredSkipsDefault", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
testTool := &streamableTestTool{name: "test_tool", result: "streamable_output"}
|
|
mockModel := &mockToolCallingModel{toolCallName: "test_tool"}
|
|
|
|
agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
|
|
Name: "TestAgent",
|
|
Description: "Test agent",
|
|
Model: mockModel,
|
|
ToolsConfig: ToolsConfig{
|
|
ToolsNodeConfig: compose.ToolsNodeConfig{
|
|
Tools: []tool.BaseTool{testTool},
|
|
},
|
|
},
|
|
Handlers: []ChatModelAgentMiddleware{NewEventSenderToolWrapper()},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: true})
|
|
it := r.Run(ctx, []Message{schema.UserMessage("test")})
|
|
|
|
toolEvents := collectToolEvents(it)
|
|
assert.Equal(t, 1, len(toolEvents))
|
|
})
|
|
|
|
t.Run("InnermostGetsOriginalOutput", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
originalResult := "original_streamable_output"
|
|
modifiedResult := "modified_streamable_output"
|
|
testTool := &streamableTestTool{name: "test_tool", result: originalResult}
|
|
mockModel := &mockToolCallingModel{toolCallName: "test_tool"}
|
|
|
|
agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
|
|
Name: "TestAgent",
|
|
Description: "Test agent",
|
|
Model: mockModel,
|
|
ToolsConfig: ToolsConfig{
|
|
ToolsNodeConfig: compose.ToolsNodeConfig{
|
|
Tools: []tool.BaseTool{testTool},
|
|
},
|
|
},
|
|
Handlers: []ChatModelAgentMiddleware{
|
|
&streamableResultModifier{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
modifiedResult: modifiedResult,
|
|
},
|
|
NewEventSenderToolWrapper(),
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: true})
|
|
it := r.Run(ctx, []Message{schema.UserMessage("test")})
|
|
|
|
toolEvents := collectToolEvents(it)
|
|
assert.GreaterOrEqual(t, len(toolEvents), 1)
|
|
contents := collectToolContent(toolEvents)
|
|
assert.Contains(t, contents, originalResult)
|
|
})
|
|
})
|
|
|
|
t.Run("EnhancedInvokable", func(t *testing.T) {
|
|
t.Run("DefaultSendsEvent", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
testTool := &enhancedInvokableTestTool{name: "test_tool", result: "enhanced_invokable_output"}
|
|
mockModel := &mockToolCallingModel{toolCallName: "test_tool"}
|
|
|
|
agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
|
|
Name: "TestAgent",
|
|
Description: "Test agent",
|
|
Model: mockModel,
|
|
ToolsConfig: ToolsConfig{
|
|
ToolsNodeConfig: compose.ToolsNodeConfig{
|
|
Tools: []tool.BaseTool{testTool},
|
|
},
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: false})
|
|
it := r.Run(ctx, []Message{schema.UserMessage("test")})
|
|
|
|
toolEvents := collectToolEvents(it)
|
|
assert.Equal(t, 1, len(toolEvents))
|
|
contents := collectToolContent(toolEvents)
|
|
assert.Contains(t, contents, "enhanced_invokable_output")
|
|
})
|
|
|
|
t.Run("UserConfiguredSkipsDefault", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
testTool := &enhancedInvokableTestTool{name: "test_tool", result: "enhanced_invokable_output"}
|
|
mockModel := &mockToolCallingModel{toolCallName: "test_tool"}
|
|
|
|
agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
|
|
Name: "TestAgent",
|
|
Description: "Test agent",
|
|
Model: mockModel,
|
|
ToolsConfig: ToolsConfig{
|
|
ToolsNodeConfig: compose.ToolsNodeConfig{
|
|
Tools: []tool.BaseTool{testTool},
|
|
},
|
|
},
|
|
Handlers: []ChatModelAgentMiddleware{NewEventSenderToolWrapper()},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: false})
|
|
it := r.Run(ctx, []Message{schema.UserMessage("test")})
|
|
|
|
toolEvents := collectToolEvents(it)
|
|
assert.Equal(t, 1, len(toolEvents))
|
|
})
|
|
|
|
t.Run("InnermostGetsOriginalOutput", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
originalResult := "original_enhanced_invokable_output"
|
|
modifiedResult := "modified_enhanced_invokable_output"
|
|
testTool := &enhancedInvokableTestTool{name: "test_tool", result: originalResult}
|
|
mockModel := &mockToolCallingModel{toolCallName: "test_tool"}
|
|
|
|
agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
|
|
Name: "TestAgent",
|
|
Description: "Test agent",
|
|
Model: mockModel,
|
|
ToolsConfig: ToolsConfig{
|
|
ToolsNodeConfig: compose.ToolsNodeConfig{
|
|
Tools: []tool.BaseTool{testTool},
|
|
},
|
|
},
|
|
Handlers: []ChatModelAgentMiddleware{
|
|
&enhancedInvokableResultModifier{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
modifiedResult: modifiedResult,
|
|
},
|
|
NewEventSenderToolWrapper(),
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: false})
|
|
it := r.Run(ctx, []Message{schema.UserMessage("test")})
|
|
|
|
toolEvents := collectToolEvents(it)
|
|
assert.GreaterOrEqual(t, len(toolEvents), 1)
|
|
contents := collectToolContent(toolEvents)
|
|
assert.Contains(t, contents, originalResult)
|
|
})
|
|
})
|
|
|
|
t.Run("EnhancedStreamable", func(t *testing.T) {
|
|
t.Run("DefaultSendsEvent", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
testTool := &enhancedStreamableTestTool{name: "test_tool", result: "enhanced_streamable_output"}
|
|
mockModel := &mockToolCallingModel{toolCallName: "test_tool"}
|
|
|
|
agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
|
|
Name: "TestAgent",
|
|
Description: "Test agent",
|
|
Model: mockModel,
|
|
ToolsConfig: ToolsConfig{
|
|
ToolsNodeConfig: compose.ToolsNodeConfig{
|
|
Tools: []tool.BaseTool{testTool},
|
|
},
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: true})
|
|
it := r.Run(ctx, []Message{schema.UserMessage("test")})
|
|
|
|
toolEvents := collectToolEvents(it)
|
|
assert.Equal(t, 1, len(toolEvents))
|
|
contents := collectToolContent(toolEvents)
|
|
assert.Contains(t, contents, "enhanced_streamable_output")
|
|
})
|
|
|
|
t.Run("UserConfiguredSkipsDefault", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
testTool := &enhancedStreamableTestTool{name: "test_tool", result: "enhanced_streamable_output"}
|
|
mockModel := &mockToolCallingModel{toolCallName: "test_tool"}
|
|
|
|
agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
|
|
Name: "TestAgent",
|
|
Description: "Test agent",
|
|
Model: mockModel,
|
|
ToolsConfig: ToolsConfig{
|
|
ToolsNodeConfig: compose.ToolsNodeConfig{
|
|
Tools: []tool.BaseTool{testTool},
|
|
},
|
|
},
|
|
Handlers: []ChatModelAgentMiddleware{NewEventSenderToolWrapper()},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: true})
|
|
it := r.Run(ctx, []Message{schema.UserMessage("test")})
|
|
|
|
toolEvents := collectToolEvents(it)
|
|
assert.Equal(t, 1, len(toolEvents))
|
|
})
|
|
|
|
t.Run("InnermostGetsOriginalOutput", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
originalResult := "original_enhanced_streamable_output"
|
|
modifiedResult := "modified_enhanced_streamable_output"
|
|
testTool := &enhancedStreamableTestTool{name: "test_tool", result: originalResult}
|
|
mockModel := &mockToolCallingModel{toolCallName: "test_tool"}
|
|
|
|
agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
|
|
Name: "TestAgent",
|
|
Description: "Test agent",
|
|
Model: mockModel,
|
|
ToolsConfig: ToolsConfig{
|
|
ToolsNodeConfig: compose.ToolsNodeConfig{
|
|
Tools: []tool.BaseTool{testTool},
|
|
},
|
|
},
|
|
Handlers: []ChatModelAgentMiddleware{
|
|
&enhancedStreamableResultModifier{
|
|
BaseChatModelAgentMiddleware: &BaseChatModelAgentMiddleware{},
|
|
modifiedResult: modifiedResult,
|
|
},
|
|
NewEventSenderToolWrapper(),
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
r := NewRunner(ctx, RunnerConfig{Agent: agent, EnableStreaming: true})
|
|
it := r.Run(ctx, []Message{schema.UserMessage("test")})
|
|
|
|
toolEvents := collectToolEvents(it)
|
|
assert.GreaterOrEqual(t, len(toolEvents), 1)
|
|
contents := collectToolContent(toolEvents)
|
|
assert.Contains(t, contents, originalResult)
|
|
})
|
|
})
|
|
}
|
|
|
|
// mockAgenticToolCallingModel is a model.BaseModel[*schema.AgenticMessage] that
|
|
// returns a tool call on the first Generate, then a final answer on the second.
|
|
type mockAgenticToolCallingModel struct {
|
|
toolCallName string
|
|
callCount int32
|
|
}
|
|
|
|
func (m *mockAgenticToolCallingModel) Generate(_ context.Context, _ []*schema.AgenticMessage, _ ...model.Option) (*schema.AgenticMessage, error) {
|
|
idx := atomic.AddInt32(&m.callCount, 1)
|
|
if idx == 1 {
|
|
return agenticToolCallMsg(m.toolCallName, "tc-1", `{"input":"test"}`), nil
|
|
}
|
|
return agenticMsg("done"), nil
|
|
}
|
|
|
|
func (m *mockAgenticToolCallingModel) Stream(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.StreamReader[*schema.AgenticMessage], error) {
|
|
msg, err := m.Generate(ctx, input, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r, w := schema.Pipe[*schema.AgenticMessage](1)
|
|
go func() { defer w.Close(); w.Send(msg, nil) }()
|
|
return r, nil
|
|
}
|
|
|
|
// collectAgenticToolEvents filters tool result events from the agentic iterator.
|
|
// Agentic tool results have AgenticRole == AgenticRoleTypeUser and contain
|
|
// FunctionToolResult content blocks.
|
|
func collectAgenticToolEvents(it *AsyncIterator[*agenticAgentEvent]) []*agenticAgentEvent {
|
|
var toolEvents []*agenticAgentEvent
|
|
for {
|
|
ev, ok := it.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
if ev.Output == nil || ev.Output.MessageOutput == nil {
|
|
continue
|
|
}
|
|
mo := ev.Output.MessageOutput
|
|
if mo.AgenticRole == schema.AgenticRoleTypeUser {
|
|
toolEvents = append(toolEvents, ev)
|
|
}
|
|
}
|
|
return toolEvents
|
|
}
|
|
|
|
// collectAgenticToolContent extracts text from agentic tool result events.
|
|
func collectAgenticToolContent(events []*agenticAgentEvent) []string {
|
|
var contents []string
|
|
for _, ev := range events {
|
|
mo := ev.Output.MessageOutput
|
|
if !mo.IsStreaming && mo.Message != nil {
|
|
for _, cb := range mo.Message.ContentBlocks {
|
|
if cb.FunctionToolResult != nil {
|
|
for _, b := range cb.FunctionToolResult.Content {
|
|
if b.Text != nil {
|
|
contents = append(contents, b.Text.Text)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
if mo.IsStreaming && mo.MessageStream != nil {
|
|
for {
|
|
msg, err := mo.MessageStream.Recv()
|
|
if err != nil {
|
|
break
|
|
}
|
|
for _, cb := range msg.ContentBlocks {
|
|
if cb.FunctionToolResult != nil {
|
|
for _, b := range cb.FunctionToolResult.Content {
|
|
if b.Text != nil {
|
|
contents = append(contents, b.Text.Text)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return contents
|
|
}
|
|
|
|
func newAgenticEventSenderToolWrapper() TypedChatModelAgentMiddleware[*schema.AgenticMessage] {
|
|
return &typedEventSenderToolWrapper[*schema.AgenticMessage]{
|
|
TypedBaseChatModelAgentMiddleware: &TypedBaseChatModelAgentMiddleware[*schema.AgenticMessage]{},
|
|
}
|
|
}
|
|
|
|
// TestAgenticEventSenderToolHandler exercises the *schema.AgenticMessage branches
|
|
// in typedToolInvokeEvent, typedToolStreamEvent, typedToolEnhancedInvokeEvent,
|
|
// typedToolEnhancedStreamEvent, plus the helpers textToFunctionToolResultBlocks,
|
|
// toolResultToBlocks, and derefString.
|
|
func TestAgenticEventSenderToolHandler(t *testing.T) {
|
|
t.Run("Invokable", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
testTool := &invokableTestTool{name: "test_tool", result: "invokable_output"}
|
|
mdl := &mockAgenticToolCallingModel{toolCallName: "test_tool"}
|
|
|
|
agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{
|
|
Name: "TestAgent",
|
|
Description: "test",
|
|
Model: mdl,
|
|
ToolsConfig: ToolsConfig{
|
|
ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{testTool}},
|
|
},
|
|
Handlers: []TypedChatModelAgentMiddleware[*schema.AgenticMessage]{newAgenticEventSenderToolWrapper()},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
r := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{Agent: agent, EnableStreaming: false})
|
|
it := r.Query(ctx, "test")
|
|
|
|
toolEvents := collectAgenticToolEvents(it)
|
|
assert.Equal(t, 1, len(toolEvents))
|
|
contents := collectAgenticToolContent(toolEvents)
|
|
assert.Contains(t, contents, "invokable_output")
|
|
})
|
|
|
|
t.Run("Streamable", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
testTool := &streamableTestTool{name: "test_tool", result: "streamable_output"}
|
|
mdl := &mockAgenticToolCallingModel{toolCallName: "test_tool"}
|
|
|
|
agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{
|
|
Name: "TestAgent",
|
|
Description: "test",
|
|
Model: mdl,
|
|
ToolsConfig: ToolsConfig{
|
|
ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{testTool}},
|
|
},
|
|
Handlers: []TypedChatModelAgentMiddleware[*schema.AgenticMessage]{newAgenticEventSenderToolWrapper()},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
r := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{Agent: agent, EnableStreaming: true})
|
|
it := r.Query(ctx, "test")
|
|
|
|
toolEvents := collectAgenticToolEvents(it)
|
|
assert.Equal(t, 1, len(toolEvents))
|
|
contents := collectAgenticToolContent(toolEvents)
|
|
assert.Contains(t, contents, "streamable_output")
|
|
})
|
|
|
|
t.Run("EnhancedInvokable", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
testTool := &enhancedInvokableTestTool{name: "test_tool", result: "enhanced_output"}
|
|
mdl := &mockAgenticToolCallingModel{toolCallName: "test_tool"}
|
|
|
|
agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{
|
|
Name: "TestAgent",
|
|
Description: "test",
|
|
Model: mdl,
|
|
ToolsConfig: ToolsConfig{
|
|
ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{testTool}},
|
|
},
|
|
Handlers: []TypedChatModelAgentMiddleware[*schema.AgenticMessage]{newAgenticEventSenderToolWrapper()},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
r := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{Agent: agent, EnableStreaming: false})
|
|
it := r.Query(ctx, "test")
|
|
|
|
toolEvents := collectAgenticToolEvents(it)
|
|
assert.Equal(t, 1, len(toolEvents))
|
|
contents := collectAgenticToolContent(toolEvents)
|
|
assert.Contains(t, contents, "enhanced_output")
|
|
})
|
|
|
|
t.Run("EnhancedStreamable", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
testTool := &enhancedStreamableTestTool{name: "test_tool", result: "enhanced_stream_output"}
|
|
mdl := &mockAgenticToolCallingModel{toolCallName: "test_tool"}
|
|
|
|
agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{
|
|
Name: "TestAgent",
|
|
Description: "test",
|
|
Model: mdl,
|
|
ToolsConfig: ToolsConfig{
|
|
ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{testTool}},
|
|
},
|
|
Handlers: []TypedChatModelAgentMiddleware[*schema.AgenticMessage]{newAgenticEventSenderToolWrapper()},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
r := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{Agent: agent, EnableStreaming: true})
|
|
it := r.Query(ctx, "test")
|
|
|
|
toolEvents := collectAgenticToolEvents(it)
|
|
assert.Equal(t, 1, len(toolEvents))
|
|
contents := collectAgenticToolContent(toolEvents)
|
|
assert.Contains(t, contents, "enhanced_stream_output")
|
|
})
|
|
|
|
t.Run("EnhancedInvokableMultimodal", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
imgURL := "https://example.com/img.png"
|
|
testTool := &multimodalEnhancedInvokableTestTool{
|
|
name: "test_tool",
|
|
result: &schema.ToolResult{
|
|
Parts: []schema.ToolOutputPart{
|
|
{Type: schema.ToolPartTypeText, Text: "caption"},
|
|
{Type: schema.ToolPartTypeImage, Image: &schema.ToolOutputImage{MessagePartCommon: schema.MessagePartCommon{URL: &imgURL}}},
|
|
},
|
|
},
|
|
}
|
|
mdl := &mockAgenticToolCallingModel{toolCallName: "test_tool"}
|
|
|
|
agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{
|
|
Name: "TestAgent",
|
|
Description: "test",
|
|
Model: mdl,
|
|
ToolsConfig: ToolsConfig{
|
|
ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{testTool}},
|
|
},
|
|
Handlers: []TypedChatModelAgentMiddleware[*schema.AgenticMessage]{newAgenticEventSenderToolWrapper()},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
r := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{Agent: agent, EnableStreaming: false})
|
|
it := r.Query(ctx, "test")
|
|
|
|
toolEvents := collectAgenticToolEvents(it)
|
|
require.Equal(t, 1, len(toolEvents))
|
|
|
|
// Verify multimodal content
|
|
msg := toolEvents[0].Output.MessageOutput.Message
|
|
require.NotNil(t, msg)
|
|
require.Len(t, msg.ContentBlocks, 1)
|
|
ftr := msg.ContentBlocks[0].FunctionToolResult
|
|
require.NotNil(t, ftr)
|
|
require.Len(t, ftr.Content, 2)
|
|
assert.Equal(t, "caption", ftr.Content[0].Text.Text)
|
|
assert.Equal(t, "https://example.com/img.png", ftr.Content[1].Image.URL)
|
|
})
|
|
|
|
t.Run("EnhancedStreamableMultimodal", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
audioURL := "https://example.com/audio.mp3"
|
|
testTool := &multimodalEnhancedStreamableTestTool{
|
|
name: "test_tool",
|
|
result: &schema.ToolResult{
|
|
Parts: []schema.ToolOutputPart{
|
|
{Type: schema.ToolPartTypeText, Text: "transcript"},
|
|
{Type: schema.ToolPartTypeAudio, Audio: &schema.ToolOutputAudio{MessagePartCommon: schema.MessagePartCommon{URL: &audioURL}}},
|
|
},
|
|
},
|
|
}
|
|
mdl := &mockAgenticToolCallingModel{toolCallName: "test_tool"}
|
|
|
|
agent, err := NewTypedChatModelAgent(ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{
|
|
Name: "TestAgent",
|
|
Description: "test",
|
|
Model: mdl,
|
|
ToolsConfig: ToolsConfig{
|
|
ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{testTool}},
|
|
},
|
|
Handlers: []TypedChatModelAgentMiddleware[*schema.AgenticMessage]{newAgenticEventSenderToolWrapper()},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
r := NewTypedRunner(TypedRunnerConfig[*schema.AgenticMessage]{Agent: agent, EnableStreaming: true})
|
|
it := r.Query(ctx, "test")
|
|
|
|
toolEvents := collectAgenticToolEvents(it)
|
|
require.Equal(t, 1, len(toolEvents))
|
|
|
|
// Drain the stream and verify multimodal content
|
|
mo := toolEvents[0].Output.MessageOutput
|
|
require.True(t, mo.IsStreaming)
|
|
var allBlocks []*schema.FunctionToolResultContentBlock
|
|
for {
|
|
msg, err := mo.MessageStream.Recv()
|
|
if err != nil {
|
|
break
|
|
}
|
|
for _, cb := range msg.ContentBlocks {
|
|
if cb.FunctionToolResult != nil {
|
|
allBlocks = append(allBlocks, cb.FunctionToolResult.Content...)
|
|
}
|
|
}
|
|
}
|
|
require.Len(t, allBlocks, 2)
|
|
assert.Equal(t, "transcript", allBlocks[0].Text.Text)
|
|
assert.Equal(t, "https://example.com/audio.mp3", allBlocks[1].Audio.URL)
|
|
})
|
|
}
|
|
|
|
func TestTypedToolStreamEventAgenticMessageSetsStreamingMeta(t *testing.T) {
|
|
event := typedToolStreamEvent[*schema.AgenticMessage](
|
|
"call_1",
|
|
"execute",
|
|
"msg_1",
|
|
schema.StreamReaderFromArray([]string{"first\n", "second\n"}),
|
|
)
|
|
require.NotNil(t, event)
|
|
require.NotNil(t, event.Output)
|
|
require.NotNil(t, event.Output.MessageOutput)
|
|
require.True(t, event.Output.MessageOutput.IsStreaming)
|
|
require.NotNil(t, event.Output.MessageOutput.MessageStream)
|
|
|
|
first, err := event.Output.MessageOutput.MessageStream.Recv()
|
|
require.NoError(t, err)
|
|
require.Len(t, first.ContentBlocks, 1)
|
|
assert.Equal(t, &schema.StreamingMeta{Index: 0}, first.ContentBlocks[0].StreamingMeta)
|
|
|
|
second, err := event.Output.MessageOutput.MessageStream.Recv()
|
|
require.NoError(t, err)
|
|
require.Len(t, second.ContentBlocks, 1)
|
|
assert.Equal(t, &schema.StreamingMeta{Index: 0}, second.ContentBlocks[0].StreamingMeta)
|
|
|
|
result, err := schema.ConcatAgenticMessages([]*schema.AgenticMessage{first, second})
|
|
require.NoError(t, err)
|
|
require.Len(t, result.ContentBlocks, 1)
|
|
assert.Nil(t, result.ContentBlocks[0].StreamingMeta)
|
|
require.NotNil(t, result.ContentBlocks[0].FunctionToolResult)
|
|
require.Len(t, result.ContentBlocks[0].FunctionToolResult.Content, 2)
|
|
assert.Equal(t, "first\n", result.ContentBlocks[0].FunctionToolResult.Content[0].Text.Text)
|
|
assert.Equal(t, "second\n", result.ContentBlocks[0].FunctionToolResult.Content[1].Text.Text)
|
|
}
|
|
|
|
func TestTypedToolEnhancedStreamEventAgenticMessageSetsStreamingMeta(t *testing.T) {
|
|
event := typedToolEnhancedStreamEvent[*schema.AgenticMessage](
|
|
"call_1",
|
|
"execute",
|
|
"msg_1",
|
|
schema.StreamReaderFromArray([]*schema.ToolResult{
|
|
{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "first\n"}}},
|
|
{Parts: []schema.ToolOutputPart{{Type: schema.ToolPartTypeText, Text: "second\n"}}},
|
|
}),
|
|
)
|
|
require.NotNil(t, event)
|
|
require.NotNil(t, event.Output)
|
|
require.NotNil(t, event.Output.MessageOutput)
|
|
require.True(t, event.Output.MessageOutput.IsStreaming)
|
|
require.NotNil(t, event.Output.MessageOutput.MessageStream)
|
|
|
|
first, err := event.Output.MessageOutput.MessageStream.Recv()
|
|
require.NoError(t, err)
|
|
require.Len(t, first.ContentBlocks, 1)
|
|
assert.Equal(t, &schema.StreamingMeta{Index: 0}, first.ContentBlocks[0].StreamingMeta)
|
|
|
|
second, err := event.Output.MessageOutput.MessageStream.Recv()
|
|
require.NoError(t, err)
|
|
require.Len(t, second.ContentBlocks, 1)
|
|
assert.Equal(t, &schema.StreamingMeta{Index: 0}, second.ContentBlocks[0].StreamingMeta)
|
|
|
|
result, err := schema.ConcatAgenticMessages([]*schema.AgenticMessage{first, second})
|
|
require.NoError(t, err)
|
|
require.Len(t, result.ContentBlocks, 1)
|
|
assert.Nil(t, result.ContentBlocks[0].StreamingMeta)
|
|
require.NotNil(t, result.ContentBlocks[0].FunctionToolResult)
|
|
require.Len(t, result.ContentBlocks[0].FunctionToolResult.Content, 2)
|
|
assert.Equal(t, "first\n", result.ContentBlocks[0].FunctionToolResult.Content[0].Text.Text)
|
|
assert.Equal(t, "second\n", result.ContentBlocks[0].FunctionToolResult.Content[1].Text.Text)
|
|
}
|
|
|
|
// multimodalEnhancedInvokableTestTool returns a pre-built multimodal ToolResult.
|
|
type multimodalEnhancedInvokableTestTool struct {
|
|
name string
|
|
result *schema.ToolResult
|
|
}
|
|
|
|
func (t *multimodalEnhancedInvokableTestTool) Info(_ context.Context) (*schema.ToolInfo, error) {
|
|
return &schema.ToolInfo{
|
|
Name: t.name, Desc: "multimodal test tool",
|
|
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
|
"input": {Desc: "input", Required: true, Type: schema.String},
|
|
}),
|
|
}, nil
|
|
}
|
|
|
|
func (t *multimodalEnhancedInvokableTestTool) InvokableRun(_ context.Context, _ *schema.ToolArgument, _ ...tool.Option) (*schema.ToolResult, error) {
|
|
return t.result, nil
|
|
}
|
|
|
|
// multimodalEnhancedStreamableTestTool returns a pre-built multimodal ToolResult as a stream.
|
|
type multimodalEnhancedStreamableTestTool struct {
|
|
name string
|
|
result *schema.ToolResult
|
|
}
|
|
|
|
func (t *multimodalEnhancedStreamableTestTool) Info(_ context.Context) (*schema.ToolInfo, error) {
|
|
return &schema.ToolInfo{
|
|
Name: t.name, Desc: "multimodal streaming test tool",
|
|
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
|
"input": {Desc: "input", Required: true, Type: schema.String},
|
|
}),
|
|
}, nil
|
|
}
|
|
|
|
func (t *multimodalEnhancedStreamableTestTool) StreamableRun(_ context.Context, _ *schema.ToolArgument, _ ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error) {
|
|
return schema.StreamReaderFromArray([]*schema.ToolResult{t.result}), nil
|
|
}
|
|
|
|
func Test_functionToolResultAgenticMessage(t *testing.T) {
|
|
t.Run("basic", func(t *testing.T) {
|
|
blocks := []*schema.FunctionToolResultContentBlock{
|
|
{Type: schema.FunctionToolResultContentBlockTypeText, Text: &schema.UserInputText{Text: "result_str"}},
|
|
}
|
|
msg := functionToolResultAgenticMessage("call_1", "tool_name", blocks)
|
|
assert.Equal(t, schema.AgenticRoleTypeUser, msg.Role)
|
|
assert.Len(t, msg.ContentBlocks, 1)
|
|
assert.Equal(t, schema.ContentBlockTypeFunctionToolResult, msg.ContentBlocks[0].Type)
|
|
ftr := msg.ContentBlocks[0].FunctionToolResult
|
|
assert.Equal(t, "call_1", ftr.CallID)
|
|
assert.Equal(t, "tool_name", ftr.Name)
|
|
assert.Len(t, ftr.Content, 1)
|
|
assert.Equal(t, "result_str", ftr.Content[0].Text.Text)
|
|
})
|
|
|
|
t.Run("multimodal", func(t *testing.T) {
|
|
blocks := []*schema.FunctionToolResultContentBlock{
|
|
{Type: schema.FunctionToolResultContentBlockTypeText, Text: &schema.UserInputText{Text: "description"}},
|
|
{Type: schema.FunctionToolResultContentBlockTypeImage, Image: &schema.UserInputImage{URL: "https://example.com/img.png"}},
|
|
}
|
|
msg := functionToolResultAgenticMessage("call_2", "vision_tool", blocks)
|
|
assert.Equal(t, schema.AgenticRoleTypeUser, msg.Role)
|
|
ftr := msg.ContentBlocks[0].FunctionToolResult
|
|
assert.Equal(t, "call_2", ftr.CallID)
|
|
assert.Equal(t, "vision_tool", ftr.Name)
|
|
assert.Len(t, ftr.Content, 2)
|
|
assert.Equal(t, "description", ftr.Content[0].Text.Text)
|
|
assert.Equal(t, "https://example.com/img.png", ftr.Content[1].Image.URL)
|
|
})
|
|
}
|
|
|
|
func TestTypedToolEnhancedEventAgenticToolSearchResult(t *testing.T) {
|
|
result := &schema.ToolResult{Parts: []schema.ToolOutputPart{
|
|
{
|
|
Type: schema.ToolPartTypeToolSearchResult,
|
|
ToolSearchResult: &schema.ToolSearchResult{Tools: []*schema.ToolInfo{
|
|
{Name: "dynamic_tool", Desc: "dynamic tool"},
|
|
}},
|
|
},
|
|
}}
|
|
|
|
t.Run("invoke", func(t *testing.T) {
|
|
event, err := typedToolEnhancedInvokeEvent[*schema.AgenticMessage]("call_1", "tool_search", "msg_1", result)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, event)
|
|
require.NotNil(t, event.Output)
|
|
require.NotNil(t, event.Output.MessageOutput)
|
|
|
|
msg := event.Output.MessageOutput.Message
|
|
require.NotNil(t, msg)
|
|
require.Len(t, msg.ContentBlocks, 1)
|
|
block := msg.ContentBlocks[0]
|
|
assert.Equal(t, schema.ContentBlockTypeToolSearchResult, block.Type)
|
|
require.NotNil(t, block.ToolSearchFunctionToolResult)
|
|
assert.Equal(t, "call_1", block.ToolSearchFunctionToolResult.CallID)
|
|
assert.Equal(t, "tool_search", block.ToolSearchFunctionToolResult.Name)
|
|
require.NotNil(t, block.ToolSearchFunctionToolResult.Result)
|
|
require.Len(t, block.ToolSearchFunctionToolResult.Result.Tools, 1)
|
|
assert.Equal(t, "dynamic_tool", block.ToolSearchFunctionToolResult.Result.Tools[0].Name)
|
|
})
|
|
|
|
t.Run("stream", func(t *testing.T) {
|
|
event := typedToolEnhancedStreamEvent[*schema.AgenticMessage](
|
|
"call_2",
|
|
"tool_search",
|
|
"msg_2",
|
|
schema.StreamReaderFromArray([]*schema.ToolResult{result}),
|
|
)
|
|
require.NotNil(t, event)
|
|
require.NotNil(t, event.Output)
|
|
require.NotNil(t, event.Output.MessageOutput)
|
|
|
|
msg, err := event.Output.MessageOutput.MessageStream.Recv()
|
|
require.NoError(t, err)
|
|
require.NotNil(t, msg)
|
|
require.Len(t, msg.ContentBlocks, 1)
|
|
block := msg.ContentBlocks[0]
|
|
assert.Equal(t, schema.ContentBlockTypeToolSearchResult, block.Type)
|
|
assert.Equal(t, &schema.StreamingMeta{Index: 0}, block.StreamingMeta)
|
|
require.NotNil(t, block.ToolSearchFunctionToolResult)
|
|
assert.Equal(t, "call_2", block.ToolSearchFunctionToolResult.CallID)
|
|
assert.Equal(t, "tool_search", block.ToolSearchFunctionToolResult.Name)
|
|
require.NotNil(t, block.ToolSearchFunctionToolResult.Result)
|
|
require.Len(t, block.ToolSearchFunctionToolResult.Result.Tools, 1)
|
|
assert.Equal(t, "dynamic_tool", block.ToolSearchFunctionToolResult.Result.Tools[0].Name)
|
|
})
|
|
}
|
|
|
|
func TestExtractToolIdentifiersToolSearchResult(t *testing.T) {
|
|
msg := &schema.AgenticMessage{
|
|
Role: schema.AgenticRoleTypeUser,
|
|
ContentBlocks: []*schema.ContentBlock{
|
|
schema.NewContentBlock(&schema.ToolSearchFunctionToolResult{
|
|
CallID: "call_1",
|
|
Name: "tool_search",
|
|
Result: &schema.ToolSearchResult{},
|
|
}),
|
|
},
|
|
}
|
|
|
|
toolName, callID := extractToolIdentifiers(msg)
|
|
assert.Equal(t, "tool_search", toolName)
|
|
assert.Equal(t, "call_1", callID)
|
|
}
|