mirror of
https://github.com/httprunner/httprunner.git
synced 2026-06-26 01:51:29 +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
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/httprunner/httprunner/v5/internal/version"
|
||||
"github.com/httprunner/httprunner/v5/uixt"
|
||||
"github.com/httprunner/httprunner/v5/uixt/ai"
|
||||
"github.com/mark3labs/mcp-go/client"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"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")
|
||||
}
|
||||
|
||||
var tools []*schema.ToolInfo
|
||||
var allTools []*schema.ToolInfo
|
||||
for _, serverTools := range results {
|
||||
if serverTools.Err != nil {
|
||||
log.Error().Err(serverTools.Err).
|
||||
@@ -421,29 +422,13 @@ func (h *MCPHost) GetEinoToolInfos(ctx context.Context) ([]*schema.ToolInfo, err
|
||||
continue
|
||||
}
|
||||
|
||||
var toolNames []string
|
||||
for _, tool := range serverTools.Tools {
|
||||
einoTool, err := h.GetEinoTool(ctx, serverTools.ServerName, tool.Name)
|
||||
if err != nil {
|
||||
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")
|
||||
// convert MCP tools to eino tools
|
||||
einoTools := ai.ConvertMCPToolsToEinoToolInfos(
|
||||
serverTools.Tools, serverTools.ServerName)
|
||||
allTools = append(allTools, einoTools...)
|
||||
}
|
||||
|
||||
return tools, nil
|
||||
return allTools, nil
|
||||
}
|
||||
|
||||
// parseHeaders parses header strings into a map
|
||||
|
||||
@@ -101,7 +101,7 @@ func TestStartToGoal(t *testing.T) {
|
||||
`
|
||||
|
||||
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),
|
||||
TestSteps: []hrp.IStep{
|
||||
hrp.NewStep("启动抖音「连了又连」小游戏").
|
||||
|
||||
@@ -3,6 +3,7 @@ package ai
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
)
|
||||
|
||||
@@ -10,6 +11,8 @@ import (
|
||||
type ILLMService interface {
|
||||
Call(ctx context.Context, opts *PlanningOptions) (*PlanningResult, 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) {
|
||||
@@ -49,3 +52,12 @@ func (c *combinedLLMService) Call(ctx context.Context, opts *PlanningOptions) (*
|
||||
func (c *combinedLLMService) Assert(ctx context.Context, opts *AssertOptions) (*AssertionResult, error) {
|
||||
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
|
||||
}
|
||||
|
||||
100
uixt/ai/converter.go
Normal file
100
uixt/ai/converter.go
Normal file
@@ -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
|
||||
}
|
||||
84
uixt/ai/converter_test.go
Normal file
84
uixt/ai/converter_test.go
Normal file
@@ -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 {
|
||||
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
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -38,6 +38,13 @@ func NewXTDriver(driver IDriver, opts ...option.AIServiceOption) (*XTDriver, err
|
||||
if err != nil {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user