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
+}