Copilot Toolkit: The New Prompt Dialog Page Explained

What is Copilot Toolkit

At Directions EMEA 2023, Microsoft announced the Copilot Toolkit for Business Central.

This toolkit is a combination of UI components and an AL framework designed to help developers easily integrate AI functionalities into their Business Central extensions.

Composition of the Copilot Toolkit:

  • Prompt Dialog Page (UI)
  • AI module
  • AI administration

To get hands-on with the toolkit quickly, follow the links to:

Find already implemented copilots in Business Central 2023 Wave 2’s features such as:

For developers aiming to embed AI into their extensions, the workflow would involve:

  1. Extending Copilot Capability with Custom Copilot
  2. Registering Custom Copilot in the Copilot & AI Capabilities 
  3. Developing Prompt Dialog Page
  4. Developing AI logic, using prompt engineering and AI module.
  5. Calling the Prompt Dialog Page from the Business Central Page

This blog will focus on the UI aspect – the Prompt Dialog Page. I will address other components, including the AI module in the next blog.

What is Prompt Dialog Page

A picture is worth a thousand words.

What you see in the video are three stages of the generation process:

  1. Input Screen
  2. Processing Screen
  3. Result Screen

In Business Central 23.1 (yes, a minor update is required), this is accomplished through a PromptDialog type page. Here’s an example of what the code might look like:

				
					page 50103 "GPT Copilot Proposal"
{
    Caption = 'Generate with Copilot';
    PageType = PromptDialog;
    IsPreview = true;
    Extensible = false;
    ApplicationArea = All;

    layout
    {
        area(Prompt)
        {
            field(InputText; InputText)
            {
                ShowCaption = false;
                MultiLine = true;
                ApplicationArea = All;
            }

        }
        area(Content)
        {
            field(GeneratedText; GeneratedText)
            {
                ShowCaption = false;
                MultiLine = true;
                ApplicationArea = All;
            }
        }
    }
    actions
    {
        area(SystemActions)
        {
            systemaction(Generate)
            {
                trigger OnAction()
                begin
                    GeneratedText := 'You asked me about: ' + InputText;
                end;
            }
            systemaction(Cancel)
            {
                ToolTip = 'Discards all suggestions and dismisses the dialog';
            }
            systemaction(Ok)
            {
                Caption = 'Keep it';
                ToolTip = 'Accepts the current suggestion and dismisses the dialog';
            }
        }
    }

    var
        InputText: Text;
        GeneratedText: Text;
}
				
			

The code defines three screens corresponding to specific area types within the layout:

  • Input Screen corresponds to the Prompt area, where users enter their information.
  • Processing Screen doesn’t correspond to any specific area, as it’s a transitional phase.
  • Result Screen matches the Content area, where the outcomes are displayed.

Essentially, the layout determines what is displayed on each screen.

How platform understand what screen to show up?

The PromptDialog has three modes:

  1. Prompt Mode: For user input.
  2. Generate Mode: For processing the input.
  3. Content Mode: For displaying results.

Connecting these modes to our earlier logic:

  • The Input Screen uses the Prompt Mode, which is associated with the Prompt area.
  • The Processing Screen operates in Generate Mode, which does not have a specific area.
  • The Result Screen is in Content Mode, linked to the Content area.

Additionally, if you want the PromptDialog page to appear only in one specific mode, you can set this preference using the PromptMode property.

				
					PromptMode = Content;
				
			

This approach is ideal when input is unnecessary, and only the result needs to be shown.

Using prompt modes platform understands what controls to show on the screen

How platform understand what mode to activate?

By default, the mode is set to Prompt. To switch from the Prompt mode to the Generate mode you need to add a systemaction of type Generate

				
					    actions
    {
        area(SystemActions)
        {
            systemaction(Generate)
            {
                trigger OnAction()
                begin
                    // Your generative code goes here
                end;
            }
        }
    }

				
			

The platform transitions from Generate to Content mode upon completion of the code in the OnAction trigger.

Why do you need Content mode?

The essence of the Copilot toolkit is to keep the user in command.

Whenever AI generates content, there’s a chance it might not be completely accurate, so it’s crucial that the user reviews and either approves or rejects the outcome. The content should only be saved to the database after the user has given their consent.

This is the purpose of Content mode. It exists to give the user the option to either confirm the information or exit the page without saving any changes. For this process, you should incorporate two system actions: Ok to confirm and Cancel to exit without saving.

This is also the reason why you cannot have a physical SourceTable in a PromptDialog page (you either use a temporary table, or no table)

				
					
            systemaction(Cancel)
            {
                ToolTip = 'Discards all suggestions and dismisses the dialog';
            }
            systemaction(Ok)
            {
                Caption = 'Keep it';
                ToolTip = 'Accepts the current suggestion and dismisses the dialog';
            }
        
				
			

Enhancing the User Experience

For a more user-friendly experience, you can add an additional Regenerate system action in Content mode:

				
					            systemaction(Regenerate)
            {
                trigger OnAction()
                begin
                    GeneratedText := 'Your input was: ' + InputText + ' and I regenerated it';
                end;
            }

				
			

