Business Central (Part 29)

Neil HaddleyMay 24, 2024

PromptDialog (Part 3)

Business CentralAIprompt-dialogal-languagecopilotbusiness-central

I extended the PromptDialog to render Markdown-formatted LLM responses as HTML. Because LLMs typically return Markdown, displaying the raw text was hard to read. I added a controladdin that uses the ShowdownJS library to convert Markdown to HTML and inject it into the page.

I provided a prompt that would return a Markdown formatted response

I provided a prompt that would return a Markdown formatted response

The Markdown formatted result was hard to read

The Markdown formatted result was hard to read

I added a controladdin to the PromptDialog

I added a controladdin to the PromptDialog

The CreateHTMLFromMarkdown JavaScript function is the key move

The CreateHTMLFromMarkdown JavaScript function is the key move

I opened the file in the Sources panel

I opened the file in the Sources panel

Debugging the JavaScript

Debugging the JavaScript

MarkdownBoxFunctions.js

JAVASCRIPT
1function CreateHTMLFromMarkdown(markdown) {
2
3    const container = document.getElementById("controlAddIn");
4
5    if (!container) return;
6
7    container.innerHTML = "";
8
9    // https://showdownjs.com/docs/markdown-syntax/#tables
10
11    const converter = new showdown.Converter({ tables: true });
12
13    const htmlContent = converter.makeHtml(markdown);
14
15    const contentDiv = document.createElement("div");
16
17    contentDiv.id = "response";
18
19    contentDiv.style.cssText = "overflow: auto; padding: 10px;";
20
21    contentDiv.innerHTML = htmlContent;
22
23    container.appendChild(contentDiv);
24
25    // Adjust content height dynamically after rendering
26
27    setTimeout(() => {
28
29        contentDiv.style.height = `calc(100vh)`;
30
31    }, 0);
32
33    // Notify Business Central that control is ready
34
35    Microsoft.Dynamics.NAV.InvokeExtensibilityMethod("ControlReady");
36
37}

Pag50100.LLM.al

AL
1namespace LLM.LLM;
2
3using System.AI;
4
5controladdin MarkdownBox
6{
7    RequestedHeight = 500;
8    MaximumHeight = 700;
9    VerticalStretch = false;
10    VerticalShrink = false;
11    HorizontalStretch = true;
12    HorizontalShrink = true;
13    Scripts = 'showdown.min.js', 'MarkdownBoxFunctions.js';
14    // ShowdownJS v 2.0 is released under the MIT license. Previous versions are released under BSD. https://github.com/showdownjs/showdown
15
16    event ControlReady()
17    procedure CreateHTMLFromMarkdown(markdown: Text);
18}
19
20enumextension 50100 LLM extends "Copilot Capability"
21{
22    value(50100; "LLM") { }
23}
24
25page 50100 LLM
26{
27    ApplicationArea = All;
28    Caption = 'LLM';
29    PageType = PromptDialog;
30    Extensible = false;
31    DataCaptionExpression = Prompt;
32    IsPreview = true;
33
34
35    layout
36    {
37        #region input section
38        area(Prompt)
39        {
40            field(PromptField; Prompt)
41            {
42                ShowCaption = false;
43                MultiLine = true;
44                InstructionalText = 'Message Large Language Model';
45            }
46        }
47        #endregion
48
49        #region output section
50        area(Content)
51        {
52            usercontrol(Formatted; MarkdownBox)
53            {
54                ApplicationArea = All;
55            }
56            
57            /*field(OutputFiled; Output)
58            {
59                ShowCaption = false;
60                MultiLine = true;
61            }*/
62        }
63        #endregion
64    }
65    actions
66    {
67        #region prompt guide
68        area(PromptGuide)
69        {
70            action("Capital City")
71            {
72                ApplicationArea = All;
73                Caption = 'Capital City';
74                ToolTip = 'What is the Capital of ...';
75
76                trigger OnAction()
77                begin
78                    Prompt := 'What is the Capital of <Country>?';
79                end;
80            }
81        }
82        #endregion
83
84        #region system actions
85        area(SystemActions)
86        {
87            systemaction(Generate)
88            {
89                Caption = 'Generate';
90                ToolTip = 'Generate a response.';
91
92                trigger OnAction()
93                begin
94                    RunGeneration();
95                    CurrPage.Formatted.CreateHTMLFromMarkdown(Output);
96                end;
97            }
98            systemaction(OK)
99            {
100                Caption = 'OK';
101                ToolTip = 'Save the result.';
102            }
103            systemaction(Cancel)
104            {
105                Caption = 'Cancel';
106                ToolTip = 'Discard the result.';
107            }
108            systemaction(Regenerate)
109            {
110                Caption = 'Regenerate';
111                ToolTip = 'Regenerate a response.';
112
113                trigger OnAction()
114                begin
115                    RunGeneration();
116                    CurrPage.Formatted.CreateHTMLFromMarkdown(Output);
117                end;
118            }
119            #endregion
120        }
121    }
122    var
123        Prompt: Text;
124        Output: Text;
125
126
127    local procedure RunGeneration()
128    var
129        CopilotCapability: Codeunit "Copilot Capability";
130        AzureOpenAI: Codeunit "Azure OpenAI";
131        AOAIDeployments: Codeunit "AOAI Deployments";
132        AOAIChatCompletionParams: Codeunit "AOAI Chat Completion Params";
133        AOAIChatMessages: Codeunit "AOAI Chat Messages";
134        AOAIOperationResponse: Codeunit "AOAI Operation Response";
135        UserMessage: TextBuilder;
136    begin
137
138        // CopilotCapability.UnregisterCapability(Enum::"Copilot Capability"::LLM);
139
140        if not CopilotCapability.IsCapabilityRegistered(Enum::"Copilot Capability"::LLM) then
141            CopilotCapability.RegisterCapability(Enum::"Copilot Capability"::LLM, 'https://about:none');
142
143        AzureOpenAI.SetAuthorization(Enum::"AOAI Model Type"::"Chat Completions", GetEndpoint(), GetDeployment(), GetApiKey());
144        AzureOpenAI.SetCopilotCapability(Enum::"Copilot Capability"::LLM);
145
146
147        AOAIChatCompletionParams.SetMaxTokens(2500);
148        AOAIChatCompletionParams.SetTemperature(0);
149        AOAIChatMessages.AddUserMessage(Prompt);
150
151        AzureOpenAI.GenerateChatCompletion(AOAIChatMessages, AOAIChatCompletionParams, AOAIOperationResponse);
152        if (AOAIOperationResponse.IsSuccess()) then
153            Output := AOAIChatMessages.GetLastMessage()
154            
155        else
156            Output := ('Error: ' + AOAIOperationResponse.GetError());
157
158    end;
159
160
161    local procedure GetEndpoint(): Text
162    var
163    begin
164        exit('https://haddley-ai.openai.azure.com');
165    end;
166
167    local procedure GetDeployment(): Text
168    var
169    begin
170        exit('gpt-4o');
171    end;
172
173    [NonDebuggable]
174    local procedure GetApiKey(): Text //SecretText
175    var
176    begin
177        exit('1234567890123456789012');
178    end;
179}