ruby_llm-agents 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +132 -1263
  3. data/app/controllers/concerns/ruby_llm/agents/filterable.rb +5 -1
  4. data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +2 -1
  5. data/app/controllers/ruby_llm/agents/agents_controller.rb +21 -2
  6. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +137 -9
  7. data/app/controllers/ruby_llm/agents/executions_controller.rb +83 -5
  8. data/app/models/ruby_llm/agents/execution/analytics.rb +103 -12
  9. data/app/models/ruby_llm/agents/execution/scopes.rb +25 -0
  10. data/app/models/ruby_llm/agents/execution/workflow.rb +299 -0
  11. data/app/models/ruby_llm/agents/execution.rb +28 -59
  12. data/app/models/ruby_llm/agents/tenant_budget.rb +165 -0
  13. data/app/services/ruby_llm/agents/agent_registry.rb +118 -7
  14. data/app/views/layouts/ruby_llm/agents/application.html.erb +430 -0
  15. data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +23 -0
  16. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +125 -0
  17. data/app/views/ruby_llm/agents/agents/index.html.erb +93 -0
  18. data/app/views/ruby_llm/agents/agents/show.html.erb +775 -0
  19. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +112 -0
  20. data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +75 -0
  21. data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_execution_item.html.erb +7 -4
  22. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +84 -0
  23. data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +115 -0
  24. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +49 -0
  25. data/app/views/ruby_llm/agents/dashboard/index.html.erb +155 -0
  26. data/app/views/{rubyllm → ruby_llm}/agents/executions/_execution.html.erb +1 -1
  27. data/app/views/{rubyllm → ruby_llm}/agents/executions/_filters.html.erb +39 -11
  28. data/app/views/{rubyllm → ruby_llm}/agents/executions/_list.html.erb +19 -9
  29. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +101 -0
  30. data/app/views/ruby_llm/agents/executions/index.html.erb +88 -0
  31. data/app/views/{rubyllm → ruby_llm}/agents/executions/show.html.erb +260 -200
  32. data/app/views/{rubyllm → ruby_llm}/agents/settings/show.html.erb +1 -1
  33. data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +48 -0
  34. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +251 -0
  35. data/app/views/{rubyllm → ruby_llm}/agents/shared/_filter_dropdown.html.erb +1 -1
  36. data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +27 -0
  37. data/app/views/{rubyllm → ruby_llm}/agents/shared/_select_dropdown.html.erb +1 -1
  38. data/app/views/{rubyllm → ruby_llm}/agents/shared/_status_badge.html.erb +1 -1
  39. data/app/views/{rubyllm → ruby_llm}/agents/shared/_status_dot.html.erb +1 -1
  40. data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +26 -0
  41. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +61 -0
  42. data/config/routes.rb +2 -0
  43. data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +97 -0
  44. data/lib/generators/ruby_llm_agents/templates/add_attempts_migration.rb.tt +3 -3
  45. data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +23 -0
  46. data/lib/generators/ruby_llm_agents/templates/add_tool_calls_migration.rb.tt +2 -2
  47. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +38 -0
  48. data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +45 -0
  49. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +17 -5
  50. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +13 -0
  51. data/lib/ruby_llm/agents/alert_manager.rb +20 -16
  52. data/lib/ruby_llm/agents/base/caching.rb +40 -0
  53. data/lib/ruby_llm/agents/base/cost_calculation.rb +105 -0
  54. data/lib/ruby_llm/agents/base/dsl.rb +261 -0
  55. data/lib/ruby_llm/agents/base/execution.rb +258 -0
  56. data/lib/ruby_llm/agents/base/reliability_execution.rb +136 -0
  57. data/lib/ruby_llm/agents/base/response_building.rb +86 -0
  58. data/lib/ruby_llm/agents/base/tool_tracking.rb +57 -0
  59. data/lib/ruby_llm/agents/base.rb +37 -801
  60. data/lib/ruby_llm/agents/budget_tracker.rb +250 -139
  61. data/lib/ruby_llm/agents/cache_helper.rb +98 -0
  62. data/lib/ruby_llm/agents/circuit_breaker.rb +48 -30
  63. data/lib/ruby_llm/agents/configuration.rb +40 -1
  64. data/lib/ruby_llm/agents/engine.rb +65 -1
  65. data/lib/ruby_llm/agents/inflections.rb +14 -0
  66. data/lib/ruby_llm/agents/instrumentation.rb +66 -0
  67. data/lib/ruby_llm/agents/reliability.rb +8 -2
  68. data/lib/ruby_llm/agents/version.rb +1 -1
  69. data/lib/ruby_llm/agents/workflow/instrumentation.rb +254 -0
  70. data/lib/ruby_llm/agents/workflow/parallel.rb +282 -0
  71. data/lib/ruby_llm/agents/workflow/pipeline.rb +306 -0
  72. data/lib/ruby_llm/agents/workflow/result.rb +390 -0
  73. data/lib/ruby_llm/agents/workflow/router.rb +429 -0
  74. data/lib/ruby_llm/agents/workflow.rb +232 -0
  75. data/lib/ruby_llm/agents.rb +1 -0
  76. metadata +57 -75
  77. data/app/channels/ruby_llm/agents/executions_channel.rb +0 -46
  78. data/app/javascript/ruby_llm/agents/controllers/filter_controller.js +0 -56
  79. data/app/javascript/ruby_llm/agents/controllers/index.js +0 -12
  80. data/app/javascript/ruby_llm/agents/controllers/refresh_controller.js +0 -83
  81. data/app/views/layouts/rubyllm/agents/application.html.erb +0 -626
  82. data/app/views/rubyllm/agents/agents/index.html.erb +0 -20
  83. data/app/views/rubyllm/agents/agents/show.html.erb +0 -772
  84. data/app/views/rubyllm/agents/dashboard/_budgets_bar.html.erb +0 -165
  85. data/app/views/rubyllm/agents/dashboard/_now_strip.html.erb +0 -10
  86. data/app/views/rubyllm/agents/dashboard/_now_strip_values.html.erb +0 -71
  87. data/app/views/rubyllm/agents/dashboard/index.html.erb +0 -197
  88. data/app/views/rubyllm/agents/executions/index.html.erb +0 -28
  89. data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +0 -18
  90. data/app/views/rubyllm/agents/shared/_executions_table.html.erb +0 -193
  91. /data/app/views/{rubyllm → ruby_llm}/agents/agents/_agent.html.erb +0 -0
  92. /data/app/views/{rubyllm → ruby_llm}/agents/agents/_version_comparison.html.erb +0 -0
  93. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_action_center.html.erb +0 -0
  94. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_alerts_feed.html.erb +0 -0
  95. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_breaker_strip.html.erb +0 -0
  96. /data/app/views/{rubyllm → ruby_llm}/agents/executions/dry_run.html.erb +0 -0
  97. /data/app/views/{rubyllm → ruby_llm}/agents/shared/_stat_card.html.erb +0 -0
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Migration to create the tenant_budgets table for multi-tenancy support
4
+ #
5
+ # This table stores per-tenant budget configuration, allowing different
6
+ # tenants to have their own budget limits and enforcement modes.
7
+ #
8
+ # Features:
9
+ # - Per-tenant daily and monthly budget limits
10
+ # - Per-agent budget limits within a tenant
11
+ # - Configurable enforcement mode (none, soft, hard)
12
+ # - Option to inherit global defaults for unset limits
13
+ #
14
+ # Run with: rails db:migrate
15
+ class CreateRubyLLMAgentsTenantBudgets < ActiveRecord::Migration<%= migration_version %>
16
+ def change
17
+ create_table :ruby_llm_agents_tenant_budgets do |t|
18
+ # Unique identifier for the tenant (e.g., organization ID, workspace ID)
19
+ t.string :tenant_id, null: false
20
+
21
+ # Global budget limits for this tenant
22
+ t.decimal :daily_limit, precision: 12, scale: 6
23
+ t.decimal :monthly_limit, precision: 12, scale: 6
24
+
25
+ # Per-agent budget limits (JSON hash)
26
+ # Format: { "AgentName" => limit_value }
27
+ t.json :per_agent_daily, null: false, default: {}
28
+ t.json :per_agent_monthly, null: false, default: {}
29
+
30
+ # Enforcement mode for this tenant: "none", "soft", or "hard"
31
+ # - none: no enforcement, only tracking
32
+ # - soft: log warnings when limits exceeded
33
+ # - hard: block execution when limits exceeded
34
+ t.string :enforcement, default: "soft"
35
+
36
+ # Whether to inherit from global config for unset limits
37
+ t.boolean :inherit_global_defaults, default: true
38
+
39
+ t.timestamps
40
+ end
41
+
42
+ # Ensure unique tenant IDs
43
+ add_index :ruby_llm_agents_tenant_budgets, :tenant_id, unique: true
44
+ end
45
+ end
@@ -54,10 +54,10 @@ class CreateRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_versi
54
54
  t.decimal :output_cost, precision: 12, scale: 6