Saving Results

Attempting to “save to database” within the OnAction trigger of the Ok system action will result in an error

Instead, you should place this code in the OnQueryClosePage trigger.

				
					    trigger OnQueryClosePage(CloseAction: Action): Boolean
    begin
        if CloseAction = CloseAction::Ok then begin
            // Here you can do something with the generated text and save result to a database
            Message('You accepted the generated text: ' + GeneratedText);
        end;
    end;

				
			

Using Captions on SystemActions

Here’s important update from Microsoft regarding the use of captions for SystemActions:

It is now recommended to always provide a caption for system actions. We now display both the icon and caption text for these system actions in the footer of the prompt dialog. Even if the caption may not be visible in the dialog, it's crucial to include one for accessibility purposes, ensuring that tools like screen readers can provide assistance for all actions.

Calling the PromptDialog Page

Nothing special here, you call it as a normal page in a RunModal mode. The only difference is that action image should be  Sparkle

				
					pageextension 50100 "GPT Customer card Ext" extends "Customer Card"
{
    actions
    {
        addfirst("&Customer")
        {
            action("GPT Happy Customer")
            {
                Caption = 'Make Customer Happy!';
                ToolTip = 'Make Customer Happy with Copilot';
                ApplicationArea = All;
                Image = Sparkle;
                trigger OnAction()
                begin
                    Page.RunModal(Page::"GPT Copilot Proposal");
                end;
            }
        }
    }
}
				
			

Or, if you need to pass some input variables to the PromptDialog page

				
					page 50103 "GPT Copilot Proposal"
{
    ...
    procedure SetCustomer(Customer: Record Customer)
    begin
        InputText := 'Make ' + Customer.Name + ' happy';
    end;
}

pageextension 50100 "GPT Customer card Ext" extends "Customer Card"
{
    actions
    {
        addfirst("&Customer")
        {
            action("GPT Happy Customer")
            {
                Caption = 'Make Customer Happy!';
                ToolTip = 'Make Customer Happy with Copilot';
                ApplicationArea = All;
                Image = Sparkle;
                trigger OnAction()
                var
                    CopilotProposal: Page "GPT Copilot Proposal";
                begin
                    CopilotProposal.SetCustomer(Rec);
                    CopilotProposal.RunModal;
                end;
            }
        }

        addfirst(Promoted)
        {
            actionref("GPTHappyCustomer_Promoted"; "GPT Happy Customer") { }
        }
    }
}
				
			

Understanding When to Use Sparkle and SparkleFilled Icons

Here’s important update from Microsoft regarding the use of Sparkle icon:

Regarding the new sparkle image icon, there are two versions available: Sparkle and SparkleFilled. To clarify when to use each: 1. By default, use the Sparkle image for all Copilot actions. 2. If there are multiple Copilot actions in an area, and one of them is considered more "important" or needs to be emphasized, then use the SparkleFilled icon. Typically, there will be zero or one "filled" action, while the rest will use the normal Sparkle icon.

Beyond the Basics

The methods outlined represent the foundational elements to keep in mind. However, there are extra controls at your disposal that can simplify and enhance the management of the generative process.

Structured or Non-structured controls

Prompt and Content modes can have not only free text UI. Depending on your task, you can add fields or even pages. 

You can’t use repeater in the PromptDialog page. 

To add a list page, use part

				
					page 50103 "GPT Copilot Proposal"
{
    ...
    SourceTable = Customer;
    SourceTableTemporary = true;

    layout
    {
        area(Prompt)
        {
            field(CustomerName; Rec.Name)
            {
                Caption = 'Name';
                ApplicationArea = All;
            }
            field(CustomerBalance; Rec.Balance)
            {
                Caption = 'Balance';
                ApplicationArea = All;
            }
            part(SupPartInvoices; "GPT Posted Sales Invoices Part")
            {
                SubPageLink = "Sell-to Customer No." = field("No.");
            }
            ...
        }
       ...
    }
    ...
    }

    ...

    procedure SetCustomer(Customer: Record Customer)
    begin
        ...
        Rec := Customer;
        Rec.Insert();
    end;
}

page 50104 "GPT Posted Sales Invoices Part"
{
    PageType = ListPart;
    SourceTable = "Sales Invoice Header";
    Editable = false;
    Caption = 'Invoices';
    layout
    {
        area(Content)
        {
            repeater(Invoices)
            {
                field("No."; Rec."No.")
                {
                    ApplicationArea = All;
                }
                field("Posting Date"; Rec."Posting Date")
                {
                    ApplicationArea = All;
                }
                field(Amount; Rec.Amount)
                {
                    ApplicationArea = All;
                }
            }
        }
    }
}
				
			

Upload attachments

There is a systemaction of type Attach that is considered to show up in the Prompt mode. The idea is to upload any document, that will be required during the AI Generation process. 

However, if you don’t require such feature, I found this to be useful place to call the Custom Copilot Setup page. 

				
					            systemaction(Attach)
            {
                Caption = 'Setup';
                trigger OnAction()
                begin
                    Page.Run(Page::"GPT Copilot Setup");
                end;
            }
				
			

