diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index 7de32ade10d..6147ab32006 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -195,18 +195,134 @@ func newInitCommand(rootFlags *rootFlagsDefinition) *cobra.Command { return err } } else { - action := &InitFromCodeAction{ - azdClient: azdClient, - flags: flags, - httpClient: httpClient, - } - - if err := action.Run(ctx); err != nil { + // No manifest provided - prompt user for init mode + initMode, err := promptInitMode(ctx, azdClient) + if err != nil { if exterrors.IsCancellation(err) { return exterrors.Cancelled("initialization was cancelled") } return err } + + switch initMode { + case initModeTemplate: + // User chose to start from a template - select one + selectedTemplate, err := promptAgentTemplate(ctx, azdClient, httpClient) + if err != nil { + if exterrors.IsCancellation(err) { + return exterrors.Cancelled("initialization was cancelled") + } + return err + } + + switch selectedTemplate.EffectiveType() { + case TemplateTypeAzd: + // Full azd template - dispatch azd init -t + initArgs := []string{"init", "-t", selectedTemplate.Source} + if flags.env != "" { + initArgs = append(initArgs, "--environment", flags.env) + } else { + cwd, err := os.Getwd() + if err == nil { + sanitizedDirectoryName := sanitizeAgentName(filepath.Base(cwd)) + initArgs = append(initArgs, "--environment", sanitizedDirectoryName+"-dev") + } + } + + workflow := &azdext.Workflow{ + Name: "init", + Steps: []*azdext.WorkflowStep{ + {Command: &azdext.WorkflowCommand{Args: initArgs}}, + }, + } + + _, err := azdClient.Workflow().Run(ctx, &azdext.RunWorkflowRequest{ + Workflow: workflow, + }) + if err != nil { + if exterrors.IsCancellation(err) { + return exterrors.Cancelled("initialization was cancelled") + } + return exterrors.Dependency( + exterrors.CodeProjectInitFailed, + fmt.Sprintf("failed to initialize project from template: %s", err), + "", + ) + } + + fmt.Printf("\nProject initialized from template: %s\n", selectedTemplate.Title) + + default: + // Agent manifest template - use existing -m flow + flags.manifestPointer = selectedTemplate.Source + + azureContext, projectConfig, environment, err := ensureAzureContext(ctx, flags, azdClient) + if err != nil { + if exterrors.IsCancellation(err) { + return exterrors.Cancelled("initialization was cancelled") + } + return err + } + + credential, err := azidentity.NewAzureDeveloperCLICredential(&azidentity.AzureDeveloperCLICredentialOptions{ + TenantID: azureContext.Scope.TenantId, + AdditionallyAllowedTenants: []string{"*"}, + }) + if err != nil { + return exterrors.Auth( + exterrors.CodeCredentialCreationFailed, + fmt.Sprintf("failed to create Azure credential: %s", err), + "run 'azd auth login' to authenticate", + ) + } + + console := input.NewConsole( + false, // noPrompt + true, // isTerminal + input.Writers{Output: os.Stdout}, + input.ConsoleHandles{ + Stderr: os.Stderr, + Stdin: os.Stdin, + Stdout: os.Stdout, + }, + nil, // formatter + nil, // externalPromptCfg + ) + + action := &InitAction{ + azdClient: azdClient, + azureContext: azureContext, + console: console, + credential: credential, + projectConfig: projectConfig, + environment: environment, + flags: flags, + httpClient: httpClient, + } + + if err := action.Run(ctx); err != nil { + if exterrors.IsCancellation(err) { + return exterrors.Cancelled("initialization was cancelled") + } + return err + } + } + + default: + // initModeFromCode - use existing code in current directory + action := &InitFromCodeAction{ + azdClient: azdClient, + flags: flags, + httpClient: httpClient, + } + + if err := action.Run(ctx); err != nil { + if exterrors.IsCancellation(err) { + return exterrors.Cancelled("initialization was cancelled") + } + return err + } + } } return nil diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_templates.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_templates.go new file mode 100644 index 00000000000..6d63329355a --- /dev/null +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_templates.go @@ -0,0 +1,190 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "azureaiagent/internal/exterrors" + + "github.com/azure/azure-dev/cli/azd/pkg/azdext" +) + +const agentTemplatesURL = "https://aka.ms/foundry-agents" + +// Template type constants +const ( + // TemplateTypeAgent is a template that points to an agent.yaml manifest file. + TemplateTypeAgent = "agent" + + // TemplateTypeAzd is a full azd template repository. + TemplateTypeAzd = "azd" +) + +// AgentTemplate represents an agent template entry from the remote JSON catalog. +type AgentTemplate struct { + Title string `json:"title"` + Description string `json:"description"` + Language string `json:"language"` + Framework string `json:"framework"` + Source string `json:"source"` + Tags []string `json:"tags"` +} + +// EffectiveType determines the template type by inspecting the source URL. +// If it ends with agent.yaml or agent.manifest.yaml, it's an agent manifest. +// Otherwise, it's treated as a full azd template repo. +func (t *AgentTemplate) EffectiveType() string { + lower := strings.ToLower(t.Source) + if strings.HasSuffix(lower, "/agent.yaml") || + strings.HasSuffix(lower, "/agent.manifest.yaml") || + lower == "agent.yaml" || + lower == "agent.manifest.yaml" { + return TemplateTypeAgent + } + return TemplateTypeAzd +} + +const ( + initModeFromCode = "from_code" + initModeTemplate = "template" +) + +// promptInitMode asks the user whether to use existing code or start from a template. +// Returns initModeFromCode or initModeTemplate. +func promptInitMode(ctx context.Context, azdClient *azdext.AzdClient) (string, error) { + choices := []*azdext.SelectChoice{ + {Label: "Use the code in the current directory", Value: initModeFromCode}, + {Label: "Start new from a template", Value: initModeTemplate}, + } + + resp, err := azdClient.Prompt().Select(ctx, &azdext.SelectRequest{ + Options: &azdext.SelectOptions{ + Message: "How do you want to initialize your agent?", + Choices: choices, + }, + }) + if err != nil { + if exterrors.IsCancellation(err) { + return "", exterrors.Cancelled("initialization mode selection was cancelled") + } + return "", fmt.Errorf("failed to prompt for initialization mode: %w", err) + } + + return choices[*resp.Value].Value, nil +} + +// fetchAgentTemplates retrieves the agent template catalog from the remote JSON URL. +func fetchAgentTemplates(ctx context.Context, httpClient *http.Client) ([]AgentTemplate, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, agentTemplatesURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + resp, err := httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to fetch agent templates: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to fetch agent templates: HTTP %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read agent templates response: %w", err) + } + + var templates []AgentTemplate + if err := json.Unmarshal(body, &templates); err != nil { + return nil, fmt.Errorf("failed to parse agent templates: %w", err) + } + + return templates, nil +} + +// promptAgentTemplate guides the user through language selection and template selection. +// Returns the selected AgentTemplate. The caller should check EffectiveType() to determine +// whether to use the agent.yaml manifest flow or the full azd template flow. +func promptAgentTemplate( + ctx context.Context, + azdClient *azdext.AzdClient, + httpClient *http.Client, +) (*AgentTemplate, error) { + fmt.Println("Retrieving agent templates...") + + templates, err := fetchAgentTemplates(ctx, httpClient) + if err != nil { + return nil, fmt.Errorf("failed to retrieve agent templates: %w", err) + } + + if len(templates) == 0 { + return nil, fmt.Errorf("no agent templates available") + } + + // Prompt for language + languageChoices := []*azdext.SelectChoice{ + {Label: "Python", Value: "python"}, + {Label: "C#", Value: "csharp"}, + } + + langResp, err := azdClient.Prompt().Select(ctx, &azdext.SelectRequest{ + Options: &azdext.SelectOptions{ + Message: "Select a language:", + Choices: languageChoices, + }, + }) + if err != nil { + if exterrors.IsCancellation(err) { + return nil, exterrors.Cancelled("language selection was cancelled") + } + return nil, fmt.Errorf("failed to prompt for language: %w", err) + } + + selectedLanguage := languageChoices[*langResp.Value].Value + + // Filter templates by selected language + var filtered []AgentTemplate + for _, t := range templates { + if t.Language == selectedLanguage { + filtered = append(filtered, t) + } + } + + if len(filtered) == 0 { + return nil, fmt.Errorf("no agent templates available for %s", languageChoices[*langResp.Value].Label) + } + + // Build template choices with framework in label + templateChoices := make([]*azdext.SelectChoice, len(filtered)) + for i, t := range filtered { + label := fmt.Sprintf("%s (%s)", t.Title, t.Framework) + templateChoices[i] = &azdext.SelectChoice{ + Label: label, + Value: fmt.Sprintf("%d", i), + } + } + + templateResp, err := azdClient.Prompt().Select(ctx, &azdext.SelectRequest{ + Options: &azdext.SelectOptions{ + Message: "Select an agent template:", + Choices: templateChoices, + }, + }) + if err != nil { + if exterrors.IsCancellation(err) { + return nil, exterrors.Cancelled("template selection was cancelled") + } + return nil, fmt.Errorf("failed to prompt for template: %w", err) + } + + selectedTemplate := filtered[*templateResp.Value] + return &selectedTemplate, nil +}