Skip to content

Commit

Permalink
OpenAI-DotNet 7.7.2 (#250)
Browse files Browse the repository at this point in the history
- Added FunctionParameterAttribute to help better inform the feature how to format the Function json
  • Loading branch information
StephenHodgson authored Feb 27, 2024
1 parent d3e59d1 commit ab8773d
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 40 deletions.
23 changes: 13 additions & 10 deletions OpenAI-DotNet-Tests/TestFixture_03_Chat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ public async Task Test_02_01_GetChatToolCompletion()

var messages = new List<Message>
{
new(Role.System, "You are a helpful weather assistant.\n\r- Always prompt the user for their location."),
new(Role.System, "You are a helpful weather assistant. Always ask the user for their location."),
new(Role.User, "What's the weather like today?"),
};

Expand All @@ -143,7 +143,7 @@ public async Task Test_02_01_GetChatToolCompletion()
}

var tools = Tool.GetAllAvailableTools(false, forceUpdate: true, clearCache: true);
var chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto");
var chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "none");
var response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest);
Assert.IsNotNull(response);
Assert.IsNotNull(response.Choices);
Expand Down Expand Up @@ -179,6 +179,7 @@ public async Task Test_02_01_GetChatToolCompletion()
}

Assert.IsTrue(response.FirstChoice.FinishReason == "tool_calls");
Assert.IsTrue(response.FirstChoice.Message.ToolCalls.Count == 1);
var usedTool = response.FirstChoice.Message.ToolCalls[0];
Assert.IsNotNull(usedTool);
Assert.IsTrue(usedTool.Function.Name.Contains(nameof(WeatherService.GetCurrentWeatherAsync)));
Expand All @@ -188,7 +189,7 @@ public async Task Test_02_01_GetChatToolCompletion()
Assert.IsNotNull(functionResult);
messages.Add(new Message(usedTool, functionResult));
Console.WriteLine($"{Role.Tool}: {functionResult}");
chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto");
chatRequest = new ChatRequest(messages);
response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest);
Console.WriteLine(response);
}
Expand All @@ -199,7 +200,7 @@ public async Task Test_02_02_GetChatToolCompletion_Streaming()
Assert.IsNotNull(OpenAIClient.ChatEndpoint);
var messages = new List<Message>
{
new(Role.System, "You are a helpful weather assistant.\n\r- Always prompt the user for their location."),
new(Role.System, "You are a helpful weather assistant. Always prompt the user for their location."),
new(Role.User, "What's the weather like today?"),
};

Expand All @@ -209,7 +210,7 @@ public async Task Test_02_02_GetChatToolCompletion_Streaming()
}

var tools = Tool.GetAllAvailableTools(false);
var chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto");
var chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "none");
var response = await OpenAIClient.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse =>
{
Assert.IsNotNull(partialResponse);
Expand All @@ -236,7 +237,7 @@ public async Task Test_02_02_GetChatToolCompletion_Streaming()
Assert.IsTrue(response.Choices.Count == 1);
messages.Add(response.FirstChoice.Message);

if (!string.IsNullOrEmpty(response.ToString()))
if (response.FirstChoice.FinishReason == "stop")
{
Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishReason}");

Expand All @@ -256,6 +257,7 @@ public async Task Test_02_02_GetChatToolCompletion_Streaming()
}

Assert.IsTrue(response.FirstChoice.FinishReason == "tool_calls");
Assert.IsTrue(response.FirstChoice.Message.ToolCalls.Count == 1);
var usedTool = response.FirstChoice.Message.ToolCalls[0];
Assert.IsNotNull(usedTool);
Assert.IsTrue(usedTool.Function.Name.Contains(nameof(WeatherService.GetCurrentWeatherAsync)));
Expand All @@ -266,7 +268,7 @@ public async Task Test_02_02_GetChatToolCompletion_Streaming()
messages.Add(new Message(usedTool, functionResult));
Console.WriteLine($"{Role.Tool}: {functionResult}");

chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto");
chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "none");
response = await OpenAIClient.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse =>
{
Assert.IsNotNull(partialResponse);
Expand All @@ -282,7 +284,7 @@ public async Task Test_02_03_ChatCompletion_Multiple_Tools_Streaming()
Assert.IsNotNull(OpenAIClient.ChatEndpoint);
var messages = new List<Message>
{
new(Role.System, "You are a helpful weather assistant.\n\r - Use the appropriate unit based on geographical location."),
new(Role.System, "You are a helpful weather assistant. Use the appropriate unit based on geographical location."),
new(Role.User, "What's the weather like today in Los Angeles, USA and Tokyo, Japan?"),
};

Expand All @@ -309,7 +311,7 @@ public async Task Test_02_03_ChatCompletion_Multiple_Tools_Streaming()
messages.Add(new Message(toolCall, output));
}

