RageAgainstThePixel / com.openai.unity

A Non-Official OpenAI Rest Client for Unity (UPM)
https://openai.com
MIT License
462 stars 64 forks source link

Custom Assistant in Sample ChatBehaviour #162

Open chameleon-training opened 9 months ago

chameleon-training commented 9 months ago

I have been searching for a long time until I came across your code. I am looking for a way to use the assistant I trained with OpenAI via the API in Unity. I installed the package and got an overview with the sample. I am really impressed with your work. I have tried to extend the sample so that I can access my assistant, but unfortunately, I failed. Can you tell me what I need to adjust in the sample?

I have studied your documentation, but unfortunately, I'm struggling to get started as the topic of assistant + threads together is very complicated.

It would be a dream if you could extend the Sample ChatBehaviour so that one could enter the ID of their own assistant.

If that's too much to ask, I can absolutely understand. In that case, it would at least be helpful if you could explain the implementation startup in the documentation a bit more for dummies. ;-)

StephenHodgson commented 9 months ago

Tbh I haven't had much of a chance to play with the new API myself in any meaningful capacity.

I'll see what I can do about adding a new sample for assistants api specifically.

neohun commented 9 months ago

I have used assistants recently. The following should get you going. Edit: the code is edited for a better flexibility, clarity and robustness.


public class OpenAIEx {

    static readonly string apiKey = "yourApiKey";

    /// <summary> Create an assistant thread and run for the message. </summary>
    /// <param name="message"> The input message that you want to send to assistant. </param>
    /// <param name="assistantID"> Assistant ID </param>
    /// <returns> Run response when it's complete. </returns> 
    public static async Task<RunResponse> AssistantThread(string message, string assistantID) {
        // use try-catch for possible errors. 
        try {
            var api = new OpenAIClient(apiKey);
            // retrieve the assistant. 
            var assistant = await api.AssistantsEndpoint.RetrieveAssistantAsync(assistantID);
            // create a thread as async
            var thread = await api.ThreadsEndpoint.CreateThreadAsync();
            // create the message in the thread as async. 
            _ = await thread.CreateMessageAsync(message);

            // run the thread to return a response
            var run = await thread.CreateRunAsync(assistant);
            // write the run status to console just for clarity. 
            Debug.Log($"[{run.Id}] {run.Status} | {run.CreatedAt}");

            //  wait for run to complete to return the result. 
            run = await run.WaitForRunCompleteAsync();
            // return the run response to retrieve the message. 
            return run;
        }
        catch (Exception e) {
            Debug.LogException(e);
            return null; // return null when an error occurs.
        }
    }

}

public static class OpenAI_Extensions {

    public static async Task<RunResponse> WaitForRunCompleteAsync(this RunResponse run) {
        // wait while it is running. 
        while (run.IsRunning()) {
            run = await run.WaitForStatusChangeAsync();
            // debug to see steps
            Debug.Log($"[{run.Id}] status: {run.Status} | {run.CreatedAt}");
        }
        // return the response. 
        return run;
    }

    /// <summary> check whether it's still running. </summary>
    static bool IsRunning(this RunResponse runResponse) {
        // check whether it is running still. 
        return (runResponse.Status == RunStatus.Queued
             || runResponse.Status == RunStatus.InProgress
             || runResponse.Status == RunStatus.Cancelling);
    }

}

// *** Example Usage ***
async void Run() {

    string inputMessage = "your message";
    // pass your message and assistantID to get the response. 
    var result = await OpenAIEx.AssistantThread(inputMessage, "yourAssistantID");
    // check whether the status is completed to process the result.
    if (result.Status == RunStatus.Completed) {
        // list the messages from the result. 
        var messages = await result.ListMessagesAsync();
        // reverse the list because the first message is the last response. 
        var allMessages = messages.Items.Reverse().ToList();

        // write all messages for the run. 
        foreach (var message in allMessages) {
            // write the message role and content to console. 
            Debug.Log($"{message.Role}: {message.PrintContent()}");
        }
    }
    else {
        Debug.Log($"Assistant Run Failed | Status: -> {result.Status}"); 
    }

}
neohun commented 9 months ago

Also I want to point a minor issue while I'm here. It's about "Items" property of "ListResponse" class which is used for parsing "data" of the response but I think it causes confusion because I was following some tutorials which use python API and it's named as "data" there so I had to look all properties one by one. I think another property named "data" can be created and "Items" can be made obsolete just for the sake of clarity and parity with Python API.

StephenHodgson commented 9 months ago

Just some notes about example:

Probably better not to use a static class since you may need different endpoints, especially for Azure.

It's generally frowned upon to discard your tasks _ = await Run.Task; because you may not properly bubble up errors and handle exceptions correctly.

Instead always use try/catch, especially in unity, so that exceptions are handled properly and don't get stuck in the task queue and execute continuously.

chameleon-training commented 9 months ago

Thank you guys, I'll try to integrate my assistant based on that. This will definitely help me...You guys are awesome!

neohun commented 9 months ago

It's generally frowned upon to discard your tasks _ = await Run.Task; because you may not properly bubble up errors and handle exceptions correctly.

The code was just an example but you're right so I have fixed the problems you have pointed out.. But about _ discards I have tested and there was no problem when using them because when an error is thrown by an awaited function, try-catch works for the internal errors even if a discard is used. Discard is just for the clarity to indicate that the return value is not used to avoid confusion.

Also you can add the example for documentation if you want so other people who want to use the assistants can benefit from it.

Thanks for the library by the way It has saved us from bunch of troubles (:

neohun commented 9 months ago

Thank you guys, I'll try to integrate my assistant based on that. This will definitely help me...You guys are awesome!

You are welcome, good luck.

StephenHodgson commented 9 months ago

But about _ discards I have tested and there was no problem when using them because when an error is thrown by an awaited function, try-catch works for the internal errors even if a discard is used. Discard is just for the clarity to indicate that the return value is not used to avoid confusion.

I'm telling you from experience, don't use them in the Unity runtime environment. It's bad practice. Instead use async void for a fire and forget method, and wrap it with a try /catch.

If it was pure C#, I'd have no problem with it.