ruby-openai 6.5.0 → 7.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +2 -1
- data/CHANGELOG.md +31 -2
- data/Gemfile +1 -1
- data/Gemfile.lock +15 -15
- data/README.md +214 -61
- data/lib/openai/assistants.rb +1 -1
- data/lib/openai/batches.rb +23 -0
- data/lib/openai/client.rb +9 -5
- data/lib/openai/files.rb +36 -4
- data/lib/openai/http.rb +10 -4
- data/lib/openai/messages.rb +2 -2
- data/lib/openai/run_steps.rb +2 -2
- data/lib/openai/runs.rb +6 -2
- data/lib/openai/version.rb +1 -1
- data/lib/openai.rb +10 -9
- data/ruby-openai.gemspec +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8334c2f0658ff33f39e96e8316b5c7122ca8810f5bfaca945c8d5f1add38b85a
|
4
|
+
data.tar.gz: b761ed842f7c27ba7f3e6e3137f6d60f2d51cb1a32f585989fd2b1992d256b46
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7c53448def0a2a9849744a09c10f91fb4c16eb23fbf0d52be748574a9a96b05cb5641821c7728b9d95764cf50399f23c48bfdf3444d86ab80e65602e2264f6ba
|
7
|
+
data.tar.gz: 8806bbe03d2cde1b1fdfb691bcc2c4d803837faed6f001027135bdf9ed54a4bdf4eb01128c36d9d84b35622876ec087c494980e67b01a72a8ecb01040180cc72
|
data/.circleci/config.yml
CHANGED
@@ -8,7 +8,7 @@ jobs:
|
|
8
8
|
rubocop:
|
9
9
|
parallelism: 1
|
10
10
|
docker:
|
11
|
-
- image: cimg/ruby:3.
|
11
|
+
- image: cimg/ruby:3.2-node
|
12
12
|
steps:
|
13
13
|
- checkout
|
14
14
|
- ruby/install-deps
|
@@ -43,3 +43,4 @@ workflows:
|
|
43
43
|
- cimg/ruby:3.0-node
|
44
44
|
- cimg/ruby:3.1-node
|
45
45
|
- cimg/ruby:3.2-node
|
46
|
+
- cimg/ruby:3.3-node
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
## [7.0.0] - 2024-04-27
|
9
|
+
|
10
|
+
### Added
|
11
|
+
|
12
|
+
- Add support for Batches, thanks to [@simonx1](https://github.com/simonx1) for the PR!
|
13
|
+
- Allow use of local LLMs like Ollama! Thanks to [@ThomasSevestre](https://github.com/ThomasSevestre)
|
14
|
+
- Update to v2 of the Assistants beta & add documentation on streaming from an Assistant.
|
15
|
+
- Add Assistants endpoint to create and run a thread in one go, thank you [@quocphien90](https://github.com/
|
16
|
+
quocphien90)
|
17
|
+
- Add missing parameters (order, limit, etc) to Runs, RunSteps and Messages - thanks to [@shalecraig](https://github.com/shalecraig) and [@coezbek](https://github.com/coezbek)
|
18
|
+
- Add missing Messages#list spec - thanks [@adammeghji](https://github.com/adammeghji)
|
19
|
+
- Add Messages#modify to README - thanks to [@nas887](https://github.com/nas887)
|
20
|
+
- Don't add the api_version (`/v1/`) to base_uris that already include it - thanks to [@kaiwren](https://github.com/kaiwren) for raising this issue
|
21
|
+
- Allow passing a `StringIO` to Files#upload - thanks again to [@simonx1](https://github.com/simonx1)
|
22
|
+
- Add Ruby 3.3 to CI
|
23
|
+
|
24
|
+
### Security
|
25
|
+
|
26
|
+
- [BREAKING] ruby-openai will no longer log out API errors by default - you can reenable by passing `log_errors: true` to your client. This will help to prevent leaking secrets to logs. Thanks to [@lalunamel](https://github.com/lalunamel) for this PR.
|
27
|
+
|
28
|
+
### Removed
|
29
|
+
|
30
|
+
- [BREAKING] Remove deprecated edits endpoint.
|
31
|
+
|
32
|
+
### Fixed
|
33
|
+
|
34
|
+
- Fix README DALL·E 3 error - thanks to [@clayton](https://github.com/clayton)
|
35
|
+
- Fix README tool_calls error and add missing tool_choice info - thanks to [@Jbrito6492](https://github.com/Jbrito6492)
|
36
|
+
|
8
37
|
## [6.5.0] - 2024-03-31
|
9
38
|
|
10
39
|
### Added
|
@@ -67,13 +96,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
67
96
|
- [BREAKING] Switch from legacy Finetunes to the new Fine-tune-jobs endpoints. Implemented by [@lancecarlson](https://github.com/lancecarlson)
|
68
97
|
- [BREAKING] Remove deprecated Completions endpoints - use Chat instead.
|
69
98
|
|
70
|
-
###
|
99
|
+
### Fixed
|
71
100
|
|
72
101
|
- [BREAKING] Fix issue where :stream parameters were replaced by a boolean in the client application. Thanks to [@martinjaimem](https://github.com/martinjaimem), [@vickymadrid03](https://github.com/vickymadrid03) and [@nicastelo](https://github.com/nicastelo) for spotting and fixing this issue.
|
73
102
|
|
74
103
|
## [5.2.0] - 2023-10-30
|
75
104
|
|
76
|
-
###
|
105
|
+
### Fixed
|
77
106
|
|
78
107
|
- Added more spec-compliant SSE parsing: see here https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation
|
79
108
|
- Fixes issue where OpenAI or an intermediary returns only partial JSON per chunk of streamed data
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
ruby-openai (
|
4
|
+
ruby-openai (7.0.0)
|
5
5
|
event_stream_parser (>= 0.3.0, < 2.0.0)
|
6
6
|
faraday (>= 1)
|
7
7
|
faraday-multipart (>= 1)
|
@@ -16,10 +16,10 @@ GEM
|
|
16
16
|
byebug (11.1.3)
|
17
17
|
crack (0.4.5)
|
18
18
|
rexml
|
19
|
-
diff-lcs (1.5.
|
19
|
+
diff-lcs (1.5.1)
|
20
20
|
dotenv (2.8.1)
|
21
21
|
event_stream_parser (1.0.0)
|
22
|
-
faraday (2.
|
22
|
+
faraday (2.8.1)
|
23
23
|
base64
|
24
24
|
faraday-net_http (>= 2.0, < 3.1)
|
25
25
|
ruby2_keywords (>= 0.0.4)
|
@@ -37,19 +37,19 @@ GEM
|
|
37
37
|
rake (13.1.0)
|
38
38
|
regexp_parser (2.8.0)
|
39
39
|
rexml (3.2.6)
|
40
|
-
rspec (3.
|
41
|
-
rspec-core (~> 3.
|
42
|
-
rspec-expectations (~> 3.
|
43
|
-
rspec-mocks (~> 3.
|
44
|
-
rspec-core (3.
|
45
|
-
rspec-support (~> 3.
|
46
|
-
rspec-expectations (3.
|
40
|
+
rspec (3.13.0)
|
41
|
+
rspec-core (~> 3.13.0)
|
42
|
+
rspec-expectations (~> 3.13.0)
|
43
|
+
rspec-mocks (~> 3.13.0)
|
44
|
+
rspec-core (3.13.0)
|
45
|
+
rspec-support (~> 3.13.0)
|
46
|
+
rspec-expectations (3.13.0)
|
47
47
|
diff-lcs (>= 1.2.0, < 2.0)
|
48
|
-
rspec-support (~> 3.
|
49
|
-
rspec-mocks (3.
|
48
|
+
rspec-support (~> 3.13.0)
|
49
|
+
rspec-mocks (3.13.0)
|
50
50
|
diff-lcs (>= 1.2.0, < 2.0)
|
51
|
-
rspec-support (~> 3.
|
52
|
-
rspec-support (3.
|
51
|
+
rspec-support (~> 3.13.0)
|
52
|
+
rspec-support (3.13.1)
|
53
53
|
rubocop (1.50.2)
|
54
54
|
json (~> 2.3)
|
55
55
|
parallel (~> 1.10)
|
@@ -78,7 +78,7 @@ DEPENDENCIES
|
|
78
78
|
byebug (~> 11.1.3)
|
79
79
|
dotenv (~> 2.8.1)
|
80
80
|
rake (~> 13.1)
|
81
|
-
rspec (~> 3.
|
81
|
+
rspec (~> 3.13)
|
82
82
|
rubocop (~> 1.50.2)
|
83
83
|
ruby-openai!
|
84
84
|
vcr (~> 6.1.0)
|
data/README.md
CHANGED
@@ -22,11 +22,13 @@ Stream text with GPT-4, transcribe and translate audio with Whisper, or create i
|
|
22
22
|
- [With Config](#with-config)
|
23
23
|
- [Custom timeout or base URI](#custom-timeout-or-base-uri)
|
24
24
|
- [Extra Headers per Client](#extra-headers-per-client)
|
25
|
-
- [
|
25
|
+
- [Logging](#logging)
|
26
|
+
- [Errors](#errors)
|
27
|
+
- [Faraday middleware](#faraday-middleware)
|
26
28
|
- [Azure](#azure)
|
29
|
+
- [Ollama](#ollama)
|
27
30
|
- [Counting Tokens](#counting-tokens)
|
28
31
|
- [Models](#models)
|
29
|
-
- [Examples](#examples)
|
30
32
|
- [Chat](#chat)
|
31
33
|
- [Streaming Chat](#streaming-chat)
|
32
34
|
- [Vision](#vision)
|
@@ -34,6 +36,7 @@ Stream text with GPT-4, transcribe and translate audio with Whisper, or create i
|
|
34
36
|
- [Functions](#functions)
|
35
37
|
- [Edits](#edits)
|
36
38
|
- [Embeddings](#embeddings)
|
39
|
+
- [Batches](#batches)
|
37
40
|
- [Files](#files)
|
38
41
|
- [Finetunes](#finetunes)
|
39
42
|
- [Assistants](#assistants)
|
@@ -146,6 +149,7 @@ or when configuring the gem:
|
|
146
149
|
```ruby
|
147
150
|
OpenAI.configure do |config|
|
148
151
|
config.access_token = ENV.fetch("OPENAI_ACCESS_TOKEN")
|
152
|
+
config.log_errors = true # Optional
|
149
153
|
config.organization_id = ENV.fetch("OPENAI_ORGANIZATION_ID") # Optional
|
150
154
|
config.uri_base = "https://oai.hconeai.com/" # Optional
|
151
155
|
config.request_timeout = 240 # Optional
|
@@ -166,7 +170,19 @@ client = OpenAI::Client.new(access_token: "access_token_goes_here")
|
|
166
170
|
client.add_headers("X-Proxy-TTL" => "43200")
|
167
171
|
```
|
168
172
|
|
169
|
-
####
|
173
|
+
#### Logging
|
174
|
+
|
175
|
+
##### Errors
|
176
|
+
|
177
|
+
By default, `ruby-openai` does not log any `Faraday::Error`s encountered while executing a network request to avoid leaking data (e.g. 400s, 500s, SSL errors and more - see [here](https://www.rubydoc.info/github/lostisland/faraday/Faraday/Error) for a complete list of subclasses of `Faraday::Error` and what can cause them).
|
178
|
+
|
179
|
+
If you would like to enable this functionality, you can set `log_errors` to `true` when configuring the client:
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
client = OpenAI::Client.new(log_errors: true)
|
183
|
+
```
|
184
|
+
|
185
|
+
##### Faraday middleware
|
170
186
|
|
171
187
|
You can pass [Faraday middleware](https://lostisland.github.io/faraday/#/middleware/index) to the client in a block, eg. to enable verbose logging with Ruby's [Logger](https://ruby-doc.org/3.2.2/stdlibs/logger/Logger.html):
|
172
188
|
|
@@ -191,6 +207,38 @@ To use the [Azure OpenAI Service](https://learn.microsoft.com/en-us/azure/cognit
|
|
191
207
|
|
192
208
|
where `AZURE_OPENAI_URI` is e.g. `https://custom-domain.openai.azure.com/openai/deployments/gpt-35-turbo`
|
193
209
|
|
210
|
+
#### Ollama
|
211
|
+
|
212
|
+
Ollama allows you to run open-source LLMs, such as Llama 3, locally. It [offers chat compatibility](https://github.com/ollama/ollama/blob/main/docs/openai.md) with the OpenAI API.
|
213
|
+
|
214
|
+
You can download Ollama [here](https://ollama.com/). On macOS you can install and run Ollama like this:
|
215
|
+
|
216
|
+
```bash
|
217
|
+
brew install ollama
|
218
|
+
ollama serve
|
219
|
+
ollama pull llama3:latest # In new terminal tab.
|
220
|
+
```
|
221
|
+
|
222
|
+
Create a client using your Ollama server and the pulled model, and stream a conversation for free:
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
client = OpenAI::Client.new(
|
226
|
+
uri_base: "http://localhost:11434"
|
227
|
+
)
|
228
|
+
|
229
|
+
client.chat(
|
230
|
+
parameters: {
|
231
|
+
model: "llama3", # Required.
|
232
|
+
messages: [{ role: "user", content: "Hello!"}], # Required.
|
233
|
+
temperature: 0.7,
|
234
|
+
stream: proc do |chunk, _bytesize|
|
235
|
+
print chunk.dig("choices", 0, "delta", "content")
|
236
|
+
end
|
237
|
+
})
|
238
|
+
|
239
|
+
# => Hi! It's nice to meet you. Is there something I can help you with, or would you like to chat?
|
240
|
+
```
|
241
|
+
|
194
242
|
### Counting Tokens
|
195
243
|
|
196
244
|
OpenAI parses prompt text into [tokens](https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them), which are words or portions of words. (These tokens are unrelated to your API access_token.) Counting tokens can help you estimate your [costs](https://openai.com/pricing). It can also help you ensure your prompt text size is within the max-token limits of your model's context window, and choose an appropriate [`max_tokens`](https://platform.openai.com/docs/api-reference/chat/create#chat/create-max_tokens) completion parameter so your response will fit as well.
|
@@ -209,24 +257,9 @@ There are different models that can be used to generate text. For a full list an
|
|
209
257
|
|
210
258
|
```ruby
|
211
259
|
client.models.list
|
212
|
-
client.models.retrieve(id: "
|
260
|
+
client.models.retrieve(id: "gpt-3.5-turbo")
|
213
261
|
```
|
214
262
|
|
215
|
-
#### Examples
|
216
|
-
|
217
|
-
- [GPT-4 (limited beta)](https://platform.openai.com/docs/models/gpt-4)
|
218
|
-
- gpt-4 (uses current version)
|
219
|
-
- gpt-4-0314
|
220
|
-
- gpt-4-32k
|
221
|
-
- [GPT-3.5](https://platform.openai.com/docs/models/gpt-3-5)
|
222
|
-
- gpt-3.5-turbo
|
223
|
-
- gpt-3.5-turbo-0301
|
224
|
-
- text-davinci-003
|
225
|
-
- [GPT-3](https://platform.openai.com/docs/models/gpt-3)
|
226
|
-
- text-ada-001
|
227
|
-
- text-babbage-001
|
228
|
-
- text-curie-001
|
229
|
-
|
230
263
|
### Chat
|
231
264
|
|
232
265
|
GPT is a model that can be used to generate text in a conversational style. You can use it to [generate a response](https://platform.openai.com/docs/api-reference/chat/create) to a sequence of [messages](https://platform.openai.com/docs/guides/chat/introduction):
|
@@ -338,9 +371,10 @@ You can stream it as well!
|
|
338
371
|
|
339
372
|
### Functions
|
340
373
|
|
341
|
-
You can describe and pass in functions and the model will intelligently choose to output a JSON object containing arguments to call
|
374
|
+
You can describe and pass in functions and the model will intelligently choose to output a JSON object containing arguments to call them - eg., to use your method `get_current_weather` to get the weather in a given location. Note that tool_choice is optional, but if you exclude it, the model will choose whether to use the function or not ([see here](https://platform.openai.com/docs/api-reference/chat/create#chat-create-tool_choice)).
|
342
375
|
|
343
376
|
```ruby
|
377
|
+
|
344
378
|
def get_current_weather(location:, unit: "fahrenheit")
|
345
379
|
# use a weather api to fetch weather
|
346
380
|
end
|
@@ -348,7 +382,7 @@ end
|
|
348
382
|
response =
|
349
383
|
client.chat(
|
350
384
|
parameters: {
|
351
|
-
model: "gpt-3.5-turbo
|
385
|
+
model: "gpt-3.5-turbo",
|
352
386
|
messages: [
|
353
387
|
{
|
354
388
|
"role": "user",
|
@@ -378,16 +412,22 @@ response =
|
|
378
412
|
},
|
379
413
|
}
|
380
414
|
],
|
415
|
+
tool_choice: {
|
416
|
+
type: "function",
|
417
|
+
function: {
|
418
|
+
name: "get_current_weather"
|
419
|
+
}
|
420
|
+
}
|
381
421
|
},
|
382
422
|
)
|
383
423
|
|
384
424
|
message = response.dig("choices", 0, "message")
|
385
425
|
|
386
426
|
if message["role"] == "assistant" && message["tool_calls"]
|
387
|
-
function_name = message.dig("tool_calls", "function",
|
427
|
+
function_name = message.dig("tool_calls", 0, "function", "name")
|
388
428
|
args =
|
389
429
|
JSON.parse(
|
390
|
-
message.dig("tool_calls", "function", "arguments"),
|
430
|
+
message.dig("tool_calls", 0, "function", "arguments"),
|
391
431
|
{ symbolize_names: true },
|
392
432
|
)
|
393
433
|
|
@@ -406,7 +446,7 @@ Hit the OpenAI API for a completion using other GPT-3 models:
|
|
406
446
|
```ruby
|
407
447
|
response = client.completions(
|
408
448
|
parameters: {
|
409
|
-
model: "
|
449
|
+
model: "gpt-3.5-turbo",
|
410
450
|
prompt: "Once upon a time",
|
411
451
|
max_tokens: 5
|
412
452
|
})
|
@@ -414,22 +454,6 @@ puts response["choices"].map { |c| c["text"] }
|
|
414
454
|
# => [", there lived a great"]
|
415
455
|
```
|
416
456
|
|
417
|
-
### Edits
|
418
|
-
|
419
|
-
Send a string and some instructions for what to do to the string:
|
420
|
-
|
421
|
-
```ruby
|
422
|
-
response = client.edits(
|
423
|
-
parameters: {
|
424
|
-
model: "text-davinci-edit-001",
|
425
|
-
input: "What day of the wek is it?",
|
426
|
-
instruction: "Fix the spelling mistakes"
|
427
|
-
}
|
428
|
-
)
|
429
|
-
puts response.dig("choices", 0, "text")
|
430
|
-
# => What day of the week is it?
|
431
|
-
```
|
432
|
-
|
433
457
|
### Embeddings
|
434
458
|
|
435
459
|
You can use the embeddings endpoint to get a vector of numbers representing an input. You can then compare these vectors for different inputs to efficiently check how similar the inputs are.
|
@@ -446,6 +470,94 @@ puts response.dig("data", 0, "embedding")
|
|
446
470
|
# => Vector representation of your embedding
|
447
471
|
```
|
448
472
|
|
473
|
+
### Batches
|
474
|
+
|
475
|
+
The Batches endpoint allows you to create and manage large batches of API requests to run asynchronously. Currently, only the `/v1/chat/completions` endpoint is supported for batches.
|
476
|
+
|
477
|
+
To use the Batches endpoint, you need to first upload a JSONL file containing the batch requests using the Files endpoint. The file must be uploaded with the purpose set to `batch`. Each line in the JSONL file represents a single request and should have the following format:
|
478
|
+
|
479
|
+
```json
|
480
|
+
{
|
481
|
+
"custom_id": "request-1",
|
482
|
+
"method": "POST",
|
483
|
+
"url": "/v1/chat/completions",
|
484
|
+
"body": {
|
485
|
+
"model": "gpt-3.5-turbo",
|
486
|
+
"messages": [
|
487
|
+
{ "role": "system", "content": "You are a helpful assistant." },
|
488
|
+
{ "role": "user", "content": "What is 2+2?" }
|
489
|
+
]
|
490
|
+
}
|
491
|
+
}
|
492
|
+
```
|
493
|
+
|
494
|
+
Once you have uploaded the JSONL file, you can create a new batch by providing the file ID, endpoint, and completion window:
|
495
|
+
|
496
|
+
```ruby
|
497
|
+
response = client.batches.create(
|
498
|
+
parameters: {
|
499
|
+
input_file_id: "file-abc123",
|
500
|
+
endpoint: "/v1/chat/completions",
|
501
|
+
completion_window: "24h"
|
502
|
+
}
|
503
|
+
)
|
504
|
+
batch_id = response["id"]
|
505
|
+
```
|
506
|
+
|
507
|
+
You can retrieve information about a specific batch using its ID:
|
508
|
+
|
509
|
+
```ruby
|
510
|
+
batch = client.batches.retrieve(id: batch_id)
|
511
|
+
```
|
512
|
+
|
513
|
+
To cancel a batch that is in progress:
|
514
|
+
|
515
|
+
```ruby
|
516
|
+
client.batches.cancel(id: batch_id)
|
517
|
+
```
|
518
|
+
|
519
|
+
You can also list all the batches:
|
520
|
+
|
521
|
+
```ruby
|
522
|
+
client.batches.list
|
523
|
+
```
|
524
|
+
|
525
|
+
Once the batch["completed_at"] is present, you can fetch the output or error files:
|
526
|
+
|
527
|
+
```ruby
|
528
|
+
batch = client.batches.retrieve(id: batch_id)
|
529
|
+
output_file_id = batch["output_file_id"]
|
530
|
+
output_response = client.files.content(id: output_file_id)
|
531
|
+
error_file_id = batch["error_file_id"]
|
532
|
+
error_response = client.files.content(id: error_file_id)
|
533
|
+
```
|
534
|
+
|
535
|
+
These files are in JSONL format, with each line representing the output or error for a single request. The lines can be in any order:
|
536
|
+
|
537
|
+
```json
|
538
|
+
{
|
539
|
+
"id": "response-1",
|
540
|
+
"custom_id": "request-1",
|
541
|
+
"response": {
|
542
|
+
"id": "chatcmpl-abc123",
|
543
|
+
"object": "chat.completion",
|
544
|
+
"created": 1677858242,
|
545
|
+
"model": "gpt-3.5-turbo-0301",
|
546
|
+
"choices": [
|
547
|
+
{
|
548
|
+
"index": 0,
|
549
|
+
"message": {
|
550
|
+
"role": "assistant",
|
551
|
+
"content": "2+2 equals 4."
|
552
|
+
}
|
553
|
+
}
|
554
|
+
]
|
555
|
+
}
|
556
|
+
}
|
557
|
+
```
|
558
|
+
|
559
|
+
If a request fails with a non-HTTP error, the error object will contain more information about the cause of the failure.
|
560
|
+
|
449
561
|
### Files
|
450
562
|
|
451
563
|
Put your data in a `.jsonl` file like this:
|
@@ -455,7 +567,7 @@ Put your data in a `.jsonl` file like this:
|
|
455
567
|
{"prompt":"@lakers disappoint for a third straight night ->", "completion":" negative"}
|
456
568
|
```
|
457
569
|
|
458
|
-
and pass the path to `client.files.upload` to upload it to OpenAI, and then interact with it:
|
570
|
+
and pass the path (or a StringIO object) to `client.files.upload` to upload it to OpenAI, and then interact with it:
|
459
571
|
|
460
572
|
```ruby
|
461
573
|
client.files.upload(parameters: { file: "path/to/sentiment.jsonl", purpose: "fine-tune" })
|
@@ -480,7 +592,7 @@ You can then use this file ID to create a fine tuning job:
|
|
480
592
|
response = client.finetunes.create(
|
481
593
|
parameters: {
|
482
594
|
training_file: file_id,
|
483
|
-
model: "gpt-3.5-turbo
|
595
|
+
model: "gpt-3.5-turbo"
|
484
596
|
})
|
485
597
|
fine_tune_id = response["id"]
|
486
598
|
```
|
@@ -519,9 +631,9 @@ client.finetunes.list_events(id: fine_tune_id)
|
|
519
631
|
|
520
632
|
### Assistants
|
521
633
|
|
522
|
-
Assistants
|
634
|
+
Assistants are stateful actors that can have many conversations and use tools to perform tasks (see [Assistant Overview](https://platform.openai.com/docs/assistants/overview)).
|
523
635
|
|
524
|
-
To create a new assistant
|
636
|
+
To create a new assistant:
|
525
637
|
|
526
638
|
```ruby
|
527
639
|
response = client.assistants.create(
|
@@ -605,17 +717,36 @@ client.messages.retrieve(thread_id: thread_id, id: message_id) # -> Fails after
|
|
605
717
|
|
606
718
|
### Runs
|
607
719
|
|
608
|
-
To submit a thread to be evaluated with the model of an assistant, create a `Run` as follows
|
720
|
+
To submit a thread to be evaluated with the model of an assistant, create a `Run` as follows:
|
609
721
|
|
610
722
|
```ruby
|
611
723
|
# Create run (will use instruction/model/tools from Assistant's definition)
|
612
724
|
response = client.runs.create(thread_id: thread_id,
|
613
725
|
parameters: {
|
614
|
-
assistant_id: assistant_id
|
726
|
+
assistant_id: assistant_id,
|
727
|
+
max_prompt_tokens: 256,
|
728
|
+
max_completion_tokens: 16
|
615
729
|
})
|
616
730
|
run_id = response['id']
|
731
|
+
```
|
617
732
|
|
618
|
-
|
733
|
+
You can stream the message chunks as they come through:
|
734
|
+
|
735
|
+
```ruby
|
736
|
+
client.runs.create(thread_id: thread_id,
|
737
|
+
parameters: {
|
738
|
+
assistant_id: assistant_id,
|
739
|
+
max_prompt_tokens: 256,
|
740
|
+
max_completion_tokens: 16,
|
741
|
+
stream: proc do |chunk, _bytesize|
|
742
|
+
print chunk.dig("delta", "content", 0, "text", "value") if chunk["object"] == "thread.message.delta"
|
743
|
+
end
|
744
|
+
})
|
745
|
+
```
|
746
|
+
|
747
|
+
To get the status of a Run:
|
748
|
+
|
749
|
+
```
|
619
750
|
response = client.runs.retrieve(id: run_id, thread_id: thread_id)
|
620
751
|
status = response['status']
|
621
752
|
```
|
@@ -624,23 +755,22 @@ The `status` response can include the following strings `queued`, `in_progress`,
|
|
624
755
|
|
625
756
|
```ruby
|
626
757
|
while true do
|
627
|
-
|
628
758
|
response = client.runs.retrieve(id: run_id, thread_id: thread_id)
|
629
759
|
status = response['status']
|
630
760
|
|
631
761
|
case status
|
632
762
|
when 'queued', 'in_progress', 'cancelling'
|
633
|
-
|
634
|
-
|
763
|
+
puts 'Sleeping'
|
764
|
+
sleep 1 # Wait one second and poll again
|
635
765
|
when 'completed'
|
636
|
-
|
766
|
+
break # Exit loop and report result to user
|
637
767
|
when 'requires_action'
|
638
|
-
|
768
|
+
# Handle tool calls (see below)
|
639
769
|
when 'cancelled', 'failed', 'expired'
|
640
|
-
|
641
|
-
|
770
|
+
puts response['last_error'].inspect
|
771
|
+
break # or `exit`
|
642
772
|
else
|
643
|
-
|
773
|
+
puts "Unknown status response: #{status}"
|
644
774
|
end
|
645
775
|
end
|
646
776
|
```
|
@@ -649,10 +779,10 @@ If the `status` response indicates that the `run` is `completed`, the associated
|
|
649
779
|
|
650
780
|
```ruby
|
651
781
|
# Either retrieve all messages in bulk again, or...
|
652
|
-
messages = client.messages.list(thread_id: thread_id
|
782
|
+
messages = client.messages.list(thread_id: thread_id, parameters: { order: 'asc' })
|
653
783
|
|
654
784
|
# Alternatively retrieve the `run steps` for the run which link to the messages:
|
655
|
-
run_steps = client.run_steps.list(thread_id: thread_id, run_id: run_id)
|
785
|
+
run_steps = client.run_steps.list(thread_id: thread_id, run_id: run_id, parameters: { order: 'asc' })
|
656
786
|
new_message_ids = run_steps['data'].filter_map { |step|
|
657
787
|
if step['type'] == 'message_creation'
|
658
788
|
step.dig('step_details', "message_creation", "message_id")
|
@@ -679,10 +809,33 @@ new_messages.each { |msg|
|
|
679
809
|
}
|
680
810
|
```
|
681
811
|
|
682
|
-
|
812
|
+
You can also update the metadata on messages, including messages that come from the assistant.
|
683
813
|
|
684
814
|
```ruby
|
685
|
-
|
815
|
+
metadata = {
|
816
|
+
user_id: "abc123"
|
817
|
+
}
|
818
|
+
message = client.messages.modify(id: message_id, thread_id: thread_id, parameters: { metadata: metadata })
|
819
|
+
```
|
820
|
+
|
821
|
+
At any time you can list all runs which have been performed on a particular thread or are currently running:
|
822
|
+
|
823
|
+
```ruby
|
824
|
+
client.runs.list(thread_id: thread_id, parameters: { order: "asc", limit: 3 })
|
825
|
+
```
|
826
|
+
|
827
|
+
#### Create and Run
|
828
|
+
|
829
|
+
You can also create a thread and run in one call like this:
|
830
|
+
|
831
|
+
```ruby
|
832
|
+
response = client.threads.create_and_run(
|
833
|
+
parameters: {
|
834
|
+
model: 'gpt-3.5-turbo',
|
835
|
+
messages: [{ role: 'user', content: "What's deep learning?"}]
|
836
|
+
})
|
837
|
+
run_id = response['id']
|
838
|
+
thread_id = response['thread_id']
|
686
839
|
```
|
687
840
|
|
688
841
|
#### Runs involving function tools
|
@@ -746,7 +899,7 @@ puts response.dig("data", 0, "url")
|
|
746
899
|
For DALL·E 3 the size of any generated images must be one of `1024x1024`, `1024x1792` or `1792x1024`. Additionally the quality of the image can be specified to either `standard` or `hd`.
|
747
900
|
|
748
901
|
```ruby
|
749
|
-
response = client.images.generate(parameters: { prompt: "A springer spaniel cooking pasta wearing a hat of some sort", size: "1024x1792", quality: "standard" })
|
902
|
+
response = client.images.generate(parameters: { prompt: "A springer spaniel cooking pasta wearing a hat of some sort", model: "dall-e-3", size: "1024x1792", quality: "standard" })
|
750
903
|
puts response.dig("data", 0, "url")
|
751
904
|
# => "https://oaidalleapiprodscus.blob.core.windows.net/private/org-Rf437IxKhh..."
|
752
905
|
```
|
@@ -845,7 +998,7 @@ HTTP errors can be caught like this:
|
|
845
998
|
|
846
999
|
```
|
847
1000
|
begin
|
848
|
-
OpenAI::Client.new.models.retrieve(id: "
|
1001
|
+
OpenAI::Client.new.models.retrieve(id: "gpt-3.5-turbo")
|
849
1002
|
rescue Faraday::Error => e
|
850
1003
|
raise "Got a Faraday error: #{e}"
|
851
1004
|
end
|
data/lib/openai/assistants.rb
CHANGED
@@ -0,0 +1,23 @@
|
|
1
|
+
module OpenAI
|
2
|
+
class Batches
|
3
|
+
def initialize(client:)
|
4
|
+
@client = client.beta(assistants: "v1")
|
5
|
+
end
|
6
|
+
|
7
|
+
def list
|
8
|
+
@client.get(path: "/batches")
|
9
|
+
end
|
10
|
+
|
11
|
+
def retrieve(id:)
|
12
|
+
@client.get(path: "/batches/#{id}")
|
13
|
+
end
|
14
|
+
|
15
|
+
def create(parameters: {})
|
16
|
+
@client.json_post(path: "/batches", parameters: parameters)
|
17
|
+
end
|
18
|
+
|
19
|
+
def cancel(id:)
|
20
|
+
@client.post(path: "/batches/#{id}/cancel")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/openai/client.rb
CHANGED
@@ -6,6 +6,7 @@ module OpenAI
|
|
6
6
|
api_type
|
7
7
|
api_version
|
8
8
|
access_token
|
9
|
+
log_errors
|
9
10
|
organization_id
|
10
11
|
uri_base
|
11
12
|
request_timeout
|
@@ -17,7 +18,10 @@ module OpenAI
|
|
17
18
|
CONFIG_KEYS.each do |key|
|
18
19
|
# Set instance variables like api_type & access_token. Fall back to global config
|
19
20
|
# if not present.
|
20
|
-
instance_variable_set(
|
21
|
+
instance_variable_set(
|
22
|
+
"@#{key}",
|
23
|
+
config[key].nil? ? OpenAI.configuration.send(key) : config[key]
|
24
|
+
)
|
21
25
|
end
|
22
26
|
@faraday_middleware = faraday_middleware
|
23
27
|
end
|
@@ -26,10 +30,6 @@ module OpenAI
|
|
26
30
|
json_post(path: "/chat/completions", parameters: parameters)
|
27
31
|
end
|
28
32
|
|
29
|
-
def edits(parameters: {})
|
30
|
-
json_post(path: "/edits", parameters: parameters)
|
31
|
-
end
|
32
|
-
|
33
33
|
def embeddings(parameters: {})
|
34
34
|
json_post(path: "/embeddings", parameters: parameters)
|
35
35
|
end
|
@@ -78,6 +78,10 @@ module OpenAI
|
|
78
78
|
@run_steps ||= OpenAI::RunSteps.new(client: self)
|
79
79
|
end
|
80
80
|
|
81
|
+
def batches
|
82
|
+
@batches ||= OpenAI::Batches.new(client: self)
|
83
|
+
end
|
84
|
+
|
81
85
|
def moderations(parameters: {})
|
82
86
|
json_post(path: "/moderations", parameters: parameters)
|
83
87
|
end
|
data/lib/openai/files.rb
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
module OpenAI
|
2
2
|
class Files
|
3
|
+
PURPOSES = %w[
|
4
|
+
assistants
|
5
|
+
batch
|
6
|
+
fine-tune
|
7
|
+
].freeze
|
8
|
+
|
3
9
|
def initialize(client:)
|
4
10
|
@client = client
|
5
11
|
end
|
@@ -9,12 +15,17 @@ module OpenAI
|
|
9
15
|
end
|
10
16
|
|
11
17
|
def upload(parameters: {})
|
12
|
-
|
18
|
+
file_input = parameters[:file]
|
19
|
+
file = prepare_file_input(file_input: file_input)
|
20
|
+
|
21
|
+
validate(file: file, purpose: parameters[:purpose], file_input: file_input)
|
13
22
|
|
14
23
|
@client.multipart_post(
|
15
24
|
path: "/files",
|
16
|
-
parameters: parameters.merge(file:
|
25
|
+
parameters: parameters.merge(file: file)
|
17
26
|
)
|
27
|
+
ensure
|
28
|
+
file.close if file.is_a?(File)
|
18
29
|
end
|
19
30
|
|
20
31
|
def retrieve(id:)
|
@@ -31,12 +42,33 @@ module OpenAI
|
|
31
42
|
|
32
43
|
private
|
33
44
|
|
34
|
-
def
|
35
|
-
|
45
|
+
def prepare_file_input(file_input:)
|
46
|
+
if file_input.is_a?(String)
|
47
|
+
File.open(file_input)
|
48
|
+
elsif file_input.respond_to?(:read) && file_input.respond_to?(:rewind)
|
49
|
+
file_input
|
50
|
+
else
|
51
|
+
raise ArgumentError, "Invalid file - must be a StringIO object or a path to a file."
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def validate(file:, purpose:, file_input:)
|
56
|
+
raise ArgumentError, "`file` is required" if file.nil?
|
57
|
+
unless PURPOSES.include?(purpose)
|
58
|
+
raise ArgumentError, "`purpose` must be one of `#{PURPOSES.join(',')}`"
|
59
|
+
end
|
60
|
+
|
61
|
+
validate_jsonl(file: file) if file_input.is_a?(String) && file_input.end_with?(".jsonl")
|
62
|
+
end
|
63
|
+
|
64
|
+
def validate_jsonl(file:)
|
65
|
+
file.each_line.with_index do |line, index|
|
36
66
|
JSON.parse(line)
|
37
67
|
rescue JSON::ParserError => e
|
38
68
|
raise JSON::ParserError, "#{e.message} - found on line #{index + 1} of #{file}"
|
39
69
|
end
|
70
|
+
ensure
|
71
|
+
file.rewind
|
40
72
|
end
|
41
73
|
end
|
42
74
|
end
|
data/lib/openai/http.rb
CHANGED
@@ -6,8 +6,8 @@ module OpenAI
|
|
6
6
|
module HTTP
|
7
7
|
include HTTPHeaders
|
8
8
|
|
9
|
-
def get(path:)
|
10
|
-
parse_jsonl(conn.get(uri(path: path)) do |req|
|
9
|
+
def get(path:, parameters: nil)
|
10
|
+
parse_jsonl(conn.get(uri(path: path), parameters) do |req|
|
11
11
|
req.headers = headers
|
12
12
|
end&.body)
|
13
13
|
end
|
@@ -74,7 +74,7 @@ module OpenAI
|
|
74
74
|
connection = Faraday.new do |f|
|
75
75
|
f.options[:timeout] = @request_timeout
|
76
76
|
f.request(:multipart) if multipart
|
77
|
-
f.use MiddlewareErrors
|
77
|
+
f.use MiddlewareErrors if @log_errors
|
78
78
|
f.response :raise_error
|
79
79
|
f.response :json
|
80
80
|
end
|
@@ -88,6 +88,8 @@ module OpenAI
|
|
88
88
|
if azure?
|
89
89
|
base = File.join(@uri_base, path)
|
90
90
|
"#{base}?api-version=#{@api_version}"
|
91
|
+
elsif @uri_base.include?(@api_version)
|
92
|
+
File.join(@uri_base, path)
|
91
93
|
else
|
92
94
|
File.join(@uri_base, @api_version, path)
|
93
95
|
end
|
@@ -97,10 +99,14 @@ module OpenAI
|
|
97
99
|
parameters&.transform_values do |value|
|
98
100
|
next value unless value.respond_to?(:close) # File or IO object.
|
99
101
|
|
102
|
+
# Faraday::UploadIO does not require a path, so we will pass it
|
103
|
+
# only if it is available. This allows StringIO objects to be
|
104
|
+
# passed in as well.
|
105
|
+
path = value.respond_to?(:path) ? value.path : nil
|
100
106
|
# Doesn't seem like OpenAI needs mime_type yet, so not worth
|
101
107
|
# the library to figure this out. Hence the empty string
|
102
108
|
# as the second argument.
|
103
|
-
Faraday::UploadIO.new(value, "",
|
109
|
+
Faraday::UploadIO.new(value, "", path)
|
104
110
|
end
|
105
111
|
end
|
106
112
|
|
data/lib/openai/messages.rb
CHANGED
@@ -4,8 +4,8 @@ module OpenAI
|
|
4
4
|
@client = client.beta(assistants: "v1")
|
5
5
|
end
|
6
6
|
|
7
|
-
def list(thread_id:)
|
8
|
-
@client.get(path: "/threads/#{thread_id}/messages")
|
7
|
+
def list(thread_id:, parameters: {})
|
8
|
+
@client.get(path: "/threads/#{thread_id}/messages", parameters: parameters)
|
9
9
|
end
|
10
10
|
|
11
11
|
def retrieve(thread_id:, id:)
|
data/lib/openai/run_steps.rb
CHANGED
@@ -4,8 +4,8 @@ module OpenAI
|
|
4
4
|
@client = client.beta(assistants: "v1")
|
5
5
|
end
|
6
6
|
|
7
|
-
def list(thread_id:, run_id:)
|
8
|
-
@client.get(path: "/threads/#{thread_id}/runs/#{run_id}/steps")
|
7
|
+
def list(thread_id:, run_id:, parameters: {})
|
8
|
+
@client.get(path: "/threads/#{thread_id}/runs/#{run_id}/steps", parameters: parameters)
|
9
9
|
end
|
10
10
|
|
11
11
|
def retrieve(thread_id:, run_id:, id:)
|
data/lib/openai/runs.rb
CHANGED
@@ -4,8 +4,8 @@ module OpenAI
|
|
4
4
|
@client = client.beta(assistants: "v1")
|
5
5
|
end
|
6
6
|
|
7
|
-
def list(thread_id:)
|
8
|
-
@client.get(path: "/threads/#{thread_id}/runs")
|
7
|
+
def list(thread_id:, parameters: {})
|
8
|
+
@client.get(path: "/threads/#{thread_id}/runs", parameters: parameters)
|
9
9
|
end
|
10
10
|
|
11
11
|
def retrieve(thread_id:, id:)
|
@@ -24,6 +24,10 @@ module OpenAI
|
|
24
24
|
@client.post(path: "/threads/#{thread_id}/runs/#{id}/cancel")
|
25
25
|
end
|
26
26
|
|
27
|
+
def create_thread_and_run(parameters: {})
|
28
|
+
@client.json_post(path: "/threads/runs", parameters: parameters)
|
29
|
+
end
|
30
|
+
|
27
31
|
def submit_tool_outputs(thread_id:, run_id:, parameters: {})
|
28
32
|
@client.json_post(path: "/threads/#{thread_id}/runs/#{run_id}/submit_tool_outputs",
|
29
33
|
parameters: parameters)
|
data/lib/openai/version.rb
CHANGED
data/lib/openai.rb
CHANGED
@@ -14,6 +14,7 @@ require_relative "openai/runs"
|
|
14
14
|
require_relative "openai/run_steps"
|
15
15
|
require_relative "openai/audio"
|
16
16
|
require_relative "openai/version"
|
17
|
+
require_relative "openai/batches"
|
17
18
|
|
18
19
|
module OpenAI
|
19
20
|
class Error < StandardError; end
|
@@ -36,30 +37,30 @@ module OpenAI
|
|
36
37
|
end
|
37
38
|
|
38
39
|
class Configuration
|
39
|
-
|
40
|
-
|
40
|
+
attr_accessor :access_token,
|
41
|
+
:api_type,
|
42
|
+
:api_version,
|
43
|
+
:log_errors,
|
44
|
+
:organization_id,
|
45
|
+
:uri_base,
|
46
|
+
:request_timeout,
|
41
47
|
:extra_headers
|
42
48
|
|
43
49
|
DEFAULT_API_VERSION = "v1".freeze
|
44
50
|
DEFAULT_URI_BASE = "https://api.openai.com/".freeze
|
45
51
|
DEFAULT_REQUEST_TIMEOUT = 120
|
52
|
+
DEFAULT_LOG_ERRORS = false
|
46
53
|
|
47
54
|
def initialize
|
48
55
|
@access_token = nil
|
49
56
|
@api_type = nil
|
50
57
|
@api_version = DEFAULT_API_VERSION
|
58
|
+
@log_errors = DEFAULT_LOG_ERRORS
|
51
59
|
@organization_id = nil
|
52
60
|
@uri_base = DEFAULT_URI_BASE
|
53
61
|
@request_timeout = DEFAULT_REQUEST_TIMEOUT
|
54
62
|
@extra_headers = {}
|
55
63
|
end
|
56
|
-
|
57
|
-
def access_token
|
58
|
-
return @access_token if @access_token
|
59
|
-
|
60
|
-
error_text = "OpenAI access token missing! See https://github.com/alexrudall/ruby-openai#usage"
|
61
|
-
raise ConfigurationError, error_text
|
62
|
-
end
|
63
64
|
end
|
64
65
|
|
65
66
|
class << self
|
data/ruby-openai.gemspec
CHANGED
@@ -6,7 +6,7 @@ Gem::Specification.new do |spec|
|
|
6
6
|
spec.authors = ["Alex"]
|
7
7
|
spec.email = ["alexrudall@users.noreply.github.com"]
|
8
8
|
|
9
|
-
spec.summary = "OpenAI API + Ruby!
|
9
|
+
spec.summary = "OpenAI API + Ruby! 🤖❤️"
|
10
10
|
spec.homepage = "https://github.com/alexrudall/ruby-openai"
|
11
11
|
spec.license = "MIT"
|
12
12
|
spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-openai
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 7.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alex
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-04-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: event_stream_parser
|
@@ -89,6 +89,7 @@ files:
|
|
89
89
|
- lib/openai.rb
|
90
90
|
- lib/openai/assistants.rb
|
91
91
|
- lib/openai/audio.rb
|
92
|
+
- lib/openai/batches.rb
|
92
93
|
- lib/openai/client.rb
|
93
94
|
- lib/openai/compatibility.rb
|
94
95
|
- lib/openai/files.rb
|
@@ -129,8 +130,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
129
130
|
- !ruby/object:Gem::Version
|
130
131
|
version: '0'
|
131
132
|
requirements: []
|
132
|
-
rubygems_version: 3.
|
133
|
+
rubygems_version: 3.5.9
|
133
134
|
signing_key:
|
134
135
|
specification_version: 4
|
135
|
-
summary: "OpenAI API + Ruby! \U0001F916
|
136
|
+
summary: "OpenAI API + Ruby! \U0001F916❤️"
|
136
137
|
test_files: []
|