Skip to content

Commit

Permalink
com.openai.unity 7.7.2 (#198)
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 3db1aeb commit 19e5bab
Show file tree
Hide file tree
Showing 11 changed files with 105 additions and 46 deletions.
51 changes: 34 additions & 17 deletions Documentation~/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -765,22 +765,29 @@ Debug.Log($"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
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.
var toolOutputs = await testAssistant.GetToolOutputsAsync(run.RequiredAction.SubmitToolOutputs.ToolCalls);

Expand Down Expand Up @@ -888,13 +895,11 @@ Debug.Log($"[{choice.Index}] {choice.Message.Role}: {choice.Message} | Finish Re

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

> Only available with the latest 0613 model series!
```csharp
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 @@ -904,7 +909,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 @@ -919,24 +931,29 @@ response = await api.ChatEndpoint.GetCompletionAsync(chatRequest);

messages.Add(response.FirstChoice.Message);

if (!string.IsNullOrEmpty(response.ToString()))
if (response.FirstChoice.FinishReason == "stop")
{
Debug.Log($"{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);
Debug.Log($"{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];
Debug.Log($"{response.FirstChoice.Message.Role}: {usedTool.Function.Name} | Finish Reason: {response.FirstChoice.FinishReason}");
Debug.Log($"{usedTool.Function.Arguments}");
// Invoke the used tool to get the function result!
var functionResult = await usedTool.InvokeFunctionAsync();
messages.Add(new Message(usedTool, functionResult));
Debug.Log($"{Role.Tool}: {functionResult}");
// iterate over all tool calls and invoke them
foreach (var toolCall in response.FirstChoice.Message.ToolCalls)
{
Debug.Log($"{response.FirstChoice.Message.Role}: {toolCall.Function.Name} | Finish Reason: {response.FirstChoice.FinishReason}");
Debug.Log($"{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));
Debug.Log($"{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 @@ -946,7 +963,7 @@ Debug.Log($"{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
5 changes: 0 additions & 5 deletions Runtime/Chat/ChatEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,6 @@ public async Task<ChatResponse> StreamCompletionAsync(ChatRequest chatRequest, A
{
try
{
if (EnableDebug)
{
Debug.Log(eventData);
}

var partialResponse = JsonConvert.DeserializeObject<ChatResponse>(eventData, OpenAIClient.JsonSerializationOptions);

if (chatResponse == null)
Expand Down
14 changes: 7 additions & 7 deletions Runtime/Common/Function.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ public JToken Arguments
if (arguments == null &&
!string.IsNullOrWhiteSpace(argumentsString))
{
arguments = JToken.FromObject(argumentsString);
arguments = JToken.FromObject(argumentsString, JsonSerializer.Create(OpenAIClient.JsonSerializationOptions));
}

return arguments;
Expand Down Expand Up @@ -264,12 +264,12 @@ public string Invoke()
}

var result = Invoke<object>();
return JsonConvert.SerializeObject(new { result });
return JsonConvert.SerializeObject(new { result }, OpenAIClient.JsonSerializationOptions);
}
catch (Exception e)
{
Debug.LogException(e);
return JsonConvert.SerializeObject(new { error = e.Message });
return JsonConvert.SerializeObject(new { error = e.Message }, OpenAIClient.JsonSerializationOptions);
}
}

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

var result = await InvokeAsync<object>(cancellationToken);
return JsonConvert.SerializeObject(new { result });
return JsonConvert.SerializeObject(new { result }, OpenAIClient.JsonSerializationOptions);
}
catch (Exception e)
{
Debug.LogException(e);
return JsonConvert.SerializeObject(new { error = e.Message });
return JsonConvert.SerializeObject(new { error = e.Message }, OpenAIClient.JsonSerializationOptions);
}
}

Expand Down Expand Up @@ -402,11 +402,11 @@ 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 JObject json)
{
invokeArgs[i] = json.ToObject(parameter.ParameterType);
invokeArgs[i] = json.ToObject(parameter.ParameterType, JsonSerializer.Create(OpenAIClient.JsonSerializationOptions));
}
else
{
Expand Down
17 changes: 17 additions & 0 deletions Runtime/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; }
}
}
11 changes: 11 additions & 0 deletions Runtime/Common/FunctionParameterAttribute.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Runtime/Common/FunctionPropertyAttribute.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions Runtime/Extensions/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ public static JObject 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
22 changes: 12 additions & 10 deletions Tests/TestFixture_03_Chat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,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 @@ -116,7 +116,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 @@ -152,6 +152,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 @@ -161,7 +162,7 @@ public async Task Test_02_01_GetChatToolCompletion()
Assert.IsNotNull(functionResult);
messages.Add(new Message(usedTool, functionResult));
Debug.Log($"{Role.Tool}: {functionResult}");
chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto");
chatRequest = new ChatRequest(messages);
response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest);
Debug.Log(response);
}
Expand All @@ -172,7 +173,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 ask the user for their location."),
new(Role.User, "What's the weather like today?"),
};

Expand All @@ -182,7 +183,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 @@ -209,7 +210,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")
{
Debug.Log($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishReason}");

Expand All @@ -229,6 +230,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 @@ -239,7 +241,7 @@ public async Task Test_02_02_GetChatToolCompletion_Streaming()
messages.Add(new Message(usedTool, functionResult));
Debug.Log($"{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 @@ -255,7 +257,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 @@ -282,7 +284,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 @@ -304,7 +306,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 Down
16 changes: 12 additions & 4 deletions Tests/TestFixture_12_Threads.cs
Original file line number Diff line number Diff line change
Expand Up @@ -377,14 +377,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);
Debug.Log($"tool call arguments: {toolCall.FunctionCall.Arguments}");
var toolOutput = await testAssistant.GetToolOutputAsync(toolCall);
Debug.Log($"tool call output: {toolOutput.Output}");
run = await run.SubmitToolOutputsAsync(toolOutput);
Console.WriteLine($"tool call arguments: {toolCall.FunctionCall.Arguments}");

// 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
4 changes: 3 additions & 1 deletion Tests/Weather/WeatherService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ internal enum WeatherUnit
}

[Function("Get the current weather in a given location")]
public static async Task<string> GetCurrentWeatherAsync(string location, WeatherUnit unit)
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"displayName": "OpenAI",
"description": "A OpenAI package for the Unity Game Engine to use GPT-4, GPT-3.5, GPT-3 and Dall-E though their RESTful API (currently in beta).\n\nIndependently developed, this is not an official library and I am not affiliated with OpenAI.\n\nAn OpenAI API account is required.",
"keywords": [],
"version": "7.7.1",
"version": "7.7.2",
"unity": "2021.3",
"documentationUrl": "https://github.com/RageAgainstThePixel/com.openai.unity#documentation",
"changelogUrl": "https://github.com/RageAgainstThePixel/com.openai.unity/releases",
Expand Down

0 comments on commit 19e5bab

Please sign in to comment.