ruby_llm 0.1.0.pre18 → 0.1.0.pre20

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: cb34b8e16d243dfdc95d7fc6a70a67ecb2afac895da5a646e57dbc915da0a8e9
4
- data.tar.gz: 6e95fb39b3670a38422b0dd79ec97263c34e6143751fef2272a6401ccac79c95
3
+ metadata.gz: c12d4699a9959fb454d21065bc5d666ef59873274f6bcaacb79c63cb7fdf9559
4
+ data.tar.gz: f391e7cb0970a1362238a5251bb7fc7316d5358af9542f059c112e84d0bed6cd
5
5
  SHA512:
6
- metadata.gz: af33908a0ff937c4a7d0b074b4406c4211456dbb92c39d5c2237ca6463f3bd8129141a4b2c23270da85fbf90d26c0e8c6b295aaedb8574a23e0ab3eb107abd39
7
- data.tar.gz: c6ce63d8d361fffcfaf6bfa2fbdea1df42ddf75e2969d68c5f789fbb14fccb61284c9a6b46fb0c0472c0e18cb31da150544b3cf2e9af4a1e53039a164b4d3e9f
6
+ metadata.gz: c20e3a9addf60aaa9e475f433ac3ae707d5f07a89f7f4b95c785749f9ad051194d306d6f00ba9570c06cf9b0488ac711646c44251528899304ec2945883fb097
7
+ data.tar.gz: 9f9f1dca17a24b0c478d98755722721410706f3e5f2993d19290fc55c69da3d3cc3d058fe01826e92e828446e74a552a36b3ede40561d32a65ffd72237ce9a36
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Carmine Paolino
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -82,16 +82,13 @@ Need vector embeddings for your text? RubyLLM makes it simple:
82
82
 
83
83
  ```ruby
84
84
  # Get embeddings with the default model
85
- vector = RubyLLM.embed "Hello, world!"
85
+ RubyLLM.embed "Hello, world!"
86
86
 
87
87
  # Use a specific model
88
- vector = RubyLLM.embed(
89
- "Ruby is awesome!",
90
- model: "text-embedding-3-large"
91
- )
88
+ RubyLLM.embed "Ruby is awesome!", model: "text-embedding-3-large"
92
89
 
93
90
  # Process multiple texts at once
94
- vectors = RubyLLM.embed([
91
+ RubyLLM.embed([
95
92
  "First document",
96
93
  "Second document",
97
94
  "Third document"
@@ -191,16 +188,30 @@ end
191
188
  class CreateMessages < ActiveRecord::Migration[8.0]
192
189
  def change
193
190
  create_table :messages do |t|
194
- t.references :chat
191
+ t.references :chat, null: false
195
192
  t.string :role
196
193
  t.text :content
197
- t.json :tool_calls, default: {}
198
- t.string :tool_call_id
194
+ t.string :model_id
199
195
  t.integer :input_tokens
200
196
  t.integer :output_tokens
201
- t.string :model_id
197
+ t.references :tool_call
198
+ t.timestamps
199
+ end
200
+ end
201
+ end
202
+
203
+ # db/migrate/YYYYMMDDHHMMSS_create_tool_calls.rb
204
+ class CreateToolCalls < ActiveRecord::Migration[8.0]
205
+ def change
206
+ create_table :tool_calls do |t|
207
+ t.references :message, null: false
208
+ t.string :tool_call_id, null: false
209
+ t.string :name, null: false
210
+ t.jsonb :arguments, default: {}
202
211
  t.timestamps
203
212
  end
213
+
214
+ add_index :tool_calls, :tool_call_id
204
215
  end
205
216
  end
206
217
  ```
@@ -218,20 +229,24 @@ end
218
229
  class Message < ApplicationRecord
219
230
  acts_as_message
220
231
  end
232
+
233
+ class ToolCall < ApplicationRecord
234
+ acts_as_tool_call
235
+ end
221
236
  ```
222
237
 
223
238
  That's it! Now you can use chats straight from your models:
224
239
 
225
240
  ```ruby
226
241
  # Create a new chat
227
- chat = Chat.create!(model_id: "gpt-4")
242
+ chat = Chat.create! model_id: "gpt-4o-mini"
228
243
 
229
244
  # Ask questions - messages are automatically saved
230
245
  chat.ask "What's the weather in Paris?"
231
246
 
232
247
  # Stream responses in real-time
233
248
  chat.ask "Tell me a story" do |chunk|
234
- broadcast_chunk(chunk)
249
+ broadcast_chunk chunk
235
250
  end
236
251
 
237
252
  # Everything is persisted automatically
