Skip to content

Commit

Permalink
.Net: Function calling: Add function author role and name property (m…
Browse files Browse the repository at this point in the history
…icrosoft#3421)

### Motivation and Context

Replaces microsoft#3302

Resolves microsoft#3017 and enables the full function calling scenario as
described in the [OpenAI function calling
documentation](https://platform.openai.com/docs/guides/gpt/function-calling).

### Description

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->
Adds the "function" author role to allow adding function messages to
chat history. Function messages require a "name" parameter, so this
change also adds that to chat message model.

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [x] The code builds clean without any errors or warnings
- [x] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [x] All unit tests pass, and I have added new tests where possible
- [x] I didn't break anyone 😄

---------

Co-authored-by: Gina Triolo <[email protected]>
  • Loading branch information
markwallace-microsoft and gitri-ms authored Nov 9, 2023
1 parent dd38e34 commit e83570d
Show file tree
Hide file tree
Showing 8 changed files with 348 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ public static async Task RunAsync()
// Set FunctionCall to the name of a specific function to force the model to use that function.
requestSettings.FunctionCall = "TimePlugin-Date";
await CompleteChatWithFunctionsAsync("What day is today?", chatHistory, chatCompletion, kernel, requestSettings);
await StreamingCompleteChatWithFunctionsAsync("What day is today?", chatHistory, chatCompletion, kernel, requestSettings);

// Uncomment the samples and run them one at a time
//await StreamingCompleteChatWithFunctionsAsync("What day is today?", chatHistory, chatCompletion, kernel, requestSettings);

// Set FunctionCall to auto to let the model choose the best function to use.
requestSettings.FunctionCall = OpenAIRequestSettings.FunctionCallAuto;
await CompleteChatWithFunctionsAsync("What computer tablets are available for under $200?", chatHistory, chatCompletion, kernel, requestSettings);
await StreamingCompleteChatWithFunctionsAsync("What computer tablets are available for under $200?", chatHistory, chatCompletion, kernel, requestSettings);
//await CompleteChatWithFunctionsAsync("What computer tablets are available for under $200?", chatHistory, chatCompletion, kernel, requestSettings);
//await StreamingCompleteChatWithFunctionsAsync("What computer tablets are available for under $200?", chatHistory, chatCompletion, kernel, requestSettings);
}

private static async Task<IKernel> InitializeKernelAsync()
Expand All @@ -66,56 +68,43 @@ private static async Task CompleteChatWithFunctionsAsync(string ask, ChatHistory
Console.WriteLine($"User message: {ask}");
chatHistory.AddUserMessage(ask);

// Send request
// Send request and add response to chat history
var chatResult = (await chatCompletion.GetChatCompletionsAsync(chatHistory, requestSettings))[0];
chatHistory.AddAssistantMessage(chatResult);

// Check for message response
var chatMessage = await chatResult.GetChatMessageAsync();
if (!string.IsNullOrEmpty(chatMessage.Content))
{
Console.WriteLine(chatMessage.Content);

// Add the response to chat history
chatHistory.AddAssistantMessage(chatMessage.Content);
}
await PrintChatResultAsync(chatResult);

// Check for function response
OpenAIFunctionResponse? functionResponse = chatResult.GetOpenAIFunctionResponse();
if (functionResponse is not null)
{
// Print function response details
Console.WriteLine("Function name: " + functionResponse.FunctionName);
Console.WriteLine("Plugin name: " + functionResponse.PluginName);
Console.WriteLine("Arguments: ");
foreach (var parameter in functionResponse.Parameters)
{
Console.WriteLine($"- {parameter.Key}: {parameter.Value}");
}

// If the function returned by OpenAI is an SKFunction registered with the kernel,
// you can invoke it using the following code.
if (kernel.Functions.TryGetFunctionAndContext(functionResponse, out ISKFunction? func, out ContextVariables? context))
{
var kernelResult = await kernel.RunAsync(func, context);
var result = (await kernel.RunAsync(func, context)).GetValue<object>();

var result = kernelResult.GetValue<object>();

string? resultMessage = null;
string? resultContent = null;
if (result is RestApiOperationResponse apiResponse)
{
resultMessage = apiResponse.Content?.ToString();
resultContent = apiResponse.Content?.ToString();
}
else if (result is string str)
{
resultMessage = str;
resultContent = str;
}

if (!string.IsNullOrEmpty(resultMessage))
if (!string.IsNullOrEmpty(resultContent))
{
Console.WriteLine(resultMessage);

// Add the function result to chat history
chatHistory.AddAssistantMessage(resultMessage);
chatHistory.AddFunctionMessage(resultContent, functionResponse.FullyQualifiedName);

// Get another completion
requestSettings.FunctionCall = OpenAIRequestSettings.FunctionCallNone;
chatResult = (await chatCompletion.GetChatCompletionsAsync(chatHistory, requestSettings))[0];
chatHistory.AddAssistantMessage(chatResult);

await PrintChatResultAsync(chatResult);
}
}
else
Expand All @@ -125,6 +114,30 @@ private static async Task CompleteChatWithFunctionsAsync(string ask, ChatHistory
}
}

private static async Task PrintChatResultAsync(IChatResult chatResult)
{
// Check for message response
var chatMessage = await chatResult.GetChatMessageAsync();
if (!string.IsNullOrEmpty(chatMessage.Content))
{
Console.WriteLine($"Assistant response: {chatMessage.Content}");
}

// Check for function response
OpenAIFunctionResponse? functionResponse = chatResult.GetOpenAIFunctionResponse();
if (functionResponse is not null)
{
// Print function response details
Console.WriteLine("Function name: " + functionResponse.FunctionName);
Console.WriteLine("Plugin name: " + functionResponse.PluginName);
Console.WriteLine("Arguments: ");
foreach (var parameter in functionResponse.Parameters)
{
Console.WriteLine($"- {parameter.Key}: {parameter.Value}");
}
}
}

private static async Task StreamingCompleteChatWithFunctionsAsync(string ask, ChatHistory chatHistory, IChatCompletion chatCompletion, IKernel kernel, OpenAIRequestSettings requestSettings)
{
Console.WriteLine($"User message: {ask}");
Expand Down
27 changes: 13 additions & 14 deletions dotnet/src/Connectors/Connectors.AI.OpenAI/AzureSdk/ClientBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ namespace Microsoft.SemanticKernel.Connectors.AI.OpenAI.AzureSdk;
public abstract class ClientBase
{
private const int MaxResultsPerPrompt = 128;
private const string NameProperty = "Name";
private const string ArgumentsProperty = "Arguments";

// Prevent external inheritors
private protected ClientBase(ILoggerFactory? loggerFactory = null)
Expand Down Expand Up @@ -407,25 +409,22 @@ private static ChatCompletionsOptions CreateChatCompletionsOptions(OpenAIRequest

foreach (var message in chatHistory)
{
var validRole = GetValidChatRole(message.Role);
options.Messages.Add(new ChatMessage(validRole, message.Content));
}
var azureMessage = new ChatMessage(new ChatRole(message.Role.Label), message.Content);

return options;
}
if (message.AdditionalProperties?.TryGetValue(NameProperty, out string? name) is true)
{
azureMessage.Name = name;

private static ChatRole GetValidChatRole(AuthorRole role)
{
var validRole = new ChatRole(role.Label);
if (message.AdditionalProperties?.TryGetValue(ArgumentsProperty, out string? arguments) is true)
{
azureMessage.FunctionCall = new FunctionCall(name, arguments);
}
}

if (validRole != ChatRole.User &&
validRole != ChatRole.System &&
validRole != ChatRole.Assistant)
{
throw new ArgumentException($"Invalid chat message author role: {role}");
options.Messages.Add(azureMessage);
}

return validRole;
return options;
}

private static void ValidateMaxTokens(int? maxTokens)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Text.Json;
using Azure.AI.OpenAI;
using Microsoft.SemanticKernel.Text;

namespace Microsoft.SemanticKernel.Connectors.AI.OpenAI.AzureSdk;

Expand All @@ -27,6 +28,14 @@ public class OpenAIFunctionResponse
/// </summary>
public Dictionary<string, object> Parameters { get; set; } = new();

/// <summary>
/// Fully qualified name of the function. This is the concatenation of the plugin name and the function name,
/// separated by the value of <see cref="OpenAIFunction.NameSeparator"/>.
/// If there is no plugin name, this is the same as the function name.
/// </summary>
public string FullyQualifiedName =>
this.PluginName.IsNullOrEmpty() ? this.FunctionName : $"{this.PluginName}{OpenAIFunction.NameSeparator}{this.FunctionName}";

/// <summary>
/// Parses the function call and parameter information generated by the model.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Microsoft. All rights reserved.

#pragma warning disable IDE0130
using System.Collections.Generic;
using Microsoft.SemanticKernel.Text;

namespace Microsoft.SemanticKernel.AI.ChatCompletion;
#pragma warning restore IDE0130

/// <summary>
/// OpenAI-specific extensions to the <see cref="ChatHistory"/> class.
/// </summary>
public static class ChatHistoryExtensions
{
/// <summary>
/// Add a function message to the chat history
/// </summary>
/// <param name="chatHistory">Chat history</param>
/// <param name="message">Message content</param>
/// <param name="functionName">Name of the function that generated the content</param>
public static void AddFunctionMessage(this ChatHistory chatHistory, string message, string functionName)
{
chatHistory.AddMessage(AuthorRole.Function, message, new Dictionary<string, string> { { "Name", functionName } });
}

/// <summary>
/// Add an assistant message to the chat history.
/// </summary>
/// <param name="chatHistory">Chat history</param>
/// <param name="chatResult">Chat result from the model</param>
public static void AddAssistantMessage(this ChatHistory chatHistory, IChatResult chatResult)
{
var chatMessage = chatResult.ModelResult.GetOpenAIChatResult().Choice.Message;

if (!chatMessage.Content.IsNullOrEmpty())
{
chatHistory.AddAssistantMessage(chatMessage.Content);
}

var functionCall = chatMessage.FunctionCall;
if (functionCall is not null)
{
chatHistory.AddMessage(AuthorRole.Assistant, string.Empty, new Dictionary<string, string>
{
{ "Name", functionCall.Name },
{ "Arguments", functionCall.Arguments }
});
}
}
}
Loading

0 comments on commit e83570d

Please sign in to comment.