dify_llm 1.6.4
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +157 -0
- data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install/templates/create_chats_legacy_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_legacy_migration.rb.tt +16 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +16 -0
- data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +43 -0
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +15 -0
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +9 -0
- data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +4 -0
- data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install_generator.rb +184 -0
- data/lib/generators/ruby_llm/migrate_model_fields/templates/migration.rb.tt +142 -0
- data/lib/generators/ruby_llm/migrate_model_fields_generator.rb +84 -0
- data/lib/ruby_llm/active_record/acts_as.rb +137 -0
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +398 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +315 -0
- data/lib/ruby_llm/active_record/message_methods.rb +72 -0
- data/lib/ruby_llm/active_record/model_methods.rb +84 -0
- data/lib/ruby_llm/aliases.json +274 -0
- data/lib/ruby_llm/aliases.rb +38 -0
- data/lib/ruby_llm/attachment.rb +191 -0
- data/lib/ruby_llm/chat.rb +212 -0
- data/lib/ruby_llm/chunk.rb +6 -0
- data/lib/ruby_llm/configuration.rb +69 -0
- data/lib/ruby_llm/connection.rb +137 -0
- data/lib/ruby_llm/content.rb +50 -0
- data/lib/ruby_llm/context.rb +29 -0
- data/lib/ruby_llm/embedding.rb +29 -0
- data/lib/ruby_llm/error.rb +76 -0
- data/lib/ruby_llm/image.rb +49 -0
- data/lib/ruby_llm/message.rb +76 -0
- data/lib/ruby_llm/mime_type.rb +67 -0
- data/lib/ruby_llm/model/info.rb +103 -0
- data/lib/ruby_llm/model/modalities.rb +22 -0
- data/lib/ruby_llm/model/pricing.rb +48 -0
- data/lib/ruby_llm/model/pricing_category.rb +46 -0
- data/lib/ruby_llm/model/pricing_tier.rb +33 -0
- data/lib/ruby_llm/model.rb +7 -0
- data/lib/ruby_llm/models.json +31418 -0
- data/lib/ruby_llm/models.rb +235 -0
- data/lib/ruby_llm/models_schema.json +168 -0
- data/lib/ruby_llm/provider.rb +215 -0
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +134 -0
- data/lib/ruby_llm/providers/anthropic/chat.rb +106 -0
- data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
- data/lib/ruby_llm/providers/anthropic/media.rb +91 -0
- data/lib/ruby_llm/providers/anthropic/models.rb +48 -0
- data/lib/ruby_llm/providers/anthropic/streaming.rb +43 -0
- data/lib/ruby_llm/providers/anthropic/tools.rb +107 -0
- data/lib/ruby_llm/providers/anthropic.rb +36 -0
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +63 -0
- data/lib/ruby_llm/providers/bedrock/media.rb +60 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +98 -0
- data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +51 -0
- data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +56 -0
- data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +67 -0
- data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +78 -0
- data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +78 -0
- data/lib/ruby_llm/providers/bedrock/streaming.rb +18 -0
- data/lib/ruby_llm/providers/bedrock.rb +82 -0
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +130 -0
- data/lib/ruby_llm/providers/deepseek/chat.rb +16 -0
- data/lib/ruby_llm/providers/deepseek.rb +30 -0
- data/lib/ruby_llm/providers/dify/capabilities.rb +16 -0
- data/lib/ruby_llm/providers/dify/chat.rb +59 -0
- data/lib/ruby_llm/providers/dify/media.rb +37 -0
- data/lib/ruby_llm/providers/dify/streaming.rb +28 -0
- data/lib/ruby_llm/providers/dify.rb +48 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +276 -0
- data/lib/ruby_llm/providers/gemini/chat.rb +171 -0
- data/lib/ruby_llm/providers/gemini/embeddings.rb +37 -0
- data/lib/ruby_llm/providers/gemini/images.rb +47 -0
- data/lib/ruby_llm/providers/gemini/media.rb +54 -0
- data/lib/ruby_llm/providers/gemini/models.rb +40 -0
- data/lib/ruby_llm/providers/gemini/streaming.rb +61 -0
- data/lib/ruby_llm/providers/gemini/tools.rb +77 -0
- data/lib/ruby_llm/providers/gemini.rb +36 -0
- data/lib/ruby_llm/providers/gpustack/chat.rb +27 -0
- data/lib/ruby_llm/providers/gpustack/media.rb +45 -0
- data/lib/ruby_llm/providers/gpustack/models.rb +90 -0
- data/lib/ruby_llm/providers/gpustack.rb +34 -0
- data/lib/ruby_llm/providers/mistral/capabilities.rb +155 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +24 -0
- data/lib/ruby_llm/providers/mistral/embeddings.rb +33 -0
- data/lib/ruby_llm/providers/mistral/models.rb +48 -0
- data/lib/ruby_llm/providers/mistral.rb +32 -0
- data/lib/ruby_llm/providers/ollama/chat.rb +27 -0
- data/lib/ruby_llm/providers/ollama/media.rb +45 -0
- data/lib/ruby_llm/providers/ollama/models.rb +36 -0
- data/lib/ruby_llm/providers/ollama.rb +30 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +291 -0
- data/lib/ruby_llm/providers/openai/chat.rb +83 -0
- data/lib/ruby_llm/providers/openai/embeddings.rb +33 -0
- data/lib/ruby_llm/providers/openai/images.rb +38 -0
- data/lib/ruby_llm/providers/openai/media.rb +80 -0
- data/lib/ruby_llm/providers/openai/models.rb +39 -0
- data/lib/ruby_llm/providers/openai/streaming.rb +41 -0
- data/lib/ruby_llm/providers/openai/tools.rb +78 -0
- data/lib/ruby_llm/providers/openai.rb +42 -0
- data/lib/ruby_llm/providers/openrouter/models.rb +73 -0
- data/lib/ruby_llm/providers/openrouter.rb +26 -0
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +137 -0
- data/lib/ruby_llm/providers/perplexity/chat.rb +16 -0
- data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
- data/lib/ruby_llm/providers/perplexity.rb +48 -0
- data/lib/ruby_llm/providers/vertexai/chat.rb +14 -0
- data/lib/ruby_llm/providers/vertexai/embeddings.rb +32 -0
- data/lib/ruby_llm/providers/vertexai/models.rb +130 -0
- data/lib/ruby_llm/providers/vertexai/streaming.rb +14 -0
- data/lib/ruby_llm/providers/vertexai.rb +55 -0
- data/lib/ruby_llm/railtie.rb +41 -0
- data/lib/ruby_llm/stream_accumulator.rb +97 -0
- data/lib/ruby_llm/streaming.rb +153 -0
- data/lib/ruby_llm/tool.rb +83 -0
- data/lib/ruby_llm/tool_call.rb +22 -0
- data/lib/ruby_llm/utils.rb +45 -0
- data/lib/ruby_llm/version.rb +5 -0
- data/lib/ruby_llm.rb +97 -0
- data/lib/tasks/models.rake +525 -0
- data/lib/tasks/release.rake +67 -0
- data/lib/tasks/ruby_llm.rake +15 -0
- data/lib/tasks/vcr.rake +92 -0
- metadata +291 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: cf640d22ec6e4390de10874e92f3d5386df4a7fb16249fdc3b9e3c685aef24d8
|
4
|
+
data.tar.gz: d3a832819f2300fdbe92fc8123344c862244af1e066a0d3ed4ab99c8e37146be
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 051e1512fb39e9c775fda04aef83e4749cf1eb00140e96d5c7d8e40944300532162971bc75da9c1b93084682fe77fbb81bc631c8a5a8369cee8392cf488fc06f
|
7
|
+
data.tar.gz: 0e63f109548b3a41ec18a64b00c243a27eb793a85f303a9d35fa9516c98d8c029654e7c7b4d9f34bf6e248424435d39f20954c51b26faee48caf033a1e627a96
|
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
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
<div align="center">
|
2
|
+
|
3
|
+
<picture>
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="/docs/assets/images/logotype_dark.svg">
|
5
|
+
<img src="/docs/assets/images/logotype.svg" alt="RubyLLM" height="120" width="250">
|
6
|
+
</picture>
|
7
|
+
|
8
|
+
<strong>One *beautiful* Ruby API for GPT, Claude, Gemini, and more.</strong>
|
9
|
+
|
10
|
+
Battle tested at [<picture><source media="(prefers-color-scheme: dark)" srcset="https://chatwithwork.com/logotype-dark.svg"><img src="https://chatwithwork.com/logotype.svg" alt="Chat with Work" height="30" align="absmiddle"></picture>](https://chatwithwork.com) — *Claude Code for your documents*
|
11
|
+
|
12
|
+
[](https://badge.fury.io/rb/ruby_llm)
|
13
|
+
[](https://github.com/testdouble/standard)
|
14
|
+
[](https://rubygems.org/gems/ruby_llm)
|
15
|
+
[](https://codecov.io/gh/crmne/ruby_llm)
|
16
|
+
|
17
|
+
<a href="https://trendshift.io/repositories/13640" target="_blank"><img src="https://trendshift.io/api/badge/repositories/13640" alt="crmne%2Fruby_llm | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
18
|
+
</div>
|
19
|
+
|
20
|
+
> [!NOTE]
|
21
|
+
> Using RubyLLM in production? [Share your story](https://tally.so/r/3Na02p)! Takes 5 minutes.
|
22
|
+
|
23
|
+
---
|
24
|
+
|
25
|
+
Build chatbots, AI agents, RAG applications. Works with OpenAI, Anthropic, Google, AWS, local models, and any OpenAI-compatible API.
|
26
|
+
|
27
|
+
## Why RubyLLM?
|
28
|
+
|
29
|
+
Every AI provider ships their own bloated client. Different APIs. Different response formats. Different conventions. It's exhausting.
|
30
|
+
|
31
|
+
RubyLLM gives you one beautiful API for all of them. Same interface whether you're using GPT, Claude, or your local Ollama. Just three dependencies: Faraday, Zeitwerk, and Marcel. That's it.
|
32
|
+
|
33
|
+
## Show me the code
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
# Just ask questions
|
37
|
+
chat = RubyLLM.chat
|
38
|
+
chat.ask "What's the best way to learn Ruby?"
|
39
|
+
```
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
# Analyze any file type
|
43
|
+
chat.ask "What's in this image?", with: "ruby_conf.jpg"
|
44
|
+
chat.ask "Describe this meeting", with: "meeting.wav"
|
45
|
+
chat.ask "Summarize this document", with: "contract.pdf"
|
46
|
+
chat.ask "Explain this code", with: "app.rb"
|
47
|
+
```
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
# Multiple files at once
|
51
|
+
chat.ask "Analyze these files", with: ["diagram.png", "report.pdf", "notes.txt"]
|
52
|
+
```
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
# Stream responses
|
56
|
+
chat.ask "Tell me a story about Ruby" do |chunk|
|
57
|
+
print chunk.content
|
58
|
+
end
|
59
|
+
```
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
# Generate images
|
63
|
+
RubyLLM.paint "a sunset over mountains in watercolor style"
|
64
|
+
```
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
# Create embeddings
|
68
|
+
RubyLLM.embed "Ruby is elegant and expressive"
|
69
|
+
```
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
# Let AI use your code
|
73
|
+
class Weather < RubyLLM::Tool
|
74
|
+
description "Get current weather"
|
75
|
+
param :latitude
|
76
|
+
param :longitude
|
77
|
+
|
78
|
+
def execute(latitude:, longitude:)
|
79
|
+
url = "https://api.open-meteo.com/v1/forecast?latitude=#{latitude}&longitude=#{longitude}¤t=temperature_2m,wind_speed_10m"
|
80
|
+
JSON.parse(Faraday.get(url).body)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
chat.with_tool(Weather).ask "What's the weather in Berlin?"
|
85
|
+
```
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
# Get structured output
|
89
|
+
class ProductSchema < RubyLLM::Schema
|
90
|
+
string :name
|
91
|
+
number :price
|
92
|
+
array :features do
|
93
|
+
string
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
response = chat.with_schema(ProductSchema).ask "Analyze this product", with: "product.txt"
|
98
|
+
```
|
99
|
+
|
100
|
+
## Features
|
101
|
+
|
102
|
+
* **Chat:** Conversational AI with `RubyLLM.chat`
|
103
|
+
* **Vision:** Analyze images and screenshots
|
104
|
+
* **Audio:** Transcribe and understand speech
|
105
|
+
* **Documents:** Extract from PDFs, CSVs, JSON, any file type
|
106
|
+
* **Image generation:** Create images with `RubyLLM.paint`
|
107
|
+
* **Embeddings:** Vector search with `RubyLLM.embed`
|
108
|
+
* **Tools:** Let AI call your Ruby methods
|
109
|
+
* **Structured output:** JSON schemas that just work
|
110
|
+
* **Streaming:** Real-time responses with blocks
|
111
|
+
* **Rails:** ActiveRecord integration with `acts_as_chat`
|
112
|
+
* **Async:** Fiber-based concurrency
|
113
|
+
* **Model registry:** 500+ models with capability detection and pricing
|
114
|
+
* **Providers:** OpenAI, Anthropic, Gemini, VertexAI, Bedrock, DeepSeek, Mistral, Ollama, OpenRouter, Perplexity, GPUStack, and any OpenAI-compatible API
|
115
|
+
|
116
|
+
## Installation
|
117
|
+
|
118
|
+
Add to your Gemfile:
|
119
|
+
```ruby
|
120
|
+
gem 'ruby_llm'
|
121
|
+
```
|
122
|
+
Then `bundle install`.
|
123
|
+
|
124
|
+
Configure your API keys:
|
125
|
+
```ruby
|
126
|
+
# config/initializers/ruby_llm.rb
|
127
|
+
RubyLLM.configure do |config|
|
128
|
+
config.openai_api_key = ENV['OPENAI_API_KEY']
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
## Rails
|
133
|
+
|
134
|
+
```bash
|
135
|
+
rails generate ruby_llm:install
|
136
|
+
```
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
class Chat < ApplicationRecord
|
140
|
+
acts_as_chat
|
141
|
+
end
|
142
|
+
|
143
|
+
chat = Chat.create! model_id: "claude-sonnet-4"
|
144
|
+
chat.ask "What's in this file?", with: "report.pdf"
|
145
|
+
```
|
146
|
+
|
147
|
+
## Documentation
|
148
|
+
|
149
|
+
[rubyllm.com](https://rubyllm.com)
|
150
|
+
|
151
|
+
## Contributing
|
152
|
+
|
153
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
154
|
+
|
155
|
+
## License
|
156
|
+
|
157
|
+
Released under the MIT License.
|
@@ -0,0 +1,8 @@
|
|
1
|
+
class Create<%= options[:chat_model_name].pluralize %> < ActiveRecord::Migration<%= migration_version %>
|
2
|
+
def change
|
3
|
+
create_table :<%= options[:chat_model_name].tableize %> do |t|
|
4
|
+
t.references :<%= options[:model_model_name].tableize.singularize %>, foreign_key: true
|
5
|
+
t.timestamps
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class Create<%= options[:message_model_name].pluralize %> < ActiveRecord::Migration<%= migration_version %>
|
2
|
+
def change
|
3
|
+
create_table :<%= options[:message_model_name].tableize %> do |t|
|
4
|
+
t.references :<%= options[:chat_model_name].tableize.singularize %>, null: false, foreign_key: true
|
5
|
+
t.string :role, null: false
|
6
|
+
t.text :content
|
7
|
+
t.string :model_id
|
8
|
+
t.integer :input_tokens
|
9
|
+
t.integer :output_tokens
|
10
|
+
t.references :<%= options[:tool_call_model_name].tableize.singularize %>, foreign_key: true
|
11
|
+
t.timestamps
|
12
|
+
end
|
13
|
+
|
14
|
+
add_index :<%= options[:message_model_name].tableize %>, :role
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class Create<%= options[:message_model_name].pluralize %> < ActiveRecord::Migration<%= migration_version %>
|
2
|
+
def change
|
3
|
+
create_table :<%= options[:message_model_name].tableize %> do |t|
|
4
|
+
t.references :<%= options[:chat_model_name].tableize.singularize %>, null: false, foreign_key: true
|
5
|
+
t.string :role, null: false
|
6
|
+
t.text :content
|
7
|
+
t.references :<%= options[:model_model_name].tableize.singularize %>, foreign_key: true
|
8
|
+
t.integer :input_tokens
|
9
|
+
t.integer :output_tokens
|
10
|
+
t.references :<%= options[:tool_call_model_name].tableize.singularize %>, foreign_key: true
|
11
|
+
t.timestamps
|
12
|
+
end
|
13
|
+
|
14
|
+
add_index :<%= options[:message_model_name].tableize %>, :role
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class Create<%= options[:model_model_name].pluralize %> < ActiveRecord::Migration<%= migration_version %>
|
2
|
+
def change
|
3
|
+
create_table :<%= options[:model_model_name].tableize %> do |t|
|
4
|
+
t.string :model_id, null: false
|
5
|
+
t.string :name, null: false
|
6
|
+
t.string :provider, null: false
|
7
|
+
t.string :family
|
8
|
+
t.datetime :model_created_at
|
9
|
+
t.integer :context_window
|
10
|
+
t.integer :max_output_tokens
|
11
|
+
t.date :knowledge_cutoff
|
12
|
+
<% if postgresql? %>
|
13
|
+
t.jsonb :modalities, default: {}
|
14
|
+
t.jsonb :capabilities, default: []
|
15
|
+
t.jsonb :pricing, default: {}
|
16
|
+
t.jsonb :metadata, default: {}
|
17
|
+
<% else %>
|
18
|
+
t.json :modalities, default: {}
|
19
|
+
t.json :capabilities, default: []
|
20
|
+
t.json :pricing, default: {}
|
21
|
+
t.json :metadata, default: {}
|
22
|
+
<% end %>
|
23
|
+
t.timestamps
|
24
|
+
|
25
|
+
t.index [:provider, :model_id], unique: true
|
26
|
+
t.index :provider
|
27
|
+
t.index :family
|
28
|
+
<% if postgresql? %>
|
29
|
+
t.index :capabilities, using: :gin
|
30
|
+
t.index :modalities, using: :gin
|
31
|
+
<% end %>
|
32
|
+
end
|
33
|
+
|
34
|
+
# Load models from JSON
|
35
|
+
say_with_time "Loading models from models.json" do
|
36
|
+
RubyLLM.models.load_from_json!
|
37
|
+
model_class = '<%= options[:model_model_name] %>'.constantize
|
38
|
+
model_class.save_to_database
|
39
|
+
|
40
|
+
"Loaded #{model_class.count} models"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<%#- # Migration for creating tool_calls table with database-specific JSON handling -%>
|
2
|
+
class Create<%= options[:tool_call_model_name].pluralize %> < ActiveRecord::Migration<%= migration_version %>
|
3
|
+
def change
|
4
|
+
create_table :<%= options[:tool_call_model_name].tableize %> do |t|
|
5
|
+
t.references :<%= options[:message_model_name].tableize.singularize %>, null: false, foreign_key: true
|
6
|
+
t.string :tool_call_id, null: false
|
7
|
+
t.string :name, null: false
|
8
|
+
t.<%= postgresql? ? 'jsonb' : 'json' %> :arguments, default: {}
|
9
|
+
t.timestamps
|
10
|
+
end
|
11
|
+
|
12
|
+
add_index :<%= options[:tool_call_model_name].tableize %>, :tool_call_id, unique: true
|
13
|
+
add_index :<%= options[:tool_call_model_name].tableize %>, :name
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
RubyLLM.configure do |config|
|
2
|
+
config.openai_api_key = Rails.application.credentials.dig(:openai_api_key)
|
3
|
+
# config.default_model = "gpt-4.1-nano"
|
4
|
+
|
5
|
+
<% unless skip_model_registry? -%>
|
6
|
+
# DB-backed model registry for rich model metadata
|
7
|
+
config.model_registry_class = "<%= options[:model_model_name] %>"
|
8
|
+
<% end -%>
|
9
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
require 'rails/generators/active_record'
|
5
|
+
|
6
|
+
module RubyLLM
|
7
|
+
# Generator for RubyLLM Rails models and migrations
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
9
|
+
include Rails::Generators::Migration
|
10
|
+
|
11
|
+
namespace 'ruby_llm:install'
|
12
|
+
|
13
|
+
source_root File.expand_path('install/templates', __dir__)
|
14
|
+
|
15
|
+
class_option :chat_model_name, type: :string, default: 'Chat',
|
16
|
+
desc: 'Name of the Chat model class'
|
17
|
+
class_option :message_model_name, type: :string, default: 'Message',
|
18
|
+
desc: 'Name of the Message model class'
|
19
|
+
class_option :tool_call_model_name, type: :string, default: 'ToolCall',
|
20
|
+
desc: 'Name of the ToolCall model class'
|
21
|
+
class_option :model_model_name, type: :string, default: 'Model',
|
22
|
+
desc: 'Name of the Model model class (for model registry)'
|
23
|
+
class_option :skip_model_registry, type: :boolean, default: false,
|
24
|
+
desc: 'Skip creating Model registry (uses string fields instead)'
|
25
|
+
class_option :skip_active_storage, type: :boolean, default: false,
|
26
|
+
desc: 'Skip ActiveStorage installation and attachment setup'
|
27
|
+
|
28
|
+
desc 'Creates models and migrations for RubyLLM Rails integration with ActiveStorage for file attachments'
|
29
|
+
|
30
|
+
def self.next_migration_number(dirname)
|
31
|
+
::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
32
|
+
end
|
33
|
+
|
34
|
+
def migration_version
|
35
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
36
|
+
end
|
37
|
+
|
38
|
+
def postgresql?
|
39
|
+
::ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
|
40
|
+
rescue StandardError
|
41
|
+
false
|
42
|
+
end
|
43
|
+
|
44
|
+
def acts_as_chat_declaration
|
45
|
+
acts_as_chat_params = []
|
46
|
+
if options[:message_model_name] != 'Message'
|
47
|
+
acts_as_chat_params << "message_class: \"#{options[:message_model_name]}\""
|
48
|
+
end
|
49
|
+
if options[:tool_call_model_name] != 'ToolCall'
|
50
|
+
acts_as_chat_params << "tool_call_class: \"#{options[:tool_call_model_name]}\""
|
51
|
+
end
|
52
|
+
if acts_as_chat_params.any?
|
53
|
+
"acts_as_chat #{acts_as_chat_params.join(', ')}"
|
54
|
+
else
|
55
|
+
'acts_as_chat'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def acts_as_message_declaration
|
60
|
+
acts_as_message_params = []
|
61
|
+
acts_as_message_params << "chat_class: \"#{options[:chat_model_name]}\"" if options[:chat_model_name] != 'Chat'
|
62
|
+
if options[:tool_call_model_name] != 'ToolCall'
|
63
|
+
acts_as_message_params << "tool_call_class: \"#{options[:tool_call_model_name]}\""
|
64
|
+
end
|
65
|
+
if acts_as_message_params.any?
|
66
|
+
"acts_as_message #{acts_as_message_params.join(', ')}"
|
67
|
+
else
|
68
|
+
'acts_as_message'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def acts_as_tool_call_declaration
|
73
|
+
acts_as_tool_call_params = []
|
74
|
+
if options[:message_model_name] != 'Message'
|
75
|
+
acts_as_tool_call_params << "message_class: \"#{options[:message_model_name]}\""
|
76
|
+
end
|
77
|
+
if acts_as_tool_call_params.any?
|
78
|
+
"acts_as_tool_call #{acts_as_tool_call_params.join(', ')}"
|
79
|
+
else
|
80
|
+
'acts_as_tool_call'
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def acts_as_model_declaration
|
85
|
+
'acts_as_model'
|
86
|
+
end
|
87
|
+
|
88
|
+
def skip_model_registry?
|
89
|
+
options[:skip_model_registry]
|
90
|
+
end
|
91
|
+
|
92
|
+
def create_migration_files
|
93
|
+
# Create migrations with timestamps to ensure proper order
|
94
|
+
# First create chats table
|
95
|
+
template_file = skip_model_registry? ? 'create_chats_legacy_migration.rb.tt' : 'create_chats_migration.rb.tt'
|
96
|
+
migration_template template_file,
|
97
|
+
"db/migrate/create_#{options[:chat_model_name].tableize}.rb"
|
98
|
+
|
99
|
+
# Then create messages table (must come before tool_calls due to foreign key)
|
100
|
+
sleep 1 # Ensure different timestamp
|
101
|
+
template_file = if skip_model_registry?
|
102
|
+
'create_messages_legacy_migration.rb.tt'
|
103
|
+
else
|
104
|
+
'create_messages_migration.rb.tt'
|
105
|
+
end
|
106
|
+
migration_template template_file,
|
107
|
+
"db/migrate/create_#{options[:message_model_name].tableize}.rb"
|
108
|
+
|
109
|
+
# Then create tool_calls table (references messages)
|
110
|
+
sleep 1 # Ensure different timestamp
|
111
|
+
migration_template 'create_tool_calls_migration.rb.tt',
|
112
|
+
"db/migrate/create_#{options[:tool_call_model_name].tableize}.rb"
|
113
|
+
|
114
|
+
# Create models table unless using legacy or skipping
|
115
|
+
return if skip_model_registry?
|
116
|
+
|
117
|
+
sleep 1 # Ensure different timestamp
|
118
|
+
migration_template 'create_models_migration.rb.tt',
|
119
|
+
"db/migrate/create_#{options[:model_model_name].tableize}.rb"
|
120
|
+
end
|
121
|
+
|
122
|
+
def create_model_files
|
123
|
+
template 'chat_model.rb.tt', "app/models/#{options[:chat_model_name].underscore}.rb"
|
124
|
+
template 'message_model.rb.tt', "app/models/#{options[:message_model_name].underscore}.rb"
|
125
|
+
template 'tool_call_model.rb.tt', "app/models/#{options[:tool_call_model_name].underscore}.rb"
|
126
|
+
|
127
|
+
# Only create Model class if not using legacy
|
128
|
+
return if skip_model_registry?
|
129
|
+
|
130
|
+
template 'model_model.rb.tt', "app/models/#{options[:model_model_name].underscore}.rb"
|
131
|
+
end
|
132
|
+
|
133
|
+
def create_initializer
|
134
|
+
template 'initializer.rb.tt', 'config/initializers/ruby_llm.rb'
|
135
|
+
end
|
136
|
+
|
137
|
+
def install_active_storage
|
138
|
+
return if options[:skip_active_storage]
|
139
|
+
|
140
|
+
say ' Installing ActiveStorage for file attachments...', :cyan
|
141
|
+
rails_command 'active_storage:install'
|
142
|
+
end
|
143
|
+
|
144
|
+
def show_install_info
|
145
|
+
say "\n ✅ RubyLLM installed!", :green
|
146
|
+
|
147
|
+
say ' ✅ ActiveStorage configured for file attachments support', :green unless options[:skip_active_storage]
|
148
|
+
|
149
|
+
say "\n Next steps:", :yellow
|
150
|
+
say ' 1. Run: rails db:migrate'
|
151
|
+
say ' 2. Set your API keys in config/initializers/ruby_llm.rb'
|
152
|
+
|
153
|
+
if skip_model_registry?
|
154
|
+
say " 3. Start chatting: #{options[:chat_model_name]}.create!(model_id: 'gpt-4.1-nano').ask('Hello!')"
|
155
|
+
|
156
|
+
say "\n Note: Using string-based model fields", :yellow
|
157
|
+
say ' For rich model metadata, consider adding the model registry:'
|
158
|
+
say ' rails generate ruby_llm:migrate_model_fields'
|
159
|
+
else
|
160
|
+
say " 3. Start chatting: #{options[:chat_model_name]}.create!(model: 'gpt-4.1-nano').ask('Hello!')"
|
161
|
+
|
162
|
+
say "\n 🚀 Model registry is database-backed!", :cyan
|
163
|
+
say ' Models automatically load from the database'
|
164
|
+
say ' Pass model names as strings - RubyLLM handles the rest!'
|
165
|
+
end
|
166
|
+
say " Specify provider when needed: Chat.create!(model: 'gemini-2.5-flash', provider: 'vertexai')"
|
167
|
+
|
168
|
+
if options[:skip_active_storage]
|
169
|
+
say "\n 📎 Note: ActiveStorage was skipped", :yellow
|
170
|
+
say ' File attachments won\'t work without ActiveStorage.'
|
171
|
+
say ' To enable later:'
|
172
|
+
say ' 1. Run: rails active_storage:install && rails db:migrate'
|
173
|
+
say " 2. Add to your #{options[:message_model_name]} model: has_many_attached :attachments"
|
174
|
+
end
|
175
|
+
|
176
|
+
say "\n 📚 Documentation: https://rubyllm.com", :cyan
|
177
|
+
|
178
|
+
say "\n ❤️ Love RubyLLM?", :magenta
|
179
|
+
say ' • ⭐ Star on GitHub: https://github.com/crmne/ruby_llm'
|
180
|
+
say ' • 🐦 Follow for updates: https://x.com/paolino'
|
181
|
+
say "\n"
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
class MigrateToRubyLLMModelReferences < ActiveRecord::Migration<%= migration_version %>
|
2
|
+
def up
|
3
|
+
# Ensure Models table exists
|
4
|
+
unless table_exists?(:<%= options[:model_model_name].tableize %>)
|
5
|
+
raise "Models table doesn't exist. Please run 'rails generate ruby_llm:install' with model registry first."
|
6
|
+
end
|
7
|
+
|
8
|
+
model_class = <%= options[:model_model_name] %>
|
9
|
+
chat_class = <%= options[:chat_model_name] %>
|
10
|
+
message_class = <%= options[:message_model_name] %>
|
11
|
+
|
12
|
+
# Then check for any models in existing data that aren't in models.json
|
13
|
+
say_with_time "Checking for additional models in existing data" do
|
14
|
+
collect_and_create_models(chat_class, :<%= options[:chat_model_name].tableize %>, model_class)
|
15
|
+
collect_and_create_models(message_class, :<%= options[:message_model_name].tableize %>, model_class)
|
16
|
+
model_class.count
|
17
|
+
end
|
18
|
+
|
19
|
+
# Migrate foreign keys
|
20
|
+
migrate_foreign_key(:<%= options[:chat_model_name].tableize %>, chat_class, model_class, :<%= options[:model_model_name].underscore %>)
|
21
|
+
migrate_foreign_key(:<%= options[:message_model_name].tableize %>, message_class, model_class, :<%= options[:model_model_name].underscore %>)
|
22
|
+
end
|
23
|
+
|
24
|
+
def down
|
25
|
+
# Remove foreign key references
|
26
|
+
if column_exists?(:<%= options[:message_model_name].tableize %>, :<%= options[:model_model_name].underscore %>_id)
|
27
|
+
remove_reference :<%= options[:message_model_name].tableize %>, :<%= options[:model_model_name].underscore %>, foreign_key: true
|
28
|
+
end
|
29
|
+
|
30
|
+
if column_exists?(:<%= options[:chat_model_name].tableize %>, :<%= options[:model_model_name].underscore %>_id)
|
31
|
+
remove_reference :<%= options[:chat_model_name].tableize %>, :<%= options[:model_model_name].underscore %>, foreign_key: true
|
32
|
+
end
|
33
|
+
|
34
|
+
# Restore original model_id string columns
|
35
|
+
if column_exists?(:<%= options[:message_model_name].tableize %>, :model_id_string)
|
36
|
+
rename_column :<%= options[:message_model_name].tableize %>, :model_id_string, :model_id
|
37
|
+
end
|
38
|
+
|
39
|
+
if column_exists?(:<%= options[:chat_model_name].tableize %>, :model_id_string)
|
40
|
+
rename_column :<%= options[:chat_model_name].tableize %>, :model_id_string, :model_id
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def collect_and_create_models(record_class, table_name, model_class)
|
47
|
+
return unless column_exists?(table_name, :model_id)
|
48
|
+
|
49
|
+
has_provider = column_exists?(table_name, :provider)
|
50
|
+
|
51
|
+
# Collect unique model/provider combinations using read_attribute to bypass overrides
|
52
|
+
models_set = Set.new
|
53
|
+
|
54
|
+
record_class.find_each do |record|
|
55
|
+
model_id = record.read_attribute(:model_id)
|
56
|
+
next if model_id.blank?
|
57
|
+
|
58
|
+
provider = has_provider ? record.read_attribute(:provider) : nil
|
59
|
+
models_set.add([ model_id, provider ])
|
60
|
+
end
|
61
|
+
|
62
|
+
models_set.each do |model_id, provider|
|
63
|
+
find_or_create_model(model_id, provider, model_class)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def find_or_create_model(model_id, provider, model_class)
|
68
|
+
return if model_id.blank?
|
69
|
+
|
70
|
+
begin
|
71
|
+
model_info, _provider = RubyLLM.models.resolve(model_id, provider: provider)
|
72
|
+
|
73
|
+
model_class.find_or_create_by!(
|
74
|
+
model_id: model_info.id,
|
75
|
+
provider: model_info.provider
|
76
|
+
) do |m|
|
77
|
+
m.name = model_info.name || model_info.id
|
78
|
+
m.family = model_info.family
|
79
|
+
m.model_created_at = model_info.created_at
|
80
|
+
m.context_window = model_info.context_window
|
81
|
+
m.max_output_tokens = model_info.max_output_tokens
|
82
|
+
m.knowledge_cutoff = model_info.knowledge_cutoff
|
83
|
+
m.modalities = model_info.modalities.to_h
|
84
|
+
m.capabilities = model_info.capabilities
|
85
|
+
m.pricing = model_info.pricing.to_h
|
86
|
+
m.metadata = model_info.metadata
|
87
|
+
end
|
88
|
+
rescue => e
|
89
|
+
# Skip models that can't be resolved - they'll need manual fixing
|
90
|
+
Rails.logger.warn "Skipping unresolvable model: #{model_id} - will need manual update"
|
91
|
+
nil
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
def migrate_foreign_key(table_name, record_class, model_class, foreign_key_name)
|
97
|
+
return unless column_exists?(table_name, :model_id)
|
98
|
+
|
99
|
+
# Check if we need to rename the string column to avoid collision
|
100
|
+
if column_exists?(table_name, :model_id) && !foreign_key_exists?(table_name, :models)
|
101
|
+
# Temporarily rename the string column
|
102
|
+
rename_column table_name, :model_id, :model_id_string
|
103
|
+
end
|
104
|
+
|
105
|
+
# Add the foreign key reference
|
106
|
+
unless column_exists?(table_name, "#{foreign_key_name}_id")
|
107
|
+
add_reference table_name, foreign_key_name, foreign_key: true
|
108
|
+
end
|
109
|
+
|
110
|
+
say_with_time "Migrating #{table_name} model references" do
|
111
|
+
record_class.reset_column_information
|
112
|
+
has_provider = column_exists?(table_name, :provider)
|
113
|
+
|
114
|
+
# Determine which column to read from (renamed or original)
|
115
|
+
model_id_column = column_exists?(table_name, :model_id_string) ? :model_id_string : :model_id
|
116
|
+
|
117
|
+
record_class.find_each do |record|
|
118
|
+
model_id = record.read_attribute(model_id_column)
|
119
|
+
next if model_id.blank?
|
120
|
+
|
121
|
+
provider = has_provider ? record.read_attribute(:provider) : nil
|
122
|
+
|
123
|
+
model = if has_provider && provider.present?
|
124
|
+
model_class.find_by(model_id: model_id, provider: provider)
|
125
|
+
else
|
126
|
+
find_model_for_record(model_id, model_class)
|
127
|
+
end
|
128
|
+
|
129
|
+
record.update_column("#{foreign_key_name}_id", model.id) if model
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def find_model_for_record(model_id, model_class)
|
135
|
+
begin
|
136
|
+
model_info, _provider = RubyLLM.models.resolve(model_id)
|
137
|
+
model_class.find_by(model_id: model_info.id, provider: model_info.provider)
|
138
|
+
rescue => e
|
139
|
+
model_class.find_by(model_id: model_id)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|