ruby_llm 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +80 -133
- data/lib/ruby_llm/active_record/acts_as.rb +144 -47
- data/lib/ruby_llm/aliases.json +187 -17
- data/lib/ruby_llm/attachment.rb +164 -0
- data/lib/ruby_llm/chat.rb +31 -20
- data/lib/ruby_llm/configuration.rb +34 -1
- data/lib/ruby_llm/connection.rb +121 -0
- data/lib/ruby_llm/content.rb +27 -79
- data/lib/ruby_llm/context.rb +30 -0
- data/lib/ruby_llm/embedding.rb +13 -5
- data/lib/ruby_llm/error.rb +2 -1
- data/lib/ruby_llm/image.rb +15 -8
- data/lib/ruby_llm/message.rb +14 -6
- data/lib/ruby_llm/mime_type.rb +67 -0
- data/lib/ruby_llm/model/info.rb +101 -0
- data/lib/ruby_llm/model/modalities.rb +22 -0
- data/lib/ruby_llm/model/pricing.rb +51 -0
- data/lib/ruby_llm/model/pricing_category.rb +48 -0
- data/lib/ruby_llm/model/pricing_tier.rb +34 -0
- data/lib/ruby_llm/model.rb +7 -0
- data/lib/ruby_llm/models.json +26279 -2362
- data/lib/ruby_llm/models.rb +95 -14
- data/lib/ruby_llm/provider.rb +48 -90
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +76 -13
- data/lib/ruby_llm/providers/anthropic/chat.rb +7 -14
- data/lib/ruby_llm/providers/anthropic/media.rb +49 -28
- data/lib/ruby_llm/providers/anthropic/models.rb +16 -16
- data/lib/ruby_llm/providers/anthropic/tools.rb +2 -2
- data/lib/ruby_llm/providers/anthropic.rb +3 -3
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +61 -2
- data/lib/ruby_llm/providers/bedrock/chat.rb +30 -73
- data/lib/ruby_llm/providers/bedrock/media.rb +59 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +50 -58
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +16 -0
- data/lib/ruby_llm/providers/bedrock.rb +14 -25
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +35 -2
- data/lib/ruby_llm/providers/deepseek.rb +3 -3
- data/lib/ruby_llm/providers/gemini/capabilities.rb +84 -3
- data/lib/ruby_llm/providers/gemini/chat.rb +8 -37
- data/lib/ruby_llm/providers/gemini/embeddings.rb +18 -34
- data/lib/ruby_llm/providers/gemini/images.rb +4 -3
- data/lib/ruby_llm/providers/gemini/media.rb +28 -111
- data/lib/ruby_llm/providers/gemini/models.rb +17 -23
- data/lib/ruby_llm/providers/gemini/tools.rb +1 -1
- data/lib/ruby_llm/providers/gemini.rb +3 -3
- data/lib/ruby_llm/providers/ollama/chat.rb +28 -0
- data/lib/ruby_llm/providers/ollama/media.rb +48 -0
- data/lib/ruby_llm/providers/ollama.rb +34 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +78 -3
- data/lib/ruby_llm/providers/openai/chat.rb +6 -4
- data/lib/ruby_llm/providers/openai/embeddings.rb +8 -12
- data/lib/ruby_llm/providers/openai/images.rb +3 -2
- data/lib/ruby_llm/providers/openai/media.rb +48 -21
- data/lib/ruby_llm/providers/openai/models.rb +17 -18
- data/lib/ruby_llm/providers/openai/tools.rb +9 -5
- data/lib/ruby_llm/providers/openai.rb +7 -5
- data/lib/ruby_llm/providers/openrouter/models.rb +88 -0
- data/lib/ruby_llm/providers/openrouter.rb +31 -0
- data/lib/ruby_llm/stream_accumulator.rb +4 -4
- data/lib/ruby_llm/streaming.rb +48 -13
- data/lib/ruby_llm/utils.rb +27 -0
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +15 -5
- data/lib/tasks/aliases.rake +235 -0
- data/lib/tasks/models_docs.rake +164 -121
- data/lib/tasks/models_update.rake +79 -0
- data/lib/tasks/release.rake +32 -0
- data/lib/tasks/vcr.rake +4 -2
- metadata +56 -32
- data/lib/ruby_llm/model_info.rb +0 -56
- data/lib/tasks/browser_helper.rb +0 -97
- data/lib/tasks/capability_generator.rb +0 -123
- data/lib/tasks/capability_scraper.rb +0 -224
- data/lib/tasks/cli_helper.rb +0 -22
- data/lib/tasks/code_validator.rb +0 -29
- data/lib/tasks/model_updater.rb +0 -66
- data/lib/tasks/models.rake +0 -43
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8a1f6385e98e9396c7f9d8021bd2e574f5f8c8aa1cc05c7f10311e2059101017
|
4
|
+
data.tar.gz: e7617280adc17488f9dbc810b424b22ee21f36c0684f0a5f37762dd290d6c0de
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 80bcef1cb440519b7eb146bd4064f8eb5a40cf1fd5e309ac582c6be8a3aee0a33b244e8c4961bc69ac763b2665adab918238a0d8787b41c2fb6e5e7fd6343ae3
|
7
|
+
data.tar.gz: 3d95b550d6d879ad29e20bb328cb09d7191b4055180cba95e237ddab2d1173845463d62e407c777d6b76425958531fb6e7abc664939478c9e89b41565971ad2b
|
data/README.md
CHANGED
@@ -1,26 +1,37 @@
|
|
1
1
|
<img src="/docs/assets/images/logotype.svg" alt="RubyLLM" height="120" width="250">
|
2
2
|
|
3
|
-
A delightful Ruby way to work with AI
|
4
|
-
|
5
|
-
<div
|
6
|
-
<img src="https://
|
7
|
-
 
