mirror of
https://github.com/httprunner/httprunner.git
synced 2026-09-07 16:40:50 +08:00
feat: add MCP tools registration to LLM service
- Add RegisterTools method to ILLMService interface - Create shared MCP to eino tool converter - Auto-register built-in uixt tools in XTDriver initialization - Refactor MCPHost to use shared converter - Add comprehensive test coverage for tool conversion This enables doubao-1.5-thinking-vision-pro model to access MCP tools through function calling mechanism.
This commit is contained in:
@@ -1 +1 @@
|
|||||||
v5.0.0-beta-2506092052
|
v5.0.0-beta-2506092219
|
||||||
|
|||||||
+7
-22
@@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/cloudwego/eino/schema"
|
"github.com/cloudwego/eino/schema"
|
||||||
"github.com/httprunner/httprunner/v5/internal/version"
|
"github.com/httprunner/httprunner/v5/internal/version"
|
||||||
"github.com/httprunner/httprunner/v5/uixt"
|
"github.com/httprunner/httprunner/v5/uixt"
|
||||||
|
"github.com/httprunner/httprunner/v5/uixt/ai"
|
||||||
"github.com/mark3labs/mcp-go/client"
|
"github.com/mark3labs/mcp-go/client"
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -413,7 +414,7 @@ func (h *MCPHost) GetEinoToolInfos(ctx context.Context) ([]*schema.ToolInfo, err
|
|||||||
return nil, fmt.Errorf("no MCP servers loaded")
|
return nil, fmt.Errorf("no MCP servers loaded")
|
||||||
}
|
}
|
||||||
|
|
||||||
var tools []*schema.ToolInfo
|
var allTools []*schema.ToolInfo
|
||||||
for _, serverTools := range results {
|
for _, serverTools := range results {
|
||||||
if serverTools.Err != nil {
|
if serverTools.Err != nil {
|
||||||
log.Error().Err(serverTools.Err).
|
log.Error().Err(serverTools.Err).
|
||||||
@@ -421,29 +422,13 @@ func (h *MCPHost) GetEinoToolInfos(ctx context.Context) ([]*schema.ToolInfo, err
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var toolNames []string
|
// convert MCP tools to eino tools
|
||||||
for _, tool := range serverTools.Tools {
|
einoTools := ai.ConvertMCPToolsToEinoToolInfos(
|
||||||
einoTool, err := h.GetEinoTool(ctx, serverTools.ServerName, tool.Name)
|
serverTools.Tools, serverTools.ServerName)
|
||||||
if err != nil {
|
allTools = append(allTools, einoTools...)
|
||||||
log.Error().Err(err).Str("server", serverTools.ServerName).
|
|
||||||
Str("tool", tool.Name).Msg("failed to get eino tool")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
einoToolInfo, err := einoTool.Info(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Str("server", serverTools.ServerName).
|
|
||||||
Str("tool", tool.Name).Msg("failed to get eino tool info")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
einoToolInfo.Name = fmt.Sprintf("%s__%s", serverTools.ServerName, tool.Name)
|
|
||||||
tools = append(tools, einoToolInfo)
|
|
||||||
toolNames = append(toolNames, tool.Name)
|
|
||||||
}
|
|
||||||
log.Debug().Str("server", serverTools.ServerName).
|
|
||||||
Strs("tools", toolNames).Msg("loaded MCP tools")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return tools, nil
|
return allTools, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseHeaders parses header strings into a map
|
// parseHeaders parses header strings into a map
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ func TestStartToGoal(t *testing.T) {
|
|||||||
`
|
`
|
||||||
|
|
||||||
testCase := &hrp.TestCase{
|
testCase := &hrp.TestCase{
|
||||||
Config: hrp.NewConfig("run ui action with start to goal").
|
Config: hrp.NewConfig("连连看小游戏自动化测试").
|
||||||
SetLLMService(option.DOUBAO_1_5_THINKING_VISION_PRO_250428),
|
SetLLMService(option.DOUBAO_1_5_THINKING_VISION_PRO_250428),
|
||||||
TestSteps: []hrp.IStep{
|
TestSteps: []hrp.IStep{
|
||||||
hrp.NewStep("启动抖音「连了又连」小游戏").
|
hrp.NewStep("启动抖音「连了又连」小游戏").
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package ai
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/cloudwego/eino/schema"
|
||||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -10,6 +11,8 @@ import (
|
|||||||
type ILLMService interface {
|
type ILLMService interface {
|
||||||
Call(ctx context.Context, opts *PlanningOptions) (*PlanningResult, error)
|
Call(ctx context.Context, opts *PlanningOptions) (*PlanningResult, error)
|
||||||
Assert(ctx context.Context, opts *AssertOptions) (*AssertionResult, error)
|
Assert(ctx context.Context, opts *AssertOptions) (*AssertionResult, error)
|
||||||
|
// RegisterTools registers tools for function calling
|
||||||
|
RegisterTools(tools []*schema.ToolInfo) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLLMService(modelType option.LLMServiceType) (ILLMService, error) {
|
func NewLLMService(modelType option.LLMServiceType) (ILLMService, error) {
|
||||||
@@ -49,3 +52,12 @@ func (c *combinedLLMService) Call(ctx context.Context, opts *PlanningOptions) (*
|
|||||||
func (c *combinedLLMService) Assert(ctx context.Context, opts *AssertOptions) (*AssertionResult, error) {
|
func (c *combinedLLMService) Assert(ctx context.Context, opts *AssertOptions) (*AssertionResult, error) {
|
||||||
return c.asserter.Assert(ctx, opts)
|
return c.asserter.Assert(ctx, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterTools registers tools for function calling
|
||||||
|
func (c *combinedLLMService) RegisterTools(tools []*schema.ToolInfo) error {
|
||||||
|
// Only register tools to planner since asserter doesn't need tools
|
||||||
|
if planner, ok := c.planner.(*Planner); ok {
|
||||||
|
return planner.RegisterTools(tools)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package ai
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/cloudwego/eino/schema"
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConvertMCPToolToEinoToolInfo converts an MCP tool to eino ToolInfo
|
||||||
|
func ConvertMCPToolToEinoToolInfo(mcpTool mcp.Tool, namePrefix string) *schema.ToolInfo {
|
||||||
|
// Create eino ToolInfo from MCP tool
|
||||||
|
toolName := mcpTool.Name
|
||||||
|
if namePrefix != "" {
|
||||||
|
toolName = fmt.Sprintf("%s__%s", namePrefix, mcpTool.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
toolInfo := &schema.ToolInfo{
|
||||||
|
Name: toolName,
|
||||||
|
Desc: mcpTool.Description,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert input schema
|
||||||
|
if mcpTool.InputSchema.Properties != nil {
|
||||||
|
params := make(map[string]*schema.ParameterInfo)
|
||||||
|
|
||||||
|
for propName, propValue := range mcpTool.InputSchema.Properties {
|
||||||
|
if propMap, ok := propValue.(map[string]interface{}); ok {
|
||||||
|
paramInfo := &schema.ParameterInfo{}
|
||||||
|
|
||||||
|
if propType, exists := propMap["type"]; exists {
|
||||||
|
if typeStr, ok := propType.(string); ok {
|
||||||
|
switch typeStr {
|
||||||
|
case "string":
|
||||||
|
paramInfo.Type = schema.String
|
||||||
|
case "number":
|
||||||
|
paramInfo.Type = schema.Number
|
||||||
|
case "integer":
|
||||||
|
paramInfo.Type = schema.Integer
|
||||||
|
case "boolean":
|
||||||
|
paramInfo.Type = schema.Boolean
|
||||||
|
case "array":
|
||||||
|
paramInfo.Type = schema.Array
|
||||||
|
case "object":
|
||||||
|
paramInfo.Type = schema.Object
|
||||||
|
default:
|
||||||
|
paramInfo.Type = schema.String // default to string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if description, exists := propMap["description"]; exists {
|
||||||
|
if descStr, ok := description.(string); ok {
|
||||||
|
paramInfo.Desc = descStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if enum, exists := propMap["enum"]; exists {
|
||||||
|
if enumSlice, ok := enum.([]interface{}); ok {
|
||||||
|
var enumStrings []string
|
||||||
|
for _, enumVal := range enumSlice {
|
||||||
|
if enumStr, ok := enumVal.(string); ok {
|
||||||
|
enumStrings = append(enumStrings, enumStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
paramInfo.Enum = enumStrings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this parameter is required
|
||||||
|
for _, requiredField := range mcpTool.InputSchema.Required {
|
||||||
|
if requiredField == propName {
|
||||||
|
paramInfo.Required = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params[propName] = paramInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(params) > 0 {
|
||||||
|
toolInfo.ParamsOneOf = schema.NewParamsOneOfByParams(params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return toolInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertMCPToolsToEinoToolInfos converts multiple MCP tools to eino ToolInfos
|
||||||
|
func ConvertMCPToolsToEinoToolInfos(mcpTools []mcp.Tool, namePrefix string) []*schema.ToolInfo {
|
||||||
|
var einoTools []*schema.ToolInfo
|
||||||
|
for _, mcpTool := range mcpTools {
|
||||||
|
einoTool := ConvertMCPToolToEinoToolInfo(mcpTool, namePrefix)
|
||||||
|
if einoTool != nil {
|
||||||
|
einoTools = append(einoTools, einoTool)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return einoTools
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package ai
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConvertMCPToolToEinoToolInfo(t *testing.T) {
|
||||||
|
// Create a mock MCP tool
|
||||||
|
mcpTool := mcp.Tool{
|
||||||
|
Name: "test_tool",
|
||||||
|
Description: "Test tool description",
|
||||||
|
InputSchema: mcp.ToolInputSchema{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]interface{}{
|
||||||
|
"param1": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Test parameter 1",
|
||||||
|
},
|
||||||
|
"param2": map[string]interface{}{
|
||||||
|
"type": "number",
|
||||||
|
"description": "Test parameter 2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: []string{"param1"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to eino ToolInfo using shared converter
|
||||||
|
einoTool := ConvertMCPToolToEinoToolInfo(mcpTool, "uixt")
|
||||||
|
|
||||||
|
// Verify the conversion
|
||||||
|
assert.NotNil(t, einoTool)
|
||||||
|
assert.Equal(t, "uixt__test_tool", einoTool.Name)
|
||||||
|
assert.Equal(t, "Test tool description", einoTool.Desc)
|
||||||
|
assert.NotNil(t, einoTool.ParamsOneOf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertMCPToolWithoutParams(t *testing.T) {
|
||||||
|
// Create a mock MCP tool without parameters
|
||||||
|
mcpTool := mcp.Tool{
|
||||||
|
Name: "simple_tool",
|
||||||
|
Description: "Simple tool without parameters",
|
||||||
|
InputSchema: mcp.ToolInputSchema{
|
||||||
|
Type: "object",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to eino ToolInfo using shared converter
|
||||||
|
einoTool := ConvertMCPToolToEinoToolInfo(mcpTool, "uixt")
|
||||||
|
|
||||||
|
// Verify the conversion
|
||||||
|
assert.NotNil(t, einoTool)
|
||||||
|
assert.Equal(t, "uixt__simple_tool", einoTool.Name)
|
||||||
|
assert.Equal(t, "Simple tool without parameters", einoTool.Desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertMCPToolsToEinoToolInfos(t *testing.T) {
|
||||||
|
// Create multiple mock MCP tools
|
||||||
|
mcpTools := []mcp.Tool{
|
||||||
|
{
|
||||||
|
Name: "tool1",
|
||||||
|
Description: "First tool",
|
||||||
|
InputSchema: mcp.ToolInputSchema{Type: "object"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "tool2",
|
||||||
|
Description: "Second tool",
|
||||||
|
InputSchema: mcp.ToolInputSchema{Type: "object"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to eino ToolInfos using shared converter
|
||||||
|
einoTools := ConvertMCPToolsToEinoToolInfos(mcpTools, "test_server")
|
||||||
|
|
||||||
|
// Verify the conversion
|
||||||
|
assert.Len(t, einoTools, 2)
|
||||||
|
assert.Equal(t, "test_server__tool1", einoTools[0].Name)
|
||||||
|
assert.Equal(t, "test_server__tool2", einoTools[1].Name)
|
||||||
|
assert.Equal(t, "First tool", einoTools[0].Desc)
|
||||||
|
assert.Equal(t, "Second tool", einoTools[1].Desc)
|
||||||
|
}
|
||||||
@@ -77,6 +77,15 @@ func (p *Planner) RegisterTools(tools []*schema.ToolInfo) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to register tools")
|
return errors.Wrap(err, "failed to register tools")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var toolNames []string
|
||||||
|
for _, tool := range tools {
|
||||||
|
toolNames = append(toolNames, tool.Name)
|
||||||
|
}
|
||||||
|
log.Debug().Strs("tools", toolNames).
|
||||||
|
Str("model", string(p.modelConfig.ModelType)).
|
||||||
|
Msg("registered tools to model")
|
||||||
|
|
||||||
p.model = toolCallingModel
|
p.model = toolCallingModel
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,13 @@ func NewXTDriver(driver IDriver, opts ...option.AIServiceOption) (*XTDriver, err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "init llm service failed")
|
return nil, errors.Wrap(err, "init llm service failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register uixt MCP tools to LLM service
|
||||||
|
mcpTools := driverExt.client.Server.ListTools()
|
||||||
|
einoTools := ai.ConvertMCPToolsToEinoToolInfos(mcpTools, "uixt")
|
||||||
|
if err := driverExt.LLMService.RegisterTools(einoTools); err != nil {
|
||||||
|
log.Warn().Err(err).Msg("failed to register uixt tools")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return driverExt, nil
|
return driverExt, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user