Files
cloudwego--eino/compose/agentic_tools_node_test.go
2026-07-13 12:49:10 +08:00

468 lines
13 KiB
Go

/*
* Copyright 2024 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 compose
import (
"io"
"testing"
"github.com/bytedance/sonic"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/cloudwego/eino/schema"
)
func TestAgenticMessageToToolCallMessage(t *testing.T) {
input := &schema.AgenticMessage{
ContentBlocks: []*schema.ContentBlock{
{
Type: schema.ContentBlockTypeFunctionToolCall,
FunctionToolCall: &schema.FunctionToolCall{
CallID: "1",
Name: "name1",
Arguments: "arg1",
},
},
{
Type: schema.ContentBlockTypeFunctionToolCall,
FunctionToolCall: &schema.FunctionToolCall{
CallID: "2",
Name: "name2",
Arguments: "arg2",
},
},
{
Type: schema.ContentBlockTypeFunctionToolCall,
FunctionToolCall: &schema.FunctionToolCall{
CallID: "3",
Name: "name3",
Arguments: "arg3",
},
},
},
}
ret := agenticMessageToToolCallMessage(input)
assert.Equal(t, schema.Assistant, ret.Role)
assert.Equal(t, []schema.ToolCall{
{
ID: "1",
Function: schema.FunctionCall{
Name: "name1",
Arguments: "arg1",
},
},
{
ID: "2",
Function: schema.FunctionCall{
Name: "name2",
Arguments: "arg2",
},
},
{
ID: "3",
Function: schema.FunctionCall{
Name: "name3",
Arguments: "arg3",
},
},
}, ret.ToolCalls)
}
func TestToolMessageToAgenticMessage(t *testing.T) {
t.Run("text only", func(t *testing.T) {
input := []*schema.Message{
{
Role: schema.Tool,
Content: "content1",
ToolCallID: "1",
ToolName: "name1",
},
{
Role: schema.Tool,
Content: "content2",
ToolCallID: "2",
ToolName: "name2",
},
{
Role: schema.Tool,
Content: "content3",
ToolCallID: "3",
ToolName: "name3",
},
}
ret := toolMessageToAgenticMessage(input)
assert.Equal(t, 3, len(ret))
for i, msg := range ret {
assert.Equal(t, schema.AgenticRoleTypeUser, msg.Role)
assert.Equal(t, 1, len(msg.ContentBlocks))
assert.Equal(t, schema.ContentBlockTypeFunctionToolResult, msg.ContentBlocks[0].Type)
ftr := msg.ContentBlocks[0].FunctionToolResult
assert.Equal(t, input[i].ToolCallID, ftr.CallID)
assert.Equal(t, input[i].ToolName, ftr.Name)
assert.Equal(t, 1, len(ftr.Content))
assert.Equal(t, input[i].Content, ftr.Content[0].Text.Text)
}
})
t.Run("with multimodal content", func(t *testing.T) {
imageURL := "https://example.com/image.png"
audioBase64 := "YXVkaW9kYXRh"
videoURL := "https://example.com/video.mp4"
fileURL := "https://example.com/file.pdf"
input := []*schema.Message{
{
Role: schema.Tool,
Content: "text result",
ToolCallID: "1",
ToolName: "tool1",
UserInputMultiContent: []schema.MessageInputPart{
{Type: schema.ChatMessagePartTypeText, Text: "hello"},
{Type: schema.ChatMessagePartTypeImageURL, Image: &schema.MessageInputImage{
MessagePartCommon: schema.MessagePartCommon{URL: &imageURL, MIMEType: "image/png"},
Detail: schema.ImageURLDetailHigh,
}},
{Type: schema.ChatMessagePartTypeAudioURL, Audio: &schema.MessageInputAudio{
MessagePartCommon: schema.MessagePartCommon{Base64Data: &audioBase64, MIMEType: "audio/wav"},
}},
{Type: schema.ChatMessagePartTypeVideoURL, Video: &schema.MessageInputVideo{
MessagePartCommon: schema.MessagePartCommon{URL: &videoURL, MIMEType: "video/mp4"},
}},
{Type: schema.ChatMessagePartTypeFileURL, File: &schema.MessageInputFile{
MessagePartCommon: schema.MessagePartCommon{URL: &fileURL, MIMEType: "application/pdf"},
}},
},
},
{
Role: schema.Tool,
Content: "plain result",
ToolCallID: "2",
ToolName: "tool2",
},
}
ret := toolMessageToAgenticMessage(input)
assert.Equal(t, 2, len(ret))
// first message: multimodal tool result
assert.Equal(t, schema.AgenticRoleTypeUser, ret[0].Role)
assert.Equal(t, 1, len(ret[0].ContentBlocks))
ftr1 := ret[0].ContentBlocks[0].FunctionToolResult
assert.Equal(t, "1", ftr1.CallID)
assert.Equal(t, 5, len(ftr1.Content))
assert.Equal(t, "hello", ftr1.Content[0].Text.Text)
assert.Equal(t, imageURL, ftr1.Content[1].Image.URL)
assert.Equal(t, schema.ImageURLDetailHigh, ftr1.Content[1].Image.Detail)
assert.Equal(t, audioBase64, ftr1.Content[2].Audio.Base64Data)
assert.Equal(t, videoURL, ftr1.Content[3].Video.URL)
assert.Equal(t, fileURL, ftr1.Content[4].File.URL)
// second message: text-only tool result
assert.Equal(t, schema.AgenticRoleTypeUser, ret[1].Role)
assert.Equal(t, 1, len(ret[1].ContentBlocks))
ftr2 := ret[1].ContentBlocks[0].FunctionToolResult
assert.Equal(t, "2", ftr2.CallID)
assert.Equal(t, 1, len(ftr2.Content))
assert.Equal(t, "plain result", ftr2.Content[0].Text.Text)
})
t.Run("nil media fields are skipped", func(t *testing.T) {
input := []*schema.Message{
{
Role: schema.Tool,
Content: "result",
ToolCallID: "1",
ToolName: "tool1",
UserInputMultiContent: []schema.MessageInputPart{
{Type: schema.ChatMessagePartTypeImageURL, Image: nil},
{Type: schema.ChatMessagePartTypeAudioURL, Audio: nil},
{Type: schema.ChatMessagePartTypeVideoURL, Video: nil},
{Type: schema.ChatMessagePartTypeFileURL, File: nil},
{Type: schema.ChatMessagePartTypeText, Text: "only text"},
},
},
}
ret := toolMessageToAgenticMessage(input)
assert.Equal(t, 1, len(ret))
ftr := ret[0].ContentBlocks[0].FunctionToolResult
assert.Equal(t, 1, len(ftr.Content))
assert.Equal(t, "only text", ftr.Content[0].Text.Text)
})
t.Run("tool search result", func(t *testing.T) {
input := []*schema.Message{
{
Role: schema.Tool,
ToolCallID: "call_1",
ToolName: "tool_search",
UserInputMultiContent: []schema.MessageInputPart{
{
Type: schema.ChatMessagePartTypeToolSearchResult,
ToolSearchResult: &schema.ToolSearchResult{Tools: []*schema.ToolInfo{
{Name: "dynamic_tool", Desc: "dynamic tool"},
}},
},
},
},
}
ret := toolMessageToAgenticMessage(input)
require.Len(t, ret, 1)
require.Len(t, ret[0].ContentBlocks, 1)
block := ret[0].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)
})
}
func TestStreamToolMessageToAgenticMessage(t *testing.T) {
t.Run("text only", func(t *testing.T) {
testStreamToolMessageTextOnly(t)
})
t.Run("with multimodal content", func(t *testing.T) {
imageURL := "https://example.com/image.png"
input := schema.StreamReaderFromArray([][]*schema.Message{
{
{
Role: schema.Tool,
Content: "result1",
ToolName: "tool1",
ToolCallID: "1",
UserInputMultiContent: []schema.MessageInputPart{
{Type: schema.ChatMessagePartTypeText, Text: "text part"},
{Type: schema.ChatMessagePartTypeImageURL, Image: &schema.MessageInputImage{
MessagePartCommon: schema.MessagePartCommon{URL: &imageURL},
}},
},
},
nil,
},
{
nil,
{
Role: schema.Tool,
Content: "result2",
ToolName: "tool2",
ToolCallID: "2",
},
},
})
ret := streamToolMessageToAgenticMessage(input)
var chunks [][]*schema.AgenticMessage
for {
chunk, err := ret.Recv()
if err == io.EOF {
break
}
assert.NoError(t, err)
chunks = append(chunks, chunk)
}
result, err := schema.ConcatAgenticMessagesArray(chunks)
assert.NoError(t, err)
assert.Equal(t, 2, len(result))
// first message: multimodal tool result (single chunk → StreamingMeta preserved)
assert.Equal(t, schema.AgenticRoleTypeUser, result[0].Role)
assert.Equal(t, 1, len(result[0].ContentBlocks))
ftr1 := result[0].ContentBlocks[0].FunctionToolResult
assert.Equal(t, "1", ftr1.CallID)
assert.Equal(t, 2, len(ftr1.Content))
assert.NotNil(t, ftr1.Content[0].Text)
assert.NotNil(t, ftr1.Content[1].Image)
assert.Equal(t, imageURL, ftr1.Content[1].Image.URL)
// second message: text-only tool result (single chunk → StreamingMeta preserved)
assert.Equal(t, schema.AgenticRoleTypeUser, result[1].Role)
assert.Equal(t, 1, len(result[1].ContentBlocks))
ftr2 := result[1].ContentBlocks[0].FunctionToolResult
assert.Equal(t, "2", ftr2.CallID)
assert.Equal(t, 1, len(ftr2.Content))
assert.Equal(t, "result2", ftr2.Content[0].Text.Text)
})
t.Run("tool search result", func(t *testing.T) {
input := schema.StreamReaderFromArray([][]*schema.Message{
{
{
Role: schema.Tool,
ToolName: "tool_search",
ToolCallID: "call_1",
UserInputMultiContent: []schema.MessageInputPart{
{
Type: schema.ChatMessagePartTypeToolSearchResult,
ToolSearchResult: &schema.ToolSearchResult{Tools: []*schema.ToolInfo{
{Name: "dynamic_tool", Desc: "dynamic tool"},
}},
},
},
},
},
})
ret := streamToolMessageToAgenticMessage(input)
chunk, err := ret.Recv()
require.NoError(t, err)
require.Len(t, chunk, 1)
require.Len(t, chunk[0].ContentBlocks, 1)
block := chunk[0].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_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)
})
}
func testStreamToolMessageTextOnly(t *testing.T) {
input := schema.StreamReaderFromArray([][]*schema.Message{
{
{
Role: schema.Tool,
Content: "content1-1",
ToolName: "name1",
ToolCallID: "1",
},
nil, nil,
},
{
nil,
{
Role: schema.Tool,
Content: "content2-1",
ToolName: "name2",
ToolCallID: "2",
},
nil,
},
{
nil,
{
Role: schema.Tool,
Content: "content2-2",
ToolName: "name2",
ToolCallID: "2",
},
nil,
},
{
nil, nil,
{
Role: schema.Tool,
Content: "content3-1",
ToolName: "name3",
ToolCallID: "3",
},
},
{
nil, nil,
{
Role: schema.Tool,
Content: "content3-2",
ToolName: "name3",
ToolCallID: "3",
},
},
})
ret := streamToolMessageToAgenticMessage(input)
var chunks [][]*schema.AgenticMessage
for {
chunk, err := ret.Recv()
if err == io.EOF {
break
}
assert.NoError(t, err)
chunks = append(chunks, chunk)
}
result, err := schema.ConcatAgenticMessagesArray(chunks)
assert.NoError(t, err)
actualStr, err := sonic.MarshalString(result)
assert.NoError(t, err)
expected := []*schema.AgenticMessage{
{
Role: schema.AgenticRoleTypeUser,
ContentBlocks: []*schema.ContentBlock{
{
Type: schema.ContentBlockTypeFunctionToolResult,
FunctionToolResult: &schema.FunctionToolResult{
CallID: "1",
Name: "name1",
Content: []*schema.FunctionToolResultContentBlock{
{Type: schema.FunctionToolResultContentBlockTypeText, Text: &schema.UserInputText{Text: "content1-1"}},
},
},
StreamingMeta: &schema.StreamingMeta{Index: 0},
},
},
},
{
Role: schema.AgenticRoleTypeUser,
ContentBlocks: []*schema.ContentBlock{
{
Type: schema.ContentBlockTypeFunctionToolResult,
FunctionToolResult: &schema.FunctionToolResult{
CallID: "2",
Name: "name2",
Content: []*schema.FunctionToolResultContentBlock{
{Type: schema.FunctionToolResultContentBlockTypeText, Text: &schema.UserInputText{Text: "content2-1"}},
{Type: schema.FunctionToolResultContentBlockTypeText, Text: &schema.UserInputText{Text: "content2-2"}},
},
},
},
},
},
{
Role: schema.AgenticRoleTypeUser,
ContentBlocks: []*schema.ContentBlock{
{
Type: schema.ContentBlockTypeFunctionToolResult,
FunctionToolResult: &schema.FunctionToolResult{
CallID: "3",
Name: "name3",
Content: []*schema.FunctionToolResultContentBlock{
{Type: schema.FunctionToolResultContentBlockTypeText, Text: &schema.UserInputText{Text: "content3-1"}},
{Type: schema.FunctionToolResultContentBlockTypeText, Text: &schema.UserInputText{Text: "content3-2"}},
},
},
},
},
},
}
expectedStr, err := sonic.MarshalString(expected)
assert.NoError(t, err)
assert.Equal(t, expectedStr, actualStr)
}