ruby_llm-agents 3.1.0 → 3.3.0

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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -0
  3. data/app/controllers/ruby_llm/agents/agents_controller.rb +16 -14
  4. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +20 -20
  5. data/app/controllers/ruby_llm/agents/executions_controller.rb +5 -7
  6. data/app/helpers/ruby_llm/agents/application_helper.rb +57 -58
  7. data/app/models/ruby_llm/agents/execution/analytics.rb +27 -27
  8. data/app/models/ruby_llm/agents/execution/scopes.rb +4 -6
  9. data/app/models/ruby_llm/agents/execution.rb +25 -25
  10. data/app/models/ruby_llm/agents/tenant/budgetable.rb +16 -10
  11. data/app/models/ruby_llm/agents/tenant/resettable.rb +12 -12
  12. data/app/models/ruby_llm/agents/tenant/trackable.rb +7 -7
  13. data/app/services/ruby_llm/agents/agent_registry.rb +6 -6
  14. data/app/views/ruby_llm/agents/executions/_audio_player.html.erb +57 -0
  15. data/app/views/ruby_llm/agents/executions/show.html.erb +8 -0
  16. data/lib/generators/ruby_llm_agents/agent_generator.rb +4 -4
  17. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +6 -6
  18. data/lib/generators/ruby_llm_agents/embedder_generator.rb +4 -4
  19. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +7 -7
  20. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +4 -4
  21. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +6 -6
  22. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +9 -9
  23. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +6 -6
  24. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +4 -4
  25. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +4 -4
  26. data/lib/generators/ruby_llm_agents/install_generator.rb +3 -3
  27. data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +4 -4
  28. data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +2 -2
  29. data/lib/generators/ruby_llm_agents/restructure_generator.rb +13 -13
  30. data/lib/generators/ruby_llm_agents/speaker_generator.rb +6 -6
  31. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +4 -4
  32. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +2 -2
  33. data/lib/ruby_llm/agents/audio/speaker/active_storage_support.rb +87 -0
  34. data/lib/ruby_llm/agents/audio/speaker.rb +50 -31
  35. data/lib/ruby_llm/agents/audio/speech_client.rb +328 -0
  36. data/lib/ruby_llm/agents/audio/speech_pricing.rb +273 -0
  37. data/lib/ruby_llm/agents/audio/transcriber.rb +43 -33
  38. data/lib/ruby_llm/agents/base_agent.rb +14 -14
  39. data/lib/ruby_llm/agents/core/base/callbacks.rb +3 -3
  40. data/lib/ruby_llm/agents/core/configuration.rb +90 -73
  41. data/lib/ruby_llm/agents/core/errors.rb +27 -2
  42. data/lib/ruby_llm/agents/core/instrumentation.rb +64 -66
  43. data/lib/ruby_llm/agents/core/llm_tenant.rb +7 -7
  44. data/lib/ruby_llm/agents/core/version.rb +1 -1
  45. data/lib/ruby_llm/agents/dsl/base.rb +3 -3
  46. data/lib/ruby_llm/agents/dsl/reliability.rb +9 -9
  47. data/lib/ruby_llm/agents/image/analyzer/dsl.rb +1 -1
  48. data/lib/ruby_llm/agents/image/analyzer/execution.rb +4 -4
  49. data/lib/ruby_llm/agents/image/background_remover/dsl.rb +1 -1
  50. data/lib/ruby_llm/agents/image/background_remover/execution.rb +3 -3
  51. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +8 -8
  52. data/lib/ruby_llm/agents/image/editor/execution.rb +1 -1
  53. data/lib/ruby_llm/agents/image/generator/pricing.rb +9 -10
  54. data/lib/ruby_llm/agents/image/generator.rb +6 -6
  55. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +6 -6
  56. data/lib/ruby_llm/agents/image/pipeline/execution.rb +9 -9
  57. data/lib/ruby_llm/agents/image/pipeline.rb +1 -1
  58. data/lib/ruby_llm/agents/image/transformer/execution.rb +1 -1
  59. data/lib/ruby_llm/agents/image/upscaler/dsl.rb +1 -1
  60. data/lib/ruby_llm/agents/image/upscaler/execution.rb +3 -5
  61. data/lib/ruby_llm/agents/image/variator/execution.rb +1 -1
  62. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +4 -4
  63. data/lib/ruby_llm/agents/infrastructure/attempt_tracker.rb +4 -4
  64. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +9 -9
  65. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +3 -3
  66. data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +1 -1
  67. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +17 -17
  68. data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +1 -0
  69. data/lib/ruby_llm/agents/infrastructure/execution_logger_job.rb +1 -1
  70. data/lib/ruby_llm/agents/infrastructure/reliability.rb +6 -6
  71. data/lib/ruby_llm/agents/pipeline/builder.rb +11 -11
  72. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +3 -3
  73. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +4 -4
  74. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +83 -22
  75. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +2 -3
  76. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +7 -7
  77. data/lib/ruby_llm/agents/results/background_removal_result.rb +6 -6
  78. data/lib/ruby_llm/agents/results/embedding_result.rb +15 -15
  79. data/lib/ruby_llm/agents/results/image_analysis_result.rb +7 -7
  80. data/lib/ruby_llm/agents/results/image_edit_result.rb +4 -4
  81. data/lib/ruby_llm/agents/results/image_generation_result.rb +5 -5
  82. data/lib/ruby_llm/agents/results/image_pipeline_result.rb +4 -4
  83. data/lib/ruby_llm/agents/results/image_transform_result.rb +4 -4
  84. data/lib/ruby_llm/agents/results/image_upscale_result.rb +5 -5
  85. data/lib/ruby_llm/agents/results/image_variation_result.rb +4 -4
  86. data/lib/ruby_llm/agents/results/speech_result.rb +12 -7
  87. data/lib/ruby_llm/agents/results/transcription_result.rb +1 -1
  88. data/lib/ruby_llm/agents/text/embedder.rb +13 -13
  89. metadata +5 -1
