llm.rb 0.7.2 → 0.9.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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +93 -63
  3. data/lib/llm/{chat → bot}/builder.rb +1 -1
  4. data/lib/llm/bot/conversable.rb +31 -0
  5. data/lib/llm/{chat → bot}/prompt/completion.rb +14 -4
  6. data/lib/llm/{chat → bot}/prompt/respond.rb +16 -5
  7. data/lib/llm/{chat.rb → bot.rb} +48 -66
  8. data/lib/llm/buffer.rb +2 -2
  9. data/lib/llm/error.rb +24 -16
  10. data/lib/llm/event_handler.rb +44 -0
  11. data/lib/llm/eventstream/event.rb +69 -0
  12. data/lib/llm/eventstream/parser.rb +88 -0
  13. data/lib/llm/eventstream.rb +8 -0
  14. data/lib/llm/function.rb +9 -12
  15. data/lib/{json → llm/json}/schema/array.rb +1 -1
  16. data/lib/llm/message.rb +1 -1
  17. data/lib/llm/model.rb +1 -1
  18. data/lib/llm/object/builder.rb +38 -0
  19. data/lib/llm/object/kernel.rb +45 -0
  20. data/lib/llm/object.rb +77 -0
  21. data/lib/llm/provider.rb +68 -26
  22. data/lib/llm/providers/anthropic/error_handler.rb +3 -3
  23. data/lib/llm/providers/anthropic/models.rb +3 -7
  24. data/lib/llm/providers/anthropic/response_parser/completion_parser.rb +5 -5
  25. data/lib/llm/providers/anthropic/response_parser.rb +1 -0
  26. data/lib/llm/providers/anthropic/stream_parser.rb +66 -0
  27. data/lib/llm/providers/anthropic.rb +9 -4
  28. data/lib/llm/providers/deepseek/format/completion_format.rb +68 -0
  29. data/lib/llm/providers/deepseek/format.rb +28 -0
  30. data/lib/llm/providers/deepseek.rb +60 -0
  31. data/lib/llm/providers/gemini/error_handler.rb +4 -4
  32. data/lib/llm/providers/gemini/files.rb +13 -16
  33. data/lib/llm/providers/gemini/images.rb +4 -8
  34. data/lib/llm/providers/gemini/models.rb +3 -7
  35. data/lib/llm/providers/gemini/response_parser/completion_parser.rb +2 -2
  36. data/lib/llm/providers/gemini/stream_parser.rb +69 -0
  37. data/lib/llm/providers/gemini.rb +19 -11
  38. data/lib/llm/providers/llamacpp.rb +16 -2
  39. data/lib/llm/providers/ollama/error_handler.rb +3 -3
  40. data/lib/llm/providers/ollama/format/completion_format.rb +1 -1
  41. data/lib/llm/providers/ollama/models.rb +3 -7
  42. data/lib/llm/providers/ollama/response_parser/completion_parser.rb +2 -2
  43. data/lib/llm/providers/ollama/stream_parser.rb +44 -0
  44. data/lib/llm/providers/ollama.rb +16 -9
  45. data/lib/llm/providers/openai/audio.rb +5 -9
  46. data/lib/llm/providers/openai/error_handler.rb +3 -3
  47. data/lib/llm/providers/openai/files.rb +15 -18
  48. data/lib/llm/providers/openai/format/moderation_format.rb +35 -0
  49. data/lib/llm/providers/openai/format.rb +3 -3
  50. data/lib/llm/providers/openai/images.rb +8 -11
  51. data/lib/llm/providers/openai/models.rb +3 -7
  52. data/lib/llm/providers/openai/moderations.rb +67 -0
  53. data/lib/llm/providers/openai/response_parser/completion_parser.rb +5 -5
  54. data/lib/llm/providers/openai/response_parser/respond_parser.rb +2 -2
  55. data/lib/llm/providers/openai/response_parser.rb +15 -0
  56. data/lib/llm/providers/openai/responses.rb +14 -16
  57. data/lib/llm/providers/openai/stream_parser.rb +77 -0
  58. data/lib/llm/providers/openai.rb +22 -7
  59. data/lib/llm/providers/voyageai/error_handler.rb +3 -3
  60. data/lib/llm/providers/voyageai.rb +1 -1
  61. data/lib/llm/response/filelist.rb +1 -1
  62. data/lib/llm/response/image.rb +1 -1
  63. data/lib/llm/response/modellist.rb +1 -1
  64. data/lib/llm/response/moderationlist/moderation.rb +47 -0
  65. data/lib/llm/response/moderationlist.rb +51 -0
  66. data/lib/llm/response.rb +1 -0
  67. data/lib/llm/version.rb +1 -1
  68. data/lib/llm.rb +13 -4
  69. data/llm.gemspec +2 -2
  70. metadata +42 -28
  71. data/lib/llm/chat/conversable.rb +0 -53
  72. data/lib/llm/core_ext/ostruct.rb +0 -43
  73. /data/lib/{json → llm/json}/schema/boolean.rb +0 -0
  74. /data/lib/{json → llm/json}/schema/integer.rb +0 -0
  75. /data/lib/{json → llm/json}/schema/leaf.rb +0 -0
  76. /data/lib/{json → llm/json}/schema/null.rb +0 -0
  77. /data/lib/{json → llm/json}/schema/number.rb +0 -0
  78. /data/lib/{json → llm/json}/schema/object.rb +0 -0
  79. /data/lib/{json → llm/json}/schema/string.rb +0 -0
  80. /data/lib/{json → llm/json}/schema/version.rb +0 -0
  81. /data/lib/{json → llm/json}/schema.rb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2c8c175d5b640f4d04e114569781bb2cad10b17f76489f4eaecd2ea7963c2baf
