-
Notifications
You must be signed in to change notification settings - Fork 6
Prompts and Schemas
Learn how to craft effective prompts and structure LLM outputs with schemas.
RubyLLM::Agents uses a three-role DSL for building conversations:
| Role | DSL | Purpose | When Sent |
|---|---|---|---|
| System | system |
Sets the agent's role and behavior | First message in conversation |
| User | user |
The specific request to process | Each call |
| Assistant | assistant |
Pre-fill / steer the response | After user message, before generation |
| (context) | messages |
Conversation history for context | Before user message |
Note: In versions prior to v2.2.0, the
promptDSL was used instead ofuser. Thepromptmethod still works as a deprecated alias but will emit a warning. All new code should useuser.
For multi-turn conversations, see Conversation History.
system "You are a helpful assistant."system do
<<~S
You are a professional content analyst specializing in e-commerce.
Your responsibilities:
- Analyze product descriptions for quality
- Identify missing information
- Suggest improvements
- Rate content on a scale of 1-10
Guidelines:
- Be specific and actionable
- Focus on SEO best practices
- Consider mobile readability
- Maintain brand voice consistency
S
endUse the block form when you need conditionals or method calls:
system do
<<~S
You are an assistant for #{company_name}.
Current context:
- User role: #{user_role}
- Department: #{department}
- Language: #{locale}
#{additional_instructions if admin_mode}
S
endUse {placeholder} syntax to auto-register params:
user "{query}"user do
<<~S
## Task
Analyze the following product description.
## Input
{product_description}
## Requirements
- Identify at least 3 strengths
- Identify at least 3 areas for improvement
- Provide an overall quality score
S
enduser do
<<~S
Classify the following customer message.
## Examples
Message: "When will my order arrive?"
Classification: shipping_inquiry
Message: "I want to return this item"
Classification: return_request
Message: "The product is broken"
Classification: complaint
## Your Task
Message: "{customer_message}"
Classification:
S
endThe assistant DSL pre-fills the assistant turn so the LLM continues from that text instead of generating from scratch. This is the third role in the three-role DSL.
The most common use case is forcing the model to begin its response with {, which reliably produces valid JSON:
class EntityExtractor < ApplicationAgent
model "claude-sonnet-4-20250514"
system "You extract named entities from text and return them as JSON."
user "{text}"
assistant "{"
returns do
array :people, of: :string
array :organizations, of: :string
array :locations, of: :string
end
endYou can use longer prefills to steer the structure of the response:
class StepByStepSolver < ApplicationAgent
model "gpt-4o"
system "You are a math tutor."
user "Solve: {problem}"
assistant "Let me solve this step by step.\n\nStep 1:"
endUse the block form when the prefill depends on runtime data:
class TranslationAgent < ApplicationAgent
model "gpt-4o"
system "You are a translator."
user "Translate to {language}: {text}"
assistant { "#{language.capitalize} translation:" }
end.ask is a one-shot convenience method for sending a user message without pre-defining a user prompt on the class. It is ideal for ad-hoc queries, REPL exploration, and scripts.
result = MyAgent.ask("Summarize this article: #{text}")
result.contentresult = MyAgent.ask("Translate {text} to {language}", text: article, language: "French")result = MyAgent.ask { "The time is #{Time.current}. What day is it?" }| Method | Best for |
|---|---|
.call |
Production agents with a user prompt defined on the class |
.ask |
Ad-hoc queries, scripts, REPL sessions, one-off tasks |
# .call -- the user prompt is defined on the class
class SummaryAgent < ApplicationAgent
model "gpt-4o"
user "Summarize: {text}"
end
SummaryAgent.call(text: article)
# .ask -- no user prompt needed on the class
SummaryAgent.ask("What is the capital of France?")Schemas ensure LLMs return structured, typed data.
def schema
@schema ||= RubyLLM::Schema.create do
string :result, description: "The analysis result"
end
enddef schema
@schema ||= RubyLLM::Schema.create do
# String
string :name, description: "User's name"
# Number (float)
number :score, description: "Score from 0 to 1"
# Integer
integer :count, description: "Number of items"
# Boolean
boolean :is_valid, description: "Whether input is valid"
# Array
array :tags, of: :string, description: "List of tags"
# Enum (restricted values)
string :status,
enum: ["pending", "approved", "rejected"],
description: "Current status"
end
enddef schema
@schema ||= RubyLLM::Schema.create do
string :category, description: "Primary category"
string :subcategory, description: "Subcategory", nullable: true
integer :priority, description: "Priority level", nullable: true
end
enddef schema
@schema ||= RubyLLM::Schema.create do
string :summary, description: "Brief summary"
object :metadata do
string :author
string :created_at
integer :word_count
end
array :sections, of: :object do
string :title
string :content
integer :order
end
end
enddef schema
@schema ||= RubyLLM::Schema.create do
string :refined_query,
description: "Cleaned and normalized search query"
array :filters, of: :object do
string :field, description: "Field name to filter"
string :operator,
enum: ["eq", "neq", "gt", "lt", "contains"],
description: "Filter operator"
string :value, description: "Filter value"
end
object :sorting do
string :field, description: "Field to sort by"
string :direction,
enum: ["asc", "desc"],
description: "Sort direction"
end
integer :category_id,
description: "Detected category ID",
nullable: true
number :confidence,
description: "Confidence score from 0 to 1"
array :suggestions,
of: :string,
description: "Alternative search suggestions"
end
endThe schema automatically parses the response:
result = MyAgent.call(query: "test")
result.content # Already parsed and typedOverride process_response for additional processing:
def process_response(response)
result = super(response)
# Transform data
result[:tags] = result[:tags].map(&:downcase).uniq
# Add computed fields
result[:word_count] = result[:summary].split.size
result[:processed_at] = Time.current
# Validate
result[:score] = result[:score].clamp(0, 1)
result
enddef process_response(response)
result = super(response)
# Handle missing fields gracefully
result[:category] ||= "uncategorized"
result[:tags] ||= []
# Type coercion
result[:count] = result[:count].to_i
result
rescue => e
Rails.logger.error("Response processing failed: #{e}")
{ error: "Processing failed", raw: response }
end# Less effective
user "Summarize this: {text}"
# More effective
user do
<<~S
Create a 2-3 sentence summary of the following text.
Focus on the main argument and key supporting points.
Use simple language suitable for a general audience.
Text:
{text}
S
endsystem do
<<~S
You are a customer service assistant for TechStore, an electronics retailer.
Key information:
- Return policy: 30 days for unopened items
- Shipping: Free over $50, otherwise $5.99
- Support hours: 9 AM - 9 PM EST
Always be helpful, professional, and accurate.
S
enduser do
<<~S
Analyze this product review and extract:
1. Overall sentiment (positive/negative/neutral)
2. Key points mentioned
3. Any issues reported
4. Purchase recommendation
Review:
{review_text}
S
enduser do
<<~S
Parse the following address into components.
If any component is missing or unclear:
- Leave it as null
- Do not guess or infer values
- Note the issue in the "parsing_notes" field
Address:
{raw_address}
S
end- Agent DSL - Full DSL reference
- Parameters - Input parameters
- Conversation History - Multi-turn conversations
- Result Object - Working with responses
- Examples - Real-world patterns