@@ -61,7 +61,7 @@ module RubyLLM
61
61
  # @return [Float] New total
62
62
  def increment_spend(scope, period, amount, agent_type: nil, tenant_id: nil)
63
63
  key = budget_cache_key(scope, period, agent_type: agent_type, tenant_id: tenant_id)
64
- ttl = period == :daily ? 1.day : 31.days
64
+ ttl = (period == :daily) ? 1.day : 31.days
65
65
 
66
66
  # Read-modify-write for float values (cache increment is for integers)
67
67
  current = (SpendRecorder.cache_read(key) || 0).to_f
@@ -81,7 +81,7 @@ module RubyLLM
81
81
  def increment_tokens(scope, period, tokens, agent_type: nil, tenant_id: nil)
82
82
  # For now, we only track global token usage (not per-agent)
83
83
  key = token_cache_key(period, tenant_id: tenant_id)
84
- ttl = period == :daily ? 1.day : 31.days
84
+ ttl = (period == :daily) ? 1.day : 31.days
85
85
 
86
86
  current = (SpendRecorder.cache_read(key) || 0).to_i
87
87
  new_total = current + tokens
@@ -102,7 +102,7 @@ module RubyLLM
102
102
  # @param period [Symbol] :daily or :monthly
103
103
  # @return [String] Date string
104
104
  def date_key_part(period)
105
- period == :daily ? Date.current.to_s : Date.current.strftime("%Y-%m")
105
+ (period == :daily) ? Date.current.to_s : Date.current.strftime("%Y-%m")
106
106
  end
107
107
 
108
108
  # Generates an alert cache key
@@ -156,28 +156,28 @@ module RubyLLM
156
156
  def check_soft_cap_alerts(agent_type, tenant_id, budget_config)
157
157
  # Check global daily
158
158
  check_budget_alert(:global_daily, budget_config[:global_daily],
159
- BudgetQuery.current_spend(:global, :daily, tenant_id: tenant_id),
160
- agent_type, tenant_id, budget_config)
159
+ BudgetQuery.current_spend(:global, :daily, tenant_id: tenant_id),
160
+ agent_type, tenant_id, budget_config)
161
161
 
162
162
  # Check global monthly
163
163
  check_budget_alert(:global_monthly, budget_config[:global_monthly],
164
- BudgetQuery.current_spend(:global, :monthly, tenant_id: tenant_id),
165
- agent_type, tenant_id, budget_config)
164
+ BudgetQuery.current_spend(:global, :monthly, tenant_id: tenant_id),
165
+ agent_type, tenant_id, budget_config)
166
166
 
167
167
  # Check per-agent daily
