cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go (1,559 lines of code) (raw):

// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. package cmd import ( "context" "encoding/json" "fmt" "os" "path/filepath" "slices" "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure" "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/azure/ai" "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/qna" "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.ai.builder/internal/pkg/util" "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/azure/azure-dev/cli/azd/pkg/output" "github.com/azure/azure-dev/cli/azd/pkg/ux" "github.com/spf13/cobra" ) type scenarioInput struct { SelectedScenario string `json:"selectedScenario,omitempty"` UseCustomData bool `json:"useCustomData,omitempty"` DataTypes []string `json:"dataTypes,omitempty"` DataLocations []string `json:"dataLocations,omitempty"` InteractionTypes []string `json:"interactionTypes,omitempty"` LocalFilePath string `json:"localFilePath,omitempty"` LocalFileSelection string `json:"localFileSelection,omitempty"` LocalFileGlobFilter string `json:"localFileGlobFilter,omitempty"` DatabaseType string `json:"databaseType,omitempty"` StorageAccountId string `json:"storageAccountId,omitempty"` DatabaseId string `json:"databaseId,omitempty"` MessagingType string `json:"messagingType,omitempty"` MessagingId string `json:"messagingId,omitempty"` ModelTasks []string `json:"modelTasks,omitempty"` ModelSelections []string `json:"modelSelections,omitempty"` AppHostTypes []string `json:"appHostTypes,omitempty"` AppLanguages []string `json:"appLanguages,omitempty"` AppResourceIds []string `json:"appResourceIds,omitempty"` VectorStoreType string `json:"vectorStoreType,omitempty"` VectorStoreId string `json:"vectorStoreId,omitempty"` } type resourceTypeConfig struct { ResourceType string ResourceTypeDisplayName string Kinds []string } var ( appResourceMap = map[string]resourceTypeConfig{ "host.webapp": { ResourceType: "Microsoft.Web/sites", ResourceTypeDisplayName: "Web App", Kinds: []string{"app"}, }, "host.containerapp": { ResourceType: "Microsoft.App/containerApps", ResourceTypeDisplayName: "Container App", }, "host.functionapp": { ResourceType: "Microsoft.Web/sites", ResourceTypeDisplayName: "Function App", Kinds: []string{"functionapp"}, }, "host.staticwebapp": { ResourceType: "Microsoft.Web/staticSites", ResourceTypeDisplayName: "Static Web App", }, } defaultModelMap = map[string]string{ "chatCompletion": "gpt-4o", "embeddings": "text-embedding-3-small", "imageGenerations": "dall-e-3", "audio": "whisper", } defaultAppLanguageMap = map[string]string{ "rag-ui": "ts", "rag-api": "python", "agent-ui": "ts", "agent-api": "python", "agent-messaging": "python", } appUsesMap = map[string][]string{ "rag-ui": { "host.containerapp", }, "rag-api": { "ai.project", "ai.search", "db.cosmos", "db.postgres", "db.redis", "db.mongo", "db.mysql", "messaging.eventhubs", "messaging.servicebus", "storage", }, } ) func newStartCommand() *cobra.Command { return &cobra.Command{ Use: "start", Short: "Get the context of the AZD project & environment.", RunE: func(cmd *cobra.Command, args []string) error { // Create a new context that includes the AZD access token ctx := azdext.WithAccessToken(cmd.Context()) // Create a new AZD client azdClient, err := azdext.NewAzdClient() if err != nil { return fmt.Errorf("failed to create azd client: %w", err) } defer azdClient.Close() fmt.Println() fmt.Println(output.WithHintFormat("Welcome to the AI Builder!")) fmt.Println("This tool will help you build an AI scenario using Azure services.") fmt.Println() azureContext, projectConfig, err := ensureAzureContext(ctx, azdClient) if err != nil { return fmt.Errorf("failed to ensure azure context: %w", err) } getComposedResourcesResponse, err := azdClient.Compose().ListResources(ctx, &azdext.EmptyRequest{}) if err != nil { return fmt.Errorf("failed to get composed resources: %w", err) } credential, err := azidentity.NewAzureDeveloperCLICredential(&azidentity.AzureDeveloperCLICredentialOptions{ TenantID: azureContext.Scope.TenantId, AdditionallyAllowedTenants: []string{"*"}, }) if err != nil { return fmt.Errorf("failed to create azure credential: %w", err) } action := &startAction{ azdClient: azdClient, credential: credential, azureContext: azureContext, composedResources: getComposedResourcesResponse.Resources, azureClient: azure.NewAzureClient(credential), modelCatalogService: ai.NewModelCatalogService(credential), projectConfig: projectConfig, scenarioData: &scenarioInput{}, } if err := action.Run(ctx, args); err != nil { return fmt.Errorf("failed to run start action: %w", err) } return nil }, } } type startAction struct { credential azcore.TokenCredential azdClient *azdext.AzdClient azureContext *azdext.AzureContext modelCatalogService *ai.ModelCatalogService azureClient *azure.AzureClient projectConfig *azdext.ProjectConfig scenarioData *scenarioInput modelCatalog map[string]*ai.AiModel composedResources []*azdext.ComposedResource } func (a *startAction) Run(ctx context.Context, args []string) error { // Build up list of questions listOfQuestions, err := a.createQuestions(ctx) if err != nil { return fmt.Errorf("failed to generate prompt: %w", err) } decisionTree := qna.NewDecisionTree(listOfQuestions) if err := decisionTree.Run(ctx); err != nil { return fmt.Errorf("failed to run decision tree: %w", err) } spinner := ux.NewSpinner(&ux.SpinnerOptions{ Text: "Updating project configuration", ClearOnStop: true, }) fmt.Println() if err := spinner.Start(ctx); err != nil { return fmt.Errorf("failed to start spinner: %w", err) } resourcesToAdd := map[string]*azdext.ComposedResource{} servicesToAdd := map[string]*azdext.ServiceConfig{} // Add database resources if a.scenarioData.DatabaseType != "" { desiredName := strings.ReplaceAll(a.scenarioData.DatabaseType, "db.", "") dbResource := &azdext.ComposedResource{ Name: a.generateResourceName(desiredName), Type: a.scenarioData.DatabaseType, } resourcesToAdd[dbResource.Name] = dbResource } // Add messaging resources if a.scenarioData.MessagingType != "" { desiredName := strings.ReplaceAll(a.scenarioData.MessagingType, "messaging.", "") messagingResource := &azdext.ComposedResource{ Name: a.generateResourceName(desiredName), Type: a.scenarioData.MessagingType, } resourcesToAdd[messagingResource.Name] = messagingResource } // Add vector store resources if a.scenarioData.VectorStoreType != "" { vectorStoreResource := &azdext.ComposedResource{ Name: a.generateResourceName("vector-store"), Type: a.scenarioData.VectorStoreType, } resourcesToAdd[vectorStoreResource.Name] = vectorStoreResource } // Add storage resources if a.scenarioData.UseCustomData && a.scenarioData.StorageAccountId != "" { storageConfig := map[string]any{ "containers": []string{ "data", "embeddings", }, } storageConfigJson, err := json.Marshal(storageConfig) if err != nil { return fmt.Errorf("failed to marshal storage config: %w", err) } storageResource := &azdext.ComposedResource{ Name: a.generateResourceName("storage"), Type: "storage", Config: storageConfigJson, } resourcesToAdd[storageResource.Name] = storageResource } models := []*ai.AiModelDeployment{} type AiProjectResourceConfig struct { Models []*ai.AiModelDeployment `json:"models,omitempty"` } // Add AI model resources if len(a.scenarioData.ModelSelections) > 0 { var aiProject *azdext.ComposedResource var aiProjectConfig *AiProjectResourceConfig for _, resource := range a.composedResources { if resource.Type == "ai.project" { aiProject = resource if err := json.Unmarshal(resource.Config, &aiProjectConfig); err != nil { return fmt.Errorf("failed to unmarshal AI project config: %w", err) } break } } if aiProject == nil { aiProject = &azdext.ComposedResource{ Name: a.generateResourceName("ai-project"), Type: "ai.project", } aiProjectConfig = &AiProjectResourceConfig{} } modelMap := map[string]*ai.AiModelDeployment{} for _, modelDeployment := range aiProjectConfig.Models { modelMap[modelDeployment.Name] = modelDeployment } for _, modelName := range a.scenarioData.ModelSelections { aiModel, exists := a.modelCatalog[modelName] if exists { modelDeployment, err := a.modelCatalogService.GetModelDeployment(ctx, aiModel, nil) if err != nil { return fmt.Errorf("failed to get model deployment: %w", err) } if _, has := modelMap[modelDeployment.Name]; !has { modelMap[modelDeployment.Name] = modelDeployment aiProjectConfig.Models = append(aiProjectConfig.Models, modelDeployment) models = append(models, modelDeployment) } } } configJson, err := json.Marshal(aiProjectConfig) if err != nil { return fmt.Errorf("failed to marshal AI project config: %w", err) } aiProject.Config = configJson resourcesToAdd[aiProject.Name] = aiProject } // Add host resources such as container apps. for i, appKey := range a.scenarioData.InteractionTypes { if i >= len(a.scenarioData.AppHostTypes) { break } appType := a.scenarioData.AppHostTypes[i] if appType == "" || appType == "choose-app" { appType = "host.containerapp" } languageType := a.scenarioData.AppLanguages[i] appConfig := map[string]any{ "port": 8080, } appConfigJson, err := json.Marshal(appConfig) if err != nil { return fmt.Errorf("failed to marshal app config: %w", err) } appResource := &azdext.ComposedResource{ Name: a.generateResourceName(appKey), Type: appType, Config: appConfigJson, Uses: []string{}, } serviceName := a.generateServiceName(appKey) serviceConfig := &azdext.ServiceConfig{ Name: serviceName, Language: languageType, Host: strings.ReplaceAll(appType, "host.", ""), RelativePath: filepath.Join("src", serviceName), } // Setting the key of the service to the scenario interaction type since this is used for the // file copying. servicesToAdd[appKey] = serviceConfig resourcesToAdd[appResource.Name] = appResource } // Adds any new services to the azure.yaml. for interactionName, service := range servicesToAdd { _, err := a.azdClient.Project().AddService(ctx, &azdext.AddServiceRequest{ Service: service, }) if err != nil { return fmt.Errorf("failed to add service %s: %w", service.Name, err) } // Copy files from the embedded resources to the local service path. destServicePath := filepath.Join(a.projectConfig.Path, service.RelativePath) if err := os.MkdirAll(destServicePath, os.ModePerm); err != nil { return fmt.Errorf("failed to create service path %s: %w", destServicePath, err) } if !util.IsDirEmpty(destServicePath) { if err := spinner.Stop(ctx); err != nil { return fmt.Errorf("failed to stop spinner: %w", err) } overwriteResponse, err := a.azdClient.Prompt().Confirm(ctx, &azdext.ConfirmRequest{ Options: &azdext.ConfirmOptions{ DefaultValue: to.Ptr(false), Message: fmt.Sprintf( "The directory %s is not empty. Do you want to overwrite it?", output.WithHighLightFormat(service.RelativePath), ), }, }) if err != nil { return fmt.Errorf("failed to confirm overwrite: %w", err) } if !*overwriteResponse.Value { continue } if err := spinner.Start(ctx); err != nil { return fmt.Errorf("failed to start spinner: %w", err) } } // Identify dependent resources. uses := appUsesMap[interactionName] resource := resourcesToAdd[service.Name] resourceUseMap := map[string]struct{}{} if len(uses) > 0 { for _, dependentResource := range resourcesToAdd { // Skip if the resource type is already added. if _, has := resourceUseMap[dependentResource.Type]; has { continue } if slices.Contains(uses, dependentResource.Type) && resource.Name != dependentResource.Name { resource.Uses = append(resource.Uses, dependentResource.Name) resourceUseMap[dependentResource.Type] = struct{}{} } } for _, existingResource := range a.composedResources { // Skip if the resource type is already added. if _, has := resourceUseMap[existingResource.Type]; has { continue } if slices.Contains(uses, existingResource.Type) && resource.Name != existingResource.Name { resource.Uses = append(resource.Uses, existingResource.Name) resourceUseMap[existingResource.Type] = struct{}{} } } } } // Add any new resources to the azure.yaml. for _, resource := range resourcesToAdd { _, err := a.azdClient.Compose().AddResource(ctx, &azdext.AddResourceRequest{ Resource: resource, }) if err != nil { return fmt.Errorf("failed to add resource %s: %w", resource.Name, err) } } if err := spinner.Stop(ctx); err != nil { return fmt.Errorf("failed to stop spinner: %w", err) } fmt.Println(output.WithSuccessFormat("SUCCESS! The following have been staged for provisioning and deployment:")) if len(servicesToAdd) > 0 { fmt.Println() fmt.Println(output.WithHintFormat("Services")) for _, service := range servicesToAdd { fmt.Printf(" - %s %s\n", service.Name, output.WithGrayFormat( "(Host: %s, Language: %s)", service.Host, service.Language, ), ) } } if len(resourcesToAdd) > 0 { fmt.Println() fmt.Println(output.WithHintFormat("Resources")) for _, resource := range resourcesToAdd { fmt.Printf(" - %s %s\n", resource.Name, output.WithGrayFormat("(%s)", resource.Type)) } } if len(models) > 0 { fmt.Println() fmt.Println(output.WithHintFormat("AI Models")) for _, modelDeployment := range models { fmt.Printf(" - %s %s\n", modelDeployment.Name, output.WithGrayFormat( "(Format: %s, Version: %s, SKU: %s)", modelDeployment.Format, modelDeployment.Version, modelDeployment.Sku.Name, ), ) } } fmt.Println() confirmResponse, err := a.azdClient.Prompt().Confirm(ctx, &azdext.ConfirmRequest{ Options: &azdext.ConfirmOptions{ Message: "Do you want to provision resources to your project now?", DefaultValue: to.Ptr(true), HelpMessage: "Provisioning resources will create the necessary Azure infrastructure for your application.", }, }) if err != nil { return fmt.Errorf("failed to confirm provisioning: %w", err) } if !*confirmResponse.Value { fmt.Println() fmt.Printf("To provision resources later, run %s\n", output.WithHighLightFormat("azd provision")) return nil } workflow := &azdext.Workflow{ Name: "provision", Steps: []*azdext.WorkflowStep{ { Command: &azdext.WorkflowCommand{ Args: []string{"provision"}, }, }, }, } _, err = a.azdClient.Workflow().Run(ctx, &azdext.RunWorkflowRequest{ Workflow: workflow, }) if err != nil { return fmt.Errorf("failed to run provision workflow: %w", err) } fmt.Println() fmt.Println(output.WithSuccessFormat("SUCCESS! Your Azure resources have been provisioned.")) fmt.Printf( "You can add additional resources to your project by running %s\n", output.WithHighLightFormat("azd compose add"), ) return nil } func (a *startAction) loadAiCatalog(ctx context.Context) error { if a.modelCatalog != nil { return nil } spinner := ux.NewSpinner(&ux.SpinnerOptions{ Text: "Loading AI Model Catalog", ClearOnStop: true, }) if err := spinner.Start(ctx); err != nil { return fmt.Errorf("failed to start spinner: %w", err) } aiModelCatalog, err := a.modelCatalogService.ListAllModels(ctx, a.azureContext.Scope.SubscriptionId) if err != nil { return fmt.Errorf("failed to load AI model catalog: %w", err) } if err := spinner.Stop(ctx); err != nil { return err } a.modelCatalog = aiModelCatalog return nil } func ensureProject(ctx context.Context, azdClient *azdext.AzdClient) (*azdext.ProjectConfig, error) { projectResponse, err := azdClient.Project().Get(ctx, &azdext.EmptyRequest{}) if err != nil { fmt.Println("Lets get your project initialized.") // We don't have a project yet // Dispatch a workflow to init the project and create a new environment workflow := &azdext.Workflow{ Name: "init", Steps: []*azdext.WorkflowStep{ {Command: &azdext.WorkflowCommand{Args: []string{"init"}}}, }, } _, err := azdClient.Workflow().Run(ctx, &azdext.RunWorkflowRequest{ Workflow: workflow, }) if err != nil { return nil, fmt.Errorf("failed to initialize project: %w", err) } projectResponse, err = azdClient.Project().Get(ctx, &azdext.EmptyRequest{}) if err != nil { return nil, fmt.Errorf("failed to get project: %w", err) } fmt.Println() } if projectResponse.Project == nil { return nil, fmt.Errorf("project not found") } return projectResponse.Project, nil } func ensureEnvironment(ctx context.Context, azdClient *azdext.AzdClient) (*azdext.Environment, error) { envResponse, err := azdClient.Environment().GetCurrent(ctx, &azdext.EmptyRequest{}) if err != nil { fmt.Println("Lets create a new default environment for your project.") // We don't have a project yet // Dispatch a workflow to init the project and create a new environment workflow := &azdext.Workflow{ Name: "env new", Steps: []*azdext.WorkflowStep{ {Command: &azdext.WorkflowCommand{Args: []string{"env", "new"}}}, }, } _, err = azdClient.Workflow().Run(ctx, &azdext.RunWorkflowRequest{ Workflow: workflow, }) if err != nil { return nil, fmt.Errorf("failed to create new environment: %w", err) } envResponse, err = azdClient.Environment().GetCurrent(ctx, &azdext.EmptyRequest{}) if err != nil { return nil, fmt.Errorf("failed to get current environment: %w", err) } fmt.Println() } if envResponse.Environment == nil { return nil, fmt.Errorf("environment not found") } return envResponse.Environment, nil } func ensureAzureContext( ctx context.Context, azdClient *azdext.AzdClient, ) (*azdext.AzureContext, *azdext.ProjectConfig, error) { project, err := ensureProject(ctx, azdClient) if err != nil { return nil, nil, fmt.Errorf("failed to ensure environment: %w", err) } env, err := ensureEnvironment(ctx, azdClient) if err != nil { return nil, nil, fmt.Errorf("failed to ensure environment: %w", err) } envValues, err := azdClient.Environment().GetValues(ctx, &azdext.GetEnvironmentRequest{ Name: env.Name, }) if err != nil { return nil, nil, fmt.Errorf("failed to get environment values: %w", err) } envValueMap := make(map[string]string) for _, value := range envValues.KeyValues { envValueMap[value.Key] = value.Value } azureContext := &azdext.AzureContext{ Scope: &azdext.AzureScope{ TenantId: envValueMap["AZURE_TENANT_ID"], SubscriptionId: envValueMap["AZURE_SUBSCRIPTION_ID"], Location: envValueMap["AZURE_LOCATION"], }, Resources: []string{}, } if azureContext.Scope.SubscriptionId == "" { fmt.Print() fmt.Println("It looks like we first need to connect to your Azure subscription.") subscriptionResponse, err := azdClient.Prompt().PromptSubscription(ctx, &azdext.PromptSubscriptionRequest{}) if err != nil { return nil, nil, fmt.Errorf("failed to prompt for subscription: %w", err) } azureContext.Scope.SubscriptionId = subscriptionResponse.Subscription.Id azureContext.Scope.TenantId = subscriptionResponse.Subscription.TenantId // Set the subscription ID in the environment _, err = azdClient.Environment().SetValue(ctx, &azdext.SetEnvRequest{ EnvName: env.Name, Key: "AZURE_TENANT_ID", Value: azureContext.Scope.TenantId, }) if err != nil { return nil, nil, fmt.Errorf("failed to set tenant ID in environment: %w", err) } // Set the tenant ID in the environment _, err = azdClient.Environment().SetValue(ctx, &azdext.SetEnvRequest{ EnvName: env.Name, Key: "AZURE_SUBSCRIPTION_ID", Value: azureContext.Scope.SubscriptionId, }) if err != nil { return nil, nil, fmt.Errorf("failed to set subscription ID in environment: %w", err) } } if azureContext.Scope.Location == "" { fmt.Println() fmt.Println( "Next, we need to select a default Azure location that will be used as the target for your infrastructure.", ) locationResponse, err := azdClient.Prompt().PromptLocation(ctx, &azdext.PromptLocationRequest{ AzureContext: azureContext, }) if err != nil { return nil, nil, fmt.Errorf("failed to prompt for location: %w", err) } azureContext.Scope.Location = locationResponse.Location.Name // Set the location in the environment _, err = azdClient.Environment().SetValue(ctx, &azdext.SetEnvRequest{ EnvName: env.Name, Key: "AZURE_LOCATION", Value: azureContext.Scope.Location, }) if err != nil { return nil, nil, fmt.Errorf("failed to set location in environment: %w", err) } } return azureContext, project, nil } func (a *startAction) createQuestions(ctx context.Context) (map[string]qna.Question, error) { resourceTypes, err := a.azdClient.Compose().ListResourceTypes(ctx, &azdext.EmptyRequest{}) if err != nil { return nil, fmt.Errorf("failed to list resource types: %w", err) } dbResourceMap := make(map[string]*azdext.ComposedResourceType) vectorStoreMap := make(map[string]*azdext.ComposedResourceType) messagingResourceMap := make(map[string]*azdext.ComposedResourceType) for _, resourceType := range resourceTypes.ResourceTypes { key := resourceType.Name if strings.HasPrefix(key, "db.") { dbResourceMap[key] = resourceType } else if strings.HasPrefix(key, "messaging.") { messagingResourceMap[key] = resourceType } if strings.Contains(key, "ai.search") || strings.Contains(key, "db.cosmos") { vectorStoreMap[key] = resourceType } } return map[string]qna.Question{ "root": { Binding: &a.scenarioData.SelectedScenario, Heading: "Identify AI Scenario", Message: "Let's start drilling into your AI scenario to identify all the required infrastructure we will need.", Prompt: &qna.SingleSelectPrompt{ Client: a.azdClient, Message: "What type of AI scenario are you building?", HelpMessage: "Choose the scenario that best fits your needs.", EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ {Label: "RAG Application (Retrieval-Augmented Generation)", Value: "rag"}, {Label: "AI Agent", Value: "agent"}, {Label: "Other Scenarios (Coming Soon)", Value: "other-scenarios"}, }, }, Branches: map[any][]qna.QuestionReference{ "rag": {{Key: "use-custom-data"}}, "agent": {{Key: "agent-tasks"}}, }, }, "use-custom-data": { Binding: &a.scenarioData.UseCustomData, Prompt: &qna.ConfirmPrompt{ Client: a.azdClient, Message: "Does your application require custom data?", HelpMessage: "Custom data is data that is not publicly available and is specific to your application.", DefaultValue: to.Ptr(true), }, AfterAsk: func(ctx context.Context, q *qna.Question, _ any) error { switch a.scenarioData.SelectedScenario { case "rag": q.Branches = map[any][]qna.QuestionReference{ true: {{Key: "choose-data-types"}}, false: {{Key: "rag-user-interaction"}}, } case "agent": q.Branches = map[any][]qna.QuestionReference{ true: {{Key: "choose-data-types"}}, false: {{Key: "agent-tasks"}}, } } return nil }, }, "choose-data-types": { Binding: &a.scenarioData.DataTypes, Heading: "Data Sources", Message: "Lets identify all the data source that will be used in your application.", Prompt: &qna.MultiSelectPrompt{ Client: a.azdClient, Message: "What type of data are you using?", HelpMessage: "Select all the data types that apply to your application.", EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ {Label: "Structured documents, ex. JSON, CSV", Value: "structured-documents"}, {Label: "Unstructured documents, ex. PDF, Word", Value: "unstructured-documents"}, {Label: "Videos", Value: "videos"}, {Label: "Images", Value: "images"}, {Label: "Audio", Value: "audio"}, }, }, Next: []qna.QuestionReference{{Key: "data-location"}}, }, "data-location": { Binding: &a.scenarioData.DataLocations, Prompt: &qna.MultiSelectPrompt{ Client: a.azdClient, Message: "Where is your data located?", HelpMessage: "Select all the data locations that apply to your application.", EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ {Label: "Azure Blob Storage", Value: "blob-storage"}, {Label: "Azure Database", Value: "databases"}, {Label: "Local file system", Value: "local-file-system"}, {Label: "Other", Value: "other-datasource"}, }, }, Branches: map[any][]qna.QuestionReference{ "blob-storage": {{Key: "choose-storage"}}, "databases": {{Key: "choose-database"}}, "local-file-system": {{Key: "local-file-system"}}, }, Next: []qna.QuestionReference{{Key: "choose-vector-store"}}, }, "choose-storage": { Heading: "Storage Account", Message: "We'll need to setup a storage account to store the data for your application.", BeforeAsk: func(ctx context.Context, q *qna.Question, _ any) error { hasStorageResource := false for _, resource := range a.composedResources { if resource.Type == "storage" { hasStorageResource = true break } } promptMessage := "It looks like you already have a configured storage account. Do you want to reuse it?" if hasStorageResource { q.Prompt = &qna.ConfirmPrompt{ Client: a.azdClient, Message: promptMessage, DefaultValue: to.Ptr(true), HelpMessage: "Using an existing storage account will save you time and resources.", } } q.State["hasStorageResource"] = hasStorageResource return nil }, AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { hasStorageResource := q.State["hasStorageResource"].(bool) reuseStorage, ok := value.(bool) if !hasStorageResource || ok && !reuseStorage { q.Next = []qna.QuestionReference{{Key: "choose-storage-resource"}} } return nil }, }, "choose-storage-resource": { Binding: &a.scenarioData.StorageAccountId, Prompt: &qna.SubscriptionResourcePrompt{ Client: a.azdClient, ResourceType: "Microsoft.Storage/storageAccounts", ResourceTypeDisplayName: "Storage Account", HelpMessage: "Select an existing storage account or create a new one.", AzureContext: a.azureContext, }, }, "choose-database": { Heading: "Database", Message: "We'll need to setup a database that will be used by your application to power AI model(s).", BeforeAsk: func(ctx context.Context, q *qna.Question, _ any) error { hasDatabaseResource := false for _, resource := range a.composedResources { if strings.HasPrefix(resource.Type, "db.") { hasDatabaseResource = true break } } if hasDatabaseResource { q.Prompt = &qna.ConfirmPrompt{ Client: a.azdClient, Message: "It looks like you already have a configured database. Do you want to reuse it?", DefaultValue: to.Ptr(true), HelpMessage: "Using an existing database will save you time and resources.", } } q.State["hasDatabaseResource"] = hasDatabaseResource return nil }, AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { hasDatabaseResource := q.State["hasDatabaseResource"].(bool) reuseDatabase, ok := value.(bool) if !hasDatabaseResource || ok && !reuseDatabase { q.Next = []qna.QuestionReference{{Key: "choose-database-type"}} } return nil }, }, "choose-database-type": { Binding: &a.scenarioData.DatabaseType, Prompt: &qna.SingleSelectPrompt{ Message: "Which type of database?", HelpMessage: "Select the type of database that best fits your needs.", Client: a.azdClient, EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ {Label: "CosmosDB", Value: "db.cosmos"}, {Label: "PostgreSQL", Value: "db.postgres"}, {Label: "MySQL", Value: "db.mysql"}, {Label: "Redis", Value: "db.redis"}, {Label: "MongoDB", Value: "db.mongo"}, }, }, Next: []qna.QuestionReference{{Key: "choose-database-resource"}}, }, "choose-database-resource": { Binding: &a.scenarioData.DatabaseId, Prompt: &qna.SubscriptionResourcePrompt{ HelpMessage: "Select an existing database or create a new one.", Client: a.azdClient, AzureContext: a.azureContext, BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SubscriptionResourcePrompt) error { resourceType, has := dbResourceMap[a.scenarioData.DatabaseType] if !has { return fmt.Errorf( "unknown resource type for database: %s", a.scenarioData.DatabaseType, ) } p.ResourceType = resourceType.Type p.Kinds = resourceType.Kinds p.ResourceTypeDisplayName = resourceType.DisplayName return nil }, }, }, "local-file-system": { Binding: &a.scenarioData.LocalFilePath, Heading: "Local File System", Message: "Lets identify the files that will be used in your application. " + "Later on we will upload these files to Azure so they can be used by your application.", Prompt: &qna.TextPrompt{ Client: a.azdClient, Message: "Path to the local files", HelpMessage: "This path can be absolute or relative to the current working directory. " + "Please make sure the path is accessible from the machine running this command.", Placeholder: "./data", }, Next: []qna.QuestionReference{{Key: "local-file-choose-files"}}, }, "local-file-choose-files": { Binding: &a.scenarioData.LocalFileSelection, Prompt: &qna.SingleSelectPrompt{ Client: a.azdClient, Message: "Which files?", HelpMessage: "Select all files or use a glob expression to filter the files.", EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ {Label: "All Files", Value: "all-files"}, {Label: "Glob Expression", Value: "glob-expression"}, }, }, Branches: map[any][]qna.QuestionReference{ "glob-expression": {{Key: "local-file-glob"}}, }, }, "local-file-glob": { Binding: &a.scenarioData.LocalFileGlobFilter, Prompt: &qna.TextPrompt{ Client: a.azdClient, Message: "Enter a glob expression to filter files", HelpMessage: "A glob expression is a string that uses wildcard characters to match file names. " + " For example, *.txt will match all text files in the current directory.", Placeholder: "*.json", }, }, "choose-vector-store": { Heading: "Vector Store", Message: "Based on your choices we're going to need a vector store to store the text embeddings for your data.", BeforeAsk: func(ctx context.Context, q *qna.Question, _ any) error { hasVectorStoreResource := false for _, resource := range a.composedResources { if resource.Type == "ai.search" { hasVectorStoreResource = true break } } if hasVectorStoreResource { q.Prompt = &qna.ConfirmPrompt{ Client: a.azdClient, Message: "It looks like you already have a configured vector store. Do you want to reuse it?", DefaultValue: to.Ptr(true), HelpMessage: "Using an existing vector store will save you time and resources.", } } q.State["hasVectorStoreResource"] = hasVectorStoreResource return nil }, AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { hasVectorStoreResource := q.State["hasVectorStoreResource"].(bool) reuseVectorStore, ok := value.(bool) var next string if a.scenarioData.SelectedScenario == "rag" { next = "rag-user-interaction" } else { next = "agent-interaction" } if !hasVectorStoreResource || ok && !reuseVectorStore { next = "choose-vector-store-type" } q.Next = []qna.QuestionReference{{Key: next}} return nil }, }, "choose-vector-store-type": { Binding: &a.scenarioData.VectorStoreType, Prompt: &qna.SingleSelectPrompt{ Message: "What type of vector store do you want to use?", HelpMessage: "Select the type of vector store that best fits your needs.", Client: a.azdClient, EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ {Label: "Choose for me", Value: "ai.search"}, {Label: "AI Search", Value: "ai.search"}, {Label: "CosmosDB", Value: "db.cosmos"}, }, }, Branches: map[any][]qna.QuestionReference{ "ai.search": {{Key: "choose-vector-store-resource"}}, "db.cosmos": {{Key: "choose-vector-store-resource"}}, }, AfterAsk: func(ctx context.Context, q *qna.Question, _ any) error { switch a.scenarioData.SelectedScenario { case "rag": q.Next = []qna.QuestionReference{{Key: "rag-user-interaction"}} case "agent": q.Next = []qna.QuestionReference{{Key: "agent-interaction"}} } return nil }, }, "choose-vector-store-resource": { Binding: &a.scenarioData.VectorStoreId, Prompt: &qna.SubscriptionResourcePrompt{ HelpMessage: "Select an existing vector store or create a new one.", Client: a.azdClient, AzureContext: a.azureContext, BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SubscriptionResourcePrompt) error { resourceType, has := vectorStoreMap[a.scenarioData.VectorStoreType] if !has { return fmt.Errorf( "unknown resource type for vector store: %s", a.scenarioData.VectorStoreType, ) } p.ResourceType = resourceType.Type p.Kinds = resourceType.Kinds p.ResourceTypeDisplayName = resourceType.DisplayName return nil }, }, }, "rag-user-interaction": { Binding: &a.scenarioData.InteractionTypes, Heading: "User Interaction", Message: "Now we will figure out all the different ways users will interact with your application.", Prompt: &qna.MultiSelectPrompt{ Client: a.azdClient, Message: "How do you want users to interact with the data?", HelpMessage: "Select all the data interaction types that apply to your application.", EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ {Label: "Chatbot UI Frontend", Value: "rag-ui"}, {Label: "API Backend Application", Value: "rag-api"}, }, }, Branches: map[any][]qna.QuestionReference{ "rag-ui": {{Key: "choose-app"}}, "rag-api": {{Key: "choose-app"}}, }, AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { q.State["interactionTypes"] = value return nil }, Next: []qna.QuestionReference{{Key: "start-choose-models"}}, }, "choose-app": { BeforeAsk: func(ctx context.Context, q *qna.Question, value any) error { q.Heading = fmt.Sprintf("Configure '%s' Application", value) q.Message = fmt.Sprintf("Lets collect some information about your %s application.", value) q.State["interactionType"] = value hasHostAppResource := false appHostCount := 0 for _, resource := range a.composedResources { if strings.HasPrefix(resource.Type, "host.") { appHostCount++ hasHostAppResource = true } } hostName := "host" hostName2 := "it" if appHostCount > 1 { hostName = "hosts" hostName2 = "them" } msg := fmt.Sprintf( "It looks like you project already contains %d application %s. Do you want to reuse %s?", appHostCount, hostName, hostName2, ) if hasHostAppResource { q.Prompt = &qna.ConfirmPrompt{ Client: a.azdClient, Message: msg, DefaultValue: to.Ptr(true), HelpMessage: "Using an existing application host will save you time and resources.", } } q.State["hasHostAppResource"] = hasHostAppResource return nil }, AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { hasHostAppResource := q.State["hasHostAppResource"].(bool) reuseHostApp, ok := value.(bool) if !hasHostAppResource || ok && !reuseHostApp { q.Next = []qna.QuestionReference{{Key: "choose-app-type"}} } delete(q.State, "hasHostAppResource") return nil }, }, "choose-app-type": { Binding: &a.scenarioData.AppHostTypes, Prompt: &qna.SingleSelectPrompt{ Message: "Which application host do you want to use?", Client: a.azdClient, EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ {Label: "Choose for me", Value: "choose-app"}, {Label: "Container App", Value: "host.containerapp"}, {Label: "App Service (Coming Soon)", Value: "host.webapp"}, {Label: "Function App (Coming Soon)", Value: "host.functionapp"}, {Label: "Static Web App (Coming Soon)", Value: "host.staticwebapp"}, {Label: "Other", Value: "other-app"}, }, }, Branches: map[any][]qna.QuestionReference{ "host.containerapp": {{Key: "choose-app-resource"}}, }, Next: []qna.QuestionReference{ {Key: "choose-app-language"}, }, }, "choose-app-language": { Prompt: &qna.SingleSelectPrompt{ Client: a.azdClient, Message: "Which programming language do you want to use?", HelpMessage: "Select the programming language that best fits your needs.", EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ {Label: "Choose for me", Value: "default"}, {Label: "C#", Value: "csharp"}, {Label: "Python", Value: "python"}, {Label: "JavaScript", Value: "js"}, {Label: "TypeScript", Value: "ts"}, {Label: "Java", Value: "java"}, {Label: "Other", Value: "other"}, }, }, AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { selectedLanguage := value.(string) // Find the default language for the selected interaction type if available. if selectedLanguage == "default" { interactionType := q.State["interactionType"].(string) interactionDefault, has := defaultAppLanguageMap[interactionType] if has { selectedLanguage = interactionDefault } else { selectedLanguage = "python" } } a.scenarioData.AppLanguages = append(a.scenarioData.AppLanguages, selectedLanguage) return nil }, }, "choose-app-resource": { Binding: &a.scenarioData.AppResourceIds, BeforeAsk: func(ctx context.Context, q *qna.Question, value any) error { q.State["appType"] = value return nil }, Prompt: &qna.SubscriptionResourcePrompt{ HelpMessage: "Select an existing application or create a new one.", Client: a.azdClient, AzureContext: a.azureContext, BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SubscriptionResourcePrompt) error { appType := q.State["appType"].(string) resourceType, has := appResourceMap[appType] if !has { return fmt.Errorf( "unknown resource type for database: %s", appType, ) } p.ResourceType = resourceType.ResourceType p.Kinds = resourceType.Kinds p.ResourceTypeDisplayName = resourceType.ResourceTypeDisplayName return nil }, }, }, "agent-interaction": { Binding: &a.scenarioData.InteractionTypes, Heading: "Agent Hosting", Message: "Now we will figure out all the different ways users and systems will interact with your agent.", Prompt: &qna.MultiSelectPrompt{ Client: a.azdClient, Message: "How do you want users to interact with the agent?", HelpMessage: "Select all the data interaction types that apply to your application.", EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ {Label: "Chatbot UI Frontend", Value: "agent-ui"}, {Label: "API Backend Application", Value: "agent-api"}, {Label: "Message based Backed Queue", Value: "agent-messaging"}, }, }, Branches: map[any][]qna.QuestionReference{ "agent-ui": {{Key: "choose-app"}}, "agent-api": {{Key: "choose-app"}}, "agent-messaging": {{Key: "choose-app"}, {Key: "choose-messaging"}}, }, AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { q.State["interactionTypes"] = value return nil }, Next: []qna.QuestionReference{{Key: "start-choose-models"}}, }, "agent-tasks": { Binding: &a.scenarioData.ModelTasks, Prompt: &qna.MultiSelectPrompt{ Client: a.azdClient, Message: "What tasks do you want the AI agent to perform?", HelpMessage: "Select all the tasks that apply to your application.", EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ {Label: "Custom Function Calling", Value: "custom-function-calling"}, {Label: "Integrate with Open API based services", Value: "openapi"}, {Label: "Run Azure Functions", Value: "azure-functions"}, {Label: "Other", Value: "other-model-tasks"}, }, }, Next: []qna.QuestionReference{{Key: "use-custom-data"}}, }, "choose-messaging": { BeforeAsk: func(ctx context.Context, q *qna.Question, _ any) error { hasMessagingResource := false for _, resource := range a.composedResources { if strings.HasPrefix(resource.Type, "messaging.") { hasMessagingResource = true break } } if hasMessagingResource { promptMessage := "It looks like you already have a configured messaging source. Do you want to reuse it?" q.Prompt = &qna.ConfirmPrompt{ Client: a.azdClient, Message: promptMessage, DefaultValue: to.Ptr(true), HelpMessage: "Using an existing database will save you time and resources.", } } q.State["hasMessagingResource"] = hasMessagingResource return nil }, AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { hasMessagingResource := q.State["hasMessagingResource"].(bool) reuseMessaging, ok := value.(bool) if !hasMessagingResource || ok && !reuseMessaging { q.Next = []qna.QuestionReference{{Key: "choose-messaging-type"}} } return nil }, }, "choose-messaging-type": { Binding: &a.scenarioData.MessagingType, Prompt: &qna.SingleSelectPrompt{ Client: a.azdClient, Message: "Which messaging service do you want to use?", HelpMessage: "Select the messaging service that best fits your needs.", EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ {Label: "Choose for me", Value: "messaging.servicebus"}, {Label: "Azure Service Bus", Value: "messaging.servicebus"}, {Label: "Azure Event Hubs", Value: "messaging.eventhubs"}, }, }, Branches: map[any][]qna.QuestionReference{ "messaging.eventhubs": {{Key: "choose-messaging-resource"}}, "messaging.servicebus": {{Key: "choose-messaging-resource"}}, }, }, "choose-messaging-resource": { Binding: &a.scenarioData.MessagingId, Prompt: &qna.SubscriptionResourcePrompt{ HelpMessage: "Select an existing messaging service or create a new one.", Client: a.azdClient, AzureContext: a.azureContext, BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SubscriptionResourcePrompt) error { resourceType, has := messagingResourceMap[a.scenarioData.MessagingType] if !has { return fmt.Errorf( "unknown resource type for messaging: %s", a.scenarioData.MessagingType, ) } p.ResourceType = resourceType.Type p.Kinds = resourceType.Kinds p.ResourceTypeDisplayName = resourceType.DisplayName return nil }, }, }, "start-choose-models": { Heading: "AI Model Selection", Message: "Now we will figure out the best AI model(s) for your application.", AfterAsk: func(ctx context.Context, question *qna.Question, value any) error { if err := a.loadAiCatalog(ctx); err != nil { return fmt.Errorf("failed to load AI model catalog: %w", err) } allModelTypes := map[string]struct { Heading string Description string QuestionReference qna.QuestionReference }{ "llm": { Heading: "Large Language Model (LLM) (For generating responses)", Description: "Processes user queries and retrieved documents to generate intelligent responses.", QuestionReference: qna.QuestionReference{ Key: "start-choose-model", State: map[string]any{ "modelSelectMessage": "Lets choose a chat completion model", "capabilities": []string{"chatCompletion"}, }, }, }, "embeddings": { Heading: "Embedding Model (For vectorizing text)", Description: "Used to convert documents and queries into vector representations " + "for efficient similarity searches.", QuestionReference: qna.QuestionReference{ Key: "start-choose-model", State: map[string]any{ "modelSelectMessage": "Lets choose a text embedding model", "capabilities": []string{"embeddings"}, }, }, }, "audio": { Heading: "Audio Model (For transcribing audio)", Description: "Used to convert audio files into text for further processing.", QuestionReference: qna.QuestionReference{ Key: "start-choose-model", State: map[string]any{ "modelSelectMessage": "Lets choose a audio model", "capabilities": []string{"audio"}, }, }, }, "images": { Heading: "Image Generation Model (For generating images)", Description: "Used to generate images based on text prompts.", QuestionReference: qna.QuestionReference{ Key: "start-choose-model", State: map[string]any{ "modelSelectMessage": "Lets choose a image generation model", "capabilities": []string{"imageGenerations"}, }, }, }, } requiredModels := []string{"llm"} if slices.Contains(a.scenarioData.DataTypes, "structured-documents") || slices.Contains(a.scenarioData.DataTypes, "unstructured-documents") { requiredModels = append(requiredModels, "embeddings") } if slices.Contains(a.scenarioData.DataTypes, "audio") { requiredModels = append(requiredModels, "audio") } if slices.Contains(a.scenarioData.DataTypes, "images") || slices.Contains(a.scenarioData.DataTypes, "videos") { requiredModels = append(requiredModels, "images") } nextQuestions := []qna.QuestionReference{} fmt.Printf(" Based on your choices, you will need the following AI models:\n\n") for _, model := range requiredModels { if modelType, ok := allModelTypes[model]; ok { fmt.Printf(" - %s\n", output.WithBold("%s", modelType.Heading)) fmt.Printf(" %s\n", output.WithGrayFormat(modelType.Description)) fmt.Println() nextQuestions = append(nextQuestions, modelType.QuestionReference) } } question.Next = nextQuestions return nil }, }, "start-choose-model": { BeforeAsk: func(ctx context.Context, question *qna.Question, value any) error { if err := a.loadAiCatalog(ctx); err != nil { return fmt.Errorf("failed to load AI model catalog: %w", err) } return nil }, Prompt: &qna.SingleSelectPrompt{ Client: a.azdClient, BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SingleSelectPrompt) error { // Override the message if message, ok := q.State["modelSelectMessage"].(string); ok { p.Message = message } return nil }, Message: "How do you want to find the right model?", HelpMessage: "Select the option that best fits your needs.", EnableFiltering: to.Ptr(false), Choices: []qna.Choice{ {Label: "Choose for me", Value: "choose-model"}, {Label: "Help me choose", Value: "guide-model"}, {Label: "I will choose model", Value: "user-model"}, }, }, Branches: map[any][]qna.QuestionReference{ "guide-model": {{Key: "guide-model-select"}}, "user-model": {{Key: "user-model-select"}}, }, AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { selectedValue := value.(string) if selectedValue == "choose-model" { capabilities, ok := q.State["capabilities"].([]string) if !ok { return nil } // If the user selected "choose-model", we need to set the model selection for key, value := range defaultModelMap { if slices.Contains(capabilities, key) { a.scenarioData.ModelSelections = append( a.scenarioData.ModelSelections, value, ) } } } return nil }, }, "guide-model-select": { Prompt: &qna.MultiSelectPrompt{ Client: a.azdClient, Message: "Filter AI Models", HelpMessage: "Select all the filters that apply to your application. " + "These filters will help you narrow down the type of models you need.", EnableFiltering: to.Ptr(false), BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.MultiSelectPrompt) error { choices := []qna.Choice{} if _, has := q.State["capabilities"]; !has { choices = append(choices, qna.Choice{ Label: "Filter by capabilities", Value: "filter-model-capability", }) } if _, has := q.State["formats"]; !has { choices = append(choices, qna.Choice{ Label: "Filter by author", Value: "filter-model-format", }) } if _, has := q.State["status"]; !has { choices = append(choices, qna.Choice{ Label: "Filter by status", Value: "filter-model-status", }) } if _, has := q.State["locations"]; !has { choices = append(choices, qna.Choice{ Label: "Filter by location", Value: "filter-model-location", }) } p.Choices = choices return nil }, }, Branches: map[any][]qna.QuestionReference{ "filter-model-capability": {{Key: "filter-model-capability"}}, "filter-model-format": {{Key: "filter-model-format"}}, "filter-model-status": {{Key: "filter-model-status"}}, "filter-model-location": {{Key: "filter-model-location"}}, }, Next: []qna.QuestionReference{{Key: "user-model-select"}}, }, "user-model-select": { Binding: &a.scenarioData.ModelSelections, Prompt: &qna.SingleSelectPrompt{ BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.SingleSelectPrompt) error { var capabilities []string var formats []string var statuses []string var locations []string if val, ok := q.State["capabilities"]; ok { capabilities = val.([]string) } if val, ok := q.State["formats"]; ok { formats = val.([]string) } if val, ok := q.State["status"]; ok { statuses = val.([]string) } if val, ok := q.State["locations"]; ok { locations = val.([]string) } filterOptions := &ai.FilterOptions{ Capabilities: capabilities, Formats: formats, Statuses: statuses, Locations: locations, } filteredModels := a.modelCatalogService.ListFilteredModels(ctx, a.modelCatalog, filterOptions) choices := make([]qna.Choice, len(filteredModels)) for i, model := range filteredModels { choices[i] = qna.Choice{ Label: fmt.Sprintf("%s %s", model.Name, output.WithGrayFormat("(%s)", *model.Locations[0].Model.Model.Format), ), Value: model.Name, } } p.Choices = choices return nil }, Client: a.azdClient, Message: "Which model do you want to use?", HelpMessage: "Select the model that best fits your needs.", }, AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { delete(q.State, "capabilities") delete(q.State, "formats") delete(q.State, "status") delete(q.State, "locations") return nil }, }, "filter-model-capability": { Prompt: &qna.MultiSelectPrompt{ Client: a.azdClient, Message: "What capabilities do you want the model to have?", HelpMessage: "Select all the capabilities that apply to your application.", Choices: []qna.Choice{ {Label: "Audio", Value: "audio"}, {Label: "Chat Completion", Value: "chatCompletion"}, {Label: "Text Completion", Value: "completion"}, {Label: "Generate Vector Embeddings", Value: "embeddings"}, {Label: "Image Generation", Value: "imageGenerations"}, }, }, AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { q.State["capabilities"] = value return nil }, }, "filter-model-format": { Prompt: &qna.MultiSelectPrompt{ BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.MultiSelectPrompt) error { formats := a.modelCatalogService.ListAllFormats(ctx, a.modelCatalog) choices := make([]qna.Choice, len(formats)) for i, format := range formats { choices[i] = qna.Choice{ Label: format, Value: format, } } p.Choices = choices return nil }, Client: a.azdClient, Message: "Filter my by company or creator", HelpMessage: "Select all the companies or creators that apply to your application.", }, AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { q.State["formats"] = value return nil }, }, "filter-model-status": { Prompt: &qna.MultiSelectPrompt{ BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.MultiSelectPrompt) error { statuses := a.modelCatalogService.ListAllStatuses(ctx, a.modelCatalog) choices := make([]qna.Choice, len(statuses)) for i, status := range statuses { choices[i] = qna.Choice{ Label: status, Value: status, } } p.Choices = choices return nil }, Client: a.azdClient, Message: "Filter by model release status?", HelpMessage: "Select all the model release status that apply to your application.", EnableFiltering: to.Ptr(false), }, AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { q.State["status"] = value return nil }, }, "filter-model-location": { Prompt: &qna.MultiSelectPrompt{ BeforeAsk: func(ctx context.Context, q *qna.Question, p *qna.MultiSelectPrompt) error { spinner := ux.NewSpinner(&ux.SpinnerOptions{ Text: "Loading Locations", }) err := spinner.Run(ctx, func(ctx context.Context) error { locations, err := a.azureClient.ListLocations(ctx, a.azureContext.Scope.SubscriptionId) if err != nil { return fmt.Errorf("failed to list locations: %w", err) } choices := make([]qna.Choice, len(locations)) for i, location := range locations { choices[i] = qna.Choice{ Label: fmt.Sprintf("%s (%s)", *location.DisplayName, *location.Name), Value: *location.Name, } } p.Choices = choices return nil }) if err != nil { return fmt.Errorf("failed to load locations: %w", err) } return nil }, Client: a.azdClient, Message: "Filter by model location?", HelpMessage: "Select all the model locations that apply to your application.", }, AfterAsk: func(ctx context.Context, q *qna.Question, value any) error { q.State["locations"] = value return nil }, }, }, nil } func (a *startAction) generateResourceName(desiredName string) string { resourceMap := map[string]struct{}{} for _, resource := range a.composedResources { resourceMap[resource.Name] = struct{}{} } if _, exists := resourceMap[desiredName]; !exists { return desiredName } // If the desired name already exists, append a number (always 2 digits) to the name nextIndex := 1 for { newName := fmt.Sprintf("%s-%02d", desiredName, nextIndex) if _, exists := resourceMap[newName]; !exists { return newName } nextIndex++ } } func (a *startAction) generateServiceName(desiredName string) string { if _, exists := a.projectConfig.Services[desiredName]; !exists { return desiredName } // If the desired name already exists, append a number (always 2 digits) to the name nextIndex := 1 for { newName := fmt.Sprintf("%s-%02d", desiredName, nextIndex) if _, exists := a.projectConfig.Services[newName]; !exists { return newName } nextIndex++ } }