Skip to content

Commit 19e5bab

Browse files
com.openai.unity 7.7.2 (#198)
- Added FunctionParameterAttribute to help better inform the feature how to format the Function json
1 parent 3db1aeb commit 19e5bab

File tree

11 files changed

+105
-46
lines changed

11 files changed

+105
-46
lines changed

Documentation~/README.md

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -765,22 +765,29 @@ Debug.Log($"Modify run {run.Id} -> {run.Metadata["key"]}");
765765

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

768-
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.
768+
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.
769+
All outputs must be submitted in a single request.
769770

770771
```csharp
771772
var api = new OpenAIClient();
772773
var tools = new List<Tool>
773774
{
774775
// Use a predefined tool
775-
Tool.Retrieval,
776+
Tool.Retrieval, Tool.CodeInterpreter,
776777
// Or create a tool from a type and the name of the method you want to use for function calling
777-
Tool.GetOrCreateTool(typeof(WeatherService), nameof(WeatherService.GetCurrentWeatherAsync))
778+
Tool.GetOrCreateTool(typeof(WeatherService), nameof(WeatherService.GetCurrentWeatherAsync)),
779+
// Pass in an instance of an object to call a method on it
780+
Tool.GetOrCreateTool(OpenAIClient.ImagesEndPoint, nameof(ImagesEndpoint.GenerateImageAsync))),
781+
// Define func<,> callbacks
782+
Tool.FromFunc("name_of_func", () => { /* callback function */ }),
783+
Tool.FromFunc<T1,T2,TResult>("func_with_multiple_params", (t1, t2) => { /* logic that calculates return value */ return tResult; })
778784
};
779785
var assistantRequest = new CreateAssistantRequest(tools: tools, instructions: "You are a helpful weather assistant. Use the appropriate unit based on geographical location.");
780786
var testAssistant = await OpenAIClient.AssistantsEndpoint.CreateAssistantAsync(assistantRequest);
781787
var run = await testAssistant.CreateThreadAndRunAsync("I'm in Kuala-Lumpur, please tell me what's the temperature now?");
782788
// waiting while run is Queued and InProgress
783789
run = await run.WaitForStatusChangeAsync();
790+
784791
// Invoke all of the tool call functions and return the tool outputs.
785792
var toolOutputs = await testAssistant.GetToolOutputsAsync(run.RequiredAction.SubmitToolOutputs.ToolCalls);
786793

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

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

891-
> Only available with the latest 0613 model series!
892-
893898
```csharp
894899
var api = new OpenAIClient();
895900
var messages = new List<Message>
896901
{
897-
new Message(Role.System, "You are a helpful weather assistant."),
902+
new(Role.System, "You are a helpful weather assistant. Always prompt the user for their location."),
898903
new Message(Role.User, "What's the weather like today?"),
899904
};
900905

@@ -904,7 +909,14 @@ foreach (var message in messages)
904909
}
905910

906911
// Define the tools that the assistant is able to use:
907-
var tools = Tool.GetAllAvailableTools(includeDefaults: false);
912+
// 1. Get a list of all the static methods decorated with FunctionAttribute
913+
var tools = Tool.GetAllAvailableTools(includeDefaults: false, forceUpdate: true, clearCache: true);
914+
// 2. Define a custom list of tools:
915+
var tools = new List<Tool>
916+
{
917+
Tool.GetOrCreateTool(objectInstance, "TheNameOfTheMethodToCall"),
918+
Tool.FromFunc("a_custom_name_for_your_function", ()=> { /* Some logic to run */ })
919+
};
908920
var chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto");
909921
var response = await api.ChatEndpoint.GetCompletionAsync(chatRequest);
910922
messages.Add(response.FirstChoice.Message);
@@ -919,24 +931,29 @@ response = await api.ChatEndpoint.GetCompletionAsync(chatRequest);
919931

920932
messages.Add(response.FirstChoice.Message);
921933

922-
if (!string.IsNullOrEmpty(response.ToString()))
934+
if (response.FirstChoice.FinishReason == "stop")
923935
{
924936
Debug.Log($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishReason}");
925937

926-
var unitMessage = new Message(Role.User, "celsius");
938+
var unitMessage = new Message(Role.User, "Fahrenheit");
927939
messages.Add(unitMessage);
928940
Debug.Log($"{unitMessage.Role}: {unitMessage.Content}");
929941
chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto");
930942
response = await api.ChatEndpoint.GetCompletionAsync(chatRequest);
931943
}
932944