168
168
  agent_daily_limit = budget_config[:per_agent_daily]&.dig(agent_type)
169
169
  if agent_daily_limit
170
170
  check_budget_alert(:per_agent_daily, agent_daily_limit,
171
- BudgetQuery.current_spend(:agent, :daily, agent_type: agent_type, tenant_id: tenant_id),
172
- agent_type, tenant_id, budget_config)
171
+ BudgetQuery.current_spend(:agent, :daily, agent_type: agent_type, tenant_id: tenant_id),
172
+ agent_type, tenant_id, budget_config)
173
173
  end
174
174
 
175
175
  # Check per-agent monthly
176
176
  agent_monthly_limit = budget_config[:per_agent_monthly]&.dig(agent_type)
177
177
  if agent_monthly_limit
178
178
  check_budget_alert(:per_agent_monthly, agent_monthly_limit,
179
- BudgetQuery.current_spend(:agent, :monthly, agent_type: agent_type, tenant_id: tenant_id),
180
- agent_type, tenant_id, budget_config)
179
+ BudgetQuery.current_spend(:agent, :monthly, agent_type: agent_type, tenant_id: tenant_id),
180
+ agent_type, tenant_id, budget_config)
181
181
  end
182
182
  end
183
183
 
@@ -194,7 +194,7 @@ module RubyLLM
194
194
  return unless limit
195
195
  return if current <= limit
196
196
 
197
- event = budget_config[:enforcement] == :hard ? :budget_hard_cap : :budget_soft_cap
197
+ event = (budget_config[:enforcement] == :hard) ? :budget_hard_cap : :budget_soft_cap
198
198
 
199
199
  # Prevent duplicate alerts by using a cache key (include tenant for isolation)
200
200
  key = alert_cache_key("budget_alert", scope, tenant_id)
@@ -221,13 +221,13 @@ module RubyLLM
221
221
  def check_soft_token_alerts(agent_type, tenant_id, budget_config)
222
222
  # Check global daily tokens
223
223
  check_token_alert(:global_daily_tokens, budget_config[:global_daily_tokens],
224
- BudgetQuery.current_tokens(:daily, tenant_id: tenant_id),
225
- agent_type, tenant_id, budget_config)
224
+ BudgetQuery.current_tokens(:daily, tenant_id: tenant_id),
225
+ agent_type, tenant_id, budget_config)
226
226
 
227
227
  # Check global monthly tokens
228
228
  check_token_alert(:global_monthly_tokens, budget_config[:global_monthly_tokens],
229
- BudgetQuery.current_tokens(:monthly, tenant_id: tenant_id),
230
- agent_type, tenant_id, budget_config)
229
+ BudgetQuery.current_tokens(:monthly, tenant_id: tenant_id),
230
+ agent_type, tenant_id, budget_config)
231
231
  end
232
232
 
233
233
  # Checks if a token alert should be fired
@@ -243,7 +243,7 @@ module RubyLLM
243
243
  return unless limit
244
244
  return if current <= limit
245
245
 
246
- event = budget_config[:enforcement] == :hard ? :token_hard_cap : :token_soft_cap
246
+ event = (budget_config[:enforcement] == :hard) ? :token_hard_cap : :token_soft_cap
247
247
 
248
248
  # Prevent duplicate alerts
249
249
  key = alert_cache_key("token_alert", scope, tenant_id)
@@ -30,6 +30,7 @@ module RubyLLM
30
30
  # @api public
31
31
  class CircuitBreaker
32
32
  include CacheHelper
33
+
33
34
  attr_reader :agent_type, :model_id, :tenant_id, :errors_threshold, :window_seconds, :cooldown_seconds
34
35
 
35
36
  # @param agent_type [String] The agent class name
@@ -33,7 +33,7 @@ module RubyLLM
33
33
  execution = Execution.create!(filtered_data)
34
34
 
35
35
  # Create detail record if present
36
- if detail_data && detail_data.values.any? { |v| v.present? && v != {} && v != [] }
36
+ if detail_data&.values&.any? { |v| v.present? && v != {} && v != [] }
37
37
  execution.create_detail!(detail_data)
38
38
  end
39
39
 
@@ -108,11 +108,11 @@ module RubyLLM
108
108
 
109
109
  @attempts.filter_map do |attempt|
110
110
  if attempt["short_circuited"]
