ruby_llm-agents 3.1.0 → 3.2.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 (85) 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/lib/generators/ruby_llm_agents/agent_generator.rb +4 -4
  15. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +6 -6
  16. data/lib/generators/ruby_llm_agents/embedder_generator.rb +4 -4
  17. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +7 -7
  18. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +4 -4
  19. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +6 -6
  20. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +9 -9
  21. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +6 -6
  22. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +4 -4
  23. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +4 -4
  24. data/lib/generators/ruby_llm_agents/install_generator.rb +3 -3
  25. data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +4 -4
  26. data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +2 -2
  27. data/lib/generators/ruby_llm_agents/restructure_generator.rb +13 -13
  28. data/lib/generators/ruby_llm_agents/speaker_generator.rb +6 -6
  29. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +4 -4
  30. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +2 -2
  31. data/lib/ruby_llm/agents/audio/speaker.rb +40 -31
  32. data/lib/ruby_llm/agents/audio/speech_client.rb +328 -0
  33. data/lib/ruby_llm/agents/audio/speech_pricing.rb +273 -0
  34. data/lib/ruby_llm/agents/audio/transcriber.rb +33 -33
  35. data/lib/ruby_llm/agents/base_agent.rb +14 -14
  36. data/lib/ruby_llm/agents/core/base/callbacks.rb +3 -3
  37. data/lib/ruby_llm/agents/core/configuration.rb +86 -73
  38. data/lib/ruby_llm/agents/core/errors.rb +27 -2
  39. data/lib/ruby_llm/agents/core/instrumentation.rb +64 -66
  40. data/lib/ruby_llm/agents/core/llm_tenant.rb +7 -7
  41. data/lib/ruby_llm/agents/core/version.rb +1 -1
  42. data/lib/ruby_llm/agents/dsl/base.rb +3 -3
  43. data/lib/ruby_llm/agents/dsl/reliability.rb +9 -9
  44. data/lib/ruby_llm/agents/image/analyzer/dsl.rb +1 -1
  45. data/lib/ruby_llm/agents/image/analyzer/execution.rb +4 -4
  46. data/lib/ruby_llm/agents/image/background_remover/dsl.rb +1 -1
  47. data/lib/ruby_llm/agents/image/background_remover/execution.rb +3 -3
  48. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +8 -8
  49. data/lib/ruby_llm/agents/image/editor/execution.rb +1 -1
  50. data/lib/ruby_llm/agents/image/generator/pricing.rb +9 -10
  51. data/lib/ruby_llm/agents/image/generator.rb +6 -6
  52. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +6 -6
  53. data/lib/ruby_llm/agents/image/pipeline/execution.rb +9 -9
  54. data/lib/ruby_llm/agents/image/pipeline.rb +1 -1
  55. data/lib/ruby_llm/agents/image/transformer/execution.rb +1 -1
  56. data/lib/ruby_llm/agents/image/upscaler/dsl.rb +1 -1
  57. data/lib/ruby_llm/agents/image/upscaler/execution.rb +3 -5
  58. data/lib/ruby_llm/agents/image/variator/execution.rb +1 -1
  59. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +4 -4
  60. data/lib/ruby_llm/agents/infrastructure/attempt_tracker.rb +4 -4
  61. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +9 -9
  62. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +3 -3
  63. data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +1 -1
  64. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +17 -17
  65. data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +1 -0
  66. data/lib/ruby_llm/agents/infrastructure/execution_logger_job.rb +1 -1
  67. data/lib/ruby_llm/agents/infrastructure/reliability.rb +6 -6
  68. data/lib/ruby_llm/agents/pipeline/builder.rb +11 -11
  69. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +3 -3
  70. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +4 -4
  71. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +34 -22
  72. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +2 -3
  73. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +7 -7
  74. data/lib/ruby_llm/agents/results/background_removal_result.rb +6 -6
  75. data/lib/ruby_llm/agents/results/embedding_result.rb +15 -15
  76. data/lib/ruby_llm/agents/results/image_analysis_result.rb +7 -7
  77. data/lib/ruby_llm/agents/results/image_edit_result.rb +4 -4
  78. data/lib/ruby_llm/agents/results/image_generation_result.rb +5 -5
  79. data/lib/ruby_llm/agents/results/image_pipeline_result.rb +4 -4
  80. data/lib/ruby_llm/agents/results/image_transform_result.rb +4 -4
  81. data/lib/ruby_llm/agents/results/image_upscale_result.rb +5 -5
  82. data/lib/ruby_llm/agents/results/image_variation_result.rb +4 -4
  83. data/lib/ruby_llm/agents/results/transcription_result.rb +1 -1
  84. data/lib/ruby_llm/agents/text/embedder.rb +13 -13
  85. metadata +3 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 552fe8427ac0b47f41b14e4652ac65eac1c7b0389a603b71576f8171aba504f7