4
- data.tar.gz: b00e4c4b7bf9211157e66f1ac015540ecec86b1f4d6d8b3bcaa46068ac97fbaf
3
+ metadata.gz: eb2885c3c77d0ac7555b59fd57ccbf15ed5c72e2b385f5f4d83f8ea906b34171
4
+ data.tar.gz: 0cf0fa38bec61167de57441f11c389f13a1f86ea9dd04caf597698363fa53c71
5
5
  SHA512:
6
- metadata.gz: e1d27b0fa9a59ec4baf55e7863675c71827c66af091f4b4d2db268e08d83709bbb87c23175c0dda8e2583ced40a2c76bafcba95f118ec64e6f44245878e668dd
7
- data.tar.gz: 0201f576e4180dfef2ace9b699711eb013ac4556baf2bc9a584060000ec94fb7f764d1bb1e0d9d8187f76deeea6761e2323f661974e5e93fb50a29e0a62207d8
6
+ metadata.gz: 280bccde2d4d730485845440986b27ce7b753cdde7f080bc3c7e2f1381a3e924fa1aebe48fe734d9d6d58c5d0d5ff7e182a5d7bba5b3e390c79c4c2738bbd8c2
7
+ data.tar.gz: dba89769f1fe6f35ac98e7ad6fce9e90775e8b61e47ee3215e8fb8df555d9787f9d289206bd3cdc60b3b1bdee4a5217e8e69b1d3aa33f29e0e193ba90f0e33f8
data/README.md CHANGED
@@ -1,9 +1,10 @@
1
1
  ## About
2
2
 
3
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.
4
+ includes OpenAI, Gemini, Anthropic, DeepSeek, Ollama, and LlamaCpp.
5
+ It's fast, simple and composable – with full support for chat,
6
+ streaming, tool calling, audio, images, files, and JSON Schema
7
+ generation.
7
8
 
8
9
  ## Features
9
10
 
@@ -11,12 +12,12 @@ images, files, and JSON Schema generation.
11
12
  - ✅ A single unified interface for multiple providers
12
13
  - 📦 Zero dependencies outside Ruby's standard library
13
14
  - 🚀 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
18
18
  - 🤖 Tool calling and function execution
19
19
  - 🗂️ JSON Schema support for structured, validated responses
20
+ - 📡 Streaming support for real-time response updates
20
21
 
21
22
  #### Media
22
23
  - 🗣️ Text-to-speech, transcription, and translation
