Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[releases/24.x][Copilot] Backport of update to function calling #1036

Merged
merged 7 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 97 additions & 97 deletions src/System Application/App/AI/app.json
Original file line number Diff line number Diff line change
@@ -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/"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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);
Expand All @@ -342,42 +347,137 @@ 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;
CompletionToken: JsonToken;
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());
if Response.SelectToken(XPathToolCallsLbl, CompletionToken) then begin
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")
var
ALCopilotAuthorization: DotNet ALCopilotAuthorization;
ALCopilotFunctions: DotNet ALCopilotFunctions;
ALCopilotOperationResponse: DotNet ALCopilotOperationResponse;
Error: Text;
begin
ClearLastError();
ALCopilotAuthorization := ALCopilotAuthorization.Create(AOAIAuthorization.GetEndpoint(), AOAIAuthorization.GetDeployment(), AOAIAuthorization.GetApiKey());
Expand All @@ -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);
Expand Down
Loading
Loading