llm.rb 2.1.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +125 -133
- data/lib/llm/bot.rb +4 -4
- data/lib/llm/buffer.rb +0 -9
- data/lib/llm/contract/completion.rb +65 -0
- data/lib/llm/contract.rb +48 -0
- data/lib/llm/error.rb +22 -14
- data/lib/llm/eventhandler.rb +6 -4
- data/lib/llm/eventstream/parser.rb +18 -13
- data/lib/llm/function.rb +1 -1
- data/lib/llm/json_adapter.rb +109 -0
- data/lib/llm/message.rb +9 -29
- data/lib/llm/multipart/enumerator_io.rb +86 -0
- data/lib/llm/multipart.rb +32 -51
- data/lib/llm/object/builder.rb +6 -6
- data/lib/llm/object/kernel.rb +4 -4
- data/lib/llm/object.rb +65 -10
- data/lib/llm/provider.rb +11 -3
- data/lib/llm/providers/anthropic/error_handler.rb +1 -1
- data/lib/llm/providers/anthropic/files.rb +4 -5
- data/lib/llm/providers/anthropic/models.rb +1 -2
- data/lib/llm/providers/anthropic/{format/completion_format.rb → request_adapter/completion.rb} +19 -19
- data/lib/llm/providers/anthropic/{format.rb → request_adapter.rb} +7 -7
- data/lib/llm/providers/anthropic/response_adapter/completion.rb +72 -0
- data/lib/llm/providers/anthropic/{response → response_adapter}/enumerable.rb +1 -1
- data/lib/llm/providers/anthropic/{response → response_adapter}/file.rb +1 -1
- data/lib/llm/providers/anthropic/{response → response_adapter}/web_search.rb +3 -3
- data/lib/llm/providers/anthropic/response_adapter.rb +36 -0
- data/lib/llm/providers/anthropic/stream_parser.rb +25 -11
- data/lib/llm/providers/anthropic.rb +26 -19
- data/lib/llm/providers/deepseek/{format/completion_format.rb → request_adapter/completion.rb} +15 -15
- data/lib/llm/providers/deepseek/{format.rb → request_adapter.rb} +7 -7
- data/lib/llm/providers/deepseek.rb +2 -2
- data/lib/llm/providers/gemini/audio.rb +2 -2
- data/lib/llm/providers/gemini/error_handler.rb +3 -3
- data/lib/llm/providers/gemini/files.rb +4 -7
- data/lib/llm/providers/gemini/images.rb +9 -14
- data/lib/llm/providers/gemini/models.rb +1 -2
- data/lib/llm/providers/gemini/{format/completion_format.rb → request_adapter/completion.rb} +14 -14
- data/lib/llm/providers/gemini/{format.rb → request_adapter.rb} +8 -8
- data/lib/llm/providers/gemini/response_adapter/completion.rb +73 -0
- data/lib/llm/providers/gemini/{response → response_adapter}/embedding.rb +1 -1
- data/lib/llm/providers/gemini/{response → response_adapter}/file.rb +1 -1
- data/lib/llm/providers/gemini/{response → response_adapter}/files.rb +1 -1
- data/lib/llm/providers/gemini/{response → response_adapter}/image.rb +3 -3
- data/lib/llm/providers/gemini/{response → response_adapter}/models.rb +1 -1
- data/lib/llm/providers/gemini/{response → response_adapter}/web_search.rb +3 -3
- data/lib/llm/providers/gemini/response_adapter.rb +42 -0
- data/lib/llm/providers/gemini/stream_parser.rb +41 -32
- data/lib/llm/providers/gemini.rb +30 -24
- data/lib/llm/providers/ollama/error_handler.rb +1 -1
- data/lib/llm/providers/ollama/{format/completion_format.rb → request_adapter/completion.rb} +19 -19
- data/lib/llm/providers/ollama/{format.rb → request_adapter.rb} +7 -7
- data/lib/llm/providers/ollama/response_adapter/completion.rb +67 -0
- data/lib/llm/providers/ollama/{response → response_adapter}/embedding.rb +1 -1
- data/lib/llm/providers/ollama/response_adapter.rb +32 -0
- data/lib/llm/providers/ollama/stream_parser.rb +2 -2
- data/lib/llm/providers/ollama.rb +26 -18
- data/lib/llm/providers/openai/audio.rb +1 -1
- data/lib/llm/providers/openai/error_handler.rb +12 -2
- data/lib/llm/providers/openai/files.rb +3 -6
- data/lib/llm/providers/openai/images.rb +4 -5
- data/lib/llm/providers/openai/models.rb +1 -3
- data/lib/llm/providers/openai/moderations.rb +3 -5
- data/lib/llm/providers/openai/{format/completion_format.rb → request_adapter/completion.rb} +22 -22
- data/lib/llm/providers/openai/{format/moderation_format.rb → request_adapter/moderation.rb} +5 -5
- data/lib/llm/providers/openai/{format/respond_format.rb → request_adapter/respond.rb} +16 -16
- data/lib/llm/providers/openai/{format.rb → request_adapter.rb} +12 -12
- data/lib/llm/providers/openai/{response → response_adapter}/audio.rb +1 -1
- data/lib/llm/providers/openai/response_adapter/completion.rb +71 -0
- data/lib/llm/providers/openai/{response → response_adapter}/embedding.rb +1 -1
- data/lib/llm/providers/openai/{response → response_adapter}/enumerable.rb +1 -1
- data/lib/llm/providers/openai/{response → response_adapter}/file.rb +1 -1
- data/lib/llm/providers/openai/{response → response_adapter}/image.rb +1 -1
- data/lib/llm/providers/openai/{response → response_adapter}/moderations.rb +1 -1
- data/lib/llm/providers/openai/{response → response_adapter}/responds.rb +6 -10
- data/lib/llm/providers/openai/{response → response_adapter}/web_search.rb +3 -3
- data/lib/llm/providers/openai/response_adapter.rb +47 -0
- data/lib/llm/providers/openai/responses/stream_parser.rb +22 -22
- data/lib/llm/providers/openai/responses.rb +6 -8
- data/lib/llm/providers/openai/stream_parser.rb +10 -7
- data/lib/llm/providers/openai/vector_stores.rb +8 -9
- data/lib/llm/providers/openai.rb +33 -23
- data/lib/llm/response.rb +14 -7
- data/lib/llm/usage.rb +11 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +36 -1
- metadata +44 -35
- data/lib/llm/providers/anthropic/response/completion.rb +0 -39
- data/lib/llm/providers/gemini/response/completion.rb +0 -35
- data/lib/llm/providers/ollama/response/completion.rb +0 -28
- data/lib/llm/providers/openai/response/completion.rb +0 -40
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fa682f0c6793298daeaac88092cb52f03652cbbbf28adfd6b62f94b8a263f3f3
|
|
4
|
+
data.tar.gz: 1fb08983372becef70d866bdc4ee79ee8d8bba55ace5d4be4637a69e91341747
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 720e09be8b25a9fde7d92887636d572edcdbd39a1b3a23ae1f44baaddb9f881c95927f63f16248f3a3d22da1704973f69f51309c487e1c97195175b772499b0d
|
|
7
|
+
data.tar.gz: 8cf35f7829b4e66ef002652643779658cf9c8cf8726f8b563eb5ca59ebcfc3a71eeb9b4cc473dfc4556324448855b6733fe3d48a73fb6e70fb91102544eb7061
|
data/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
> **Minimal footprint** <br>
|
|
2
|
+
> Zero dependencies outside Ruby’s standard library. <br>
|
|
3
|
+
> Zero runtime dependencies.
|
|
4
|
+
|
|
1
5
|
## About
|
|
2
6
|
|
|
3
7
|
llm.rb is a zero-dependency Ruby toolkit for Large Language Models that
|
|
@@ -15,32 +19,38 @@ A simple chatbot that maintains a conversation and streams responses in real-tim
|
|
|
15
19
|
#!/usr/bin/env ruby
|
|
16
20
|
require "llm"
|
|
17
21
|
|
|
18
|
-
llm = LLM.openai(key: ENV
|
|
22
|
+
llm = LLM.openai(key: ENV.fetch("KEY"))
|
|
19
23
|
bot = LLM::Bot.new(llm, stream: $stdout)
|
|
20
24
|
loop do
|
|
21
25
|
print "> "
|
|
22
|
-
bot.chat(gets)
|
|
23
|
-
|
|
26
|
+
bot.chat(STDIN.gets)
|
|
27
|
+
puts
|
|
24
28
|
end
|
|
25
29
|
```
|
|
26
30
|
|
|
27
31
|
#### Prompts
|
|
28
32
|
|
|
33
|
+
> ℹ️ **Tip:** Some providers (such as OpenAI) support `system` and `developer`
|
|
34
|
+
> roles, but the examples in this README stick to `user` roles since they are
|
|
35
|
+
> supported across all providers.
|
|
36
|
+
|
|
29
37
|
A prompt builder that produces a chain of messages that can be sent in one request:
|
|
30
38
|
|
|
31
39
|
```ruby
|
|
32
40
|
#!/usr/bin/env ruby
|
|
33
41
|
require "llm"
|
|
34
42
|
|
|
35
|
-
llm = LLM.openai(key: ENV
|
|
43
|
+
llm = LLM.openai(key: ENV.fetch("KEY"))
|
|
36
44
|
bot = LLM::Bot.new(llm)
|
|
45
|
+
|
|
37
46
|
prompt = bot.build_prompt do
|
|
38
|
-
it.
|
|
47
|
+
it.user "Answer concisely."
|
|
39
48
|
it.user "Was 2024 a leap year?"
|
|
40
|
-
it.user "How many days in
|
|
49
|
+
it.user "How many days were in that year?"
|
|
41
50
|
end
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
|
|
52
|
+
res = bot.chat(prompt)
|
|
53
|
+
res.choices.each { |m| puts "[#{m.role}] #{m.content}" }
|
|
44
54
|
```
|
|
45
55
|
|
|
46
56
|
#### Schema
|
|
@@ -52,20 +62,20 @@ A bot that instructs the LLM to respond in JSON, and according to the given sche
|
|
|
52
62
|
require "llm"
|
|
53
63
|
|
|
54
64
|
class Estimation < LLM::Schema
|
|
55
|
-
property :age, Integer, "
|
|
56
|
-
property :confidence, Number, "
|
|
57
|
-
property :notes, String, "
|
|
65
|
+
property :age, Integer, "Estimated age", required: true
|
|
66
|
+
property :confidence, Number, "0.0–1.0", required: true
|
|
67
|
+
property :notes, String, "Short notes", optional: true
|
|
58
68
|
end
|
|
59
69
|
|
|
60
|
-
llm = LLM.openai(key: ENV
|
|
70
|
+
llm = LLM.openai(key: ENV.fetch("KEY"))
|
|
61
71
|
bot = LLM::Bot.new(llm, schema: Estimation)
|
|
62
72
|
img = llm.images.create(prompt: "A man in his 30s")
|
|
63
|
-
res = bot.chat bot.image_url(img.urls
|
|
64
|
-
|
|
73
|
+
res = bot.chat bot.image_url(img.urls.first)
|
|
74
|
+
data = res.choices.find(&:assistant?).content!
|
|
65
75
|
|
|
66
|
-
puts "age: #{
|
|
67
|
-
puts "confidence: #{
|
|
68
|
-
puts "notes: #{
|
|
76
|
+
puts "age: #{data["age"]}"
|
|
77
|
+
puts "confidence: #{data["confidence"]}"
|
|
78
|
+
puts "notes: #{data["notes"]}" if data["notes"]
|
|
69
79
|
```
|
|
70
80
|
|
|
71
81
|
#### Tools
|
|
@@ -79,58 +89,57 @@ require "llm"
|
|
|
79
89
|
class System < LLM::Tool
|
|
80
90
|
name "system"
|
|
81
91
|
description "Run a shell command"
|
|
82
|
-
param :command, String, "
|
|
92
|
+
param :command, String, "Command to execute", required: true
|
|
83
93
|
|
|
84
94
|
def call(command:)
|
|
85
95
|
{success: system(command)}
|
|
86
96
|
end
|
|
87
97
|
end
|
|
88
98
|
|
|
89
|
-
llm = LLM.openai(key: ENV
|
|
99
|
+
llm = LLM.openai(key: ENV.fetch("KEY"))
|
|
90
100
|
bot = LLM::Bot.new(llm, tools: [System])
|
|
101
|
+
|
|
91
102
|
prompt = bot.build_prompt do
|
|
92
|
-
it.
|
|
93
|
-
it.user "
|
|
103
|
+
it.user "You can run safe shell commands."
|
|
104
|
+
it.user "Run `date`."
|
|
94
105
|
end
|
|
106
|
+
|
|
95
107
|
bot.chat(prompt)
|
|
96
|
-
bot.chat
|
|
97
|
-
bot.messages.select(&:assistant?).each {
|
|
108
|
+
bot.chat(bot.functions.map(&:call))
|
|
109
|
+
bot.messages.select(&:assistant?).each { |m| puts "[#{m.role}] #{m.content}" }
|
|
98
110
|
```
|
|
99
111
|
|
|
100
112
|
## Features
|
|
101
113
|
|
|
102
114
|
#### General
|
|
103
|
-
- ✅
|
|
104
|
-
- 📦
|
|
105
|
-
-
|
|
106
|
-
- ♻️
|
|
115
|
+
- ✅ Unified API across providers
|
|
116
|
+
- 📦 Zero runtime deps (stdlib-only)
|
|
117
|
+
- 🧩 Pluggable JSON adapters (JSON, Oj, Yajl, etc)
|
|
118
|
+
- ♻️ Optional persistent HTTP pool (net-http-persistent)
|
|
107
119
|
|
|
108
120
|
#### Chat, Agents
|
|
109
|
-
- 🧠
|
|
110
|
-
- 🤖
|
|
111
|
-
- 🗂️
|
|
112
|
-
- 📡
|
|
121
|
+
- 🧠 Stateless + stateful chat (completions + responses)
|
|
122
|
+
- 🤖 Tool calling / function execution
|
|
123
|
+
- 🗂️ JSON Schema structured output
|
|
124
|
+
- 📡 Streaming responses
|
|
113
125
|
|
|
114
126
|
#### Media
|
|
115
|
-
- 🗣️
|
|
116
|
-
- 🖼️
|
|
117
|
-
- 📎
|
|
118
|
-
-
|
|
127
|
+
- 🗣️ TTS, transcription, translation
|
|
128
|
+
- 🖼️ Image generation + editing
|
|
129
|
+
- 📎 Files API + prompt-aware file inputs
|
|
130
|
+
- 📦 Streaming multipart uploads (no full buffering)
|
|
131
|
+
- 💡 Multimodal prompts (text, documents, audio, images, video, URLs)
|
|
119
132
|
|
|
120
133
|
#### Embeddings
|
|
121
|
-
- 🧮
|
|
122
|
-
- 🧱
|
|
134
|
+
- 🧮 Embeddings
|
|
135
|
+
- 🧱 OpenAI vector stores (RAG)
|
|
123
136
|
|
|
124
137
|
#### Miscellaneous
|
|
125
|
-
- 📜
|
|
126
|
-
- 🔧
|
|
138
|
+
- 📜 Models API
|
|
139
|
+
- 🔧 OpenAI responses + moderations
|
|
127
140
|
|
|
128
141
|
## Matrix
|
|
129
142
|
|
|
130
|
-
While the Features section above gives you the high-level picture, the table below
|
|
131
|
-
breaks things down by provider, so you can see exactly what’s supported where.
|
|
132
|
-
|
|
133
|
-
|
|
134
143
|
| Feature / Provider | OpenAI | Anthropic | Gemini | DeepSeek | xAI (Grok) | zAI | Ollama | LlamaCpp |
|
|
135
144
|
|--------------------------------------|:------:|:---------:|:------:|:--------:|:----------:|:------:|:------:|:--------:|
|
|
136
145
|
| **Chat Completions** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
@@ -181,6 +190,27 @@ llm = LLM.ollama(key: nil)
|
|
|
181
190
|
llm = LLM.llamacpp(key: nil)
|
|
182
191
|
```
|
|
183
192
|
|
|
193
|
+
#### LLM::Response
|
|
194
|
+
|
|
195
|
+
All provider methods that perform requests return an
|
|
196
|
+
[LLM::Response](https://0x1eef.github.io/x/llm.rb/LLM/Response.html).
|
|
197
|
+
If the HTTP response is JSON (`content-type: application/json`),
|
|
198
|
+
`response.body` is parsed into an
|
|
199
|
+
[LLM::Object](https://0x1eef.github.io/x/llm.rb/LLM/Object.html) for
|
|
200
|
+
dot-access. For non-JSON responses, `response.body` is a raw string.
|
|
201
|
+
It is also possible to access top-level keys directly on the response
|
|
202
|
+
(eg: `res.object` instead of `res.body.object`):
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
#!/usr/bin/env ruby
|
|
206
|
+
require "llm"
|
|
207
|
+
|
|
208
|
+
llm = LLM.openai(key: ENV["KEY"])
|
|
209
|
+
res = llm.models.all
|
|
210
|
+
puts res.object
|
|
211
|
+
puts res.data.first.id
|
|
212
|
+
```
|
|
213
|
+
|
|
184
214
|
#### Persistence
|
|
185
215
|
|
|
186
216
|
The llm.rb library can maintain a process-wide connection pool
|
|
@@ -197,7 +227,7 @@ llm = LLM.openai(key: ENV["KEY"], persistent: true)
|
|
|
197
227
|
res1 = llm.responses.create "message 1"
|
|
198
228
|
res2 = llm.responses.create "message 2", previous_response_id: res1.response_id
|
|
199
229
|
res3 = llm.responses.create "message 3", previous_response_id: res2.response_id
|
|
200
|
-
|
|
230
|
+
puts res3.output_text
|
|
201
231
|
```
|
|
202
232
|
|
|
203
233
|
#### Thread Safety
|
|
@@ -230,15 +260,17 @@ require "llm"
|
|
|
230
260
|
|
|
231
261
|
llm = LLM.openai(key: ENV["KEY"])
|
|
232
262
|
bot = LLM::Bot.new(llm)
|
|
233
|
-
|
|
263
|
+
image_url = "https://upload.wikimedia.org/wikipedia/commons/9/97/The_Earth_seen_from_Apollo_17.jpg"
|
|
264
|
+
image_path = "/tmp/llm-logo.png"
|
|
265
|
+
pdf_path = "/tmp/llm-handbook.pdf"
|
|
234
266
|
|
|
235
267
|
prompt = bot.build_prompt do
|
|
236
|
-
it.
|
|
237
|
-
it.user ["Tell me about this
|
|
238
|
-
it.user ["Tell me about this PDF", bot.local_file(
|
|
268
|
+
it.user ["Tell me about this image", bot.image_url(image_url)]
|
|
269
|
+
it.user ["Tell me about this image", bot.local_file(image_path)]
|
|
270
|
+
it.user ["Tell me about this PDF", bot.local_file(pdf_path)]
|
|
239
271
|
end
|
|
240
272
|
bot.chat(prompt)
|
|
241
|
-
bot.messages.each {
|
|
273
|
+
bot.messages.each { |m| puts "[#{m.role}] #{m.content}" }
|
|
242
274
|
```
|
|
243
275
|
|
|
244
276
|
#### Streaming
|
|
@@ -256,20 +288,20 @@ require "llm"
|
|
|
256
288
|
|
|
257
289
|
llm = LLM.openai(key: ENV["KEY"])
|
|
258
290
|
bot = LLM::Bot.new(llm, stream: $stdout)
|
|
259
|
-
|
|
291
|
+
image_url = "https://upload.wikimedia.org/wikipedia/commons/9/97/The_Earth_seen_from_Apollo_17.jpg"
|
|
292
|
+
image_path = "/tmp/llm-logo.png"
|
|
293
|
+
pdf_path = "/tmp/llm-handbook.pdf"
|
|
260
294
|
|
|
261
295
|
prompt = bot.build_prompt do
|
|
262
|
-
it.
|
|
263
|
-
it.user ["Tell me about this
|
|
264
|
-
it.user ["Tell me about the PDF", bot.local_file(
|
|
296
|
+
it.user ["Tell me about this image", bot.image_url(image_url)]
|
|
297
|
+
it.user ["Tell me about this image", bot.local_file(image_path)]
|
|
298
|
+
it.user ["Tell me about the PDF", bot.local_file(pdf_path)]
|
|
265
299
|
end
|
|
266
300
|
bot.chat(prompt)
|
|
267
301
|
```
|
|
268
302
|
|
|
269
303
|
### Schema
|
|
270
304
|
|
|
271
|
-
#### Object
|
|
272
|
-
|
|
273
305
|
All LLM providers except Anthropic and DeepSeek allow a client to describe
|
|
274
306
|
the structure of a response that a LLM emits according to a schema that is
|
|
275
307
|
described by JSON. The schema lets a client describe what JSON object
|
|
@@ -280,56 +312,21 @@ its ability:
|
|
|
280
312
|
#!/usr/bin/env ruby
|
|
281
313
|
require "llm"
|
|
282
314
|
|
|
283
|
-
llm = LLM.openai(key: ENV["KEY"])
|
|
284
|
-
|
|
285
|
-
##
|
|
286
|
-
# Objects
|
|
287
|
-
schema = llm.schema.object(probability: llm.schema.number.required)
|
|
288
|
-
bot = LLM::Bot.new(llm, schema:)
|
|
289
|
-
bot.chat "Does the earth orbit the sun?", role: :user
|
|
290
|
-
puts bot.messages.find(&:assistant?).content! # => {probability: 1.0}
|
|
291
|
-
|
|
292
|
-
##
|
|
293
|
-
# Enums
|
|
294
|
-
schema = llm.schema.object(fruit: llm.schema.string.enum("Apple", "Orange", "Pineapple"))
|
|
295
|
-
bot = LLM::Bot.new(llm, schema:) :system
|
|
296
|
-
bot.chat "What fruit is your favorite?", role: :user
|
|
297
|
-
puts bot.messages.find(&:assistant?).content! # => {fruit: "Pineapple"}
|
|
298
|
-
|
|
299
|
-
##
|
|
300
|
-
# Arrays
|
|
301
|
-
schema = llm.schema.object(answers: llm.schema.array(llm.schema.integer.required))
|
|
302
|
-
bot = LLM::Bot.new(llm, schema:)
|
|
303
|
-
bot.chat "Tell me the answer to ((5 + 5) / 2) * 2 + 1", role: :user
|
|
304
|
-
puts bot.messages.find(&:assistant?).content! # => {answers: [11]}
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
#### Class
|
|
308
|
-
|
|
309
|
-
Other than the object form we saw in the previous example, a class form
|
|
310
|
-
is also supported. Under the hood, it is implemented with the object form
|
|
311
|
-
and the class form primarily exists to provide structure and organization
|
|
312
|
-
that the object form lacks:
|
|
313
|
-
|
|
314
|
-
```ruby
|
|
315
|
-
#!/usr/bin/env ruby
|
|
316
|
-
require "llm"
|
|
317
|
-
|
|
318
315
|
class Player < LLM::Schema
|
|
319
316
|
property :name, String, "The player's name", required: true
|
|
320
|
-
property :
|
|
317
|
+
property :position, Array[Number], "The player's [x, y] position", required: true
|
|
321
318
|
end
|
|
322
319
|
|
|
323
320
|
llm = LLM.openai(key: ENV["KEY"])
|
|
324
321
|
bot = LLM::Bot.new(llm, schema: Player)
|
|
325
322
|
prompt = bot.build_prompt do
|
|
326
|
-
it.
|
|
327
|
-
it.user "
|
|
323
|
+
it.user "The player's name is Sam and their position is (7, 12)."
|
|
324
|
+
it.user "Return the player's name and position"
|
|
328
325
|
end
|
|
329
326
|
|
|
330
327
|
player = bot.chat(prompt).content!
|
|
331
|
-
puts "name: #{player
|
|
332
|
-
puts "
|
|
328
|
+
puts "name: #{player['name']}"
|
|
329
|
+
puts "position: #{player['position'].join(', ')}"
|
|
333
330
|
```
|
|
334
331
|
|
|
335
332
|
### Tools
|
|
@@ -377,7 +374,7 @@ tool = LLM.function(:system) do |fn|
|
|
|
377
374
|
end
|
|
378
375
|
|
|
379
376
|
bot = LLM::Bot.new(llm, tools: [tool])
|
|
380
|
-
bot.chat "Your task is to run shell commands via a tool.", role: :
|
|
377
|
+
bot.chat "Your task is to run shell commands via a tool.", role: :user
|
|
381
378
|
|
|
382
379
|
bot.chat "What is the current date?", role: :user
|
|
383
380
|
bot.chat bot.functions.map(&:call) # report return value to the LLM
|
|
@@ -424,7 +421,7 @@ end
|
|
|
424
421
|
|
|
425
422
|
llm = LLM.openai(key: ENV["KEY"])
|
|
426
423
|
bot = LLM::Bot.new(llm, tools: [System])
|
|
427
|
-
bot.chat "Your task is to run shell commands via a tool.", role: :
|
|
424
|
+
bot.chat "Your task is to run shell commands via a tool.", role: :user
|
|
428
425
|
|
|
429
426
|
bot.chat "What is the current date?", role: :user
|
|
430
427
|
bot.chat bot.functions.map(&:call) # report return value to the LLM
|
|
@@ -454,9 +451,9 @@ require "llm"
|
|
|
454
451
|
|
|
455
452
|
llm = LLM.openai(key: ENV["KEY"])
|
|
456
453
|
bot = LLM::Bot.new(llm)
|
|
457
|
-
file = llm.files.create(file: "/book.pdf")
|
|
454
|
+
file = llm.files.create(file: "/tmp/llm-book.pdf")
|
|
458
455
|
res = bot.chat ["Tell me about this file", file]
|
|
459
|
-
res.choices.each {
|
|
456
|
+
res.choices.each { |m| puts "[#{m.role}] #{m.content}" }
|
|
460
457
|
```
|
|
461
458
|
|
|
462
459
|
### Prompts
|
|
@@ -487,17 +484,19 @@ require "llm"
|
|
|
487
484
|
|
|
488
485
|
llm = LLM.openai(key: ENV["KEY"])
|
|
489
486
|
bot = LLM::Bot.new(llm)
|
|
490
|
-
|
|
487
|
+
image_url = "https://upload.wikimedia.org/wikipedia/commons/9/97/The_Earth_seen_from_Apollo_17.jpg"
|
|
488
|
+
image_path = "/tmp/llm-logo.png"
|
|
489
|
+
pdf_path = "/tmp/llm-book.pdf"
|
|
491
490
|
|
|
492
|
-
res1 = bot.chat ["Tell me about this URL", bot.image_url(
|
|
493
|
-
res1.choices.each {
|
|
491
|
+
res1 = bot.chat ["Tell me about this image URL", bot.image_url(image_url)]
|
|
492
|
+
res1.choices.each { |m| puts "[#{m.role}] #{m.content}" }
|
|
494
493
|
|
|
495
|
-
file = llm.files.create(file:
|
|
494
|
+
file = llm.files.create(file: pdf_path)
|
|
496
495
|
res2 = bot.chat ["Tell me about this PDF", bot.remote_file(file)]
|
|
497
|
-
res2.choices.each {
|
|
496
|
+
res2.choices.each { |m| puts "[#{m.role}] #{m.content}" }
|
|
498
497
|
|
|
499
|
-
res3 = bot.chat ["Tell me about this image", bot.local_file(
|
|
500
|
-
res3.choices.each {
|
|
498
|
+
res3 = bot.chat ["Tell me about this image", bot.local_file(image_path)]
|
|
499
|
+
res3.choices.each { |m| puts "[#{m.role}] #{m.content}" }
|
|
501
500
|
```
|
|
502
501
|
|
|
503
502
|
### Audio
|
|
@@ -534,7 +533,7 @@ llm = LLM.openai(key: ENV["KEY"])
|
|
|
534
533
|
res = llm.audio.create_transcription(
|
|
535
534
|
file: File.join(Dir.home, "hello.mp3")
|
|
536
535
|
)
|
|
537
|
-
|
|
536
|
+
puts res.text # => "Hello world."
|
|
538
537
|
```
|
|
539
538
|
|
|
540
539
|
#### Translate
|
|
@@ -552,7 +551,7 @@ llm = LLM.openai(key: ENV["KEY"])
|
|
|
552
551
|
res = llm.audio.create_translation(
|
|
553
552
|
file: File.join(Dir.home, "bomdia.mp3")
|
|
554
553
|
)
|
|
555
|
-
|
|
554
|
+
puts res.text # => "Good morning."
|
|
556
555
|
```
|
|
557
556
|
|
|
558
557
|
### Images
|
|
@@ -582,8 +581,8 @@ end
|
|
|
582
581
|
#### Edit
|
|
583
582
|
|
|
584
583
|
The following example is focused on editing a local image with the aid
|
|
585
|
-
of a prompt. The image (`/
|
|
586
|
-
|
|
584
|
+
of a prompt. The image (`/tmp/llm-logo.png`) is returned to us with a hat.
|
|
585
|
+
The image is then moved to `${HOME}/logo-with-hat.png` as
|
|
587
586
|
the final step:
|
|
588
587
|
|
|
589
588
|
```ruby
|
|
@@ -594,20 +593,20 @@ require "fileutils"
|
|
|
594
593
|
|
|
595
594
|
llm = LLM.openai(key: ENV["KEY"])
|
|
596
595
|
res = llm.images.edit(
|
|
597
|
-
image: "/
|
|
598
|
-
prompt: "a
|
|
596
|
+
image: "/tmp/llm-logo.png",
|
|
597
|
+
prompt: "add a hat to the logo",
|
|
599
598
|
)
|
|
600
599
|
res.urls.each do |url|
|
|
601
600
|
FileUtils.mv OpenURI.open_uri(url).path,
|
|
602
|
-
File.join(Dir.home, "
|
|
601
|
+
File.join(Dir.home, "logo-with-hat.png")
|
|
603
602
|
end
|
|
604
603
|
```
|
|
605
604
|
|
|
606
605
|
#### Variations
|
|
607
606
|
|
|
608
607
|
The following example is focused on creating variations of a local image.
|
|
609
|
-
The image (`/
|
|
610
|
-
The images are then moved to `${HOME}/
|
|
608
|
+
The image (`/tmp/llm-logo.png`) is returned to us with five different variations.
|
|
609
|
+
The images are then moved to `${HOME}/logo-variation0.png`, `${HOME}/logo-variation1.png`
|
|
611
610
|
and so on as the final step:
|
|
612
611
|
|
|
613
612
|
```ruby
|
|
@@ -618,12 +617,12 @@ require "fileutils"
|
|
|
618
617
|
|
|
619
618
|
llm = LLM.openai(key: ENV["KEY"])
|
|
620
619
|
res = llm.images.create_variation(
|
|
621
|
-
image: "/
|
|
620
|
+
image: "/tmp/llm-logo.png",
|
|
622
621
|
n: 5
|
|
623
622
|
)
|
|
624
623
|
res.urls.each.with_index do |url, index|
|
|
625
624
|
FileUtils.mv OpenURI.open_uri(url).path,
|
|
626
|
-
File.join(Dir.home, "
|
|
625
|
+
File.join(Dir.home, "logo-variation#{index}.png")
|
|
627
626
|
end
|
|
628
627
|
```
|
|
629
628
|
|
|
@@ -633,13 +632,8 @@ end
|
|
|
633
632
|
|
|
634
633
|
The
|
|
635
634
|
[`LLM::Provider#embed`](https://0x1eef.github.io/x/llm.rb/LLM/Provider.html#embed-instance_method)
|
|
636
|
-
method
|
|
637
|
-
|
|
638
|
-
a common use-case for them is to store chunks of text in a
|
|
639
|
-
vector database, and then to query the database for *semantically
|
|
640
|
-
similar* text. These chunks of similar text can then support the
|
|
641
|
-
generation of a prompt that is used to query a large language model,
|
|
642
|
-
which will go on to generate a response:
|
|
635
|
+
method returns vector embeddings for one or more text inputs. A common
|
|
636
|
+
use is semantic search (store vectors, then query for similar text):
|
|
643
637
|
|
|
644
638
|
```ruby
|
|
645
639
|
#!/usr/bin/env ruby
|
|
@@ -647,9 +641,9 @@ require "llm"
|
|
|
647
641
|
|
|
648
642
|
llm = LLM.openai(key: ENV["KEY"])
|
|
649
643
|
res = llm.embed(["programming is fun", "ruby is a programming language", "sushi is art"])
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
644
|
+
puts res.class
|
|
645
|
+
puts res.embeddings.size
|
|
646
|
+
puts res.embeddings[0].size
|
|
653
647
|
|
|
654
648
|
##
|
|
655
649
|
# LLM::Response
|
|
@@ -663,10 +657,8 @@ print res.embeddings[0].size, "\n"
|
|
|
663
657
|
|
|
664
658
|
Almost all LLM providers provide a models endpoint that allows a client to
|
|
665
659
|
query the list of models that are available to use. The list is dynamic,
|
|
666
|
-
maintained by LLM providers, and it is independent of a specific llm.rb
|
|
667
|
-
|
|
668
|
-
objects can be used instead of a string that describes a model name (although
|
|
669
|
-
either works). Let's take a look at an example:
|
|
660
|
+
maintained by LLM providers, and it is independent of a specific llm.rb
|
|
661
|
+
release:
|
|
670
662
|
|
|
671
663
|
```ruby
|
|
672
664
|
#!/usr/bin/env ruby
|
|
@@ -676,7 +668,7 @@ require "llm"
|
|
|
676
668
|
# List all models
|
|
677
669
|
llm = LLM.openai(key: ENV["KEY"])
|
|
678
670
|
llm.models.all.each do |model|
|
|
679
|
-
|
|
671
|
+
puts "model: #{model.id}"
|
|
680
672
|
end
|
|
681
673
|
|
|
682
674
|
##
|
|
@@ -684,7 +676,7 @@ end
|
|
|
684
676
|
model = llm.models.all.find { |m| m.id == "gpt-3.5-turbo" }
|
|
685
677
|
bot = LLM::Bot.new(llm, model: model.id)
|
|
686
678
|
res = bot.chat "Hello #{model.id} :)"
|
|
687
|
-
res.choices.each {
|
|
679
|
+
res.choices.each { |m| puts "[#{m.role}] #{m.content}" }
|
|
688
680
|
```
|
|
689
681
|
|
|
690
682
|
## Install
|
data/lib/llm/bot.rb
CHANGED
|
@@ -119,7 +119,7 @@ module LLM
|
|
|
119
119
|
# if there are no assistant messages
|
|
120
120
|
# @return [LLM::Object]
|
|
121
121
|
def usage
|
|
122
|
-
@messages.find(&:assistant?)&.usage || LLM::Object.
|
|
122
|
+
@messages.find(&:assistant?)&.usage || LLM::Object.from({})
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
##
|
|
@@ -141,7 +141,7 @@ module LLM
|
|
|
141
141
|
# @return [LLM::Object]
|
|
142
142
|
# Returns a tagged object
|
|
143
143
|
def image_url(url)
|
|
144
|
-
LLM::Object.
|
|
144
|
+
LLM::Object.from(value: url, kind: :image_url)
|
|
145
145
|
end
|
|
146
146
|
|
|
147
147
|
##
|
|
@@ -151,7 +151,7 @@ module LLM
|
|
|
151
151
|
# @return [LLM::Object]
|
|
152
152
|
# Returns a tagged object
|
|
153
153
|
def local_file(path)
|
|
154
|
-
LLM::Object.
|
|
154
|
+
LLM::Object.from(value: LLM.File(path), kind: :local_file)
|
|
155
155
|
end
|
|
156
156
|
|
|
157
157
|
##
|
|
@@ -161,7 +161,7 @@ module LLM
|
|
|
161
161
|
# @return [LLM::Object]
|
|
162
162
|
# Returns a tagged object
|
|
163
163
|
def remote_file(res)
|
|
164
|
-
LLM::Object.
|
|
164
|
+
LLM::Object.from(value: res, kind: :remote_file)
|
|
165
165
|
end
|
|
166
166
|
|
|
167
167
|
private
|
data/lib/llm/buffer.rb
CHANGED
|
@@ -35,15 +35,6 @@ module LLM
|
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
##
|
|
39
|
-
# Returns an array of unread messages
|
|
40
|
-
# @see LLM::Message#read?
|
|
41
|
-
# @see LLM::Message#read!
|
|
42
|
-
# @return [Array<LLM::Message>]
|
|
43
|
-
def unread
|
|
44
|
-
reject(&:read?)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
38
|
##
|
|
48
39
|
# Find a message (in descending order)
|
|
49
40
|
# @return [LLM::Message, nil]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM::Contract
|
|
4
|
+
##
|
|
5
|
+
# Defines the interface all completion responses must implement
|
|
6
|
+
# @abstract
|
|
7
|
+
module Completion
|
|
8
|
+
extend LLM::Contract
|
|
9
|
+
|
|
10
|
+
##
|
|
11
|
+
# @return [Array<LLM::Messsage>]
|
|
12
|
+
# Returns one or more messages
|
|
13
|
+
def messages
|
|
14
|
+
raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
|
|
15
|
+
end
|
|
16
|
+
alias_method :choices, :messages
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
# @return [Integer]
|
|
20
|
+
# Returns the number of input tokens
|
|
21
|
+
def input_tokens
|
|
22
|
+
raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
# @return [Integer]
|
|
27
|
+
# Returns the number of output tokens
|
|
28
|
+
def output_tokens
|
|
29
|
+
raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# @return [Integer]
|
|
34
|
+
# Returns the number of reasoning tokens
|
|
35
|
+
def reasoning_tokens
|
|
36
|
+
raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
##
|
|
40
|
+
# @return [Integer]
|
|
41
|
+
# Returns the total number of tokens
|
|
42
|
+
def total_tokens
|
|
43
|
+
raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
##
|
|
47
|
+
# @return [LLM::Usage]
|
|
48
|
+
# Returns usage information
|
|
49
|
+
def usage
|
|
50
|
+
LLM::Usage.new(
|
|
51
|
+
input_tokens:,
|
|
52
|
+
output_tokens:,
|
|
53
|
+
reasoning_tokens:,
|
|
54
|
+
total_tokens:
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
##
|
|
59
|
+
# @return [String]
|
|
60
|
+
# Returns the model name
|
|
61
|
+
def model
|
|
62
|
+
raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/llm/contract.rb
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM
|
|
4
|
+
##
|
|
5
|
+
# The `LLM::Contract` module provides the ability for modules
|
|
6
|
+
# who are extended by it to implement contracts which must be
|
|
7
|
+
# implemented by other modules who include a given contract.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# module LLM::Contract
|
|
11
|
+
# # ..
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# module LLM::Contract
|
|
15
|
+
# module Completion
|
|
16
|
+
# extend LLM::Contract
|
|
17
|
+
# # inheriting modules must implement these methods
|
|
18
|
+
# # otherwise an error is raised on include
|
|
19
|
+
# def foo = nil
|
|
20
|
+
# def bar = nil
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# module LLM::OpenAI::ResponseAdapter
|
|
25
|
+
# module Completion
|
|
26
|
+
# def foo = nil
|
|
27
|
+
# def bar = nil
|
|
28
|
+
# include LLM::Contract::Completion
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
module Contract
|
|
32
|
+
ContractError = Class.new(LLM::Error)
|
|
33
|
+
require_relative "contract/completion"
|
|
34
|
+
|
|
35
|
+
##
|
|
36
|
+
# @api private
|
|
37
|
+
def included(mod)
|
|
38
|
+
meths = mod.instance_methods(false)
|
|
39
|
+
if meths.empty?
|
|
40
|
+
raise ContractError, "#{mod} does not implement any methods required by #{self}"
|
|
41
|
+
end
|
|
42
|
+
missing = instance_methods - meths
|
|
43
|
+
if missing.any?
|
|
44
|
+
raise ContractError, "#{mod} does not implement methods (#{missing.join(", ")}) required by #{self}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|