ruby_llm-sequel 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +266 -0
- data/lib/ruby_llm/sequel/chat_methods.rb +333 -0
- data/lib/ruby_llm/sequel/message_methods.rb +53 -0
- data/lib/ruby_llm/sequel/model_methods.rb +188 -0
- data/lib/ruby_llm/sequel/tool_call_methods.rb +31 -0
- data/lib/ruby_llm/sequel/version.rb +7 -0
- data/lib/ruby_llm/sequel.rb +19 -0
- data/lib/sequel/plugins/ruby_llm.rb +204 -0
- metadata +80 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b0cdecd16447df344e6e618c6e0883890348068cfa185085304e6bd26bffbb9a
|
|
4
|
+
data.tar.gz: 2984799b793177bf89fef310270444aa3728c7ad4f9fee0558df599065cebc4a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 91a0ad1ef357c27909b064abcbbd6b5cc83b256d1de8b280f8b0d60e6d7fa71c9b9bda191a8d80ded993eb100cb3ad9b68f2483fd9fe9dd15ace65e7d17bc834
|
|
7
|
+
data.tar.gz: 85d122fcad93f5c14af4e5897c9173a69b0c1725a231f60bd8d0b606da31f696cf8f837d9653e2973540230ae8273079fbdde15fe92f132a759aa90c509b8976
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Julian Pasquale
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# RubyLLM::Sequel
|
|
2
|
+
|
|
3
|
+
Sequel ORM integration for [RubyLLM](https://github.com/contextco/ruby_llm), providing the same ActiveRecord-style `acts_as_*` methods for Sequel models to interact with LLM providers.
|
|
4
|
+
|
|
5
|
+
This gem enables your Sequel models to serve as:
|
|
6
|
+
- **Chat interfaces** - Direct LLM conversations with persistent message history
|
|
7
|
+
- **Message storage** - User, assistant, system, and tool messages
|
|
8
|
+
- **Tool call tracking** - Function/tool invocations and results
|
|
9
|
+
- **Model registry** - Track available models, pricing, and capabilities
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
Add this line to your application's Gemfile:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
gem 'ruby_llm-sequel'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
And then execute:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bundle install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or install it yourself as:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
gem install ruby_llm-sequel
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
### Setup
|
|
34
|
+
|
|
35
|
+
First, require the gem in your application:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
require 'ruby_llm/sequel'
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Database Schema
|
|
42
|
+
|
|
43
|
+
Your database needs tables for models, chats, messages, and tool calls. Here's an example schema:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
DB.create_table :models do
|
|
47
|
+
primary_key :id
|
|
48
|
+
String :model_id, null: false
|
|
49
|
+
String :name, null: false
|
|
50
|
+
String :provider, null: false
|
|
51
|
+
String :family
|
|
52
|
+
Integer :context_window
|
|
53
|
+
Integer :max_output_tokens
|
|
54
|
+
|
|
55
|
+
String :capabilities, text: true
|
|
56
|
+
String :pricing, text: true
|
|
57
|
+
# or use jsonb for Postgres
|
|
58
|
+
jsonb :capabilities, text: true
|
|
59
|
+
jsonb :pricing, text: true
|
|
60
|
+
|
|
61
|
+
DateTime :created_at
|
|
62
|
+
DateTime :updated_at
|
|
63
|
+
|
|
64
|
+
index [:provider, :model_id], unique: true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
DB.create_table :chats do
|
|
68
|
+
primary_key :id
|
|
69
|
+
foreign_key :model_id, :models
|
|
70
|
+
TrueClass :active, default: true
|
|
71
|
+
DateTime :created_at
|
|
72
|
+
DateTime :updated_at
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
DB.create_table :messages do
|
|
76
|
+
primary_key :id
|
|
77
|
+
foreign_key :chat_id, :chats, null: false
|
|
78
|
+
String :role, null: false
|
|
79
|
+
String :content, text: true
|
|
80
|
+
Integer :input_tokens
|
|
81
|
+
Integer :output_tokens
|
|
82
|
+
|
|
83
|
+
String :content_raw, text: true
|
|
84
|
+
# or use jsonb for Postgres
|
|
85
|
+
jsonb :content_raw
|
|
86
|
+
|
|
87
|
+
foreign_key :model_id, :models
|
|
88
|
+
foreign_key :tool_call_id, :tool_calls
|
|
89
|
+
DateTime :created_at
|
|
90
|
+
DateTime :updated_at
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
DB.create_table :tool_calls do
|
|
94
|
+
primary_key :id
|
|
95
|
+
foreign_key :message_id, :messages, null: false
|
|
96
|
+
String :tool_call_id, null: false, unique: true
|
|
97
|
+
String :name, null: false
|
|
98
|
+
String :arguments, text: true
|
|
99
|
+
DateTime :created_at
|
|
100
|
+
DateTime :updated_at
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Model Setup
|
|
105
|
+
|
|
106
|
+
Define your Sequel models with the RubyLLM plugin and appropriate `acts_as_*` methods:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
class Model < Sequel::Model
|
|
110
|
+
plugin ::Sequel::Plugins::RubyLLM
|
|
111
|
+
|
|
112
|
+
acts_as_model
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
class Chat < Sequel::Model
|
|
116
|
+
plugin ::Sequel::Plugins::RubyLLM
|
|
117
|
+
|
|
118
|
+
acts_as_chat(model: :llm_model,model_class: 'ApiFr::Agents::Model')
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
class Message < Sequel::Model
|
|
122
|
+
plugin ::Sequel::Plugins::RubyLLM
|
|
123
|
+
|
|
124
|
+
acts_as_message(model: :llm_model, model_class: 'ApiFr::Agents::Model',)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
class ToolCall < Sequel::Model
|
|
128
|
+
plugin ::Sequel::Plugins::RubyLLM
|
|
129
|
+
|
|
130
|
+
acts_as_tool_call
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Basic Chat Usage
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
# Configure RubyLLM
|
|
138
|
+
RubyLLM.configure do |config|
|
|
139
|
+
config.openai_api_key = ENV['OPENAI_API_KEY']
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Create a chat
|
|
143
|
+
chat = Chat.create
|
|
144
|
+
|
|
145
|
+
# Set instructions
|
|
146
|
+
chat.with_instructions("You are a helpful assistant")
|
|
147
|
+
|
|
148
|
+
# Send a message and get a response
|
|
149
|
+
chat.create_user_message("Hello!")
|
|
150
|
+
response = chat.ask # Calls the LLM and stores the response
|
|
151
|
+
|
|
152
|
+
puts response.content
|
|
153
|
+
# => "Hello! How can I help you today?"
|
|
154
|
+
|
|
155
|
+
# Access message history
|
|
156
|
+
chat.messages.each do |message|
|
|
157
|
+
puts "#{message.role}: #{message.content}"
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Tool Calls
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
# Define a tool
|
|
165
|
+
class Weather < RubyLLM::Tool
|
|
166
|
+
description "Gets current weather for a location"
|
|
167
|
+
|
|
168
|
+
params do # the params DSL is only available in v1.9+. older versions should use the param helper instead
|
|
169
|
+
string :latitude, description: "Latitude (e.g., 52.5200)"
|
|
170
|
+
string :longitude, description: "Longitude (e.g., 13.4050)"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def execute(latitude:, longitude:)
|
|
174
|
+
url = "https://api.open-meteo.com/v1/forecast?latitude=#{latitude}&longitude=#{longitude}¤t=temperature_2m,wind_speed_10m"
|
|
175
|
+
|
|
176
|
+
response = Faraday.get(url)
|
|
177
|
+
data = JSON.parse(response.body)
|
|
178
|
+
rescue => e
|
|
179
|
+
{ error: e.message }
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Use the tool in a chat
|
|
184
|
+
chat.with_tool(Weather.new)
|
|
185
|
+
response = chat.ask("What's the weather in San Francisco?")
|
|
186
|
+
|
|
187
|
+
# Tool calls are automatically tracked
|
|
188
|
+
response.tool_calls.each do |tool_call|
|
|
189
|
+
puts "Called: #{tool_call.name}"
|
|
190
|
+
puts "Arguments: #{tool_call.arguments}"
|
|
191
|
+
end
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Model Registry
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
# Refresh model information from providers
|
|
198
|
+
Model.refresh! # Fetches latest models from all configured providers
|
|
199
|
+
|
|
200
|
+
# Query models
|
|
201
|
+
gpt4 = Model.first(provider: 'openai', model_id: 'gpt-4')
|
|
202
|
+
|
|
203
|
+
# Check capabilities
|
|
204
|
+
gpt4.function_calling? # => true
|
|
205
|
+
gpt4.streaming? # => true
|
|
206
|
+
gpt4.supports?('vision') # => false
|
|
207
|
+
|
|
208
|
+
# Get pricing
|
|
209
|
+
gpt4.input_price_per_million # => 30.0
|
|
210
|
+
gpt4.output_price_per_million # => 60.0
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Important Notes
|
|
214
|
+
|
|
215
|
+
### Database Compatibility
|
|
216
|
+
|
|
217
|
+
The gem supports both PostgreSQL (with jsonb) and other databases (JSON as text):
|
|
218
|
+
- PostgreSQL: Uses `jsonb` columns automatically
|
|
219
|
+
- SQLite/MySQL: Stores JSON as text strings
|
|
220
|
+
- JSON parsing/serialization is handled transparently
|
|
221
|
+
|
|
222
|
+
### The model association
|
|
223
|
+
|
|
224
|
+
The Sequel gem already defines the `model` instance method for subclasses of `Sequel::Model` so it's recommended to use a different association name like `llm_model` to avoid name conflicts
|
|
225
|
+
|
|
226
|
+
## Development
|
|
227
|
+
|
|
228
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
229
|
+
|
|
230
|
+
### Running Tests
|
|
231
|
+
|
|
232
|
+
The test suite uses Minitest and runs against an in-memory SQLite database. To run the tests:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
bundle exec rake test
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
To run a specific test file:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
ruby -Ilib:spec spec/ruby_llm/sequel/chat_methods_spec.rb
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Test Configuration
|
|
245
|
+
|
|
246
|
+
The tests use:
|
|
247
|
+
- **Minitest** for the test framework
|
|
248
|
+
- **SQLite** in-memory database for fast test execution
|
|
249
|
+
- **VCR + WebMock** for HTTP interaction recording (when testing with real API calls)
|
|
250
|
+
- **Transaction rollback** to isolate tests and maintain a clean database state
|
|
251
|
+
|
|
252
|
+
All test configuration is in `spec/spec_helper.rb`.
|
|
253
|
+
|
|
254
|
+
### Contributing to Tests
|
|
255
|
+
|
|
256
|
+
When adding new features, please include corresponding tests. Test files are located in `spec/ruby_llm/sequel/` and follow the naming convention `*_methods_spec.rb`.
|
|
257
|
+
|
|
258
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
259
|
+
|
|
260
|
+
## Contributing
|
|
261
|
+
|
|
262
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/ruby_llm-sequel.
|
|
263
|
+
|
|
264
|
+
## License
|
|
265
|
+
|
|
266
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Sequel
|
|
5
|
+
module ChatMethods
|
|
6
|
+
attr_accessor :assume_model_exists, :context
|
|
7
|
+
|
|
8
|
+
def before_save
|
|
9
|
+
super
|
|
10
|
+
resolve_model_from_strings
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def model=(value)
|
|
14
|
+
@model_string = value if value.is_a?(String)
|
|
15
|
+
return if value.is_a?(String)
|
|
16
|
+
|
|
17
|
+
send("#{self.class.model_association_name}=", value)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def model_id=(value)
|
|
21
|
+
if value.is_a?(Integer) || value.nil?
|
|
22
|
+
super
|
|
23
|
+
else
|
|
24
|
+
@model_string = value
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def provider=(value)
|
|
29
|
+
@provider_string = value
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def provider
|
|
33
|
+
model_record = send(self.class.model_association_name)
|
|
34
|
+
model_record&.provider
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_llm
|
|
38
|
+
model_record = send(self.class.model_association_name)
|
|
39
|
+
@chat ||= (context || ::RubyLLM).chat(
|
|
40
|
+
model: model_record.model_id,
|
|
41
|
+
provider: model_record.provider.to_sym,
|
|
42
|
+
assume_model_exists: assume_model_exists || false
|
|
43
|
+
)
|
|
44
|
+
@chat.reset_messages!
|
|
45
|
+
|
|
46
|
+
message_class = self.class.message_class.constantize
|
|
47
|
+
tool_calls_assoc = message_class.tool_calls_association_name
|
|
48
|
+
|
|
49
|
+
messages_association_dataset.eager(
|
|
50
|
+
tool_calls_assoc, :parent_tool_call, self.class.model_association_name
|
|
51
|
+
).all.each do |msg|
|
|
52
|
+
@chat.add_message(msg.to_llm)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
setup_persistence_callbacks
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def with_instructions(instructions, replace: false)
|
|
59
|
+
db.transaction do
|
|
60
|
+
messages_association_dataset.where(role: 'system').destroy if replace
|
|
61
|
+
send("add_#{self.class.messages_association_name.to_s.singularize}", role: 'system', content: instructions)
|
|
62
|
+
end
|
|
63
|
+
to_llm.with_instructions(instructions)
|
|
64
|
+
self
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def with_tool(...)
|
|
68
|
+
to_llm.with_tool(...)
|
|
69
|
+
self
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def with_tools(...)
|
|
73
|
+
to_llm.with_tools(...)
|
|
74
|
+
self
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def with_model(model_name, provider: nil, assume_exists: false)
|
|
78
|
+
self.model = model_name
|
|
79
|
+
self.provider = provider if provider
|
|
80
|
+
self.assume_model_exists = assume_exists
|
|
81
|
+
resolve_model_from_strings
|
|
82
|
+
save
|
|
83
|
+
model_record = send(self.class.model_association_name)
|
|
84
|
+
to_llm.with_model(model_record.model_id, provider: model_record.provider.to_sym, assume_exists: assume_exists)
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def with_temperature(...)
|
|
89
|
+
to_llm.with_temperature(...)
|
|
90
|
+
self
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def with_params(...)
|
|
94
|
+
to_llm.with_params(...)
|
|
95
|
+
self
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def with_headers(...)
|
|
99
|
+
to_llm.with_headers(...)
|
|
100
|
+
self
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def with_schema(...)
|
|
104
|
+
to_llm.with_schema(...)
|
|
105
|
+
self
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def on_new_message(&block)
|
|
109
|
+
to_llm
|
|
110
|
+
|
|
111
|
+
existing_callback = @chat.instance_variable_get(:@on)[:new_message]
|
|
112
|
+
|
|
113
|
+
@chat.on_new_message do
|
|
114
|
+
existing_callback&.call
|
|
115
|
+
block&.call
|
|
116
|
+
end
|
|
117
|
+
self
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def on_end_message(&block)
|
|
121
|
+
to_llm
|
|
122
|
+
|
|
123
|
+
existing_callback = @chat.instance_variable_get(:@on)[:end_message]
|
|
124
|
+
|
|
125
|
+
@chat.on_end_message do |msg|
|
|
126
|
+
existing_callback&.call(msg)
|
|
127
|
+
block&.call(msg)
|
|
128
|
+
end
|
|
129
|
+
self
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def on_tool_call(...)
|
|
133
|
+
to_llm.on_tool_call(...)
|
|
134
|
+
self
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def on_tool_result(...)
|
|
138
|
+
to_llm.on_tool_result(...)
|
|
139
|
+
self
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def create_user_message(content, with: nil)
|
|
143
|
+
raise UnsupportedFeatureError, "Cannot use 'with' parameter with Sequel integration" if with
|
|
144
|
+
|
|
145
|
+
content_text, _, content_raw = prepare_content_for_storage(content)
|
|
146
|
+
|
|
147
|
+
message_record = send("add_#{self.class.messages_association_name.to_s.singularize}", role: 'user',
|
|
148
|
+
content: content_text)
|
|
149
|
+
message_record.update(content_raw: content_raw) if message_record.columns.include?(:content_raw) && content_raw
|
|
150
|
+
|
|
151
|
+
message_record
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def ask(message, with: nil, &)
|
|
155
|
+
create_user_message(message, with: with)
|
|
156
|
+
complete(&)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
alias say ask
|
|
160
|
+
|
|
161
|
+
def complete(...)
|
|
162
|
+
to_llm.complete(...)
|
|
163
|
+
rescue ::RubyLLM::Error => e
|
|
164
|
+
cleanup_failed_messages if !@message.id.nil? && @message.content && @message.content != ''
|
|
165
|
+
cleanup_orphaned_tool_results
|
|
166
|
+
raise e
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
private
|
|
170
|
+
|
|
171
|
+
# Helper to handle JSON data for both PostgreSQL (jsonb) and other databases (text)
|
|
172
|
+
def jsonb_or_string(value)
|
|
173
|
+
return nil if value.nil?
|
|
174
|
+
|
|
175
|
+
# If the database supports pg_jsonb (PostgreSQL), use it
|
|
176
|
+
if defined?(::Sequel::Postgres) && db.database_type == :postgres
|
|
177
|
+
::Sequel.pg_jsonb(value)
|
|
178
|
+
else
|
|
179
|
+
# For other databases (like SQLite), store as JSON string
|
|
180
|
+
value.is_a?(String) ? value : JSON.generate(value)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def resolve_model_from_strings
|
|
185
|
+
config = context&.config || ::RubyLLM.config
|
|
186
|
+
@model_string ||= config.default_model unless send(self.class.model_association_name)
|
|
187
|
+
return unless @model_string
|
|
188
|
+
|
|
189
|
+
model_info, _provider = ::RubyLLM::Models.resolve(
|
|
190
|
+
@model_string,
|
|
191
|
+
provider: @provider_string,
|
|
192
|
+
assume_exists: assume_model_exists || false,
|
|
193
|
+
config: config
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
model_record = self.class.model_class.constantize.find_or_create(
|
|
197
|
+
model_id: model_info.id,
|
|
198
|
+
provider: model_info.provider
|
|
199
|
+
) do |m|
|
|
200
|
+
m.name = model_info.name || model_info.id
|
|
201
|
+
m.family = model_info.family
|
|
202
|
+
m.context_window = model_info.context_window
|
|
203
|
+
m.max_output_tokens = model_info.max_output_tokens
|
|
204
|
+
m.capabilities = jsonb_or_string(model_info.capabilities || [])
|
|
205
|
+
m.modalities = jsonb_or_string(model_info.modalities.to_h)
|
|
206
|
+
m.pricing = jsonb_or_string(model_info.pricing.to_h)
|
|
207
|
+
m.metadata = jsonb_or_string(model_info.metadata || {})
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
send("#{self.class.model_association_name}=", model_record)
|
|
211
|
+
@model_string = nil
|
|
212
|
+
@provider_string = nil
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def setup_persistence_callbacks
|
|
216
|
+
return @chat if @chat.instance_variable_get(:@_persistence_callbacks_setup)
|
|
217
|
+
|
|
218
|
+
@chat.on_new_message { persist_new_message }
|
|
219
|
+
@chat.on_end_message { |msg| persist_message_completion(msg) }
|
|
220
|
+
|
|
221
|
+
@chat.instance_variable_set(:@_persistence_callbacks_setup, true)
|
|
222
|
+
@chat
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def persist_new_message
|
|
226
|
+
@message = send("add_#{self.class.messages_association_name.to_s.singularize}", role: 'assistant', content: '')
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def persist_message_completion(message)
|
|
230
|
+
return unless message
|
|
231
|
+
|
|
232
|
+
tool_call_id = find_tool_call_id(message.tool_call_id) if message.tool_call_id
|
|
233
|
+
|
|
234
|
+
db.transaction do
|
|
235
|
+
content_text, _attachments_to_persist, content_raw = prepare_content_for_storage(message.content)
|
|
236
|
+
|
|
237
|
+
attrs = {
|
|
238
|
+
role: message.role,
|
|
239
|
+
content: content_text,
|
|
240
|
+
input_tokens: message.input_tokens,
|
|
241
|
+
output_tokens: message.output_tokens
|
|
242
|
+
}
|
|
243
|
+
attrs[:cached_tokens] = message.cached_tokens if @message.columns.include?(:cached_tokens)
|
|
244
|
+
if @message.columns.include?(:cache_creation_tokens)
|
|
245
|
+
attrs[:cache_creation_tokens] = message.cache_creation_tokens
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
attrs[self.class.model_association_name] = send(self.class.model_association_name)
|
|
249
|
+
|
|
250
|
+
if tool_call_id
|
|
251
|
+
parent_tool_call_assoc = @message.class.association_reflection(:parent_tool_call)
|
|
252
|
+
attrs[parent_tool_call_assoc[:key]] = tool_call_id if parent_tool_call_assoc
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
@message.update(attrs)
|
|
256
|
+
if @message.columns.include?(:content_raw) && content_raw
|
|
257
|
+
@message.update(content_raw: ::Sequel.pg_jsonb(content_raw))
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
persist_tool_calls(message.tool_calls) unless message.tool_calls.nil? || message.tool_calls.empty?
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def persist_tool_calls(tool_calls)
|
|
265
|
+
tool_calls.each_value do |tool_call|
|
|
266
|
+
attributes = tool_call.to_h
|
|
267
|
+
attributes[:tool_call_id] = attributes.delete(:id)
|
|
268
|
+
@message.send("add_#{@message.class.tool_calls_association_name.to_s.singularize}", **attributes)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def find_tool_call_id(tool_call_id)
|
|
273
|
+
message_class = self.class.message_class.constantize
|
|
274
|
+
tool_call_class = message_class.tool_call_class.constantize
|
|
275
|
+
|
|
276
|
+
# Find the tool_call record by its string tool_call_id within this chat's messages
|
|
277
|
+
tool_call = tool_call_class
|
|
278
|
+
.where(tool_call_id: tool_call_id)
|
|
279
|
+
.where(message_id: messages_association_dataset.select(:id))
|
|
280
|
+
.first
|
|
281
|
+
|
|
282
|
+
tool_call&.id
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def prepare_content_for_storage(content)
|
|
286
|
+
attachments = nil
|
|
287
|
+
content_raw = nil
|
|
288
|
+
content_text = content
|
|
289
|
+
|
|
290
|
+
case content
|
|
291
|
+
when ::RubyLLM::Content::Raw
|
|
292
|
+
content_raw = content.value
|
|
293
|
+
content_text = nil
|
|
294
|
+
when ::RubyLLM::Content
|
|
295
|
+
attachments = content.attachments if content.attachments.any?
|
|
296
|
+
content_text = content.text
|
|
297
|
+
when Hash, Array
|
|
298
|
+
content_raw = content
|
|
299
|
+
content_text = nil
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
[content_text, attachments, content_raw]
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def cleanup_failed_messages
|
|
306
|
+
::RubyLLM.logger.warn "RubyLLM: API call failed, destroying message: #{@message.id}"
|
|
307
|
+
@message.destroy
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def cleanup_orphaned_tool_results
|
|
311
|
+
reload
|
|
312
|
+
last = messages_association_dataset.order(:id).last
|
|
313
|
+
|
|
314
|
+
return unless last&.tool_call? || last&.tool_result?
|
|
315
|
+
|
|
316
|
+
if last.tool_call?
|
|
317
|
+
last.destroy
|
|
318
|
+
elsif last.tool_result?
|
|
319
|
+
tool_call_message = last.parent_tool_call.message_association
|
|
320
|
+
expected_results = tool_call_message.tool_calls_association.map(&:id)
|
|
321
|
+
actual_results = tool_call_message.tool_calls_association.select(&:result_association).map(&:id)
|
|
322
|
+
|
|
323
|
+
if expected_results.sort != actual_results.sort
|
|
324
|
+
tool_call_message.tool_calls_association.each do |tc|
|
|
325
|
+
tc.result_association&.destroy
|
|
326
|
+
end
|
|
327
|
+
tool_call_message.destroy
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Sequel
|
|
5
|
+
module MessageMethods
|
|
6
|
+
def to_llm
|
|
7
|
+
cached = columns.include?(:cached_tokens) ? self[:cached_tokens] : nil
|
|
8
|
+
cache_creation = columns.include?(:cache_creation_tokens) ? self[:cache_creation_tokens] : nil
|
|
9
|
+
|
|
10
|
+
::RubyLLM::Message.new(
|
|
11
|
+
role: role.to_sym,
|
|
12
|
+
content: extract_content,
|
|
13
|
+
tool_calls: extract_tool_calls,
|
|
14
|
+
tool_call_id: extract_tool_call_id,
|
|
15
|
+
input_tokens: input_tokens,
|
|
16
|
+
output_tokens: output_tokens,
|
|
17
|
+
cached_tokens: cached,
|
|
18
|
+
cache_creation_tokens: cache_creation,
|
|
19
|
+
model_id: model_association&.model_id
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def tool_call?
|
|
24
|
+
role == 'assistant' && tool_calls_association.any?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def tool_result?
|
|
28
|
+
role == 'tool' && !tool_call_id.nil?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def extract_content
|
|
34
|
+
return ::RubyLLM::Content::Raw.new(content_raw) if columns.include?(:content_raw) && !content_raw.nil?
|
|
35
|
+
|
|
36
|
+
content
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def extract_tool_calls
|
|
40
|
+
tool_calls_association.to_h do |tool_call|
|
|
41
|
+
[
|
|
42
|
+
tool_call.tool_call_id,
|
|
43
|
+
tool_call.to_llm
|
|
44
|
+
]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def extract_tool_call_id
|
|
49
|
+
parent_tool_call&.tool_call_id
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Sequel
|
|
5
|
+
module ModelMethods
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.extend(ClassMethods)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module ClassMethods
|
|
11
|
+
# Helper to handle JSON data for both PostgreSQL (jsonb) and other databases (text)
|
|
12
|
+
def jsonb_or_string(value)
|
|
13
|
+
return nil if value.nil?
|
|
14
|
+
|
|
15
|
+
# If the database supports pg_jsonb (PostgreSQL), use it
|
|
16
|
+
if defined?(::Sequel::Postgres) && db.database_type == :postgres
|
|
17
|
+
::Sequel.pg_jsonb(value)
|
|
18
|
+
else
|
|
19
|
+
# For other databases (like SQLite), store as JSON string
|
|
20
|
+
value.is_a?(String) ? value : JSON.generate(value)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def refresh!
|
|
25
|
+
::RubyLLM.models.refresh!
|
|
26
|
+
save_to_database
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def save_to_database
|
|
30
|
+
::Sequel::Model.db.transaction do
|
|
31
|
+
::RubyLLM.models.all.each do |model_info|
|
|
32
|
+
from_llm(model_info)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def from_llm(model_info)
|
|
38
|
+
modalities_hash =
|
|
39
|
+
if model_info.modalities.respond_to?(:to_h)
|
|
40
|
+
model_info.modalities.to_h
|
|
41
|
+
else
|
|
42
|
+
model_info.modalities || {}
|
|
43
|
+
end
|
|
44
|
+
capabilities_array = model_info.capabilities || []
|
|
45
|
+
pricing_hash = model_info.pricing.respond_to?(:to_h) ? model_info.pricing.to_h : (model_info.pricing || {})
|
|
46
|
+
metadata_hash =
|
|
47
|
+
if model_info.metadata.respond_to?(:to_h)
|
|
48
|
+
model_info.metadata.to_h
|
|
49
|
+
else
|
|
50
|
+
model_info.metadata || {}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
model_data = {
|
|
54
|
+
model_id: model_info.id, # RubyLLM::Model::Info uses 'id' not 'model_id'
|
|
55
|
+
name: model_info.name,
|
|
56
|
+
provider: model_info.provider,
|
|
57
|
+
family: model_info.family,
|
|
58
|
+
model_created_at: model_info.created_at,
|
|
59
|
+
context_window: model_info.context_window,
|
|
60
|
+
max_output_tokens: model_info.max_output_tokens,
|
|
61
|
+
knowledge_cutoff: model_info.knowledge_cutoff,
|
|
62
|
+
modalities: jsonb_or_string(modalities_hash),
|
|
63
|
+
capabilities: jsonb_or_string(capabilities_array),
|
|
64
|
+
pricing: jsonb_or_string(pricing_hash),
|
|
65
|
+
metadata: jsonb_or_string(metadata_hash)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
existing = first(provider: model_info.provider, model_id: model_info.id)
|
|
69
|
+
if existing
|
|
70
|
+
existing.update(model_data)
|
|
71
|
+
existing
|
|
72
|
+
else
|
|
73
|
+
create(model_data)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def to_llm
|
|
79
|
+
parse_jsonb = lambda do |value|
|
|
80
|
+
return nil if value.nil?
|
|
81
|
+
|
|
82
|
+
value.is_a?(String) ? ::JSON.parse(value) : value
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
deep_symbolize = lambda do |obj|
|
|
86
|
+
case obj
|
|
87
|
+
when Hash
|
|
88
|
+
obj.transform_keys(&:to_sym).transform_values { |v| deep_symbolize.call(v) }
|
|
89
|
+
when Array
|
|
90
|
+
obj.map { |item| deep_symbolize.call(item) }
|
|
91
|
+
else
|
|
92
|
+
obj
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
parsed_pricing = parse_jsonb.call(pricing)
|
|
97
|
+
symbolized_pricing = parsed_pricing ? deep_symbolize.call(parsed_pricing) : nil
|
|
98
|
+
|
|
99
|
+
::RubyLLM::Model::Info.new(
|
|
100
|
+
id: model_id, # RubyLLM::Model::Info expects 'id' not 'model_id'
|
|
101
|
+
name: name,
|
|
102
|
+
provider: provider,
|
|
103
|
+
family: family,
|
|
104
|
+
created_at: model_created_at,
|
|
105
|
+
context_window: context_window,
|
|
106
|
+
max_output_tokens: max_output_tokens,
|
|
107
|
+
knowledge_cutoff: knowledge_cutoff,
|
|
108
|
+
modalities: parse_jsonb.call(modalities),
|
|
109
|
+
capabilities: parse_jsonb.call(capabilities),
|
|
110
|
+
pricing: symbolized_pricing,
|
|
111
|
+
metadata: parse_jsonb.call(metadata)
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def supports?(capability)
|
|
116
|
+
return false unless capabilities
|
|
117
|
+
|
|
118
|
+
caps_array = capabilities.is_a?(String) ? ::JSON.parse(capabilities) : capabilities
|
|
119
|
+
|
|
120
|
+
caps_array.include?(capability.to_s)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def supports_vision?
|
|
124
|
+
supports?('vision')
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def supports_functions?
|
|
128
|
+
supports?('function_calling') || supports?('tools')
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def input_price_per_million
|
|
132
|
+
return nil unless pricing
|
|
133
|
+
|
|
134
|
+
price_hash = pricing.is_a?(String) ? JSON.parse(pricing) : pricing
|
|
135
|
+
|
|
136
|
+
if price_hash.dig('text_tokens', 'standard', 'input_per_million')
|
|
137
|
+
price_hash.dig('text_tokens', 'standard', 'input_per_million').to_f
|
|
138
|
+
elsif price_hash.dig('text_tokens', 'input')
|
|
139
|
+
price_hash.dig('text_tokens', 'input').to_f
|
|
140
|
+
elsif price_hash['input'] || price_hash[:input]
|
|
141
|
+
(price_hash['input'] || price_hash[:input]).to_f
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def output_price_per_million
|
|
146
|
+
return nil unless pricing
|
|
147
|
+
|
|
148
|
+
price_hash = pricing.is_a?(String) ? JSON.parse(pricing) : pricing
|
|
149
|
+
|
|
150
|
+
if price_hash.dig('text_tokens', 'standard', 'output_per_million')
|
|
151
|
+
price_hash.dig('text_tokens', 'standard', 'output_per_million').to_f
|
|
152
|
+
elsif price_hash.dig('text_tokens', 'output')
|
|
153
|
+
price_hash.dig('text_tokens', 'output').to_f
|
|
154
|
+
elsif price_hash['output'] || price_hash[:output]
|
|
155
|
+
(price_hash['output'] || price_hash[:output]).to_f
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def type
|
|
160
|
+
to_llm.type
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def function_calling?
|
|
164
|
+
to_llm.function_calling?
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def structured_output?
|
|
168
|
+
to_llm.structured_output?
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def batch?
|
|
172
|
+
to_llm.batch?
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def reasoning?
|
|
176
|
+
to_llm.reasoning?
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def citations?
|
|
180
|
+
to_llm.citations?
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def streaming?
|
|
184
|
+
to_llm.streaming?
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Sequel
|
|
5
|
+
module ToolCallMethods
|
|
6
|
+
def to_llm
|
|
7
|
+
::RubyLLM::ToolCall.new(
|
|
8
|
+
id: tool_call_id,
|
|
9
|
+
name: name,
|
|
10
|
+
arguments: parse_arguments
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def result_message
|
|
15
|
+
message_association.chat_association.messages_association_dataset.where(tool_call_id: id).first
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def parse_arguments
|
|
21
|
+
if defined?(::Sequel::Postgres::JSONBHash) && arguments.is_a?(::Sequel::Postgres::JSONBHash)
|
|
22
|
+
return arguments.to_h
|
|
23
|
+
end
|
|
24
|
+
return arguments if arguments.is_a?(Hash)
|
|
25
|
+
return {} if arguments.nil? || arguments.empty?
|
|
26
|
+
|
|
27
|
+
JSON.parse(arguments)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'sequel'
|
|
4
|
+
|
|
5
|
+
Sequel.extension :inflector
|
|
6
|
+
Sequel::Model.plugin :validation_helpers
|
|
7
|
+
|
|
8
|
+
require_relative 'sequel/version'
|
|
9
|
+
require_relative 'sequel/model_methods'
|
|
10
|
+
require_relative 'sequel/chat_methods'
|
|
11
|
+
require_relative 'sequel/message_methods'
|
|
12
|
+
require_relative 'sequel/tool_call_methods'
|
|
13
|
+
require_relative '../sequel/plugins/ruby_llm'
|
|
14
|
+
|
|
15
|
+
module RubyLLM
|
|
16
|
+
module Sequel
|
|
17
|
+
class UnsupportedFeatureError < StandardError; end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sequel
|
|
4
|
+
module Plugins
|
|
5
|
+
# RubyLLM plugin for Sequel models
|
|
6
|
+
# Provides acts_as_model, acts_as_chat, acts_as_message, and acts_as_tool_call methods
|
|
7
|
+
module RubyLLM
|
|
8
|
+
module ClassMethods
|
|
9
|
+
def acts_as_model(chats: :chats, chat_class: nil)
|
|
10
|
+
Plugins.inherited_instance_variables(singleton_class,
|
|
11
|
+
:@chats_association_name => nil,
|
|
12
|
+
:@chat_class => nil)
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
attr_accessor :chats_association_name, :chat_class
|
|
16
|
+
end
|
|
17
|
+
include ::RubyLLM::Sequel::ModelMethods
|
|
18
|
+
|
|
19
|
+
chat_class ||= chats.to_s.classify
|
|
20
|
+
|
|
21
|
+
self.chats_association_name = chats
|
|
22
|
+
self.chat_class = chat_class
|
|
23
|
+
|
|
24
|
+
one_to_many chats, class: chat_class, key: :model_id
|
|
25
|
+
|
|
26
|
+
define_method(:chats_association) do
|
|
27
|
+
send(self.class.chats_association_name)
|
|
28
|
+
end
|
|
29
|
+
define_method(:chats_association_dataset) do
|
|
30
|
+
public_send(:"#{self.class.chats_association_name}_dataset")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
define_method(:validate) do
|
|
34
|
+
super()
|
|
35
|
+
validates_presence %i[model_id provider name]
|
|
36
|
+
validates_unique %i[model_id provider]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def acts_as_chat(messages: :messages, message_class: nil, model: :llm_model, model_class: 'Model',
|
|
41
|
+
model_key: :model_id)
|
|
42
|
+
Plugins.inherited_instance_variables(singleton_class,
|
|
43
|
+
:@messages_association_name => nil,
|
|
44
|
+
:@message_class => nil,
|
|
45
|
+
:@model_association_name => nil,
|
|
46
|
+
:@model_class => nil)
|
|
47
|
+
|
|
48
|
+
class << self
|
|
49
|
+
attr_accessor :messages_association_name, :message_class,
|
|
50
|
+
:model_association_name, :model_class
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
include ::RubyLLM::Sequel::ChatMethods
|
|
54
|
+
|
|
55
|
+
message_class ||= messages.to_s
|
|
56
|
+
model_class ||= model.to_s
|
|
57
|
+
|
|
58
|
+
self.messages_association_name = messages
|
|
59
|
+
self.message_class = message_class.classify
|
|
60
|
+
self.model_association_name = model
|
|
61
|
+
self.model_class = model_class.classify
|
|
62
|
+
|
|
63
|
+
define_method(:messages_association) { public_send(self.class.messages_association_name) }
|
|
64
|
+
define_method(:messages_association_dataset) do
|
|
65
|
+
public_send(:"#{self.class.messages_association_name}_dataset")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
define_method(:model_association) { public_send(self.class.model_association_name) }
|
|
69
|
+
define_method(:model_association_dataset) do
|
|
70
|
+
public_send(:"#{self.class.model_association_name}_dataset")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
association_options = { class: model_class }
|
|
74
|
+
association_options[:key] = model_key if model_key
|
|
75
|
+
many_to_one model, **association_options
|
|
76
|
+
|
|
77
|
+
one_to_many messages, class: message_class, key: :"#{table_name.to_s.singularize}_id",
|
|
78
|
+
order: ::Sequel.qualify(message_class.constantize.table_name.to_sym, :created_at).asc
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def acts_as_message(chat: :chat, chat_class: nil, tool_calls: :tool_calls,
|
|
82
|
+
tool_call_class: nil, model: :llm_model, model_class: 'Model',
|
|
83
|
+
model_key: :model_id, touch_chat: false)
|
|
84
|
+
Plugins.inherited_instance_variables(singleton_class,
|
|
85
|
+
:@chat_association_name => nil,
|
|
86
|
+
:@chat_class => nil,
|
|
87
|
+
:@tool_calls_association_name => nil,
|
|
88
|
+
:@tool_call_class => nil,
|
|
89
|
+
:@model_association_name => nil,
|
|
90
|
+
:@model_class => nil)
|
|
91
|
+
|
|
92
|
+
class << self
|
|
93
|
+
attr_accessor :chat_association_name, :chat_class,
|
|
94
|
+
:tool_calls_association_name, :tool_call_class,
|
|
95
|
+
:model_association_name, :model_class
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
include ::RubyLLM::Sequel::MessageMethods
|
|
99
|
+
|
|
100
|
+
chat_class ||= chat.to_s.classify
|
|
101
|
+
tool_call_class ||= tool_calls.to_s.singularize.classify
|
|
102
|
+
model_class ||= model.to_s.classify
|
|
103
|
+
|
|
104
|
+
self.chat_association_name = chat
|
|
105
|
+
self.chat_class = chat_class
|
|
106
|
+
self.tool_calls_association_name = tool_calls
|
|
107
|
+
self.tool_call_class = tool_call_class
|
|
108
|
+
self.model_association_name = model
|
|
109
|
+
self.model_class = model_class
|
|
110
|
+
|
|
111
|
+
many_to_one chat, class: chat_class
|
|
112
|
+
|
|
113
|
+
model_association_options = { class: model_class }
|
|
114
|
+
model_association_options[:key] = model_key if model_key
|
|
115
|
+
many_to_one model, **model_association_options
|
|
116
|
+
|
|
117
|
+
many_to_one :parent_tool_call, class: tool_call_class, key: :tool_call_id
|
|
118
|
+
one_to_many tool_calls, class: tool_call_class, key: :"#{table_name.to_s.singularize}_id"
|
|
119
|
+
|
|
120
|
+
plugin :touch, column: :updated_at if touch_chat
|
|
121
|
+
|
|
122
|
+
define_method(:validate) do
|
|
123
|
+
super()
|
|
124
|
+
validates_presence [:role]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
define_method(:chat_association) do
|
|
128
|
+
public_send(self.class.chat_association_name)
|
|
129
|
+
end
|
|
130
|
+
define_method(:chat_association_dataset) do
|
|
131
|
+
public_send(:"#{self.class.chat_association_name}_dataset")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
define_method(:tool_calls_association) do
|
|
135
|
+
public_send(self.class.tool_calls_association_name)
|
|
136
|
+
end
|
|
137
|
+
define_method(:tool_calls_association_dataset) do
|
|
138
|
+
public_send(:"#{self.class.tool_calls_association_name}_dataset")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
define_method(:model_association) do
|
|
142
|
+
public_send(self.class.model_association_name)
|
|
143
|
+
end
|
|
144
|
+
define_method(:model_association_dataset) do
|
|
145
|
+
public_send(:"#{self.class.model_association_name}_dataset")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
return if chat == :chat
|
|
149
|
+
|
|
150
|
+
alias_method :chat, chat
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def acts_as_tool_call(message: :message, message_class: nil, result: :result, result_class: nil)
|
|
154
|
+
Plugins.inherited_instance_variables(singleton_class,
|
|
155
|
+
:@message_association_name => nil,
|
|
156
|
+
:@message_class => nil,
|
|
157
|
+
:@result_association_name => nil,
|
|
158
|
+
:@result_class => nil)
|
|
159
|
+
|
|
160
|
+
class << self
|
|
161
|
+
attr_accessor :message_association_name, :message_class,
|
|
162
|
+
:result_association_name, :result_class
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
include ::RubyLLM::Sequel::ToolCallMethods
|
|
166
|
+
|
|
167
|
+
message_class ||= message.to_s.classify
|
|
168
|
+
result_class ||= message_class
|
|
169
|
+
|
|
170
|
+
self.message_association_name = message
|
|
171
|
+
self.message_class = message_class
|
|
172
|
+
self.result_association_name = result
|
|
173
|
+
self.result_class = result_class
|
|
174
|
+
|
|
175
|
+
plugin :association_dependencies
|
|
176
|
+
|
|
177
|
+
many_to_one message, class: message_class, key: :"#{message}_id", eager_loader_key: nil
|
|
178
|
+
one_to_one result, class: result_class, key: :tool_call_id, eager_loader_key: nil
|
|
179
|
+
|
|
180
|
+
add_association_dependencies result => :nullify
|
|
181
|
+
|
|
182
|
+
define_method(:validate) do
|
|
183
|
+
super()
|
|
184
|
+
validates_presence %i[tool_call_id name]
|
|
185
|
+
validates_unique [:tool_call_id]
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
define_method(:message_association) do
|
|
189
|
+
send(self.class.message_association_name)
|
|
190
|
+
end
|
|
191
|
+
define_method(:messages_association_dataset) do
|
|
192
|
+
send("#{self.class.message_association_name}_dataset")
|
|
193
|
+
end
|
|
194
|
+
define_method(:result_association) do
|
|
195
|
+
send(self.class.result_association_name)
|
|
196
|
+
end
|
|
197
|
+
define_method(:result_association_dataset) do
|
|
198
|
+
send("#{self.class.result_association_name}_dataset")
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ruby_llm-sequel
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Julian Pasquale
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ruby_llm
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.9'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.9'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: sequel
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '5.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '5.0'
|
|
40
|
+
description: Provides an adapter to integrate RubyLLM with Sequel ORM.
|
|
41
|
+
email:
|
|
42
|
+
- jpasquale@fu.do
|
|
43
|
+
executables: []
|
|
44
|
+
extensions: []
|
|
45
|
+
extra_rdoc_files: []
|
|
46
|
+
files:
|
|
47
|
+
- LICENSE
|
|
48
|
+
- README.md
|
|
49
|
+
- lib/ruby_llm/sequel.rb
|
|
50
|
+
- lib/ruby_llm/sequel/chat_methods.rb
|
|
51
|
+
- lib/ruby_llm/sequel/message_methods.rb
|
|
52
|
+
- lib/ruby_llm/sequel/model_methods.rb
|
|
53
|
+
- lib/ruby_llm/sequel/tool_call_methods.rb
|
|
54
|
+
- lib/ruby_llm/sequel/version.rb
|
|
55
|
+
- lib/sequel/plugins/ruby_llm.rb
|
|
56
|
+
homepage: https://gitlab.com/fudo/poc/ruby_llm-sequel
|
|
57
|
+
licenses:
|
|
58
|
+
- MIT
|
|
59
|
+
metadata:
|
|
60
|
+
homepage_uri: https://gitlab.com/fudo/poc/ruby_llm-sequel
|
|
61
|
+
source_code_uri: https://gitlab.com/fudo/poc/ruby_llm-sequel
|
|
62
|
+
rubygems_mfa_required: 'true'
|
|
63
|
+
rdoc_options: []
|
|
64
|
+
require_paths:
|
|
65
|
+
- lib
|
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: 3.1.0
|
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
requirements: []
|
|
77
|
+
rubygems_version: 3.7.2
|
|
78
|
+
specification_version: 4
|
|
79
|
+
summary: Sequel adapter for RubyLLM models.
|
|
80
|
+
test_files: []
|