111
- { model: attempt["model_id"], error_class: "CircuitBreakerOpen", error_message: "circuit breaker open",
112
- error_backtrace: nil }
111
+ {model: attempt["model_id"], error_class: "CircuitBreakerOpen", error_message: "circuit breaker open",
112
+ error_backtrace: nil}
113
113
  elsif attempt["error_class"]
114
- { model: attempt["model_id"], error_class: attempt["error_class"], error_message: attempt["error_message"],
115
- error_backtrace: attempt["error_backtrace"] }
114
+ {model: attempt["model_id"], error_class: attempt["error_class"], error_message: attempt["error_message"],
115
+ error_backtrace: attempt["error_backtrace"]}
116
116
  end
117
117
  end
118
118
  end
@@ -125,11 +125,11 @@ module RubyLLM
125
125
  if attempt["short_circuited"]
126
126
  parts << " #{model}: circuit breaker open"
127
127
  elsif attempt["error_class"]
128
- parts << " #{model}: #{attempt['error_class']} - #{attempt['error_message']}"
128
+ parts << " #{model}: #{attempt["error_class"]} - #{attempt["error_message"]}"
129
129
  end
130
130
  end
131
131
  else
132
- parts << " Models: #{@models_tried.join(', ')}"
132
+ parts << " Models: #{@models_tried.join(", ")}"
133
133
  parts << " Last error: #{@last_error.message}"
134
134
  end
135
135
  parts.join("\n")
@@ -164,7 +164,7 @@ module RubyLLM
164
164
  # @return [Boolean]
165
165
  def budgets_enabled?
166
166
  RubyLLM::Agents.configuration.budgets_enabled?
167
- rescue StandardError
167
+ rescue
168
168
  false
169
169
  end
170
170
 
@@ -176,7 +176,7 @@ module RubyLLM
176
176
  return false unless agent_class
177
177
 
178
178
  agent_class.respond_to?(:cache_enabled?) && agent_class.cache_enabled?
179
- rescue StandardError
179
+ rescue
180
180
  false
181
181
  end
182
182
 
@@ -192,20 +192,20 @@ module RubyLLM
192
192
  return false unless agent_class
193
193
 
194
194
  retries = if agent_class.respond_to?(:retries)
195
- agent_class.retries
196
- else
197
- 0
198
- end
195
+ agent_class.retries
196
+ else
197
+ 0
198
+ end
199
199
 
200
200
  fallbacks = if agent_class.respond_to?(:fallback_models)
201
- agent_class.fallback_models
202
- else
203
- []
204
- end
201
+ agent_class.fallback_models
202
+ else
203
+ []
204
+ end
205
205
 
206
206
  (retries.is_a?(Integer) && retries.positive?) ||
207
207
  (fallbacks.is_a?(Array) && fallbacks.any?)
208
- rescue StandardError
208
+ rescue
209
209
  false
210
210
  end
211
211
  end
@@ -51,7 +51,7 @@ module RubyLLM
51
51
  # @return [Boolean]
52
52
  def budgets_enabled?
53
53
  global_config.budgets_enabled?
54
- rescue StandardError
54
+ rescue
55
55
  false
56
56
  end
57
57
 
@@ -78,7 +78,7 @@ module RubyLLM
78
78
  )
79
79
  rescue RubyLLM::Agents::Reliability::BudgetExceededError
80
80
  raise
81
- rescue StandardError => e
81
+ rescue => e
82
82
  error("Budget check failed: #{e.message}")
83
83
  end
84
84
 
@@ -117,7 +117,7 @@ module RubyLLM
117
117
  tenant_id: context.tenant_id
118
118
  )
119
119
  end
120
- rescue StandardError => e
120
+ rescue => e
121
121
  error("Failed to record spend: #{e.message}")
122
122
  end
123
123
  end
@@ -71,7 +71,7 @@ module RubyLLM
71
71
  # @return [ActiveSupport::Cache::Store, nil]
72
72
  def cache_store
73
73
  global_config.cache_store
74
- rescue StandardError
74
+ rescue
75
75
  nil
76
76
  end
77
77
 
@@ -130,7 +130,7 @@ module RubyLLM
130
130
  else
131
131
  input.to_json
132
132
  end
133
- rescue StandardError
133
+ rescue
134
134
  input.to_s
135
135
  end
136
136
 
@@ -140,7 +140,7 @@ module RubyLLM
140
140
  # @return [Object, nil] Cached value or nil
