Module: RubyLLM::ActiveRecord::ChatMethods
- Extended by:
- ActiveSupport::Concern
- Includes:
- Enumerable, AttachmentHelpers
- Defined in:
- lib/ruby_llm/active_record/chat_methods.rb
Overview
ChatMethods provides the RubyLLM::Chat API on ActiveRecord models
declared with acts_as_chat, persisting every message to the
database. Configuration methods return self so calls can be chained.
class Chat < ApplicationRecord
acts_as_chat
end
chat = Chat.create!(model: 'gpt-5.6-luna')
chat.ask "What is the capital of France?"
chat..count # => 2
Constant Summary collapse
- CANCELLATION_POLL_INTERVAL =
:nodoc:
1.0- COMPLETION_ERRORS =
:nodoc:
[ # :nodoc: RubyLLM::CancelledError, RubyLLM::Error, Faraday::Error, Timeout::Error, Errno::ETIMEDOUT ].freeze
Instance Attribute Summary collapse
-
#assume_model_exists ⇒ Object
When
true, skips the model registry lookup so unregistered model ids are accepted. -
#context ⇒ Object
An optional RubyLLM::Context supplying per-chat configuration, used when building the underlying chat.
-
#protocol ⇒ Object
Overrides the wire protocol the provider would pick for the model, such as
:responsesor:chat_completionsfor OpenAI, ornilfor the provider default.
Instance Method Summary collapse
-
#add_message(message_or_attributes) ⇒ Object
Persists
message_or_attributesas a message record, including any attachments and tool calls. -
#approve(tool_call) ⇒ Object
Records approval for
tool_call(a ToolCall, a tool call record, or an id) on the persisted tool call, so the next #complete executes it from any process. -
#ask(message = nil, with: nil) ⇒ Object
(also: #say)
Persists
messageas a user message, then runs the conversation loop and returns the latest assistant RubyLLM::Message. -
#ask_later(message = nil, with: nil) ⇒ Object
Persists
messageas a user message without calling the model, so #complete can run later. -
#awaiting_approval? ⇒ Boolean
Returns whether the conversation is waiting on tool calls that require approval and have no recorded decision.
-
#cache_until_here ⇒ Object
Marks the latest persisted message as a prompt cache boundary, or the latest in-memory message when none is persisted yet.
-
#cancel ⇒ Object
Requests cancellation of the current in-flight chat operation.
-
#cancelled? ⇒ Boolean
Returns whether this record or its memoized in-memory chat has a pending cancellation request.
-
#compact ⇒ Object
Compacts the model context and persists its assistant Message without deleting earlier messages.
-
#complete ⇒ Object
Runs the completion loop on the underlying chat, persisting each message, and returns the latest RubyLLM::Message.
-
#complete? ⇒ Boolean
Returns whether the conversation has no pending work, neither a response to generate nor tool calls to run.
-
#cost ⇒ Object
Returns a RubyLLM::Cost aggregating every persisted usage entry, including retries and attempts that did not produce a message.
-
#deny(tool_call) ⇒ Object
Records denial for
tool_call(a ToolCall, a tool call record, or an id) on the persisted tool call. -
#generate ⇒ Object
Makes a single generation attempt, persists the response, and returns it as a RubyLLM::Message.
-
#messages_association ⇒ Object
:nodoc:.
-
#model=(value) ⇒ Object
Sets the chat's model from an id, a RubyLLM::Model value, or the associated internal model record.
-
#model_id ⇒ Object
Returns the model id of the associated model record, or
nil. -
#model_id=(value) ⇒ Object
Stores
valueas the model id, resolved to a model record before save. -
#name ⇒ Object
:method: render :call-seq: render.
-
#pending_approvals ⇒ Object
Returns the persisted tool call records that require approval and have no recorded decision, ready to render as approval cards.
-
#provider ⇒ Object
Returns the provider of the associated model record, or
nil. -
#provider=(value) ⇒ Object
Stores
valueas the provider used when resolving the model id before save. -
#reload ⇒ Object
Reloads the record from the database, Rails-style, and refreshes the underlying chat's persisted message history to match.
-
#run_tools ⇒ Object
Executes the pending tool calls and persists their results without calling the model.
-
#step ⇒ Object
Advances the conversation by one move: runs the pending tool calls if there are any, otherwise generates a response.
-
#to_llm ⇒ Object
Returns the underlying RubyLLM::Chat for this record, building it on first call and memoizing it.
-
#tokens ⇒ Object
Returns token usage aggregated across every persisted usage entry, including retries and attempts that did not produce a message.
-
#with_context(value) ⇒ Object
Rebinds the underlying chat to
valueso subsequent requests use its configuration. -
#with_instructions(instructions, append: false, persist: true, cache_until_here: false) ⇒ Object
Sets the system instructions, persisting them as a message with the
:systemrole. -
#with_model(model_name, provider: nil, protocol: nil, assume_model_exists: false) ⇒ Object
Switches the chat to
model_name, resolving and saving the model record and updating the underlying chat.
Instance Attribute Details
#assume_model_exists ⇒ Object
When true, skips the model registry lookup so unregistered model ids
are accepted. Not persisted; set it again after reloading the record.
36 37 38 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 36 def assume_model_exists @assume_model_exists end |
#context ⇒ Object
An optional RubyLLM::Context supplying per-chat configuration, used when building the underlying chat. Not persisted; set it again after reloading the record.
46 47 48 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 46 def context @context end |
#protocol ⇒ Object
Overrides the wire protocol the provider would pick for the model, such
as :responses or :chat_completions for OpenAI, or nil for the
provider default. Not persisted; set it again after reloading the record.
41 42 43 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 41 def protocol @protocol end |
Instance Method Details
#add_message(message_or_attributes) ⇒ Object
Persists message_or_attributes as a message record, including any
attachments and tool calls. Accepts a RubyLLM::Message, an attributes
Hash, or a record responding to to_llm.
Returns the message record.
chat.(role: :user, content: long_context)
509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 509 def () = = .to_llm if .respond_to?(:to_llm) = RubyLLM::Message.new() unless .is_a?(RubyLLM::Message) = .create!(()) if .tool_call_id && (tool_call = find_tool_call(.tool_call_id)) tool_call.update!(result: ) end persist_content(, .) if ..any? persist_tool_calls(.tool_calls, message_record:) if .tool_calls.present? @chat&.() end |
#approve(tool_call) ⇒ Object
Records approval for tool_call (a ToolCall, a tool call record, or
an id) on the persisted tool call, so the next #complete executes it
from any process. Returns self.
chat.approve(params[:tool_call_id])
CompleteJob.perform_later(chat.id)
75 76 77 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 75 def approve(tool_call) record_tool_call_decision(tool_call, 'approved') end |
#ask(message = nil, with: nil) ⇒ Object Also known as: say
Persists message as a user message, then runs the conversation loop
and returns the latest assistant RubyLLM::Message. The loop pauses
when #awaiting_approval? is true. Yields streaming chunks to a block.
chat.ask "What is the capital of France?"
chat.ask "What's in this file?", with: "diagram.png"
573 574 575 576 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 573 def ask( = nil, with: nil, &) ask_later(, with: with) complete(&) end |
#ask_later(message = nil, with: nil) ⇒ Object
Persists message as a user message without calling the model, so
#complete can run later. Returns self.
chat.ask_later "Summarize this document."
chat.complete
586 587 588 589 590 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 586 def ask_later( = nil, with: nil) to_llm.raise_if_pending_tool_calls! (role: :user, content: , attachments: with) self end |
#awaiting_approval? ⇒ Boolean
Returns whether the conversation is waiting on tool calls that require approval and have no recorded decision. See RubyLLM::Chat#awaiting_approval?.
90 91 92 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 90 def awaiting_approval? to_llm.awaiting_approval? end |
#cache_until_here ⇒ Object
Marks the latest persisted message as a prompt cache boundary, or the
latest in-memory message when none is persisted yet. Returns self.
chat.with_instructions('Reusable analysis prompt').cache_until_here
Raises ArgumentError if the chat has no messages.
534 535 536 537 538 539 540 541 542 543 544 545 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 534 def cache_until_here = .order(:id).last if .cache_until_here elsif @chat&.&.any? @chat.cache_until_here else raise ArgumentError, 'No messages to cache' end self end |
#cancel ⇒ Object
Requests cancellation of the current in-flight chat operation. The request is persisted so a background job can observe it from another process.
51 52 53 54 55 56 57 58 59 60 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 51 def cancel if persisted? update_column(:cancelled, true) else self[:cancelled] = true end @chat&.cancel self end |
#cancelled? ⇒ Boolean
Returns whether this record or its memoized in-memory chat has a pending cancellation request.
64 65 66 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 64 def cancelled? @chat&.cancelled? || self[:cancelled] end |
#compact ⇒ Object
Compacts the model context and persists its assistant Message without deleting earlier messages. See RubyLLM::Chat#compact.
604 605 606 607 608 609 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 604 def compact to_llm.compact rescue *COMPLETION_ERRORS => e cleanup_after_failure(e) raise end |
#complete ⇒ Object
Runs the completion loop on the underlying chat, persisting each message, and returns the latest RubyLLM::Message. Pauses when a tool requires approval. When the API call fails, destroys the empty assistant message and any orphaned tool results, then re-raises the error.
642 643 644 645 646 647 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 642 def complete(...) to_llm.complete(...) rescue *COMPLETION_ERRORS => e cleanup_after_failure(e) raise end |
#complete? ⇒ Boolean
Returns whether the conversation has no pending work, neither a response to generate nor tool calls to run. See RubyLLM::Chat#complete?.
634 635 636 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 634 def complete? to_llm.complete? end |
#cost ⇒ Object
Returns a RubyLLM::Cost aggregating every persisted usage entry, including retries and attempts that did not produce a message.
chat.cost.total
561 562 563 564 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 561 def cost records = ruby_llm_usages.to_a RubyLLM::Cost.aggregate(records.map(&:cost), complete: records.all?(&:cost_available?)) end |
#deny(tool_call) ⇒ Object
Records denial for tool_call (a ToolCall, a tool call record, or
an id) on the persisted tool call. The next #complete appends a
structured denial result instead of executing the tool. Returns
self.
83 84 85 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 83 def deny(tool_call) record_tool_call_decision(tool_call, 'denied') end |
#generate ⇒ Object
Makes a single generation attempt, persists the response, and returns it as a RubyLLM::Message. Tool calls in the response are not executed. See RubyLLM::Chat#generate.
595 596 597 598 599 600 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 595 def generate(...) to_llm.generate(...) rescue *COMPLETION_ERRORS => e cleanup_after_failure(e) raise end |
#messages_association ⇒ Object
:nodoc:
110 111 112 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 110 def # :nodoc: send() end |
#model=(value) ⇒ Object
Sets the chat's model from an id, a RubyLLM::Model value, or the associated internal model record.
chat.model = 'gpt-5.6-luna'
119 120 121 122 123 124 125 126 127 128 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 119 def model=(value) if value.is_a?(RubyLLM::ActiveRecord::Model) @pending_model_id = nil @pending_provider = nil super else @pending_model_id = value.respond_to?(:id) ? value.id : value @pending_provider = value.provider if value.respond_to?(:provider) end end |
#model_id ⇒ Object
Returns the model id of the associated model record, or nil.
chat.model_id # => "gpt-5.6-luna"
139 140 141 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 139 def model_id model&.model_id || @pending_model_id end |
#model_id=(value) ⇒ Object
Stores value as the model id, resolved to a model record before save.
131 132 133 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 131 def model_id=(value) @pending_model_id = value end |
#name ⇒ Object
:method: render :call-seq: render
Returns the next request payload with #before_request hooks applied.
439 440 441 442 443 444 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 439 CHAINABLE_CHAT_DELEGATES.each do |name| define_method(name) do |*args, **kwargs, &block| to_llm.public_send(name, *args, **kwargs, &block) self end end |
#pending_approvals ⇒ Object
Returns the persisted tool call records that require approval and
have no recorded decision, ready to render as approval cards. Pass
a record (or its tool_call_id) to #approve or #deny.
A record's remote? identifies a provider-executed call.
chat.pending_approvals.each { |record| render record }
101 102 103 104 105 106 107 108 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 101 def pending_approvals ids = to_llm.pending_approvals.map(&:id) RubyLLM::ActiveRecord::ToolCall.where( tool_call_id: ids, message_type: self.class..constantize.polymorphic_name, message_id: .select(:id) ) end |
#provider ⇒ Object
Returns the provider of the associated model record, or nil.
150 151 152 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 150 def provider model&.provider || @pending_provider end |
#provider=(value) ⇒ Object
Stores value as the provider used when resolving the model id
before save.
145 146 147 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 145 def provider=(value) @pending_provider = value end |
#reload ⇒ Object
Reloads the record from the database, Rails-style, and refreshes the
underlying chat's persisted message history to match. Runtime-only
configuration such as tools, temperature, and callbacks is preserved.
Returns self.
167 168 169 170 171 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 167 def reload(...) super if @chat self end |
#run_tools ⇒ Object
Executes the pending tool calls and persists their results without
calling the model. See RubyLLM::Chat#run_tools. Returns self.
613 614 615 616 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 613 def run_tools to_llm.run_tools self end |
#step ⇒ Object
Advances the conversation by one move: runs the pending tool calls if
there are any, otherwise generates a response. Returns nil once the
chat is complete or waiting for approval. See RubyLLM::Chat#step.
chat.step until chat.complete? || chat.awaiting_approval?
624 625 626 627 628 629 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 624 def step(...) to_llm.step(...) rescue *COMPLETION_ERRORS => e cleanup_after_failure(e) raise end |
#to_llm ⇒ Object
Returns the underlying RubyLLM::Chat for this record, building it on first call and memoizing it. The chat is loaded with the persisted messages and wired to persist new ones. Subsequent calls return the same chat without touching the database; use #reload to refresh its message history from the record.
159 160 161 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 159 def to_llm @chat ||= build_llm_chat # rubocop:disable Naming/MemoizedInstanceVariableName end |
#tokens ⇒ Object
Returns token usage aggregated across every persisted usage entry, including retries and attempts that did not produce a message.
chat.tokens.input
552 553 554 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 552 def tokens RubyLLM::Tokens.aggregate(ruby_llm_usages.map(&:tokens)) end |
#with_context(value) ⇒ Object
Rebinds the underlying chat to value so subsequent requests use its
configuration. Pass nil to return to the global RubyLLM
configuration. The Context itself is runtime-only and is not
persisted.
177 178 179 180 181 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 177 def with_context(value) self.context = value @chat&.with_context(value) self end |
#with_instructions(instructions, append: false, persist: true, cache_until_here: false) ⇒ Object
Sets the system instructions, persisting them as a message with the
:system role. Replaces any persisted system messages unless
append: is true. Pass persist: false to apply the
instructions only to the in-memory chat for this record instance.
With cache_until_here: true the instruction becomes an
explicit prompt cache boundary. Returns self.
chat.with_instructions "You are a Ruby expert."
chat.with_instructions "Use short bullet points.", append: true
chat.with_instructions current_context, persist: false
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 194 def with_instructions(instructions, append: false, persist: true, cache_until_here: false) to_llm if persist if instructions.nil? clear_persisted_system_instructions else persist_system_instruction(instructions, append:, cache_until_here:) end else store_unpersisted_instruction(instructions, append:, cache_until_here:) end self end |
#with_model(model_name, provider: nil, protocol: nil, assume_model_exists: false) ⇒ Object
Switches the chat to model_name, resolving and saving the model
record and updating the underlying chat. Falls back to the configured
default model when model_name is nil. Pass protocol: to override
the wire protocol the provider would pick for the model. Returns self.
chat.with_model 'claude-sonnet-5'
490 491 492 493 494 495 496 497 498 499 500 |
# File 'lib/ruby_llm/active_record/chat_methods.rb', line 490 def with_model(model_name, provider: nil, protocol: nil, assume_model_exists: false) model_name ||= (context&.config || RubyLLM.config).default_model self.model = model_name self.provider = provider if provider self.protocol = protocol self.assume_model_exists = assume_model_exists resolve_model save! to_llm.with_model(model_id, provider: provider&.to_sym, protocol:, assume_model_exists:) self end |