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
93 changes: 90 additions & 3 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ type initFlags struct {
src string
env string
protocols []string
// deploy mode flags for non-interactive code deploy support
deployMode string // "container" or "code"; empty = prompt interactively
runtime string // e.g. "python_3_13", "python_3_14", "dotnet_10"
entryPoint string // e.g. "app.py", "MyAgent.dll"
depResolution string // "remote_build" or "bundled"; defaults to "remote_build"
// force, when true, lets headless callers (--no-prompt) pre-consent to
// overwrite prompts that would otherwise return a structured error. It
// mirrors the `--force` convention used by `azd down`, `azd env remove`,
Expand Down Expand Up @@ -613,7 +618,11 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`,
azd ai agent init -m ./agent.manifest.yaml --agent-name my-unique-agent

# Initialize from local agent code
azd ai agent init --src ./src/my-agent --agent-name my-unique-agent`,
azd ai agent init --src ./src/my-agent --agent-name my-unique-agent

# Non-interactive code deploy (CI/CD)
azd ai agent init --no-prompt --project-id "<resource-id>" \
--deploy-mode code --runtime python_3_13 --entry-point app.py`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
flags.noPrompt = extCtx.NoPrompt
Expand Down Expand Up @@ -852,6 +861,18 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`,
cmd.Flags().StringSliceVar(&flags.protocols, "protocol", nil,
"Protocols supported by the agent (e.g., 'responses', 'invocations'). Can be specified multiple times.")

cmd.Flags().StringVar(&flags.deployMode, "deploy-mode", "",
"Deployment mode: 'container' (Docker image) or 'code' (ZIP upload). Defaults to 'container' in --no-prompt.")

cmd.Flags().StringVar(&flags.runtime, "runtime", "",
"Runtime for code deploy (e.g., 'python_3_13', 'python_3_14', 'dotnet_10'). Required with --deploy-mode code --no-prompt.")

cmd.Flags().StringVar(&flags.entryPoint, "entry-point", "",
"Entry point file for code deploy (e.g., 'app.py', 'MyAgent.dll'). Required with --deploy-mode code --no-prompt.")

cmd.Flags().StringVar(&flags.depResolution, "dep-resolution", "",
"Dependency resolution for code deploy: 'remote_build' or 'bundled'. Defaults to 'remote_build'.")
Comment thread
v1212 marked this conversation as resolved.

cmd.Flags().BoolVar(&flags.force, "force", false,
"Overwrite an input manifest that already lives inside the generated src tree without prompting. "+
"Required together with --no-prompt when init would otherwise need confirmation.")
Expand All @@ -875,6 +896,11 @@ func (a *InitAction) Run(ctx context.Context) error {
a.flags.src = relPath
}

// Validate code deploy flags
if err := a.validateCodeDeployFlags(); err != nil {
return err
}

// If --manifest is given
if a.flags.manifestPointer != "" {
// Validate that the manifest pointer is either a valid URL or existing file path
Expand Down Expand Up @@ -910,15 +936,19 @@ func (a *InitAction) Run(ctx context.Context) error {
// Code deploy is supported for Python and .NET projects.
if _, ok := agentManifest.Template.(agent_yaml.ContainerAgent); ok {
showCodeDeploy := isPythonProject(targetDir) || isDotnetProject(targetDir)
deployMode, err := promptDeployMode(ctx, a.azdClient, a.flags.noPrompt, showCodeDeploy)
deployMode, err := promptDeployMode(ctx, a.azdClient, a.flags.noPrompt, showCodeDeploy, a.flags.deployMode)
if err != nil {
return fmt.Errorf("prompting for deploy mode: %w", err)
}
a.isCodeDeploy = (deployMode == "code")

if a.isCodeDeploy {
// Prompt for code configuration and update the manifest
codeConfig, err := promptCodeConfig(ctx, a.azdClient, targetDir, a.flags.noPrompt)
codeConfig, err := promptCodeConfig(ctx, a.azdClient, targetDir, a.flags.noPrompt, codeDeployOptions{
runtime: a.flags.runtime,
entryPoint: a.flags.entryPoint,
depResolution: a.flags.depResolution,
})
if err != nil {
return fmt.Errorf("prompting for code configuration: %w", err)
}
Expand Down Expand Up @@ -2947,3 +2977,60 @@ func extractConnectionConfigs(

return connections, credentialEnvVars, nil
}

// validateCodeDeployFlags checks that required flags are present when using
// --deploy-mode code in --no-prompt mode.
func (a *InitAction) validateCodeDeployFlags() error {
return validateCodeDeployInput(
a.flags.noPrompt, a.flags.deployMode, a.flags.runtime, a.flags.entryPoint, a.flags.depResolution)
}

// validateCodeDeployInput is the shared validation logic for code deploy flags.
// Used by both InitAction and InitFromCodeAction.
func validateCodeDeployInput(noPrompt bool, deployMode, runtime, entryPoint, depResolution string) error {
if deployMode != "" && deployMode != "container" && deployMode != "code" {
return exterrors.Validation(
exterrors.CodeInvalidParameter,
"--deploy-mode must be 'container' or 'code'",
"Specify --deploy-mode container or --deploy-mode code",
)
}
if runtime != "" {
validRuntimes := map[string]bool{
"python_3_13": true,
"python_3_14": true,
"dotnet_10": true,
}
if !validRuntimes[runtime] {
return exterrors.Validation(
exterrors.CodeInvalidParameter,
"--runtime must be one of: python_3_13, python_3_14, dotnet_10",
"Specify a valid runtime value",
)
}
}
if depResolution != "" && depResolution != "remote_build" && depResolution != "bundled" {
return exterrors.Validation(
exterrors.CodeInvalidParameter,
"--dep-resolution must be 'remote_build' or 'bundled'",
"Specify --dep-resolution remote_build or --dep-resolution bundled",
)
}
if noPrompt && deployMode == "code" {
if runtime == "" {
return exterrors.Validation(
exterrors.CodeInvalidParameter,
"--runtime is required when using --deploy-mode code with --no-prompt",
"Specify --runtime (e.g., python_3_13, python_3_14, dotnet_10)",
)
}
if entryPoint == "" {
return exterrors.Validation(
exterrors.CodeInvalidParameter,
"--entry-point is required when using --deploy-mode code with --no-prompt",
"Specify --entry-point (e.g., app.py, main.py, MyAgent.dll)",
)
}
}
return nil
}
67 changes: 53 additions & 14 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ func (a *InitFromCodeAction) Run(ctx context.Context) error {
a.flags.src = relPath
}

// Validate code deploy flags
if err := validateCodeDeployInput(
a.flags.noPrompt, a.flags.deployMode, a.flags.runtime, a.flags.entryPoint, a.flags.depResolution,
); err != nil {
return err
}

// Default src to current directory when not specified
srcDir := a.flags.src
if srcDir == "" {
Expand Down Expand Up @@ -487,7 +494,7 @@ func (a *InitFromCodeAction) createDefinitionFromLocalAgent(ctx context.Context)
srcDir, _ = os.Getwd()
}
showCodeDeploy := isPythonProject(srcDir) || isDotnetProject(srcDir)
deployMode, err := promptDeployMode(ctx, a.azdClient, a.flags.noPrompt, showCodeDeploy)
deployMode, err := promptDeployMode(ctx, a.azdClient, a.flags.noPrompt, showCodeDeploy, a.flags.deployMode)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -1008,7 +1015,11 @@ func (a *InitFromCodeAction) addToProject(ctx context.Context, targetDir string,

// promptCodeConfiguration prompts the user for code deploy configuration settings.
func (a *InitFromCodeAction) promptCodeConfiguration(ctx context.Context, srcDir string) (*agent_yaml.CodeConfiguration, error) {
return promptCodeConfig(ctx, a.azdClient, srcDir, a.flags.noPrompt)
return promptCodeConfig(ctx, a.azdClient, srcDir, a.flags.noPrompt, codeDeployOptions{
runtime: a.flags.runtime,
entryPoint: a.flags.entryPoint,
depResolution: a.flags.depResolution,
})
}

// protocolInfo pairs a protocol name with the default version used when generating agent.yaml.
Expand Down Expand Up @@ -1135,22 +1146,37 @@ func knownProtocolNames() string {
}

// promptDeployMode asks the user to choose between code deploy and container deploy.
// When noPrompt is true, defaults to "container" for backward compatibility.
// When showCodeDeploy is false, code deploy is not offered (e.g. for non-Python languages).
func promptDeployMode(ctx context.Context, azdClient *azdext.AzdClient, noPrompt bool, showCodeDeploy bool) (string, error) {
// When deployModeFlag is set, it is used directly (for --no-prompt with explicit flag).
// When noPrompt is true and no flag is provided, defaults to "container" for backward compatibility.
// When showCodeDeploy is false and no explicit flag overrides, code deploy is not offered.
func promptDeployMode(ctx context.Context, azdClient *azdext.AzdClient, noPrompt bool, showCodeDeploy bool, deployModeFlag string) (string, error) {
// Explicit flag takes precedence
if deployModeFlag != "" {
switch deployModeFlag {
case "container", "code":
return deployModeFlag, nil
default:
return "", exterrors.Validation(
exterrors.CodeInvalidParameter,
fmt.Sprintf("invalid --deploy-mode value %q; must be 'container' or 'code'", deployModeFlag),
"Use --deploy-mode container or --deploy-mode code",
)
}
}

if !showCodeDeploy {
return "container", nil
}

if noPrompt {
return "container", nil
}

deployModeChoices := []*azdext.SelectChoice{
{Label: "Container Image (Docker)", Value: "container"},
{Label: "Source Code (ZIP upload)", Value: "code"},
}

if noPrompt {
return "container", nil
}

defaultIdx := int32(0) // Container is the default for backward compatibility
deployModeResp, err := azdClient.Prompt().Select(ctx, &azdext.SelectRequest{
Options: &azdext.SelectOptions{
Expand Down Expand Up @@ -1221,9 +1247,16 @@ func extractAssemblyName(csprojContent string) string {
return name
}

// codeDeployOptions holds optional flag overrides for code deploy configuration.
type codeDeployOptions struct {
runtime string
entryPoint string
depResolution string
}

// promptCodeConfig prompts for code deploy configuration (runtime, entry point,
// dependency resolution). When noPrompt is true, defaults are used without prompting.
func promptCodeConfig(ctx context.Context, azdClient *azdext.AzdClient, srcDir string, noPrompt bool) (*agent_yaml.CodeConfiguration, error) {
// dependency resolution). When noPrompt is true, flags or defaults are used without prompting.
func promptCodeConfig(ctx context.Context, azdClient *azdext.AzdClient, srcDir string, noPrompt bool, opts codeDeployOptions) (*agent_yaml.CodeConfiguration, error) {
if srcDir == "" {
srcDir = "."
}
Expand Down Expand Up @@ -1252,7 +1285,9 @@ func promptCodeConfig(ctx context.Context, azdClient *azdext.AzdClient, srcDir s
}

var runtime string
if noPrompt {
if opts.runtime != "" {
runtime = opts.runtime
} else if noPrompt {
if isDotnet && !isPython {
runtime = "dotnet_10"
} else {
Expand Down Expand Up @@ -1280,7 +1315,9 @@ func promptCodeConfig(ctx context.Context, azdClient *azdext.AzdClient, srcDir s
defaultEntryPoint := detectDefaultEntryPoint(srcDir, runtime)

var entryPoint string
if noPrompt {
if opts.entryPoint != "" {
entryPoint = opts.entryPoint
} else if noPrompt {
entryPoint = defaultEntryPoint
} else {
entryPointResp, err := azdClient.Prompt().Prompt(ctx, &azdext.PromptRequest{
Expand All @@ -1305,7 +1342,9 @@ func promptCodeConfig(ctx context.Context, azdClient *azdext.AzdClient, srcDir s
}

var depResolution string
if noPrompt {
if opts.depResolution != "" {
depResolution = opts.depResolution
} else if noPrompt {
depResolution = "remote_build"
} else {
Comment thread
v1212 marked this conversation as resolved.
depDefaultIdx := int32(0)
Expand Down
Loading
Loading