diff --git a/src/System Application/App/AI/app.json b/src/System Application/App/AI/app.json
index 2eb4a70aca..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": 7759,
- "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/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al
index 4523fd9dcc..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,9 +41,11 @@ 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;
+ 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 +60,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;
@@ -334,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);
@@ -342,15 +347,41 @@ codeunit 7772 "Azure OpenAI Impl"
exit;
end;
- ProcessChatCompletionResponse(AOAIOperationResponse.GetResult(), ChatMessages, CallerModuleInfo);
+ ProcessChatCompletionResponse(ChatMessages, AOAIOperationResponse, CallerModuleInfo);
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(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 +389,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 +397,79 @@ codeunit 7772 "Azure OpenAI Impl"
CompletionToken.AsArray().WriteTo(ToolsCall);
ChatMessages.AddAssistantMessage(ToolsCall);
+ AOAIFunctionResponse := AOAIOperationResponse.GetFunctionResponse();
+ if not ProcessFunctionCall(CompletionToken.AsArray(), ChatMessages, AOAIFunctionResponse) then
+ AOAIFunctionResponse.SetFunctionCallingResponse(true, false, '', '', '', '', '');
+
AddTelemetryCustomDimensions(CustomDimensions, CallerModuleInfo);
+ 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 AOAIFunctionResponse: Codeunit "AOAI Function Response"): Boolean
+ var
+ Function: JsonObject;
+ Arguments: JsonObject;
+ Token: JsonToken;
+ FunctionName: Text;
+ FunctionId: Text;
+ AOAIFunction: Interface "AOAI Function";
+ FunctionResult: Variant;
+ begin
+ if Functions.Count = 0 then
+ exit(false);
+
+ Functions.Get(0, Token);
+ Function := Token.AsObject();
+
+ if Function.Get('type', Token) then begin
+ if Token.AsValue().AsText() <> 'function' then
+ exit(false);
+ 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
+ exit(false);
+
+ if Function.Get('name', Token) then
+ FunctionName := Token.AsValue().AsText()
+ else
+ 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 begin
+ AOAIFunctionResponse.SetFunctionCallingResponse(true, true, AOAIFunction.GetName(), FunctionId, FunctionResult, '', '');
+ exit(true);
+ end else begin
+ AOAIFunctionResponse.SetFunctionCallingResponse(true, false, AOAIFunction.GetName(), FunctionId, FunctionResult, GetLastErrorText(), GetLastErrorCallStack());
+ exit(true);
+ end
+ else begin
+ AOAIFunctionResponse.SetFunctionCallingResponse(true, false, FunctionName, FunctionId, FunctionResult, StrSubstNo(FunctionCallingFunctionNotFoundErr, FunctionName), '');
+ exit(true);
+ end;
+ 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")
@@ -378,6 +477,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());
@@ -393,7 +493,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/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 65484c610c..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,18 +190,28 @@ 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.
///
/// 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 +221,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 +234,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 +334,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/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 1f451ed6fc..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
@@ -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,12 +180,54 @@ 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
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/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..9282f50623
--- /dev/null
+++ b/src/System Application/App/AI/src/Azure OpenAI/Operation Response/AOAIFunctionResponse.Codeunit.al
@@ -0,0 +1,93 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+ FunctionId: 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 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.
+ ///
+ /// The error call stack from the function.
+ procedure GetErrorCallstack(): Text
+ begin
+ exit(ErrorCallStack);
+ end;
+
+ internal procedure IsFunctionCall(): Boolean
+ begin
+ exit(FunctionCall);
+ end;
+
+ 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;
+ 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 63%
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..2eaa595040 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
@@ -4,8 +4,6 @@
// ------------------------------------------------------------------------------------------------
namespace System.AI;
-using System;
-
///
/// The status and result of an operation.
///
@@ -16,6 +14,7 @@ codeunit 7770 "AOAI Operation Response"
InherentPermissions = X;
var
+ AOAIFunctionResponse: Codeunit "AOAI Function Response";
StatusCode: Integer;
Success: Boolean;
Result: Text;
@@ -57,14 +56,29 @@ codeunit 7770 "AOAI Operation Response"
exit(Error);
end;
- internal procedure SetOperationResponse(var ALCopilotOperationResponse: DotNet ALCopilotOperationResponse)
+ ///
+ /// 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
- Success := ALCopilotOperationResponse.IsSuccess();
- StatusCode := ALCopilotOperationResponse.StatusCode;
- Result := ALCopilotOperationResponse.Result();
- Error := ALCopilotOperationResponse.ErrorText();
+ exit(AOAIFunctionResponse);
+ end;
- if Error = '' then
- Error := GetLastErrorText();
+ internal procedure SetOperationResponse(NewSuccess: Boolean; NewStatusCode: Integer; NewResult: Text; NewError: Text)
+ begin
+ Success := NewSuccess;
+ StatusCode := NewStatusCode;
+ Result := NewResult;
+ Error := NewError;
end;
}
\ 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..4e4563d735 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,14 @@ 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;
+
+ 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/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..876982832d 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,7 +219,25 @@ 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();
@@ -91,6 +245,33 @@ codeunit 132686 "Azure OpenAI Tools Test"
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);
@@ -118,6 +303,270 @@ codeunit 132686 "Azure OpenAI Tools Test"
LibraryAssert.AreEqual(Format(Function2Tool), Format(Tool2), 'Tool should have same value.');
end;
+ [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);
+ AzureOpenAITestLibrary.GetAOAIChatCompletionParametersPayload(AOAIChatCompletionParams, 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
+ AzureOpenAITestLibrary: Codeunit "Azure OpenAI Test Library";
+ AOAIChatCompletionParams: Codeunit "AOAI Chat Completion Params";
+ Payload: JsonObject;
+ begin
+ AOAIChatCompletionParams.SetJsonMode(false);
+ AzureOpenAITestLibrary.GetAOAIChatCompletionParametersPayload(AOAIChatCompletionParams, Payload);
+
+ LibraryAssert.IsFalse(Payload.Contains('response_format'), 'Response format should not exist');
+ end;
+
+ [Test]
+ procedure TestNoJsonModeInParametersByDefault()
+ var
+ AzureOpenAITestLibrary: Codeunit "Azure OpenAI Test Library";
+ AOAIChatCompletionParams: Codeunit "AOAI Chat Completion Params";
+ Payload: JsonObject;
+ begin
+ AzureOpenAITestLibrary.GetAOAIChatCompletionParametersPayload(AOAIChatCompletionParams, 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
+ 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";
+ 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';
+ AOAIFunctionResponse := AOAIOperationResponse.GetFunctionResponse();
+ AOAIChatMessages.AddAssistantMessage(StrSubstNo(ToolSelectionResponseLbl, ToolCallId, TestFunction1.GetName()));
+
+ // Selected function was executed by system
+ FunctionExecutionResult := 'test function execution result';
+ AzureOpenAITestLibrary.SetAOAIFunctionResponse(AOAIFunctionResponse, true, true, TestFunction1.GetName(), ToolCallId, FunctionExecutionResult, '', '');
+
+ 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.');
+ end;
+
+ [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";
+ 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';
+ 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());
+
+ 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
+ AzureOpenAITestLibrary: Codeunit "Azure OpenAI Test Library";
+ 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';
+ 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());
+
+ // 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';
+ 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());
+
+ // 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;
@@ -137,6 +586,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