Rails Integration
This guide covers integrating the AI Agents library with Ruby on Rails applications, including conversation persistence with ActiveRecord and session management.
Setup
Add the gem to your Rails application:
# Gemfile
gem 'ai-agents'
Configure your LLM providers in an initializer:
# config/initializers/ai_agents.rb
Agents.configure do |config|
config.openai_api_key = Rails.application.credentials.openai_api_key
config.anthropic_api_key = Rails.application.credentials.anthropic_api_key
config.default_model = 'gpt-4o-mini'
config.debug = Rails.env.development?
# Optional: Configure model registry for staying up-to-date with latest models
config.model_registry_file = Rails.root.join('config', 'models.json')
end
Model Registry Configuration
The SDK uses RubyLLM’s model registry to maintain an up-to-date list of available models from your configured providers. This is particularly useful when new models are released.
Automatic Model Refresh on Application Start
To ensure your application always has access to the latest models, you can refresh the registry on initialization:
# config/initializers/ai_agents.rb
Agents.configure do |config|
config.openai_api_key = Rails.application.credentials.openai_api_key
config.anthropic_api_key = Rails.application.credentials.anthropic_api_key
config.model_registry_file = Rails.root.join('config', 'models.json')
end
# Refresh models on application start
# Only refresh in production/staging to avoid unnecessary API calls in development
if Rails.env.production? || Rails.env.staging?
begin
Agents.refresh_and_save_models!(remote_only: true)
Rails.logger.info "AI Agents: Model registry refreshed successfully"
rescue StandardError => e
Rails.logger.warn "AI Agents: Failed to refresh model registry - #{e.message}"
# Application continues with existing model registry
end
end
Note: Using remote_only: true excludes local providers like Ollama from the refresh, which is typically what you want for production environments.
Manual Model Refresh
You can also refresh models manually via a Rails console or rake task:
# In Rails console
Agents.refresh_and_save_models!(remote_only: true)
# Or via a rake task
# lib/tasks/agents.rake
namespace :agents do
desc "Refresh AI model registry"
task refresh_models: :environment do
puts "Refreshing AI model registry..."
Agents.refresh_and_save_models!(remote_only: true)
puts "Model registry updated successfully!"
rescue StandardError => e
puts "Error refreshing model registry: #{e.message}"
exit 1
end
end
# Run with: rails agents:refresh_models
Development vs Production Considerations
Development:
- Consider skipping automatic refresh to reduce startup time
- The gem includes a default model registry that works for most use cases
- Manually refresh when you need to test new models
Production:
- Enable automatic refresh on deployment or application restart
- Cache the registry in Redis for faster access across instances
- Monitor refresh failures and fall back to cached registry
# Example: Cache model registry in Redis
if Rails.env.production?
begin
cached_timestamp = Rails.cache.read('agents:model_registry_timestamp')
# Refresh if cache is older than 24 hours
if cached_timestamp.nil? || cached_timestamp < 24.hours.ago
Agents.refresh_and_save_models!(remote_only: true)
Rails.cache.write('agents:model_registry_timestamp', Time.current)
end
rescue StandardError => e
Rails.logger.warn "Model registry refresh failed: #{e.message}"
end
end
ActiveRecord Integration
Conversation Persistence
Create a model to store conversation contexts:
# Generate migration
rails generate model Conversation user:references context:text current_agent:string
# db/migrate/xxx_create_conversations.rb
class CreateConversations < ActiveRecord::Migration[7.0]
def change
create_table :conversations do |t|
t.references :user, null: false, foreign_key: true
t.text :context, null: false
t.string :current_agent
t.timestamps
end
add_index :conversations, [:user_id, :created_at]
end
end
Define the Conversation model:
# app/models/conversation.rb
class Conversation < ApplicationRecord
belongs_to :user
# Serialize context as JSON
serialize :context, JSON
validates :context, presence: true
def self.for_user(user)
where(user: user).order(:created_at)
end
def self.latest_for_user(user)
for_user(user).last
end
# Convert to agent context hash
def to_agent_context
context.deep_symbolize_keys
end
# Create from agent result
def self.from_agent_result(user, result)
create!(
user: user,
context: result.context.to_h,
current_agent: result.context[:current_agent]
)
end
end
Session Management
Create a service to manage agent conversations:
# app/services/agent_conversation_service.rb
class AgentConversationService
def initialize(user)
@user = user
@runner = create_agent_runner
end
def send_message(message)
# Get existing conversation context
context = load_conversation_context
# Run agent with message
result = @runner.run(message, context: context)
# Persist updated conversation
save_conversation(result)
result
end
def reset_conversation
Conversation.where(user: @user).destroy_all
end
private
def create_agent_runner
# Create your agents here
triage_agent = Agents::Agent.new(
name: "Triage",
instructions: build_triage_instructions,
tools: [CustomerLookupTool.new]
)
billing_agent = Agents::Agent.new(
name: "Billing",
instructions: "Handle billing and payment inquiries.",
tools: [BillingTool.new, PaymentTool.new]
)
support_agent = Agents::Agent.new(
name: "Support",
instructions: "Provide technical support and troubleshooting.",
tools: [TechnicalTool.new]
)
triage_agent.register_handoffs(billing_agent, support_agent)
Agents::Runner.with_agents(triage_agent, billing_agent, support_agent)
end
def build_triage_instructions
->(context) {
user_info = context[:user_info] || {}
<<~INSTRUCTIONS
You are a customer service triage agent for #{@user.name}.
Customer Details:
- Name: #{@user.name}
- Email: #{@user.email}
- Account Type: #{user_info[:account_type] || 'standard'}
Route customers to the appropriate department:
- Billing: Payment issues, account billing, refunds
- Support: Technical problems, product questions
Always be professional and helpful.
INSTRUCTIONS
}
end
def load_conversation_context
latest_conversation = Conversation.latest_for_user(@user)
return initial_context unless latest_conversation
latest_conversation.to_agent_context
end
def initial_context
{
user_id: @user.id,
user_info: {
name: @user.name,
email: @user.email,
account_type: @user.account_type
}
}
end
def save_conversation(result)
Conversation.from_agent_result(@user, result)
end
end
Controller Integration
Create a controller for handling agent conversations:
# app/controllers/agent_conversations_controller.rb
class AgentConversationsController < ApplicationController
before_action :authenticate_user!
def create
service = AgentConversationService.new(current_user)
begin
result = service.send_message(params[:message])
render json: {
response: result.output,
agent: result.context[:current_agent],
conversation_id: result.context[:conversation_id]
}
rescue => e
Rails.logger.error "Agent conversation error: #{e.message}"
render json: { error: "Unable to process your request" }, status: 500
end
end
def reset
service = AgentConversationService.new(current_user)
service.reset_conversation
render json: { message: "Conversation reset successfully" }
end
def history
conversations = Conversation.for_user(current_user)
.includes(:user)
.limit(50)
render json: conversations.map do |conv|
{
id: conv.id,
agent: conv.current_agent,
timestamp: conv.created_at,
context_keys: conv.context.keys
}
end
end
end
Custom Rails Tools
Create Rails-specific tools for database operations:
# app/tools/customer_lookup_tool.rb
class CustomerLookupTool < Agents::Tool
name "lookup_customer"
description "Look up customer information by email or ID"
param :identifier, type: "string", desc: "Email address or customer ID"
def perform(tool_context, identifier:)
# Access Rails models safely
customer = User.find_by(email: identifier) || User.find_by(id: identifier)
return "Customer not found" unless customer
{
name: customer.name,
email: customer.email,
account_type: customer.account_type,
created_at: customer.created_at,
last_login: customer.last_sign_in_at
}
end
end
# app/tools/billing_tool.rb
class BillingTool < Agents::Tool
name "get_billing_info"
description "Retrieve billing information for a customer"
param :user_id, type: "integer", desc: "Customer user ID"
def perform(tool_context, user_id:)
user = User.find(user_id)
billing_info = user.billing_profile
return "No billing information found" unless billing_info
{
plan: billing_info.plan_name,
status: billing_info.status,
next_billing_date: billing_info.next_billing_date,
amount: billing_info.monthly_amount
}
rescue ActiveRecord::RecordNotFound
"Customer not found"
end
end
Background Processing
For longer conversations, use background jobs:
# app/jobs/agent_conversation_job.rb
class AgentConversationJob < ApplicationJob
queue_as :default
def perform(user_id, message, conversation_id = nil)
user = User.find(user_id)
service = AgentConversationService.new(user)
result = service.send_message(message)
# Broadcast result via ActionCable
ActionCable.server.broadcast(
"agent_conversation_#{user_id}",
{
response: result.output,
agent: result.context[:current_agent],
conversation_id: conversation_id
}
)
end
end
# Enqueue job from controller
def create_async
job_id = AgentConversationJob.perform_later(
current_user.id,
params[:message],
params[:conversation_id]
)
render json: { job_id: job_id }
end
Error Handling
Implement comprehensive error handling:
# app/services/agent_conversation_service.rb
class AgentConversationService
class AgentError < StandardError; end
class ContextError < StandardError; end
def send_message(message)
validate_message(message)
context = load_conversation_context
begin
result = @runner.run(message, context: context)
save_conversation(result)
result
rescue RubyLLM::Error => e
Rails.logger.error "LLM Error: #{e.message}"
raise AgentError, "AI service temporarily unavailable"
rescue JSON::ParserError => e
Rails.logger.error "Context parsing error: #{e.message}"
raise ContextError, "Conversation context corrupted"
end
end
private
def validate_message(message)
raise ArgumentError, "Message cannot be blank" if message.blank?
raise ArgumentError, "Message too long" if message.length > 5000
end
end
Testing
Test Rails integration with RSpec:
# spec/services/agent_conversation_service_spec.rb
RSpec.describe AgentConversationService do
let(:user) { create(:user) }
let(:service) { described_class.new(user) }
describe '#send_message' do
it 'creates a conversation record' do
expect {
service.send_message("Hello")
}.to change(Conversation, :count).by(1)
end
it 'persists context correctly' do
result = service.send_message("Hello")
conversation = Conversation.last
expect(conversation.user).to eq(user)
expect(conversation.context).to include('user_id' => user.id)
end
end
describe '#reset_conversation' do
before { service.send_message("Hello") }
it 'destroys all conversations for user' do
expect {
service.reset_conversation
}.to change(Conversation, :count).by(-1)
end
end
end
Deployment Considerations
Environment Variables
# config/credentials.yml.enc
openai_api_key: your_openai_key
anthropic_api_key: your_anthropic_key
# Or use environment variables
ENV['OPENAI_API_KEY']
ENV['ANTHROPIC_API_KEY']
Database Indexing
# Add indexes for better query performance
add_index :conversations, [:user_id, :current_agent]
add_index :conversations, :created_at
Memory Management
# Cleanup old conversations
# config/schedule.rb (whenever gem)
every 1.day, at: '2:00 am' do
runner "Conversation.where('created_at < ?', 30.days.ago).destroy_all"
end