From 5b4308313a97a07348ad8fa2228e5207d2acdc73 Mon Sep 17 00:00:00 2001 From: Djordje Cenic Date: Mon, 10 Aug 2026 15:00:19 +0200 Subject: [PATCH 1/2] [Bank Acc. Reconciliation with Copilot] Read the functional part of the prompt from .resources --- .../.resources/BankAccRecAIMatchingTask.md | 38 +++++++++++++++++++ .../BankAccRecAITransToGLAccountTask.md | 9 +++++ src/Apps/W1/BankAccRecWithAI/app/app.json | 3 ++ .../app/src/BankAccRecTransToAcc.Codeunit.al | 9 +++-- .../app/src/BankRecAIMatchingImpl.Codeunit.al | 8 ++-- 5 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 src/Apps/W1/BankAccRecWithAI/app/.resources/BankAccRecAIMatchingTask.md create mode 100644 src/Apps/W1/BankAccRecWithAI/app/.resources/BankAccRecAITransToGLAccountTask.md diff --git a/src/Apps/W1/BankAccRecWithAI/app/.resources/BankAccRecAIMatchingTask.md b/src/Apps/W1/BankAccRecWithAI/app/.resources/BankAccRecAIMatchingTask.md new file mode 100644 index 00000000000..c2d0ae35581 --- /dev/null +++ b/src/Apps/W1/BankAccRecWithAI/app/.resources/BankAccRecAIMatchingTask.md @@ -0,0 +1,38 @@ +## Task +You are an AI assistant simulating the behavior of an experienced accountant. Your task is to match bank statement lines with ledger entries. + +You will receive: +* A list of bank statement lines (with Id, Date, Amount, Description, PaymentReference, DocumentNo). +* A list of ledger entries (with Id, Date, Amount, Description, PaymentReference, ExtDocNo, DocumentNo). +* Dates on statement lines and ledger entries are specified in the format YYYY-MM-DD + +You must return: +* A list of match representations +* Represent each match as (Bank Statement Line ID, [Ledger Entry ID1, Ledger Entry ID2...]). Enclose each match in parentheses. Multiple matches for a single statement line should be in square brackets, separated by commas within that match entry. +* Return **only appropriate matches**, by following the matching instructions. + +### Matching Instructions: +* First look for matches on PaymentReference, by using this rule: + ** If a bank statement line PaymentReference is equal to a ledger entry Description, match them. + ** If a bank statement line PaymentReference is a substring of a ledger entry Description, match them. +* Next, among the statement lines and ledger entries that are left unmatched, look for matches on ledger entry ExtDocNo, by using these rules: + ** If a ledger entry ExtDocNo is equal to a bank statement line DocumentNo, match them. + ** If a ledger entry ExtDocNo is equal to a bank statement line Description, match them. + ** If a ledger entry ExtDocNo is a substring of a bank statement line Description, match them. +* Next, among the statement lines and ledger entries that are left unmatched, look for matches on DocumentNo, by using these rules: + ** If DocumentNo fields are equal on a bank statement line and ledger entry, match them. + ** If a ledger entry DocumentNo is equal to bank statement line DocumentNo, match them. + ** If a ledger entry DocumentNo is a substring of bank statement line Description, match them. +* Next, among the statement lines and ledger entries that are left unmatched, use Amount, Description and Date to match them, by using these rules: + ** If both Date and Amount fields are equal on a bank statement line and ledger entry, match them. If there are multiple pairs of statement lines and ledger entries with equal Date and Amount, use the similarity of their Description to decide which ones to match. + ** If Amount fields are equal on a bank statement line and a ledger entry, and it is a unique match on Amount, match them, but only if Date is off by less than one month. + ** If Amount fields are equal on a bank statement line and a ledger entry, and there are multiple ledger entries with the same Amount, match the ledger entry with the Date closest to the Date of the statement line. + ** If Amount fields are equal on a bank statement line and a ledger entry, and there are multiple statement lines with the same Amount, match the statement line with the closest Date to the Date of the ledger entry. +* Next, among the statement lines and ledger entries that are left unmatched, look for one-to-many and many-to-one matches based on Amount, by using these rules: + ** A single statement line may match multiple ledger entries if the customer made a combined payment for multiple open ledger entries. The sum of the amounts in the matched ledger entries should bring the net balance to 0, considering the difference between the statement line amount and the combined ledger entry amounts. + ** Multiple statement lines may match a single ledger entry if the customer made multiple installments for a single ledger entry. The sum of the amounts in the matched statement lines should bring the net balance to 0, considering the difference between the combined statement line amounts and ledger entry amount. + +### General Instructions: +* Prioritize precision over completeness. +* Think long and before making a match. Don't just take a first acceptable match for each line, but provide the best set of matches. +* If a bank statement line can't be matched to any leger entry, do not return any output for that bank statement line. \ No newline at end of file diff --git a/src/Apps/W1/BankAccRecWithAI/app/.resources/BankAccRecAITransToGLAccountTask.md b/src/Apps/W1/BankAccRecWithAI/app/.resources/BankAccRecAITransToGLAccountTask.md new file mode 100644 index 00000000000..1b1178d1863 --- /dev/null +++ b/src/Apps/W1/BankAccRecWithAI/app/.resources/BankAccRecAITransToGLAccountTask.md @@ -0,0 +1,9 @@ +# Task: +Match the given **statement line descriptions** (which represent expense descriptions) to one of the given **G/L Accounts** (which represent expense categories). + +# Instructions: +- Find the G/L Account whose name matches the statement line description best. +- A match is acceptable if the expense described by statement line description matches the expense category described by the G/L Account name. +- Include only acceptable matches in the answer. +- Provide the answer **only for matched lines** by representing each match as a pair of numbers enclosed in brackets, like this: (Statement Line Id, G/L Account Id). +- If you cannot match a statement line, don't return anything for it. diff --git a/src/Apps/W1/BankAccRecWithAI/app/app.json b/src/Apps/W1/BankAccRecWithAI/app/app.json index 58e3b9dbe8a..64b628c469a 100644 --- a/src/Apps/W1/BankAccRecWithAI/app/app.json +++ b/src/Apps/W1/BankAccRecWithAI/app/app.json @@ -11,6 +11,9 @@ "url": "https://go.microsoft.com/fwlink/?LinkId=724011", "logo": "ExtensionLogo.png", "dependencies": [], + "resourceFolders": [ + ".resources" + ], "internalsVisibleTo": [ { "id": "2932b2a8-7399-4f8c-b1c0-1acfc2014ffb", diff --git a/src/Apps/W1/BankAccRecWithAI/app/src/BankAccRecTransToAcc.Codeunit.al b/src/Apps/W1/BankAccRecWithAI/app/src/BankAccRecTransToAcc.Codeunit.al index db0df01e20a..90a11139b65 100644 --- a/src/Apps/W1/BankAccRecWithAI/app/src/BankAccRecTransToAcc.Codeunit.al +++ b/src/Apps/W1/BankAccRecWithAI/app/src/BankAccRecTransToAcc.Codeunit.al @@ -194,11 +194,12 @@ codeunit 7251 "Bank Acc. Rec. Trans. to Acc." var BankRecAIMatchingImpl: Codeunit "Bank Rec. AI Matching Impl."; CompletionTaskTxt: SecretText; - CompletionTaskPartTxt: SecretText; + SafetyClauseTxt: SecretText; CompletionTaskBuildingFromKeyVaultFailed: Boolean; + ConcatSubstrTok: Label '%1%2', Locked = true; begin - if BankRecAIMatchingImpl.GetAzureKeyVaultSecret(CompletionTaskPartTxt, 'BankAccRecAITransToGLAccount1') then - CompletionTaskTxt := CompletionTaskPartTxt + if BankRecAIMatchingImpl.GetAzureKeyVaultSecret(SafetyClauseTxt, 'BankAccRecAITransToGLAccountSft') then + CompletionTaskTxt := SafetyClauseTxt else CompletionTaskBuildingFromKeyVaultFailed := true; @@ -207,6 +208,8 @@ codeunit 7251 "Bank Acc. Rec. Trans. to Acc." Error(ConstructingPromptFailedErr); end; + CompletionTaskTxt := SecretStrSubstNo(ConcatSubstrTok, CompletionTaskTxt, NavApp.GetResourceAsText('BankAccRecAITransToGLAccountTask.md', TextEncoding::UTF8)); + exit(CompletionTaskTxt); end; diff --git a/src/Apps/W1/BankAccRecWithAI/app/src/BankRecAIMatchingImpl.Codeunit.al b/src/Apps/W1/BankAccRecWithAI/app/src/BankRecAIMatchingImpl.Codeunit.al index 93839f378e7..64b4c4e81b0 100644 --- a/src/Apps/W1/BankAccRecWithAI/app/src/BankRecAIMatchingImpl.Codeunit.al +++ b/src/Apps/W1/BankAccRecWithAI/app/src/BankRecAIMatchingImpl.Codeunit.al @@ -16,11 +16,11 @@ codeunit 7250 "Bank Rec. AI Matching Impl." procedure BuildBankRecCompletionTask(IncludeFewShotExample: Boolean): SecretText var CompletionTaskTxt: SecretText; - CompletionTaskPartTxt: SecretText; + SafetyClauseTxt: SecretText; CompletionTaskBuildingFromKeyVaultFailed: Boolean; begin - if GetAzureKeyVaultSecret(CompletionTaskPartTxt, 'BankAccRecAIMatching1') then - CompletionTaskTxt := CompletionTaskPartTxt + if GetAzureKeyVaultSecret(SafetyClauseTxt, 'BankAccRecAIMatchingSft') then + CompletionTaskTxt := SafetyClauseTxt else CompletionTaskBuildingFromKeyVaultFailed := true; @@ -29,6 +29,8 @@ codeunit 7250 "Bank Rec. AI Matching Impl." Error(ConstructingPromptFailedErr); end; + CompletionTaskTxt := AddCompletionPromptLine(CompletionTaskTxt, NavApp.GetResourceAsText('BankAccRecAIMatchingTask.md', TextEncoding::UTF8)); + if (IncludeFewShotExample) then begin CompletionTaskTxt := AddCompletionPromptLine(CompletionTaskTxt, '\n**Example 1**:\n'); CompletionTaskTxt := AddCompletionPromptLine(CompletionTaskTxt, 'Statement Line: Id: 1, Description: A, Amount: 100, Date: 2023-07-01\n'); From 5f08f935df309fd807b18a4a14befd22e53b047b Mon Sep 17 00:00:00 2001 From: Djordje Cenic Date: Mon, 10 Aug 2026 22:49:21 +0200 Subject: [PATCH 2/2] [Bank Acc. Reconciliation with Copilot] Read the functional part of the prompt from .resources --- .../app/src/BankAccRecTransToAcc.Codeunit.al | 14 ++++++++----- .../app/src/BankRecAIMatchingImpl.Codeunit.al | 20 ++++++++++--------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/Apps/W1/BankAccRecWithAI/app/src/BankAccRecTransToAcc.Codeunit.al b/src/Apps/W1/BankAccRecWithAI/app/src/BankAccRecTransToAcc.Codeunit.al index 90a11139b65..bd14a5d674a 100644 --- a/src/Apps/W1/BankAccRecWithAI/app/src/BankAccRecTransToAcc.Codeunit.al +++ b/src/Apps/W1/BankAccRecWithAI/app/src/BankAccRecTransToAcc.Codeunit.al @@ -195,20 +195,24 @@ codeunit 7251 "Bank Acc. Rec. Trans. to Acc." BankRecAIMatchingImpl: Codeunit "Bank Rec. AI Matching Impl."; CompletionTaskTxt: SecretText; SafetyClauseTxt: SecretText; - CompletionTaskBuildingFromKeyVaultFailed: Boolean; - ConcatSubstrTok: Label '%1%2', Locked = true; + TaskPromptTxt: Text; + CompletionTaskBuildingFailed: Boolean; begin if BankRecAIMatchingImpl.GetAzureKeyVaultSecret(SafetyClauseTxt, 'BankAccRecAITransToGLAccountSft') then CompletionTaskTxt := SafetyClauseTxt else - CompletionTaskBuildingFromKeyVaultFailed := true; + CompletionTaskBuildingFailed := true; + + TaskPromptTxt := NavApp.GetResourceAsText('BankAccRecAITransToGLAccountTask.md', TextEncoding::UTF8); + if TaskPromptTxt = '' then + CompletionTaskBuildingFailed := true; - if CompletionTaskBuildingFromKeyVaultFailed then begin + if CompletionTaskBuildingFailed then begin Session.LogMessage('0000LFI', TelemetryConstructingPromptFailedErr, Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::All, 'Category', BankRecAIMatchingImpl.FeatureName()); Error(ConstructingPromptFailedErr); end; - CompletionTaskTxt := SecretStrSubstNo(ConcatSubstrTok, CompletionTaskTxt, NavApp.GetResourceAsText('BankAccRecAITransToGLAccountTask.md', TextEncoding::UTF8)); + CompletionTaskTxt := BankRecAIMatchingImpl.AddCompletionPromptLine(CompletionTaskTxt, TaskPromptTxt); exit(CompletionTaskTxt); end; diff --git a/src/Apps/W1/BankAccRecWithAI/app/src/BankRecAIMatchingImpl.Codeunit.al b/src/Apps/W1/BankAccRecWithAI/app/src/BankRecAIMatchingImpl.Codeunit.al index 64b4c4e81b0..1317303bed4 100644 --- a/src/Apps/W1/BankAccRecWithAI/app/src/BankRecAIMatchingImpl.Codeunit.al +++ b/src/Apps/W1/BankAccRecWithAI/app/src/BankRecAIMatchingImpl.Codeunit.al @@ -17,19 +17,24 @@ codeunit 7250 "Bank Rec. AI Matching Impl." var CompletionTaskTxt: SecretText; SafetyClauseTxt: SecretText; - CompletionTaskBuildingFromKeyVaultFailed: Boolean; + TaskPromptTxt: Text; + CompletionTaskBuildingFailed: Boolean; begin if GetAzureKeyVaultSecret(SafetyClauseTxt, 'BankAccRecAIMatchingSft') then CompletionTaskTxt := SafetyClauseTxt else - CompletionTaskBuildingFromKeyVaultFailed := true; + CompletionTaskBuildingFailed := true; - if CompletionTaskBuildingFromKeyVaultFailed then begin + TaskPromptTxt := NavApp.GetResourceAsText('BankAccRecAIMatchingTask.md', TextEncoding::UTF8); + if TaskPromptTxt = '' then + CompletionTaskBuildingFailed := true; + + if CompletionTaskBuildingFailed then begin Session.LogMessage('0000LFJ', TelemetryConstructingPromptFailedErr, Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::All, 'Category', FeatureName()); Error(ConstructingPromptFailedErr); end; - CompletionTaskTxt := AddCompletionPromptLine(CompletionTaskTxt, NavApp.GetResourceAsText('BankAccRecAIMatchingTask.md', TextEncoding::UTF8)); + CompletionTaskTxt := AddCompletionPromptLine(CompletionTaskTxt, TaskPromptTxt); if (IncludeFewShotExample) then begin CompletionTaskTxt := AddCompletionPromptLine(CompletionTaskTxt, '\n**Example 1**:\n'); @@ -66,9 +71,7 @@ codeunit 7250 "Bank Rec. AI Matching Impl." exit(CompletionTaskTxt); end; - local procedure AddCompletionPromptLine(Completion: SecretText; NewPromptLine: Text): SecretText - var - ConcatSubstrTok: Label '%1%2', Locked = true; + internal procedure AddCompletionPromptLine(Completion: SecretText; NewPromptLine: Text): SecretText begin exit(SecretStrSubstNo(ConcatSubstrTok, Completion, NewPromptLine)); end; @@ -76,7 +79,6 @@ codeunit 7250 "Bank Rec. AI Matching Impl." procedure BuildBankRecCompletionPrompt(TaskPrompt: SecretText; StatementLines: Text; LedgerLines: Text): SecretText var CompletionPrompt: SecretText; - ConcatSubstrTok: Label '%1%2', Locked = true; begin LedgerLines += '"""\n**Matches**:'; // close the ledger lines section StatementLines += '"""\n'; // close the statement lines section @@ -89,7 +91,6 @@ codeunit 7250 "Bank Rec. AI Matching Impl." var UserMessageTxt: SecretText; EmptyText: SecretText; - ConcatSubstrTok: Label '%1%2', Locked = true; begin LedgerLines += '"""\n**Matches**:'; // close the ledger lines section StatementLines += '"""\n'; // close the statement lines section @@ -729,6 +730,7 @@ codeunit 7250 "Bank Rec. AI Matching Impl." var AOAIToken: Codeunit "AOAI Token"; + ConcatSubstrTok: Label '%1%2', Locked = true; MatchedByCopilotTxt: label 'Matched by Copilot based on semantic similarity.', Comment = 'Copilot is a Microsoft service name and must not be translated'; SuccessfullyFilteredBLEListTxt: label 'Successfully filtered bank account ledger entries based on posting date.', Locked = true; UnableToFilterBLEListUnderTokenLimitTxt: label 'Unable to filter the bank account ledger entry list.', Locked = true;