llm.rb 0.6.2 → 0.7.1
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 +63 -87
- data/lib/llm/chat/builder.rb +31 -0
- data/lib/llm/chat/conversable.rb +53 -0
- data/lib/llm/chat/prompt/completion.rb +21 -0
- data/lib/llm/chat/prompt/respond.rb +29 -0
- data/lib/llm/chat.rb +49 -57
- data/lib/llm/function.rb +68 -5
- data/lib/llm/multipart.rb +41 -39
- data/lib/llm/version.rb +1 -1
- data/llm.gemspec +6 -5
- metadata +15 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7d5d93a645b666da3d6947c2076189063aec26e7bc3381cfb84d6a6aea4ce8fa
|
4
|
+
data.tar.gz: db764cd8e9180a3c21ca5bf2b35d8a5fa3f525cb63101381aca09f9e92cb5d37
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c17999419e02e8c2d9d689299d149dd76077b1f52154557253dd9fe3876ff1109eaaa29d7d7aceda8147f38aa572261b455eaa253fbede212b874af42c8e03e0
|
7
|
+
data.tar.gz: 1061af3f752a1e8cbc37c8929a23f6bfa97328f66f3adf030e435f468785ffa6a3aae32fe5120936f872b60d8a48a31fcbf35cee0de66b8ee12ddedf17228beb
|
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
llm.rb is a zero-dependency Ruby toolkit for Large Language Models that
|
4
4
|
includes OpenAI, Gemini, Anthropic, Ollama, and LlamaCpp. It’s fast, simple
|
5
|
-
and composable – with full support for chat, tool calling, audio,
|
5
|
+
and composable – with full support for chat, tool calling, audio,
|
6
6
|
images, files, and JSON Schema generation.
|
7
7
|
|
8
8
|
## Features
|
@@ -22,11 +22,28 @@ images, files, and JSON Schema generation.
|
|
22
22
|
- 🗣️ Text-to-speech, transcription, and translation
|
23
23
|
- 🖼️ Image generation, editing, and variation support
|
24
24
|
- 📎 File uploads and prompt-aware file interaction
|
25
|
-
- 💡 Multimodal prompts (text, URLs, files)
|
25
|
+
- 💡 Multimodal prompts (text, images, PDFs, URLs, files)
|
26
26
|
|
27
27
|
#### Embeddings
|
28
28
|
- 🧮 Text embeddings and vector support
|
29
29
|
|
30
|
+
## Demos
|
31
|
+
|
32
|
+
<details>
|
33
|
+
<summary><b>1. Tools: "system" function</b></summary>
|
34
|
+
<img src="share/llm-shell/examples/toolcalls.gif">
|
35
|
+
</details>
|
36
|
+
|
37
|
+
<details>
|
38
|
+
<summary><b>2. Files: import at boot time</b></summary>
|
39
|
+
<img src="share/llm-shell/examples/files-boottime.gif">
|
40
|
+
</details>
|
41
|
+
|
42
|
+
<details>
|
43
|
+
<summary><b>3. Files: import at runtime</b></summary>
|
44
|
+
<img src="share/llm-shell/examples/files-runtime.gif">
|
45
|
+
</details>
|
46
|
+
|
30
47
|
## Examples
|
31
48
|
|
32
49
|
### Providers
|
@@ -54,10 +71,15 @@ llm = LLM.voyageai(key: "yourapikey")
|
|
54
71
|
|
55
72
|
#### Completions
|
56
73
|
|
74
|
+
> This example uses the stateless chat completions API that all
|
75
|
+
> providers support. A similar example for OpenAI's stateful
|
76
|
+
> responses API is available in the [docs/](docs/OPENAI_RESPONSES.md)
|
77
|
+
> directory.
|
78
|
+
|
57
79
|
The following example enables lazy mode for a
|
58
80
|
[LLM::Chat](https://0x1eef.github.io/x/llm.rb/LLM/Chat.html)
|
59
|
-
object by entering into a
|
60
|
-
sent to the provider only when necessary.
|
81
|
+
object by entering into a conversation where messages are buffered and
|
82
|
+
sent to the provider only when necessary. Both lazy and non-lazy conversations
|
61
83
|
maintain a message thread that can be reused as context throughout a conversation.
|
62
84
|
The example captures the spirit of llm.rb by demonstrating how objects cooperate
|
63
85
|
together through composition, and it uses the stateless chat completions API that
|
@@ -67,13 +89,17 @@ all LLM providers support:
|
|
67
89
|
#!/usr/bin/env ruby
|
68
90
|
require "llm"
|
69
91
|
|
70
|
-
llm
|
71
|
-
bot
|
72
|
-
bot.chat
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
92
|
+
llm = LLM.openai(key: ENV["KEY"])
|
93
|
+
bot = LLM::Chat.new(llm).lazy
|
94
|
+
msgs = bot.chat do |prompt|
|
95
|
+
prompt.system File.read("./share/llm/prompts/system.txt")
|
96
|
+
prompt.user "Tell me the answer to 5 + 15"
|
97
|
+
prompt.user "Tell me the answer to (5 + 15) * 2"
|
98
|
+
prompt.user "Tell me the answer to ((5 + 15) * 2) / 10"
|
99
|
+
end
|
100
|
+
|
101
|
+
# At this point, we execute a single request
|
102
|
+
msgs.each { print "[#{_1.role}] ", _1.content, "\n" }
|
77
103
|
|
78
104
|
##
|
79
105
|
# [system] You are my math assistant.
|
@@ -91,46 +117,6 @@ bot.messages.each { print "[#{_1.role}] ", _1.content, "\n" }
|
|
91
117
|
# The answer to ((5 + 15) * 2) / 10 is 4.
|
92
118
|
```
|
93
119
|
|
94
|
-
#### Responses
|
95
|
-
|
96
|
-
The responses API is a recent addition
|
97
|
-
[provided by OpenAI](https://platform.openai.com/docs/guides/conversation-state?api-mode=responses)
|
98
|
-
that lets a client store message state on their servers – and in turn
|
99
|
-
a client can avoid maintaining state manually as well as avoid sending
|
100
|
-
the entire conversation with each request that is made. Although it is
|
101
|
-
primarily supported by OpenAI at the moment, we might see other providers
|
102
|
-
support it in the future. For now
|
103
|
-
[llm.rb supports the responses API](https://0x1eef.github.io/x/llm.rb/LLM/OpenAI/Responses.html)
|
104
|
-
for the OpenAI provider:
|
105
|
-
|
106
|
-
```ruby
|
107
|
-
#!/usr/bin/env ruby
|
108
|
-
require "llm"
|
109
|
-
|
110
|
-
llm = LLM.openai(key: ENV["KEY"])
|
111
|
-
bot = LLM::Chat.new(llm).lazy
|
112
|
-
bot.respond File.read("./share/llm/prompts/system.txt"), role: :developer
|
113
|
-
bot.respond "Tell me the answer to 5 + 15", role: :user
|
114
|
-
bot.respond "Tell me the answer to (5 + 15) * 2", role: :user
|
115
|
-
bot.respond "Tell me the answer to ((5 + 15) * 2) / 10", role: :user
|
116
|
-
bot.messages.each { print "[#{_1.role}] ", _1.content, "\n" }
|
117
|
-
|
118
|
-
##
|
119
|
-
# [developer] You are my math assistant.
|
120
|
-
# I will provide you with (simple) equations.
|
121
|
-
# You will provide answers in the format "The answer to <equation> is <answer>".
|
122
|
-
# I will provide you a set of messages. Reply to all of them.
|
123
|
-
# A message is considered unanswered if there is no corresponding assistant response.
|
124
|
-
#
|
125
|
-
# [user] Tell me the answer to 5 + 15
|
126
|
-
# [user] Tell me the answer to (5 + 15) * 2
|
127
|
-
# [user] Tell me the answer to ((5 + 15) * 2) / 10
|
128
|
-
#
|
129
|
-
# [assistant] The answer to 5 + 15 is 20.
|
130
|
-
# The answer to (5 + 15) * 2 is 40.
|
131
|
-
# The answer to ((5 + 15) * 2) / 10 is 4.
|
132
|
-
```
|
133
|
-
|
134
120
|
### Schema
|
135
121
|
|
136
122
|
#### Structured
|
@@ -139,13 +125,9 @@ All LLM providers except Anthropic allow a client to describe the structure
|
|
139
125
|
of a response that a LLM emits according to a schema that is described by JSON.
|
140
126
|
The schema lets a client describe what JSON object (or value) an LLM should emit,
|
141
127
|
and the LLM will abide by the schema. See also: [JSON Schema website](https://json-schema.org/overview/what-is-jsonschema).
|
142
|
-
|
143
|
-
True to the llm.rb spirit of doing one thing well, and solving problems through the
|
144
|
-
composition of objects, the generation of a schema is delegated to another object
|
145
|
-
who is responsible for and an expert in the generation of JSON schemas. We will use
|
146
|
-
the
|
128
|
+
We will use the
|
147
129
|
[llmrb/json-schema](https://github.com/llmrb/json-schema)
|
148
|
-
library for the sake of the examples – the interface is designed so you
|
130
|
+
library for the sake of the examples – the interface is designed so you
|
149
131
|
could drop in any other library in its place:
|
150
132
|
|
151
133
|
```ruby
|
@@ -153,19 +135,19 @@ could drop in any other library in its place:
|
|
153
135
|
require "llm"
|
154
136
|
|
155
137
|
llm = LLM.openai(key: ENV["KEY"])
|
156
|
-
schema = llm.schema.object({
|
157
|
-
bot = LLM::Chat.new(llm, schema:)
|
158
|
-
bot.chat "
|
159
|
-
bot.chat "What
|
160
|
-
bot.messages.find(&:assistant?).content! # => {
|
138
|
+
schema = llm.schema.object({fruit: llm.schema.string.enum("Apple", "Orange", "Pineapple")})
|
139
|
+
bot = LLM::Chat.new(llm, schema:).lazy
|
140
|
+
bot.chat "Your favorite fruit is Pineapple", role: :system
|
141
|
+
bot.chat "What fruit is your favorite?", role: :user
|
142
|
+
bot.messages.find(&:assistant?).content! # => {fruit: "Pineapple"}
|
161
143
|
|
162
144
|
schema = llm.schema.object({answer: llm.schema.integer.required})
|
163
|
-
bot = LLM::Chat.new(llm, schema:)
|
145
|
+
bot = LLM::Chat.new(llm, schema:).lazy
|
164
146
|
bot.chat "Tell me the answer to ((5 + 5) / 2)", role: :user
|
165
147
|
bot.messages.find(&:assistant?).content! # => {answer: 5}
|
166
148
|
|
167
149
|
schema = llm.schema.object({probability: llm.schema.number.required})
|
168
|
-
bot = LLM::Chat.new(llm, schema:)
|
150
|
+
bot = LLM::Chat.new(llm, schema:).lazy
|
169
151
|
bot.chat "Does the earth orbit the sun?", role: :user
|
170
152
|
bot.messages.find(&:assistant?).content! # => {probability: 1}
|
171
153
|
```
|
@@ -195,14 +177,18 @@ arbitrary commands from a LLM without sanitizing the input first :) Without furt
|
|
195
177
|
#!/usr/bin/env ruby
|
196
178
|
require "llm"
|
197
179
|
|
198
|
-
llm
|
180
|
+
llm = LLM.openai(key: ENV["KEY"])
|
199
181
|
tool = LLM.function(:system) do |fn|
|
200
182
|
fn.description "Run a shell command"
|
201
183
|
fn.params do |schema|
|
202
184
|
schema.object(command: schema.string.required)
|
203
185
|
end
|
204
186
|
fn.define do |params|
|
205
|
-
|
187
|
+
ro, wo = IO.pipe
|
188
|
+
re, we = IO.pipe
|
189
|
+
Process.wait Process.spawn(params.command, out: wo, err: we)
|
190
|
+
[wo,we].each(&:close)
|
191
|
+
{stderr: re.read, stdout: ro.read}
|
206
192
|
end
|
207
193
|
end
|
208
194
|
|
@@ -216,8 +202,8 @@ bot.chat "What operating system am I running? (short version please!)", role: :u
|
|
216
202
|
bot.chat bot.functions.map(&:call) # report return value to the LLM
|
217
203
|
|
218
204
|
##
|
219
|
-
# Thu May 1 10:01:02 UTC 2025
|
220
|
-
# FreeBSD
|
205
|
+
# {stderr: "", stdout: "Thu May 1 10:01:02 UTC 2025"}
|
206
|
+
# {stderr: "", stdout: "FreeBSD"}
|
221
207
|
```
|
222
208
|
|
223
209
|
### Audio
|
@@ -228,8 +214,7 @@ Some but not all providers implement audio generation capabilities that
|
|
228
214
|
can create speech from text, transcribe audio to text, or translate
|
229
215
|
audio to text (usually English). The following example uses the OpenAI provider
|
230
216
|
to create an audio file from a text prompt. The audio is then moved to
|
231
|
-
`${HOME}/hello.mp3` as the final step
|
232
|
-
documentation for more information on how to use the audio generation API:
|
217
|
+
`${HOME}/hello.mp3` as the final step:
|
233
218
|
|
234
219
|
```ruby
|
235
220
|
#!/usr/bin/env ruby
|
@@ -245,8 +230,7 @@ IO.copy_stream res.audio, File.join(Dir.home, "hello.mp3")
|
|
245
230
|
The following example transcribes an audio file to text. The audio file
|
246
231
|
(`${HOME}/hello.mp3`) was theoretically created in the previous example,
|
247
232
|
and the result is printed to the console. The example uses the OpenAI
|
248
|
-
provider to transcribe the audio file
|
249
|
-
documentation for more information on how to use the audio transcription API:
|
233
|
+
provider to transcribe the audio file:
|
250
234
|
|
251
235
|
```ruby
|
252
236
|
#!/usr/bin/env ruby
|
@@ -264,9 +248,7 @@ print res.text, "\n" # => "Hello world."
|
|
264
248
|
The following example translates an audio file to text. In this example
|
265
249
|
the audio file (`${HOME}/bomdia.mp3`) is theoretically in Portuguese,
|
266
250
|
and it is translated to English. The example uses the OpenAI provider,
|
267
|
-
and at the time of writing, it can only translate to English
|
268
|
-
consult the provider's documentation for more information on how to use
|
269
|
-
the audio translation API:
|
251
|
+
and at the time of writing, it can only translate to English:
|
270
252
|
|
271
253
|
```ruby
|
272
254
|
#!/usr/bin/env ruby
|
@@ -308,11 +290,7 @@ end
|
|
308
290
|
The following example is focused on editing a local image with the aid
|
309
291
|
of a prompt. The image (`/images/cat.png`) is returned to us with the cat
|
310
292
|
now wearing a hat. The image is then moved to `${HOME}/catwithhat.png` as
|
311
|
-
the final step
|
312
|
-
|
313
|
-
Results and quality may vary, consider prompt adjustments if the results
|
314
|
-
are not as expected, and consult the provider's documentation
|
315
|
-
for more information on how to use the image editing API:
|
293
|
+
the final step:
|
316
294
|
|
317
295
|
```ruby
|
318
296
|
#!/usr/bin/env ruby
|
@@ -336,8 +314,7 @@ end
|
|
336
314
|
The following example is focused on creating variations of a local image.
|
337
315
|
The image (`/images/cat.png`) is returned to us with five different variations.
|
338
316
|
The images are then moved to `${HOME}/catvariation0.png`, `${HOME}/catvariation1.png`
|
339
|
-
and so on as the final step
|
340
|
-
on how to use the image variations API:
|
317
|
+
and so on as the final step:
|
341
318
|
|
342
319
|
```ruby
|
343
320
|
#!/usr/bin/env ruby
|
@@ -458,10 +435,8 @@ print res.embeddings[0].size, "\n"
|
|
458
435
|
Almost all LLM providers provide a models endpoint that allows a client to
|
459
436
|
query the list of models that are available to use. The list is dynamic,
|
460
437
|
maintained by LLM providers, and it is independent of a specific llm.rb release.
|
461
|
-
True to the llm.rb spirit of small, composable objects that cooperate with
|
462
|
-
each other, a
|
463
438
|
[LLM::Model](https://0x1eef.github.io/x/llm.rb/LLM/Model.html)
|
464
|
-
|
439
|
+
objects can be used instead of a string that describes a model name (although
|
465
440
|
either works). Let's take a look at an example:
|
466
441
|
|
467
442
|
```ruby
|
@@ -497,7 +472,8 @@ over or doesn't cover at all. The API reference is available at
|
|
497
472
|
|
498
473
|
The [docs/](docs/) directory contains some additional documentation that
|
499
474
|
didn't quite make it into the README. It covers the design guidelines that
|
500
|
-
the library follows,
|
475
|
+
the library follows, some strategies for memory management, and other
|
476
|
+
provider-specific features.
|
501
477
|
|
502
478
|
## See also
|
503
479
|
|
@@ -506,7 +482,7 @@ the library follows, and some strategies for memory management.
|
|
506
482
|
An extensible, developer-oriented command line utility that is powered by
|
507
483
|
llm.rb and serves as a demonstration of the library's capabilities. The
|
508
484
|
[demo](https://github.com/llmrb/llm-shell#demos) section has a number of GIF
|
509
|
-
previews might be especially interesting
|
485
|
+
previews might be especially interesting.
|
510
486
|
|
511
487
|
## Install
|
512
488
|
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class LLM::Chat
|
4
|
+
##
|
5
|
+
# @private
|
6
|
+
module Builder
|
7
|
+
private
|
8
|
+
|
9
|
+
##
|
10
|
+
# @param [String] prompt The prompt
|
11
|
+
# @param [Hash] params
|
12
|
+
# @return [LLM::Response::Respond]
|
13
|
+
def create_response!(prompt, params)
|
14
|
+
@provider.responses.create(
|
15
|
+
prompt,
|
16
|
+
@params.merge(params.merge(@response ? {previous_response_id: @response.id} : {}))
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# @param [String] prompt The prompt
|
22
|
+
# @param [Hash] params
|
23
|
+
# @return [LLM::Response::Completion]
|
24
|
+
def create_completion!(prompt, params)
|
25
|
+
@provider.complete(
|
26
|
+
prompt,
|
27
|
+
@params.merge(params.merge(messages:))
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class LLM::Chat
|
4
|
+
##
|
5
|
+
# @private
|
6
|
+
module Conversable
|
7
|
+
private
|
8
|
+
|
9
|
+
##
|
10
|
+
# Queues a response to be sent to the provider.
|
11
|
+
# @param [String] prompt The prompt
|
12
|
+
# @param [Hash] params
|
13
|
+
# @return [void]
|
14
|
+
def async_response(prompt, params = {})
|
15
|
+
role = params.delete(:role)
|
16
|
+
@messages << [LLM::Message.new(role, prompt), @params.merge(params), :respond]
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# Sends a response to the provider and returns the response.
|
21
|
+
# @param [String] prompt The prompt
|
22
|
+
# @param [Hash] params
|
23
|
+
# @return [LLM::Response::Respond]
|
24
|
+
def sync_response(prompt, params = {})
|
25
|
+
role = params[:role]
|
26
|
+
@response = create_response!(prompt, params)
|
27
|
+
@messages.concat [Message.new(role, prompt), @response.outputs[0]]
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Queues a completion to be sent to the provider.
|
32
|
+
# @param [String] prompt The prompt
|
33
|
+
# @param [Hash] params
|
34
|
+
# @return [void]
|
35
|
+
def async_completion(prompt, params = {})
|
36
|
+
role = params.delete(:role)
|
37
|
+
@messages.push [LLM::Message.new(role, prompt), @params.merge(params), :complete]
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Sends a completion to the provider and returns the completion.
|
42
|
+
# @param [String] prompt The prompt
|
43
|
+
# @param [Hash] params
|
44
|
+
# @return [LLM::Response::Completion]
|
45
|
+
def sync_completion(prompt, params = {})
|
46
|
+
role = params[:role]
|
47
|
+
completion = create_completion!(prompt, params)
|
48
|
+
@messages.concat [Message.new(role, prompt), completion.choices[0]]
|
49
|
+
end
|
50
|
+
|
51
|
+
include LLM
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LLM::Chat::Prompt
|
4
|
+
class Completion < Struct.new(:bot)
|
5
|
+
##
|
6
|
+
# @param [String] prompt
|
7
|
+
# @param [Hash] params (see LLM::Provider#complete)
|
8
|
+
# @return [LLM::Chat]
|
9
|
+
def system(prompt, params = {})
|
10
|
+
bot.chat prompt, params.merge(role: :system)
|
11
|
+
end
|
12
|
+
|
13
|
+
##
|
14
|
+
# @param [String] prompt
|
15
|
+
# @param [Hash] params (see LLM::Provider#complete)
|
16
|
+
# @return [LLM::Chat]
|
17
|
+
def user(prompt, params = {})
|
18
|
+
bot.chat prompt, params.merge(role: :user)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LLM::Chat::Prompt
|
4
|
+
class Respond < Struct.new(:bot)
|
5
|
+
##
|
6
|
+
# @param [String] prompt
|
7
|
+
# @param [Hash] params (see LLM::Provider#complete)
|
8
|
+
# @return [LLM::Chat]
|
9
|
+
def system(prompt, params = {})
|
10
|
+
bot.respond prompt, params.merge(role: :system)
|
11
|
+
end
|
12
|
+
|
13
|
+
##
|
14
|
+
# @param [String] prompt
|
15
|
+
# @param [Hash] params (see LLM::Provider#complete)
|
16
|
+
# @return [LLM::Chat]
|
17
|
+
def developer(prompt, params = {})
|
18
|
+
bot.respond prompt, params.merge(role: :developer)
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# @param [String] prompt
|
23
|
+
# @param [Hash] params (see LLM::Provider#complete)
|
24
|
+
# @return [LLM::Chat]
|
25
|
+
def user(prompt, params = {})
|
26
|
+
bot.respond prompt, params.merge(role: :user)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/llm/chat.rb
CHANGED
@@ -11,14 +11,36 @@ module LLM
|
|
11
11
|
# #!/usr/bin/env ruby
|
12
12
|
# require "llm"
|
13
13
|
#
|
14
|
+
# llm = LLM.openai(ENV["KEY"])
|
15
|
+
# bot = LLM::Chat.new(llm).lazy
|
16
|
+
# msgs = bot.chat do |prompt|
|
17
|
+
# prompt.system "Answer the following questions."
|
18
|
+
# prompt.user "What is 5 + 7 ?"
|
19
|
+
# prompt.user "Why is the sky blue ?"
|
20
|
+
# prompt.user "Why did the chicken cross the road ?"
|
21
|
+
# end
|
22
|
+
# msgs.map { print "[#{_1.role}]", _1.content, "\n" }
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# #!/usr/bin/env ruby
|
26
|
+
# require "llm"
|
27
|
+
#
|
14
28
|
# llm = LLM.openai(ENV["KEY"])
|
15
29
|
# bot = LLM::Chat.new(llm).lazy
|
16
|
-
# bot.chat
|
17
|
-
# bot.chat
|
18
|
-
# bot.chat
|
19
|
-
# bot.chat
|
30
|
+
# bot.chat "Answer the following questions.", role: :system
|
31
|
+
# bot.chat "What is 5 + 7 ?", role: :user
|
32
|
+
# bot.chat "Why is the sky blue ?", role: :user
|
33
|
+
# bot.chat "Why did the chicken cross the road ?", role: :user
|
20
34
|
# bot.messages.map { print "[#{_1.role}]", _1.content, "\n" }
|
21
35
|
class Chat
|
36
|
+
require_relative "chat/prompt/completion"
|
37
|
+
require_relative "chat/prompt/respond"
|
38
|
+
require_relative "chat/conversable"
|
39
|
+
require_relative "chat/builder"
|
40
|
+
|
41
|
+
include Conversable
|
42
|
+
include Builder
|
43
|
+
|
22
44
|
##
|
23
45
|
# @return [Array<LLM::Message>]
|
24
46
|
attr_reader :messages
|
@@ -44,18 +66,18 @@ module LLM
|
|
44
66
|
# Maintain a conversation via the chat completions API
|
45
67
|
# @param prompt (see LLM::Provider#complete)
|
46
68
|
# @param params (see LLM::Provider#complete)
|
47
|
-
# @
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
69
|
+
# @yieldparam [LLM::Chat::CompletionPrompt] prompt Yields a prompt
|
70
|
+
# @return [LLM::Chat, Array<LLM::Message>, LLM::Buffer]
|
71
|
+
# Returns self unless given a block, otherwise returns messages
|
72
|
+
def chat(prompt = nil, params = {})
|
73
|
+
if block_given?
|
74
|
+
yield Prompt::Completion.new(self)
|
75
|
+
messages
|
76
|
+
elsif prompt.nil?
|
77
|
+
raise ArgumentError, "wrong number of arguments (given 0, expected 1)"
|
54
78
|
else
|
55
|
-
|
56
|
-
|
57
|
-
@messages.concat [Message.new(role, prompt), completion.choices[0]]
|
58
|
-
self
|
79
|
+
params = {role: :user}.merge!(params)
|
80
|
+
tap { lazy? ? async_completion(prompt, params) : sync_completion(prompt, params) }
|
59
81
|
end
|
60
82
|
end
|
61
83
|
|
@@ -64,36 +86,20 @@ module LLM
|
|
64
86
|
# @note Not all LLM providers support this API
|
65
87
|
# @param prompt (see LLM::Provider#complete)
|
66
88
|
# @param params (see LLM::Provider#complete)
|
67
|
-
# @return [LLM::Chat]
|
68
|
-
|
69
|
-
|
70
|
-
if
|
71
|
-
|
72
|
-
|
73
|
-
|
89
|
+
# @return [LLM::Chat, Array<LLM::Message>, LLM::Buffer]
|
90
|
+
# Returns self unless given a block, otherwise returns messages
|
91
|
+
def respond(prompt = nil, params = {})
|
92
|
+
if block_given?
|
93
|
+
yield Prompt::Respond.new(self)
|
94
|
+
messages
|
95
|
+
elsif prompt.nil?
|
96
|
+
raise ArgumentError, "wrong number of arguments (given 0, expected 1)"
|
74
97
|
else
|
75
|
-
|
76
|
-
|
77
|
-
@messages.concat [Message.new(role, prompt), @response.outputs[0]]
|
78
|
-
self
|
98
|
+
params = {role: :user}.merge!(params)
|
99
|
+
tap { lazy? ? async_response(prompt, params) : sync_response(prompt, params) }
|
79
100
|
end
|
80
101
|
end
|
81
102
|
|
82
|
-
##
|
83
|
-
# The last message in the conversation.
|
84
|
-
# @note
|
85
|
-
# The `read_response` and `recent_message` methods are aliases of
|
86
|
-
# the `last_message` method, and you can choose the name that best
|
87
|
-
# fits your context or code style.
|
88
|
-
# @param [#to_s] role
|
89
|
-
# The role of the last message.
|
90
|
-
# @return [LLM::Message]
|
91
|
-
def last_message(role: @provider.assistant_role)
|
92
|
-
messages.reverse_each.find { _1.role == role.to_s }
|
93
|
-
end
|
94
|
-
alias_method :recent_message, :last_message
|
95
|
-
alias_method :read_response, :last_message
|
96
|
-
|
97
103
|
##
|
98
104
|
# Enables lazy mode for the conversation.
|
99
105
|
# @return [LLM::Chat]
|
@@ -121,13 +127,13 @@ module LLM
|
|
121
127
|
end
|
122
128
|
|
123
129
|
##
|
124
|
-
# Returns an array of functions that
|
130
|
+
# Returns an array of functions that can be called
|
125
131
|
# @return [Array<LLM::Function>]
|
126
132
|
def functions
|
127
133
|
messages
|
128
134
|
.select(&:assistant?)
|
129
135
|
.flat_map(&:functions)
|
130
|
-
.
|
136
|
+
.select(&:pending?)
|
131
137
|
end
|
132
138
|
|
133
139
|
private
|
@@ -144,19 +150,5 @@ module LLM
|
|
144
150
|
end
|
145
151
|
end
|
146
152
|
private_constant :Array
|
147
|
-
|
148
|
-
def respond!(prompt, params)
|
149
|
-
@provider.responses.create(
|
150
|
-
prompt,
|
151
|
-
@params.merge(params.merge(@response ? {previous_response_id: @response.id} : {}))
|
152
|
-
)
|
153
|
-
end
|
154
|
-
|
155
|
-
def complete!(prompt, params)
|
156
|
-
@provider.complete(
|
157
|
-
prompt,
|
158
|
-
@params.merge(params.merge(messages:))
|
159
|
-
)
|
160
|
-
end
|
161
153
|
end
|
162
154
|
end
|
data/lib/llm/function.rb
CHANGED
@@ -1,5 +1,37 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
##
|
4
|
+
# The {LLM::Function LLM::Function} class represents a function that can
|
5
|
+
# be called by an LLM. It comes in two forms: a Proc-based function,
|
6
|
+
# or a Class-based function.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# # Proc-based
|
10
|
+
# LLM.function(:system) do |fn|
|
11
|
+
# fn.description "Runs system commands, emits their output"
|
12
|
+
# fn.params do |schema|
|
13
|
+
# schema.object(command: schema.string.required)
|
14
|
+
# end
|
15
|
+
# fn.define do |params|
|
16
|
+
# Kernel.system(params.command)
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# # Class-based
|
22
|
+
# class System
|
23
|
+
# def call(params)
|
24
|
+
# Kernel.system(params.command)
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# LLM.function(:system) do |fn|
|
29
|
+
# fn.description "Runs system commands, emits their output"
|
30
|
+
# fn.params do |schema|
|
31
|
+
# schema.object(command: schema.string.required)
|
32
|
+
# end
|
33
|
+
# fn.register(System)
|
34
|
+
# end
|
3
35
|
class LLM::Function
|
4
36
|
class Return < Struct.new(:id, :value)
|
5
37
|
end
|
@@ -25,6 +57,8 @@ class LLM::Function
|
|
25
57
|
def initialize(name, &b)
|
26
58
|
@name = name
|
27
59
|
@schema = JSON::Schema.new
|
60
|
+
@called = false
|
61
|
+
@cancelled = false
|
28
62
|
yield(self)
|
29
63
|
end
|
30
64
|
|
@@ -45,21 +79,36 @@ class LLM::Function
|
|
45
79
|
|
46
80
|
##
|
47
81
|
# Set the function implementation
|
48
|
-
# @param [Proc] b The function implementation
|
82
|
+
# @param [Proc, Class] b The function implementation
|
49
83
|
# @return [void]
|
50
|
-
def define(&b)
|
51
|
-
@runner = b
|
84
|
+
def define(klass = nil, &b)
|
85
|
+
@runner = klass || b
|
52
86
|
end
|
87
|
+
alias_method :register, :define
|
53
88
|
|
54
89
|
##
|
55
90
|
# Call the function
|
56
|
-
# @return [
|
91
|
+
# @return [LLM::Function::Return] The result of the function call
|
57
92
|
def call
|
58
|
-
Return.new id, @runner.call(arguments)
|
93
|
+
Return.new id, (Class === @runner) ? @runner.new.call(arguments) : @runner.call(arguments)
|
59
94
|
ensure
|
60
95
|
@called = true
|
61
96
|
end
|
62
97
|
|
98
|
+
##
|
99
|
+
# Returns a value that communicates that the function call was cancelled
|
100
|
+
# @example
|
101
|
+
# llm = LLM.openai(key: ENV["KEY"])
|
102
|
+
# bot = LLM::Chat.new(llm, tools: [fn1, fn2])
|
103
|
+
# bot.chat "I want to run the functions"
|
104
|
+
# bot.chat bot.functions.map(&:cancel)
|
105
|
+
# @return [LLM::Function::Return]
|
106
|
+
def cancel(reason: "function call cancelled")
|
107
|
+
Return.new(id, {cancelled: true, reason:})
|
108
|
+
ensure
|
109
|
+
@cancelled = true
|
110
|
+
end
|
111
|
+
|
63
112
|
##
|
64
113
|
# Returns true when a function has been called
|
65
114
|
# @return [Boolean]
|
@@ -67,6 +116,20 @@ class LLM::Function
|
|
67
116
|
@called
|
68
117
|
end
|
69
118
|
|
119
|
+
##
|
120
|
+
# Returns true when a function has been cancelled
|
121
|
+
# @return [Boolean]
|
122
|
+
def cancelled?
|
123
|
+
@cancelled
|
124
|
+
end
|
125
|
+
|
126
|
+
##
|
127
|
+
# Returns true when a function has neither been called nor cancelled
|
128
|
+
# @return [Boolean]
|
129
|
+
def pending?
|
130
|
+
!@called && !@cancelled
|
131
|
+
end
|
132
|
+
|
70
133
|
##
|
71
134
|
# @return [Hash]
|
72
135
|
def format(provider)
|
data/lib/llm/multipart.rb
CHANGED
@@ -27,20 +27,6 @@ class LLM::Multipart
|
|
27
27
|
"multipart/form-data; boundary=#{@boundary}"
|
28
28
|
end
|
29
29
|
|
30
|
-
##
|
31
|
-
# Returns the multipart request body parts
|
32
|
-
# @return [Array<String>]
|
33
|
-
def parts
|
34
|
-
params.map do |key, value|
|
35
|
-
locals = {key: key.to_s.b, boundary: boundary.to_s.b}
|
36
|
-
if value.respond_to?(:path)
|
37
|
-
file_part(key, value, locals)
|
38
|
-
else
|
39
|
-
data_part(key, value, locals)
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
30
|
##
|
45
31
|
# Returns the multipart request body
|
46
32
|
# @return [String]
|
@@ -54,47 +40,63 @@ class LLM::Multipart
|
|
54
40
|
|
55
41
|
attr_reader :params
|
56
42
|
|
57
|
-
def
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
43
|
+
def file(locals, file)
|
44
|
+
locals = locals.merge(attributes(file))
|
45
|
+
build_file(locals) do |body|
|
46
|
+
IO.copy_stream(file.path, body)
|
47
|
+
body << "\r\n"
|
48
|
+
end
|
62
49
|
end
|
63
50
|
|
64
|
-
def
|
65
|
-
|
66
|
-
|
67
|
-
|
51
|
+
def form(locals, value)
|
52
|
+
locals = locals.merge(value:)
|
53
|
+
build_form(locals) do |body|
|
54
|
+
body << value.to_s
|
55
|
+
body << "\r\n"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def build_file(locals)
|
60
|
+
StringIO.new("".b).tap do |io|
|
61
|
+
io << "--#{locals[:boundary]}" \
|
68
62
|
"\r\n" \
|
69
63
|
"Content-Disposition: form-data; name=\"#{locals[:key]}\";" \
|
70
64
|
"filename=\"#{locals[:filename]}\"" \
|
71
65
|
"\r\n" \
|
72
66
|
"Content-Type: #{locals[:content_type]}" \
|
73
67
|
"\r\n\r\n"
|
74
|
-
|
75
|
-
|
76
|
-
|
68
|
+
yield(io)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def build_form(locals)
|
73
|
+
StringIO.new("".b).tap do |io|
|
74
|
+
io << "--#{locals[:boundary]}" \
|
77
75
|
"\r\n" \
|
78
76
|
"Content-Disposition: form-data; name=\"#{locals[:key]}\"" \
|
79
77
|
"\r\n\r\n"
|
80
|
-
|
81
|
-
raise "unknown type: #{type}"
|
78
|
+
yield(io)
|
82
79
|
end
|
83
80
|
end
|
84
81
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
82
|
+
##
|
83
|
+
# Returns the multipart request body parts
|
84
|
+
# @return [Array<String>]
|
85
|
+
def parts
|
86
|
+
params.map do |key, value|
|
87
|
+
locals = {key: key.to_s.b, boundary: boundary.to_s.b}
|
88
|
+
if value.respond_to?(:path)
|
89
|
+
file(locals, value)
|
90
|
+
else
|
91
|
+
form(locals, value)
|
92
|
+
end
|
90
93
|
end
|
91
94
|
end
|
92
95
|
|
93
|
-
def
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
end
|
96
|
+
def attributes(file)
|
97
|
+
{
|
98
|
+
filename: File.basename(file.path).b,
|
99
|
+
content_type: LLM::Mime[file].b
|
100
|
+
}
|
99
101
|
end
|
100
102
|
end
|
data/lib/llm/version.rb
CHANGED
data/llm.gemspec
CHANGED
@@ -8,14 +8,15 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.authors = ["Antar Azri", "0x1eef"]
|
9
9
|
spec.email = ["azantar@proton.me", "0x1eef@proton.me"]
|
10
10
|
|
11
|
-
spec.summary = "llm.rb is a
|
12
|
-
"
|
13
|
-
"
|
14
|
-
"
|
11
|
+
spec.summary = "llm.rb is a zero-dependency Ruby toolkit for " \
|
12
|
+
"Large Language Models that includes OpenAI, Gemini, " \
|
13
|
+
"Anthropic, Ollama, and LlamaCpp. It’s fast, simple " \
|
14
|
+
"and composable – with full support for chat, tool calling, audio, " \
|
15
|
+
"images, files, and JSON Schema generation."
|
15
16
|
spec.description = spec.summary
|
16
17
|
spec.homepage = "https://github.com/llmrb/llm"
|
17
18
|
spec.license = "0BSDL"
|
18
|
-
spec.required_ruby_version = ">= 3.
|
19
|
+
spec.required_ruby_version = ">= 3.2.0"
|
19
20
|
|
20
21
|
spec.metadata["homepage_uri"] = spec.homepage
|
21
22
|
spec.metadata["source_code_uri"] = "https://github.com/llmrb/llm"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: llm.rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Antar Azri
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2025-05-
|
12
|
+
date: 2025-05-11 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: webmock
|
@@ -151,9 +151,10 @@ dependencies:
|
|
151
151
|
- - "~>"
|
152
152
|
- !ruby/object:Gem::Version
|
153
153
|
version: '2.8'
|
154
|
-
description: llm.rb is a
|
155
|
-
|
156
|
-
|
154
|
+
description: llm.rb is a zero-dependency Ruby toolkit for Large Language Models that
|
155
|
+
includes OpenAI, Gemini, Anthropic, Ollama, and LlamaCpp. It’s fast, simple and
|
156
|
+
composable – with full support for chat, tool calling, audio, images, files, and
|
157
|
+
JSON Schema generation.
|
157
158
|
email:
|
158
159
|
- azantar@proton.me
|
159
160
|
- 0x1eef@proton.me
|
@@ -176,6 +177,10 @@ files:
|
|
176
177
|
- lib/llm.rb
|
177
178
|
- lib/llm/buffer.rb
|
178
179
|
- lib/llm/chat.rb
|
180
|
+
- lib/llm/chat/builder.rb
|
181
|
+
- lib/llm/chat/conversable.rb
|
182
|
+
- lib/llm/chat/prompt/completion.rb
|
183
|
+
- lib/llm/chat/prompt/respond.rb
|
179
184
|
- lib/llm/core_ext/ostruct.rb
|
180
185
|
- lib/llm/error.rb
|
181
186
|
- lib/llm/file.rb
|
@@ -255,7 +260,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
255
260
|
requirements:
|
256
261
|
- - ">="
|
257
262
|
- !ruby/object:Gem::Version
|
258
|
-
version: 3.
|
263
|
+
version: 3.2.0
|
259
264
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
260
265
|
requirements:
|
261
266
|
- - ">="
|
@@ -265,7 +270,8 @@ requirements: []
|
|
265
270
|
rubygems_version: 3.5.23
|
266
271
|
signing_key:
|
267
272
|
specification_version: 4
|
268
|
-
summary: llm.rb is a
|
269
|
-
|
270
|
-
|
273
|
+
summary: llm.rb is a zero-dependency Ruby toolkit for Large Language Models that includes
|
274
|
+
OpenAI, Gemini, Anthropic, Ollama, and LlamaCpp. It’s fast, simple and composable
|
275
|
+
– with full support for chat, tool calling, audio, images, files, and JSON Schema
|
276
|
+
generation.
|
271
277
|
test_files: []
|