diff --git a/go/cli/cmd/kagent/main.go b/go/cli/cmd/kagent/main.go index a8c4da19c..ed4a2845b 100644 --- a/go/cli/cmd/kagent/main.go +++ b/go/cli/cmd/kagent/main.go @@ -209,7 +209,124 @@ func main() { getCmd.AddCommand(getSessionCmd, getAgentCmd, getToolCmd) - rootCmd.AddCommand(installCmd, uninstallCmd, invokeCmd, bugReportCmd, versionCmd, dashboardCmd, getCmd) + initCfg := &cli.InitCfg{ + Config: cfg, + } + + initCmd := &cobra.Command{ + Use: "init [framework] [language] [agent-name]", + Short: "Initialize a new agent project", + Long: `Initialize a new agent project using the specified framework and language. + +You can customize the root agent instructions using the --instruction-file flag. +You can select a specific model using --model-provider and --model-name flags. +If no custom instruction file is provided, a default dice-rolling instruction will be used. +If no model is specified, the agent will need to be configured later. + +Examples: + kagent init adk python dice + kagent init adk python dice --instruction-file instructions.md + kagent init adk python dice --model-provider Gemini --model-name gemini-2.0-flash`, + Args: cobra.ExactArgs(3), + Run: func(cmd *cobra.Command, args []string) { + initCfg.Framework = args[0] + initCfg.Language = args[1] + initCfg.AgentName = args[2] + + if err := cli.InitCmd(initCfg); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + }, + Example: `kagent init adk python dice`, + } + + // Add flags for custom instructions and model selection + initCmd.Flags().StringVar(&initCfg.InstructionFile, "instruction-file", "", "Path to file containing custom instructions for the root agent") + initCmd.Flags().StringVar(&initCfg.ModelProvider, "model-provider", "Gemini", "Model provider (OpenAI, Anthropic, Gemini)") + initCmd.Flags().StringVar(&initCfg.ModelName, "model-name", "gemini-2.0-flash", "Model name (e.g., gpt-4, claude-3-5-sonnet, gemini-2.0-flash)") + initCmd.Flags().StringVar(&initCfg.Description, "description", "", "Description for the agent") + + buildCfg := &cli.BuildCfg{ + Config: cfg, + } + + buildCmd := &cobra.Command{ + Use: "build [project-directory]", + Short: "Build a Docker image for an agent project", + Long: `Build a Docker image for an agent project created with the init command. + +This command will look for a Dockerfile in the specified project directory and build +a Docker image using docker build. The image can optionally be pushed to a registry. + +Image naming: +- If --image is provided, it will be used as the full image specification (e.g., ghcr.io/myorg/my-agent:v1.0.0) +- Otherwise, defaults to localhost:5001/{agentName}:latest where agentName is loaded from kagent.yaml + +Examples: + kagent build ./my-agent + kagent build ./my-agent --image ghcr.io/myorg/my-agent:v1.0.0 + kagent build ./my-agent --image ghcr.io/myorg/my-agent:v1.0.0 --push`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + buildCfg.ProjectDir = args[0] + + if err := cli.BuildCmd(buildCfg); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + }, + Example: `kagent build ./my-agent`, + } + + // Add flags for build command + buildCmd.Flags().StringVar(&buildCfg.Image, "image", "", "Full image specification (e.g., ghcr.io/myorg/my-agent:v1.0.0)") + buildCmd.Flags().BoolVar(&buildCfg.Push, "push", false, "Push the image to the registry") + + deployCfg := &cli.DeployCfg{ + Config: cfg, + } + + deployCmd := &cobra.Command{ + Use: "deploy [project-directory]", + Short: "Deploy an agent to Kubernetes", + Long: `Deploy an agent to Kubernetes. + +This command will read the kagent.yaml file from the specified project directory, +create or reference a Kubernetes secret with the API key, and create an Agent CRD. + +The command will: +1. Load the agent configuration from kagent.yaml +2. Either create a new secret with the provided API key or verify an existing secret +3. Create an Agent CRD with the appropriate configuration + +API Key Options: + --api-key: Convenience option to create a new secret with the provided API key + --api-key-secret: Canonical way to reference an existing secret by name + +Examples: + kagent deploy ./my-agent --api-key-secret "my-existing-secret" + kagent deploy ./my-agent --api-key "your-api-key-here" --image "myregistry/myagent:v1.0" + kagent deploy ./my-agent --api-key-secret "my-secret" --namespace "my-namespace"`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + deployCfg.ProjectDir = args[0] + + if err := cli.DeployCmd(ctx, deployCfg); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + }, + Example: `kagent deploy ./my-agent --api-key-secret "my-existing-secret"`, + } + + // Add flags for deploy command + deployCmd.Flags().StringVarP(&deployCfg.Image, "image", "i", "", "Image to use (defaults to localhost:5001/{agentName}:latest)") + deployCmd.Flags().StringVar(&deployCfg.APIKey, "api-key", "", "API key for the model provider (convenience option to create secret)") + deployCmd.Flags().StringVar(&deployCfg.APIKeySecret, "api-key-secret", "", "Name of existing secret containing API key") + deployCmd.Flags().StringVar(&deployCfg.Config.Namespace, "namespace", "", "Kubernetes namespace to deploy to") + + rootCmd.AddCommand(installCmd, uninstallCmd, invokeCmd, bugReportCmd, versionCmd, dashboardCmd, getCmd, initCmd, buildCmd, deployCmd) // Initialize config if err := config.Init(); err != nil { diff --git a/go/cli/internal/cli/build.go b/go/cli/internal/cli/build.go new file mode 100644 index 000000000..cc4d3b958 --- /dev/null +++ b/go/cli/internal/cli/build.go @@ -0,0 +1,167 @@ +package cli + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/kagent-dev/kagent/go/cli/internal/config" + "github.com/kagent-dev/kagent/go/cli/internal/frameworks/common" +) + +type BuildCfg struct { + ProjectDir string + Image string + Push bool + Config *config.Config +} + +// BuildCmd builds a Docker image for an agent project +func BuildCmd(cfg *BuildCfg) error { + // Validate project directory + if cfg.ProjectDir == "" { + return fmt.Errorf("project directory is required") + } + + // Check if project directory exists + if _, err := os.Stat(cfg.ProjectDir); os.IsNotExist(err) { + return fmt.Errorf("project directory does not exist: %s", cfg.ProjectDir) + } + + // Check if Dockerfile exists in project directory + dockerfilePath := filepath.Join(cfg.ProjectDir, "Dockerfile") + if _, err := os.Stat(dockerfilePath); os.IsNotExist(err) { + return fmt.Errorf("dockerfile not found in project directory: %s", dockerfilePath) + } + + // Check if Docker is available and running + if err := checkDockerAvailability(); err != nil { + return fmt.Errorf("docker check failed: %v", err) + } + + // Build the Docker image + if err := buildDockerImage(cfg); err != nil { + return fmt.Errorf("failed to build Docker image: %v", err) + } + + // Push the image if requested + if cfg.Push { + // Docker availability is already checked above, but we could add another check here if needed + if err := pushDockerImage(cfg); err != nil { + return fmt.Errorf("failed to push Docker image: %v", err) + } + } + + return nil +} + +// buildDockerImage builds the Docker image using docker build +func buildDockerImage(cfg *BuildCfg) error { + // Construct the image name + imageName := constructImageName(cfg) + + // Build command arguments + args := []string{"build", "-t", imageName, "."} + + // Execute docker build command + cmd := exec.Command("docker", args...) + cmd.Dir = cfg.ProjectDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if cfg.Config.Verbose { + fmt.Printf("Executing: docker %s\n", strings.Join(args, " ")) + fmt.Printf("Working directory: %s\n", cmd.Dir) + } + + if err := cmd.Run(); err != nil { + return fmt.Errorf("docker build failed: %v", err) + } + + fmt.Printf("Successfully built Docker image: %s\n", imageName) + return nil +} + +// pushDockerImage pushes the Docker image to the specified registry +func pushDockerImage(cfg *BuildCfg) error { + // Construct the image name + imageName := constructImageName(cfg) + + // Execute docker push command + cmd := exec.Command("docker", "push", imageName) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if cfg.Config.Verbose { + fmt.Printf("Executing: docker push %s\n", imageName) + } + + if err := cmd.Run(); err != nil { + return fmt.Errorf("docker push failed: %v", err) + } + + fmt.Printf("Successfully pushed Docker image: %s\n", imageName) + return nil +} + +// constructImageName constructs the full image name from the provided image or defaults +func constructImageName(cfg *BuildCfg) string { + // If a full image specification is provided, use it as-is + if cfg.Image != "" { + return cfg.Image + } + + // Otherwise, construct from defaults + // Get agent name from kagent.yaml file + agentName := getAgentNameFromManifest(cfg.ProjectDir) + + // If no agent name found in manifest, fall back to directory name + if agentName == "" { + agentName = filepath.Base(cfg.ProjectDir) + } + + // Use default registry and tag + registry := "localhost:5001" + tag := "latest" + + // Construct full image name: registry/agent-name:tag + return fmt.Sprintf("%s/%s:%s", registry, agentName, tag) +} + +// getAgentNameFromManifest attempts to load the agent name from kagent.yaml +func getAgentNameFromManifest(projectDir string) string { + // Use the Manager to load the manifest + manager := common.NewManifestManager(projectDir) + manifest, err := manager.Load() + if err != nil { + // Silently fail and return empty string to fall back to directory name + return "" + } + + return manifest.Name +} + +// checkDockerAvailability checks if Docker is installed and running +func checkDockerAvailability() error { + // Check if docker command exists + if _, err := exec.LookPath("docker"); err != nil { + return fmt.Errorf("docker command not found in PATH. Please install Docker") + } + + // Check if Docker daemon is running by running docker version + cmd := exec.Command("docker", "version", "--format", "{{.Server.Version}}") + output, err := cmd.Output() + if err != nil { + return fmt.Errorf("docker daemon is not running or not accessible. Please start Docker Desktop or Docker daemon") + } + + // Check if we got a valid version string + version := strings.TrimSpace(string(output)) + if version == "" { + return fmt.Errorf("docker daemon returned empty version. Docker may not be properly installed") + } + + return nil +} diff --git a/go/cli/internal/cli/deploy.go b/go/cli/internal/cli/deploy.go new file mode 100644 index 000000000..974ca31aa --- /dev/null +++ b/go/cli/internal/cli/deploy.go @@ -0,0 +1,282 @@ +package cli + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/kagent-dev/kagent/go/api/v1alpha2" + "github.com/kagent-dev/kagent/go/cli/internal/config" + "github.com/kagent-dev/kagent/go/cli/internal/frameworks/common" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type DeployCfg struct { + ProjectDir string + Image string + APIKey string + APIKeySecret string + Config *config.Config +} + +// DeployCmd deploys an agent to Kubernetes +func DeployCmd(ctx context.Context, cfg *DeployCfg) error { + // Validate project directory + if cfg.ProjectDir == "" { + return fmt.Errorf("project directory is required") + } + + // Check if project directory exists + if _, err := os.Stat(cfg.ProjectDir); os.IsNotExist(err) { + return fmt.Errorf("project directory does not exist: %s", cfg.ProjectDir) + } + + // Load the kagent.yaml manifest + manifest, err := loadManifest(cfg.ProjectDir) + if err != nil { + return fmt.Errorf("failed to load kagent.yaml: %v", err) + } + + // Determine the API key environment variable name based on model provider + apiKeyEnvVar := getAPIKeyEnvVar(manifest.ModelProvider) + if apiKeyEnvVar == "" { + return fmt.Errorf("unsupported model provider: %s", manifest.ModelProvider) + } + + // Create Kubernetes client + k8sClient, err := createKubernetesClient() + if err != nil { + return fmt.Errorf("failed to create Kubernetes client: %v", err) + } + + // If namespace is not set, use default + if cfg.Config.Namespace == "" { + cfg.Config.Namespace = "default" + } + + // Handle secret creation or reference to existing secret + var secretName string + if cfg.APIKeySecret != "" { + // Use existing secret + secretName = cfg.APIKeySecret + // Verify the secret exists + if err := verifySecretExists(ctx, k8sClient, cfg.Config.Namespace, secretName, apiKeyEnvVar); err != nil { + return err + } + if cfg.Config.Verbose { + fmt.Printf("Using existing secret '%s' in namespace '%s'\n", secretName, cfg.Config.Namespace) + } + } else if cfg.APIKey != "" { + // Create new secret with provided API key + secretName = fmt.Sprintf("%s-%s", manifest.Name, strings.ToLower(manifest.ModelProvider)) + if err := createSecret(ctx, k8sClient, cfg.Config.Namespace, secretName, apiKeyEnvVar, cfg.APIKey, cfg.Config.Verbose); err != nil { + return err + } + } else { + return fmt.Errorf("either --api-key or --api-key-secret must be provided") + } + + // Create the Agent CRD + if err := createAgentCRD(ctx, k8sClient, cfg, manifest, secretName, apiKeyEnvVar, cfg.Config.Verbose); err != nil { + return err + } + + fmt.Printf("Successfully deployed agent '%s' to namespace '%s'\n", manifest.Name, cfg.Config.Namespace) + return nil +} + +// loadManifest loads the kagent.yaml file from the project directory +func loadManifest(projectDir string) (*common.AgentManifest, error) { + // Use the Manager to load the manifest + manager := common.NewManifestManager(projectDir) + manifest, err := manager.Load() + if err != nil { + return nil, fmt.Errorf("failed to load kagent.yaml: %v", err) + } + // Additional validation for deploy-specific requirements + if manifest.ModelProvider == "" { + return nil, fmt.Errorf("model provider is required in kagent.yaml") + } + return manifest, nil +} + +// getAPIKeyEnvVar returns the environment variable name for the given model provider +func getAPIKeyEnvVar(modelProvider string) string { + switch modelProvider { + case strings.ToLower(string(v1alpha2.ModelProviderAnthropic)): + return "ANTHROPIC_API_KEY" + case strings.ToLower(string(v1alpha2.ModelProviderOpenAI)): + return "OPENAI_API_KEY" + case strings.ToLower(string(v1alpha2.ModelProviderGemini)): + return "GOOGLE_API_KEY" + default: + return "" + } +} + +// createKubernetesClient creates a Kubernetes client +func createKubernetesClient() (client.Client, error) { + // Use the standard kubeconfig loading rules + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + configOverrides := &clientcmd.ConfigOverrides{} + + clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) + + config, err := clientConfig.ClientConfig() + if err != nil { + return nil, fmt.Errorf("failed to get Kubernetes config: %v", err) + } + + schemes := runtime.NewScheme() + if err := scheme.AddToScheme(schemes); err != nil { + return nil, fmt.Errorf("failed to add core scheme: %v", err) + } + if err := v1alpha2.AddToScheme(schemes); err != nil { + return nil, fmt.Errorf("failed to add kagent v1alpha2 scheme: %v", err) + } + + k8sClient, err := client.New(config, client.Options{Scheme: schemes}) + if err != nil { + return nil, fmt.Errorf("failed to create Kubernetes client: %v", err) + } + + return k8sClient, nil +} + +// verifySecretExists verifies that a secret exists and contains the required key +func verifySecretExists(ctx context.Context, k8sClient client.Client, namespace, secretName, apiKeyEnvVar string) error { + secret := &corev1.Secret{} + err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: secretName}, secret) + if err != nil { + if errors.IsNotFound(err) { + return fmt.Errorf("secret '%s' not found in namespace '%s'", secretName, namespace) + } + return fmt.Errorf("failed to check if secret exists: %v", err) + } + + // Verify the secret contains the required key + if _, exists := secret.Data[apiKeyEnvVar]; !exists { + return fmt.Errorf("secret '%s' does not contain key '%s'", secretName, apiKeyEnvVar) + } + + return nil +} + +// createSecret creates a Kubernetes secret with the API key +func createSecret(ctx context.Context, k8sClient client.Client, namespace, secretName, apiKeyEnvVar, apiKeyValue string, verbose bool) error { + // Check if secret already exists + existingSecret := &corev1.Secret{} + err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: secretName}, existingSecret) + + if err != nil { + if errors.IsNotFound(err) { + // Create new secret + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + }, + Data: map[string][]byte{ + apiKeyEnvVar: []byte(apiKeyValue), + }, + } + if err := k8sClient.Create(ctx, secret); err != nil { + return fmt.Errorf("failed to create secret: %v", err) + } + if verbose { + fmt.Printf("Created secret '%s' in namespace '%s'\n", secretName, namespace) + } + return nil + } + return fmt.Errorf("failed to check if secret exists: %v", err) + } + + // Secret exists, update it + existingSecret.Data[apiKeyEnvVar] = []byte(apiKeyValue) + if err := k8sClient.Update(ctx, existingSecret); err != nil { + return fmt.Errorf("failed to update existing secret: %v", err) + } + if verbose { + fmt.Printf("Updated existing secret '%s' in namespace '%s'\n", secretName, namespace) + } + return nil +} + +// createAgentCRD creates the Agent CRD +func createAgentCRD(ctx context.Context, k8sClient client.Client, cfg *DeployCfg, manifest *common.AgentManifest, secretName, apiKeyEnvVar string, verbose bool) error { + // Determine image name + imageName := cfg.Image + if imageName == "" { + // Use default registry and tag + registry := "localhost:5001" + tag := "latest" + imageName = fmt.Sprintf("%s/%s:%s", registry, manifest.Name, tag) + } + + // Create the Agent CRD + agent := &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{ + Name: manifest.Name, + Namespace: cfg.Config.Namespace, + }, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_BYO, + Description: manifest.Description, + BYO: &v1alpha2.BYOAgentSpec{ + Deployment: &v1alpha2.ByoDeploymentSpec{ + Image: imageName, + SharedDeploymentSpec: v1alpha2.SharedDeploymentSpec{ + Env: []corev1.EnvVar{ + { + Name: apiKeyEnvVar, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: secretName, + }, + Key: apiKeyEnvVar, + }, + }, + }, + }, + }, + }, + }, + }, + } + + // Check if agent already exists + existingAgent := &v1alpha2.Agent{} + err := k8sClient.Get(ctx, client.ObjectKey{Namespace: cfg.Config.Namespace, Name: manifest.Name}, existingAgent) + + if err != nil { + if errors.IsNotFound(err) { + // Agent does not exist, create it + if err := k8sClient.Create(ctx, agent); err != nil { + return fmt.Errorf("failed to create agent: %v", err) + } + if verbose { + fmt.Printf("Created agent '%s' in namespace '%s'\n", manifest.Name, cfg.Config.Namespace) + } + return nil + } + return fmt.Errorf("failed to check if agent exists: %v", err) + } + + // Agent exists, update it + existingAgent.Spec = agent.Spec + if err := k8sClient.Update(ctx, existingAgent); err != nil { + return fmt.Errorf("failed to update existing agent: %v", err) + } + if verbose { + fmt.Printf("Updated existing agent '%s' in namespace '%s'\n", manifest.Name, cfg.Config.Namespace) + } + return nil +} diff --git a/go/cli/internal/cli/init.go b/go/cli/internal/cli/init.go new file mode 100644 index 000000000..e4dd52f53 --- /dev/null +++ b/go/cli/internal/cli/init.go @@ -0,0 +1,99 @@ +package cli + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/kagent-dev/kagent/go/api/v1alpha2" + "github.com/kagent-dev/kagent/go/cli/internal/config" + "github.com/kagent-dev/kagent/go/cli/internal/frameworks" + "github.com/kagent-dev/kagent/go/internal/version" +) + +type InitCfg struct { + Framework string + Language string + AgentName string + InstructionFile string + ModelProvider string + ModelName string + Description string + Config *config.Config +} + +func InitCmd(cfg *InitCfg) error { + // Validate framework and language + if cfg.Framework != "adk" { + return fmt.Errorf("unsupported framework: %s. Only 'adk' is supported", cfg.Framework) + } + + if cfg.Language != "python" { + return fmt.Errorf("unsupported language: %s. Only 'python' is supported for ADK", cfg.Language) + } + + if cfg.ModelName != "" && cfg.ModelProvider == "" { + return fmt.Errorf("model provider is required when model name is provided") + } + + // Validate model provider if specified + if cfg.ModelProvider != "" { + if err := validateModelProvider(cfg.ModelProvider); err != nil { + return err + } + } + + // use lower case for model provider since the templates expect the model provider in lower case + cfg.ModelProvider = strings.ToLower(cfg.ModelProvider) + + // Get current working directory for project creation + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current working directory: %v", err) + } + + // Create project directory + projectDir := filepath.Join(cwd, cfg.AgentName) + if err := os.MkdirAll(projectDir, 0755); err != nil { + return fmt.Errorf("failed to create project directory: %v", err) + } + + // Initialize the framework generator + generator, err := frameworks.NewGenerator(cfg.Framework, cfg.Language) + if err != nil { + return fmt.Errorf("failed to create generator: %v", err) + } + + // Load instruction from file if specified + var instruction string + if cfg.InstructionFile != "" { + content, err := os.ReadFile(cfg.InstructionFile) + if err != nil { + return fmt.Errorf("failed to read instruction file '%s': %v", cfg.InstructionFile, err) + } + instruction = string(content) + } + + // Get the kagent version + kagentVersion := version.Version + + // Generate the project + if err := generator.Generate(projectDir, cfg.AgentName, instruction, cfg.ModelProvider, cfg.ModelName, cfg.Description, cfg.Config.Verbose, kagentVersion); err != nil { + return fmt.Errorf("failed to generate project: %v", err) + } + + return nil +} + +// validateModelProvider checks if the provided model provider is supported +func validateModelProvider(provider string) error { + switch v1alpha2.ModelProvider(provider) { + case v1alpha2.ModelProviderOpenAI, + v1alpha2.ModelProviderAnthropic, + v1alpha2.ModelProviderGemini: + return nil + default: + return fmt.Errorf("unsupported model provider: %s. Supported providers: OpenAI, Anthropic, Gemini", provider) + } +} diff --git a/go/cli/internal/frameworks/adk/python/dice-agent-instruction.md b/go/cli/internal/frameworks/adk/python/dice-agent-instruction.md new file mode 100644 index 000000000..047a10d72 --- /dev/null +++ b/go/cli/internal/frameworks/adk/python/dice-agent-instruction.md @@ -0,0 +1,15 @@ +You roll dice and answer questions about the outcome of the dice rolls. +You can roll dice of different sizes. +You can use multiple tools in parallel by calling functions in parallel(in one request and in one round). +It is ok to discuss previous dice roles, and comment on the dice rolls. +When you are asked to roll a die, you must call the roll_die tool with the number of sides. Be sure to pass in an integer. Do not pass in a string. +You should never roll a die on your own. +When checking prime numbers, call the check_prime tool with a list of integers. Be sure to pass in a list of integers. You should never pass in a string. +You should not check prime numbers before calling the tool. +When you are asked to roll a die and check prime numbers, you should always make the following two function calls: +1. You should first call the roll_die tool to get a roll. Wait for the function response before calling the check_prime tool. +2. After you get the function response from roll_die tool, you should call the check_prime tool with the roll_die result. +2.1 If user asks you to check primes based on previous rolls, make sure you include the previous rolls in the list. +3. When you respond, you must include the roll_die result from step 1. +You should always perform the previous 3 steps when asking for a roll and checking prime numbers. +You should not rely on the previous history on prime results. \ No newline at end of file diff --git a/go/cli/internal/frameworks/adk/python/generator.go b/go/cli/internal/frameworks/adk/python/generator.go new file mode 100644 index 000000000..98aebbb85 --- /dev/null +++ b/go/cli/internal/frameworks/adk/python/generator.go @@ -0,0 +1,126 @@ +package python + +import ( + "embed" + "fmt" + "os" + "path/filepath" + + "github.com/kagent-dev/kagent/go/cli/internal/frameworks/common" +) + +//go:embed templates/* templates/agent/* dice-agent-instruction.md +var templatesFS embed.FS + +// PythonGenerator generates Python ADK projects +type PythonGenerator struct { + *common.BaseGenerator +} + +// NewPythonGenerator creates a new ADK Python generator +func NewPythonGenerator() *PythonGenerator { + return &PythonGenerator{ + BaseGenerator: common.NewBaseGenerator(templatesFS), + } +} + +// Generate creates a new Python ADK project +func (g *PythonGenerator) Generate(projectDir, agentName, instruction, modelProvider, modelName, description string, verbose bool, kagentVersion string) error { + // Create the main project directory structure + subDir := filepath.Join(projectDir, agentName) + if err := os.MkdirAll(subDir, 0755); err != nil { + return fmt.Errorf("failed to create subdirectory: %v", err) + } + // Load default instructions if none provided + if instruction == "" { + if verbose { + fmt.Println("šŸŽ² No instruction provided, using default dice-roller instructions") + } + defaultInstructions, _ := templatesFS.ReadFile("dice-agent-instruction.md") + instruction = string(defaultInstructions) + } + + // agent project configuration + agentConfig := common.AgentConfig{ + Name: agentName, + Directory: projectDir, + Framework: "adk", + Language: "python", + Verbose: verbose, + Instruction: instruction, + ModelProvider: modelProvider, + ModelName: modelName, + KagentVersion: kagentVersion, + } + + // Use the base generator to create the project + if err := g.GenerateProject(agentConfig); err != nil { + return fmt.Errorf("failed to generate project: %v", err) + } + + // Generate project manifest file + projectManifest := common.NewProjectManifest( + agentConfig.Name, + agentConfig.Language, + agentConfig.Framework, + agentConfig.ModelProvider, + agentConfig.ModelName, + description, + ) + + // Save the manifest using the Manager + manager := common.NewManifestManager(projectDir) + if err := manager.Save(projectManifest); err != nil { + return fmt.Errorf("failed to write project manifest: %v", err) + } + + // Move agent files from agent/ subdirectory to {agentName} subdirectory + agentDir := filepath.Join(projectDir, "agent") + if _, err := os.Stat(agentDir); err == nil { + // Move all files from agent/ to project subdirectory + entries, err := os.ReadDir(agentDir) + if err != nil { + return fmt.Errorf("failed to read agent directory: %v", err) + } + + for _, entry := range entries { + if !entry.IsDir() { + srcPath := filepath.Join(agentDir, entry.Name()) + dstPath := filepath.Join(subDir, entry.Name()) + + if err := os.Rename(srcPath, dstPath); err != nil { + return fmt.Errorf("failed to move %s to %s: %v", srcPath, dstPath, err) + } + } + } + + // Remove the now-empty agent directory + if err := os.Remove(agentDir); err != nil { + return fmt.Errorf("failed to remove agent directory: %v", err) + } + } + + fmt.Printf("āœ… Successfully created %s project in %s\n", agentConfig.Framework, projectDir) + fmt.Printf("šŸ¤– Model configuration for project: %s (%s)\n", agentConfig.ModelProvider, agentConfig.ModelName) + fmt.Printf("šŸ“ Project structure:\n") + fmt.Printf(" %s/\n", agentConfig.Name) + fmt.Printf(" ā”œā”€ā”€ %s/\n", agentConfig.Name) + fmt.Printf(" │ ā”œā”€ā”€ __init__.py\n") + fmt.Printf(" │ ā”œā”€ā”€ agent.py\n") + fmt.Printf(" │ └── agent-card.json\n") + fmt.Printf(" ā”œā”€ā”€ %s\n", common.ManifestFileName) + fmt.Printf(" ā”œā”€ā”€ pyproject.toml\n") + fmt.Printf(" ā”œā”€ā”€ Dockerfile\n") + fmt.Printf(" └── README.md\n") + fmt.Printf("\nšŸš€ Next steps:\n") + fmt.Printf(" 1. cd %s\n", agentConfig.Name) + fmt.Printf(" 2. Customize the agent in %s/agent.py\n", agentConfig.Name) + fmt.Printf(" 3. Build the agent image and push it to the local registry\n") + fmt.Printf(" kagent build %s --push\n", agentConfig.Name) + fmt.Printf(" 4. Deploy the agent to your local cluster\n") + fmt.Printf(" kagent deploy %s --api-key-secret \n", agentConfig.Name) + fmt.Printf(" Or use --api-key for convenience: kagent deploy %s --api-key \n", agentConfig.Name) + fmt.Printf(" Support for using a credential file is coming soon\n") + + return nil +} diff --git a/go/cli/internal/frameworks/adk/python/templates/.python-version b/go/cli/internal/frameworks/adk/python/templates/.python-version new file mode 100644 index 000000000..86f8c02eb --- /dev/null +++ b/go/cli/internal/frameworks/adk/python/templates/.python-version @@ -0,0 +1 @@ +3.13.5 diff --git a/go/cli/internal/frameworks/adk/python/templates/Dockerfile.tmpl b/go/cli/internal/frameworks/adk/python/templates/Dockerfile.tmpl new file mode 100644 index 000000000..86ac1fd50 --- /dev/null +++ b/go/cli/internal/frameworks/adk/python/templates/Dockerfile.tmpl @@ -0,0 +1,14 @@ +ARG DOCKER_REGISTRY=ghcr.io +ARG VERSION={{.KagentVersion}} +FROM $DOCKER_REGISTRY/kagent-dev/kagent/kagent-adk:$VERSION + +WORKDIR /app + +COPY {{.Name}}/ {{.Name}}/ +COPY pyproject.toml pyproject.toml +COPY README.md README.md +COPY .python-version .python-version + +RUN uv sync + +CMD ["{{.Name}}"] diff --git a/go/cli/internal/frameworks/adk/python/templates/README.md.tmpl b/go/cli/internal/frameworks/adk/python/templates/README.md.tmpl new file mode 100644 index 000000000..191d2dccd --- /dev/null +++ b/go/cli/internal/frameworks/adk/python/templates/README.md.tmpl @@ -0,0 +1,33 @@ +# {{.Name}} Agent + +This is a {{.Name}} agent that can be used to test KAgent BYO agent with ADK. + +## Model Configuration + +This agent is configured to use the **{{.ModelProvider}}** provider with model **{{.ModelName}}**. + +## Usage + +1. Build the agent image and push it to the local registry using the KAgent CLI + +```bash +kagent build {{.Name}} +``` + +2. Deploy the agent + +```bash +kagent deploy {{.Name}} --api-key +``` + +Or create a secret with the api key + +```bash +kubectl create secret generic my-secret -n --from-literal=_API_KEY=$API_KEY --dry-run=client -oyaml | k apply -f - +``` + +And then deploy the agent + +```bash +kagent deploy {{.Name}} --api-key-secret "my-secret" +``` \ No newline at end of file diff --git a/go/cli/internal/frameworks/adk/python/templates/agent/__init__.py.tmpl b/go/cli/internal/frameworks/adk/python/templates/agent/__init__.py.tmpl new file mode 100644 index 000000000..6844c43f9 --- /dev/null +++ b/go/cli/internal/frameworks/adk/python/templates/agent/__init__.py.tmpl @@ -0,0 +1,2 @@ +from . import agent + diff --git a/go/cli/internal/frameworks/adk/python/templates/agent/agent-card.json.tmpl b/go/cli/internal/frameworks/adk/python/templates/agent/agent-card.json.tmpl new file mode 100644 index 000000000..0f5b59470 --- /dev/null +++ b/go/cli/internal/frameworks/adk/python/templates/agent/agent-card.json.tmpl @@ -0,0 +1,19 @@ +{ + "name": "{{.Name}}", + "description": "A {{.Name}} agent", + "url": "localhost:8080", + "version": "0.0.1", + "capabilities": { + "streaming": true + }, + "defaultInputModes": ["text"], + "defaultOutputModes": ["text"], + "skills": [ + { + "id": "{{.Name}}", + "name": "{{.Name}}", + "description": "A {{.Name}} agent", + "tags": ["{{.Name}}"] + } + ] +} diff --git a/go/cli/internal/frameworks/adk/python/templates/agent/agent.py.tmpl b/go/cli/internal/frameworks/adk/python/templates/agent/agent.py.tmpl new file mode 100644 index 000000000..96ffc13ad --- /dev/null +++ b/go/cli/internal/frameworks/adk/python/templates/agent/agent.py.tmpl @@ -0,0 +1,80 @@ +import random + +from google.adk import Agent +from google.adk.tools.tool_context import ToolContext +from google.adk.models.lite_llm import LiteLlm + + +def roll_die(sides: int, tool_context: ToolContext) -> int: + """Roll a die and return the rolled result. + Args: + sides: The integer number of sides the die has. + Returns: + An integer of the result of rolling the die. + """ + result = random.randint(1, sides) + if "rolls" not in tool_context.state: + tool_context.state["rolls"] = [] + + tool_context.state["rolls"] = tool_context.state["rolls"] + [result] + return result + + +async def check_prime(nums: list[int]) -> str: + """Check if a given list of numbers are prime. + Args: + nums: The list of numbers to check. + Returns: + A str indicating which number is prime. + """ + primes = set() + for number in nums: + number = int(number) + if number <= 1: + continue + is_prime = True + for i in range(2, int(number**0.5) + 1): + if number % i == 0: + is_prime = False + break + if is_prime: + primes.add(number) + return "No prime numbers found." if not primes else f"{', '.join(str(num) for num in primes)} are prime numbers." + + +# Create model based on provider and model name +{{if eq .ModelProvider "gemini"}} +def create_model(): + """Create a Gemini model instance.""" + return "{{.ModelName}}" +{{else if eq .ModelProvider "openai"}} +def create_model(): + """Create an OpenAI model instance using LiteLLM.""" + return LiteLlm(model="openai/{{.ModelName}}") +{{else if eq .ModelProvider "anthropic"}} +def create_model(): + """Create an Anthropic model instance using LiteLLM.""" + return LiteLlm(model="anthropic/{{.ModelName}}") +{{else if eq .ModelProvider "azureopenai"}} +def create_model(): + """Create an Azure OpenAI model instance using LiteLLM.""" + return LiteLlm(model="azure/{{.ModelName}}") +{{else}} +def create_model(): + """Create a custom model instance.""" + return "{{.ModelName}}" +{{end}} + + +root_agent = Agent( + model=create_model(), + name="{{.Name}}_agent", + description=("{{.Name}} agent."), + instruction=""" +{{.Instruction}} + """, + tools=[ + roll_die, + check_prime, + ], +) \ No newline at end of file diff --git a/go/cli/internal/frameworks/adk/python/templates/pyproject.toml.tmpl b/go/cli/internal/frameworks/adk/python/templates/pyproject.toml.tmpl new file mode 100644 index 000000000..12b3c6e6e --- /dev/null +++ b/go/cli/internal/frameworks/adk/python/templates/pyproject.toml.tmpl @@ -0,0 +1,8 @@ +[project] +name = "{{.Name}}" +version = "0.1" +description = "{{.Name}} agent" +readme = "README.md" +dependencies = [ + "google-adk>=1.8.0", +] diff --git a/go/cli/internal/frameworks/common/base_generator.go b/go/cli/internal/frameworks/common/base_generator.go new file mode 100644 index 000000000..2aa22d642 --- /dev/null +++ b/go/cli/internal/frameworks/common/base_generator.go @@ -0,0 +1,109 @@ +package common + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + "text/template" +) + +// AgentConfig holds the configuration for agent project generation +type AgentConfig struct { + Name string + Directory string + Verbose bool + Instruction string + ModelProvider string + ModelName string + Framework string + Language string + KagentVersion string +} + +// BaseGenerator provides common functionality for all project generators +type BaseGenerator struct { + TemplateFiles fs.FS +} + +// NewBaseGenerator creates a new base generator +func NewBaseGenerator(templateFiles fs.FS) *BaseGenerator { + return &BaseGenerator{ + TemplateFiles: templateFiles, + } +} + +// GenerateProject generates a new project using the provided templates +func (g *BaseGenerator) GenerateProject(config AgentConfig) error { + // Get templates subdirectory + templateRoot, err := fs.Sub(g.TemplateFiles, "templates") + if err != nil { + return fmt.Errorf("failed to get templates subdirectory: %w", err) + } + + // Walk through all template files + err = fs.WalkDir(templateRoot, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + // Skip directories, we'll create them as needed + if d.IsDir() { + return nil + } + + // Determine destination path by removing .tmpl extension + destPath := filepath.Join(config.Directory, strings.TrimSuffix(path, ".tmpl")) + + // Create the directory if it doesn't exist + if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil { + return fmt.Errorf("failed to create directory %s: %w", filepath.Dir(destPath), err) + } + + // Read template file + templateContent, err := fs.ReadFile(templateRoot, path) + if err != nil { + return fmt.Errorf("failed to read template file %s: %w", path, err) + } + + // Render template content + renderedContent, err := g.renderTemplate(string(templateContent), config) + if err != nil { + return fmt.Errorf("failed to render template for %s: %w", path, err) + } + + // Create file + if err := os.WriteFile(destPath, []byte(renderedContent), 0644); err != nil { + return fmt.Errorf("failed to write file %s: %w", destPath, err) + } + + if config.Verbose { + // print the generated files + fmt.Printf(" Generated: %s\n", destPath) + } + + return nil + }) + + if err != nil { + return fmt.Errorf("failed to walk templates: %w", err) + } + + return nil +} + +// renderTemplate renders a template string with the provided data +func (g *BaseGenerator) renderTemplate(tmplContent string, data interface{}) (string, error) { + tmpl, err := template.New("template").Parse(tmplContent) + if err != nil { + return "", fmt.Errorf("failed to parse template: %w", err) + } + + var result strings.Builder + if err := tmpl.Execute(&result, data); err != nil { + return "", fmt.Errorf("failed to execute template: %w", err) + } + + return result.String(), nil +} diff --git a/go/cli/internal/frameworks/common/manifest_manager.go b/go/cli/internal/frameworks/common/manifest_manager.go new file mode 100644 index 000000000..505286db5 --- /dev/null +++ b/go/cli/internal/frameworks/common/manifest_manager.go @@ -0,0 +1,110 @@ +package common + +import ( + "fmt" + "os" + "path/filepath" + "time" + + "gopkg.in/yaml.v3" +) + +const ManifestFileName = "kagent.yaml" + +// AgentManifest represents the agent project configuration and metadata +type AgentManifest struct { + Name string `yaml:"agentName"` + Language string `yaml:"language"` + Framework string `yaml:"framework"` + ModelProvider string `yaml:"modelProvider"` + ModelName string `yaml:"modelName"` + Description string `yaml:"description"` + UpdatedAt time.Time `yaml:"updatedAt,omitempty"` +} + +// Manager handles loading and saving of agent manifests +type Manager struct { + projectRoot string +} + +// NewManifestManager creates a new manifest manager for the given project root +func NewManifestManager(projectRoot string) *Manager { + return &Manager{ + projectRoot: projectRoot, + } +} + +// Load reads and parses the kagent.yaml file +func (m *Manager) Load() (*AgentManifest, error) { + manifestPath := filepath.Join(m.projectRoot, ManifestFileName) + + data, err := os.ReadFile(manifestPath) + if err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("kagent.yaml not found in %s", m.projectRoot) + } + return nil, fmt.Errorf("failed to read kagent.yaml: %w", err) + } + + var manifest AgentManifest + if err := yaml.Unmarshal(data, &manifest); err != nil { + return nil, fmt.Errorf("failed to parse kagent.yaml: %w", err) + } + + // Validate the manifest + if err := m.Validate(&manifest); err != nil { + return nil, fmt.Errorf("invalid kagent.yaml: %w", err) + } + + return &manifest, nil +} + +// Save writes the manifest to kagent.yaml +func (m *Manager) Save(manifest *AgentManifest) error { + // Update timestamp + manifest.UpdatedAt = time.Now() + + // Validate before saving + if err := m.Validate(manifest); err != nil { + return fmt.Errorf("invalid manifest: %w", err) + } + + data, err := yaml.Marshal(manifest) + if err != nil { + return fmt.Errorf("failed to marshal manifest: %w", err) + } + + manifestPath := filepath.Join(m.projectRoot, ManifestFileName) + if err := os.WriteFile(manifestPath, data, 0644); err != nil { + return fmt.Errorf("failed to write kagent.yaml: %w", err) + } + + return nil +} + +// Validate checks if the manifest is valid +func (m *Manager) Validate(manifest *AgentManifest) error { + if manifest.Name == "" { + return fmt.Errorf("agent name is required") + } + if manifest.Language == "" { + return fmt.Errorf("language is required") + } + if manifest.Framework == "" { + return fmt.Errorf("framework is required") + } + return nil +} + +// NewProjectManifest creates a new AgentManifest with the given values +func NewProjectManifest(agentName, language, framework, modelProvider, modelName, description string) *AgentManifest { + return &AgentManifest{ + Name: agentName, + Language: language, + Framework: framework, + ModelProvider: modelProvider, + ModelName: modelName, + Description: description, + UpdatedAt: time.Now(), + } +} diff --git a/go/cli/internal/frameworks/frameworks.go b/go/cli/internal/frameworks/frameworks.go new file mode 100644 index 000000000..16e1d9009 --- /dev/null +++ b/go/cli/internal/frameworks/frameworks.go @@ -0,0 +1,27 @@ +package frameworks + +import ( + "fmt" + + adk_python "github.com/kagent-dev/kagent/go/cli/internal/frameworks/adk/python" +) + +// Generator interface for project generation +type Generator interface { + Generate(projectDir, agentName, instruction, modelProvider, modelName, description string, verbose bool, kagentVersion string) error +} + +// NewGenerator creates a new generator for the specified framework and language +func NewGenerator(framework, language string) (Generator, error) { + switch framework { + case "adk": + switch language { + case "python": + return adk_python.NewPythonGenerator(), nil + default: + return nil, fmt.Errorf("unsupported language '%s' for adk", language) + } + default: + return nil, fmt.Errorf("unsupported framework: %s", framework) + } +}