Skip to content
Merged
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
40 changes: 40 additions & 0 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,46 @@ func newInitCommand(rootFlags *rootFlagsDefinition) *cobra.Command {
Timeout: 30 * time.Second,
}

// Auto-detect an existing agent manifest in the target directory
// when no --manifest flag was provided.
if flags.manifestPointer == "" {
checkDir := flags.src
if checkDir == "" {
checkDir = "."
}
detected, detectErr := detectLocalManifest(checkDir)
if detectErr != nil {
return fmt.Errorf("checking for existing manifest: %w", detectErr)
}
if detected != "" {
useExisting := flags.NoPrompt
if !flags.NoPrompt {
confirmResp, promptErr := azdClient.Prompt().Confirm(ctx, &azdext.ConfirmRequest{
Options: &azdext.ConfirmOptions{
Message: fmt.Sprintf(
"An existing agent manifest was found at %q. Use it?",
detected,
),
DefaultValue: new(true),
},
})
if promptErr != nil {
if exterrors.IsCancellation(promptErr) {
return exterrors.Cancelled("initialization was cancelled")
}
return fmt.Errorf("prompting for manifest detection: %w", promptErr)
}
useExisting = *confirmResp.Value
}
if useExisting {
flags.manifestPointer = detected
if flags.src == "" {
flags.src = checkDir
}
}
}
}

if flags.manifestPointer != "" {
if err := runInitFromManifest(ctx, flags, azdClient, httpClient); err != nil {
if exterrors.IsCancellation(err) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,37 @@ func (a *InitFromCodeAction) Run(ctx context.Context) error {
a.flags.src = relPath
}

// Default src to current directory when not specified
srcDir := a.flags.src
if srcDir == "" {
srcDir = "."
}

// Check if agent.yaml already exists before the interactive setup so the user
// doesn't complete the full agent configuration only to have it discarded.
agentYamlPath := filepath.Join(srcDir, "agent.yaml")
if _, statErr := os.Stat(agentYamlPath); statErr == nil {
if a.flags.NoPrompt {
return exterrors.Cancelled("agent.yaml already exists; overwrite declined in no-prompt mode")
}

confirmResp, err := a.azdClient.Prompt().Confirm(ctx, &azdext.ConfirmRequest{
Options: &azdext.ConfirmOptions{
Message: fmt.Sprintf("An agent.yaml already exists in %q. Overwrite?", srcDir),
DefaultValue: new(false),
},
})
if err != nil {
if exterrors.IsCancellation(err) {
return exterrors.Cancelled("overwrite confirmation was cancelled")
}
return fmt.Errorf("prompting for overwrite confirmation: %w", err)
}
if !*confirmResp.Value {
return exterrors.Cancelled("agent.yaml already exists; overwrite declined")
}
}

// No manifest pointer provided - process local agent code
// Create a definition based on user prompts
localDefinition, err := a.createDefinitionFromLocalAgent(ctx)
Expand All @@ -79,11 +110,6 @@ func (a *InitFromCodeAction) Run(ctx context.Context) error {
}

if localDefinition != nil {
// Default src to current directory when not specified
srcDir := a.flags.src
if srcDir == "" {
srcDir = "."
}

// Write the definition to a file in the src directory
_, err := a.writeDefinitionToSrcDir(localDefinition, srcDir)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package cmd
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
Expand All @@ -16,6 +17,7 @@ import (
"strings"

"azureaiagent/internal/exterrors"
"azureaiagent/internal/pkg/agents/agent_yaml"

"github.com/azure/azure-dev/cli/azd/pkg/azdext"
"github.com/fatih/color"
Expand Down Expand Up @@ -260,3 +262,44 @@ func findAgentManifest(dir string) (string, error) {

return found, nil
}

// detectLocalManifest checks only the immediate directory for an agent manifest file.
// Returns the path to the found manifest (preferring agent.manifest.yaml over agent.yaml,
// then .yml variants), or an empty string if none contain valid manifest content.
// Returns a non-nil error for unexpected I/O failures (e.g. permission errors).
func detectLocalManifest(dir string) (string, error) {
candidates := []string{
"agent.manifest.yaml",
"agent.yaml",
"agent.manifest.yml",
"agent.yml",
}

for _, name := range candidates {
candidate := filepath.Join(dir, name)
_, err := os.Stat(candidate)
if errors.Is(err, os.ErrNotExist) {
continue
}
if err != nil {
return "", fmt.Errorf("checking for manifest %s: %w", candidate, err)
}
if isValidManifestFile(candidate) {
return candidate, nil
}
}
return "", nil
}

// isValidManifestFile reads the file and checks whether it can be loaded as
// a valid AgentManifest via LoadAndValidateAgentManifest.
func isValidManifestFile(path string) bool {
//nolint:gosec // path comes from a known filename in a user-controlled directory
content, err := os.ReadFile(path)
if err != nil {
return false
}

_, err = agent_yaml.LoadAndValidateAgentManifest(content)
return err == nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -298,3 +298,141 @@ func TestDirIsEmpty(t *testing.T) {
require.False(t, empty)
})
}

func TestDetectLocalManifest(t *testing.T) {
t.Parallel()

// Valid agent manifest content (has template with kind + name)
validManifest := `name: test-agent
template:
kind: hosted
name: test-agent
protocols:
- protocol: responses
version: v1
`

t.Run("no manifest files", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "main.py"), []byte("print()"), 0600))

result, err := detectLocalManifest(dir)
require.NoError(t, err)
require.Empty(t, result)
})