@@ -24,24 +25,25 @@ images, files, and JSON Schema generation.
24
25
  - 📎 File uploads and prompt-aware file interaction
25
26
  - 💡 Multimodal prompts (text, images, PDFs, URLs, files)
26
27
 
27
- #### Embeddings
28
+ #### Miscellaneous
28
29
  - 🧮 Text embeddings and vector support
30
+ - 🔌 Retrieve models dynamically for introspection and selection
29
31
 
30
32
  ## Demos
31
33
 
32
34
  <details>
33
35
  <summary><b>1. Tools: "system" function</b></summary>
34
- <img src="share/llm-shell/examples/toolcalls.gif">
36
+ <img src="https://github.com/llmrb/llm/raw/main/share/llm-shell/examples/toolcalls.gif">
35
37
  </details>
36
38
 
37
39
  <details>
38
- <summary><b>2. Files: import at boot time</b></summary>
39
- <img src="share/llm-shell/examples/files-boottime.gif">
40
+ <summary><b>2. Files: import at runtime</b></summary>
41
+ <img src="https://github.com/llmrb/llm/raw/main/share/llm-shell/examples/files-runtime.gif">
40
42
  </details>
41
43
 
42
44
  <details>
43
- <summary><b>3. Files: import at runtime</b></summary>
44
- <img src="share/llm-shell/examples/files-runtime.gif">
45
+ <summary><b>3. Files: import at boot time</b></summary>
46
+ <img src="https://github.com/llmrb/llm/raw/main/share/llm-shell/examples/files-boottime.gif">
45
47
  </details>
46
48
 
47
49
  ## Examples
@@ -59,12 +61,18 @@ using an API key (if required) and an optional set of configuration options via
59
61
  #!/usr/bin/env ruby
60
62
  require "llm"
61
63
 
64
+ ##
65
+ # remote providers
62
66
  llm = LLM.openai(key: "yourapikey")
63
67
  llm = LLM.gemini(key: "yourapikey")
64
68
  llm = LLM.anthropic(key: "yourapikey")
69
+ llm = LLM.deepseek(key: "yourapikey")
70
+ llm = LLM.voyageai(key: "yourapikey")
71
+
72
+ ##
73
+ # local providers
65
74
  llm = LLM.ollama(key: nil)
66
75
  llm = LLM.llamacpp(key: nil)
67
- llm = LLM.voyageai(key: "yourapikey")
68
76
  ```
69
77
 
70
78
  ### Conversations
@@ -73,24 +81,24 @@ llm = LLM.voyageai(key: "yourapikey")
73
81
 
74
82
  > This example uses the stateless chat completions API that all
75
83
  > providers support. A similar example for OpenAI's stateful
76
- > responses API is available in the [docs/](docs/OPENAI_RESPONSES.md)
84
+ > responses API is available in the [docs/](docs/OPENAI.md#responses)
77
85
  > directory.
78
86
 
79
- The following example enables lazy mode for a
80
- [LLM::Chat](https://0x1eef.github.io/x/llm.rb/LLM/Chat.html)
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
83
- maintain a message thread that can be reused as context throughout a conversation.
84
- The example captures the spirit of llm.rb by demonstrating how objects cooperate
85
- together through composition, and it uses the stateless chat completions API that
86
- all LLM providers support:
87
+ The following example creates an instance of
88
+ [LLM::Bot](https://0x1eef.github.io/x/llm.rb/LLM/Bot.html)
89
+ by entering into a conversation where messages are buffered and
90
+ sent to the provider on-demand. This is the default behavior
91
+ because it can reduce the number of requests sent to a provider,
92
+ and avoids unneccessary requests until an attempt to iterate over
93
+ [LLM::Bot#messages](https://0x1eef.github.io/x/llm.rb/LLM/Bot.html#messages-instance_method)
94
+ is made:
87
95
 
88
96
  ```ruby
89
97
  #!/usr/bin/env ruby
90
98
  require "llm"
91
99
 
92
100
  llm = LLM.openai(key: ENV["KEY"])
93
- bot = LLM::Chat.new(llm).lazy
101
+ bot = LLM::Bot.new(llm)
94
102
  msgs = bot.chat do |prompt|
