UNPKG

al-development-collection

Version:

AI Native AL Development toolkit for Microsoft Dynamics 365 Business Central with GitHub Copilot integration

668 lines (541 loc) 20.2 kB
--- agent: agent model: Claude Sonnet 4.5 description: 'Create a complete PromptDialog page for Copilot features in Business Central. Includes all areas (PromptOptions, Prompt, Content, PromptGuide), system actions, and Azure OpenAI integration.' tools: ['microsoft-docs/*', 'edit', 'search', 'new', 'Azure MCP/search', 'usages', 'vscodeAPI', 'problems', 'changes', 'ms-dynamics-smb.al/al_build', 'ms-dynamics-smb.al/al_incremental_publish'] --- # Copilot PromptDialog Page Creation Workflow Complete workflow to create a PromptDialog page with all areas, system actions, and Azure OpenAI integration for Copilot features in Business Central. ## Objective Create a fully functional PromptDialog page that allows users to interact with AI-powered Copilot features, with proper UX, error handling, and responsible AI practices. ## Context Loading Phase Before starting, review: - [Existing PromptDialog pages in codebase] - [Microsoft Docs: Build Copilot Experience](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/ai-build-experience) - [Real Example: Item Substitution](https://github.com/microsoft/BCTech/blob/master/samples/AzureOpenAI/2-ItemSubstitution/PromptDialog/ItemSubstAIProposal.Page.al) - [Real Example: SuggestJob - Proposal](https://github.com/microsoft/BCTech/blob/master/samples/AzureOpenAI/4-SuggestJob%20with%20Tools/src/Pages/SuggestJobProposal.Page.al) - [Registered Copilot capabilities] - [app.json for object ID ranges] ## Prerequisites - Copilot capability already registered (use `al-copilot-capability` if not) - Available object ID range - Understanding of desired user experience - Optional: Temporary table for result storage (for history control) ## Step-by-Step Process ### Phase 1: Gather Information Ask the user for: 1. **Page Purpose**: What will this Copilot dialog do? - Example: "Generate sales insights", "Suggest item substitutions", "Analyze customer patterns" 2. **Interaction Model**: - User provides text input → AI generates response - User selects options → AI generates structured suggestions - Auto-generate on open → AI provides imMEDIUMte insights 3. **PromptMode**: How should the dialog behave? - `Prompt` (default): Ask user for input first - `Generate`: Auto-run generation when opened - `Content`: Show content only (no generation) 4. **Input Requirements**: - Free text input? - Dropdown options (PromptOptions)? - Pre-filled context from calling page? 5. **Output Format**: - Simple text response? - Structured list/table (needs subpage)? - JSON data for processing? 6. **Object IDs**: Available IDs for page and subpage (if needed)? 7. **PromptGuide Examples**: What examples should help users? - 2-3 example prompts to guide users ### Phase 2: Design Data Structure #### If Structured Output Needed: Create temporary table for results: **File**: `<FeatureName>Proposal.Table.al` **Template**: ```al namespace <Namespace>; table <ObjectID> "<Feature Name> Proposal" { TableType = Temporary; Caption = '<Feature Name> Proposal'; fields { field(1; "Entry No."; Integer) { Caption = 'Entry No.'; AutoIncrement = true; } field(10; "No."; Code[20]) { Caption = 'No.'; } field(20; Description; Text[100]) { Caption = 'Description'; } field(30; Explanation; Text[250]) { Caption = 'Explanation'; } field(40; "Confidence Score"; Decimal) { Caption = 'Confidence Score'; MinValue = 0; MaxValue = 1; } field(50; "Full Explanation"; Blob) { Caption = 'Full Explanation'; } } keys { key(PK; "Entry No.") { Clustered = true; } } } ``` #### If Subpage Needed: Create subpage for displaying results: **File**: `<FeatureName>ProposalSub.Page.al` **Template**: ```al namespace <Namespace>; page <ObjectID> "<Feature Name> Proposal Sub" { PageType = ListPart; SourceTable = "<Feature Name> Proposal"; SourceTableTemporary = true; Caption = 'Suggestions'; layout { area(Content) { repeater(Group) { field("No."; Rec."No.") { ApplicationArea = All; ToolTip = 'Specifies the number.'; } field(Description; Rec.Description) { ApplicationArea = All; ToolTip = 'Specifies the description.'; } field(Explanation; Rec.Explanation) { ApplicationArea = All; ToolTip = 'Specifies why this was suggested.'; MultiLine = true; } field("Confidence Score"; Rec."Confidence Score") { ApplicationArea = All; ToolTip = 'AI confidence level for this suggestion.'; } } } } procedure Load(var TmpSource: Record "<Feature Name> Proposal" temporary) begin Rec.Copy(TmpSource, true); CurrPage.Update(false); end; } ``` ### Phase 3: Create Main PromptDialog Page **File**: `<FeatureName>Copilot.Page.al` **Template Structure**: ```al namespace <Namespace>; using System.AI; page <ObjectID> "<Feature Name> Copilot" { PageType = PromptDialog; Extensible = false; IsPreview = true; Caption = '<User-Friendly Caption>'; // Choose PromptMode based on user requirements PromptMode = Prompt; // or Generate, or Content // Optional: For history control // SourceTable = "<Feature Name> Proposal"; // SourceTableTemporary = true; layout { // AREA 1: PromptOptions (Optional settings) // Only use for Option or Enum fields area(PromptOptions) { field(OptionField; OptionVariable) { ApplicationArea = All; Caption = '<Option Caption>'; ToolTip = '<What this option controls>'; ShowCaption = true; } } // AREA 2: Prompt (User Input) area(Prompt) { field(UserInput; UserPromptText) { ShowCaption = false; MultiLine = true; ApplicationArea = All; InstructionalText = '<Helpful instruction for user>'; trigger OnValidate() begin CurrPage.Update(); end; } } // AREA 3: Content (AI Output) area(Content) { // Option A: Simple text response field(AIResponse; AIResponseText) { ApplicationArea = All; Caption = 'AI Suggestion'; MultiLine = true; Editable = false; } // Option B: Structured response with subpage // part(Proposals; "<Feature Name> Proposal Sub") // { // ApplicationArea = All; // } } } actions { // AREA 4: PromptGuide (Help users with examples) area(PromptGuide) { action(Example1) { ApplicationArea = All; Caption = '<Example 1 Caption>'; ToolTip = '<What this example does>'; trigger OnAction() begin UserPromptText := '<Example 1 text>'; CurrPage.Update(false); end; } action(Example2) { ApplicationArea = All; Caption = '<Example 2 Caption>'; ToolTip = '<What this example does>'; trigger OnAction() begin UserPromptText := '<Example 2 text>'; CurrPage.Update(false); end; } } // AREA 5: SystemActions (Main Copilot actions) area(SystemActions) { systemaction(Generate) { Caption = 'Generate'; ToolTip = 'Generate AI suggestions'; trigger OnAction() begin RunGeneration(); end; } systemaction(Regenerate) { Caption = 'Regenerate'; ToolTip = 'Generate different suggestions'; trigger OnAction() begin RunGeneration(); end; } systemaction(OK) { Caption = 'Keep it'; ToolTip = 'Accept and apply suggestions'; } systemaction(Cancel) { Caption = 'Discard'; ToolTip = 'Discard suggestions without applying'; } } } // Page triggers trigger OnQueryClosePage(CloseAction: Action): Boolean begin if CloseAction = CloseAction::OK then begin // User confirmed - apply suggestions ApplySuggestions(); end; end; // Main generation logic local procedure RunGeneration() var GenerationCodeunit: Codeunit "<Feature Name> Generation"; TmpResult: Record "<Feature Name> Proposal" temporary; Attempts: Integer; begin // Clear previous results ClearResults(); // Configure generation GenerationCodeunit.SetUserPrompt(UserPromptText); // Apply any options // if OptionVariable = OptionVariable::"Special Filter" then // GenerationCodeunit.SetSpecialFilter(); // Retry logic for transient failures TmpResult.DeleteAll(); Attempts := 0; while TmpResult.IsEmpty and (Attempts < 5) do begin if GenerationCodeunit.Run() then GenerationCodeunit.GetResult(TmpResult); Attempts += 1; end; if Attempts < 5 then LoadResults(TmpResult) else Error('Failed to generate suggestions after %1 attempts. Please try again. %2', Attempts, GetLastErrorText()); end; local procedure LoadResults(var TmpResult: Record "<Feature Name> Proposal" temporary) begin // Option A: Simple text // AIResponseText := Format(TmpResult); // Option B: Subpage // CurrPage.Proposals.Page.Load(TmpResult); CurrPage.Update(false); end; local procedure ClearResults() begin // Clear previous results AIResponseText := ''; end; local procedure ApplySuggestions() begin // Implement logic to apply user-approved suggestions Message('Applying suggestions...'); end; // Public procedures for calling page procedure SetContext(ContextRecord: Variant) begin // Set context from calling page // Example: SourceItem := ContextRecord; end; // Variables var UserPromptText: Text; AIResponseText: Text; OptionVariable: Option "All","Filtered"; } ``` ### Phase 4: Create Generation Codeunit **File**: `<FeatureName>Generation.Codeunit.al` **Template**: ```al namespace <Namespace>; using System.AI; codeunit <ObjectID> "<Feature Name> Generation" { trigger OnRun() begin GenerateSuggestions(); end; procedure SetUserPrompt(InputPrompt: Text) begin UserPrompt := InputPrompt; end; procedure GetResult(var TmpResult: Record "<Feature Name> Proposal" temporary) begin TmpResult.Copy(TmpProposal, true); end; internal procedure GetCompletionResult(): Text begin exit(RawCompletionResult); end; local procedure GenerateSuggestions() var ResponseText: Text; JResponse: JsonToken; JItems: JsonToken; JsonArray: JsonArray; begin RawCompletionResult := ''; // Call Azure OpenAI ResponseText := Chat(GetSystemPrompt(), GetUserPromptWithContext(UserPrompt)); // Parse JSON response JResponse.ReadFrom(ResponseText); JResponse.AsObject().Get('items', JItems); JsonArray := JItems.AsArray(); // Process results ProcessResults(JsonArray); end; procedure Chat(SystemPrompt: Text; UserPrompt: Text): Text var AzureOpenAI: Codeunit "Azure OpenAI"; AOAIOperationResponse: Codeunit "AOAI Operation Response"; AOAIChatCompletionParams: Codeunit "AOAI Chat Completion Params"; AOAIChatMessages: Codeunit "AOAI Chat Messages"; AOAIDeployments: Codeunit "AOAI Deployments"; IsolatedStorageWrapper: Codeunit "Isolated Storage Wrapper"; Result: Text; begin // Use Microsoft's managed Azure OpenAI (recommended for production) AzureOpenAI.SetManagedResourceAuthorization( Enum::"AOAI Model Type"::"Chat Completions", IsolatedStorageWrapper.GetEndpoint(), IsolatedStorageWrapper.GetDeployment(), IsolatedStorageWrapper.GetSecretKey(), AOAIDeployments.GetGPT4oLatest()); AzureOpenAI.SetCopilotCapability(Enum::"Copilot Capability"::"<Your Capability>"); // Configure completion parameters AOAIChatCompletionParams.SetMaxTokens(2500); AOAIChatCompletionParams.SetTemperature(0); // 0=deterministic, 1=creative AOAIChatCompletionParams.SetJsonMode(true); // Ensure JSON response // Build chat messages AOAIChatMessages.AddSystemMessage(SystemPrompt); AOAIChatMessages.AddUserMessage(UserPrompt); // Generate completion AzureOpenAI.GenerateChatCompletion(AOAIChatMessages, AOAIChatCompletionParams, AOAIOperationResponse); if AOAIOperationResponse.IsSuccess() then Result := AOAIChatMessages.GetLastMessage() else Error(AOAIOperationResponse.GetError()); RawCompletionResult := Result; exit(Result); end; local procedure GetSystemPrompt(): Text var PromptBuilder: TextBuilder; begin PromptBuilder.AppendLine('# Role'); PromptBuilder.AppendLine('You are an expert assistant for <describe feature> in Business Central.'); PromptBuilder.AppendLine(''); PromptBuilder.AppendLine('# Task'); PromptBuilder.AppendLine('<Describe what AI should do>'); PromptBuilder.AppendLine(''); PromptBuilder.AppendLine('# Guidelines'); PromptBuilder.AppendLine('- Base suggestions only on provided data'); PromptBuilder.AppendLine('- Provide clear explanations for each suggestion'); PromptBuilder.AppendLine('- Do not make assumptions'); PromptBuilder.AppendLine(''); PromptBuilder.AppendLine('# Output Format (JSON)'); PromptBuilder.AppendLine('{"items": [{"no": "...", "description": "...", "explanation": "..."}]}'); exit(PromptBuilder.ToText()); end; local procedure GetUserPromptWithContext(InputPrompt: Text): Text var PromptBuilder: TextBuilder; begin PromptBuilder.AppendLine('# Context'); PromptBuilder.AppendLine(GetBusinessCentralContext()); PromptBuilder.AppendLine(''); PromptBuilder.AppendLine('# User Request'); PromptBuilder.AppendLine(InputPrompt); exit(PromptBuilder.ToText()); end; local procedure GetBusinessCentralContext(): Text begin // Build context from BC data // Example: Query items, customers, sales orders, etc. exit('Current company: ' + CompanyName + ', Date: ' + Format(Today)); end; local procedure ProcessResults(JsonArray: JsonArray) var JItem: JsonToken; i: Integer; begin TmpProposal.DeleteAll(); for i := 0 to JsonArray.Count() - 1 do begin JsonArray.Get(i, JItem); ProcessSingleResult(JItem); end; end; local procedure ProcessSingleResult(JItem: JsonToken) var NumberToken: JsonToken; DescToken: JsonToken; ExplToken: JsonToken; begin TmpProposal.Init(); if JItem.AsObject().Get('no', NumberToken) then TmpProposal."No." := CopyStr(NumberToken.AsValue().AsText(), 1, MaxStrLen(TmpProposal."No.")); if JItem.AsObject().Get('description', DescToken) then TmpProposal.Description := CopyStr(DescToken.AsValue().AsText(), 1, MaxStrLen(TmpProposal.Description)); if JItem.AsObject().Get('explanation', ExplToken) then TmpProposal.Explanation := CopyStr(ExplToken.AsValue().AsText(), 1, MaxStrLen(TmpProposal.Explanation)); TmpProposal.Insert(); end; var TmpProposal: Record "<Feature Name> Proposal" temporary; UserPrompt: Text; RawCompletionResult: Text; } ``` ### Phase 5: Update Permissions Add to permission set: ```al page "<Feature Name> Copilot" = X, page "<Feature Name> Proposal Sub" = X, table "<Feature Name> Proposal" = RIMD, codeunit "<Feature Name> Generation" = X; ``` ### Phase 6: Build, Test, and Iterate 1. **Compile**: Use `al_build` 2. **Fix errors**: Review `problems` 3. **Publish**: Use `al_incremental_publish` 4. **Test**: - Open PromptDialog from calling page - Try all PromptGuide examples - Test Generate/Regenerate - Test OK/Cancel actions - Verify error handling ## Structured Output Requirements Deliver the following files: - [ ] Main PromptDialog page - [ ] Optional: Temporary table for results - [ ] Optional: Subpage for displaying results - [ ] Generation codeunit with Azure OpenAI integration - [ ] Permission set updates - [ ] Documentation comments ## Human Validation Gate 🚨 **STOP**: Before completing, verify with user: - [ ] All PromptDialog areas implemented (PromptOptions, Prompt, Content, PromptGuide, SystemActions) - [ ] PromptMode matches requirements - [ ] Generate/Regenerate work correctly - [ ] OK/Cancel actions behave as expected - [ ] Error handling is graceful - [ ] Prompts follow responsible AI guidelines - [ ] Code compiles without errors - [ ] Page works in Business Central ## Common Issues & Solutions ### Issue: "SystemAction not found" **Solution**: Ensure PageType = PromptDialog (not Card or Document) ### Issue: "Area not supported" **Solution**: Verify PromptOptions, Prompt, Content, PromptGuide are spelled correctly ### Issue: AI returns empty results **Solution**: - Check system prompt clarity - Verify JsonMode is true - Ensure context is being passed - Test with simpler prompt first ### Issue: "Cannot set PromptMode" **Solution**: PromptMode must be set at page level, not in trigger ## Success Criteria - [ ] Page opens correctly - [ ] InstructionalText guides user - [ ] PromptGuide examples work - [ ] Generate produces results - [ ] Regenerate produces different results - [ ] OK applies suggestions - [ ] Cancel discards suggestions - [ ] Error handling is graceful - [ ] Responsible AI principles followed --- **Framework Compliance**: This workflow implements AI-Native Instructions Architecture with Context Loading, Human Validation Gates, and complete Copilot UX patterns.