@@ -289,7 +304,7 @@ The persistence works seamlessly with background jobs:
289
304
  ```ruby
290
305
  class ChatJob < ApplicationJob
291
306
  def perform(chat_id, message)
292
- chat = Chat.find(chat_id)
307
+ chat = Chat.find chat_id
293
308
 
294
309
  chat.ask(message) do |chunk|
295
310
  # Optional: Broadcast chunks for real-time updates
@@ -323,8 +338,8 @@ class WeatherTool < RubyLLM::Tool
323
338
  end
324
339
 
325
340
  # Use tools with your persisted chats
326
- chat = Chat.create!(model_id: "gpt-4")
327
- chat.chat.with_tool(WeatherTool.new)
341
+ chat = Chat.create! model_id: "gpt-4"
342
+ chat.chat.with_tool WeatherTool.new
328
343
 
329
344
  # Ask about weather - tool usage is automatically saved
330
345
  chat.ask "What's the weather in Paris?"
@@ -334,8 +349,6 @@ pp chat.messages.map(&:role)
334
349
  #=> [:user, :assistant, :tool, :assistant]
335
350
  ```
336
351
 
337
- Looking for more examples? Check out the [example Rails app](https://github.com/example/ruby_llm_rails) showing these patterns in action!
338
-
339
352
  ## Development
340
353
 
341
354
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt.
@@ -346,4 +359,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/crmne/
346
359
 
347
360
  ## License
348
361
 
349
- Released under the MIT License. See LICENSE.txt for details.
362
+ Released under the MIT License. See [LICENSE](LICENSE) for details.
@@ -8,13 +8,16 @@ module RubyLLM
8
8
  module ActsAs
9
9
  extend ActiveSupport::Concern
10
10
 
11
- class_methods do
12
- def acts_as_chat(message_class: 'Message') # rubocop:disable Metrics/MethodLength
11
+ class_methods do # rubocop:disable Metrics/BlockLength
12
+ def acts_as_chat(message_class: 'Message', tool_call_class: 'ToolCall') # rubocop:disable Metrics/MethodLength
13
13
  include ChatMethods
14
14
 
15
+ @message_class = message_class.to_s
16
+ @tool_call_class = tool_call_class.to_s
17
+
15
18
  has_many :messages,
16
19
  -> { order(created_at: :asc) },
17
- class_name: message_class.to_s,
20
+ class_name: @message_class,
18
21
  dependent: :destroy
19
22
 
20
23
  delegate :complete,
@@ -28,13 +31,35 @@ module RubyLLM
28
31
  to: :to_llm
29
32
  end
30
33
 
31
- def acts_as_message(chat_class: 'Chat')
34
+ def acts_as_message(chat_class: 'Chat', tool_call_class: 'ToolCall') # rubocop:disable Metrics/MethodLength
32
35
  include MessageMethods
33
36
 
34
- belongs_to :chat, class_name: chat_class.to_s
37
+ @chat_class = chat_class.to_s
38
+ @tool_call_class = tool_call_class.to_s
39
+
40
+ belongs_to :chat, class_name: @chat_class
41
+ has_many :tool_calls, class_name: @tool_call_class, dependent: :destroy
42
+
43
+ belongs_to :parent_tool_call,
44
+ class_name: @tool_call_class,
45
+ foreign_key: 'tool_call_id',
46
+ optional: true,
47
+ inverse_of: :result
35
48
 
36
49
  delegate :tool_call?, :tool_result?, :tool_results, to: :to_llm
37
50
  end
51
+
52
+ def acts_as_tool_call(message_class: 'Message')
53
+ @message_class = message_class.to_s
54
+
55
+ belongs_to :message, class_name: @message_class
56
+
57
+ has_one :result,
58
+ class_name: @message_class,
59
+ foreign_key: 'tool_call_id',
60
+ inverse_of: :parent_tool_call,
61
+ dependent: :nullify
62
+ end
38
63
  end
39
64
  end
40
65
 
@@ -43,6 +68,10 @@ module RubyLLM
43
68
  module ChatMethods
44
69
  extend ActiveSupport::Concern
45
70
 
71
+ class_methods do
72
+ attr_reader :tool_call_class
73
+ end
74
+
46
75
  def to_llm
47
76
  chat = RubyLLM.chat(model: model_id)
48
77
 
@@ -73,18 +102,32 @@ module RubyLLM
73
102
  )
74
103
  end
75
104
 
76
- def persist_message_completion(message)
105
+ def persist_message_completion(message) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
77
106
  return unless message
78
107
 
