diff --git a/src/System Application/App/AI/src/Azure OpenAI/AOAIAccountVerificationLog.Table.al b/src/System Application/App/AI/src/Azure OpenAI/AOAIAccountVerificationLog.Table.al new file mode 100644 index 0000000000..b1e3d4d38b --- /dev/null +++ b/src/System Application/App/AI/src/Azure OpenAI/AOAIAccountVerificationLog.Table.al @@ -0,0 +1,35 @@ +namespace System.AI; + +table 7767 "AOAI Account Verification Log" +{ + Caption = 'AOAI Account Verification Log'; + Access = Internal; + Extensible = false; + InherentEntitlements = RIMDX; + InherentPermissions = X; + DataPerCompany = false; + ReplicateData = false; + + fields + { + field(1; AccountName; Text[100]) + { + Caption = 'Account Name'; + DataClassification = CustomerContent; + } + + field(2; LastSuccessfulVerification; DateTime) + { + Caption = 'Access Verified'; + DataClassification = SystemMetadata; + } + } + + keys + { + key(PrimaryKey; AccountName) + { + Clustered = false; + } + } +} \ No newline at end of file diff --git a/src/System Application/App/AI/src/Azure OpenAI/AOAIAuthorization.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/AOAIAuthorization.Codeunit.al index 2e5ba3e42a..4540a6a49e 100644 --- a/src/System Application/App/AI/src/Azure OpenAI/AOAIAuthorization.Codeunit.al +++ b/src/System Application/App/AI/src/Azure OpenAI/AOAIAuthorization.Codeunit.al @@ -6,6 +6,7 @@ namespace System.AI; using System; +using System.Telemetry; /// /// Store the authorization information for the AOAI service. /// @@ -14,6 +15,7 @@ codeunit 7767 "AOAI Authorization" Access = Internal; InherentEntitlements = X; InherentPermissions = X; + Permissions = tabledata "AOAI Account Verification Log" = RIMD; var [NonDebuggable] @@ -25,6 +27,9 @@ codeunit 7767 "AOAI Authorization" [NonDebuggable] ManagedResourceDeployment: Text; ResourceUtilization: Enum "AOAI Resource Utilization"; + FirstPartyAuthorization: Boolean; + SelfManagedAuthorization: Boolean; + MicrosoftManagedAuthorization: Boolean; [NonDebuggable] procedure IsConfigured(CallerModule: ModuleInfo): Boolean @@ -37,11 +42,11 @@ codeunit 7767 "AOAI Authorization" case ResourceUtilization of Enum::"AOAI Resource Utilization"::"First Party": - exit((ManagedResourceDeployment <> '') and ALCopilotFunctions.IsPlatformAuthorizationConfigured(CallerModule.Publisher(), CurrentModule.Publisher())); + exit(FirstPartyAuthorization and ALCopilotFunctions.IsPlatformAuthorizationConfigured(CallerModule.Publisher(), CurrentModule.Publisher())); Enum::"AOAI Resource Utilization"::"Self-Managed": - exit((Deployment <> '') and (Endpoint <> '') and (not ApiKey.IsEmpty())); + exit(SelfManagedAuthorization); Enum::"AOAI Resource Utilization"::"Microsoft Managed": - exit((Deployment <> '') and (Endpoint <> '') and (not ApiKey.IsEmpty()) and (ManagedResourceDeployment <> '') and AzureOpenAiImpl.IsTenantAllowlistedForFirstPartyCopilotCalls()); + exit(MicrosoftManagedAuthorization and AzureOpenAiImpl.IsTenantAllowlistedForFirstPartyCopilotCalls()); end; exit(false); @@ -57,6 +62,22 @@ codeunit 7767 "AOAI Authorization" Deployment := NewDeployment; ApiKey := NewApiKey; ManagedResourceDeployment := NewManagedResourceDeployment; + MicrosoftManagedAuthorization := true; + end; + + [NonDebuggable] + procedure SetMicrosoftManagedAuthorization(AOAIAccountName: Text; NewApiKey: SecretText; NewManagedResourceDeployment: Text) + var + IsVerified: Boolean; + begin + ClearVariables(); + IsVerified := VerifyAOAIAccount(AOAIAccountName, NewApiKey); + + if IsVerified then begin + ResourceUtilization := Enum::"AOAI Resource Utilization"::"Microsoft Managed"; + ManagedResourceDeployment := NewManagedResourceDeployment; + MicrosoftManagedAuthorization := true; + end; end; [NonDebuggable] @@ -68,6 +89,7 @@ codeunit 7767 "AOAI Authorization" Endpoint := NewEndpoint; Deployment := NewDeployment; ApiKey := NewApiKey; + SelfManagedAuthorization := true; end; [NonDebuggable] @@ -77,6 +99,7 @@ codeunit 7767 "AOAI Authorization" ResourceUtilization := Enum::"AOAI Resource Utilization"::"First Party"; ManagedResourceDeployment := NewDeployment; + FirstPartyAuthorization := true; end; [NonDebuggable] @@ -115,5 +138,178 @@ codeunit 7767 "AOAI Authorization" Clear(Deployment); Clear(ManagedResourceDeployment); Clear(ResourceUtilization); + Clear(FirstPartyAuthorization); + clear(SelfManagedAuthorization); + Clear(MicrosoftManagedAuthorization); + end; + + [NonDebuggable] + local procedure PerformAOAIAccountVerification(AOAIAccountName: Text; NewApiKey: SecretText): Boolean + var + HttpClient: HttpClient; + HttpRequestMessage: HttpRequestMessage; + HttpResponseMessage: HttpResponseMessage; + HttpContent: HttpContent; + ContentHeaders: HttpHeaders; + Url: Text; + IsSuccessful: Boolean; + UrlFormatTxt: Label 'https://%1.openai.azure.com/openai/models?api-version=2024-06-01', Locked = true; + begin + Url := StrSubstNo(UrlFormatTxt, AOAIAccountName); + + HttpContent.GetHeaders(ContentHeaders); + if ContentHeaders.Contains('Content-Type') then + ContentHeaders.Remove('Content-Type'); + ContentHeaders.Add('Content-Type', 'application/json'); + ContentHeaders.Add('api-key', NewApiKey); + + HttpRequestMessage.Method := 'GET'; + HttpRequestMessage.SetRequestUri(Url); + HttpRequestMessage.Content(HttpContent); + + IsSuccessful := HttpClient.Send(HttpRequestMessage, HttpResponseMessage); + + if not IsSuccessful then + exit(false); + + if not HttpResponseMessage.IsSuccessStatusCode() then + exit(false); + + exit(true); + end; + + local procedure FormatDurationAsString(DurationValue: Duration): Text + var + Hours: Integer; + Minutes: Integer; + Seconds: Integer; + Milliseconds: Integer; + begin + // Convert milliseconds into hours, minutes, seconds + Hours := DurationValue div (60 * 60 * 1000); + DurationValue := DurationValue mod (60 * 60 * 1000); + + Minutes := DurationValue div (60 * 1000); + DurationValue := DurationValue mod (60 * 1000); + + Seconds := DurationValue div 1000; + Milliseconds := DurationValue mod 1000; + + // Format as HH:MM:SS.mmm + exit(StrSubstNo('%1:%2:%3.%4', + Format(Hours, 2, ''), + Format(Minutes, 2, ''), + Format(Seconds, 2, ''), + Format(Milliseconds, 3, ''))); + end; + + local procedure VerifyAOAIAccount(AOAIAccountName: Text; NewApiKey: SecretText): Boolean + var + Notif: Notification; + IsVerified: Boolean; + GracePeriod: Duration; + CachePeriod: Duration; + TruncatedAccountName: Text[100]; + begin + Message('Starting VerifyAOAIAccount procedure. Variables: AOAIAccountName=' + AOAIAccountName); + + GracePeriod := 15 * 60 * 1000;//14 * 24 * 60 * 60 * 1000; // 2 weeks in milliseconds + CachePeriod := 1 * 60 * 1000;//24 * 60 * 60 * 1000; // 1 day in milliseconds + + TruncatedAccountName := CopyStr(AOAIAccountName, 1, 100); + + Message('Variables: GracePeriod=' + FormatDurationAsString(GracePeriod) + ', CachePeriod=' + FormatDurationAsString(CachePeriod) + ', TruncatedAccountName=' + TruncatedAccountName); + + if IsAccountVerifiedWithinPeriod(TruncatedAccountName, CachePeriod) then begin + Message('Function IsAccountVerifiedWithinPeriod called. Result: Verification skipped (within cache period).'); + exit(true); + end; + + IsVerified := PerformAOAIAccountVerification(AOAIAccountName, NewApiKey); + + Message('Function PerformAOAIAccountVerification called. Result: IsVerified=' + Format(IsVerified)); + + // Handle failed verification + if not IsVerified then begin + SendNotification(Notif); + LogTelemetry(AOAIAccountName, Today); + if IsAccountVerifiedWithinPeriod(TruncatedAccountName, GracePeriod) then begin + Message('Function IsAccountVerifiedWithinPeriod called. Result: Verification failed, but account is still valid (within grace period).'); + exit(true); // Verified if within grace period + end; + Message('Function IsAccountVerifiedWithinPeriod called. Result: Verification failed, and account is no longer valid (grace period expired).'); + exit(false); // Failed verification if grace period has been exceeded + end; + SaveVerificationTime(TruncatedAccountName); + Message('Function SaveVerificationTime called. Verification successful. Record saved.'); + exit(true); + end; + + local procedure IsAccountVerifiedWithinPeriod(AccountName: Text[100]; Period: Duration): Boolean + var + Rec: Record "AOAI Account Verification Log"; + IsVerified: Boolean; + begin + ; + Message('Starting IsAccountVerifiedWithinPeriod procedure. Variables: AccountName=' + AccountName + ', Period=' + FormatDurationAsString(Period)); + + if Rec.Get(AccountName) then begin + Message('Record found. Variables: CurrentDateTime=' + Format(CurrentDateTime) + ', Rec.LastSuccessfulVerification=' + Format(Rec.LastSuccessfulVerification)); + IsVerified := CurrentDateTime - Rec.LastSuccessfulVerification <= Period; + Message('Verification result: ' + Format(IsVerified)); + exit(IsVerified); + end; + + Message('Record not found. Exiting with false.'); + exit(false); + end; + + local procedure SaveVerificationTime(AccountName: Text[100]) + var + Rec: Record "AOAI Account Verification Log"; + begin + Message('Starting SaveVerificationTime procedure. Variables: AccountName=' + AccountName); + if Rec.Get(AccountName) then begin + Rec.LastSuccessfulVerification := CurrentDateTime; + Rec.Modify(true); + Message('Record updated. Variables: Rec.LastSuccessfulVerification=' + Format(Rec.LastSuccessfulVerification)); + end else begin + Rec.Init(); + Rec.AccountName := AccountName; + Rec.LastSuccessfulVerification := CurrentDateTime; + Rec.Insert(true); + Message('Record inserted. Variables: Rec.AccountName=' + Rec.AccountName + ', Rec.LastSuccessfulVerification=' + Format(Rec.LastSuccessfulVerification)); + end; + end; + + local procedure SendNotification(var Notif: Notification) + var + MessageLbl: Label 'Azure Open AI authorization failed. AI functionality will be disabled within 2 weeks. Please contact your system administrator or the extension developer for assistance.'; + begin + Notif.Message := MessageLbl; + Notif.Scope := NotificationScope::LocalScope; + Notif.Send(); + end; + + local procedure LogTelemetry(AccountName: Text; VerificationDate: Date) + var + Telemetry: Codeunit Telemetry; + MessageLbl: Label 'Azure Open AI authorization failed for account %1 on %2 because it is not authorized to access AI services. The connection will be terminated within 2 weeks if not rectified', Comment = 'Telemetry message where %1 is the name of the Azure Open AI account name and %2 is the date where verification has taken place'; + CustomDimensions: Dictionary of [Text, Text]; + begin + Message('Starting LogTelemetry procedure. Variables: AccountName=' + AccountName + ', VerificationDate=' + Format(VerificationDate)); + + CustomDimensions.Add('AccountName', AccountName); + CustomDimensions.Add('VerificationDate', Format(VerificationDate)); + + Telemetry.LogMessage( + '0000AA1', // Event ID + StrSubstNo(MessageLbl, AccountName, VerificationDate), // Message + Verbosity::Warning, + DataClassification::SystemMetadata, + Enum::"AL Telemetry Scope"::All, + CustomDimensions + ); + Message('Telemetry logged successfully. CustomDimensions: AccountName=' + AccountName + ', VerificationDate=' + Format(VerificationDate)); 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..0b24397137 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 @@ -100,11 +100,26 @@ codeunit 7771 "Azure OpenAI" /// Deployment would look like: gpt-35-turbo-16k /// [NonDebuggable] + [Obsolete('Using Managed AI resources now requires different input parameters. Use the other overload for SetManagedResourceAuthorization instead.', '26.0')] procedure SetManagedResourceAuthorization(ModelType: Enum "AOAI Model Type"; Endpoint: Text; Deployment: Text; ApiKey: SecretText; ManagedResourceDeployment: Text) begin AzureOpenAIImpl.SetManagedResourceAuthorization(ModelType, Endpoint, Deployment, ApiKey, ManagedResourceDeployment); end; + /// + /// Sets the managed Azure OpenAI API authorization to use for a specific model type. + /// This will send the Azure OpenAI call to the deployment specified in , and will use the other parameters to verify that you have access to Azure OpenAI. + /// + /// The model type to set authorization for. + /// Azure OpenAI account name) + /// The API key to use to verify access to Azure OpenAI. This is used only for verification, not for actual Azure OpenAI calls. + /// The managed deployment to use for the model type. + [NonDebuggable] + procedure SetManagedResourceAuthorization(ModelType: Enum "AOAI Model Type"; AOAIAccountName: Text; ApiKey: SecretText; ManagedResourceDeployment: Text) + begin + AzureOpenAIImpl.SetManagedResourceAuthorization(ModelType, AOAIAccountName, ApiKey, ManagedResourceDeployment); + end; + /// /// Sets the Azure OpenAI API authorization to use for a specific model type and endpoint. /// 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..6d43985055 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 @@ -207,6 +207,21 @@ codeunit 7772 "Azure OpenAI Impl" end; end; + [NonDebuggable] + procedure SetManagedResourceAuthorization(ModelType: Enum "AOAI Model Type"; AOAIAccountName: Text; ApiKey: SecretText; ManagedResourceDeployment: Text) + begin + case ModelType of + Enum::"AOAI Model Type"::"Text Completions": + TextCompletionsAOAIAuthorization.SetMicrosoftManagedAuthorization(AOAIAccountName, ApiKey, ManagedResourceDeployment); + Enum::"AOAI Model Type"::Embeddings: + EmbeddingsAOAIAuthorization.SetMicrosoftManagedAuthorization(AOAIAccountName, ApiKey, ManagedResourceDeployment); + Enum::"AOAI Model Type"::"Chat Completions": + ChatCompletionsAOAIAuthorization.SetMicrosoftManagedAuthorization(AOAIAccountName, ApiKey, ManagedResourceDeployment); + else + Error(InvalidModelTypeErr); + end; + end; + [NonDebuggable] procedure GenerateTextCompletion(Prompt: SecretText; var AOAIOperationResponse: Codeunit "AOAI Operation Response"; CallerModuleInfo: ModuleInfo): Text var @@ -757,5 +772,4 @@ codeunit 7772 "Azure OpenAI Impl" Session.LogMessage('0000MLE', TelemetryTenantAllowlistedMsg, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CopilotCapabilityImpl.GetAzureOpenAICategory()); exit(true); end; - -} \ No newline at end of file +}