141
141
  def cache_read(key)
142
142
  cache_store.read(key)
143
- rescue StandardError => e
143
+ rescue => e
144
144
  error("Cache read failed: #{e.message}")
145
145
  nil
146
146
  end
@@ -154,7 +154,7 @@ module RubyLLM
154
154
  options[:expires_in] = cache_ttl if cache_ttl
155
155
 
156
156
  cache_store.write(key, value, **options)
157
- rescue StandardError => e
157
+ rescue => e
158
158
  error("Cache write failed: #{e.message}")
159
159
  end
160
160
  end
@@ -52,10 +52,10 @@ module RubyLLM
52
52
  begin
53
53
  complete_execution(execution, context, status: "success")
54
54
  status_update_completed = true
55
- rescue StandardError
55
+ rescue
56
56
  # Let ensure block handle via mark_execution_failed!
57
57
  end
58
- rescue StandardError => e
58
+ rescue => e
59
59
  context.completed_at = Time.current
60
60
  context.error = e
61
61
  raised_exception = e
@@ -63,7 +63,7 @@ module RubyLLM
63
63
  begin
64
64
  complete_execution(execution, context, status: determine_error_status(e))
65
65
  status_update_completed = true
66
- rescue StandardError
66
+ rescue
67
67
  # Let ensure block handle via mark_execution_failed!
68
68
  end
69
69
 
@@ -100,7 +100,7 @@ module RubyLLM
100
100
  end
101
101
 
102
102
  execution
103
- rescue StandardError => e
103
+ rescue => e
104
104
  error("Failed to create running execution record: #{e.message}")
105
105
  nil
106
106
  end
@@ -131,7 +131,7 @@ module RubyLLM
131
131
 
132
132
  # Save detail data (prompts, responses, tool calls, etc.)
133
133
  save_execution_details(execution, context, status)
134
- rescue StandardError => e
134
+ rescue => e
135
135
  error("Failed to complete execution record: #{e.message}")
136
136
  raise # Re-raise for ensure block to handle via mark_execution_failed!
137
137
  end
@@ -160,16 +160,16 @@ module RubyLLM
160
160
 
161
161
  # Store error_message in detail table (best-effort)
162
162
  begin
163
- detail_attrs = { error_message: error_message }
163
+ detail_attrs = {error_message: error_message}
164
164
  if execution.detail
165
165
  execution.detail.update_columns(detail_attrs)
166
166
  else
167
167
  RubyLLM::Agents::ExecutionDetail.create!(detail_attrs.merge(execution_id: execution.id))
168
168
  end
169
- rescue StandardError
169
+ rescue
170
170
  # Non-critical
171
171
  end
172
- rescue StandardError => e
172
+ rescue => e
173
173
  error("CRITICAL: Failed emergency status update for execution #{execution&.id}: #{e.message}")
174
174
  end
175
175
 
@@ -223,7 +223,11 @@ module RubyLLM
223
223
  }
224
224
 
225
225
  # Store niche cache key in metadata
226
- merged_metadata = context.metadata.dup rescue {}
226
+ merged_metadata = begin
227
+ context.metadata.dup
228
+ rescue
229
+ {}
230
+ end
227
231
  if context.cached? && context[:cache_key]
228
232
  merged_metadata["response_cache_key"] = context[:cache_key]
229
233
  end
@@ -276,6 +280,9 @@ module RubyLLM
276
280
  detail_data[:response] = serialize_response(context)
277
281
  end
278
282
 
283
+ # Persist audio data for Speaker executions
284
+ maybe_persist_audio_response(context, detail_data)
285
+
279
286
  has_data = detail_data.values.any? { |v| v.present? && v != {} && v != [] }
280
287
  return unless has_data
281
288
 
@@ -284,7 +291,7 @@ module RubyLLM
284
291
  else
285
292
  execution.create_detail!(detail_data)
286
293
  end
287
- rescue StandardError => e
294
+ rescue => e
288
295
  error("Failed to save execution details: #{e.message}")
289
296
  end
290
297
 
@@ -304,7 +311,7 @@ module RubyLLM
304
311
  else
305
312
  create_execution_record(data)
306
313
  end
307
- rescue StandardError => e
314
+ rescue => e
308
315
  error("Failed to record execution: #{e.message}")
309
316
  end
310
317
 