55
55
  t.decimal :total_cost, precision: 12, scale: 6
56
56
 
57
- # Data (JSONB for PostgreSQL, JSON for others)
58
- t.jsonb :parameters, null: false, default: {}
59
- t.jsonb :response, default: {}
60
- t.jsonb :metadata, null: false, default: {}
57
+ # Data (JSON - works with PostgreSQL, MySQL, SQLite3)
58
+ t.json :parameters, null: false, default: {}
59
+ t.json :response, default: {}
60
+ t.json :metadata, null: false, default: {}
61
61
 
62
62
  # Error tracking
63
63
  t.string :error_class
@@ -68,9 +68,16 @@ class CreateRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_versi
68
68
  t.text :user_prompt
69
69
 
70
70
  # Tool calls tracking
71
- t.jsonb :tool_calls, null: false, default: []
71
+ t.json :tool_calls, null: false, default: []
72
72
  t.integer :tool_calls_count, null: false, default: 0
73
73
 
74
+ # Workflow orchestration
75
+ t.string :workflow_id
76
+ t.string :workflow_type
77
+ t.string :workflow_step
78
+ t.string :routed_to
79
+ t.json :classification_result
80
+
74
81
  t.timestamps
75
82
  end
76
83
 
@@ -96,6 +103,11 @@ class CreateRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_versi
96
103
  # Tool calls index
