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

Several improvements / fixes for functions #320

Merged
merged 11 commits into from
Jul 25, 2023
59 changes: 40 additions & 19 deletions OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using OpenAI.Interfaces;
using OpenAI.ObjectModels;
using OpenAI.ObjectModels.RequestModels;
using OpenAI.ObjectModels.SharedModels;

namespace OpenAI.Playground.TestHelpers;

Expand Down Expand Up @@ -102,20 +103,23 @@ public static async Task RunChatFunctionCallTest(IOpenAIService sdk)
// https://github.com/openai/openai-cookbook/blob/main/examples/How_to_call_functions_with_chat_models.ipynb

var fn1 = new FunctionDefinitionBuilder("get_current_weather", "Get the current weather")
.AddParameter("location", "string", "The city and state, e.g. San Francisco, CA")
.AddParameter("format", "string", "The temperature unit to use. Infer this from the users location.",
new List<string> {"celsius", "fahrenheit"})
.AddParameter("location", PropertyDefinition.DefineString("The city and state, e.g. San Francisco, CA"))
.AddParameter("format", PropertyDefinition.DefineEnum(new List<string> {"celsius", "fahrenheit"},"The temperature unit to use. Infer this from the users location."))
.Validate()
.Build();

var fn2 = new FunctionDefinitionBuilder("get_n_day_weather_forecast", "Get an N-day weather forecast")
.AddParameter("location", "string", "The city and state, e.g. San Francisco, CA")
.AddParameter("format", "string", "The temperature unit to use. Infer this from the users location.",
new List<string> {"celsius", "fahrenheit"})
.AddParameter("num_days", "integer", "The number of days to forecast")
.AddParameter("location", new() { Type = "string",Description = "The city and state, e.g. San Francisco, CA"})
.AddParameter("format", PropertyDefinition.DefineEnum(new List<string> {"celsius", "fahrenheit"}, "The temperature unit to use. Infer this from the users location."))
.AddParameter("num_days", PropertyDefinition.DefineInteger("The number of days to forecast"))
.Validate()
.Build();
var fn3 = new FunctionDefinitionBuilder("get_current_datetime", "Get the current date and time, e.g. 'Saturday, June 24, 2023 6:14:14 PM'")
.Build();

var fn4 = new FunctionDefinitionBuilder("identify_number_sequence", "Get a sequence of numbers present in the user message")
.AddParameter("values", PropertyDefinition.DefineArray(PropertyDefinition.DefineNumber("Sequence of numbers specified by the user")))
.Build();
try
{
ConsoleExtensions.WriteLine("Chat Function Call Test:", ConsoleColor.DarkCyan);
Expand All @@ -126,11 +130,11 @@ public static async Task RunChatFunctionCallTest(IOpenAIService sdk)
ChatMessage.FromSystem("Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."),
ChatMessage.FromUser("Give me a weather report for Chicago, USA, for the next 5 days.")
},
Functions = new List<FunctionDefinition> {fn1, fn2},
Functions = new List<FunctionDefinition> { fn1, fn2, fn3, fn4 },
// optionally, to force a specific function:
// FunctionCall = new Dictionary<string, string> { { "name", "get_current_weather" } },
MaxTokens = 50,
Model = Models.Gpt_3_5_Turbo_0613
Model = Models.Gpt_3_5_Turbo
});

/* expected output along the lines of:
Expand Down Expand Up @@ -183,19 +187,24 @@ public static async Task RunChatFunctionCallTestAsStream(IOpenAIService sdk)
// https://github.com/openai/openai-cookbook/blob/main/examples/How_to_call_functions_with_chat_models.ipynb

var fn1 = new FunctionDefinitionBuilder("get_current_weather", "Get the current weather")
.AddParameter("location", "string", "The city and state, e.g. San Francisco, CA")
.AddParameter("format", "string", "The temperature unit to use. Infer this from the users location.",
new List<string> {"celsius", "fahrenheit"})
.AddParameter("location", PropertyDefinition.DefineString("The city and state, e.g. San Francisco, CA"))
.AddParameter("format", PropertyDefinition.DefineEnum(new List<string> {"celsius", "fahrenheit"},"The temperature unit to use. Infer this from the users location."))
.Validate()
.Build();

var fn2 = new FunctionDefinitionBuilder("get_n_day_weather_forecast", "Get an N-day weather forecast")
.AddParameter("location", "string", "The city and state, e.g. San Francisco, CA")
.AddParameter("format", "string", "The temperature unit to use. Infer this from the users location.",
new List<string> {"celsius", "fahrenheit"})
.AddParameter("num_days", "integer", "The number of days to forecast")
.AddParameter("location", new PropertyDefinition{ Type = "string",Description = "The city and state, e.g. San Francisco, CA"})
.AddParameter("format", PropertyDefinition.DefineEnum(new List<string> {"celsius", "fahrenheit"}, "The temperature unit to use. Infer this from the users location."))
.AddParameter("num_days", PropertyDefinition.DefineInteger("The number of days to forecast"))
.Validate()
.Build();

var fn3 = new FunctionDefinitionBuilder("get_current_datetime", "Get the current date and time, e.g. 'Saturday, June 24, 2023 6:14:14 PM'")
.Build();

var fn4 = new FunctionDefinitionBuilder("identify_number_sequence", "Get a sequence of numbers present in the user message")
.AddParameter("values", PropertyDefinition.DefineArray(PropertyDefinition.DefineNumber("Sequence of numbers specified by the user")))
.Build();

try
{
Expand All @@ -205,16 +214,21 @@ public static async Task RunChatFunctionCallTestAsStream(IOpenAIService sdk)
Messages = new List<ChatMessage>
{
ChatMessage.FromSystem("Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."),
ChatMessage.FromUser("Give me a weather report for Chicago, USA, for the next 5 days.")

// to test weather forecast functions:
ChatMessage.FromUser("Give me a weather report for Chicago, USA, for the next 5 days."),

// or to test array functions, use this instead:
// ChatMessage.FromUser("The combination is: One. Two. Three. Four. Five."),
},
Functions = new List<FunctionDefinition> {fn1, fn2},
Functions = new List<FunctionDefinition> { fn1, fn2, fn3, fn4 },
// optionally, to force a specific function:
// FunctionCall = new Dictionary<string, string> { { "name", "get_current_weather" } },
MaxTokens = 50,
Model = Models.Gpt_3_5_Turbo_0613
});

/* expected output along the lines of:
/* when testing weather forecasts, expected output should be along the lines of:

Message:
Function call: get_n_day_weather_forecast
Expand All @@ -223,6 +237,13 @@ public static async Task RunChatFunctionCallTestAsStream(IOpenAIService sdk)
num_days: 5
*/

