Class: RubyLLM::Agents::BaseAgent

Inherits:
Object
  • Object
show all
Extended by:
DSL::Base, DSL::Caching, DSL::Knowledge, DSL::Queryable, DSL::Reliability
Includes:
CacheHelper, DSL::Knowledge::InstanceMethods
Defined in:
lib/ruby_llm/agents/base_agent.rb

Overview

Base class for all agents using the middleware pipeline architecture.

BaseAgent provides a unified foundation for building LLM-powered agents with configurable middleware for caching, reliability, instrumentation, budgeting, and multi-tenancy.

Examples:

Creating an agent

class SearchAgent < RubyLLM::Agents::BaseAgent
  model "gpt-4o"
  description "Searches for relevant documents"
  timeout 30

  cache_for 1.hour

  reliability do
    retries max: 3, backoff: :exponential
    fallback_models "gpt-4o-mini"
  end

  param :query, required: true
  param :limit, default: 10

  def system_prompt
    "You are a search assistant..."
  end

  def user_prompt
    "Search for: #{query}"
  end
end

Calling an agent

SearchAgent.call(query: "red dress")
SearchAgent.call(query: "red dress", dry_run: true)
SearchAgent.call(query: "red dress", skip_cache: true)

Direct Known Subclasses

Base, Embedder, ImageGenerator, Speaker, Transcriber

Constant Summary

Constants included from DSL::Base

DSL::Base::PLACEHOLDER_PATTERN

Constants included from DSL::Caching

DSL::Caching::DEFAULT_CACHE_TTL

Constants included from CacheHelper

CacheHelper::NAMESPACE

Thinking DSL collapse

Custom Middleware DSL collapse

Parameter DSL collapse

Streaming DSL collapse

Tools DSL collapse

Temperature DSL collapse

Thinking DSL collapse

Template Methods (override in subclasses) collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from DSL::Base

active_overrides, assistant, assistant_config, cache_prompts, clear_override_cache!, description, overridable?, overridable_fields, prompt, returns, system, system_config, timeout, user, user_config

Methods included from DSL::Reliability

circuit_breaker, circuit_breaker_config, fallback_models, fallback_provider, fallback_providers, non_fallback_errors, on_failure, reliability, reliability_config, reliability_configured?, retries, retries_config, retryable_patterns, total_timeout

Methods included from DSL::Caching

cache, cache_enabled?, cache_for, cache_key_excludes, cache_key_includes, cache_ttl, caching_config

Methods included from DSL::Queryable

cost_by_model, executions, failures, last_run, stats, total_spent, with_params

Methods included from DSL::Knowledge

knowledge_entries, knowledge_path, knows

Methods included from CacheHelper

#cache_delete, #cache_exist?, #cache_increment, #cache_key, #cache_read, #cache_store, #cache_write

Methods included from DSL::Knowledge::InstanceMethods

#compiled_knowledge

Constructor Details

#initialize(model: self.class.model, temperature: self.class.temperature, **options) ⇒ BaseAgent

Creates a new agent instance

Parameters:

  • model (String) (defaults to: self.class.model)

    Override the class-level model setting

  • temperature (Float) (defaults to: self.class.temperature)

    Override the class-level temperature

  • options (Hash)

    Agent parameters defined via the param DSL



365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
# File 'lib/ruby_llm/agents/base_agent.rb', line 365

def initialize(model: self.class.model, temperature: self.class.temperature, **options)
  # Merge tracker defaults (shared options like tenant) — explicit opts win
  tracker = Thread.current[:ruby_llm_agents_tracker]
  if tracker
    options = tracker.defaults.merge(options)
    @_track_request_id = tracker.request_id
    @_track_tags = tracker.tags
  end

  @ask_message = options.delete(:_ask_message)
  @parent_execution_id = options.delete(:_parent_execution_id)
  @root_execution_id = options.delete(:_root_execution_id)
  @model = model
  @temperature = temperature
  @options = options
  @tracked_tool_calls = []
  @pending_tool_call = nil
  validate_required_params! unless @ask_message
end

Instance Attribute Details

#clientRubyLLM::Chat (readonly)

Returns The configured RubyLLM client.

Returns:

  • (RubyLLM::Chat)

    The configured RubyLLM client



358
# File 'lib/ruby_llm/agents/base_agent.rb', line 358

attr_reader :model, :temperature, :client, :tracked_tool_calls

#modelString (readonly)

Returns The LLM model being used.