chatRequest = new ChatRequest(messages, model: "gpt-4-turbo-preview", tools: tools, toolChoice: "auto");
chatRequest = new ChatRequest(messages, model: "gpt-4-turbo-preview", tools: tools, toolChoice: "none");
response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest);

Assert.IsNotNull(response);
Expand All @@ -331,7 +333,7 @@ public async Task Test_02_04_GetChatToolForceCompletion()
}

var tools = Tool.GetAllAvailableTools(false, forceUpdate: true, clearCache: true);
var chatRequest = new ChatRequest(messages, tools: tools);
var chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "none");
var response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest);
Assert.IsNotNull(response);
Assert.IsNotNull(response.Choices);
Expand All @@ -356,6 +358,7 @@ public async Task Test_02_04_GetChatToolForceCompletion()
messages.Add(response.FirstChoice.Message);

Assert.IsTrue(response.FirstChoice.FinishReason == "stop");
Assert.IsTrue(response.FirstChoice.Message.ToolCalls.Count == 1);
var usedTool = response.FirstChoice.Message.ToolCalls[0];
Assert.IsNotNull(usedTool);
Assert.IsTrue(usedTool.Function.Name.Contains(nameof(WeatherService.GetCurrentWeatherAsync)));
Expand Down
14 changes: 11 additions & 3 deletions OpenAI-DotNet-Tests/TestFixture_12_Threads.cs
Original file line number Diff line number Diff line change
Expand Up @@ -376,14 +376,22 @@ public async Task Test_07_01_SubmitToolOutput()
}

var toolCall = run.RequiredAction.SubmitToolOutputs.ToolCalls[0];
Assert.IsTrue(run.RequiredAction.SubmitToolOutputs.ToolCalls.Count == 1);
Assert.AreEqual("function", toolCall.Type);
Assert.IsNotNull(toolCall.FunctionCall);
Assert.IsTrue(toolCall.FunctionCall.Name.Contains(nameof(WeatherService.GetCurrentWeatherAsync)));
Assert.IsNotNull(toolCall.FunctionCall.Arguments);
Console.WriteLine($"tool call arguments: {toolCall.FunctionCall.Arguments}");
var toolOutput = await testAssistant.GetToolOutputAsync(toolCall);
Console.WriteLine($"tool call output: {toolOutput.Output}");
run = await run.SubmitToolOutputsAsync(toolOutput);

// Invoke all the tool call functions and return the tool outputs.
var toolOutputs = await testAssistant.GetToolOutputsAsync(run.RequiredAction.SubmitToolOutputs.ToolCalls);

foreach (var toolOutput in toolOutputs)
{
Console.WriteLine($"tool output: {toolOutput}");
}

run = await run.SubmitToolOutputsAsync(toolOutputs);
// waiting while run in Queued and InProgress
run = await run.WaitForStatusChangeAsync();
Assert.AreEqual(RunStatus.Completed, run.Status);
Expand Down
6 changes: 4 additions & 2 deletions OpenAI-DotNet-Tests/TestServices/WeatherService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ internal enum WeatherUnit
Fahrenheit
}

[Function("Get the current weather in a given location")]
public static async Task<string> GetCurrentWeatherAsync(string location, WeatherUnit unit)
[Function("Get the current weather in a given location.")]
public static async Task<string> GetCurrentWeatherAsync(
[FunctionParameter("The location the user is currently in.")] string location,
[FunctionParameter("The units the use has requested temperature in. Typically this is based on the users location.")] WeatherUnit unit)
{
var temp = new Random().Next(-10, 40);

Expand Down
10 changes: 5 additions & 5 deletions OpenAI-DotNet/Common/Function.cs
Original file line number Diff line number Diff line change
Expand Up @@ -253,12 +253,12 @@ public string Invoke()
}

var result = Invoke<object>();
return JsonSerializer.Serialize(new { result });
return JsonSerializer.Serialize(new { result }, OpenAIClient.JsonSerializationOptions);
}
catch (Exception e)
{
Console.WriteLine(e);
return JsonSerializer.Serialize(new { error = e.Message });
return JsonSerializer.Serialize(new { error = e.Message }, OpenAIClient.JsonSerializationOptions);
}
}

