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

OpenAI-DotNet 7.7.2 #250

Merged
merged 2 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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