Files
bifrost/core/mcp/agentadaptors.go
Beyhan Oğur 880f412e2c first commit
2026-04-26 21:52:23 +03:00

585 lines
22 KiB
Go

package mcp
import (
"fmt"
"github.com/maximhq/bifrost/core/schemas"
)
// agentAPIAdapter defines the interface for API-specific operations in agent mode.
// This adapter pattern allows the agent execution logic to work with both Chat Completions
// and Responses APIs without requiring API-specific code in the agent loop.
//
// The adapter handles format conversions at the boundaries:
// - Responses API requests/responses are converted to/from Chat API format
// - Tool calls are extracted in Chat format for uniform processing
// - Results are converted back to the original API format for the response
//
// This design ensures that:
// 1. Tool execution logic is format-agnostic
// 2. Both APIs have feature parity
// 3. Conversions are localized to adapters
// 4. The agent loop remains API-neutral
type agentAPIAdapter interface {
// Extract conversation history from the original request
getConversationHistory() []interface{}
// Get original request
getOriginalRequest() interface{}
// Get initial response
getInitialResponse() interface{}
// Check if response has tool calls
hasToolCalls(response interface{}) bool
// Extract tool calls from response.
// For Chat API: Returns tool calls directly from the response.
// For Responses API: Converts ResponsesMessage tool calls to ChatAssistantMessageToolCall for processing.
extractToolCalls(response interface{}) []schemas.ChatAssistantMessageToolCall
// Add assistant message with tool calls to conversation
addAssistantMessage(conversation []interface{}, response interface{}) []interface{}
// Add tool results to conversation.
// For Chat API: Adds ChatMessage results directly.
// For Responses API: Converts ChatMessage results to ResponsesMessage via ToResponsesToolMessage().
addToolResults(conversation []interface{}, toolResults []*schemas.ChatMessage) []interface{}
// Create new request with updated conversation
createNewRequest(conversation []interface{}) interface{}
// Make LLM call
makeLLMCall(ctx *schemas.BifrostContext, request interface{}) (interface{}, *schemas.BifrostError)
// Create response with executed tools and non-auto-executable calls
createResponseWithExecutedTools(
response interface{},
executedToolResults []*schemas.ChatMessage,
executedToolCalls []schemas.ChatAssistantMessageToolCall,
nonAutoExecutableToolCalls []schemas.ChatAssistantMessageToolCall,
) interface{}
// extractUsage returns the token usage from a response as BifrostLLMUsage.
extractUsage(response interface{}) *schemas.BifrostLLMUsage
// applyUsage sets accumulated usage on the response in place.
applyUsage(response interface{}, usage *schemas.BifrostLLMUsage)
}
// chatAPIAdapter implements agentAPIAdapter for Chat API
type chatAPIAdapter struct {
originalReq *schemas.BifrostChatRequest
initialResponse *schemas.BifrostChatResponse
makeReq func(ctx *schemas.BifrostContext, req *schemas.BifrostChatRequest) (*schemas.BifrostChatResponse, *schemas.BifrostError)
}
// responsesAPIAdapter implements agentAPIAdapter for Responses API.
// It enables the agent mode execution loop to work with Responses API requests and responses
// by handling format conversions transparently.
//
// Key conversions performed:
// - extractToolCalls(): Converts ResponsesMessage tool calls to ChatAssistantMessageToolCall
// via BifrostResponsesResponse.ToBifrostChatResponse() and existing extraction logic
// - addToolResults(): Converts ChatMessage tool results back to ResponsesMessage
// via ChatMessage.ToResponsesMessages() and ToResponsesToolMessage()
// - createNewRequest(): Builds a new BifrostResponsesRequest from converted conversation
// - createResponseWithExecutedTools(): Creates a Responses response with results and pending tools
//
// This adapter enables full feature parity between Chat Completions and Responses APIs
// for tool execution in agent mode.
type responsesAPIAdapter struct {
originalReq *schemas.BifrostResponsesRequest
initialResponse *schemas.BifrostResponsesResponse
makeReq func(ctx *schemas.BifrostContext, req *schemas.BifrostResponsesRequest) (*schemas.BifrostResponsesResponse, *schemas.BifrostError)
}
// Chat API adapter implementations
func (c *chatAPIAdapter) getConversationHistory() []interface{} {
history := make([]interface{}, 0)
if c.originalReq.Input != nil {
for _, msg := range c.originalReq.Input {
history = append(history, msg)
}
}
return history
}
func (c *chatAPIAdapter) getOriginalRequest() interface{} {
return c.originalReq
}
func (c *chatAPIAdapter) getInitialResponse() interface{} {
return c.initialResponse
}
func (c *chatAPIAdapter) hasToolCalls(response interface{}) bool {
chatResponse := response.(*schemas.BifrostChatResponse)
return hasToolCallsForChatResponse(chatResponse)
}
func (c *chatAPIAdapter) extractToolCalls(response interface{}) []schemas.ChatAssistantMessageToolCall {
chatResponse := response.(*schemas.BifrostChatResponse)
return extractToolCalls(chatResponse)
}
func (c *chatAPIAdapter) addAssistantMessage(conversation []interface{}, response interface{}) []interface{} {
chatResponse := response.(*schemas.BifrostChatResponse)
for _, choice := range chatResponse.Choices {
if choice.ChatNonStreamResponseChoice != nil && choice.ChatNonStreamResponseChoice.Message != nil {
conversation = append(conversation, *choice.ChatNonStreamResponseChoice.Message)
}
}
return conversation
}
func (c *chatAPIAdapter) addToolResults(conversation []interface{}, toolResults []*schemas.ChatMessage) []interface{} {
for _, toolResult := range toolResults {
conversation = append(conversation, *toolResult)
}
return conversation
}
func (c *chatAPIAdapter) createNewRequest(conversation []interface{}) interface{} {
// Convert conversation back to ChatMessage slice
chatMessages := make([]schemas.ChatMessage, 0, len(conversation))
for _, msg := range conversation {
if msg == nil {
continue
}
if chatMessage, ok := msg.(schemas.ChatMessage); ok {
chatMessages = append(chatMessages, chatMessage)
}
}
return &schemas.BifrostChatRequest{
Provider: c.originalReq.Provider,
Model: c.originalReq.Model,
Fallbacks: c.originalReq.Fallbacks,
Params: c.originalReq.Params,
Input: chatMessages,
}
}
func (c *chatAPIAdapter) makeLLMCall(ctx *schemas.BifrostContext, request interface{}) (interface{}, *schemas.BifrostError) {
chatRequest := request.(*schemas.BifrostChatRequest)
return c.makeReq(ctx, chatRequest)
}
func (c *chatAPIAdapter) createResponseWithExecutedTools(
response interface{},
executedToolResults []*schemas.ChatMessage,
executedToolCalls []schemas.ChatAssistantMessageToolCall,
nonAutoExecutableToolCalls []schemas.ChatAssistantMessageToolCall,
) interface{} {
chatResponse := response.(*schemas.BifrostChatResponse)
return createChatResponseWithExecutedToolsAndNonAutoExecutableCalls(
chatResponse,
executedToolResults,
executedToolCalls,
nonAutoExecutableToolCalls,
)
}
func (c *chatAPIAdapter) extractUsage(response interface{}) *schemas.BifrostLLMUsage {
return response.(*schemas.BifrostChatResponse).Usage
}
func (c *chatAPIAdapter) applyUsage(response interface{}, usage *schemas.BifrostLLMUsage) {
response.(*schemas.BifrostChatResponse).Usage = usage
}
// createChatResponseWithExecutedToolsAndNonAutoExecutableCalls creates a chat response
// that includes executed tool results and non-auto-executable tool calls. The response
// contains a formatted text summary of executed tool results and includes the non-auto-executable
// tool calls for the caller to handle. The finish reason is set to "stop" to prevent
// further agent loop iterations.
//
// Parameters:
// - originalResponse: The original chat response to copy metadata from
// - executedToolResults: List of tool execution results from auto-executable tools
// - executedToolCalls: List of tool calls that were executed
// - nonAutoExecutableToolCalls: List of tool calls that require manual execution
//
// Returns:
// - *schemas.BifrostChatResponse: A new chat response with executed results and pending tool calls
func createChatResponseWithExecutedToolsAndNonAutoExecutableCalls(
originalResponse *schemas.BifrostChatResponse,
executedToolResults []*schemas.ChatMessage,
executedToolCalls []schemas.ChatAssistantMessageToolCall,
nonAutoExecutableToolCalls []schemas.ChatAssistantMessageToolCall,
) *schemas.BifrostChatResponse {
// Start with a copy of the original response metadata
response := &schemas.BifrostChatResponse{
ID: originalResponse.ID,
Object: originalResponse.Object,
Created: originalResponse.Created,
Model: originalResponse.Model,
Choices: make([]schemas.BifrostResponseChoice, 0),
ServiceTier: originalResponse.ServiceTier,
SystemFingerprint: originalResponse.SystemFingerprint,
Usage: originalResponse.Usage,
ExtraFields: originalResponse.ExtraFields,
SearchResults: originalResponse.SearchResults,
Videos: originalResponse.Videos,
Citations: originalResponse.Citations,
}
// Build a map from tool call ID to tool name for easy lookup
toolCallIDToName := make(map[string]string)
for _, toolCall := range executedToolCalls {
if toolCall.ID != nil && toolCall.Function.Name != nil {
toolCallIDToName[*toolCall.ID] = *toolCall.Function.Name
}
}
// Build content text showing executed tool results
var contentText string
if len(executedToolResults) > 0 {
// Format tool results as JSON-like structure
toolResultsMap := make(map[string]interface{})
for _, toolResult := range executedToolResults {
// Get tool name from tool call ID mapping
var toolName string
if toolResult.ChatToolMessage != nil && toolResult.ChatToolMessage.ToolCallID != nil {
toolCallID := *toolResult.ChatToolMessage.ToolCallID
if name, ok := toolCallIDToName[toolCallID]; ok {
toolName = name
} else {
toolName = toolCallID // Fallback to tool call ID if name not found
}
} else {
toolName = "unknown_tool"
}
// Extract output from tool result
var output interface{}
if toolResult.Content != nil {
if toolResult.Content.ContentStr != nil {
output = *toolResult.Content.ContentStr
} else if toolResult.Content.ContentBlocks != nil {
// Convert content blocks to a readable format
blocks := make([]map[string]interface{}, 0)
for _, block := range toolResult.Content.ContentBlocks {
blockMap := make(map[string]interface{})
blockMap["type"] = string(block.Type)
if block.Text != nil {
blockMap["text"] = *block.Text
}
blocks = append(blocks, blockMap)
}
output = blocks
}
}
toolResultsMap[toolName] = output
}
// Convert to JSON string for display
jsonBytes, err := schemas.MarshalSorted(toolResultsMap)
if err != nil {
// Fallback to simple string representation
contentText = fmt.Sprintf("The Output from allowed tools calls is - %v\n\nNow I shall call these tools next...", toolResultsMap)
} else {
contentText = fmt.Sprintf("The Output from allowed tools calls is - %s\n\nNow I shall call these tools next...", string(jsonBytes))
}
} else {
contentText = "Now I shall call these tools next..."
}
// Create content with the formatted text
content := &schemas.ChatMessageContent{
ContentStr: &contentText,
}
// Determine finish reason
// Note: We set finish_reason to "stop" (not "tool_calls") for non-auto-executable tools
// to prevent the agent loop from retrying. The tool calls are still included in the response
// for the caller to handle, but setting finish_reason to "stop" ensures hasToolCalls returns false
// and the agent loop exits properly.
finishReason := "stop"
// Create a single choice with the formatted content and non-auto-executable tool calls
response.Choices = append(response.Choices, schemas.BifrostResponseChoice{
Index: 0,
FinishReason: &finishReason,
ChatNonStreamResponseChoice: &schemas.ChatNonStreamResponseChoice{
Message: &schemas.ChatMessage{
Role: schemas.ChatMessageRoleAssistant,
Content: content,
ChatAssistantMessage: &schemas.ChatAssistantMessage{
ToolCalls: nonAutoExecutableToolCalls,
},
},
},
})
return response
}
// Responses API adapter implementations
func (r *responsesAPIAdapter) getConversationHistory() []interface{} {
history := make([]interface{}, 0)
if r.originalReq.Input != nil {
for _, msg := range r.originalReq.Input {
history = append(history, msg)
}
}
return history
}
func (r *responsesAPIAdapter) getOriginalRequest() interface{} {
return r.originalReq
}
func (r *responsesAPIAdapter) getInitialResponse() interface{} {
return r.initialResponse
}
func (r *responsesAPIAdapter) hasToolCalls(response interface{}) bool {
responsesResponse := response.(*schemas.BifrostResponsesResponse)
return hasToolCallsForResponsesResponse(responsesResponse)
}
func (r *responsesAPIAdapter) extractToolCalls(response interface{}) []schemas.ChatAssistantMessageToolCall {
responsesResponse := response.(*schemas.BifrostResponsesResponse)
// Convert to Chat format and extract tool calls using existing logic
chatResponse := responsesResponse.ToBifrostChatResponse()
return extractToolCalls(chatResponse)
}
func (r *responsesAPIAdapter) addAssistantMessage(conversation []interface{}, response interface{}) []interface{} {
responsesResponse := response.(*schemas.BifrostResponsesResponse)
for _, output := range responsesResponse.Output {
conversation = append(conversation, output)
}
return conversation
}
func (r *responsesAPIAdapter) addToolResults(conversation []interface{}, toolResults []*schemas.ChatMessage) []interface{} {
for _, toolResult := range toolResults {
// Convert using existing converter
responsesMessages := toolResult.ToResponsesMessages()
for _, respMsg := range responsesMessages {
conversation = append(conversation, respMsg)
}
}
return conversation
}
func (r *responsesAPIAdapter) createNewRequest(conversation []interface{}) interface{} {
// Convert conversation back to ResponsesMessage slice
responsesMessages := make([]schemas.ResponsesMessage, 0, len(conversation))
for _, msg := range conversation {
responsesMessages = append(responsesMessages, msg.(schemas.ResponsesMessage))
}
return &schemas.BifrostResponsesRequest{
Provider: r.originalReq.Provider,
Model: r.originalReq.Model,
Fallbacks: r.originalReq.Fallbacks,
Params: r.originalReq.Params,
Input: responsesMessages,
}
}
func (r *responsesAPIAdapter) makeLLMCall(ctx *schemas.BifrostContext, request interface{}) (interface{}, *schemas.BifrostError) {
responsesRequest := request.(*schemas.BifrostResponsesRequest)
return r.makeReq(ctx, responsesRequest)
}
func (r *responsesAPIAdapter) createResponseWithExecutedTools(
response interface{},
executedToolResults []*schemas.ChatMessage,
executedToolCalls []schemas.ChatAssistantMessageToolCall,
nonAutoExecutableToolCalls []schemas.ChatAssistantMessageToolCall,
) interface{} {
responsesResponse := response.(*schemas.BifrostResponsesResponse)
// Create response with executed tools directly on Responses schema
return createResponsesResponseWithExecutedToolsAndNonAutoExecutableCalls(
responsesResponse,
executedToolResults,
executedToolCalls,
nonAutoExecutableToolCalls,
)
}
func (r *responsesAPIAdapter) extractUsage(response interface{}) *schemas.BifrostLLMUsage {
return response.(*schemas.BifrostResponsesResponse).Usage.ToBifrostLLMUsage()
}
func (r *responsesAPIAdapter) applyUsage(response interface{}, usage *schemas.BifrostLLMUsage) {
response.(*schemas.BifrostResponsesResponse).Usage = usage.ToResponsesResponseUsage()
}
// createResponsesResponseWithExecutedToolsAndNonAutoExecutableCalls creates a responses response
// that includes executed tool results and non-auto-executable tool calls. The response
// contains a formatted text summary of executed tool results and includes the non-auto-executable
// tool calls for the caller to handle. All Response-specific fields are preserved.
//
// Parameters:
// - originalResponse: The original responses response to copy metadata from
// - executedToolResults: List of tool execution results from auto-executable tools
// - executedToolCalls: List of tool calls that were executed
// - nonAutoExecutableToolCalls: List of tool calls that require manual execution
//
// Returns:
// - *schemas.BifrostResponsesResponse: A new responses response with executed results and pending tool calls
func createResponsesResponseWithExecutedToolsAndNonAutoExecutableCalls(
originalResponse *schemas.BifrostResponsesResponse,
executedToolResults []*schemas.ChatMessage,
executedToolCalls []schemas.ChatAssistantMessageToolCall,
nonAutoExecutableToolCalls []schemas.ChatAssistantMessageToolCall,
) *schemas.BifrostResponsesResponse {
// Start with a copy of the original response, preserving all Response-specific fields
response := &schemas.BifrostResponsesResponse{
ID: originalResponse.ID,
Background: originalResponse.Background,
Conversation: originalResponse.Conversation,
CreatedAt: originalResponse.CreatedAt,
Error: originalResponse.Error,
Include: originalResponse.Include,
IncompleteDetails: originalResponse.IncompleteDetails,
Instructions: originalResponse.Instructions,
MaxOutputTokens: originalResponse.MaxOutputTokens,
MaxToolCalls: originalResponse.MaxToolCalls,
Metadata: originalResponse.Metadata,
ParallelToolCalls: originalResponse.ParallelToolCalls,
PreviousResponseID: originalResponse.PreviousResponseID,
Prompt: originalResponse.Prompt,
PromptCacheKey: originalResponse.PromptCacheKey,
Reasoning: originalResponse.Reasoning,
SafetyIdentifier: originalResponse.SafetyIdentifier,
ServiceTier: originalResponse.ServiceTier,
StreamOptions: originalResponse.StreamOptions,
Store: originalResponse.Store,
Temperature: originalResponse.Temperature,
Text: originalResponse.Text,
TopLogProbs: originalResponse.TopLogProbs,
TopP: originalResponse.TopP,
ToolChoice: originalResponse.ToolChoice,
Tools: originalResponse.Tools,
Truncation: originalResponse.Truncation,
Usage: originalResponse.Usage,
ExtraFields: originalResponse.ExtraFields,
// Perplexity-specific fields
SearchResults: originalResponse.SearchResults,
Videos: originalResponse.Videos,
Citations: originalResponse.Citations,
Output: make([]schemas.ResponsesMessage, 0),
}
// Build a map from tool call ID to tool name for easy lookup
toolCallIDToName := make(map[string]string)
for _, toolCall := range executedToolCalls {
if toolCall.ID != nil && toolCall.Function.Name != nil {
toolCallIDToName[*toolCall.ID] = *toolCall.Function.Name
}
}
// Build content text showing executed tool results
var contentText string
if len(executedToolResults) > 0 {
// Format tool results as JSON-like structure
toolResultsMap := make(map[string]interface{})
for _, toolResult := range executedToolResults {
// Get tool name from tool call ID mapping
var toolName string
if toolResult.ChatToolMessage != nil && toolResult.ChatToolMessage.ToolCallID != nil {
toolCallID := *toolResult.ChatToolMessage.ToolCallID
if name, ok := toolCallIDToName[toolCallID]; ok {
toolName = name
} else {
toolName = toolCallID // Fallback to tool call ID if name not found
}
} else {
toolName = "unknown_tool"
}
// Extract output from tool result
var output interface{}
if toolResult.Content != nil {
if toolResult.Content.ContentStr != nil {
output = *toolResult.Content.ContentStr
} else if toolResult.Content.ContentBlocks != nil {
// Convert content blocks to a readable format
blocks := make([]map[string]interface{}, 0)
for _, block := range toolResult.Content.ContentBlocks {
blockMap := make(map[string]interface{})
blockMap["type"] = string(block.Type)
if block.Text != nil {
blockMap["text"] = *block.Text
}
blocks = append(blocks, blockMap)
}
output = blocks
}
}
toolResultsMap[toolName] = output
}
// Convert to JSON string for display
jsonBytes, err := schemas.MarshalSorted(toolResultsMap)
if err != nil {
// Fallback to simple string representation
contentText = fmt.Sprintf("The Output from allowed tools calls is - %v\n\nNow I shall call these tools next...", toolResultsMap)
} else {
contentText = fmt.Sprintf("The Output from allowed tools calls is - %s\n\nNow I shall call these tools next...", string(jsonBytes))
}
} else {
contentText = "Now I shall call these tools next..."
}
// Create assistant message with the formatted text content
messageType := schemas.ResponsesMessageTypeMessage
role := schemas.ResponsesInputMessageRoleAssistant
assistantMessage := schemas.ResponsesMessage{
Type: &messageType,
Role: &role,
Content: &schemas.ResponsesMessageContent{
ContentBlocks: []schemas.ResponsesMessageContentBlock{
{
Type: schemas.ResponsesOutputMessageContentTypeText,
Text: &contentText,
},
},
},
}
response.Output = append(response.Output, assistantMessage)
// Add non-auto-executable tool calls as separate function_call messages
for _, toolCall := range nonAutoExecutableToolCalls {
functionCallType := schemas.ResponsesMessageTypeFunctionCall
assistantRole := schemas.ResponsesInputMessageRoleAssistant
var callID *string
if toolCall.ID != nil && *toolCall.ID != "" {
callID = toolCall.ID
}
var namePtr *string
if toolCall.Function.Name != nil && *toolCall.Function.Name != "" {
namePtr = toolCall.Function.Name
}
var argumentsPtr *string
if toolCall.Function.Arguments != "" {
argumentsPtr = &toolCall.Function.Arguments
}
toolCallMessage := schemas.ResponsesMessage{
Type: &functionCallType,
Role: &assistantRole,
ResponsesToolMessage: &schemas.ResponsesToolMessage{
CallID: callID,
Name: namePtr,
Arguments: argumentsPtr,
},
}
response.Output = append(response.Output, toolCallMessage)
}
return response
}