97
104
  add_index :ruby_llm_agents_executions, :tool_calls_count
98
105
 
106
+ # Workflow indexes
107
+ add_index :ruby_llm_agents_executions, :workflow_id
108
+ add_index :ruby_llm_agents_executions, :workflow_type
109
+ add_index :ruby_llm_agents_executions, [:workflow_id, :workflow_step]
110
+
99
111
  # Foreign keys for execution hierarchy
100
112
  add_foreign_key :ruby_llm_agents_executions, :ruby_llm_agents_executions,
101
113
  column: :parent_execution_id, on_delete: :nullify
@@ -120,6 +120,19 @@ module RubyLlmAgents
120
120
  )
121
121
  end
122
122
 
123
+ def create_add_workflow_migration
124
+ # Check if columns already exist
125
+ if column_exists?(:ruby_llm_agents_executions, :workflow_id)
126
+ say_status :skip, "workflow_id column already exists", :yellow
127
+ return
128
+ end
129
+
130
+ migration_template(
131
+ "add_workflow_migration.rb.tt",
132
+ File.join(db_migrate_path, "add_workflow_to_ruby_llm_agents_executions.rb")
133
+ )
134
+ end
135
+
123
136
  def show_post_upgrade_message
124
137
  say ""
125
138
  say "RubyLLM::Agents upgrade migration created!", :green
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "net/http"
3
+ require "faraday"
4
4
  require "json"
5
5
 
6
6
  module RubyLLM
@@ -177,30 +177,34 @@ module RubyLLM
177
177
  end
178
178
  end
179
179
 
180
- # Posts JSON to a URL
180
+ # Posts JSON to a URL using Faraday
181
181
  #
182
182
  # @param url [String] The URL
183
183
  # @param payload [Hash] The payload
184
- # @return [Net::HTTPResponse]
184
+ # @return [Faraday::Response]
185
185
  def post_json(url, payload)
186
- uri = URI.parse(url)
187
- http = Net::HTTP.new(uri.host, uri.port)
188
- http.use_ssl = uri.scheme == "https"
189
- http.open_timeout = 5
190
- http.read_timeout = 10
191
-
192
- request = Net::HTTP::Post.new(uri.request_uri)
193
- request["Content-Type"] = "application/json"
194
- request.body = payload.to_json
195
-
196
- response = http.request(request)
186
+ response = http_client.post(url) do |req|
187
+ req.headers["Content-Type"] = "application/json"
188
+ req.body = payload.to_json
189
+ end
197
190
 
