diff --git a/src/System Application/App/Entity Text/app.json b/src/System Application/App/Entity Text/app.json index 7b2f3979d5..7b1e577725 100644 --- a/src/System Application/App/Entity Text/app.json +++ b/src/System Application/App/Entity Text/app.json @@ -76,6 +76,12 @@ "name": "Upgrade Tags", "publisher": "Microsoft", "version": "24.4.0.0" + }, + { + "id": "de35f591-7216-4e60-8be1-1911d71a7fc2", + "name": "Telemetry", + "publisher": "Microsoft", + "version": "24.4.0.0" } ], "internalsVisibleTo": [], @@ -85,8 +91,8 @@ "idRanges": [ { "from": 2010, - "to": 2015 + "to": 2018 } ], "contextSensitiveHelpUrl": "https://learn.microsoft.com/dynamics365/business-central/" -} +} \ No newline at end of file diff --git a/src/System Application/App/Entity Text/src/EntityText.Codeunit.al b/src/System Application/App/Entity Text/src/EntityText.Codeunit.al index 10338f69dc..a109e193b0 100644 --- a/src/System Application/App/Entity Text/src/EntityText.Codeunit.al +++ b/src/System Application/App/Entity Text/src/EntityText.Codeunit.al @@ -89,10 +89,9 @@ codeunit 2010 "Entity Text" /// The new entity text content. procedure UpdateText(var EntityText: Record "Entity Text"; EntityTextContent: Text) var - TelemetryCategoryLbl: Label 'Entity Text', Locked = true; TelemetryUpdateRecordTxt: Label 'Updating text on record for table %1 and scenario %2.', Locked = true, Comment = '%1 the table id, %2 the scenario id'; begin - Session.LogMessage('0000JVL', StrSubstNo(TelemetryUpdateRecordTxt, Format(EntityText."Source Table Id"), Format(EntityText.Scenario)), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryLbl); + Session.LogMessage('0000JVL', StrSubstNo(TelemetryUpdateRecordTxt, Format(EntityText."Source Table Id"), Format(EntityText.Scenario)), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', EntityTextImpl.GetFeatureName()); EntityTextImpl.SetText(EntityText, EntityTextContent); end; diff --git a/src/System Application/App/Entity Text/src/EntityTextFactboxPart.Page.al b/src/System Application/App/Entity Text/src/EntityTextFactboxPart.Page.al index 769b7481fe..4a53732886 100644 --- a/src/System Application/App/Entity Text/src/EntityTextFactboxPart.Page.al +++ b/src/System Application/App/Entity Text/src/EntityTextFactboxPart.Page.al @@ -125,6 +125,7 @@ page 2011 "Entity Text Factbox Part" procedure SetContext(SourceTableId: Integer; SourceSystemId: Guid; SourceScenario: Enum "Entity Text Scenario"; PlaceholderText: Text) var EntityTextRec: Record "Entity Text"; + EntityTextImpl: Codeunit "Entity Text Impl."; begin HasContext := false; @@ -167,7 +168,7 @@ page 2011 "Entity Text Factbox Part" CurrPage.Update(false); - Session.LogMessage('0000JVC', StrSubstNo(TelemetrySetContextTxt, Format(SourceTableId), Format(SourceScenario), Format(CallerModuleInfo.Id()), CallerModuleInfo.Publisher()), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryLbl); + Session.LogMessage('0000JVC', StrSubstNo(TelemetrySetContextTxt, Format(SourceTableId), Format(SourceScenario), Format(CallerModuleInfo.Id()), CallerModuleInfo.Publisher()), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', EntityTextImpl.GetFeatureName()); end; local procedure OpenEditPage(SourceAction: Enum "Entity Text Actions") @@ -192,11 +193,11 @@ page 2011 "Entity Text Factbox Part" EntityTextCod.OnEditEntityTextWithTriggerAction(TempEntityText, EditAction, Handled, SourceAction); if not Handled then begin - Session.LogMessage('0000LJ4', StrSubstNo(TelemetryNoEditPageTxt, Format(CurrentTableId), Format(CurrentScenario)), Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryLbl); + Session.LogMessage('0000LJ4', StrSubstNo(TelemetryNoEditPageTxt, Format(CurrentTableId), Format(CurrentScenario)), Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', EntityTextImpl.GetFeatureName()); Error(NoHandlerErr); end; - Session.LogMessage('0000JVB', StrSubstNo(TelemetryEditHandledTxt, Format(Handled), Format(EditAction)), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryLbl); + Session.LogMessage('0000JVB', StrSubstNo(TelemetryEditHandledTxt, Format(Handled), Format(EditAction)), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', EntityTextImpl.GetFeatureName()); if EditAction in [Action::LookupOK, Action::OK] then begin EntityTextImpl.InsertSuggestion(CurrentTableId, CurrentSystemId, CurrentScenario, EntityTextImpl.GetText(TempEntityText)); @@ -239,7 +240,6 @@ page 2011 "Entity Text Factbox Part" NotEnabledPlaceholderTxt: Label 'Select Edit to add text', Comment = 'Edit refers to an action on this part with the same name'; DefaultPlaceholderTxt: Label '[Create text](). Then review and edit based on your needs.', Comment = 'Text contained in [here]() will be clickable to invoke the suggest action'; ContextNotSetErr: Label 'The context has not been set on the part. Ensure SetContext has been called from the parent page, contact your partner to fix this.'; - TelemetryCategoryLbl: Label 'Entity Text', Locked = true; NoHandlerErr: Label 'There was no handler to provide an edit page for this entity. Contact your partner.'; TelemetryNoEditPageTxt: Label 'No custom page was specified for edit by partner: table %1, scenario %2.', Locked = true; TelemetryEditHandledTxt: Label 'Edit result was handled: %1, with action %2.', Locked = true; diff --git a/src/System Application/App/Entity Text/src/EntityTextImpl.Codeunit.al b/src/System Application/App/Entity Text/src/EntityTextImpl.Codeunit.al index 8b83354373..31f79c125a 100644 --- a/src/System Application/App/Entity Text/src/EntityTextImpl.Codeunit.al +++ b/src/System Application/App/Entity Text/src/EntityTextImpl.Codeunit.al @@ -8,7 +8,7 @@ namespace System.Text; using System; using System.Utilities; using System.AI; -using System.Azure.KeyVault; +using System.Telemetry; /// /// Implements functionality to handle text suggestions. @@ -29,32 +29,43 @@ codeunit 2012 "Entity Text Impl." exit(AzureOpenAI.IsEnabled(Enum::"Copilot Capability"::"Entity Text", Silent)); end; + internal procedure GetFeatureName(): Text + begin + exit('Entity Text'); + end; + procedure CanSuggest(): Boolean var + EntityTextPrompts: Codeunit "Entity Text Prompts"; EntityTextAOAISettings: Codeunit "Entity Text AOAI Settings"; begin if not EntityTextAOAISettings.IsEnabled(true) then exit(false); - exit(HasPromptInfo()); + exit(EntityTextPrompts.HasPromptInfo()); end; [NonDebuggable] procedure GenerateSuggestion(Facts: Dictionary of [Text, Text]; Tone: Enum "Entity Text Tone"; TextFormat: Enum "Entity Text Format"; TextEmphasis: Enum "Entity Text Emphasis"; CallerModuleInfo: ModuleInfo): Text var + EntityTextPrompts: Codeunit "Entity Text Prompts"; SystemPrompt: Text; UserPrompt: Text; Suggestion: Text; + FactsList: Text; + Category: Text; begin if not IsEnabled(true) then Error(CapabilityDisabledErr); if not CanSuggest() then Error(CannotGenerateErr); - BuildPrompts(Facts, Tone, TextFormat, TextEmphasis, SystemPrompt, UserPrompt); - Session.LogMessage('0000JVG', TelemetryGenerationRequestedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryLbl); + FactsList := BuildFacts(Facts, Category, TextFormat); + EntityTextPrompts.BuildPrompts(FactsList, Category, Tone, TextFormat, TextEmphasis, SystemPrompt, UserPrompt); + + Session.LogMessage('0000JVG', TelemetryGenerationRequestedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', GetFeatureName()); - Suggestion := GenerateAndReviewCompletion(SystemPrompt, UserPrompt, TextFormat, Facts, CallerModuleInfo, Tone, TextEmphasis); + Suggestion := GenerateAndReviewCompletion(SystemPrompt, UserPrompt, TextFormat, Facts, CallerModuleInfo); exit(Suggestion); end; @@ -78,7 +89,7 @@ codeunit 2012 "Entity Text Impl." if not EntityText.Insert() then EntityText.Modify(); - Session.LogMessage('0000JVH', StrSubstNo(TelemetrySuggestionCreatedTxt, Format(SourceTableId), Format(SourceScenario)), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryLbl); + Session.LogMessage('0000JVH', StrSubstNo(TelemetrySuggestionCreatedTxt, Format(SourceTableId), Format(SourceScenario)), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', GetFeatureName()); end; procedure SetText(var EntityText: Record "Entity Text"; Content: Text) @@ -139,93 +150,6 @@ codeunit 2012 "Entity Text Impl." ApiKey := NewApiKey; end; - [NonDebuggable] - local procedure BuildPrompts(var Facts: Dictionary of [Text, Text]; Tone: Enum "Entity Text Tone"; TextFormat: Enum "Entity Text Format"; TextEmphasis: Enum "Entity Text Emphasis"; var SystemPrompt: Text; var UserPrompt: Text) - var - EntityTextAOAISettings: Codeunit "Entity Text AOAI Settings"; - PromptInfo: JsonObject; - SystemPromptJson: JsonToken; - UserPromptJson: JsonToken; - FactsList: Text; - LanguageName: Text; - Category: Text; - begin - FactsList := BuildFacts(Facts, Category, TextFormat); - LanguageName := EntityTextAOAISettings.GetLanguageName(); - - PromptInfo := GetPromptInfo(); - PromptInfo.Get('system', SystemPromptJson); - PromptInfo.Get('user', UserPromptJson); - - SystemPrompt := BuildSinglePrompt(SystemPromptJson.AsObject(), LanguageName, FactsList, Category, Tone, TextFormat, TextEmphasis); - UserPrompt := BuildSinglePrompt(UserPromptJson.AsObject(), LanguageName, FactsList, Category, Tone, TextFormat, TextEmphasis); - end; - - [NonDebuggable] - local procedure BuildSinglePrompt(PromptInfo: JsonObject; LanguageName: Text; FactsList: Text; Category: Text; Tone: Enum "Entity Text Tone"; TextFormat: Enum "Entity Text Format"; TextEmphasis: Enum "Entity Text Emphasis") Prompt: Text - var - PromptHints: JsonToken; - PromptOrder: JsonToken; - PromptHint: JsonToken; - HintName: Text; - NewLineChar: Char; - PromptIndex: Integer; - begin - NewLineChar := 10; - - PromptInfo.Get('prompt', PromptHints); - PromptInfo.Get('order', PromptOrder); - - foreach PromptHint in PromptOrder.AsArray() do begin - HintName := PromptHint.AsValue().AsText(); - if PromptHints.AsObject().Get(HintName, PromptHint) then begin - // found the hint - if PromptHint.IsArray() then begin - PromptIndex := 0; // default value - case HintName of - 'tone': - PromptIndex := Tone.AsInteger(); - 'format': - PromptIndex := TextFormat.AsInteger(); - 'emphasis': - PromptIndex := TextEmphasis.AsInteger(); - end; - - if not PromptHint.AsArray().Get(PromptIndex, PromptHint) then - PromptHint.AsArray().Get(0, PromptHint); - end; - - Prompt += StrSubstNo(PromptHint.AsValue().AsText(), NewLineChar, LanguageName, FactsList, Category); - end; - end; - end; - - [NonDebuggable] - local procedure GetPromptInfo(): JsonObject - var - AzureKeyVault: Codeunit "Azure Key Vault"; - PromptObject: JsonObject; - PromptObjectText: Text; - begin - if not AzureKeyVault.GetAzureKeyVaultSecret(PromptObjectKeyTxt, PromptObjectText) then - Error(PromptNotFoundErr); - - if not PromptObject.ReadFrom(PromptObjectText) then - Error(PromptFormatInvalidErr); - - if (not PromptObject.Contains('user')) or (not PromptObject.Contains('system')) then - Error(PromptFormatMissingPropsErr); - - exit(PromptObject); - end; - - [TryFunction] - [NonDebuggable] - procedure HasPromptInfo() - begin - GetPromptInfo(); - end; - [NonDebuggable] local procedure BuildFacts(var Facts: Dictionary of [Text, Text]; var Category: Text; TextFormat: Enum "Entity Text Format"): Text var @@ -251,7 +175,7 @@ codeunit 2012 "Entity Text Impl." MaxFacts := 20; MaxFactLength := 250; if TotalFacts > MaxFacts then - Session.LogMessage('0000JWA', StrSubstNo(TelemetryPromptManyFactsTxt, Format(Facts.Count()), MaxFacts), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryLbl); + Session.LogMessage('0000JWA', StrSubstNo(TelemetryPromptManyFactsTxt, Format(Facts.Count()), MaxFacts), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', GetFeatureName()); TotalFacts := 0; foreach FactKey in Facts.Keys() do begin @@ -272,38 +196,29 @@ codeunit 2012 "Entity Text Impl." end; [NonDebuggable] - local procedure GenerateAndReviewCompletion(SystemPrompt: Text; UserPrompt: Text; TextFormat: Enum "Entity Text Format"; Facts: Dictionary of [Text, Text]; CallerModuleInfo: ModuleInfo; Tone: Enum "Entity Text Tone"; TextEmphasis: Enum "Entity Text Emphasis"): Text + local procedure GenerateAndReviewCompletion(SystemPrompt: Text; UserPrompt: Text; TextFormat: Enum "Entity Text Format"; Facts: Dictionary of [Text, Text]; CallerModuleInfo: ModuleInfo): Text var + MagicFunction: Codeunit "Magic Function"; + EmptyArguments: JsonObject; Completion: Text; - CompletionTag: Text; - CompletionPar: Text; MaxAttempts: Integer; Attempt: Integer; begin MaxAttempts := 5; for Attempt := 0 to MaxAttempts do begin - if TextFormat = TextFormat::TaglineParagraph then begin - BuildPrompts(Facts, Tone, TextFormat::Tagline, TextEmphasis, SystemPrompt, UserPrompt); - CompletionTag := GenerateCompletion(SystemPrompt, UserPrompt, CallerModuleInfo); - - BuildPrompts(Facts, Tone, TextFormat::Paragraph, TextEmphasis, SystemPrompt, UserPrompt); - CompletionPar := GenerateCompletion(SystemPrompt, UserPrompt, CallerModuleInfo); - Completion := CompletionTag + EncodedNewlineTok + EncodedNewlineTok + CompletionPar; - end - else - Completion := GenerateCompletion(SystemPrompt, UserPrompt, CallerModuleInfo); + Completion := GenerateCompletion(TextFormat, SystemPrompt, UserPrompt, CallerModuleInfo); if (not CompletionContainsPrompt(Completion, SystemPrompt)) and IsGoodCompletion(Completion, TextFormat, Facts) then exit(Completion); Sleep(500); - Session.LogMessage('0000LVP', StrSubstNo(TelemetryGenerationRetryTxt, Attempt + 1), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryLbl); + Session.LogMessage('0000LVP', StrSubstNo(TelemetryGenerationRetryTxt, Attempt + 1), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', GetFeatureName()); end; // this completion is of low quality - Session.LogMessage('0000JYB', TelemetryLowQualityCompletionTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryLbl); + Session.LogMessage('0000JYB', TelemetryLowQualityCompletionTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', GetFeatureName()); - exit(''); + Error(MagicFunction.Execute(EmptyArguments)); end; [NonDebuggable] @@ -320,7 +235,7 @@ codeunit 2012 "Entity Text Impl." if PromptSentence <> '' then if Completion.Contains(PromptSentence) then begin - Session.LogMessage('0000JZG', StrSubstNo(TelemetryCompletionHasPromptTxt, PromptSentence), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryLbl); + Session.LogMessage('0000JZG', StrSubstNo(TelemetryCompletionHasPromptTxt, PromptSentence), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', GetFeatureName()); exit(true); end; end; @@ -341,12 +256,12 @@ codeunit 2012 "Entity Text Impl." FormatValid: Boolean; begin if Completion = '' then begin - Session.LogMessage('0000JWJ', TelemetryCompletionEmptyTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryLbl); + Session.LogMessage('0000JWJ', TelemetryCompletionEmptyTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', GetFeatureName()); exit(false); end; if Completion.ToLower().StartsWith('tagline:') then begin - Session.LogMessage('0000JYD', TelemetryTaglineCleanedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryLbl); + Session.LogMessage('0000JYD', TelemetryTaglineCleanedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', GetFeatureName()); Completion := CopyStr(Completion, 9).Trim(); end; FormatValid := true; @@ -361,11 +276,11 @@ codeunit 2012 "Entity Text Impl." TextFormat::Tagline: FormatValid := not Completion.Contains(EncodedNewlineTok); // a tagline should not have any newline TextFormat::Brief: - FormatValid := Completion.Contains(EncodedNewlineTok + EncodedNewlineTok) and (Completion.Contains(EncodedNewlineTok + '-') or Completion.Contains(EncodedNewlineTok + '•')); // the brief should contain a pargraph and a list + FormatValid := Completion.Contains(EncodedNewlineTok + EncodedNewlineTok) and (Completion.Contains(EncodedNewlineTok + '-') or Completion.Contains(EncodedNewlineTok + '•') or Completion.Contains(EncodedNewlineTok + '*')); // the brief should contain a pargraph and a list end; if not FormatValid then begin - Session.LogMessage('0000JYC', StrSubstNo(TelemetryCompletionInvalidFormatTxt, TextFormat), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryLbl); + Session.LogMessage('0000JYC', StrSubstNo(TelemetryCompletionInvalidFormatTxt, TextFormat), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', GetFeatureName()); exit(false); end; @@ -388,13 +303,13 @@ codeunit 2012 "Entity Text Impl." end; if not FoundNumber then begin - Session.LogMessage('0000JYE', StrSubstNo(TelemetryCompletionInvalidNumberTxt, CandidateNumber), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryLbl); + Session.LogMessage('0000JYE', StrSubstNo(TelemetryCompletionInvalidNumberTxt, CandidateNumber), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', GetFeatureName()); exit(false); // made up number end; until TempMatches.Next() = 0; if Completion.Contains(StrSubstNo(NoteParagraphTxt, EncodedNewlineTok)) or Completion.Contains(StrSubstNo(TranslationParagraphTxt, EncodedNewlineTok)) then begin - Session.LogMessage('0000LHZ', TelemetryCompletionExtraTextTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryLbl); + Session.LogMessage('0000LHZ', TelemetryCompletionExtraTextTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', GetFeatureName()); exit(false); end; @@ -402,7 +317,7 @@ codeunit 2012 "Entity Text Impl." end; [NonDebuggable] - local procedure GenerateCompletion(SystemPrompt: Text; UserPrompt: Text; CallerModuleInfo: ModuleInfo): Text + local procedure GenerateCompletion(TextFormat: Enum "Entity Text Format"; SystemPrompt: Text; UserPrompt: Text; CallerModuleInfo: ModuleInfo): Text var AzureOpenAI: Codeunit "Azure OpenAI"; EntityTextAOAISettings: Codeunit "Entity Text AOAI Settings"; @@ -410,22 +325,28 @@ codeunit 2012 "Entity Text Impl." AOAIOperationResponse: Codeunit "AOAI Operation Response"; AOAICompletionParams: Codeunit "AOAI Chat Completion Params"; AOAIChatMessages: Codeunit "AOAI Chat Messages"; - HttpUtility: DotNet HttpUtility; + MagicFunction: Codeunit "Magic Function"; + GenerateProdMktAdFunction: Codeunit "Generate Prod Mkt Ad Function"; + AOAIFunctionResponse: Codeunit "AOAI Function Response"; + FeatureTelemetry: Codeunit "Feature Telemetry"; + TelemetryCD: Dictionary of [Text, Text]; + StartDateTime: DateTime; + DurationAsBigInt: BigInteger; Result: Text; - NewLineChar: Char; EntityTextModuleInfo: ModuleInfo; + ResponseErr: Label 'AOAI Operation failed, response error code: %1', Comment = '%1 = Error code', Locked = true; begin - NewLineChar := 10; - NavApp.GetCurrentModuleInfo(EntityTextModuleInfo); if (not (Endpoint = '')) and (not (Deployment = '')) then AzureOpenAI.SetAuthorization(Enum::"AOAI Model Type"::"Chat Completions", Endpoint, Deployment, ApiKey) else if (not IsNullGuid(CallerModuleInfo.Id())) and (CallerModuleInfo.Publisher() = EntityTextModuleInfo.Publisher()) then - AzureOpenAI.SetAuthorization(Enum::"AOAI Model Type"::"Chat Completions", AOAIDeployments.GetGPT35TurboLatest()) + AzureOpenAI.SetAuthorization(Enum::"AOAI Model Type"::"Chat Completions", AOAIDeployments.GetGPT4Preview()) else begin - Session.LogMessage('0000LJB', TelemetryNoAuthorizationHandlerTxt, Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryLbl); + TelemetryCD.Add('CallerModuleInfo', Format(CallerModuleInfo.Publisher())); + TelemetryCD.Add('EntityTextModuleInfo', Format(EntityTextModuleInfo.Publisher())); + FeatureTelemetry.LogError('0000LJB', GetFeatureName(), 'Entity Text Authorization', TelemetryNoAuthorizationHandlerTxt, '', TelemetryCD); Error(NoAuthorizationHandlerErr); end; @@ -433,23 +354,44 @@ codeunit 2012 "Entity Text Impl." AOAICompletionParams.SetMaxTokens(2500); AOAICompletionParams.SetTemperature(0.7); + + AOAIChatMessages.AddTool(MagicFunction); + + GenerateProdMktAdFunction.SetTextFormat(TextFormat); + AOAIChatMessages.AddTool(GenerateProdMktAdFunction); + AOAIChatMessages.SetToolChoice('auto'); + AOAIChatMessages.SetPrimarySystemMessage(SystemPrompt); AOAIChatMessages.AddUserMessage(UserPrompt); + StartDateTime := CurrentDateTime(); AzureOpenAI.GenerateChatCompletion(AOAIChatMessages, AOAICompletionParams, AOAIOperationResponse); - if not AOAIOperationResponse.IsSuccess() then begin + DurationAsBigInt := CurrentDateTime() - StartDateTime; + TelemetryCD.Add('Response time', Format(DurationAsBigInt)); + + if AOAIOperationResponse.IsSuccess() then + if AOAIOperationResponse.IsFunctionCall() then begin + AOAIFunctionResponse := AOAIOperationResponse.GetFunctionResponse(); + FeatureTelemetry.LogUsage('0000N5C', GetFeatureName(), 'Call Chat Completion API', TelemetryCD); + if AOAIFunctionResponse.IsSuccess() then begin + Result := AOAIFunctionResponse.GetResult(); + if AOAIFunctionResponse.GetFunctionName() = MagicFunction.GetName() then + Error(Result) + end else begin + Clear(Result); + FeatureTelemetry.LogError('0000N5Z', GetFeatureName(), 'Call Chat Completion API', 'AOAI Function response is not sucessfull', '', TelemetryCD); + end; + end else begin + Clear(Result); + FeatureTelemetry.LogError('0000N5A', GetFeatureName(), 'Call Chat Completion API', 'AOAI response is not a function call', '', TelemetryCD); + end + else begin Clear(Result); - Error(CompletionDeniedPhraseErr); + FeatureTelemetry.LogError('0000N5B', GetFeatureName(), 'Call Chat Completion API', StrSubstNo(ResponseErr, AOAIOperationResponse.GetStatusCode()), '', TelemetryCD); end; - Result := HttpUtility.HtmlEncode(AOAIChatMessages.GetLastMessage()); - Result := Result.Replace(NewLineChar, EncodedNewlineTok); - - if EntityTextAOAISettings.ContainsWordsInDenyList(Result) then begin + if EntityTextAOAISettings.ContainsWordsInDenyList(Result) then Clear(Result); - Error(CompletionDeniedPhraseErr); - end; - exit(Result); end; @@ -457,7 +399,6 @@ codeunit 2012 "Entity Text Impl." Endpoint: Text; Deployment: Text; ApiKey: SecretText; - PromptObjectKeyTxt: Label 'AOAI-Prompt-23', Locked = true; FactTemplateTxt: Label '- %1: %2%3', Locked = true; EncodedNewlineTok: Label '
', Locked = true; NoteParagraphTxt: Label '%1Note:%1', Locked = true, Comment = 'This constant is used to limit the cases when the model goes out of format and must stay in English only.'; @@ -467,12 +408,7 @@ codeunit 2012 "Entity Text Impl." CapabilityDisabledErr: Label 'Sorry, your Copilot isn''t activated for Entity Text. Contact the system administrator.'; MinFactsErr: Label 'There''s not enough information available to draft a text. Please provide more.'; NotEnoughFactsForFormatErr: Label 'There''s not enough information available to draft a text for the chosen format. Please provide more, or choose another format.'; - PromptNotFoundErr: Label 'The prompt definition could not be found.'; - PromptFormatInvalidErr: Label 'The prompt definition is in an invalid format.'; - CompletionDeniedPhraseErr: Label 'Sorry, we could not generate a good suggestion for this. Review the information provided, consider your choice of words, and try again.'; - PromptFormatMissingPropsErr: Label 'Required properties are missing from the prompt definition.'; NoAuthorizationHandlerErr: Label 'There was no handler to provide authorization information for the suggestion. Contact your partner.'; - TelemetryCategoryLbl: Label 'Entity Text', Locked = true; TelemetryGenerationRequestedTxt: Label 'New suggestion requested.', Locked = true; TelemetrySuggestionCreatedTxt: Label 'A new suggestion was generated for table %1, scenario %2', Locked = true; TelemetryCompletionEmptyTxt: Label 'The returned completion was empty.', Locked = true; diff --git a/src/System Application/App/Entity Text/src/EntityTextPart.Page.al b/src/System Application/App/Entity Text/src/EntityTextPart.Page.al index 2b69760bcf..939682371f 100644 --- a/src/System Application/App/Entity Text/src/EntityTextPart.Page.al +++ b/src/System Application/App/Entity Text/src/EntityTextPart.Page.al @@ -58,6 +58,7 @@ page 2012 "Entity Text Part" /// Text cannot be suggested without calling SetContext. procedure SetContext(InitialText: Text; var InitialFacts: Dictionary of [Text, Text]; var InitialTextTone: Enum "Entity Text Tone"; var InitialTextFormat: Enum "Entity Text Format") var + EntityTextImpl: Codeunit "Entity Text Impl."; CurrentModuleInfo: ModuleInfo; EntityTextModuleInfo: ModuleInfo; begin @@ -67,7 +68,7 @@ page 2012 "Entity Text Part" if CurrentModuleInfo.Id() <> EntityTextModuleInfo.Id() then CallerModuleInfo := CurrentModuleInfo; - Session.LogMessage('0000JVK', StrSubstNo(TelemetrySetContextTxt, Format(CallerModuleInfo.Id()), CallerModuleInfo.Publisher()), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryLbl); + Session.LogMessage('0000JVK', StrSubstNo(TelemetrySetContextTxt, Format(CallerModuleInfo.Id()), CallerModuleInfo.Publisher()), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', EntityTextImpl.GetFeatureName()); Facts := InitialFacts; TextTone := InitialTextTone; @@ -146,7 +147,7 @@ page 2012 "Entity Text Part" var EntityTextImpl: Codeunit "Entity Text Impl."; begin - Session.LogMessage('0000JVL', StrSubstNo(TelemetryUpdateRecordTxt, Format(EntityText."Source Table Id"), Format(EntityText.Scenario)), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryLbl); + Session.LogMessage('0000JVL', StrSubstNo(TelemetryUpdateRecordTxt, Format(EntityText."Source Table Id"), Format(EntityText.Scenario)), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', EntityTextImpl.GetFeatureName()); EntityTextImpl.SetText(EntityText, EntityTextContent); end; @@ -162,13 +163,14 @@ page 2012 "Entity Text Part" internal procedure SetModuleInfo(NewModuleInfo: ModuleInfo) var + EntityTextImpl: Codeunit "Entity Text Impl."; CurrentModuleInfo: ModuleInfo; EntityTextModuleInfo: ModuleInfo; begin NavApp.GetCurrentModuleInfo(EntityTextModuleInfo); NavApp.GetCallerModuleInfo(CurrentModuleInfo); - Session.LogMessage('0000JVM', StrSubstNo(TelemetrySetModuleTxt, Format(NewModuleInfo.Id()), NewModuleInfo.Publisher(), Format(CurrentModuleInfo.Id()), CurrentModuleInfo.Publisher()), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryLbl); + Session.LogMessage('0000JVM', StrSubstNo(TelemetrySetModuleTxt, Format(NewModuleInfo.Id()), NewModuleInfo.Publisher(), Format(CurrentModuleInfo.Id()), CurrentModuleInfo.Publisher()), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', EntityTextImpl.GetFeatureName()); if CurrentModuleInfo.Id() <> EntityTextModuleInfo.Id() then exit; @@ -185,7 +187,6 @@ page 2012 "Entity Text Part" CallerModuleInfo: ModuleInfo; ContentCaption: Text; ContentLbl: Label 'Content'; - TelemetryCategoryLbl: Label 'Entity Text', Locked = true; TelemetrySetContextTxt: Label 'Context set for the entity text edit page. Calling module %1 (%2).', Locked = true, Comment = '%1 = the app id, %2 = the publisher name'; TelemetrySetModuleTxt: Label 'Attempting to update the calling module to %1 (%2). This was requested by %3 (%4).', Locked = true, Comment = '%1 the new app id, %2 the new publisher, %3 the calling app id, %4 the calling publisher'; TelemetryUpdateRecordTxt: Label 'Updating text on record for table %1 and scenario %2.', Locked = true, Comment = '%1 the table id, %2 the scenario id'; diff --git a/src/System Application/App/Entity Text/src/EntityTextPrompts.Codeunit.al b/src/System Application/App/Entity Text/src/EntityTextPrompts.Codeunit.al new file mode 100644 index 0000000000..5aecf035a6 --- /dev/null +++ b/src/System Application/App/Entity Text/src/EntityTextPrompts.Codeunit.al @@ -0,0 +1,148 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.Text; + +using System.Azure.KeyVault; +using System.Telemetry; + +codeunit 2016 "Entity Text Prompts" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + [NonDebuggable] + internal procedure GetAzureKeyVaultSecret(var SecretValue: Text; SecretName: Text) + var + EntityTextImpl: Codeunit "Entity Text Impl."; + AzureKeyVault: Codeunit "Azure Key Vault"; + FeatureTelemetry: Codeunit "Feature Telemetry"; + begin + if not AzureKeyVault.GetAzureKeyVaultSecret(SecretName, SecretValue) then begin + FeatureTelemetry.LogError('0000N5D', EntityTextImpl.GetFeatureName(), 'Get prompt from Key Vault', TelemetryConstructingPromptFailedErr); + Error(ConstructingPromptFailedErr); + end; + end; + + [NonDebuggable] + internal procedure GetGenerateProdMktAdFuncPrompt(TextFormat: Enum "Entity Text Format"): Text + var + PromptObject: JsonObject; + FunctPropArrayToken: JsonToken; + FunctPropReqArrayToken: JsonToken; + FuncPropArray: JsonArray; + FuncPropReqArray: JsonArray; + FunctionPropObject: JsonToken; + FunctionPropReqObject: JsonToken; + BCETGenerateProdMAdFuncPrompt: Text; + BCETPromptObject: Text; + begin + GetAzureKeyVaultSecret(BCETPromptObject, 'BCETPromptObject'); + PromptObject.ReadFrom(BCETPromptObject); + PromptObject.Get('function-properties', FunctPropArrayToken); + FuncPropArray := FunctPropArrayToken.AsArray(); + FuncPropArray.Get(TextFormat.AsInteger(), FunctionPropObject); + + PromptObject.Get('required-properties', FunctPropReqArrayToken); + FuncPropReqArray := FunctPropReqArrayToken.AsArray(); + FuncPropReqArray.Get(TextFormat.AsInteger(), FunctionPropReqObject); + + GetAzureKeyVaultSecret(BCETGenerateProdMAdFuncPrompt, 'BCETGenerateProdMAdFuncPrompt'); + BCETGenerateProdMAdFuncPrompt := StrSubstNo(BCETGenerateProdMAdFuncPrompt, Format(FunctionPropObject), Format(FunctionPropReqObject)); + + exit(BCETGenerateProdMAdFuncPrompt); + end; + + [NonDebuggable] + internal procedure GetMagicFunctionPrompt(): Text + var + BCETMagicFunctionPrompt: Text; + begin + GetAzureKeyVaultSecret(BCETMagicFunctionPrompt, 'BCETMagicFunctionPrompt'); + exit(BCETMagicFunctionPrompt); + end; + + [NonDebuggable] + procedure BuildPrompts(FactsList: Text; Category: Text; Tone: Enum "Entity Text Tone"; TextFormat: Enum "Entity Text Format"; TextEmphasis: Enum "Entity Text Emphasis"; var SystemPrompt: Text; var UserPrompt: Text) + var + EntityTextAOAISettings: Codeunit "Entity Text AOAI Settings"; + PromptObject: JsonObject; + SystemPromptJson: JsonToken; + UserPromptJson: JsonToken; + BCETPromptObject: Text; + LanguageName: Text; + begin + LanguageName := EntityTextAOAISettings.GetLanguageName(); + + GetAzureKeyVaultSecret(BCETPromptObject, 'BCETPromptObject'); + PromptObject.ReadFrom(BCETPromptObject); + PromptObject.Get('system', SystemPromptJson); + PromptObject.Get('user', UserPromptJson); + + SystemPrompt := BuildSinglePrompt(SystemPromptJson.AsObject(), LanguageName, FactsList, Category, Tone, TextFormat, TextEmphasis); + UserPrompt := BuildSinglePrompt(UserPromptJson.AsObject(), LanguageName, FactsList, Category, Tone, TextFormat, TextEmphasis); + end; + + [NonDebuggable] + local procedure BuildSinglePrompt(PromptInfo: JsonObject; LanguageName: Text; FactsList: Text; Category: Text; Tone: Enum "Entity Text Tone"; TextFormat: Enum "Entity Text Format"; TextEmphasis: Enum "Entity Text Emphasis") Prompt: Text + var + PromptHints: JsonToken; + PromptOrder: JsonToken; + PromptArray: JsonArray; + PromptHint: JsonToken; + CategoryIndex: Integer; + HintName: Text; + NewLineChar: Char; + PromptIndex: Integer; + begin + NewLineChar := 10; + + PromptInfo.Get('prompt', PromptHints); + PromptInfo.Get('order', PromptOrder); + + PromptArray := PromptOrder.AsArray(); + CategoryIndex := PromptArray.IndexOf('category'); + + if CategoryIndex <> -1 then + if Category = '' then + PromptArray.RemoveAt(CategoryIndex); + + foreach PromptHint in PromptArray do begin + HintName := PromptHint.AsValue().AsText(); + if PromptHints.AsObject().Get(HintName, PromptHint) then begin + // found the hint + if PromptHint.IsArray() then begin + PromptIndex := 0; // default value + case HintName of + 'tone': + PromptIndex := Tone.AsInteger(); + 'format': + PromptIndex := TextFormat.AsInteger(); + 'emphasis': + PromptIndex := TextEmphasis.AsInteger(); + end; + + if not PromptHint.AsArray().Get(PromptIndex, PromptHint) then + PromptHint.AsArray().Get(0, PromptHint); + end; + + Prompt += StrSubstNo(PromptHint.AsValue().AsText(), NewLineChar, LanguageName, FactsList, Category); + end; + end; + end; + + [TryFunction] + [NonDebuggable] + procedure HasPromptInfo() + var + BCETPromptObject: Text; + begin + GetAzureKeyVaultSecret(BCETPromptObject, 'BCETPromptObject'); + end; + + var + ConstructingPromptFailedErr: label 'There was an error with sending the call to Copilot. Log a Business Central support request about this.', Comment = 'Copilot is a Microsoft service name and must not be translated'; + TelemetryConstructingPromptFailedErr: label 'There was an error with constructing the chat completion prompt from the Key Vault.', Locked = true; +} \ No newline at end of file diff --git a/src/System Application/App/Entity Text/src/FunctionsImpl/GenerateProdMktAdFunction.Codeunit.al b/src/System Application/App/Entity Text/src/FunctionsImpl/GenerateProdMktAdFunction.Codeunit.al new file mode 100644 index 0000000000..04facae4ff --- /dev/null +++ b/src/System Application/App/Entity Text/src/FunctionsImpl/GenerateProdMktAdFunction.Codeunit.al @@ -0,0 +1,82 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.Text; + +using System.AI; +using System.Telemetry; + +codeunit 2017 "Generate Prod Mkt Ad Function" implements "AOAI Function" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + var + FunctionNameLbl: Label 'generate_product_marketing_ad', Locked = true; + TextFormat: Enum "Entity Text Format"; + + [NonDebuggable] + procedure GetPrompt(): JsonObject + var + Prompt: Codeunit "Entity Text Prompts"; + PromptJson: JsonObject; + begin + PromptJson.ReadFrom(Prompt.GetGenerateProdMktAdFuncPrompt(TextFormat)); + exit(PromptJson); + end; + + [NonDebuggable] + procedure Execute(Arguments: JsonObject): Variant + var + EntityTextImpl: Codeunit "Entity Text Impl."; + FeatureTelemetry: Codeunit "Feature Telemetry"; + FeatureTelemetryCD: Dictionary of [Text, Text]; + TaglineToken: JsonToken; + ParagraphToken: JsonToken; + BriefToken: JsonToken; + Result: Text; + EncodedNewlineTok: Label '
', Locked = true; + NewLineChar: Char; + begin + NewLineChar := 10; + case TextFormat of + TextFormat::TaglineParagraph: + begin + Arguments.Get('tagline', TaglineToken); + Arguments.Get('paragraph', ParagraphToken); + Result := TaglineToken.AsValue().AsText() + EncodedNewlineTok + EncodedNewlineTok + ParagraphToken.AsValue().AsText(); + Result := Result.Replace(NewLineChar, EncodedNewlineTok); + end; + TextFormat::Paragraph: + begin + Arguments.Get('paragraph', ParagraphToken); + Result := ParagraphToken.AsValue().AsText().Replace(NewLineChar, EncodedNewlineTok); + end; + TextFormat::Tagline: + begin + Arguments.Get('tagline', TaglineToken); + Result := TaglineToken.AsValue().AsText().Replace(NewLineChar, EncodedNewlineTok); + end; + TextFormat::Brief: + begin + Arguments.Get('brief-ad', BriefToken); + Result := BriefToken.AsValue().AsText().Replace(NewLineChar, EncodedNewlineTok); + end; + end; + FeatureTelemetryCD.Add('Text Format', TextFormat.Names.Get(TextFormat.Ordinals.IndexOf(TextFormat.AsInteger()))); + FeatureTelemetry.LogUsage('0000N58', EntityTextImpl.GetFeatureName(), 'function_call: generate_product_marketing_ad', FeatureTelemetryCD); + exit(Result); + end; + + procedure GetName(): Text + begin + exit(FunctionNameLbl); + end; + + procedure SetTextFormat(Format: Enum "Entity Text Format") + begin + TextFormat := Format; + end; +} \ No newline at end of file diff --git a/src/System Application/App/Entity Text/src/FunctionsImpl/MagicFunction.Codeunit.al b/src/System Application/App/Entity Text/src/FunctionsImpl/MagicFunction.Codeunit.al new file mode 100644 index 0000000000..e9ed3b3ba5 --- /dev/null +++ b/src/System Application/App/Entity Text/src/FunctionsImpl/MagicFunction.Codeunit.al @@ -0,0 +1,44 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.Text; + +using System.AI; +using System.Telemetry; + +codeunit 2018 "Magic Function" implements "AOAI Function" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + var + FunctionNameLbl: Label 'magic_function', Locked = true; + CompletionDeniedPhraseErr: Label 'Sorry, we could not generate a good suggestion for this. Review the information provided, consider your choice of words, and try again.', Locked = true; + + [NonDebuggable] + procedure GetPrompt(): JsonObject + var + Prompt: Codeunit "Entity Text Prompts"; + PromptJson: JsonObject; + begin + PromptJson.ReadFrom(Prompt.GetMagicFunctionPrompt()); + exit(PromptJson); + end; + + [NonDebuggable] + procedure Execute(Arguments: JsonObject): Variant + var + EntityTextImpl: Codeunit "Entity Text Impl."; + FeatureTelemetry: Codeunit "Feature Telemetry"; + begin + FeatureTelemetry.LogUsage('0000N59', EntityTextImpl.GetFeatureName(), 'function_call: magic_function'); + exit(CompletionDeniedPhraseErr); + end; + + procedure GetName(): Text + begin + exit(FunctionNameLbl); + end; +} \ No newline at end of file