95
103
  prompt.system File.read("./share/llm/prompts/system.txt")
96
104
  prompt.user "Tell me the answer to 5 + 15"
@@ -100,70 +108,92 @@ end
100
108
 
101
109
  # At this point, we execute a single request
102
110
  msgs.each { print "[#{_1.role}] ", _1.content, "\n" }
111
+ ```
103
112
 
104
- ##
105
- # [system] You are my math assistant.
106
- # I will provide you with (simple) equations.
107
- # You will provide answers in the format "The answer to <equation> is <answer>".
108
- # I will provide you a set of messages. Reply to all of them.
109
- # A message is considered unanswered if there is no corresponding assistant response.
110
- #
111
- # [user] Tell me the answer to 5 + 15
112
- # [user] Tell me the answer to (5 + 15) * 2
113
- # [user] Tell me the answer to ((5 + 15) * 2) / 10
114
- #
115
- # [assistant] The answer to 5 + 15 is 20.
116
- # The answer to (5 + 15) * 2 is 40.
117
- # The answer to ((5 + 15) * 2) / 10 is 4.
113
+ #### Streaming
114
+
115
+ > There Is More Than One Way To Do It (TIMTOWTDI) when you are
116
+ > using llm.rb &ndash; and this is especially true when it
117
+ > comes to streaming. See the streaming documentation in
118
+ > [docs/](docs/STREAMING.md#flexibility) for more details.
119
+
120
+ The following example streams the messages in a conversation
121
+ as they are generated in real-time. This feature can be useful
122
+ in case you want to see the contents of a message as it is
123
+ generated, or in case you want to avoid potential read timeouts
124
+ during the generation of a response.
125
+
126
+ The `stream` option can be set to an IO object, or the value `true`
127
+ to enable streaming &ndash; and at the end of the request, `bot.chat`
128
+ returns the same response as the non-streaming version which allows
129
+ you to process a response in the same way:
130
+
131
+ ```ruby
132
+ #!/usr/bin/env ruby
133
+ require "llm"
134
+
135
+ llm = LLM.openai(key: ENV["KEY"])
136
+ bot = LLM::Bot.new(llm)
137
+ bot.chat(stream: $stdout) do |prompt|
138
+ prompt.system "You are my math assistant."
139
+ prompt.user "Tell me the answer to 5 + 15"
140
+ prompt.user "Tell me the answer to (5 + 15) * 2"
141
+ prompt.user "Tell me the answer to ((5 + 15) * 2) / 10"
142
+ end.to_a
118
143
  ```
119
144
 
120
145
  ### Schema
121
146
 
122
147
  #### Structured
123
148
 
124
- All LLM providers except Anthropic allow a client to describe the structure
125
- of a response that a LLM emits according to a schema that is described by JSON.
126
- The schema lets a client describe what JSON object (or value) an LLM should emit,
127
- and the LLM will abide by the schema. See also: [JSON Schema website](https://json-schema.org/overview/what-is-jsonschema).
128
- We will use the
129
- [llmrb/json-schema](https://github.com/llmrb/json-schema)
130
- library for the sake of the examples &ndash; the interface is designed so you
131
- could drop in any other library in its place:
149
+ All LLM providers except Anthropic and DeepSeek allow a client to describe
150
+ the structure of a response that a LLM emits according to a schema that is
151
+ described by JSON. The schema lets a client describe what JSON object (or value)
152
+ an LLM should emit, and the LLM will abide by the schema:
132
153
 
133
154
  ```ruby
134
155
  #!/usr/bin/env ruby
135
156
  require "llm"
136
157
 
158
+ ##
159
+ # Objects
137
160
  llm = LLM.openai(key: ENV["KEY"])
138
- schema = llm.schema.object({fruit: llm.schema.string.enum("Apple", "Orange", "Pineapple")})
139
- bot = LLM::Chat.new(llm, schema:).lazy
161
+ schema = llm.schema.object(answer: llm.schema.integer.required)
162
+ bot = LLM::Bot.new(llm, schema:)
163
+ bot.chat "Does the earth orbit the sun?", role: :user
164
+ bot.messages.find(&:assistant?).content! # => {probability: 1}
165
+
166
+ ##
167
+ # Enums
168
+ schema = llm.schema.object(fruit: llm.schema.string.enum("Apple", "Orange", "Pineapple"))
169
+ bot = LLM::Bot.new(llm, schema:)
140
170
  bot.chat "Your favorite fruit is Pineapple", role: :system
