From 22e9e48406cae4c638d7a766a0dc6fb3413d26f0 Mon Sep 17 00:00:00 2001 From: Darrick Date: Mon, 10 Feb 2025 10:30:33 +0100 Subject: [PATCH] [AI] Monetization notifications (#2901) #### Summary Notifications being added: - You may be charged in the future - You're running out of AI quota - You're out of AI quota ![image](https://github.com/user-attachments/assets/cfa5cd15-bd3a-43b2-ac8f-303bf27f0443) ![image](https://github.com/user-attachments/assets/3ebcecc9-2b67-4000-b747-bc51e7a8ad18) ![image](https://github.com/user-attachments/assets/b77d34dd-2778-480d-be49-ad9d942207ca) When the `Learn More` is pressed, a non-admin user will be directed to the docs. An admin will see the following dialogs ![image](https://github.com/user-attachments/assets/a3ad66ca-1b1e-4bf3-b05c-5b51af3c317a) ![image](https://github.com/user-attachments/assets/7acc8505-e528-4a91-a7b6-268f599c79aa) The notification will also show when you run `generate` ![image](https://github.com/user-attachments/assets/c4a7f737-c4b8-4018-8460-4b362e7c90fd) ![image](https://github.com/user-attachments/assets/906eebe6-8745-41cb-9dac-d14cf08201ad) #### Work Item(s) Fixes [AB#563277](https://dynamicssmb2.visualstudio.com/1fcb79e7-ab07-432a-a3c6-6cf5a88ba4a5/_workitems/edit/563277) --- src/System Application/App/AI/app.json | 2 +- .../Azure OpenAI/AzureOpenAIImpl.Codeunit.al | 4 + .../src/Copilot/CopilotAICapabilities.Page.al | 10 +- .../Copilot/CopilotCapabilityImpl.Codeunit.al | 34 ---- .../Copilot/CopilotNotifications.Codeunit.al | 153 ++++++++++++++++++ .../App/DotNet Aliases/src/dotnet.al | 4 + 6 files changed, 170 insertions(+), 37 deletions(-) create mode 100644 src/System Application/App/AI/src/Copilot/CopilotNotifications.Codeunit.al diff --git a/src/System Application/App/AI/app.json b/src/System Application/App/AI/app.json index cc9675aba2..aee58d9565 100644 --- a/src/System Application/App/AI/app.json +++ b/src/System Application/App/AI/app.json @@ -89,7 +89,7 @@ "platform": "26.0.0.0", "idRanges": [ { - "from": 7758, + "from": 7757, "to": 7778 } ], diff --git a/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al index 932a32220b..1e03e39e13 100644 --- a/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al +++ b/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al @@ -513,6 +513,7 @@ codeunit 7772 "Azure OpenAI Impl" [NonDebuggable] local procedure SendRequest(ModelType: Enum "AOAI Model Type"; AOAIAuthorization: Codeunit "AOAI Authorization"; Payload: Text; var AOAIOperationResponse: Codeunit "AOAI Operation Response"; CallerModuleInfo: ModuleInfo) var + CopilotNotifications: Codeunit "Copilot Notifications"; ALCopilotAuthorization: DotNet ALCopilotAuthorization; ALCopilotCapability: DotNet ALCopilotCapability; ALCopilotFunctions: DotNet ALCopilotFunctions; @@ -548,6 +549,9 @@ codeunit 7772 "Azure OpenAI Impl" Error := GetLastErrorText(); AOAIOperationResponse.SetOperationResponse(ALCopilotOperationResponse.IsSuccess(), ALCopilotOperationResponse.StatusCode(), ALCopilotOperationResponse.Result(), Error); + if AOAIOperationResponse.GetStatusCode() = 402 then + CopilotNotifications.CheckAIQuotaAndShowNotification(); + if not ALCopilotOperationResponse.IsSuccess() then Error(GenerateRequestFailedErr); end; diff --git a/src/System Application/App/AI/src/Copilot/CopilotAICapabilities.Page.al b/src/System Application/App/AI/src/Copilot/CopilotAICapabilities.Page.al index 7a7c160902..cfda74d386 100644 --- a/src/System Application/App/AI/src/Copilot/CopilotAICapabilities.Page.al +++ b/src/System Application/App/AI/src/Copilot/CopilotAICapabilities.Page.al @@ -248,6 +248,7 @@ page 7775 "Copilot AI Capabilities" trigger OnOpenPage() var + CopilotNotifications: Codeunit "Copilot Notifications"; EnvironmentInformation: Codeunit "Environment Information"; WithinGeo: Boolean; WithinEUDB: Boolean; @@ -272,10 +273,10 @@ page 7775 "Copilot AI Capabilities" CurrPage.EarlyPreviewCapabilities.Page.SetDataMovement(AllowDataMovement); if not EnvironmentInformation.IsSaaSInfrastructure() then - CopilotCapabilityImpl.ShowCapabilitiesNotAvailableOnPremNotification(); + CopilotNotifications.ShowCapabilitiesNotAvailableOnPremNotification(); if (WithinGeo and not WithinEUDB) and (not AllowDataMovement) then - CopilotCapabilityImpl.ShowPrivacyNoticeDisagreedNotification(); + CopilotNotifications.ShowPrivacyNoticeDisagreedNotification(); CopilotCapabilityImpl.UpdateGuidedExperience(AllowDataMovement); @@ -284,6 +285,11 @@ page 7775 "Copilot AI Capabilities" WithinEUDBArea := WithinEUDB; WithinAOAIServicesInRegionArea := WithinGeo and (not WithinEUDB); WithinAOAIOutOfRegionArea := (not WithinGeo) and (not WithinEUDB); + + if EnvironmentInformation.IsSaaSInfrastructure() then begin + CopilotNotifications.ShowBillingInTheFutureNotification(); + CopilotNotifications.CheckAIQuotaAndShowNotification(); + end; end; local procedure HasEarlyPreviewCapabilities(): Boolean diff --git a/src/System Application/App/AI/src/Copilot/CopilotCapabilityImpl.Codeunit.al b/src/System Application/App/AI/src/Copilot/CopilotCapabilityImpl.Codeunit.al index 58800c420b..366a56b901 100644 --- a/src/System Application/App/AI/src/Copilot/CopilotCapabilityImpl.Codeunit.al +++ b/src/System Application/App/AI/src/Copilot/CopilotCapabilityImpl.Codeunit.al @@ -26,17 +26,11 @@ codeunit 7774 "Copilot Capability Impl" AzureOpenAiTxt: Label 'Azure OpenAI', Locked = true; AlreadyRegisteredErr: Label 'Capability has already been registered.'; NotRegisteredErr: Label 'Copilot capability has not been registered by the module.'; - ReviewPrivacyNoticeLbl: Label 'Review the privacy notice'; - PrivacyNoticeDisagreedNotificationMessageLbl: Label 'To enable Copilot, please review and accept the privacy notice.'; - CapabilitiesNotAvailableOnPremNotificationMessageLbl: Label 'Copilot capabilities published by Microsoft are not available on-premises. You can extend Copilot with custom capabilities and use them on-premises for development purposes only.'; TelemetryRegisteredNewCopilotCapabilityLbl: Label 'New copilot capability registered.', Locked = true; TelemetryModifiedCopilotCapabilityLbl: Label 'Copilot capability modified', Locked = true; TelemetryUnregisteredCopilotCapabilityLbl: Label 'Copilot capability unregistered.', Locked = true; TelemetryActivatedCopilotCapabilityLbl: Label 'Copilot capability activated.', Locked = true; TelemetryDeactivatedCopilotCapabilityLbl: Label 'Copilot capability deactivated.', Locked = true; - NotificationPrivacyNoticeDisagreedLbl: Label 'bd91b436-29ba-4823-824c-fc926c9842c2', Locked = true; - NotificationCapabilitiesNotAvailableOnPremLbl: Label 'ada1592d-9728-485c-897e-8d18e8dd7dee', Locked = true; - procedure RegisterCapability(CopilotCapability: Enum "Copilot Capability"; LearnMoreUrl: Text[2048]; CallerModuleInfo: ModuleInfo) begin @@ -181,34 +175,6 @@ codeunit 7774 "Copilot Capability Impl" GlobalLanguage(SavedGlobalLanguageId); end; - procedure ShowPrivacyNoticeDisagreedNotification() - var - Notification: Notification; - NotificationGuid: Guid; - begin - NotificationGuid := NotificationPrivacyNoticeDisagreedLbl; - Notification.Id(NotificationGuid); - Notification.Message(PrivacyNoticeDisagreedNotificationMessageLbl); - Notification.AddAction(ReviewPrivacyNoticeLbl, Codeunit::"Copilot Capability Impl", 'OpenPrivacyNotice'); - Notification.Send(); - end; - - procedure ShowCapabilitiesNotAvailableOnPremNotification() - var - Notification: Notification; - NotificationGuid: Guid; - begin - NotificationGuid := NotificationCapabilitiesNotAvailableOnPremLbl; - Notification.Id(NotificationGuid); - Notification.Message(CapabilitiesNotAvailableOnPremNotificationMessageLbl); - Notification.Send(); - end; - - procedure OpenPrivacyNotice(Notification: Notification) - begin - Page.Run(Page::"Privacy Notices"); - end; - procedure GetAzureOpenAICategory(): Code[50] begin exit(AzureOpenAiTxt); diff --git a/src/System Application/App/AI/src/Copilot/CopilotNotifications.Codeunit.al b/src/System Application/App/AI/src/Copilot/CopilotNotifications.Codeunit.al new file mode 100644 index 0000000000..f84fec2352 --- /dev/null +++ b/src/System Application/App/AI/src/Copilot/CopilotNotifications.Codeunit.al @@ -0,0 +1,153 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.AI; + +using System; +using System.Privacy; + +codeunit 7757 "Copilot Notifications" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + var + CopilotCapabilityImpl: Codeunit "Copilot Capability Impl"; + ReviewPrivacyNoticeLbl: Label 'Review the privacy notice'; + PrivacyNoticeDisagreedNotificationMessageLbl: Label 'To enable Copilot, please review and accept the privacy notice.'; + CapabilitiesNotAvailableOnPremNotificationMessageLbl: Label 'Copilot capabilities published by Microsoft are not available on-premises. You can extend Copilot with custom capabilities and use them on-premises for development purposes only.'; + NotificationPrivacyNoticeDisagreedLbl: Label 'bd91b436-29ba-4823-824c-fc926c9842c2', Locked = true; + NotificationCapabilitiesNotAvailableOnPremLbl: Label 'ada1592d-9728-485c-897e-8d18e8dd7dee', Locked = true; + BillingInTheFutureNotificationGuidTok: Label 'cb577f99-d252-4de7-a1ab-922ac2af12b7', Locked = true; + BillingInTheFutureNotificationMsg: Label 'By activating AI capabilities, you understand your organization may be billed for its use in the future.'; + BillingInTheFutureLearnMoreLinkLbl: Label 'https://go.microsoft.com/fwlink/?linkid=2302317', Locked = true; + AIQuotaUsedUpNotificationGuidTok: Label 'eced148b-4721-4ff9-b4c8-a8b5b1209692', Locked = true; + AIQuotaUsedUpNotificationMsg: Label 'AI capabilities are currently unavailable because your organization has used up its AI quota.'; + AIQuotaUsedUpLearnMoreLinkLbl: Label 'https://go.microsoft.com/fwlink/?linkid=2302511', Locked = true; + AIQuotaUsedUpAdminMsg: Label 'AI capabilities in Business Central require AI quota.\\Your organization has used up its AI quota, so AI capabilities are currently unavailable.\\Would you like to open the Business Central administration center to learn more about AI quota?'; + AIQuotaNearlyUsedUpNotificationGuidTok: Label '4a15b17c-1f88-4cc6-a342-4300ba400c8a', Locked = true; + AIQuotaNearlyUsedUpNotificationMsg: Label 'The AI quota in this environment is nearly used up. When it is, AI capabilities will be unavailable.'; + AIQuotaNearlyUsedUpLearnMoreLinkLbl: Label 'https://go.microsoft.com/fwlink/?linkid=2302603', Locked = true; + AIQuotaNearlyUsedUpAdminMsg: Label 'AI capabilities in Business Central require AI quota, and your organization has a limited amount remaining.\\When it''s used up, AI capabilities will be unavailable until AI quota is available again.\\Would you like to open the Business Central administration center to learn more about AI quota?'; + BCAdminCenterSaaSLinkTxt: Label '%1/admin', Comment = '%1 - BC url', Locked = true; + LearnMoreLbl: Label 'Learn more', Locked = true; + + procedure CheckAIQuotaAndShowNotification() + var + ALCopilotFunctions: DotNet ALCopilotFunctions; + ALCopilotQuotaDetails: Dotnet ALCopilotQuotaDetails; + begin + ALCopilotQuotaDetails := ALCopilotFunctions.GetCopilotQuotaDetails(); + + if IsNull(ALCopilotQuotaDetails) then + exit; + + if not ALCopilotQuotaDetails.CanConsume() then begin + ShowAIQuotaUsedUpNotification(); + exit; + end; + + if ALCopilotQuotaDetails.HasSetupBilling() then + exit; + + if ALCopilotQuotaDetails.QuotaUsedPercentage() >= 80.0 then + ShowAIQuotaNearlyUsedUpNotification(); + end; + + procedure ShowPrivacyNoticeDisagreedNotification() + var + Notification: Notification; + NotificationGuid: Guid; + begin + NotificationGuid := NotificationPrivacyNoticeDisagreedLbl; + Notification.Id(NotificationGuid); + Notification.Message(PrivacyNoticeDisagreedNotificationMessageLbl); + Notification.AddAction(ReviewPrivacyNoticeLbl, Codeunit::"Copilot Notifications", 'OpenPrivacyNotice'); + Notification.Send(); + end; + + procedure OpenPrivacyNotice(Notification: Notification) + begin + Page.Run(Page::"Privacy Notices"); + end; + + procedure ShowCapabilitiesNotAvailableOnPremNotification() + var + Notification: Notification; + NotificationGuid: Guid; + begin + NotificationGuid := NotificationCapabilitiesNotAvailableOnPremLbl; + Notification.Id(NotificationGuid); + Notification.Message(CapabilitiesNotAvailableOnPremNotificationMessageLbl); + Notification.Send(); + end; + + procedure ShowBillingInTheFutureNotification() + var + BillingInTheFutureNotification: Notification; + begin + BillingInTheFutureNotification.Id := BillingInTheFutureNotificationGuidTok; + BillingInTheFutureNotification.Message := BillingInTheFutureNotificationMsg; + BillingInTheFutureNotification.Scope := NotificationScope::LocalScope; + BillingInTheFutureNotification.AddAction(LearnMoreLbl, Codeunit::"Copilot Notifications", 'ShowBillingInTheFutureLearnMore'); + BillingInTheFutureNotification.Send(); + end; + + procedure ShowBillingInTheFutureLearnMore(BillingInTheFutureNotification: Notification) + begin + Hyperlink(BillingInTheFutureLearnMoreLinkLbl); + end; + + procedure ShowAIQuotaUsedUpNotification() + var + AIQuotaUsedUpNotification: Notification; + begin + AIQuotaUsedUpNotification.Id := AIQuotaUsedUpNotificationGuidTok; + AIQuotaUsedUpNotification.Message := AIQuotaUsedUpNotificationMsg; + AIQuotaUsedUpNotification.Scope := NotificationScope::LocalScope; + AIQuotaUsedUpNotification.AddAction(LearnMoreLbl, Codeunit::"Copilot Notifications", 'ShowAIQuotaUsedUpLearnMore'); + AIQuotaUsedUpNotification.Send(); + end; + + procedure ShowAIQuotaUsedUpLearnMore(AIQuotaUsedUpNotification: Notification) + begin + if CopilotCapabilityImpl.IsAdmin() then begin + if Dialog.Confirm(AIQuotaUsedUpAdminMsg) then + OpenBCAdminCenter(); + end + else + Hyperlink(AIQuotaUsedUpLearnMoreLinkLbl); + end; + + procedure ShowAIQuotaNearlyUsedUpNotification() + var + AIQuotaNearlyUsedUpNotification: Notification; + begin + AIQuotaNearlyUsedUpNotification.Id := AIQuotaNearlyUsedUpNotificationGuidTok; + AIQuotaNearlyUsedUpNotification.Message := AIQuotaNearlyUsedUpNotificationMsg; + AIQuotaNearlyUsedUpNotification.Scope := NotificationScope::LocalScope; + AIQuotaNearlyUsedUpNotification.AddAction(LearnMoreLbl, Codeunit::"Copilot Notifications", 'ShowAIQuotaNearlyUsedUpLearnMore'); + AIQuotaNearlyUsedUpNotification.Send(); + end; + + procedure ShowAIQuotaNearlyUsedUpLearnMore(AIQuotaNearlyUsedUpNotification: Notification) + begin + if CopilotCapabilityImpl.IsAdmin() then begin + if Dialog.Confirm(AIQuotaNearlyUsedUpAdminMsg) then + OpenBCAdminCenter(); + end + else + Hyperlink(AIQuotaNearlyUsedUpLearnMoreLinkLbl); + end; + + local procedure OpenBCAdminCenter() + var + Url: Text; + begin + Url := GetUrl(ClientType::Web); + Url := StrSubstNo(BCAdminCenterSaaSLinkTxt, CopyStr(Url, 1, Url.LastIndexOf('/') - 1)); + Hyperlink(Url); + end; +} \ No newline at end of file diff --git a/src/System Application/App/DotNet Aliases/src/dotnet.al b/src/System Application/App/DotNet Aliases/src/dotnet.al index 3391696606..515b0653b4 100644 --- a/src/System Application/App/DotNet Aliases/src/dotnet.al +++ b/src/System Application/App/DotNet Aliases/src/dotnet.al @@ -2137,6 +2137,10 @@ dotnet type("Microsoft.Dynamics.Nav.Service.CopilotApi.AL.ALCopilotCapability"; ALCopilotCapability) { } + + type("Microsoft.Dynamics.Nav.Service.CopilotApi.AL.ALCopilotQuotaDetails"; ALCopilotQuotaDetails) + { + } } assembly("Microsoft.Dynamics.Nav.DataSearch") {