Skip to content

Completed README additions for Assistants API #404

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Dec 12, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 161 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ Assistants can call models to interact with threads and use tools to perform tas

To create a new assistant (see [API documentation](https://platform.openai.com/docs/api-reference/assistants/createAssistant)):

```
```ruby
response = client.assistants.create(
parameters: {
model: "gpt-3.5-turbo-1106", # Retrieve via client.models.list. Assistants need 'gpt-3.5-turbo-1106' or later.
Expand All @@ -475,19 +475,19 @@ assistant_id = response["id"]

Given an `assistant_id` you can `retrieve` the current field values:

```
```ruby
client.assistants.retrieve(id: assistant_id)
```

You can get a `list` of all assistants currently available under the organization:

```
```ruby
client.assistants.list
```

You can modify an existing assistant using the assistant's id (see [API documentation](https://platform.openai.com/docs/api-reference/assistants/modifyAssistant)):

```
```ruby
response = client.assistants.modify(
id: assistant_id,
parameters: {
Expand All @@ -502,6 +502,163 @@ You can delete assistants:
client.assistants.delete(id: assistant_id)
```

### Threads and Messages

Once you have created an assistant as described above, you need to prepare a `Thread` of `Messages` for the assistant to work on (see [introduction on Assistants](https://platform.openai.com/docs/assistants/how-it-works)). For example, as an initial setup you could do:

```ruby
# Create thread
response = client.threads.create # Note: Once you create a thread, there is no way to list it
# or recover it currently (as of 2023-12-10). So hold onto the `id`
thread_id = response["id"]

# Add initial message from user (see https://platform.openai.com/docs/api-reference/messages/createMessage)
message_id = client.messages.create(
thread_id: thread_id,
parameters: {
role: "user", # Required for manually created messages
content: "Can you help me write an API library to interact with the OpenAI API please?"
})["id"]

# Retrieve individual message
message = client.messages.retrieve(thread_id: thread_id, id: message_id)

# Review all messages on the thread
messages = client.messages.list(thread_id: thread_id)
```

To clean up after a thread is no longer needed:

```ruby
# To delete the thread (and all associated messages):
client.threads.delete(id: thread_id)

client.messages.retrieve(thread_id: thread_id, id: message_id) # -> Fails after thread is deleted
```


### Runs

To submit a thread to be evaluated with the model of an assistant, create a `Run` as follows (Note: This is one place where OpenAI will take your money):

```ruby
# Create run (will use instruction/model/tools from Assistant's definition)
response = client.runs.create(thread_id: thread_id,
parameters: {
assistant_id: assistant_id
})
run_id = response['id']

# Retrieve/poll Run to observe status
response = client.runs.retrieve(id: run_id, thread_id: thread_id)
status = response['status']
```

The `status` response can include the following strings `queued`, `in_progress`, `requires_action`, `cancelling`, `cancelled`, `failed`, `completed`, or `expired` which you can handle as follows:

```ruby
while true do

response = client.runs.retrieve(id: run_id, thread_id: thread_id)
status = response['status']

case status
when 'queued', 'in_progress', 'cancelling'
puts 'Sleeping'
sleep 1 # Wait one second and poll again
when 'completed'
break # Exit loop and report result to user
when 'requires_action'
# Handle tool calls (see below)
when 'cancelled', 'failed', 'expired'
puts response['last_error'].inspect
break # or `exit`
else
puts "Unknown status response: #{status}"
end
end
```

If the `status` response indicates that the `run` is `completed`, the associated `thread` will have one or more new `messages` attached:

```ruby
# Either retrieve all messages in bulk again, or...
messages = client.messages.list(thread_id: thread_id) # Note: as of 2023-12-11 adding limit or order options isn't working, yet

# Alternatively retrieve the `run steps` for the run which link to the messages:
run_steps = client.run_steps.list(thread_id: thread_id, run_id: run_id)
new_message_ids = run_steps['data'].filter_map { |step|
if step['type'] == 'message_creation'
step.dig('step_details', "message_creation", "message_id")
end # Ignore tool calls, because they don't create new messages.
}

# Retrieve the individual messages
new_messages = new_message_ids.map { |msg_id|
client.messages.retrieve(id: msg_id, thread_id: thread_id)
}

# Find the actual response text in the content array of the messages
new_messages.each { |msg|
msg['content'].each { |content_item|
case content_item['type']
when 'text'
puts content_item.dig('text', 'value')
# Also handle annotations
when 'image_file'
# Use File endpoint to retrieve file contents via id
id = content_item.dig('image_file', 'file_id')
end
}
}
```

At any time you can list all runs which have been performed on a particular thread or are currently running (in descending/newest first order):

```ruby
client.runs.list(thread_id: thread_id)
```

#### Runs involving function tools

In case you are allowing the assistant to access `function` tools (they are defined in the same way as functions during chat completion), you might get a status code of `requires_action` when the assistant wants you to evaluate one or more function tools:

```ruby
def get_current_weather(location:, unit: "celsius")
# Your function code goes here
if location =~ /San Francisco/i
return unit == "celsius" ? "The weather is nice 🌞 at 27°C" : "The weather is nice 🌞 at 80°F"
else
return unit == "celsius" ? "The weather is icy 🥶 at -5°C" : "The weather is icy 🥶 at 23°F"
end
end

if status == 'requires_action'

tools_to_call = response.dig('required_action', 'submit_tool_outputs', 'tool_calls')

my_tool_outputs = tools_to_call.map { |tool|
# Call the functions based on the tool's name
function_name = tool.dig('function', 'name')
arguments = JSON.parse(
tool.dig("function", "arguments"),
{ symbolize_names: true },
)

tool_output = case function_name
when "get_current_weather"
get_current_weather(**arguments)
end

{ tool_call_id: tool['id'], output: tool_output }
}

client.runs.submit_tool_outputs(thread_id: thread_id, run_id: run_id, parameters: { tool_outputs: my_tool_outputs })
end
```

Note that you have 10 minutes to submit your tool output before the run expires.

### Image Generation

Generate an image using DALL·E! The size of any generated images must be one of `256x256`, `512x512` or `1024x1024` -
Expand Down