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);