diff --git a/src/System Application/App/AI/app.json b/src/System Application/App/AI/app.json index aee58d9565..a4de64d7de 100644 --- a/src/System Application/App/AI/app.json +++ b/src/System Application/App/AI/app.json @@ -90,7 +90,7 @@ "idRanges": [ { "from": 7757, - "to": 7778 + "to": 7780 } ], "target": "OnPrem", diff --git a/src/System Application/App/AI/src/Azure AI Document Intelligence/ADIModelType.Enum.al b/src/System Application/App/AI/src/Azure AI Document Intelligence/ADIModelType.Enum.al new file mode 100644 index 0000000000..00df30b255 --- /dev/null +++ b/src/System Application/App/AI/src/Azure AI Document Intelligence/ADIModelType.Enum.al @@ -0,0 +1,29 @@ +// ------------------------------------------------------------------------------------------------ +// 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.DocumentIntelligence; + +/// +/// The supported model types for Azure Document Intelligence. +/// +enum 7779 "ADI Model Type" +{ + Access = Public; + Extensible = false; + + /// + /// Invoice model type. + /// + value(0; Invoice) + { + } + + /// + /// Receipt model type. + /// + value(1; Receipt) + { + } + +} \ No newline at end of file diff --git a/src/System Application/App/AI/src/Azure AI Document Intelligence/AzureDIImpl.Codeunit.al b/src/System Application/App/AI/src/Azure AI Document Intelligence/AzureDIImpl.Codeunit.al new file mode 100644 index 0000000000..cf70ecb106 --- /dev/null +++ b/src/System Application/App/AI/src/Azure AI Document Intelligence/AzureDIImpl.Codeunit.al @@ -0,0 +1,147 @@ +// ------------------------------------------------------------------------------------------------ +// 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.DocumentIntelligence; + +using System.Telemetry; +using System; +using System.AI; + +/// +/// Azure Document Intelligence implementation. +/// +codeunit 7779 "Azure DI Impl." implements "AI Service Name" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + var + CopilotCapabilityImpl: Codeunit "Copilot Capability Impl"; + FeatureTelemetry: Codeunit "Feature Telemetry"; + AzureDocumentIntelligenceCapabilityTok: Label 'ADI', Locked = true; + TelemetryAnalyzeInvoiceFailureLbl: Label 'Analyze invoice failed.', Locked = true; + TelemetryAnalyzeInvoiceCompletedLbl: Label 'Analyze invoice completed.', Locked = true; + TelemetryAnalyzeReceiptFailureLbl: Label 'Analyze receipt failed.', Locked = true; + TelemetryAnalyzeReceiptCompletedLbl: Label 'Analyze receipt completed.', Locked = true; + GenerateRequestFailedErr: Label 'The request did not return a success status code.'; + AzureAiDocumentIntelligenceTxt: Label 'Azure AI Document Intelligence', Locked = true; + CapabilityNotEnabledErr: Label 'Copilot capability ''%1'' has not been enabled. Please contact your system administrator.', Comment = '%1 is the name of the Copilot Capability'; + + procedure SetCopilotCapability(Capability: Enum "Copilot Capability"; CallerModuleInfo: ModuleInfo) + begin + CopilotCapabilityImpl.SetCopilotCapability(Capability, CallerModuleInfo, Enum::"Azure AI Service Type"::"Azure Document Intelligence"); + end; + + procedure RegisterCopilotCapability(CopilotCapability: Enum "Copilot Capability"; CopilotAvailability: Enum "Copilot Availability"; LearnMoreUrl: Text[2048]; CallerModuleInfo: ModuleInfo) + begin + CopilotCapabilityImpl.RegisterCapability(CopilotCapability, CopilotAvailability, Enum::"Azure AI Service Type"::"Azure Document Intelligence", LearnMoreUrl, CallerModuleInfo); + end; + + /// + /// Analyze a single invoice. + /// + /// Data to analyze. + /// The module info of the caller. + /// The analyzed result. + procedure AnalyzeInvoice(Base64Data: Text; CallerModuleInfo: ModuleInfo) Result: Text + var + CustomDimensions: Dictionary of [Text, Text]; + begin + CopilotCapabilityImpl.CheckCapabilitySet(); + if not CopilotCapabilityImpl.IsCapabilityActive(CallerModuleInfo) then + Error(CapabilityNotEnabledErr, CopilotCapabilityImpl.GetCapabilityName()); + + CopilotCapabilityImpl.CheckCapabilityServiceType(Enum::"Azure AI Service Type"::"Azure Document Intelligence"); + CopilotCapabilityImpl.AddTelemetryCustomDimensions(CustomDimensions, CallerModuleInfo); + + if not SendRequest(Base64Data, Enum::"ADI Model Type"::Invoice, CallerModuleInfo, Result) then begin + FeatureTelemetry.LogError('0000OLK', AzureDocumentIntelligenceCapabilityTok, TelemetryAnalyzeInvoiceFailureLbl, GetLastErrorText(), '', Enum::"AL Telemetry Scope"::All, CustomDimensions); + exit; + end; + + FeatureTelemetry.LogUsage('0000OLM', AzureDocumentIntelligenceCapabilityTok, TelemetryAnalyzeInvoiceCompletedLbl, Enum::"AL Telemetry Scope"::All, CustomDimensions); + end; + + /// + /// Analyze a single receipt. + /// + /// Data to analyze. + /// The module info of the caller. + /// The analyzed result. + procedure AnalyzeReceipt(Base64Data: Text; CallerModuleInfo: ModuleInfo) Result: Text + var + CustomDimensions: Dictionary of [Text, Text]; + begin + CopilotCapabilityImpl.CheckCapabilitySet(); + if not CopilotCapabilityImpl.IsCapabilityActive(CallerModuleInfo) then + Error(CapabilityNotEnabledErr, CopilotCapabilityImpl.GetCapabilityName()); + + CopilotCapabilityImpl.AddTelemetryCustomDimensions(CustomDimensions, CallerModuleInfo); + + if not SendRequest(Base64Data, Enum::"ADI Model Type"::Receipt, CallerModuleInfo, Result) then begin + FeatureTelemetry.LogError('0000OLL', AzureDocumentIntelligenceCapabilityTok, TelemetryAnalyzeReceiptFailureLbl, GetLastErrorText(), '', Enum::"AL Telemetry Scope"::All, CustomDimensions); + exit; + end; + + FeatureTelemetry.LogUsage('0000OLN', AzureDocumentIntelligenceCapabilityTok, TelemetryAnalyzeReceiptCompletedLbl, Enum::"AL Telemetry Scope"::All, CustomDimensions); + end; + + [TryFunction] + [NonDebuggable] + local procedure SendRequest(Base64Data: Text; ModelType: Enum "ADI Model Type"; CallerModuleInfo: ModuleInfo; var Result: Text) + var + ALCopilotFunctions: DotNet ALCopilotFunctions; + ALCopilotCapability: DotNet ALCopilotCapability; + ALCopilotResponse: DotNet ALCopilotOperationResponse; + ErrorMsg: Text; + begin + ClearLastError(); + ALCopilotCapability := ALCopilotCapability.ALCopilotCapability(CallerModuleInfo.Publisher(), CallerModuleInfo.Id(), Format(CallerModuleInfo.AppVersion()), AzureDocumentIntelligenceCapabilityTok); + case ModelType of + Enum::"ADI Model Type"::Invoice: + ALCopilotResponse := ALCopilotFunctions.GenerateInvoiceIntelligence(GenerateJsonForSingleInput(Base64Data), ALCopilotCapability); + Enum::"ADI Model Type"::Receipt: + ALCopilotResponse := ALCopilotFunctions.GenerateReceiptIntelligence(GenerateJsonForSingleInput(Base64Data), ALCopilotCapability); + end; + ErrorMsg := ALCopilotResponse.ErrorText(); + if ErrorMsg <> '' then + Error(ErrorMsg); + + if not ALCopilotResponse.IsSuccess() then + Error(GenerateRequestFailedErr); + + Result := ALCopilotResponse.Result(); + end; + + local procedure GenerateJsonForSingleInput(Base64: Text): Text + var + JsonObject: JsonObject; + InputsObject: JsonObject; + InnerObject: JsonObject; + JsonText: Text; + begin + // Create the inner object with the base64Encoded property + InnerObject.Add('base64_encoded', Base64); + // Create the inputs object and add the inner object to it + InputsObject.Add('1', InnerObject); + // Create the main JSON object and add the inputs object to it + JsonObject.Add('inputs', InputsObject); + // Convert the JSON object to text + JsonObject.WriteTo(JsonText); + // Return the JSON text + exit(JsonText); + end; + + procedure GetServiceName(): Text[250] + begin + exit(AzureAiDocumentIntelligenceTxt); + end; + + procedure GetServiceId(): Code[50]; + begin + exit(AzureAiDocumentIntelligenceTxt); + end; + +} \ No newline at end of file diff --git a/src/System Application/App/AI/src/Azure AI Document Intelligence/AzureDocumentIntelligence.Codeunit.al b/src/System Application/App/AI/src/Azure AI Document Intelligence/AzureDocumentIntelligence.Codeunit.al new file mode 100644 index 0000000000..d9ca35a59b --- /dev/null +++ b/src/System Application/App/AI/src/Azure AI Document Intelligence/AzureDocumentIntelligence.Codeunit.al @@ -0,0 +1,75 @@ +// ------------------------------------------------------------------------------------------------ +// 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.DocumentIntelligence; + +using System.AI; + +/// +/// Provides functionality to invoke Azure Document Intelligence services. +/// +codeunit 7780 "Azure Document Intelligence" +{ + Access = Public; + InherentEntitlements = X; + InherentPermissions = X; + + var + AzureDIImpl: Codeunit "Azure DI Impl."; + + /// + /// Analyze the invoice. + /// + /// Data to analyze. + /// The analyzed result. + [Scope('OnPrem')] + procedure AnalyzeInvoice(Base64Data: Text): Text + var + CallerModuleInfo: ModuleInfo; + begin + NavApp.GetCallerModuleInfo(CallerModuleInfo); + exit(AzureDIImpl.AnalyzeInvoice(Base64Data, CallerModuleInfo)); + end; + + /// + /// Analyze the Receipt. + /// + /// Data to analyze. + /// The analyzed result. + [Scope('OnPrem')] + procedure AnalyzeReceipt(Base64Data: Text): Text + var + CallerModuleInfo: ModuleInfo; + begin + NavApp.GetCallerModuleInfo(CallerModuleInfo); + exit(AzureDIImpl.AnalyzeReceipt(Base64Data, CallerModuleInfo)); + end; + + /// + /// Register a capability for Azure Document Intelligence. + /// + /// The capability. + /// The availability. + /// The learn more url. + procedure RegisterCopilotCapability(CopilotCapability: Enum "Copilot Capability"; CopilotAvailability: Enum "Copilot Availability"; LearnMoreUrl: Text[2048]) + var + CallerModuleInfo: ModuleInfo; + begin + NavApp.GetCallerModuleInfo(CallerModuleInfo); + AzureDIImpl.RegisterCopilotCapability(CopilotCapability, CopilotAvailability, LearnMoreUrl, CallerModuleInfo); + end; + + /// + /// Sets the copilot capability that the API is running for. + /// + /// The copilot capability to set. + procedure SetCopilotCapability(CopilotCapability: Enum "Copilot Capability") + var + CallerModuleInfo: ModuleInfo; + begin + NavApp.GetCallerModuleInfo(CallerModuleInfo); + AzureDIImpl.SetCopilotCapability(CopilotCapability, CallerModuleInfo); + end; + +} \ No newline at end of file diff --git a/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAI.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAI.Codeunit.al index 7992af7d3f..21579c9bab 100644 --- a/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAI.Codeunit.al +++ b/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAI.Codeunit.al @@ -270,6 +270,7 @@ codeunit 7771 "Azure OpenAI" NavApp.GetCallerModuleInfo(CallerModuleInfo); AzureOpenAIImpl.SetCopilotCapability(CopilotCapability, CallerModuleInfo); end; + #if not CLEAN24 /// /// Gets the approximate token count for the input. 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 1e03e39e13..d400d41e96 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 @@ -8,11 +8,10 @@ using System; using System.Azure.Identity; using System.Azure.KeyVault; using System.Environment; -using System.Globalization; using System.Privacy; using System.Telemetry; -codeunit 7772 "Azure OpenAI Impl" +codeunit 7772 "Azure OpenAI Impl" implements "AI Service Name" { Access = Internal; InherentEntitlements = X; @@ -20,8 +19,6 @@ codeunit 7772 "Azure OpenAI Impl" Permissions = tabledata "Copilot Settings" = r; var - CopilotSettings: Record "Copilot Settings"; - CopilotCapabilityCU: Codeunit "Copilot Capability"; CopilotCapabilityImpl: Codeunit "Copilot Capability Impl"; ChatCompletionsAOAIAuthorization: Codeunit "AOAI Authorization"; TextCompletionsAOAIAuthorization: Codeunit "AOAI Authorization"; @@ -35,16 +32,10 @@ codeunit 7772 "Azure OpenAI Impl" EmbeddingsFailedWithCodeErr: Label 'Embeddings failed to be generated.'; ChatCompletionsFailedWithCodeErr: Label 'Chat completions failed to be generated.'; AuthenticationNotConfiguredErr: Label 'The authentication was not configured.'; - CopilotNotEnabledErr: Label 'Copilot is not enabled. Please contact your system administrator.'; - CopilotCapabilityNotSetErr: Label 'Copilot capability has not been set.'; CapabilityBackgroundErr: Label 'Microsoft Copilot Capabilities are not allowed in the background.'; - CopilotDisabledForTenantErr: Label 'Copilot is not enabled for the tenant. Please contact your system administrator.'; - CapabilityNotRegisteredErr: Label 'Copilot capability ''%1'' has not been registered by the module.', Comment = '%1 is the name of the Copilot Capability'; - CapabilityNotEnabledErr: Label 'Copilot capability ''%1'' has not been enabled. Please contact your system administrator.', Comment = '%1 is the name of the Copilot Capability'; MessagesMustContainJsonWordWhenResponseFormatIsJsonErr: Label 'The messages must contain the word ''json'' in some form, to use ''response format'' of type ''json_object''.'; EmptyMetapromptErr: Label 'The metaprompt has not been set, please provide a metaprompt.'; MetapromptLoadingErr: Label 'Metaprompt not found.'; - EnabledKeyTok: Label 'AOAI-Enabled', Locked = true; FunctionCallingFunctionNotFoundErr: Label 'Function call not found, %1.', Comment = '%1 is the name of the function'; AllowlistedTenantsAkvKeyTok: Label 'AOAI-Allow-1P-Auth', Locked = true; TelemetryGenerateTextCompletionLbl: Label 'Text completion generated.', Locked = true; @@ -52,95 +43,27 @@ codeunit 7772 "Azure OpenAI Impl" TelemetryGenerateChatCompletionLbl: Label 'Chat Completion generated.', Locked = true; TelemetryChatCompletionToolCallLbl: Label 'Tools called by chat completion.', Locked = true; TelemetryChatCompletionToolUsedLbl: Label 'Tools added to chat completion.', Locked = true; - TelemetrySetCapabilityLbl: Label 'Set Capability', Locked = true; - TelemetryCopilotCapabilityNotRegisteredLbl: Label 'Copilot capability not registered.', Locked = true; - TelemetryIsEnabledLbl: Label 'Is Enabled', Locked = true; - TelemetryUnableToCheckEnvironmentKVTxt: Label 'Unable to check if environment is allowed to run AOAI.', Locked = true; - TelemetryEnvironmentNotAllowedtoRunCopilotTxt: Label 'Copilot is not allowed on this environment.', Locked = true; TelemetryProhibitedCharactersTxt: Label 'Prohibited characters removed from the prompt.', Locked = true; TelemetryTokenCountLbl: Label 'Metaprompt token count: %1, Prompt token count: %2, Total token count: %3', Comment = '%1 is the number of tokens in the metaprompt, %2 is the number of tokens in the prompt, %3 is the total number of tokens', Locked = true; TelemetryMetapromptRetrievalErr: Label 'Unable to retrieve metaprompt from Azure Key Vault.', Locked = true; TelemetryFunctionCallingFailedErr: Label 'Function calling failed for function: %1', Comment = '%1 is the name of the function', Locked = true; TelemetryEmptyTenantIdErr: Label 'Empty or malformed tenant ID.', Locked = true; TelemetryTenantAllowlistedMsg: Label 'Current tenant allowlisted for first party auth.', Locked = true; + AzureOpenAiTxt: Label 'Azure OpenAI', Locked = true; procedure IsEnabled(Capability: Enum "Copilot Capability"; CallerModuleInfo: ModuleInfo): Boolean begin - exit(IsEnabled(Capability, false, CallerModuleInfo)); + CopilotCapabilityImpl.IsCapabilityEnabled(Capability, CallerModuleInfo); end; procedure IsEnabled(Capability: Enum "Copilot Capability"; Silent: Boolean; CallerModuleInfo: ModuleInfo): Boolean - var - CopilotNotAvailable: Page "Copilot Not Available"; begin - if not IsTenantAllowed() then begin - if not Silent then - Error(CopilotDisabledForTenantErr); // Copilot capabilities cannot be run on this environment. - - exit(false); - end; - - if not CopilotCapabilityCU.IsCapabilityActive(Capability, CallerModuleInfo.Id()) then begin - if not Silent then begin - CopilotNotAvailable.SetCopilotCapability(Capability); - CopilotNotAvailable.Run(); - end; - - exit(false); - end; - - exit(CheckPrivacyNoticeState(Silent, Capability)); + CopilotCapabilityImpl.IsCapabilityEnabled(Capability, Silent, CallerModuleInfo); end; - [NonDebuggable] - local procedure IsTenantAllowed(): Boolean - var - EnvironmentInformation: Codeunit "Environment Information"; - AzureKeyVault: Codeunit "Azure Key Vault"; - AzureAdTenant: Codeunit "Azure AD Tenant"; - ModuleInfo: ModuleInfo; - BlockList: Text; - begin - if not EnvironmentInformation.IsSaaSInfrastructure() then - exit(true); - - NavApp.GetCurrentModuleInfo(ModuleInfo); - if ModuleInfo.Publisher <> 'Microsoft' then - exit(true); - - if (not AzureKeyVault.GetAzureKeyVaultSecret(EnabledKeyTok, BlockList)) or (BlockList.Trim() = '') then begin - FeatureTelemetry.LogError('0000KYC', CopilotCapabilityImpl.GetAzureOpenAICategory(), TelemetryIsEnabledLbl, TelemetryUnableToCheckEnvironmentKVTxt); - exit(false); - end; - - if BlockList.Contains(AzureAdTenant.GetAadTenantId()) then begin - FeatureTelemetry.LogError('0000LFP', CopilotCapabilityImpl.GetAzureOpenAICategory(), TelemetryIsEnabledLbl, TelemetryEnvironmentNotAllowedtoRunCopilotTxt); - exit(false); - end; - - exit(true); - end; - - local procedure CheckPrivacyNoticeState(Silent: Boolean; Capability: Enum "Copilot Capability"): Boolean - var - PrivacyNotice: Codeunit "Privacy Notice"; - CopilotNotAvailable: Page "Copilot Not Available"; + procedure SetCopilotCapability(Capability: Enum "Copilot Capability"; CallerModuleInfo: ModuleInfo) begin - case PrivacyNotice.GetPrivacyNoticeApprovalState(CopilotCapabilityImpl.GetAzureOpenAICategory(), false) of - Enum::"Privacy Notice Approval State"::Agreed: - exit(true); - Enum::"Privacy Notice Approval State"::Disagreed: - begin - if not Silent then begin - CopilotNotAvailable.SetCopilotCapability(Capability); - CopilotNotAvailable.Run(); - end; - - exit(false); - end; - else - exit(true); - end; + CopilotCapabilityImpl.SetCopilotCapability(Capability, CallerModuleInfo, Enum::"Azure AI Service Type"::"Azure OpenAI"); end; procedure IsAuthorizationConfigured(ModelType: Enum "AOAI Model Type"; CallerModule: ModuleInfo): Boolean @@ -239,11 +162,11 @@ codeunit 7772 "Azure OpenAI Impl" begin GuiCheck(TextCompletionsAOAIAuthorization); - CheckCapabilitySet(); - CheckEnabled(CallerModuleInfo); + CopilotCapabilityImpl.CheckCapabilitySet(); + CopilotCapabilityImpl.CheckEnabled(CallerModuleInfo); CheckAuthorizationEnabled(TextCompletionsAOAIAuthorization, CallerModuleInfo); - AddTelemetryCustomDimensions(CustomDimensions, CallerModuleInfo); + CopilotCapabilityImpl.AddTelemetryCustomDimensions(CustomDimensions, CallerModuleInfo); CheckTextCompletionMetaprompt(Metaprompt, CustomDimensions); UnwrappedPrompt := Metaprompt.Unwrap() + Prompt.Unwrap(); @@ -256,11 +179,11 @@ codeunit 7772 "Azure OpenAI Impl" SendTokenCountTelemetry(AOAIToken.GetGPT4TokenCount(Metaprompt), AOAIToken.GetGPT4TokenCount(Prompt), CustomDimensions); if not SendRequest(Enum::"AOAI Model Type"::"Text Completions", TextCompletionsAOAIAuthorization, PayloadText, AOAIOperationResponse, CallerModuleInfo) then begin - FeatureTelemetry.LogError('0000KVD', CopilotCapabilityImpl.GetAzureOpenAICategory(), TelemetryGenerateTextCompletionLbl, CompletionsFailedWithCodeErr, '', Enum::"AL Telemetry Scope"::All, CustomDimensions); + FeatureTelemetry.LogError('0000KVD', GetAzureOpenAICategory(), TelemetryGenerateTextCompletionLbl, CompletionsFailedWithCodeErr, '', Enum::"AL Telemetry Scope"::All, CustomDimensions); exit; end; - FeatureTelemetry.LogUsage('0000KVL', CopilotCapabilityImpl.GetAzureOpenAICategory(), TelemetryGenerateTextCompletionLbl, Enum::"AL Telemetry Scope"::All, CustomDimensions); + FeatureTelemetry.LogUsage('0000KVL', GetAzureOpenAICategory(), TelemetryGenerateTextCompletionLbl, Enum::"AL Telemetry Scope"::All, CustomDimensions); Result := AOAIOperationResponse.GetResult(); end; @@ -273,21 +196,21 @@ codeunit 7772 "Azure OpenAI Impl" begin GuiCheck(EmbeddingsAOAIAuthorization); - CheckCapabilitySet(); - CheckEnabled(CallerModuleInfo); + CopilotCapabilityImpl.CheckCapabilitySet(); + CopilotCapabilityImpl.CheckEnabled(CallerModuleInfo); CheckAuthorizationEnabled(EmbeddingsAOAIAuthorization, CallerModuleInfo); Payload.Add('input', Input.Unwrap()); Payload.WriteTo(PayloadText); - AddTelemetryCustomDimensions(CustomDimensions, CallerModuleInfo); + CopilotCapabilityImpl.AddTelemetryCustomDimensions(CustomDimensions, CallerModuleInfo); SendTokenCountTelemetry(0, AOAIToken.GetAdaTokenCount(Input), CustomDimensions); if not SendRequest(Enum::"AOAI Model Type"::Embeddings, EmbeddingsAOAIAuthorization, PayloadText, AOAIOperationResponse, CallerModuleInfo) then begin - FeatureTelemetry.LogError('0000KVE', CopilotCapabilityImpl.GetAzureOpenAICategory(), TelemetryGenerateEmbeddingLbl, EmbeddingsFailedWithCodeErr, '', Enum::"AL Telemetry Scope"::All, CustomDimensions); + FeatureTelemetry.LogError('0000KVE', GetAzureOpenAICategory(), TelemetryGenerateEmbeddingLbl, EmbeddingsFailedWithCodeErr, '', Enum::"AL Telemetry Scope"::All, CustomDimensions); exit; end; - FeatureTelemetry.LogUsage('0000KVM', CopilotCapabilityImpl.GetAzureOpenAICategory(), TelemetryGenerateEmbeddingLbl, Enum::"AL Telemetry Scope"::All, CustomDimensions); + FeatureTelemetry.LogUsage('0000KVM', GetAzureOpenAICategory(), TelemetryGenerateEmbeddingLbl, Enum::"AL Telemetry Scope"::All, CustomDimensions); exit(ProcessEmbeddingResponse(AOAIOperationResponse)); end; @@ -327,10 +250,10 @@ codeunit 7772 "Azure OpenAI Impl" begin GuiCheck(ChatCompletionsAOAIAuthorization); - CheckCapabilitySet(); - CheckEnabled(CallerModuleInfo); + CopilotCapabilityImpl.CheckCapabilitySet(); + CopilotCapabilityImpl.CheckEnabled(CallerModuleInfo); CheckAuthorizationEnabled(ChatCompletionsAOAIAuthorization, CallerModuleInfo); - AddTelemetryCustomDimensions(CustomDimensions, CallerModuleInfo); + CopilotCapabilityImpl.AddTelemetryCustomDimensions(CustomDimensions, CallerModuleInfo); AOAIChatCompletionParams.AddChatCompletionsParametersToPayload(Payload); Payload.Add('messages', ChatMessages.AssembleHistory(MetapromptTokenCount, PromptTokenCount)); @@ -356,13 +279,13 @@ codeunit 7772 "Azure OpenAI Impl" SendTokenCountTelemetry(MetapromptTokenCount, PromptTokenCount, CustomDimensions); if not SendRequest(Enum::"AOAI Model Type"::"Chat Completions", ChatCompletionsAOAIAuthorization, PayloadText, AOAIOperationResponse, CallerModuleInfo) then begin - FeatureTelemetry.LogError('0000KVF', CopilotCapabilityImpl.GetAzureOpenAICategory(), TelemetryGenerateChatCompletionLbl, ChatCompletionsFailedWithCodeErr, '', Enum::"AL Telemetry Scope"::All, CustomDimensions); + FeatureTelemetry.LogError('0000KVF', GetAzureOpenAICategory(), TelemetryGenerateChatCompletionLbl, ChatCompletionsFailedWithCodeErr, '', Enum::"AL Telemetry Scope"::All, CustomDimensions); exit; end; ProcessChatCompletionResponse(ChatMessages, AOAIOperationResponse, CallerModuleInfo); - FeatureTelemetry.LogUsage('0000KVN', CopilotCapabilityImpl.GetAzureOpenAICategory(), TelemetryGenerateChatCompletionLbl, Enum::"AL Telemetry Scope"::All, CustomDimensions); + FeatureTelemetry.LogUsage('0000KVN', GetAzureOpenAICategory(), TelemetryGenerateChatCompletionLbl, Enum::"AL Telemetry Scope"::All, CustomDimensions); if (AOAIOperationResponse.GetFunctionResponses().Count() > 0) and (ChatMessages.GetToolInvokePreference() = Enum::"AOAI Tool Invoke Preference"::Automatic) then GenerateChatCompletion(ChatMessages, AOAIChatCompletionParams, AOAIOperationResponse, CallerModuleInfo); @@ -418,10 +341,10 @@ codeunit 7772 "Azure OpenAI Impl" AOAIOperationResponse.AddFunctionResponse(AOAIFunctionResponse); end; - AddTelemetryCustomDimensions(CustomDimensions, CallerModuleInfo); + CopilotCapabilityImpl.AddTelemetryCustomDimensions(CustomDimensions, CallerModuleInfo); foreach AOAIFunctionResponse in AOAIOperationResponse.GetFunctionResponses() do if not AOAIFunctionResponse.IsSuccess() then - FeatureTelemetry.LogError('0000MTB', CopilotCapabilityImpl.GetAzureOpenAICategory(), StrSubstNo(TelemetryFunctionCallingFailedErr, AOAIFunctionResponse.GetFunctionName()), AOAIFunctionResponse.GetError(), AOAIFunctionResponse.GetErrorCallstack(), Enum::"AL Telemetry Scope"::All, CustomDimensions); + FeatureTelemetry.LogError('0000MTB', GetAzureOpenAICategory(), StrSubstNo(TelemetryFunctionCallingFailedErr, AOAIFunctionResponse.GetFunctionName()), AOAIFunctionResponse.GetError(), AOAIFunctionResponse.GetErrorCallstack(), Enum::"AL Telemetry Scope"::All, CustomDimensions); if ChatMessages.GetToolInvokePreference() in [Enum::"AOAI Tool Invoke Preference"::"Invoke Tools Only", Enum::"AOAI Tool Invoke Preference"::Automatic] then AOAIOperationResponse.AppendFunctionResponsesToChatMessages(ChatMessages); @@ -531,7 +454,7 @@ codeunit 7772 "Azure OpenAI Impl" ALCopilotAuthorization := ALCopilotAuthorization.Create(AOAIAuthorization.GetEndpoint(), AOAIAuthorization.GetDeployment(), AOAIAuthorization.GetApiKey()); end; - ALCopilotCapability := ALCopilotCapability.ALCopilotCapability(CallerModuleInfo.Publisher(), CallerModuleInfo.Id(), Format(CallerModuleInfo.AppVersion()), GetCapabilityName()); + ALCopilotCapability := ALCopilotCapability.ALCopilotCapability(CallerModuleInfo.Publisher(), CallerModuleInfo.Id(), Format(CallerModuleInfo.AppVersion()), CopilotCapabilityImpl.GetCapabilityName()); case ModelType of Enum::"AOAI Model Type"::"Text Completions": @@ -556,22 +479,6 @@ codeunit 7772 "Azure OpenAI Impl" Error(GenerateRequestFailedErr); end; - local procedure GetCapabilityName(): Text - var - CapabilityIndex: Integer; - CapabilityName: Text; - begin - CheckCapabilitySet(); - - CapabilityIndex := CopilotSettings.Capability.Ordinals.IndexOf(CopilotSettings.Capability.AsInteger()); - CapabilityName := CopilotSettings.Capability.Names.Get(CapabilityIndex); - - if CapabilityName.Trim() = '' then - exit(Format(CopilotSettings.Capability, 0, 9)); - - exit(CapabilityName); - end; - local procedure SendTokenCountTelemetry(Metaprompt: Integer; Prompt: Integer; CustomDimensions: Dictionary of [Text, Text]) begin Telemetry.LogMessage('0000LT4', StrSubstNo(TelemetryTokenCountLbl, Metaprompt, Prompt, Metaprompt + Prompt), Verbosity::Normal, DataClassification::OrganizationIdentifiableInformation, Enum::"AL Telemetry Scope"::All, CustomDimensions); @@ -588,70 +495,12 @@ codeunit 7772 "Azure OpenAI Impl" Error(CapabilityBackgroundErr); end; - local procedure AddTelemetryCustomDimensions(var CustomDimensions: Dictionary of [Text, Text]; CallerModuleInfo: ModuleInfo) - var - Language: Codeunit Language; - SavedGlobalLanguageId: Integer; - begin - SavedGlobalLanguageId := GlobalLanguage(); - GlobalLanguage(Language.GetDefaultApplicationLanguageId()); - - CustomDimensions.Add('Capability', Format(CopilotSettings.Capability)); - CustomDimensions.Add('AppId', Format(CopilotSettings."App Id")); - CustomDimensions.Add('Publisher', CallerModuleInfo.Publisher); - CustomDimensions.Add('UserLanguage', Format(GlobalLanguage())); - - GlobalLanguage(SavedGlobalLanguageId); - end; - - procedure SetCopilotCapability(Capability: Enum "Copilot Capability"; CallerModuleInfo: ModuleInfo) - var - CopilotTelemetry: Codeunit "Copilot Telemetry"; - Language: Codeunit Language; - SavedGlobalLanguageId: Integer; - CustomDimensions: Dictionary of [Text, Text]; - ErrorMessage: Text; - begin - if not CopilotCapabilityCU.IsCapabilityRegistered(Capability, CallerModuleInfo.Id()) then begin - SavedGlobalLanguageId := GlobalLanguage(); - GlobalLanguage(Language.GetDefaultApplicationLanguageId()); - CustomDimensions.Add('Capability', Format(Capability)); - CustomDimensions.Add('AppId', Format(CallerModuleInfo.Id())); - GlobalLanguage(SavedGlobalLanguageId); - - FeatureTelemetry.LogError('0000LFN', CopilotCapabilityImpl.GetAzureOpenAICategory(), TelemetrySetCapabilityLbl, TelemetryCopilotCapabilityNotRegisteredLbl, '', Enum::"AL Telemetry Scope"::All, CustomDimensions); - ErrorMessage := StrSubstNo(CapabilityNotRegisteredErr, Capability); - Error(ErrorMessage); - end; - - CopilotSettings.ReadIsolation(IsolationLevel::ReadCommitted); - CopilotSettings.SetLoadFields(Status); - CopilotSettings.Get(Capability, CallerModuleInfo.Id()); - if CopilotSettings.Status = Enum::"Copilot Status"::Inactive then begin - ErrorMessage := StrSubstNo(CapabilityNotEnabledErr, Capability); - Error(ErrorMessage); - end; - CopilotTelemetry.SetCopilotCapability(Capability, CallerModuleInfo.Id()); - end; - - local procedure CheckEnabled(CallerModuleInfo: ModuleInfo) - begin - if not IsEnabled(CopilotSettings.Capability, true, CallerModuleInfo) then - Error(CopilotNotEnabledErr); - end; - local procedure CheckAuthorizationEnabled(AOAIAuthorization: Codeunit "AOAI Authorization"; CallerModuleInfo: ModuleInfo) begin if not AOAIAuthorization.IsConfigured(CallerModuleInfo) then Error(AuthenticationNotConfiguredErr); end; - local procedure CheckCapabilitySet() - begin - if CopilotSettings.Capability.AsInteger() = 0 then - Error(CopilotCapabilityNotSetErr); - end; - [NonDebuggable] procedure RemoveProhibitedCharacters(Prompt: Text) Result: Text begin @@ -692,7 +541,7 @@ codeunit 7772 "Azure OpenAI Impl" ModuleInfo: ModuleInfo; begin if Metaprompt.Unwrap().Trim() = '' then begin - FeatureTelemetry.LogError('0000LO8', CopilotCapabilityImpl.GetAzureOpenAICategory(), TelemetryGenerateTextCompletionLbl, EmptyMetapromptErr, '', Enum::"AL Telemetry Scope"::All, CustomDimensions); + FeatureTelemetry.LogError('0000LO8', GetAzureOpenAICategory(), TelemetryGenerateTextCompletionLbl, EmptyMetapromptErr, '', Enum::"AL Telemetry Scope"::All, CustomDimensions); NavApp.GetCurrentModuleInfo(ModuleInfo); if ModuleInfo.Publisher = 'Microsoft' then @@ -751,15 +600,39 @@ codeunit 7772 "Azure OpenAI Impl" EntraTenantIdAsText := AzureAdTenant.GetAadTenantId(); if (EntraTenantIdAsText = '') or not Evaluate(EntraTenantIdAsGuid, EntraTenantIdAsText) or IsNullGuid(EntraTenantIdAsGuid) then begin - Session.LogMessage('0000MLN', TelemetryEmptyTenantIdErr, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CopilotCapabilityImpl.GetAzureOpenAICategory()); + Session.LogMessage('0000MLN', TelemetryEmptyTenantIdErr, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', GetAzureOpenAICategory()); exit(false); end; if not AllowlistedTenants.Contains(EntraTenantIdAsText) then exit(false); - Session.LogMessage('0000MLE', TelemetryTenantAllowlistedMsg, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CopilotCapabilityImpl.GetAzureOpenAICategory()); + Session.LogMessage('0000MLE', TelemetryTenantAllowlistedMsg, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', GetAzureOpenAICategory()); exit(true); end; + procedure GetAzureOpenAICategory(): Code[50] + begin + exit(AzureOpenAiTxt); + end; + + procedure GetServiceName(): Text[250]; + begin + exit(AzureOpenAiTxt); + end; + + procedure GetServiceId(): Code[50]; + begin + exit(AzureOpenAiTxt); + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Privacy Notice", 'OnRegisterPrivacyNotices', '', false, false)] + local procedure CreatePrivacyNoticeRegistrations(var TempPrivacyNotice: Record "Privacy Notice" temporary) + begin + TempPrivacyNotice.Init(); + TempPrivacyNotice.ID := GetAzureOpenAICategory(); + TempPrivacyNotice."Integration Service Name" := GetServiceName(); + if not TempPrivacyNotice.Insert() then; + end; + } \ No newline at end of file diff --git a/src/System Application/App/AI/src/Copilot/AzureAIServiceType.Enum.al b/src/System Application/App/AI/src/Copilot/AzureAIServiceType.Enum.al new file mode 100644 index 0000000000..3705990822 --- /dev/null +++ b/src/System Application/App/AI/src/Copilot/AzureAIServiceType.Enum.al @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------------------------------ +// 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.AI.DocumentIntelligence; + +/// +/// The supported service types for Azure AI. +/// +enum 7778 "Azure AI Service Type" implements "AI Service Name" +{ + Access = Public; + Extensible = false; + + /// + /// Azure OpenAI service type. + /// + value(0; "Azure OpenAI") + { + Caption = 'Azure OpenAI'; + Implementation = "AI Service Name" = "Azure OpenAI Impl"; + } + + /// + /// Azure Document Intelligence service type. + /// + value(1; "Azure Document Intelligence") + { + Caption = 'Azure Document Intelligence'; + Implementation = "AI Service Name" = "Azure DI Impl."; + } +} \ No newline at end of file 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 cfda74d386..2ecd9c7ed1 100644 --- a/src/System Application/App/AI/src/Copilot/CopilotAICapabilities.Page.al +++ b/src/System Application/App/AI/src/Copilot/CopilotAICapabilities.Page.al @@ -257,7 +257,7 @@ page 7775 "Copilot AI Capabilities" CopilotCapabilityImpl.CheckGeoAndEUDB(WithinGeo, WithinEUDB); - case PrivacyNotice.GetPrivacyNoticeApprovalState(CopilotCapabilityImpl.GetAzureOpenAICategory(), false) of + case PrivacyNotice.GetPrivacyNoticeApprovalState(AzureOpenAIImpl.GetAzureOpenAICategory(), false) of Enum::"Privacy Notice Approval State"::Agreed: AllowDataMovement := true; Enum::"Privacy Notice Approval State"::Disagreed: @@ -297,15 +297,16 @@ page 7775 "Copilot AI Capabilities" CopilotSettings: Record "Copilot Settings"; begin CopilotSettings.SetRange(Availability, Enum::"Copilot Availability"::"Early Preview"); + CopilotSettings.SetRange("Service Type", Enum::"Azure AI Service Type"::"Azure OpenAI"); exit(not CopilotSettings.IsEmpty()); end; local procedure UpdateAllowDataMovement() begin if AllowDataMovement then - PrivacyNotice.SetApprovalState(CopilotCapabilityImpl.GetAzureOpenAICategory(), Enum::"Privacy Notice Approval State"::Agreed) + PrivacyNotice.SetApprovalState(AzureOpenAIImpl.GetAzureOpenAICategory(), Enum::"Privacy Notice Approval State"::Agreed) else - PrivacyNotice.SetApprovalState(CopilotCapabilityImpl.GetAzureOpenAICategory(), Enum::"Privacy Notice Approval State"::Disagreed); + PrivacyNotice.SetApprovalState(AzureOpenAIImpl.GetAzureOpenAICategory(), Enum::"Privacy Notice Approval State"::Disagreed); CurrPage.GenerallyAvailableCapabilities.Page.SetDataMovement(AllowDataMovement); CurrPage.PreviewCapabilities.Page.SetDataMovement(AllowDataMovement); @@ -319,6 +320,7 @@ page 7775 "Copilot AI Capabilities" end; var + AzureOpenAIImpl: Codeunit "Azure OpenAI Impl"; CopilotCapabilityImpl: Codeunit "Copilot Capability Impl"; PrivacyNotice: Codeunit "Privacy Notice"; WithinEUDBArea: Boolean; diff --git a/src/System Application/App/AI/src/Copilot/CopilotCapEarlyPreview.Page.al b/src/System Application/App/AI/src/Copilot/CopilotCapEarlyPreview.Page.al index f322325fb9..f044a41d1f 100644 --- a/src/System Application/App/AI/src/Copilot/CopilotCapEarlyPreview.Page.al +++ b/src/System Application/App/AI/src/Copilot/CopilotCapEarlyPreview.Page.al @@ -16,7 +16,7 @@ page 7770 "Copilot Cap. Early Preview" Editable = false; Extensible = false; SourceTable = "Copilot Settings"; - SourceTableView = where(Availability = const("Early Preview")); + SourceTableView = where(Availability = const("Early Preview"), "Service Type" = const("Azure AI Service Type"::"Azure OpenAI")); Permissions = tabledata "Copilot Settings" = rm; InherentEntitlements = X; InherentPermissions = X; diff --git a/src/System Application/App/AI/src/Copilot/CopilotCapabilitiesGA.Page.al b/src/System Application/App/AI/src/Copilot/CopilotCapabilitiesGA.Page.al index b76b8e4350..241e75d182 100644 --- a/src/System Application/App/AI/src/Copilot/CopilotCapabilitiesGA.Page.al +++ b/src/System Application/App/AI/src/Copilot/CopilotCapabilitiesGA.Page.al @@ -16,7 +16,7 @@ page 7774 "Copilot Capabilities GA" Editable = false; Extensible = false; SourceTable = "Copilot Settings"; - SourceTableView = where(Availability = const("Generally Available")); + SourceTableView = where(Availability = const("Generally Available"), "Service Type" = const("Azure AI Service Type"::"Azure OpenAI")); Permissions = tabledata "Copilot Settings" = rm; InherentEntitlements = X; InherentPermissions = X; diff --git a/src/System Application/App/AI/src/Copilot/CopilotCapabilitiesPreview.Page.al b/src/System Application/App/AI/src/Copilot/CopilotCapabilitiesPreview.Page.al index 38e90454e1..5c528a18a6 100644 --- a/src/System Application/App/AI/src/Copilot/CopilotCapabilitiesPreview.Page.al +++ b/src/System Application/App/AI/src/Copilot/CopilotCapabilitiesPreview.Page.al @@ -16,7 +16,7 @@ page 7773 "Copilot Capabilities Preview" Editable = false; Extensible = false; SourceTable = "Copilot Settings"; - SourceTableView = where(Availability = const(Preview)); + SourceTableView = where(Availability = const(Preview), "Service Type" = const("Azure AI Service Type"::"Azure OpenAI")); Permissions = tabledata "Copilot Settings" = rm; InherentEntitlements = X; InherentPermissions = X; 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 366a56b901..1989341ee8 100644 --- a/src/System Application/App/AI/src/Copilot/CopilotCapabilityImpl.Codeunit.al +++ b/src/System Application/App/AI/src/Copilot/CopilotCapabilityImpl.Codeunit.al @@ -6,6 +6,7 @@ namespace System.AI; using System; using System.Azure.Identity; +using System.Azure.KeyVault; using System.Environment; using System.Environment.Configuration; using System.Globalization; @@ -21,11 +22,23 @@ codeunit 7774 "Copilot Capability Impl" Permissions = tabledata "Copilot Settings" = rimd; var + CopilotSettings: Record "Copilot Settings"; + FeatureTelemetry: Codeunit "Feature Telemetry"; Telemetry: Codeunit Telemetry; CopilotCategoryLbl: Label 'Copilot', Locked = true; - AzureOpenAiTxt: Label 'Azure OpenAI', Locked = true; AlreadyRegisteredErr: Label 'Capability has already been registered.'; NotRegisteredErr: Label 'Copilot capability has not been registered by the module.'; + CapabilityNotRegisteredErr: Label 'Copilot capability ''%1'' has not been registered by the module.', Comment = '%1 is the name of the Copilot Capability'; + CapabilityNotEnabledErr: Label 'Copilot capability ''%1'' has not been enabled. Please contact your system administrator.', Comment = '%1 is the name of the Copilot Capability'; + TelemetrySetCapabilityLbl: Label 'Set Capability', Locked = true; + CopilotNotEnabledErr: Label 'Copilot is not enabled. Please contact your system administrator.'; + CopilotCapabilityNotSetErr: Label 'Copilot capability has not been set.'; + CopilotDisabledForTenantErr: Label 'Copilot is not enabled for the tenant. Please contact your system administrator.'; + TelemetryIsEnabledLbl: Label 'Is Enabled', Locked = true; + TelemetryUnableToCheckEnvironmentKVTxt: Label 'Unable to check if environment is allowed to run AOAI.', Locked = true; + TelemetryEnvironmentNotAllowedtoRunCopilotTxt: Label 'Copilot is not allowed on this environment.', Locked = true; + EnabledKeyTok: Label 'AOAI-Enabled', Locked = true; + TelemetryCopilotCapabilityNotRegisteredLbl: Label 'Copilot capability not registered.', Locked = true; TelemetryRegisteredNewCopilotCapabilityLbl: Label 'New copilot capability registered.', Locked = true; TelemetryModifiedCopilotCapabilityLbl: Label 'Copilot capability modified', Locked = true; TelemetryUnregisteredCopilotCapabilityLbl: Label 'Copilot capability unregistered.', Locked = true; @@ -38,19 +51,25 @@ codeunit 7774 "Copilot Capability Impl" end; procedure RegisterCapability(CopilotCapability: Enum "Copilot Capability"; CopilotAvailability: Enum "Copilot Availability"; LearnMoreUrl: Text[2048]; CallerModuleInfo: ModuleInfo) + begin + RegisterCapability(CopilotCapability, CopilotAvailability, Enum::"Azure AI Service Type"::"Azure OpenAI", LearnMoreUrl, CallerModuleInfo); + end; + + procedure RegisterCapability(CopilotCapability: Enum "Copilot Capability"; CopilotAvailability: Enum "Copilot Availability"; AzureAIServiceType: Enum "Azure AI Service Type"; LearnMoreUrl: Text[2048]; CallerModuleInfo: ModuleInfo) var - CopilotSettings: Record "Copilot Settings"; CustomDimensions: Dictionary of [Text, Text]; begin if IsCapabilityRegistered(CopilotCapability, CallerModuleInfo) then Error(AlreadyRegisteredErr); + Clear(CopilotSettings); CopilotSettings.Init(); CopilotSettings.Capability := CopilotCapability; CopilotSettings."App Id" := CallerModuleInfo.Id(); CopilotSettings.Publisher := CopyStr(CallerModuleInfo.Publisher, 1, MaxStrLen(CopilotSettings.Publisher)); CopilotSettings.Availability := CopilotAvailability; CopilotSettings."Learn More Url" := LearnMoreUrl; + CopilotSettings."Service Type" := AzureAIServiceType; if CopilotSettings.Availability = Enum::"Copilot Availability"::"Early Preview" then CopilotSettings.Status := Enum::"Copilot Status"::Inactive else @@ -62,9 +81,40 @@ codeunit 7774 "Copilot Capability Impl" Telemetry.LogMessage('0000LDV', TelemetryRegisteredNewCopilotCapabilityLbl, Verbosity::Normal, DataClassification::OrganizationIdentifiableInformation, Enum::"AL Telemetry Scope"::All, CustomDimensions); end; + procedure SetCopilotCapability(Capability: Enum "Copilot Capability"; CallerModuleInfo: ModuleInfo; AIServiceType: Enum "Azure AI Service Type") + var + CopilotTelemetry: Codeunit "Copilot Telemetry"; + Language: Codeunit Language; + IAIServicename: Interface "AI Service Name"; + SavedGlobalLanguageId: Integer; + CustomDimensions: Dictionary of [Text, Text]; + ErrorMessage: Text; + begin + if not IsCapabilityRegistered(Capability, CallerModuleInfo.Id()) then begin + SavedGlobalLanguageId := GlobalLanguage(); + GlobalLanguage(Language.GetDefaultApplicationLanguageId()); + CustomDimensions.Add('Capability', Format(Capability)); + CustomDimensions.Add('AppId', Format(CallerModuleInfo.Id())); + GlobalLanguage(SavedGlobalLanguageId); + + IAIServicename := AIServiceType; + FeatureTelemetry.LogError('0000LFN', IAIServicename.GetServiceName(), TelemetrySetCapabilityLbl, TelemetryCopilotCapabilityNotRegisteredLbl, '', Enum::"AL Telemetry Scope"::All, CustomDimensions); + ErrorMessage := StrSubstNo(CapabilityNotRegisteredErr, Capability); + Error(ErrorMessage); + end; + + CopilotSettings.ReadIsolation(IsolationLevel::ReadCommitted); + CopilotSettings.SetLoadFields(Status); + CopilotSettings.Get(Capability, CallerModuleInfo.Id()); + if CopilotSettings.Status = Enum::"Copilot Status"::Inactive then begin + ErrorMessage := StrSubstNo(CapabilityNotEnabledErr, Capability); + Error(ErrorMessage); + end; + CopilotTelemetry.SetCopilotCapability(Capability, CallerModuleInfo.Id()); + end; + procedure ModifyCapability(CopilotCapability: Enum "Copilot Capability"; CopilotAvailability: Enum "Copilot Availability"; LearnMoreUrl: Text[2048]; CallerModuleInfo: ModuleInfo) var - CopilotSettings: Record "Copilot Settings"; CustomDimensions: Dictionary of [Text, Text]; begin if not IsCapabilityRegistered(CopilotCapability, CallerModuleInfo) then @@ -88,7 +138,6 @@ codeunit 7774 "Copilot Capability Impl" procedure UnregisterCapability(CopilotCapability: Enum "Copilot Capability"; var CallerModuleInfo: ModuleInfo) var - CopilotSettings: Record "Copilot Settings"; CustomDimensions: Dictionary of [Text, Text]; begin if not IsCapabilityRegistered(CopilotCapability, CallerModuleInfo) then @@ -110,8 +159,6 @@ codeunit 7774 "Copilot Capability Impl" end; procedure IsCapabilityRegistered(CopilotCapability: Enum "Copilot Capability"; AppId: Guid): Boolean - var - CopilotSettings: Record "Copilot Settings"; begin CopilotSettings.ReadIsolation(IsolationLevel::ReadCommitted); CopilotSettings.SetRange("Capability", CopilotCapability); @@ -119,6 +166,11 @@ codeunit 7774 "Copilot Capability Impl" exit(not CopilotSettings.IsEmpty()); end; + procedure IsCapabilityActive(CallerModuleInfo: ModuleInfo): Boolean + begin + exit(IsCapabilityActive(CopilotSettings.Capability, CallerModuleInfo.Id())); + end; + procedure IsCapabilityActive(CopilotCapability: Enum "Copilot Capability"; CallerModuleInfo: ModuleInfo): Boolean begin exit(IsCapabilityActive(CopilotCapability, CallerModuleInfo.Id())); @@ -126,7 +178,6 @@ codeunit 7774 "Copilot Capability Impl" procedure IsCapabilityActive(CopilotCapability: Enum "Copilot Capability"; AppId: Guid): Boolean var - CopilotSettings: Record "Copilot Settings"; CopilotCapabilityCU: Codeunit "Copilot Capability"; PrivacyNotice: Codeunit "Privacy Notice"; RequiredPrivacyNotices: List of [Code[50]]; @@ -150,6 +201,140 @@ codeunit 7774 "Copilot Capability Impl" exit(true); end; + procedure GetCapabilityName(): Text + var + CapabilityIndex: Integer; + CapabilityName: Text; + begin + CheckCapabilitySet(); + + CapabilityIndex := CopilotSettings.Capability.Ordinals.IndexOf(CopilotSettings.Capability.AsInteger()); + CapabilityName := CopilotSettings.Capability.Names.Get(CapabilityIndex); + + if CapabilityName.Trim() = '' then + exit(Format(CopilotSettings.Capability, 0, 9)); + + exit(CapabilityName); + end; + + procedure CheckCapabilitySet() + begin + if CopilotSettings.Capability.AsInteger() = 0 then + Error(CopilotCapabilityNotSetErr); + end; + + procedure CheckCapabilityServiceType(ServiceType: Enum "Azure AI Service Type") + begin + if CopilotSettings."Service Type" <> ServiceType then + Error(CopilotCapabilityNotSetErr); + end; + + procedure CheckEnabled(CallerModuleInfo: ModuleInfo) + begin + if not IsCapabilityEnabled(CopilotSettings.Capability, true, CallerModuleInfo) then + Error(CopilotNotEnabledErr); + end; + + procedure IsCapabilityEnabled(Capability: Enum "Copilot Capability"; CallerModuleInfo: ModuleInfo): Boolean + begin + exit(IsCapabilityEnabled(Capability, false, CallerModuleInfo)); + end; + + procedure IsCapabilityEnabled(Capability: Enum "Copilot Capability"; Silent: Boolean; CallerModuleInfo: ModuleInfo): Boolean + var + CopilotNotAvailable: Page "Copilot Not Available"; + begin + if not IsTenantAllowedToUseAOAI() then begin + if not Silent then + Error(CopilotDisabledForTenantErr); // Copilot capabilities cannot be run on this environment. + + exit(false); + end; + + if not IsCapabilityActive(Capability, CallerModuleInfo.Id()) then begin + if not Silent then begin + CopilotNotAvailable.SetCopilotCapability(Capability); + CopilotNotAvailable.Run(); + end; + + exit(false); + end; + + exit(CheckPrivacyNoticeState(Silent, Capability)); + end; + + [NonDebuggable] + local procedure IsTenantAllowedToUseAOAI(): Boolean + var + EnvironmentInformation: Codeunit "Environment Information"; + AzureOpenAIImpl: Codeunit "Azure OpenAI Impl"; + AzureKeyVault: Codeunit "Azure Key Vault"; + AzureAdTenant: Codeunit "Azure AD Tenant"; + ModuleInfo: ModuleInfo; + BlockList, TelemtryTok : Text; + begin + if not EnvironmentInformation.IsSaaSInfrastructure() then + exit(true); + + NavApp.GetCurrentModuleInfo(ModuleInfo); + if ModuleInfo.Publisher <> 'Microsoft' then + exit(true); + + TelemtryTok := AzureOpenAIImpl.GetAzureOpenAICategory(); + if (not AzureKeyVault.GetAzureKeyVaultSecret(EnabledKeyTok, BlockList)) or (BlockList.Trim() = '') then begin + FeatureTelemetry.LogError('0000KYC', TelemtryTok, TelemetryIsEnabledLbl, TelemetryUnableToCheckEnvironmentKVTxt); + exit(false); + end; + + if BlockList.Contains(AzureAdTenant.GetAadTenantId()) then begin + FeatureTelemetry.LogError('0000LFP', TelemtryTok, TelemetryIsEnabledLbl, TelemetryEnvironmentNotAllowedtoRunCopilotTxt); + exit(false); + end; + + exit(true); + end; + + local procedure CheckPrivacyNoticeState(Silent: Boolean; Capability: Enum "Copilot Capability"): Boolean + var + PrivacyNotice: Codeunit "Privacy Notice"; + AzureOpenAIImpl: Codeunit "Azure OpenAI Impl"; + CopilotNotAvailable: Page "Copilot Not Available"; + PrivacyNoticeApprovalState: Enum "Privacy Notice Approval State"; + begin + PrivacyNoticeApprovalState := PrivacyNotice.GetPrivacyNoticeApprovalState(AzureOpenAIImpl.GetAzureOpenAICategory(), false); + case PrivacyNoticeApprovalState of + Enum::"Privacy Notice Approval State"::Agreed: + exit(true); + Enum::"Privacy Notice Approval State"::Disagreed: + begin + if not Silent then begin + CopilotNotAvailable.SetCopilotCapability(Capability); + CopilotNotAvailable.Run(); + end; + + exit(false); + end; + else + exit(true); + end; + end; + + procedure AddTelemetryCustomDimensions(var CustomDimensions: Dictionary of [Text, Text]; CallerModuleInfo: ModuleInfo) + var + Language: Codeunit Language; + SavedGlobalLanguageId: Integer; + begin + SavedGlobalLanguageId := GlobalLanguage(); + GlobalLanguage(Language.GetDefaultApplicationLanguageId()); + + CustomDimensions.Add('Capability', Format(CopilotSettings.Capability)); + CustomDimensions.Add('AppId', Format(CopilotSettings."App Id")); + CustomDimensions.Add('Publisher', CallerModuleInfo.Publisher); + CustomDimensions.Add('UserLanguage', Format(GlobalLanguage())); + + GlobalLanguage(SavedGlobalLanguageId); + end; + procedure SendActivateTelemetry(CopilotCapability: Enum "Copilot Capability"; AppId: Guid) var CustomDimensions: Dictionary of [Text, Text]; @@ -175,11 +360,6 @@ codeunit 7774 "Copilot Capability Impl" GlobalLanguage(SavedGlobalLanguageId); end; - procedure GetAzureOpenAICategory(): Code[50] - begin - exit(AzureOpenAiTxt); - end; - procedure GetCopilotCategory(): Code[50] begin exit(CopilotCategoryLbl); @@ -242,15 +422,6 @@ codeunit 7774 "Copilot Capability Impl" GuidedExperience.ResetAssistedSetup(ObjectType::Page, Page::"Copilot AI Capabilities"); end; - [EventSubscriber(ObjectType::Codeunit, Codeunit::"Privacy Notice", 'OnRegisterPrivacyNotices', '', false, false)] - local procedure CreatePrivacyNoticeRegistrations(var TempPrivacyNotice: Record "Privacy Notice" temporary) - begin - TempPrivacyNotice.Init(); - TempPrivacyNotice.ID := AzureOpenAiTxt; - TempPrivacyNotice."Integration Service Name" := AzureOpenAiTxt; - if not TempPrivacyNotice.Insert() then; - end; - [EventSubscriber(ObjectType::Codeunit, Codeunit::"System Action Triggers", 'GetCopilotCapabilityStatus', '', false, false)] local procedure GetCopilotCapabilityStatus(Capability: Integer; var IsEnabled: Boolean; AppId: Guid; Silent: Boolean) var diff --git a/src/System Application/App/AI/src/Copilot/CopilotSettings.Table.al b/src/System Application/App/AI/src/Copilot/CopilotSettings.Table.al index 9ee6c06bfb..e86861ff8f 100644 --- a/src/System Application/App/AI/src/Copilot/CopilotSettings.Table.al +++ b/src/System Application/App/AI/src/Copilot/CopilotSettings.Table.al @@ -44,6 +44,10 @@ table 7775 "Copilot Settings" { DataClassification = SystemMetadata; } + field(7; "Service Type"; Enum "Azure AI Service Type") + { + DataClassification = SystemMetadata; + } } keys diff --git a/src/System Application/App/AI/src/Copilot/Interfaces/AIServiceName.Interface.al b/src/System Application/App/AI/src/Copilot/Interfaces/AIServiceName.Interface.al new file mode 100644 index 0000000000..f7ac245c07 --- /dev/null +++ b/src/System Application/App/AI/src/Copilot/Interfaces/AIServiceName.Interface.al @@ -0,0 +1,25 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + +/// +/// Interface for providing naming information for a given AI service. +/// +interface "AI Service Name" +{ + + /// + /// Get the name of the service. + /// + /// The name of the service. + procedure GetServiceName(): Text[250]; + + /// + /// Get the id of the service. Will often be the service name in Code form. + /// + /// The id of the service. + procedure GetServiceId(): Code[50]; + +} \ No newline at end of file diff --git a/src/System Application/Test/AI/src/AzureDITest.Codeunit.al b/src/System Application/Test/AI/src/AzureDITest.Codeunit.al new file mode 100644 index 0000000000..767f6b6dd5 --- /dev/null +++ b/src/System Application/Test/AI/src/AzureDITest.Codeunit.al @@ -0,0 +1,95 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Test.AI; + +using System.AI; +using System.TestLibraries.AI; +using System.TestLibraries.Environment; +using System.AI.DocumentIntelligence; +using System.TestLibraries.Utilities; + +codeunit 132685 "Azure DI Test" +{ + Subtype = Test; + + var + CopilotTestLibrary: Codeunit "Copilot Test Library"; + EnvironmentInfoTestLibrary: Codeunit "Environment Info Test Library"; + LibraryAssert: Codeunit "Library Assert"; + + [Test] + procedure TestSetCopilotCapabilityInactive() + var + AzureDI: Codeunit "Azure Document Intelligence"; + begin + + // [GIVEN] Capability is set + RegisterCapability(Enum::"Copilot Capability"::"Text Capability"); + CopilotTestLibrary.SetCopilotStatus(Enum::"Copilot Capability"::"Text Capability", GetModuleAppId(), Enum::"Copilot Status"::Inactive); + + // [WHEN] SetCopilotCapability is called + asserterror AzureDI.SetCopilotCapability(Enum::"Copilot Capability"::"Text Capability"); + + // [THEN] SetCopilotCapability returns an error + LibraryAssert.ExpectedError('Copilot capability ''Text Capability'' has not been enabled. Please contact your system administrator.'); + end; + + [Test] + procedure AnalyzeInvoiceCopilotCapabilityNotSet() + var + AzureDI: Codeunit "Azure Document Intelligence"; + begin + // [SCENARIO] AnalyzeInvoice returns an error when capability is not set + + EnvironmentInfoTestLibrary.SetTestabilitySoftwareAsAService(false); + + // [WHEN] AnalyzeInvoice is called + asserterror AzureDI.AnalyzeInvoice('Text'); + + // [THEN] AnalyzeInvoice returns an error + LibraryAssert.ExpectedError('Copilot capability has not been set.'); + end; + + [Test] + procedure AnalyzeInvoiceCapabilityInactive() + var + AzureDI: Codeunit "Azure Document Intelligence"; + begin + // [SCENARIO] AnalyzeInvoice returns an error when capability is not active + + EnvironmentInfoTestLibrary.SetTestabilitySoftwareAsAService(false); + + // [GIVEN] Capability is set + RegisterCapability(Enum::"Copilot Capability"::"Text Capability"); + AzureDI.SetCopilotCapability(Enum::"Copilot Capability"::"Text Capability"); + CopilotTestLibrary.SetCopilotStatus(Enum::"Copilot Capability"::"Text Capability", GetModuleAppId(), Enum::"Copilot Status"::Inactive); + + // [WHEN] AnalyzeInvoice is called + asserterror AzureDI.AnalyzeInvoice('Test'); + + // [THEN] AnalyzeInvoice returns an error + LibraryAssert.ExpectedError('Copilot capability ''Text Capability'' has not been enabled. Please contact your system administrator.'); + end; + + local procedure RegisterCapability(Capability: Enum "Copilot Capability") + var + AzureDI: Codeunit "Azure Document Intelligence"; + CopilotCapability: Codeunit "Copilot Capability"; + begin + if CopilotCapability.IsCapabilityRegistered(Capability) then + exit; + + AzureDI.RegisterCopilotCapability(Capability, Enum::"Copilot Availability"::Preview, ''); + end; + + local procedure GetModuleAppId(): Guid + var + CurrentModuleInfo: ModuleInfo; + begin + NavApp.GetCurrentModuleInfo(CurrentModuleInfo); + exit(CurrentModuleInfo.Id()); + end; +} \ No newline at end of file