Skip to content

Commit

Permalink
OpenAI-DotNet 7.0.0 (#118)
Browse files Browse the repository at this point in the history
- Add function calling to chat models

---------

Co-authored-by: damiant3 <[email protected]>
  • Loading branch information
StephenHodgson and damiant3 authored Jun 17, 2023
1 parent 2020870 commit 8c836e3
Show file tree
Hide file tree
Showing 11 changed files with 347 additions and 12 deletions.
88 changes: 88 additions & 0 deletions OpenAI-DotNet-Tests/TestFixture_03_Chat.cs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<Message>
{
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<Function>
{
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<WeatherArgs>(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}");
}
}
}
23 changes: 23 additions & 0 deletions OpenAI-DotNet-Tests/TestServices/WeatherService.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
38 changes: 34 additions & 4 deletions OpenAI-DotNet/Chat/ChatRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ public sealed class ChatRequest
/// The list of messages for the current chat session.
/// </param>
/// <param name="model">
/// ID of the model to use.<br/>
/// Currently, only gpt-4, gpt-3.5-turbo and gpt-3.5-turbo-0301 are supported.
/// Id of the model to use.<br/>
/// Currently, only gpt-4 and gpt-3.5-turbo and their variants are supported.
/// </param>
/// <param name="temperature">
/// What sampling temperature to use, between 0 and 2.
Expand Down Expand Up @@ -66,6 +66,12 @@ public sealed class ChatRequest
/// <param name="user">
/// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse.
/// </param>
/// <param name="functionCall">
/// 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"}
/// </param>
/// <param name="functions">
/// An optional list of functions to get arguments for.
/// </param>
public ChatRequest(
IEnumerable<Message> messages,
string model = null,
Expand All @@ -76,8 +82,10 @@ public ChatRequest(
int? maxTokens = null,
double? presencePenalty = null,
double? frequencyPenalty = null,
Dictionary<string, double> logitBias = null,
string user = null)
IReadOnlyDictionary<string, double> logitBias = null,
string user = null,
string functionCall = null,
IEnumerable<Function> functions = null)
{
Model = string.IsNullOrWhiteSpace(model) ? Models.Model.GPT3_5_Turbo : model;

Expand All @@ -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\": \"<insert-function-name>\"})");
}

FunctionCall = functionCall;
Functions = functions?.ToList();
}