141
171
  bot.chat "What fruit is your favorite?", role: :user
142
172
  bot.messages.find(&:assistant?).content! # => {fruit: "Pineapple"}
143
173
 
144
- schema = llm.schema.object({answer: llm.schema.integer.required})
145
- bot = LLM::Chat.new(llm, schema:).lazy
174
+ ##
175
+ # Arrays
176
+ schema = llm.schema.object(answers: llm.schema.array(llm.schema.integer.required))
177
+ bot = LLM::Bot.new(llm, schema:)
178
+ bot.chat "Answer all of my questions", role: :system
146
179
  bot.chat "Tell me the answer to ((5 + 5) / 2)", role: :user
147
- bot.messages.find(&:assistant?).content! # => {answer: 5}
148
-
149
- schema = llm.schema.object({probability: llm.schema.number.required})
150
- bot = LLM::Chat.new(llm, schema:).lazy
151
- bot.chat "Does the earth orbit the sun?", role: :user
152
- bot.messages.find(&:assistant?).content! # => {probability: 1}
180
+ bot.chat "Tell me the answer to ((5 + 5) / 2) * 2", role: :user
181
+ bot.chat "Tell me the answer to ((5 + 5) / 2) * 2 + 1", role: :user
182
+ bot.messages.find(&:assistant?).content! # => {answers: [5, 10, 11]}
153
183
  ```
154
184
 
155
185
  ### Tools
156
186
 
157
187
  #### Functions
158
188
 
159
- The OpenAI, Anthropic, Gemini and Ollama providers support a powerful feature known as
160
- tool calling, and although it is a little complex to understand at first,
161
- it can be powerful for building agents. The following example demonstrates how we
162
- can define a local function (which happens to be a tool), and OpenAI can
163
- then detect when we should call the function.
189
+ All providers support a powerful feature known as tool calling, and although
190
+ it is a little complex to understand at first, it can be powerful for building
191
+ agents. The following example demonstrates how we can define a local function
192
+ (which happens to be a tool), and a provider (such as OpenAI) can then detect
193
+ when we should call the function.
164
194
 
165
195
  The
166
- [LLM::Chat#functions](https://0x1eef.github.io/x/llm.rb/LLM/Chat.html#functions-instance_method)
196
+ [LLM::Bot#functions](https://0x1eef.github.io/x/llm.rb/LLM/Bot.html#functions-instance_method)
167
197
  method returns an array of functions that can be called after sending a message and
168
198
  it will only be populated if the LLM detects a function should be called. Each function
169
199
  corresponds to an element in the "tools" array. The array is emptied after a function call,
@@ -192,7 +222,7 @@ tool = LLM.function(:system) do |fn|
192
222
  end
193
223
  end
194
224
 
195
- bot = LLM::Chat.new(llm, tools: [tool]).lazy
225
+ bot = LLM::Bot.new(llm, tools: [tool])
196
226
  bot.chat "Your task is to run shell commands via a tool.", role: :system
197
227
 
198
228
  bot.chat "What is the current date?", role: :user
@@ -351,7 +381,7 @@ can be given to the chat method:
351
381
  require "llm"
352
382
 
353
383
  llm = LLM.openai(key: ENV["KEY"])
354
- bot = LLM::Chat.new(llm).lazy
384
+ bot = LLM::Bot.new(llm)
355
385
  file = llm.files.create(file: "/documents/openbsd_is_awesome.pdf")
356
386
  bot.chat(file)
357
387
  bot.chat("What is this file about?")
@@ -382,7 +412,7 @@ to a prompt:
382
412
  require "llm"
383
413
 
384
414
  llm = LLM.openai(key: ENV["KEY"])
385
- bot = LLM::Chat.new(llm).lazy
415
+ bot = LLM::Bot.new(llm)
386
416
 
387
417
  bot.chat [URI("https://example.com/path/to/image.png"), "Describe the image in the link"]
388
418
  bot.messages.select(&:assistant?).each { print "[#{_1.role}] ", _1.content, "\n" }
@@ -453,7 +483,7 @@ end
453
483
  ##
454
484
  # Select a model
455
485
  model = llm.models.all.find { |m| m.id == "gpt-3.5-turbo" }
456
- bot = LLM::Chat.new(llm, model:)
486
+ bot = LLM::Bot.new(llm, model:)
457
487
  bot.chat "Hello #{model.id} :)"
458
488
  bot.messages.select(&:assistant?).each { print "[#{_1.role}] ", _1.content, "\n" }
459
489
  ```
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Chat
3
+ class LLM::Bot
4
4
  ##