t.Run("valid agent.yaml", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "agent.yaml"), []byte(validManifest), 0600))

result, err := detectLocalManifest(dir)
require.NoError(t, err)
require.Equal(t, filepath.Join(dir, "agent.yaml"), result)
})

t.Run("valid agent.manifest.yaml", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "agent.manifest.yaml"), []byte(validManifest), 0600))

result, err := detectLocalManifest(dir)
require.NoError(t, err)
require.Equal(t, filepath.Join(dir, "agent.manifest.yaml"), result)
})

t.Run("both files prefers agent.manifest.yaml", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "agent.yaml"), []byte(validManifest), 0600))
require.NoError(t, os.WriteFile(filepath.Join(dir, "agent.manifest.yaml"), []byte(validManifest), 0600))

result, err := detectLocalManifest(dir)
require.NoError(t, err)
require.Equal(t, filepath.Join(dir, "agent.manifest.yaml"), result)
})

t.Run("does not search subdirectories", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
subDir := filepath.Join(dir, "src")
require.NoError(t, os.MkdirAll(subDir, 0700))
require.NoError(t, os.WriteFile(filepath.Join(subDir, "agent.yaml"), []byte(validManifest), 0600))

result, err := detectLocalManifest(dir)
require.NoError(t, err)
require.Empty(t, result)
})

t.Run("empty directory", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()

result, err := detectLocalManifest(dir)
require.NoError(t, err)
require.Empty(t, result)
})

t.Run("invalid YAML content is skipped", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "agent.yaml"), []byte("not: valid: yaml: ["), 0600))

result, err := detectLocalManifest(dir)
require.NoError(t, err)
require.Empty(t, result)
})

t.Run("YAML without template is skipped", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "agent.yaml"), []byte("foo: bar\n"), 0600))

result, err := detectLocalManifest(dir)
require.NoError(t, err)
require.Empty(t, result)
})

t.Run("falls back to agent.yaml when manifest.yaml is invalid", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "agent.manifest.yaml"), []byte("foo: bar\n"), 0600))
require.NoError(t, os.WriteFile(filepath.Join(dir, "agent.yaml"), []byte(validManifest), 0600))

result, err := detectLocalManifest(dir)
require.NoError(t, err)
require.Equal(t, filepath.Join(dir, "agent.yaml"), result)
})

t.Run("detects agent.yml variant", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "agent.yml"), []byte(validManifest), 0600))

result, err := detectLocalManifest(dir)
require.NoError(t, err)
require.Equal(t, filepath.Join(dir, "agent.yml"), result)
})

t.Run("detects agent.manifest.yml variant", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "agent.manifest.yml"), []byte(validManifest), 0600))

result, err := detectLocalManifest(dir)
require.NoError(t, err)
require.Equal(t, filepath.Join(dir, "agent.manifest.yml"), result)
})

t.Run("prefers yaml over yml", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "agent.yaml"), []byte(validManifest), 0600))
require.NoError(t, os.WriteFile(filepath.Join(dir, "agent.yml"), []byte(validManifest), 0600))

result, err := detectLocalManifest(dir)
require.NoError(t, err)
require.Equal(t, filepath.Join(dir, "agent.yaml"), result)
})
}
Loading