// Copyright (c) 2025 Alibaba Group Holding Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package agent import ( "fmt" "io/fs" "os" "path/filepath" "text/template" "github.com/AlecAivazis/survey/v2" "github.com/alibaba/higress/hgctl/pkg/agent/prompt" "github.com/alibaba/higress/hgctl/pkg/manifests" "github.com/alibaba/higress/hgctl/pkg/util" "github.com/spf13/cobra" "github.com/spf13/viper" ) const ( ASRuntimeMainPyFile = "as_runtime_main.py" AgentRunMainPyFile = "agentrun_main.py" ToolKitPyFile = "toolkit.py" AgentClassFile = "agent.py" CorePromptFile = "claude.md" // TODO: support qoder AGENTS.md SConfigYAML = "s.yaml" ARTemplate = "agentrun.tmpl" ASTemplate = "agentscope.tmpl" AgentClassTemplate = "agent.tmpl" ToolKitTemplate = "toolkit.tmpl" SConfigTemplate = "agentrun_s.tmpl" ) var ASAvailiableTools = []string{ "execute_python_code", "execute_shell_command", "view_text_file", "write_text_file", "insert_text_file", "dashscope_text_to_image", "dashscope_text_to_audio", "dashscope_image_to_text", "openai_text_to_image", "openai_text_to_audio", "openai_edit_image", "openai_create_image_variation", "openai_image_to_text", "openai_audio_to_text", } const ( MinPythonVersion = "3.12" DefaultServerLessAccessKey = "hgctl-credential" ) // Callback type for post-agent-creation actions type PostAgentAction func(config *AgentConfig) error type MCPServerConfig struct { Name string // MCP Client Name URL string // MCP Server URL Transport string // transport `streamable_http` or `see` or `stdio` Headers map[string]string // HTTP Headers } type ServerlessConfig struct { AccessKey string ResourceName string Region string AgentName string AgentDesc string Port uint DiskSize uint Timeout uint GlobalConfig HgctlAgentConfig } type AgentConfig struct { AppName string // "app" AppDescription string // "A helpful assistant and useful agent" AgentName string // "Friday" AvailableTools []string // availiable tools (built-in agentscope) SysPromptPath string // "You are a helpful assistant" ChatModel string // "qwen-max" Provider string // "Aliyun" APIKeyEnvVar string // DASHCOPE_API_KEY DeploymentPort int // 8090 HostBinding string // 0.0.0.0 EnableStreaming bool // true EnableThinking bool // true MCPServers []MCPServerConfig Type DeployType ServerlessCfg ServerlessConfig } func createAgentCmd() *cobra.Command { agentRun := false deployDirect := false var createAgentCmd = &cobra.Command{ Use: "new", Short: "Create a new agent or import one from core", Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { config := &AgentConfig{ Type: Local, } if agentRun { config.Type = AgentRun config.ServerlessCfg = ServerlessConfig{ AccessKey: DefaultServerLessAccessKey, Port: 9000, DiskSize: 512, Timeout: 600, GlobalConfig: GlobalConfig, } } if err := getAgentConfig(config); err != nil { fmt.Printf("Error get Agent config: %v\n", err) os.Exit(1) } if err := createAgentTemplate(config); err != nil { fmt.Printf("Error creating agent: %v\n", err) os.Exit(1) } if err := afterCreatedAgent(config); err != nil { fmt.Println(err) os.Exit(1) } }, } createAgentCmd.PersistentFlags().BoolVar(&agentRun, "agent-run", false, "Use agentRun to deploy to Alibaba cloud, default is false") createAgentCmd.PersistentFlags().BoolVar(&deployDirect, "deploy", false, "After agent creation, deploy it directly") return createAgentCmd } func afterCreatedAgent(config *AgentConfig) error { options := []string{ "Deploy it directly", fmt.Sprintf("Improve and test it using agentic core (%s)", viper.GetString(HGCTL_AGENT_CORE)), "Do nothing and quit", } callbacks := map[string]PostAgentAction{ options[0]: func(cfg *AgentConfig) error { handler := &DeployHandler{Name: cfg.AgentName} return handler.Deploy() }, options[1]: func(cfg *AgentConfig) error { return runAgenticCoreImprovement(cfg) }, } if err := promptAfterCreatedAgent(options, config, callbacks); err != nil { fmt.Fprintf(os.Stderr, "Failed to handle post-creation action: %v\n", err) return nil } return nil } func runAgenticCoreImprovement(cfg *AgentConfig) error { core, err := getCore() if err != nil { return fmt.Errorf("failed to invoke agent core: %s", err) } if err := core.ImproveNewAgent(cfg); err != nil { return fmt.Errorf("failed to use core to improve new agent: %s", err) } return nil } func promptAfterCreatedAgent(options []string, config *AgentConfig, callbacks map[string]PostAgentAction) error { promptChoice := &survey.Select{ Message: "What's next?:", Options: options, Help: "Choose an action to perform after agent creation.", } var response string if err := survey.AskOne(promptChoice, &response); err != nil { return fmt.Errorf("failed to read user choice: %w", err) } if callback, ok := callbacks[response]; ok { return callback(config) } if response == options[2] { os.Exit(1) } return fmt.Errorf("unknown action selected: %q", response) } func createAgentTemplate(config *AgentConfig) error { agentsDir := util.GetHomeHgctlDir() + "/agents" if err := os.MkdirAll(agentsDir, 0755); err != nil { return fmt.Errorf("failed to create agents directory: %v", err) } agentDir := filepath.Join(agentsDir, config.AgentName) if err := os.MkdirAll(agentDir, 0755); err != nil { return fmt.Errorf("failed to create agent directory: %v", err) } switch config.Type { case Local: // parse agentscope file asMain := filepath.Join(agentDir, ASRuntimeMainPyFile) asTemplateStr, err := get_template(ASTemplate) if err != nil { return fmt.Errorf("failed to read agentscope template: %v", err) } if err := renderTemplateFile(asTemplateStr, asMain, config); err != nil { return fmt.Errorf("failed to render agentscope runtime's file: %s", err) } case AgentRun: // Details see: https://github.com/Serverless-Devs/agentrun-sdk-python // parse agentrun file arMain := filepath.Join(agentDir, AgentRunMainPyFile) arTemplateStr, err := get_template(ARTemplate) if err != nil { return fmt.Errorf("failed to read agentrun template: %v", err) } if err := renderTemplateFile(arTemplateStr, arMain, config); err != nil { return fmt.Errorf("failed to render agentscope runtime's file: %s", err) } // parse s.yaml s := filepath.Join(agentDir, SConfigYAML) STmplStr, err := get_template(SConfigTemplate) if err != nil { return fmt.Errorf("failed to read agentrun's serverless config file template: %v", err) } if err := renderTemplateFile(STmplStr, s, config.ServerlessCfg); err != nil { return fmt.Errorf("failed to render agentscope runtime's file: %s", err) } // write requirements fileContent := "agentrun-sdk[agentscope,server]>=0.0.3" targetFilePath := filepath.Join(agentDir, "requirements.txt") if err := util.WriteFileString(targetFilePath, fileContent, os.ModePerm); err != nil { return fmt.Errorf("failed to write requirements.txt file to target agent directory: %s", err) } } // parse toolkitPath toolkitPath := filepath.Join(agentDir, ToolKitPyFile) toolkitTmpl, err := get_template(ToolKitTemplate) if err != nil { return fmt.Errorf("failed to read toolkit template: %v", err) } if err := renderTemplateFile(toolkitTmpl, toolkitPath, config); err != nil { return fmt.Errorf("failed to render toolkit file: %s", err) } // write agent.py agentPath := filepath.Join(agentDir, AgentClassFile) agentTmpl, err := get_template(AgentClassTemplate) if err != nil { return fmt.Errorf("failed to read agent class template: %v", err) } if err := renderTemplateFile(agentTmpl, agentPath, config); err != nil { return fmt.Errorf("failed to render agent class file: %s", err) } // write core_prompt.md if core, err := getCore(); err == nil { corePromptPath := filepath.Join(agentDir, core.GetPromptFileName()) if err := util.WriteFileString(corePromptPath, prompt.AgentDevelopmentGuide, os.ModePerm); err != nil { return fmt.Errorf("failed to write %s file to target agent directory: %s", core.GetPromptFileName(), err) } return nil } else { return fmt.Errorf("failed to add instruction file in agent dir: %s", err) } } func renderTemplateFile(templateStr string, targetPath string, data interface{}) error { // sync with python funcMap := template.FuncMap{ "boolToPython": func(b bool) string { if b { return "True" } return "False" }, } tmpl, err := template.New("agent").Funcs(funcMap).Parse(templateStr) if err != nil { return fmt.Errorf("failed to parse template: %v", err) } file, err := os.Create(targetPath) if err != nil { return fmt.Errorf("failed to create output file: %v", err) } defer file.Close() if err := tmpl.Execute(file, data); err != nil { return fmt.Errorf("failed to render template: %v", err) } return nil } func get_template(templatePath string) (string, error) { f := manifests.BuiltinOrDir("") templatePath = "agent/template/" + templatePath data, err := fs.ReadFile(f, templatePath) if err != nil { return "", fmt.Errorf("failed to read template: %w", err) } return string(data), nil }