933-
var usedTool = response.FirstChoice.Message.ToolCalls[0];
934-
Debug.Log($"{response.FirstChoice.Message.Role}: {usedTool.Function.Name} | Finish Reason: {response.FirstChoice.FinishReason}");
935-
Debug.Log($"{usedTool.Function.Arguments}");
936-
// Invoke the used tool to get the function result!
937-
var functionResult = await usedTool.InvokeFunctionAsync();
938-
messages.Add(new Message(usedTool, functionResult));
939-
Debug.Log($"{Role.Tool}: {functionResult}");
945+
// iterate over all tool calls and invoke them
946+
foreach (var toolCall in response.FirstChoice.Message.ToolCalls)
947+
{
948+
Debug.Log($"{response.FirstChoice.Message.Role}: {toolCall.Function.Name} | Finish Reason: {response.FirstChoice.FinishReason}");
949+
Debug.Log($"{toolCall.Function.Arguments}");
950+
// Invokes function to get a generic json result to return for tool call.
951+
var functionResult = await toolCall.InvokeFunctionAsync();
952+
// If you know the return type and do additional processing you can use generic overload
953+
var functionResult = await toolCall.InvokeFunctionAsync<string>();
954+
messages.Add(new Message(toolCall, functionResult));
955+
Debug.Log($"{Role.Tool}: {functionResult}");
956+
}
940957
// System: You are a helpful weather assistant.
941958
// User: What's the weather like today?
942959
// Assistant: Sure, may I know your current location? | Finish Reason: stop
@@ -946,7 +963,7 @@ Debug.Log($"{Role.Tool}: {functionResult}");
946963
// "location": "Glasgow, Scotland",
947964
// "unit": "celsius"
948965
// }
949-
// Tool: The current weather in Glasgow, Scotland is 20 celsius
966+
// Tool: The current weather in Glasgow, Scotland is 39°C.
950967
```
951968

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

Runtime/Chat/ChatEndpoint.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,6 @@ public async Task<ChatResponse> StreamCompletionAsync(ChatRequest chatRequest, A
5151
{
5252
try
5353
{
54-
if (EnableDebug)
55-
{
56-
Debug.Log(eventData);
57-
}
58-
5954
var partialResponse = JsonConvert.DeserializeObject<ChatResponse>(eventData, OpenAIClient.JsonSerializationOptions);
6055

6156
if (chatResponse == null)

Runtime/Common/Function.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ public JToken Arguments
196196
if (arguments == null &&
197197
!string.IsNullOrWhiteSpace(argumentsString))
198198
{
199-
arguments = JToken.FromObject(argumentsString);
199+
arguments = JToken.FromObject(argumentsString, JsonSerializer.Create(OpenAIClient.JsonSerializationOptions));
200200
}
201201

202202
return arguments;
@@ -264,12 +264,12 @@ public string Invoke()
264264
}
265265

266266
var result = Invoke<object>();
267-
return JsonConvert.SerializeObject(new { result });
267+
return JsonConvert.SerializeObject(new { result }, OpenAIClient.JsonSerializationOptions);
268268
}
269269
catch (Exception e)
270270
{
271271
Debug.LogException(e);
272-
return JsonConvert.SerializeObject(new { error = e.Message });
272+
return JsonConvert.SerializeObject(new { error = e.Message }, OpenAIClient.JsonSerializationOptions);
273273
}
274274
}
275275

@@ -320,12 +320,12 @@ public async Task<string> InvokeAsync(CancellationToken cancellationToken = defa
320320
}
321321

322322
var result = await InvokeAsync<object>(cancellationToken);
323-
return JsonConvert.SerializeObject(new { result });
323+
return JsonConvert.SerializeObject(new { result }, OpenAIClient.JsonSerializationOptions);
324324
}
325325
catch (Exception e)
326326
{
327327
Debug.LogException(e);
328-
return JsonConvert.SerializeObject(new { error = e.Message });
328+
return JsonConvert.SerializeObject(new { error = e.Message }, OpenAIClient.JsonSerializationOptions);
329329
}
330330
}
331331

@@ -402,11 +402,11 @@ public async Task<T> InvokeAsync<T>(CancellationToken cancellationToken = defaul
402402
}
403403
else if (value is string @enum && parameter.ParameterType.IsEnum)
404404
{
405-
invokeArgs[i] = Enum.Parse(parameter.ParameterType, @enum);
405+
invokeArgs[i] = Enum.Parse(parameter.ParameterType, @enum, true);
406406
}
407407
else if (value is JObject json)
408408
{
409-
invokeArgs[i] = json.ToObject(parameter.ParameterType);
409+
invokeArgs[i] = json.ToObject(parameter.ParameterType, JsonSerializer.Create(OpenAIClient.JsonSerializationOptions));
410410
}
411411
else
412412
{
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Licensed under the MIT License. See LICENSE in the project root for license information.
2+
3+
using System;
4+
5+
namespace OpenAI
6+
{
7+
[AttributeUsage(AttributeTargets.Parameter)]
8+
public sealed class FunctionParameterAttribute : Attribute
9+
{
10+
public FunctionParameterAttribute(string description)
11+
{
12+
Description = description;
13+
}
14+
15+
public string Description { get; }
16+
}
17+
}

Runtime/Common/FunctionParameterAttribute.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Runtime/Common/FunctionPropertyAttribute.cs.meta

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Runtime/Extensions/TypeExtensions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ public static JObject GenerateJsonSchema(this MethodInfo methodInfo)
4545
}
4646

4747
schema["properties"]![parameter.Name] = GenerateJsonSchema(parameter.ParameterType);
48+
49+
var functionParameterAttribute = parameter.GetCustomAttribute<FunctionParameterAttribute>();
50+
51+
if (functionParameterAttribute != null)
52+
{
53+
schema["properties"]![parameter.Name]!["description"] = functionParameterAttribute.Description;
54+
}
4855
}
4956

5057
if (requiredParameters.Count > 0)

Tests/TestFixture_03_Chat.cs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public async Task Test_02_01_GetChatToolCompletion()
106106

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

@@ -116,7 +116,7 @@ public async Task Test_02_01_GetChatToolCompletion()
116116
}
117117

118118
var tools = Tool.GetAllAvailableTools(false, forceUpdate: true, clearCache: true);
119-
var chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto");
119+
var chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "none");
120120
var response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest);
121121
Assert.IsNotNull(response);
122122
Assert.IsNotNull(response.Choices);
@@ -152,6 +152,7 @@ public async Task Test_02_01_GetChatToolCompletion()
152152
}
153153

154154
Assert.IsTrue(response.FirstChoice.FinishReason == "tool_calls");
155+
Assert.IsTrue(response.FirstChoice.Message.ToolCalls.Count == 1);
155156
var usedTool = response.FirstChoice.Message.ToolCalls[0];
156157
Assert.IsNotNull(usedTool);
157158
Assert.IsTrue(usedTool.Function.Name.Contains(nameof(WeatherService.GetCurrentWeatherAsync)));
@@ -161,7 +162,7 @@ public async Task Test_02_01_GetChatToolCompletion()
161162
Assert.IsNotNull(functionResult);
162163
messages.Add(new Message(usedTool, functionResult));
163164
Debug.Log($"{Role.Tool}: {functionResult}");
164-
chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto");
165+
chatRequest = new ChatRequest(messages);
165166
response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest);
166167
Debug.Log(response);
167168
}
@@ -172,7 +173,7 @@ public async Task Test_02_02_GetChatToolCompletion_Streaming()
172173
Assert.IsNotNull(OpenAIClient.ChatEndpoint);
173174
var messages = new List<Message>
174175
{
175-
new(Role.System, "You are a helpful weather assistant.\n\r- Always prompt the user for their location."),
176+
new(Role.System, "You are a helpful weather assistant. Always ask the user for their location."),
176177
new(Role.User, "What's the weather like today?"),
177178
};
178179

@@ -182,7 +183,7 @@ public async Task Test_02_02_GetChatToolCompletion_Streaming()
182183
}
183184

184185
var tools = Tool.GetAllAvailableTools(false);
185-
var chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto");
186+
var chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "none");
186187
var response = await OpenAIClient.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse =>
187188
{
188189
Assert.IsNotNull(partialResponse);
@@ -209,7 +210,7 @@ public async Task Test_02_02_GetChatToolCompletion_Streaming()
209210
Assert.IsTrue(response.Choices.Count == 1);
210211
messages.Add(response.FirstChoice.Message);
211212

212-
if (!string.IsNullOrEmpty(response.ToString()))
213+
if (response.FirstChoice.FinishReason == "stop")
213214
{
214215
Debug.Log($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishReason}");
215216

@@ -229,6 +230,7 @@ public async Task Test_02_02_GetChatToolCompletion_Streaming()
229230
}
230231

231232
Assert.IsTrue(response.FirstChoice.FinishReason == "tool_calls");
233+
Assert.IsTrue(response.FirstChoice.Message.ToolCalls.Count == 1);
232234
var usedTool = response.FirstChoice.Message.ToolCalls[0];
233235
Assert.IsNotNull(usedTool);
234236
Assert.IsTrue(usedTool.Function.Name.Contains(nameof(WeatherService.GetCurrentWeatherAsync)));
@@ -239,7 +241,7 @@ public async Task Test_02_02_GetChatToolCompletion_Streaming()
239241
messages.Add(new Message(usedTool, functionResult));
240242
Debug.Log($"{Role.Tool}: {functionResult}");
241243

242-
chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto");
244+
chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "none");
243245
response = await OpenAIClient.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse =>
244246
{
245247
Assert.IsNotNull(partialResponse);
@@ -255,7 +257,7 @@ public async Task Test_02_03_ChatCompletion_Multiple_Tools_Streaming()
255257
Assert.IsNotNull(OpenAIClient.ChatEndpoint);
256258
var messages = new List<Message>
257259
{
258-
new(Role.System, "You are a helpful weather assistant.\n\r - Use the appropriate unit based on geographical location."),
260+
new(Role.System, "You are a helpful weather assistant. Use the appropriate unit based on geographical location."),
259261
new(Role.User, "What's the weather like today in Los Angeles, USA and Tokyo, Japan?"),
260262
};
261263

@@ -282,7 +284,7 @@ public async Task Test_02_03_ChatCompletion_Multiple_Tools_Streaming()
282284
messages.Add(new Message(toolCall, output));
283285
}
284286

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

288290
Assert.IsNotNull(response);
@@ -304,7 +306,7 @@ public async Task Test_02_04_GetChatToolForceCompletion()
304306
}
305307

306308
var tools = Tool.GetAllAvailableTools(false, forceUpdate: true, clearCache: true);
307-
var chatRequest = new ChatRequest(messages, tools: tools);
309+
var chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "none");
308310
var response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest);
309311
Assert.IsNotNull(response);
310312
Assert.IsNotNull(response.Choices);

Tests/TestFixture_12_Threads.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -377,14 +377,22 @@ public async Task Test_07_01_SubmitToolOutput()
377377
}
378378

379379
var toolCall = run.RequiredAction.SubmitToolOutputs.ToolCalls[0];
380+
Assert.IsTrue(run.RequiredAction.SubmitToolOutputs.ToolCalls.Count == 1);
380381
Assert.AreEqual("function", toolCall.Type);
381382
Assert.IsNotNull(toolCall.FunctionCall);
382383
Assert.IsTrue(toolCall.FunctionCall.Name.Contains(nameof(WeatherService.GetCurrentWeatherAsync)));
383384
Assert.IsNotNull(toolCall.FunctionCall.Arguments);
384-
Debug.Log($"tool call arguments: {toolCall.FunctionCall.Arguments}");
385-
var toolOutput = await testAssistant.GetToolOutputAsync(toolCall);
386-
Debug.Log($"tool call output: {toolOutput.Output}");
387-
run = await run.SubmitToolOutputsAsync(toolOutput);
385+
Console.WriteLine($"tool call arguments: {toolCall.FunctionCall.Arguments}");
386+
387+
// Invoke all the tool call functions and return the tool outputs.
388+
var toolOutputs = await testAssistant.GetToolOutputsAsync(run.RequiredAction.SubmitToolOutputs.ToolCalls);
389+
390+
foreach (var toolOutput in toolOutputs)
391+
{
392+
Console.WriteLine($"tool output: {toolOutput}");
393+
}
394+
395+
run = await run.SubmitToolOutputsAsync(toolOutputs);
388396
// waiting while run in Queued and InProgress
389397
run = await run.WaitForStatusChangeAsync();
390398
Assert.AreEqual(RunStatus.Completed, run.Status);

Tests/Weather/WeatherService.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ internal enum WeatherUnit
1414
}
1515

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

0 commit comments

Comments
 (0)