Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ go build -o agk main.go
# Initialize a new project with the quickstart template
./agk init my-agent --template quickstart --llm openai

# ...or let the interactive wizard guide you through template + provider
./agk init -i

# Navigate to the project
cd my-agent

Expand Down Expand Up @@ -214,6 +217,7 @@ agk trace mermaid > trace_flow.md
- **Smart Scaffolding** (Quickstart, Workflow bases)
- **Eval Framework** (Semantic matching, LLM-as-judge, professional reports)
- **Trace System** (Interactive TUI, Mermaid export, detailed spans)
- **Interactive Init Wizard** (`agk init -i` — guided template & provider setup)
- **Streaming Support** (Native across all templates)

### In Progress
Expand All @@ -224,7 +228,6 @@ agk trace mermaid > trace_flow.md
- **Template Distribution** (`pack`, `push`)
- **Cloud Deployment Engine** (`agk deploy`)
- **Workflow Visualization** (Interactive graph editor)
- **Interactive Init Wizard** (`agk init -i`)
- **MCP Server Management**
- **RAG & Knowledge Base Management**

Expand Down
24 changes: 20 additions & 4 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,10 @@ Examples:
# List available templates
agk init --list`,
Args: func(cmd *cobra.Command, args []string) error {
// Allow zero args only when listing templates
if initListTemplates {
return nil
// Allow zero args when listing templates or in interactive mode
// (the wizard prompts for the project name).
if initListTemplates || initInteractive {
return cobra.MaximumNArgs(1)(cmd, args)
}
return cobra.ExactArgs(1)(cmd, args)
},
Expand All @@ -94,7 +95,22 @@ func runInitCommand(cmd *cobra.Command, args []string) error {
return nil
}

projectName := args[0]
projectName := ""
if len(args) > 0 {
projectName = args[0]
}

// Interactive mode: walk the user through setup and fill in any unset options.
if initInteractive {
res := runInitWizard(os.Stdin, os.Stdout, projectName)
projectName = res.ProjectName
initTemplate = res.Template
initLLMProvider = res.LLMProvider
if res.Description != "" {
initDescription = res.Description
}
}

span.SetAttributes(
attribute.String("project_name", projectName),
attribute.String("template", initTemplate),
Expand Down
91 changes: 91 additions & 0 deletions cmd/init_wizard.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package cmd

import (
"bufio"
"fmt"
"io"
"strconv"
"strings"
)

// InitWizardResult holds the answers collected by the interactive init wizard.
type InitWizardResult struct {
ProjectName string
Template string
LLMProvider string
Description string
}

// prompter reads typed answers from a reader and writes prompts to a writer.
// It is decoupled from os.Stdin/os.Stdout so the wizard can be unit-tested.
type prompter struct {
reader *bufio.Reader
out io.Writer
}

func newPrompter(in io.Reader, out io.Writer) *prompter {
return &prompter{reader: bufio.NewReader(in), out: out}
}

// ask prompts for a free-text value, returning def when the user enters nothing
// (or input ends).
func (p *prompter) ask(label, def string) string {
if def != "" {
fmt.Fprintf(p.out, "%s [%s]: ", label, def)
} else {
fmt.Fprintf(p.out, "%s: ", label)
}
line, _ := p.reader.ReadString('\n')
line = strings.TrimSpace(line)
if line == "" {
return def
}
return line
}

// choose prompts the user to pick one of options, by number or name. def must be a
// member of options and is returned on empty input or EOF, so this never loops forever.
func (p *prompter) choose(label string, options []string, def string) string {
fmt.Fprintln(p.out, label)
for i, o := range options {
marker := " "
if o == def {
marker = "*"
}
fmt.Fprintf(p.out, " %s %d) %s\n", marker, i+1, o)
}

for {
ans := p.ask("Choose (number or name)", def)
if n, err := strconv.Atoi(ans); err == nil && n >= 1 && n <= len(options) {
return options[n-1]
}
for _, o := range options {
if strings.EqualFold(o, ans) {
return o
}
}
fmt.Fprintf(p.out, " invalid choice: %q\n", ans)
// On EOF, ask() returns def (a valid option), so the loop terminates.
}
}

// runInitWizard walks the user through project setup, pre-filling the project name
// when one was already provided on the command line.
func runInitWizard(in io.Reader, out io.Writer, name string) InitWizardResult {
fmt.Fprintln(out, "🧙 AGK interactive project setup")
fmt.Fprintln(out)

p := newPrompter(in, out)

res := InitWizardResult{ProjectName: name}
if res.ProjectName == "" {
res.ProjectName = p.ask("Project name", "my-agent")
}
res.Template = p.choose("Template:", []string{"quickstart", "workflow"}, "quickstart")
res.LLMProvider = p.choose("LLM provider:", []string{"openai", "anthropic", "ollama"}, "openai")
res.Description = p.ask("Description (optional)", "")

fmt.Fprintln(out)
return res
}
63 changes: 63 additions & 0 deletions cmd/init_wizard_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package cmd

import (
"bytes"
"strings"
"testing"
)

func TestRunInitWizard(t *testing.T) {
// name pre-filled; choose workflow (by number), ollama (by name), with a description.
in := strings.NewReader("2\nollama\nA test agent\n")
var out bytes.Buffer

res := runInitWizard(in, &out, "my-proj")

if res.ProjectName != "my-proj" {
t.Errorf("ProjectName = %q, want my-proj", res.ProjectName)
}
if res.Template != "workflow" {
t.Errorf("Template = %q, want workflow", res.Template)
}
if res.LLMProvider != "ollama" {
t.Errorf("LLMProvider = %q, want ollama", res.LLMProvider)
}
if res.Description != "A test agent" {
t.Errorf("Description = %q, want 'A test agent'", res.Description)
}
}

func TestRunInitWizardPromptsForName(t *testing.T) {
// No name pre-filled: first answer is the project name; then accept defaults.
in := strings.NewReader("cool-bot\n\n\n\n")
var out bytes.Buffer

res := runInitWizard(in, &out, "")

if res.ProjectName != "cool-bot" {
t.Errorf("ProjectName = %q, want cool-bot", res.ProjectName)
}
// Empty answers should fall back to the defaults.
if res.Template != "quickstart" {
t.Errorf("Template = %q, want quickstart (default)", res.Template)
}
if res.LLMProvider != "openai" {
t.Errorf("LLMProvider = %q, want openai (default)", res.LLMProvider)
}
}

func TestRunInitWizardEOFUsesDefaults(t *testing.T) {
// Empty input (immediate EOF): name default + all defaults, no infinite loop.
res := runInitWizard(strings.NewReader(""), new(bytes.Buffer), "")
if res.ProjectName != "my-agent" || res.Template != "quickstart" || res.LLMProvider != "openai" {
t.Errorf("unexpected defaults: %+v", res)
}
}

func TestPrompterChooseInvalidThenValid(t *testing.T) {
p := newPrompter(strings.NewReader("nope\nworkflow\n"), new(bytes.Buffer))
got := p.choose("Template:", []string{"quickstart", "workflow"}, "quickstart")
if got != "workflow" {
t.Errorf("choose = %q, want workflow", got)
}
}
Loading