/* when testing array functions, expected output should be along the lines of:

Message:
Function call: identify_number_sequence
values: [1, 2, 3, 4, 5]
*/

await foreach (var completionResult in completionResults)
{
if (completionResult.Successful)
Expand Down
62 changes: 10 additions & 52 deletions OpenAI.SDK/Builders/FunctionDefinitionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,46 +28,23 @@ public FunctionDefinitionBuilder(string name, string? description = null)
_definition = new FunctionDefinition
{
Name = name,
Description = description
Description = description,
Parameters = new PropertyDefinition
{
Properties = new Dictionary<string, PropertyDefinition>()
}
};
}

/// <summary>
/// Adds a parameter to the function definition with a type expressed as an enumeration.
/// </summary>
/// <param name="name">The name of the parameter</param>
/// <param name="type">The type of the parameter</param>
/// <param name="description">The optional description of the parameter</param>
/// <param name="enum">The optional list of possible string values for the parameter</param>
/// <param name="required">Whether this parameter is required (default is true)</param>
/// <returns>The FunctionDefinitionBuilder instance for fluent configuration</returns>
public FunctionDefinitionBuilder AddParameter(string name, FunctionParameters.FunctionObjectTypes type, string? description = null, IList<string>? @enum = null, bool required = true)
public FunctionDefinitionBuilder AddParameter(string name, PropertyDefinition value, bool required = true)
{
var typeStr = ConvertTypeToString(type);
return AddParameter(name, typeStr, description, @enum, required);
}

/// <summary>
/// Adds a parameter to the function definition with a type expressed as a string.
/// </summary>
/// <param name="name">The name of the parameter</param>
/// <param name="type">The type of the parameter</param>
/// <param name="description">The optional description of the parameter</param>
/// <param name="enum">The optional list of possible string values for the parameter</param>
/// <param name="required">Whether this parameter is required (default is true)</param>
/// <returns>The FunctionDefinitionBuilder instance for fluent configuration</returns>
public FunctionDefinitionBuilder AddParameter(string name, string type, string? description = null, IList<string>? @enum = null, bool required = true)
{
_definition.Parameters ??= new FunctionParameters();
_definition.Parameters.Properties ??= new Dictionary<string, FunctionParameters.FunctionParameterPropertyValue>();

_definition.Parameters.Properties[name] =
new FunctionParameters.FunctionParameterPropertyValue {Type = type, Description = description, Enum = @enum};
var pars = _definition.Parameters!;
pars.Properties![name] = value;

if (required)
{
_definition.Parameters.Required ??= new List<string>();
_definition.Parameters.Required.Add(name);
pars.Required ??= new List<string>();
pars.Required.Add(name);
}

return this;
Expand Down Expand Up @@ -115,23 +92,4 @@ public static void ValidateName(string functionName)
throw new ArgumentOutOfRangeException(nameof(functionName), message);
}
}

