module RubyLLM::ActiveRecord::ChatMethods
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-6-luna') chat.ask "What is the capital of France?" chat.messages.count # => 2
Attributes
When true, skips the model registry lookup so unregistered model ids are accepted. Not persisted; set it again after reloading the record.
An optional RubyLLM::Context supplying per-chat configuration, used when building the underlying chat. Not persisted; set it again after reloading the record.
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.
Public Instance Methods
# File lib/ruby_llm/active_record/chat_methods.rb, line 649 def add_message(message_or_attributes) llm_message = to_llm.prepare_message(message_or_attributes) message_record = messages_association.create!(message_attributes(llm_message)) @last_message_id = message_record.id if llm_message.tool_call_id && (tool_call = find_tool_call(llm_message.tool_call_id)) tool_call.update!(result: message_record) end persist_content(message_record, llm_message.attachments) if llm_message.attachments.any? persist_tool_calls(llm_message.tool_calls, message_record:) if llm_message.tool_calls.present? @chat.add_message(llm_message) bind_message_chat(message_record) end
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.add_message(role: :user, content: long_context)
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 433
Applies Chat#after_fallback and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 403
Applies Chat#after_message and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 421
Applies Chat#after_tool_progress and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 415
Applies Chat#after_tool_result and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 282
Applies Chat#answer and returns this record. The answer persists on the tool call, so any process can resume the call.
# File lib/ruby_llm/active_record/chat_methods.rb, line 83 def approve(tool_call, remember: false) record = tool_call_record(tool_call) to_llm.approve(record.to_llm, remember:) record_tool_call_decision(record, remember ? 'remembered' : 'approved') self end
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. Approving a call that is running or answered changes nothing. With remember: true, later calls to the same tool run without asking for the rest of the chat, as RubyLLM::Chat#approve describes, in every process. Returns self.
chat.approve(params[:tool_call_id]) chat.approve(params[:tool_call_id], remember: true) CompleteJob.perform_later(chat.id)
# File lib/ruby_llm/active_record/chat_methods.rb, line 714 def ask(message = nil, with: nil, &) ask_later(message, with: with) complete(&) end
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"
# File lib/ruby_llm/active_record/chat_methods.rb, line 727 def ask_later(message = nil, with: nil) to_llm.raise_if_pending_tool_calls! if message.is_a?(RubyLLM::MCP::Prompt) message.messages.each { |prompt_message| add_message(prompt_message) } else add_message(role: :user, content: message, attachments: with) end self end
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
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 129 def awaiting_approval? to_llm.awaiting_approval? end
Returns whether the conversation is waiting on tool calls that require approval and have no recorded decision. See RubyLLM::Chat#awaiting_approval?.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 553
Delegates to Chat#awaiting_input?. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 566
Delegates to Chat#awaiting_tasks?. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 427
Applies Chat#before_fallback and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 397
Applies Chat#before_message and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 391
Applies Chat#before_request and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 409
Applies Chat#before_tool_call and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 675 def cache_until_here(ttl: nil) message_record = messages_association.order(:id).last if message_record bind_message_chat(message_record).cache_until_here(ttl:) elsif @chat&.messages&.any? @chat.cache_until_here(ttl:) else raise ArgumentError, 'No messages to cache' end self end
Marks the latest persisted message as a prompt cache boundary, or the latest in-memory message when none is persisted yet. Pass ttl: to give the boundary its own cache lifetime. Returns self.
chat.with_instructions('Reusable analysis prompt').cache_until_here chat.with_instructions('Reusable analysis prompt').cache_until_here(ttl: "1h")
Raises ArgumentError if the chat has no messages.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 439
Delegates to Chat#caching. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 55 def cancel if persisted? update_column(:cancelled, true) else self[:cancelled] = true end @chat&.cancel self end
Requests cancellation of the current in-flight chat operation. The request is persisted so a background job can observe it from another process.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 68 def cancelled? @chat&.cancelled? || self[:cancelled] end
Returns whether this record or its memoized in-memory chat has a pending cancellation request.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 445
Delegates to Chat#citations. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 451
Delegates to Chat#code_execution. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 749 def compact exclusively(-> {}) { completing { to_llm.compact } } end
Compacts the model context and persists its assistant Message without deleting earlier messages. See RubyLLM::Chat#compact. Returns nil without calling the model while another process is running? the chat.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 463
Delegates to Chat#compaction. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 795 def complete(&block) on_chunk = ChunkMethods.streaming(block) { @message } loop do busy = false response = exclusively(-> { busy = true }) { completing { to_llm.complete(&on_chunk) } } return latest_message if busy return response unless work_arrived_during_run? end ensure broadcast_refresh_to(self) if broadcasts_messages? end
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.
Only one process runs a chat at a time. While another process is running? it, returns the latest message without calling the model or running tools, and waiting? is true. Work recorded while this process ran, such as an approval, continues before it returns.
Each chunk passed to the block also answers message, the record it streams into, and broadcast (see ChunkMethods). When the messages broadcast with Turbo, the chat’s page refreshes once the run ends.
chat.complete(&:broadcast)
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 775 def complete? to_llm.complete? end
Returns whether the conversation has no pending work, neither a response to generate nor tool calls to run. See RubyLLM::Chat#complete?.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 469
Delegates to Chat#concurrency. See that method for arguments and return values.
# File lib/ruby_llm/active_record/chat_methods.rb, line 385
Applies Chat#convert_unsupported_attachments and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 702 def cost records = ruby_llm_usages.to_a RubyLLM::Cost.aggregate(records.map(&:cost), complete: records.all?(&:cost_available?)) end
Returns a RubyLLM::Cost aggregating every persisted usage entry, including retries and attempts that did not produce a message.
chat.cost.total
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 597
Returns the number of input tokens the next request would carry, counted by the provider over the persisted conversation.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 289
Applies Chat#decline and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 541
Delegates to Chat#deferred_tools. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 97 def deny(tool_call) record_tool_call_decision(tool_call, 'denied') self end
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. Denying an answered call changes nothing. Returns self.
Raises RubyLLM::ToolCallRunningError when the call is already running.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 604
Yields each message in the conversation. Returns an Enumerator without a block.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 475
Delegates to Chat#end_user. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 481
Delegates to Chat#fallbacks. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 741 def generate(...) exclusively(-> {}) { completing { to_llm.generate(...) } } end
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. Returns nil without calling the model while another process is running? the chat.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 487
Delegates to Chat#headers. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 493
Delegates to Chat#max_output_tokens. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 547
Delegates to Chat#mcp. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 163 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
Sets the chat’s model from an id, a RubyLLM::Model value, or the associated internal model record.
chat.model = 'gpt-6-luna'
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 183 def model_id model&.model_id || @pending_model_id end
Returns the model id of the associated model record, or nil.
chat.model_id # => "gpt-6-luna"
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 175 def model_id=(value) @pending_model_id = value end
Stores value as the model id, resolved to a model record before save.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 141 def pending_approvals ids = to_llm.pending_approvals.map(&:id) RubyLLM::ActiveRecord::ToolCall.where( message_type: self.class.message_class.constantize.polymorphic_name, message_id: messages_association.select(:id) ).in_order_of(:tool_call_id, ids) end
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. Records come in the order the model made the calls.
chat.pending_approvals.each { |record| render record }
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 559
Delegates to Chat#pending_inputs. Requests persist on their tool calls, so they survive restarts.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 578 CHAINABLE_CHAT_DELEGATES.each do |name| define_method(name) do |*args, **kwargs, &block| to_llm.public_send(name, *args, **kwargs, &block) self end end
Delegates to Chat#pending_tasks. Tasks persist on their tool calls, so any process can check on them and resume the chat.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 194 def provider model&.provider || @pending_provider end
Returns the provider of the associated model record, or nil.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 189 def provider=(value) @pending_provider = value end
Stores value as the provider used when resolving the model id before save.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 499
Delegates to Chat#provider_options. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 511
Delegates to Chat#provider_tools. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 211 def reload(...) super sync_messages if @chat self end
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.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 615 PASSTHROUGH_CHAT_DELEGATES.each do |name| define_method(name) do |*args, **kwargs, &block| to_llm.public_send(name, *args, **kwargs, &block) end end
Returns the next request payload with before_request hooks applied.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 756 def run_tools exclusively(-> {}) { to_llm.run_tools } self end
Executes the pending tool calls and persists their results without calling the model. See RubyLLM::Chat#run_tools. Does nothing while another process is running? the chat. Returns self.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 110 def running? return false unless run_claims? claimed_until = self.class.uncached { chat_row.pick(:claimed_until) } claimed_until&.future? || false end
Returns whether a process is running this chat’s conversation loop: calling the model or running tools from complete, step, generate, run_tools, or compact. Reads the database, so it sees runs in other processes. While another process runs the chat, those methods return without doing anything and waiting? is true.
CompleteJob.set(wait: 1.minute).perform_later(chat.id) if chat.running?
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 505
Delegates to Chat#schema. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 457
Delegates to Chat#skills. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 768 def step(...) exclusively(-> {}) { completing { to_llm.step(...) } } end
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?, including while another process is running? it. See RubyLLM::Chat#step.
chat.step until chat.complete? || chat.waiting?
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 517
Delegates to Chat#temperature. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 523
Delegates to Chat#thinking. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 203 def to_llm @chat ||= build_llm_chat # rubocop:disable Naming/MemoizedInstanceVariableName end
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.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 693 def tokens RubyLLM::Tokens.aggregate(ruby_llm_usages.map(&:tokens)) end
Returns token usage aggregated across every persisted usage entry, including retries and attempts that did not produce a message.
chat.tokens.input
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 529
Delegates to Chat#tool_options. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 535
Delegates to Chat#tools. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 122 def waiting? to_llm.waiting? || running_elsewhere? end
Returns whether the conversation can’t continue until something outside it happens, as RubyLLM::Chat#waiting? describes, or until the process that is running? it finishes. Approvals, answers, and tasks persist on their tool calls, so any process sees what the chat waits on.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 337
Applies Chat#with_caching and returns this record.
# File lib/ruby_llm/active_record/chat_methods.rb, line 331
Applies Chat#with_citations and returns this record.
# File lib/ruby_llm/active_record/chat_methods.rb, line 355
Applies Chat#with_code_execution and returns this record.
# File lib/ruby_llm/active_record/chat_methods.rb, line 349
Applies Chat#with_compaction and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 221 def with_context(value) self.context = value @chat&.with_context(value) self end
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 343
Applies Chat#with_end_user and returns this record.
# File lib/ruby_llm/active_record/chat_methods.rb, line 307
Applies Chat#with_fallbacks and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 373
Applies Chat#with_headers and returns this record.
# File lib/ruby_llm/active_record/chat_methods.rb, line 239 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 sync_messages self end
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, and cache_until_here: { ttl: "1h" } gives that boundary its own lifetime. 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
# File lib/ruby_llm/active_record/chat_methods.rb, line 319
Applies Chat#with_max_output_tokens and returns this record.
# File lib/ruby_llm/active_record/chat_methods.rb, line 276
Applies Chat#with_mcp and returns this record.
# File lib/ruby_llm/active_record/chat_methods.rb, line 630 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
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'
# File lib/ruby_llm/active_record/chat_methods.rb, line 367
Applies Chat#with_provider_options and returns this record.
# File lib/ruby_llm/active_record/chat_methods.rb, line 301
Applies Chat#with_provider_tools and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 379
Applies Chat#with_schema and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 361
Applies Chat#with_skills and returns this record.
# File lib/ruby_llm/active_record/chat_methods.rb, line 313
Applies Chat#with_temperature and returns this record.
# File lib/ruby_llm/active_record/chat_methods.rb, line 325
Applies Chat#with_thinking and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 295
Applies Chat#with_tool_options and returns this record.
# File lib/ruby_llm/active_record/chat_methods.rb, line 270
Applies Chat#with_tools and returns this record.