diff --git a/internal/version/VERSION b/internal/version/VERSION index ae9b6534..172a4f1c 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2506092052 +v5.0.0-beta-2506092219 diff --git a/mcphost/host.go b/mcphost/host.go index b7c453c9..61e36ecb 100644 --- a/mcphost/host.go +++ b/mcphost/host.go @@ -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 diff --git a/tests/step_ui_test.go b/tests/step_ui_test.go index 38a0daf3..9cc00440 100644 --- a/tests/step_ui_test.go +++ b/tests/step_ui_test.go @@ -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("启动抖音「连了又连」小游戏"). diff --git a/uixt/ai/ai.go b/uixt/ai/ai.go index 40414229..3a433aad 100644 --- a/uixt/ai/ai.go +++ b/uixt/ai/ai.go @@ -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 +} diff --git a/uixt/ai/converter.go b/uixt/ai/converter.go new file mode 100644 index 00000000..12904292 --- /dev/null +++ b/uixt/ai/converter.go @@ -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 +} diff --git a/uixt/ai/converter_test.go b/uixt/ai/converter_test.go new file mode 100644 index 00000000..bd47b461 --- /dev/null +++ b/uixt/ai/converter_test.go @@ -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) +} diff --git a/uixt/ai/planner.go b/uixt/ai/planner.go index ea9d823d..c24ff6ad 100644 --- a/uixt/ai/planner.go +++ b/uixt/ai/planner.go @@ -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 } diff --git a/uixt/sdk.go b/uixt/sdk.go index caf96bf5..50e1148d 100644 --- a/uixt/sdk.go +++ b/uixt/sdk.go @@ -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