Expand Down Expand Up @@ -305,12 +305,12 @@ public async Task<string> InvokeAsync(CancellationToken cancellationToken = defa
}

var result = await InvokeAsync<object>(cancellationToken);
return JsonSerializer.Serialize(new { result });
return JsonSerializer.Serialize(new { result }, OpenAIClient.JsonSerializationOptions);
}
catch (Exception e)
{
Console.WriteLine(e);
return JsonSerializer.Serialize(new { error = e.Message });
return JsonSerializer.Serialize(new { error = e.Message }, OpenAIClient.JsonSerializationOptions);
}
}

Expand Down Expand Up @@ -384,7 +384,7 @@ public async Task<T> InvokeAsync<T>(CancellationToken cancellationToken = defaul
}
else if (value is string @enum && parameter.ParameterType.IsEnum)
{
invokeArgs[i] = Enum.Parse(parameter.ParameterType, @enum);
invokeArgs[i] = Enum.Parse(parameter.ParameterType, @enum, true);
}
else if (value is JsonElement element)
{
Expand Down
17 changes: 17 additions & 0 deletions OpenAI-DotNet/Common/FunctionParameterAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System;

namespace OpenAI
{
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class FunctionParameterAttribute : Attribute
{
public FunctionParameterAttribute(string description)
{
Description = description;
}

public string Description { get; }
}
}
7 changes: 7 additions & 0 deletions OpenAI-DotNet/Extensions/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ public static JsonObject GenerateJsonSchema(this MethodInfo methodInfo)
}

schema["properties"]![parameter.Name] = GenerateJsonSchema(parameter.ParameterType);

var functionParameterAttribute = parameter.GetCustomAttribute<FunctionParameterAttribute>();

if (functionParameterAttribute != null)
{
schema["properties"]![parameter.Name]!["description"] = functionParameterAttribute.Description;
}
}

if (requiredParameters.Count > 0)
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 @@ -28,12 +28,14 @@ More context [on Roger Pincombe's blog](https://rogerpincombe.com/openai-dotnet-
<AssemblyOriginatorKeyFile>OpenAI-DotNet.pfx</AssemblyOriginatorKeyFile>
<IncludeSymbols>True</IncludeSymbols>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
<Version>7.7.1</Version>
<Version>7.7.2</Version>
<PackageReleaseNotes>
Version 7.7.2
- Added FunctionParameterAttribute to help better inform the feature how to format the Function json
Version 7.7.1
- More Function utilities and invoking methods
- Added FunctionPropertyAttribute to help better inform the feature how to format the Function json
- Added FromFunc-&gt;,-&lt; overloads for convenance
- Added FromFunc&lt;,&gt; overloads for convenance
- Fixed invoke args sometimes being casting to wrong type
- Added additional protections for static and instanced function calls
- Added additional tool utilities:
Expand Down
53 changes: 35 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -766,23 +766,30 @@ Console.WriteLine($"Modify run {run.Id} -> {run.Metadata["key"]}");

##### [Thread Submit Tool Outputs to Run](https://platform.openai.com/docs/api-reference/runs/submitToolOutputs)

When a run has the status: `requires_action` and `required_action.type` is `submit_tool_outputs`, this endpoint can be used to submit the outputs from the tool calls once they're all completed. All outputs must be submitted in a single request.
When a run has the status: `requires_action` and `required_action.type` is `submit_tool_outputs`, this endpoint can be used to submit the outputs from the tool calls once they're all completed.
All outputs must be submitted in a single request.

```csharp
using var api = new OpenAIClient();
var tools = new List<Tool>
{
// Use a predefined tool
Tool.Retrieval,
Tool.Retrieval, Tool.CodeInterpreter,
// Or create a tool from a type and the name of the method you want to use for function calling
Tool.GetOrCreateTool(typeof(WeatherService), nameof(WeatherService.GetCurrentWeatherAsync))
Tool.GetOrCreateTool(typeof(WeatherService), nameof(WeatherService.GetCurrentWeatherAsync)),
// Pass in an instance of an object to call a method on it
Tool.GetOrCreateTool(OpenAIClient.ImagesEndPoint, nameof(ImagesEndpoint.GenerateImageAsync))),
// Define func<,> callbacks
Tool.FromFunc("name_of_func", () => { /* callback function */ }),
Tool.FromFunc<T1,T2,TResult>("func_with_multiple_params", (t1, t2) => { /* logic that calculates return value */ return tResult; })
};
var assistantRequest = new CreateAssistantRequest(tools: tools, instructions: "You are a helpful weather assistant. Use the appropriate unit based on geographical location.");
var testAssistant = await OpenAIClient.AssistantsEndpoint.CreateAssistantAsync(assistantRequest);
var run = await testAssistant.CreateThreadAndRunAsync("I'm in Kuala-Lumpur, please tell me what's the temperature now?");
// waiting while run is Queued and InProgress
run = await run.WaitForStatusChangeAsync();
// Invoke all of the tool call functions and return the tool outputs.

// Invoke all the tool call functions and return the tool outputs.
var toolOutputs = await testAssistant.GetToolOutputsAsync(run.RequiredAction.SubmitToolOutputs.ToolCalls);

foreach (var toolOutput in toolOutputs)
Expand Down Expand Up @@ -913,13 +920,11 @@ Console.WriteLine(cumulativeDelta);

#### [Chat Tools](https://platform.openai.com/docs/guides/function-calling)

> Only available with the latest 0613 model series!
```csharp
using var api = new OpenAIClient();
var messages = new List<Message>
{
new Message(Role.System, "You are a helpful weather assistant."),
new(Role.System, "You are a helpful weather assistant. Always prompt the user for their location."),
new Message(Role.User, "What's the weather like today?"),
};

Expand All @@ -929,7 +934,14 @@ foreach (var message in messages)
}