/// <summary>
/// Converts a FunctionObjectTypes enumeration value to its corresponding string representation.
/// </summary>
/// <param name="type">The type to convert</param>
/// <returns>The string representation of the given type</returns>
private static string ConvertTypeToString(FunctionParameters.FunctionObjectTypes type)
{
return type switch
{
FunctionParameters.FunctionObjectTypes.String => "string",
FunctionParameters.FunctionObjectTypes.Number => "number",
FunctionParameters.FunctionObjectTypes.Object => "object",
FunctionParameters.FunctionObjectTypes.Array => "array",
FunctionParameters.FunctionObjectTypes.Boolean => "boolean",
FunctionParameters.FunctionObjectTypes.Null => "null",
_ => throw new ArgumentOutOfRangeException(nameof(type), $"Unknown type: {type}")
};
}
}
12 changes: 1 addition & 11 deletions OpenAI.SDK/Managers/OpenAIFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,7 @@ public async Task<FileResponse> RetrieveFile(string fileId, CancellationToken ca
{
var response = await _httpClient.GetAsync(_endpointProvider.FileRetrieveContent(fileId), cancellationToken);

if (!response.IsSuccessStatusCode)
{
return new FileContentResponse<T?>
{
Error = new Error
{
Message = $"Api returned Status Code: {(int) response.StatusCode} {response.StatusCode}",
Code = ((int) response.StatusCode).ToString()
}
};
}
response.EnsureSuccessStatusCode();

if (typeof(T) == typeof(string))
{
Expand Down
59 changes: 1 addition & 58 deletions OpenAI.SDK/ObjectModels/RequestModels/ChatMessage.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using OpenAI.ObjectModels.SharedModels;
using System.Text.Json.Serialization;

namespace OpenAI.ObjectModels.RequestModels;

Expand Down Expand Up @@ -72,59 +70,4 @@ public static ChatMessage FromSystem(string content, string? name = null)
{
return new ChatMessage(StaticValues.ChatMessageRoles.System, content, name);
}
}

/// <summary>
/// Describes a function call returned from GPT.
/// A function call contains a function name, and a dictionary
/// mapping function argument names to their values.
/// </summary>
public class FunctionCall
{
/// <summary>
/// Function name
/// </summary>
[JsonPropertyName("name")]
public string? Name { get; set; }

/// <summary>
/// Function arguments, returned as a JSON-encoded dictionary mapping
/// argument names to argument values.
/// </summary>
[JsonPropertyName("arguments")]
public string? Arguments { get; set; }

public Dictionary<string, object> ParseArguments()
{
var result = !string.IsNullOrWhiteSpace(Arguments) ? JsonSerializer.Deserialize<Dictionary<string, object>>(Arguments) : null;
return result ?? new Dictionary<string, object>();
}
}

/// <summary>
/// Definition of a valid function call.
/// </summary>
public class FunctionDefinition
{
/// <summary>
/// Required. The name of the function to be called. Must be a-z, A-Z, 0-9,
/// or contain underscores and dashes, with a maximum length of 64.
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; }

/// <summary>
/// Optional. The description of what the function does.
/// </summary>
[JsonPropertyName("description")]
public string? Description { get; set; }

/// <summary>
/// Optional. The parameters the functions accepts, described as a JSON Schema object.
/// See the guide (https://platform.openai.com/docs/guides/gpt/function-calling) for examples,
/// and the JSON Schema reference (https://json-schema.org/understanding-json-schema/)
/// for documentation about the format.
/// </summary>
[JsonPropertyName("parameters")]
public FunctionParameters? Parameters { get; set; }
}
31 changes: 31 additions & 0 deletions OpenAI.SDK/ObjectModels/RequestModels/FunctionCall.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace OpenAI.ObjectModels.RequestModels;

/// <summary>
/// Describes a function call returned from GPT.
/// A function call contains a function name, and a dictionary
/// mapping function argument names to their values.
/// </summary>
public class FunctionCall
{
/// <summary>
/// Function name
/// </summary>
[JsonPropertyName("name")]
public string? Name { get; set; }

/// <summary>
/// Function arguments, returned as a JSON-encoded dictionary mapping
/// argument names to argument values.
/// </summary>
[JsonPropertyName("arguments")]
public string? Arguments { get; set; }

public Dictionary<string, object> ParseArguments()
{
var result = !string.IsNullOrWhiteSpace(Arguments) ? JsonSerializer.Deserialize<Dictionary<string, object>>(Arguments) : null;
return result ?? new Dictionary<string, object>();
}
}
32 changes: 32 additions & 0 deletions OpenAI.SDK/ObjectModels/RequestModels/FunctionDefinition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Text.Json.Serialization;
using OpenAI.ObjectModels.SharedModels;

namespace OpenAI.ObjectModels.RequestModels;

/// <summary>
/// Definition of a valid function call.
/// </summary>
public class FunctionDefinition
{
/// <summary>
/// Required. The name of the function to be called. Must be a-z, A-Z, 0-9,
/// or contain underscores and dashes, with a maximum length of 64.
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; }

/// <summary>
/// Optional. The description of what the function does.
/// </summary>
[JsonPropertyName("description")]
public string? Description { get; set; }

/// <summary>
/// Optional. The parameters the functions accepts, described as a JSON Schema object.
/// See the guide (https://platform.openai.com/docs/guides/gpt/function-calling) for examples,
/// and the JSON Schema reference (https://json-schema.org/understanding-json-schema/)
/// for documentation about the format.
/// </summary>
[JsonPropertyName("parameters")]
public PropertyDefinition? Parameters { get; set; }
}
Loading