func()

in cli/azd/extensions/microsoft.azd.ai.builder/internal/cmd/start.go [185:539]


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
}