From 816520c6e27c8e9a23ebf3e3726de1e81dfa1d3c Mon Sep 17 00:00:00 2001 From: Dmitry Katson Date: Fri, 27 Sep 2024 22:21:08 +0700 Subject: [PATCH] Title: Fix Intent Mapping and Field Validation in Number Series Generation Summary: This commit resolves three bugs in the Number Series generation process. It improves intent mapping for the "Generate" command, corrects field checks for existing Number Series, and refines validation logic to allow optional fields to be blank. These fixes enhance the accuracy and reliability of Number Series generation. Fixes #2106 --- .../NoSeriesCopilotObjects.permissionset.al | 15 ++++ .../Copilot/NoSeriesCopilotImpl.Codeunit.al | 74 +++++++++++++------ .../src/Copilot/NoSeriesGeneration.Page.al | 16 ++-- .../NoSeriesTextMatchImpl.Codeunit.al} | 30 ++++++-- .../Tools/NoSeriesCopAddIntent.Codeunit.al | 6 +- .../Tools/NoSeriesCopChangeIntent.Codeunit.al | 6 +- .../Tools/NoSeriesCopNxtYrIntent.Codeunit.al | 4 +- .../Tools/NoSeriesCopToolsImpl.Codeunit.al | 21 ++---- 8 files changed, 115 insertions(+), 57 deletions(-) create mode 100644 src/Business Foundation/App/NoSeriesCopilot/Permissions/NoSeriesCopilotObjects.permissionset.al rename src/Business Foundation/App/NoSeriesCopilot/src/Copilot/{Tools/RecordMatchImpl.Codeunit.al => Search/Classic/NoSeriesTextMatchImpl.Codeunit.al} (84%) diff --git a/src/Business Foundation/App/NoSeriesCopilot/Permissions/NoSeriesCopilotObjects.permissionset.al b/src/Business Foundation/App/NoSeriesCopilot/Permissions/NoSeriesCopilotObjects.permissionset.al new file mode 100644 index 0000000000..db8cdf26b4 --- /dev/null +++ b/src/Business Foundation/App/NoSeriesCopilot/Permissions/NoSeriesCopilotObjects.permissionset.al @@ -0,0 +1,15 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace Microsoft.Foundation.NoSeries; + +permissionset 330 "No. Series Copilot - Objects" +{ + Access = Internal; + Assignable = false; + Permissions = + codeunit "No. Series Copilot Impl." = X, + codeunit "No. Series Text Match Impl." = X; +} \ No newline at end of file diff --git a/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/NoSeriesCopilotImpl.Codeunit.al b/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/NoSeriesCopilotImpl.Codeunit.al index a2cd936b26..71d3b19062 100644 --- a/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/NoSeriesCopilotImpl.Codeunit.al +++ b/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/NoSeriesCopilotImpl.Codeunit.al @@ -150,7 +150,7 @@ codeunit 324 "No. Series Copilot Impl." Telemetry: Codeunit Telemetry; ToolsSelectionPrompt: Text; begin - if not AzureKeyVault.GetAzureKeyVaultSecret('NoSeriesCopilotToolsSelectionPrompt', ToolsSelectionPrompt) then begin + if not AzureKeyVault.GetAzureKeyVaultSecret('NoSeriesCopilotToolsSelectionPromptV2', ToolsSelectionPrompt) then begin // TODO: As the prompt should be different for v25.0 and v25.1, we need to add a new secret for v25.1 and update the code to get the correct prompt based on the version Telemetry.LogMessage('0000NDY', TelemetryToolsSelectionPromptRetrievalErr, Verbosity::Error, DataClassification::SystemMetadata); Error(ToolLoadingErr); end; @@ -346,43 +346,69 @@ codeunit 324 "No. Series Copilot Impl." for i := 0 to Json.GetCollectionCount() - 1 do begin Json.GetObjectFromCollectionByIndex(i, NoSeriesObj); Json.InitializeObject(NoSeriesObj); - CheckTextPropertyExistAndCheckIfNotEmpty('seriesCode', Json); - CheckMaximumLengthOfPropertyValue('seriesCode', Json, 20); - CheckTextPropertyExistAndCheckIfNotEmpty('description', Json); - CheckTextPropertyExistAndCheckIfNotEmpty('startingNo', Json); - CheckMaximumLengthOfPropertyValue('startingNo', Json, 20); - CheckTextPropertyExistAndCheckIfNotEmpty('endingNo', Json); - CheckMaximumLengthOfPropertyValue('endingNo', Json, 20); - CheckTextPropertyExistAndCheckIfNotEmpty('warningNo', Json); - CheckMaximumLengthOfPropertyValue('warningNo', Json, 20); - CheckIntegerPropertyExistAndCheckIfNotEmpty('incrementByNo', Json); - CheckIntegerPropertyExistAndCheckIfNotEmpty('tableId', Json); - CheckIntegerPropertyExistAndCheckIfNotEmpty('fieldId', Json); + CheckMandatoryProperties(Json); end; end; + local procedure CheckMandatoryProperties(var Json: Codeunit Json) + begin + if not CheckIfNumberSeriesIsGenerated(Json) then + exit; + + CheckJsonTextProperty('seriesCode', Json, true); + CheckMaximumLengthOfPropertyValue('seriesCode', Json, 20); + CheckJsonTextProperty('description', Json, true); + CheckJsonTextProperty('startingNo', Json, true); + CheckMaximumLengthOfPropertyValue('startingNo', Json, 20); + CheckJsonTextProperty('endingNo', Json, false); + CheckMaximumLengthOfPropertyValue('endingNo', Json, 20); + CheckJsonTextProperty('warningNo', Json, false); + CheckMaximumLengthOfPropertyValue('warningNo', Json, 20); + CheckJsonIntegerProperty('incrementByNo', Json, false); + CheckJsonIntegerProperty('tableId', Json, true); + CheckJsonIntegerProperty('fieldId', Json, true); + end; + + local procedure CheckIfNumberSeriesIsGenerated(var Json: Codeunit Json): Boolean + var + IsExists: Boolean; + begin + Json.GetBoolPropertyValueFromJObjectByName('exists', IsExists); + exit(not IsExists); + end; + local procedure CheckIfArrayIsNotEmpty(NumberOfGeneratedNoSeries: Integer) begin if NumberOfGeneratedNoSeries = 0 then Error(EmptyCompletionErr); end; - local procedure CheckTextPropertyExistAndCheckIfNotEmpty(propertyName: Text; var Json: Codeunit Json) + local procedure CheckJsonTextProperty(propertyName: Text; var Json: Codeunit Json; IsRequired: Boolean) var value: Text; begin Json.GetStringPropertyValueByName(propertyName, value); - if value = '' then - Error(IncorrectCompletionErr, propertyName); + if not IsRequired then + exit; + + if value <> '' then + exit; + + Error(IncorrectCompletionErr, propertyName); end; - local procedure CheckIntegerPropertyExistAndCheckIfNotEmpty(propertyName: Text; var Json: Codeunit Json) + local procedure CheckJsonIntegerProperty(propertyName: Text; var Json: Codeunit Json; IsRequired: Boolean) var PropertyValue: Integer; begin Json.GetIntegerPropertyValueFromJObjectByName(propertyName, PropertyValue); - if PropertyValue = 0 then - Error(IncorrectCompletionErr, propertyName); + if not IsRequired then + exit; + + if PropertyValue <> 0 then + exit; + + Error(IncorrectCompletionErr, propertyName); end; local procedure CheckMaximumLengthOfPropertyValue(propertyName: Text; var Json: Codeunit Json; maxLength: Integer) @@ -390,8 +416,10 @@ codeunit 324 "No. Series Copilot Impl." value: Text; begin Json.GetStringPropertyValueByName(propertyName, value); - if StrLen(value) > maxLength then - Error(TextLengthIsOverMaxLimitErr, propertyName, maxLength); + if StrLen(value) <= maxLength then + exit; + + Error(TextLengthIsOverMaxLimitErr, propertyName, maxLength); end; local procedure ReadGeneratedNumberSeriesJArray(Completion: Text) NoSeriesJArray: JsonArray @@ -445,14 +473,12 @@ codeunit 324 "No. Series Copilot Impl." var NoSeriesCode: Text; NoSeriesObj: Text; - IsExists: Boolean; begin Json.GetObjectFromCollectionByIndex(i, NoSeriesObj); Json.InitializeObject(NoSeriesObj); - Json.GetBoolPropertyValueFromJObjectByName('exists', IsExists); Json.GetStringPropertyValueByName('seriesCode', NoSeriesCode); - if NoSeriesCodes.Contains(NoSeriesCode) and (not IsExists) then begin + if NoSeriesCodes.Contains(NoSeriesCode) and (CheckIfNumberSeriesIsGenerated(Json)) then begin Json.RemoveJObjectFromCollection(i); exit; end; diff --git a/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/NoSeriesGeneration.Page.al b/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/NoSeriesGeneration.Page.al index 7dee646d97..e4db90211b 100644 --- a/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/NoSeriesGeneration.Page.al +++ b/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/NoSeriesGeneration.Page.al @@ -114,23 +114,23 @@ page 332 "No. Series Generation" { Caption = 'Prepare for next year'; - action(SetupForNextYear) + action(PrepareForNextYear) { - Caption = 'Set up number series for the next year'; + Caption = 'Prepare number series for the next year'; ToolTip = 'Sample prompt for setting up number series for the next year.'; trigger OnAction() begin - InputText := SetupForNextYearLbl; + InputText := PrepareForNextYearLbl; CurrPage.Update(); end; } - action(SetupModuleForNextYear) + action(PrepareModuleForNextYear) { - Caption = 'Set up number series for the [sales] module for the next year'; + Caption = 'Prepare number series for the [sales] module for the next year'; ToolTip = 'Sample prompt for setting up number series for a specific module for the next year. Replace [sales] with the module you want to set up number series for.'; trigger OnAction() begin - InputText := SetupModuleForNextYearLbl; + InputText := PrepareModuleForNextYearLbl; CurrPage.Update(); end; } @@ -178,8 +178,8 @@ page 332 "No. Series Generation" CreateNoSeriesForModuleWithPatternLbl: Label 'Create number series for [specify here] module in the format '; CreateNoSeriesForCompanyLbl: Label 'Create numbers series for the new company'; ChangeNumberLbl: Label 'Change the [specify here] number to '; - SetupForNextYearLbl: Label 'Set up number series for the next year'; - SetupModuleForNextYearLbl: Label 'Set up number series for the [specify here] module for the next year'; + PrepareForNextYearLbl: Label 'Prepare number series for the next year'; + PrepareModuleForNextYearLbl: Label 'Prepare number series for the [specify here] module for the next year'; trigger OnAfterGetCurrRecord() begin diff --git a/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/Tools/RecordMatchImpl.Codeunit.al b/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/Search/Classic/NoSeriesTextMatchImpl.Codeunit.al similarity index 84% rename from src/Business Foundation/App/NoSeriesCopilot/src/Copilot/Tools/RecordMatchImpl.Codeunit.al rename to src/Business Foundation/App/NoSeriesCopilot/src/Copilot/Search/Classic/NoSeriesTextMatchImpl.Codeunit.al index ae9b640c59..6147c6df1c 100644 --- a/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/Tools/RecordMatchImpl.Codeunit.al +++ b/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/Search/Classic/NoSeriesTextMatchImpl.Codeunit.al @@ -5,11 +5,21 @@ namespace Microsoft.Foundation.NoSeries; -codeunit 337 "Record Match Impl." +codeunit 337 "No. Series Text Match Impl." { Access = Internal; - InherentPermissions = X; - InherentEntitlements = X; + + procedure IsRelevant(FirstString: Text; SecondString: Text): Boolean + var + Score: Decimal; + begin + FirstString := RemoveShortWords(FirstString); + SecondString := RemoveShortWords(SecondString); + + Score := CalculateStringNearness(FirstString, SecondString, GetMatchLengthTreshold(), 100) / 100; + + exit(Score >= RequiredNearness()); + end; /// /// Computes a nearness score between strings. Nearness is based on repeatedly finding longest common substrings. @@ -21,7 +31,7 @@ codeunit 337 "Record Match Impl." /// Substring matches below Threshold are not considered /// Max value returned by this procedure /// A number between 0 and NormalizingFactor, representing how much of the strings was matched - procedure CalculateStringNearness(FirstString: Text; SecondString: Text; Threshold: Integer; NormalizingFactor: Integer): Integer + local procedure CalculateStringNearness(FirstString: Text; SecondString: Text; Threshold: Integer; NormalizingFactor: Integer): Integer var Result: Text; TotalMatchedChars: Integer; @@ -98,7 +108,7 @@ codeunit 337 "Record Match Impl." exit(MinThreshold <= Length); end; - procedure RemoveShortWords(OriginalText: Text[250]): Text[250]; + local procedure RemoveShortWords(OriginalText: Text[250]): Text[250]; var Words: List of [Text]; Word: Text[250]; @@ -112,4 +122,14 @@ codeunit 337 "Record Match Impl." OriginalText := Result; // assign the result back to the text parameter exit(OriginalText); end; + + local procedure GetMatchLengthTreshold(): Decimal + begin + exit(2); + end; + + local procedure RequiredNearness(): Decimal + begin + exit(0.9) + end; } \ No newline at end of file diff --git a/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/Tools/NoSeriesCopAddIntent.Codeunit.al b/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/Tools/NoSeriesCopAddIntent.Codeunit.al index b2377be60d..164b896655 100644 --- a/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/Tools/NoSeriesCopAddIntent.Codeunit.al +++ b/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/Tools/NoSeriesCopAddIntent.Codeunit.al @@ -28,6 +28,7 @@ codeunit 331 "No. Series Cop. Add Intent" implements "AOAI Function" NumberOfAddedTablesPlaceholderLbl: Label '{number_of_tables}', Locked = true; TelemetryTool1PromptRetrievalErr: Label 'Unable to retrieve the prompt for No. Series Copilot Tool 1 from Azure Key Vault.', Locked = true; TelemetryTool1DefinitionRetrievalErr: Label 'Unable to retrieve the definition for No. Series Copilot Tool 1 from Azure Key Vault.', Locked = true; + ToolProgressDialogTextLbl: Label 'Searching for tables with number series related to your query'; ToolLoadingErr: Label 'Unable to load the No. Series Copilot Tool 1. Please try again later.'; ExistingNoSeriesMessageLbl: Label 'Number series already configured. If you wish to modify the existing series, please use the `Modify number series` prompt.'; @@ -62,7 +63,9 @@ codeunit 331 "No. Series Cop. Add Intent" implements "AOAI Function" NewNoSeriesPrompt, CustomPatternsPromptList, TablesYamlList, EmptyList : List of [Text]; NumberOfToolResponses, i, ActualTablesChunkSize : Integer; NumberOfAddedTables: Integer; + Progress: Dialog; begin + Progress.Open(ToolProgressDialogTextLbl); GetTablesRequireNoSeries(Arguments, TempSetupTable, TempNoSeriesField); ToolsImpl.GetUserSpecifiedOrExistingNumberPatternsGuidelines(Arguments, CustomPatternsPromptList, EmptyList, false); @@ -80,7 +83,8 @@ codeunit 331 "No. Series Cop. Add Intent" implements "AOAI Function" .Replace(NumberOfAddedTablesPlaceholderLbl, Format(ActualTablesChunkSize))); ToolResults.Add(ToolsImpl.ConvertListToText(NewNoSeriesPrompt), ActualTablesChunkSize); - end + end; + Progress.Close(); end; local procedure GetTablesRequireNoSeries(var Arguments: JsonObject; var TempSetupTable: Record "Table Metadata" temporary; var TempNoSeriesField: Record "Field" temporary) diff --git a/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/Tools/NoSeriesCopChangeIntent.Codeunit.al b/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/Tools/NoSeriesCopChangeIntent.Codeunit.al index b4dfc2698a..4c78718f90 100644 --- a/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/Tools/NoSeriesCopChangeIntent.Codeunit.al +++ b/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/Tools/NoSeriesCopChangeIntent.Codeunit.al @@ -32,6 +32,7 @@ codeunit 334 "No. Series Cop. Change Intent" implements "AOAI Function" NumberOfAddedTablesPlaceholderLbl: Label '{number_of_tables}', Locked = true; TelemetryTool2PromptRetrievalErr: Label 'Unable to retrieve the prompt for No. Series Copilot Tool 2 from Azure Key Vault.', Locked = true; TelemetryTool2DefinitionRetrievalErr: Label 'Unable to retrieve the definition for No. Series Copilot Tool 2 from Azure Key Vault.', Locked = true; + ToolProgressDialogTextLbl: Label 'Searching for tables with number series related to your query'; ToolLoadingErr: Label 'Unable to load the No. Series Copilot Tool 2. Please try again later.'; procedure GetName(): Text @@ -71,12 +72,14 @@ codeunit 334 "No. Series Cop. Change Intent" implements "AOAI Function" ChangeNoSeriesPrompt, CustomPatternsPromptList, TablesYamlList, ExistingNoSeriesToChangeList : List of [Text]; NumberOfToolResponses, i, ActualTablesChunkSize : Integer; NumberOfChangedTables: Integer; + Progress: Dialog; begin if not CheckIfUserSpecifiedNoSeriesToChange(Arguments) then begin NoSeriesCopilotImpl.SendNotification(GetLastErrorText()); exit; end; + Progress.Open(ToolProgressDialogTextLbl); GetTablesWithNoSeries(Arguments, TempSetupTable, TempNoSeriesField, ExistingNoSeriesToChangeList); ToolsImpl.GetUserSpecifiedOrExistingNumberPatternsGuidelines(Arguments, CustomPatternsPromptList, ExistingNoSeriesToChangeList, UpdateForNextYear); @@ -97,7 +100,8 @@ codeunit 334 "No. Series Cop. Change Intent" implements "AOAI Function" .Replace(NumberOfAddedTablesPlaceholderLbl, Format(ActualTablesChunkSize))); ToolResults.Add(ToolsImpl.ConvertListToText(ChangeNoSeriesPrompt), ActualTablesChunkSize); - end + end; + Progress.Close(); end; [TryFunction] diff --git a/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/Tools/NoSeriesCopNxtYrIntent.Codeunit.al b/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/Tools/NoSeriesCopNxtYrIntent.Codeunit.al index 21d5effda1..91c4ef7cf0 100644 --- a/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/Tools/NoSeriesCopNxtYrIntent.Codeunit.al +++ b/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/Tools/NoSeriesCopNxtYrIntent.Codeunit.al @@ -17,7 +17,7 @@ codeunit 349 "No. Series Cop. Nxt Yr. Intent" implements "AOAI Function" var Telemetry: Codeunit Telemetry; - FunctionNameLbl: Label 'GenerateNextYearNumberSeries', Locked = true; + FunctionNameLbl: Label 'PrepareNextYearNumberSeries', Locked = true; TelemetryTool3DefinitionRetrievalErr: Label 'Unable to retrieve the definition for No. Series Copilot Tool 3 from Azure Key Vault.', Locked = true; ToolLoadingErr: Label 'Unable to load the No. Series Copilot Tool 3. Please try again later.'; @@ -45,7 +45,7 @@ codeunit 349 "No. Series Cop. Nxt Yr. Intent" implements "AOAI Function" var AzureKeyVault: Codeunit "Azure Key Vault"; begin - if not AzureKeyVault.GetAzureKeyVaultSecret('NoSeriesCopilotTool3Definition', Definition) then begin + if not AzureKeyVault.GetAzureKeyVaultSecret('NoSeriesCopilotTool3DefinitionV2', Definition) then begin // TODO: As the prompt should be different for v25.0 and v25.1, we need to add a new secret for v25.1 and update the code to get the correct prompt based on the version Telemetry.LogMessage('0000ND9', TelemetryTool3DefinitionRetrievalErr, Verbosity::Error, DataClassification::SystemMetadata); Error(ToolLoadingErr); end; diff --git a/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/Tools/NoSeriesCopToolsImpl.Codeunit.al b/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/Tools/NoSeriesCopToolsImpl.Codeunit.al index 7fc97a5beb..c132c22a73 100644 --- a/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/Tools/NoSeriesCopToolsImpl.Codeunit.al +++ b/src/Business Foundation/App/NoSeriesCopilot/src/Copilot/Tools/NoSeriesCopToolsImpl.Codeunit.al @@ -225,32 +225,21 @@ codeunit 336 "No. Series Cop. Tools Impl." procedure IsRelevant(TableMetadata: Record "Table Metadata"; Field: Record "Field"; Entities: List of [Text]): Boolean var - RecordMatchMgtCopy: Codeunit "Record Match Impl."; + TextMatchImpl: Codeunit "No. Series Text Match Impl."; Entity: Text[250]; String1: Text[250]; String2: Text[250]; - Score: Decimal; begin foreach Entity in Entities do begin - String1 := RecordMatchMgtCopy.RemoveShortWords(RemoveTextPart(TableMetadata.Caption, ' Setup') + ' ' + RemoveTextParts(Field.FieldName, GetNoSeriesAbbreviations())); - String2 := RecordMatchMgtCopy.RemoveShortWords(Entity); - Score := RecordMatchMgtCopy.CalculateStringNearness(String1, String2, GetMatchLengthThreshold(), 100) / 100; - if Score >= RequiredNearness() then + String1 := RemoveTextPart(TableMetadata.Name, '& ') + ' ' + Field.FieldName; + String2 := Entity; + + if TextMatchImpl.IsRelevant(String1, String2) then exit(true); end; exit(false); end; - local procedure GetMatchLengthThreshold(): Decimal - begin - exit(2); - end; - - local procedure RequiredNearness(): Decimal - begin - exit(0.9) - end; - procedure GenerateChunkedTablesListInYamlFormat(var TablesYamlList: List of [Text]; var TempSetupTable: Record "Table Metadata" temporary; var TempNoSeriesField: Record "Field" temporary; var NumberOfAddedTables: Integer) begin Clear(TablesYamlList);