hiddenhatpress/openai-assistants

A quick and dirty client for OpenAI's assistants API

v1.0.7 2024-04-21 20:31 UTC

This package is auto-updated.

Last update: 2024-04-21 20:31:57 UTC


README

A quick and dirty client for OpenAI's assistants API.

NOTE

Although this package is still maintained for now (I'm using it in Shelley), the Assistants API is now included in the full OpenAI Client at orhanerday/open-ai. You might want to use that for a much wider feature set. This library plugged a gap that has now been filled. That said, read on for a worked example of file retrieval with the OpenAI Assistants API.

OpenAI API docs

Installation

The easiest way to get this code is via Composer:

composer require hiddenhatpress/openai-assistants

Overview

use hiddenhatpress\openai\assistants\Assistants;
use hiddenhatpress\openai\assistants\AsstComms;

$token = getenv('OPENAI_API_KEY');
// this model needed for retrieval tools
$model = "gpt-4-1106-preview";

$asstcomms = new AsstComms($model, $token);
$assistants = new Assistants($asstcomms);

The Assistants class is really only a factory for objects which provide thin client access to the assistant, thread. messages and runs APIs.

$asstservice = $assistants->getAssistantService();
$fileservice  = $assistants->getAssistantFileService();
$threadservice = $assistants->getThreadService();
$runservice = $assistants->getRunService();
$messageservice = $assistants->getMessageService();

Quick start

The basic workflow for the Assistants API is:

  1. Create an assistant
  2. Optionally add files to the assistant
  3. Create a thread (so that different users can use the assistant through your interface)
  4. Create a message representing a user's query and add it to thread
  5. Run the thread
  6. Poll the status of the the run until its status is completed
  7. Get the latest message (the system's response) from the thread and return to the user
  8. Repeat from step 4 as needed

We're going to create an assistant to help us read the letters of Pliny the Younger.

Access assistants

First, let's check that we haven't already created an assistant named pliny-assistant:

// will get 20 by default
$entities = $asstservice->list();
$assistantid = null;
$name = "pliny-assistant";

foreach ($entities['data'] as $asst) {
    if ($asst['name'] == $name) {
        $assistantid = $asst['id'];
    }
}

NOTE because the list endpoint returns 20 elements by default, this approach would not scale if you had more than 20 asssistants. In a robust system you'd likely have stored an assistant id. If you wanted to create a reliable version of this dynamic name-based logic you'd need to page through the data. list() supports limit -- up to 100 -- as well as before and after fields.

Create an assistant and upload a file

For a first run, we'll need to actually create the assistant and upload a source file (the text version of Pliny's letters saved as pliny.txt).

if (empty($assistantid)) {
    // create the assistant
    $asstresp = $asstservice->create(
         $name,
         "You are an ancient history assistant specialising in Pliny the Younger",
         ["retrieval"] 
    );
    $assistantid = $asstresp['id'];

    // upload file
    $fileresp = $fileservice->createAndAssignAssistantFile($assistantid, "pliny.txt" );
}

The arguments to create() are a name, a set of instructions, and a list of tool types. These can be code_interpreter, retrieval, or function. We are creating a retrieval assistant -- that is, an assistant specialised in working with texts we provide. We're giving it a historical text -- but the assistant would likely come into its own interpreting files that the model has not already been trained on -- a novel-in-progress perhaps, or corporate documents.

The AssistantFile class accesses the file aspect of the assistants API and the File API. So createAndAssignAssistantFile() uploads a given file and then associates it with an assistant.

Now we have an assistant with access to the text we are interested in. Let's try asking it a question.

Setting up a message for running

In order to send a message we need to create a thread and add a message to it.

// create a thread
$threadresp = $threadservice->create();   
$threadid = $threadresp['id'];

// create a message and add to the thread
$content = "Discuss the ways that Pliny talks about fish in his letters.";
$msgresp = $messageservice->create($threadid, $content);

Running the thread to send the message

Next, we need to tell the API to run the thread. We use the runs API for this.

$runresp = $runservice->create($threadid, $assistantid);
while($runresp['status'] != "completed") {
    sleep(1);
    print "# polling {$runresp['status']}\n";
    $runresp = $runservice->retrieve($threadid, $runresp['id']);
}

Because the service does not block, we need to poll it until it the run status is completed.

Accessing the latest message from the thread

We can list the messages using the messages API.

// access the response
$msgs = $messageservice->listMessages($threadid);
print $msgs['data'][0]['content'][0]['text']['value'];
print "\n";

By default, messages are returned in descending order, so the first element will be the latest.

Some output

Let's run the code and get some ancient fish news.

# polling queued
# polling in_progress
# polling in_progress
...

Pliny the Younger mentioned fish in the context of his letters to highlight certain aspects or qualities of his environment or surroundings:

  1. He discusses the offerings of his local sea and expresses a somewhat limited pride in its bounty. He says, "I cannot boast that our sea is plentiful in choice fish" but then goes on to recognize that it does provide for "capital soles and prawns." This indicates a modest abundance of certain kinds of fish, and he contrasts this with the abundant provisions of other types, such as milk, which he proudly notes his villa's ability to excel in even when compared to inland places【7†source】.

...

Tidying up: unassign and delete assistant files

In real world code, we would not usually build an assistant only to tear it down again at the end of our process. We'd be more likely to establish an assistant and use it over time, creating new threads for new users. These threads might also persist for some time.

Here, however, we want to leave things as we found them. First, let's delete the file we uploaded.

We can get an assistant's file ids from the assistants API. In this example, we have access to this data already, but let's assume we only have an assistant id to hand.

// get the files from the assistant
$files = $fileservice->listAssistantFiles($assistantid);
foreach ($files['data'] as $finfo) {
    // unassign and delete
    $fileservice->unassignAndDeleteAssistantFile($assistantid, $finfo['id']);
}

AssistantFiles::listAssistantFiles() gives us an array of associated files. We can use the id field of each with unassignAndDeleteAssistantFile() to remove the association between assistant and file and then delete the file from the repository.

Tidying up: delete the assistant

Finally, we delete the assistant altogether.

// delete the assistant
$del = $asstservice->del($assistantid);