198
- unless response.is_a?(Net::HTTPSuccess)
199
- Rails.logger.warn("[RubyLLM::Agents::AlertManager] Webhook returned #{response.code}: #{response.body}")
191
+ unless response.success?
192
+ Rails.logger.warn("[RubyLLM::Agents::AlertManager] Webhook returned #{response.status}: #{response.body}")
200
193
  end
201
194
 
202
195
  response
203
196
  end
197
+
198
+ # Returns a configured Faraday HTTP client
199
+ #
200
+ # @return [Faraday::Connection]
201
+ def http_client
202
+ @http_client ||= Faraday.new do |conn|
203
+ conn.options.open_timeout = 5
204
+ conn.options.timeout = 10
205
+ conn.adapter Faraday.default_adapter
206
+ end
207
+ end
204
208
  end
205
209
  end
206
210
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../cache_helper"
4
+
5
+ module RubyLLM
6
+ module Agents
7
+ class Base
8
+ # Cache management for agent responses
9
+ #
10
+ # Handles cache key generation and store access for
11
+ # caching agent execution results.
12
+ module Caching
13
+ include CacheHelper
14
+
15
+ # Generates the full cache key for this agent invocation
16
+ #
17
+ # @return [String] Cache key in format "ruby_llm_agent/ClassName/version/hash"
18
+ def agent_cache_key
19
+ ["ruby_llm_agent", self.class.name, self.class.version, cache_key_hash].join("/")
20
+ end
21
+
22
+ # Generates a hash of the cache key data
23
+ #
24
+ # @return [String] SHA256 hex digest of the cache key data
25
+ def cache_key_hash
26
+ Digest::SHA256.hexdigest(cache_key_data.to_json)
27
+ end
28
+
29
+ # Returns data to include in cache key generation
30
+ #
31
+ # Override to customize what parameters affect cache invalidation.
32
+ #
33
+ # @return [Hash] Data to hash for cache key
34
+ def cache_key_data
35
+ @options.except(:skip_cache, :dry_run, :with)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ class Base
6
+ # Cost calculation methods for token and pricing calculations
7
+ #
8
+ # Handles input/output cost calculations, model info resolution,
9
+ # and budget tracking for agent executions.
10
+ module CostCalculation
11
+ # Calculates input cost from tokens
12
+ #
13
+ # @param input_tokens [Integer, nil] Number of input tokens
14
+ # @param response_model_id [String, nil] Model that responded
15
+ # @return [Float, nil] Input cost in USD
16
+ def result_input_cost(input_tokens, response_model_id)
17
+ return nil unless input_tokens
18
+ model_info = result_model_info(response_model_id)
19
+ return nil unless model_info&.pricing
20
+ price = model_info.pricing.text_tokens&.input || 0
21
+ (input_tokens / 1_000_000.0 * price).round(6)
22
+ end
23
+
24
+ # Calculates output cost from tokens
25
+ #
26
+ # @param output_tokens [Integer, nil] Number of output tokens
27
+ # @param response_model_id [String, nil] Model that responded
28
+ # @return [Float, nil] Output cost in USD
29
+ def result_output_cost(output_tokens, response_model_id)
30
+ return nil unless output_tokens
31
+ model_info = result_model_info(response_model_id)
32
+ return nil unless model_info&.pricing
33
+ price = model_info.pricing.text_tokens&.output || 0
34
+ (output_tokens / 1_000_000.0 * price).round(6)
35
+ end
36
+
37
+ # Calculates total cost from tokens
38
+ #
39
+ # @param input_tokens [Integer, nil] Number of input tokens
40
+ # @param output_tokens [Integer, nil] Number of output tokens
41
+ # @param response_model_id [String, nil] Model that responded
42
+ # @return [Float, nil] Total cost in USD
43
+ def result_total_cost(input_tokens, output_tokens, response_model_id)
44
+ input_cost = result_input_cost(input_tokens, response_model_id)
45
+ output_cost = result_output_cost(output_tokens, response_model_id)
46
+ return nil unless input_cost || output_cost
47
+ ((input_cost || 0) + (output_cost || 0)).round(6)
48
+ end
49
+
50
+ # Resolves model info for cost calculation
51
+ #
52
+ # @param response_model_id [String, nil] Model ID from response
53
+ # @return [Object, nil] Model info or nil
54
+ def result_model_info(response_model_id)
55
+ lookup_id = response_model_id || model
56
+ return nil unless lookup_id
57
+ model_obj, _provider = RubyLLM::Models.resolve(lookup_id)
58
+ model_obj
59
+ rescue StandardError
60
+ nil
61
+ end
62
+
63
+ # Resolves model info for cost calculation (alternate method)
64
+ #
65
+ # @param model_id [String] The model identifier
66
+ # @return [Object, nil] Model info or nil
67
+ def resolve_model_info(model_id)
68
+ model_obj, _provider = RubyLLM::Models.resolve(model_id)
69
+ model_obj
70
+ rescue StandardError
71
+ nil
72
+ end
73
+
74
+ # Records cost from an attempt to the budget tracker
75
+ #
76
+ # @param attempt_tracker [AttemptTracker] The attempt tracker
77
+ # @param tenant_id [String, nil] Optional tenant identifier for multi-tenant tracking
78
+ # @return [void]
79
+ def record_attempt_cost(attempt_tracker, tenant_id: nil)
80
+ successful = attempt_tracker.successful_attempt
81
+ return unless successful
82
+
83
+ # Calculate cost for this execution
84
+ # Note: Full cost calculation happens in instrumentation, but we
85
+ # record the spend here for budget tracking
86
+ model_info = resolve_model_info(successful[:model_id])
87
+ return unless model_info&.pricing
88
+
89
+ input_tokens = successful[:input_tokens] || 0
90
+ output_tokens = successful[:output_tokens] || 0
91
+
92
+ input_price = model_info.pricing.text_tokens&.input || 0
93
+ output_price = model_info.pricing.text_tokens&.output || 0
94
+
95
+ total_cost = (input_tokens / 1_000_000.0 * input_price) +
96
+ (output_tokens / 1_000_000.0 * output_price)
97
+
98
+ BudgetTracker.record_spend!(self.class.name, total_cost, tenant_id: tenant_id)
99
+ rescue StandardError => e
100
+ Rails.logger.warn("[RubyLLM::Agents] Failed to record budget spend: #{e.message}")
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ class Base
6
+ # Class-level DSL for configuring agents
7
+ #
8
+ # Provides methods for setting model, temperature, timeout, caching,
9
+ # reliability, streaming, tools, and parameters.
10
+ module DSL
11
+ # @!visibility private
12
+ VERSION = "1.0"
13
+ # @!visibility private
14
+ CACHE_TTL = 1.hour
15
+
16
+ # @!group Configuration DSL
17
+
18
+ # Sets or returns the LLM model for this agent class
19
+ #
20
+ # @param value [String, nil] The model identifier to set
21
+ # @return [String] The current model setting
22
+ # @example
23
+ # model "gpt-4o"
24
+ def model(value = nil)
25
+ @model = value if value
26
+ @model || inherited_or_default(:model, RubyLLM::Agents.configuration.default_model)
27
+ end
28
+
29
+ # Sets or returns the temperature for LLM responses
30
+ #
31
+ # @param value [Float, nil] Temperature value (0.0-2.0)
32
+ # @return [Float] The current temperature setting
33
+ # @example
34
+ # temperature 0.7
35
+ def temperature(value = nil)
36
+ @temperature = value if value
37
+ @temperature || inherited_or_default(:temperature, RubyLLM::Agents.configuration.default_temperature)
38
+ end
39
+
40
+ # Sets or returns the version string for cache invalidation
41
+ #
42
+ # @param value [String, nil] Version string
43
+ # @return [String] The current version
44
+ # @example
45
+ # version "2.0"
46
+ def version(value = nil)
47
+ @version = value if value
48
+ @version || inherited_or_default(:version, VERSION)
49
+ end
50
+
51
+ # Sets or returns the timeout in seconds for LLM requests
52
+ #
53
+ # @param value [Integer, nil] Timeout in seconds
54
+ # @return [Integer] The current timeout setting
55
+ # @example
56
+ # timeout 30
57
+ def timeout(value = nil)
58
+ @timeout = value if value
59
+ @timeout || inherited_or_default(:timeout, RubyLLM::Agents.configuration.default_timeout)
60
+ end
61
+
62
+ # @!endgroup
63
+
64
+ # @!group Reliability DSL
65
+
66
+ # Configures retry behavior for this agent
67
+ #
68
+ # @param max [Integer] Maximum number of retry attempts (default: 0)
69
+ # @param backoff [Symbol] Backoff strategy (:constant or :exponential)
70
+ # @param base [Float] Base delay in seconds
71
+ # @param max_delay [Float] Maximum delay between retries
72
+ # @param on [Array<Class>] Error classes to retry on (extends defaults)
73
+ # @return [Hash] The current retry configuration
74
+ # @example
75
+ # retries max: 2, backoff: :exponential, base: 0.4, max_delay: 3.0, on: [Timeout::Error]
76
+ def retries(max: nil, backoff: nil, base: nil, max_delay: nil, on: nil)
77
+ if max || backoff || base || max_delay || on
78
+ @retries_config ||= RubyLLM::Agents.configuration.default_retries.dup
79
+ @retries_config[:max] = max if max
80
+ @retries_config[:backoff] = backoff if backoff
81
+ @retries_config[:base] = base if base
82
+ @retries_config[:max_delay] = max_delay if max_delay
83
+ @retries_config[:on] = on if on
84
+ end
85
+ @retries_config || inherited_or_default(:retries_config, RubyLLM::Agents.configuration.default_retries)
86
+ end
87
+
88
+ # Returns the retry configuration for this agent
89
+ #
90
+ # @return [Hash, nil] The retry configuration
91
+ def retries_config
92
+ @retries_config || (superclass.respond_to?(:retries_config) ? superclass.retries_config : nil)
93
+ end
94
+
95
+ # Sets or returns fallback models to try when primary model fails
96
+ #
97
+ # @param models [Array<String>, nil] Model identifiers to use as fallbacks
98
+ # @return [Array<String>] The current fallback models
99
+ # @example
100
+ # fallback_models ["gpt-4o-mini", "gpt-4o"]
101
+ def fallback_models(models = nil)
102
+ @fallback_models = models if models
103
+ @fallback_models || inherited_or_default(:fallback_models, RubyLLM::Agents.configuration.default_fallback_models)
104
+ end
105
+
106
+ # Sets or returns the total timeout for all retry/fallback attempts
107
+ #
108
+ # @param seconds [Integer, nil] Total timeout in seconds
109
+ # @return [Integer, nil] The current total timeout
110
+ # @example
111
+ # total_timeout 20
112
+ def total_timeout(seconds = nil)
113
+ @total_timeout = seconds if seconds
114
+ @total_timeout || inherited_or_default(:total_timeout, RubyLLM::Agents.configuration.default_total_timeout)
115
+ end
116
+
117
+ # Configures circuit breaker for this agent
118
+ #
119
+ # @param errors [Integer] Number of errors to trigger open state
120
+ # @param within [Integer] Rolling window in seconds
121
+ # @param cooldown [Integer] Cooldown period in seconds when open
122
+ # @return [Hash, nil] The current circuit breaker configuration
123
+ # @example
124
+ # circuit_breaker errors: 10, within: 60, cooldown: 300
125
+ def circuit_breaker(errors: nil, within: nil, cooldown: nil)
126
+ if errors || within || cooldown
127
+ @circuit_breaker_config ||= { errors: 10, within: 60, cooldown: 300 }
128
+ @circuit_breaker_config[:errors] = errors if errors
129
+ @circuit_breaker_config[:within] = within if within
130
+ @circuit_breaker_config[:cooldown] = cooldown if cooldown
131
+ end
132
+ @circuit_breaker_config || (superclass.respond_to?(:circuit_breaker_config) ? superclass.circuit_breaker_config : nil)
133
+ end
134
+
135
+ # Returns the circuit breaker configuration for this agent
136
+ #
137
+ # @return [Hash, nil] The circuit breaker configuration
138
+ def circuit_breaker_config
139
+ @circuit_breaker_config || (superclass.respond_to?(:circuit_breaker_config) ? superclass.circuit_breaker_config : nil)
140
+ end
141
+
142
+ # @!endgroup
143
+
144
+ # @!group Parameter DSL
145
+
146
+ # Defines a parameter for the agent
147
+ #
148
+ # Creates an accessor method for the parameter that retrieves values
149
+ # from the options hash, falling back to the default value.
150
+ #
151
+ # @param name [Symbol] The parameter name
152
+ # @param required [Boolean] Whether the parameter is required
153
+ # @param default [Object, nil] Default value if not provided
154
+ # @return [void]
155
+ # @example
156
+ # param :query, required: true
157
+ # param :limit, default: 10
158
+ def param(name, required: false, default: nil)
159
+ @params ||= {}
160
+ @params[name] = { required: required, default: default }
161
+ define_method(name) do
162
+ @options[name] || @options[name.to_s] || self.class.params.dig(name, :default)
163
+ end
164
+ end
165
+
166
+ # Returns all defined parameters including inherited ones
167
+ #
168
+ # @return [Hash{Symbol => Hash}] Parameter definitions
169
+ def params
170
+ parent = superclass.respond_to?(:params) ? superclass.params : {}
171
+ parent.merge(@params || {})
172
+ end
173
+
174
+ # @!endgroup
175
+
176
+ # @!group Caching DSL
177
+
178
+ # Enables caching for this agent with optional TTL
179
+ #
180
+ # @param ttl [ActiveSupport::Duration] Time-to-live for cached responses
181
+ # @return [void]
182
+ # @example
183
+ # cache 1.hour
184
+ def cache(ttl = CACHE_TTL)
185
+ @cache_enabled = true
186
+ @cache_ttl = ttl
187
+ end
188
+
189
+ # Returns whether caching is enabled for this agent
190
+ #
191
+ # @return [Boolean] true if caching is enabled
192
+ def cache_enabled?
193
+ @cache_enabled || false
194
+ end
195
+
196
+ # Returns the cache TTL for this agent
197
+ #
198
+ # @return [ActiveSupport::Duration] The cache TTL
199
+ def cache_ttl
200
+ @cache_ttl || CACHE_TTL
201
+ end
202
+
203
+ # @!endgroup
204
+
205
+ # @!group Streaming DSL
206
+
207
+ # Enables or returns streaming mode for this agent
208
+ #
209
+ # When streaming is enabled and a block is passed to call,
210
+ # chunks will be yielded to the block as they arrive.
211
+ #
212
+ # @param value [Boolean, nil] Whether to enable streaming
213
+ # @return [Boolean] The current streaming setting
214
+ # @example
215
+ # streaming true
216
+ def streaming(value = nil)
217
+ @streaming = value unless value.nil?
218
+ return @streaming unless @streaming.nil?
219
+
220
+ inherited_or_default(:streaming, RubyLLM::Agents.configuration.default_streaming)
221
+ end
222
+
223
+ # @!endgroup
224
+
225
+ # @!group Tools DSL
226
+
227
+ # Sets or returns the tools available to this agent
228
+ #
229
+ # Tools are RubyLLM::Tool classes that the model can invoke.
230
+ # The agent will automatically execute tool calls and continue
231
+ # until the model produces a final response.
232
+ #
233
+ # @param tool_classes [Array<Class>] Tool classes to make available
234
+ # @return [Array<Class>] The current tools
235
+ # @example Single tool
236
+ # tools WeatherTool
237
+ # @example Multiple tools
238
+ # tools WeatherTool, SearchTool, CalculatorTool
239
+ def tools(*tool_classes)
240
+ if tool_classes.any?
241
+ @tools = tool_classes.flatten
242
+ end
243
+ @tools || inherited_or_default(:tools, RubyLLM::Agents.configuration.default_tools)
244
+ end
245
+
246
+ # @!endgroup
247
+
248
+ private
249
+
250
+ # Looks up setting from superclass or uses default
251
+ #
252
+ # @param method [Symbol] The method to call on superclass
253
+ # @param default [Object] Default value if not found
254
+ # @return [Object] The resolved value
255
+ def inherited_or_default(method, default)
256
+ superclass.respond_to?(method) ? superclass.send(method) : default
257
+ end
258
+ end
259
+ end
260
+ end
261
+ end