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
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
require 'rails/generators/active_record'
|
5
|
+
|
6
|
+
module RubyLLM
|
7
|
+
class MigrateModelFieldsGenerator < Rails::Generators::Base # rubocop:disable Style/Documentation
|
8
|
+
include Rails::Generators::Migration
|
9
|
+
|
10
|
+
namespace 'ruby_llm:migrate_model_fields'
|
11
|
+
source_root File.expand_path('migrate_model_fields/templates', __dir__)
|
12
|
+
|
13
|
+
class_option :chat_model_name, type: :string, default: 'Chat'
|
14
|
+
class_option :message_model_name, type: :string, default: 'Message'
|
15
|
+
class_option :model_model_name, type: :string, default: 'Model'
|
16
|
+
class_option :tool_call_model_name, type: :string, default: 'ToolCall'
|
17
|
+
|
18
|
+
def self.next_migration_number(dirname)
|
19
|
+
::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_migration_file
|
23
|
+
migration_template 'migration.rb.tt',
|
24
|
+
'db/migrate/migrate_to_ruby_llm_model_references.rb',
|
25
|
+
migration_version: migration_version
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_model_file
|
29
|
+
# Check if Model file already exists
|
30
|
+
model_path = "app/models/#{options[:model_model_name].underscore}.rb"
|
31
|
+
|
32
|
+
if File.exist?(Rails.root.join(model_path))
|
33
|
+
say_status :skip, model_path, :yellow
|
34
|
+
else
|
35
|
+
create_file model_path do
|
36
|
+
<<~RUBY
|
37
|
+
class #{options[:model_model_name]} < ApplicationRecord
|
38
|
+
acts_as_model
|
39
|
+
end
|
40
|
+
RUBY
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def acts_as_model_declaration
|
46
|
+
'acts_as_model'
|
47
|
+
end
|
48
|
+
|
49
|
+
def update_initializer
|
50
|
+
say_status :info, 'Update your config/initializers/ruby_llm.rb:', :yellow
|
51
|
+
say <<~INSTRUCTIONS
|
52
|
+
|
53
|
+
Add this line to enable the DB-backed model registry:
|
54
|
+
config.model_registry_class = "#{options[:model_model_name]}"
|
55
|
+
|
56
|
+
INSTRUCTIONS
|
57
|
+
end
|
58
|
+
|
59
|
+
def show_next_steps
|
60
|
+
say_status :success, 'Migration created!', :green
|
61
|
+
say <<~INSTRUCTIONS
|
62
|
+
|
63
|
+
Next steps:
|
64
|
+
1. Review the migration: db/migrate/*_migrate_to_ruby_llm_model_references.rb
|
65
|
+
2. Run: rails db:migrate
|
66
|
+
3. Update config/initializers/ruby_llm.rb as shown above
|
67
|
+
4. Test your application thoroughly
|
68
|
+
|
69
|
+
The migration will:
|
70
|
+
- Create the Models table if it doesn't exist
|
71
|
+
- Load all models from models.json
|
72
|
+
- Migrate your existing data to use foreign keys
|
73
|
+
- Preserve all existing data (string columns renamed to model_id_string)
|
74
|
+
|
75
|
+
INSTRUCTIONS
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def migration_version
|
81
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module ActiveRecord
|
5
|
+
# Adds chat and message persistence capabilities to ActiveRecord models.
|
6
|
+
module ActsAs
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
# When ActsAs is included, ensure models are loaded from database
|
10
|
+
def self.included(base)
|
11
|
+
super
|
12
|
+
# Monkey-patch Models to use database when ActsAs is active
|
13
|
+
RubyLLM::Models.class_eval do
|
14
|
+
def load_models
|
15
|
+
read_from_database
|
16
|
+
rescue StandardError => e
|
17
|
+
RubyLLM.logger.debug "Failed to load models from database: #{e.message}, falling back to JSON"
|
18
|
+
read_from_json
|
19
|
+
end
|
20
|
+
|
21
|
+
def load_from_database!
|
22
|
+
@models = read_from_database
|
23
|
+
end
|
24
|
+
|
25
|
+
def read_from_database
|
26
|
+
model_class = RubyLLM.config.model_registry_class
|
27
|
+
model_class = model_class.constantize if model_class.is_a?(String)
|
28
|
+
model_class.all.map(&:to_llm)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class_methods do # rubocop:disable Metrics/BlockLength
|
34
|
+
def acts_as_chat(message_class: 'Message', tool_call_class: 'ToolCall',
|
35
|
+
model_class: 'Model', model_foreign_key: nil)
|
36
|
+
include RubyLLM::ActiveRecord::ChatMethods
|
37
|
+
|
38
|
+
@message_class = message_class.to_s
|
39
|
+
@tool_call_class = tool_call_class.to_s
|
40
|
+
@model_class = model_class.to_s
|
41
|
+
@model_foreign_key = model_foreign_key || ActiveSupport::Inflector.foreign_key(@model_class)
|
42
|
+
|
43
|
+
has_many :messages,
|
44
|
+
-> { order(created_at: :asc) },
|
45
|
+
class_name: @message_class,
|
46
|
+
inverse_of: :chat,
|
47
|
+
dependent: :destroy
|
48
|
+
|
49
|
+
belongs_to :model,
|
50
|
+
class_name: @model_class,
|
51
|
+
foreign_key: @model_foreign_key,
|
52
|
+
optional: true
|
53
|
+
|
54
|
+
delegate :add_message, to: :to_llm
|
55
|
+
end
|
56
|
+
|
57
|
+
def acts_as_model(chat_class: 'Chat')
|
58
|
+
include RubyLLM::ActiveRecord::ModelMethods
|
59
|
+
|
60
|
+
@chat_class = chat_class.to_s
|
61
|
+
|
62
|
+
validates :model_id, presence: true, uniqueness: { scope: :provider }
|
63
|
+
validates :provider, presence: true
|
64
|
+
validates :name, presence: true
|
65
|
+
|
66
|
+
has_many :chats,
|
67
|
+
class_name: @chat_class,
|
68
|
+
foreign_key: ActiveSupport::Inflector.foreign_key(name)
|
69
|
+
end
|
70
|
+
|
71
|
+
def acts_as_message(chat_class: 'Chat', # rubocop:disable Metrics/ParameterLists
|
72
|
+
chat_foreign_key: nil,
|
73
|
+
tool_call_class: 'ToolCall',
|
74
|
+
tool_call_foreign_key: nil,
|
75
|
+
model_class: 'Model',
|
76
|
+
model_foreign_key: nil,
|
77
|
+
touch_chat: false)
|
78
|
+
include RubyLLM::ActiveRecord::MessageMethods
|
79
|
+
|
80
|
+
@chat_class = chat_class.to_s
|
81
|
+
@chat_foreign_key = chat_foreign_key || ActiveSupport::Inflector.foreign_key(@chat_class)
|
82
|
+
|
83
|
+
@tool_call_class = tool_call_class.to_s
|
84
|
+
@tool_call_foreign_key = tool_call_foreign_key || ActiveSupport::Inflector.foreign_key(@tool_call_class)
|
85
|
+
|
86
|
+
@model_class = model_class.to_s
|
87
|
+
@model_foreign_key = model_foreign_key || ActiveSupport::Inflector.foreign_key(@model_class)
|
88
|
+
|
89
|
+
belongs_to :chat,
|
90
|
+
class_name: @chat_class,
|
91
|
+
foreign_key: @chat_foreign_key,
|
92
|
+
inverse_of: :messages,
|
93
|
+
touch: touch_chat
|
94
|
+
|
95
|
+
has_many :tool_calls,
|
96
|
+
class_name: @tool_call_class,
|
97
|
+
dependent: :destroy
|
98
|
+
|
99
|
+
belongs_to :parent_tool_call,
|
100
|
+
class_name: @tool_call_class,
|
101
|
+
foreign_key: @tool_call_foreign_key,
|
102
|
+
optional: true,
|
103
|
+
inverse_of: :result
|
104
|
+
|
105
|
+
has_many :tool_results,
|
106
|
+
through: :tool_calls,
|
107
|
+
source: :result,
|
108
|
+
class_name: @message_class
|
109
|
+
|
110
|
+
belongs_to :model,
|
111
|
+
class_name: @model_class,
|
112
|
+
foreign_key: @model_foreign_key,
|
113
|
+
optional: true
|
114
|
+
|
115
|
+
delegate :tool_call?, :tool_result?, to: :to_llm
|
116
|
+
end
|
117
|
+
|
118
|
+
def acts_as_tool_call(message_class: 'Message', message_foreign_key: nil, result_foreign_key: nil)
|
119
|
+
@message_class = message_class.to_s
|
120
|
+
@message_foreign_key = message_foreign_key || ActiveSupport::Inflector.foreign_key(@message_class)
|
121
|
+
@result_foreign_key = result_foreign_key || ActiveSupport::Inflector.foreign_key(name)
|
122
|
+
|
123
|
+
belongs_to :message,
|
124
|
+
class_name: @message_class,
|
125
|
+
foreign_key: @message_foreign_key,
|
126
|
+
inverse_of: :tool_calls
|
127
|
+
|
128
|
+
has_one :result,
|
129
|
+
class_name: @message_class,
|
130
|
+
foreign_key: @result_foreign_key,
|
131
|
+
inverse_of: :parent_tool_call,
|
132
|
+
dependent: :nullify
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,398 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module ActiveRecord
|
5
|
+
# Adds chat and message persistence capabilities to ActiveRecord models.
|
6
|
+
module ActsAsLegacy
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
class_methods do # rubocop:disable Metrics/BlockLength
|
10
|
+
def acts_as_chat(message_class: 'Message', tool_call_class: 'ToolCall')
|
11
|
+
include ChatLegacyMethods
|
12
|
+
|
13
|
+
@message_class = message_class.to_s
|
14
|
+
@tool_call_class = tool_call_class.to_s
|
15
|
+
|
16
|
+
has_many :messages,
|
17
|
+
-> { order(created_at: :asc) },
|
18
|
+
class_name: @message_class,
|
19
|
+
inverse_of: :chat,
|
20
|
+
dependent: :destroy
|
21
|
+
|
22
|
+
delegate :add_message, to: :to_llm
|
23
|
+
end
|
24
|
+
|
25
|
+
def acts_as_message(chat_class: 'Chat',
|
26
|
+
chat_foreign_key: nil,
|
27
|
+
tool_call_class: 'ToolCall',
|
28
|
+
tool_call_foreign_key: nil,
|
29
|
+
touch_chat: false)
|
30
|
+
include MessageLegacyMethods
|
31
|
+
|
32
|
+
@chat_class = chat_class.to_s
|
33
|
+
@chat_foreign_key = chat_foreign_key || ActiveSupport::Inflector.foreign_key(@chat_class)
|
34
|
+
|
35
|
+
@tool_call_class = tool_call_class.to_s
|
36
|
+
@tool_call_foreign_key = tool_call_foreign_key || ActiveSupport::Inflector.foreign_key(@tool_call_class)
|
37
|
+
|
38
|
+
belongs_to :chat,
|
39
|
+
class_name: @chat_class,
|
40
|
+
foreign_key: @chat_foreign_key,
|
41
|
+
inverse_of: :messages,
|
42
|
+
touch: touch_chat
|
43
|
+
|
44
|
+
has_many :tool_calls,
|
45
|
+
class_name: @tool_call_class,
|
46
|
+
dependent: :destroy
|
47
|
+
|
48
|
+
belongs_to :parent_tool_call,
|
49
|
+
class_name: @tool_call_class,
|
50
|
+
foreign_key: @tool_call_foreign_key,
|
51
|
+
optional: true,
|
52
|
+
inverse_of: :result
|
53
|
+
|
54
|
+
has_many :tool_results,
|
55
|
+
through: :tool_calls,
|
56
|
+
source: :result,
|
57
|
+
class_name: @message_class
|
58
|
+
|
59
|
+
delegate :tool_call?, :tool_result?, to: :to_llm
|
60
|
+
end
|
61
|
+
|
62
|
+
def acts_as_tool_call(message_class: 'Message', message_foreign_key: nil, result_foreign_key: nil)
|
63
|
+
@message_class = message_class.to_s
|
64
|
+
@message_foreign_key = message_foreign_key || ActiveSupport::Inflector.foreign_key(@message_class)
|
65
|
+
@result_foreign_key = result_foreign_key || ActiveSupport::Inflector.foreign_key(name)
|
66
|
+
|
67
|
+
belongs_to :message,
|
68
|
+
class_name: @message_class,
|
69
|
+
foreign_key: @message_foreign_key,
|
70
|
+
inverse_of: :tool_calls
|
71
|
+
|
72
|
+
has_one :result,
|
73
|
+
class_name: @message_class,
|
74
|
+
foreign_key: @result_foreign_key,
|
75
|
+
inverse_of: :parent_tool_call,
|
76
|
+
dependent: :nullify
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Methods mixed into chat models.
|
82
|
+
module ChatLegacyMethods
|
83
|
+
extend ActiveSupport::Concern
|
84
|
+
|
85
|
+
class_methods do
|
86
|
+
attr_reader :tool_call_class
|
87
|
+
end
|
88
|
+
|
89
|
+
def to_llm(context: nil)
|
90
|
+
# model_id is a string that RubyLLM can resolve
|
91
|
+
@chat ||= if context
|
92
|
+
context.chat(model: model_id)
|
93
|
+
else
|
94
|
+
RubyLLM.chat(model: model_id)
|
95
|
+
end
|
96
|
+
@chat.reset_messages!
|
97
|
+
|
98
|
+
messages.each do |msg|
|
99
|
+
@chat.add_message(msg.to_llm)
|
100
|
+
end
|
101
|
+
|
102
|
+
setup_persistence_callbacks
|
103
|
+
end
|
104
|
+
|
105
|
+
def with_instructions(instructions, replace: false)
|
106
|
+
transaction do
|
107
|
+
messages.where(role: :system).destroy_all if replace
|
108
|
+
messages.create!(role: :system, content: instructions)
|
109
|
+
end
|
110
|
+
to_llm.with_instructions(instructions)
|
111
|
+
self
|
112
|
+
end
|
113
|
+
|
114
|
+
def with_tool(...)
|
115
|
+
to_llm.with_tool(...)
|
116
|
+
self
|
117
|
+
end
|
118
|
+
|
119
|
+
def with_tools(...)
|
120
|
+
to_llm.with_tools(...)
|
121
|
+
self
|
122
|
+
end
|
123
|
+
|
124
|
+
def with_model(...)
|
125
|
+
update(model_id: to_llm.with_model(...).model.id)
|
126
|
+
self
|
127
|
+
end
|
128
|
+
|
129
|
+
def with_temperature(...)
|
130
|
+
to_llm.with_temperature(...)
|
131
|
+
self
|
132
|
+
end
|
133
|
+
|
134
|
+
def with_context(context)
|
135
|
+
to_llm(context: context)
|
136
|
+
self
|
137
|
+
end
|
138
|
+
|
139
|
+
def with_params(...)
|
140
|
+
to_llm.with_params(...)
|
141
|
+
self
|
142
|
+
end
|
143
|
+
|
144
|
+
def with_headers(...)
|
145
|
+
to_llm.with_headers(...)
|
146
|
+
self
|
147
|
+
end
|
148
|
+
|
149
|
+
def with_schema(...)
|
150
|
+
to_llm.with_schema(...)
|
151
|
+
self
|
152
|
+
end
|
153
|
+
|
154
|
+
def on_new_message(&block)
|
155
|
+
to_llm
|
156
|
+
|
157
|
+
existing_callback = @chat.instance_variable_get(:@on)[:new_message]
|
158
|
+
|
159
|
+
@chat.on_new_message do
|
160
|
+
existing_callback&.call
|
161
|
+
block&.call
|
162
|
+
end
|
163
|
+
self
|
164
|
+
end
|
165
|
+
|
166
|
+
def on_end_message(&block)
|
167
|
+
to_llm
|
168
|
+
|
169
|
+
existing_callback = @chat.instance_variable_get(:@on)[:end_message]
|
170
|
+
|
171
|
+
@chat.on_end_message do |msg|
|
172
|
+
existing_callback&.call(msg)
|
173
|
+
block&.call(msg)
|
174
|
+
end
|
175
|
+
self
|
176
|
+
end
|
177
|
+
|
178
|
+
def on_tool_call(...)
|
179
|
+
to_llm.on_tool_call(...)
|
180
|
+
self
|
181
|
+
end
|
182
|
+
|
183
|
+
def on_tool_result(...)
|
184
|
+
to_llm.on_tool_result(...)
|
185
|
+
self
|
186
|
+
end
|
187
|
+
|
188
|
+
def create_user_message(content, with: nil)
|
189
|
+
message_record = messages.create!(role: :user, content: content)
|
190
|
+
persist_content(message_record, with) if with.present?
|
191
|
+
message_record
|
192
|
+
end
|
193
|
+
|
194
|
+
def ask(message, with: nil, &)
|
195
|
+
create_user_message(message, with:)
|
196
|
+
complete(&)
|
197
|
+
end
|
198
|
+
|
199
|
+
alias say ask
|
200
|
+
|
201
|
+
def complete(...)
|
202
|
+
to_llm.complete(...)
|
203
|
+
rescue RubyLLM::Error => e
|
204
|
+
cleanup_failed_messages if @message&.persisted? && @message.content.blank?
|
205
|
+
cleanup_orphaned_tool_results
|
206
|
+
raise e
|
207
|
+
end
|
208
|
+
|
209
|
+
private
|
210
|
+
|
211
|
+
def cleanup_failed_messages
|
212
|
+
RubyLLM.logger.warn "RubyLLM: API call failed, destroying message: #{@message.id}"
|
213
|
+
@message.destroy
|
214
|
+
end
|
215
|
+
|
216
|
+
def cleanup_orphaned_tool_results # rubocop:disable Metrics/PerceivedComplexity
|
217
|
+
messages.reload
|
218
|
+
last = messages.order(:id).last
|
219
|
+
|
220
|
+
return unless last&.tool_call? || last&.tool_result?
|
221
|
+
|
222
|
+
if last.tool_call?
|
223
|
+
last.destroy
|
224
|
+
elsif last.tool_result?
|
225
|
+
tool_call_message = last.parent_tool_call.message
|
226
|
+
expected_results = tool_call_message.tool_calls.pluck(:id)
|
227
|
+
actual_results = tool_call_message.tool_results.pluck(:tool_call_id)
|
228
|
+
|
229
|
+
if expected_results.sort != actual_results.sort
|
230
|
+
tool_call_message.tool_results.each(&:destroy)
|
231
|
+
tool_call_message.destroy
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def setup_persistence_callbacks
|
237
|
+
return @chat if @chat.instance_variable_get(:@_persistence_callbacks_setup)
|
238
|
+
|
239
|
+
@chat.on_new_message { persist_new_message }
|
240
|
+
@chat.on_end_message { |msg| persist_message_completion(msg) }
|
241
|
+
|
242
|
+
@chat.instance_variable_set(:@_persistence_callbacks_setup, true)
|
243
|
+
@chat
|
244
|
+
end
|
245
|
+
|
246
|
+
def persist_new_message
|
247
|
+
@message = messages.create!(role: :assistant, content: '')
|
248
|
+
end
|
249
|
+
|
250
|
+
def persist_message_completion(message) # rubocop:disable Metrics/PerceivedComplexity
|
251
|
+
return unless message
|
252
|
+
|
253
|
+
tool_call_id = find_tool_call_id(message.tool_call_id) if message.tool_call_id
|
254
|
+
|
255
|
+
transaction do
|
256
|
+
content = message.content
|
257
|
+
attachments_to_persist = nil
|
258
|
+
|
259
|
+
if content.is_a?(RubyLLM::Content)
|
260
|
+
attachments_to_persist = content.attachments if content.attachments.any?
|
261
|
+
content = content.text
|
262
|
+
elsif content.is_a?(Hash) || content.is_a?(Array)
|
263
|
+
content = content.to_json
|
264
|
+
end
|
265
|
+
|
266
|
+
@message.update!(
|
267
|
+
role: message.role,
|
268
|
+
content: content,
|
269
|
+
model_id: message.model_id,
|
270
|
+
input_tokens: message.input_tokens,
|
271
|
+
output_tokens: message.output_tokens
|
272
|
+
)
|
273
|
+
@message.write_attribute(@message.class.tool_call_foreign_key, tool_call_id) if tool_call_id
|
274
|
+
@message.save!
|
275
|
+
|
276
|
+
persist_content(@message, attachments_to_persist) if attachments_to_persist
|
277
|
+
persist_tool_calls(message.tool_calls) if message.tool_calls.present?
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def persist_tool_calls(tool_calls)
|
282
|
+
tool_calls.each_value do |tool_call|
|
283
|
+
attributes = tool_call.to_h
|
284
|
+
attributes[:tool_call_id] = attributes.delete(:id)
|
285
|
+
@message.tool_calls.create!(**attributes)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
def find_tool_call_id(tool_call_id)
|
290
|
+
self.class.tool_call_class.constantize.find_by(tool_call_id: tool_call_id)&.id
|
291
|
+
end
|
292
|
+
|
293
|
+
def persist_content(message_record, attachments)
|
294
|
+
return unless message_record.respond_to?(:attachments)
|
295
|
+
|
296
|
+
attachables = prepare_for_active_storage(attachments)
|
297
|
+
message_record.attachments.attach(attachables) if attachables.any?
|
298
|
+
end
|
299
|
+
|
300
|
+
def prepare_for_active_storage(attachments)
|
301
|
+
Utils.to_safe_array(attachments).filter_map do |attachment|
|
302
|
+
case attachment
|
303
|
+
when ActionDispatch::Http::UploadedFile, ActiveStorage::Blob
|
304
|
+
attachment
|
305
|
+
when ActiveStorage::Attached::One, ActiveStorage::Attached::Many
|
306
|
+
attachment.blobs
|
307
|
+
when Hash
|
308
|
+
attachment.values.map { |v| prepare_for_active_storage(v) }
|
309
|
+
else
|
310
|
+
convert_to_active_storage_format(attachment)
|
311
|
+
end
|
312
|
+
end.flatten.compact
|
313
|
+
end
|
314
|
+
|
315
|
+
def convert_to_active_storage_format(source)
|
316
|
+
return if source.blank?
|
317
|
+
|
318
|
+
attachment = source.is_a?(RubyLLM::Attachment) ? source : RubyLLM::Attachment.new(source)
|
319
|
+
|
320
|
+
{
|
321
|
+
io: StringIO.new(attachment.content),
|
322
|
+
filename: attachment.filename,
|
323
|
+
content_type: attachment.mime_type
|
324
|
+
}
|
325
|
+
rescue StandardError => e
|
326
|
+
RubyLLM.logger.warn "Failed to process attachment #{source}: #{e.message}"
|
327
|
+
nil
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
# Methods mixed into message models.
|
332
|
+
module MessageLegacyMethods
|
333
|
+
extend ActiveSupport::Concern
|
334
|
+
|
335
|
+
class_methods do
|
336
|
+
attr_reader :chat_class, :tool_call_class, :chat_foreign_key, :tool_call_foreign_key
|
337
|
+
end
|
338
|
+
|
339
|
+
def to_llm
|
340
|
+
RubyLLM::Message.new(
|
341
|
+
role: role.to_sym,
|
342
|
+
content: extract_content,
|
343
|
+
tool_calls: extract_tool_calls,
|
344
|
+
tool_call_id: extract_tool_call_id,
|
345
|
+
input_tokens: input_tokens,
|
346
|
+
output_tokens: output_tokens,
|
347
|
+
model_id: model_id
|
348
|
+
)
|
349
|
+
end
|
350
|
+
|
351
|
+
private
|
352
|
+
|
353
|
+
def extract_tool_calls
|
354
|
+
tool_calls.to_h do |tool_call|
|
355
|
+
[
|
356
|
+
tool_call.tool_call_id,
|
357
|
+
RubyLLM::ToolCall.new(
|
358
|
+
id: tool_call.tool_call_id,
|
359
|
+
name: tool_call.name,
|
360
|
+
arguments: tool_call.arguments
|
361
|
+
)
|
362
|
+
]
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
def extract_tool_call_id
|
367
|
+
parent_tool_call&.tool_call_id
|
368
|
+
end
|
369
|
+
|
370
|
+
def extract_content
|
371
|
+
return content unless respond_to?(:attachments) && attachments.attached?
|
372
|
+
|
373
|
+
RubyLLM::Content.new(content).tap do |content_obj|
|
374
|
+
@_tempfiles = []
|
375
|
+
|
376
|
+
attachments.each do |attachment|
|
377
|
+
tempfile = download_attachment(attachment)
|
378
|
+
content_obj.add_attachment(tempfile, filename: attachment.filename.to_s)
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
def download_attachment(attachment)
|
384
|
+
ext = File.extname(attachment.filename.to_s)
|
385
|
+
basename = File.basename(attachment.filename.to_s, ext)
|
386
|
+
tempfile = Tempfile.new([basename, ext])
|
387
|
+
tempfile.binmode
|
388
|
+
|
389
|
+
attachment.download { |chunk| tempfile.write(chunk) }
|
390
|
+
|
391
|
+
tempfile.flush
|
392
|
+
tempfile.rewind
|
393
|
+
@_tempfiles << tempfile
|
394
|
+
tempfile
|
395
|
+
end
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|