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?
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::AgentRunner.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