Skip to content

Commit

Permalink
com.openai.unity 7.7.0 (#194)
Browse files Browse the repository at this point in the history
- Added `Tool` call and `Function` call Utilities and helper methods
- Added `FunctionAttribute` to decorate methods to be identified and used in function calling
- `Chat.Message.ToolCalls` can be directly invoked using `Function.Invoke()` or `Function.InvokeAsync(CancellationToken)`
- Assistant tool call outputs can be easily generated using `assistnat.GetToolOutputAsync(run.RequiredAction.SubmitToolOutputs.ToolCalls)`
  - Check updated docs for more details and examples
- Fixed `ChatRequest` seed parameter not being set correctly when using tools
  • Loading branch information
StephenHodgson authored Feb 22, 2024
1 parent 817978b commit 0aa5d79
Show file tree
Hide file tree
Showing 39 changed files with 689 additions and 360 deletions.
94 changes: 30 additions & 64 deletions Documentation~/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ Create an assistant with a model and instructions.

```csharp
var api = new OpenAIClient();
var request = new CreateAssistantRequest("gpt-3.5-turbo-1106");
var request = new CreateAssistantRequest("gpt-3.5-turbo");
var assistant = await api.AssistantsEndpoint.CreateAssistantAsync(request);
```

Expand All @@ -417,10 +417,10 @@ Modifies an assistant.

```csharp
var api = new OpenAIClient();
var createRequest = new CreateAssistantRequest("gpt-3.5-turbo-1106");
var createRequest = new CreateAssistantRequest("gpt-3.5-turbo");
var assistant = await api.AssistantsEndpoint.CreateAssistantAsync(createRequest);
var modifyRequest = new CreateAssistantRequest("gpt-4-1106-preview");
var modifiedAssistant = await api.AssistantsEndpoint.ModifyAsync(assistant.Id, modifyRequest);
var modifyRequest = new CreateAssistantRequest("gpt-4-turbo-preview");
var modifiedAssistant = await api.AssistantsEndpoint.ModifyAssistantAsync(assistant.Id, modifyRequest);
// OR AssistantExtension for easier use!
var modifiedAssistantEx = await assistant.ModifyAsync(modifyRequest);
```
Expand Down Expand Up @@ -549,7 +549,7 @@ var assistant = await api.AssistantsEndpoint.CreateAssistantAsync(
new CreateAssistantRequest(
name: "Math Tutor",
instructions: "You are a personal math tutor. Answer questions briefly, in a sentence or less.",
model: "gpt-4-1106-preview"));
model: "gpt-4-turbo-preview"));
var messages = new List<Message> { "I need to solve the equation `3x + 11 = 14`. Can you help me?" };
var threadRequest = new CreateThreadRequest(messages);
var run = await assistant.CreateThreadAndRunAsync(threadRequest);
Expand Down Expand Up @@ -725,7 +725,7 @@ var assistant = await api.AssistantsEndpoint.CreateAssistantAsync(
new CreateAssistantRequest(
name: "Math Tutor",
instructions: "You are a personal math tutor. Answer questions briefly, in a sentence or less.",
model: "gpt-4-1106-preview"));
model: "gpt-4-turbo-preview"));
var thread = await api.ThreadsEndpoint.CreateThreadAsync();
var message = await thread.CreateMessageAsync("I need to solve the equation `3x + 11 = 14`. Can you help me?");
var run = await thread.CreateRunAsync(assistant);
Expand Down Expand Up @@ -769,37 +769,27 @@ When a run has the status: `requires_action` and `required_action.type` is `subm

```csharp
var api = new OpenAIClient();
var function = new Function(
nameof(WeatherService.GetCurrentWeather),
"Get the current weather in a given location",
new JObject
{
["type"] = "object",
["properties"] = new JObject
{
["location"] = new JObject
{
["type"] = "string",
["description"] = "The city and state, e.g. San Francisco, CA"
},
["unit"] = new JObject
{
["type"] = "string",
["enum"] = new JArray { "celsius", "fahrenheit" }
}
},
["required"] = new JArray { "location", "unit" }
});
testAssistant = await api.AssistantsEndpoint.CreateAssistantAsync(new CreateAssistantRequest(tools: new Tool[] { function }));
var run = await testAssistant.CreateThreadAndRunAsync("I'm in Kuala-Lumpur, please tell me what's the temperature in celsius now?");
var tools = new List<Tool>
{
// Use a predefined tool
Tool.Retrieval,
// 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))
};
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();
var toolCall = run.RequiredAction.SubmitToolOutputs.ToolCalls[0];
Debug.Log($"tool call arguments: {toolCall.FunctionCall.Arguments}");
var functionArgs = JsonConvert.DeserializeObject<WeatherArgs>(toolCall.FunctionCall.Arguments);
var functionResult = WeatherService.GetCurrentWeather(functionArgs);
var toolOutput = new ToolOutput(toolCall.Id, functionResult);
run = await run.SubmitToolOutputsAsync(toolOutput);
// Invoke all of the tool call functions and return the tool outputs.
var toolOutputs = await testAssistant.GetToolOutputsAsync(run.RequiredAction.SubmitToolOutputs.ToolCalls);

