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. Not persisted; set it again after reloading the record. Assigning one rebinds the underlying chat, like with_context.
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 708 def add_message(message_or_attributes) llm_message = to_llm.prepare_message(message_or_attributes) do |id| RubyLLM::ActiveRecord::ToolCall.exists?(tool_call_id: id) end message_record = messages_association.create!(message_attributes(llm_message)) @last_message_id = message_record.id tool_call = find_tool_call(llm_message.tool_call_id) if llm_message.tool_call_id if tool_call && tool_call.result_id.nil? tool_call.mcp_result = llm_message.mcp_result&.dump if tool_call.has_attribute?(:mcp_result) 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 477
Applies Chat#after_dequeue and returns this record. The block receives the message record and the QueuedMessage record, in the process that runs the chat.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 464
Applies Chat#after_fallback and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 392
Applies Chat#after_file_write and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 434
Applies Chat#after_message and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 470
Applies Chat#after_queue and returns this record. The block receives the QueuedMessage record, in the process that queued it.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 452
Applies Chat#after_tool_progress and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 446
Applies Chat#after_tool_result and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 485
Applies Chat#after_unqueue and returns this record. The block receives the QueuedMessage record, in the process that unqueued it.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 307
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 95 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 778 def ask(message = nil, with: nil, &) ask_later(message, with: with) complete(&) end
Persists message as a user message, after every queued message (see queue), 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 792 def ask_later(message = nil, with: nil) to_llm.raise_if_pending_tool_calls! to_llm.take_queued_messages 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
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 141 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 612
Delegates to Chat#awaiting_input?. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 625
Delegates to Chat#awaiting_tasks?. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 458
Applies Chat#before_fallback and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 428
Applies Chat#before_message and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 422
Applies Chat#before_request and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 440
Applies Chat#before_tool_call and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 738 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 492
Delegates to Chat#caching. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 67 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 80 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 498
Delegates to Chat#citations. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 504
Delegates to Chat#code_execution. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 816 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 522
Delegates to Chat#compaction. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 871 def complete(&block) operation = :complete loop do busy = false response = exclusively(-> { busy = true }) do completing(block) { |on_chunk| to_llm.public_send(operation, &on_chunk) } end return latest_message if busy return response unless work_arrived_during_run? operation = :continue_turn 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. When the block raises, destroys the empty assistant message and re-raises. Tool calls paused on a task or an MCP input request stay, so the next complete picks them up.
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)
Queued messages join as RubyLLM::Chat#complete describes, including those another process queues while this one runs. A message that steers, queued as the run ends, starts another loop before complete returns.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 843 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 528
Delegates to Chat#concurrency. See that method for arguments and return values.
# File lib/ruby_llm/active_record/chat_methods.rb, line 416
Applies Chat#convert_unsupported_attachments and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 765 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 656
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 314
Applies Chat#decline and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 600
Delegates to Chat#deferred_tools. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 109 def deny(tool_call) to_llm.deny(record_tool_call_decision(tool_call, 'denied').to_llm) 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 663
Yields each message in the conversation. Returns an Enumerator without a block.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 534
Delegates to Chat#end_user. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 540
Delegates to Chat#fallbacks. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 516
Delegates to Chat#file_store. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 808 def generate(&block) exclusively(-> {}) { completing(block) { |on_chunk| to_llm.generate(&on_chunk) } } 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. A block receives the chunks as it does in complete.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 546
Delegates to Chat#headers. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 552
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 606
Delegates to Chat#mcp. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 175 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 @chat_model_stale = true if @chat 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 197 def model_id @pending_model_id || model&.model_id end
Returns the model id assigned since the last save, or that of the associated model record, or nil.
chat.model_id # => "gpt-6-luna"
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 188 def model_id=(value) self.model = 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 153 def pending_approvals ids = to_llm.pending_approvals.map(&:id) RubyLLM::ActiveRecord::ToolCall.where( message_type: messages_association.klass.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 618
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 637 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 54 def protocol=(value) @protocol = value @chat_model_stale = true if @chat end
Sets the protocol override, switching the underlying chat to it when that is already built.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 211 def provider @pending_provider || (model&.provider unless @pending_model_id) end
Returns the provider assigned since the last save, or that of the associated model record, or nil. A model id assigned without a provider has none until save resolves it.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 203 def provider=(value) @pending_provider = value @chat_model_stale = true if @chat 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 558
Delegates to Chat#provider_options. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 570
Delegates to Chat#provider_tools. See that method for arguments and return values.
# File lib/ruby_llm/active_record/chat_methods.rb, line 900 def queue(message = nil, with: nil, steer: true, owner: nil) to_llm.queue(message, with:, steer:, owner:) end
Queues message in the chat’s ruby_llm_queued_messages table, as RubyLLM::Chat#queue describes, and returns the QueuedMessage record. Attachments given with with: are stored with Active Storage and move to the message it becomes. owner is any record, such as the user who asked; it is stored and handed back, never interpreted.
Any process can queue a message. The process that runs the chat takes it at its next step; when none is running, run complete, usually in a job.
chat.queue params[:content], owner: Current.user ChatResponseJob.perform_later(chat.id) unless chat.running?
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 906 def queued_messages = ruby_llm_queued_messages.scope
Returns the QueuedMessage records waiting in the queue, oldest first, ready to render.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 233 def reload(...) super @chat_model_stale = true if @chat && (@pending_model_id || @pending_provider) @pending_model_id = @pending_provider = nil 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; a model or provider assigned since the last save is discarded. Returns self.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 674 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 823 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 122 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 564
Delegates to Chat#schema. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 510
Delegates to Chat#skills. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 836 def step(&block) exclusively(-> {}) { completing(block) { |on_chunk| to_llm.step(&on_chunk) } } 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. A block receives the chunks as it does in complete.
chat.step until chat.complete? || chat.waiting?
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 576
Delegates to Chat#temperature. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 582
Delegates to Chat#thinking. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 221 def to_llm @chat ||= build_llm_chat @chat.with_model(model_id, provider: provider&.to_sym, protocol:, assume_model_exists:) if @chat_model_stale @chat_model_stale = false @chat 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, switched to any model assigned since; use reload to refresh its message history from the record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 756 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 588
Delegates to Chat#tool_options. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 594
Delegates to Chat#tools. See that method for arguments and return values.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 912 def unqueue(queued) to_llm.unqueue(queued) end
Takes queued, a QueuedMessage record or its id, out of the queue and deletes its attachments. Returns true, or false once the loop has taken it into the conversation or it was unqueued already. See RubyLLM::Chat#unqueue.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 134 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 362
Applies Chat#with_caching and returns this record.
# File lib/ruby_llm/active_record/chat_methods.rb, line 356
Applies Chat#with_citations and returns this record.
# File lib/ruby_llm/active_record/chat_methods.rb, line 380
Applies Chat#with_code_execution and returns this record.
# File lib/ruby_llm/active_record/chat_methods.rb, line 374
Applies Chat#with_compaction and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 245 def with_context(value) @context = value @chat&.with_context(value) self end
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.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 368
Applies Chat#with_end_user and returns this record.
# File lib/ruby_llm/active_record/chat_methods.rb, line 332
Applies Chat#with_fallbacks and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 404
Applies Chat#with_headers and returns this record.
# File lib/ruby_llm/active_record/chat_methods.rb, line 264 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 344
Applies Chat#with_max_output_tokens and returns this record.
# File lib/ruby_llm/active_record/chat_methods.rb, line 301
Applies Chat#with_mcp and returns this record.
# File lib/ruby_llm/active_record/chat_methods.rb, line 689 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 398
Applies Chat#with_provider_options and returns this record.
# File lib/ruby_llm/active_record/chat_methods.rb, line 326
Applies Chat#with_provider_tools and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 410
Applies Chat#with_schema and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 386
Applies Chat#with_skills and returns this record.
# File lib/ruby_llm/active_record/chat_methods.rb, line 338
Applies Chat#with_temperature and returns this record.
# File lib/ruby_llm/active_record/chat_methods.rb, line 350
Applies Chat#with_thinking and returns this record.
Source
# File lib/ruby_llm/active_record/chat_methods.rb, line 320
Applies Chat#with_tool_options and returns this record.
# File lib/ruby_llm/active_record/chat_methods.rb, line 295
Applies Chat#with_tools and returns this record.