Skip to content

Examples

adham90 edited this page Feb 20, 2026 · 7 revisions

Examples

Real-world agent patterns and use cases.

Search & Classification

Search Intent Extraction

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"], ... }

Email Classifier

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
end

Content Generation

Blog Post Generator

class 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
end

Product Description Writer

class 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
end

Data Extraction

Invoice Parser

class 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"
)

Resume Parser

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
end

Conversational Agents

Customer Support Bot

class 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
end

Multi-Agent Composition

Agent-as-Tool (LLM-Driven Orchestration)

Pass 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)

Customer Support Delegation

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")

Content Pipeline (Sequential)

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])

Intent Router

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?")

Assistant Prefill for JSON Output

Structured Data Extractor

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"] }

Code Review Agent with Prefill

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."
end

Quick Queries with .ask

Use .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}")

Testing Agents

RSpec Example

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

Rails Integration

Controller Integration

# 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

Background Job Pattern

# 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)

Streaming with Action Cable

# 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

Multi-Tenant Agent with Execution Metadata

# 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

Rake Task Usage

# 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

Error Handling and Recovery

# 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

Service Object Pattern

# 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

Related Pages

Clone this wiki locally