foreach (var toolOutput in toolOutputs)
{
Debug.Log($"tool call output: {toolOutput.Output}");
}
// submit the tool outputs
run = await run.SubmitToolOutputsAsync(toolOutputs);
// waiting while run in Queued and InProgress
run = await run.WaitForStatusChangeAsync();
var messages = await run.ListMessagesAsync();
Expand Down Expand Up @@ -890,7 +880,7 @@ var messages = new List<Message>
var chatRequest = new ChatRequest(messages);
var response = await api.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse =>
{
Console.Write(partialResponse.FirstChoice.Delta.ToString());
Debug.Log(partialResponse.FirstChoice.Delta.ToString());
});
var choice = response.FirstChoice;
Debug.Log($"[{choice.Index}] {choice.Message.Role}: {choice.Message} | Finish Reason: {choice.FinishReason}");
Expand All @@ -914,31 +904,7 @@ foreach (var message in messages)
}

// Define the tools that the assistant is able to use:
var tools = new List<Tool>
{
new Function(
nameof(WeatherService.GetCurrentWeather),
"Get the current weather in a given location",
new JObject
{
["type"] = "object",
["properties"] = new JObject
{
["location"] = new JObject
{
["type"] = "string",
["description"] = "The city and state, e.g. San Francisco, CA"
},
["unit"] = new JObject
{
["type"] = "string",
["enum"] = new JArray {"celsius", "fahrenheit"}
}
},
["required"] = new JArray { "location", "unit" }
})
};

var tools = Tool.GetAllAvailableTools(includeDefaults: false);
var chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto");
var response = await api.ChatEndpoint.GetCompletionAsync(chatRequest);
messages.Add(response.FirstChoice.Message);
Expand Down Expand Up @@ -967,8 +933,8 @@ if (!string.IsNullOrEmpty(response.ToString()))
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}");
var functionArgs = JsonConvert.DeserializeObject<WeatherArgs>(usedTool.Function.Arguments.ToString());
var functionResult = WeatherService.GetCurrentWeather(functionArgs);
// 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}");
// System: You are a helpful weather assistant.
Expand Down Expand Up @@ -1038,7 +1004,7 @@ var messages = new List<Message>
new Message(Role.System, "You are a helpful assistant designed to output JSON."),
new Message(Role.User, "Who won the world series in 2020?"),
};
var chatRequest = new ChatRequest(messages, "gpt-4-1106-preview", responseFormat: ChatResponseFormat.Json);
var chatRequest = new ChatRequest(messages, "gpt-4-turbo-preview", responseFormat: ChatResponseFormat.Json);
var response = await api.ChatEndpoint.GetCompletionAsync(chatRequest);

foreach (var choice in response.Choices)
Expand Down
2 changes: 1 addition & 1 deletion Editor/OpenAIDashboard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ private static void RenderTrainingJobQueue()

private static ListResponse<FineTuneJobResponse> fineTuneJobList;
private static int trainingJobCount = 25;
private static readonly Stack<string> trainingJobIds = new Stack<string>();
private static readonly Stack<string> trainingJobIds = new();