Returns:

  • (String)

    The LLM model being used



358
359
360
# File 'lib/ruby_llm/agents/base_agent.rb', line 358

def model
  @model
end

#temperatureFloat (readonly)

Returns The temperature setting.

Returns:

  • (Float)

    The temperature setting



358
# File 'lib/ruby_llm/agents/base_agent.rb', line 358

attr_reader :model, :temperature, :client, :tracked_tool_calls

#tracked_tool_callsObject (readonly)

Returns the value of attribute tracked_tool_calls.



358
# File 'lib/ruby_llm/agents/base_agent.rb', line 358

attr_reader :model, :temperature, :client, :tracked_tool_calls

Class Method Details

.agent_middlewareArray<Hash>

Returns custom middleware registered on this agent (including inherited)

Returns:

  • (Array<Hash>)

    Middleware entries with :klass, :before, :after keys



197
198
199
# File 'lib/ruby_llm/agents/base_agent.rb', line 197

def agent_middleware
  @agent_middleware || (superclass.respond_to?(:agent_middleware) ? superclass.agent_middleware : []) || []
end

.agent_typeSymbol

Returns the agent type for this class

Used by middleware to determine which tracking/budget config to use. Subclasses should override this method.

Returns:

  • (Symbol)

    The agent type (:conversation, :embedding, :image, etc.)



121
122
123
# File 'lib/ruby_llm/agents/base_agent.rb', line 121

def agent_type
  :conversation
end

.aliases(*names) ⇒ Array<String>

Declares previous class names for this agent

When an agent is renamed, old execution records still reference the previous class name. Declaring aliases allows scopes, analytics, and budget checks to automatically include records from all previous names.

Examples:

class SupportBot < ApplicationAgent
  aliases "CustomerSupportAgent", "HelpDeskAgent"
end

Parameters:

  • names (Array<String>)

    Previous class names

Returns:

  • (Array<String>)

    All declared aliases



138
139
140
141
142
143
# File 'lib/ruby_llm/agents/base_agent.rb', line 138

def aliases(*names)
  if names.any?
    @agent_aliases = names.map(&:to_s)
  end
  @agent_aliases || []
end

.all_agent_namesArray<String>

Returns all known names for this agent (current + aliases)

Returns:

  • (Array<String>)

    Current name followed by any aliases



148
149
150
# File 'lib/ruby_llm/agents/base_agent.rb', line 148

def all_agent_names
  [name, *aliases].compact.uniq
end

.ask(message, with: nil, **kwargs) {|chunk| ... } ⇒ Result

Executes the agent with a freeform message as the user prompt

Designed for conversational agents that define a persona (system + optional assistant prefill) but accept freeform input at runtime. Also works on template agents as an escape hatch to bypass the user template.

Examples:

Basic usage

RubyExpert.ask("What is metaprogramming?")

With streaming

RubyExpert.ask("Explain closures") { |chunk| print chunk.content }

With attachments

RubyExpert.ask("What's in this image?", with: "photo.jpg")

Parameters:

  • message (String)

    The user message to send

  • with (String, Array<String>, nil) (defaults to: nil)

    Attachments (files, URLs)

  • kwargs (Hash)

    Additional options (model:, temperature:, etc.)

Yields:

  • (chunk)

    Yields chunks when streaming

Returns:

  • (Result)

    The processed response



104
105
106
107
108
109
110
111
112
113
# File 'lib/ruby_llm/agents/base_agent.rb', line 104

def ask(message, with: nil, **kwargs, &block)
  opts = kwargs.merge(_ask_message: message)
  opts[:with] = with if with

  if block
    stream(**opts, &block)
  else
    call(**opts)
  end
end

.call(**kwargs) {|chunk| ... } ⇒ Object

Factory method to instantiate and execute an agent

Parameters:

  • kwargs (Hash)

    Named parameters for the agent

Options Hash (**kwargs):

  • :dry_run (Boolean)

    Return prompt info without API call

  • :skip_cache (Boolean)

    Bypass caching even if enabled

  • :tenant (Hash, Object)

    Tenant context for multi-tenancy

  • :with (String, Array<String>)

    Attachments (files, URLs)

Yields:

  • (chunk)

    Yields chunks when streaming is enabled

Returns:

  • (Object)

    The processed response from the agent



64
65
66
# File 'lib/ruby_llm/agents/base_agent.rb', line 64

def call(**kwargs, &block)
  new(**kwargs).call(&block)
end

.config_summaryHash

