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