5
5
  # @private
6
6
  module Builder
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Bot
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
+ # Queues a completion to be sent to the provider.
21
+ # @param [String] prompt The prompt
22
+ # @param [Hash] params
23
+ # @return [void]
24
+ def async_completion(prompt, params = {})
25
+ role = params.delete(:role)
26
+ @messages.push [LLM::Message.new(role, prompt), @params.merge(params), :complete]
27
+ end
28
+
29
+ include LLM
30
+ end
31
+ end
@@ -1,20 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::Chat::Prompt
4
- class Completion < Struct.new(:bot)
3
+ module LLM::Bot::Prompt
4
+ class Completion < Struct.new(:bot, :defaults)
5
+ ##
6
+ # @param [LLM::Bot] bot
7
+ # @param [Hash] defaults
8
+ # @return [LLM::Bot::Prompt::Completion]
9
+ def initialize(bot, defaults)
10
+ super(bot, defaults || {})
11
+ end
12
+
5
13
  ##
6
14
  # @param [String] prompt
7
15
  # @param [Hash] params (see LLM::Provider#complete)
8
- # @return [LLM::Chat]
16
+ # @return [LLM::Bot]
9
17
  def system(prompt, params = {})
18
+ params = defaults.merge(params)
10
19
  bot.chat prompt, params.merge(role: :system)
11
20
  end
12
21
 
13
22
  ##
14
23
  # @param [String] prompt
15
24
  # @param [Hash] params (see LLM::Provider#complete)
16
- # @return [LLM::Chat]
25
+ # @return [LLM::Bot]
17
26
  def user(prompt, params = {})
27
+ params = defaults.merge(params)
18
28
  bot.chat prompt, params.merge(role: :user)
19
29
  end
20
30
  end
@@ -1,28 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::Chat::Prompt
4
- class Respond < Struct.new(:bot)
3
+ module LLM::Bot::Prompt
4
+ class Respond < Struct.new(:bot, :defaults)
5
+ ##
6
+ # @param [LLM::Bot] bot
7
+ # @param [Hash] defaults
8
+ # @return [LLM::Bot::Prompt::Completion]
9
+ def initialize(bot, defaults)
10
+ super(bot, defaults || {})
11
+ end
12
+
5
13
  ##
6
14
  # @param [String] prompt
7
15
  # @param [Hash] params (see LLM::Provider#complete)
8
- # @return [LLM::Chat]
16
+ # @return [LLM::Bot]
9
17
  def system(prompt, params = {})
18
+ params = defaults.merge(params)
10
19
  bot.respond prompt, params.merge(role: :system)
11
20
  end
12
21
 
13
22
  ##
14
23
  # @param [String] prompt
15
24
  # @param [Hash] params (see LLM::Provider#complete)
16
- # @return [LLM::Chat]
25
+ # @return [LLM::Bot]
17
26
  def developer(prompt, params = {})
27
+ params = defaults.merge(params)
18
28
  bot.respond prompt, params.merge(role: :developer)
19
29
  end
20
30
 
21
31
  ##
22
32
  # @param [String] prompt
23
33
  # @param [Hash] params (see LLM::Provider#complete)
24
- # @return [LLM::Chat]
34
+ # @return [LLM::Bot]
25
35
  def user(prompt, params = {})
