From 8008b775639ae307d0db1c3bb173acad92d51b00 Mon Sep 17 00:00:00 2001 From: Darrick Date: Fri, 12 Apr 2024 11:45:38 +0200 Subject: [PATCH 1/6] Backport 882 --- src/System Application/App/AI/app.json | 2 +- .../Azure OpenAI/AzureOpenAIImpl.Codeunit.al | 66 +++++- .../AOAIChatMessages.Codeunit.al | 90 +++++++- .../Chat Completion/AOAIToolsImpl.Codeunit.al | 129 +++++++++++- .../Tools/AOAIFunction.Interface.al | 33 +++ .../AOAIFunctionResponse.Codeunit.al | 91 ++++++++ .../AOAIOperationResponse.Codeunit.al | 36 ++++ src/System Application/Test/AI/app.json | 4 +- .../AI/src/AzureOpenAIToolsTest.Codeunit.al | 194 +++++++++++++++++- .../Functions/BadTestFunction1.Codeunit.al | 21 ++ .../Functions/BadTestFunction2.Codeunit.al | 21 ++ .../src/Functions/TestFunction1.Codeunit.al | 21 ++ .../src/Functions/TestFunction2.Codeunit.al | 21 ++ 13 files changed, 704 insertions(+), 25 deletions(-) create mode 100644 src/System Application/App/AI/src/Azure OpenAI/Chat Completion/Tools/AOAIFunction.Interface.al create mode 100644 src/System Application/App/AI/src/Azure OpenAI/Operation Response/AOAIFunctionResponse.Codeunit.al rename src/System Application/App/AI/src/Azure OpenAI/{ => Operation Response}/AOAIOperationResponse.Codeunit.al (54%) create mode 100644 src/System Application/Test/AI/src/Functions/BadTestFunction1.Codeunit.al create mode 100644 src/System Application/Test/AI/src/Functions/BadTestFunction2.Codeunit.al create mode 100644 src/System Application/Test/AI/src/Functions/TestFunction1.Codeunit.al create mode 100644 src/System Application/Test/AI/src/Functions/TestFunction2.Codeunit.al diff --git a/src/System Application/App/AI/app.json b/src/System Application/App/AI/app.json index 2eb4a70aca..12ec270b33 100644 --- a/src/System Application/App/AI/app.json +++ b/src/System Application/App/AI/app.json @@ -89,7 +89,7 @@ "platform": "24.0.0.0", "idRanges": [ { - "from": 7759, + "from": 7758, "to": 7778 } ], diff --git a/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al index 4523fd9dcc..a00096e2f8 100644 --- a/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al +++ b/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al @@ -44,6 +44,7 @@ codeunit 7772 "Azure OpenAI Impl" EmptyMetapromptErr: Label 'The metaprompt has not been set, please provide a metaprompt.'; MetapromptLoadingErr: Label 'Metaprompt not found.'; EnabledKeyTok: Label 'AOAI-Enabled', Locked = true; + FunctionCallingFunctionNotFoundErr: Label 'Function call not found, %1.', Comment = '%1 is the name of the function'; AllowlistedTenantsAkvKeyTok: Label 'AOAI-Allow-1P-Auth', Locked = true; TelemetryGenerateTextCompletionLbl: Label 'Generate Text Completion', Locked = true; TelemetryGenerateEmbeddingLbl: Label 'Generate Embedding', Locked = true; @@ -58,6 +59,7 @@ codeunit 7772 "Azure OpenAI Impl" TelemetryProhibitedCharactersTxt: Label 'Prohibited characters were removed from the prompt.', Locked = true; TelemetryTokenCountLbl: Label 'Metaprompt token count: %1, Prompt token count: %2, Total token count: %3', Comment = '%1 is the number of tokens in the metaprompt, %2 is the number of tokens in the prompt, %3 is the total number of tokens', Locked = true; TelemetryMetapromptRetrievalErr: Label 'Unable to retrieve metaprompt from Azure Key Vault.', Locked = true; + TelemetryFunctionCallingFailedErr: Label 'Function calling failed for function: %1', Comment = '%1 is the name of the function', Locked = true; TelemetryEmptyTenantIdErr: Label 'Empty or malformed tenant ID.', Locked = true; TelemetryTenantAllowlistedMsg: Label 'The current tenant is allowlisted for first party auth.', Locked = true; @@ -342,15 +344,16 @@ codeunit 7772 "Azure OpenAI Impl" exit; end; - ProcessChatCompletionResponse(AOAIOperationResponse.GetResult(), ChatMessages, CallerModuleInfo); + ProcessChatCompletionResponse(ChatMessages, AOAIOperationResponse, CallerModuleInfo); FeatureTelemetry.LogUsage('0000KVN', CopilotCapabilityImpl.GetAzureOpenAICategory(), TelemetryGenerateChatCompletionLbl, CustomDimensions); end; [NonDebuggable] [TryFunction] - local procedure ProcessChatCompletionResponse(ResponseText: Text; var ChatMessages: Codeunit "AOAI Chat Messages"; CallerModuleInfo: ModuleInfo) + local procedure ProcessChatCompletionResponse(var ChatMessages: Codeunit "AOAI Chat Messages"; var AOAIOperationResponse: Codeunit "AOAI Operation Response"; CallerModuleInfo: ModuleInfo) var + AOAIFunctionResponse: Codeunit "AOAI Function Response"; CustomDimensions: Dictionary of [Text, Text]; ToolsCall: Text; Response: JsonObject; @@ -358,7 +361,7 @@ codeunit 7772 "Azure OpenAI Impl" XPathLbl: Label '$.content', Comment = 'For more details on response, see https://aka.ms/AAlrz36', Locked = true; XPathToolCallsLbl: Label '$.tool_calls', Comment = 'For more details on response, see https://aka.ms/AAlrz36', Locked = true; begin - Response.ReadFrom(ResponseText); + Response.ReadFrom(AOAIOperationResponse.GetResult()); if Response.SelectToken(XPathLbl, CompletionToken) then if not CompletionToken.AsValue().IsNull() then ChatMessages.AddAssistantMessage(CompletionToken.AsValue().AsText()); @@ -366,11 +369,68 @@ codeunit 7772 "Azure OpenAI Impl" CompletionToken.AsArray().WriteTo(ToolsCall); ChatMessages.AddAssistantMessage(ToolsCall); + ProcessFunctionCall(CompletionToken.AsArray(), ChatMessages, AOAIOperationResponse); + AddTelemetryCustomDimensions(CustomDimensions, CallerModuleInfo); + AOAIFunctionResponse := AOAIOperationResponse.GetFunctionResponse(); + AOAIFunctionResponse.SetIsFunctionCall(true); + if not AOAIFunctionResponse.IsSuccess() then + FeatureTelemetry.LogError('0000MTB', CopilotCapabilityImpl.GetAzureOpenAICategory(), StrSubstNo(TelemetryFunctionCallingFailedErr, AOAIFunctionResponse.GetFunctionName()), AOAIFunctionResponse.GetError(), AOAIFunctionResponse.GetErrorCallstack(), CustomDimensions); + FeatureTelemetry.LogUsage('0000MFH', CopilotCapabilityImpl.GetAzureOpenAICategory(), TelemetryChatCompletionToolCallLbl, CustomDimensions); end; end; + local procedure ProcessFunctionCall(Functions: JsonArray; var ChatMessages: Codeunit "AOAI Chat Messages"; var AOAIOperationResponse: Codeunit "AOAI Operation Response"): Boolean + var + Function: JsonObject; + Arguments: JsonObject; + Token: JsonToken; + FunctionName: Text; + AOAIFunction: Interface "AOAI Function"; + FunctionResult: Variant; + begin + if Functions.Count = 0 then + exit; + + Functions.Get(0, Token); + Function := Token.AsObject(); + + if Function.Get('type', Token) then begin + if Token.AsValue().AsText() <> 'function' then + exit; + end else + exit; + + if Function.Get('function', Token) then + Function := Token.AsObject() + else + exit; + + if Function.Get('name', Token) then + FunctionName := Token.AsValue().AsText() + else + exit; + + if Function.Get('arguments', Token) then + // Arguments are stored as a string in the JSON + Arguments.ReadFrom(Token.AsValue().AsText()); + + if ChatMessages.GetFunctionTool(FunctionName, AOAIFunction) then + if TryExecuteFunction(AOAIFunction, Arguments, FunctionResult) then + AOAIOperationResponse.SetFunctionCallingResponse(true, AOAIFunction.GetName(), FunctionResult) + else + AOAIOperationResponse.SetFunctionCallingResponse(false, AOAIFunction.GetName(), GetLastErrorText(), GetLastErrorCallStack()) + else + AOAIOperationResponse.SetFunctionCallingResponse(false, FunctionName, StrSubstNo(FunctionCallingFunctionNotFoundErr, FunctionName), ''); + end; + + [TryFunction] + local procedure TryExecuteFunction(AOAIFunction: Interface "AOAI Function"; Arguments: JsonObject; var Result: Variant) + begin + Result := AOAIFunction.Execute(Arguments); + end; + [TryFunction] [NonDebuggable] local procedure SendRequest(ModelType: Enum "AOAI Model Type"; AOAIAuthorization: Codeunit "AOAI Authorization"; Payload: Text; var AOAIOperationResponse: Codeunit "AOAI Operation Response") diff --git a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatMessages.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatMessages.Codeunit.al index 65484c610c..57d25ff594 100644 --- a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatMessages.Codeunit.al +++ b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatMessages.Codeunit.al @@ -178,18 +178,19 @@ codeunit 7763 "AOAI Chat Messages" exit(AOAIChatMessagesImpl.PrepareHistory(SystemMessageTokenCount, MessagesTokenCount)); end; +#if not CLEAN25 /// /// Appends a Tool to the payload. /// /// The Tool to be added to the payload. /// See more details here: https://go.microsoft.com/fwlink/?linkid=2254538 [NonDebuggable] + [Obsolete('Use AddTool that takes in an AOAI Function interface.', '25.0')] procedure AddTool(NewTool: JsonObject) - var - CallerModuleInfo: ModuleInfo; begin - NavApp.GetCallerModuleInfo(CallerModuleInfo); - AOAIToolsImpl.AddTool(NewTool, CallerModuleInfo); +#pragma warning disable AL0432 + AOAIToolsImpl.AddTool(NewTool); +#pragma warning restore AL0432 end; /// @@ -199,9 +200,12 @@ codeunit 7763 "AOAI Chat Messages" /// The new Tool. /// Message id does not exist. [NonDebuggable] + [Obsolete('Deprecated with no replacement. Use DeleteFunctionTool and AddTool.', '25.0')] procedure ModifyTool(Id: Integer; NewTool: JsonObject) begin +#pragma warning disable AL0432 AOAIToolsImpl.ModifyTool(Id, NewTool); +#pragma warning restore AL0432 end; /// @@ -209,20 +213,76 @@ codeunit 7763 "AOAI Chat Messages" /// /// Id of the Tool. /// Message id does not exist. + [Obsolete('Use DeleteFunctionTool that takes in a function name instead.', '25.0')] procedure DeleteTool(Id: Integer) begin +#pragma warning disable AL0432 AOAIToolsImpl.DeleteTool(Id); +#pragma warning restore AL0432 + end; +#endif + + /// + /// Adds a function to the payload. + /// + /// The function to be added + procedure AddTool(Function: Interface "AOAI Function") + begin + AOAIToolsImpl.AddTool(Function); end; + /// + /// Deletes a Function from the list of Functions. + /// + /// Name of the Function. + /// Message id does not exist. + procedure DeleteFunctionTool(Name: Text): Boolean + begin + exit(AOAIToolsImpl.DeleteTool(Name)); + end; + + /// + /// Remove all tools. + /// + procedure ClearTools() + begin + AOAIToolsImpl.ClearTools(); + end; + + /// + /// Gets the function associated with the specified name. + /// + /// Name of the function to get. + /// The function codeunit. + /// Tool not found. + procedure GetFunctionTool(Name: Text; var Function: Interface "AOAI Function"): Boolean + begin + exit(AOAIToolsImpl.GetTool(Name, Function)); + end; + + /// + /// Gets the list of names of Function Tools that have been added. + /// + /// List of function tool names. + procedure GetFunctionTools(): List of [Text] + begin + exit(AOAIToolsImpl.GetFunctionTools()); + end; + +#if not CLEAN25 /// /// Gets the list of Tools. /// /// List of Tools. [NonDebuggable] + [Obsolete('Use GetFunctionTool() that takes in a function name and returns the interface.', '25.0')] procedure GetTools(): List of [JsonObject] begin +#pragma warning disable AL0432 exit(AOAIToolsImpl.GetTools()); +#pragma warning restore AL0432 end; +#endif /// /// Checks if at least one Tools exists in the list. @@ -253,6 +313,28 @@ codeunit 7763 "AOAI Chat Messages" AOAIToolsImpl.SetToolChoice(ToolChoice); end; + /// + /// Sets the function as the tool choice to be called. + /// + /// The function name parameter. + /// See more details here: https://go.microsoft.com/fwlink/?linkid=2254538 + [NonDebuggable] + procedure SetFunctionAsToolChoice(FunctionName: Text) + begin + AOAIToolsImpl.SetFunctionAsToolChoice(FunctionName); + end; + + /// + /// Sets the function as the tool choice to be called. + /// + /// The function codeunit. + /// See more details here: https://go.microsoft.com/fwlink/?linkid=2254538 + [NonDebuggable] + procedure SetFunctionAsToolChoice(Function: Interface "AOAI Function") + begin + AOAIToolsImpl.SetFunctionAsToolChoice(Function); + end; + /// /// Gets the Tool choice parameter. /// diff --git a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIToolsImpl.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIToolsImpl.Codeunit.al index 1f451ed6fc..cac244cf3e 100644 --- a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIToolsImpl.Codeunit.al +++ b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIToolsImpl.Codeunit.al @@ -12,18 +12,59 @@ codeunit 7778 "AOAI Tools Impl" InherentPermissions = X; var + Functions: array[20] of Interface "AOAI Function"; + FunctionNames: Dictionary of [Text, Integer]; Initialized: Boolean; AddToolToPayload: Boolean; [NonDebuggable] ToolChoice: Text; +#if not CLEAN25 [NonDebuggable] Tools: List of [JsonObject]; ToolIdDoesNotExistErr: Label 'Tool id does not exist.'; +#endif ToolObjectInvalidErr: Label '%1 object does not contain %2 property.', Comment = '%1 is the object name and %2 is the property that is missing.'; ToolTypeErr: Label 'Tool type must be of function type.'; + TooManyFunctionsAddedErr: Label 'Too many functions have been added. Maximum number of functions is %1', Comment = '%1 is the maximum number of tools that can be added.'; + FunctionAlreadyExistsErr: Label 'Function with the name, %1, already exists.', Comment = '%1 is the function name.'; + procedure AddTool(Tool: Interface "AOAI Function") + var + Index: Integer; + begin + Initialize(); + Index := FunctionNames.Count() + 1; + + ValidateTool(Tool.GetPrompt()); + + if FunctionNames.ContainsKey(Tool.GetName()) then + Error(FunctionAlreadyExistsErr, Tool.GetName()); + + if Index > ArrayLen(Functions) then + Error(TooManyFunctionsAddedErr, ArrayLen(Functions)); + + Functions[Index] := Tool; + FunctionNames.Add(Tool.GetName(), Index); + end; + + procedure GetTool(Name: Text; var Function: Interface "AOAI Function"): Boolean + begin + if FunctionNames.ContainsKey(Name) then begin + Function := Functions[FunctionNames.get(Name)]; + exit(true); + end else + exit(false); + end; + + procedure GetFunctionTools(): List of [Text] + begin + exit(FunctionNames.Keys()); + end; + +#if not CLEAN25 [NonDebuggable] - procedure AddTool(NewTool: JsonObject; CallerModuleInfo: ModuleInfo) + [Obsolete('Use AddTool that takes in an AOAI Function interface instead.', '25.0')] + procedure AddTool(NewTool: JsonObject) begin Initialize(); if ValidateTool(NewTool) then @@ -31,6 +72,7 @@ codeunit 7778 "AOAI Tools Impl" end; [NonDebuggable] + [Obsolete('Use ModifyTool that takes in an AOAI Function interface instead.', '25.0')] procedure ModifyTool(Id: Integer; NewTool: JsonObject) begin if (Id < 1) or (Id > Tools.Count) then @@ -39,6 +81,7 @@ codeunit 7778 "AOAI Tools Impl" Tools.Set(Id, NewTool); end; + [Obsolete('Use DeleteTool that takes in a function name instead.', '25.0')] procedure DeleteTool(Id: Integer) begin if (Id < 1) or (Id > Tools.Count) then @@ -48,34 +91,82 @@ codeunit 7778 "AOAI Tools Impl" end; [NonDebuggable] + [Obsolete('Use GetTool() that takes in a function name and var for AOAI Function interface.', '25.0')] procedure GetTools(): List of [JsonObject] begin exit(Tools); end; +#endif + + procedure DeleteTool(Name: Text): Boolean + var + Index: Integer; + begin + if not FunctionNames.ContainsKey(Name) then + exit(false); + + Index := FunctionNames.get(Name); + FunctionNames.Remove(Name); + + for Index := Index to FunctionNames.Count() do begin + Functions[Index] := Functions[Index + 1]; + FunctionNames.Set(Functions[Index].GetName(), Index); + end; + Clear(Functions[Index + 1]); + exit(true); + end; + + procedure ClearTools() + begin +#if not CLEAN25 + Clear(Tools); +#endif + Clear(Functions); + Clear(FunctionNames); + end; [NonDebuggable] procedure PrepareTools() ToolsResult: JsonArray var Counter: Integer; +#if not CLEAN25 Tool: JsonObject; +#endif begin - if Tools.Count = 0 then - exit; - Initialize(); Counter := 1; - repeat - Clear(Tool); - Tools.Get(Counter, Tool); - ToolsResult.Add(Tool); - Counter += 1; - until Counter > Tools.Count; + if FunctionNames.Count <> 0 then + repeat + ToolsResult.Add(Functions[Counter].GetPrompt()); + Counter += 1; + until Counter > FunctionNames.Count(); + +#if not CLEAN25 + Counter := 1; + if Tools.Count <> 0 then + repeat + Clear(Tool); + Tools.Get(Counter, Tool); + ToolsResult.Add(Tool); + Counter += 1; + until Counter > Tools.Count; +#endif end; procedure ToolsExists(): Boolean begin - exit(AddToolToPayload and (Tools.Count > 0)); + if not AddToolToPayload then + exit(false); + +#if not CLEAN25 + if (FunctionNames.Count() = 0) and (Tools.Count = 0) then +#else + if (FunctionNames.Count() = 0) then +#endif + exit(false); + + exit(true); end; procedure SetAddToolToPayload(AddToolsToPayload: Boolean) @@ -89,6 +180,22 @@ codeunit 7778 "AOAI Tools Impl" ToolChoice := NewToolChoice; end; + procedure SetFunctionAsToolChoice(Function: Interface "AOAI Function") + begin + SetFunctionAsToolChoice(Function.GetName()); + end; + + procedure SetFunctionAsToolChoice(FunctionName: Text) + var + ToolChoiceObject: JsonObject; + FunctionObject: JsonObject; + begin + ToolChoiceObject.add('type', 'function'); + FunctionObject.add('name', FunctionName); + ToolChoiceObject.add('function', FunctionObject); + ToolChoiceObject.WriteTo(ToolChoice); + end; + [NonDebuggable] procedure GetToolChoice(): Text begin diff --git a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/Tools/AOAIFunction.Interface.al b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/Tools/AOAIFunction.Interface.al new file mode 100644 index 0000000000..c5dfcee33c --- /dev/null +++ b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/Tools/AOAIFunction.Interface.al @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.AI; + +interface "AOAI Function" +{ + Access = Public; + + /// + /// Get the prompt for the Function. Function prompt object describes the Function and the should contain the following fields: + /// - Type: The name of the Function, currently only function type is supported. For functions following fields are allowed: + /// -- Name: The name of the Function. (Required) + /// -- Description: The description of the Function. (Optional) + /// -- Parameters: The parameters of the Function. (Required) + /// More details can be found here: https://go.microsoft.com/fwlink/?linkid=2254538 + /// + procedure GetPrompt(): JsonObject; + + /// + /// This function is invoked as a response from Azure Open AI. + /// -Arguments: The expected parameters of the Function defined. + /// The function returns a variant, and it's up to the implementation to decide what to return. + /// + procedure Execute(Arguments: JsonObject): Variant; + + /// + /// Get the name of the function. + /// + /// This needs to match the function name in GetPrompt. + procedure GetName(): Text; +} \ No newline at end of file diff --git a/src/System Application/App/AI/src/Azure OpenAI/Operation Response/AOAIFunctionResponse.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/Operation Response/AOAIFunctionResponse.Codeunit.al new file mode 100644 index 0000000000..25b016a69a --- /dev/null +++ b/src/System Application/App/AI/src/Azure OpenAI/Operation Response/AOAIFunctionResponse.Codeunit.al @@ -0,0 +1,91 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.AI; + +/// +/// The status and result of an functions. +/// +codeunit 7758 "AOAI Function Response" +{ + Access = Public; + InherentEntitlements = X; + InherentPermissions = X; + + var + Success: Boolean; + FunctionCall: Boolean; + FunctionName: Text; + Result: Variant; + Error: Text; + ErrorCallStack: Text; + + /// + /// Get whether the function call was successful. + /// + /// True if the call was successful, false otherwise. + procedure IsSuccess(): Boolean + begin + exit(Success); + end; + + /// + /// Get the return value of the function that was called. + /// + /// The return value from the function + procedure GetResult(): Variant + begin + exit(Result); + end; + + /// + /// Get the error message from the function that was called. + /// + /// The error message from the function. + procedure GetError(): Text + begin + exit(Error); + end; + + /// + /// Get the name of the function that was called. + /// + /// The name of the function that was called. + procedure GetFunctionName(): Text + begin + exit(FunctionName); + end; + + /// + /// Get the error call stack from the function that was called. + /// + /// The error call stack from the function. + procedure GetErrorCallstack(): Text + begin + exit(ErrorCallStack); + end; + + /// + /// Get whether the operation was a function call. + /// + /// True if it was a function call, false otherwise. + internal procedure IsFunctionCall(): Boolean + begin + exit(FunctionCall); + end; + + internal procedure SetIsFunctionCall(NewIsFunctionCall: Boolean) + begin + FunctionCall := NewIsFunctionCall; + end; + + internal procedure SetFunctionCallingResponse(NewFunctionCallSuccess: Boolean; NewFunctionCalled: Text; NewFunctionResult: Variant; NewFunctionError: Text; NewFunctionErrorCallStack: Text) + begin + Success := NewFunctionCallSuccess; + FunctionName := NewFunctionCalled; + Result := NewFunctionResult; + Error := NewFunctionError; + ErrorCallStack := NewFunctionErrorCallStack; + end; +} \ No newline at end of file diff --git a/src/System Application/App/AI/src/Azure OpenAI/AOAIOperationResponse.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/Operation Response/AOAIOperationResponse.Codeunit.al similarity index 54% rename from src/System Application/App/AI/src/Azure OpenAI/AOAIOperationResponse.Codeunit.al rename to src/System Application/App/AI/src/Azure OpenAI/Operation Response/AOAIOperationResponse.Codeunit.al index 0bcbdb87ea..03eac88a20 100644 --- a/src/System Application/App/AI/src/Azure OpenAI/AOAIOperationResponse.Codeunit.al +++ b/src/System Application/App/AI/src/Azure OpenAI/Operation Response/AOAIOperationResponse.Codeunit.al @@ -16,6 +16,7 @@ codeunit 7770 "AOAI Operation Response" InherentPermissions = X; var + AOAIFunctionResponse: Codeunit "AOAI Function Response"; StatusCode: Integer; Success: Boolean; Result: Text; @@ -57,6 +58,24 @@ codeunit 7770 "AOAI Operation Response" exit(Error); end; + /// + /// Get whether the operation was a function call. + /// + /// True if it was a function call, false otherwise. + procedure IsFunctionCall(): Boolean + begin + exit(AOAIFunctionResponse.IsFunctionCall()); + end; + + /// + /// Get the function response codeunit which contains the response details. + /// + /// The codeunit which contains response details for the function call. + procedure GetFunctionResponse(): Codeunit "AOAI Function Response" + begin + exit(AOAIFunctionResponse); + end; + internal procedure SetOperationResponse(var ALCopilotOperationResponse: DotNet ALCopilotOperationResponse) begin Success := ALCopilotOperationResponse.IsSuccess(); @@ -67,4 +86,21 @@ codeunit 7770 "AOAI Operation Response" if Error = '' then Error := GetLastErrorText(); end; + + internal procedure SetFunctionCallingResponse(NewFunctionCallSuccess: Boolean; NewFunctionCalled: Text; NewFunctionError: Text; NewFunctionErrorCallStack: Text) + var + EmptyVariant: Variant; + begin + SetFunctionCallingResponse(NewFunctionCallSuccess, NewFunctionCalled, EmptyVariant, NewFunctionError, NewFunctionErrorCallStack); + end; + + internal procedure SetFunctionCallingResponse(NewFunctionCallSuccess: Boolean; NewFunctionCalled: Text; NewFunctionResult: Variant) + begin + SetFunctionCallingResponse(NewFunctionCallSuccess, NewFunctionCalled, NewFunctionResult, '', ''); + end; + + local procedure SetFunctionCallingResponse(NewFunctionCallSuccess: Boolean; NewFunctionCalled: Text; NewFunctionResult: Variant; NewFunctionError: Text; NewFunctionErrorCallStack: Text) + begin + AOAIFunctionResponse.SetFunctionCallingResponse(NewFunctionCallSuccess, NewFunctionCalled, NewFunctionResult, NewFunctionError, NewFunctionErrorCallStack); + end; } \ No newline at end of file diff --git a/src/System Application/Test/AI/app.json b/src/System Application/Test/AI/app.json index e288beb696..d020d7b90d 100644 --- a/src/System Application/Test/AI/app.json +++ b/src/System Application/Test/AI/app.json @@ -52,8 +52,8 @@ "platform": "24.0.0.0", "idRanges": [ { - "from": 132683, - "to": 132686 + "from": 132683, + "to": 132690 } ], "target": "OnPrem" diff --git a/src/System Application/Test/AI/src/AzureOpenAIToolsTest.Codeunit.al b/src/System Application/Test/AI/src/AzureOpenAIToolsTest.Codeunit.al index bf70a06483..f0201fa142 100644 --- a/src/System Application/Test/AI/src/AzureOpenAIToolsTest.Codeunit.al +++ b/src/System Application/Test/AI/src/AzureOpenAIToolsTest.Codeunit.al @@ -11,17 +11,32 @@ codeunit 132686 "Azure OpenAI Tools Test" var LibraryAssert: Codeunit "Library Assert"; ToolObjectInvalidErr: Label '%1 object does not contain %2 property.', Comment = '%1 is the object name and %2 is the property that is missing.'; - +#if not CLEAN25 [Test] procedure TestAddingToolsInChatMessages() var AOAIChatMessages: Codeunit "AOAI Chat Messages"; begin LibraryAssert.IsFalse(AOAIChatMessages.ToolsExists(), 'Tool should not exist'); +#pragma warning disable AL0432 AOAIChatMessages.AddTool(GetTestFunction1Tool()); - LibraryAssert.IsTrue(AOAIChatMessages.ToolsExists(), 'Tool should exist'); +#pragma warning restore AL0432 + LibraryAssert.IsTrue(AOAIChatMessages.ToolsExists(), 'Tool via JsonObject should exist'); end; +#endif + [Test] + procedure TestAddingFunctionsInChatMessages() + var + AOAIChatMessages: Codeunit "AOAI Chat Messages"; + TestFunction1: Codeunit "Test Function 1"; + begin + LibraryAssert.IsFalse(AOAIChatMessages.ToolsExists(), 'Tool should not exist'); + AOAIChatMessages.AddTool(TestFunction1); + LibraryAssert.IsTrue(AOAIChatMessages.ToolsExists(), 'Tool via interface should exist'); + end; + +#if not CLEAN25 [Test] procedure TestModifyToolsInChatMessages() var @@ -32,32 +47,133 @@ codeunit 132686 "Azure OpenAI Tools Test" begin Function1Tool := GetTestFunction1Tool(); Function2Tool := GetTestFunction2Tool(); - +#pragma warning disable AL0432 AOAIChatMessages.AddTool(Function1Tool); Tools := AOAIChatMessages.GetTools(); +#pragma warning restore AL0432 LibraryAssert.AreEqual(1, Tools.Count, 'Tool should exist'); LibraryAssert.AreEqual(Format(Function1Tool), Format(Tools.Get(1)), 'Tool should have same value.'); - +#pragma warning disable AL0432 AOAIChatMessages.ModifyTool(1, Function2Tool); +#pragma warning restore AL0432 LibraryAssert.AreEqual(Format(Function2Tool), Format(Tools.Get(1)), 'Tool should have same value.'); +#pragma warning disable AL0432 + AOAIChatMessages.DeleteTool(1); +#pragma warning restore AL0432 + LibraryAssert.IsFalse(AOAIChatMessages.ToolsExists(), 'Tool should not exist'); + end; + [Test] + procedure TestDeleteToolInChatMessages() + var + AOAIChatMessages: Codeunit "AOAI Chat Messages"; + Tools: List of [JsonObject]; + ToolObject: JsonObject; + Payload: Text; + begin + LibraryAssert.IsFalse(AOAIChatMessages.ToolsExists(), 'Tool should not exist'); +#pragma warning disable AL0432 + AOAIChatMessages.AddTool(GetTestFunction1Tool()); + AOAIChatMessages.AddTool(GetTestFunction2Tool()); +#pragma warning restore AL0432 + LibraryAssert.IsTrue(AOAIChatMessages.ToolsExists(), 'Tool should exist'); +#pragma warning disable AL0432 AOAIChatMessages.DeleteTool(1); +#pragma warning restore AL0432 + LibraryAssert.IsTrue(AOAIChatMessages.ToolsExists(), 'Tool should exist'); +#pragma warning disable AL0432 + Tools := AOAIChatMessages.GetTools(); +#pragma warning restore AL0432 + Tools.Get(1, ToolObject); + ToolObject.WriteTo(Payload); + LibraryAssert.AreEqual(Format(GetTestFunction2Tool()), Payload, 'Tool should have same value.'); + end; +#endif + + [Test] + procedure TestDeleteFunctionToolInChatMessages() + var + AOAIChatMessages: Codeunit "AOAI Chat Messages"; + TestFunction1: Codeunit "Test Function 1"; + TestFunction2: Codeunit "Test Function 2"; + Function: Interface "AOAI Function"; + FunctionNames: List of [Text]; + Payload: Text; + begin + LibraryAssert.IsFalse(AOAIChatMessages.ToolsExists(), 'Tool should not exist'); + AOAIChatMessages.AddTool(TestFunction1); + AOAIChatMessages.AddTool(TestFunction2); + LibraryAssert.IsTrue(AOAIChatMessages.ToolsExists(), 'Tool should exist'); + AOAIChatMessages.DeleteFunctionTool(TestFunction1.GetName()); + LibraryAssert.IsTrue(AOAIChatMessages.ToolsExists(), 'Tool should exist'); + + FunctionNames := AOAIChatMessages.GetFunctionTools(); + LibraryAssert.IsTrue(AOAIChatMessages.GetFunctionTool(FunctionNames.Get(1), Function), 'Function does not exist.'); + Function.GetPrompt().WriteTo(Payload); + LibraryAssert.AreEqual(Format(TestFunction2.GetPrompt()), Payload, 'Tool should have same value.'); + end; + +#if not CLEAN25 + [Test] + procedure TestClearToolsInChatMessagesObsoleted() + var + AOAIChatMessages: Codeunit "AOAI Chat Messages"; + begin + LibraryAssert.IsFalse(AOAIChatMessages.ToolsExists(), 'Tool should not exist'); +#pragma warning disable AL0432 + AOAIChatMessages.AddTool(GetTestFunction1Tool()); + AOAIChatMessages.AddTool(GetTestFunction2Tool()); +#pragma warning restore AL0432 + LibraryAssert.IsTrue(AOAIChatMessages.ToolsExists(), 'Tool should exist'); + AOAIChatMessages.ClearTools(); + LibraryAssert.IsFalse(AOAIChatMessages.ToolsExists(), 'No tool should exist'); + end; +#endif + + [Test] + procedure TestClearToolsInChatMessages() + var + AOAIChatMessages: Codeunit "AOAI Chat Messages"; + TestFunction1: Codeunit "Test Function 1"; + TestFunction2: Codeunit "Test Function 2"; + begin LibraryAssert.IsFalse(AOAIChatMessages.ToolsExists(), 'Tool should not exist'); + AOAIChatMessages.AddTool(TestFunction1); + AOAIChatMessages.AddTool(TestFunction2); + LibraryAssert.IsTrue(AOAIChatMessages.ToolsExists(), 'Tool should exist'); + AOAIChatMessages.ClearTools(); + LibraryAssert.IsFalse(AOAIChatMessages.ToolsExists(), 'No tool should exist'); end; +#if not CLEAN25 [Test] procedure TestSetAddToolsToChatMessages() var AOAIChatMessages: Codeunit "AOAI Chat Messages"; begin +#pragma warning disable AL0432 AOAIChatMessages.AddTool(GetTestFunction1Tool()); +#pragma warning restore AL0432 LibraryAssert.IsTrue(AOAIChatMessages.ToolsExists(), 'Tool should exist'); AOAIChatMessages.SetAddToolsToPayload(false); LibraryAssert.IsFalse(AOAIChatMessages.ToolsExists(), 'Tool should not exist'); end; +#endif + [Test] + procedure TestSetAddFunctionToolsToChatMessages() + var + AOAIChatMessages: Codeunit "AOAI Chat Messages"; + TestFunction1: Codeunit "Test Function 1"; + begin + AOAIChatMessages.AddTool(TestFunction1); + LibraryAssert.IsTrue(AOAIChatMessages.ToolsExists(), 'Tool should exist'); + AOAIChatMessages.SetAddToolsToPayload(false); + LibraryAssert.IsFalse(AOAIChatMessages.ToolsExists(), 'Tool should not exist'); + end; +#if not CLEAN25 [Test] procedure TestToolFormatInChatMessages() var @@ -66,15 +182,35 @@ codeunit 132686 "Azure OpenAI Tools Test" begin Function1Tool := GetTestFunction1Tool(); Function1Tool.Remove('type'); +#pragma warning disable AL0432 asserterror AOAIChatMessages.AddTool(Function1Tool); +#pragma warning restore AL0432 LibraryAssert.ExpectedError(StrSubstNo(ToolObjectInvalidErr, 'Tool', 'type')); Function1Tool := GetTestFunction1Tool(); Function1Tool.Remove('function'); +#pragma warning disable AL0432 asserterror AOAIChatMessages.AddTool(Function1Tool); +#pragma warning restore AL0432 LibraryAssert.ExpectedError(StrSubstNo(ToolObjectInvalidErr, 'Tool', 'function')); end; +#endif + [Test] + procedure TestFunctionToolFormatInChatMessages() + var + AOAIChatMessages: Codeunit "AOAI Chat Messages"; + BadTestFunction1: Codeunit "Bad Test Function 1"; + BadTestFunction2: Codeunit "Bad Test Function 2"; + begin + asserterror AOAIChatMessages.AddTool(BadTestFunction1); + LibraryAssert.ExpectedError(StrSubstNo(ToolObjectInvalidErr, 'Tool', 'type')); + + asserterror AOAIChatMessages.AddTool(BadTestFunction2); + LibraryAssert.ExpectedError(StrSubstNo(ToolObjectInvalidErr, 'Tool', 'function')); + end; + +#if not CLEAN25 [Test] procedure TestToolCoiceInChatMessages() var @@ -83,14 +219,59 @@ codeunit 132686 "Azure OpenAI Tools Test" ToolChoice: Text; begin Function1Tool := GetTestFunction1Tool(); +#pragma warning disable AL0432 AOAIChatMessages.AddTool(GetTestFunction1Tool()); +#pragma warning restore AL0432 LibraryAssert.AreEqual('auto', AOAIChatMessages.GetToolChoice(), 'Tool choice should be auto by default.'); ToolChoice := GetToolChoice(); AOAIChatMessages.SetToolChoice(ToolChoice); LibraryAssert.AreEqual(ToolChoice, AOAIChatMessages.GetToolChoice(), 'Tool choice should be equal to what was set.'); end; +#endif + + [Test] + procedure TestToolChoiceInChatMessages() + var + AOAIChatMessages: Codeunit "AOAI Chat Messages"; + TestFunction1: Codeunit "Test Function 1"; + ToolChoice: Text; + begin + AOAIChatMessages.AddTool(TestFunction1); + LibraryAssert.AreEqual('auto', AOAIChatMessages.GetToolChoice(), 'Tool choice should be auto by default.'); + + ToolChoice := GetToolChoice(); + AOAIChatMessages.SetToolChoice(ToolChoice); + LibraryAssert.AreEqual(ToolChoice, AOAIChatMessages.GetToolChoice(), 'Tool choice should be equal to what was set.'); + end; + + [Test] + procedure TestAssembleFunctionToolsInChatMessages() + var + AzureOpenAITestLibrary: Codeunit "Azure OpenAI Test Library"; + AOAIChatMessages: Codeunit "AOAI Chat Messages"; + TestFunction1: Codeunit "Test Function 1"; + TestFunction2: Codeunit "Test Function 2"; + FunctionNames: List of [Text]; + Tool1: JsonToken; + Tool2: JsonToken; + Tools: JsonArray; + begin + AOAIChatMessages.AddTool(TestFunction1); + AOAIChatMessages.AddTool(TestFunction2); + + FunctionNames := AOAIChatMessages.GetFunctionTools(); + Tools := AzureOpenAITestLibrary.GetAOAIAssembleTools(AOAIChatMessages); + + Tools.Get(0, Tool1); + Tools.Get(1, Tool2); + + LibraryAssert.AreEqual(2, Tools.Count, 'Tools should have 2 items.'); + LibraryAssert.AreEqual(Format(TestFunction1.GetPrompt()), Format(Tool1), 'Tool should have same value.'); + LibraryAssert.AreEqual(Format(TestFunction2.GetPrompt()), Format(Tool2), 'Tool should have same value.'); + end; +#if not CLEAN25 [Test] procedure TestAssembleToolsInChatMessages() var @@ -103,10 +284,14 @@ codeunit 132686 "Azure OpenAI Tools Test" Tools: JsonArray; begin Function1Tool := GetTestFunction1Tool(); +#pragma warning disable AL0432 AOAIChatMessages.AddTool(GetTestFunction1Tool()); +#pragma warning restore AL0432 Function2Tool := GetTestFunction2Tool(); +#pragma warning disable AL0432 AOAIChatMessages.AddTool(GetTestFunction2Tool()); +#pragma warning restore AL0432 Tools := AzureOpenAITestLibrary.GetAOAIAssembleTools(AOAIChatMessages); @@ -137,6 +322,7 @@ codeunit 132686 "Azure OpenAI Tools Test" ToolJsonObject.ReadFrom(TestTool); exit(ToolJsonObject); end; +#endif local procedure GetToolChoice(): Text begin diff --git a/src/System Application/Test/AI/src/Functions/BadTestFunction1.Codeunit.al b/src/System Application/Test/AI/src/Functions/BadTestFunction1.Codeunit.al new file mode 100644 index 0000000000..eb3e1f2473 --- /dev/null +++ b/src/System Application/Test/AI/src/Functions/BadTestFunction1.Codeunit.al @@ -0,0 +1,21 @@ +namespace System.Test.AI; + +using System.AI; + +codeunit 132689 "Bad Test Function 1" implements "AOAI Function" +{ + procedure GetPrompt() Function: JsonObject; + begin + Function.ReadFrom('{"function": {"name": "bad_test_function_1", "parameters": {"type": "object", "properties": {"message": {"type": "string", "description": "The input from user."}}}}}'); + end; + + procedure Execute(Arguments: JsonObject): Variant + begin + exit('This is bad test function 1'); + end; + + procedure GetName(): Text + begin + exit('bad_test_function_1'); + end; +} \ No newline at end of file diff --git a/src/System Application/Test/AI/src/Functions/BadTestFunction2.Codeunit.al b/src/System Application/Test/AI/src/Functions/BadTestFunction2.Codeunit.al new file mode 100644 index 0000000000..03d5826a05 --- /dev/null +++ b/src/System Application/Test/AI/src/Functions/BadTestFunction2.Codeunit.al @@ -0,0 +1,21 @@ +namespace System.Test.AI; + +using System.AI; + +codeunit 132690 "Bad Test Function 2" implements "AOAI Function" +{ + procedure GetPrompt() Function: JsonObject; + begin + Function.ReadFrom('{"type": "function"}'); + end; + + procedure Execute(Arguments: JsonObject): Variant + begin + exit('This is bad test function 2'); + end; + + procedure GetName(): Text + begin + exit('bad_test_function_2'); + end; +} \ No newline at end of file diff --git a/src/System Application/Test/AI/src/Functions/TestFunction1.Codeunit.al b/src/System Application/Test/AI/src/Functions/TestFunction1.Codeunit.al new file mode 100644 index 0000000000..6d6c09e504 --- /dev/null +++ b/src/System Application/Test/AI/src/Functions/TestFunction1.Codeunit.al @@ -0,0 +1,21 @@ +namespace System.Test.AI; + +using System.AI; + +codeunit 132687 "Test Function 1" implements "AOAI Function" +{ + procedure GetPrompt() Function: JsonObject; + begin + Function.ReadFrom('{"type": "function", "function": {"name": "test_function_1", "parameters": {"type": "object", "properties": {"message": {"type": "string", "description": "The input from user."}}}}}'); + end; + + procedure Execute(Arguments: JsonObject): Variant + begin + exit('This is test function 1'); + end; + + procedure GetName(): Text + begin + exit('test_function_1'); + end; +} \ No newline at end of file diff --git a/src/System Application/Test/AI/src/Functions/TestFunction2.Codeunit.al b/src/System Application/Test/AI/src/Functions/TestFunction2.Codeunit.al new file mode 100644 index 0000000000..d5c28b38b6 --- /dev/null +++ b/src/System Application/Test/AI/src/Functions/TestFunction2.Codeunit.al @@ -0,0 +1,21 @@ +namespace System.Test.AI; + +using System.AI; + +codeunit 132688 "Test Function 2" implements "AOAI Function" +{ + procedure GetPrompt() Function: JsonObject; + begin + Function.ReadFrom('{"type": "function", "function": {"name": "test_function_2", "parameters": {"type": "object", "properties": {"message": {"type": "string", "description": "The input from user."}}}}}'); + end; + + procedure Execute(Arguments: JsonObject): Variant + begin + exit('This is test function 2'); + end; + + procedure GetName(): Text + begin + exit('test_function_2'); + end; +} \ No newline at end of file From 823c49a6eb71cf30aa448b13f6864cc22c03665b Mon Sep 17 00:00:00 2001 From: Darrick Date: Fri, 12 Apr 2024 15:07:41 +0200 Subject: [PATCH 2/6] [Copilot] Minor changes to the internals of OperationResponse and FunctionResponse (#928) #### Summary Minor changes to the internals of OperationResponse and FunctionResponse. Conforming closer to facade principals #### Work Item(s) Fixes [AB#493212](https://dynamicssmb2.visualstudio.com/1fcb79e7-ab07-432a-a3c6-6cf5a88ba4a5/_workitems/edit/493212) --- .../Azure OpenAI/AzureOpenAIImpl.Codeunit.al | 20 +++++++----- .../AOAIFunctionResponse.Codeunit.al | 10 +----- .../AOAIOperationResponse.Codeunit.al | 32 +++---------------- 3 files changed, 18 insertions(+), 44 deletions(-) diff --git a/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al index a00096e2f8..d0ffdc35c1 100644 --- a/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al +++ b/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al @@ -369,11 +369,11 @@ codeunit 7772 "Azure OpenAI Impl" CompletionToken.AsArray().WriteTo(ToolsCall); ChatMessages.AddAssistantMessage(ToolsCall); - ProcessFunctionCall(CompletionToken.AsArray(), ChatMessages, AOAIOperationResponse); + AOAIFunctionResponse := AOAIOperationResponse.GetFunctionResponse(); + if not ProcessFunctionCall(CompletionToken.AsArray(), ChatMessages, AOAIFunctionResponse) then + AOAIFunctionResponse.SetFunctionCallingResponse(true, false, '', '', '', ''); AddTelemetryCustomDimensions(CustomDimensions, CallerModuleInfo); - AOAIFunctionResponse := AOAIOperationResponse.GetFunctionResponse(); - AOAIFunctionResponse.SetIsFunctionCall(true); if not AOAIFunctionResponse.IsSuccess() then FeatureTelemetry.LogError('0000MTB', CopilotCapabilityImpl.GetAzureOpenAICategory(), StrSubstNo(TelemetryFunctionCallingFailedErr, AOAIFunctionResponse.GetFunctionName()), AOAIFunctionResponse.GetError(), AOAIFunctionResponse.GetErrorCallstack(), CustomDimensions); @@ -381,7 +381,7 @@ codeunit 7772 "Azure OpenAI Impl" end; end; - local procedure ProcessFunctionCall(Functions: JsonArray; var ChatMessages: Codeunit "AOAI Chat Messages"; var AOAIOperationResponse: Codeunit "AOAI Operation Response"): Boolean + local procedure ProcessFunctionCall(Functions: JsonArray; var ChatMessages: Codeunit "AOAI Chat Messages"; var AOAIFunctionResponse: Codeunit "AOAI Function Response"): Boolean var Function: JsonObject; Arguments: JsonObject; @@ -418,11 +418,11 @@ codeunit 7772 "Azure OpenAI Impl" if ChatMessages.GetFunctionTool(FunctionName, AOAIFunction) then if TryExecuteFunction(AOAIFunction, Arguments, FunctionResult) then - AOAIOperationResponse.SetFunctionCallingResponse(true, AOAIFunction.GetName(), FunctionResult) + AOAIFunctionResponse.SetFunctionCallingResponse(true, true, AOAIFunction.GetName(), FunctionResult, '', '') else - AOAIOperationResponse.SetFunctionCallingResponse(false, AOAIFunction.GetName(), GetLastErrorText(), GetLastErrorCallStack()) + AOAIFunctionResponse.SetFunctionCallingResponse(true, false, AOAIFunction.GetName(), FunctionResult, GetLastErrorText(), GetLastErrorCallStack()) else - AOAIOperationResponse.SetFunctionCallingResponse(false, FunctionName, StrSubstNo(FunctionCallingFunctionNotFoundErr, FunctionName), ''); + AOAIFunctionResponse.SetFunctionCallingResponse(true, false, FunctionName, FunctionResult, StrSubstNo(FunctionCallingFunctionNotFoundErr, FunctionName), ''); end; [TryFunction] @@ -438,6 +438,7 @@ codeunit 7772 "Azure OpenAI Impl" ALCopilotAuthorization: DotNet ALCopilotAuthorization; ALCopilotFunctions: DotNet ALCopilotFunctions; ALCopilotOperationResponse: DotNet ALCopilotOperationResponse; + Error: Text; begin ClearLastError(); ALCopilotAuthorization := ALCopilotAuthorization.Create(AOAIAuthorization.GetEndpoint(), AOAIAuthorization.GetDeployment(), AOAIAuthorization.GetApiKey()); @@ -453,7 +454,10 @@ codeunit 7772 "Azure OpenAI Impl" Error(InvalidModelTypeErr) end; - AOAIOperationResponse.SetOperationResponse(ALCopilotOperationResponse); + Error := ALCopilotOperationResponse.ErrorText(); + if Error = '' then + Error := GetLastErrorText(); + AOAIOperationResponse.SetOperationResponse(ALCopilotOperationResponse.IsSuccess(), ALCopilotOperationResponse.StatusCode(), ALCopilotOperationResponse.Result(), Error); if not ALCopilotOperationResponse.IsSuccess() then Error(GenerateRequestFailedErr); diff --git a/src/System Application/App/AI/src/Azure OpenAI/Operation Response/AOAIFunctionResponse.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/Operation Response/AOAIFunctionResponse.Codeunit.al index 25b016a69a..371db456fb 100644 --- a/src/System Application/App/AI/src/Azure OpenAI/Operation Response/AOAIFunctionResponse.Codeunit.al +++ b/src/System Application/App/AI/src/Azure OpenAI/Operation Response/AOAIFunctionResponse.Codeunit.al @@ -66,22 +66,14 @@ codeunit 7758 "AOAI Function Response" exit(ErrorCallStack); end; - /// - /// Get whether the operation was a function call. - /// - /// True if it was a function call, false otherwise. internal procedure IsFunctionCall(): Boolean begin exit(FunctionCall); end; - internal procedure SetIsFunctionCall(NewIsFunctionCall: Boolean) + internal procedure SetFunctionCallingResponse(NewIsFunctionCall: Boolean; NewFunctionCallSuccess: Boolean; NewFunctionCalled: Text; NewFunctionResult: Variant; NewFunctionError: Text; NewFunctionErrorCallStack: Text) begin FunctionCall := NewIsFunctionCall; - end; - - internal procedure SetFunctionCallingResponse(NewFunctionCallSuccess: Boolean; NewFunctionCalled: Text; NewFunctionResult: Variant; NewFunctionError: Text; NewFunctionErrorCallStack: Text) - begin Success := NewFunctionCallSuccess; FunctionName := NewFunctionCalled; Result := NewFunctionResult; diff --git a/src/System Application/App/AI/src/Azure OpenAI/Operation Response/AOAIOperationResponse.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/Operation Response/AOAIOperationResponse.Codeunit.al index 03eac88a20..2eaa595040 100644 --- a/src/System Application/App/AI/src/Azure OpenAI/Operation Response/AOAIOperationResponse.Codeunit.al +++ b/src/System Application/App/AI/src/Azure OpenAI/Operation Response/AOAIOperationResponse.Codeunit.al @@ -4,8 +4,6 @@ // ------------------------------------------------------------------------------------------------ namespace System.AI; -using System; - /// /// The status and result of an operation. /// @@ -76,31 +74,11 @@ codeunit 7770 "AOAI Operation Response" exit(AOAIFunctionResponse); end; - internal procedure SetOperationResponse(var ALCopilotOperationResponse: DotNet ALCopilotOperationResponse) - begin - Success := ALCopilotOperationResponse.IsSuccess(); - StatusCode := ALCopilotOperationResponse.StatusCode; - Result := ALCopilotOperationResponse.Result(); - Error := ALCopilotOperationResponse.ErrorText(); - - if Error = '' then - Error := GetLastErrorText(); - end; - - internal procedure SetFunctionCallingResponse(NewFunctionCallSuccess: Boolean; NewFunctionCalled: Text; NewFunctionError: Text; NewFunctionErrorCallStack: Text) - var - EmptyVariant: Variant; - begin - SetFunctionCallingResponse(NewFunctionCallSuccess, NewFunctionCalled, EmptyVariant, NewFunctionError, NewFunctionErrorCallStack); - end; - - internal procedure SetFunctionCallingResponse(NewFunctionCallSuccess: Boolean; NewFunctionCalled: Text; NewFunctionResult: Variant) - begin - SetFunctionCallingResponse(NewFunctionCallSuccess, NewFunctionCalled, NewFunctionResult, '', ''); - end; - - local procedure SetFunctionCallingResponse(NewFunctionCallSuccess: Boolean; NewFunctionCalled: Text; NewFunctionResult: Variant; NewFunctionError: Text; NewFunctionErrorCallStack: Text) + internal procedure SetOperationResponse(NewSuccess: Boolean; NewStatusCode: Integer; NewResult: Text; NewError: Text) begin - AOAIFunctionResponse.SetFunctionCallingResponse(NewFunctionCallSuccess, NewFunctionCalled, NewFunctionResult, NewFunctionError, NewFunctionErrorCallStack); + Success := NewSuccess; + StatusCode := NewStatusCode; + Result := NewResult; + Error := NewError; end; } \ No newline at end of file From 1b572fceadaf4ec699230558ea63c7f568d8cfef Mon Sep 17 00:00:00 2001 From: Sun Haoran Date: Wed, 17 Apr 2024 08:32:02 +0200 Subject: [PATCH 3/6] Fix return value for ProcessFunctionCall (#945) #### Summary Fix return value for procedure `ProcessFunctionCall`. Also explicit return value false #### Work Item(s) Fixes [AB#525473](https://dynamicssmb2.visualstudio.com/1fcb79e7-ab07-432a-a3c6-6cf5a88ba4a5/_workitems/edit/525473) --- .../Azure OpenAI/AzureOpenAIImpl.Codeunit.al | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al index d0ffdc35c1..82ad5dc208 100644 --- a/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al +++ b/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al @@ -391,38 +391,43 @@ codeunit 7772 "Azure OpenAI Impl" FunctionResult: Variant; begin if Functions.Count = 0 then - exit; + exit(false); Functions.Get(0, Token); Function := Token.AsObject(); if Function.Get('type', Token) then begin if Token.AsValue().AsText() <> 'function' then - exit; + exit(false); end else - exit; + exit(false); if Function.Get('function', Token) then Function := Token.AsObject() else - exit; + exit(false); if Function.Get('name', Token) then FunctionName := Token.AsValue().AsText() else - exit; + exit(false); if Function.Get('arguments', Token) then // Arguments are stored as a string in the JSON Arguments.ReadFrom(Token.AsValue().AsText()); if ChatMessages.GetFunctionTool(FunctionName, AOAIFunction) then - if TryExecuteFunction(AOAIFunction, Arguments, FunctionResult) then - AOAIFunctionResponse.SetFunctionCallingResponse(true, true, AOAIFunction.GetName(), FunctionResult, '', '') - else - AOAIFunctionResponse.SetFunctionCallingResponse(true, false, AOAIFunction.GetName(), FunctionResult, GetLastErrorText(), GetLastErrorCallStack()) - else + if TryExecuteFunction(AOAIFunction, Arguments, FunctionResult) then begin + AOAIFunctionResponse.SetFunctionCallingResponse(true, true, AOAIFunction.GetName(), FunctionResult, '', ''); + exit(true); + end else begin + AOAIFunctionResponse.SetFunctionCallingResponse(true, false, AOAIFunction.GetName(), FunctionResult, GetLastErrorText(), GetLastErrorCallStack()); + exit(true); + end + else begin AOAIFunctionResponse.SetFunctionCallingResponse(true, false, FunctionName, FunctionResult, StrSubstNo(FunctionCallingFunctionNotFoundErr, FunctionName), ''); + exit(true); + end; end; [TryFunction] From dd3e5b1014696c90d848c1b7cc263aaa813bd868 Mon Sep 17 00:00:00 2001 From: Dmitry Katson Date: Wed, 1 May 2024 09:05:30 +0000 Subject: [PATCH 4/6] Backport 719 --- .../Azure OpenAI/AzureOpenAIImpl.Codeunit.al | 42 ++- .../AOAIChatComplParamsImpl.Codeunit.al | 26 ++ .../AOAIChatCompletionParams.Codeunit.al | 12 + .../AOAIChatMessages.Codeunit.al | 21 ++ .../AOAIChatMessagesImpl.Codeunit.al | 56 +++- .../Chat Completion/AOAIChatRoles.Enum.al | 8 + .../Chat Completion/AOAIToolsImpl.Codeunit.al | 26 ++ .../AOAIFunctionResponse.Codeunit.al | 12 +- .../AI/src/AzureOpenAIToolsTest.Codeunit.al | 256 ++++++++++++++++++ 9 files changed, 453 insertions(+), 6 deletions(-) diff --git a/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al index 82ad5dc208..c715e1a144 100644 --- a/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al +++ b/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al @@ -41,6 +41,7 @@ codeunit 7772 "Azure OpenAI Impl" CopilotDisabledForTenantErr: Label 'Copilot is not enabled for the tenant. Please contact your system administrator.'; CapabilityNotRegisteredErr: Label 'Copilot capability ''%1'' has not been registered by the module.', Comment = '%1 is the name of the Copilot Capability'; CapabilityNotEnabledErr: Label 'Copilot capability ''%1'' has not been enabled. Please contact your system administrator.', Comment = '%1 is the name of the Copilot Capability'; + MessagesMustContainJsonWordWhenResponseFormatIsJsonErr: Label 'The messages must contain the word ''json'' in some form, to use ''response format'' of type ''json_object''.'; EmptyMetapromptErr: Label 'The metaprompt has not been set, please provide a metaprompt.'; MetapromptLoadingErr: Label 'Metaprompt not found.'; EnabledKeyTok: Label 'AOAI-Enabled', Locked = true; @@ -336,6 +337,8 @@ codeunit 7772 "Azure OpenAI Impl" FeatureTelemetry.LogUsage('0000MFG', CopilotCapabilityImpl.GetAzureOpenAICategory(), TelemetryChatCompletionToolUsedLbl, CustomDimensions); end; + CheckJsonModeCompatibility(Payload); + Payload.WriteTo(PayloadText); SendTokenCountTelemetry(MetapromptTokenCount, PromptTokenCount, CustomDimensions); @@ -349,6 +352,31 @@ codeunit 7772 "Azure OpenAI Impl" FeatureTelemetry.LogUsage('0000KVN', CopilotCapabilityImpl.GetAzureOpenAICategory(), TelemetryGenerateChatCompletionLbl, CustomDimensions); end; + local procedure CheckJsonModeCompatibility(Payload: JsonObject) + var + ResponseFormatToken: JsonToken; + MessagesToken: JsonToken; + Messages: Text; + TypeToken: JsonToken; + XPathLbl: Label '$.type', Locked = true; + begin + if not Payload.Get('response_format', ResponseFormatToken) then + exit; + + if not Payload.Get('messages', MessagesToken) then + exit; + + if not ResponseFormatToken.SelectToken(XPathLbl, TypeToken) then + exit; + + if TypeToken.AsValue().AsText() <> 'json_object' then + exit; + + MessagesToken.WriteTo(Messages); + if not LowerCase(Messages).Contains('json') then + Error(MessagesMustContainJsonWordWhenResponseFormatIsJsonErr); + end; + [NonDebuggable] [TryFunction] local procedure ProcessChatCompletionResponse(var ChatMessages: Codeunit "AOAI Chat Messages"; var AOAIOperationResponse: Codeunit "AOAI Operation Response"; CallerModuleInfo: ModuleInfo) @@ -371,7 +399,7 @@ codeunit 7772 "Azure OpenAI Impl" AOAIFunctionResponse := AOAIOperationResponse.GetFunctionResponse(); if not ProcessFunctionCall(CompletionToken.AsArray(), ChatMessages, AOAIFunctionResponse) then - AOAIFunctionResponse.SetFunctionCallingResponse(true, false, '', '', '', ''); + AOAIFunctionResponse.SetFunctionCallingResponse(true, false, '', '', '', '', ''); AddTelemetryCustomDimensions(CustomDimensions, CallerModuleInfo); if not AOAIFunctionResponse.IsSuccess() then @@ -387,6 +415,7 @@ codeunit 7772 "Azure OpenAI Impl" Arguments: JsonObject; Token: JsonToken; FunctionName: Text; + FunctionId: Text; AOAIFunction: Interface "AOAI Function"; FunctionResult: Variant; begin @@ -402,6 +431,11 @@ codeunit 7772 "Azure OpenAI Impl" end else exit(false); + if Function.Get('id', Token) then + FunctionId := Token.AsValue().AsText() + else + exit(false); + if Function.Get('function', Token) then Function := Token.AsObject() else @@ -418,14 +452,14 @@ codeunit 7772 "Azure OpenAI Impl" if ChatMessages.GetFunctionTool(FunctionName, AOAIFunction) then if TryExecuteFunction(AOAIFunction, Arguments, FunctionResult) then begin - AOAIFunctionResponse.SetFunctionCallingResponse(true, true, AOAIFunction.GetName(), FunctionResult, '', ''); + AOAIFunctionResponse.SetFunctionCallingResponse(true, true, AOAIFunction.GetName(), FunctionId, FunctionResult, '', ''); exit(true); end else begin - AOAIFunctionResponse.SetFunctionCallingResponse(true, false, AOAIFunction.GetName(), FunctionResult, GetLastErrorText(), GetLastErrorCallStack()); + AOAIFunctionResponse.SetFunctionCallingResponse(true, false, AOAIFunction.GetName(), FunctionId, FunctionResult, GetLastErrorText(), GetLastErrorCallStack()); exit(true); end else begin - AOAIFunctionResponse.SetFunctionCallingResponse(true, false, FunctionName, FunctionResult, StrSubstNo(FunctionCallingFunctionNotFoundErr, FunctionName), ''); + AOAIFunctionResponse.SetFunctionCallingResponse(true, false, FunctionName, FunctionId, FunctionResult, StrSubstNo(FunctionCallingFunctionNotFoundErr, FunctionName), ''); exit(true); end; end; diff --git a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatComplParamsImpl.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatComplParamsImpl.Codeunit.al index 943a128c62..6e9dea192b 100644 --- a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatComplParamsImpl.Codeunit.al +++ b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatComplParamsImpl.Codeunit.al @@ -14,6 +14,7 @@ codeunit 7762 "AOAI Chat Compl Params Impl" Initialized: Boolean; Temperature: Decimal; MaxTokens: Integer; + JsonMode: Boolean; MaxHistory: Integer; PresencePenalty: Decimal; FrequencyPenalty: Decimal; @@ -38,6 +39,14 @@ codeunit 7762 "AOAI Chat Compl Params Impl" exit(MaxTokens); end; + procedure IsJsonMode(): Boolean + begin + if not Initialized then + InitializeDefaults(); + + exit(JsonMode); + end; + procedure GetMaxHistory(): Integer begin if not Initialized then @@ -81,6 +90,14 @@ codeunit 7762 "AOAI Chat Compl Params Impl" MaxTokens := NewMaxTokens; end; + procedure SetJsonMode(NewJsonMode: Boolean) + begin + if not Initialized then + InitializeDefaults(); + + JsonMode := NewJsonMode; + end; + procedure SetMaxHistory(NewMaxHistory: Integer) begin if not Initialized then @@ -121,6 +138,14 @@ codeunit 7762 "AOAI Chat Compl Params Impl" Payload.Add('temperature', GetTemperature()); Payload.Add('presence_penalty', GetPresencePenalty()); Payload.Add('frequency_penalty', GetFrequencyPenalty()); + + if IsJsonMode() then + Payload.Add('response_format', GetJsonResponseFormat()); + end; + + local procedure GetJsonResponseFormat() ResponseFormat: JsonObject + begin + ResponseFormat.Add('type', 'json_object'); end; local procedure InitializeDefaults() @@ -132,5 +157,6 @@ codeunit 7762 "AOAI Chat Compl Params Impl" SetFrequencyPenalty(0); SetMaxTokens(0); SetMaxHistory(10); + SetJsonMode(false); end; } \ No newline at end of file diff --git a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatCompletionParams.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatCompletionParams.Codeunit.al index f2e05daf9e..7aa9b7ca05 100644 --- a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatCompletionParams.Codeunit.al +++ b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatCompletionParams.Codeunit.al @@ -84,6 +84,18 @@ codeunit 7761 "AOAI Chat Completion Params" AOAIChatComplParamsImpl.SetMaxTokens(NewMaxTokens); end; + /// + /// Sets if the model should return a valid JSON object as a chat completion. + /// + /// The new Json mode for the chat completion: true or false. + /// Default is false. + /// When true, the model will return a valid JSON object as a chat completion. Including guidance to the model that it should produce JSON as part of the messages conversation is required + /// When true, the word 'json' must be included in at least one message. + procedure SetJsonMode(NewJsonMode: Boolean) + begin + AOAIChatComplParamsImpl.SetJsonMode(NewJsonMode); + end; + /// /// Sets the maximum number of messages to send back as the message history. /// diff --git a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatMessages.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatMessages.Codeunit.al index 57d25ff594..6fb58731ae 100644 --- a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatMessages.Codeunit.al +++ b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatMessages.Codeunit.al @@ -69,6 +69,18 @@ codeunit 7763 "AOAI Chat Messages" AOAIChatMessagesImpl.AddAssistantMessage(NewMessage); end; + /// + /// Adds a tool result to the chat messages history. + /// + /// The id of the tool call. + /// The name of the called function. + /// The result of the tool call. + [NonDebuggable] + procedure AddToolMessage(ToolCallId: Text; FunctionName: Text; FunctionResult: Text) + begin + AOAIChatMessagesImpl.AddToolMessage(ToolCallId, FunctionName, FunctionResult); + end; + /// /// Modifies a message in the chat messages history. /// @@ -178,6 +190,15 @@ codeunit 7763 "AOAI Chat Messages" exit(AOAIChatMessagesImpl.PrepareHistory(SystemMessageTokenCount, MessagesTokenCount)); end; + /// + /// Gets the number of tokens used by the primary system messages and all other messages. + /// + [NonDebuggable] + procedure GetHistoryTokenCount(): Integer + begin + exit(AOAIChatMessagesImpl.GetHistoryTokenCount()); + end; + #if not CLEAN25 /// /// Appends a Tool to the payload. diff --git a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatMessagesImpl.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatMessagesImpl.Codeunit.al index bf32c29b28..e51fd26117 100644 --- a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatMessagesImpl.Codeunit.al +++ b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatMessagesImpl.Codeunit.al @@ -27,6 +27,8 @@ codeunit 7764 "AOAI Chat Messages Impl" HistoryRoles: List of [Enum "AOAI Chat Roles"]; [NonDebuggable] HistoryNames: List of [Text[2048]]; + [NonDebuggable] + HistoryToolCallIds: List of [Text]; IsSystemMessageSet: Boolean; MessageIdDoesNotExistErr: Label 'Message id does not exist.'; HistoryLengthErr: Label 'History length must be greater than 0.'; @@ -72,6 +74,17 @@ codeunit 7764 "AOAI Chat Messages Impl" AddMessage(NewMessage, '', Enum::"AOAI Chat Roles"::Assistant); end; + [NonDebuggable] + procedure AddToolMessage(ToolCallId: Text; FunctionName: Text; FunctionResult: Text) + var + FunctionNameTruncated: Text[2048]; + begin + Initialize(); + FunctionNameTruncated := CopyStr(FunctionName, 1, MaxStrLen(FunctionNameTruncated)); + AddMessage(FunctionResult, FunctionNameTruncated, ToolCallId, Enum::"AOAI Chat Roles"::Tool); + end; + + [NonDebuggable] procedure ModifyMessage(Id: Integer; NewMessage: Text; NewRole: Enum "AOAI Chat Roles"; NewName: Text[2048]) begin @@ -92,6 +105,7 @@ codeunit 7764 "AOAI Chat Messages Impl" History.RemoveAt(Id); HistoryRoles.RemoveAt(Id); HistoryNames.RemoveAt(Id); + HistoryToolCallIds.RemoveAt(Id); end; [NonDebuggable] @@ -112,6 +126,12 @@ codeunit 7764 "AOAI Chat Messages Impl" exit(HistoryRoles); end; + [NonDebuggable] + procedure GetHistoryToolCallIds(): List of [Text] + begin + exit(HistoryToolCallIds); + end; + [NonDebuggable] procedure GetLastMessage() LastMessage: Text begin @@ -130,6 +150,12 @@ codeunit 7764 "AOAI Chat Messages Impl" HistoryNames.Get(HistoryNames.Count, LastName); end; + [NonDebuggable] + procedure GetLastToolCallId() LastToolCall: Text + begin + HistoryToolCallIds.Get(HistoryToolCallIds.Count, LastToolCall); + end; + [NonDebuggable] procedure SetHistoryLength(NewHistoryLength: Integer) begin @@ -139,16 +165,28 @@ codeunit 7764 "AOAI Chat Messages Impl" HistoryLength := NewHistoryLength; end; + [NonDebuggable] + procedure GetHistoryTokenCount(): Integer + var + SystemMessageTokenCount: Integer; + MessagesTokenCount: Integer; + begin + PrepareHistory(SystemMessageTokenCount, MessagesTokenCount); + exit(SystemMessageTokenCount + MessagesTokenCount); + end; + [NonDebuggable] procedure PrepareHistory(var SystemMessageTokenCount: Integer; var MessagesTokenCount: Integer) HistoryResult: JsonArray var AzureOpenAIImpl: Codeunit "Azure OpenAI Impl"; + AOAIToolsImpl: Codeunit "AOAI Tools Impl"; Counter: Integer; MessageJsonObject: JsonObject; Message: Text; TotalMessages: Text; Name: Text[2048]; Role: Enum "AOAI Chat Roles"; + ToolCallId: Text; UsingMicrosoftMetaprompt: Boolean; begin if History.Count = 0 then @@ -174,15 +212,21 @@ codeunit 7764 "AOAI Chat Messages Impl" HistoryRoles.Get(Counter, Role); History.Get(Counter, Message); HistoryNames.Get(Counter, Name); + HistoryToolCallIds.Get(Counter, ToolCallId); MessageJsonObject.Add('role', Format(Role)); if UsingMicrosoftMetaprompt and (Role = Enum::"AOAI Chat Roles"::User) then Message := WrapUserMessages(AzureOpenAIImpl.RemoveProhibitedCharacters(Message)) else Message := AzureOpenAIImpl.RemoveProhibitedCharacters(Message); - MessageJsonObject.Add('content', Message); + if AOAIToolsImpl.IsToolsList(Message) then + MessageJsonObject.Add('tool_calls', AOAIToolsImpl.ConvertToJsonArray(Message)) + else + MessageJsonObject.Add('content', Message); if Name <> '' then MessageJsonObject.Add('name', Name); + if ToolCallId <> '' then + MessageJsonObject.Add('tool_call_id', ToolCallId); HistoryResult.Add(MessageJsonObject); Counter += 1; TotalMessages += Format(Role); @@ -209,6 +253,16 @@ codeunit 7764 "AOAI Chat Messages Impl" History.Add(NewMessage); HistoryRoles.Add(NewRole); HistoryNames.Add(NewName); + HistoryToolCallIds.Add(''); + end; + + [NonDebuggable] + local procedure AddMessage(NewMessage: Text; NewName: Text[2048]; NewToolCallId: Text; NewRole: Enum "AOAI Chat Roles") + begin + History.Add(NewMessage); + HistoryRoles.Add(NewRole); + HistoryNames.Add(NewName); + HistoryToolCallIds.Add(NewToolCallId); end; [NonDebuggable] diff --git a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatRoles.Enum.al b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatRoles.Enum.al index 67ac89261f..bd6a1f1ee2 100644 --- a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatRoles.Enum.al +++ b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIChatRoles.Enum.al @@ -35,4 +35,12 @@ enum 7772 "AOAI Chat Roles" { Caption = 'assistant', Locked = true; } + + /// + /// Tool chat role messages provides the results of tool calling to the model. + /// + value(3; Tool) + { + Caption = 'tool', Locked = true; + } } \ No newline at end of file diff --git a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIToolsImpl.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIToolsImpl.Codeunit.al index cac244cf3e..cb56511ab4 100644 --- a/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIToolsImpl.Codeunit.al +++ b/src/System Application/App/AI/src/Azure OpenAI/Chat Completion/AOAIToolsImpl.Codeunit.al @@ -202,6 +202,32 @@ codeunit 7778 "AOAI Tools Impl" exit(ToolChoice); end; + [TryFunction] + [NonDebuggable] + procedure IsToolsList(Message: Text) + var + MessageJArray: JsonArray; + ToolToken: JsonToken; + TypeToken: JsonToken; + XPathLbl: Label '$.type', Comment = 'For more details on response, see https://aka.ms/AAlrz36', Locked = true; + i: Integer; + begin + MessageJArray := ConvertToJsonArray(Message); + + for i := 0 to MessageJArray.Count - 1 do begin + MessageJArray.Get(i, ToolToken); + ToolToken.SelectToken(XPathLbl, TypeToken); + if TypeToken.AsValue().AsText() <> 'function' then + Error(''); + end; + end; + + [NonDebuggable] + procedure ConvertToJsonArray(Message: Text) MessageJArray: JsonArray; + begin + MessageJArray.ReadFrom(Message); + end; + local procedure Initialize() begin if Initialized then diff --git a/src/System Application/App/AI/src/Azure OpenAI/Operation Response/AOAIFunctionResponse.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/Operation Response/AOAIFunctionResponse.Codeunit.al index 371db456fb..9282f50623 100644 --- a/src/System Application/App/AI/src/Azure OpenAI/Operation Response/AOAIFunctionResponse.Codeunit.al +++ b/src/System Application/App/AI/src/Azure OpenAI/Operation Response/AOAIFunctionResponse.Codeunit.al @@ -17,6 +17,7 @@ codeunit 7758 "AOAI Function Response" Success: Boolean; FunctionCall: Boolean; FunctionName: Text; + FunctionId: Text; Result: Variant; Error: Text; ErrorCallStack: Text; @@ -57,6 +58,14 @@ codeunit 7758 "AOAI Function Response" exit(FunctionName); end; + /// + /// Get the id of the function that was called. + /// + procedure GetFunctionId(): Text + begin + exit(FunctionId); + end; + /// /// Get the error call stack from the function that was called. /// @@ -71,11 +80,12 @@ codeunit 7758 "AOAI Function Response" exit(FunctionCall); end; - internal procedure SetFunctionCallingResponse(NewIsFunctionCall: Boolean; NewFunctionCallSuccess: Boolean; NewFunctionCalled: Text; NewFunctionResult: Variant; NewFunctionError: Text; NewFunctionErrorCallStack: Text) + internal procedure SetFunctionCallingResponse(NewIsFunctionCall: Boolean; NewFunctionCallSuccess: Boolean; NewFunctionCalled: Text; NewFunctionId: Text; NewFunctionResult: Variant; NewFunctionError: Text; NewFunctionErrorCallStack: Text) begin FunctionCall := NewIsFunctionCall; Success := NewFunctionCallSuccess; FunctionName := NewFunctionCalled; + FunctionId := NewFunctionId; Result := NewFunctionResult; Error := NewFunctionError; ErrorCallStack := NewFunctionErrorCallStack; diff --git a/src/System Application/Test/AI/src/AzureOpenAIToolsTest.Codeunit.al b/src/System Application/Test/AI/src/AzureOpenAIToolsTest.Codeunit.al index f0201fa142..2186299e15 100644 --- a/src/System Application/Test/AI/src/AzureOpenAIToolsTest.Codeunit.al +++ b/src/System Application/Test/AI/src/AzureOpenAIToolsTest.Codeunit.al @@ -303,6 +303,262 @@ codeunit 132686 "Azure OpenAI Tools Test" LibraryAssert.AreEqual(Format(Function2Tool), Format(Tool2), 'Tool should have same value.'); end; + [Test] + procedure TestJsonModeInParameters() + var + AOAIChatCompletionParams: Codeunit "AOAI Chat Completion Params"; + Payload: JsonObject; + ResponseFormatJTok: JsonToken; + TypeJTok: JsonToken; + begin + AOAIChatCompletionParams.SetJsonMode(true); + AOAIChatCompletionParams.AddChatCompletionsParametersToPayload(Payload); + + Payload.Get('response_format', ResponseFormatJtok); + ResponseFormatJTok.AsObject().Get('type', TypeJTok); + + LibraryAssert.AreEqual(TypeJTok.AsValue().AsText(), 'json_object', 'Response format should be json_object'); + end; + + [Test] + procedure TestNoJsonModeInParameters() + var + AOAIChatCompletionParams: Codeunit "AOAI Chat Completion Params"; + Payload: JsonObject; + begin + AOAIChatCompletionParams.SetJsonMode(false); + AOAIChatCompletionParams.AddChatCompletionsParametersToPayload(Payload); + + LibraryAssert.IsFalse(Payload.Contains('response_format'), 'Response format should not exist'); + end; + + [Test] + procedure TestNoJsonModeInParametersByDefault() + var + AOAIChatCompletionParams: Codeunit "AOAI Chat Completion Params"; + Payload: JsonObject; + begin + AOAIChatCompletionParams.AddChatCompletionsParametersToPayload(Payload); + + LibraryAssert.IsFalse(Payload.Contains('response_format'), 'Response format should not exist'); + end; + + [Test] + procedure TestToolRegistrationAndVerification() + var + AOAIChatMessages: Codeunit "AOAI Chat Messages"; + TestFunction1: Codeunit "Test Function 1"; + TestFunction2: Codeunit "Test Function 2"; + begin + AOAIChatMessages.AddTool(TestFunction1); + AOAIChatMessages.AddTool(TestFunction2); + + AOAIChatMessages.AddSystemMessage('test system message'); + AOAIChatMessages.AddUserMessage('test user message'); + + LibraryAssert.IsTrue(AOAIChatMessages.ToolsExists(), 'Tool should exist'); + end; + + [Test] + procedure TestToolSelection() + var + AOAIChatMessages: Codeunit "AOAI Chat Messages"; + TestFunction1: Codeunit "Test Function 1"; + TestFunction2: Codeunit "Test Function 2"; + ToolCallId: Text; + ToolSelectionResponseLbl: Label '[{"id":"%1","type":"function","function":{"name":"%2","arguments":"{}"}}]', Locked = true; + begin + AOAIChatMessages.AddTool(TestFunction1); + AOAIChatMessages.AddTool(TestFunction2); + + AOAIChatMessages.AddSystemMessage('test system message'); + AOAIChatMessages.AddUserMessage('test user message'); + + // Function is been selected by LLM + ToolCallId := 'call_of7GnOMuBT4H95XkuN14qfai'; + AOAIChatMessages.AddAssistantMessage(StrSubstNo(ToolSelectionResponseLbl, ToolCallId, TestFunction1.GetName())); + + LibraryAssert.AreEqual(AOAIChatMessages.GetLastMessage(), StrSubstNo(ToolSelectionResponseLbl, ToolCallId, TestFunction1.GetName()), 'Last message should be the tool selection response.'); + end; + + [Test] + procedure TestFunctionCallResult() + var + AOAIChatMessages: Codeunit "AOAI Chat Messages"; + AOAIFunctionResponse: Codeunit "AOAI Function Response"; + TestFunction1: Codeunit "Test Function 1"; + TestFunction2: Codeunit "Test Function 2"; + ToolCallId: Text; + ToolSelectionResponseLbl: Label '[{"id":"%1","type":"function","function":{"name":"%2","arguments":"{}"}}]', Locked = true; + FunctionExecutionResult: Text; + begin + AOAIChatMessages.AddTool(TestFunction1); + AOAIChatMessages.AddTool(TestFunction2); + + AOAIChatMessages.AddSystemMessage('test system message'); + AOAIChatMessages.AddUserMessage('test user message'); + + // Function is been selected by LLM + ToolCallId := 'call_of7GnOMuBT4H95XkuN14qfai'; + AOAIChatMessages.AddAssistantMessage(StrSubstNo(ToolSelectionResponseLbl, ToolCallId, TestFunction1.GetName())); + + // Selected function was executed by system + FunctionExecutionResult := 'test function execution result'; + AOAIFunctionResponse.SetFunctionCallingResponse(true, true, TestFunction1.GetName(), ToolCallId, FunctionExecutionResult, '', ''); + + LibraryAssert.IsTrue(AOAIFunctionResponse.IsFunctionCall(), 'Function call should be true.'); + LibraryAssert.AreEqual(AOAIFunctionResponse.GetFunctionName(), TestFunction1.GetName(), 'Function name should be the same as the value set.'); + LibraryAssert.AreEqual(AOAIFunctionResponse.GetFunctionId(), ToolCallId, 'Function id should be the same as the value set.'); + LibraryAssert.AreEqual(AOAIFunctionResponse.GetResult(), FunctionExecutionResult, 'Function response should be the same as the value set.'); + end; + + [Test] + procedure TestAddFunctionResultToChatMessages() + var + AOAIChatMessages: Codeunit "AOAI Chat Messages"; + AOAIFunctionResponse: Codeunit "AOAI Function Response"; + TestFunction1: Codeunit "Test Function 1"; + TestFunction2: Codeunit "Test Function 2"; + ToolCallId: Text; + ToolSelectionResponseLbl: Label '[{"id":"%1","type":"function","function":{"name":"%2","arguments":"{}"}}]', Locked = true; + FunctionExecutionResult: Text; + begin + AOAIChatMessages.AddTool(TestFunction1); + AOAIChatMessages.AddTool(TestFunction2); + + AOAIChatMessages.AddSystemMessage('test system message'); + AOAIChatMessages.AddUserMessage('test user message'); + + // Function is been selected by LLM + ToolCallId := 'call_of7GnOMuBT4H95XkuN14qfai'; + AOAIChatMessages.AddAssistantMessage(StrSubstNo(ToolSelectionResponseLbl, ToolCallId, TestFunction1.GetName())); + + // Selected function was executed by system + FunctionExecutionResult := 'test function execution result'; + AOAIFunctionResponse.SetFunctionCallingResponse(true, true, TestFunction1.GetName(), ToolCallId, FunctionExecutionResult, '', ''); + + // Save the function execution result to the chat messages + AOAIChatMessages.AddToolMessage(AOAIFunctionResponse.GetFunctionId(), AOAIFunctionResponse.GetFunctionName(), AOAIFunctionResponse.GetResult()); + + LibraryAssert.AreEqual(Enum::"AOAI Chat Roles"::Tool, AOAIChatMessages.GetLastRole(), 'The message should be a tool message'); + LibraryAssert.AreEqual(AOAIChatMessages.GetLastMessage(), FunctionExecutionResult, 'Last message should be the function execution result.'); + end; + + [Test] + procedure TestToolCleanup() + var + AOAIChatMessages: Codeunit "AOAI Chat Messages"; + TestFunction1: Codeunit "Test Function 1"; + TestFunction2: Codeunit "Test Function 2"; + AOAIFunctionResponse: Codeunit "AOAI Function Response"; + ToolCallId: Text; + ToolSelectionResponseLbl: Label '[{"id":"%1","type":"function","function":{"name":"%2","arguments":"{}"}}]', Locked = true; + FunctionExecutionResult: Text; + begin + AOAIChatMessages.AddTool(TestFunction1); + AOAIChatMessages.AddTool(TestFunction2); + + AOAIChatMessages.AddSystemMessage('test system message'); + AOAIChatMessages.AddUserMessage('test user message'); + + // Function is been selected by LLM + ToolCallId := 'call_of7GnOMuBT4H95XkuN14qfai'; + AOAIChatMessages.AddAssistantMessage(StrSubstNo(ToolSelectionResponseLbl, ToolCallId, TestFunction1.GetName())); + + // Selected function was executed by system + FunctionExecutionResult := 'test function execution result'; + AOAIFunctionResponse.SetFunctionCallingResponse(true, true, TestFunction1.GetName(), ToolCallId, FunctionExecutionResult, '', ''); + + // Save the function execution result to the chat messages + AOAIChatMessages.AddToolMessage(AOAIFunctionResponse.GetFunctionId(), AOAIFunctionResponse.GetFunctionName(), AOAIFunctionResponse.GetResult()); + + // Remove the functions from the tool list + AOAIChatMessages.ClearTools(); + + LibraryAssert.IsFalse(AOAIChatMessages.ToolsExists(), 'Tool should not exist'); + end; + + [Test] + procedure TestJsonRepresentationOfChatMessagesHistory() + var + AOAIChatMessages: Codeunit "AOAI Chat Messages"; + TestFunction1: Codeunit "Test Function 1"; + TestFunction2: Codeunit "Test Function 2"; + AOAIFunctionResponse: Codeunit "AOAI Function Response"; + AzureOpenAITestLibrary: Codeunit "Azure OpenAI Test Library"; + ToolCallId: Text; + ToolSelectionResponseLbl: Label '[{"id":"%1","type":"function","function":{"name":"%2","arguments":"{}"}}]', Locked = true; + FunctionExecutionResult: Text; + HistoryJsonArray: JsonArray; + MessageJsonTok: JsonToken; + JsonTok: JsonToken; + TextValue: Text; + begin + AOAIChatMessages.AddTool(TestFunction1); + AOAIChatMessages.AddTool(TestFunction2); + + AOAIChatMessages.AddSystemMessage('test system message'); + AOAIChatMessages.AddUserMessage('test user message'); + + // Function is been selected by LLM + ToolCallId := 'call_of7GnOMuBT4H95XkuN14qfai'; + AOAIChatMessages.AddAssistantMessage(StrSubstNo(ToolSelectionResponseLbl, ToolCallId, TestFunction1.GetName())); + + // Selected function was executed by system + FunctionExecutionResult := 'test function execution result'; + AOAIFunctionResponse.SetFunctionCallingResponse(true, true, TestFunction1.GetName(), ToolCallId, FunctionExecutionResult, '', ''); + + // Save the function execution result to the chat messages + AOAIChatMessages.AddToolMessage(AOAIFunctionResponse.GetFunctionId(), AOAIFunctionResponse.GetFunctionName(), AOAIFunctionResponse.GetResult()); + + // Remove the functions from the tool list + AOAIChatMessages.ClearTools(); + + // Check Json representation of the chat messages history + HistoryJsonArray := AzureOpenAITestLibrary.GetAOAIHistory(5, AOAIChatMessages); + LibraryAssert.AreEqual(4, HistoryJsonArray.Count, 'History should have 4 items.'); + + // Check system message + HistoryJsonArray.Get(0, MessageJsonTok); + MessageJsonTok.AsObject().Get('role', JsonTok); + LibraryAssert.AreEqual(JsonTok.AsValue().AsText(), 'system', 'Role should be system'); + + MessageJsonTok.AsObject().Get('content', JsonTok); + LibraryAssert.AreEqual(JsonTok.AsValue().AsText(), 'test system message', 'Content should be test system message'); + + // Check user message + HistoryJsonArray.Get(1, MessageJsonTok); + MessageJsonTok.AsObject().Get('role', JsonTok); + LibraryAssert.AreEqual(JsonTok.AsValue().AsText(), 'user', 'Role should be user'); + + MessageJsonTok.AsObject().Get('content', JsonTok); + LibraryAssert.AreEqual(JsonTok.AsValue().AsText(), 'test user message', 'Content should be test user message'); + + // Check assistant message + HistoryJsonArray.Get(2, MessageJsonTok); + MessageJsonTok.AsObject().Get('role', JsonTok); + LibraryAssert.AreEqual(JsonTok.AsValue().AsText(), 'assistant', 'Role should be assistant'); + + MessageJsonTok.AsObject().Get('tool_calls', JsonTok); + JsonTok.WriteTo(TextValue); + LibraryAssert.AreEqual(TextValue, StrSubstNo(ToolSelectionResponseLbl, ToolCallId, TestFunction1.GetName()), 'Tool call should be the same as the value set.'); + + // Check tool message + HistoryJsonArray.Get(3, MessageJsonTok); + MessageJsonTok.AsObject().Get('role', JsonTok); + LibraryAssert.AreEqual(JsonTok.AsValue().AsText(), 'tool', 'Role should be tool'); + + MessageJsonTok.AsObject().Get('content', JsonTok); + LibraryAssert.AreEqual(JsonTok.AsValue().AsText(), FunctionExecutionResult, 'Content should be the function execution result'); + + MessageJsonTok.AsObject().Get('name', JsonTok); + LibraryAssert.AreEqual(JsonTok.AsValue().AsText(), TestFunction1.GetName(), 'Function name should be the same as the value set.'); + + MessageJsonTok.AsObject().Get('tool_call_id', JsonTok); + LibraryAssert.AreEqual(JsonTok.AsValue().AsText(), ToolCallId, 'Tool call id should be the same as the value set.'); + end; + + local procedure GetTestFunction1Tool(): JsonObject var TestTool: Text; From 1944c1ed00f1a4ed11af9a91473937d4994d9a86 Mon Sep 17 00:00:00 2001 From: Darrick Joo Date: Thu, 2 May 2024 09:33:31 +0200 Subject: [PATCH 5/6] Fix tests and remove internalsvisibleto test and redirect to test library --- src/System Application/App/AI/app.json | 194 +++++++++--------- .../AI/src/AzureOpenAITestLibrary.Codeunit.al | 5 + .../AI/src/AzureOpenAIToolsTest.Codeunit.al | 9 +- 3 files changed, 108 insertions(+), 100 deletions(-) diff --git a/src/System Application/App/AI/app.json b/src/System Application/App/AI/app.json index 12ec270b33..5ded27ebfd 100644 --- a/src/System Application/App/AI/app.json +++ b/src/System Application/App/AI/app.json @@ -1,98 +1,98 @@ { - "id": "d3433b68-4901-445f-9547-fdfeca57575a", - "name": "AI SDK", - "publisher": "Microsoft", - "brief": "AI SDK to build AI-powered experiences", - "description": "AI SDK to build AI-powered experiences", - "version": "24.2.0.0", - "privacyStatement": "https://go.microsoft.com/fwlink/?linkid=724009", - "EULA": "https://go.microsoft.com/fwlink/?linkid=2009120", - "help": "https://go.microsoft.com/fwlink/?linkid=2103698", - "url": "https://go.microsoft.com/fwlink/?linkid=724011", - "logo": "", - "dependencies": [ - { - "id": "daa5d70e-eaf5-4256-bf80-53545ef7629a", - "name": "Privacy Notice", - "publisher": "Microsoft", - "version": "24.2.0.0" - }, - { - "id": "2673d810-273e-402f-9093-2eaef7e03b83", - "name": "Environment Information", - "publisher": "Microsoft", - "version": "24.2.0.0" - }, - { - "id": "de35f591-7216-4e60-8be1-1911d71a7fc2", - "name": "Telemetry", - "publisher": "Microsoft", - "version": "24.2.0.0" - }, - { - "id": "7e3b999e-1182-45d2-8b82-d5127ddba9b2", - "publisher": "Microsoft", - "name": "DotNet Aliases", - "version": "24.2.0.0" - }, - { - "id": "3a56c7d2-a594-4682-bd90-b10bfb177620", - "name": "Azure Key Vault", - "publisher": "Microsoft", - "version": "24.2.0.0" - }, - { - "id": "95025170-61fc-4808-9505-4ba1fe1d05d9", - "name": "Azure AD Tenant", - "publisher": "Microsoft", - "version": "24.2.0.0" - }, - { - "id": "5c36f279-480c-451b-b513-c1af8cfb0744", - "name": "Language", - "publisher": "Microsoft", - "version": "24.2.0.0" - }, - { - "id": "f2cc2ef8-949f-47d1-85b8-10bd6f8bc61c", - "name": "Azure AD User", - "publisher": "Microsoft", - "version": "24.2.0.0" - }, - { - "id": "c1d53fcd-ec4f-4ac5-ba49-af91a1dea38c", - "name": "Azure AD Plan", - "publisher": "Microsoft", - "version": "24.2.0.0" - }, - { - "id": "c56e3ef4-7ab0-4636-ae87-013a62f12213", - "name": "User Permissions", - "publisher": "Microsoft", - "version": "24.2.0.0" - }, - { - "id": "c64d75f0-e9f1-4d0f-9949-cd453b9b1466", - "name": "Guided Experience", - "publisher": "Microsoft", - "version": "24.2.0.0" - } - ], - "screenshots": [], - "internalsVisibleTo": [ - { - "id": "f2d92a20-33a7-4174-a82f-666e8e2ad69e", - "name": "AI Test Library", - "publisher": "Microsoft" - } - ], - "platform": "24.0.0.0", - "idRanges": [ - { - "from": 7758, - "to": 7778 - } - ], - "target": "OnPrem", - "contextSensitiveHelpUrl": "https://learn.microsoft.com/dynamics365/business-central/" -} + "id": "d3433b68-4901-445f-9547-fdfeca57575a", + "name": "AI SDK", + "publisher": "Microsoft", + "brief": "AI SDK to build AI-powered experiences", + "description": "AI SDK to build AI-powered experiences", + "version": "24.2.0.0", + "privacyStatement": "https://go.microsoft.com/fwlink/?linkid=724009", + "EULA": "https://go.microsoft.com/fwlink/?linkid=2009120", + "help": "https://go.microsoft.com/fwlink/?linkid=2103698", + "url": "https://go.microsoft.com/fwlink/?linkid=724011", + "logo": "", + "dependencies": [ + { + "id": "daa5d70e-eaf5-4256-bf80-53545ef7629a", + "name": "Privacy Notice", + "publisher": "Microsoft", + "version": "24.2.0.0" + }, + { + "id": "2673d810-273e-402f-9093-2eaef7e03b83", + "name": "Environment Information", + "publisher": "Microsoft", + "version": "24.2.0.0" + }, + { + "id": "de35f591-7216-4e60-8be1-1911d71a7fc2", + "name": "Telemetry", + "publisher": "Microsoft", + "version": "24.2.0.0" + }, + { + "id": "7e3b999e-1182-45d2-8b82-d5127ddba9b2", + "publisher": "Microsoft", + "name": "DotNet Aliases", + "version": "24.2.0.0" + }, + { + "id": "3a56c7d2-a594-4682-bd90-b10bfb177620", + "name": "Azure Key Vault", + "publisher": "Microsoft", + "version": "24.2.0.0" + }, + { + "id": "95025170-61fc-4808-9505-4ba1fe1d05d9", + "name": "Azure AD Tenant", + "publisher": "Microsoft", + "version": "24.2.0.0" + }, + { + "id": "5c36f279-480c-451b-b513-c1af8cfb0744", + "name": "Language", + "publisher": "Microsoft", + "version": "24.2.0.0" + }, + { + "id": "f2cc2ef8-949f-47d1-85b8-10bd6f8bc61c", + "name": "Azure AD User", + "publisher": "Microsoft", + "version": "24.2.0.0" + }, + { + "id": "c1d53fcd-ec4f-4ac5-ba49-af91a1dea38c", + "name": "Azure AD Plan", + "publisher": "Microsoft", + "version": "24.2.0.0" + }, + { + "id": "c56e3ef4-7ab0-4636-ae87-013a62f12213", + "name": "User Permissions", + "publisher": "Microsoft", + "version": "24.2.0.0" + }, + { + "id": "c64d75f0-e9f1-4d0f-9949-cd453b9b1466", + "name": "Guided Experience", + "publisher": "Microsoft", + "version": "24.2.0.0" + } + ], + "screenshots": [], + "internalsVisibleTo": [ + { + "id": "f2d92a20-33a7-4174-a82f-666e8e2ad69e", + "name": "AI Test Library", + "publisher": "Microsoft" + } + ], + "platform": "24.0.0.0", + "idRanges": [ + { + "from": 7758, + "to": 7778 + } + ], + "target": "OnPrem", + "contextSensitiveHelpUrl": "https://learn.microsoft.com/dynamics365/business-central/" +} \ No newline at end of file diff --git a/src/System Application/Test Library/AI/src/AzureOpenAITestLibrary.Codeunit.al b/src/System Application/Test Library/AI/src/AzureOpenAITestLibrary.Codeunit.al index 53f363049e..7a2204567a 100644 --- a/src/System Application/Test Library/AI/src/AzureOpenAITestLibrary.Codeunit.al +++ b/src/System Application/Test Library/AI/src/AzureOpenAITestLibrary.Codeunit.al @@ -23,4 +23,9 @@ codeunit 132933 "Azure OpenAI Test Library" exit(AOAIChatMessages.AssembleTools()); end; + procedure GetAOAIChatCompletionParametersPayload(AOAIChatCompletionParams: Codeunit "AOAI Chat Completion Params"; var Payload: JsonObject) + begin + AOAIChatCompletionParams.AddChatCompletionsParametersToPayload(Payload); + end; + } \ No newline at end of file diff --git a/src/System Application/Test/AI/src/AzureOpenAIToolsTest.Codeunit.al b/src/System Application/Test/AI/src/AzureOpenAIToolsTest.Codeunit.al index 2186299e15..b516d5975f 100644 --- a/src/System Application/Test/AI/src/AzureOpenAIToolsTest.Codeunit.al +++ b/src/System Application/Test/AI/src/AzureOpenAIToolsTest.Codeunit.al @@ -306,13 +306,14 @@ codeunit 132686 "Azure OpenAI Tools Test" [Test] procedure TestJsonModeInParameters() var + AzureOpenAITestLibrary: Codeunit "Azure OpenAI Test Library"; AOAIChatCompletionParams: Codeunit "AOAI Chat Completion Params"; Payload: JsonObject; ResponseFormatJTok: JsonToken; TypeJTok: JsonToken; begin AOAIChatCompletionParams.SetJsonMode(true); - AOAIChatCompletionParams.AddChatCompletionsParametersToPayload(Payload); + AzureOpenAITestLibrary.GetAOAIChatCompletionParametersPayload(AOAIChatCompletionParams, Payload); Payload.Get('response_format', ResponseFormatJtok); ResponseFormatJTok.AsObject().Get('type', TypeJTok); @@ -323,11 +324,12 @@ codeunit 132686 "Azure OpenAI Tools Test" [Test] procedure TestNoJsonModeInParameters() var + AzureOpenAITestLibrary: Codeunit "Azure OpenAI Test Library"; AOAIChatCompletionParams: Codeunit "AOAI Chat Completion Params"; Payload: JsonObject; begin AOAIChatCompletionParams.SetJsonMode(false); - AOAIChatCompletionParams.AddChatCompletionsParametersToPayload(Payload); + AzureOpenAITestLibrary.GetAOAIChatCompletionParametersPayload(AOAIChatCompletionParams, Payload); LibraryAssert.IsFalse(Payload.Contains('response_format'), 'Response format should not exist'); end; @@ -335,10 +337,11 @@ codeunit 132686 "Azure OpenAI Tools Test" [Test] procedure TestNoJsonModeInParametersByDefault() var + AzureOpenAITestLibrary: Codeunit "Azure OpenAI Test Library"; AOAIChatCompletionParams: Codeunit "AOAI Chat Completion Params"; Payload: JsonObject; begin - AOAIChatCompletionParams.AddChatCompletionsParametersToPayload(Payload); + AzureOpenAITestLibrary.GetAOAIChatCompletionParametersPayload(AOAIChatCompletionParams, Payload); LibraryAssert.IsFalse(Payload.Contains('response_format'), 'Response format should not exist'); end; From b9e1861be07b02f38fe3a52bbf62fa1e6ff891fc Mon Sep 17 00:00:00 2001 From: Darrick Date: Mon, 6 May 2024 09:27:52 +0200 Subject: [PATCH 6/6] Backport 0d9d9ce37c9ad2bd770a999874e6aa301ceb6b1e --- .../AI/src/AzureOpenAITestLibrary.Codeunit.al | 5 +++++ .../Test/AI/src/AzureOpenAIToolsTest.Codeunit.al | 15 ++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/System Application/Test Library/AI/src/AzureOpenAITestLibrary.Codeunit.al b/src/System Application/Test Library/AI/src/AzureOpenAITestLibrary.Codeunit.al index 7a2204567a..4e4563d735 100644 --- a/src/System Application/Test Library/AI/src/AzureOpenAITestLibrary.Codeunit.al +++ b/src/System Application/Test Library/AI/src/AzureOpenAITestLibrary.Codeunit.al @@ -28,4 +28,9 @@ codeunit 132933 "Azure OpenAI Test Library" AOAIChatCompletionParams.AddChatCompletionsParametersToPayload(Payload); end; + procedure SetAOAIFunctionResponse(var AOAIFunctionResponse: Codeunit "AOAI Function Response"; NewIsFunctionCall: Boolean; NewFunctionCallSuccess: Boolean; NewFunctionCalled: Text; NewFunctionId: Text; NewFunctionResult: Variant; NewFunctionError: Text; NewFunctionErrorCallStack: Text) + begin + AOAIFunctionResponse.SetFunctionCallingResponse(NewIsFunctionCall, NewFunctionCallSuccess, NewFunctionCalled, NewFunctionId, NewFunctionResult, NewFunctionError, NewFunctionErrorCallStack); + end; + } \ No newline at end of file diff --git a/src/System Application/Test/AI/src/AzureOpenAIToolsTest.Codeunit.al b/src/System Application/Test/AI/src/AzureOpenAIToolsTest.Codeunit.al index b516d5975f..876982832d 100644 --- a/src/System Application/Test/AI/src/AzureOpenAIToolsTest.Codeunit.al +++ b/src/System Application/Test/AI/src/AzureOpenAIToolsTest.Codeunit.al @@ -387,7 +387,9 @@ codeunit 132686 "Azure OpenAI Tools Test" [Test] procedure TestFunctionCallResult() var + AzureOpenAITestLibrary: Codeunit "Azure OpenAI Test Library"; AOAIChatMessages: Codeunit "AOAI Chat Messages"; + AOAIOperationResponse: Codeunit "AOAI Operation Response"; AOAIFunctionResponse: Codeunit "AOAI Function Response"; TestFunction1: Codeunit "Test Function 1"; TestFunction2: Codeunit "Test Function 2"; @@ -403,13 +405,14 @@ codeunit 132686 "Azure OpenAI Tools Test" // Function is been selected by LLM ToolCallId := 'call_of7GnOMuBT4H95XkuN14qfai'; + AOAIFunctionResponse := AOAIOperationResponse.GetFunctionResponse(); AOAIChatMessages.AddAssistantMessage(StrSubstNo(ToolSelectionResponseLbl, ToolCallId, TestFunction1.GetName())); // Selected function was executed by system FunctionExecutionResult := 'test function execution result'; - AOAIFunctionResponse.SetFunctionCallingResponse(true, true, TestFunction1.GetName(), ToolCallId, FunctionExecutionResult, '', ''); + AzureOpenAITestLibrary.SetAOAIFunctionResponse(AOAIFunctionResponse, true, true, TestFunction1.GetName(), ToolCallId, FunctionExecutionResult, '', ''); - LibraryAssert.IsTrue(AOAIFunctionResponse.IsFunctionCall(), 'Function call should be true.'); + LibraryAssert.IsTrue(AOAIOperationResponse.IsFunctionCall(), 'Function call should be true.'); LibraryAssert.AreEqual(AOAIFunctionResponse.GetFunctionName(), TestFunction1.GetName(), 'Function name should be the same as the value set.'); LibraryAssert.AreEqual(AOAIFunctionResponse.GetFunctionId(), ToolCallId, 'Function id should be the same as the value set.'); LibraryAssert.AreEqual(AOAIFunctionResponse.GetResult(), FunctionExecutionResult, 'Function response should be the same as the value set.'); @@ -418,6 +421,7 @@ codeunit 132686 "Azure OpenAI Tools Test" [Test] procedure TestAddFunctionResultToChatMessages() var + AzureOpenAITestLibrary: Codeunit "Azure OpenAI Test Library"; AOAIChatMessages: Codeunit "AOAI Chat Messages"; AOAIFunctionResponse: Codeunit "AOAI Function Response"; TestFunction1: Codeunit "Test Function 1"; @@ -438,7 +442,7 @@ codeunit 132686 "Azure OpenAI Tools Test" // Selected function was executed by system FunctionExecutionResult := 'test function execution result'; - AOAIFunctionResponse.SetFunctionCallingResponse(true, true, TestFunction1.GetName(), ToolCallId, FunctionExecutionResult, '', ''); + AzureOpenAITestLibrary.SetAOAIFunctionResponse(AOAIFunctionResponse, true, true, TestFunction1.GetName(), ToolCallId, FunctionExecutionResult, '', ''); // Save the function execution result to the chat messages AOAIChatMessages.AddToolMessage(AOAIFunctionResponse.GetFunctionId(), AOAIFunctionResponse.GetFunctionName(), AOAIFunctionResponse.GetResult()); @@ -450,6 +454,7 @@ codeunit 132686 "Azure OpenAI Tools Test" [Test] procedure TestToolCleanup() var + AzureOpenAITestLibrary: Codeunit "Azure OpenAI Test Library"; AOAIChatMessages: Codeunit "AOAI Chat Messages"; TestFunction1: Codeunit "Test Function 1"; TestFunction2: Codeunit "Test Function 2"; @@ -470,7 +475,7 @@ codeunit 132686 "Azure OpenAI Tools Test" // Selected function was executed by system FunctionExecutionResult := 'test function execution result'; - AOAIFunctionResponse.SetFunctionCallingResponse(true, true, TestFunction1.GetName(), ToolCallId, FunctionExecutionResult, '', ''); + AzureOpenAITestLibrary.SetAOAIFunctionResponse(AOAIFunctionResponse, true, true, TestFunction1.GetName(), ToolCallId, FunctionExecutionResult, '', ''); // Save the function execution result to the chat messages AOAIChatMessages.AddToolMessage(AOAIFunctionResponse.GetFunctionId(), AOAIFunctionResponse.GetFunctionName(), AOAIFunctionResponse.GetResult()); @@ -509,7 +514,7 @@ codeunit 132686 "Azure OpenAI Tools Test" // Selected function was executed by system FunctionExecutionResult := 'test function execution result'; - AOAIFunctionResponse.SetFunctionCallingResponse(true, true, TestFunction1.GetName(), ToolCallId, FunctionExecutionResult, '', ''); + AzureOpenAITestLibrary.SetAOAIFunctionResponse(AOAIFunctionResponse, true, true, TestFunction1.GetName(), ToolCallId, FunctionExecutionResult, '', ''); // Save the function execution result to the chat messages AOAIChatMessages.AddToolMessage(AOAIFunctionResponse.GetFunctionId(), AOAIFunctionResponse.GetFunctionName(), AOAIFunctionResponse.GetResult());