llm.rb 0.6.1 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a1f511ff8e5ea40a91c14f89d76a746e8ca96a866a7e99e2eb1c409d2cdebf74
4
- data.tar.gz: 2d6b59d9fec4e9a38995571165b5aae96510f349d0011dd1de7e2c0776370279
3
+ metadata.gz: a7175b2fe81c74e007dd41db2e0fe1bd3f3639bed375af25da0f8ed2778ea2b5
4
+ data.tar.gz: 1c752e61cb288fed412b342b66279e7dfdb0337705e33af3e2a1deb1d408b8d0
5
5
  SHA512:
6
- metadata.gz: fa1bedadae41e2c53fcbb0a8be37e158e5627a7e1f57aff0f34f49829a78386f342fc092222b50ab90e4b2e88603e5a5fd17912a99cccafb78f4cea23ac6b523
7
- data.tar.gz: 8f78f02800ec4ecd829ebc2ecc459bdc7313bcae7c0d231a312e60a8f1b1c5aad7a912a0b7dafe6816180d7545edd5890d22c496fe53c556a326f93b3f0ce183
6
+ metadata.gz: 9af91ba96e63b2c43c7f6a836db5fed48da19ba1f2bdbb48894cc71fb940eca261930fec6b8fd9a6f641fc9c69402de9cdd6fd7f9cad9a7035b69ddad04de65a
7
+ data.tar.gz: b3f8af44ebb2522aba58621d19a19424805471f6f9a6f8ec834ebe417e6aa08e9f0233ef19a12695e829d44ea61b164ae57697379aebdf24450b829fdc04ac25
data/README.md CHANGED
@@ -1,17 +1,17 @@
1
1
  ## About
2
2
 
3
- llm.rb is a zero-dependency Ruby toolkit for Large Language Models like
4
- OpenAI, Gemini, Anthropic, and more. It’s fast, clean, and composable –
5
- with full support for chat, tool calling, audio, images, files, and
6
- JSON Schema generation.
3
+ llm.rb is a zero-dependency Ruby toolkit for Large Language Models that
4
+ includes OpenAI, Gemini, Anthropic, Ollama, and LlamaCpp. It’s fast, simple
5
+ and composable – with full support for chat, tool calling, audio,
6
+ images, files, and JSON Schema generation.
7
7
 
8
8
  ## Features
9
9
 
10
10
  #### General
11
- - ✅ Unified interface for OpenAI, Gemini, Anthropic, Ollama, and more
11
+ - ✅ A single unified interface for multiple providers
12
12
  - 📦 Zero dependencies outside Ruby's standard library
13
- - 🔌 Model introspection and selection
14
13
  - 🚀 Optimized for performance and low memory usage
14
+ - 🔌 Retrieve models dynamically for introspection and selection
15
15
 
16
16
  #### Chat, Agents
17
17
  - 🧠 Stateless and stateful chat via completions and responses API
@@ -27,6 +27,23 @@ JSON Schema generation.
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
@@ -46,6 +63,7 @@ llm = LLM.openai(key: "yourapikey")
46
63
  llm = LLM.gemini(key: "yourapikey")
47
64
  llm = LLM.anthropic(key: "yourapikey")
48
65
  llm = LLM.ollama(key: nil)
66
+ llm = LLM.llamacpp(key: nil)
49
67
  llm = LLM.voyageai(key: "yourapikey")
50
68
  ```
51
69
 
@@ -53,6 +71,11 @@ llm = LLM.voyageai(key: "yourapikey")
53
71
 
54
72
  #### Completions
55
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
+
56
79
  The following example enables lazy mode for a
57
80
  [LLM::Chat](https://0x1eef.github.io/x/llm.rb/LLM/Chat.html)
58
81
  object by entering into a "lazy" conversation where messages are buffered and
@@ -66,13 +89,15 @@ all LLM providers support:
66
89
  #!/usr/bin/env ruby
67
90
  require "llm"
68
91
 
69
- llm = LLM.openai(key: ENV["KEY"])
70
- bot = LLM::Chat.new(llm).lazy
71
- bot.chat File.read("./share/llm/prompts/system.txt"), role: :system
72
- bot.chat "Tell me the answer to 5 + 15", role: :user
73
- bot.chat "Tell me the answer to (5 + 15) * 2", role: :user
74
- bot.chat "Tell me the answer to ((5 + 15) * 2) / 10", role: :user
75
- bot.messages.each { print "[#{_1.role}] ", _1.content, "\n" }
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
+ msgs.each { print "[#{_1.role}] ", _1.content, "\n" }
76
101
 
77
102
  ##
78
103
  # [system] You are my math assistant.
@@ -90,46 +115,6 @@ bot.messages.each { print "[#{_1.role}] ", _1.content, "\n" }
90
115
  # The answer to ((5 + 15) * 2) / 10 is 4.
91
116
  ```
