diff --git a/core/bifrost.go b/core/bifrost.go index f754cc810..26e8e5e5e 100644 --- a/core/bifrost.go +++ b/core/bifrost.go @@ -1693,7 +1693,7 @@ func (bifrost *Bifrost) RegisterMCPTool(name, description string, handler func(a return bifrost.mcpManager.RegisterTool(name, description, handler, toolSchema) } -// ExecuteMCPTool executes an MCP tool call and returns the result as a tool message. +// ExecuteChatMCPTool executes an MCP tool call and returns the result as a chat message. // This is the main public API for manual MCP tool execution. // // Parameters: @@ -1703,7 +1703,7 @@ func (bifrost *Bifrost) RegisterMCPTool(name, description string, handler func(a // Returns: // - schemas.ChatMessage: Tool message with execution result // - schemas.BifrostError: Any execution error -func (bifrost *Bifrost) ExecuteMCPTool(ctx context.Context, toolCall schemas.ChatAssistantMessageToolCall) (*schemas.ChatMessage, *schemas.BifrostError) { +func (bifrost *Bifrost) ExecuteChatMCPTool(ctx context.Context, toolCall schemas.ChatAssistantMessageToolCall) (*schemas.ChatMessage, *schemas.BifrostError) { if bifrost.mcpManager == nil { return nil, &schemas.BifrostError{ IsBifrostError: false, @@ -1716,7 +1716,7 @@ func (bifrost *Bifrost) ExecuteMCPTool(ctx context.Context, toolCall schemas.Cha } } - result, err := bifrost.mcpManager.ExecuteTool(ctx, toolCall) + result, err := bifrost.mcpManager.ExecuteChatTool(ctx, toolCall) if err != nil { return nil, &schemas.BifrostError{ IsBifrostError: false, @@ -1732,6 +1732,44 @@ func (bifrost *Bifrost) ExecuteMCPTool(ctx context.Context, toolCall schemas.Cha return result, nil } +// ExecuteResponsesMCPTool executes an MCP tool call and returns the result as a responses message. + +// Parameters: +// - ctx: Execution context +// - toolCall: The tool call to execute (from assistant message) +// +// Returns: +// - schemas.ResponsesMessage: Tool message with execution result +// - schemas.BifrostError: Any execution error +func (bifrost *Bifrost) ExecuteResponsesMCPTool(ctx context.Context, toolCall *schemas.ResponsesToolMessage) (*schemas.ResponsesMessage, *schemas.BifrostError) { + if bifrost.mcpManager == nil { + return nil, &schemas.BifrostError{ + IsBifrostError: false, + Error: &schemas.ErrorField{ + Message: "MCP is not configured in this Bifrost instance", + }, + ExtraFields: schemas.BifrostErrorExtraFields{ + RequestType: schemas.ResponsesRequest, // MCP tools are used with responses requests + }, + } + } + + result, err := bifrost.mcpManager.ExecuteResponsesTool(ctx, toolCall) + if err != nil { + return nil, &schemas.BifrostError{ + IsBifrostError: false, + Error: &schemas.ErrorField{ + Message: err.Error(), + }, + ExtraFields: schemas.BifrostErrorExtraFields{ + RequestType: schemas.ResponsesRequest, // MCP tools are used with responses requests + }, + } + } + + return result, nil +} + // IMPORTANT: Running the MCP client management operations (GetMCPClients, AddMCPClient, RemoveMCPClient, EditMCPClientTools) // may temporarily increase latency for incoming requests while the operations are being processed. // These operations involve network I/O and connection management that require mutex locks diff --git a/core/chatbot_test.go b/core/chatbot_test.go index ff8cef7a2..67aae2972 100644 --- a/core/chatbot_test.go +++ b/core/chatbot_test.go @@ -563,7 +563,7 @@ func (s *ChatSession) handleToolCalls(assistantMessage schemas.ChatMessage) (str stopChan, wg := startLoader() // Execute the tool using Bifrost's integrated MCP functionality - toolResult, err := s.client.ExecuteMCPTool(context.Background(), toolCall) + toolResult, err := s.client.ExecuteChatMCPTool(context.Background(), toolCall) // Stop loading animation stopLoader(stopChan, wg) diff --git a/core/mcp/agent_adaptors.go b/core/mcp/agent_adaptors.go index 9aa99b31f..d6118cbf7 100644 --- a/core/mcp/agent_adaptors.go +++ b/core/mcp/agent_adaptors.go @@ -8,7 +8,20 @@ import ( "github.com/maximhq/bifrost/core/schemas" ) -// agentAPIAdapter defines the interface for API-specific operations +// 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{} @@ -22,13 +35,17 @@ type agentAPIAdapter interface { // Check if response has tool calls hasToolCalls(response interface{}) bool - // Extract tool calls from response + // 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 + // 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 @@ -53,7 +70,20 @@ type chatAPIAdapter struct { makeReq func(ctx context.Context, req *schemas.BifrostChatRequest) (*schemas.BifrostChatResponse, *schemas.BifrostError) } -// responsesAPIAdapter implements agentAPIAdapter for Responses API +// 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 diff --git a/core/mcp/agent_test.go b/core/mcp/agent_test.go index 97ba0c630..86a24e6cf 100644 --- a/core/mcp/agent_test.go +++ b/core/mcp/agent_test.go @@ -2,6 +2,7 @@ package mcp import ( "context" + "encoding/json" "testing" "github.com/maximhq/bifrost/core/schemas" @@ -478,3 +479,241 @@ func TestExecuteAgentForResponsesRequest_WithNonAutoExecutableTools(t *testing.T t.Errorf("Expected 0 LLM calls for non-auto-executable tools, got %d", llmCaller.responsesCallCount) } } + +// ============================================================================ +// CONVERTER TESTS (Phase 2) +// ============================================================================ + +// TestResponsesToolMessageToChatAssistantMessageToolCall tests conversion of Responses tool message to Chat tool call +func TestResponsesToolMessageToChatAssistantMessageToolCall(t *testing.T) { + // Test with valid tool message + responsesToolMsg := &schemas.ResponsesToolMessage{ + CallID: schemas.Ptr("call-123"), + Name: schemas.Ptr("calculate"), + Arguments: schemas.Ptr("{\"x\": 10, \"y\": 20}"), + } + + chatToolCall := responsesToolMsg.ToChatAssistantMessageToolCall() + + if chatToolCall == nil { + t.Fatal("Expected non-nil ChatAssistantMessageToolCall") + } + + if chatToolCall.Type == nil || *chatToolCall.Type != "function" { + t.Errorf("Expected Type 'function', got %v", chatToolCall.Type) + } + + if chatToolCall.Function.Name == nil || *chatToolCall.Function.Name != "calculate" { + t.Errorf("Expected Name 'calculate', got %v", chatToolCall.Function.Name) + } + + if chatToolCall.Function.Arguments != `{"x": 10, "y": 20}` { + t.Errorf("Expected Arguments '{\"x\": 10, \"y\": 20}', got %s", chatToolCall.Function.Arguments) + } +} + +// TestResponsesToolMessageToChatAssistantMessageToolCall_Nil tests nil handling +func TestResponsesToolMessageToChatAssistantMessageToolCall_Nil(t *testing.T) { + responsesToolMsg := &schemas.ResponsesToolMessage{ + CallID: schemas.Ptr("call-123"), + Name: schemas.Ptr("calculate"), + Arguments: nil, // Test nil Arguments case + } + + chatToolCall := responsesToolMsg.ToChatAssistantMessageToolCall() + if chatToolCall == nil { + t.Fatal("Expected non-nil ChatAssistantMessageToolCall") + } + + // Assert that nil Arguments produces a valid empty JSON object + if chatToolCall.Function.Arguments != "{}" { + t.Errorf("Expected Arguments '{}' for nil input, got %q", chatToolCall.Function.Arguments) + } + + // Verify it's valid JSON by attempting to unmarshal + var args map[string]interface{} + if err := json.Unmarshal([]byte(chatToolCall.Function.Arguments), &args); err != nil { + t.Errorf("Expected valid JSON, but unmarshaling failed: %v", err) + } +} + +// TestChatMessageToResponsesToolMessage tests conversion of Chat tool result to Responses tool message +func TestChatMessageToResponsesToolMessage(t *testing.T) { + // Test with valid chat tool message + chatMsg := &schemas.ChatMessage{ + Role: schemas.ChatMessageRoleTool, + ChatToolMessage: &schemas.ChatToolMessage{ + ToolCallID: schemas.Ptr("call-123"), + }, + Content: &schemas.ChatMessageContent{ + ContentStr: schemas.Ptr("Result: 30"), + }, + } + + responsesMsg := chatMsg.ToResponsesToolMessage() + + if responsesMsg == nil { + t.Fatal("Expected non-nil ResponsesMessage") + } + + if responsesMsg.Type == nil || *responsesMsg.Type != schemas.ResponsesMessageTypeFunctionCallOutput { + t.Errorf("Expected Type 'function_call_output', got %v", responsesMsg.Type) + } + + if responsesMsg.ResponsesToolMessage == nil { + t.Fatal("Expected non-nil ResponsesToolMessage") + } + + if responsesMsg.ResponsesToolMessage.CallID == nil || *responsesMsg.ResponsesToolMessage.CallID != "call-123" { + t.Errorf("Expected CallID 'call-123', got %v", responsesMsg.ResponsesToolMessage.CallID) + } + + if responsesMsg.ResponsesToolMessage.Output == nil { + t.Fatal("Expected non-nil Output") + } + + if responsesMsg.ResponsesToolMessage.Output.ResponsesToolCallOutputStr == nil { + t.Fatal("Expected non-nil ResponsesToolCallOutputStr") + } + + if *responsesMsg.ResponsesToolMessage.Output.ResponsesToolCallOutputStr != "Result: 30" { + t.Errorf("Expected Output 'Result: 30', got %s", *responsesMsg.ResponsesToolMessage.Output.ResponsesToolCallOutputStr) + } +} + +// TestChatMessageToResponsesToolMessage_Nil tests nil handling +func TestChatMessageToResponsesToolMessage_Nil(t *testing.T) { + var chatMsg *schemas.ChatMessage + + responsesMsg := chatMsg.ToResponsesToolMessage() + + if responsesMsg != nil { + t.Errorf("Expected nil for nil input, got %v", responsesMsg) + } +} + +// TestChatMessageToResponsesToolMessage_NoToolMessage tests with non-tool message +func TestChatMessageToResponsesToolMessage_NoToolMessage(t *testing.T) { + // Chat message without ChatToolMessage + chatMsg := &schemas.ChatMessage{ + Role: schemas.ChatMessageRoleAssistant, + } + + responsesMsg := chatMsg.ToResponsesToolMessage() + + if responsesMsg != nil { + t.Errorf("Expected nil for non-tool message, got %v", responsesMsg) + } +} + +// ============================================================================ +// RESPONSES API TOOL CONVERSION TESTS (Phase 3) +// ============================================================================ + +// TestExecuteAgentForResponsesRequest_ConversionRoundTrip tests that tool calls survive format conversion +// This is a unit test of the conversion logic only, not full agent execution +func TestExecuteAgentForResponsesRequest_ConversionRoundTrip(t *testing.T) { + // Create a tool message in Responses format + responsesToolMsg := &schemas.ResponsesToolMessage{ + CallID: schemas.Ptr("call-456"), + Name: schemas.Ptr("readToolFile"), + Arguments: schemas.Ptr("{\"file\": \"test.txt\"}"), + } + + // Step 1: Convert Responses format to Chat format + chatToolCall := responsesToolMsg.ToChatAssistantMessageToolCall() + + if chatToolCall == nil { + t.Fatal("Failed to convert Responses to Chat format") + } + + if *chatToolCall.ID != "call-456" { + t.Errorf("ID lost in conversion: expected 'call-456', got %s", *chatToolCall.ID) + } + + if *chatToolCall.Function.Name != "readToolFile" { + t.Errorf("Name lost in conversion: expected 'readToolFile', got %s", *chatToolCall.Function.Name) + } + + if chatToolCall.Function.Arguments != "{\"file\": \"test.txt\"}" { + t.Errorf("Arguments lost in conversion: expected '%s', got %s", + "{\"file\": \"test.txt\"}", chatToolCall.Function.Arguments) + } + + // Step 2: Simulate tool execution by creating a result message + chatResultMsg := &schemas.ChatMessage{ + Role: schemas.ChatMessageRoleTool, + ChatToolMessage: &schemas.ChatToolMessage{ + ToolCallID: chatToolCall.ID, + }, + Content: &schemas.ChatMessageContent{ + ContentStr: schemas.Ptr("File contents here"), + }, + } + + // Step 3: Convert tool result back to Responses format + responsesResultMsg := chatResultMsg.ToResponsesToolMessage() + + if responsesResultMsg == nil { + t.Fatal("Failed to convert Chat result to Responses format") + } + + if responsesResultMsg.ResponsesToolMessage.CallID == nil { + t.Error("CallID lost in round-trip conversion") + } else if *responsesResultMsg.ResponsesToolMessage.CallID != "call-456" { + t.Errorf("CallID changed in round-trip: expected 'call-456', got %s", *responsesResultMsg.ResponsesToolMessage.CallID) + } + + // Verify output is preserved + if responsesResultMsg.ResponsesToolMessage.Output == nil { + t.Error("Output lost in conversion") + } else if responsesResultMsg.ResponsesToolMessage.Output.ResponsesToolCallOutputStr == nil { + t.Error("Output content lost in conversion") + } else if *responsesResultMsg.ResponsesToolMessage.Output.ResponsesToolCallOutputStr != "File contents here" { + t.Errorf("Output content changed: expected 'File contents here', got %s", + *responsesResultMsg.ResponsesToolMessage.Output.ResponsesToolCallOutputStr) + } + + // Verify message type is correct + if responsesResultMsg.Type == nil || *responsesResultMsg.Type != schemas.ResponsesMessageTypeFunctionCallOutput { + t.Errorf("Expected message type 'function_call_output', got %v", responsesResultMsg.Type) + } +} + +// TestExecuteAgentForResponsesRequest_OutputStructured tests conversion with structured output blocks +func TestExecuteAgentForResponsesRequest_OutputStructured(t *testing.T) { + chatResultMsg := &schemas.ChatMessage{ + Role: schemas.ChatMessageRoleTool, + ChatToolMessage: &schemas.ChatToolMessage{ + ToolCallID: schemas.Ptr("call-789"), + }, + Content: &schemas.ChatMessageContent{ + ContentBlocks: []schemas.ChatContentBlock{ + { + Type: schemas.ChatContentBlockTypeText, + Text: schemas.Ptr("Block 1"), + }, + { + Type: schemas.ChatContentBlockTypeText, + Text: schemas.Ptr("Block 2"), + }, + }, + }, + } + + responsesMsg := chatResultMsg.ToResponsesToolMessage() + + if responsesMsg == nil { + t.Fatal("Expected non-nil ResponsesMessage for structured output") + } + + if responsesMsg.ResponsesToolMessage.Output == nil { + t.Fatal("Expected non-nil Output for structured content") + } + + if responsesMsg.ResponsesToolMessage.Output.ResponsesFunctionToolCallOutputBlocks == nil { + t.Error("Expected output blocks for structured content") + } else if len(responsesMsg.ResponsesToolMessage.Output.ResponsesFunctionToolCallOutputBlocks) != 2 { + t.Errorf("Expected 2 output blocks, got %d", len(responsesMsg.ResponsesToolMessage.Output.ResponsesFunctionToolCallOutputBlocks)) + } +} diff --git a/core/mcp/mcp.go b/core/mcp/mcp.go index 388a4f6ab..01b10b0cb 100644 --- a/core/mcp/mcp.go +++ b/core/mcp/mcp.go @@ -109,18 +109,39 @@ func (m *MCPManager) GetAvailableTools(ctx context.Context) []schemas.ChatTool { return m.toolsHandler.GetAvailableTools(ctx) } -// ExecuteTool executes a single tool call from a chat assistant message. -// It handles tool execution, error handling, and returns the result as a chat message. +// ExecuteChatTool executes a single tool call and returns the result as a chat message. +// This is the primary tool executor and is used by both Chat Completions and Responses APIs. +// +// The method accepts tool calls in Chat API format (ChatAssistantMessageToolCall) and returns +// results in Chat API format (ChatMessage). For Responses API users: +// - Convert ResponsesToolMessage to ChatAssistantMessageToolCall using ToChatAssistantMessageToolCall() +// - Execute the tool with this method +// - Convert the result back using ChatMessage.ToResponsesToolMessage() +// +// Alternatively, use ExecuteResponsesTool() in the ToolsManager for a type-safe wrapper +// that handles format conversions automatically. // // Parameters: // - ctx: Context for the tool execution -// - toolCall: The tool call to execute, containing tool name and arguments +// - toolCall: The tool call to execute in Chat API format // // Returns: // - *schemas.ChatMessage: The result message containing tool execution output // - error: Any error that occurred during tool execution -func (m *MCPManager) ExecuteTool(ctx context.Context, toolCall schemas.ChatAssistantMessageToolCall) (*schemas.ChatMessage, error) { - return m.toolsHandler.ExecuteTool(ctx, toolCall) +func (m *MCPManager) ExecuteChatTool(ctx context.Context, toolCall schemas.ChatAssistantMessageToolCall) (*schemas.ChatMessage, error) { + return m.toolsHandler.ExecuteChatTool(ctx, toolCall) +} + +// ExecuteResponsesTool executes a single tool call and returns the result as a responses message. + +// - ctx: Context for the tool execution +// - toolCall: The tool call to execute in Responses API format +// +// Returns: +// - *schemas.ResponsesMessage: The result message containing tool execution output +// - error: Any error that occurred during tool execution +func (m *MCPManager) ExecuteResponsesTool(ctx context.Context, toolCall *schemas.ResponsesToolMessage) (*schemas.ResponsesMessage, error) { + return m.toolsHandler.ExecuteResponsesTool(ctx, toolCall) } // UpdateToolManagerConfig updates the configuration for the tool manager. @@ -136,6 +157,15 @@ func (m *MCPManager) UpdateToolManagerConfig(config *schemas.MCPToolManagerConfi // and if so, executes agent mode to handle the tool calls iteratively. If no tool calls // are present, it returns the original response unchanged. // +// Agent mode enables autonomous tool execution where: +// 1. Tool calls are automatically executed +// 2. Results are fed back to the LLM +// 3. The loop continues until no more tool calls are made or max depth is reached +// 4. Non-auto-executable tools are returned to the caller +// +// This method is available for both Chat Completions and Responses APIs. +// For Responses API, use CheckAndExecuteAgentForResponsesRequest(). +// // Parameters: // - ctx: Context for the agent execution // - req: The original chat request @@ -172,6 +202,21 @@ func (m *MCPManager) CheckAndExecuteAgentForChatRequest( // and if so, executes agent mode to handle the tool calls iteratively. If no tool calls // are present, it returns the original response unchanged. // +// Agent mode for Responses API works identically to Chat API: +// 1. Detects tool calls in the response (function_call messages) +// 2. Automatically executes tools in parallel when possible +// 3. Feeds results back to the LLM in Responses API format +// 4. Continues the loop until no more tool calls or max depth reached +// 5. Returns non-auto-executable tools to the caller +// +// Format Handling: +// This method automatically handles format conversions: +// - Responses tool calls (ResponsesToolMessage) are converted to Chat format for execution +// - Tool execution results are converted back to Responses format (ResponsesMessage) +// - All conversions use the adapters in agent_adaptors.go and converters in schemas/mux.go +// +// This provides full feature parity between Chat Completions and Responses APIs for tool execution. +// // Parameters: // - ctx: Context for the agent execution // - req: The original responses request diff --git a/core/mcp/toolmanager.go b/core/mcp/toolmanager.go index ca4233234..7bb048645 100644 --- a/core/mcp/toolmanager.go +++ b/core/mcp/toolmanager.go @@ -306,16 +306,21 @@ func (m *ToolsManager) ParseAndAddToolsToRequest(ctx context.Context, req *schem // TOOL REGISTRATION AND DISCOVERY // ============================================================================ -// executeTool executes a tool call and returns the result as a tool message. +// ExecuteChatTool executes a tool call in Chat Completions API format and returns the result as a chat tool message. +// This is the primary tool executor that works with both Chat Completions and Responses APIs. +// +// For Responses API users, use ExecuteResponsesTool() for a more type-safe interface. +// However, internally this method is format-agnostic - it executes the tool and returns +// a ChatMessage which can then be converted to ResponsesMessage via ToResponsesToolMessage(). // // Parameters: // - ctx: Execution context // - toolCall: The tool call to execute (from assistant message) // // Returns: -// - schemas.ChatMessage: Tool message with execution result +// - *schemas.ChatMessage: Tool message with execution result // - error: Any execution error -func (m *ToolsManager) ExecuteTool(ctx context.Context, toolCall schemas.ChatAssistantMessageToolCall) (*schemas.ChatMessage, error) { +func (m *ToolsManager) ExecuteChatTool(ctx context.Context, toolCall schemas.ChatAssistantMessageToolCall) (*schemas.ChatMessage, error) { if toolCall.Function.Name == nil { return nil, fmt.Errorf("tool call missing function name") } @@ -402,6 +407,64 @@ func (m *ToolsManager) ExecuteTool(ctx context.Context, toolCall schemas.ChatAss } } +// ExecuteToolForResponses executes a tool call from a Responses API tool message and returns +// the result in Responses API format. This is a type-safe wrapper around ExecuteTool that +// handles the conversion between Responses and Chat API formats. +// +// This method: +// 1. Converts the Responses tool message to Chat API format +// 2. Executes the tool using the standard tool executor +// 3. Converts the result back to Responses API format +// +// Parameters: +// - ctx: Execution context +// - toolMessage: The Responses API tool message to execute +// - callID: The original call ID from the Responses API +// +// Returns: +// - *schemas.ResponsesMessage: Tool result message in Responses API format +// - error: Any execution error +// +// Example: +// +// responsesToolMsg := &schemas.ResponsesToolMessage{ +// Name: Ptr("calculate"), +// Arguments: Ptr("{\"x\": 10, \"y\": 20}"), +// } +// resultMsg, err := toolsManager.ExecuteResponsesTool(ctx, responsesToolMsg, "call-123") +// // resultMsg is a ResponsesMessage with type=function_call_output +func (m *ToolsManager) ExecuteResponsesTool( + ctx context.Context, + toolMessage *schemas.ResponsesToolMessage, +) (*schemas.ResponsesMessage, error) { + if toolMessage == nil { + return nil, fmt.Errorf("tool message is nil") + } + if toolMessage.Name == nil { + return nil, fmt.Errorf("tool call missing function name") + } + + // Convert Responses format to Chat format for execution + chatToolCall := toolMessage.ToChatAssistantMessageToolCall() + if chatToolCall == nil { + return nil, fmt.Errorf("failed to convert Responses tool message to Chat format") + } + + // Execute the tool using the standard executor + chatResult, err := m.ExecuteChatTool(ctx, *chatToolCall) + if err != nil { + return nil, err + } + + // Convert the result back to Responses format + responsesMessage := chatResult.ToResponsesToolMessage() + if responsesMessage == nil { + return nil, fmt.Errorf("failed to convert tool result to Responses format") + } + + return responsesMessage, nil +} + // ExecuteAgentForChatRequest executes agent mode for a chat request, handling // iterative tool calls up to the configured maximum depth. It delegates to the // shared agent execution logic with the manager's configuration and dependencies. @@ -428,7 +491,7 @@ func (m *ToolsManager) ExecuteAgentForChatRequest( resp, makeReq, m.fetchNewRequestIDFunc, - m.ExecuteTool, + m.ExecuteChatTool, m.clientManager, ) } @@ -459,7 +522,7 @@ func (m *ToolsManager) ExecuteAgentForResponsesRequest( resp, makeReq, m.fetchNewRequestIDFunc, - m.ExecuteTool, + m.ExecuteChatTool, m.clientManager, ) } diff --git a/core/schemas/mux.go b/core/schemas/mux.go index f27194e3b..83f36c701 100644 --- a/core/schemas/mux.go +++ b/core/schemas/mux.go @@ -113,6 +113,99 @@ func (rt *ResponsesTool) ToChatTool() *ChatTool { return ct } +// ToChatAssistantMessageToolCall converts a ResponsesToolMessage to ChatAssistantMessageToolCall format. +// This is useful for executing Responses API tool calls using the Chat API tool executor. +// +// Returns: +// - *ChatAssistantMessageToolCall: The converted tool call in Chat API format +// +// Example: +// +// responsesToolMsg := &ResponsesToolMessage{ +// CallID: Ptr("call-123"), +// Name: Ptr("calculate"), +// Arguments: Ptr("{\"x\": 10, \"y\": 20}"), +// } +// chatToolCall := responsesToolMsg.ToChatAssistantMessageToolCall() +func (rtm *ResponsesToolMessage) ToChatAssistantMessageToolCall() *ChatAssistantMessageToolCall { + if rtm == nil { + return nil + } + + toolCall := &ChatAssistantMessageToolCall{ + ID: rtm.CallID, + Type: Ptr("function"), + Function: ChatAssistantMessageToolCallFunction{ + Name: rtm.Name, + Arguments: "{}", // Default to empty JSON object for valid JSON unmarshaling + }, + } + + // Extract arguments string + if rtm.Arguments != nil { + toolCall.Function.Arguments = *rtm.Arguments + } + + return toolCall +} + +// ToResponsesToolMessage converts a ChatToolMessage (tool execution result) to ResponsesToolMessage format. +// This creates a function_call_output message suitable for the Responses API. +// +// Returns: +// - *ResponsesMessage: A ResponsesMessage with type=function_call_output containing the tool result +// +// Example: +// +// chatToolMsg := &ChatMessage{ +// Role: ChatMessageRoleTool, +// ChatToolMessage: &ChatToolMessage{ +// ToolCallID: Ptr("call-123"), +// }, +// Content: &ChatMessageContent{ +// ContentStr: Ptr("Result: 30"), +// }, +// } +// responsesMsg := chatToolMsg.ToResponsesToolMessage() +func (cm *ChatMessage) ToResponsesToolMessage() *ResponsesMessage { + if cm == nil || cm.ChatToolMessage == nil { + return nil + } + + msgType := ResponsesMessageTypeFunctionCallOutput + + respMsg := &ResponsesMessage{ + Type: &msgType, + ResponsesToolMessage: &ResponsesToolMessage{ + CallID: cm.ChatToolMessage.ToolCallID, + }, + } + + // Extract output from content + if cm.Content != nil { + if cm.Content.ContentStr != nil { + output := *cm.Content.ContentStr + respMsg.ResponsesToolMessage.Output = &ResponsesToolMessageOutputStruct{ + ResponsesToolCallOutputStr: &output, + } + } else if len(cm.Content.ContentBlocks) > 0 { + // For structured content blocks, convert to ResponsesMessageContentBlock + respBlocks := make([]ResponsesMessageContentBlock, len(cm.Content.ContentBlocks)) + for i, block := range cm.Content.ContentBlocks { + respBlocks[i] = ResponsesMessageContentBlock{ + Type: ResponsesMessageContentBlockType(block.Type), + Text: block.Text, + } + } + respMsg.ResponsesToolMessage.Output = &ResponsesToolMessageOutputStruct{ + ResponsesFunctionToolCallOutputBlocks: respBlocks, + } + } + } + + return respMsg +} + // ============================================================================= // TOOL CHOICE CONVERSION METHODS // ============================================================================= diff --git a/transports/bifrost-http/handlers/mcp.go b/transports/bifrost-http/handlers/mcp.go index c6cdec129..6799eb299 100644 --- a/transports/bifrost-http/handlers/mcp.go +++ b/transports/bifrost-http/handlers/mcp.go @@ -52,6 +52,21 @@ func (h *MCPHandler) RegisterRoutes(r *router.Router, middlewares ...lib.Bifrost // executeTool handles POST /v1/mcp/tool/execute - Execute MCP tool func (h *MCPHandler) executeTool(ctx *fasthttp.RequestCtx) { + // Check format query parameter + format := strings.ToLower(string(ctx.QueryArgs().Peek("format"))) + switch format { + case "chat", "": + h.executeChatMCPTool(ctx) + case "responses": + h.executeResponsesMCPTool(ctx) + default: + SendError(ctx, fasthttp.StatusBadRequest, "Invalid format value, must be 'chat' or 'responses'") + return + } +} + +// executeChatMCPTool handles POST /v1/mcp/tool/execute?format=chat - Execute MCP tool +func (h *MCPHandler) executeChatMCPTool(ctx *fasthttp.RequestCtx) { var req schemas.ChatAssistantMessageToolCall if err := json.Unmarshal(ctx.PostBody(), &req); err != nil { SendError(ctx, fasthttp.StatusBadRequest, fmt.Sprintf("Invalid request format: %v", err)) @@ -73,14 +88,47 @@ func (h *MCPHandler) executeTool(ctx *fasthttp.RequestCtx) { } // Execute MCP tool - resp, bifrostErr := h.client.ExecuteMCPTool(*bifrostCtx, req) + toolMessage, bifrostErr := h.client.ExecuteChatMCPTool(*bifrostCtx, req) + if bifrostErr != nil { + SendBifrostError(ctx, bifrostErr) + return + } + + // Send successful response + SendJSON(ctx, toolMessage) +} + +// executeResponsesMCPTool handles POST /v1/mcp/tool/execute?format=responses - Execute MCP tool +func (h *MCPHandler) executeResponsesMCPTool(ctx *fasthttp.RequestCtx) { + var req schemas.ResponsesToolMessage + if err := json.Unmarshal(ctx.PostBody(), &req); err != nil { + SendError(ctx, fasthttp.StatusBadRequest, fmt.Sprintf("Invalid request format: %v", err)) + return + } + + // Validate required fields + if req.Name == nil || *req.Name == "" { + SendError(ctx, fasthttp.StatusBadRequest, "Tool function name is required") + return + } + + // Convert context + bifrostCtx, cancel := lib.ConvertToBifrostContext(ctx, false) + defer cancel() // Ensure cleanup on function exit + if bifrostCtx == nil { + SendError(ctx, fasthttp.StatusInternalServerError, "Failed to convert context") + return + } + + // Execute MCP tool + toolMessage, bifrostErr := h.client.ExecuteResponsesMCPTool(*bifrostCtx, &req) if bifrostErr != nil { SendBifrostError(ctx, bifrostErr) return } // Send successful response - SendJSON(ctx, resp) + SendJSON(ctx, toolMessage) } // getMCPClients handles GET /api/mcp/clients - Get all MCP clients diff --git a/transports/bifrost-http/handlers/mcp_server.go b/transports/bifrost-http/handlers/mcp_server.go index 1ccf8dfc5..1bdf5b6ca 100644 --- a/transports/bifrost-http/handlers/mcp_server.go +++ b/transports/bifrost-http/handlers/mcp_server.go @@ -25,7 +25,8 @@ import ( // MCPToolExecutor interface defines the method needed for executing MCP tools type MCPToolManager interface { GetAvailableMCPTools(ctx context.Context) []schemas.ChatTool - ExecuteTool(ctx context.Context, toolCall schemas.ChatAssistantMessageToolCall) (*schemas.ChatMessage, *schemas.BifrostError) + ExecuteChatMCPTool(ctx context.Context, toolCall schemas.ChatAssistantMessageToolCall) (*schemas.ChatMessage, *schemas.BifrostError) + ExecuteResponsesMCPTool(ctx context.Context, toolCall *schemas.ResponsesToolMessage) (*schemas.ResponsesMessage, *schemas.BifrostError) } // MCPServerHandler manages HTTP requests for MCP server operations @@ -238,7 +239,7 @@ func (h *MCPServerHandler) syncServer(server *server.MCPServer, availableTools [ } // Execute the tool via tool executor - toolMessage, err := h.toolManager.ExecuteTool(ctx, toolCall) + toolMessage, err := h.toolManager.ExecuteChatMCPTool(ctx, toolCall) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Tool execution failed: %v", bifrost.GetErrorMessage(err))), nil } diff --git a/transports/bifrost-http/server/server.go b/transports/bifrost-http/server/server.go index ef31b68e3..802f7401b 100644 --- a/transports/bifrost-http/server/server.go +++ b/transports/bifrost-http/server/server.go @@ -487,9 +487,14 @@ func (s *BifrostHTTPServer) RemoveMCPClient(ctx context.Context, id string) erro return nil } -// ExecuteTool executes an MCP tool call and returns the result -func (s *BifrostHTTPServer) ExecuteTool(ctx context.Context, toolCall schemas.ChatAssistantMessageToolCall) (*schemas.ChatMessage, *schemas.BifrostError) { - return s.Client.ExecuteMCPTool(ctx, toolCall) +// ExecuteChatMCPTool executes an MCP tool call and returns the result as a chat message. +func (s *BifrostHTTPServer) ExecuteChatMCPTool(ctx context.Context, toolCall schemas.ChatAssistantMessageToolCall) (*schemas.ChatMessage, *schemas.BifrostError) { + return s.Client.ExecuteChatMCPTool(ctx, toolCall) +} + +// ExecuteResponsesMCPTool executes an MCP tool call and returns the result as a responses message. +func (s *BifrostHTTPServer) ExecuteResponsesMCPTool(ctx context.Context, toolCall *schemas.ResponsesToolMessage) (*schemas.ResponsesMessage, *schemas.BifrostError) { + return s.Client.ExecuteResponsesMCPTool(ctx, toolCall) } func (s *BifrostHTTPServer) GetAvailableMCPTools(ctx context.Context) []schemas.ChatTool {