@@ -314,7 +321,11 @@ module RubyLLM
314
321
  # @param status [String] "success" or "error"
315
322
  # @return [Hash] Execution data
316
323
  def build_execution_data(context, status)
317
- merged_metadata = context.metadata.dup rescue {}
324
+ merged_metadata = begin
325
+ context.metadata.dup
326
+ rescue
327
+ {}
328
+ end
318
329
  if context.cached? && context[:cache_key]
319
330
  merged_metadata["response_cache_key"] = context[:cache_key]
320
331
  end
@@ -355,7 +366,7 @@ module RubyLLM
355
366
  end
356
367
 
357
368
  # Store detail data for separate creation
358
- detail_data = { parameters: sanitize_parameters(context) }
369
+ detail_data = {parameters: sanitize_parameters(context)}
359
370
  if global_config.persist_prompts
360
371
  exec_opts = context.options[:options] || {}
361
372
  detail_data[:system_prompt] = exec_opts[:system_prompt]
@@ -368,6 +379,10 @@ module RubyLLM
368
379
  if global_config.persist_responses && context.output.respond_to?(:content)
369
380
  detail_data[:response] = serialize_response(context)
370
381
  end
382
+
383
+ # Persist audio data for Speaker executions
384
+ maybe_persist_audio_response(context, detail_data)
385
+
371
386
  data[:_detail_data] = detail_data
372
387
 
373
388
  data
@@ -396,7 +411,11 @@ module RubyLLM
396
411
  def sanitize_parameters(context)
397
412
  return {} unless context.agent_instance.respond_to?(:options, true)
398
413
 
399
- params = context.agent_instance.send(:options) rescue {}
414
+ params = begin
415
+ context.agent_instance.send(:options)
416
+ rescue
417
+ {}
418
+ end
400
419
  params = params.dup
401
420
  params.transform_keys!(&:to_s)
402
421
 
@@ -421,7 +440,7 @@ module RubyLLM
421
440
  return "" if message.nil?
422
441
 
423
442
  message.to_s.truncate(1000)
424
- rescue StandardError
443
+ rescue
425
444
  message.to_s[0, 1000]
426
445
  end
427
446
 
@@ -436,7 +455,7 @@ module RubyLLM
436
455
  return nil if content.nil?
437
456
 
438
457
  # Build response hash similar to core instrumentation
439
- response_data = { content: content }
458
+ response_data = {content: content}
440
459
 
441
460
  # Add model_id if available
442
461
  response_data[:model_id] = context.model_used if context.model_used
@@ -446,11 +465,53 @@ module RubyLLM
446
465
  response_data[:output_tokens] = context.output_tokens if context.output_tokens
447
466
 
448
467
  response_data
449
- rescue StandardError => e
468
+ rescue => e
450
469
  error("Failed to serialize response: #{e.message}")
451
470
  nil
452
471
  end
453
472
 
473
+ # Persists audio response data for Speaker executions
474
+ #
475
+ # When persist_audio_data is enabled and the output is a SpeechResult with
476
+ # audio binary data, stores a base64 data URI in the response column.
477
+ # Always stores audio_url if present (lightweight, no binary).
478
+ #
479
+ # @param context [Context] The execution context
480
+ # @param detail_data [Hash] The detail data hash to modify
481
+ def maybe_persist_audio_response(context, detail_data)
482
+ return unless context.output.is_a?(RubyLLM::Agents::SpeechResult)
483
+
484
+ # Always persist audio_url if present (it's just a string, no binary)
485
+ if context.output.audio_url.present?
486
+ detail_data[:response] ||= {}
487
+ detail_data[:response][:audio_url] = context.output.audio_url
488
+ end
489
+
490
+ # Persist full audio data URI only when opted in
491
+ return unless global_config.respond_to?(:persist_audio_data) && global_config.persist_audio_data
492
+ return unless context.output.audio.present?
493
+
494
+ detail_data[:response] = serialize_audio_response(context.output)
495
+ rescue => e
496
+ error("Failed to persist audio response: #{e.message}")
497
+ end
498
+
499
+ # Serializes a SpeechResult into a hash for the response column
500
+ #
501
+ # @param result [SpeechResult] The speech result to serialize
502
+ # @return [Hash] Serialized audio response data
503
+ def serialize_audio_response(result)
504
+ {
505
+ audio_data_uri: result.to_data_uri,
506
+ audio_url: result.audio_url,
507
+ format: result.format.to_s,
508
+ duration: result.duration,
509
+ file_size: result.file_size,
510
+ voice_id: result.voice_id,
511
+ provider: result.provider.to_s
512
+ }.compact
513
+ end
514
+
454
515
  # Queues async logging via background job