Returns a summary of the agent’s DSL configuration

Useful for debugging in the Rails console to see how an agent is configured without instantiating it.

Examples:

MyAgent.config_summary

Returns:

  • (Hash)

    Agent configuration summary



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/ruby_llm/agents/base_agent.rb', line 160

def config_summary
  {
    agent_type: agent_type,
    model: model,
    temperature: temperature,
    timeout: timeout,
    streaming: streaming,
    system_prompt: system_config,
    user_prompt: user_config,
    assistant_prompt: assistant_config,
    description: description,
    schema: schema&.respond_to?(:name) ? schema.name : schema&.class&.name,
    tools: tools.map { |t| t.respond_to?(:name) ? t.name : t.to_s },
    parameters: params.transform_values { |v| v.slice(:type, :required, :default, :desc) },
    thinking: thinking_config,
    cache_prompts: cache_prompts || nil,
    caching: caching_config,
    reliability: reliability_configured? ? reliability_config : nil
  }.compact
end

.param(name, required: false, default: nil, type: nil, desc: nil, description: nil) ⇒ void

This method returns an undefined value.

Defines a parameter for the agent

Creates an accessor method for the parameter that retrieves values from the options hash, falling back to the default value.

Parameters:

  • name (Symbol)

    The parameter name

  • required (Boolean) (defaults to: false)

    Whether the parameter is required

  • default (Object, nil) (defaults to: nil)

    Default value if not provided

  • type (Class, nil) (defaults to: nil)

    Optional type for validation



215
216
217
218
219
220
221
# File 'lib/ruby_llm/agents/base_agent.rb', line 215

def param(name, required: false, default: nil, type: nil, desc: nil, description: nil)
  @params ||= {}
  @params[name] = {required: required, default: default, type: type, desc: desc || description}
  define_method(name) do
    @options[name] || @options[name.to_s] || self.class.params.dig(name, :default)
  end
end

.paramsHash{Symbol => Hash}

Returns all defined parameters including inherited ones

Returns:

  • (Hash{Symbol => Hash})

    Parameter definitions



226
227
228
229
# File 'lib/ruby_llm/agents/base_agent.rb', line 226

def params
  parent = superclass.respond_to?(:params) ? superclass.params : {}
  parent.merge(@params || {})
end

.stream(**kwargs) {|chunk| ... } ⇒ Result

Streams agent execution, yielding chunks as they arrive

Parameters:

  • kwargs (Hash)

    Agent parameters

Yields:

  • (chunk)

    Yields each chunk as it arrives

Returns:

  • (Result)

    The final result after streaming completes

Raises:

  • (ArgumentError)

    If no block is provided



74
75
76
77
78
79
80
# File 'lib/ruby_llm/agents/base_agent.rb', line 74

def stream(**kwargs, &block)
  raise ArgumentError, "Block required for streaming" unless block_given?

  instance = new(**kwargs)
  instance.instance_variable_set(:@force_streaming, true)
  instance.call(&block)
end

.streaming(value = nil, overridable: nil) ⇒ Boolean

Enables or returns streaming mode for this agent

Parameters:

  • value (Boolean, nil) (defaults to: nil)

    Whether to enable streaming

  • overridable (Boolean, nil) (defaults to: nil)

    When true, this field can be changed from the dashboard

Returns:

  • (Boolean)

    The current streaming setting



240
241
242
243
244
245
246
247
248
249
250
# File 'lib/ruby_llm/agents/base_agent.rb', line 240

def streaming(value = nil, overridable: nil)
  @streaming = value unless value.nil?
  register_overridable(:streaming) if overridable
  base = if @streaming.nil?
    superclass.respond_to?(:streaming) ? superclass.streaming : default_streaming
  else
    @streaming
  end

  apply_override(:streaming, base)
end

.temperature(value = nil, overridable: nil) ⇒ Float

Sets or returns the temperature for LLM responses

Parameters:

  • value (Float, nil) (defaults to: nil)

    Temperature value (0.0-2.0)

  • overridable (Boolean, nil) (defaults to: nil)

    When true, this field can be changed from the dashboard

Returns:

  • (Float)

    The current temperature setting



292
293
294
295
296
297
298
# File 'lib/ruby_llm/agents/base_agent.rb', line 292

def temperature(value = nil, overridable: nil)
  @temperature = value if value
  register_overridable(:temperature) if overridable
  base = @temperature || (superclass.respond_to?(:temperature) ? superclass.temperature : default_temperature)

  apply_override(:temperature, base)
