ruby_llm-mongoid 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.
@@ -0,0 +1,23 @@
1
+ class <%= model_model_name %>
2
+ include Mongoid::Document
3
+ include Mongoid::Timestamps
4
+
5
+ field :model_id, type: String
6
+ field :name, type: String
7
+ field :provider, type: String
8
+ field :family, type: String
9
+ field :model_created_at, type: Time
10
+ field :context_window, type: Integer
11
+ field :max_output_tokens, type: Integer
12
+ field :knowledge_cutoff, type: Date
13
+ field :modalities, type: Hash, default: {}
14
+ field :capabilities, type: Array, default: []
15
+ field :pricing, type: Hash, default: {}
16
+ field :metadata, type: Hash, default: {}
17
+
18
+ index({ provider: 1, model_id: 1 }, unique: true)
19
+ index({ provider: 1 })
20
+ index({ family: 1 })
21
+
22
+ acts_as_model
23
+ end
@@ -0,0 +1,14 @@
1
+ class <%= tool_call_model_name %>
2
+ include Mongoid::Document
3
+ include Mongoid::Timestamps
4
+
5
+ field :tool_call_id, type: String
6
+ field :name, type: String
7
+ field :arguments, type: Hash, default: {}
8
+ field :thought_signature, type: String
9
+
10
+ index({ tool_call_id: 1 }, unique: true)
11
+ index({ name: 1 })
12
+
13
+ acts_as_tool_call
14
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+ require "active_support/inflector"
5
+ require "ruby_llm/mongoid/chat_methods"
6
+ require "ruby_llm/mongoid/message_methods"
7
+ require "ruby_llm/mongoid/tool_call_methods"
8
+ require "ruby_llm/mongoid/model_methods"
9
+
10
+ module RubyLLM
11
+ module Mongoid
12
+ # Provides acts_as_chat, acts_as_message, acts_as_tool_call, and acts_as_model
13
+ # class macros for Mongoid documents. Include this module (or let the Railtie do it)
14
+ # and call the appropriate macro inside your document class.
15
+ module ActsAs
16
+ extend ActiveSupport::Concern
17
+
18
+ def self.included(base)
19
+ super
20
+ RubyLLM.config.model_registry_source ||= RubyLLM::Mongoid::MongoidSource.new
21
+ end
22
+
23
+ @@install_lock = Mutex.new # rubocop:disable Style/ClassVars
24
+
25
+ # Hook executed when a class does `include Mongoid::Document`.
26
+ # Injects our class-method macros onto every Mongoid document automatically
27
+ # so users don't have to `include RubyLLM::Mongoid::ActsAs` explicitly.
28
+ def self.install!
29
+ return unless defined?(::Mongoid::Document)
30
+
31
+ @@install_lock.synchronize do
32
+ return if ::Mongoid::Document.respond_to?(:acts_as_chat)
33
+
34
+ ::Mongoid::Document.module_eval do
35
+ include RubyLLM::Mongoid::ActsAs
36
+ end
37
+ end
38
+ end
39
+
40
+ class_methods do # rubocop:disable Metrics/BlockLength
41
+ # -----------------------------------------------------------------------
42
+ # acts_as_chat
43
+ # -----------------------------------------------------------------------
44
+ def acts_as_chat(messages: :messages, message_class: nil,
45
+ model: :model, model_class: nil)
46
+ include RubyLLM::Mongoid::ChatMethods
47
+
48
+ class_attribute :messages_association_name, :model_association_name,
49
+ :message_class, :model_class
50
+
51
+ self.messages_association_name = messages
52
+ self.model_association_name = model
53
+ self.message_class = (message_class || messages.to_s.classify).to_s
54
+ self.model_class = (model_class || model.to_s.classify).to_s
55
+
56
+ has_many messages,
57
+ class_name: self.message_class,
58
+ dependent: :destroy,
59
+ order: :created_at.asc
60
+
61
+ belongs_to model,
62
+ class_name: self.model_class,
63
+ optional: true
64
+
65
+ define_method(:messages_association) { send(messages_association_name) }
66
+ define_method(:model_association) { send(model_association_name) }
67
+ define_method(:"model_association=") { |v| send(:"#{model_association_name}=", v) }
68
+ end
69
+
70
+ # -----------------------------------------------------------------------
71
+ # acts_as_model
72
+ # -----------------------------------------------------------------------
73
+ def acts_as_model(chats: :chats, chat_class: nil)
74
+ include RubyLLM::Mongoid::ModelMethods
75
+
76
+ class_attribute :chats_association_name, :chat_class
77
+
78
+ self.chats_association_name = chats
79
+ self.chat_class = (chat_class || chats.to_s.classify).to_s
80
+
81
+ validates :model_id, presence: true
82
+ validates :provider, presence: true
83
+ validates :name, presence: true
84
+ validates :model_id, uniqueness: { scope: :provider }
85
+
86
+ has_many chats, class_name: self.chat_class
87
+
88
+ define_method(:chats_association) { send(chats_association_name) }
89
+ end
90
+
91
+ # -----------------------------------------------------------------------
92
+ # acts_as_message
93
+ # -----------------------------------------------------------------------
94
+ def acts_as_message(chat: :chat, chat_class: nil, touch_chat: false, # rubocop:disable Metrics/ParameterLists
95
+ tool_calls: :tool_calls, tool_call_class: nil,
96
+ model: :model, model_class: nil)
97
+ include RubyLLM::Mongoid::MessageMethods
98
+
99
+ class_attribute :chat_association_name, :tool_calls_association_name,
100
+ :model_association_name, :chat_class, :tool_call_class,
101
+ :model_class
102
+
103
+ self.chat_association_name = chat
104
+ self.tool_calls_association_name = tool_calls
105
+ self.model_association_name = model
106
+ self.chat_class = (chat_class || chat.to_s.classify).to_s
107
+ self.tool_call_class = (tool_call_class || tool_calls.to_s.classify).to_s
108
+ self.model_class = (model_class || model.to_s.classify).to_s
109
+
110
+ belongs_to chat,
111
+ class_name: self.chat_class,
112
+ touch: touch_chat
113
+
114
+ has_many tool_calls,
115
+ class_name: self.tool_call_class,
116
+ dependent: :destroy
117
+
118
+ # parent_tool_call links a tool-result message back to the ToolCall doc
119
+ # that produced the call. We use a named field `parent_tool_call_id`
120
+ # (BSON::ObjectId) rather than the string `tool_call_id` field to avoid
121
+ # a type collision.
122
+ belongs_to :parent_tool_call,
123
+ class_name: self.tool_call_class,
124
+ optional: true
125
+
126
+ belongs_to model,
127
+ class_name: self.model_class,
128
+ optional: true
129
+
130
+ delegate :tool_call?, :tool_result?, to: :to_llm
131
+
132
+ define_method(:chat_association) { send(chat_association_name) }
133
+ define_method(:tool_calls_association) { send(tool_calls_association_name) }
134
+ define_method(:model_association) { send(model_association_name) }
135
+ end
136
+
137
+ # -----------------------------------------------------------------------
138
+ # acts_as_tool_call
139
+ # -----------------------------------------------------------------------
140
+ def acts_as_tool_call(message: :message, message_class: nil,
141
+ result: :result, result_class: nil)
142
+ include RubyLLM::Mongoid::ToolCallMethods
143
+
144
+ class_attribute :message_association_name, :result_association_name,
145
+ :message_class, :result_class
146
+
147
+ self.message_association_name = message
148
+ self.result_association_name = result
149
+ self.message_class = (message_class || message.to_s.classify).to_s
150
+ self.result_class = (result_class || self.message_class).to_s
151
+
152
+ belongs_to message,
153
+ class_name: self.message_class
154
+
155
+ has_one result,
156
+ class_name: self.result_class,
157
+ foreign_key: :parent_tool_call_id,
158
+ dependent: :nullify
159
+
160
+ define_method(:message_association) { send(message_association_name) }
161
+ define_method(:result_association) { send(result_association_name) }
162
+ end
163
+ end
164
+ end
165
+
166
+ # ---------------------------------------------------------------------------
167
+ # Model registry source — plugs into RubyLLM.config.model_registry_source
168
+ # ---------------------------------------------------------------------------
169
+ class MongoidSource
170
+ def read
171
+ model_class = resolve_model_class
172
+ return [] unless model_class.respond_to?(:all)
173
+
174
+ model_class.all.map(&:to_llm)
175
+ rescue StandardError => e
176
+ RubyLLM.logger.debug { "Failed to load models from MongoDB: #{e.message}, falling back to JSON" }
177
+ []
178
+ end
179
+
180
+ private
181
+
182
+ def resolve_model_class
183
+ klass = RubyLLM.config.model_registry_class
184
+ return klass unless klass.is_a?(String)
185
+
186
+ klass.split("::").inject(Object) { |scope, name| scope.const_get(name) }
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,472 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+ require "ruby_llm/mongoid/transaction"
5
+
6
+ module RubyLLM
7
+ module Mongoid
8
+ # Mixes into a Mongoid document that represents a persisted chat session.
9
+ # Mirrors RubyLLM::ActiveRecord::ChatMethods, replacing AR-specific persistence
10
+ # and query calls with Mongoid equivalents.
11
+ module ChatMethods
12
+ extend ActiveSupport::Concern
13
+ include Transaction
14
+
15
+ included do
16
+ before_save :resolve_model_from_strings
17
+ end
18
+
19
+ attr_accessor :assume_model_exists, :context
20
+
21
+ # -------------------------------------------------------------------------
22
+ # Model / provider assignment
23
+ # -------------------------------------------------------------------------
24
+
25
+ def model=(value)
26
+ @model_string = value if value.is_a?(String)
27
+ return if value.is_a?(String)
28
+
29
+ if self.class.model_association_name == :model
30
+ super
31
+ else
32
+ self.model_association = value
33
+ end
34
+ end
35
+
36
+ def model_id=(value)
37
+ if value.is_a?(String)
38
+ @model_string = value
39
+ else
40
+ super
41
+ end
42
+ end
43
+
44
+ def model_id
45
+ model_association&.model_id
46
+ end
47
+
48
+ def provider=(value)
49
+ @provider_string = value
50
+ end
51
+
52
+ def provider
53
+ model_association&.provider
54
+ end
55
+
56
+ # -------------------------------------------------------------------------
57
+ # Chat interface — mirrors the AR version
58
+ # -------------------------------------------------------------------------
59
+
60
+ def to_llm
61
+ model_record = model_association
62
+ @chat ||= (context || RubyLLM).chat(
63
+ model: model_record.model_id,
64
+ provider: model_record.provider.to_sym,
65
+ assume_model_exists: assume_model_exists || false
66
+ )
67
+ @chat.reset_messages!
68
+
69
+ ordered_messages = order_messages_for_llm(messages_association.to_a)
70
+ ordered_messages.each { |msg| @chat.add_message(msg.to_llm) }
71
+ reapply_runtime_instructions(@chat)
72
+
73
+ setup_persistence_callbacks
74
+ end
75
+
76
+ def with_instructions(instructions, append: false, replace: nil)
77
+ append = append_instructions?(append: append, replace: replace)
78
+ persist_system_instruction(instructions, append: append)
79
+ to_llm.with_instructions(instructions, append: append, replace: replace)
80
+ self
81
+ end
82
+
83
+ def with_runtime_instructions(instructions, append: false, replace: nil)
84
+ append = append_instructions?(append: append, replace: replace)
85
+ store_runtime_instruction(instructions, append: append)
86
+ to_llm.with_instructions(instructions, append: append, replace: replace)
87
+ self
88
+ end
89
+
90
+ def with_tool(...)
91
+ to_llm.with_tool(...)
92
+ self
93
+ end
94
+
95
+ def with_tools(...)
96
+ to_llm.with_tools(...)
97
+ self
98
+ end
99
+
100
+ def with_model(model_name, provider: nil, assume_exists: false)
101
+ self.model = model_name
102
+ self.provider = provider if provider
103
+ self.assume_model_exists = assume_exists
104
+ resolve_model_from_strings
105
+ save!
106
+ to_llm.with_model(model_association.model_id, provider: model_association.provider.to_sym,
107
+ assume_exists: assume_exists)
108
+ self
109
+ end
110
+
111
+ def with_temperature(...)
112
+ to_llm.with_temperature(...)
113
+ self
114
+ end
115
+
116
+ def with_thinking(...)
117
+ to_llm.with_thinking(...)
118
+ self
119
+ end
120
+
121
+ def with_params(...)
122
+ to_llm.with_params(...)
123
+ self
124
+ end
125
+
126
+ def with_headers(...)
127
+ to_llm.with_headers(...)
128
+ self
129
+ end
130
+
131
+ def with_schema(...)
132
+ to_llm.with_schema(...)
133
+ self
134
+ end
135
+
136
+ def on_new_message(&)
137
+ to_llm.on_new_message(&)
138
+ self
139
+ end
140
+
141
+ def on_end_message(&)
142
+ to_llm.on_end_message(&)
143
+ self
144
+ end
145
+
146
+ def before_message(...)
147
+ to_llm.before_message(...)
148
+ self
149
+ end
150
+
151
+ def after_message(...)
152
+ to_llm.after_message(...)
153
+ self
154
+ end
155
+
156
+ def before_tool_call(...)
157
+ to_llm.before_tool_call(...)
158
+ self
159
+ end
160
+
161
+ def after_tool_result(...)
162
+ to_llm.after_tool_result(...)
163
+ self
164
+ end
165
+
166
+ def on_tool_call(...)
167
+ to_llm.on_tool_call(...)
168
+ self
169
+ end
170
+
171
+ def on_tool_result(...)
172
+ to_llm.on_tool_result(...)
173
+ self
174
+ end
175
+
176
+ def add_message(message_or_attributes)
177
+ llm_message = message_or_attributes.is_a?(RubyLLM::Message) ? message_or_attributes : RubyLLM::Message.new(message_or_attributes)
178
+ content_text, attachments, content_raw = prepare_content_for_storage(llm_message.content)
179
+
180
+ attrs = { role: llm_message.role, content: content_text }
181
+
182
+ if llm_message.tool_call_id
183
+ tc_db_id = find_tool_call_db_id(llm_message.tool_call_id)
184
+ attrs[:parent_tool_call_id] = tc_db_id if tc_db_id
185
+ end
186
+
187
+ message_record = messages_association.create!(attrs)
188
+ message_record.update!(content_raw: content_raw) if content_raw_field?(message_record)
189
+
190
+ persist_content(message_record, attachments) if attachments.present?
191
+ persist_tool_calls(llm_message.tool_calls, message_record: message_record) if llm_message.tool_calls.present?
192
+
193
+ message_record
194
+ end
195
+
196
+ def cost
197
+ RubyLLM::Cost.aggregate(messages_association.map(&:cost))
198
+ end
199
+
200
+ def create_user_message(content, with: nil)
201
+ add_message(role: :user, content: build_content(content, with))
202
+ end
203
+
204
+ def ask(message = nil, with: nil, &)
205
+ add_message(role: :user, content: build_content(message, with))
206
+ complete(&)
207
+ end
208
+
209
+ alias say ask
210
+
211
+ def complete(...)
212
+ to_llm.complete(...)
213
+ rescue RubyLLM::Error => e
214
+ cleanup_failed_messages if @message&.persisted? && @message.content.blank?
215
+ cleanup_orphaned_tool_results
216
+ raise e
217
+ end
218
+
219
+ # -------------------------------------------------------------------------
220
+ # Private implementation
221
+ # -------------------------------------------------------------------------
222
+
223
+ private
224
+
225
+ def resolve_model_from_strings
226
+ config = context&.config || RubyLLM.config
227
+ @model_string ||= config.default_model unless model_association
228
+ return unless @model_string
229
+
230
+ model_info, _provider = RubyLLM::Models.resolve(
231
+ @model_string,
232
+ provider: @provider_string,
233
+ assume_exists: assume_model_exists || false,
234
+ config: config
235
+ )
236
+
237
+ model_klass = self.class.model_class.constantize
238
+ model_record = model_klass.find_or_create_by!(
239
+ model_id: model_info.id,
240
+ provider: model_info.provider
241
+ ) do |m|
242
+ m.name = model_info.name || model_info.id
243
+ m.family = model_info.family
244
+ m.context_window = model_info.context_window
245
+ m.max_output_tokens = model_info.max_output_tokens
246
+ m.capabilities = model_info.capabilities || []
247
+ m.modalities = model_info.modalities.to_h
248
+ m.pricing = model_info.pricing.to_h
249
+ m.metadata = model_info.metadata || {}
250
+ end
251
+
252
+ self.model_association = model_record
253
+ @model_string = nil
254
+ @provider_string = nil
255
+ end
256
+
257
+ def setup_persistence_callbacks
258
+ return @chat if @chat.instance_variable_get(:@_persistence_callbacks_setup)
259
+
260
+ @chat.before_message { persist_new_message }
261
+ @chat.after_message { |msg| persist_message_completion(msg) }
262
+
263
+ @chat.instance_variable_set(:@_persistence_callbacks_setup, true)
264
+ @chat
265
+ end
266
+
267
+ def persist_new_message
268
+ @message = messages_association.create!(role: :assistant, content: "")
269
+ end
270
+
271
+ def persist_message_completion(message)
272
+ return unless message
273
+
274
+ tool_call_db_id = find_tool_call_db_id(message.tool_call_id) if message.tool_call_id
275
+
276
+ with_transaction do
277
+ content_text, _attachments, content_raw = prepare_content_for_storage(message.content)
278
+
279
+ attrs = {
280
+ role: message.role,
281
+ content: content_text
282
+ }
283
+ attrs[:input_tokens] = message.input_tokens if field_declared?(@message, :input_tokens)
284
+ attrs[:output_tokens] = message.output_tokens if field_declared?(@message, :output_tokens)
285
+ attrs[:cached_tokens] = message.cached_tokens if field_declared?(@message, :cached_tokens)
286
+ attrs[:cache_creation_tokens] = message.cache_creation_tokens if field_declared?(@message,
287
+ :cache_creation_tokens)
288
+ attrs[:thinking_text] = message.thinking&.text if field_declared?(@message, :thinking_text)
289
+ attrs[:thinking_signature] = message.thinking&.signature if field_declared?(@message, :thinking_signature)
290
+ attrs[:thinking_tokens] = message.thinking_tokens if field_declared?(@message, :thinking_tokens)
291
+
292
+ attrs[self.class.model_association_name] = model_association
293
+
294
+ attrs[:parent_tool_call_id] = tool_call_db_id if tool_call_db_id
295
+
296
+ @message.assign_attributes(attrs)
297
+ @message.content_raw = content_raw if content_raw_field?(@message) && content_raw
298
+ @message.save!
299
+
300
+ persist_tool_calls(message.tool_calls) if message.tool_calls.present?
301
+ end
302
+ end
303
+
304
+ def persist_tool_calls(tool_calls, message_record: @message)
305
+ return if tool_calls.blank?
306
+
307
+ supports_thought_signature = message_record.tool_calls_association.klass.fields.key?("thought_signature")
308
+
309
+ tool_calls.each_value do |tc|
310
+ attributes = tc.to_h
311
+ attributes.delete(:thought_signature) unless supports_thought_signature
312
+ attributes[:tool_call_id] = attributes.delete(:id)
313
+ message_record.tool_calls_association.create!(**attributes)
314
+ end
315
+ end
316
+
317
+ # Resolves a provider-issued tool_call_id string to the Mongo _id of the
318
+ # ToolCall document. Done with two targeted queries instead of a SQL JOIN.
319
+ def find_tool_call_db_id(tool_call_id)
320
+ message_klass = messages_association.klass
321
+ tc_assoc_name = message_klass.tool_calls_association_name
322
+ tc_klass = message_klass.relations[tc_assoc_name.to_s].klass
323
+
324
+ tc = tc_klass.where(tool_call_id: tool_call_id).first
325
+ tc&.id
326
+ end
327
+
328
+ def cleanup_failed_messages
329
+ RubyLLM.logger.warn "RubyLLM: API call failed, destroying message: #{@message.id}"
330
+ @message.destroy
331
+ end
332
+
333
+ def cleanup_orphaned_tool_results
334
+ last = messages_association.order_by(_id: :asc).last
335
+
336
+ return unless last&.tool_call? || last&.tool_result?
337
+
338
+ if last.tool_call?
339
+ last.destroy
340
+ elsif last.tool_result?
341
+ tc_message = last.parent_tool_call.message_association
342
+ tc_ids = tc_message.tool_calls_association.distinct(:_id)
343
+ result_parent_ids = message_klass_for(last).where(
344
+ parent_tool_call_id: { "$in" => tc_ids }
345
+ ).distinct(:parent_tool_call_id)
346
+
347
+ if tc_ids.sort != result_parent_ids.sort
348
+ message_klass_for(last).where(parent_tool_call_id: { "$in" => tc_ids }).destroy_all
349
+ tc_message.destroy
350
+ end
351
+ end
352
+ end
353
+
354
+ def message_klass_for(msg)
355
+ msg.class
356
+ end
357
+
358
+ def persist_system_instruction(instructions, append:)
359
+ with_transaction do
360
+ if append
361
+ messages_association.create!(role: :system, content: instructions)
362
+ else
363
+ replace_persisted_system_instructions(instructions)
364
+ end
365
+ end
366
+ end
367
+
368
+ def replace_persisted_system_instructions(instructions)
369
+ system_messages = messages_association.where(role: :system).order_by(_id: :asc).to_a
370
+
371
+ if system_messages.empty?
372
+ messages_association.create!(role: :system, content: instructions)
373
+ return
374
+ end
375
+
376
+ primary = system_messages.shift
377
+ primary.update!(content: instructions) if primary.content != instructions
378
+ system_messages.each(&:destroy)
379
+ end
380
+
381
+ def append_instructions?(append:, replace:)
382
+ return append if replace.nil?
383
+
384
+ append || (replace == false)
385
+ end
386
+
387
+ def order_messages_for_llm(messages)
388
+ system_msgs, other_msgs = messages.partition { |m| m.role.to_s == "system" }
389
+ system_msgs + other_msgs
390
+ end
391
+
392
+ def runtime_instructions
393
+ @runtime_instructions ||= []
394
+ end
395
+
396
+ def store_runtime_instruction(instructions, append:)
397
+ if append
398
+ runtime_instructions << instructions
399
+ else
400
+ @runtime_instructions = [instructions]
401
+ end
402
+ end
403
+
404
+ def reapply_runtime_instructions(chat)
405
+ return if runtime_instructions.empty?
406
+
407
+ first, *rest = runtime_instructions
408
+ chat.with_instructions(first)
409
+ rest.each { |instr| chat.with_instructions(instr, append: true) }
410
+ end
411
+
412
+ def build_content(message, attachments)
413
+ return message if content_like?(message)
414
+
415
+ RubyLLM::Content.new(message, attachments)
416
+ end
417
+
418
+ def content_like?(object)
419
+ object.is_a?(RubyLLM::Content) || object.is_a?(RubyLLM::Content::Raw)
420
+ end
421
+
422
+ def prepare_content_for_storage(content)
423
+ attachments = nil
424
+ content_raw = nil
425
+ content_text = content
426
+
427
+ case content
428
+ when RubyLLM::Content::Raw
429
+ content_raw = content.value
430
+ content_text = nil
431
+ when RubyLLM::Content
432
+ attachments = content.attachments.presence
433
+ content_text = content.text
434
+ when Hash, Array
435
+ content_raw = content
436
+ content_text = nil
437
+ end
438
+
439
+ [content_text, attachments, content_raw]
440
+ end
441
+
442
+ def persist_content(message_record, attachments)
443
+ return unless message_record.respond_to?(:gridfs_file_ids)
444
+
445
+ ids = attachments.filter_map { |att| upload_to_gridfs(message_record, att) }
446
+ message_record.push(gridfs_file_ids: ids) if ids.any?
447
+ end
448
+
449
+ def upload_to_gridfs(message_record, att)
450
+ att = RubyLLM::Attachment.new(att) unless att.is_a?(RubyLLM::Attachment)
451
+ io = StringIO.new(att.content.to_s.b)
452
+ file_id = message_record.class.gridfs_bucket.upload_from_stream(
453
+ att.filename,
454
+ io,
455
+ metadata: { content_type: att.mime_type }
456
+ )
457
+ { "id" => file_id, "filename" => att.filename, "content_type" => att.mime_type }
458
+ rescue StandardError => e
459
+ RubyLLM.logger.warn "RubyLLM: GridFS upload failed for #{att.filename}: #{e.message}"
460
+ nil
461
+ end
462
+
463
+ def content_raw_field?(record)
464
+ record.class.fields.key?("content_raw")
465
+ end
466
+
467
+ def field_declared?(record, name)
468
+ record.class.fields.key?(name.to_s)
469
+ end
470
+ end
471
+ end
472
+ end