Custom progress indicator message

You can substitute the default ‘Generating with Copilot’ 

				
					            systemaction(Generate)
            {
                trigger OnAction()
                var
                    ProgressDialog: Dialog;
                begin
                    ProgressDialog.Open('Making Customer Happy...');
                    // Here you can call your Copilot API
                    ProgressDialog.Close();
                end;
            }

				
			

Custom page caption

You can update the page caption. This is useful, to show the user initial prompt after result was generated

				
					page 50103 "GPT Copilot Proposal"
{
    ...
    DataCaptionExpression = InputText;
    ...
}
				
			

Adding Prompt Parameters

You can also incorporate further customizable options to guide the AI generation process, such as tone, style, context, length, and specificity, allowing for more tailored and precise outputs.

Use area(PromptOptions) for this. Here you can add only enum or option fields. 

				
					page 50103 "GPT Copilot Proposal"
{
    ...
    layout
    {
        area(PromptOptions)
        {
            field(HappinessLevel; HappinessLevel)
            {
                ApplicationArea = All;
                Caption = 'How happy should the customer be?';
            }
        }
        ...
    }
    var
    ...
    HappinessLevel: Enum "GPT Customer Happiness Level";
    ...
}

enum 50100 "GPT Customer Happiness Level"
{
    value(0; "Very Happy") { }
    value(1; "Happy") { }
    value(2; "Neutral") { }
    value(3; "Unhappy") { }
    value(4; "Very Unhappy") { }
}

				
			

Prompt options will also appear on the Content mode, so you can regenerate the result with different options. 

Adding Generation History

This is interesting feature. To manage and review AI-generated content later, it’s add a SourceTable to the PromptDialog page. This table should be set as a temporary. It will capture and store the outcomes after each generation cycle, enabling you to save, track, and access previous AI outputs as needed.

Also, temporary table can save prompt parameters, so on each iteration you can see what parameters where used. 

				
					table 50102 "GPT Happy Customer"
{
    TableType = Temporary;

    fields
    {
        field(1; "Entry No."; Integer)
        {
        }
        field(2; "Customer No."; Code[20])
        {

        }
        field(3; "Happiness Level"; enum "GPT Customer Happiness Level")
        {

        }
        field(4; "Generated Text"; text[250])
        {

        }
    }

    keys
    {
        key(PK; "Entry No.")
        {
            Clustered = true;
        }
    }
}
				
			

Now, add this table as a source and adjust logic a bit

				
					page 50103 "GPT Copilot Proposal"
{
    ...
    SourceTable = "GPT Happy Customer";
    SourceTableTemporary = true;

    layout
    {
    ...
        area(Content)
        {
            field(GeneratedText; Rec."Generated Text")
           ...
        }
    }
    actions
    {
        area(SystemActions)
        {
            systemaction(Generate)
            {
                trigger OnAction()
                begin
                    Rec."Generated Text" := GenerateText();
                    SaveToHistory();
                end;
            }
            ...
            systemaction(Regenerate)
            {
                trigger OnAction()
                begin
                    Rec."Generated Text" := GenerateText();
                    SaveToHistory();
                end;
            }
        }
    }

    ...

    trigger OnAfterGetCurrRecord()
    begin
        HappinessLevel := Rec."Happiness Level";
    end;

    ...
    local procedure GenerateText(): Text
    begin
        exit('You asked me to ' + InputText + ' and I did it with the level of happiness ' + Format(HappinessLevel));
    end;

    local procedure SaveToHistory()
    begin
        Rec."Entry No." := Rec.Count + 1;
        Rec."Happiness Level" := HappinessLevel;
        Rec.Insert();
    end;
}
				
			

One more thing

As of the time this post was written, there is an bug.

Occasionally, when you define field variables in the areas (such as PromptOptions, Prompt), the platform may not retain the values during the transition from one mode to another.

Microsoft has been notified of this issue and is actively working on a resolution.

In the meantime, to address this problem, you can implement an empty OnValidate trigger.

So, instead of

				
					        area(Prompt)
        {
            field(InputText; InputText)
            {
                ShowCaption = false;
                MultiLine = true;
                ApplicationArea = All;
            }
        }

				
			

do this

				
					        area(Prompt)
        {
            field(InputText; InputText)
            {
                ShowCaption = false;
                MultiLine = true;
                ApplicationArea = All;
                trigger OnValidate()
                begin

                end;
            }
        }
				
			

What's next?

Now you know how to craft great UI for the generative AI experience. The next step is to make AI experience itself.

In the next blog I will dive into the new AI module. Stay tuned. 

Share Post:

Leave a Reply

About Me

DMITRY KATSON

A Microsoft MVP, Business Central architect and a project manager, blogger and a speaker, husband and a twice a father. With more than 15 years in business, I went from developer to company owner. Having a great team, still love to put my hands on code and create AI powered Business Central Apps that just works.

Follow Me

Recent Posts

Tags