end

.thinking(effort: nil, budget: nil) ⇒ Hash?

Configures extended thinking/reasoning for this agent

Parameters:

  • effort (Symbol, nil) (defaults to: nil)

    Thinking depth (:none, :low, :medium, :high)

  • budget (Integer, nil) (defaults to: nil)

    Token budget for thinking

Returns:

  • (Hash, nil)

    The current thinking configuration



309
310
311
312
313
314
315
316
# File 'lib/ruby_llm/agents/base_agent.rb', line 309

def thinking(effort: nil, budget: nil)
  if effort || budget
    @thinking_config = {}
    @thinking_config[:effort] = effort if effort
    @thinking_config[:budget] = budget if budget
  end
  thinking_config
end

.thinking_configHash?

Returns the thinking configuration

Falls back to global configuration default if not set at class level.

Returns:

  • (Hash, nil)

    The thinking configuration



323
324
325
326
327
328
329
330
331
# File 'lib/ruby_llm/agents/base_agent.rb', line 323

def thinking_config
  return @thinking_config if @thinking_config
  return superclass.thinking_config if superclass.respond_to?(:thinking_config) && superclass.thinking_config

  # Fall back to global configuration default
  RubyLLM::Agents.configuration.default_thinking
rescue
  nil
end

.tool_concurrency(*value) ⇒ Boolean, ...

Sets or returns how this agent runs multiple tool calls returned in a single LLM response.

Mirrors RubyLLM’s tool_concurrency: false runs them sequentially, true or :threads runs them in Ruby threads, and :fibers runs them in fibers (requires the async gem). When unset, the agent inherits its superclass value and ultimately the global RubyLLM tool_concurrency configuration.

Parameters:

  • value (Boolean, Symbol)

    Concurrency mode (omit to read)

Returns:

  • (Boolean, Symbol, nil)

    Configured mode, or nil when unset



276
277
278
279
280
281
# File 'lib/ruby_llm/agents/base_agent.rb', line 276

def tool_concurrency(*value)
  @tool_concurrency = value.first unless value.empty?
  return @tool_concurrency if instance_variable_defined?(:@tool_concurrency)

  superclass.respond_to?(:tool_concurrency) ? superclass.tool_concurrency : nil
end

.tools(*tool_classes) ⇒ Array<Class>

Sets or returns the tools available to this agent

Parameters:

  • tool_classes (Class, Array<Class>)

    Tool classes to make available

Returns:

  • (Array<Class>)

    The current tools



260
261
262
263
# File 'lib/ruby_llm/agents/base_agent.rb', line 260

def tools(*tool_classes)
  @tools = tool_classes.flatten if tool_classes.any?
  @tools || (superclass.respond_to?(:tools) ? superclass.tools : [])
end

.use_middleware(middleware_class, before: nil, after: nil) ⇒ void

This method returns an undefined value.

Registers a custom middleware for this agent class

Parameters:

  • middleware_class (Class)

    Must inherit from Pipeline::Middleware::Base

  • before (Class, nil) (defaults to: nil)

    Insert before this built-in middleware

  • after (Class, nil) (defaults to: nil)

    Insert after this built-in middleware



189
190
191
192
# File 'lib/ruby_llm/agents/base_agent.rb', line 189

def use_middleware(middleware_class, before: nil, after: nil)
  @agent_middleware ||= []
  @agent_middleware << {klass: middleware_class, before: before, after: after}
end

Instance Method Details

#agent_cache_keyString

Generates the cache key for this agent invocation

Cache keys are content-based, using a hash of the prompts and parameters. This automatically invalidates caches when prompts change.

Returns:

  • (String)

    Cache key in format “ruby_llm_agent/ClassName/hash”



485
486
487
# File 'lib/ruby_llm/agents/base_agent.rb', line 485

def agent_cache_key
  ["ruby_llm_agent", self.class.name, cache_key_hash].join("/")
end

#assistant_promptString?

Assistant prefill to prime the model’s response

