Codeunit 2020 Image Analysis Management

App
Base Application
Namespace
System.AI
Versions
17-28

Procedures, 26Events, 3Obsolete, 2

Versions171819202122232425262728

Source29

Source in 29

src/Layers/W1/BaseApp/System/AI/ImageAnalysis/ImageAnalysisManagement.Codeunit.al439 lines, Copyright (c) Microsoft Corporation. MIT

namespace System.AI;

using System;
using System.Azure.KeyVault;
using System.Environment;
using System.IO;
using System.Security.Authentication;
using System.Text;
using System.Utilities;

codeunit 2020 "Image Analysis Management"
{
    var
        [NonDebuggable]
        "Key": SecretText;
        Uri: Text;
        LimitType: Option Year,Month,Day,Hour;
        LimitValue: Integer;
        ImagePath: Text;
        SetMediaErr: Label 'There was a problem uploading the image file. Please try again.';
        NoApiKeyUriErr: Label 'To analyze images, you must provide an API key and an API URI for Computer Vision.';
        NoImageErr: Label 'You haven''t uploaded an image to analyze.';
        LastError: Text;
        IsLastErrorUsageLimitError: Boolean;
        UseOAuth2: Boolean;
        IsInitialized: Boolean;
        GenericErrorErr: Label 'There was an error in contacting the Computer Vision API. Please try again or contact an administrator.';
        ChangingLimitAfterInitErr: Label 'You cannot change the limit setting after initialization.';
        ImageAnalysisSecretTxt: Label 'cognitive-vision-params', Locked = true;
        ImageAnalysisOAuth2SecretTxt: Label 'cognitive-vision-oauth2-params', Locked = true;
        MissingImageAnalysisSecretErr: Label 'There is a missing configuration value on our end. Try again later.';
        ImageAnalysisProvider: Interface "Image Analysis Provider";
        ImageAnalysisTelemetryCategoryTxt: Label 'AL Image Analysis', Locked = true;
        StartingImageAnalysisTelemetryMsg: Label 'Starting image analysis. Key empty: %1, Url empty: %2, Path empty: %3, Limit reached: %4.', Locked = true;
        FirstPartyAppIdKVNameLbl: Label 'cognitive-vision-appid', Locked = true;
        FirstPartyAppCertKVNameLbl: Label 'cognitive-vision-certname', Locked = true;
        FirstPartyAppAuthUrlKVNameLbl: Label 'cognitive-vision-authority', Locked = true;
        CognitiveServicesScopeTok: Label 'https://cognitiveservices.azure.com/.default', Locked = true;
        OAuthAcquireTokenFailedTelemetryMsg: Label 'Failed to acquire OAuth2 token for Computer Vision.', Locked = true;
        OAuthEmptyAccessTokenTelemetryMsg: Label 'OAuth2 access token for Computer Vision is empty.', Locked = true;
        UsingOAuth2TelemetryMsg: Label 'Using OAuth2 token authentication for Computer Vision.', Locked = true;
        UsingApiKeyTelemetryMsg: Label 'Using API key authentication for Computer Vision (OAuth2 not configured or failed).', Locked = true;
        UsingCustomConfigTelemetryMsg: Label 'Using customer-configured API key and URI for Computer Vision.', Locked = true;

    [NonDebuggable]
    procedure Initialize()
    begin
        Initialize(Enum::"Image Analysis Provider"::"v1.0");
    end;

    [NonDebuggable]
    procedure Initialize(InputImageAnalysisProvider: Enum "Image Analysis Provider")
    var
        ImageAnalysisSetup: Record "Image Analysis Setup";
        EnvironmentInformation: Codeunit "Environment Information";
        AzureAIUsage: Codeunit "Azure AI Usage";
        AzureAIService: Enum "Azure AI Service";
    begin
        if IsInitialized then
            exit;
        if not ImageAnalysisSetup.Get() then begin
            ImageAnalysisSetup.Init();
            ImageAnalysisSetup.Insert();
        end;

        if Key.IsEmpty() or (Uri = '') then begin
            Key := ImageAnalysisSetup.GetApiKeyAsSecret();
            Uri := ImageAnalysisSetup."Api Uri";
            AzureAIUsage.SetImageAnalysisIsSetup(false);
        end else begin
            AzureAIUsage.SetImageAnalysisIsSetup(true);
            Session.LogMessage('0000THU', UsingCustomConfigTelemetryMsg, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', ImageAnalysisTelemetryCategoryTxt);
        end;

        if LimitValue = 0 then begin
            AzureAIService := AzureAIService::"Computer Vision";
            LimitType := AzureAIUsage.GetLimitPeriod(AzureAIService);
            LimitValue := AzureAIUsage.GetResourceLimit(AzureAIService);
        end;

        if LimitValue = 0 then
            SetLimitInYears(999);

        if (Key.IsEmpty() or (Uri = '')) and EnvironmentInformation.IsSaaS() then
            if not TryAcquireOAuth2Token(Key, Uri, LimitType, LimitValue) then begin
                GetImageAnalysisCredentials(Key, Uri, LimitType, LimitValue);
                Session.LogMessage('0000THV', UsingApiKeyTelemetryMsg, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', ImageAnalysisTelemetryCategoryTxt);
            end;

        ImageAnalysisProvider := InputImageAnalysisProvider;

        IsInitialized := true;
    end;

    procedure SetMedia(MediaId: Guid)
    var
        TenantMedia: Record "Tenant Media";
        FileManagement: Codeunit "File Management";
    begin
        if TenantMedia.Get(MediaId) then begin
            ImageAnalysisProvider.IsMediaSupported(MediaId);
            ImagePath := FileManagement.ServerTempFileName('');
            TenantMedia.CalcFields(Content);
            TenantMedia.Content.Export(ImagePath);
        end else
            Error(SetMediaErr);
    end;

    procedure SetImagePath(Path: Text)
    var
        FileManagement: Codeunit "File Management";
    begin
        FileManagement.IsAllowedPath(Path, false);
        ImagePath := Path;
    end;

    procedure SetBlob(TempBlob: Codeunit "Temp Blob")
    var
        FileManagement: Codeunit "File Management";
    begin
        ImagePath := FileManagement.ServerTempFileName('');
        FileManagement.BLOBExportToServerFile(TempBlob, ImagePath);
    end;


    procedure SetUriAndKey(UriValue: Text; KeyValue: SecretText)
    begin
        Uri := UriValue;
        Key := KeyValue;
    end;

    procedure SetLimitInYears(Value: Integer)
    begin
        if IsInitialized then
            Error(ChangingLimitAfterInitErr);
        if Value <= 0 then
            exit;
        LimitType := LimitType::Year;
        LimitValue := Value;
    end;

    procedure SetLimitInMonths(Value: Integer)
    begin
        if IsInitialized then
            Error(ChangingLimitAfterInitErr);
        if Value <= 0 then
            exit;
        LimitType := LimitType::Month;
        LimitValue := Value;
    end;

    procedure SetLimitInDays(Value: Integer)
    begin
        if IsInitialized then
            Error(ChangingLimitAfterInitErr);
        if Value <= 0 then
            exit;
        LimitType := LimitType::Day;
        LimitValue := Value;
    end;

    procedure SetLimitInHours(Value: Integer)
    begin
        if IsInitialized then
            Error(ChangingLimitAfterInitErr);
        if Value <= 0 then
            exit;
        LimitType := LimitType::Hour;
        LimitValue := Value;
    end;

    procedure AnalyzeTags(var ImageAnalysisResult: Codeunit "Image Analysis Result"): Boolean
    begin
        exit(Analyze(ImageAnalysisResult, Enum::"Image Analysis Type"::Tags));
    end;

    procedure AnalyzeColors(var ImageAnalysisResult: Codeunit "Image Analysis Result"): Boolean
    begin
        exit(Analyze(ImageAnalysisResult, Enum::"Image Analysis Type"::Color));
    end;

    procedure AnalyzeFaces(var ImageAnalysisResult: Codeunit "Image Analysis Result"): Boolean
    begin
        exit(Analyze(ImageAnalysisResult, Enum::"Image Analysis Type"::Faces));
    end;

    procedure Analyze(var ImageAnalysisResult: Codeunit "Image Analysis Result"; AnalysisType: Enum "Image Analysis Type"): Boolean
    var
        AnalysisTypes: List of [Enum "Image Analysis Type"];
    begin
        AnalysisTypes.Add(AnalysisType);
        exit(Analyze(ImageAnalysisResult, AnalysisTypes));
    end;

    procedure Analyze(var ImageAnalysisResult: Codeunit "Image Analysis Result"; AnalysisTypes: List of [Enum "Image Analysis Type"]): Boolean
    var
        ImageAnalysisSetup: Record "Image Analysis Setup";
        ResultJSONManagement: Codeunit "JSON Management";
        UsageLimitError: Text;
    begin
        Initialize();
        SetLastError('', false);
        OnBeforeImageAnalysis();

        Session.LogMessage('0000JYW',
            StrSubstNo(StartingImageAnalysisTelemetryMsg, Key.IsEmpty(), Uri = '', ImagePath = '', ImageAnalysisSetup.IsUsageLimitReached(UsageLimitError, LimitValue, LimitType)),
            Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', ImageAnalysisTelemetryCategoryTxt);

        if (Key.IsEmpty()) or (Uri = '') then
            SetLastError(NoApiKeyUriErr, false)
        else
            if ImagePath = '' then
                SetLastError(NoImageErr, false)
            else
                if ImageAnalysisSetup.IsUsageLimitReached(UsageLimitError, LimitValue, LimitType) then
                    SetLastError(UsageLimitError, true)
                else
                    if InvokeAnalysisWithAuth(ResultJSONManagement, Uri, Key, ImagePath, AnalysisTypes, GlobalLanguage()) then
                        ImageAnalysisSetup.Increment()
                    else
                        if ImageAnalysisProvider.GetLastError() <> '' then
                            SetLastError(ImageAnalysisProvider.GetLastError(), false)
                        else
                            SetLastError(GenericErrorErr, false);

        ImageAnalysisResult.SetResult(ResultJSONManagement, AnalysisTypes);
        OnAfterImageAnalysis(ImageAnalysisResult);

        exit(not HasError());
    end;

    [Scope('OnPrem')]
    procedure SetHttpMessageHandler(NewHttpMessageHandler: DotNet HttpMessageHandler)
    var
        ImageAnalysisWrapperV10: Codeunit "Image Analysis Wrapper V1.0";
    begin
        // This should only be used for testing.
        ImageAnalysisWrapperV10.SetHttpMessageHandler(NewHttpMessageHandler);
        ImageAnalysisProvider := ImageAnalysisWrapperV10;
    end;

    local procedure InvokeAnalysisWithAuth(var JSONManagement: Codeunit "JSON Management"; BaseUrl: Text; ImageAnalysisKey: SecretText; Path: Text; AnalysisTypes: List of [Enum "Image Analysis Type"]; LanguageId: Integer): Boolean
    begin
        ImageAnalysisProvider.SetUseOAuth2(UseOAuth2);
        exit(ImageAnalysisProvider.InvokeAnalysis(JSONManagement, BaseUrl, ImageAnalysisKey, Path, AnalysisTypes, LanguageId));
    end;

    procedure GetLastError(var Message: Text; var IsUsageLimitError: Boolean): Boolean
    begin
        Message := LastError;
        IsUsageLimitError := IsLastErrorUsageLimitError;
        exit(HasError());
    end;

    local procedure SetLastError(ErrorMsg: Text; IsUsageLimitError: Boolean)
    begin
        LastError := ErrorMsg;
        IsLastErrorUsageLimitError := IsUsageLimitError;
    end;

    procedure GetTelemetryCategory(): Text
    begin
        exit(ImageAnalysisTelemetryCategoryTxt);
    end;

    procedure GetNoImageErr(): Text
    begin
        exit(NoImageErr);
    end;

    procedure HasError(): Boolean
    begin
        exit(LastError <> '');
    end;

    procedure GetLimitParams(var LimitTypeOut: Option Year,Month,Day,Hour; var LimitValueOut: Integer)
    begin
        LimitTypeOut := LimitType;
        LimitValueOut := LimitValue;
    end;

    procedure IsCurrentUserLanguageSupported(AnalysisTypes: List of [Enum "Image Analysis Type"]): Boolean
    begin
        exit(ImageAnalysisProvider.IsLanguageSupported(AnalysisTypes, GlobalLanguage()));
    end;


    [NonDebuggable]
    local procedure ExtractParameterValue(Parameters: JsonObject; ParameterName: Text; IsMandatory: Boolean): Text
    var
        ParameterValue: JsonToken;
        ParameterValueText: Text;
    begin
        if not Parameters.Get(ParameterName, ParameterValue) then begin
            if IsMandatory then
                Error(MissingImageAnalysisSecretErr);
            exit('');
        end;

        ParameterValueText := ParameterValue.AsValue().AsText();
        if (ParameterValueText = '') and IsMandatory then
            Error(MissingImageAnalysisSecretErr);

        exit(ParameterValueText);
    end;

    [NonDebuggable]
    [TryFunction]
    [Scope('OnPrem')]
    local procedure TryAcquireOAuth2Token(var ApiToken: SecretText; var ApiUri: Text; var LocalLimitType: Option; var LocalLimitValue: Integer)
    var
        AzureKeyVault: Codeunit "Azure Key Vault";
        MachineLearningKeyVaultMgmt: Codeunit "Machine Learning KeyVaultMgmt.";
        OAuth2: Codeunit OAuth2;
        ImageAnalysisParametersList: JsonArray;
        ImageAnalysisParameters: JsonObject;
        JToken: JsonToken;
        Cert: SecretText;
        IdToken: Text;
        AppId: Text;
        AuthUrl: Text;
        CertName: Text;
        ImageAnalysisParametersText: Text;
        LimitTypeTxt: Text;
        LimitValueTxt: Text;
        RedirectUrl: Text;
        Scopes: List of [Text];
    begin
        if not AzureKeyVault.GetAzureKeyVaultSecret(FirstPartyAppIdKVNameLbl, AppId) then
            Error('');
        if AppId = '' then
            Error('');
        if not AzureKeyVault.GetAzureKeyVaultSecret(FirstPartyAppCertKVNameLbl, CertName) then
            Error('');
        if not AzureKeyVault.GetAzureKeyVaultCertificate(CertName, Cert) then
            Error('');
        if not AzureKeyVault.GetAzureKeyVaultSecret(FirstPartyAppAuthUrlKVNameLbl, AuthUrl) then
            Error('');
        if not AzureKeyVault.GetAzureKeyVaultSecret(ImageAnalysisOAuth2SecretTxt, ImageAnalysisParametersText) then
            Error('');
        if not ImageAnalysisParametersList.ReadFrom(ImageAnalysisParametersText) then
            Error('');
        if not (ImageAnalysisParametersList.Count() > 0) then
            Error('');
        if not ImageAnalysisParametersList.Get(Random(ImageAnalysisParametersList.Count()) - 1, JToken) then
            Error('');

        ImageAnalysisParameters := JToken.AsObject();
        ApiUri := ExtractParameterValue(ImageAnalysisParameters, 'endpoint', true);
        LimitTypeTxt := ExtractParameterValue(ImageAnalysisParameters, 'limittype', true);
        LimitValueTxt := ExtractParameterValue(ImageAnalysisParameters, 'limitvalue', true);

        LocalLimitType := MachineLearningKeyVaultMgmt.GetLimitTypeOptionFromText(LimitTypeTxt);
        Evaluate(LocalLimitValue, LimitValueTxt);

        Scopes.Add(CognitiveServicesScopeTok);
        if not OAuth2.AcquireTokensWithCertificate(AppId, Cert, RedirectUrl, AuthUrl, Scopes, ApiToken, IdToken) then begin
            Session.LogMessage('0000THB', OAuthAcquireTokenFailedTelemetryMsg, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', ImageAnalysisTelemetryCategoryTxt);
            Error('');
        end;

        if ApiToken.IsEmpty() then begin
            Session.LogMessage('0000THC', OAuthEmptyAccessTokenTelemetryMsg, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', ImageAnalysisTelemetryCategoryTxt);
            Error('');
        end;

        UseOAuth2 := true;

        Session.LogMessage('0000THD', UsingOAuth2TelemetryMsg, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', ImageAnalysisTelemetryCategoryTxt);
    end;

    [NonDebuggable]
    [TryFunction]
    [Scope('OnPrem')]
    procedure GetImageAnalysisCredentials(var ApiKey: SecretText; var ApiUri: Text; var LocalLimitType: Option; var LocalLimitValue: Integer)
    var
        AzureKeyVault: Codeunit "Azure Key Vault";
        MachineLearningKeyVaultMgmt: Codeunit "Machine Learning KeyVaultMgmt.";
        ImageAnalysisParametersList: JsonArray;
        ImageAnalysisParameters: JsonObject;
        JToken: JsonToken;
        ImageAnalysisParametersText: Text;
        LimitTypeTxt: Text;
        LimitValueTxt: Text;
    begin
        if not AzureKeyVault.GetAzureKeyVaultSecret(ImageAnalysisSecretTxt, ImageAnalysisParametersText) then
            Error(MissingImageAnalysisSecretErr);

        // Check if the value is a proper JSON array
        if not ImageAnalysisParametersList.ReadFrom(ImageAnalysisParametersText) then
            exit;

        // Check if the JSON array has values
        if not (ImageAnalysisParametersList.Count() > 0) then
            exit;

        if not ImageAnalysisParametersList.Get(Random(ImageAnalysisParametersList.Count()) - 1, JToken) then
            exit;

        ImageAnalysisParameters := JToken.AsObject();

        ApiKey := ExtractParameterValue(ImageAnalysisParameters, 'key', false);
        ApiUri := ExtractParameterValue(ImageAnalysisParameters, 'endpoint', false);
        LimitTypeTxt := ExtractParameterValue(ImageAnalysisParameters, 'limittype', true);
        LimitValueTxt := ExtractParameterValue(ImageAnalysisParameters, 'limitvalue', true);

        LocalLimitType := MachineLearningKeyVaultMgmt.GetLimitTypeOptionFromText(LimitTypeTxt);
        Evaluate(LocalLimitValue, LimitValueTxt);
    end;

    internal procedure ToCommaSeparatedList(ImageAnalysisTypes: List of [Enum "Image Analysis Type"]) ListAsText: Text
    var
        ImageAnalysisType: Enum "Image Analysis Type";
        UntranslatedTypeName: Text;
    begin
        foreach ImageAnalysisType in ImageAnalysisTypes do begin
            UntranslatedTypeName := ImageAnalysisType.Names().Get(ImageAnalysisType.Ordinals().IndexOf(ImageAnalysisType.AsInteger()));
            ListAsText += ',' + UntranslatedTypeName;
        end;

        if StrLen(ListAsText) > 1 then
            ListAsText := CopyStr(ListAsText, 2); // Remove comma
    end;

    [IntegrationEvent(true, false)]
    local procedure OnBeforeImageAnalysis()
    begin
    end;

    [IntegrationEvent(true, false)]
    local procedure OnAfterImageAnalysis(ImageAnalysisResult: Codeunit "Image Analysis Result")
    begin
    end;

    [InternalEvent(false)]
    internal procedure OnBeforeSendImageAnalysisRequest(HttpContent: HttpContent; RequestUrl: Text; var HttpStatusCode: Integer; var HttpResponseContentText: Text; var Handled: Boolean)
    begin
    end;
}