455
516
  #
456
517
  # @param data [Hash] Execution data
@@ -464,7 +525,7 @@ module RubyLLM
464
525
  def create_execution_record(data)
465
526
  detail_data = data.delete(:_detail_data)
466
527
  execution = Execution.create!(data)
467
- if detail_data && detail_data.values.any? { |v| v.present? && v != {} && v != [] }
528
+ if detail_data&.values&.any? { |v| v.present? && v != {} && v != [] }
468
529
  execution.create_detail!(detail_data)
469
530
  end
470
531
  execution
@@ -487,7 +548,7 @@ module RubyLLM
487
548
  else
488
549
  cfg.track_executions
489
550
  end
490
- rescue StandardError
551
+ rescue
491
552
  false
492
553
  end
493
554
 
@@ -496,7 +557,7 @@ module RubyLLM
496
557
  # @return [Boolean]
497
558
  def track_cache_hits?
498
559
  global_config.respond_to?(:track_cache_hits) && global_config.track_cache_hits
499
- rescue StandardError
560
+ rescue
500
561
  false
501
562
  end
502
563
 
@@ -505,7 +566,7 @@ module RubyLLM
505
566
  # @return [Boolean]
506
567
  def async_logging?
507
568
  global_config.async_logging && defined?(Infrastructure::ExecutionLoggerJob)
508
- rescue StandardError
569
+ rescue
509
570
  false
510
571
  end
511
572
 
@@ -520,7 +581,7 @@ module RubyLLM
520
581
  @_assistant_prompt_column_exists = begin
521
582
  defined?(RubyLLM::Agents::ExecutionDetail) &&
522
583
  RubyLLM::Agents::ExecutionDetail.column_names.include?("assistant_prompt")
523
- rescue StandardError
584
+ rescue
524
585
  false
525
586
  end
526
587
  end
@@ -172,8 +172,7 @@ module RubyLLM
172
172
  tracker.complete_attempt(attempt, success: true, response: context.output)
173
173
 
174
174
  return context
175
-
176
- rescue StandardError => e
175
+ rescue => e
177
176
  context.error = e
178
177
  breaker&.record_failure!
179
178
  tracker.complete_attempt(attempt, success: false, error: e)
@@ -305,7 +304,7 @@ module RubyLLM
305
304
  else
306
305
  sleep(seconds)
307
306
  end
308
- rescue StandardError
307
+ rescue
309
308
  # Fall back to regular sleep if async detection fails
310
309
  sleep(seconds)
311
310
  end
@@ -79,8 +79,8 @@ module RubyLLM
79
79
  context.tenant_config = extract_tenant_config(tenant_value)
80
80
  else
81
81
  raise ArgumentError,
82
- "tenant must respond to :llm_tenant_id (use llm_tenant DSL), " \
83
- "or be a Hash with :id key, got #{tenant_value.class}"
82
+ "tenant must respond to :llm_tenant_id (use llm_tenant DSL), " \
83
+ "or be a Hash with :id key, got #{tenant_value.class}"
84
84
  end
85
85
  end
86
86
  end
@@ -106,7 +106,7 @@ module RubyLLM
106
106
  # For hash-based or string tenants, ensure a minimal record exists
107
107
  RubyLLM::Agents::Tenant.find_or_create_by!(tenant_id: context.tenant_id)
108
108
  end
109
- rescue StandardError => e
109
+ rescue => e
110
110
  # Don't fail the execution if tenant record creation fails
111
111
  log_tenant_warning("ensure tenant record", e)
112
112
  end
@@ -117,7 +117,7 @@ module RubyLLM
117
117
  def ensure_tenant_for_model!(tenant_object)
118
118
  # Check polymorphic link first, then tenant_id
119
119
  existing = RubyLLM::Agents::Tenant.find_by(tenant_record: tenant_object) ||
120
- RubyLLM::Agents::Tenant.find_by(tenant_id: tenant_object.llm_tenant_id)
120
+ RubyLLM::Agents::Tenant.find_by(tenant_id: tenant_object.llm_tenant_id)
121
121
  return if existing