If a class-level ‘assistant` DSL is defined, it will be used. Otherwise returns nil (no prefill).

Returns:

  • (String, nil)

    The assistant prefill, or nil for none



442
443
444
445
446
447
# File 'lib/ruby_llm/agents/base_agent.rb', line 442

def assistant_prompt
  config = self.class.assistant_config
  return resolve_prompt_from_config(config) if config

  nil
end

#cache_key_dataHash

Returns data to include in cache key generation

Returns:

  • (Hash)

    Data to hash for cache key



499
500
501
502
503
504
505
506
507
508
509
510
# File 'lib/ruby_llm/agents/base_agent.rb', line 499

def cache_key_data
  excludes = self.class.cache_key_excludes || %i[skip_cache dry_run with]
  base_data = @options.except(*excludes)

  # Include model and other relevant config
  base_data.merge(
    model: model,
    system_prompt: system_prompt,
    user_prompt: user_prompt,
    assistant_prompt: assistant_prompt
  )
end

#cache_key_hashString

Generates a hash of the cache key data

Returns:

  • (String)

    SHA256 hex digest of the cache key data



492
493
494
# File 'lib/ruby_llm/agents/base_agent.rb', line 492

def cache_key_hash
  Digest::SHA256.hexdigest(cache_key_data.to_json)
end

#call {|chunk| ... } ⇒ Object

Executes the agent through the middleware pipeline

Yields:

  • (chunk)

    Yields chunks when streaming is enabled

Returns:

  • (Object)

    The processed response



389
390
391
392
393
394
395
# File 'lib/ruby_llm/agents/base_agent.rb', line 389

def call(&block)
  return dry_run_response if @options[:dry_run]

  context = build_context(&block)
  result_context = Pipeline::Executor.execute(context)
  result_context.output
end

#messagesArray<Hash>

Conversation history for multi-turn conversations

Returns:

  • (Array<Hash>)

    Array of messages with :role and :content keys



462
463
464
# File 'lib/ruby_llm/agents/base_agent.rb', line 462

def messages
  []
end

#process_response(response) ⇒ Object

Post-processes the LLM response

Parameters:

  • response (RubyLLM::Message)

    The raw response from the LLM

Returns:

  • (Object)

    The processed result



470
471
472
473
474
475
# File 'lib/ruby_llm/agents/base_agent.rb', line 470

def process_response(response)
  content = response.content
  return content unless content.is_a?(Hash)

  content.deep_symbolize_keys
end

#resolved_thinkingHash?

Resolves thinking configuration

Public for testing and introspection.

Returns:

  • (Hash, nil)

    Thinking configuration



517
518
519
520
521
522
523
524
525
526
527
# File 'lib/ruby_llm/agents/base_agent.rb', line 517

def resolved_thinking
  # Check for :none effort which means disabled
  if @options.key?(:thinking)
    thinking_option = @options[:thinking]
    return nil if thinking_option == false
    return nil if thinking_option.is_a?(Hash) && thinking_option[:effort] == :none
    return thinking_option if thinking_option.is_a?(Hash)
  end

  self.class.thinking_config
end

#schemaRubyLLM::Schema?

Response schema for structured output

Delegates to the class-level schema DSL by default. Override in subclass instances to customize per-instance.

Returns:

  • (RubyLLM::Schema, nil)

    Schema definition, or nil for free-form



455
456
457
# File 'lib/ruby_llm/agents/base_agent.rb', line 455

def schema
  self.class.schema
end

#system_promptString?

System prompt for LLM instructions

If a class-level ‘system` DSL is defined, it will be used. Knowledge entries declared via `knows` are auto-appended.

Returns:

  • (String, nil)

    System instructions, or nil for none



424
425
426
427
428
429
430
431
432
433
434
# File 'lib/ruby_llm/agents/base_agent.rb', line 424

def system_prompt
  system_config = self.class.system_config
  base = system_config ? resolve_prompt_from_config(system_config) : nil

  knowledge = compiled_knowledge
  if knowledge.present?
    base ? "#{base}\n\n#{knowledge}" : knowledge
  else
    base
  end
end

#user_promptString

User prompt to send to the LLM

Resolution order:

  1. Subclass method override (standard Ruby dispatch — this method is never called)

  2. .ask(message) runtime message — bypasses template

  3. Class-level ‘user` / `prompt` template — interpolated with placeholders

  4. Inherited from superclass

  5. NotImplementedError

Returns:

  • (String)

    The user prompt

Raises:

  • (NotImplementedError)


409
410
411
412
413
414
415
416
# File 'lib/ruby_llm/agents/base_agent.rb', line 409

def user_prompt
  return @ask_message if @ask_message

  config = self.class.user_config
  return resolve_prompt_from_config(config) if config

  raise NotImplementedError, "#{self.class} must implement #user_prompt, use the `user` DSL, or call with .ask(message)"
end