36
+ params = defaults.merge(params)
26
37
  bot.respond prompt, params.merge(role: :user)
27
38
  end
28
39
  end
@@ -2,47 +2,48 @@
2
2
 
3
3
  module LLM
4
4
  ##
5
- # {LLM::Chat LLM::Chat} provides a chat object that maintains a
6
- # thread of messages that acts as context throughout a conversation.
7
- # A conversation can use the chat completions API that most LLM providers
8
- # support or the responses API that a select few LLM providers support.
5
+ # {LLM::Bot LLM::Bot} provides a bot object that can maintain a
6
+ # a conversation. A conversation can use the chat completions API
7
+ # that all LLM providers support or the responses API that a select
8
+ # few LLM providers support.
9
9
  #
10
- # @example
10
+ # @example example #1
11
11
  # #!/usr/bin/env ruby
12
12
  # require "llm"
13
13
  #
14
14
  # llm = LLM.openai(ENV["KEY"])
15
- # bot = LLM::Chat.new(llm).lazy
15
+ # bot = LLM::Bot.new(llm)
16
16
  # msgs = bot.chat do |prompt|
17
17
  # prompt.system "Answer the following questions."
18
18
  # prompt.user "What is 5 + 7 ?"
19
19
  # prompt.user "Why is the sky blue ?"
20
20
  # prompt.user "Why did the chicken cross the road ?"
21
21
  # end
22
- # msgs.map { print "[#{_1.role}]", _1.content, "\n" }
22
+ # msgs.each { print "[#{_1.role}]", _1.content, "\n" }
23
23
  #
24
- # @example
24
+ # @example example #2
25
25
  # #!/usr/bin/env ruby
26
26
  # require "llm"
27
27
  #
28
28
  # llm = LLM.openai(ENV["KEY"])
29
- # bot = LLM::Chat.new(llm).lazy
29
+ # bot = LLM::Bot.new(llm)
30
30
  # bot.chat "Answer the following questions.", role: :system
31
31
  # bot.chat "What is 5 + 7 ?", role: :user
32
32
  # bot.chat "Why is the sky blue ?", role: :user
33
33
  # bot.chat "Why did the chicken cross the road ?", role: :user
34
- # bot.messages.map { print "[#{_1.role}]", _1.content, "\n" }
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"
34
+ # bot.messages.each { print "[#{_1.role}]", _1.content, "\n" }
35
+ class Bot
36
+ require_relative "bot/prompt/completion"
37
+ require_relative "bot/prompt/respond"
38
+ require_relative "bot/conversable"
39
+ require_relative "bot/builder"
40
40
 
41
41
  include Conversable
42
42
  include Builder
43
43
 
44
44
  ##
45
- # @return [Array<LLM::Message>]
45
+ # Returns an Enumerable for the messages in a conversation
46
+ # @return [LLM::Buffer<LLM::Message>]
46
47
  attr_reader :messages
47
48
 
48
49
  ##
@@ -58,72 +59,68 @@ module LLM
58
59
  def initialize(provider, params = {})
59
60
  @provider = provider
60
61
  @params = {model: provider.default_model, schema: nil}.compact.merge!(params)
61
- @lazy = false
62
- @messages = [].extend(Array)
62
+ @messages = LLM::Buffer.new(provider)
63
63
  end
64
64
 
65
65
  ##
66
66
  # Maintain a conversation via the chat completions API
67
- # @param prompt (see LLM::Provider#complete)
68
- # @param params (see LLM::Provider#complete)
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
67
+ # @overload def chat(prompt, params = {})
68
+ # @param prompt (see LLM::Provider#complete)
69
+ # @param params The params
70
+ # @return [LLM::Bot]
71
+ # Returns self
72
+ # @overload def chat(prompt, params, &block)
73
+ # @param prompt (see LLM::Provider#complete)
74
+ # @param params The params
75
+ # @yield prompt Yields a prompt
76
+ # @return [LLM::Buffer]
77
+ # Returns messages
72
78
  def chat(prompt = nil, params = {})
73
79
  if block_given?
74
- yield Prompt::Completion.new(self)
80
+ params = prompt
81
+ yield Prompt::Completion.new(self, params)
75
82
  messages