4
- data.tar.gz: d356d7fc391f93194bc0d4dc1cd179178361af5db8c6b00270c8cbd9bcea19b8
3
+ metadata.gz: d8c4a83ecc9e39e7df7243b98a51d1c249a963f3a1f96551ebefae13becb50c5
4
+ data.tar.gz: d042bda1737b7593187896e879b3065cc855e990e1406410e99d1d853819f3a9
5
5
  SHA512:
6
- metadata.gz: 8d7d722cc72e9baa30a3da29f234ed039965f12743f541ae354f7d477aa7a9a5798c0639ddf37872fa1b67c58c7244b7490d0d9f548a03f3734e101a717153a9
7
- data.tar.gz: 33f0e37172dbd4b607f86ca27d27b2be997b1650544dcbf41877dd00e094f66bde86b81afaa7aacbf31d7049eda2f9567f3a27b3970fad64c01113a6e436785b
6
+ metadata.gz: 78b6fa31a8a656c36e0bb51f6e7a405101e90f47afc3ca35f87c392878b6a2d986b2b275c6424826ffb6b5d4bfb059f4632e8976aa5b2a473eab540619bd18cb
7
+ data.tar.gz: 3d5890ea864aea3531e96571b6010c9fdcedc5a15e8966c7974138cde0ebaec89a771e33fd20e3fd2d5097a1ecfc398236d833f9852f046fd3b1f720eaf7fb6e
data/README.md CHANGED
@@ -135,6 +135,7 @@ result.save("logo.png")
135
135
  | **Attachments** | Images, PDFs, and multimodal support | [Attachments](https://github.com/adham90/ruby_llm-agents/wiki/Attachments) |
136
136
  | **Embeddings** | Vector embeddings with batching, caching, and preprocessing | [Embeddings](https://github.com/adham90/ruby_llm-agents/wiki/Embeddings) |
137
137
  | **Image Operations** | Generation, analysis, editing, pipelines with cost tracking | [Images](https://github.com/adham90/ruby_llm-agents/wiki/Image-Generation) |
138
+ | **Audio** | Text-to-speech (OpenAI, ElevenLabs) and speech-to-text with cost tracking | [Audio](https://github.com/adham90/ruby_llm-agents/wiki/Audio) |
138
139
  | **Alerts** | Slack, webhook, and custom notifications | [Alerts](https://github.com/adham90/ruby_llm-agents/wiki/Alerts) |
139
140
 
140
141
  ## Quick Start
@@ -55,14 +55,14 @@ module RubyLLM
55
55
 
56
56
  @agent_count = @agents.size
57
57
  @deleted_count = @deleted_agents.size
58
- rescue StandardError => e
58
+ rescue => e
59
59
  Rails.logger.error("[RubyLLM::Agents] Error loading agents: #{e.message}")
60
60
  @agents = []
61
61
  @deleted_agents = []
62
- @agents_by_type = { agent: [], embedder: [], speaker: [], transcriber: [], image_generator: [] }
62
+ @agents_by_type = {agent: [], embedder: [], speaker: [], transcriber: [], image_generator: []}
63
63
  @agent_count = 0
64
64
  @deleted_count = 0
65
- @sort_params = { column: DEFAULT_AGENT_SORT_COLUMN, direction: DEFAULT_AGENT_SORT_DIRECTION }
65
+ @sort_params = {column: DEFAULT_AGENT_SORT_COLUMN, direction: DEFAULT_AGENT_SORT_DIRECTION}
66
66
  flash.now[:alert] = "Error loading agents list"
67
67
  end
68
68
 
@@ -88,7 +88,7 @@ module RubyLLM
88
88
  # Only load circuit breaker status for base agents
89
89
  load_circuit_breaker_status if @agent_type_kind == "agent"
90
90
  end
91
- rescue StandardError => e
91
+ rescue => e
92
92
  Rails.logger.error("[RubyLLM::Agents] Error loading agent #{@agent_type}: #{e.message}")
93
93
  redirect_to ruby_llm_agents.agents_path, alert: "Error loading agent details"
94
94
  end
@@ -118,9 +118,9 @@ module RubyLLM
118
118
  def load_filter_options
119
119
  # Single query to get all filter options (fixes N+1)
120
120
  filter_data = Execution.by_agent(@agent_type)
121
- .where.not(model_id: nil)
122
- .or(Execution.by_agent(@agent_type).where.not(temperature: nil))
123
- .pluck(:model_id, :temperature)
121
+ .where.not(model_id: nil)
122
+ .or(Execution.by_agent(@agent_type).where.not(temperature: nil))
123
+ .pluck(:model_id, :temperature)
124
124
 
125
125
  @models = filter_data.map(&:first).compact.uniq.sort
126
126
  @temperatures = filter_data.map(&:last).compact.uniq.sort
@@ -167,9 +167,7 @@ module RubyLLM
167
167
 
168
168
  # Apply time range filter with validation
169
169
  days = parse_days_param
170
- scope = apply_time_filter(scope, days)
171
-
172
- scope
170
+ apply_time_filter(scope, days)
173
171
  end
174
172
 
175
173
  # Loads chart data for agent performance visualization
@@ -300,7 +298,7 @@ module RubyLLM
300
298
  def safe_config_call(method)
301
299
  return nil unless @agent_class&.respond_to?(method)
302
300
  @agent_class.public_send(method)
303
- rescue StandardError
301
+ rescue
304
302
  nil
305
303
  end
306
304
 
@@ -313,7 +311,11 @@ module RubyLLM
313
311
  def load_circuit_breaker_status
314
312
  return unless @agent_class.respond_to?(:reliability_config)
315
313
 
316
- config = @agent_class.reliability_config rescue nil
314
+ config = begin
315
+ @agent_class.reliability_config
316
+ rescue
317
+ nil
318
+ end
317
319
  return unless config
318
320
 
319
321
  # Collect all models: primary + fallbacks
@@ -355,7 +357,7 @@ module RubyLLM
355
357
 
356
358
  @circuit_breaker_status[model_id] = status
357
359
  end
358
- rescue StandardError => e
360
+ rescue => e
359
361
  Rails.logger.debug("[RubyLLM::Agents] Could not load circuit breaker status: #{e.message}")
360
362
  @circuit_breaker_status = {}
361
363
  end
@@ -394,7 +396,7 @@ module RubyLLM
394
396
  end
395
397
  end
396
398
 
397
- direction == "desc" ? sorted.reverse : sorted
399
+ (direction == "desc") ? sorted.reverse : sorted
398
400
  end
399
401
  end
400
402
  end
@@ -135,9 +135,9 @@ module RubyLLM
135
135
  total_cost: model_cost,
136
136
  total_tokens: model_tokens,
137
137
  avg_duration_ms: durations[model_id]&.round || 0,
138
- success_rate: count > 0 ? (successful.to_f / count * 100).round(1) : 0,
139
- cost_per_1k_tokens: model_tokens > 0 ? (model_cost / model_tokens * 1000).round(4) : 0,
140
- cost_percentage: total_cost > 0 ? (model_cost / total_cost * 100).round(1) : 0
138
+ success_rate: (count > 0) ? (successful.to_f / count * 100).round(1) : 0,
139
+ cost_per_1k_tokens: (model_tokens > 0) ? (model_cost / model_tokens * 1000).round(4) : 0,
140
+ cost_percentage: (total_cost > 0) ? (model_cost / total_cost * 100).round(1) : 0
141
141
  }
142
142
  end.sort_by { |m| -(m[:total_cost] || 0) }
143
143
  end
@@ -151,16 +151,16 @@ module RubyLLM
151
151
  total_errors = scope.count
152
152
 
153
153
  scope.group(:error_class)
154
- .select("error_class, COUNT(*) as count, MAX(created_at) as last_seen")
155
- .order("count DESC")
156
- .limit(5)
157
- .map do |row|
158
- {
159
- error_class: row.error_class || "Unknown Error",
160
- count: row.count,
161
- percentage: total_errors > 0 ? (row.count.to_f / total_errors * 100).round(1) : 0,
162
- last_seen: row.last_seen
163
- }
154
+ .select("error_class, COUNT(*) as count, MAX(created_at) as last_seen")
155
+ .order("count DESC")
156
+ .limit(5)
157
+ .map do |row|
158
+ {
159
+ error_class: row.error_class || "Unknown Error",
160
+ count: row.count,
161
+ percentage: (total_errors > 0) ? (row.count.to_f / total_errors * 100).round(1) : 0,
162
+ last_seen: row.last_seen
163
+ }
164
164
  end
165
165
  end
166
166
 
@@ -249,7 +249,7 @@ module RubyLLM
249
249
  end
250
250
 
251
251
  open_breakers
252
- rescue StandardError => e
252
+ rescue => e
253
253
  Rails.logger.debug("[RubyLLM::Agents] Error loading open breakers: #{e.message}")
254
254
  []
255
255
  end
@@ -277,8 +277,8 @@ module RubyLLM
277
277
  monthly_limit: tenant.effective_monthly_limit,
278
278
  daily_spend: daily_spend,
279
279
  monthly_spend: monthly_spend,
280
- daily_percentage: tenant.effective_daily_limit.to_f > 0 ? (daily_spend / tenant.effective_daily_limit * 100).round(1) : 0,
281
- monthly_percentage: tenant.effective_monthly_limit.to_f > 0 ? (monthly_spend / tenant.effective_monthly_limit * 100).round(1) : 0,
280
+ daily_percentage: (tenant.effective_daily_limit.to_f > 0) ? (daily_spend / tenant.effective_daily_limit * 100).round(1) : 0,
281
+ monthly_percentage: (tenant.effective_monthly_limit.to_f > 0) ? (monthly_spend / tenant.effective_monthly_limit * 100).round(1) : 0,
282
282
  enforcement: tenant.effective_enforcement,
283
283
  per_agent_daily: tenant.per_agent_daily || {}
284
284
  }
@@ -308,7 +308,7 @@ module RubyLLM
308
308
 
309
309
  # Open circuit breakers
310
310
  load_open_breakers.each do |breaker|
311
- alerts << { type: :breaker, data: breaker }
311
+ alerts << {type: :breaker, data: breaker}
312
312
  end
313
313
 
314
314
  # Budget breaches (>100% of limit)
@@ -341,7 +341,7 @@ module RubyLLM
341
341
  # Error spike detection (>5 errors in last 15 minutes)
342
342
  error_count_15m = base_scope.status_error.where("created_at > ?", 15.minutes.ago).count
343
343
  if error_count_15m >= 5
344
- alerts << { type: :error_spike, data: { count: error_count_15m } }
344
+ alerts << {type: :error_spike, data: {count: error_count_15m}}
345
345
  end
346
346
 
347
347
  alerts.take(3)
@@ -366,9 +366,9 @@ module RubyLLM
366
366
  hash[agent_type] = {
367
367
  count: count,
368
368
  total_cost: total_cost,
369
- avg_cost: count > 0 ? (total_cost / count).round(6) : 0,
369
+ avg_cost: (count > 0) ? (total_cost / count).round(6) : 0,
370
370
  avg_duration_ms: durations[agent_type]&.round || 0,
371
- success_rate: count > 0 ? (successful.to_f / count * 100).round(1) : 0
371
+ success_rate: (count > 0) ? (successful.to_f / count * 100).round(1) : 0
372
372
  }
373
373
  end
374
374
  end
@@ -17,7 +17,7 @@ module RubyLLM
17
17
  include Sortable
18
18
 
19
19
  CSV_COLUMNS = %w[id agent_type status model_id total_tokens total_cost
20
- duration_ms created_at error_class error_message].freeze
20
+ duration_ms created_at error_class error_message].freeze
21
21
 
22
22
  # Lists all executions with filtering and pagination
23
23
  #
@@ -55,7 +55,7 @@ module RubyLLM
55
55
  render turbo_stream: turbo_stream.replace(
56
56
  "executions_list",
57
57
  partial: "ruby_llm/agents/executions/list",
58
- locals: { executions: @executions, pagination: @pagination, filter_stats: @filter_stats }
58
+ locals: {executions: @executions, pagination: @pagination, filter_stats: @filter_stats}
59
59
  )
60
60
  end
61
61
  end
@@ -92,8 +92,8 @@ module RubyLLM
92
92
  # @return [String] CSV row string
93
93
  def generate_csv_row(execution)
94
94
  redacted_error_message = if execution.error_message.present?
95
- Redactor.redact_string(execution.error_message)
96
- end
95
+ Redactor.redact_string(execution.error_message)
96
+ end
97
97
 
98
98
  CSV.generate_line([
99
99
  execution.id,
@@ -207,9 +207,7 @@ module RubyLLM
207
207
  scope = scope.where(parent_execution_id: nil)
208
208
 
209
209
  # Eager load children for grouping
210
- scope = scope.includes(:child_executions)
211
-
212
- scope
210
+ scope.includes(:child_executions)
213
211
  end
214
212
  end
215
213
  end
@@ -12,7 +12,7 @@ module RubyLLM
12
12
  include Chartkick::Helper if defined?(Chartkick)
13
13
 
14
14
  # Wiki base URL for documentation links
15
- WIKI_BASE_URL = "https://github.com/adham90/ruby_llm-agents/wiki/".freeze
15
+ WIKI_BASE_URL = "https://github.com/adham90/ruby_llm-agents/wiki/"
16
16
 
17
17
  # Page to documentation mapping
18
18
  DOC_PAGES = {
@@ -176,7 +176,7 @@ module RubyLLM
176
176
  def render_sparkline(trend_data, metric_key, color_class: "text-blue-500")
177
177
  return "".html_safe if trend_data.blank? || trend_data.length < 2
178
178
 
179
- values = trend_data.map { |d| d[metric_key].to_f || 0 }
179
+ values = trend_data.map { |d| d[metric_key].to_f }
180
180
  max_val = values.max || 1
181
181
  min_val = values.min || 0
182
182
  range = max_val - min_val
@@ -197,8 +197,7 @@ module RubyLLM
197
197
  "stroke-width": "2",
198
198
  "stroke-linecap": "round",
199
199
  "stroke-linejoin": "round",
200
- class: color_class
201
- )
200
+ class: color_class)
202
201
  end
203
202
  end
204
203
 
@@ -212,27 +211,27 @@ module RubyLLM
212
211
  # @return [ActiveSupport::SafeBuffer] HTML badge element
213
212
  def comparison_badge(change_pct, metric_type)
214
213
  threshold = case metric_type
215
- when :success_rate then 5
216
- when :cost, :tokens then 15
217
- when :duration then 20
218
- when :count then 25
219
- else 10
220
- end
214
+ when :success_rate then 5
215
+ when :cost, :tokens then 15
216
+ when :duration then 20
217
+ when :count then 25
218
+ else 10
219
+ end
221
220
 
222
221
  # Determine what "improvement" means for this metric
223
222
  # For cost/tokens/duration: negative change is good (lower is better)
224
223
  # For success_rate/count: positive change is good (higher is better)
225
224
  is_improvement = case metric_type
226
- when :success_rate, :count then change_pct > threshold
227
- when :cost, :tokens, :duration then change_pct < -threshold
228
- else false
229
- end
225
+ when :success_rate, :count then change_pct > threshold
226
+ when :cost, :tokens, :duration then change_pct < -threshold
227
+ else false
228
+ end
230
229
 
231
230
  is_regression = case metric_type
232
- when :success_rate, :count then change_pct < -threshold
233
- when :cost, :tokens, :duration then change_pct > threshold
234
- else false
235
- end
231
+ when :success_rate, :count then change_pct < -threshold
232
+ when :cost, :tokens, :duration then change_pct > threshold
233
+ else false
234
+ end
236
235
 
237
236
  if is_improvement
238
237
  content_tag(:span, class: "inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium text-green-700 dark:text-green-300 bg-green-100 dark:bg-green-500/20 rounded-full") do
@@ -279,7 +278,7 @@ module RubyLLM
279
278
  positive_is_good = metric_type.in?(%i[success tokens count])
280
279
  is_improvement = positive_is_good ? change_pct > 0 : change_pct < 0
281
280
 
282
- arrow = change_pct > 0 ? "\u2191" : "\u2193"
281
+ arrow = (change_pct > 0) ? "\u2191" : "\u2193"
283
282
  color = is_improvement ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"
284
283
 
285
284
  content_tag(:span, "#{arrow}#{change_pct.abs}%", class: "text-xs font-medium #{color} ml-1")
@@ -323,24 +322,24 @@ module RubyLLM
323
322
  # @return [String] Tailwind CSS classes for row background
324
323
  def comparison_row_class(change_pct, metric_type)
325
324
  threshold = case metric_type
326
- when :success_rate then 5
327
- when :cost, :tokens then 15
328
- when :duration then 20
329
- when :count then 25
330
- else 10
331
- end
325
+ when :success_rate then 5
326
+ when :cost, :tokens then 15
327
+ when :duration then 20
328
+ when :count then 25
329
+ else 10
330
+ end
332
331
 
333
332
  is_improvement = case metric_type
334
- when :success_rate, :count then change_pct > threshold
335
- when :cost, :tokens, :duration then change_pct < -threshold
336
- else false
337
- end
333
+ when :success_rate, :count then change_pct > threshold
334
+ when :cost, :tokens, :duration then change_pct < -threshold
335
+ else false
336
+ end
338
337
 
339
338
  is_regression = case metric_type
340
- when :success_rate, :count then change_pct < -threshold
341
- when :cost, :tokens, :duration then change_pct > threshold
342
- else false
343
- end
339
+ when :success_rate, :count then change_pct < -threshold
340
+ when :cost, :tokens, :duration then change_pct > threshold
341
+ else false
342
+ end
344
343
 
345
344
  if is_improvement
346
345
  "bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800"
@@ -433,7 +432,7 @@ module RubyLLM
433
432
  str_start = i
434
433
  i += 1
435
434
  while i < chars.length
436
- if chars[i] == '\\'
435
+ if chars[i] == "\\"
437
436
  i += 2
438
437
  elsif chars[i] == '"'
439
438
  i += 1
@@ -442,49 +441,49 @@ module RubyLLM
442
441
  i += 1
443
442
  end
444
443
  end
445
- tokens << { type: :string, value: chars[str_start...i].join }
446
- when /[0-9\-]/
444
+ tokens << {type: :string, value: chars[str_start...i].join}
445
+ when /[0-9-]/
447
446
  # Number token: starts with digit or minus, continues with digits/decimals/exponents
448
447
  num_start = i
449
448
  i += 1
450
- while i < chars.length && chars[i] =~ /[0-9.eE+\-]/
449
+ while i < chars.length && chars[i] =~ /[0-9.eE+-]/
451
450
  i += 1
452
451
  end
453
- tokens << { type: :number, value: chars[num_start...i].join }
454
- when 't'
452
+ tokens << {type: :number, value: chars[num_start...i].join}
453
+ when "t"
455
454
  # Boolean token: check for "true" keyword
456
- if chars[i, 4].join == 'true'
457
- tokens << { type: :boolean, value: 'true' }
455
+ if chars[i, 4].join == "true"
456
+ tokens << {type: :boolean, value: "true"}
458
457
  i += 4
459
458
  else
460
- tokens << { type: :text, value: char }
459
+ tokens << {type: :text, value: char}
461
460
  i += 1
462
461
  end
463
- when 'f'
462
+ when "f"
464
463
  # Boolean token: check for "false" keyword
465
- if chars[i, 5].join == 'false'
466
- tokens << { type: :boolean, value: 'false' }
464
+ if chars[i, 5].join == "false"
465
+ tokens << {type: :boolean, value: "false"}
467
466
  i += 5
468
467
  else
469
- tokens << { type: :text, value: char }
468
+ tokens << {type: :text, value: char}
470
469
  i += 1
471
470
  end
472
- when 'n'
471
+ when "n"
473
472
  # Null token: check for "null" keyword
474
- if chars[i, 4].join == 'null'
475
- tokens << { type: :null, value: 'null' }
473
+ if chars[i, 4].join == "null"
474
+ tokens << {type: :null, value: "null"}
476
475
  i += 4
477
476
  else
478
- tokens << { type: :text, value: char }
477
+ tokens << {type: :text, value: char}
479
478
  i += 1
480
479
  end
481
- when ':', ',', '{', '}', '[', ']', ' ', "\n", "\t"
480
+ when ":", ",", "{", "}", "[", "]", " ", "\n", "\t"
482
481
  # Punctuation token: structural characters and whitespace
483
- tokens << { type: :punct, value: char }
482
+ tokens << {type: :punct, value: char}
484
483
  i += 1
485
484
  else
486
485
  # Fallback for unexpected characters
487
- tokens << { type: :text, value: char }
486
+ tokens << {type: :text, value: char}
488
487
  i += 1
489
488
  end
490
489
  end
@@ -503,10 +502,10 @@ module RubyLLM
503
502
  is_key = false
504
503
  (idx + 1...tokens.length).each do |j|
505
504
  if tokens[j][:type] == :punct
506
- if tokens[j][:value] == ':'
505
+ if tokens[j][:value] == ":"
507
506
  is_key = true
508
507
  break
509
- elsif tokens[j][:value] !~ /\s/
508
+ elsif !/\s/.match?(tokens[j][:value])
510
509
  # Non-whitespace punct that isn't colon - not a key
511
510
  break
512
511
  end
@@ -517,10 +516,10 @@ module RubyLLM
517
516
  end
518
517
 
519
518
  escaped_value = ERB::Util.html_escape(token[:value])
520
- if is_key
521
- result << %(<span class="text-purple-600 dark:text-purple-400">#{escaped_value}</span>)
519
+ result << if is_key
520
+ %(<span class="text-purple-600 dark:text-purple-400">#{escaped_value}</span>)
522
521
  else
523
- result << %(<span class="text-green-600 dark:text-green-400">#{escaped_value}</span>)
522
+ %(<span class="text-green-600 dark:text-green-400">#{escaped_value}</span>)
524
523
  end
525
524
  when :number
526
525
  result << %(<span class="text-blue-600 dark:text-blue-400">#{token[:value]}</span>)
@@ -72,7 +72,7 @@ module RubyLLM
72
72
  period: period,
73
73
  count: count,
74
74
  total_cost: total_cost,
75
- avg_cost: count > 0 ? (total_cost / count).round(6) : 0,
75
+ avg_cost: (count > 0) ? (total_cost / count).round(6) : 0,
76
76
  total_tokens: scope.total_tokens_sum || 0,
77
77
  avg_tokens: scope.avg_tokens&.round || 0,
78
78
  avg_duration_ms: scope.avg_duration&.round || 0,
@@ -179,7 +179,7 @@ module RubyLLM
179
179
  total_duration_count = 0
180
180
  total_tokens = 0
181
181
 
182
- (23.downto(0)).each do |hours_ago|
182
+ 23.downto(0).each do |hours_ago|
183
183
  bucket_time = (reference_time - hours_ago.hours).beginning_of_hour
184
184
  rows = results[bucket_time] || []
185
185
 
@@ -206,7 +206,7 @@ module RubyLLM
206
206
  total_duration_count += duration_rows.count
207
207
  end
208
208
 
209
- avg_duration_ms = total_duration_count > 0 ? (total_duration_sum / total_duration_count).round : 0
209
+ avg_duration_ms = (total_duration_count > 0) ? (total_duration_sum / total_duration_count).round : 0
210
210
 
211
211
  {
212
212
  range: "today",
@@ -218,11 +218,11 @@ module RubyLLM
218
218
  tokens: total_tokens
219
219
  },
220
220
  series: [
221
- { name: "Success", data: success_data },
222
- { name: "Errors", data: failed_data },
223
- { name: "Cost", data: cost_data },
224
- { name: "Duration", data: duration_data },
225
- { name: "Tokens", data: tokens_data }
221
+ {name: "Success", data: success_data},
222
+ {name: "Errors", data: failed_data},
223
+ {name: "Cost", data: cost_data},
224
+ {name: "Duration", data: duration_data},
225
+ {name: "Tokens", data: tokens_data}
226
226
  ]
227
227
  }
228
228
  end
@@ -283,7 +283,7 @@ module RubyLLM
283
283
  total_duration_count += duration_rows.count
284
284
  end
285
285
 
286
- avg_duration_ms = total_duration_count > 0 ? (total_duration_sum / total_duration_count).round : 0
286
+ avg_duration_ms = (total_duration_count > 0) ? (total_duration_sum / total_duration_count).round : 0
287
287
 
288
288
  {
289
289
  range: "#{days}d",
@@ -296,11 +296,11 @@ module RubyLLM
296
296
  tokens: total_tokens
297
297
  },
298
298
  series: [
299
- { name: "Success", data: success_data },
300
- { name: "Errors", data: failed_data },
301
- { name: "Cost", data: cost_data },
302
- { name: "Duration", data: duration_data },
303
- { name: "Tokens", data: tokens_data }
299
+ {name: "Success", data: success_data},
300
+ {name: "Errors", data: failed_data},
301
+ {name: "Cost", data: cost_data},
302
+ {name: "Duration", data: duration_data},
303
+ {name: "Tokens", data: tokens_data}
304
304
  ]
305
305
  }
306
306
  end
@@ -359,7 +359,7 @@ module RubyLLM
359
359
  total_duration_count += duration_rows.count
360
360
  end
361
361
 
362
- avg_duration_ms = total_duration_count > 0 ? (total_duration_sum / total_duration_count).round : 0
362
+ avg_duration_ms = (total_duration_count > 0) ? (total_duration_sum / total_duration_count).round : 0
363
363
 
364
364
  {
365
365
  range: "custom",
@@ -374,11 +374,11 @@ module RubyLLM
374
374
  tokens: total_tokens
375
375
  },
376
376
  series: [
377
- { name: "Success", data: success_data },
378
- { name: "Errors", data: failed_data },
379
- { name: "Cost", data: cost_data },
380
- { name: "Duration", data: duration_data },
381
- { name: "Tokens", data: tokens_data }
377
+ {name: "Success", data: success_data},
378
+ {name: "Errors", data: failed_data},
379
+ {name: "Cost", data: cost_data},
380
+ {name: "Duration", data: duration_data},
381
+ {name: "Tokens", data: tokens_data}
382
382
  ]
383
383
  }
384
384
  end
@@ -398,7 +398,7 @@ module RubyLLM
398
398
  reference_time = Time.current.beginning_of_hour
399
399
 
400
400
  # Create entries for the last 24 hours ending at current hour
401
- (23.downto(0)).each do |hours_ago|
401
+ 23.downto(0).each do |hours_ago|
402
402
  start_time = reference_time - hours_ago.hours
403
403
  end_time = start_time + 1.hour
404
404
  time_label = start_time.in_time_zone.strftime("%H:%M")
@@ -409,8 +409,8 @@ module RubyLLM
409
409
  end
410
410
 
411
411
  [
412
- { name: "Success", data: success_data },
413
- { name: "Failed", data: failed_data }
412
+ {name: "Success", data: success_data},
413
+ {name: "Failed", data: failed_data}
414
414
  ]
415
415
  end
416
416
 
@@ -447,8 +447,8 @@ module RubyLLM
447
447
  end
448
448
 
449
449
  [
450
- { name: "Input Cost", data: input_cost_data },
451
- { name: "Output Cost", data: output_cost_data }
450
+ {name: "Input Cost", data: input_cost_data},
451
+ {name: "Output Cost", data: output_cost_data}
452
452
  ]
453
453
  end
454
454
 
@@ -514,7 +514,7 @@ module RubyLLM
514
514
  (rate_limited_count.to_f / total * 100).round(1)
515
515
  end
516
516
 
517
- private
517
+ private
518
518
 
519
519
  # Calculates success rate percentage for a scope
520
520
  #
@@ -547,7 +547,7 @@ module RubyLLM
547
547
  {
548
548
  count: count,
549
549
  total_cost: total_cost,
550
- avg_cost: count > 0 ? (total_cost / count).round(6) : 0,
550
+ avg_cost: (count > 0) ? (total_cost / count).round(6) : 0,
551
551
  avg_tokens: scope.avg_tokens&.round || 0,
552
552
  avg_duration_ms: scope.avg_duration&.round || 0,
553
553
  success_rate: calculate_success_rate(scope)
@@ -149,12 +149,10 @@ module RubyLLM
149
149
  else
150
150
  joined.where("json_extract(#{detail_table}.parameters, ?) IS NOT NULL", "$.#{key}")
151
151
  end
152
+ elsif value
153
+ joined.where("#{detail_table}.parameters @> ?", {key => value}.to_json)
152
154
  else
153
- if value
154
- joined.where("#{detail_table}.parameters @> ?", { key => value }.to_json)
155
- else
156
- joined.where("#{detail_table}.parameters ? :key", key: key.to_s)
157
- end
155
+ joined.where("#{detail_table}.parameters ? :key", key: key.to_s)
158
156
  end
159
157
  end
160
158
 
@@ -299,7 +297,7 @@ module RubyLLM
299
297
  if connection.adapter_name.downcase.include?("sqlite")
300
298
  where("json_extract(metadata, ?) = 1", "$.#{key}")
301
299
  else
302
- where("metadata @> ?", { key.to_s => true }.to_json)
300
+ where("metadata @> ?", {key.to_s => true}.to_json)
303
301
  end
304
302
  end
305
303