Files
2026-07-13 12:49:10 +08:00

261 lines
8.2 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 (
"context"
"github.com/cloudwego/eino/schema"
)
// NewAgenticToolsNode creates a new AgenticToolsNode.
// e.g.
//
// conf := &ToolsNodeConfig{
// Tools: []tool.BaseTool{invokableTool1, streamableTool2},
// }
// toolsNode, err := NewAgenticToolsNode(ctx, conf)
func NewAgenticToolsNode(ctx context.Context, conf *ToolsNodeConfig) (*AgenticToolsNode, error) {
tn, err := NewToolNode(ctx, conf)
if err != nil {
return nil, err
}
return &AgenticToolsNode{inner: tn}, nil
}
type AgenticToolsNode struct {
inner *ToolsNode
}
func (a *AgenticToolsNode) Invoke(ctx context.Context, input *schema.AgenticMessage, opts ...ToolsNodeOption) ([]*schema.AgenticMessage, error) {
result, err := a.inner.Invoke(ctx, agenticMessageToToolCallMessage(input), opts...)
if err != nil {
return nil, err
}
return toolMessageToAgenticMessage(result), nil
}
func (a *AgenticToolsNode) Stream(ctx context.Context, input *schema.AgenticMessage,
opts ...ToolsNodeOption) (*schema.StreamReader[[]*schema.AgenticMessage], error) {
result, err := a.inner.Stream(ctx, agenticMessageToToolCallMessage(input), opts...)
if err != nil {
return nil, err
}
return streamToolMessageToAgenticMessage(result), nil
}
func agenticMessageToToolCallMessage(input *schema.AgenticMessage) *schema.Message {
var tc []schema.ToolCall
for _, block := range input.ContentBlocks {
if block.Type != schema.ContentBlockTypeFunctionToolCall || block.FunctionToolCall == nil {
continue
}
tc = append(tc, schema.ToolCall{
ID: block.FunctionToolCall.CallID,
Function: schema.FunctionCall{
Name: block.FunctionToolCall.Name,
Arguments: block.FunctionToolCall.Arguments,
},
Extra: block.Extra,
})
}
return &schema.Message{
Role: schema.Assistant,
ToolCalls: tc,
}
}
func toolMessageToAgenticMessage(input []*schema.Message) []*schema.AgenticMessage {
results := make([]*schema.AgenticMessage, len(input))
for i, m := range input {
if msg, ok := toolSearchResultMessageToAgenticMessage(m, nil); ok {
results[i] = msg
continue
}
ftr := &schema.FunctionToolResult{
CallID: m.ToolCallID,
Name: m.ToolName,
}
if len(m.UserInputMultiContent) > 0 {
ftr.Content = messageInputPartsToFunctionToolBlocks(m.UserInputMultiContent)
} else if m.Content != "" {
ftr.Content = []*schema.FunctionToolResultContentBlock{
newFuncToolResultContentBlock(&schema.UserInputText{Text: m.Content}),
}
}
results[i] = &schema.AgenticMessage{
Role: schema.AgenticRoleTypeUser,
ContentBlocks: []*schema.ContentBlock{{
Type: schema.ContentBlockTypeFunctionToolResult,
FunctionToolResult: ftr,
Extra: m.Extra,
}},
Extra: m.Extra,
}
}
return results
}
func streamToolMessageToAgenticMessage(input *schema.StreamReader[[]*schema.Message]) *schema.StreamReader[[]*schema.AgenticMessage] {
return schema.StreamReaderWithConvert(input, func(t []*schema.Message) ([]*schema.AgenticMessage, error) {
results := make([]*schema.AgenticMessage, len(t))
for i, m := range t {
if m == nil {
continue
}
if msg, ok := toolSearchResultMessageToAgenticMessage(m, &schema.StreamingMeta{Index: i}); ok {
results[i] = msg
continue
}
ftr := &schema.FunctionToolResult{
CallID: m.ToolCallID,
Name: m.ToolName,
}
if len(m.UserInputMultiContent) > 0 {
ftr.Content = messageInputPartsToFunctionToolBlocks(m.UserInputMultiContent)
} else if m.Content != "" {
ftr.Content = []*schema.FunctionToolResultContentBlock{
newFuncToolResultContentBlock(&schema.UserInputText{Text: m.Content}),
}
}
results[i] = &schema.AgenticMessage{
Role: schema.AgenticRoleTypeUser,
ContentBlocks: []*schema.ContentBlock{{
Type: schema.ContentBlockTypeFunctionToolResult,
FunctionToolResult: ftr,
StreamingMeta: &schema.StreamingMeta{Index: i},
Extra: m.Extra,
}},
Extra: m.Extra,
}
}
return results, nil
})
}
func toolSearchResultMessageToAgenticMessage(m *schema.Message, meta *schema.StreamingMeta) (*schema.AgenticMessage, bool) {
if m == nil || len(m.UserInputMultiContent) != 1 {
return nil, false
}
part := m.UserInputMultiContent[0]
if part.Type != schema.ChatMessagePartTypeToolSearchResult || part.ToolSearchResult == nil {
return nil, false
}
block := schema.NewContentBlock(&schema.ToolSearchFunctionToolResult{
CallID: m.ToolCallID,
Name: m.ToolName,
Result: part.ToolSearchResult,
})
block.StreamingMeta = meta
block.Extra = m.Extra
return &schema.AgenticMessage{
Role: schema.AgenticRoleTypeUser,
ContentBlocks: []*schema.ContentBlock{block},
Extra: m.Extra,
}, true
}
func messageInputPartsToFunctionToolBlocks(parts []schema.MessageInputPart) []*schema.FunctionToolResultContentBlock {
blocks := make([]*schema.FunctionToolResultContentBlock, 0, len(parts))
for _, p := range parts {
var block *schema.FunctionToolResultContentBlock
switch p.Type {
case schema.ChatMessagePartTypeText:
block = newFuncToolResultContentBlock(&schema.UserInputText{Text: p.Text})
block.Extra = p.Extra
case schema.ChatMessagePartTypeImageURL:
if p.Image != nil {
block = newFuncToolResultContentBlock(&schema.UserInputImage{
URL: derefString(p.Image.URL),
Base64Data: derefString(p.Image.Base64Data),
MIMEType: p.Image.MIMEType,
Detail: p.Image.Detail,
})
block.Extra = p.Extra
}
case schema.ChatMessagePartTypeAudioURL:
if p.Audio != nil {
block = newFuncToolResultContentBlock(&schema.UserInputAudio{
URL: derefString(p.Audio.URL),
Base64Data: derefString(p.Audio.Base64Data),
MIMEType: p.Audio.MIMEType,
})
block.Extra = p.Extra
}
case schema.ChatMessagePartTypeVideoURL:
if p.Video != nil {
block = newFuncToolResultContentBlock(&schema.UserInputVideo{
URL: derefString(p.Video.URL),
Base64Data: derefString(p.Video.Base64Data),
MIMEType: p.Video.MIMEType,
})
block.Extra = p.Extra
}
case schema.ChatMessagePartTypeFileURL:
if p.File != nil {
block = newFuncToolResultContentBlock(&schema.UserInputFile{
URL: derefString(p.File.URL),
Base64Data: derefString(p.File.Base64Data),
Name: p.File.Name,
MIMEType: p.File.MIMEType,
})
block.Extra = p.Extra
}
}
if block != nil {
blocks = append(blocks, block)
}
}
return blocks
}
type userInputVariant interface {
schema.UserInputText | schema.UserInputImage | schema.UserInputAudio | schema.UserInputVideo | schema.UserInputFile
}
// newFuncToolResultContentBlock creates a FunctionToolResultContentBlock from a typed content pointer.
func newFuncToolResultContentBlock[T userInputVariant](content *T) *schema.FunctionToolResultContentBlock {
switch c := any(content).(type) {
case *schema.UserInputText:
return &schema.FunctionToolResultContentBlock{Type: schema.FunctionToolResultContentBlockTypeText, Text: c}
case *schema.UserInputImage:
return &schema.FunctionToolResultContentBlock{Type: schema.FunctionToolResultContentBlockTypeImage, Image: c}
case *schema.UserInputAudio:
return &schema.FunctionToolResultContentBlock{Type: schema.FunctionToolResultContentBlockTypeAudio, Audio: c}
case *schema.UserInputVideo:
return &schema.FunctionToolResultContentBlock{Type: schema.FunctionToolResultContentBlockTypeVideo, Video: c}
case *schema.UserInputFile:
return &schema.FunctionToolResultContentBlock{Type: schema.FunctionToolResultContentBlockTypeFile, File: c}
default:
return nil
}
}
func derefString(s *string) string {
if s == nil {
return ""
}
return *s
}
func (a *AgenticToolsNode) GetType() string { return "" }