76
83
  elsif prompt.nil?
77
84
  raise ArgumentError, "wrong number of arguments (given 0, expected 1)"
78
85
  else
79
86
  params = {role: :user}.merge!(params)
80
- tap { lazy? ? async_completion(prompt, params) : sync_completion(prompt, params) }
87
+ tap { async_completion(prompt, params) }
81
88
  end
82
89
  end
83
90
 
84
91
  ##
85
92
  # Maintain a conversation via the responses API
86
- # @note Not all LLM providers support this API
87
- # @param prompt (see LLM::Provider#complete)
88
- # @param params (see LLM::Provider#complete)
89
- # @return [LLM::Chat, Array<LLM::Message>, LLM::Buffer]
90
- # Returns self unless given a block, otherwise returns messages
93
+ # @overload def respond(prompt, params = {})
94
+ # @param prompt (see LLM::Provider#complete)
95
+ # @param params The params
96
+ # @return [LLM::Bot]
97
+ # Returns self
98
+ # @overload def respond(prompt, params, &block)
99
+ # @note Not all LLM providers support this API
100
+ # @param prompt (see LLM::Provider#complete)
101
+ # @param params The params
102
+ # @yield prompt Yields a prompt
103
+ # @return [LLM::Buffer]
104
+ # Returns messages
91
105
  def respond(prompt = nil, params = {})
92
106
  if block_given?
93
- yield Prompt::Respond.new(self)
107
+ params = prompt
108
+ yield Prompt::Respond.new(self, params)
94
109
  messages
95
110
  elsif prompt.nil?
96
111
  raise ArgumentError, "wrong number of arguments (given 0, expected 1)"
97
112
  else
98
113
  params = {role: :user}.merge!(params)
99
- tap { lazy? ? async_response(prompt, params) : sync_response(prompt, params) }
114
+ tap { async_response(prompt, params) }
100
115
  end
101
116
  end
102
117
 
103
- ##
104
- # Enables lazy mode for the conversation.
105
- # @return [LLM::Chat]
106
- def lazy
107
- tap do
108
- next if lazy?
109
- @lazy = true
110
- @messages = LLM::Buffer.new(@provider)
111
- end
112
- end
113
-
114
- ##
115
- # @return [Boolean]
116
- # Returns true if the conversation is lazy
117
- def lazy?
118
- @lazy
119
- end
120
-
121
118
  ##
122
119
  # @return [String]
123
120
  def inspect
124
121
  "#<#{self.class.name}:0x#{object_id.to_s(16)} " \
125
122
  "@provider=#{@provider.class}, @params=#{@params.inspect}, " \
126
- "@messages=#{@messages.inspect}, @lazy=#{@lazy.inspect}>"
123
+ "@messages=#{@messages.inspect}>"
127
124
  end
128
125
 
129
126
  ##
@@ -135,20 +132,5 @@ module LLM
135
132
  .flat_map(&:functions)
136
133
  .select(&:pending?)
137
134
  end
138
-
139
- private
140
-
141
- ##
142
- # @private
143
- module Array
144
- def find(...)
145
- reverse_each.find(...)
146
- end
147
-
148
- def unread
149
- reject(&:read?)
150
- end
151
- end
152
- private_constant :Array
153
135
  end
154
136
  end
data/lib/llm/buffer.rb CHANGED
@@ -82,7 +82,7 @@ module LLM
82
82
  message.content,
83
83
  params.merge(role:, messages:)
84
84
  )
85
- @completed.concat([*pendings, message, completion.choices[0]])
85
+ @completed.concat([*pendings, message, *completion.choices[0]])
86
86
  @pending.clear
87
87
  end
88
88
 
@@ -95,7 +95,7 @@ module LLM
95
95
  @response ? {previous_response_id: @response.id} : {}
96
96
  ].inject({}, &:merge!)
97
97
  @response = @provider.responses.create(message.content, params.merge(role:))
98
- @completed.concat([*pendings, message, @response.outputs[0]])
98
+ @completed.concat([*pendings, message, *@response.outputs[0]])
99
99
  @pending.clear
100
100
  end
101
101
  end