2285 lines
69 KiB
Go
2285 lines
69 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 (
|
|
"bytes"
|
|
"context"
|
|
"encoding/gob"
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/bytedance/sonic"
|
|
"github.com/eino-contrib/jsonschema"
|
|
|
|
"github.com/cloudwego/eino/internal"
|
|
"github.com/cloudwego/eino/schema/claude"
|
|
"github.com/cloudwego/eino/schema/gemini"
|
|
"github.com/cloudwego/eino/schema/openai"
|
|
)
|
|
|
|
type ContentBlockType string
|
|
|
|
const (
|
|
ContentBlockTypeReasoning ContentBlockType = "reasoning"
|
|
ContentBlockTypeUserInputText ContentBlockType = "user_input_text"
|
|
ContentBlockTypeUserInputImage ContentBlockType = "user_input_image"
|
|
ContentBlockTypeUserInputAudio ContentBlockType = "user_input_audio"
|
|
ContentBlockTypeUserInputVideo ContentBlockType = "user_input_video"
|
|
ContentBlockTypeUserInputFile ContentBlockType = "user_input_file"
|
|
ContentBlockTypeToolSearchResult ContentBlockType = "tool_search_result"
|
|
ContentBlockTypeAssistantGenText ContentBlockType = "assistant_gen_text"
|
|
ContentBlockTypeAssistantGenImage ContentBlockType = "assistant_gen_image"
|
|
ContentBlockTypeAssistantGenAudio ContentBlockType = "assistant_gen_audio"
|
|
ContentBlockTypeAssistantGenVideo ContentBlockType = "assistant_gen_video"
|
|
ContentBlockTypeFunctionToolCall ContentBlockType = "function_tool_call"
|
|
ContentBlockTypeFunctionToolResult ContentBlockType = "function_tool_result"
|
|
ContentBlockTypeServerToolCall ContentBlockType = "server_tool_call"
|
|
ContentBlockTypeServerToolResult ContentBlockType = "server_tool_result"
|
|
ContentBlockTypeMCPToolCall ContentBlockType = "mcp_tool_call"
|
|
ContentBlockTypeMCPToolResult ContentBlockType = "mcp_tool_result"
|
|
ContentBlockTypeMCPListToolsResult ContentBlockType = "mcp_list_tools_result"
|
|
ContentBlockTypeMCPToolApprovalRequest ContentBlockType = "mcp_tool_approval_request"
|
|
ContentBlockTypeMCPToolApprovalResponse ContentBlockType = "mcp_tool_approval_response"
|
|
)
|
|
|
|
type AgenticRoleType string
|
|
|
|
const (
|
|
AgenticRoleTypeSystem AgenticRoleType = "system"
|
|
AgenticRoleTypeUser AgenticRoleType = "user"
|
|
AgenticRoleTypeAssistant AgenticRoleType = "assistant"
|
|
)
|
|
|
|
type AgenticMessage struct {
|
|
// Role is the message role.
|
|
Role AgenticRoleType `json:"role"`
|
|
|
|
// ContentBlocks is the list of content blocks.
|
|
ContentBlocks []*ContentBlock `json:"content_blocks,omitempty"`
|
|
|
|
// ResponseMeta is the response metadata.
|
|
ResponseMeta *AgenticResponseMeta `json:"response_meta,omitempty"`
|
|
|
|
// Extra is the additional information.
|
|
Extra map[string]any `json:"extra,omitempty"`
|
|
}
|
|
|
|
type AgenticResponseMeta struct {
|
|
// TokenUsage is the token usage.
|
|
TokenUsage *TokenUsage `json:"token_usage,omitempty"`
|
|
|
|
// OpenAIExtension is the extension for OpenAI.
|
|
OpenAIExtension *openai.ResponseMetaExtension `json:"openai_extension,omitempty"`
|
|
|
|
// GeminiExtension is the extension for Gemini.
|
|
GeminiExtension *gemini.ResponseMetaExtension `json:"gemini_extension,omitempty"`
|
|
|
|
// ClaudeExtension is the extension for Claude.
|
|
ClaudeExtension *claude.ResponseMetaExtension `json:"claude_extension,omitempty"`
|
|
|
|
// Extension is the extension for other models, supplied by the component implementer.
|
|
Extension any `json:"extension,omitempty"`
|
|
}
|
|
|
|
type ContentBlock struct {
|
|
Type ContentBlockType `json:"type"`
|
|
|
|
// Reasoning contains the reasoning content generated by the model.
|
|
Reasoning *Reasoning `json:"reasoning,omitempty"`
|
|
|
|
// UserInputText contains the text content provided by the user.
|
|
UserInputText *UserInputText `json:"user_input_text,omitempty"`
|
|
|
|
// UserInputImage contains the image content provided by the user.
|
|
UserInputImage *UserInputImage `json:"user_input_image,omitempty"`
|
|
|
|
// UserInputAudio contains the audio content provided by the user.
|
|
UserInputAudio *UserInputAudio `json:"user_input_audio,omitempty"`
|
|
|
|
// UserInputVideo contains the video content provided by the user.
|
|
UserInputVideo *UserInputVideo `json:"user_input_video,omitempty"`
|
|
|
|
// UserInputFile contains the file content provided by the user.
|
|
UserInputFile *UserInputFile `json:"user_input_file,omitempty"`
|
|
|
|
// AssistantGenText contains the text content generated by the model.
|
|
AssistantGenText *AssistantGenText `json:"assistant_gen_text,omitempty"`
|
|
|
|
// AssistantGenImage contains the image content generated by the model.
|
|
AssistantGenImage *AssistantGenImage `json:"assistant_gen_image,omitempty"`
|
|
|
|
// AssistantGenAudio contains the audio content generated by the model.
|
|
AssistantGenAudio *AssistantGenAudio `json:"assistant_gen_audio,omitempty"`
|
|
|
|
// AssistantGenVideo contains the video content generated by the model.
|
|
AssistantGenVideo *AssistantGenVideo `json:"assistant_gen_video,omitempty"`
|
|
|
|
// FunctionToolCall contains the invocation details for a user-defined tool.
|
|
FunctionToolCall *FunctionToolCall `json:"function_tool_call,omitempty"`
|
|
|
|
// FunctionToolResult contains the result returned from a user-defined tool call.
|
|
FunctionToolResult *FunctionToolResult `json:"function_tool_result,omitempty"`
|
|
|
|
// ToolSearchFunctionToolResult contains the result of a client-side custom tool search tool call.
|
|
// It carries the full definitions of newly discovered tools so that the model can
|
|
// recognize which tools have been added and are now available for invocation.
|
|
ToolSearchFunctionToolResult *ToolSearchFunctionToolResult `json:"tool_search_function_tool_result,omitempty"`
|
|
|
|
// ServerToolCall contains the invocation details for a provider built-in tool executed on the model server.
|
|
ServerToolCall *ServerToolCall `json:"server_tool_call,omitempty"`
|
|
|
|
// ServerToolResult contains the result returned from a provider built-in tool executed on the model server.
|
|
ServerToolResult *ServerToolResult `json:"server_tool_result,omitempty"`
|
|
|
|
// MCPToolCall contains the invocation details for an MCP tool managed by the model server.
|
|
MCPToolCall *MCPToolCall `json:"mcp_tool_call,omitempty"`
|
|
|
|
// MCPToolResult contains the result returned from an MCP tool managed by the model server.
|
|
MCPToolResult *MCPToolResult `json:"mcp_tool_result,omitempty"`
|
|
|
|
// MCPListToolsResult contains the list of available MCP tools reported by the model server.
|
|
MCPListToolsResult *MCPListToolsResult `json:"mcp_list_tools_result,omitempty"`
|
|
|
|
// MCPToolApprovalRequest contains the user approval request for an MCP tool call when required.
|
|
MCPToolApprovalRequest *MCPToolApprovalRequest `json:"mcp_tool_approval_request,omitempty"`
|
|
|
|
// MCPToolApprovalResponse contains the user's approval decision for an MCP tool call.
|
|
MCPToolApprovalResponse *MCPToolApprovalResponse `json:"mcp_tool_approval_response,omitempty"`
|
|
|
|
// StreamingMeta contains metadata for streaming responses.
|
|
// Only set for streaming responses.
|
|
StreamingMeta *StreamingMeta `json:"streaming_meta,omitempty"`
|
|
|
|
// Extra contains additional information for the content block.
|
|
Extra map[string]any `json:"extra,omitempty"`
|
|
}
|
|
|
|
type StreamingMeta struct {
|
|
// Index specifies the index position of this block in the final response.
|
|
Index int `json:"index"`
|
|
}
|
|
|
|
type UserInputText struct {
|
|
// Text is the text content.
|
|
Text string `json:"text,omitempty"`
|
|
}
|
|
|
|
type UserInputImage struct {
|
|
// URL is the HTTP/HTTPS link.
|
|
URL string `json:"url,omitempty"`
|
|
|
|
// Base64Data is the binary data in Base64 encoded string format.
|
|
Base64Data string `json:"base64_data,omitempty"`
|
|
|
|
// MIMEType is the mime type, e.g. "image/png".
|
|
MIMEType string `json:"mime_type,omitempty"`
|
|
|
|
// Detail is the quality of the image url.
|
|
Detail ImageURLDetail `json:"detail,omitempty"`
|
|
}
|
|
|
|
type UserInputAudio struct {
|
|
// URL is the HTTP/HTTPS link.
|
|
URL string `json:"url,omitempty"`
|
|
|
|
// Base64Data is the binary data in Base64 encoded string format.
|
|
Base64Data string `json:"base64_data,omitempty"`
|
|
|
|
// MIMEType is the mime type, e.g. "audio/wav".
|
|
MIMEType string `json:"mime_type,omitempty"`
|
|
}
|
|
|
|
type UserInputVideo struct {
|
|
// URL is the HTTP/HTTPS link.
|
|
URL string `json:"url,omitempty"`
|
|
|
|
// Base64Data is the binary data in Base64 encoded string format.
|
|
Base64Data string `json:"base64_data,omitempty"`
|
|
|
|
// MIMEType is the mime type, e.g. "video/mp4".
|
|
MIMEType string `json:"mime_type,omitempty"`
|
|
}
|
|
|
|
type UserInputFile struct {
|
|
// URL is the HTTP/HTTPS link.
|
|
URL string `json:"url,omitempty"`
|
|
|
|
// Name is the filename.
|
|
Name string `json:"name,omitempty"`
|
|
|
|
// Base64Data is the binary data in Base64 encoded string format.
|
|
Base64Data string `json:"base64_data,omitempty"`
|
|
|
|
// MIMEType is the mime type, e.g. "application/pdf".
|
|
MIMEType string `json:"mime_type,omitempty"`
|
|
}
|
|
|
|
type AssistantGenText struct {
|
|
// Text is the generated text.
|
|
Text string `json:"text,omitempty"`
|
|
|
|
// OpenAIExtension is the extension for OpenAI.
|
|
OpenAIExtension *openai.AssistantGenTextExtension `json:"openai_extension,omitempty"`
|
|
|
|
// ClaudeExtension is the extension for Claude.
|
|
ClaudeExtension *claude.AssistantGenTextExtension `json:"claude_extension,omitempty"`
|
|
|
|
// Extension is the extension for other models, supplied by the component implementer.
|
|
Extension any `json:"extension,omitempty"`
|
|
}
|
|
|
|
type AssistantGenImage struct {
|
|
// URL is the HTTP/HTTPS link.
|
|
URL string `json:"url,omitempty"`
|
|
|
|
// Base64Data is the binary data in Base64 encoded string format.
|
|
Base64Data string `json:"base64_data,omitempty"`
|
|
|
|
// MIMEType is the mime type, e.g. "image/png".
|
|
MIMEType string `json:"mime_type,omitempty"`
|
|
}
|
|
|
|
type AssistantGenAudio struct {
|
|
// URL is the HTTP/HTTPS link.
|
|
URL string `json:"url,omitempty"`
|
|
|
|
// Base64Data is the binary data in Base64 encoded string format.
|
|
Base64Data string `json:"base64_data,omitempty"`
|
|
|
|
// MIMEType is the mime type, e.g. "audio/wav".
|
|
MIMEType string `json:"mime_type,omitempty"`
|
|
}
|
|
|
|
type AssistantGenVideo struct {
|
|
// URL is the HTTP/HTTPS link.
|
|
URL string `json:"url,omitempty"`
|
|
|
|
// Base64Data is the binary data in Base64 encoded string format.
|
|
Base64Data string `json:"base64_data,omitempty"`
|
|
|
|
// MIMEType is the mime type, e.g. "video/mp4".
|
|
MIMEType string `json:"mime_type,omitempty"`
|
|
}
|
|
|
|
type Reasoning struct {
|
|
// Text is either the thought summary or the raw reasoning text itself.
|
|
Text string `json:"text,omitempty"`
|
|
|
|
// Signature contains encrypted reasoning tokens.
|
|
// Required by some models when passing reasoning text back.
|
|
Signature string `json:"signature,omitempty"`
|
|
|
|
// OpenAIExtension is the extension for OpenAI.
|
|
OpenAIExtension *openai.ReasoningExtension `json:"openai_extension,omitempty"`
|
|
}
|
|
|
|
type FunctionToolCall struct {
|
|
// CallID is the unique identifier for the tool call.
|
|
CallID string `json:"call_id,omitempty"`
|
|
|
|
// Name specifies the function tool invoked.
|
|
Name string `json:"name"`
|
|
|
|
// Arguments is the JSON string arguments for the function tool call.
|
|
Arguments string `json:"arguments,omitempty"`
|
|
}
|
|
|
|
// FunctionToolResultContentBlockType identifies which media field of a
|
|
// FunctionToolResultContentBlock is populated.
|
|
type FunctionToolResultContentBlockType string
|
|
|
|
const (
|
|
FunctionToolResultContentBlockTypeText FunctionToolResultContentBlockType = "text"
|
|
FunctionToolResultContentBlockTypeImage FunctionToolResultContentBlockType = "image"
|
|
FunctionToolResultContentBlockTypeAudio FunctionToolResultContentBlockType = "audio"
|
|
FunctionToolResultContentBlockTypeVideo FunctionToolResultContentBlockType = "video"
|
|
FunctionToolResultContentBlockTypeFile FunctionToolResultContentBlockType = "file"
|
|
)
|
|
|
|
// FunctionToolResultContentBlock represents a single content block within a multimodal
|
|
// function tool result. Type identifies which of the media fields is populated;
|
|
// exactly one of the media fields should be set to match Type.
|
|
type FunctionToolResultContentBlock struct {
|
|
// Type identifies which media field below is populated.
|
|
Type FunctionToolResultContentBlockType `json:"type"`
|
|
// Text contains the text content of the block.
|
|
Text *UserInputText `json:"text,omitempty"`
|
|
// Image contains the image content of the block.
|
|
Image *UserInputImage `json:"image,omitempty"`
|
|
// Audio contains the audio content of the block.
|
|
Audio *UserInputAudio `json:"audio,omitempty"`
|
|
// Video contains the video content of the block.
|
|
Video *UserInputVideo `json:"video,omitempty"`
|
|
// File contains the file content of the block.
|
|
File *UserInputFile `json:"file,omitempty"`
|
|
|
|
// Extra holds additional metadata for model-specific or custom extensions.
|
|
Extra map[string]any `json:"extra,omitempty"`
|
|
}
|
|
|
|
func (b *FunctionToolResultContentBlock) String() string {
|
|
switch b.Type {
|
|
case FunctionToolResultContentBlockTypeText:
|
|
if b.Text != nil {
|
|
return b.Text.String()
|
|
}
|
|
return "empty text block\n"
|
|
case FunctionToolResultContentBlockTypeImage:
|
|
if b.Image != nil {
|
|
return b.Image.String()
|
|
}
|
|
return "empty image block\n"
|
|
case FunctionToolResultContentBlockTypeAudio:
|
|
if b.Audio != nil {
|
|
return b.Audio.String()
|
|
}
|
|
return "empty audio block\n"
|
|
case FunctionToolResultContentBlockTypeVideo:
|
|
if b.Video != nil {
|
|
return b.Video.String()
|
|
}
|
|
return "empty video block\n"
|
|
case FunctionToolResultContentBlockTypeFile:
|
|
if b.File != nil {
|
|
return b.File.String()
|
|
}
|
|
return "empty file block\n"
|
|
case "":
|
|
return "unknown block type: <empty>\n"
|
|
default:
|
|
return fmt.Sprintf("unknown block type: %s\n", b.Type)
|
|
}
|
|
}
|
|
|
|
type FunctionToolResult struct {
|
|
// CallID is the unique identifier for the tool call.
|
|
CallID string `json:"call_id,omitempty"`
|
|
|
|
// Name specifies the function tool invoked.
|
|
Name string `json:"name"`
|
|
|
|
// Content holds the tool execution output as an ordered list of content blocks.
|
|
// Each block carries its own type (text, image, audio, video, file), allowing
|
|
// text-only and multimodal results to share a uniform representation.
|
|
Content []*FunctionToolResultContentBlock `json:"content,omitempty"`
|
|
}
|
|
|
|
// ToolSearchFunctionToolResult represents the result of a client-side custom tool search
|
|
// function tool call. Unlike a regular FunctionToolResult, this carries a ToolSearchResult
|
|
// containing the full definitions of newly discovered tools, so the model can recognize
|
|
// which tools have been added and are now available for invocation.
|
|
type ToolSearchFunctionToolResult struct {
|
|
// CallID is the unique identifier for the tool call.
|
|
CallID string `json:"call_id,omitempty"`
|
|
|
|
// Name specifies the function tool invoked.
|
|
Name string `json:"name"`
|
|
|
|
// Result is the function tool result returned by the user
|
|
Result *ToolSearchResult `json:"result,omitempty"`
|
|
}
|
|
|
|
func (t *ToolSearchFunctionToolResult) String() string {
|
|
if t.Result != nil {
|
|
return t.Result.String()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
type ServerToolCall struct {
|
|
// Name specifies the server-side tool invoked.
|
|
// Supplied by the model server (e.g., `web_search` for OpenAI, `googleSearch` for Gemini).
|
|
Name string `json:"name"`
|
|
|
|
// CallID is the unique identifier for the tool call.
|
|
// Empty if not provided by the model server.
|
|
CallID string `json:"call_id,omitempty"`
|
|
|
|
// Arguments are the raw inputs to the server-side tool,
|
|
// supplied by the component implementer.
|
|
Arguments any `json:"arguments,omitempty"`
|
|
}
|
|
|
|
type ServerToolResult struct {
|
|
// Name specifies the server-side tool invoked.
|
|
// Supplied by the model server (e.g., `web_search` for OpenAI, `googleSearch` for Gemini).
|
|
Name string `json:"name"`
|
|
|
|
// CallID is the unique identifier for the tool call.
|
|
// Empty if not provided by the model server.
|
|
CallID string `json:"call_id,omitempty"`
|
|
|
|
// Content refers to the raw output generated by the server-side tool,
|
|
// supplied by the component implementer.
|
|
Content any `json:"content,omitempty"`
|
|
}
|
|
|
|
type MCPToolCall struct {
|
|
// ServerLabel is the MCP server label used to identify it in tool calls
|
|
ServerLabel string `json:"server_label,omitempty"`
|
|
|
|
// ApprovalRequestID is the approval request ID.
|
|
ApprovalRequestID string `json:"approval_request_id,omitempty"`
|
|
|
|
// CallID is the unique ID of the tool call.
|
|
CallID string `json:"call_id,omitempty"`
|
|
|
|
// Name is the name of the tool to run.
|
|
Name string `json:"name"`
|
|
|
|
// Arguments is the JSON string arguments for the tool call.
|
|
Arguments string `json:"arguments,omitempty"`
|
|
}
|
|
|
|
type MCPToolResult struct {
|
|
// ServerLabel is the MCP server label used to identify it in tool calls
|
|
ServerLabel string `json:"server_label,omitempty"`
|
|
|
|
// CallID is the unique ID of the tool call.
|
|
CallID string `json:"call_id,omitempty"`
|
|
|
|
// Name is the name of the tool to run.
|
|
Name string `json:"name"`
|
|
|
|
// Content is the JSON string with the tool result.
|
|
Content string `json:"content,omitempty"`
|
|
|
|
// Error returned when the server fails to run the tool.
|
|
Error *MCPToolCallError `json:"error,omitempty"`
|
|
}
|
|
|
|
type MCPToolCallError struct {
|
|
// Code is the error code.
|
|
Code *int64 `json:"code,omitempty"`
|
|
|
|
// Message is the error message.
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
type MCPListToolsResult struct {
|
|
// ServerLabel is the MCP server label used to identify it in tool calls.
|
|
ServerLabel string `json:"server_label,omitempty"`
|
|
|
|
// Tools is the list of tools available on the server.
|
|
Tools []*MCPListToolsItem `json:"tools,omitempty"`
|
|
|
|
// Error returned when the server fails to list tools.
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
type MCPListToolsItem struct {
|
|
// Name is the name of the tool.
|
|
Name string `json:"name"`
|
|
|
|
// Description is the description of the tool.
|
|
Description string `json:"description"`
|
|
|
|
// InputSchema is the JSON schema that describes the tool input parameters.
|
|
InputSchema *jsonschema.Schema `json:"input_schema,omitempty"`
|
|
}
|
|
|
|
type mcpListToolsItemGob struct {
|
|
Name string
|
|
Description string
|
|
InputSchemaJSON []byte
|
|
}
|
|
|
|
func (m *MCPListToolsItem) GobEncode() ([]byte, error) {
|
|
g := mcpListToolsItemGob{
|
|
Name: m.Name,
|
|
Description: m.Description,
|
|
}
|
|
if m.InputSchema != nil {
|
|
b, err := json.Marshal(m.InputSchema)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal MCPListToolsItem.InputSchema: %w", err)
|
|
}
|
|
g.InputSchemaJSON = b
|
|
}
|
|
var buf bytes.Buffer
|
|
if err := gob.NewEncoder(&buf).Encode(&g); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func (m *MCPListToolsItem) GobDecode(data []byte) error {
|
|
var g mcpListToolsItemGob
|
|
if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&g); err != nil {
|
|
return err
|
|
}
|
|
m.Name = g.Name
|
|
m.Description = g.Description
|
|
if len(g.InputSchemaJSON) > 0 {
|
|
m.InputSchema = &jsonschema.Schema{}
|
|
if err := sonic.Unmarshal(g.InputSchemaJSON, m.InputSchema); err != nil {
|
|
return fmt.Errorf("failed to unmarshal MCPListToolsItem.InputSchema: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type MCPToolApprovalRequest struct {
|
|
// ID is the approval request ID.
|
|
ID string `json:"id,omitempty"`
|
|
|
|
// Name is the name of the tool to run.
|
|
Name string `json:"name"`
|
|
|
|
// Arguments is the JSON string arguments for the tool call.
|
|
Arguments string `json:"arguments,omitempty"`
|
|
|
|
// ServerLabel is the MCP server label used to identify it in tool calls.
|
|
ServerLabel string `json:"server_label,omitempty"`
|
|
}
|
|
|
|
type MCPToolApprovalResponse struct {
|
|
// ApprovalRequestID is the approval request ID being responded to.
|
|
ApprovalRequestID string `json:"approval_request_id,omitempty"`
|
|
|
|
// Approve indicates whether the request is approved.
|
|
Approve bool `json:"approve"`
|
|
|
|
// Reason is the rationale for the decision.
|
|
// Optional.
|
|
Reason string `json:"reason,omitempty"`
|
|
}
|
|
|
|
// SystemAgenticMessage represents a message with AgenticRoleType "system".
|
|
func SystemAgenticMessage(text string) *AgenticMessage {
|
|
return &AgenticMessage{
|
|
Role: AgenticRoleTypeSystem,
|
|
ContentBlocks: []*ContentBlock{NewContentBlock(&UserInputText{Text: text})},
|
|
}
|
|
}
|
|
|
|
// UserAgenticMessage represents a message with AgenticRoleType "user".
|
|
func UserAgenticMessage(text string) *AgenticMessage {
|
|
return &AgenticMessage{
|
|
Role: AgenticRoleTypeUser,
|
|
ContentBlocks: []*ContentBlock{NewContentBlock(&UserInputText{Text: text})},
|
|
}
|
|
}
|
|
|
|
type contentBlockVariant interface {
|
|
Reasoning | userInputVariant | assistantGenVariant | functionToolCallVariant | serverToolCallVariant | mcpToolCallVariant
|
|
}
|
|
|
|
type userInputVariant interface {
|
|
UserInputText | UserInputImage | UserInputAudio | UserInputVideo | UserInputFile
|
|
}
|
|
|
|
type assistantGenVariant interface {
|
|
AssistantGenText | AssistantGenImage | AssistantGenAudio | AssistantGenVideo
|
|
}
|
|
|
|
type functionToolCallVariant interface {
|
|
FunctionToolCall | FunctionToolResult | ToolSearchFunctionToolResult
|
|
}
|
|
|
|
type serverToolCallVariant interface {
|
|
ServerToolCall | ServerToolResult
|
|
}
|
|
|
|
type mcpToolCallVariant interface {
|
|
MCPToolCall | MCPToolResult | MCPListToolsResult | MCPToolApprovalRequest | MCPToolApprovalResponse
|
|
}
|
|
|
|
// NewContentBlock creates a new ContentBlock with the given content.
|
|
func NewContentBlock[T contentBlockVariant](content *T) *ContentBlock {
|
|
switch b := any(content).(type) {
|
|
case *Reasoning:
|
|
return &ContentBlock{Type: ContentBlockTypeReasoning, Reasoning: b}
|
|
case *UserInputText:
|
|
return &ContentBlock{Type: ContentBlockTypeUserInputText, UserInputText: b}
|
|
case *UserInputImage:
|
|
return &ContentBlock{Type: ContentBlockTypeUserInputImage, UserInputImage: b}
|
|
case *UserInputAudio:
|
|
return &ContentBlock{Type: ContentBlockTypeUserInputAudio, UserInputAudio: b}
|
|
case *UserInputVideo:
|
|
return &ContentBlock{Type: ContentBlockTypeUserInputVideo, UserInputVideo: b}
|
|
case *UserInputFile:
|
|
return &ContentBlock{Type: ContentBlockTypeUserInputFile, UserInputFile: b}
|
|
case *ToolSearchFunctionToolResult:
|
|
return &ContentBlock{Type: ContentBlockTypeToolSearchResult, ToolSearchFunctionToolResult: b}
|
|
case *AssistantGenText:
|
|
return &ContentBlock{Type: ContentBlockTypeAssistantGenText, AssistantGenText: b}
|
|
case *AssistantGenImage:
|
|
return &ContentBlock{Type: ContentBlockTypeAssistantGenImage, AssistantGenImage: b}
|
|
case *AssistantGenAudio:
|
|
return &ContentBlock{Type: ContentBlockTypeAssistantGenAudio, AssistantGenAudio: b}
|
|
case *AssistantGenVideo:
|
|
return &ContentBlock{Type: ContentBlockTypeAssistantGenVideo, AssistantGenVideo: b}
|
|
case *FunctionToolCall:
|
|
return &ContentBlock{Type: ContentBlockTypeFunctionToolCall, FunctionToolCall: b}
|
|
case *FunctionToolResult:
|
|
return &ContentBlock{Type: ContentBlockTypeFunctionToolResult, FunctionToolResult: b}
|
|
case *ServerToolCall:
|
|
return &ContentBlock{Type: ContentBlockTypeServerToolCall, ServerToolCall: b}
|
|
case *ServerToolResult:
|
|
return &ContentBlock{Type: ContentBlockTypeServerToolResult, ServerToolResult: b}
|
|
case *MCPToolCall:
|
|
return &ContentBlock{Type: ContentBlockTypeMCPToolCall, MCPToolCall: b}
|
|
case *MCPToolResult:
|
|
return &ContentBlock{Type: ContentBlockTypeMCPToolResult, MCPToolResult: b}
|
|
case *MCPListToolsResult:
|
|
return &ContentBlock{Type: ContentBlockTypeMCPListToolsResult, MCPListToolsResult: b}
|
|
case *MCPToolApprovalRequest:
|
|
return &ContentBlock{Type: ContentBlockTypeMCPToolApprovalRequest, MCPToolApprovalRequest: b}
|
|
case *MCPToolApprovalResponse:
|
|
return &ContentBlock{Type: ContentBlockTypeMCPToolApprovalResponse, MCPToolApprovalResponse: b}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// NewContentBlockChunk creates a new ContentBlock with the given content and streaming metadata.
|
|
func NewContentBlockChunk[T contentBlockVariant](content *T, meta *StreamingMeta) *ContentBlock {
|
|
block := NewContentBlock(content)
|
|
block.StreamingMeta = meta
|
|
return block
|
|
}
|
|
|
|
// AgenticMessagesTemplate is the interface for agentic messages template.
|
|
// It's used to render a template to a list of agentic messages.
|
|
// e.g.
|
|
//
|
|
// chatTemplate := prompt.FromAgenticMessages(
|
|
// &schema.AgenticMessage{
|
|
// Role: schema.AgenticRoleTypeSystem,
|
|
// ContentBlocks: []*schema.ContentBlock{
|
|
// {Type: schema.ContentBlockTypeUserInputText, UserInputText: &schema.UserInputText{Text: "you are an eino helper"}},
|
|
// },
|
|
// },
|
|
// schema.AgenticMessagesPlaceholder("history", false), // <= this will use the value of "history" in params
|
|
// )
|
|
// msgs, err := chatTemplate.Format(ctx, params)
|
|
type AgenticMessagesTemplate interface {
|
|
Format(ctx context.Context, vs map[string]any, formatType FormatType) ([]*AgenticMessage, error)
|
|
}
|
|
|
|
var _ AgenticMessagesTemplate = &AgenticMessage{}
|
|
var _ AgenticMessagesTemplate = AgenticMessagesPlaceholder("", false)
|
|
|
|
type agenticMessagesPlaceholder struct {
|
|
key string
|
|
optional bool
|
|
}
|
|
|
|
// AgenticMessagesPlaceholder can render a placeholder to a list of agentic messages in params.
|
|
// e.g.
|
|
//
|
|
// placeholder := AgenticMessagesPlaceholder("history", false)
|
|
// params := map[string]any{
|
|
// "history": []*schema.AgenticMessage{
|
|
// &schema.AgenticMessage{
|
|
// Role: schema.AgenticRoleTypeSystem,
|
|
// ContentBlocks: []*schema.ContentBlock{
|
|
// {Type: schema.ContentBlockTypeUserInputText, UserInputText: &schema.UserInputText{Text: "you are an eino helper"}},
|
|
// },
|
|
// },
|
|
// },
|
|
// }
|
|
// chatTemplate := chatTpl := prompt.FromMessages(
|
|
// schema.AgenticMessagesPlaceholder("history", false), // <= this will use the value of "history" in params
|
|
// )
|
|
// msgs, err := chatTemplate.Format(ctx, params)
|
|
func AgenticMessagesPlaceholder(key string, optional bool) AgenticMessagesTemplate {
|
|
return &agenticMessagesPlaceholder{
|
|
key: key,
|
|
optional: optional,
|
|
}
|
|
}
|
|
|
|
func (p *agenticMessagesPlaceholder) Format(_ context.Context, vs map[string]any, _ FormatType) ([]*AgenticMessage, error) {
|
|
v, ok := vs[p.key]
|
|
if !ok {
|
|
if p.optional {
|
|
return []*AgenticMessage{}, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("message placeholder format: %s not found", p.key)
|
|
}
|
|
|
|
msgs, ok := v.([]*AgenticMessage)
|
|
if !ok {
|
|
return nil, fmt.Errorf("only agentic messages can be used to format message placeholder, key: %v, actual type: %v", p.key, reflect.TypeOf(v))
|
|
}
|
|
|
|
return msgs, nil
|
|
}
|
|
|
|
// Format returns the agentic messages after rendering by the given formatType.
|
|
// It formats only the user input fields (UserInputText, UserInputImage, UserInputAudio, UserInputVideo, UserInputFile).
|
|
// e.g.
|
|
//
|
|
// msg := &schema.AgenticMessage{
|
|
// Role: schema.AgenticRoleTypeUser,
|
|
// ContentBlocks: []*schema.ContentBlock{
|
|
// {Type: schema.ContentBlockTypeUserInputText, UserInputText: &schema.UserInputText{Text: "hello {name}"}},
|
|
// },
|
|
// }
|
|
// msgs, err := msg.Format(ctx, map[string]any{"name": "eino"}, schema.FString)
|
|
// // msgs[0].ContentBlocks[0].UserInputText.Text will be "hello eino"
|
|
func (m *AgenticMessage) Format(_ context.Context, vs map[string]any, formatType FormatType) ([]*AgenticMessage, error) {
|
|
copied := *m
|
|
|
|
if len(m.ContentBlocks) > 0 {
|
|
copiedBlocks := make([]*ContentBlock, len(m.ContentBlocks))
|
|
for i, block := range m.ContentBlocks {
|
|
if block == nil {
|
|
copiedBlocks[i] = nil
|
|
continue
|
|
}
|
|
|
|
copiedBlock := *block
|
|
var err error
|
|
|
|
switch block.Type {
|
|
case ContentBlockTypeUserInputText:
|
|
if block.UserInputText != nil {
|
|
copiedBlock.UserInputText, err = formatUserInputText(block.UserInputText, vs, formatType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
case ContentBlockTypeUserInputImage:
|
|
if block.UserInputImage != nil {
|
|
copiedBlock.UserInputImage, err = formatUserInputImage(block.UserInputImage, vs, formatType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
case ContentBlockTypeUserInputAudio:
|
|
if block.UserInputAudio != nil {
|
|
copiedBlock.UserInputAudio, err = formatUserInputAudio(block.UserInputAudio, vs, formatType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
case ContentBlockTypeUserInputVideo:
|
|
if block.UserInputVideo != nil {
|
|
copiedBlock.UserInputVideo, err = formatUserInputVideo(block.UserInputVideo, vs, formatType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
case ContentBlockTypeUserInputFile:
|
|
if block.UserInputFile != nil {
|
|
copiedBlock.UserInputFile, err = formatUserInputFile(block.UserInputFile, vs, formatType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
copiedBlocks[i] = &copiedBlock
|
|
}
|
|
copied.ContentBlocks = copiedBlocks
|
|
}
|
|
|
|
return []*AgenticMessage{&copied}, nil
|
|
}
|
|
|
|
func formatUserInputText(uit *UserInputText, vs map[string]any, formatType FormatType) (*UserInputText, error) {
|
|
text, err := formatContent(uit.Text, vs, formatType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
copied := *uit
|
|
copied.Text = text
|
|
return &copied, nil
|
|
}
|
|
|
|
func formatUserInputImage(uii *UserInputImage, vs map[string]any, formatType FormatType) (*UserInputImage, error) {
|
|
copied := *uii
|
|
if uii.URL != "" {
|
|
url, err := formatContent(uii.URL, vs, formatType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
copied.URL = url
|
|
}
|
|
if uii.Base64Data != "" {
|
|
base64data, err := formatContent(uii.Base64Data, vs, formatType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
copied.Base64Data = base64data
|
|
}
|
|
return &copied, nil
|
|
}
|
|
|
|
func formatUserInputAudio(uia *UserInputAudio, vs map[string]any, formatType FormatType) (*UserInputAudio, error) {
|
|
copied := *uia
|
|
if uia.URL != "" {
|
|
url, err := formatContent(uia.URL, vs, formatType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
copied.URL = url
|
|
}
|
|
if uia.Base64Data != "" {
|
|
base64data, err := formatContent(uia.Base64Data, vs, formatType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
copied.Base64Data = base64data
|
|
}
|
|
return &copied, nil
|
|
}
|
|
|
|
func formatUserInputVideo(uiv *UserInputVideo, vs map[string]any, formatType FormatType) (*UserInputVideo, error) {
|
|
copied := *uiv
|
|
if uiv.URL != "" {
|
|
url, err := formatContent(uiv.URL, vs, formatType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
copied.URL = url
|
|
}
|
|
if uiv.Base64Data != "" {
|
|
base64data, err := formatContent(uiv.Base64Data, vs, formatType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
copied.Base64Data = base64data
|
|
}
|
|
return &copied, nil
|
|
}
|
|
|
|
func formatUserInputFile(uif *UserInputFile, vs map[string]any, formatType FormatType) (*UserInputFile, error) {
|
|
copied := *uif
|
|
if uif.URL != "" {
|
|
url, err := formatContent(uif.URL, vs, formatType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
copied.URL = url
|
|
}
|
|
if uif.Name != "" {
|
|
name, err := formatContent(uif.Name, vs, formatType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
copied.Name = name
|
|
}
|
|
if uif.Base64Data != "" {
|
|
base64data, err := formatContent(uif.Base64Data, vs, formatType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
copied.Base64Data = base64data
|
|
}
|
|
return &copied, nil
|
|
}
|
|
|
|
// ConcatAgenticMessagesArray concatenates multiple streams of AgenticMessage into a single slice of AgenticMessage.
|
|
func ConcatAgenticMessagesArray(mas [][]*AgenticMessage) ([]*AgenticMessage, error) {
|
|
return buildConcatGenericArray[AgenticMessage](ConcatAgenticMessages)(mas)
|
|
}
|
|
|
|
// ConcatAgenticMessages concatenates a list of AgenticMessage chunks into a single AgenticMessage.
|
|
func ConcatAgenticMessages(msgs []*AgenticMessage) (*AgenticMessage, error) {
|
|
var (
|
|
role AgenticRoleType
|
|
blocks []*ContentBlock
|
|
metas []*AgenticResponseMeta
|
|
extra map[string]any
|
|
blockIndices []int
|
|
indexToBlocks = map[int][]*ContentBlock{}
|
|
extraList = make([]map[string]any, 0, len(msgs))
|
|
)
|
|
|
|
if len(msgs) == 1 {
|
|
return msgs[0], nil
|
|
}
|
|
|
|
for idx, msg := range msgs {
|
|
if msg == nil {
|
|
return nil, fmt.Errorf("message at index %d is nil", idx)
|
|
}
|
|
|
|
if msg.Role != "" {
|
|
if role == "" {
|
|
role = msg.Role
|
|
} else if role != msg.Role {
|
|
return nil, fmt.Errorf("cannot concat messages with different roles: got '%s' and '%s'", role, msg.Role)
|
|
}
|
|
}
|
|
|
|
for _, block := range msg.ContentBlocks {
|
|
if block == nil {
|
|
continue
|
|
}
|
|
if block.StreamingMeta == nil {
|
|
// Non-streaming block
|
|
if len(blockIndices) > 0 {
|
|
// Cannot mix streaming and non-streaming blocks
|
|
return nil, fmt.Errorf("found non-streaming block after streaming blocks")
|
|
}
|
|
// Collect non-streaming block
|
|
blocks = append(blocks, block)
|
|
} else {
|
|
// Streaming block
|
|
if len(blocks) > 0 {
|
|
// Cannot mix non-streaming and streaming blocks
|
|
return nil, fmt.Errorf("found streaming block after non-streaming blocks")
|
|
}
|
|
// Collect streaming block by index
|
|
if blocks_, ok := indexToBlocks[block.StreamingMeta.Index]; ok {
|
|
indexToBlocks[block.StreamingMeta.Index] = append(blocks_, block)
|
|
} else {
|
|
blockIndices = append(blockIndices, block.StreamingMeta.Index)
|
|
indexToBlocks[block.StreamingMeta.Index] = []*ContentBlock{block}
|
|
}
|
|
}
|
|
}
|
|
|
|
if msg.ResponseMeta != nil {
|
|
metas = append(metas, msg.ResponseMeta)
|
|
}
|
|
|
|
if msg.Extra != nil {
|
|
extraList = append(extraList, msg.Extra)
|
|
}
|
|
}
|
|
|
|
meta, err := concatAgenticResponseMeta(metas)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to concat agentic response meta: %w", err)
|
|
}
|
|
|
|
if len(blockIndices) > 0 {
|
|
// All blocks are streaming, concat each group by index
|
|
indexToBlock := map[int]*ContentBlock{}
|
|
for idx, bs := range indexToBlocks {
|
|
var b *ContentBlock
|
|
b, err = concatChunksOfSameContentBlock(bs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
indexToBlock[idx] = b
|
|
}
|
|
blocks = make([]*ContentBlock, 0, len(blockIndices))
|
|
sort.Slice(blockIndices, func(i, j int) bool {
|
|
return blockIndices[i] < blockIndices[j]
|
|
})
|
|
for _, idx := range blockIndices {
|
|
blocks = append(blocks, indexToBlock[idx])
|
|
}
|
|
}
|
|
|
|
if len(extraList) > 0 {
|
|
extra, err = concatExtra(extraList)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return &AgenticMessage{
|
|
Role: role,
|
|
ResponseMeta: meta,
|
|
ContentBlocks: blocks,
|
|
Extra: extra,
|
|
}, nil
|
|
}
|
|
|
|
func concatAgenticResponseMeta(metas []*AgenticResponseMeta) (ret *AgenticResponseMeta, err error) {
|
|
if len(metas) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
openaiExtensions := make([]*openai.ResponseMetaExtension, 0, len(metas))
|
|
claudeExtensions := make([]*claude.ResponseMetaExtension, 0, len(metas))
|
|
geminiExtensions := make([]*gemini.ResponseMetaExtension, 0, len(metas))
|
|
tokenUsages := make([]*TokenUsage, 0, len(metas))
|
|
|
|
var (
|
|
extType reflect.Type
|
|
extensions reflect.Value
|
|
)
|
|
|
|
for _, meta := range metas {
|
|
if meta.TokenUsage != nil {
|
|
tokenUsages = append(tokenUsages, meta.TokenUsage)
|
|
}
|
|
|
|
var isConsistent bool
|
|
|
|
if meta.Extension != nil {
|
|
extType, isConsistent = validateExtensionType(extType, meta.Extension)
|
|
if !isConsistent {
|
|
return nil, fmt.Errorf("inconsistent extension types in response meta chunks: '%s' vs '%s'",
|
|
extType, reflect.TypeOf(meta.Extension))
|
|
}
|
|
if !extensions.IsValid() {
|
|
extensions = reflect.MakeSlice(reflect.SliceOf(extType), 0, len(metas))
|
|
}
|
|
extensions = reflect.Append(extensions, reflect.ValueOf(meta.Extension))
|
|
}
|
|
|
|
if meta.OpenAIExtension != nil {
|
|
extType, isConsistent = validateExtensionType(extType, meta.OpenAIExtension)
|
|
if !isConsistent {
|
|
return nil, fmt.Errorf("inconsistent extension types in response meta chunks: '%s' vs '%s'",
|
|
extType, reflect.TypeOf(meta.OpenAIExtension))
|
|
}
|
|
openaiExtensions = append(openaiExtensions, meta.OpenAIExtension)
|
|
}
|
|
|
|
if meta.ClaudeExtension != nil {
|
|
extType, isConsistent = validateExtensionType(extType, meta.ClaudeExtension)
|
|
if !isConsistent {
|
|
return nil, fmt.Errorf("inconsistent extension types in response meta chunks: '%s' vs '%s'",
|
|
extType, reflect.TypeOf(meta.ClaudeExtension))
|
|
}
|
|
claudeExtensions = append(claudeExtensions, meta.ClaudeExtension)
|
|
}
|
|
|
|
if meta.GeminiExtension != nil {
|
|
extType, isConsistent = validateExtensionType(extType, meta.GeminiExtension)
|
|
if !isConsistent {
|
|
return nil, fmt.Errorf("inconsistent extension types in response meta chunks: '%s' vs '%s'",
|
|
extType, reflect.TypeOf(meta.GeminiExtension))
|
|
}
|
|
geminiExtensions = append(geminiExtensions, meta.GeminiExtension)
|
|
}
|
|
}
|
|
|
|
ret = &AgenticResponseMeta{
|
|
TokenUsage: concatTokenUsage(tokenUsages),
|
|
}
|
|
|
|
if extensions.IsValid() && !extensions.IsZero() {
|
|
var extension reflect.Value
|
|
extension, err = internal.ConcatSliceValue(extensions)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to concat extensions: %w", err)
|
|
}
|
|
ret.Extension = extension.Interface()
|
|
}
|
|
|
|
if len(openaiExtensions) > 0 {
|
|
ret.OpenAIExtension, err = openai.ConcatResponseMetaExtensions(openaiExtensions)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to concat openai extensions: %w", err)
|
|
}
|
|
}
|
|
|
|
if len(claudeExtensions) > 0 {
|
|
ret.ClaudeExtension, err = claude.ConcatResponseMetaExtensions(claudeExtensions)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to concat claude extensions: %w", err)
|
|
}
|
|
}
|
|
|
|
if len(geminiExtensions) > 0 {
|
|
ret.GeminiExtension, err = gemini.ConcatResponseMetaExtensions(geminiExtensions)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to concat gemini extensions: %w", err)
|
|
}
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func concatTokenUsage(usages []*TokenUsage) *TokenUsage {
|
|
if len(usages) == 0 {
|
|
return nil
|
|
}
|
|
|
|
ret := &TokenUsage{}
|
|
|
|
for _, usage := range usages {
|
|
if usage == nil {
|
|
continue
|
|
}
|
|
|
|
if usage.PromptTokens > ret.PromptTokens {
|
|
ret.PromptTokens = usage.PromptTokens
|
|
}
|
|
if usage.CompletionTokens > ret.CompletionTokens {
|
|
ret.CompletionTokens = usage.CompletionTokens
|
|
}
|
|
if usage.TotalTokens > ret.TotalTokens {
|
|
ret.TotalTokens = usage.TotalTokens
|
|
}
|
|
if usage.PromptTokenDetails.CachedTokens > ret.PromptTokenDetails.CachedTokens {
|
|
ret.PromptTokenDetails.CachedTokens = usage.PromptTokenDetails.CachedTokens
|
|
}
|
|
if usage.CompletionTokensDetails.ReasoningTokens > ret.CompletionTokensDetails.ReasoningTokens {
|
|
ret.CompletionTokensDetails.ReasoningTokens = usage.CompletionTokensDetails.ReasoningTokens
|
|
}
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func concatChunksOfSameContentBlock(blocks []*ContentBlock) (*ContentBlock, error) {
|
|
if len(blocks) == 0 {
|
|
return nil, fmt.Errorf("no content blocks to concat")
|
|
}
|
|
|
|
blockType := blocks[0].Type
|
|
|
|
switch blockType {
|
|
case ContentBlockTypeReasoning:
|
|
return concatContentBlockHelper(blocks, blockType,
|
|
func(b *ContentBlock) *Reasoning { return b.Reasoning },
|
|
concatReasoning)
|
|
|
|
case ContentBlockTypeUserInputText:
|
|
return concatContentBlockHelper(blocks, blockType,
|
|
func(b *ContentBlock) *UserInputText { return b.UserInputText },
|
|
concatUserInputTexts)
|
|
|
|
case ContentBlockTypeUserInputImage:
|
|
return concatContentBlockHelper(blocks, blockType,
|
|
func(b *ContentBlock) *UserInputImage { return b.UserInputImage },
|
|
concatUserInputImages)
|
|
|
|
case ContentBlockTypeUserInputAudio:
|
|
return concatContentBlockHelper(blocks, blockType,
|
|
func(b *ContentBlock) *UserInputAudio { return b.UserInputAudio },
|
|
concatUserInputAudios)
|
|
|
|
case ContentBlockTypeUserInputVideo:
|
|
return concatContentBlockHelper(blocks, blockType,
|
|
func(b *ContentBlock) *UserInputVideo { return b.UserInputVideo },
|
|
concatUserInputVideos)
|
|
|
|
case ContentBlockTypeUserInputFile:
|
|
return concatContentBlockHelper(blocks, blockType,
|
|
func(b *ContentBlock) *UserInputFile { return b.UserInputFile },
|
|
concatUserInputFiles)
|
|
|
|
case ContentBlockTypeToolSearchResult:
|
|
return concatContentBlockHelper(blocks, blockType,
|
|
func(b *ContentBlock) *ToolSearchFunctionToolResult { return b.ToolSearchFunctionToolResult },
|
|
concatToolSearchFunctionToolResult)
|
|
|
|
case ContentBlockTypeAssistantGenText:
|
|
return concatContentBlockHelper(blocks, blockType,
|
|
func(b *ContentBlock) *AssistantGenText { return b.AssistantGenText },
|
|
concatAssistantGenTexts)
|
|
|
|
case ContentBlockTypeAssistantGenImage:
|
|
return concatContentBlockHelper(blocks, blockType,
|
|
func(b *ContentBlock) *AssistantGenImage { return b.AssistantGenImage },
|
|
concatAssistantGenImages)
|
|
|
|
case ContentBlockTypeAssistantGenAudio:
|
|
return concatContentBlockHelper(blocks, blockType,
|
|
func(b *ContentBlock) *AssistantGenAudio { return b.AssistantGenAudio },
|
|
concatAssistantGenAudios)
|
|
|
|
case ContentBlockTypeAssistantGenVideo:
|
|
return concatContentBlockHelper(blocks, blockType,
|
|
func(b *ContentBlock) *AssistantGenVideo { return b.AssistantGenVideo },
|
|
concatAssistantGenVideos)
|
|
|
|
case ContentBlockTypeFunctionToolCall:
|
|
return concatContentBlockHelper(blocks, blockType,
|
|
func(b *ContentBlock) *FunctionToolCall { return b.FunctionToolCall },
|
|
concatFunctionToolCalls)
|
|
|
|
case ContentBlockTypeFunctionToolResult:
|
|
return concatContentBlockHelper(blocks, blockType,
|
|
func(b *ContentBlock) *FunctionToolResult { return b.FunctionToolResult },
|
|
concatFunctionToolResults)
|
|
|
|
case ContentBlockTypeServerToolCall:
|
|
return concatContentBlockHelper(blocks, blockType,
|
|
func(b *ContentBlock) *ServerToolCall { return b.ServerToolCall },
|
|
concatServerToolCalls)
|
|
|
|
case ContentBlockTypeServerToolResult:
|
|
return concatContentBlockHelper(blocks, blockType,
|
|
func(b *ContentBlock) *ServerToolResult { return b.ServerToolResult },
|
|
concatServerToolResults)
|
|
|
|
case ContentBlockTypeMCPToolCall:
|
|
return concatContentBlockHelper(blocks, blockType,
|
|
func(b *ContentBlock) *MCPToolCall { return b.MCPToolCall },
|
|
concatMCPToolCalls)
|
|
|
|
case ContentBlockTypeMCPToolResult:
|
|
return concatContentBlockHelper(blocks, blockType,
|
|
func(b *ContentBlock) *MCPToolResult { return b.MCPToolResult },
|
|
concatMCPToolResults)
|
|
|
|
case ContentBlockTypeMCPListToolsResult:
|
|
return concatContentBlockHelper(blocks, blockType,
|
|
func(b *ContentBlock) *MCPListToolsResult { return b.MCPListToolsResult },
|
|
concatMCPListToolsResults)
|
|
|
|
case ContentBlockTypeMCPToolApprovalRequest:
|
|
return concatContentBlockHelper(blocks, blockType,
|
|
func(b *ContentBlock) *MCPToolApprovalRequest { return b.MCPToolApprovalRequest },
|
|
concatMCPToolApprovalRequests)
|
|
|
|
case ContentBlockTypeMCPToolApprovalResponse:
|
|
return concatContentBlockHelper(blocks, blockType,
|
|
func(b *ContentBlock) *MCPToolApprovalResponse { return b.MCPToolApprovalResponse },
|
|
concatMCPToolApprovalResponses)
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unknown content block type: %s", blockType)
|
|
}
|
|
}
|
|
|
|
// concatContentBlockHelper is a generic helper function that reduces code duplication
|
|
// for concatenating content blocks of a specific type.
|
|
func concatContentBlockHelper[T contentBlockVariant](
|
|
blocks []*ContentBlock,
|
|
expectedType ContentBlockType,
|
|
getter func(*ContentBlock) *T,
|
|
concatFunc func([]*T) (*T, error),
|
|
) (*ContentBlock, error) {
|
|
items, err := genericGetTFromContentBlocks(blocks, func(block *ContentBlock) (*T, error) {
|
|
if block.Type != expectedType {
|
|
return nil, fmt.Errorf("content block type mismatch: expected '%s', but got '%s'", expectedType, block.Type)
|
|
}
|
|
item := getter(block)
|
|
if item == nil {
|
|
return nil, fmt.Errorf("'%s' content is nil", expectedType)
|
|
}
|
|
return item, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
concatenated, err := concatFunc(items)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to concat '%s' content blocks: %w", expectedType, err)
|
|
}
|
|
|
|
extras := make([]map[string]any, 0, len(blocks))
|
|
for _, block := range blocks {
|
|
if len(block.Extra) > 0 {
|
|
extras = append(extras, block.Extra)
|
|
}
|
|
}
|
|
|
|
var extra map[string]any
|
|
if len(extras) > 0 {
|
|
extra, err = internal.ConcatItems(extras)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to concat content block extras: %w", err)
|
|
}
|
|
}
|
|
|
|
block := NewContentBlock(concatenated)
|
|
block.Extra = extra
|
|
|
|
return block, nil
|
|
}
|
|
|
|
func genericGetTFromContentBlocks[T any](blocks []*ContentBlock, checkAndGetter func(block *ContentBlock) (T, error)) ([]T, error) {
|
|
ret := make([]T, 0, len(blocks))
|
|
for _, block := range blocks {
|
|
t, err := checkAndGetter(block)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ret = append(ret, t)
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func concatReasoning(reasons []*Reasoning) (ret *Reasoning, err error) {
|
|
if len(reasons) == 0 {
|
|
return nil, fmt.Errorf("no reasoning found")
|
|
}
|
|
|
|
ret = &Reasoning{}
|
|
|
|
openaiExtensions := make([]*openai.ReasoningExtension, 0, len(reasons))
|
|
|
|
for _, r := range reasons {
|
|
if r == nil {
|
|
continue
|
|
}
|
|
if r.Text != "" {
|
|
ret.Text += r.Text
|
|
}
|
|
if r.Signature != "" {
|
|
ret.Signature += r.Signature
|
|
}
|
|
if r.OpenAIExtension != nil {
|
|
openaiExtensions = append(openaiExtensions, r.OpenAIExtension)
|
|
}
|
|
}
|
|
|
|
if len(openaiExtensions) > 0 {
|
|
ret.OpenAIExtension, err = openai.ConcatReasoningExtensions(openaiExtensions)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to concat openai reasoning extensions: %w", err)
|
|
}
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func concatUserInputTexts(texts []*UserInputText) (*UserInputText, error) {
|
|
if len(texts) == 0 {
|
|
return nil, fmt.Errorf("no user input text found")
|
|
}
|
|
if len(texts) == 1 {
|
|
return texts[0], nil
|
|
}
|
|
return nil, fmt.Errorf("cannot concat multiple user input texts")
|
|
}
|
|
|
|
func concatUserInputImages(images []*UserInputImage) (*UserInputImage, error) {
|
|
if len(images) == 0 {
|
|
return nil, fmt.Errorf("no user input image found")
|
|
}
|
|
if len(images) == 1 {
|
|
return images[0], nil
|
|
}
|
|
return nil, fmt.Errorf("cannot concat multiple user input images")
|
|
}
|
|
|
|
func concatUserInputAudios(audios []*UserInputAudio) (*UserInputAudio, error) {
|
|
if len(audios) == 0 {
|
|
return nil, fmt.Errorf("no user input audio found")
|
|
}
|
|
if len(audios) == 1 {
|
|
return audios[0], nil
|
|
}
|
|
return nil, fmt.Errorf("cannot concat multiple user input audios")
|
|
}
|
|
|
|
func concatUserInputVideos(videos []*UserInputVideo) (*UserInputVideo, error) {
|
|
if len(videos) == 0 {
|
|
return nil, fmt.Errorf("no user input video found")
|
|
}
|
|
if len(videos) == 1 {
|
|
return videos[0], nil
|
|
}
|
|
return nil, fmt.Errorf("cannot concat multiple user input videos")
|
|
}
|
|
|
|
func concatUserInputFiles(files []*UserInputFile) (*UserInputFile, error) {
|
|
if len(files) == 0 {
|
|
return nil, fmt.Errorf("no user input file found")
|
|
}
|
|
if len(files) == 1 {
|
|
return files[0], nil
|
|
}
|
|
return nil, fmt.Errorf("cannot concat multiple user input files")
|
|
}
|
|
|
|
func concatToolSearchFunctionToolResult(results []*ToolSearchFunctionToolResult) (*ToolSearchFunctionToolResult, error) {
|
|
if len(results) == 0 {
|
|
return nil, fmt.Errorf("no tool search results found")
|
|
}
|
|
if len(results) == 1 {
|
|
return results[0], nil
|
|
}
|
|
return nil, fmt.Errorf("cannot concat multiple tool search results")
|
|
}
|
|
|
|
func concatAssistantGenTexts(texts []*AssistantGenText) (ret *AssistantGenText, err error) {
|
|
if len(texts) == 0 {
|
|
return nil, fmt.Errorf("no assistant generated text found")
|
|
}
|
|
if len(texts) == 1 {
|
|
return texts[0], nil
|
|
}
|
|
|
|
ret = &AssistantGenText{}
|
|
|
|
openaiExtensions := make([]*openai.AssistantGenTextExtension, 0, len(texts))
|
|
claudeExtensions := make([]*claude.AssistantGenTextExtension, 0, len(texts))
|
|
|
|
var (
|
|
extType reflect.Type
|
|
extensions reflect.Value
|
|
)
|
|
|
|
for _, t := range texts {
|
|
if t == nil {
|
|
continue
|
|
}
|
|
|
|
ret.Text += t.Text
|
|
|
|
var isConsistent bool
|
|
|
|
if t.Extension != nil {
|
|
extType, isConsistent = validateExtensionType(extType, t.Extension)
|
|
if !isConsistent {
|
|
return nil, fmt.Errorf("inconsistent extension types in assistant generated text chunks: '%s' vs '%s'",
|
|
extType, reflect.TypeOf(t.Extension))
|
|
}
|
|
if !extensions.IsValid() {
|
|
extensions = reflect.MakeSlice(reflect.SliceOf(extType), 0, len(texts))
|
|
}
|
|
extensions = reflect.Append(extensions, reflect.ValueOf(t.Extension))
|
|
}
|
|
|
|
if t.OpenAIExtension != nil {
|
|
extType, isConsistent = validateExtensionType(extType, t.OpenAIExtension)
|
|
if !isConsistent {
|
|
return nil, fmt.Errorf("inconsistent extension types in assistant generated text chunks: '%s' vs '%s'",
|
|
extType, reflect.TypeOf(t.OpenAIExtension))
|
|
}
|
|
openaiExtensions = append(openaiExtensions, t.OpenAIExtension)
|
|
}
|
|
|
|
if t.ClaudeExtension != nil {
|
|
extType, isConsistent = validateExtensionType(extType, t.ClaudeExtension)
|
|
if !isConsistent {
|
|
return nil, fmt.Errorf("inconsistent extension types in assistant generated text chunks: '%s' vs '%s'",
|
|
extType, reflect.TypeOf(t.ClaudeExtension))
|
|
}
|
|
claudeExtensions = append(claudeExtensions, t.ClaudeExtension)
|
|
}
|
|
}
|
|
|
|
if extensions.IsValid() && !extensions.IsZero() {
|
|
ret.Extension, err = internal.ConcatSliceValue(extensions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if len(openaiExtensions) > 0 {
|
|
ret.OpenAIExtension, err = openai.ConcatAssistantGenTextExtensions(openaiExtensions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if len(claudeExtensions) > 0 {
|
|
ret.ClaudeExtension, err = claude.ConcatAssistantGenTextExtensions(claudeExtensions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func concatAssistantGenImages(images []*AssistantGenImage) (*AssistantGenImage, error) {
|
|
if len(images) == 0 {
|
|
return nil, fmt.Errorf("no assistant gen image found")
|
|
}
|
|
if len(images) == 1 {
|
|
return images[0], nil
|
|
}
|
|
|
|
ret := &AssistantGenImage{}
|
|
|
|
for _, img := range images {
|
|
if img == nil {
|
|
continue
|
|
}
|
|
|
|
ret.Base64Data += img.Base64Data
|
|
|
|
if ret.URL == "" {
|
|
ret.URL = img.URL
|
|
} else if img.URL != "" && ret.URL != img.URL {
|
|
return nil, fmt.Errorf("inconsistent URLs in assistant generated image chunks: '%s' vs '%s'", ret.URL, img.URL)
|
|
}
|
|
|
|
if ret.MIMEType == "" {
|
|
ret.MIMEType = img.MIMEType
|
|
} else if img.MIMEType != "" && ret.MIMEType != img.MIMEType {
|
|
return nil, fmt.Errorf("inconsistent MIME types in assistant generated image chunks: '%s' vs '%s'", ret.MIMEType, img.MIMEType)
|
|
}
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func concatAssistantGenAudios(audios []*AssistantGenAudio) (*AssistantGenAudio, error) {
|
|
if len(audios) == 0 {
|
|
return nil, fmt.Errorf("no assistant gen audio found")
|
|
}
|
|
if len(audios) == 1 {
|
|
return audios[0], nil
|
|
}
|
|
|
|
ret := &AssistantGenAudio{}
|
|
|
|
for _, audio := range audios {
|
|
if audio == nil {
|
|
continue
|
|
}
|
|
|
|
ret.Base64Data += audio.Base64Data
|
|
|
|
if ret.URL == "" {
|
|
ret.URL = audio.URL
|
|
} else if audio.URL != "" && ret.URL != audio.URL {
|
|
return nil, fmt.Errorf("inconsistent URLs in assistant generated audio chunks: '%s' vs '%s'", ret.URL, audio.URL)
|
|
}
|
|
|
|
if ret.MIMEType == "" {
|
|
ret.MIMEType = audio.MIMEType
|
|
} else if audio.MIMEType != "" && ret.MIMEType != audio.MIMEType {
|
|
return nil, fmt.Errorf("inconsistent MIME types in assistant generated audio chunks: '%s' vs '%s'", ret.MIMEType, audio.MIMEType)
|
|
}
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func concatAssistantGenVideos(videos []*AssistantGenVideo) (*AssistantGenVideo, error) {
|
|
if len(videos) == 0 {
|
|
return nil, fmt.Errorf("no assistant gen video found")
|
|
}
|
|
if len(videos) == 1 {
|
|
return videos[0], nil
|
|
}
|
|
|
|
ret := &AssistantGenVideo{}
|
|
|
|
for _, video := range videos {
|
|
if video == nil {
|
|
continue
|
|
}
|
|
|
|
ret.Base64Data += video.Base64Data
|
|
|
|
if ret.URL == "" {
|
|
ret.URL = video.URL
|
|
} else if video.URL != "" && ret.URL != video.URL {
|
|
return nil, fmt.Errorf("inconsistent URLs in assistant generated video chunks: '%s' vs '%s'", ret.URL, video.URL)
|
|
}
|
|
|
|
if ret.MIMEType == "" {
|
|
ret.MIMEType = video.MIMEType
|
|
} else if video.MIMEType != "" && ret.MIMEType != video.MIMEType {
|
|
return nil, fmt.Errorf("inconsistent MIME types in assistant generated video chunks: '%s' vs '%s'", ret.MIMEType, video.MIMEType)
|
|
}
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func concatFunctionToolCalls(calls []*FunctionToolCall) (*FunctionToolCall, error) {
|
|
if len(calls) == 0 {
|
|
return nil, fmt.Errorf("no function tool call found")
|
|
}
|
|
if len(calls) == 1 {
|
|
return calls[0], nil
|
|
}
|
|
|
|
ret := &FunctionToolCall{}
|
|
|
|
for _, c := range calls {
|
|
if c == nil {
|
|
continue
|
|
}
|
|
|
|
if ret.CallID == "" {
|
|
ret.CallID = c.CallID
|
|
} else if c.CallID != "" && c.CallID != ret.CallID {
|
|
return nil, fmt.Errorf("expected call ID '%s' for function tool call, but got '%s'", ret.CallID, c.CallID)
|
|
}
|
|
|
|
if ret.Name == "" {
|
|
ret.Name = c.Name
|
|
} else if c.Name != "" && c.Name != ret.Name {
|
|
return nil, fmt.Errorf("expected tool name '%s' for function tool call, but got '%s'", ret.Name, c.Name)
|
|
}
|
|
|
|
ret.Arguments += c.Arguments
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func concatFunctionToolResults(results []*FunctionToolResult) (*FunctionToolResult, error) {
|
|
if len(results) == 0 {
|
|
return nil, fmt.Errorf("no function tool result found")
|
|
}
|
|
if len(results) == 1 {
|
|
return results[0], nil
|
|
}
|
|
|
|
ret := &FunctionToolResult{}
|
|
|
|
for _, r := range results {
|
|
if r == nil {
|
|
continue
|
|
}
|
|
|
|
if ret.CallID == "" {
|
|
ret.CallID = r.CallID
|
|
} else if r.CallID != "" && r.CallID != ret.CallID {
|
|
return nil, fmt.Errorf("expected call ID '%s' for function tool result, but got '%s'", ret.CallID, r.CallID)
|
|
}
|
|
|
|
if ret.Name == "" {
|
|
ret.Name = r.Name
|
|
} else if r.Name != "" && r.Name != ret.Name {
|
|
return nil, fmt.Errorf("expected tool name '%s' for function tool result, but got '%s'", ret.Name, r.Name)
|
|
}
|
|
|
|
for _, b := range r.Content {
|
|
if b == nil {
|
|
continue
|
|
}
|
|
ret.Content = append(ret.Content, b)
|
|
}
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func concatServerToolCalls(calls []*ServerToolCall) (ret *ServerToolCall, err error) {
|
|
if len(calls) == 0 {
|
|
return nil, fmt.Errorf("no server tool call found")
|
|
}
|
|
if len(calls) == 1 {
|
|
return calls[0], nil
|
|
}
|
|
|
|
ret = &ServerToolCall{}
|
|
|
|
var (
|
|
argsType reflect.Type
|
|
argsChunks reflect.Value
|
|
)
|
|
|
|
for _, c := range calls {
|
|
if c == nil {
|
|
continue
|
|
}
|
|
|
|
if ret.CallID == "" {
|
|
ret.CallID = c.CallID
|
|
} else if c.CallID != "" && c.CallID != ret.CallID {
|
|
return nil, fmt.Errorf("expected call ID '%s' for server tool call, but got '%s'", ret.CallID, c.CallID)
|
|
}
|
|
|
|
if ret.Name == "" {
|
|
ret.Name = c.Name
|
|
} else if c.Name != "" && c.Name != ret.Name {
|
|
return nil, fmt.Errorf("expected tool name '%s' for server tool call, but got '%s'", ret.Name, c.Name)
|
|
}
|
|
|
|
if c.Arguments != nil {
|
|
argsType_ := reflect.TypeOf(c.Arguments)
|
|
if argsType == nil {
|
|
argsType = argsType_
|
|
argsChunks = reflect.MakeSlice(reflect.SliceOf(argsType), 0, len(calls))
|
|
} else if argsType != argsType_ {
|
|
return nil, fmt.Errorf("expected type '%s' for server tool call arguments, but got '%s'", argsType, argsType_)
|
|
}
|
|
argsChunks = reflect.Append(argsChunks, reflect.ValueOf(c.Arguments))
|
|
}
|
|
}
|
|
|
|
if argsChunks.IsValid() && !argsChunks.IsZero() {
|
|
arguments, err := internal.ConcatSliceValue(argsChunks)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ret.Arguments = arguments.Interface()
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func concatServerToolResults(results []*ServerToolResult) (ret *ServerToolResult, err error) {
|
|
if len(results) == 0 {
|
|
return nil, fmt.Errorf("no server tool result found")
|
|
}
|
|
if len(results) == 1 {
|
|
return results[0], nil
|
|
}
|
|
|
|
ret = &ServerToolResult{}
|
|
|
|
var (
|
|
resType reflect.Type
|
|
resChunks reflect.Value
|
|
)
|
|
|
|
for _, r := range results {
|
|
if r == nil {
|
|
continue
|
|
}
|
|
|
|
if ret.CallID == "" {
|
|
ret.CallID = r.CallID
|
|
} else if r.CallID != "" && r.CallID != ret.CallID {
|
|
return nil, fmt.Errorf("expected call ID '%s' for server tool result, but got '%s'", ret.CallID, r.CallID)
|
|
}
|
|
|
|
if ret.Name == "" {
|
|
ret.Name = r.Name
|
|
} else if r.Name != "" && r.Name != ret.Name {
|
|
return nil, fmt.Errorf("expected tool name '%s' for server tool result, but got '%s'", ret.Name, r.Name)
|
|
}
|
|
|
|
if r.Content != nil {
|
|
resType_ := reflect.TypeOf(r.Content)
|
|
if resType == nil {
|
|
resType = resType_
|
|
resChunks = reflect.MakeSlice(reflect.SliceOf(resType), 0, len(results))
|
|
} else if resType != resType_ {
|
|
return nil, fmt.Errorf("expected type '%s' for server tool result, but got '%s'", resType, resType_)
|
|
}
|
|
resChunks = reflect.Append(resChunks, reflect.ValueOf(r.Content))
|
|
}
|
|
}
|
|
|
|
if resChunks.IsValid() && !resChunks.IsZero() {
|
|
result, err := internal.ConcatSliceValue(resChunks)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to concat server tool result: %v", err)
|
|
}
|
|
ret.Content = result.Interface()
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func concatMCPToolCalls(calls []*MCPToolCall) (*MCPToolCall, error) {
|
|
if len(calls) == 0 {
|
|
return nil, fmt.Errorf("no mcp tool call found")
|
|
}
|
|
if len(calls) == 1 {
|
|
return calls[0], nil
|
|
}
|
|
|
|
ret := &MCPToolCall{}
|
|
|
|
for _, c := range calls {
|
|
if c == nil {
|
|
continue
|
|
}
|
|
|
|
ret.Arguments += c.Arguments
|
|
|
|
if ret.ServerLabel == "" {
|
|
ret.ServerLabel = c.ServerLabel
|
|
} else if c.ServerLabel != "" && c.ServerLabel != ret.ServerLabel {
|
|
return nil, fmt.Errorf("expected server label '%s' for mcp tool call, but got '%s'", ret.ServerLabel, c.ServerLabel)
|
|
}
|
|
|
|
if ret.CallID == "" {
|
|
ret.CallID = c.CallID
|
|
} else if c.CallID != "" && c.CallID != ret.CallID {
|
|
return nil, fmt.Errorf("expected call ID '%s' for mcp tool call, but got '%s'", ret.CallID, c.CallID)
|
|
}
|
|
|
|
if ret.Name == "" {
|
|
ret.Name = c.Name
|
|
} else if c.Name != "" && c.Name != ret.Name {
|
|
return nil, fmt.Errorf("expected tool name '%s' for mcp tool call, but got '%s'", ret.Name, c.Name)
|
|
}
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func concatMCPToolResults(results []*MCPToolResult) (*MCPToolResult, error) {
|
|
if len(results) == 0 {
|
|
return nil, fmt.Errorf("no mcp tool result found")
|
|
}
|
|
if len(results) == 1 {
|
|
return results[0], nil
|
|
}
|
|
|
|
ret := &MCPToolResult{}
|
|
|
|
for _, r := range results {
|
|
if r == nil {
|
|
continue
|
|
}
|
|
|
|
if r.Content != "" {
|
|
ret.Content = r.Content
|
|
}
|
|
|
|
if ret.ServerLabel == "" {
|
|
ret.ServerLabel = r.ServerLabel
|
|
} else if r.ServerLabel != "" && r.ServerLabel != ret.ServerLabel {
|
|
return nil, fmt.Errorf("expected server label '%s' for mcp tool result, but got '%s'", ret.ServerLabel, r.ServerLabel)
|
|
}
|
|
|
|
if ret.CallID == "" {
|
|
ret.CallID = r.CallID
|
|
} else if r.CallID != "" && r.CallID != ret.CallID {
|
|
return nil, fmt.Errorf("expected call ID '%s' for mcp tool result, but got '%s'", ret.CallID, r.CallID)
|
|
}
|
|
|
|
if ret.Name == "" {
|
|
ret.Name = r.Name
|
|
} else if r.Name != "" && r.Name != ret.Name {
|
|
return nil, fmt.Errorf("expected tool name '%s' for mcp tool result, but got '%s'", ret.Name, r.Name)
|
|
}
|
|
|
|
if r.Error != nil {
|
|
ret.Error = r.Error
|
|
}
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func concatMCPListToolsResults(results []*MCPListToolsResult) (*MCPListToolsResult, error) {
|
|
if len(results) == 0 {
|
|
return nil, fmt.Errorf("no mcp list tools result found")
|
|
}
|
|
if len(results) == 1 {
|
|
return results[0], nil
|
|
}
|
|
|
|
ret := &MCPListToolsResult{}
|
|
|
|
for _, r := range results {
|
|
if r == nil {
|
|
continue
|
|
}
|
|
|
|
ret.Tools = append(ret.Tools, r.Tools...)
|
|
|
|
if r.Error != "" {
|
|
ret.Error = r.Error
|
|
}
|
|
|
|
if ret.ServerLabel == "" {
|
|
ret.ServerLabel = r.ServerLabel
|
|
} else if r.ServerLabel != "" && r.ServerLabel != ret.ServerLabel {
|
|
return nil, fmt.Errorf("expected server label '%s' for mcp list tools result, but got '%s'", ret.ServerLabel, r.ServerLabel)
|
|
}
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func concatMCPToolApprovalRequests(requests []*MCPToolApprovalRequest) (*MCPToolApprovalRequest, error) {
|
|
if len(requests) == 0 {
|
|
return nil, fmt.Errorf("no mcp tool approval request found")
|
|
}
|
|
if len(requests) == 1 {
|
|
return requests[0], nil
|
|
}
|
|
|
|
ret := &MCPToolApprovalRequest{}
|
|
|
|
for _, r := range requests {
|
|
if r == nil {
|
|
continue
|
|
}
|
|
|
|
ret.Arguments += r.Arguments
|
|
|
|
if ret.ID == "" {
|
|
ret.ID = r.ID
|
|
} else if r.ID != "" && r.ID != ret.ID {
|
|
return nil, fmt.Errorf("expected request ID '%s' for mcp tool approval request, but got '%s'", ret.ID, r.ID)
|
|
}
|
|
|
|
if ret.Name == "" {
|
|
ret.Name = r.Name
|
|
} else if r.Name != "" && r.Name != ret.Name {
|
|
return nil, fmt.Errorf("expected tool name '%s' for mcp tool approval request, but got '%s'", ret.Name, r.Name)
|
|
}
|
|
|
|
if ret.ServerLabel == "" {
|
|
ret.ServerLabel = r.ServerLabel
|
|
} else if r.ServerLabel != "" && r.ServerLabel != ret.ServerLabel {
|
|
return nil, fmt.Errorf("expected server label '%s' for mcp tool approval request, but got '%s'", ret.ServerLabel, r.ServerLabel)
|
|
}
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func concatMCPToolApprovalResponses(responses []*MCPToolApprovalResponse) (*MCPToolApprovalResponse, error) {
|
|
if len(responses) == 0 {
|
|
return nil, fmt.Errorf("no mcp tool approval response found")
|
|
}
|
|
if len(responses) == 1 {
|
|
return responses[0], nil
|
|
}
|
|
return nil, fmt.Errorf("cannot concat multiple mcp tool approval responses")
|
|
}
|
|
|
|
// String returns the string representation of AgenticMessage.
|
|
func (m *AgenticMessage) String() string {
|
|
sb := &strings.Builder{}
|
|
sb.WriteString(fmt.Sprintf("role: %s\n", m.Role))
|
|
|
|
if len(m.ContentBlocks) > 0 {
|
|
sb.WriteString("content_blocks:\n")
|
|
for i, block := range m.ContentBlocks {
|
|
if block == nil {
|
|
continue
|
|
}
|
|
sb.WriteString(fmt.Sprintf(" [%d] %s", i, block.String()))
|
|
}
|
|
}
|
|
|
|
if m.ResponseMeta != nil {
|
|
sb.WriteString(m.ResponseMeta.String())
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// String returns the string representation of ContentBlock.
|
|
// nolint
|
|
func (b *ContentBlock) String() string {
|
|
sb := &strings.Builder{}
|
|
sb.WriteString(fmt.Sprintf("type: %s\n", b.Type))
|
|
|
|
switch b.Type {
|
|
case ContentBlockTypeReasoning:
|
|
if b.Reasoning != nil {
|
|
sb.WriteString(b.Reasoning.String())
|
|
}
|
|
case ContentBlockTypeUserInputText:
|
|
if b.UserInputText != nil {
|
|
sb.WriteString(b.UserInputText.String())
|
|
}
|
|
case ContentBlockTypeUserInputImage:
|
|
if b.UserInputImage != nil {
|
|
sb.WriteString(b.UserInputImage.String())
|
|
}
|
|
case ContentBlockTypeUserInputAudio:
|
|
if b.UserInputAudio != nil {
|
|
sb.WriteString(b.UserInputAudio.String())
|
|
}
|
|
case ContentBlockTypeUserInputVideo:
|
|
if b.UserInputVideo != nil {
|
|
sb.WriteString(b.UserInputVideo.String())
|
|
}
|
|
case ContentBlockTypeUserInputFile:
|
|
if b.UserInputFile != nil {
|
|
sb.WriteString(b.UserInputFile.String())
|
|
}
|
|
case ContentBlockTypeToolSearchResult:
|
|
if b.ToolSearchFunctionToolResult != nil {
|
|
sb.WriteString(b.ToolSearchFunctionToolResult.String())
|
|
}
|
|
case ContentBlockTypeAssistantGenText:
|
|
if b.AssistantGenText != nil {
|
|
sb.WriteString(b.AssistantGenText.String())
|
|
}
|
|
case ContentBlockTypeAssistantGenImage:
|
|
if b.AssistantGenImage != nil {
|
|
sb.WriteString(b.AssistantGenImage.String())
|
|
}
|
|
case ContentBlockTypeAssistantGenAudio:
|
|
if b.AssistantGenAudio != nil {
|
|
sb.WriteString(b.AssistantGenAudio.String())
|
|
}
|
|
case ContentBlockTypeAssistantGenVideo:
|
|
if b.AssistantGenVideo != nil {
|
|
sb.WriteString(b.AssistantGenVideo.String())
|
|
}
|
|
case ContentBlockTypeFunctionToolCall:
|
|
if b.FunctionToolCall != nil {
|
|
sb.WriteString(b.FunctionToolCall.String())
|
|
}
|
|
case ContentBlockTypeFunctionToolResult:
|
|
if b.FunctionToolResult != nil {
|
|
sb.WriteString(b.FunctionToolResult.String())
|
|
}
|
|
case ContentBlockTypeServerToolCall:
|
|
if b.ServerToolCall != nil {
|
|
sb.WriteString(b.ServerToolCall.String())
|
|
}
|
|
case ContentBlockTypeServerToolResult:
|
|
if b.ServerToolResult != nil {
|
|
sb.WriteString(b.ServerToolResult.String())
|
|
}
|
|
case ContentBlockTypeMCPToolCall:
|
|
if b.MCPToolCall != nil {
|
|
sb.WriteString(b.MCPToolCall.String())
|
|
}
|
|
case ContentBlockTypeMCPToolResult:
|
|
if b.MCPToolResult != nil {
|
|
sb.WriteString(b.MCPToolResult.String())
|
|
}
|
|
case ContentBlockTypeMCPListToolsResult:
|
|
if b.MCPListToolsResult != nil {
|
|
sb.WriteString(b.MCPListToolsResult.String())
|
|
}
|
|
case ContentBlockTypeMCPToolApprovalRequest:
|
|
if b.MCPToolApprovalRequest != nil {
|
|
sb.WriteString(b.MCPToolApprovalRequest.String())
|
|
}
|
|
case ContentBlockTypeMCPToolApprovalResponse:
|
|
if b.MCPToolApprovalResponse != nil {
|
|
sb.WriteString(b.MCPToolApprovalResponse.String())
|
|
}
|
|
}
|
|
|
|
if b.StreamingMeta != nil {
|
|
sb.WriteString(fmt.Sprintf(" stream_index: %d\n", b.StreamingMeta.Index))
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// String returns the string representation of Reasoning.
|
|
func (r *Reasoning) String() string {
|
|
sb := &strings.Builder{}
|
|
sb.WriteString(fmt.Sprintf(" text: %s\n", r.Text))
|
|
if r.Signature != "" {
|
|
sb.WriteString(fmt.Sprintf(" signature: %s\n", truncateString(r.Signature, 50)))
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
// String returns the string representation of UserInputText.
|
|
func (u *UserInputText) String() string {
|
|
return fmt.Sprintf(" text: %s\n", u.Text)
|
|
}
|
|
|
|
// String returns the string representation of UserInputImage.
|
|
func (u *UserInputImage) String() string {
|
|
return formatMediaString(u.URL, u.Base64Data, u.MIMEType, string(u.Detail))
|
|
}
|
|
|
|
// String returns the string representation of UserInputAudio.
|
|
func (u *UserInputAudio) String() string {
|
|
return formatMediaString(u.URL, u.Base64Data, u.MIMEType, "")
|
|
}
|
|
|
|
// String returns the string representation of UserInputVideo.
|
|
func (u *UserInputVideo) String() string {
|
|
return formatMediaString(u.URL, u.Base64Data, u.MIMEType, "")
|
|
}
|
|
|
|
// String returns the string representation of UserInputFile.
|
|
func (u *UserInputFile) String() string {
|
|
sb := &strings.Builder{}
|
|
if u.Name != "" {
|
|
sb.WriteString(fmt.Sprintf(" name: %s\n", u.Name))
|
|
}
|
|
sb.WriteString(formatMediaString(u.URL, u.Base64Data, u.MIMEType, ""))
|
|
return sb.String()
|
|
}
|
|
|
|
// String returns the string representation of AssistantGenText.
|
|
func (a *AssistantGenText) String() string {
|
|
return fmt.Sprintf(" text: %s\n", a.Text)
|
|
}
|
|
|
|
// String returns the string representation of AssistantGenImage.
|
|
func (a *AssistantGenImage) String() string {
|
|
return formatMediaString(a.URL, a.Base64Data, a.MIMEType, "")
|
|
}
|
|
|
|
// String returns the string representation of AssistantGenAudio.
|
|
func (a *AssistantGenAudio) String() string {
|
|
return formatMediaString(a.URL, a.Base64Data, a.MIMEType, "")
|
|
}
|
|
|
|
// String returns the string representation of AssistantGenVideo.
|
|
func (a *AssistantGenVideo) String() string {
|
|
return formatMediaString(a.URL, a.Base64Data, a.MIMEType, "")
|
|
}
|
|
|
|
// String returns the string representation of FunctionToolCall.
|
|
func (f *FunctionToolCall) String() string {
|
|
sb := &strings.Builder{}
|
|
sb.WriteString(fmt.Sprintf(" call_id: %s\n", f.CallID))
|
|
sb.WriteString(fmt.Sprintf(" name: %s\n", f.Name))
|
|
sb.WriteString(fmt.Sprintf(" arguments: %s\n", f.Arguments))
|
|
return sb.String()
|
|
}
|
|
|
|
// String returns the string representation of FunctionToolResult.
|
|
func (f *FunctionToolResult) String() string {
|
|
sb := &strings.Builder{}
|
|
sb.WriteString(fmt.Sprintf(" call_id: %s\n", f.CallID))
|
|
sb.WriteString(fmt.Sprintf(" name: %s\n", f.Name))
|
|
if len(f.Content) > 0 {
|
|
sb.WriteString(fmt.Sprintf(" content: (%d blocks)\n", len(f.Content)))
|
|
for i, block := range f.Content {
|
|
if block == nil {
|
|
continue
|
|
}
|
|
sb.WriteString(fmt.Sprintf(" [%d] %s", i, block.String()))
|
|
}
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
// String returns the string representation of ServerToolCall.
|
|
func (s *ServerToolCall) String() string {
|
|
sb := &strings.Builder{}
|
|
sb.WriteString(fmt.Sprintf(" name: %s\n", s.Name))
|
|
if s.CallID != "" {
|
|
sb.WriteString(fmt.Sprintf(" call_id: %s\n", s.CallID))
|
|
}
|
|
sb.WriteString(fmt.Sprintf(" arguments: %s\n", printAny(s.Arguments)))
|
|
return sb.String()
|
|
}
|
|
|
|
// String returns the string representation of ServerToolResult.
|
|
func (s *ServerToolResult) String() string {
|
|
sb := &strings.Builder{}
|
|
sb.WriteString(fmt.Sprintf(" name: %s\n", s.Name))
|
|
if s.CallID != "" {
|
|
sb.WriteString(fmt.Sprintf(" call_id: %s\n", s.CallID))
|
|
}
|
|
sb.WriteString(fmt.Sprintf(" content: %s\n", printAny(s.Content)))
|
|
return sb.String()
|
|
}
|
|
|
|
// String returns the string representation of MCPToolCall.
|
|
func (m *MCPToolCall) String() string {
|
|
sb := &strings.Builder{}
|
|
sb.WriteString(fmt.Sprintf(" server_label: %s\n", m.ServerLabel))
|
|
sb.WriteString(fmt.Sprintf(" call_id: %s\n", m.CallID))
|
|
sb.WriteString(fmt.Sprintf(" name: %s\n", m.Name))
|
|
sb.WriteString(fmt.Sprintf(" arguments: %s\n", m.Arguments))
|
|
return sb.String()
|
|
}
|
|
|
|
// String returns the string representation of MCPToolResult.
|
|
func (m *MCPToolResult) String() string {
|
|
sb := &strings.Builder{}
|
|
sb.WriteString(fmt.Sprintf(" call_id: %s\n", m.CallID))
|
|
sb.WriteString(fmt.Sprintf(" name: %s\n", m.Name))
|
|
sb.WriteString(fmt.Sprintf(" content: %s\n", m.Content))
|
|
if m.Error != nil {
|
|
if m.Error.Code != nil {
|
|
sb.WriteString(fmt.Sprintf(" error: [%d] %s\n", *m.Error.Code, m.Error.Message))
|
|
} else {
|
|
sb.WriteString(fmt.Sprintf(" error: %s\n", m.Error.Message))
|
|
}
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
// String returns the string representation of MCPListToolsResult.
|
|
func (m *MCPListToolsResult) String() string {
|
|
sb := &strings.Builder{}
|
|
sb.WriteString(fmt.Sprintf(" server_label: %s\n", m.ServerLabel))
|
|
sb.WriteString(fmt.Sprintf(" tools: %d items\n", len(m.Tools)))
|
|
for _, tool := range m.Tools {
|
|
sb.WriteString(fmt.Sprintf(" - %s: %s\n", tool.Name, tool.Description))
|
|
}
|
|
if m.Error != "" {
|
|
sb.WriteString(fmt.Sprintf(" error: %s\n", m.Error))
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
// String returns the string representation of MCPToolApprovalRequest.
|
|
func (m *MCPToolApprovalRequest) String() string {
|
|
sb := &strings.Builder{}
|
|
sb.WriteString(fmt.Sprintf(" server_label: %s\n", m.ServerLabel))
|
|
sb.WriteString(fmt.Sprintf(" id: %s\n", m.ID))
|
|
sb.WriteString(fmt.Sprintf(" name: %s\n", m.Name))
|
|
sb.WriteString(fmt.Sprintf(" arguments: %s\n", m.Arguments))
|
|
return sb.String()
|
|
}
|
|
|
|
// String returns the string representation of MCPToolApprovalResponse.
|
|
func (m *MCPToolApprovalResponse) String() string {
|
|
sb := &strings.Builder{}
|
|
sb.WriteString(fmt.Sprintf(" approval_request_id: %s\n", m.ApprovalRequestID))
|
|
sb.WriteString(fmt.Sprintf(" approve: %v\n", m.Approve))
|
|
if m.Reason != "" {
|
|
sb.WriteString(fmt.Sprintf(" reason: %s\n", m.Reason))
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
// String returns the string representation of AgenticResponseMeta.
|
|
func (a *AgenticResponseMeta) String() string {
|
|
sb := &strings.Builder{}
|
|
sb.WriteString("response_meta:\n")
|
|
if a.TokenUsage != nil {
|
|
sb.WriteString(fmt.Sprintf(" token_usage: prompt=%d, completion=%d, total=%d\n",
|
|
a.TokenUsage.PromptTokens,
|
|
a.TokenUsage.CompletionTokens,
|
|
a.TokenUsage.TotalTokens))
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
// truncateString truncates a string to maxLen characters, adding "..." if truncated
|
|
func truncateString(s string, maxLen int) string {
|
|
if len(s) <= maxLen {
|
|
return s
|
|
}
|
|
return s[:maxLen] + "..."
|
|
}
|
|
|
|
// formatMediaString formats URL, Base64Data, MIMEType and Detail for media content
|
|
func formatMediaString(url, base64Data string, mimeType string, detail string) string {
|
|
sb := &strings.Builder{}
|
|
if url != "" {
|
|
sb.WriteString(fmt.Sprintf(" url: %s\n", truncateString(url, 100)))
|
|
}
|
|
if base64Data != "" {
|
|
// Only show first few characters of base64 data
|
|
sb.WriteString(fmt.Sprintf(" base64_data: %s... (%d bytes)\n", truncateString(base64Data, 20), len(base64Data)))
|
|
}
|
|
if mimeType != "" {
|
|
sb.WriteString(fmt.Sprintf(" mime_type: %s\n", mimeType))
|
|
}
|
|
if detail != "" {
|
|
sb.WriteString(fmt.Sprintf(" detail: %s\n", detail))
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
func validateExtensionType(expected reflect.Type, actual any) (reflect.Type, bool) {
|
|
if actual == nil {
|
|
return expected, true
|
|
}
|
|
actualType := reflect.TypeOf(actual)
|
|
if expected == nil {
|
|
return actualType, true
|
|
}
|
|
if expected != actualType {
|
|
return expected, false
|
|
}
|
|
return expected, true
|
|
}
|
|
|
|
func printAny(a any) string {
|
|
switch v := a.(type) {
|
|
case string:
|
|
return v
|
|
case fmt.Stringer:
|
|
return v.String()
|
|
default:
|
|
b, err := json.MarshalIndent(a, "", " ")
|
|
if err != nil {
|
|
return fmt.Sprintf("%v", a)
|
|
}
|
|
return string(b)
|
|
}
|
|
}
|