-
Notifications
You must be signed in to change notification settings - Fork 4
Examples
adham90 edited this page Feb 20, 2026
·
7 revisions
Real-world agent patterns and use cases.
class SearchIntentAgent < ApplicationAgent
model "gpt-4o-mini"
temperature 0.0
cache 30.minutes
param :query, required: true
user "Extract search intent from: {query}"
def schema
@schema ||= RubyLLM::Schema.create do
string :refined_query, description: "Cleaned search query"
array :filters, of: :string, description: "Filters as type:value"
integer :category_id, nullable: true
number :confidence
end
end
end
# Usage
result = SearchIntentAgent.call(query: "red dress under $50")
# => { refined_query: "red dress", filters: ["color:red", "price:<50"], ... }class EmailClassifierAgent < ApplicationAgent
model "gpt-4o-mini"
temperature 0.0
system do
<<~S
You are an email classification system. Categorize emails
based on content, urgency, and required action.
S
end
param :subject, required: true
param :body, required: true
param :sender
user do
<<~S
Subject: {subject}
From: {sender}
Body: {body}
S
end
def schema
@schema ||= RubyLLM::Schema.create do
string :category, enum: %w[urgent important routine spam]
string :department, enum: %w[sales support billing general]
boolean :requires_response
integer :priority, description: "1-5, 5 being highest"
array :tags, of: :string
end
end
endclass BlogPostAgent < ApplicationAgent
model "gpt-4o"
temperature 0.7
timeout 120
system do
<<~S
You are an expert content writer. Write engaging, well-structured
blog posts that are SEO-friendly and informative.
S
end
param :topic, required: true
param :tone, default: "professional"
param :word_count, default: 800
user do
<<~S
Write a {word_count}-word blog post about: {topic}
Tone: {tone}
Requirements:
- Engaging introduction
- 3-5 main sections with headers
- Practical examples or tips
- Strong conclusion with call-to-action
S
end
def schema
@schema ||= RubyLLM::Schema.create do
string :title
string :meta_description, description: "SEO meta description, 150 chars"
string :content, description: "Full blog post in Markdown"
array :tags, of: :string
end
end
endclass ProductDescriptionAgent < ApplicationAgent
model "gpt-4o"
temperature 0.6
param :product_name, required: true
param :features, required: true
param :target_audience, default: "general consumers"
user do
<<~S
Create a compelling product description for:
Product: #{product_name}
Features: #{features.join(", ")}
Target Audience: #{target_audience}
S
end
def schema
@schema ||= RubyLLM::Schema.create do
string :headline, description: "Attention-grabbing headline"
string :short_description, description: "50-word summary"
string :full_description, description: "Detailed description"
array :bullet_points, of: :string, description: "Key selling points"
end
end
endclass InvoiceParserAgent < ApplicationAgent
model "gpt-4o" # Vision capable
param :invoice_path, required: true
user "Extract all invoice details from this document."
def schema
@schema ||= RubyLLM::Schema.create do
string :invoice_number
string :date
string :due_date, nullable: true
object :vendor do
string :name
string :address, nullable: true
end
object :customer do
string :name
string :address, nullable: true
end
array :line_items, of: :object do
string :description
integer :quantity
number :unit_price
number :total
end
number :subtotal
number :tax, nullable: true
number :total
string :currency, default: "USD"
end
end
end
# Usage with attachment
result = InvoiceParserAgent.call(
invoice_path: "path",
with: "invoice.pdf"
)class ResumeParserAgent < ApplicationAgent
model "gpt-4o"
param :resume_text, required: true
user do
<<~S
Parse this resume and extract structured information:
{resume_text}
S
end
def schema
@schema ||= RubyLLM::Schema.create do
object :contact do
string :name
string :email, nullable: true
string :phone, nullable: true
string :location, nullable: true
end
string :summary, nullable: true
array :experience, of: :object do
string :company
string :title
string :dates
array :responsibilities, of: :string
end
array :education, of: :object do
string :institution
string :degree
string :year, nullable: true
end
array :skills, of: :string
end
end
endclass SupportAgent < ApplicationAgent
model "gpt-4o"
temperature 0.3
param :message, required: true
param :conversation_history, default: []
param :customer_info, default: {}
system do
<<~S
You are a helpful customer support agent for TechStore.
Key information:
- Return policy: 30 days, unopened items
- Shipping: Free over $50
- Support hours: 9 AM - 9 PM EST
Customer info: #{customer_info.to_json}
Be helpful, professional, and concise. If you can't help,
offer to escalate to a human agent.
S
end
user do
history = conversation_history.map do |msg|
"#{msg[:role].capitalize}: #{msg[:content]}"
end.join("\n")
"#{history}\nCustomer: #{message}"
end
def schema
@schema ||= RubyLLM::Schema.create do
string :response
boolean :needs_escalation
string :escalation_reason, nullable: true
array :suggested_actions, of: :string
end
end
endPass agent classes directly in the tools list. The orchestrating agent's LLM decides when and how to invoke sub-agents:
class ResearchAgent < ApplicationAgent
description "Researches a topic and returns key findings"
model "gpt-4o"
param :query, required: true, desc: "Topic to research"
user "Research the following topic thoroughly: {query}"
end
class FactCheckerAgent < ApplicationAgent
description "Verifies claims against reliable sources"
model "gpt-4o"
param :claim, required: true, desc: "Claim to verify"
user "Verify this claim: {claim}"
end
class ArticleWriterAgent < ApplicationAgent
description "Writes well-researched articles using specialist agents"
model "gpt-4o"
# The LLM can call ResearchAgent and FactCheckerAgent as tools
tools [ResearchAgent, FactCheckerAgent]
param :topic, required: true
system "You are a journalist. Research the topic, verify key claims, then write a thorough article."
user "Write an article about: {topic}"
end
# The LLM orchestrates: researches, fact-checks, then writes
result = ArticleWriterAgent.call(topic: "advances in quantum computing")The execution hierarchy is automatically tracked — you can query child executions:
execution = RubyLLM::Agents::Execution.last
children = RubyLLM::Agents::Execution.where(parent_execution_id: execution.id)class BillingAgent < ApplicationAgent
description "Handles billing questions: invoices, charges, refunds, payment methods"
model "gpt-4o-mini"
param :question, required: true, desc: "Customer's billing question"
system "You are a billing specialist. Be precise about amounts and dates."
user "{question}"
end
class TechSupportAgent < ApplicationAgent
description "Handles technical issues: bugs, errors, how-to questions"
model "gpt-4o-mini"
param :issue, required: true, desc: "Technical issue description"
system "You are a technical support specialist."
user "{issue}"
end
class SupportOrchestratorAgent < ApplicationAgent
model "gpt-4o"
tools [BillingAgent, TechSupportAgent]
param :message, required: true
system "You are the front-line support agent. Route questions to the appropriate specialist."
user "{message}"
end
result = SupportOrchestratorAgent.call(message: "I was charged twice for my subscription")Chain agents sequentially, passing results between them:
# Step 1: Research
class ResearchAgent < ApplicationAgent
model "gpt-4o"
param :topic, required: true
user "Research key points about: {topic}"
def schema
@schema ||= RubyLLM::Schema.create do
array :key_points, of: :string
array :sources, of: :string
end
end
end
# Step 2: Outline
class OutlineAgent < ApplicationAgent
model "gpt-4o-mini"
param :key_points, required: true
user do
"Create an outline from: #{key_points.join(', ')}"
end
def schema
@schema ||= RubyLLM::Schema.create do
array :sections, of: :object do
string :title
array :points, of: :string
end
end
end
end
# Step 3: Write
class WriterAgent < ApplicationAgent
model "gpt-4o"
param :outline, required: true
user do
"Write content following this outline: #{outline.to_json}"
end
end
# Sequential composition
research = ResearchAgent.call(topic: "AI in Healthcare")
outline = OutlineAgent.call(key_points: research.content[:key_points])
article = WriterAgent.call(outline: outline.content[:sections])class IntentClassifier < ApplicationAgent
model "gpt-4o-mini"
temperature 0.0
param :message, required: true
user "Classify intent: {message}"
def schema
@schema ||= RubyLLM::Schema.create do
string :intent, enum: %w[support sales billing general]
number :confidence
end
end
end
# Route to the appropriate agent based on classification
intent = IntentClassifier.call(message: "How do I reset my password?")
agent_class = case intent.content[:intent]
when "support" then TechnicalSupportAgent
when "sales" then SalesAgent
when "billing" then BillingAgent
else GeneralHelpAgent
end
result = agent_class.call(message: "How do I reset my password?")Use assistant to force JSON output by pre-filling the opening brace:
class DataExtractor < ApplicationAgent
model "claude-sonnet-4-20250514"
temperature 0.0
system "You extract structured data from unstructured text. Always respond with valid JSON."
user "{text}"
assistant "{"
returns do
array :people, of: :string, description: "Named people"
array :dates, of: :string, description: "Dates mentioned"
array :amounts, of: :string, description: "Monetary amounts"
end
end
result = DataExtractor.call(text: "John paid $500 on March 3rd to Jane.")
# => { people: ["John", "Jane"], dates: ["March 3rd"], amounts: ["$500"] }class CodeReviewAgent < ApplicationAgent
model "gpt-4o"
temperature 0.2
system "You are a senior code reviewer. Provide structured feedback."
user "Review this code:\n\n```\n{code}\n```"
assistant "## Code Review\n\n### Issues Found\n\n1."
endUse .ask for one-off queries without defining a user prompt on the class:
# Define a base agent with model and system prompt
class ResearchAgent < ApplicationAgent
model "gpt-4o"
system "You are a research assistant. Be concise and factual."
end
# Use .ask for ad-hoc questions
result = ResearchAgent.ask("What are the main causes of the 2008 financial crisis?")
puts result.content
# With parameters
result = ResearchAgent.ask("Compare {topic_a} and {topic_b}", topic_a: "REST", topic_b: "GraphQL")
# In a Rails console or script
SummaryAgent.ask("Summarize: #{Article.last.body}")RSpec.describe SearchIntentAgent do
describe ".call" do
it "extracts search intent" do
result = described_class.call(
query: "red dress under $50",
dry_run: true
)
expect(result[:dry_run]).to be true
expect(result[:agent]).to eq("SearchIntentAgent")
end
context "with mocked response" do
before do
allow_any_instance_of(RubyLLM::Chat).to receive(:ask)
.and_return(double(content: {
refined_query: "red dress",
filters: ["color:red"],
category_id: 42
}.to_json))
end
it "processes the response" do
result = described_class.call(query: "red dress")
expect(result[:refined_query]).to eq("red dress")
expect(result[:filters]).to include("color:red")
end
end
end
end# app/controllers/api/v1/search_controller.rb
class Api::V1::SearchController < ApplicationController
def search
result = SearchIntentAgent.call(
query: params[:q],
user_id: current_user.id
)
if result.success?
render json: {
data: result.content,
meta: {
model: result.chosen_model_id,
tokens: result.total_tokens,
cost: result.total_cost,
duration_ms: result.duration_ms
}
}
else
render json: {
error: result.error,
retryable: result.retryable?
}, status: :unprocessable_entity
end
rescue RubyLLM::Agents::BudgetExceededError
render json: { error: "Service limit reached" }, status: :service_unavailable
rescue RubyLLM::Agents::CircuitBreakerOpenError => e
response.headers["Retry-After"] = (e.remaining_ms / 1000).to_s
render json: { error: "Service temporarily unavailable" }, status: :service_unavailable
end
end# app/jobs/content_generation_job.rb
class ContentGenerationJob < ApplicationJob
queue_as :default
# Retry on transient errors
retry_on RubyLLM::Agents::CircuitBreakerOpenError, wait: :polynomially_longer, attempts: 3
# Don't retry budget errors
discard_on RubyLLM::Agents::BudgetExceededError
def perform(article_id)
article = Article.find(article_id)
result = ContentGeneratorAgent.call(
topic: article.topic,
tone: article.tone,
word_count: article.target_word_count
)
if result.success?
article.update!(
content: result.content[:content],
title: result.content[:title],
status: :completed,
generation_cost: result.total_cost
)
else
article.update!(
status: :failed,
error_message: result.error
)
end
end
end
# Enqueue the job
ContentGenerationJob.perform_later(article.id)# app/agents/streaming_chat_agent.rb
class StreamingChatAgent < ApplicationAgent
model "gpt-4o"
streaming true
param :message, required: true
param :channel, required: true
user "{message}"
def on_chunk(chunk)
channel.broadcast_chunk(chunk)
end
end
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_#{params[:room_id]}"
end
def receive(data)
StreamingChatAgent.call(
message: data["message"],
channel: self
)
end
def broadcast_chunk(chunk)
ActionCable.server.broadcast(
"chat_#{params[:room_id]}",
{ type: "chunk", content: chunk }
)
end
end# app/agents/tenant_aware_agent.rb
class TenantAwareAgent < ApplicationAgent
model "gpt-4o"
description "Processes queries with tenant isolation"
reliability do
retries max: 3, backoff: :exponential
fallback_models "gpt-4o-mini"
circuit_breaker errors: 5, within: 60, cooldown: 180
end
param :query, required: true
user "{query}"
def metadata
{
tenant_id: Current.tenant_id,
tenant_name: Current.tenant&.name,
user_id: Current.user&.id,
request_id: Current.request_id,
source: "web"
}
end
end
# Usage in controller
class QueriesController < ApplicationController
def create
result = TenantAwareAgent.call(query: params[:query])
respond_to do |format|
format.json { render json: result.content }
end
end
end# lib/tasks/agents.rake
namespace :agents do
desc "Generate content for pending articles"
task generate_content: :environment do
Article.pending.find_each do |article|
print "Processing article #{article.id}..."
result = ContentGeneratorAgent.call(
topic: article.topic,
dry_run: ENV["DRY_RUN"].present?
)
if result.success?
article.update!(content: result.content[:content], status: :completed)
puts " done (#{result.total_tokens} tokens, $#{result.total_cost})"
else
puts " failed: #{result.error}"
end
rescue RubyLLM::Agents::BudgetExceededError
puts "\nBudget exceeded, stopping."
break
end
end
desc "Show agent statistics"
task stats: :environment do
puts "Agent Statistics (Last 7 Days)"
puts "=" * 50
RubyLLM::Agents::Execution
.last_7_days
.group(:agent_type)
.select(
:agent_type,
"COUNT(*) as total",
"SUM(total_cost) as cost",
"AVG(duration_ms) as avg_duration"
)
.each do |stat|
puts "#{stat.agent_type}:"
puts " Executions: #{stat.total}"
puts " Total Cost: $#{stat.cost.round(4)}"
puts " Avg Duration: #{stat.avg_duration.round}ms"
puts
end
end
end# app/services/resilient_agent_service.rb
class ResilientAgentService
def initialize(agent_class)
@agent_class = agent_class
end
def call(**params)
result = @agent_class.call(**params)
if result.success?
Success.new(result.content)
else
handle_failure(result)
end
rescue RubyLLM::Agents::BudgetExceededError => e
Failure.new(:budget_exceeded, e.message)
rescue RubyLLM::Agents::CircuitBreakerOpenError => e
Failure.new(:circuit_open, e.message, retry_after: e.remaining_ms)
rescue RubyLLM::Agents::TimeoutError => e
Failure.new(:timeout, e.message)
end
private
def handle_failure(result)
if result.retryable?
Failure.new(:retryable, result.error)
else
Failure.new(:permanent, result.error)
end
end
Success = Struct.new(:data) do
def success? = true
def failure? = false
end
Failure = Struct.new(:type, :message, :retry_after) do
def success? = false
def failure? = true
end
end
# Usage
service = ResilientAgentService.new(SearchAgent)
result = service.call(query: "test")
case result
in Success(data:)
render json: data
in Failure(type: :budget_exceeded)
render json: { error: "Limit reached" }, status: 503
in Failure(type: :circuit_open, retry_after:)
response.headers["Retry-After"] = (retry_after / 1000).to_s
render json: { error: "Try again later" }, status: 503
in Failure(type: :retryable, message:)
AgentRetryJob.perform_later(params)
render json: { status: "queued" }, status: 202
in Failure(type: :permanent, message:)
render json: { error: message }, status: 422
end# app/services/document_analyzer.rb
class DocumentAnalyzer
def initialize(document)
@document = document
end
def analyze
sentiment = SentimentAgent.call(text: @document.content)
entities = EntityExtractorAgent.call(text: @document.content)
summary = SummarizerAgent.call(text: @document.content)
{
sentiment: sentiment.content,
entities: entities.content,
summary: summary.content,
total_cost: [sentiment, entities, summary].sum(&:total_cost)
}
end
end- Agent DSL - Configuration reference
- Prompts and Schemas - Structuring outputs
- Error Handling - Error types and recovery
- Testing Agents - Testing patterns
- Multi-Tenancy - Multi-tenant configuration