// Define the tools that the assistant is able to use:
var tools = Tool.GetAllAvailableTools(includeDefaults: false);
// 1. Get a list of all the static methods decorated with FunctionAttribute
var tools = Tool.GetAllAvailableTools(includeDefaults: false, forceUpdate: true, clearCache: true);
// 2. Define a custom list of tools:
var tools = new List<Tool>
{
Tool.GetOrCreateTool(objectInstance, "TheNameOfTheMethodToCall"),
Tool.FromFunc("a_custom_name_for_your_function", ()=> { /* Some logic to run */ })
};
var chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto");
var response = await api.ChatEndpoint.GetCompletionAsync(chatRequest);
messages.Add(response.FirstChoice.Message);
Expand All @@ -944,24 +956,29 @@ response = await api.ChatEndpoint.GetCompletionAsync(chatRequest);

messages.Add(response.FirstChoice.Message);

if (!string.IsNullOrEmpty(response.ToString()))
if (response.FirstChoice.FinishReason == "stop")
{
Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishReason}");

var unitMessage = new Message(Role.User, "celsius");
var unitMessage = new Message(Role.User, "Fahrenheit");
messages.Add(unitMessage);
Console.WriteLine($"{unitMessage.Role}: {unitMessage.Content}");
chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto");
response = await api.ChatEndpoint.GetCompletionAsync(chatRequest);
}

var usedTool = response.FirstChoice.Message.ToolCalls[0];
Console.WriteLine($"{response.FirstChoice.Message.Role}: {usedTool.Function.Name} | Finish Reason: {response.FirstChoice.FinishReason}");
Console.WriteLine($"{usedTool.Function.Arguments}");
// Invoke the used tool to get the function result!
var functionResult = await usedTool.InvokeFunctionAsync();
messages.Add(new Message(usedTool, functionResult));
Console.WriteLine($"{Role.Tool}: {functionResult}");
// iterate over all tool calls and invoke them
foreach (var toolCall in response.FirstChoice.Message.ToolCalls)
{
Console.WriteLine($"{response.FirstChoice.Message.Role}: {toolCall.Function.Name} | Finish Reason: {response.FirstChoice.FinishReason}");
Console.WriteLine($"{toolCall.Function.Arguments}");
// Invokes function to get a generic json result to return for tool call.
var functionResult = await toolCall.InvokeFunctionAsync();
// If you know the return type and do additional processing you can use generic overload
var functionResult = await toolCall.InvokeFunctionAsync<string>();
messages.Add(new Message(toolCall, functionResult));
Console.WriteLine($"{Role.Tool}: {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
Expand All @@ -971,7 +988,7 @@ Console.WriteLine($"{Role.Tool}: {functionResult}");
// "location": "Glasgow, Scotland",
// "unit": "celsius"
// }
// Tool: The current weather in Glasgow, Scotland is 20 celsius
// Tool: The current weather in Glasgow, Scotland is 39°C.
```

#### [Chat Vision](https://platform.openai.com/docs/guides/vision)
Expand Down

0 comments on commit ab8773d

Please sign in to comment.