private static async void FetchTrainingJobs(string trainingJobId = null)
{
Expand Down
79 changes: 78 additions & 1 deletion Runtime/Assistants/AssistantExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

using OpenAI.Files;
using OpenAI.Threads;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

Expand Down Expand Up @@ -92,7 +95,7 @@ public static async Task<AssistantFileResponse> UploadFileAsync(this AssistantRe
public static async Task<AssistantFileResponse> UploadFileAsync(this AssistantResponse assistant, Stream stream, string fileName, CancellationToken cancellationToken = default)
{
var file = await assistant.Client.FilesEndpoint.UploadFileAsync(new FileUploadRequest(stream, fileName, "assistants"), uploadProgress: null, cancellationToken);
return await assistant.AttachFileAsync(file, cancellationToken).ConfigureAwait(false);
return await assistant.AttachFileAsync(file, cancellationToken);
}

/// <summary>
Expand Down Expand Up @@ -173,5 +176,79 @@ public static async Task<bool> DeleteFileAsync(this AssistantResponse assistant,
}

#endregion Files

#region Tools

/// <summary>
/// Invoke the assistant's tool function using the <see cref="ToolCall"/>.
/// </summary>
/// <param name="assistant"><see cref="AssistantResponse"/>.</param>
/// <param name="toolCall"><see cref="ToolCall"/>.</param>
/// <returns>Tool output result as <see cref="string"/></returns>
public static string InvokeToolCall(this AssistantResponse assistant, ToolCall toolCall)
{
var tool = assistant.Tools.FirstOrDefault(tool => toolCall.Type == "function" && tool.Function.Name == toolCall.FunctionCall.Name) ??
throw new InvalidOperationException($"Failed to find a valid tool for [{toolCall.Id}] {toolCall.Type}");
tool.Function.Arguments = toolCall.FunctionCall.Arguments;
return tool.InvokeFunction();
}

/// <summary>
/// Invoke the assistant's tool function using the <see cref="ToolCall"/>.
/// </summary>
/// <param name="assistant"><see cref="AssistantResponse"/>.</param>
/// <param name="toolCall"><see cref="ToolCall"/>.</param>
/// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param>
/// <returns>Tool output result as <see cref="string"/></returns>
public static async Task<string> InvokeToolCallAsync(this AssistantResponse assistant, ToolCall toolCall, CancellationToken cancellationToken = default)
{
var tool = assistant.Tools.FirstOrDefault(tool => toolCall.Type == "function" && tool.Function.Name == toolCall.FunctionCall.Name) ??
throw new InvalidOperationException($"Failed to find a valid tool for [{toolCall.Id}] {toolCall.Type}");
tool.Function.Arguments = toolCall.FunctionCall.Arguments;
return await tool.InvokeFunctionAsync(cancellationToken);
}

/// <summary>
/// Calls the tool's function, with the provided arguments from the toolCall and returns the output.
/// </summary>
/// <param name="assistant"><see cref="AssistantResponse"/>.</param>
/// <param name="toolCall"><see cref="ToolCall"/>.</param>
/// <returns><see cref="ToolOutput"/>.</returns>
public static ToolOutput GetToolOutput(this AssistantResponse assistant, ToolCall toolCall)
=> new(toolCall.Id, assistant.InvokeToolCall(toolCall));

/// <summary>
/// Calls each tool's function, with the provided arguments from the toolCalls and returns the outputs.
/// </summary>
/// <param name="assistant"><see cref="AssistantResponse"/>.</param>
/// <param name="toolCalls">A collection of <see cref="ToolCall"/>s.</param>
/// <returns>A collection of <see cref="ToolOutput"/>s.</returns>
public static IReadOnlyList<ToolOutput> GetToolOutputs(this AssistantResponse assistant, IEnumerable<ToolCall> toolCalls)
=> toolCalls.Select(assistant.GetToolOutput).ToList();

/// <summary>
/// Calls the tool's function, with the provided arguments from the toolCall and returns the output.
/// </summary>
/// <param name="assistant"><see cref="AssistantResponse"/>.</param>
/// <param name="toolCall"><see cref="ToolCall"/>.</param>
/// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param>
/// <returns><see cref="ToolOutput"/>.</returns>
public static async Task<ToolOutput> GetToolOutputAsync(this AssistantResponse assistant, ToolCall toolCall, CancellationToken cancellationToken = default)
{
var output = await assistant.InvokeToolCallAsync(toolCall, cancellationToken);
return new ToolOutput(toolCall.Id, output);
}

/// <summary>
/// Calls each tool's function, with the provided arguments from the toolCalls and returns the outputs.
/// </summary>
/// <param name="assistant"><see cref="AssistantResponse"/>.</param>
/// <param name="toolCalls">A collection of <see cref="ToolCall"/>s.</param>
/// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param>
/// <returns>A collection of <see cref="ToolOutput"/>s.</returns>
public static async Task<IReadOnlyList<ToolOutput>> GetToolOutputsAsync(this AssistantResponse assistant, IEnumerable<ToolCall> toolCalls, CancellationToken cancellationToken = default)
=> await Task.WhenAll(toolCalls.Select(async toolCall => await assistant.GetToolOutputAsync(toolCall, cancellationToken))).ConfigureAwait(true);

#endregion Tools
}
}
2 changes: 1 addition & 1 deletion Runtime/Audio/AudioEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ internal AudioEndpoint(OpenAIClient client) : base(client) { }
/// <inheritdoc />
protected override string Root => "audio";