/// <summary>
Expand Down Expand Up @@ -200,5 +216,19 @@ public ChatRequest(
/// </summary>
[JsonPropertyName("user")]
public string User { get; }

/// <summary>
/// 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"}
/// </summary>
[JsonPropertyName("function_call")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string FunctionCall { get; }

/// <summary>
/// An optional list of functions to get arguments for.
/// </summary>
[JsonPropertyName("functions")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public IReadOnlyList<Function> Functions { get; }
}
}
2 changes: 2 additions & 0 deletions OpenAI-DotNet/Chat/Choice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
81 changes: 81 additions & 0 deletions OpenAI-DotNet/Chat/Function.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;

namespace OpenAI.Chat
{
/// <summary>
/// <see href="https://platform.openai.com/docs/guides/gpt/function-calling"/>
/// </summary>
public class Function
{
/// <summary>
/// Creates a new function description to insert into a chat conversation.
/// </summary>
/// <param name="name">
/// Required. The name of the function to generate arguments for based on the context in a message.<br/>
/// 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.
/// </param>
/// <param name="description">
/// An optional description of the function, used by the API to determine if it is useful to include in the response.
/// </param>
/// <param name="parameters">
/// An optional JSON object describing the parameters of the function that the model should generate in JSON schema format (json-schema.org).
/// </param>
/// <param name="arguments">
/// The arguments to use when calling the function.
/// </param>
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;
}

/// <summary>
/// The name of the function to generate arguments for.<br/>
/// 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.
/// </summary>
[JsonInclude]
[JsonPropertyName("name")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string Name { get; private set; }

/// <summary>
/// The optional description of the function.
/// </summary>
[JsonInclude]
[JsonPropertyName("description")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string Description { get; private set; }

/// <summary>
/// The optional parameters of the function.
/// Describe the parameters that the model should generate in JSON schema format (json-schema.org).
/// </summary>
[JsonInclude]
[JsonPropertyName("parameters")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public JsonNode Parameters { get; private set; }

/// <summary>
/// The arguments to use when calling the function.
/// </summary>
[JsonInclude]
[JsonPropertyName("arguments")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public JsonNode Arguments { get; private set; }
}
}
16 changes: 15 additions & 1 deletion OpenAI-DotNet/Chat/Message.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@ public sealed class Message
/// Optional, The name of the author of this message.<br/>
/// May contain a-z, A-Z, 0-9, and underscores, with a maximum length of 64 characters.
/// </param>
public Message(Role role, string content, string name = null)
/// <param name="function">
/// The function that should be called, as generated by the model.
/// </param>
public Message(Role role, string content, string name = null, Function function = null)
{
Role = role;
Content = content;
Name = name;
Function = function;
}

/// <summary>
Expand All @@ -36,14 +40,24 @@ public Message(Role role, string content, string name = null)
/// </summary>
[JsonInclude]
[JsonPropertyName("content")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string Content { get; private set; }

/// <summary>
/// The function that should be called, as generated by the model.
/// </summary>
[JsonInclude]
[JsonPropertyName("function_call")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public Function Function { get; private set; }

/// <summary>
/// Optional, The name of the author of this message.<br/>
/// May contain a-z, A-Z, 0-9, and underscores, with a maximum length of 64 characters.
/// </summary>
[JsonInclude]
[JsonPropertyName("name")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string Name { get; private set; }

public override string ToString() => Content ?? string.Empty;
Expand Down
2 changes: 2 additions & 0 deletions OpenAI-DotNet/Chat/Role.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ public enum Role
Assistant = 2,
[EnumMember(Value = "user")]
User = 3,
[EnumMember(Value = "function")]
Function = 4,
}
}
2 changes: 1 addition & 1 deletion OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public sealed class EmbeddingsRequest
/// Each input must not exceed 8192 tokens in length.
/// </param>
/// <param name="model">
/// ID of the model to use.<br>
/// ID of the model to use.<br/>
/// Defaults to: <see cref="Model.Embedding_Ada_002"/>
/// </param>
/// <param name="user">
Expand Down
12 changes: 10 additions & 2 deletions OpenAI-DotNet/Models/Model.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,14 @@ public Model(string id, string ownedBy = null)
public string Parent { get; private set; }

/// <summary>
/// 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.
/// </summary>
public static Model GPT4 { get; } = new("gpt-4", "openai");

/// <summary>
/// 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.
/// </summary>
public static Model GPT4_32K { get; } = new("gpt-4-32k", "openai");

Expand All @@ -87,6 +89,12 @@ public Model(string id, string ownedBy = null)
/// </summary>
public static Model GPT3_5_Turbo { get; } = new("gpt-3.5-turbo", "openai");

/// <summary>
/// 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.
/// </summary>
public static Model GPT3_5_Turbo_16K { get; } = new("gpt-3.5-turbo-16k", "openai");

/// <summary>
/// The most powerful, largest engine available, although the speed is quite slow.<para/>
/// Good at: Complex intent, cause and effect, summarization for audience
Expand Down
6 changes: 4 additions & 2 deletions OpenAI-DotNet/OpenAI-DotNet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ More context [on Roger Pincombe's blog](https://rogerpincombe.com/openai-dotnet-
<RepositoryUrl>https://github.com/RageAgainstThePixel/OpenAI-DotNet</RepositoryUrl>
<PackageTags>OpenAI, AI, ML, API, gpt-4, gpt-3.5-tubo, gpt-3, chatGPT, chat-gpt, gpt-2, gpt</PackageTags>
<Title>OpenAI API</Title>
<Version>6.8.7</Version>
<PackageReleaseNotes>Version 6.8.7
<Version>7.0.0</Version>
<PackageReleaseNotes>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
Expand Down
Loading

0 comments on commit 8c836e3

Please sign in to comment.