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

1730 lines
49 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 schema
import (
"context"
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestConcatAgenticMessages(t *testing.T) {
t.Run("single message", func(t *testing.T) {
msg := &AgenticMessage{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeAssistantGenText,
AssistantGenText: &AssistantGenText{
Text: "Hello",
},
},
},
}
result, err := ConcatAgenticMessages([]*AgenticMessage{msg})
assert.NoError(t, err)
assert.Equal(t, msg, result)
})
t.Run("nil message in stream", func(t *testing.T) {
msgs := []*AgenticMessage{
{Role: AgenticRoleTypeAssistant},
nil,
{Role: AgenticRoleTypeAssistant},
}
_, err := ConcatAgenticMessages(msgs)
assert.Error(t, err)
assert.ErrorContains(t, err, "message at index 1 is nil")
})
t.Run("different roles", func(t *testing.T) {
msgs := []*AgenticMessage{
{Role: AgenticRoleTypeUser},
{Role: AgenticRoleTypeAssistant},
}
_, err := ConcatAgenticMessages(msgs)
assert.Error(t, err)
assert.ErrorContains(t, err, "cannot concat messages with different roles")
})
t.Run("concat text blocks", func(t *testing.T) {
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeAssistantGenText,
AssistantGenText: &AssistantGenText{
Text: "Hello ",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeAssistantGenText,
AssistantGenText: &AssistantGenText{
Text: "World!",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
}
result, err := ConcatAgenticMessages(msgs)
assert.NoError(t, err)
assert.Equal(t, AgenticRoleTypeAssistant, result.Role)
assert.Len(t, result.ContentBlocks, 1)
assert.Equal(t, "Hello World!", result.ContentBlocks[0].AssistantGenText.Text)
})
t.Run("concat reasoning with nil index", func(t *testing.T) {
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeReasoning,
Reasoning: &Reasoning{
Text: "First ",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeReasoning,
Reasoning: &Reasoning{
Text: "Second",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
}
result, err := ConcatAgenticMessages(msgs)
assert.NoError(t, err)
assert.Len(t, result.ContentBlocks, 1)
assert.Equal(t, "First Second", result.ContentBlocks[0].Reasoning.Text)
})
t.Run("concat reasoning with index", func(t *testing.T) {
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeReasoning,
Reasoning: &Reasoning{
Text: "Part1-",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeReasoning,
Reasoning: &Reasoning{
Text: "Part3",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
}
result, err := ConcatAgenticMessages(msgs)
assert.NoError(t, err)
assert.Len(t, result.ContentBlocks, 1)
assert.Equal(t, "Part1-Part3", result.ContentBlocks[0].Reasoning.Text)
})
t.Run("concat user input text", func(t *testing.T) {
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeAssistantGenText,
AssistantGenText: &AssistantGenText{
Text: "Hello ",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeAssistantGenText,
AssistantGenText: &AssistantGenText{
Text: "World!",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
}
result, err := ConcatAgenticMessages(msgs)
assert.NoError(t, err)
assert.Len(t, result.ContentBlocks, 1)
assert.Equal(t, "Hello World!", result.ContentBlocks[0].AssistantGenText.Text)
})
t.Run("concat assistant gen image", func(t *testing.T) {
base1 := "1"
base2 := "2"
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeAssistantGenImage,
AssistantGenImage: &AssistantGenImage{
Base64Data: base1,
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeAssistantGenImage,
AssistantGenImage: &AssistantGenImage{
Base64Data: base2,
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
}
result, err := ConcatAgenticMessages(msgs)
assert.NoError(t, err)
assert.Len(t, result.ContentBlocks, 1)
assert.Equal(t, "12", result.ContentBlocks[0].AssistantGenImage.Base64Data)
})
t.Run("concat user input audio - should error", func(t *testing.T) {
url1 := "https://example.com/audio1.mp3"
url2 := "https://example.com/audio2.mp3"
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeUser,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeUserInputAudio,
UserInputAudio: &UserInputAudio{
URL: url1,
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
{
Role: AgenticRoleTypeUser,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeUserInputAudio,
UserInputAudio: &UserInputAudio{
URL: url2,
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
}
_, err := ConcatAgenticMessages(msgs)
assert.Error(t, err)
assert.ErrorContains(t, err, "cannot concat multiple user input audios")
})
t.Run("concat user input video - should error", func(t *testing.T) {
url1 := "https://example.com/video1.mp4"
url2 := "https://example.com/video2.mp4"
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeUser,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeUserInputVideo,
UserInputVideo: &UserInputVideo{
URL: url1,
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
{
Role: AgenticRoleTypeUser,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeUserInputVideo,
UserInputVideo: &UserInputVideo{
URL: url2,
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
}
_, err := ConcatAgenticMessages(msgs)
assert.Error(t, err)
assert.ErrorContains(t, err, "cannot concat multiple user input videos")
})
t.Run("concat assistant gen text", func(t *testing.T) {
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeAssistantGenText,
AssistantGenText: &AssistantGenText{
Text: "Generated ",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeAssistantGenText,
AssistantGenText: &AssistantGenText{
Text: "Text",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
}
result, err := ConcatAgenticMessages(msgs)
assert.NoError(t, err)
assert.Len(t, result.ContentBlocks, 1)
assert.Equal(t, "Generated Text", result.ContentBlocks[0].AssistantGenText.Text)
})
t.Run("concat assistant gen image", func(t *testing.T) {
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeAssistantGenImage,
AssistantGenImage: &AssistantGenImage{
Base64Data: "part1",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeAssistantGenImage,
AssistantGenImage: &AssistantGenImage{
Base64Data: "part2",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
}
result, err := ConcatAgenticMessages(msgs)
assert.NoError(t, err)
assert.Len(t, result.ContentBlocks, 1)
assert.Equal(t, "part1part2", result.ContentBlocks[0].AssistantGenImage.Base64Data)
})
t.Run("concat assistant gen audio", func(t *testing.T) {
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeAssistantGenAudio,
AssistantGenAudio: &AssistantGenAudio{
Base64Data: "audio1",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeAssistantGenAudio,
AssistantGenAudio: &AssistantGenAudio{
Base64Data: "audio2",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
}
result, err := ConcatAgenticMessages(msgs)
assert.NoError(t, err)
assert.Len(t, result.ContentBlocks, 1)
assert.Equal(t, "audio1audio2", result.ContentBlocks[0].AssistantGenAudio.Base64Data)
})
t.Run("concat assistant gen video", func(t *testing.T) {
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeAssistantGenVideo,
AssistantGenVideo: &AssistantGenVideo{
Base64Data: "video1",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeAssistantGenVideo,
AssistantGenVideo: &AssistantGenVideo{
Base64Data: "video2",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
}
result, err := ConcatAgenticMessages(msgs)
assert.NoError(t, err)
assert.Len(t, result.ContentBlocks, 1)
assert.Equal(t, "video1video2", result.ContentBlocks[0].AssistantGenVideo.Base64Data)
})
t.Run("concat function tool call", func(t *testing.T) {
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeFunctionToolCall,
FunctionToolCall: &FunctionToolCall{
CallID: "call_123",
Name: "get_weather",
Arguments: `{"location`,
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeFunctionToolCall,
FunctionToolCall: &FunctionToolCall{
Arguments: `":"NYC"}`,
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
}
result, err := ConcatAgenticMessages(msgs)
assert.NoError(t, err)
assert.Len(t, result.ContentBlocks, 1)
assert.Equal(t, "call_123", result.ContentBlocks[0].FunctionToolCall.CallID)
assert.Equal(t, "get_weather", result.ContentBlocks[0].FunctionToolCall.Name)
assert.Equal(t, `{"location":"NYC"}`, result.ContentBlocks[0].FunctionToolCall.Arguments)
})
t.Run("concat function tool result", func(t *testing.T) {
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeFunctionToolResult,
FunctionToolResult: &FunctionToolResult{
CallID: "call_123",
Name: "get_weather",
Content: []*FunctionToolResultContentBlock{
{Type: FunctionToolResultContentBlockTypeText, Text: &UserInputText{Text: `{"temp`}},
},
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeFunctionToolResult,
FunctionToolResult: &FunctionToolResult{
Content: []*FunctionToolResultContentBlock{
{Type: FunctionToolResultContentBlockTypeText, Text: &UserInputText{Text: `":72}`}},
},
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
}
result, err := ConcatAgenticMessages(msgs)
assert.NoError(t, err)
assert.Len(t, result.ContentBlocks, 1)
assert.Equal(t, "call_123", result.ContentBlocks[0].FunctionToolResult.CallID)
assert.Equal(t, "get_weather", result.ContentBlocks[0].FunctionToolResult.Name)
assert.Equal(t, 2, len(result.ContentBlocks[0].FunctionToolResult.Content))
assert.Equal(t, `{"temp`, result.ContentBlocks[0].FunctionToolResult.Content[0].Text.Text)
assert.Equal(t, `":72}`, result.ContentBlocks[0].FunctionToolResult.Content[1].Text.Text)
})
t.Run("concat server tool call", func(t *testing.T) {
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeServerToolCall,
ServerToolCall: &ServerToolCall{
CallID: "server_call_1",
Name: "server_func",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeServerToolCall,
ServerToolCall: &ServerToolCall{
Arguments: map[string]any{"key": "value"},
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
}
result, err := ConcatAgenticMessages(msgs)
assert.NoError(t, err)
assert.Len(t, result.ContentBlocks, 1)
assert.Equal(t, "server_call_1", result.ContentBlocks[0].ServerToolCall.CallID)
assert.Equal(t, "server_func", result.ContentBlocks[0].ServerToolCall.Name)
assert.NotNil(t, result.ContentBlocks[0].ServerToolCall.Arguments)
})
t.Run("concat server tool result", func(t *testing.T) {
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeServerToolResult,
ServerToolResult: &ServerToolResult{
CallID: "server_call_1",
Name: "server_func",
Content: "result1",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeServerToolResult,
ServerToolResult: &ServerToolResult{},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
}
result, err := ConcatAgenticMessages(msgs)
assert.NoError(t, err)
assert.Len(t, result.ContentBlocks, 1)
assert.Equal(t, "server_call_1", result.ContentBlocks[0].ServerToolResult.CallID)
assert.Equal(t, "server_func", result.ContentBlocks[0].ServerToolResult.Name)
assert.Equal(t, "result1", result.ContentBlocks[0].ServerToolResult.Content)
})
t.Run("concat mcp tool call", func(t *testing.T) {
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeMCPToolCall,
MCPToolCall: &MCPToolCall{
ServerLabel: "mcp-server",
CallID: "mcp_call_1",
Name: "mcp_func",
Arguments: `{"arg`,
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeMCPToolCall,
MCPToolCall: &MCPToolCall{
Arguments: `":123}`,
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
}
result, err := ConcatAgenticMessages(msgs)
assert.NoError(t, err)
assert.Len(t, result.ContentBlocks, 1)
assert.Equal(t, "mcp-server", result.ContentBlocks[0].MCPToolCall.ServerLabel)
assert.Equal(t, "mcp_call_1", result.ContentBlocks[0].MCPToolCall.CallID)
assert.Equal(t, "mcp_func", result.ContentBlocks[0].MCPToolCall.Name)
assert.Equal(t, `{"arg":123}`, result.ContentBlocks[0].MCPToolCall.Arguments)
})
t.Run("concat mcp tool result", func(t *testing.T) {
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeMCPToolResult,
MCPToolResult: &MCPToolResult{
ServerLabel: "mcp-server",
CallID: "mcp_call_1",
Name: "mcp_func",
Content: `First`,
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeMCPToolResult,
MCPToolResult: &MCPToolResult{
Content: `Second`,
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
}
result, err := ConcatAgenticMessages(msgs)
assert.NoError(t, err)
assert.Len(t, result.ContentBlocks, 1)
assert.Equal(t, "mcp-server", result.ContentBlocks[0].MCPToolResult.ServerLabel)
assert.Equal(t, "mcp_call_1", result.ContentBlocks[0].MCPToolResult.CallID)
assert.Equal(t, "mcp_func", result.ContentBlocks[0].MCPToolResult.Name)
assert.Equal(t, `Second`, result.ContentBlocks[0].MCPToolResult.Content)
})
t.Run("concat mcp list tools", func(t *testing.T) {
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeMCPListToolsResult,
MCPListToolsResult: &MCPListToolsResult{
ServerLabel: "mcp-server",
Tools: []*MCPListToolsItem{
{Name: "tool1"},
},
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeMCPListToolsResult,
MCPListToolsResult: &MCPListToolsResult{
Tools: []*MCPListToolsItem{
{Name: "tool2"},
},
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
}
result, err := ConcatAgenticMessages(msgs)
assert.NoError(t, err)
assert.Len(t, result.ContentBlocks, 1)
assert.Equal(t, "mcp-server", result.ContentBlocks[0].MCPListToolsResult.ServerLabel)
assert.Len(t, result.ContentBlocks[0].MCPListToolsResult.Tools, 2)
})
t.Run("concat mcp tool approval request", func(t *testing.T) {
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeMCPToolApprovalRequest,
MCPToolApprovalRequest: &MCPToolApprovalRequest{
ID: "approval_1",
Name: "approval_func",
ServerLabel: "mcp-server",
Arguments: `{"request`,
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeMCPToolApprovalRequest,
MCPToolApprovalRequest: &MCPToolApprovalRequest{
Arguments: `":1}`,
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
}
result, err := ConcatAgenticMessages(msgs)
assert.NoError(t, err)
assert.Len(t, result.ContentBlocks, 1)
assert.Equal(t, "approval_1", result.ContentBlocks[0].MCPToolApprovalRequest.ID)
assert.Equal(t, "approval_func", result.ContentBlocks[0].MCPToolApprovalRequest.Name)
assert.Equal(t, "mcp-server", result.ContentBlocks[0].MCPToolApprovalRequest.ServerLabel)
assert.Equal(t, `{"request":1}`, result.ContentBlocks[0].MCPToolApprovalRequest.Arguments)
})
t.Run("concat mcp tool approval response - should error", func(t *testing.T) {
response1 := &MCPToolApprovalResponse{
ApprovalRequestID: "approval_1",
Approve: false,
}
response2 := &MCPToolApprovalResponse{
ApprovalRequestID: "approval_1",
Approve: true,
}
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeMCPToolApprovalResponse,
MCPToolApprovalResponse: response1,
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeMCPToolApprovalResponse,
MCPToolApprovalResponse: response2,
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
}
_, err := ConcatAgenticMessages(msgs)
assert.Error(t, err)
assert.ErrorContains(t, err, "cannot concat multiple mcp tool approval responses")
})
t.Run("concat response meta", func(t *testing.T) {
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeAssistant,
ResponseMeta: &AgenticResponseMeta{
TokenUsage: &TokenUsage{
PromptTokens: 10,
CompletionTokens: 5,
PromptTokenDetails: PromptTokenDetails{
CachedTokens: 3,
},
CompletionTokensDetails: CompletionTokensDetails{
ReasoningTokens: 2,
},
TotalTokens: 15,
},
},
},
{
Role: AgenticRoleTypeAssistant,
ResponseMeta: &AgenticResponseMeta{
TokenUsage: &TokenUsage{
PromptTokens: 8,
CompletionTokens: 15,
PromptTokenDetails: PromptTokenDetails{
CachedTokens: 5,
},
CompletionTokensDetails: CompletionTokensDetails{
ReasoningTokens: 4,
},
TotalTokens: 23,
},
},
},
}
result, err := ConcatAgenticMessages(msgs)
assert.NoError(t, err)
assert.NotNil(t, result.ResponseMeta)
assert.Equal(t, 15, result.ResponseMeta.TokenUsage.CompletionTokens)
assert.Equal(t, 10, result.ResponseMeta.TokenUsage.PromptTokens)
assert.Equal(t, 23, result.ResponseMeta.TokenUsage.TotalTokens)
assert.Equal(t, 5, result.ResponseMeta.TokenUsage.PromptTokenDetails.CachedTokens)
assert.Equal(t, 4, result.ResponseMeta.TokenUsage.CompletionTokensDetails.ReasoningTokens)
})
t.Run("mixed streaming and non-streaming blocks error", func(t *testing.T) {
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeAssistantGenText,
AssistantGenText: &AssistantGenText{
Text: "Hello",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeAssistantGenText,
AssistantGenText: &AssistantGenText{
Text: "World",
},
// No StreamingMeta - non-streaming
},
},
},
}
_, err := ConcatAgenticMessages(msgs)
assert.Error(t, err)
assert.ErrorContains(t, err, "found non-streaming block after streaming blocks")
})
t.Run("concat MCP tool call", func(t *testing.T) {
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeMCPToolCall,
MCPToolCall: &MCPToolCall{
ServerLabel: "mcp-server",
CallID: "call_456",
Name: "list_files",
Arguments: `{"path`,
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeMCPToolCall,
MCPToolCall: &MCPToolCall{
Arguments: `":"/tmp"}`,
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
}
result, err := ConcatAgenticMessages(msgs)
assert.NoError(t, err)
assert.Len(t, result.ContentBlocks, 1)
assert.Equal(t, "mcp-server", result.ContentBlocks[0].MCPToolCall.ServerLabel)
assert.Equal(t, "call_456", result.ContentBlocks[0].MCPToolCall.CallID)
assert.Equal(t, `{"path":"/tmp"}`, result.ContentBlocks[0].MCPToolCall.Arguments)
})
t.Run("concat user input text - should error", func(t *testing.T) {
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeUser,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeUserInputText,
UserInputText: &UserInputText{
Text: "What is ",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
{
Role: AgenticRoleTypeUser,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeUserInputText,
UserInputText: &UserInputText{
Text: "the weather?",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
}
_, err := ConcatAgenticMessages(msgs)
assert.Error(t, err)
assert.ErrorContains(t, err, "cannot concat multiple user input texts")
})
t.Run("multiple stream indexes - sparse indexes", func(t *testing.T) {
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeAssistantGenText,
AssistantGenText: &AssistantGenText{
Text: "Index0-",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
{
Type: ContentBlockTypeAssistantGenText,
AssistantGenText: &AssistantGenText{
Text: "Index2-",
},
StreamingMeta: &StreamingMeta{Index: 2},
},
},
},
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeAssistantGenText,
AssistantGenText: &AssistantGenText{
Text: "Part2",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
{
Type: ContentBlockTypeAssistantGenText,
AssistantGenText: &AssistantGenText{
Text: "Part2",
},
StreamingMeta: &StreamingMeta{Index: 2},
},
},
},
}
result, err := ConcatAgenticMessages(msgs)
assert.NoError(t, err)
assert.Len(t, result.ContentBlocks, 2)
assert.Equal(t, "Index0-Part2", result.ContentBlocks[0].AssistantGenText.Text)
assert.Equal(t, "Index2-Part2", result.ContentBlocks[1].AssistantGenText.Text)
})
t.Run("multiple stream indexes - mixed content types", func(t *testing.T) {
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeAssistantGenText,
AssistantGenText: &AssistantGenText{
Text: "Text ",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
{
Type: ContentBlockTypeFunctionToolCall,
FunctionToolCall: &FunctionToolCall{
CallID: "call_1",
Name: "func1",
Arguments: `{"a`,
},
StreamingMeta: &StreamingMeta{Index: 1},
},
},
},
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeAssistantGenText,
AssistantGenText: &AssistantGenText{
Text: "Content",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
{
Type: ContentBlockTypeFunctionToolCall,
FunctionToolCall: &FunctionToolCall{
Arguments: `":1}`,
},
StreamingMeta: &StreamingMeta{Index: 1},
},
},
},
}
result, err := ConcatAgenticMessages(msgs)
assert.NoError(t, err)
assert.Len(t, result.ContentBlocks, 2)
assert.Equal(t, "Text Content", result.ContentBlocks[0].AssistantGenText.Text)
assert.Equal(t, "call_1", result.ContentBlocks[1].FunctionToolCall.CallID)
assert.Equal(t, "func1", result.ContentBlocks[1].FunctionToolCall.Name)
assert.Equal(t, `{"a":1}`, result.ContentBlocks[1].FunctionToolCall.Arguments)
})
t.Run("multiple stream indexes - three indexes", func(t *testing.T) {
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeAssistantGenText,
AssistantGenText: &AssistantGenText{
Text: "A",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
{
Type: ContentBlockTypeAssistantGenText,
AssistantGenText: &AssistantGenText{
Text: "B",
},
StreamingMeta: &StreamingMeta{Index: 1},
},
{
Type: ContentBlockTypeAssistantGenText,
AssistantGenText: &AssistantGenText{
Text: "C",
},
StreamingMeta: &StreamingMeta{Index: 2},
},
},
},
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeAssistantGenText,
AssistantGenText: &AssistantGenText{
Text: "1",
},
StreamingMeta: &StreamingMeta{Index: 0},
},
{
Type: ContentBlockTypeAssistantGenText,
AssistantGenText: &AssistantGenText{
Text: "2",
},
StreamingMeta: &StreamingMeta{Index: 1},
},
{
Type: ContentBlockTypeAssistantGenText,
AssistantGenText: &AssistantGenText{
Text: "3",
},
StreamingMeta: &StreamingMeta{Index: 2},
},
},
},
}
result, err := ConcatAgenticMessages(msgs)
assert.NoError(t, err)
assert.Len(t, result.ContentBlocks, 3)
assert.Equal(t, "A1", result.ContentBlocks[0].AssistantGenText.Text)
assert.Equal(t, "B2", result.ContentBlocks[1].AssistantGenText.Text)
assert.Equal(t, "C3", result.ContentBlocks[2].AssistantGenText.Text)
})
}
func TestAgenticMessageFormat(t *testing.T) {
m := &AgenticMessage{
Role: AgenticRoleTypeUser,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeUserInputText,
UserInputText: &UserInputText{Text: "{a}"},
},
{
Type: ContentBlockTypeUserInputImage,
UserInputImage: &UserInputImage{
URL: "{b}",
Base64Data: "{c}",
},
},
{
Type: ContentBlockTypeUserInputAudio,
UserInputAudio: &UserInputAudio{
URL: "{d}",
Base64Data: "{e}",
},
},
{
Type: ContentBlockTypeUserInputVideo,
UserInputVideo: &UserInputVideo{
URL: "{f}",
Base64Data: "{g}",
},
},
{
Type: ContentBlockTypeUserInputFile,
UserInputFile: &UserInputFile{
URL: "{h}",
Base64Data: "{i}",
},
},
},
}
result, err := m.Format(context.Background(), map[string]any{
"a": "1", "b": "2", "c": "3", "d": "4", "e": "5", "f": "6", "g": "7", "h": "8", "i": "9",
}, FString)
assert.NoError(t, err)
assert.Equal(t, []*AgenticMessage{{
Role: AgenticRoleTypeUser,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeUserInputText,
UserInputText: &UserInputText{Text: "1"},
},
{
Type: ContentBlockTypeUserInputImage,
UserInputImage: &UserInputImage{
URL: "2",
Base64Data: "3",
},
},
{
Type: ContentBlockTypeUserInputAudio,
UserInputAudio: &UserInputAudio{
URL: "4",
Base64Data: "5",
},
},
{
Type: ContentBlockTypeUserInputVideo,
UserInputVideo: &UserInputVideo{
URL: "6",
Base64Data: "7",
},
},
{
Type: ContentBlockTypeUserInputFile,
UserInputFile: &UserInputFile{
URL: "8",
Base64Data: "9",
},
},
},
}}, result)
}
func TestAgenticPlaceholderFormat(t *testing.T) {
ctx := context.Background()
ph := AgenticMessagesPlaceholder("a", false)
result, err := ph.Format(ctx, map[string]any{
"a": []*AgenticMessage{{Role: AgenticRoleTypeUser}, {Role: AgenticRoleTypeUser}},
}, FString)
assert.NoError(t, err)
assert.Equal(t, 2, len(result))
ph = AgenticMessagesPlaceholder("a", true)
result, err = ph.Format(ctx, map[string]any{}, FString)
assert.NoError(t, err)
assert.Equal(t, 0, len(result))
}
func ptrOf[T any](v T) *T {
return &v
}
func TestAgenticMessageString(t *testing.T) {
longBase64 := "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
msg := &AgenticMessage{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeUserInputText,
UserInputText: &UserInputText{
Text: "What's the weather like in New York City today?",
},
},
{
Type: ContentBlockTypeUserInputImage,
UserInputImage: &UserInputImage{
URL: "https://example.com/weather-map.jpg",
Base64Data: longBase64,
MIMEType: "image/jpeg",
Detail: ImageURLDetailHigh,
},
},
{
Type: ContentBlockTypeUserInputAudio,
UserInputAudio: &UserInputAudio{
URL: "http://audio.com",
Base64Data: "audio_data",
MIMEType: "audio/mp3",
},
},
{
Type: ContentBlockTypeUserInputVideo,
UserInputVideo: &UserInputVideo{
URL: "http://video.com",
Base64Data: "video_data",
MIMEType: "video/mp4",
},
},
{
Type: ContentBlockTypeUserInputFile,
UserInputFile: &UserInputFile{
URL: "http://file.com",
Name: "file.txt",
Base64Data: "file_data",
MIMEType: "text/plain",
},
},
{
Type: ContentBlockTypeAssistantGenText,
AssistantGenText: &AssistantGenText{
Text: "I'll check the current weather in New York City for you.",
},
},
{
Type: ContentBlockTypeAssistantGenImage,
AssistantGenImage: &AssistantGenImage{
URL: "http://gen_image.com",
Base64Data: "gen_image_data",
MIMEType: "image/png",
},
},
{
Type: ContentBlockTypeAssistantGenAudio,
AssistantGenAudio: &AssistantGenAudio{
URL: "http://gen_audio.com",
Base64Data: "gen_audio_data",
MIMEType: "audio/wav",
},
},
{
Type: ContentBlockTypeAssistantGenVideo,
AssistantGenVideo: &AssistantGenVideo{
URL: "http://gen_video.com",
Base64Data: "gen_video_data",
MIMEType: "video/mp4",
},
},
{
Type: ContentBlockTypeReasoning,
Reasoning: &Reasoning{
Text: "First, I need to identify the location (New York City) from the user's query.\n" +
"Then, I should call the weather API to get current conditions.\n" +
"Finally, I'll format the response in a user-friendly way with temperature and conditions.",
Signature: "encrypted_reasoning_content_that_is_very_long_and_will_be_truncated_for_display",
},
},
{
Type: ContentBlockTypeFunctionToolCall,
FunctionToolCall: &FunctionToolCall{
CallID: "call_weather_123",
Name: "get_current_weather",
Arguments: `{"location":"New York City","unit":"fahrenheit"}`,
},
StreamingMeta: &StreamingMeta{Index: 0},
},
{
Type: ContentBlockTypeFunctionToolResult,
FunctionToolResult: &FunctionToolResult{
CallID: "call_weather_123",
Name: "get_current_weather",
Content: []*FunctionToolResultContentBlock{
{Type: FunctionToolResultContentBlockTypeText, Text: &UserInputText{Text: `{"temperature":72,"condition":"sunny","humidity":45,"wind_speed":8}`}},
},
},
},
{
Type: ContentBlockTypeServerToolCall,
ServerToolCall: &ServerToolCall{
Name: "server_tool",
CallID: "call_1",
Arguments: map[string]any{"a": 1},
},
},
{
Type: ContentBlockTypeServerToolResult,
ServerToolResult: &ServerToolResult{
Name: "server_tool",
CallID: "call_1",
Content: map[string]any{"success": true},
},
},
{
Type: ContentBlockTypeMCPToolApprovalRequest,
MCPToolApprovalRequest: &MCPToolApprovalRequest{
ID: "req_1",
Name: "mcp_tool",
ServerLabel: "mcp_server",
Arguments: "{}",
},
},
{
Type: ContentBlockTypeMCPToolApprovalResponse,
MCPToolApprovalResponse: &MCPToolApprovalResponse{
ApprovalRequestID: "req_1",
Approve: true,
Reason: "looks good",
},
},
{
Type: ContentBlockTypeMCPToolCall,
MCPToolCall: &MCPToolCall{
ServerLabel: "weather-mcp-server",
CallID: "mcp_forecast_456",
Name: "get_7day_forecast",
Arguments: `{"city":"New York","days":7}`,
},
},
{
Type: ContentBlockTypeMCPToolResult,
MCPToolResult: &MCPToolResult{
CallID: "mcp_forecast_456",
Name: "get_7day_forecast",
Content: `{"status":"partial","days_available":3}`,
Error: &MCPToolCallError{
Code: ptrOf[int64](503),
Message: "Service temporarily unavailable for full 7-day forecast",
},
},
},
{
Type: ContentBlockTypeMCPListToolsResult,
MCPListToolsResult: &MCPListToolsResult{
ServerLabel: "weather-mcp-server",
Tools: []*MCPListToolsItem{
{Name: "get_current_weather", Description: "Get current weather conditions for a location"},
{Name: "get_7day_forecast", Description: "Get 7-day weather forecast"},
{Name: "get_weather_alerts", Description: "Get active weather alerts and warnings"},
},
},
},
},
ResponseMeta: &AgenticResponseMeta{
TokenUsage: &TokenUsage{
PromptTokens: 250,
CompletionTokens: 180,
TotalTokens: 430,
},
},
}
// Print the formatted output
output := msg.String()
assert.Equal(t, `role: assistant
content_blocks:
[0] type: user_input_text
text: What's the weather like in New York City today?
[1] type: user_input_image
url: https://example.com/weather-map.jpg
base64_data: iVBORw0KGgoAAAANSUhE...... (96 bytes)
mime_type: image/jpeg
detail: high
[2] type: user_input_audio
url: http://audio.com
base64_data: audio_data... (10 bytes)
mime_type: audio/mp3
[3] type: user_input_video
url: http://video.com
base64_data: video_data... (10 bytes)
mime_type: video/mp4
[4] type: user_input_file
name: file.txt
url: http://file.com
base64_data: file_data... (9 bytes)
mime_type: text/plain
[5] type: assistant_gen_text
text: I'll check the current weather in New York City for you.
[6] type: assistant_gen_image
url: http://gen_image.com
base64_data: gen_image_data... (14 bytes)
mime_type: image/png
[7] type: assistant_gen_audio
url: http://gen_audio.com
base64_data: gen_audio_data... (14 bytes)
mime_type: audio/wav
[8] type: assistant_gen_video
url: http://gen_video.com
base64_data: gen_video_data... (14 bytes)
mime_type: video/mp4
[9] type: reasoning
text: First, I need to identify the location (New York City) from the user's query.
Then, I should call the weather API to get current conditions.
Finally, I'll format the response in a user-friendly way with temperature and conditions.
signature: encrypted_reasoning_content_that_is_very_long_and_...
[10] type: function_tool_call
call_id: call_weather_123
name: get_current_weather
arguments: {"location":"New York City","unit":"fahrenheit"}
stream_index: 0
[11] type: function_tool_result
call_id: call_weather_123
name: get_current_weather
content: (1 blocks)
[0] text: {"temperature":72,"condition":"sunny","humidity":45,"wind_speed":8}
[12] type: server_tool_call
name: server_tool
call_id: call_1
arguments: {
"a": 1
}
[13] type: server_tool_result
name: server_tool
call_id: call_1
content: {
"success": true
}
[14] type: mcp_tool_approval_request
server_label: mcp_server
id: req_1
name: mcp_tool
arguments: {}
[15] type: mcp_tool_approval_response
approval_request_id: req_1
approve: true
reason: looks good
[16] type: mcp_tool_call
server_label: weather-mcp-server
call_id: mcp_forecast_456
name: get_7day_forecast
arguments: {"city":"New York","days":7}
[17] type: mcp_tool_result
call_id: mcp_forecast_456
name: get_7day_forecast
content: {"status":"partial","days_available":3}
error: [503] Service temporarily unavailable for full 7-day forecast
[18] type: mcp_list_tools_result
server_label: weather-mcp-server
tools: 3 items
- get_current_weather: Get current weather conditions for a location
- get_7day_forecast: Get 7-day weather forecast
- get_weather_alerts: Get active weather alerts and warnings
response_meta:
token_usage: prompt=250, completion=180, total=430
`, output)
t.Run("nil/empty fields", func(t *testing.T) {
msg := &AgenticMessage{
Role: AgenticRoleTypeUser,
ContentBlocks: []*ContentBlock{
{Type: ContentBlockTypeUserInputAudio, UserInputAudio: &UserInputAudio{}}, // empty
{Type: ContentBlockTypeUserInputVideo, UserInputVideo: &UserInputVideo{}},
{Type: ContentBlockTypeUserInputFile, UserInputFile: &UserInputFile{}},
{Type: ContentBlockTypeAssistantGenImage, AssistantGenImage: &AssistantGenImage{}},
{Type: ContentBlockTypeAssistantGenAudio, AssistantGenAudio: &AssistantGenAudio{}},
{Type: ContentBlockTypeAssistantGenVideo, AssistantGenVideo: &AssistantGenVideo{}},
{Type: ContentBlockTypeServerToolCall, ServerToolCall: &ServerToolCall{Name: "t"}}, // No CallID
{Type: ContentBlockTypeServerToolResult, ServerToolResult: &ServerToolResult{Name: "t"}}, // No CallID
{Type: ContentBlockTypeMCPToolResult, MCPToolResult: &MCPToolResult{Name: "t"}}, // No Error
{Type: ContentBlockTypeMCPListToolsResult, MCPListToolsResult: &MCPListToolsResult{}}, // No Error
{Type: ContentBlockTypeMCPToolApprovalResponse, MCPToolApprovalResponse: &MCPToolApprovalResponse{Approve: false}}, // No Reason
nil, // Nil block in slice
},
}
s := msg.String()
assert.Contains(t, s, "type: user_input_audio")
assert.NotContains(t, s, "mime_type:")
assert.Contains(t, s, "type: server_tool_call")
})
t.Run("nil content struct in block", func(t *testing.T) {
// Test cases where the specific content struct is nil but type is set
// This shouldn't crash and should just print type
msg := &AgenticMessage{
ContentBlocks: []*ContentBlock{
{Type: ContentBlockTypeReasoning, Reasoning: nil},
{Type: ContentBlockTypeUserInputText, UserInputText: nil},
{Type: ContentBlockTypeUserInputImage, UserInputImage: nil},
{Type: ContentBlockTypeUserInputAudio, UserInputAudio: nil},
{Type: ContentBlockTypeUserInputVideo, UserInputVideo: nil},
{Type: ContentBlockTypeUserInputFile, UserInputFile: nil},
{Type: ContentBlockTypeAssistantGenText, AssistantGenText: nil},
{Type: ContentBlockTypeAssistantGenImage, AssistantGenImage: nil},
{Type: ContentBlockTypeAssistantGenAudio, AssistantGenAudio: nil},
{Type: ContentBlockTypeAssistantGenVideo, AssistantGenVideo: nil},
{Type: ContentBlockTypeFunctionToolCall, FunctionToolCall: nil},
{Type: ContentBlockTypeFunctionToolResult, FunctionToolResult: nil},
{Type: ContentBlockTypeServerToolCall, ServerToolCall: nil},
{Type: ContentBlockTypeServerToolResult, ServerToolResult: nil},
{Type: ContentBlockTypeMCPToolCall, MCPToolCall: nil},
{Type: ContentBlockTypeMCPToolResult, MCPToolResult: nil},
{Type: ContentBlockTypeMCPListToolsResult, MCPListToolsResult: nil},
{Type: ContentBlockTypeMCPToolApprovalRequest, MCPToolApprovalRequest: nil},
{Type: ContentBlockTypeMCPToolApprovalResponse, MCPToolApprovalResponse: nil},
},
}
s := msg.String()
assert.Contains(t, s, "type: reasoning")
// ensure no panic and basic output present
})
}
func TestSystemAgenticMessage(t *testing.T) {
t.Run("basic", func(t *testing.T) {
msg := SystemAgenticMessage("system")
assert.Equal(t, AgenticRoleTypeSystem, msg.Role)
assert.Len(t, msg.ContentBlocks, 1)
assert.Equal(t, "system", msg.ContentBlocks[0].UserInputText.Text)
})
}
func TestUserAgenticMessage(t *testing.T) {
t.Run("basic", func(t *testing.T) {
msg := UserAgenticMessage("user")
assert.Equal(t, AgenticRoleTypeUser, msg.Role)
assert.Len(t, msg.ContentBlocks, 1)
assert.Equal(t, "user", msg.ContentBlocks[0].UserInputText.Text)
})
}
func TestNewContentBlock(t *testing.T) {
cbType := reflect.TypeOf(ContentBlock{})
for i := 0; i < cbType.NumField(); i++ {
field := cbType.Field(i)
// Skip non-content fields
if field.Name == "Type" || field.Name == "Extra" || field.Name == "StreamingMeta" {
continue
}
t.Run(field.Name, func(t *testing.T) {
// Ensure field is a pointer
assert.Equal(t, reflect.Ptr, field.Type.Kind(), "Field %s should be a pointer", field.Name)
// Create a new instance of the field's type
// field.Type is *T, so Elem() is T. reflect.New(T) returns *T.
elemType := field.Type.Elem()
inputVal := reflect.New(elemType)
input := inputVal.Interface()
// Call NewContentBlock (generic) via type switch
var block *ContentBlock
switch v := input.(type) {
case *Reasoning:
block = NewContentBlock(v)
case *UserInputText:
block = NewContentBlock(v)
case *UserInputImage:
block = NewContentBlock(v)
case *UserInputAudio:
block = NewContentBlock(v)
case *UserInputVideo:
block = NewContentBlock(v)
case *UserInputFile:
block = NewContentBlock(v)
case *ToolSearchFunctionToolResult:
block = NewContentBlock(v)
case *AssistantGenText:
block = NewContentBlock(v)
case *AssistantGenImage:
block = NewContentBlock(v)
case *AssistantGenAudio:
block = NewContentBlock(v)
case *AssistantGenVideo:
block = NewContentBlock(v)
case *FunctionToolCall:
block = NewContentBlock(v)
case *FunctionToolResult:
block = NewContentBlock(v)
case *ServerToolCall:
block = NewContentBlock(v)
case *ServerToolResult:
block = NewContentBlock(v)
case *MCPToolCall:
block = NewContentBlock(v)
case *MCPToolResult:
block = NewContentBlock(v)
case *MCPListToolsResult:
block = NewContentBlock(v)
case *MCPToolApprovalRequest:
block = NewContentBlock(v)
case *MCPToolApprovalResponse:
block = NewContentBlock(v)
default:
t.Fatalf("unsupported ContentBlock field type: %T", input)
}
// Assertions
assert.NotNil(t, block, "NewContentBlock should return non-nil for type %T", input)
// Check if the corresponding field in block is set equals to input
blockVal := reflect.ValueOf(block).Elem()
fieldVal := blockVal.FieldByName(field.Name)
assert.True(t, fieldVal.IsValid(), "Field %s not found in result", field.Name)
assert.Equal(t, input, fieldVal.Interface(), "Field %s should match input", field.Name)
// Check Type is set
typeVal := blockVal.FieldByName("Type")
assert.NotEmpty(t, typeVal.String(), "Type should be set for %s", field.Name)
})
}
}
func TestNewContentBlockChunk_NilMeta(t *testing.T) {
require.NotPanics(t, func() {
block := NewContentBlockChunk(&AssistantGenText{Text: "test"}, nil)
require.NotNil(t, block)
assert.Nil(t, block.StreamingMeta)
}, "NewContentBlockChunk should handle nil meta without panic")
}
func TestConcatAssistantGenTexts_ExtensionOverwrite(t *testing.T) {
type testExtension struct {
Value string
}
texts := []*AssistantGenText{
{Text: "Hello ", Extension: &testExtension{Value: "ext1"}},
{Text: "world", Extension: &testExtension{Value: "ext2"}},
}
result, err := concatAssistantGenTexts(texts)
if err != nil {
t.Logf("Concat error (may be expected if ConcatSliceValue doesn't handle this type): %v", err)
t.Skip("Skipping: ConcatSliceValue doesn't support test type")
}
require.NotNil(t, result)
assert.Equal(t, "Hello world", result.Text)
if result.Extension != nil {
t.Logf("Extension type: %T, value: %v", result.Extension, result.Extension)
_, isSlice := result.Extension.([]*testExtension)
if isSlice {
t.Log("WARNING: Extension is a raw slice instead of a concatenated value. " +
"Line 1381 in agentic_message.go overwrites the ConcatSliceValue result " +
"with extensions.Interface(), discarding the concatenation.")
}
}
}
func TestFunctionToolResultBlockString(t *testing.T) {
t.Run("empty type", func(t *testing.T) {
b := &FunctionToolResultContentBlock{Text: &UserInputText{Text: "x"}}
assert.Equal(t, "unknown block type: <empty>\n", b.String())
})
t.Run("known type but empty payload", func(t *testing.T) {
b := &FunctionToolResultContentBlock{Type: FunctionToolResultContentBlockTypeText}
assert.Equal(t, "empty text block\n", b.String())
})
t.Run("unknown type value", func(t *testing.T) {
b := &FunctionToolResultContentBlock{Type: FunctionToolResultContentBlockType("weird")}
assert.Equal(t, "unknown block type: weird\n", b.String())
})
}
func TestConcatFunctionToolResults(t *testing.T) {
t.Run("direct append", func(t *testing.T) {
results := []*FunctionToolResult{
{CallID: "c1", Name: "tool1", Content: []*FunctionToolResultContentBlock{
{Type: FunctionToolResultContentBlockTypeText, Text: &UserInputText{Text: "hello"}},
}},
{CallID: "c1", Name: "tool1", Content: []*FunctionToolResultContentBlock{
{Type: FunctionToolResultContentBlockTypeImage, Image: &UserInputImage{URL: "http://img.png"}},
}},
}
got, err := concatFunctionToolResults(results)
require.NoError(t, err)
assert.Len(t, got.Content, 2)
assert.Equal(t, "hello", got.Content[0].Text.Text)
assert.Equal(t, "http://img.png", got.Content[1].Image.URL)
})
}