diff --git a/go/cli/cmd/kagent/main.go b/go/cli/cmd/kagent/main.go index 415337139..dc259cbe9 100644 --- a/go/cli/cmd/kagent/main.go +++ b/go/cli/cmd/kagent/main.go @@ -213,39 +213,43 @@ func main() { } initCmd := &cobra.Command{ - Use: "init [framework] [language] [agent-name]", + Use: "init [agent-name]", Short: "Initialize a new agent project", - Long: `Initialize a new agent project using the specified framework and language. + Long: `Initialize a new agent project using the specified framework . +You can customize the framework using the --framework flag (adk, crewai, langgraph). 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. +If no custom instruction file is provided, framework-specific defaults will be used. 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), + kagent init my-agent + kagent init my-agent --framework crewai + kagent init my-agent --framework langgraph --model-provider openai --model-name gpt-4 + kagent init my-agent --framework adk --instruction-file instructions.md`, + Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - initCfg.Framework = args[0] - initCfg.Language = args[1] - initCfg.AgentName = args[2] + initCfg.AgentName = args[0] 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 + // Add flags for framework selection and customization + initCmd.Flags().StringVarP(&initCfg.Framework, "framework", "f", "adk", "Framework to use (adk, crewai, langgraph)") 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.ModelProvider, "model-provider", "Gemini", "Model provider (OpenAI, Anthropic, Gemini, AzureOpenAI)") + 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") + // Add shell completion for framework flag + _ = initCmd.RegisterFlagCompletionFunc("framework", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"adk", "crewai", "langgraph"}, cobra.ShellCompDirectiveNoFileComp + }) + buildCfg := &cli.BuildCfg{ Config: cfg, } diff --git a/go/cli/internal/agent/frameworks/adk/python/generator.go b/go/cli/internal/agent/frameworks/adk/python/generator.go index 9862d3a40..c18dbe1ef 100644 --- a/go/cli/internal/agent/frameworks/adk/python/generator.go +++ b/go/cli/internal/agent/frameworks/adk/python/generator.go @@ -13,19 +13,29 @@ import ( var templatesFS embed.FS // PythonGenerator generates Python ADK projects -type PythonGenerator struct { +type ADKGenerator struct { *common.BaseGenerator } -// NewPythonGenerator creates a new ADK Python generator -func NewPythonGenerator() *PythonGenerator { - return &PythonGenerator{ +// NewADKGenerator creates a new ADK Python generator +func NewADKGenerator() *ADKGenerator { + return &ADKGenerator{ BaseGenerator: common.NewBaseGenerator(templatesFS), } } +// GetFrameworkName returns the framework name +func (g *ADKGenerator) GetFrameworkName() string { + return "adk" +} + +// GetLanguage returns the language +func (g *ADKGenerator) GetLanguage() string { + return "python" +} + // Generate creates a new Python ADK project -func (g *PythonGenerator) Generate(projectDir, agentName, instruction, modelProvider, modelName, description string, verbose bool, kagentVersion string) error { +func (g *ADKGenerator) 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 { diff --git a/go/cli/internal/agent/frameworks/common/base_generator.go b/go/cli/internal/agent/frameworks/common/base_generator.go index 2e664108f..c91fa2fa3 100644 --- a/go/cli/internal/agent/frameworks/common/base_generator.go +++ b/go/cli/internal/agent/frameworks/common/base_generator.go @@ -45,13 +45,6 @@ type BaseGenerator struct { *generator.BaseGenerator } -// NewBaseGenerator creates a new base generator that uses the shared generator -func NewBaseGenerator(templateFiles fs.FS) *BaseGenerator { - return &BaseGenerator{ - BaseGenerator: generator.NewBaseGenerator(templateFiles, "templates"), - } -} - // GenerateProject generates a new project using the provided templates. // This delegates to the shared generator implementation. func (g *BaseGenerator) GenerateProject(config AgentConfig) error { @@ -63,3 +56,10 @@ func (g *BaseGenerator) GenerateProject(config AgentConfig) error { func (g *BaseGenerator) RenderTemplate(tmplContent string, data any) (string, error) { return g.BaseGenerator.RenderTemplate(tmplContent, data) } + +// NewBaseGenerator creates a new BaseGenerator with the given template files +func NewBaseGenerator(templateFiles fs.FS) *BaseGenerator { + return &BaseGenerator{ + BaseGenerator: generator.NewBaseGenerator(templateFiles, "templates"), + } +} diff --git a/go/cli/internal/agent/frameworks/common/framework_registry.go b/go/cli/internal/agent/frameworks/common/framework_registry.go new file mode 100644 index 000000000..c6356e53a --- /dev/null +++ b/go/cli/internal/agent/frameworks/common/framework_registry.go @@ -0,0 +1,66 @@ +package common + +import ( + "fmt" + "strings" +) + +// Generator defines the interface that all framework generators must implement +type Generator interface { + Generate(projectDir, agentName, instruction, modelProvider, modelName, description string, verbose bool, kagentVersion string) error + GetFrameworkName() string + GetLanguage() string +} + +// GeneratorRegistry manages available framework generators +type GeneratorRegistry struct { + generators map[string]Generator + validator *FrameworkValidator +} + +// NewGeneratorRegistry creates a new registry +func NewGeneratorRegistry() *GeneratorRegistry { + return &GeneratorRegistry{ + generators: make(map[string]Generator), + validator: NewFrameworkValidator(), + } +} + +// Register adds a generator to the registry +func (r *GeneratorRegistry) Register(gen Generator) error { + framework := strings.ToLower(gen.GetFrameworkName()) + + // Validate framework is supported + if err := r.validator.Validate(framework); err != nil { + return fmt.Errorf("cannot register unsupported framework: %w", err) + } + + r.generators[framework] = gen + return nil +} + +// Get retrieves a generator by framework name +func (r *GeneratorRegistry) Get(framework string) (Generator, error) { + framework = strings.ToLower(framework) + + // Validate framework + if err := r.validator.Validate(framework); err != nil { + return nil, err + } + + gen, exists := r.generators[framework] + if !exists { + return nil, fmt.Errorf("no generator registered for framework '%s'", framework) + } + + return gen, nil +} + +// List returns all registered framework names +func (r *GeneratorRegistry) List() []string { + frameworks := make([]string, 0, len(r.generators)) + for framework := range r.generators { + frameworks = append(frameworks, framework) + } + return frameworks +} diff --git a/go/cli/internal/agent/frameworks/common/framework_validator.go b/go/cli/internal/agent/frameworks/common/framework_validator.go new file mode 100644 index 000000000..7c5edf4ba --- /dev/null +++ b/go/cli/internal/agent/frameworks/common/framework_validator.go @@ -0,0 +1,38 @@ +package common + +import ( + "fmt" + "slices" + "strings" +) + +// SupportedFrameworks lists all available frameworks +var SupportedFrameworks = []string{"adk", "crewai", "langgraph"} + +// FrameworkValidator validates framework selections +type FrameworkValidator struct{} + +// NewFrameworkValidator creates a new validator +func NewFrameworkValidator() *FrameworkValidator { + return &FrameworkValidator{} +} + +// Validate checks if the framework is supported +func (v *FrameworkValidator) Validate(framework string) error { + framework = strings.ToLower(framework) + if slices.Contains(SupportedFrameworks, framework) { + return nil + } + return fmt.Errorf("unsupported framework '%s'. Supported frameworks: %s", + framework, strings.Join(SupportedFrameworks, ", ")) +} + +// GetSupportedFrameworks returns the list of supported frameworks +func (v *FrameworkValidator) GetSupportedFrameworks() []string { + return SupportedFrameworks +} + +// IsSupported checks if a framework is supported +func (v *FrameworkValidator) IsSupported(framework string) bool { + return v.Validate(framework) == nil +} diff --git a/go/cli/internal/agent/frameworks/common/template_helpers.go b/go/cli/internal/agent/frameworks/common/template_helpers.go new file mode 100644 index 000000000..760b9c788 --- /dev/null +++ b/go/cli/internal/agent/frameworks/common/template_helpers.go @@ -0,0 +1,41 @@ +package common + +import ( + "strings" + "unicode" +) + +// ToPascalCase converts a string to PascalCase +func ToPascalCase(s string) string { + // Split by common delimiters + words := strings.FieldsFunc(s, func(r rune) bool { + return r == '-' || r == '_' || r == ' ' + }) + + for i, word := range words { + if len(word) > 0 { + // Capitalize first letter, lowercase rest + runes := []rune(word) + runes[0] = unicode.ToUpper(runes[0]) + for j := 1; j < len(runes); j++ { + runes[j] = unicode.ToLower(runes[j]) + } + words[i] = string(runes) + } + } + + return strings.Join(words, "") +} + +// ToUpper converts string to uppercase +func ToUpper(s string) string { + return strings.ToUpper(s) +} + +// TemplateHelpers returns a map of helper functions for templates +func TemplateHelpers() map[string]any { + return map[string]any{ + "ToPascalCase": ToPascalCase, + "ToUpper": ToUpper, + } +} diff --git a/go/cli/internal/agent/frameworks/common/versions.go b/go/cli/internal/agent/frameworks/common/versions.go new file mode 100644 index 000000000..7b3168050 --- /dev/null +++ b/go/cli/internal/agent/frameworks/common/versions.go @@ -0,0 +1,48 @@ +package common + +// FrameworkVersions holds version information for framework dependencies +type FrameworkVersions struct { + CrewAI map[string]string + LangGraph map[string]string + KagentCore string +} + +// DefaultVersions returns the default versions for all frameworks +func DefaultVersions() *FrameworkVersions { + return &FrameworkVersions{ + CrewAI: map[string]string{ + "crewai": "^0.76.0", + "crewai-tools": "^0.12.0", + }, + LangGraph: map[string]string{ + "langgraph": "^0.2.16", + "langchain": "^0.3.0", + }, + KagentCore: "^0.3.0", + } +} + +// GetCrewAIVersion returns the CrewAI package version +func (v *FrameworkVersions) GetCrewAIVersion() string { + return v.CrewAI["crewai"] +} + +// GetCrewAIToolsVersion returns the CrewAI tools version +func (v *FrameworkVersions) GetCrewAIToolsVersion() string { + return v.CrewAI["crewai-tools"] +} + +// GetLangGraphVersion returns the LangGraph version +func (v *FrameworkVersions) GetLangGraphVersion() string { + return v.LangGraph["langgraph"] +} + +// GetLangChainVersion returns the LangChain version +func (v *FrameworkVersions) GetLangChainVersion() string { + return v.LangGraph["langchain"] +} + +// GetKagentCoreVersion returns the kagent-core version +func (v *FrameworkVersions) GetKagentCoreVersion() string { + return v.KagentCore +} diff --git a/go/cli/internal/agent/frameworks/crewai/python/generator.go b/go/cli/internal/agent/frameworks/crewai/python/generator.go new file mode 100644 index 000000000..6b49389f7 --- /dev/null +++ b/go/cli/internal/agent/frameworks/crewai/python/generator.go @@ -0,0 +1,142 @@ +package python + +import ( + "embed" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/kagent-dev/kagent/go/cli/internal/agent/frameworks/common" +) + +//go:embed templates/* templates/agent/* +var templatesFS embed.FS + +// CrewAIGenerator generates Python CrewAI projects +type CrewAIGenerator struct { + *common.BaseGenerator + versions *common.FrameworkVersions +} + +// NewCrewAIGenerator creates a new CrewAI Python generator +func NewCrewAIGenerator() *CrewAIGenerator { + return &CrewAIGenerator{ + BaseGenerator: common.NewBaseGenerator(templatesFS), + versions: common.DefaultVersions(), + } +} + +// GetFrameworkName returns the framework name +func (g *CrewAIGenerator) GetFrameworkName() string { + return "crewai" +} + +// GetLanguage returns the language +func (g *CrewAIGenerator) GetLanguage() string { + return "python" +} + +// Generate creates a new Python CrewAI project +func (g *CrewAIGenerator) Generate(projectDir, agentName, instruction, modelProvider, modelName, description string, verbose bool, kagentVersion string) error { + // Create the main project directory structure + // Convert agent name to valid Python module name (replace hyphens with underscores) + moduleName := strings.ReplaceAll(strings.ReplaceAll(agentName, "-", "_"), " ", "_") + subDir := filepath.Join(projectDir, moduleName) + if err := os.MkdirAll(subDir, 0755); err != nil { + return fmt.Errorf("failed to create subdirectory: %v", err) + } + + // Use default instruction if none provided + if instruction == "" { + instruction = "You are a helpful AI assistant built with CrewAI framework." + if verbose { + fmt.Println("ℹ️ No instruction provided, using default CrewAI instructions") + } + } + + // Agent project configuration + agentConfig := common.AgentConfig{ + Name: agentName, + Directory: projectDir, + Framework: g.GetFrameworkName(), + 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, + nil, + ) + + // 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 { + 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) + } + } + + g.printSuccessMessage(agentConfig) + return nil +} + +func (g *CrewAIGenerator) printSuccessMessage(config common.AgentConfig) { + fmt.Printf(" Successfully created %s project in %s\n", config.Framework, config.Directory) + fmt.Printf(" Model configuration for project: %s (%s)\n", config.ModelProvider, config.ModelName) + fmt.Printf(" Project structure:\n") + fmt.Printf(" %s/\n", config.Name) + fmt.Printf(" ├── %s/\n", config.Name) + fmt.Printf(" │ ├── __init__.py\n") + fmt.Printf(" │ ├── crew.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", config.Name) + fmt.Printf(" 2. Customize the crew in %s/crew.py\n", config.Name) + fmt.Printf(" 3. Build the agent image:\n") + fmt.Printf(" kagent build %s --push\n", config.Name) + fmt.Printf(" 4. Deploy the agent:\n") + fmt.Printf(" kagent deploy %s --api-key \n", config.Name) +} diff --git a/go/cli/internal/agent/frameworks/crewai/python/templates/.python-version b/go/cli/internal/agent/frameworks/crewai/python/templates/.python-version new file mode 100644 index 000000000..24ee5b1be --- /dev/null +++ b/go/cli/internal/agent/frameworks/crewai/python/templates/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/go/cli/internal/agent/frameworks/crewai/python/templates/Docker-compose.yaml.tmpl b/go/cli/internal/agent/frameworks/crewai/python/templates/Docker-compose.yaml.tmpl new file mode 100644 index 000000000..f5b9a410d --- /dev/null +++ b/go/cli/internal/agent/frameworks/crewai/python/templates/Docker-compose.yaml.tmpl @@ -0,0 +1,51 @@ +# AUTOGENERATED FILE: DO NOT EDIT +# This file is automatically generated by the kagent-adk CLI tool. +# Any changes to this file will be overwritten. + +services: + {{.Name}}: + image: localhost:5001/{{.Name}}:latest + build: + context: . + dockerfile: Dockerfile + command: ["{{.Name}}", "--local"] + ports: + - "8080:8080" + environment: + - AGENT_NAME={{.Name}} + - MODEL_PROVIDER={{.ModelProvider}} + - MODEL_NAME={{.ModelName}} + - KAGENT_URL=${KAGENT_URL:-http://localhost:8083} + - KAGENT_NAME={{.Name}} + - KAGENT_NAMESPACE=${KAGENT_NAMESPACE:-kagent} +{{- if eq .ModelProvider "anthropic" }} + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} +{{- else if eq .ModelProvider "openai" }} + - OPENAI_API_KEY=${OPENAI_API_KEY} +{{- else if eq .ModelProvider "gemini" }} + - GOOGLE_API_KEY=${GOOGLE_API_KEY} + {{- else if eq .ModelProvider "azureopenai" }} + - AZURE_OPENAI_API_KEY=${AZURE_OPENAI_API_KEY} + - AZURE_OPENAI_ENDPOINT=${AZURE_OPENAI_ENDPOINT} + - AZURE_OPENAI_API_VERSION=${AZURE_OPENAI_API_VERSION:-2024-02-15-preview} +{{- end }} +{{- range .EnvVars }} + - {{.}}=${{"${"}}{{"}"}}{{.}}{{"}"}} +{{- end }} +{{- range .McpServers }} +{{- if eq .Type "command" }} + {{.Name}}: + image: localhost:5001/{{$.Name}}-{{.Name}}:latest + build: + context: ./{{.Name}} + dockerfile: Dockerfile + {{- if .Env }} + environment: + {{- range .Env }} + - {{.}} + {{- end }} + {{- end }} + expose: + - "3000" +{{- end }} +{{- end }} \ No newline at end of file diff --git a/go/cli/internal/agent/frameworks/crewai/python/templates/Dockerfile.tmpl b/go/cli/internal/agent/frameworks/crewai/python/templates/Dockerfile.tmpl new file mode 100644 index 000000000..8ee3a4fac --- /dev/null +++ b/go/cli/internal/agent/frameworks/crewai/python/templates/Dockerfile.tmpl @@ -0,0 +1,33 @@ +FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Copy project files +COPY .python-version .python-version +COPY pyproject.toml pyproject.toml +COPY README.md README.md +COPY {{.Name | ToModuleName}}/ {{.Name | ToModuleName}}/ + +# Install dependencies +RUN uv sync + +# Set environment variables +ENV PYTHONPATH=/app +ENV PORT=8080 +ENV KAGENT_URL=http://localhost:8083 + +# Expose port +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8080/health || exit 1 + +# Run the application +CMD ["uv", "run", "{{.Name}}"] diff --git a/go/cli/internal/agent/frameworks/crewai/python/templates/README.md.tmpl b/go/cli/internal/agent/frameworks/crewai/python/templates/README.md.tmpl new file mode 100644 index 000000000..142d50203 --- /dev/null +++ b/go/cli/internal/agent/frameworks/crewai/python/templates/README.md.tmpl @@ -0,0 +1,52 @@ +# {{.Name}} Agent (CrewAI) + +This is a {{.Name}} agent built with CrewAI framework for KAgent. + +## Model Configuration + +This agent is configured to use the **{{.ModelProvider}}** provider with model **{{.ModelName}}**. + +## Agent Description + +{{.Instruction}} + +## Usage + +### 1. Build the agent image + +```bash +kagent build {{.Name}} --push +``` + +### 2. Deploy the agent + +**Option A: Direct API key** +```bash +kagent deploy {{.Name}} --api-key +``` +**Option B: Using Kubernetes secret** +```bash +Create secret +kubectl create secret generic my-secret -n +--from-literal={{.ModelProvider | upper}}_API_KEY=$API_KEY +--dry-run=client -o yaml | kubectl apply -f - + +Deploy with secret +kagent deploy {{.Name}} --api-key-secret "my-secret" +``` +## Project Structure + +```bash +{{.Name}}/ +├── {{.Name}}/ +│ ├── init.py +│ ├── crew.py # CrewAI crew definition +│ ├── agent-card.json # Agent metadata +│ └── config/ # Agent & task configurations +├── pyproject.toml +├── Dockerfile +└── README.md +``` +## Customization + +Edit `{{.Name}}/crew.py` to customize your CrewAI agents, tasks, and crew configuration. diff --git a/go/cli/internal/agent/frameworks/crewai/python/templates/agent/__init__.py.tmpl b/go/cli/internal/agent/frameworks/crewai/python/templates/agent/__init__.py.tmpl new file mode 100644 index 000000000..d940adb76 --- /dev/null +++ b/go/cli/internal/agent/frameworks/crewai/python/templates/agent/__init__.py.tmpl @@ -0,0 +1 @@ +from . import crew diff --git a/go/cli/internal/agent/frameworks/crewai/python/templates/agent/agent-card.json.tmpl b/go/cli/internal/agent/frameworks/crewai/python/templates/agent/agent-card.json.tmpl new file mode 100644 index 000000000..a8c29725f --- /dev/null +++ b/go/cli/internal/agent/frameworks/crewai/python/templates/agent/agent-card.json.tmpl @@ -0,0 +1,19 @@ +{ + "name": "{{.Name}}", + "description": "A {{.Name}} agent built with CrewAI", + "url": "localhost:8080", + "version": "0.1.0", + "capabilities": { + "streaming": true + }, + "defaultInputModes": ["text"], + "defaultOutputModes": ["text"], + "skills": [ + { + "id": "{{.Name}}", + "name": "{{.Name}}", + "description": "{{.Instruction}}", + "tags": ["crewai", "{{.Name}}"] + } + ] +} diff --git a/go/cli/internal/agent/frameworks/crewai/python/templates/agent/crew.py.tmpl b/go/cli/internal/agent/frameworks/crewai/python/templates/agent/crew.py.tmpl new file mode 100644 index 000000000..7f535da70 --- /dev/null +++ b/go/cli/internal/agent/frameworks/crewai/python/templates/agent/crew.py.tmpl @@ -0,0 +1,47 @@ +"""{{.Name}} CrewAI implementation.""" +from typing import List + +from crewai import Agent, Crew, Process, Task +from crewai.agents.agent_builder.base_agent import BaseAgent +from crewai.project import CrewBase, agent, crew, task +from .mcptools import get_mcp_tools + +# Get MCP tools +mcp_tools = get_mcp_tools() + +@CrewBase +class {{.Name | ToPascalCase}}Crew: + """{{.Name}} crew for task execution""" + + agents: List[BaseAgent] + tasks: List[Task] + + @agent + def main_agent(self) -> Agent: + """Create the main agent for this crew.""" + return Agent( + role="{{.Name}} Agent", + goal="""{{.Instruction}}""", + backstory="""You are a specialized AI agent built with CrewAI for {{.Name}}.""", + verbose=True, + allow_delegation=False, + ) + + @task + def main_task(self) -> Task: + """Define the main task for this crew.""" + return Task( + description="""Execute the assigned task based on user input.""", + expected_output="""A comprehensive response addressing the user's request.""", + agent=self.main_agent(), + ) + + @crew + def crew(self) -> Crew: + """Creates the {{.Name}} crew""" + return Crew( + agents=self.agents, + tasks=self.tasks, + process=Process.sequential, + verbose=True, + ) diff --git a/go/cli/internal/agent/frameworks/crewai/python/templates/agent/mcptools.py.tmpl b/go/cli/internal/agent/frameworks/crewai/python/templates/agent/mcptools.py.tmpl new file mode 100644 index 000000000..99d8dfed7 --- /dev/null +++ b/go/cli/internal/agent/frameworks/crewai/python/templates/agent/mcptools.py.tmpl @@ -0,0 +1,52 @@ +""" +MCP Tools integration for CrewAI agents. +Any changes to this file will be overwritten. +""" +import os +import re +from typing import List, Optional +from crewai_tools import BaseTool + +# MCP server configuration (same as LangGraph) +MCP_SERVERS = [ + {{- range .McpServers }} + { + "name": "{{.Name}}", + "type": "{{.Type}}", + {{- if eq .Type "remote" }} + "url": "{{.URL}}", + {{- if .Headers }} + "headers": { + {{- range $key, $value := .Headers }} + "{{$key}}": "{{$value}}", + {{- end }} + }, + {{- end }} + {{- end }} + }, + {{- end }} +] + +def resolve_env_vars(value: str) -> str: + """Resolve environment variables in a string value.""" + def replace_var(match): + var_name = match.group(1) + return os.environ.get(var_name, match.group(0)) + + return re.sub(r'\$\{([^}]+)\}', replace_var, value) + +def get_mcp_tools(server_names: Optional[List[str]] = None) -> List[BaseTool]: + """ + Get MCP tools from configured servers as CrewAI tools. + + Args: + server_names: Optional list of server names to include. + + Returns: + List of CrewAI BaseTool instances from MCP servers. + """ + # Implementation similar to LangGraph but adapted for CrewAI + # This would wrap MCP tools as CrewAI BaseTool instances + tools = [] + # Implementation here + return tools diff --git a/go/cli/internal/agent/frameworks/crewai/python/templates/pyproject.toml.tmpl b/go/cli/internal/agent/frameworks/crewai/python/templates/pyproject.toml.tmpl new file mode 100644 index 000000000..7a2402fb8 --- /dev/null +++ b/go/cli/internal/agent/frameworks/crewai/python/templates/pyproject.toml.tmpl @@ -0,0 +1,20 @@ +[project] +name = "{{.Name}}" +version = "0.1.0" +description = "{{.Name}} agent using CrewAI" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "kagent-crewai", + "crewai[tools]>=0.193.2,<1.0.0", + "anthropic>=0.18.0", + "openai>=1.0.0", + "google-genai>=0.6.0", +] + +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project.scripts] +{{.Name}} = "{{.Name | ToModuleName}}.cli:main" diff --git a/go/cli/internal/agent/frameworks/frameworks.go b/go/cli/internal/agent/frameworks/frameworks.go index ebbd0175b..09bd97f0c 100644 --- a/go/cli/internal/agent/frameworks/frameworks.go +++ b/go/cli/internal/agent/frameworks/frameworks.go @@ -17,7 +17,7 @@ func NewGenerator(framework, language string) (Generator, error) { case "adk": switch language { case "python": - return adk_python.NewPythonGenerator(), nil + return adk_python.NewADKGenerator(), nil default: return nil, fmt.Errorf("unsupported language '%s' for adk", language) } diff --git a/go/cli/internal/agent/frameworks/langgraph/python/generator.go b/go/cli/internal/agent/frameworks/langgraph/python/generator.go new file mode 100644 index 000000000..db6d39bba --- /dev/null +++ b/go/cli/internal/agent/frameworks/langgraph/python/generator.go @@ -0,0 +1,142 @@ +package python + +import ( + "embed" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/kagent-dev/kagent/go/cli/internal/agent/frameworks/common" +) + +//go:embed templates/* templates/agent/* +var templatesFS embed.FS + +// LangGraphGenerator generates Python LangGraph projects +type LangGraphGenerator struct { + *common.BaseGenerator + versions *common.FrameworkVersions +} + +// NewLangGraphGenerator creates a new LangGraph Python generator +func NewLangGraphGenerator() *LangGraphGenerator { + return &LangGraphGenerator{ + BaseGenerator: common.NewBaseGenerator(templatesFS), + versions: common.DefaultVersions(), + } +} + +// GetFrameworkName returns the framework name +func (g *LangGraphGenerator) GetFrameworkName() string { + return "langgraph" +} + +// GetLanguage returns the language +func (g *LangGraphGenerator) GetLanguage() string { + return "python" +} + +// Generate creates a new Python LangGraph project +func (g *LangGraphGenerator) Generate(projectDir, agentName, instruction, modelProvider, modelName, description string, verbose bool, kagentVersion string) error { + // Create the main project directory structure + // Convert agent name to valid Python module name (replace hyphens with underscores) + moduleName := strings.ReplaceAll(strings.ReplaceAll(agentName, "-", "_"), " ", "_") + subDir := filepath.Join(projectDir, moduleName) + if err := os.MkdirAll(subDir, 0755); err != nil { + return fmt.Errorf("failed to create subdirectory: %v", err) + } + + // Use default instruction if none provided + if instruction == "" { + instruction = "You are a helpful AI assistant built with LangGraph framework." + if verbose { + fmt.Println("ℹ No instruction provided, using default LangGraph instructions") + } + } + + // Agent project configuration + agentConfig := common.AgentConfig{ + Name: agentName, + Directory: projectDir, + Framework: g.GetFrameworkName(), + 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, + nil, + ) + + // 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 { + 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) + } + } + + g.printSuccessMessage(agentConfig) + return nil +} + +func (g *LangGraphGenerator) printSuccessMessage(config common.AgentConfig) { + fmt.Printf(" Successfully created %s project in %s\n", config.Framework, config.Directory) + fmt.Printf(" Model configuration for project: %s (%s)\n", config.ModelProvider, config.ModelName) + fmt.Printf(" Project structure:\n") + fmt.Printf(" %s/\n", config.Name) + fmt.Printf(" ├── %s/\n", config.Name) + fmt.Printf(" │ ├── __init__.py\n") + fmt.Printf(" │ ├── graph.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", config.Name) + fmt.Printf(" 2. Customize the graph in %s/graph.py\n", config.Name) + fmt.Printf(" 3. Build the agent image:\n") + fmt.Printf(" kagent build %s --push\n", config.Name) + fmt.Printf(" 4. Deploy the agent:\n") + fmt.Printf(" kagent deploy %s --api-key \n", config.Name) +} diff --git a/go/cli/internal/agent/frameworks/langgraph/python/templates/.python-version b/go/cli/internal/agent/frameworks/langgraph/python/templates/.python-version new file mode 100644 index 000000000..24ee5b1be --- /dev/null +++ b/go/cli/internal/agent/frameworks/langgraph/python/templates/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/go/cli/internal/agent/frameworks/langgraph/python/templates/Docker-compose.yaml.tmpl b/go/cli/internal/agent/frameworks/langgraph/python/templates/Docker-compose.yaml.tmpl new file mode 100644 index 000000000..b61b6ea7a --- /dev/null +++ b/go/cli/internal/agent/frameworks/langgraph/python/templates/Docker-compose.yaml.tmpl @@ -0,0 +1,47 @@ +# AUTOGENERATED FILE: DO NOT EDIT +# This file is automatically generated by the kagent-adk CLI tool. +# Any changes to this file will be overwritten. + +services: + {{.Name}}: + image: localhost:5001/{{.Name}}:latest + build: + context: . + dockerfile: Dockerfile + command: ["{{.Name}}", "--local"] + ports: + - "8080:8080" + environment: + - AGENT_NAME={{.Name}} + - MODEL_PROVIDER={{.ModelProvider}} + - MODEL_NAME={{.ModelName}} + - KAGENT_URL=${KAGENT_URL:-http://localhost:8083} + - KAGENT_NAME={{.Name}} + - KAGENT_NAMESPACE=${KAGENT_NAMESPACE:-kagent} +{{- if eq .ModelProvider "anthropic" }} + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} +{{- else if eq .ModelProvider "openai" }} + - OPENAI_API_KEY=${OPENAI_API_KEY} +{{- else if eq .ModelProvider "gemini" }} + - GOOGLE_API_KEY=${GOOGLE_API_KEY} +{{- end }} +{{- range .EnvVars }} + - {{.}}=${{"${"}}{{"}"}}{{.}}{{"}"}} +{{- end }} +{{- range .McpServers }} +{{- if eq .Type "command" }} + {{.Name}}: + image: localhost:5001/{{$.Name}}-{{.Name}}:latest + build: + context: ./{{.Name}} + dockerfile: Dockerfile + {{- if .Env }} + environment: + {{- range .Env }} + - {{.}} + {{- end }} + {{- end }} + expose: + - "3000" +{{- end }} +{{- end }} \ No newline at end of file diff --git a/go/cli/internal/agent/frameworks/langgraph/python/templates/Dockerfile.tmpl b/go/cli/internal/agent/frameworks/langgraph/python/templates/Dockerfile.tmpl new file mode 100644 index 000000000..8ee3a4fac --- /dev/null +++ b/go/cli/internal/agent/frameworks/langgraph/python/templates/Dockerfile.tmpl @@ -0,0 +1,33 @@ +FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Copy project files +COPY .python-version .python-version +COPY pyproject.toml pyproject.toml +COPY README.md README.md +COPY {{.Name | ToModuleName}}/ {{.Name | ToModuleName}}/ + +# Install dependencies +RUN uv sync + +# Set environment variables +ENV PYTHONPATH=/app +ENV PORT=8080 +ENV KAGENT_URL=http://localhost:8083 + +# Expose port +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8080/health || exit 1 + +# Run the application +CMD ["uv", "run", "{{.Name}}"] diff --git a/go/cli/internal/agent/frameworks/langgraph/python/templates/README.md.tmpl b/go/cli/internal/agent/frameworks/langgraph/python/templates/README.md.tmpl new file mode 100644 index 000000000..7a9c79a98 --- /dev/null +++ b/go/cli/internal/agent/frameworks/langgraph/python/templates/README.md.tmpl @@ -0,0 +1,51 @@ +# {{.Name}} Agent (LangGraph) + +This is a {{.Name}} agent built with LangGraph framework for KAgent. + +## Model Configuration + +This agent is configured to use the **{{.ModelProvider}}** provider with model **{{.ModelName}}**. + +## Agent Description + +{{.Instruction}} + +## Usage + +### 1. Build the agent image + +```bash +kagent build {{.Name}} --push +``` +### 2. Deploy the agent + +**Option A: Direct API key** +```bash +kagent deploy {{.Name}} --api-key +``` +**Option B: Using Kubernetes secret** +```bash +Create secret +kubectl create secret generic my-secret -n +--from-literal={{.ModelProvider | upper}}_API_KEY=$API_KEY +--dry-run=client -o yaml | kubectl apply -f - + +Deploy with secret +kagent deploy {{.Name}} --api-key-secret "my-secret" +``` +## Project Structure + +```bash +{{.Name}}/ +├── {{.Name}}/ +│ ├── init.py +│ ├── graph.py # LangGraph graph definition +│ └── agent-card.json # Agent metadata +├── pyproject.toml +├── Dockerfile +├── .python-version +└── README.md +``` +## Customization + +Edit `{{.Name}}/graph.py` to customize your LangGraph agent and workflow \ No newline at end of file diff --git a/go/cli/internal/agent/frameworks/langgraph/python/templates/agent/__init__.py.tmpl b/go/cli/internal/agent/frameworks/langgraph/python/templates/agent/__init__.py.tmpl new file mode 100644 index 000000000..6844c43f9 --- /dev/null +++ b/go/cli/internal/agent/frameworks/langgraph/python/templates/agent/__init__.py.tmpl @@ -0,0 +1,2 @@ +from . import agent + diff --git a/go/cli/internal/agent/frameworks/langgraph/python/templates/agent/agent-card.json.tmpl b/go/cli/internal/agent/frameworks/langgraph/python/templates/agent/agent-card.json.tmpl new file mode 100644 index 000000000..a60d51495 --- /dev/null +++ b/go/cli/internal/agent/frameworks/langgraph/python/templates/agent/agent-card.json.tmpl @@ -0,0 +1,19 @@ +{ + "name": "{{.Name}}", + "description": "A {{.Name}} agent built with LangGraph", + "url": "localhost:8080", + "version": "0.1.0", + "capabilities": { + "streaming": true + }, + "defaultInputModes": ["text"], + "defaultOutputModes": ["text"], + "skills": [ + { + "id": "{{.Name}}", + "name": "{{.Name}}", + "description": "{{.Instruction}}", + "tags": ["langgraph", "{{.Name}}"] + } + ] +} diff --git a/go/cli/internal/agent/frameworks/langgraph/python/templates/agent/agent.py.tmpl b/go/cli/internal/agent/frameworks/langgraph/python/templates/agent/agent.py.tmpl new file mode 100644 index 000000000..7434799a9 --- /dev/null +++ b/go/cli/internal/agent/frameworks/langgraph/python/templates/agent/agent.py.tmpl @@ -0,0 +1,56 @@ +"""{{.Name}} LangGraph implementation.""" +import logging + +import httpx +from kagent.core import KAgentConfig +from kagent.langgraph import KAgentCheckpointer +from langchain_core.tools import tool +from langchain_google_genai import ChatGoogleGenerativeAI +from langgraph.prebuilt import create_react_agent +from langsmith import traceable +from .mcptools import get_mcp_tools + +logger = logging.getLogger(__name__) + +kagent_checkpointer = KAgentCheckpointer( + client=httpx.AsyncClient(base_url=KAgentConfig().url), + app_name=KAgentConfig().app_name, +) + + +@traceable(name="{{.Name}}_tool") +@tool +def sample_tool(query: str) -> str: + """A sample tool for the {{.Name}} agent. + + Args: + query: The input query to process + + Returns: + A response string + """ + return f"Processed: {query}" + + +# Initialize tools list with sample tool +tools = [sample_tool] + +# Get MCP tools if available and extend the tools list +mcp_tools = get_mcp_tools() +tools.extend(mcp_tools) + +SYSTEM_INSTRUCTION = """{{.Instruction}}""" + +FORMAT_INSTRUCTION = ( + "Set response status to input_required if the user needs to provide more information to complete the request. " + "Set response status to error if there is an error while processing the request. " + "Set response status to completed if the request is complete." +) + +graph = create_react_agent( + model=ChatGoogleGenerativeAI(model="{{.ModelName}}"), + tools=tools, + checkpointer=kagent_checkpointer, + prompt=SYSTEM_INSTRUCTION + "\n\n" + FORMAT_INSTRUCTION, +) + diff --git a/go/cli/internal/agent/frameworks/langgraph/python/templates/agent/cli.py.tmpl b/go/cli/internal/agent/frameworks/langgraph/python/templates/agent/cli.py.tmpl new file mode 100644 index 000000000..3ff952d89 --- /dev/null +++ b/go/cli/internal/agent/frameworks/langgraph/python/templates/agent/cli.py.tmpl @@ -0,0 +1,42 @@ +"""CLI for the {{.Name}} LangGraph agent.""" +import json +import logging +import os + +import uvicorn +from agent import graph +from kagent.core import KAgentConfig +from kagent.langgraph import KAgentApp + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) + +logger = logging.getLogger(__name__) + + +def main(): + """Main entry point for the CLI.""" + # Load agent card from script directory + with open(os.path.join(os.path.dirname(__file__), "agent-card.json"), "r") as f: + agent_card = json.load(f) + + config = KAgentConfig() + app = KAgentApp(graph=graph, agent_card=agent_card, config=config, tracing=True) + + port = int(os.getenv("PORT", "8080")) + host = os.getenv("HOST", "0.0.0.0") + logger.info(f"Starting {{.Name}} server on {host}:{port}") + + uvicorn.run( + app.build(), + host=host, + port=port, + log_level="info", + ) + + +if __name__ == "__main__": + main() diff --git a/go/cli/internal/agent/frameworks/langgraph/python/templates/agent/mcptools.py.tmpl b/go/cli/internal/agent/frameworks/langgraph/python/templates/agent/mcptools.py.tmpl new file mode 100644 index 000000000..10a9a831a --- /dev/null +++ b/go/cli/internal/agent/frameworks/langgraph/python/templates/agent/mcptools.py.tmpl @@ -0,0 +1,83 @@ +""" +MCP Tools integration for LangGraph agents. +Any changes to this file will be overwritten. +""" +import os +import re +from typing import List, Optional +from langchain_core.tools import tool + +# MCP server configuration generated from kagent.yaml +MCP_SERVERS = [ + {{- range .McpServers }} + { + "name": "{{.Name}}", + "type": "{{.Type}}", + {{- if eq .Type "remote" }} + "url": "{{.URL}}", + {{- if .Headers }} + "headers": { + {{- range $key, $value := .Headers }} + "{{$key}}": "{{$value}}", + {{- end }} + }, + {{- end }} + {{- end }} + }, + {{- end }} +] + +def resolve_env_vars(value: str) -> str: + """Resolve environment variables in a string value.""" + def replace_var(match): + var_name = match.group(1) + return os.environ.get(var_name, match.group(0)) + + return re.sub(r'\$\{([^}]+)\}', replace_var, value) + +def get_mcp_tools(server_names: Optional[List[str]] = None) -> List: + """ + Get MCP tools from configured servers as LangChain tools. + + Args: + server_names: Optional list of server names to include. + + Returns: + List of LangChain tool instances from MCP servers. + """ + from langchain_mcp import MCPToolkit + + servers = MCP_SERVERS + + if server_names is not None: + servers = [s for s in servers if s.get("name") in server_names] + + tools = [] + for server in servers: + server_name = server["name"] + server_type = server["type"] + + # Build URL based on server type + if server_type == "command": + url = f"http://{server_name}:3000/mcp" + else: + url = server["url"] + + # Process headers with environment variable resolution + headers = {} + if "headers" in server and server["headers"]: + for key, value in server["headers"].items(): + headers[key] = resolve_env_vars(value) + + try: + # Create MCPToolkit and get tools + if headers: + toolkit = MCPToolkit(url=url, headers=headers) + else: + toolkit = MCPToolkit(url=url) + + tools.extend(toolkit.get_tools()) + except Exception as e: + print(f"Warning: Failed to load tools from {server_name}: {e}") + + return tools diff --git a/go/cli/internal/agent/frameworks/langgraph/python/templates/pyproject.toml.tmpl b/go/cli/internal/agent/frameworks/langgraph/python/templates/pyproject.toml.tmpl new file mode 100644 index 000000000..414b21bde --- /dev/null +++ b/go/cli/internal/agent/frameworks/langgraph/python/templates/pyproject.toml.tmpl @@ -0,0 +1,25 @@ +[project] +name = "{{.Name}}" +version = "0.1.0" +description = "{{.Name}} agent using LangGraph" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "kagent-langgraph", + "langgraph>=0.2.0", + "langchain-core>=0.3.0", + "langchain-google-genai>=1.0.0", + "langgraph-checkpoint-sqlite>=2.0.0", + "langsmith[otel]>=0.4.30", + "langchain-mcp>=0.1.0", + "anthropic>=0.18.0", + "openai>=1.0.0", + "google-genai>=0.6.0", +] + +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project.scripts] +{{.Name}} = "{{.Name | ToModuleName}}.cli:main" diff --git a/go/cli/internal/agent/init.go b/go/cli/internal/agent/init.go new file mode 100644 index 000000000..8898df75f --- /dev/null +++ b/go/cli/internal/agent/init.go @@ -0,0 +1,130 @@ +package agent + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/kagent-dev/kagent/go/cli/internal/agent/frameworks/adk/python" + "github.com/kagent-dev/kagent/go/cli/internal/agent/frameworks/common" + crewai "github.com/kagent-dev/kagent/go/cli/internal/agent/frameworks/crewai/python" + langgraph "github.com/kagent-dev/kagent/go/cli/internal/agent/frameworks/langgraph/python" +) + +// Default model configuration +const ( + DefaultModelProvider = "Gemini" + DefaultModelName = "gemini-2.0-flash" +) + +// InitConfig holds configuration for agent initialization +type InitConfig struct { + AgentName string + Framework string + Language string + Instruction string + ModelProvider string + ModelName string + Description string + OutputDir string + Verbose bool + KagentVersion string +} + +// InitAgent initializes a new agent project +func InitAgent(config InitConfig) error { + // Validate agent name + if config.AgentName == "" { + return fmt.Errorf("agent name is required") + } + + // Create generator registry + registry := common.NewGeneratorRegistry() + + // Register all available generators + if err := registry.Register(python.NewADKGenerator()); err != nil { + return fmt.Errorf("failed to register ADK generator: %w", err) + } + if err := registry.Register(crewai.NewCrewAIGenerator()); err != nil { + return fmt.Errorf("failed to register CrewAI generator: %w", err) + } + if err := registry.Register(langgraph.NewLangGraphGenerator()); err != nil { + return fmt.Errorf("failed to register LangGraph generator: %w", err) + } + + // Validate and normalize framework + framework := strings.ToLower(config.Framework) + if framework == "" { + framework = "adk" // Default to ADK + } + + // Normalize and validate language (keep this per @petej's request) + language := strings.ToLower(config.Language) + if language == "" { + language = "python" // Default to Python + } + if language != "python" { + return fmt.Errorf("unsupported language: %s. Only 'python' is supported for now", language) + } + + // Get the appropriate generator + generator, err := registry.Get(framework) + if err != nil { + return fmt.Errorf("failed to get generator: %w", err) + } + + // Determine output directory + outputDir := config.OutputDir + if outputDir == "" { + outputDir = config.AgentName + } + + // Create output directory + if err := os.MkdirAll(outputDir, 0755); err != nil { + return fmt.Errorf("failed to create output directory: %w", err) + } + + // Get absolute path + absOutputDir, err := filepath.Abs(outputDir) + if err != nil { + return fmt.Errorf("failed to get absolute path: %w", err) + } + + // Set default model provider and name if not specified + if config.ModelProvider == "" { + config.ModelProvider = DefaultModelProvider + } + if config.ModelName == "" { + config.ModelName = DefaultModelName + } + + if config.Verbose { + fmt.Printf("Initializing %s agent with %s framework (language: %s)\n", config.AgentName, framework, language) + fmt.Printf("Output directory: %s\n", absOutputDir) + fmt.Printf("Model: %s/%s\n", config.ModelProvider, config.ModelName) + } + + // Generate the project + err = generator.Generate( + absOutputDir, + config.AgentName, + config.Instruction, + config.ModelProvider, + config.ModelName, + config.Description, + config.Verbose, + config.KagentVersion, + ) + + if err != nil { + return fmt.Errorf("failed to generate project: %w", err) + } + + return nil +} + +// ListFrameworks returns all supported frameworks +func ListFrameworks() []string { + return common.SupportedFrameworks +} diff --git a/go/cli/internal/cli/agent/deploy.go b/go/cli/internal/cli/agent/deploy.go index 2544a156f..a26d7306a 100644 --- a/go/cli/internal/cli/agent/deploy.go +++ b/go/cli/internal/cli/agent/deploy.go @@ -68,6 +68,11 @@ type DeployCfg struct { DryRun bool } +// sanitizeResourceName converts underscores to hyphens for Kubernetes resource names +func sanitizeResourceName(name string) string { + return strings.ReplaceAll(name, "_", "-") +} + // DeployCmd deploys an agent to Kubernetes func DeployCmd(ctx context.Context, cfg *DeployCfg) error { // Step 1: Validate and load project @@ -211,14 +216,6 @@ func extractEnvVarsFromManifest(manifest *common.AgentManifest) []string { // Extract from MCP servers for _, mcpServer := range manifest.McpServers { - if mcpServer.URL != "" { - matches := envVarRegex.FindAllStringSubmatch(mcpServer.URL, -1) - for _, match := range matches { - varName := extractEnvVarName(match) - envVarSet[varName] = true - } - } - // Check headers for _, headerValue := range mcpServer.Headers { matches := envVarRegex.FindAllStringSubmatch(headerValue, -1) @@ -340,7 +337,7 @@ func handleEnvFileSecret(ctx context.Context, k8sClient client.Client, cfg *Depl }, nil } -// parseEnvFile reads and parses a .env file, returning a map of environment variables +// parseEnvFile reads and parses a .env file, returning a map of environment variable func parseEnvFile(filePath string) (map[string]string, error) { file, err := os.Open(filePath) if err != nil { @@ -693,7 +690,7 @@ func buildAgentCRD(namespace string, manifest *common.AgentManifest, imageName s agent := &v1alpha2.Agent{ ObjectMeta: metav1.ObjectMeta{ - Name: manifest.Name, + Name: sanitizeResourceName(manifest.Name), Namespace: namespace, }, Spec: v1alpha2.AgentSpec{ diff --git a/go/cli/internal/cli/agent/init.go b/go/cli/internal/cli/agent/init.go index d9998e0d8..9a4e8db01 100644 --- a/go/cli/internal/cli/agent/init.go +++ b/go/cli/internal/cli/agent/init.go @@ -7,7 +7,11 @@ import ( "strings" "github.com/kagent-dev/kagent/go/api/v1alpha2" - "github.com/kagent-dev/kagent/go/cli/internal/agent/frameworks" + "github.com/kagent-dev/kagent/go/cli/internal/agent" + "github.com/kagent-dev/kagent/go/cli/internal/agent/frameworks/adk/python" + "github.com/kagent-dev/kagent/go/cli/internal/agent/frameworks/common" + crewai "github.com/kagent-dev/kagent/go/cli/internal/agent/frameworks/crewai/python" + langgraph "github.com/kagent-dev/kagent/go/cli/internal/agent/frameworks/langgraph/python" "github.com/kagent-dev/kagent/go/cli/internal/config" "github.com/kagent-dev/kagent/go/internal/version" ) @@ -24,17 +28,29 @@ type InitCfg struct { } 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) + // Validate agent name + if cfg.AgentName == "" { + return fmt.Errorf("agent name is required") } - if cfg.Language != "python" { - return fmt.Errorf("unsupported language: %s. Only 'python' is supported for ADK", cfg.Language) + // Normalize framework name + framework := strings.ToLower(cfg.Framework) + if framework == "" { + framework = "adk" // Default to ADK } - if cfg.ModelName != "" && cfg.ModelProvider == "" { - return fmt.Errorf("model provider is required when model name is provided") + // Validate framework + validator := common.NewFrameworkValidator() + if err := validator.Validate(framework); err != nil { + return fmt.Errorf("invalid framework: %w", err) + } + + language := strings.ToLower(cfg.Language) + if language == "" { + language = "python" // Default to Python + } + if language != "python" { + return fmt.Errorf("unsupported language: %s. Only 'python' is supported for now", language) } // Validate model provider if specified @@ -47,6 +63,16 @@ func InitCmd(cfg *InitCfg) error { // use lower case for model provider since the templates expect the model provider in lower case cfg.ModelProvider = strings.ToLower(cfg.ModelProvider) + // Set default model provider if not specified + if cfg.ModelProvider == "" { + cfg.ModelProvider = strings.ToLower(agent.DefaultModelProvider) + } + + // Set default model name if not specified + if cfg.ModelName == "" { + cfg.ModelName = agent.DefaultModelName + } + // Get current working directory for project creation cwd, err := os.Getwd() if err != nil { @@ -59,10 +85,28 @@ func InitCmd(cfg *InitCfg) error { return fmt.Errorf("failed to create project directory: %v", err) } - // Initialize the framework generator - generator, err := frameworks.NewGenerator(cfg.Framework, cfg.Language) + // Create generator registry and register all generators + registry := common.NewGeneratorRegistry() + + // Register ADK generator + if err := registry.Register(python.NewADKGenerator()); err != nil { + return fmt.Errorf("failed to register ADK generator: %w", err) + } + + // Register CrewAI generator + if err := registry.Register(crewai.NewCrewAIGenerator()); err != nil { + return fmt.Errorf("failed to register CrewAI generator: %w", err) + } + + // Register LangGraph generator + if err := registry.Register(langgraph.NewLangGraphGenerator()); err != nil { + return fmt.Errorf("failed to register LangGraph generator: %w", err) + } + + // Get the appropriate generator + generator, err := registry.Get(framework) if err != nil { - return fmt.Errorf("failed to create generator: %v", err) + return fmt.Errorf("failed to get generator for framework '%s': %w", framework, err) } // Load instruction from file if specified @@ -78,6 +122,12 @@ func InitCmd(cfg *InitCfg) error { // Get the kagent version kagentVersion := version.Version + if cfg.Config.Verbose { + fmt.Printf("🚀 Initializing %s agent with %s framework (language: %s)\n", cfg.AgentName, framework, language) + fmt.Printf(" Output directory: %s\n", projectDir) + fmt.Printf(" Model: %s/%s\n", cfg.ModelProvider, cfg.ModelName) + } + // 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) @@ -91,9 +141,10 @@ func validateModelProvider(provider string) error { switch v1alpha2.ModelProvider(provider) { case v1alpha2.ModelProviderOpenAI, v1alpha2.ModelProviderAnthropic, - v1alpha2.ModelProviderGemini: + v1alpha2.ModelProviderGemini, + v1alpha2.ModelProviderAzureOpenAI: return nil default: - return fmt.Errorf("unsupported model provider: %s. Supported providers: OpenAI, Anthropic, Gemini", provider) + return fmt.Errorf("unsupported model provider: %s. Supported providers: OpenAI, Anthropic, Gemini, azureopenai", provider) } } diff --git a/go/cli/internal/cli/agent/utils.go b/go/cli/internal/cli/agent/utils.go index f21c84e11..af00def45 100644 --- a/go/cli/internal/cli/agent/utils.go +++ b/go/cli/internal/cli/agent/utils.go @@ -12,7 +12,7 @@ import ( "time" "github.com/kagent-dev/kagent/go/api/v1alpha2" - pygen "github.com/kagent-dev/kagent/go/cli/internal/agent/frameworks/adk/python" + adkgen "github.com/kagent-dev/kagent/go/cli/internal/agent/frameworks/adk/python" "github.com/kagent-dev/kagent/go/cli/internal/agent/frameworks/common" "github.com/kagent-dev/kagent/go/cli/internal/config" "github.com/kagent-dev/kagent/go/pkg/client" @@ -163,13 +163,13 @@ func IsVerbose(cfg *config.Config) bool { // ReadTemplateFile reads a template file from the embedded filesystem func ReadTemplateFile(templatePath string) ([]byte, error) { - gen := pygen.NewPythonGenerator() + gen := adkgen.NewADKGenerator() return fs.ReadFile(gen.TemplateFiles, templatePath) } // RenderTemplate reads and renders a template file with the given data func RenderTemplate(templatePath string, data any) (string, error) { - gen := pygen.NewPythonGenerator() + gen := adkgen.NewADKGenerator() tmplBytes, err := fs.ReadFile(gen.TemplateFiles, templatePath) if err != nil { return "", fmt.Errorf("failed to read template: %w", err) diff --git a/go/cli/internal/common/generator/base.go b/go/cli/internal/common/generator/base.go index f1d05c24b..1593c7bc3 100644 --- a/go/cli/internal/common/generator/base.go +++ b/go/cli/internal/common/generator/base.go @@ -9,6 +9,7 @@ import ( "path/filepath" "strings" "text/template" + "unicode" ) // ProjectConfig defines the interface that project configuration must implement. @@ -126,7 +127,13 @@ func (g *BaseGenerator) GenerateProject(config ProjectConfig) error { // RenderTemplate renders a template string with the provided data. // This is the core template rendering logic used by all generators. func (g *BaseGenerator) RenderTemplate(tmplContent string, data any) (string, error) { - tmpl, err := template.New("template").Parse(tmplContent) + funcMap := template.FuncMap{ + "upper": strings.ToUpper, + "ToUpper": strings.ToUpper, + "ToPascalCase": ToPascalCase, + "ToModuleName": ToModuleName, + } + tmpl, err := template.New("template").Funcs(funcMap).Parse(tmplContent) if err != nil { return "", fmt.Errorf("failed to parse template: %w", err) } @@ -161,3 +168,34 @@ func (g *BaseGenerator) ReadTemplateFile(templatePath string) ([]byte, error) { fullPath := filepath.Join(g.TemplateRoot, templatePath) return fs.ReadFile(g.TemplateFiles, fullPath) } + +// ToPascalCase converts a string to PascalCase (e.g., "hello-world" -> "HelloWorld") +// Handles hyphens, underscores, and spaces as word separators +func ToPascalCase(s string) string { + words := strings.FieldsFunc(s, func(r rune) bool { + return r == '-' || r == '_' || r == ' ' + }) + + for i, word := range words { + if len(word) > 0 { + // Capitalize first letter, lowercase rest + runes := []rune(word) + runes[0] = unicode.ToUpper(runes[0]) + for j := 1; j < len(runes); j++ { + runes[j] = unicode.ToLower(runes[j]) + } + words[i] = string(runes) + } + } + + return strings.Join(words, "") +} + +// ToModuleName converts a string to a valid Python module name (e.g., "hello-world" -> "hello_world") +// Replaces hyphens and spaces with underscores +func ToModuleName(s string) string { + // Replace hyphens and spaces with underscores + s = strings.ReplaceAll(s, "-", "_") + s = strings.ReplaceAll(s, " ", "_") + return s +} diff --git a/python/packages/kagent-adk/pyproject.toml b/python/packages/kagent-adk/pyproject.toml index 8862a0dea..51adf394f 100644 --- a/python/packages/kagent-adk/pyproject.toml +++ b/python/packages/kagent-adk/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "pydantic>=2.5.0", "typing-extensions>=4.8.0", "jsonref>=1.1.0", - "a2a-sdk>=0.3.1", + "a2a-sdk>=0.3.8", ] [tool.uv.sources] diff --git a/python/uv.lock b/python/uv.lock index be4e510b6..7d5983b69 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -1861,7 +1861,7 @@ test = [ [package.metadata] requires-dist = [ - { name = "a2a-sdk", specifier = ">=0.3.1" }, + { name = "a2a-sdk", specifier = ">=0.3.8" }, { name = "agentsts-adk", specifier = ">=0.0.6" }, { name = "agentsts-core", specifier = ">=0.0.6" }, { name = "aiofiles", specifier = ">=24.1.0" },