|
8
|
-
<img src="https://
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
<img src="https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/
|
13
|
-
|
14
|
-
|
15
|
-
|
3
|
+
**A delightful Ruby way to work with AI.** RubyLLM provides **one** beautiful, Ruby-like interface to interact with modern AI models. Chat, generate images, create embeddings, and use tools – all with clean, expressive code that feels like Ruby, not like patching together multiple services.
|
4
|
+
|
5
|
+
<div class="provider-icons">
|
6
|
+
<img src="https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/anthropic-text.svg" alt="Anthropic" class="logo-small">
|
7
|
+
|
8
|
+
<img src="https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/bedrock-color.svg" alt="Bedrock" class="logo-medium">
|
9
|
+
<img src="https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/bedrock-text.svg" alt="Bedrock" class="logo-small">
|
10
|
+
|
11
|
+
<img src="https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/deepseek-color.svg" alt="DeepSeek" class="logo-medium">
|
12
|
+
<img src="https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/deepseek-text.svg" alt="DeepSeek" class="logo-small">
|
13
|
+
|
14
|
+
<img src="https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini-brand-color.svg" alt="Gemini" class="logo-large">
|
15
|
+
|
16
|
+
<img src="https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ollama.svg" alt="Ollama" class="logo-medium">
|
17
|
+
<img src="https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ollama-text.svg" alt="Ollama" class="logo-medium">
|
18
|
+
|
19
|
+
<img src="https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg" alt="OpenAI" class="logo-medium">
|
20
|
+
<img src="https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai-text.svg" alt="OpenAI" class="logo-medium">
|
21
|
+
|
22
|
+
<img src="https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openrouter.svg" alt="OpenRouter" class="logo-medium">
|
23
|
+
<img src="https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openrouter-text.svg" alt="OpenRouter" class="logo-small">
|
24
|
+
|
16
25
|
</div>
|
17
26
|
|
18
|
-
<
|
19
|
-
<a href="https://
|
20
|
-
<a href="https://
|
21
|
-
<a href="https://
|
27
|
+
<div class="badge-container">
|
28
|
+
<a href="https://badge.fury.io/rb/ruby_llm"><img src="https://badge.fury.io/rb/ruby_llm.svg" alt="Gem Version" /></a>
|
29
|
+
<a href="https://github.com/testdouble/standard"><img src="https://img.shields.io/badge/code_style-standard-brightgreen.svg" alt="Ruby Style Guide" /></a>
|
30
|
+
<a href="https://rubygems.org/gems/ruby_llm"><img alt="Gem Downloads" src="https://img.shields.io/gem/dt/ruby_llm"></a>
|
31
|
+
<a href="https://codecov.io/gh/crmne/ruby_llm"><img src="https://codecov.io/gh/crmne/ruby_llm/branch/main/graph/badge.svg" alt="codecov" /></a>
|
32
|
+
</div>
|
22
33
|
|
23
|
-
🤺 Battle tested at [💬
|
34
|
+
🤺 Battle tested at [💬 Chat with Work](https://chatwithwork.com)
|
24
35
|
|
25
36
|
## The problem with AI libraries
|
26
37
|
|
@@ -28,17 +39,6 @@ Every AI provider comes with its own client library, its own response format, it
|
|
28
39
|
|
29
40
|
RubyLLM fixes all that. One beautiful API for everything. One consistent format. Minimal dependencies — just Faraday and Zeitwerk. Because working with AI should be a joy, not a chore.
|
30
41
|
|
31
|
-
## Features
|
32
|
-
|
33
|
-
- 💬 **Chat** with OpenAI, Anthropic, Gemini, AWS Bedrock Anthropic, and DeepSeek models
|
34
|
-
- 👁️ **Vision and Audio** understanding
|
35
|
-
- 📄 **PDF Analysis** for analyzing documents
|
36
|
-
- 🖼️ **Image generation** with DALL-E and other providers
|
37
|
-
- 📊 **Embeddings** for vector search and semantic analysis
|
38
|
-
- 🔧 **Tools** that let AI use your Ruby code
|
39
|
-
- 🚂 **Rails integration** to persist chats and messages with ActiveRecord
|
40
|
-
- 🌊 **Streaming** responses with proper Ruby patterns
|
41
|
-
|
42
42
|
## What makes it great
|
43
43
|
|
44
44
|
```ruby
|
@@ -85,142 +85,89 @@ end
|
|
85
85
|
chat.with_tool(Weather).ask "What's the weather in Berlin? (52.5200, 13.4050)"
|
86
86
|
```
|
87
87
|
|
88
|
+
## Core Capabilities
|
89
|
+
|
90
|
+
* 💬 **Unified Chat:** Converse with models from OpenAI, Anthropic, Gemini, Bedrock, OpenRouter, DeepSeek, Ollama, or any OpenAI-compatible API using `RubyLLM.chat`.
|
91
|
+
* 👁️ **Vision:** Analyze images within chats.
|
92
|
+
* 🔊 **Audio:** Transcribe and understand audio content.
|
93
|
+
* 📄 **PDF Analysis:** Extract information and summarize PDF documents.
|
94
|
+
* 🖼️ **Image Generation:** Create images with `RubyLLM.paint`.
|
95
|
+
* 📊 **Embeddings:** Generate text embeddings for vector search with `RubyLLM.embed`.
|
96
|
+
* 🔧 **Tools (Function Calling):** Let AI models call your Ruby code using `RubyLLM::Tool`.
|
97
|
+
* 🚂 **Rails Integration:** Easily persist chats, messages, and tool calls using `acts_as_chat` and `acts_as_message`.
|
98
|
+
* 🌊 **Streaming:** Process responses in real-time with idiomatic Ruby blocks.
|
99
|
+
|
88
100
|
## Installation
|
89
101
|
|
102
|
+
Add to your Gemfile:
|
90
103
|
```ruby
|
91
|
-
# In your Gemfile
|
92
104
|
gem 'ruby_llm'
|
93
|
-
|
94
|
-
# Then run
|
95
|
-
bundle install
|
96
|
-
|
97
|
-
# Or install it yourself
|
98
|
-
gem install ruby_llm
|
99
105
|
```
|
106
|
+
Then `bundle install`.
|
100
107
|
|
101
|
-
Configure
|
102
|
-
|
108
|
+
Configure your API keys (using environment variables is recommended):
|
103
109
|
```ruby
|
110
|
+
# config/initializers/ruby_llm.rb or similar
|
104
111
|
RubyLLM.configure do |config|
|
105
112
|
config.openai_api_key = ENV.fetch('OPENAI_API_KEY', nil)
|
106
|
-
|
107
|
-
config.
|
108
|
-
|
109
|
-
|
110
|
-
# Bedrock
|
111
|
-
config.bedrock_api_key = ENV.fetch('AWS_ACCESS_KEY_ID', nil)
|
112
|
-
config.bedrock_secret_key = ENV.fetch('AWS_SECRET_ACCESS_KEY', nil)
|
113
|
-
config.bedrock_region = ENV.fetch('AWS_REGION', nil)
|
114
|
-
config.bedrock_session_token = ENV.fetch('AWS_SESSION_TOKEN', nil)
|
113
|
+
# Add keys ONLY for providers you intend to use
|
114
|
+
# config.anthropic_api_key = ENV.fetch('ANTHROPIC_API_KEY', nil)
|
115
|
+
# ... see Configuration guide for all options ...
|
115
116
|
end
|
116
117
|
```
|
118
|
+
See the [Installation Guide](https://rubyllm.com/installation) for full details.
|
117
119
|
|
118
|
-
##
|
119
|
-
|
120
|
-
```ruby
|
121
|
-
# Start a chat with the default model (gpt-4.1-nano)
|
122
|
-
chat = RubyLLM.chat
|
123
|
-
|
124
|
-
# Or specify what you want
|
125
|
-
chat = RubyLLM.chat(model: 'claude-3-7-sonnet-20250219')
|
126
|
-
|
127
|
-
# Simple questions just work
|
128
|
-
chat.ask "What's the difference between attr_reader and attr_accessor?"
|
129
|
-
|
130
|
-
# Multi-turn conversations are seamless
|
131
|
-
chat.ask "Could you give me an example?"
|
132
|
-
|
133
|
-
# Stream responses in real-time
|
134
|
-
chat.ask "Tell me a story about a Ruby programmer" do |chunk|
|
135
|
-
print chunk.content
|
136
|
-
end
|
137
|
-
|
138
|
-
# Set personality or behavior with instructions (aka system prompts)
|
139
|
-
chat.with_instructions "You are a friendly Ruby expert who loves to help beginners"
|
140
|
-
|
141
|
-
# Understand content in multiple forms
|
142
|
-
chat.ask "Compare these diagrams", with: { image: ["diagram1.png", "diagram2.png"] }
|
143
|
-
chat.ask "Summarize this document", with: { pdf: "contract.pdf" }
|
144
|
-
chat.ask "What's being said?", with: { audio: "meeting.wav" }
|
145
|
-
|
146
|
-
# Need a different model mid-conversation? No problem
|
147
|
-
chat.with_model('gemini-2.0-flash').ask "What's your favorite algorithm?"
|
148
|
-
```
|
120
|
+
## Rails Integration
|
149
121
|
|
150
|
-
|
122
|
+
Add persistence to your chat models effortlessly:
|
151
123
|
|
152
124
|
```ruby
|
153
125
|
# app/models/chat.rb
|
154
126
|
class Chat < ApplicationRecord
|
155
|
-
acts_as_chat
|
156
|
-
|
157
|
-
# Works great with Turbo
|
158
|
-
broadcasts_to ->(chat) { "chat_#{chat.id}" }
|
127
|
+
acts_as_chat # Automatically saves messages & tool calls
|
128
|
+
# ... your other model logic ...
|
159
129
|
end
|
160
130
|
|
161
131
|
# app/models/message.rb
|
162
132
|
class Message < ApplicationRecord
|
163
133
|
acts_as_message
|
134
|
+
# ...
|
164
135
|
end
|
165
136
|
|
166
|
-
# app/models/tool_call.rb
|
137
|
+
# app/models/tool_call.rb (if using tools)
|
167
138
|
class ToolCall < ApplicationRecord
|
168
139
|
acts_as_tool_call
|
140
|
+
# ...
|
169
141
|
end
|
170
142
|
|
171
|
-
#
|
172
|
-
|
173
|
-
|
174
|
-
# Set personality or behavior with instructions (aka system prompts) - they're persisted too!
|
175
|
-
chat.with_instructions "You are a friendly Ruby expert who loves to help beginners"
|
176
|
-
|
177
|
-
chat.ask("What's your favorite Ruby gem?") do |chunk|
|
178
|
-
Turbo::StreamsChannel.broadcast_append_to(
|
179
|
-
chat,
|
180
|
-
target: "response",
|
181
|
-
partial: "messages/chunk",
|
182
|
-
locals: { chunk: chunk }
|
183
|
-
)
|
184
|
-
end
|
185
|
-
|
186
|
-
# That's it - chat history is automatically saved
|
143
|
+
# Now interacting with a Chat record persists the conversation:
|
144
|
+
chat_record = Chat.create!(model_id: "gpt-4.1-nano")
|
145
|
+
chat_record.ask("Explain Active Record callbacks.") # User & Assistant messages saved
|
187
146
|
```
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
## Learn more
|
209
|
-
|
210
|
-
Check out the guides at https://rubyllm.com for deeper dives into conversations with tools, streaming responses, embedding generations, and more.
|
147
|
+
Check the [Rails Integration Guide](https://rubyllm.com/guides/rails) for more.
|
148
|
+
|
149
|
+
## Learn More
|
150
|
+
|
151
|
+
Dive deeper with the official documentation:
|
152
|
+
|
153
|
+
- [Installation](https://rubyllm.com/installation)
|
154
|
+
- [Configuration](https://rubyllm.com/configuration)
|
155
|
+
- **Guides:**
|
156
|
+
- [Getting Started](https://rubyllm.com/guides/getting-started)
|
157
|
+
- [Chatting with AI Models](https://rubyllm.com/guides/chat)
|
158
|
+
- [Using Tools](https://rubyllm.com/guides/tools)
|
159
|
+
- [Streaming Responses](https://rubyllm.com/guides/streaming)
|
160
|
+
- [Rails Integration](https://rubyllm.com/guides/rails)
|
161
|
+
- [Image Generation](https://rubyllm.com/guides/image-generation)
|
162
|
+
- [Embeddings](https://rubyllm.com/guides/embeddings)
|
163
|
+
- [Working with Models](https://rubyllm.com/guides/models)
|
164
|
+
- [Error Handling](https://rubyllm.com/guides/error-handling)
|
165
|
+
- [Available Models](https://rubyllm.com/guides/available-models)
|
211
166
|
|
212
167
|
## Contributing
|
213
168
|
|
214
|
-
We welcome contributions
|
215
|
-
|
216
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed instructions on how to:
|
217
|
-
- Run the test suite
|
218
|
-
- Add new features
|
219
|
-
- Update documentation
|
220
|
-
- Re-record VCR cassettes when needed
|
221
|
-
|
222
|
-
We appreciate your help making RubyLLM better!
|
169
|
+
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on setup, testing, and contribution guidelines.
|
223
170
|
|
224
171
|
## License
|
225
172
|
|
226
|
-
Released under the MIT License.
|
173
|
+
Released under the MIT License.
|
@@ -3,8 +3,8 @@
|
|
3
3
|
module RubyLLM
|
4
4
|
module ActiveRecord
|
5
5
|
# Adds chat and message persistence capabilities to ActiveRecord models.
|
6
|
-
# Provides a clean interface for storing chat history
|
7
|
-
# in your database.
|
6
|
+
# Provides a clean interface for storing chat history, message metadata,
|
7
|
+
# and attachments in your database.
|
8
8
|
module ActsAs
|
9
9
|
extend ActiveSupport::Concern
|
10
10
|
|
@@ -20,37 +20,54 @@ module RubyLLM
|
|
20
20
|
class_name: @message_class,
|
21
21
|
dependent: :destroy
|
22
22
|
|
23
|
-
delegate :
|
24
|
-
:add_message,
|
25
|
-
to: :to_llm
|
23
|
+
delegate :add_message, to: :to_llm
|
26
24
|
end
|
27
25
|
|
28
|
-
def acts_as_message(chat_class: 'Chat',
|
26
|
+
def acts_as_message(chat_class: 'Chat',
|
27
|
+
chat_foreign_key: nil,
|
28
|
+
tool_call_class: 'ToolCall',
|
29
|
+
tool_call_foreign_key: nil,
|
30
|
+
touch_chat: false)
|
29
31
|
include MessageMethods
|
30
32
|
|
31
33
|
@chat_class = chat_class.to_s
|
34
|
+
@chat_foreign_key = chat_foreign_key || ActiveSupport::Inflector.foreign_key(@chat_class)
|
35
|
+
|
32
36
|
@tool_call_class = tool_call_class.to_s
|
37
|
+
@tool_call_foreign_key = tool_call_foreign_key || ActiveSupport::Inflector.foreign_key(@tool_call_class)
|
38
|
+
|
39
|
+
belongs_to :chat,
|
40
|
+
class_name: @chat_class,
|
41
|
+
foreign_key: @chat_foreign_key,
|
42
|
+
inverse_of: :messages,
|
43
|
+
touch: touch_chat
|
33
44
|
|
34
|
-
|
35
|
-
|
45
|
+
has_many :tool_calls,
|
46
|
+
class_name: @tool_call_class,
|
47
|
+
dependent: :destroy
|
36
48
|
|
37
49
|
belongs_to :parent_tool_call,
|
38
50
|
class_name: @tool_call_class,
|
39
|
-
foreign_key:
|
51
|
+
foreign_key: @tool_call_foreign_key,
|
40
52
|
optional: true,
|
41
53
|
inverse_of: :result
|
42
54
|
|
43
55
|
delegate :tool_call?, :tool_result?, :tool_results, to: :to_llm
|
44
56
|
end
|
45
57
|
|
46
|
-
def acts_as_tool_call(message_class: 'Message')
|
58
|
+
def acts_as_tool_call(message_class: 'Message', message_foreign_key: nil, result_foreign_key: nil)
|
47
59
|
@message_class = message_class.to_s
|
60
|
+
@message_foreign_key = message_foreign_key || ActiveSupport::Inflector.foreign_key(@message_class)
|
61
|
+
@result_foreign_key = result_foreign_key || ActiveSupport::Inflector.foreign_key(name)
|
48
62
|
|
49
|
-
belongs_to :message,
|
63
|
+
belongs_to :message,
|
64
|
+
class_name: @message_class,
|
65
|
+
foreign_key: @message_foreign_key,
|
66
|
+
inverse_of: :tool_calls
|
50
67
|
|
51
68
|
has_one :result,
|
52
69
|
class_name: @message_class,
|
53
|
-
foreign_key:
|
70
|
+
foreign_key: @result_foreign_key,
|
54
71
|
inverse_of: :parent_tool_call,
|
55
72
|
dependent: :nullify
|
56
73
|
end
|
@@ -68,95 +85,104 @@ module RubyLLM
|
|
68
85
|
|
69
86
|
def to_llm
|
70
87
|
@chat ||= RubyLLM.chat(model: model_id)
|
88
|
+
@chat.reset_messages!
|
71
89
|
|
72
|
-
# Load existing messages into chat
|
73
90
|
messages.each do |msg|
|
74
91
|
@chat.add_message(msg.to_llm)
|
75
92
|
end
|
76
93
|
|
77
|
-
# Set up message persistence
|
78
94
|
@chat.on_new_message { persist_new_message }
|
79
95
|
.on_end_message { |msg| persist_message_completion(msg) }
|
80
96
|
end
|
81
97
|
|
82
98
|
def with_instructions(instructions, replace: false)
|
83
99
|
transaction do
|
84
|
-
# If replace is true, remove existing system messages
|
85
100
|
messages.where(role: :system).destroy_all if replace
|
86
|
-
|
87
|
-
# Create the new system message
|
88
|
-
messages.create!(
|
89
|
-
role: :system,
|
90
|
-
content: instructions
|
91
|
-
)
|
101
|
+
messages.create!(role: :system, content: instructions)
|
92
102
|
end
|
93
103
|
to_llm.with_instructions(instructions)
|
94
104
|
self
|
95
105
|
end
|
96
106
|
|
97
|
-
def with_tool(
|
98
|
-
to_llm.with_tool(
|
107
|
+
def with_tool(...)
|
108
|
+
to_llm.with_tool(...)
|
99
109
|
self
|
100
110
|
end
|
101
111
|
|
102
|
-
def with_tools(
|
103
|
-
to_llm.with_tools(
|
112
|
+
def with_tools(...)
|
113
|
+
to_llm.with_tools(...)
|
104
114
|
self
|
105
115
|
end
|
106
116
|
|
107
|
-
def with_model(
|
108
|
-
to_llm.with_model(
|
117
|
+
def with_model(...)
|
118
|
+
update(model_id: to_llm.with_model(...).model.id)
|
109
119
|
self
|
110
120
|
end
|
111
121
|
|
112
|
-
def with_temperature(
|
113
|
-
to_llm.with_temperature(
|
122
|
+
def with_temperature(...)
|
123
|
+
to_llm.with_temperature(...)
|
114
124
|
self
|
115
125
|
end
|
116
126
|
|
117
|
-
def
|
118
|
-
to_llm.
|
127
|
+
def with_context(...)
|
128
|
+
to_llm.with_context(...)
|
119
129
|
self
|
120
130
|
end
|
121
131
|
|
122
|
-
def
|
123
|
-
to_llm.
|
132
|
+
def on_new_message(...)
|
133
|
+
to_llm.on_new_message(...)
|
124
134
|
self
|
125
135
|
end
|
126
136
|
|
127
|
-
def
|
128
|
-
|
129
|
-
|
130
|
-
|
137
|
+
def on_end_message(...)
|
138
|
+
to_llm.on_end_message(...)
|
139
|
+
self
|
140
|
+
end
|
141
|
+
|
142
|
+
def create_user_message(content, with: nil)
|
143
|
+
message_record = messages.create!(role: :user, content: content)
|
144
|
+
persist_content(message_record, with) if with.present?
|
145
|
+
message_record
|
146
|
+
end
|
147
|
+
|
148
|
+
def ask(message, with: nil, &)
|
149
|
+
create_user_message(message, with:)
|
150
|
+
complete(&)
|
131
151
|
end
|
132
152
|
|
133
153
|
alias say ask
|
134
154
|
|
155
|
+
def complete(...)
|
156
|
+
to_llm.complete(...)
|
157
|
+
rescue RubyLLM::Error => e
|
158
|
+
if @message&.persisted? && @message.content.blank?
|
159
|
+
RubyLLM.logger.debug "RubyLLM: API call failed, destroying message: #{@message.id}"
|
160
|
+
@message.destroy
|
161
|
+
end
|
162
|
+
raise e
|
163
|
+
end
|
164
|
+
|
135
165
|
private
|
136
166
|
|
137
167
|
def persist_new_message
|
138
|
-
@message = messages.create!(
|
139
|
-
role: :assistant,
|
140
|
-
content: String.new
|
141
|
-
)
|
168
|
+
@message = messages.create!(role: :assistant, content: String.new)
|
142
169
|
end
|
143
170
|
|
144
|
-
def persist_message_completion(message)
|
171
|
+
def persist_message_completion(message)
|
145
172
|
return unless message
|
146
173
|
|
147
|
-
if message.tool_call_id
|
148
|
-
tool_call_id = self.class.tool_call_class.constantize.find_by(tool_call_id: message.tool_call_id).id
|
149
|
-
end
|
174
|
+
tool_call_id = find_tool_call_id(message.tool_call_id) if message.tool_call_id
|
150
175
|
|
151
176
|
transaction do
|
152
177
|
@message.update!(
|
153
178
|
role: message.role,
|
154
179
|
content: message.content,
|
155
180
|
model_id: message.model_id,
|
156
|
-
tool_call_id: tool_call_id,
|
157
181
|
input_tokens: message.input_tokens,
|
158
182
|
output_tokens: message.output_tokens
|
159
183
|
)
|
184
|
+
@message.write_attribute(@message.class.tool_call_foreign_key, tool_call_id) if tool_call_id
|
185
|
+
@message.save!
|
160
186
|
persist_tool_calls(message.tool_calls) if message.tool_calls.present?
|
161
187
|
end
|
162
188
|
end
|
@@ -168,6 +194,48 @@ module RubyLLM
|
|
168
194
|
@message.tool_calls.create!(**attributes)
|
169
195
|
end
|
170
196
|
end
|
197
|
+
|
198
|
+
def find_tool_call_id(tool_call_id)
|
199
|
+
self.class.tool_call_class.constantize.find_by(tool_call_id: tool_call_id)&.id
|
200
|
+
end
|
201
|
+
|
202
|
+
def persist_content(message_record, attachments)
|
203
|
+
return unless message_record.respond_to?(:attachments)
|
204
|
+
|
205
|
+
attachables = prepare_for_active_storage(attachments)
|
206
|
+
message_record.attachments.attach(attachables) if attachables.any?
|
207
|
+
end
|
208
|
+
|
209
|
+
def prepare_for_active_storage(attachments)
|
210
|
+
Utils.to_safe_array(attachments).filter_map do |attachment|
|
211
|
+
case attachment
|
212
|
+
when ActionDispatch::Http::UploadedFile, ActiveStorage::Blob
|
213
|
+
attachment
|
214
|
+
when ActiveStorage::Attached::One, ActiveStorage::Attached::Many
|
215
|
+
attachment.blobs
|
216
|
+
when Hash
|
217
|
+
attachment.values.map { |v| prepare_for_active_storage(v) }
|
218
|
+
else
|
219
|
+
convert_to_active_storage_format(attachment)
|
220
|
+
end
|
221
|
+
end.flatten.compact
|
222
|
+
end
|
223
|
+
|
224
|
+
def convert_to_active_storage_format(source)
|
225
|
+
return if source.blank?
|
226
|
+
|
227
|
+
# Let RubyLLM::Attachment handle the heavy lifting
|
228
|
+
attachment = RubyLLM::Attachment.new(source)
|
229
|
+
|
230
|
+
{
|
231
|
+
io: StringIO.new(attachment.content),
|
232
|
+
filename: attachment.filename,
|
233
|
+
content_type: attachment.mime_type
|
234
|
+
}
|
235
|
+
rescue StandardError => e
|
236
|
+
RubyLLM.logger.warn "Failed to process attachment #{source}: #{e.message}"
|
237
|
+
nil
|
238
|
+
end
|
171
239
|
end
|
172
240
|
|
173
241
|
# Methods mixed into message models to handle serialization and
|
@@ -175,6 +243,10 @@ module RubyLLM
|
|
175
243
|
module MessageMethods
|
176
244
|
extend ActiveSupport::Concern
|
177
245
|
|
246
|
+
class_methods do
|
247
|
+
attr_reader :chat_class, :tool_call_class, :chat_foreign_key, :tool_call_foreign_key
|
248
|
+
end
|
249
|
+
|
178
250
|
def to_llm
|
179
251
|
RubyLLM::Message.new(
|
180
252
|
role: role.to_sym,
|
@@ -187,6 +259,8 @@ module RubyLLM
|
|
187
259
|
)
|
188
260
|
end
|
189
261
|
|
262
|
+
private
|
263
|
+
|
190
264
|
def extract_tool_calls
|
191
265
|
tool_calls.to_h do |tool_call|
|
192
266
|
[
|
@@ -205,7 +279,30 @@ module RubyLLM
|
|
205
279
|
end
|
206
280
|
|
207
281
|
def extract_content
|
208
|
-
content
|
282
|
+
return content unless respond_to?(:attachments) && attachments.attached?
|
283
|
+
|
284
|
+
RubyLLM::Content.new(content).tap do |content_obj|
|
285
|
+
@_tempfiles = []
|
286
|
+
|
287
|
+
attachments.each do |attachment|
|
288
|
+
tempfile = download_attachment(attachment)
|
289
|
+
content_obj.add_attachment(tempfile, filename: attachment.filename.to_s)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
def download_attachment(attachment)
|
295
|
+
ext = File.extname(attachment.filename.to_s)
|
296
|
+
basename = File.basename(attachment.filename.to_s, ext)
|
297
|
+
tempfile = Tempfile.new([basename, ext])
|
298
|
+
tempfile.binmode
|
299
|
+
|
300
|
+
attachment.download { |chunk| tempfile.write(chunk) }
|
301
|
+
|
302
|
+
tempfile.flush
|
303
|
+
tempfile.rewind
|
304
|
+
@_tempfiles << tempfile
|
305
|
+
tempfile
|
209
306
|
end
|
210
307
|
end
|
211
308
|
end
|