122
122
 
123
123
  options = tenant_object.class.try(:llm_tenant_options) || {}
@@ -146,7 +146,7 @@ module RubyLLM
146
146
  return @tenant_table_exists if defined?(@tenant_table_exists)
147
147
 
148
148
  @tenant_table_exists = ::ActiveRecord::Base.connection.table_exists?(:ruby_llm_agents_tenants)
149
- rescue StandardError
149
+ rescue
150
150
  @tenant_table_exists = false
151
151
  end
152
152
 
@@ -181,7 +181,7 @@ module RubyLLM
181
181
  return if api_keys.blank?
182
182
 
183
183
  apply_api_keys_to_ruby_llm(api_keys)
184
- rescue StandardError => e
184
+ rescue => e
185
185
  # Log but don't fail if API key extraction fails
186
186
  warn_api_key_error("tenant object", e)
187
187
  end
@@ -228,7 +228,7 @@ module RubyLLM
228
228
  return nil unless tenant.respond_to?(:llm_config)
229
229
 
230
230
  tenant.llm_config
231
- rescue StandardError
231
+ rescue
232
232
  nil
233
233
  end
234
234
  end
@@ -16,9 +16,9 @@ module RubyLLM
16
16
  #
17
17
  class BackgroundRemovalResult
18
18
  attr_reader :foreground, :mask, :source_image, :model_id, :output_format,
19
- :alpha_matting, :refine_edges,
20
- :started_at, :completed_at, :tenant_id, :remover_class,
21
- :error_class, :error_message
19
+ :alpha_matting, :refine_edges,
20
+ :started_at, :completed_at, :tenant_id, :remover_class,
21
+ :error_class, :error_message
22
22
 
23
23
  # Initialize a new result
24
24
  #
@@ -36,8 +36,8 @@ module RubyLLM
36
36
  # @param error_class [String, nil] Error class name if failed
37
37
  # @param error_message [String, nil] Error message if failed
38
38
  def initialize(foreground:, mask:, source_image:, model_id:, output_format:,
39
- alpha_matting:, refine_edges:, started_at:, completed_at:,
40
- tenant_id:, remover_class:, error_class: nil, error_message: nil)
39
+ alpha_matting:, refine_edges:, started_at:, completed_at:,
40
+ tenant_id:, remover_class:, error_class: nil, error_message: nil)
41
41
  @foreground = foreground
42
42
  @mask = mask
43
43
  @source_image = source_image
@@ -224,7 +224,7 @@ module RubyLLM
224
224
  # Lightweight result for cached removals
225
225
  class CachedBackgroundRemovalResult
226
226
  attr_reader :url, :data, :mask_url, :mask_data, :mime_type, :model_id,
227
- :output_format, :total_cost, :cached_at
227
+ :output_format, :total_cost, :cached_at
228
228
 
229
229
  def initialize(data)
230
230
  @url = data[:url]
@@ -156,13 +156,13 @@ module RubyLLM
156
156
  return nil if v1.nil?
157
157
 
158
158
  v2 = case other
159
- when EmbeddingResult
160
- other.vector
161
- when Array
162
- other
163
- else
164
- raise ArgumentError, "other must be EmbeddingResult or Array, got #{other.class}"
165
- end
159
+ when EmbeddingResult
160
+ other.vector
161
+ when Array
162
+ other
163
+ else
164
+ raise ArgumentError, "other must be EmbeddingResult or Array, got #{other.class}"
165
+ end
166
166
 
167
167
  return nil if v2.nil?
168
168
 
@@ -183,17 +183,17 @@ module RubyLLM
183
183
 
184
184
  similarities = others.each_with_index.map do |other, idx|
185
185
  v2 = case other
186
- when EmbeddingResult
187
- other.vector
188
- when Array
189
- other
190
- else
191
- next nil
192
- end
186
+ when EmbeddingResult
187
+ other.vector
188
+ when Array
189
+ other
190
+ else
191
+ next nil
192
+ end
193
193
 
194
194
  next nil if v2.nil?
195
195
 
196
- { index: idx, similarity: cosine_similarity(v1, v2) }
196
+ {index: idx, similarity: cosine_similarity(v1, v2)}
197
197
  end.compact
198
198
 
199
199
  similarities.sort_by { |s| -s[:similarity] }.first(limit)