79
- messages.last.update!(
80
- role: message.role,
81
- content: message.content,
82
- model_id: message.model_id,
83
- tool_calls: message.tool_calls,
84
- tool_call_id: message.tool_call_id,
85
- input_tokens: message.input_tokens,
86
- output_tokens: message.output_tokens
87
- )
108
+ if message.tool_call_id
109
+ tool_call_id = self.class.tool_call_class.constantize.find_by(tool_call_id: message.tool_call_id).id
110
+ end
111
+
112
+ transaction do
113
+ messages.last.update!(
114
+ role: message.role,
115
+ content: message.content,
116
+ model_id: message.model_id,
117
+ tool_call_id: tool_call_id,
118
+ input_tokens: message.input_tokens,
119
+ output_tokens: message.output_tokens
120
+ )
121
+ persist_tool_calls(message.tool_calls) if message.tool_calls.present?
122
+ end
123
+ end
124
+
125
+ def persist_tool_calls(tool_calls)
126
+ tool_calls.each_value do |tool_call|
127
+ attributes = tool_call.to_h
128
+ attributes[:tool_call_id] = attributes.delete(:id)
129
+ messages.last.tool_calls.create!(**attributes)
130
+ end
88
131
  end
89
132
  end
90
133
 
@@ -97,13 +140,30 @@ module RubyLLM
97
140
  RubyLLM::Message.new(
98
141
  role: role.to_sym,
99
142
  content: content,
100
- tool_calls: tool_calls,
101
- tool_call_id: tool_call_id,
143
+ tool_calls: extract_tool_calls,
144
+ tool_call_id: extract_tool_call_id,
102
145
  input_tokens: input_tokens,
103
146
  output_tokens: output_tokens,
104
147
  model_id: model_id
105
148
  )
106
149
  end
150
+
151
+ def extract_tool_calls
152
+ tool_calls.to_h do |tool_call|
153
+ [
154
+ tool_call.tool_call_id,
155
+ RubyLLM::ToolCall.new(
156
+ id: tool_call.tool_call_id,
157
+ name: tool_call.name,
158
+ arguments: tool_call.arguments
159
+ )
160
+ ]
161
+ end
162
+ end
163
+
164
+ def extract_tool_call_id
165
+ parent_tool_call&.tool_call_id
166
+ end
107
167
  end
108
168
  end
109
169
  end
@@ -43,13 +43,14 @@ module RubyLLM
43
43
  payload[:tools] = tools.map { |_, tool| tool_for(tool) }
44
44
  payload[:tool_choice] = 'auto'
45
45
  end
46
+ payload[:stream_options] = { include_usage: true } if stream
46
47
  end
47
48
  end
48
49
 
49
50
  def format_messages(messages)
50
51
  messages.map do |msg|
51
52
  {
52
- role: msg.role.to_s,
53
+ role: format_role(msg.role),
53
54
  content: msg.content,
54
55
  tool_calls: format_tool_calls(msg.tool_calls),
55
56
  tool_call_id: msg.tool_call_id
@@ -57,6 +58,15 @@ module RubyLLM
57
58
  end
58
59
  end
59
60
 
61
+ def format_role(role)
62
+ case role
63
+ when :system
64
+ 'developer'
65
+ else
66
+ role.to_s
67
+ end
68
+ end
69
+
60
70
  def build_embedding_payload(text, model:)
61
71
  {
62
72
  model: model,
@@ -156,14 +166,16 @@ module RubyLLM
156
166
  end.compact
157
167
  end
158
168
 
159
- def handle_stream(&block)
169
+ def handle_stream(&block) # rubocop:disable Metrics/MethodLength
160
170
  to_json_stream do |data|
161
171
  block.call(
162
172
  Chunk.new(
163
173
  role: :assistant,
164
174
  model_id: data['model'],
165
175
  content: data.dig('choices', 0, 'delta', 'content'),
166
- tool_calls: parse_tool_calls(data.dig('choices', 0, 'delta', 'tool_calls'), parse_arguments: false)
176
+ tool_calls: parse_tool_calls(data.dig('choices', 0, 'delta', 'tool_calls'), parse_arguments: false),
177
+ input_tokens: data.dig('usage', 'prompt_tokens'),
178
+ output_tokens: data.dig('usage', 'completion_tokens')
167
179
  )
168
180
  )
169
181
  end
@@ -19,5 +19,13 @@ module RubyLLM
19
19
  @name = name
20
20
  @arguments = arguments
21
21
  end
22
+
23
+ def to_h
24
+ {
25
+ id: @id,
26
+ name: @name,
27
+ arguments: @arguments
28
+ }
29
+ end
22
30
  end
23
31
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- VERSION = '0.1.0.pre18'
4
+ VERSION = '0.1.0.pre20'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.pre18
4
+ version: 0.1.0.pre20
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carmine Paolino
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-02-06 00:00:00.000000000 Z
11
+ date: 2025-02-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: event_stream_parser
@@ -339,6 +339,7 @@ files:
339
339
  - ".rspec"
340
340
  - ".rubocop.yml"
341
341
  - Gemfile
342
+ - LICENSE
342
343
  - README.md
343
344
  - Rakefile
344
345
  - bin/console