private static readonly object mutex = new object();
private static readonly object mutex = new();

/// <summary>
/// Generates audio from the input text.
Expand Down
2 changes: 1 addition & 1 deletion Runtime/Authentication/OpenAIAuthentication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public sealed class OpenAIAuthentication : AbstractAuthentication<OpenAIAuthenti
/// Allows implicit casting from a string, so that a simple string API key can be provided in place of an instance of <see cref="OpenAIAuthentication"/>.
/// </summary>
/// <param name="key">The API key to convert into a <see cref="OpenAIAuthentication"/>.</param>
public static implicit operator OpenAIAuthentication(string key) => new OpenAIAuthentication(key);
public static implicit operator OpenAIAuthentication(string key) => new(key);

/// <summary>
/// Instantiates an empty Authentication object.
Expand Down
14 changes: 4 additions & 10 deletions Runtime/Chat/ChatRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ public ChatRequest(
int? number = null,
double? presencePenalty = null,
ChatResponseFormat responseFormat = ChatResponseFormat.Text,
int? seed = null,
string[] stops = null,
double? temperature = null,
double? topP = null,
int? topLogProbs = null,
string user = null)
: this(messages, model, frequencyPenalty, logitBias, maxTokens, number, presencePenalty, responseFormat, number, stops, temperature, topP, topLogProbs, user)
: this(messages, model, frequencyPenalty, logitBias, maxTokens, number, presencePenalty, responseFormat, seed, stops, temperature, topP, topLogProbs, user)
{
var tooList = tools?.ToList();

Expand All @@ -45,15 +46,8 @@ public ChatRequest(
if (!toolChoice.Equals("none") &&
!toolChoice.Equals("auto"))
{
var tool = new JObject
{
["type"] = "function",
["function"] = new JObject
{
["name"] = toolChoice
}
};
ToolChoice = tool;
var tool = tooList.FirstOrDefault(t => t.Function.Name.Contains(toolChoice));
ToolChoice = tool ?? throw new ArgumentException($"The specified tool choice '{toolChoice}' was not found in the list of tools");
}
else
{
Expand Down
6 changes: 3 additions & 3 deletions Runtime/Chat/Content.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,12 @@ public Content(ContentType type, string input)
public ImageUrl ImageUrl { get; private set; }

[Preserve]
public static implicit operator Content(string input) => new Content(ContentType.Text, input);
public static implicit operator Content(string input) => new(ContentType.Text, input);

[Preserve]
public static implicit operator Content(ImageUrl imageUrl) => new Content(imageUrl);
public static implicit operator Content(ImageUrl imageUrl) => new(imageUrl);

[Preserve]
public static implicit operator Content(Texture2D texture) => new Content(texture);
public static implicit operator Content(Texture2D texture) => new(texture);
}
}
4 changes: 2 additions & 2 deletions Runtime/Chat/ResponseFormat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ public sealed class ResponseFormat
public static implicit operator ChatResponseFormat(ResponseFormat format) => format.Type;

[Preserve]
public static implicit operator ResponseFormat(ChatResponseFormat format) => new ResponseFormat(format);
public static implicit operator ResponseFormat(ChatResponseFormat format) => new(format);
}
}
}
2 changes: 1 addition & 1 deletion Runtime/Common/Event.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ public Event(
[JsonProperty("message")]
public string Message { get; }

public static implicit operator EventResponse(Event @event) => new EventResponse(@event);
public static implicit operator EventResponse(Event @event) => new(@event);
}
}
Loading

0 comments on commit 0aa5d79

Please sign in to comment.