92
117
 
93
- #### Responses
94
-
95
- The responses API is a recent addition
96
- [provided by OpenAI](https://platform.openai.com/docs/guides/conversation-state?api-mode=responses)
97
- that lets a client store message state on their servers &ndash; and in turn
98
- a client can avoid maintaining state manually as well as avoid sending
99
- the entire conversation with each request that is made. Although it is
100
- primarily supported by OpenAI at the moment, we might see other providers
101
- support it in the future. For now
102
- [llm.rb supports the responses API](https://0x1eef.github.io/x/llm.rb/LLM/OpenAI/Responses.html)
103
- for the OpenAI provider:
104
-
105
- ```ruby
106
- #!/usr/bin/env ruby
107
- require "llm"
108
-
109
- llm = LLM.openai(key: ENV["KEY"])
110
- bot = LLM::Chat.new(llm).lazy
111
- bot.respond File.read("./share/llm/prompts/system.txt"), role: :developer
112
- bot.respond "Tell me the answer to 5 + 15", role: :user
113
- bot.respond "Tell me the answer to (5 + 15) * 2", role: :user
114
- bot.respond "Tell me the answer to ((5 + 15) * 2) / 10", role: :user
115
- bot.messages.each { print "[#{_1.role}] ", _1.content, "\n" }
116
-
117
- ##
118
- # [developer] You are my math assistant.
119
- # I will provide you with (simple) equations.
120
- # You will provide answers in the format "The answer to <equation> is <answer>".
121
- # I will provide you a set of messages. Reply to all of them.
122
- # A message is considered unanswered if there is no corresponding assistant response.
123
- #
124
- # [user] Tell me the answer to 5 + 15
125
- # [user] Tell me the answer to (5 + 15) * 2
126
- # [user] Tell me the answer to ((5 + 15) * 2) / 10
127
- #
128
- # [assistant] The answer to 5 + 15 is 20.
129
- # The answer to (5 + 15) * 2 is 40.
130
- # The answer to ((5 + 15) * 2) / 10 is 4.
131
- ```
132
-
133
118
  ### Schema
134
119
 
135
120
  #### Structured
@@ -138,26 +123,21 @@ All LLM providers except Anthropic allow a client to describe the structure
138
123
  of a response that a LLM emits according to a schema that is described by JSON.
139
124
  The schema lets a client describe what JSON object (or value) an LLM should emit,
140
125
  and the LLM will abide by the schema. See also: [JSON Schema website](https://json-schema.org/overview/what-is-jsonschema).
141
-
142
- True to the llm.rb spirit of doing one thing well, and solving problems through the
143
- composition of objects, the generation of a schema is delegated to another object
144
- who is responsible for and an expert in the generation of JSON schemas. We will use
145
- the
126
+ We will use the
146
127
  [llmrb/json-schema](https://github.com/llmrb/json-schema)
147
- library for the sake of the examples - it is an optional dependency that is loaded
148
- on-demand. At least for the time being it is not necessary to install it separately.
149
- The interface is designed so you could drop in any other library in its place:
128
+ library for the sake of the examples &ndash; the interface is designed so you
129
+ could drop in any other library in its place:
150
130
 
151
131
  ```ruby
152
132
  #!/usr/bin/env ruby
153
133
  require "llm"
154
134
 
155
135
  llm = LLM.openai(key: ENV["KEY"])
156
- schema = llm.schema.object({os: llm.schema.string.enum("OpenBSD", "FreeBSD", "NetBSD")})
136
+ schema = llm.schema.object({fruit: llm.schema.string.enum("Apple", "Orange", "Pineapple")})
157
137
  bot = LLM::Chat.new(llm, schema:)
158
- bot.chat "You secretly love NetBSD", role: :system
159
- bot.chat "What operating system is the best?", role: :user
160
- bot.messages.find(&:assistant?).content! # => {os: "NetBSD"}
138
+ bot.chat "Your favorite fruit is Pineapple", role: :system
139
+ bot.chat "What fruit is your favorite?", role: :user
140
+ bot.messages.find(&:assistant?).content! # => {fruit: "Pineapple"}
161
141
 
162
142
  schema = llm.schema.object({answer: llm.schema.integer.required})
163
143
  bot = LLM::Chat.new(llm, schema:)
@@ -228,8 +208,7 @@ Some but not all providers implement audio generation capabilities that
228
208
  can create speech from text, transcribe audio to text, or translate
229
209
  audio to text (usually English). The following example uses the OpenAI provider
230
210
  to create an audio file from a text prompt. The audio is then moved to
231
- `${HOME}/hello.mp3` as the final step. As always, consult the provider's
232
- documentation for more information on how to use the audio generation API:
211
+ `${HOME}/hello.mp3` as the final step:
233
212
 
234
213
  ```ruby
235
214
  #!/usr/bin/env ruby
@@ -245,8 +224,7 @@ IO.copy_stream res.audio, File.join(Dir.home, "hello.mp3")
245
224
  The following example transcribes an audio file to text. The audio file
246
225
  (`${HOME}/hello.mp3`) was theoretically created in the previous example,
247
226
  and the result is printed to the console. The example uses the OpenAI
248
- provider to transcribe the audio file. As always, consult the provider's
249
- documentation for more information on how to use the audio transcription API:
227
+ provider to transcribe the audio file:
250
228
 
251
229
  ```ruby
252
230
  #!/usr/bin/env ruby
@@ -264,9 +242,7 @@ print res.text, "\n" # => "Hello world."
264
242
  The following example translates an audio file to text. In this example
265
243
  the audio file (`${HOME}/bomdia.mp3`) is theoretically in Portuguese,
266
244
  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. As always,
268
- consult the provider's documentation for more information on how to use
269
- the audio translation API:
245
+ and at the time of writing, it can only translate to English:
270
246
 
271
247
  ```ruby
272
248
  #!/usr/bin/env ruby
@@ -308,11 +284,7 @@ end
308
284
  The following example is focused on editing a local image with the aid
309
285
  of a prompt. The image (`/images/cat.png`) is returned to us with the cat
310
286
  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:
287
+ the final step:
316
288
 
317
289
  ```ruby
318
290
  #!/usr/bin/env ruby
@@ -336,8 +308,7 @@ end
336
308
  The following example is focused on creating variations of a local image.
337
309
  The image (`/images/cat.png`) is returned to us with five different variations.
338
310
  The images are then moved to `${HOME}/catvariation0.png`, `${HOME}/catvariation1.png`
339
- and so on as the final step. Consult the provider's documentation for more information
340
- on how to use the image variations API:
311
+ and so on as the final step:
341
312
 
342
313
  ```ruby
343
314
  #!/usr/bin/env ruby
@@ -458,10 +429,8 @@ print res.embeddings[0].size, "\n"
458
429
  Almost all LLM providers provide a models endpoint that allows a client to
459
430
  query the list of models that are available to use. The list is dynamic,
460
431
  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
432
  [LLM::Model](https://0x1eef.github.io/x/llm.rb/LLM/Model.html)
464
- object can be used instead of a string that describes a model name (although
433
+ objects can be used instead of a string that describes a model name (although
465
434
  either works). Let's take a look at an example:
466
435
 
467
436
  ```ruby
@@ -483,35 +452,9 @@ bot.chat "Hello #{model.id} :)"
483
452
  bot.messages.select(&:assistant?).each { print "[#{_1.role}] ", _1.content, "\n" }
484
453
  ```
485
454
 
486
- ### Memory
487
-
488
- #### Child process
489
-
490
- When it comes to the generation of audio, images, and video memory consumption
491
- can be a potential problem. There are a few strategies in place to deal with this,
492
- and one lesser known strategy is to let a child process handle the memory cost
493
- by delegating media generation to a child process.
494
-
495
- Once a child process exits, any memory it had used is freed immediately and
496
- the parent process can continue to have a small memory footprint. In a sense
497
- it is similar to being able to use malloc + free from Ruby. The following example
498
- demonstrates how that might look like in practice:
499
-
500
- ```ruby
501
- #!/usr/bin/env ruby
502
- require "llm"
503
-
504
- llm = LLM.gemini(key: ENV["KEY"])
505
- fork do
506
- %w[dog cat sheep goat capybara].each do |animal|
507
- res = llm.images.create(prompt: "a #{animal} on a rocket to the moon")
508
- IO.copy_stream res.images[0], "#{animal}.png"
509
- end
510
- end
511
- Process.wait
512
- ```
455
+ ## Documentation
513
456
 
514
- ## API reference
457
+ ### API
515
458
 
516
459
  The README tries to provide a high-level overview of the library. For everything
517
460
  else there's the API reference. It covers classes and methods that the README glances
@@ -519,31 +462,12 @@ over or doesn't cover at all. The API reference is available at
519
462
  [0x1eef.github.io/x/llm.rb](https://0x1eef.github.io/x/llm.rb).
520
463
 
521
464
 
522
- ### See also
523
-
524
- #### Gemini
525
-
526
- * [LLM::Gemini](https://0x1eef.github.io/x/llm.rb/LLM/Gemini.html)
527
- * [LLM::Gemini::Images](https://0x1eef.github.io/x/llm.rb/LLM/Gemini/Images.html)
528
- * [LLM::Gemini::Audio](https://0x1eef.github.io/x/llm.rb/LLM/Gemini/Audio.html)
465
+ ### Guides
529
466
 
530
- #### OpenAI
531
-
532
- * [LLM::OpenAI](https://0x1eef.github.io/x/llm.rb/LLM/OpenAI.html)
533
- * [LLM::OpenAI::Images](https://0x1eef.github.io/x/llm.rb/LLM/OpenAI/Images.html)
534
- * [LLM::OpenAI::Audio](https://0x1eef.github.io/x/llm.rb/LLM/OpenAI/Audio.html)
535
-
536
- #### Anthropic
537
- * [LLM::Anthropic](https://0x1eef.github.io/x/llm.rb/LLM/Anthropic.html)
538
-
539
- #### Ollama
540
- * [LLM::Ollama](https://0x1eef.github.io/x/llm.rb/LLM/Ollama.html)
541
-
542
- ## Install
543
-
544
- llm.rb can be installed via rubygems.org:
545
-
546
- gem install llm.rb
467
+ The [docs/](docs/) directory contains some additional documentation that
468
+ didn't quite make it into the README. It covers the design guidelines that
469
+ the library follows, some strategies for memory management, and other
470
+ provider-specific features.
547
471
 
548
472
  ## See also
549
473
 
@@ -552,26 +476,13 @@ llm.rb can be installed via rubygems.org:
552
476
  An extensible, developer-oriented command line utility that is powered by
553
477
  llm.rb and serves as a demonstration of the library's capabilities. The
554
478
  [demo](https://github.com/llmrb/llm-shell#demos) section has a number of GIF
555
- previews might be especially interesting!
556
-
557
-
558
- ## Philosophy
479
+ previews might be especially interesting.
559
480
 
560
- llm.rb provides a clean, dependency-free interface to Large Language Models,
561
- treating Ruby itself — not Rails or any specific framework — as the primary platform.
562
- It avoids hidden magic, complex metaprogramming, and heavy DSLs. It is intentionally
563
- simple and won't compromise on being a simple library, even if that means saying no to
564
- certain features.
481
+ ## Install
565
482
 
566
- Instead, it embraces a general-purpose, object-oriented design that prioritizes
567
- explicitness, composability, and clarity. Code should be easy to follow, test, and adapt.
568
- For that reason we favor small, cooperating objects over deeply nested blocks — a pattern
569
- that often emerges in DSL-heavy libraries.
483
+ llm.rb can be installed via rubygems.org:
570
484
 
571
- Each part of llm.rb is designed to be conscious of memory, ready for production, and free
572
- from global state or non-standard dependencies. While inspired by ideas from other ecosystems
573
- (especially Python) it is not a port of any other library — it is a Ruby library written
574
- by Rubyists who value borrowing good ideas from other languages and ecosystems.
485
+ gem install llm.rb
575
486
 
576
487
  ## License
577
488
 
@@ -5,7 +5,7 @@ class JSON::Schema
5
5
  # The {JSON::Schema::Boolean JSON::Schema::Boolean} class represents a
6
6
  # boolean value in a JSON schema. It is a subclass of
7
7
  # {JSON::Schema::Leaf JSON::Schema::Leaf}.
8
- class Booelean < Leaf
8
+ class Boolean < Leaf
9
9
  def to_h
10
10
  super.merge!({type: "boolean"})
11
11
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Chat
4
+ ##
5
+ # @private
6
+ module Builder
7
+ private
8
+
9
+ def create_response!(prompt, params)
10
+ @provider.responses.create(
11
+ prompt,
12
+ @params.merge(params.merge(@response ? {previous_response_id: @response.id} : {}))
13
+ )
14
+ end
15
+
16
+ def create_completion!(prompt, params)
17
+ @provider.complete(
18
+ prompt,
19
+ @params.merge(params.merge(messages:))
20
+ )
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Chat
4
+ ##
5
+ # @private
6
+ module Conversable
7
+ private
8
+
9
+ def async_response(prompt, params = {})
10
+ role = params.delete(:role)
11
+ @messages << [LLM::Message.new(role, prompt), @params.merge(params), :respond]
12
+ end
13
+
14
+ def sync_response(prompt, params = {})
15
+ role = params[:role]
16
+ @response = create_response!(prompt, params)
17
+ @messages.concat [Message.new(role, prompt), @response.outputs[0]]
18
+ end
19
+
20
+ def async_completion(prompt, params = {})
21
+ role = params.delete(:role)
22
+ @messages.push [LLM::Message.new(role, prompt), @params.merge(params), :complete]
23
+ end
24
+
25
+ def sync_completion(prompt, params = {})
26
+ role = params[:role]
27
+ completion = create_completion!(prompt, params)
28
+ @messages.concat [Message.new(role, prompt), completion.choices[0]]
29
+ end
30
+
31
+ include LLM
32
+ end
33
+ 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("Provide short and concise answers", role: :system)
17
- # bot.chat("What is 5 + 7 ?", role: :user)
18
- # bot.chat("Why is the sky blue ?", role: :user)
19
- # bot.chat("Why did the chicken cross the road ?", role: :user)
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
- # @return [LLM::Chat]
48
- def chat(prompt, params = {})
49
- params = {role: :user}.merge!(params)
50
- if lazy?
51
- role = params.delete(:role)
52
- @messages << [LLM::Message.new(role, prompt), @params.merge(params), :complete]
53
- self
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
- role = params[:role]
56
- completion = complete!(prompt, params)
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
- def respond(prompt, params = {})
69
- params = {role: :user}.merge!(params)
70
- if lazy?
71
- role = params.delete(:role)
72
- @messages << [LLM::Message.new(role, prompt), @params.merge(params), :respond]
73
- self
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
- role = params[:role]
76
- @response = respond!(prompt, params)
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 have yet to be called
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
- .reject(&:called?)
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,22 +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
- # @param [Array] args The arguments to pass to the function
57
- # @return [Object] The result of the function call
91
+ # @return [LLM::Function::Return] The result of the function call
58
92
  def call
59
- Return.new id, @runner.call(arguments)
93
+ Return.new id, (Class === @runner) ? @runner.new.call(arguments) : @runner.call(arguments)
60
94
  ensure
61
95
  @called = true
62
96
  end
63
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
+
64
112
  ##
65
113
  # Returns true when a function has been called
66
114
  # @return [Boolean]
@@ -68,6 +116,20 @@ class LLM::Function
68
116
  @called
69
117
  end
70
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
+
71
133
  ##
72
134
  # @return [Hash]
73
135
  def format(provider)
@@ -18,6 +18,9 @@ class LLM::Anthropic
18
18
 
19
19
  private
20
20
 
21
+ ##
22
+ # @param [Hash] params
23
+ # @return [Hash]
21
24
  def format_tools(params)
22
25
  return {} unless params and params[:tools]&.any?
23
26
  tools = params[:tools]
@@ -19,8 +19,7 @@ class LLM::Gemini
19
19
  private
20
20
 
21
21
  ##
22
- # @param [JSON::Schema] schema
23
- # The schema to format
22
+ # @param [Hash] params
24
23
  # @return [Hash]
25
24
  def format_schema(params)
26
25
  return {} unless params and params[:schema]
@@ -29,8 +28,7 @@ class LLM::Gemini
29
28
  end
30
29
 
31
30
  ##
32
- # @param [Array<LLM::Function>] tools
33
- # The tools to format
31
+ # @param [Hash] params
34
32
  # @return [Hash]
35
33
  def format_tools(params)
36
34
  return {} unless params and params[:tools]&.any?
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM
4
+ ##
5
+ # The LlamaCpp class implements a provider for
6
+ # [llama.cpp](https://github.com/ggml-org/llama.cpp)
7
+ # through the OpenAI-compatible API provided by the
8
+ # llama-server binary.
9
+ class LlamaCpp < OpenAI
10
+ ##
11
+ # @param (see LLM::Provider#initialize)
12
+ # @return [LLM::LlamaCpp]
13
+ def initialize(host: "localhost", port: 8080, ssl: false, **)
14
+ super
15
+ end
16
+
17
+ ##
18
+ # @raise [NotImplementedError]
19
+ def files
20
+ raise NotImplementedError
21
+ end
22
+
23
+ ##
24
+ # @raise [NotImplementedError]
25
+ def images
26
+ raise NotImplementedError
27
+ end
28
+
29
+ ##
30
+ # @raise [NotImplementedError]
31
+ def audio
32
+ raise NotImplementedError
33
+ end
34
+
35
+ ##
36
+ # Returns the default model for chat completions
37
+ # @see https://ollama.com/library llama3.2
38
+ # @return [String]
39
+ def default_model
40
+ "llama3.2"
41
+ end
42
+ end
43
+ end
@@ -19,8 +19,7 @@ class LLM::Ollama
19
19
  private
20
20
 
21
21
  ##
22
- # @param [Array<LLM::Function>] tools
23
- # The tools to format
22
+ # @param [Hash] params
24
23
  # @return [Hash]
25
24
  def format_tools(params)
26
25
  return {} unless params and params[:tools]&.any?
@@ -26,8 +26,7 @@ class LLM::OpenAI
26
26
  private
27
27
 
28
28
  ##
29
- # @param [JSON::Schema] schema
30
- # The schema to format
29
+ # @param [Hash] params
31
30
  # @return [Hash]
32
31
  def format_schema(params)
33
32
  return {} unless params and params[:schema]
@@ -41,8 +40,7 @@ class LLM::OpenAI
41
40
  end
42
41
 
43
42
  ##
44
- # @param [Array<LLM::Function>] tools
45
- # The tools to format
43
+ # @param [Hash] params
46
44
  # @return [Hash]
47
45
  def format_tools(params)
48
46
  return {} unless params and params[:tools]&.any?
@@ -15,6 +15,7 @@ module LLM
15
15
  def choices
16
16
  parsed[:choices]
17
17
  end
18
+ alias_method :messages, :choices
18
19
 
19
20
  ##
20
21
  # @return [Integer]
data/lib/llm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LLM
4
- VERSION = "0.6.1"
4
+ VERSION = "0.7.0"
5
5
  end
data/lib/llm.rb CHANGED
@@ -21,7 +21,7 @@ module LLM
21
21
  module_function
22
22
 
23
23
  ##
24
- # @param secret (see LLM::Anthropic#initialize)
24
+ # @param (see LLM::Provider#initialize)
25
25
  # @return (see LLM::Anthropic#initialize)
26
26
  def anthropic(**)
27
27
  require_relative "llm/providers/anthropic" unless defined?(LLM::Anthropic)
@@ -30,7 +30,7 @@ module LLM
30
30
  end
31
31
 
32
32
  ##
33
- # @param secret (see LLM::VoyageAI#initialize)
33
+ # @param (see LLM::Provider#initialize)
34
34
  # @return (see LLM::VoyageAI#initialize)
35
35
  def voyageai(**)
36
36
  require_relative "llm/providers/voyageai" unless defined?(LLM::VoyageAI)
@@ -38,7 +38,7 @@ module LLM
38
38
  end
39
39
 
40
40
  ##
41
- # @param secret (see LLM::Gemini#initialize)
41
+ # @param (see LLM::Provider#initialize)
42
42
  # @return (see LLM::Gemini#initialize)
43
43
  def gemini(**)
44
44
  require_relative "llm/providers/gemini" unless defined?(LLM::Gemini)
@@ -46,7 +46,7 @@ module LLM
46
46
  end
47
47
 
48
48
  ##
49
- # @param host (see LLM::Ollama#initialize)
49
+ # @param (see LLM::Provider#initialize)
50
50
  # @return (see LLM::Ollama#initialize)
51
51
  def ollama(key: nil, **)
52
52
  require_relative "llm/providers/ollama" unless defined?(LLM::Ollama)
@@ -54,7 +54,16 @@ module LLM
54
54
  end
55
55
 
56
56
  ##
57
- # @param secret (see LLM::OpenAI#initialize)
57
+ # @param key (see LLM::Provider#initialize)
58
+ # @return (see LLM::LlamaCpp#initialize)
59
+ def llamacpp(key: nil, **)
60
+ require_relative "llm/providers/openai" unless defined?(LLM::OpenAI)
61
+ require_relative "llm/providers/llamacpp" unless defined?(LLM::LlamaCpp)
62
+ LLM::LlamaCpp.new(key:, **)
63
+ end
64
+
65
+ ##
66
+ # @param key (see LLM::Provider#initialize)
58
67
  # @return (see LLM::OpenAI#initialize)
59
68
  def openai(**)
60
69
  require_relative "llm/providers/openai" unless defined?(LLM::OpenAI)
@@ -64,15 +73,15 @@ module LLM
64
73
  ##
65
74
  # Define a function
66
75
  # @example
67
- # LLM.function(:system) do |fn|
68
- # fn.description "Run system command"
69
- # fn.params do |schema|
70
- # schema.object(command: schema.string.required)
71
- # end
72
- # fn.define do |params|
73
- # system(params.command)
76
+ # LLM.function(:system) do |fn|
77
+ # fn.description "Run system command"
78
+ # fn.params do |schema|
79
+ # schema.object(command: schema.string.required)
80
+ # end
81
+ # fn.define do |params|
82
+ # system(params.command)
83
+ # end
74
84
  # end
75
- # end
76
85
  # @param [Symbol] name The name of the function
77
86
  # @param [Proc] b The block to define the function
78
87
  # @return [LLM::Function] The function object
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.6.1
4
+ version: 0.7.0
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-06 00:00:00.000000000 Z
12
+ date: 2025-05-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: webmock
@@ -176,6 +176,10 @@ files:
176
176
  - lib/llm.rb
177
177
  - lib/llm/buffer.rb
178
178
  - lib/llm/chat.rb
179
+ - lib/llm/chat/builder.rb
180
+ - lib/llm/chat/conversable.rb
181
+ - lib/llm/chat/prompt/completion.rb
182
+ - lib/llm/chat/prompt/respond.rb
179
183
  - lib/llm/core_ext/ostruct.rb
180
184
  - lib/llm/error.rb
181
185
  - lib/llm/file.rb
@@ -202,6 +206,7 @@ files:
202
206
  - lib/llm/providers/gemini/models.rb
203
207
  - lib/llm/providers/gemini/response_parser.rb
204
208
  - lib/llm/providers/gemini/response_parser/completion_parser.rb
209
+ - lib/llm/providers/llamacpp.rb
205
210
  - lib/llm/providers/ollama.rb
206
211
  - lib/llm/providers/ollama/error_handler.rb
207
212
  - lib/llm/providers/ollama/format.rb