-
Notifications
You must be signed in to change notification settings - Fork 4
First Agent
A step-by-step tutorial for building your first AI agent with RubyLLM::Agents.
We'll create a SearchIntentAgent that extracts search intent from natural language queries. Given "red summer dress under $50", it will output structured data like:
{
refined_query: "red summer dress",
filters: ["color:red", "season:summer", "price:<50"],
category_id: 42
}rails generate ruby_llm_agents:agent SearchIntent query:required limit:10This creates app/agents/search_intent_agent.rb:
class SearchIntentAgent < ApplicationAgent
model "gemini-2.0-flash"
temperature 0.0
param :limit, default: 10
system "You are a SearchIntentAgent."
user "{query}"
endThe system prompt sets the agent's behavior. Use the class-level system DSL:
system do
<<~S
You are a search assistant that parses user queries and extracts
structured search filters. Analyze natural language and identify:
1. The core search query (cleaned and refined)
2. Any filters (color, size, price range, category, etc.)
3. The most likely product category
Be precise and extract only what's explicitly or strongly implied
in the query.
S
endThe user prompt is what you send with each request. Use the class-level user DSL with {placeholder} syntax -- parameters referenced via {name} are auto-registered as required:
user "Extract search intent from this query:\n\n\"{query}\"\n\nReturn up to {limit} relevant filters."Note:
promptstill works as a deprecated alias foruser, but new code should useuser.
Schemas ensure the LLM returns valid, typed data:
def schema
@schema ||= RubyLLM::Schema.create do
string :refined_query,
description: "The cleaned and refined search query"
array :filters,
of: :string,
description: "Extracted filters in format 'type:value'"
integer :category_id,
description: "Detected product category ID",
nullable: true
number :confidence,
description: "Confidence score from 0 to 1"
end
endHere's the complete agent:
class SearchIntentAgent < ApplicationAgent
model "gpt-4o"
temperature 0.0
cache 30.minutes
param :limit, default: 10
system do
<<~S
You are a search assistant that parses user queries and extracts
structured search filters. Analyze natural language and identify:
1. The core search query (cleaned and refined)
2. Any filters (color, size, price range, category, etc.)
3. The most likely product category
Be precise and extract only what's explicitly or strongly implied.
S
end
user "Extract search intent from: \"{query}\"\nReturn up to {limit} filters."
assistant "{" # Forces JSON output by prefilling the assistant response
returns do
string :refined_query, description: "Cleaned search query"
array :filters, of: :string, description: "Filters as 'type:value'"
integer :category_id, description: "Category ID", nullable: true
number :confidence, description: "Confidence 0-1"
end
end# Basic call
result = SearchIntentAgent.call(query: "red summer dress under $50")
# Access structured response
result.content
# => {
# refined_query: "red summer dress",
# filters: ["color:red", "season:summer", "price:<50"],
# category_id: 42,
# confidence: 0.95
# }
# Access individual fields
result[:refined_query] # => "red summer dress"
result[:filters] # => ["color:red", "season:summer", "price:<50"]Every call includes rich metadata:
result = SearchIntentAgent.call(query: "blue jeans")
# Token usage
result.input_tokens # => 85
result.output_tokens # => 42
result.total_tokens # => 127
# Costs
result.input_cost # => 0.000085
result.output_cost # => 0.000084
result.total_cost # => 0.000169
# Timing
result.duration_ms # => 650
result.started_at # => 2024-01-15 10:30:00 UTC
result.completed_at # => 2024-01-15 10:30:00 UTC
# Model info
result.model_id # => "gpt-4o"
result.finish_reason # => "stop"Test without making API calls:
result = SearchIntentAgent.call(query: "test", dry_run: true)
# => {
# dry_run: true,
# agent: "SearchIntentAgent",
# model: "gpt-4o",
# temperature: 0.0,
# system_prompt: "You are a search assistant...",
# user_prompt: "Extract search intent from: \"test\"...",
# schema: "RubyLLM::Schema"
# }Visit /agents to see:
- Overview - Today's stats and trends
- Executions - All SearchIntentAgent calls
- Details - Click any execution for full details
class SearchController < ApplicationController
def search
result = SearchIntentAgent.call(query: params[:q])
@products = Product.where(category_id: result[:category_id])
.search(result[:refined_query])
.limit(20)
end
endclass SearchController < ApplicationController
def search
result = SearchIntentAgent.call(query: params[:q])
if result.success?
@products = Product.search(result[:refined_query])
else
@products = Product.search(params[:q]) # Fallback to raw query
Rails.logger.error("Agent failed: #{result.error}")
end
end
endWhile .call is stateless (one-shot), .ask maintains conversation history across turns. This is ideal for interactive or exploratory use cases:
agent = SearchIntentAgent.new(limit: 10)
# First turn
result = agent.ask("red summer dress under $50")
result.content
# => { refined_query: "red summer dress", filters: ["color:red", ...], ... }
# Follow-up turn -- the agent remembers the previous exchange
result = agent.ask("actually, make that under $30 and size medium")
result.content
# => { refined_query: "red summer dress", filters: ["color:red", "season:summer", "price:<30", "size:medium"], ... }.ask is also useful in controllers that back a chat-style UI:
class ChatSearchController < ApplicationController
def create
agent = SearchIntentAgent.new(limit: 10)
# Replay prior turns from the session
session[:search_history]&.each do |turn|
agent.ask(turn)
end
result = agent.ask(params[:message])
session[:search_history] ||= []
session[:search_history] << params[:message]
render json: result.content
end
end- Agent DSL - All configuration options
- Prompts and Schemas - Advanced prompt techniques
- Reliability - Add retries and fallbacks
- Caching - Cache expensive calls
- Examples - More real-world patterns