diff --git a/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs b/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs index fe2021b6..d588763a 100644 --- a/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs +++ b/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs @@ -1,8 +1,11 @@ using NUnit.Framework; using OpenAI.Chat; +using OpenAI.Tests.Weather; using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Threading.Tasks; namespace OpenAI.Tests @@ -95,5 +98,90 @@ public async Task Test_3_GetChatStreamingCompletionEnumerableAsync() } } } + + [Test] + public async Task Test_4_GetChatFunctionCompletion() + { + Assert.IsNotNull(OpenAIClient.ChatEndpoint); + var messages = new List + { + new Message(Role.System, "You are a helpful weather assistant."), + new Message(Role.User, "What's the weather like today?"), + }; + + foreach (var message in messages) + { + Console.WriteLine($"{message.Role}: {message.Content}"); + } + + var functions = new List + { + new Function( + nameof(WeatherService.GetCurrentWeather), + "Get the current weather in a given location", + new JsonObject + { + ["type"] = "object", + ["properties"] = new JsonObject + { + ["location"] = new JsonObject + { + ["type"] = "string", + ["description"] = "The city and state, e.g. San Francisco, CA" + }, + ["unit"] = new JsonObject + { + ["type"] = "string", + ["enum"] = new JsonArray {"celsius", "fahrenheit"} + } + }, + ["required"] = new JsonArray { "location", "unit" } + }) + }; + + var chatRequest = new ChatRequest(messages, functions: functions, functionCall: "auto", model: "gpt-3.5-turbo-0613"); + var result = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); + Assert.IsNotNull(result); + Assert.IsNotNull(result.Choices); + Assert.IsTrue(result.Choices.Count == 1); + messages.Add(result.FirstChoice.Message); + + Console.WriteLine($"{result.FirstChoice.Message.Role}: {result.FirstChoice.Message.Content} | Finish Reason: {result.FirstChoice.FinishReason}"); + + var locationMessage = new Message(Role.User, "I'm in Glasgow, Scotland"); + messages.Add(locationMessage); + Console.WriteLine($"{locationMessage.Role}: {locationMessage.Content}"); + chatRequest = new ChatRequest(messages, functions: functions, functionCall: "auto", model: "gpt-3.5-turbo-0613"); + result = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); + + Assert.IsNotNull(result); + Assert.IsNotNull(result.Choices); + Assert.IsTrue(result.Choices.Count == 1); + messages.Add(result.FirstChoice.Message); + + if (!string.IsNullOrWhiteSpace(result.FirstChoice.Message.Content)) + { + Console.WriteLine($"{result.FirstChoice.Message.Role}: {result.FirstChoice.Message.Content} | Finish Reason: {result.FirstChoice.FinishReason}"); + + var unitMessage = new Message(Role.User, "celsius"); + messages.Add(unitMessage); + Console.WriteLine($"{unitMessage.Role}: {unitMessage.Content}"); + chatRequest = new ChatRequest(messages, functions: functions, functionCall: "auto", model: "gpt-3.5-turbo-0613"); + result = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); + Assert.IsNotNull(result); + Assert.IsNotNull(result.Choices); + Assert.IsTrue(result.Choices.Count == 1); + } + + Assert.IsTrue(result.FirstChoice.FinishReason == "function_call"); + Assert.IsTrue(result.FirstChoice.Message.Function.Name == nameof(WeatherService.GetCurrentWeather)); + Console.WriteLine($"{result.FirstChoice.Message.Role}: {result.FirstChoice.Message.Function.Name} | Finish Reason: {result.FirstChoice.FinishReason}"); + Console.WriteLine($"{result.FirstChoice.Message.Function.Arguments}"); + var functionArgs = JsonSerializer.Deserialize(result.FirstChoice.Message.Function.Arguments.ToString()); + var functionResult = WeatherService.GetCurrentWeather(functionArgs); + Assert.IsNotNull(functionResult); + messages.Add(new Message(Role.Function, functionResult)); + Console.WriteLine($"{Role.Function}: {functionResult}"); + } } } \ No newline at end of file diff --git a/OpenAI-DotNet-Tests/TestServices/WeatherService.cs b/OpenAI-DotNet-Tests/TestServices/WeatherService.cs new file mode 100644 index 00000000..4158e4cd --- /dev/null +++ b/OpenAI-DotNet-Tests/TestServices/WeatherService.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; + +namespace OpenAI.Tests.Weather +{ + internal class WeatherService + { + public static string GetCurrentWeather(WeatherArgs weatherArgs) + { + return $"The current weather in {weatherArgs.Location} is 20 {weatherArgs.Unit}"; + } + } + + internal class WeatherArgs + { + [JsonPropertyName("location")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Location { get; set; } + + [JsonPropertyName("unit")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Unit { get; set; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Chat/ChatRequest.cs b/OpenAI-DotNet/Chat/ChatRequest.cs index 043e958f..e662830c 100644 --- a/OpenAI-DotNet/Chat/ChatRequest.cs +++ b/OpenAI-DotNet/Chat/ChatRequest.cs @@ -14,8 +14,8 @@ public sealed class ChatRequest /// The list of messages for the current chat session. /// /// - /// ID of the model to use.
- /// Currently, only gpt-4, gpt-3.5-turbo and gpt-3.5-turbo-0301 are supported. + /// Id of the model to use.
+ /// Currently, only gpt-4 and gpt-3.5-turbo and their variants are supported. /// /// /// What sampling temperature to use, between 0 and 2. @@ -66,6 +66,12 @@ public sealed class ChatRequest /// /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. /// + /// + /// If functions is not null or empty, this is required. Pass "auto" to let the API decide, "none" if none are to be called, or {"name": "function-name"} + /// + /// + /// An optional list of functions to get arguments for. + /// public ChatRequest( IEnumerable messages, string model = null, @@ -76,8 +82,10 @@ public ChatRequest( int? maxTokens = null, double? presencePenalty = null, double? frequencyPenalty = null, - Dictionary logitBias = null, - string user = null) + IReadOnlyDictionary logitBias = null, + string user = null, + string functionCall = null, + IEnumerable functions = null) { Model = string.IsNullOrWhiteSpace(model) ? Models.Model.GPT3_5_Turbo : model; @@ -103,6 +111,14 @@ public ChatRequest( FrequencyPenalty = frequencyPenalty; LogitBias = logitBias; User = user; + + if (string.IsNullOrEmpty(functionCall) && Functions is { Count: > 0 }) + { + throw new ArgumentException("If functions are provided, please also provide a function_call specifier e.g. (auto, none, or {\"name\": \"\"})"); + } + + FunctionCall = functionCall; + Functions = functions?.ToList(); } /// @@ -200,5 +216,19 @@ public ChatRequest( /// [JsonPropertyName("user")] public string User { get; } + + /// + /// If functions is not null or empty, this is required. Pass "auto" to let the API decide, "none" if none are to be called, or {"name": "function-name"} + /// + [JsonPropertyName("function_call")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string FunctionCall { get; } + + /// + /// An optional list of functions to get arguments for. + /// + [JsonPropertyName("functions")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public IReadOnlyList Functions { get; } } } diff --git a/OpenAI-DotNet/Chat/Choice.cs b/OpenAI-DotNet/Chat/Choice.cs index 840e8f17..38e99bc8 100644 --- a/OpenAI-DotNet/Chat/Choice.cs +++ b/OpenAI-DotNet/Chat/Choice.cs @@ -21,10 +21,12 @@ public Choice( [JsonInclude] [JsonPropertyName("message")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public Message Message { get; private set; } [JsonInclude] [JsonPropertyName("delta")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public Delta Delta { get; private set; } [JsonInclude] diff --git a/OpenAI-DotNet/Chat/Function.cs b/OpenAI-DotNet/Chat/Function.cs new file mode 100644 index 00000000..d79b4fe1 --- /dev/null +++ b/OpenAI-DotNet/Chat/Function.cs @@ -0,0 +1,81 @@ +using System; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace OpenAI.Chat +{ + /// + /// + /// + public class Function + { + /// + /// Creates a new function description to insert into a chat conversation. + /// + /// + /// Required. The name of the function to generate arguments for based on the context in a message.
+ /// May contain a-z, A-Z, 0-9, underscores and dashes, with a maximum length of 64 characters. Recommended to not begin with a number or a dash. + /// + /// + /// An optional description of the function, used by the API to determine if it is useful to include in the response. + /// + /// + /// An optional JSON object describing the parameters of the function that the model should generate in JSON schema format (json-schema.org). + /// + /// + /// The arguments to use when calling the function. + /// + public Function(string name, string description = null, JsonNode parameters = null, JsonNode arguments = null) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException($"{nameof(name)} cannot be null or whitespace.", nameof(name)); + } + + if (name.Length > 64) + { + throw new ArgumentException($"{nameof(name)} cannot be longer than 64 characters.", nameof(name)); + } + + Name = name; + Description = description; + Parameters = parameters; + Arguments = arguments; + } + + /// + /// The name of the function to generate arguments for.
+ /// May contain a-z, A-Z, 0-9, and underscores and dashes, with a maximum length of 64 characters. + /// Recommended to not begin with a number or a dash. + ///
+ [JsonInclude] + [JsonPropertyName("name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Name { get; private set; } + + /// + /// The optional description of the function. + /// + [JsonInclude] + [JsonPropertyName("description")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Description { get; private set; } + + /// + /// The optional parameters of the function. + /// Describe the parameters that the model should generate in JSON schema format (json-schema.org). + /// + [JsonInclude] + [JsonPropertyName("parameters")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public JsonNode Parameters { get; private set; } + + /// + /// The arguments to use when calling the function. + /// + [JsonInclude] + [JsonPropertyName("arguments")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public JsonNode Arguments { get; private set; } + } +} diff --git a/OpenAI-DotNet/Chat/Message.cs b/OpenAI-DotNet/Chat/Message.cs index 87210aed..8a224d01 100644 --- a/OpenAI-DotNet/Chat/Message.cs +++ b/OpenAI-DotNet/Chat/Message.cs @@ -17,11 +17,15 @@ public sealed class Message /// Optional, The name of the author of this message.
/// May contain a-z, A-Z, 0-9, and underscores, with a maximum length of 64 characters. /// - public Message(Role role, string content, string name = null) + /// + /// The function that should be called, as generated by the model. + /// + public Message(Role role, string content, string name = null, Function function = null) { Role = role; Content = content; Name = name; + Function = function; } /// @@ -36,14 +40,24 @@ public Message(Role role, string content, string name = null) /// [JsonInclude] [JsonPropertyName("content")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string Content { get; private set; } + /// + /// The function that should be called, as generated by the model. + /// + [JsonInclude] + [JsonPropertyName("function_call")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Function Function { get; private set; } + /// /// Optional, The name of the author of this message.
/// May contain a-z, A-Z, 0-9, and underscores, with a maximum length of 64 characters. ///
[JsonInclude] [JsonPropertyName("name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string Name { get; private set; } public override string ToString() => Content ?? string.Empty; diff --git a/OpenAI-DotNet/Chat/Role.cs b/OpenAI-DotNet/Chat/Role.cs index 5a903871..6d543c4c 100644 --- a/OpenAI-DotNet/Chat/Role.cs +++ b/OpenAI-DotNet/Chat/Role.cs @@ -10,5 +10,7 @@ public enum Role Assistant = 2, [EnumMember(Value = "user")] User = 3, + [EnumMember(Value = "function")] + Function = 4, } } \ No newline at end of file diff --git a/OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs b/OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs index eb75fc0d..b916fa2a 100644 --- a/OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs +++ b/OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs @@ -17,7 +17,7 @@ public sealed class EmbeddingsRequest /// Each input must not exceed 8192 tokens in length. /// /// - /// ID of the model to use.
+ /// ID of the model to use.
/// Defaults to: /// /// diff --git a/OpenAI-DotNet/Models/Model.cs b/OpenAI-DotNet/Models/Model.cs index 43b3a2e8..20f6300e 100644 --- a/OpenAI-DotNet/Models/Model.cs +++ b/OpenAI-DotNet/Models/Model.cs @@ -72,12 +72,14 @@ public Model(string id, string ownedBy = null) public string Parent { get; private set; } /// - /// More capable than any GPT-3.5 model, able to do more complex tasks, and optimized for chat. Will be updated with our latest model iteration. + /// More capable than any GPT-3.5 model, able to do more complex tasks, and optimized for chat. + /// Will be updated with our latest model iteration. /// public static Model GPT4 { get; } = new("gpt-4", "openai"); /// - /// Same capabilities as the base gpt-4 mode but with 4x the context length. Will be updated with our latest model iteration. + /// Same capabilities as the base gpt-4 mode but with 4x the context length. + /// Will be updated with our latest model iteration. Tokens are 2x the price of gpt-4. /// public static Model GPT4_32K { get; } = new("gpt-4-32k", "openai"); @@ -87,6 +89,12 @@ public Model(string id, string ownedBy = null) /// public static Model GPT3_5_Turbo { get; } = new("gpt-3.5-turbo", "openai"); + /// + /// Same capabilities as the base gpt-3.5-turbo mode but with 4x the context length. + /// Tokens are 2x the price of gpt-3.5-turbo. Will be updated with our latest model iteration. + /// + public static Model GPT3_5_Turbo_16K { get; } = new("gpt-3.5-turbo-16k", "openai"); + /// /// The most powerful, largest engine available, although the speed is quite slow. /// Good at: Complex intent, cause and effect, summarization for audience diff --git a/OpenAI-DotNet/OpenAI-DotNet.csproj b/OpenAI-DotNet/OpenAI-DotNet.csproj index 268dd919..6dc8837b 100644 --- a/OpenAI-DotNet/OpenAI-DotNet.csproj +++ b/OpenAI-DotNet/OpenAI-DotNet.csproj @@ -17,8 +17,10 @@ More context [on Roger Pincombe's blog](https://rogerpincombe.com/openai-dotnet- https://github.com/RageAgainstThePixel/OpenAI-DotNet OpenAI, AI, ML, API, gpt-4, gpt-3.5-tubo, gpt-3, chatGPT, chat-gpt, gpt-2, gpt OpenAI API - 6.8.7 - Version 6.8.7 + 7.0.0 + Version 7.0.0 +- Added function calling to chat models +Version 6.8.7 - Added ToString and string operator to Moderation Scores Version 6.8.6 - Populated finish reason in streaming chat final message content diff --git a/README.md b/README.md index 8845d4b4..0a42787f 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,8 @@ Install-Package OpenAI-DotNet - [Authentication](#authentication) - [Azure OpenAI](#azure-openai) - - [Azure Active Directory Authentication](#azure-active-directory-authentication) :new: -- [OpenAI API Proxy](#openai-api-proxy) :new: + - [Azure Active Directory Authentication](#azure-active-directory-authentication) +- [OpenAI API Proxy](#openai-api-proxy) - [Models](#models) - [List Models](#list-models) - [Retrieve Models](#retrieve-model) @@ -50,6 +50,7 @@ Install-Package OpenAI-DotNet - [Chat](#chat) - [Chat Completions](#chat-completions) - [Streaming](#chat-streaming) + - [Functions](#chat-functions) :new: - [Edits](#edits) - [Create Edit](#create-edit) - [Embeddings](#embeddings) @@ -424,6 +425,90 @@ await foreach (var result in api.ChatEndpoint.StreamCompletionEnumerableAsync(ch } ``` +##### [Chat Functions](https://platform.openai.com/docs/api-reference/chat/create#chat/create-functions) + +> Only available with the latest 0613 model series! + +```csharp +var api = new OpenAIClient(); +var messages = new List +{ + new Message(Role.System, "You are a helpful weather assistant."), + new Message(Role.User, "What's the weather like today?"), +}; + +foreach (var message in messages) +{ + Console.WriteLine($"{message.Role}: {message.Content}"); +} + +// Define the functions that the assistant is able to use: +var functions = new List +{ + new Function( + nameof(WeatherService.GetCurrentWeather), + "Get the current weather in a given location", + new JObject + { + ["type"] = "object", + ["properties"] = new JObject + { + ["location"] = new JObject + { + ["type"] = "string", + ["description"] = "The city and state, e.g. San Francisco, CA" + }, + ["unit"] = new JObject + { + ["type"] = "string", + ["enum"] = new JArray {"celsius", "fahrenheit"} + } + }, + ["required"] = new JArray { "location", "unit" } + }) +}; + +var chatRequest = new ChatRequest(messages, functions: functions, functionCall: "auto", model: "gpt-3.5-turbo-0613"); +var result = await api.ChatEndpoint.GetCompletionAsync(chatRequest); +messages.Add(result.FirstChoice.Message); +Console.WriteLine($"{result.FirstChoice.Message.Role}: {result.FirstChoice.Message.Content} | Finish Reason: {result.FirstChoice.FinishReason}"); +var locationMessage = new Message(Role.User, "I'm in Glasgow, Scotland"); +messages.Add(locationMessage); +Console.WriteLine($"{locationMessage.Role}: {locationMessage.Content}"); +chatRequest = new ChatRequest(messages, functions: functions, functionCall: "auto", model: "gpt-3.5-turbo-0613"); +result = await api.ChatEndpoint.GetCompletionAsync(chatRequest); +messages.Add(result.FirstChoice.Message); + +if (!string.IsNullOrWhiteSpace(result.FirstChoice.Message.Content)) +{ + // It's possible that the assistant will also ask you which units you want the temperature in. + Console.WriteLine($"{result.FirstChoice.Message.Role}: {result.FirstChoice.Message.Content} | Finish Reason: {result.FirstChoice.FinishReason}"); + + var unitMessage = new Message(Role.User, "celsius"); + messages.Add(unitMessage); + Console.WriteLine($"{unitMessage.Role}: {unitMessage.Content}"); + chatRequest = new ChatRequest(messages, functions: functions, functionCall: "auto", model: "gpt-3.5-turbo-0613"); + result = await api.ChatEndpoint.GetCompletionAsync(chatRequest); +} + +Console.WriteLine($"{result.FirstChoice.Message.Role}: {result.FirstChoice.Message.Function.Name} | Finish Reason: {result.FirstChoice.FinishReason}"); +Console.WriteLine($"{result.FirstChoice.Message.Function.Arguments}"); +var functionArgs = JsonConvert.DeserializeObject(result.FirstChoice.Message.Function.Arguments.ToString()); +var functionResult = WeatherService.GetCurrentWeather(functionArgs); +messages.Add(new Message(Role.Function, functionResult)); +Console.WriteLine($"{Role.Function}: {functionResult}"); +// System: You are a helpful weather assistant. +// User: What's the weather like today? +// Assistant: Sure, may I know your current location? | Finish Reason: stop +// User: I'm in Glasgow, Scotland +// Assistant: GetCurrentWeather | Finish Reason: function_call +// { +// "location": "Glasgow, Scotland", +// "unit": "celsius" +// } +// Function: The current weather in Glasgow, Scotland is 20 celsius +``` + ### [Edits](https://platform.openai.com/docs/api-reference/edits) Given a prompt and an instruction, the model will return an edited version of the prompt.