ruby_llm-agents 3.7.2 → 3.8.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/ruby_llm/agents/agents_controller.rb +14 -141
  3. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +12 -166
  4. data/app/controllers/ruby_llm/agents/executions_controller.rb +1 -1
  5. data/app/helpers/ruby_llm/agents/application_helper.rb +38 -0
  6. data/app/models/ruby_llm/agents/execution/analytics.rb +302 -103
  7. data/app/models/ruby_llm/agents/execution.rb +76 -54
  8. data/app/models/ruby_llm/agents/execution_detail.rb +2 -0
  9. data/app/models/ruby_llm/agents/tenant.rb +39 -0
  10. data/app/services/ruby_llm/agents/agent_registry.rb +98 -0
  11. data/app/views/ruby_llm/agents/executions/_list.html.erb +3 -17
  12. data/lib/generators/ruby_llm_agents/templates/add_dashboard_performance_indexes_migration.rb.tt +23 -0
  13. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +3 -0
  14. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +25 -0
  15. data/lib/ruby_llm/agents/base_agent.rb +7 -1
  16. data/lib/ruby_llm/agents/core/configuration.rb +1 -0
  17. data/lib/ruby_llm/agents/core/instrumentation.rb +15 -19
  18. data/lib/ruby_llm/agents/core/version.rb +1 -1
  19. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +4 -4
  20. data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +19 -11
  21. data/lib/ruby_llm/agents/pipeline/builder.rb +8 -4
  22. data/lib/ruby_llm/agents/pipeline/context.rb +43 -1
  23. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +6 -4
  24. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +6 -4
  25. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +26 -75
  26. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +6 -6
  27. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +23 -27
  28. data/lib/ruby_llm/agents/providers/inception/capabilities.rb +107 -0
  29. data/lib/ruby_llm/agents/providers/inception/chat.rb +17 -0
  30. data/lib/ruby_llm/agents/providers/inception/configuration.rb +9 -0
  31. data/lib/ruby_llm/agents/providers/inception/models.rb +38 -0
  32. data/lib/ruby_llm/agents/providers/inception/registry.rb +45 -0
  33. data/lib/ruby_llm/agents/providers/inception.rb +50 -0
  34. data/lib/ruby_llm/agents/results/base.rb +4 -2
  35. data/lib/ruby_llm/agents/results/image_analysis_result.rb +4 -2
  36. data/lib/ruby_llm/agents/text/embedder.rb +4 -0
  37. data/lib/ruby_llm/agents.rb +4 -0
  38. metadata +8 -1
@@ -269,11 +269,14 @@ module RubyLLM
269
269
  # Returns whether this execution made tool calls
270
270
  #
271
271
  # @return [Boolean] true if tool calls were made
272
- def has_tool_calls?
272
+ def tool_calls?
273
273
  tool_calls_count.to_i > 0
274
274
  end
275
+ alias_method :has_tool_calls?, :tool_calls?
275
276
 
276
277
  # Returns real-time dashboard data for the Now Strip
278
+ # Optimized: 3 queries (current aggregate + previous aggregate + running count)
279
+ # instead of ~15 individual count/sum/average queries.
277
280
  #
278
281
  # @param range [String] Time range: "today", "7d", "30d", or "90d"
279
282
  # @return [Hash] Now strip metrics with period-over-period comparisons
@@ -292,38 +295,31 @@ module RubyLLM
292
295
  else yesterday
293
296
  end
294
297
 
295
- current = {
296
- running: running.count,
297
- success_today: current_scope.status_success.count,
298
- errors_today: current_scope.status_error.count,
299
- timeouts_today: current_scope.status_timeout.count,
300
- cost_today: current_scope.sum(:total_cost) || 0,
301
- executions_today: current_scope.count,
302
- success_rate: calculate_period_success_rate(current_scope),
303
- avg_duration_ms: current_scope.avg_duration&.round || 0,
304
- total_tokens: current_scope.total_tokens_sum || 0
305
- }
306
-
307
- previous = {
308
- success: previous_scope.status_success.count,
309
- errors: previous_scope.status_error.count,
310
- cost: previous_scope.sum(:total_cost) || 0,
311
- avg_duration_ms: previous_scope.avg_duration&.round || 0,
312
- total_tokens: previous_scope.total_tokens_sum || 0
313
- }
298
+ curr = aggregate_period_stats(current_scope)
299
+ prev = aggregate_period_stats(previous_scope)
314
300
 
315
- current.merge(
301
+ {
302
+ running: running.count,
303
+ success_today: curr[:success],
304
+ errors_today: curr[:errors],
305
+ timeouts_today: curr[:timeouts],
306
+ cost_today: curr[:cost],
307
+ executions_today: curr[:total],
308
+ success_rate: curr[:success_rate],
309
+ avg_duration_ms: curr[:avg_duration_ms],
310
+ total_tokens: curr[:tokens],
316
311
  comparisons: {
317
- success_change: pct_change(previous[:success], current[:success_today]),
318
- errors_change: pct_change(previous[:errors], current[:errors_today]),
319
- cost_change: pct_change(previous[:cost], current[:cost_today]),
320
- duration_change: pct_change(previous[:avg_duration_ms], current[:avg_duration_ms]),
321
- tokens_change: pct_change(previous[:total_tokens], current[:total_tokens])
312
+ success_change: pct_change(prev[:success], curr[:success]),
313
+ errors_change: pct_change(prev[:errors], curr[:errors]),
314
+ cost_change: pct_change(prev[:cost], curr[:cost]),
315
+ duration_change: pct_change(prev[:avg_duration_ms], curr[:avg_duration_ms]),
316
+ tokens_change: pct_change(prev[:tokens], curr[:tokens])
322
317
  }
323
- )
318
+ }
324
319
  end
325
320
 
326
321
  # Returns Now Strip data for a custom date range
322
+ # Optimized: 3 queries instead of ~15.
327
323
  #
328
324
  # Compares the selected range against the same-length window
329
325
  # immediately preceding it.
@@ -338,35 +334,27 @@ module RubyLLM
338
334
  previous_to = from - 1.day
339
335
  previous_scope = where(created_at: previous_from.beginning_of_day..previous_to.end_of_day)
340
336
 
341
- current = {
342
- running: running.count,
343
- success_today: current_scope.status_success.count,
344
- errors_today: current_scope.status_error.count,
345
- timeouts_today: current_scope.status_timeout.count,
346
- cost_today: current_scope.sum(:total_cost) || 0,
347
- executions_today: current_scope.count,
348
- success_rate: calculate_period_success_rate(current_scope),
349
- avg_duration_ms: current_scope.avg_duration&.round || 0,
350
- total_tokens: current_scope.total_tokens_sum || 0
351
- }
352
-
353
- previous = {
354
- success: previous_scope.status_success.count,
355
- errors: previous_scope.status_error.count,
356
- cost: previous_scope.sum(:total_cost) || 0,
357
- avg_duration_ms: previous_scope.avg_duration&.round || 0,
358
- total_tokens: previous_scope.total_tokens_sum || 0
359
- }
337
+ curr = aggregate_period_stats(current_scope)
338
+ prev = aggregate_period_stats(previous_scope)
360
339
 
361
- current.merge(
340
+ {
341
+ running: running.count,
342
+ success_today: curr[:success],
343
+ errors_today: curr[:errors],
344
+ timeouts_today: curr[:timeouts],
345
+ cost_today: curr[:cost],
346
+ executions_today: curr[:total],
347
+ success_rate: curr[:success_rate],
348
+ avg_duration_ms: curr[:avg_duration_ms],
349
+ total_tokens: curr[:tokens],
362
350
  comparisons: {
363
- success_change: pct_change(previous[:success], current[:success_today]),
364
- errors_change: pct_change(previous[:errors], current[:errors_today]),
365
- cost_change: pct_change(previous[:cost], current[:cost_today]),
366
- duration_change: pct_change(previous[:avg_duration_ms], current[:avg_duration_ms]),
367
- tokens_change: pct_change(previous[:total_tokens], current[:total_tokens])
351
+ success_change: pct_change(prev[:success], curr[:success]),
352
+ errors_change: pct_change(prev[:errors], curr[:errors]),
353
+ cost_change: pct_change(prev[:cost], curr[:cost]),
354
+ duration_change: pct_change(prev[:avg_duration_ms], curr[:avg_duration_ms]),
355
+ tokens_change: pct_change(prev[:tokens], curr[:tokens])
368
356
  }
369
- )
357
+ }
370
358
  end
371
359
 
372
360
  # Calculates percentage change between old and new values
@@ -390,6 +378,39 @@ module RubyLLM
390
378
  (scope.successful.count.to_f / total * 100).round(1)
391
379
  end
392
380
 
381
+ # Returns aggregate stats for a scope in a single query using conditional aggregation
382
+ #
383
+ # Replaces ~9 individual count/sum/average queries with one SQL query.
384
+ #
385
+ # @param scope [ActiveRecord::Relation] Time-filtered scope
386
+ # @return [Hash] Aggregated metrics
387
+ def self.aggregate_period_stats(scope)
388
+ total, success, errors, timeouts, cost, avg_dur, tokens = scope.pick(
389
+ Arel.sql("COUNT(*)"),
390
+ Arel.sql("SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END)"),
391
+ Arel.sql("SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END)"),
392
+ Arel.sql("SUM(CASE WHEN status = 'timeout' THEN 1 ELSE 0 END)"),
393
+ Arel.sql("COALESCE(SUM(total_cost), 0)"),
394
+ Arel.sql("AVG(duration_ms)"),
395
+ Arel.sql("COALESCE(SUM(total_tokens), 0)")
396
+ )
397
+
398
+ total = total.to_i
399
+ success = success.to_i
400
+
401
+ {
402
+ total: total,
403
+ success: success,
404
+ errors: errors.to_i,
405
+ timeouts: timeouts.to_i,
406
+ cost: cost.to_f,
407
+ avg_duration_ms: avg_dur.to_i,
408
+ tokens: tokens.to_i,
409
+ success_rate: (total > 0) ? (success.to_f / total * 100).round(1) : 0.0
410
+ }
411
+ end
412
+ private_class_method :aggregate_period_stats
413
+
393
414
  private
394
415
 
395
416
  # Calculates and sets total_tokens from input and output
@@ -420,7 +441,8 @@ module RubyLLM
420
441
  return nil unless lookup_model_id
421
442
 
422
443
  RubyLLM::Models.find(lookup_model_id)
423
- rescue
444
+ rescue => e
445
+ Rails.logger.debug("[RubyLLM::Agents] Model lookup failed for #{lookup_model_id}: #{e.message}") if defined?(Rails) && Rails.logger
424
446
  nil
425
447
  end
426
448
  end
@@ -13,6 +13,8 @@ module RubyLLM
13
13
  self.table_name = "ruby_llm_agents_execution_details"
14
14
 
15
15
  belongs_to :execution, class_name: "RubyLLM::Agents::Execution"
16
+
17
+ validates :execution_id, presence: true
16
18
  end
17
19
  end
18
20
  end
@@ -53,6 +53,45 @@ module RubyLLM
53
53
  scope :linked, -> { where.not(tenant_record_type: nil) }
54
54
  scope :unlinked, -> { where(tenant_record_type: nil) }
55
55
 
56
+ # Returns top tenants by monthly spend for dashboard display
57
+ #
58
+ # Ensures counter resets are current before returning data.
59
+ #
60
+ # @param limit [Integer] Max tenants to return
61
+ # @return [Array<Hash>, nil] Tenant spend data or nil if none
62
+ def self.top_by_spend(limit: 5)
63
+ return nil unless table_exists?
64
+
65
+ tenants = active
66
+ .where("monthly_cost_spent > 0 OR monthly_executions_count > 0")
67
+ .order(monthly_cost_spent: :desc)
68
+ .limit(limit)
69
+
70
+ return nil if tenants.empty?
71
+
72
+ tenants.map do |tenant|
73
+ tenant.ensure_daily_reset!
74
+ tenant.ensure_monthly_reset!
75
+
76
+ monthly_limit = tenant.effective_monthly_limit
77
+ daily_limit = tenant.effective_daily_limit
78
+
79
+ {
80
+ id: tenant.id,
81
+ tenant_id: tenant.tenant_id,
82
+ name: tenant.display_name,
83
+ enforcement: tenant.effective_enforcement,
84
+ monthly_spend: tenant.monthly_cost_spent,
85
+ monthly_limit: monthly_limit,
86
+ monthly_percentage: (monthly_limit.to_f > 0) ? (tenant.monthly_cost_spent / monthly_limit * 100).round(1) : 0,
87
+ daily_spend: tenant.daily_cost_spent,
88
+ daily_limit: daily_limit,
89
+ daily_percentage: (daily_limit.to_f > 0) ? (tenant.daily_cost_spent / daily_limit * 100).round(1) : 0,
90
+ monthly_executions: tenant.monthly_executions_count
91
+ }
92
+ end
93
+ end
94
+
56
95
  # Find tenant for given record or ID
57
96
  #
58
97
  # Supports multiple lookup strategies:
@@ -54,8 +54,106 @@ module RubyLLM
54
54
  end
55
55
  end
56
56
 
57
+ # Extracts full configuration for an agent class
58
+ #
59
+ # Combines base config with type-specific config for display.
60
+ #
61
+ # @param agent_class [Class] The agent class
62
+ # @return [Hash] Configuration hash
63
+ def config_for(agent_class)
64
+ return {} unless agent_class
65
+
66
+ base = {
67
+ model: safe_call(agent_class, :model),
68
+ version: safe_call(agent_class, :version),
69
+ description: safe_call(agent_class, :description)
70
+ }
71
+
72
+ type = detect_agent_type(agent_class)
73
+ base.merge(type_config_for(agent_class, type))
74
+ end
75
+
57
76
  private
58
77
 
78
+ # Extracts type-specific configuration
79
+ #
80
+ # @param agent_class [Class] The agent class
81
+ # @param type [String] The detected agent type
82
+ # @return [Hash] Type-specific config
83
+ def type_config_for(agent_class, type)
84
+ case type
85
+ when "embedder"
86
+ {
87
+ dimensions: safe_call(agent_class, :dimensions),
88
+ batch_size: safe_call(agent_class, :batch_size),
89
+ cache_enabled: safe_call(agent_class, :cache_enabled?) || false,
90
+ cache_ttl: safe_call(agent_class, :cache_ttl)
91
+ }
92
+ when "speaker"
93
+ {
94
+ provider: safe_call(agent_class, :provider),
95
+ voice: safe_call(agent_class, :voice),
96
+ voice_id: safe_call(agent_class, :voice_id),
97
+ speed: safe_call(agent_class, :speed),
98
+ output_format: safe_call(agent_class, :output_format),
99
+ streaming: safe_call(agent_class, :streaming?),
100
+ ssml_enabled: safe_call(agent_class, :ssml_enabled?),
101
+ cache_enabled: safe_call(agent_class, :cache_enabled?) || false,
102
+ cache_ttl: safe_call(agent_class, :cache_ttl)
103
+ }
104
+ when "transcriber"
105
+ {
106
+ language: safe_call(agent_class, :language),
107
+ output_format: safe_call(agent_class, :output_format),
108
+ include_timestamps: safe_call(agent_class, :include_timestamps),
109
+ cache_enabled: safe_call(agent_class, :cache_enabled?) || false,
110
+ cache_ttl: safe_call(agent_class, :cache_ttl),
111
+ fallback_models: safe_call(agent_class, :fallback_models)
112
+ }
113
+ when "image_generator"
114
+ {
115
+ size: safe_call(agent_class, :size),
116
+ quality: safe_call(agent_class, :quality),
117
+ style: safe_call(agent_class, :style),
118
+ content_policy: safe_call(agent_class, :content_policy),
119
+ template: safe_call(agent_class, :template_string),
120
+ negative_prompt: safe_call(agent_class, :negative_prompt),
121
+ seed: safe_call(agent_class, :seed),
122
+ guidance_scale: safe_call(agent_class, :guidance_scale),
123
+ steps: safe_call(agent_class, :steps),
124
+ cache_enabled: safe_call(agent_class, :cache_enabled?) || false,
125
+ cache_ttl: safe_call(agent_class, :cache_ttl)
126
+ }
127
+ when "router"
128
+ routes = safe_call(agent_class, :routes) || {}
129
+ {
130
+ temperature: safe_call(agent_class, :temperature),
131
+ timeout: safe_call(agent_class, :timeout),
132
+ cache_enabled: safe_call(agent_class, :cache_enabled?) || false,
133
+ cache_ttl: safe_call(agent_class, :cache_ttl),
134
+ default_route: safe_call(agent_class, :default_route_name),
135
+ routes: routes.transform_values { |v| v[:description] },
136
+ route_count: routes.size,
137
+ retries: safe_call(agent_class, :retries),
138
+ fallback_models: safe_call(agent_class, :fallback_models),
139
+ total_timeout: safe_call(agent_class, :total_timeout),
140
+ circuit_breaker: safe_call(agent_class, :circuit_breaker_config)
141
+ }
142
+ else # base agent
143
+ {
144
+ temperature: safe_call(agent_class, :temperature),
145
+ timeout: safe_call(agent_class, :timeout),
146
+ cache_enabled: safe_call(agent_class, :cache_enabled?) || false,
147
+ cache_ttl: safe_call(agent_class, :cache_ttl),
148
+ params: safe_call(agent_class, :params) || {},
149
+ retries: safe_call(agent_class, :retries),
150
+ fallback_models: safe_call(agent_class, :fallback_models),
151
+ total_timeout: safe_call(agent_class, :total_timeout),
152
+ circuit_breaker: safe_call(agent_class, :circuit_breaker_config)
153
+ }
154
+ end
155
+ end
156
+
59
157
  # Finds agent classes from the file system
60
158
  #
61
159
  # @return [Array<String>] Agent class names
@@ -2,26 +2,12 @@
2
2
  <% show_tenant_column = tenant_filter_enabled? && current_tenant_id.blank? %>
3
3
 
4
4
  <%
5
- # Inline sort helper
5
+ # Sort params for the sort_header_link helper
6
6
  sort_column = @sort_params[:column]
7
7
  sort_direction = @sort_params[:direction]
8
8
 
9
- sort_link = ->(column, label, align: "left", extra_class: "") {
10
- is_active = column == sort_column
11
- next_dir = is_active && sort_direction == "asc" ? "desc" : "asc"
12
- url = url_for(request.query_parameters.merge(sort: column, direction: next_dir, page: 1))
13
-
14
- arrow = if is_active && sort_direction == "asc"
15
- raw('<svg class="w-2.5 h-2.5 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"/></svg>')
16
- elsif is_active
17
- raw('<svg class="w-2.5 h-2.5 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>')
18
- else
19
- ""
20
- end
21
-
22
- active_class = is_active ? "text-gray-700 dark:text-gray-300" : ""
23
-
24
- raw(%(<a href="#{url}" class="group inline-flex items-center gap-0.5 hover:text-gray-700 dark:hover:text-gray-300 #{active_class} #{extra_class}"><span>#{label}</span><span class="#{is_active ? 'opacity-100' : 'opacity-0 group-hover:opacity-50'} transition-opacity">#{arrow}</span></a>))
9
+ sort_link = ->(column, label, extra_class: "") {
10
+ sort_header_link(column, label, current_column: sort_column, current_direction: sort_direction, extra_class: extra_class)
25
11
  }
26
12
  %>
27
13
 
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Migration to add composite indexes for dashboard query performance
4
+ #
5
+ # These indexes optimize the most frequent dashboard queries:
6
+ # - [status, created_at]: now_strip_data conditional counts, error spike detection, top_errors
7
+ # - [model_id, status]: model_stats GROUP BY with success count
8
+ # - [cache_hit, created_at]: cache_savings queries
9
+ class AddDashboardPerformanceIndexes < ActiveRecord::Migration<%= migration_version %>
10
+ def change
11
+ add_index :ruby_llm_agents_executions, [:status, :created_at],
12
+ name: "idx_executions_status_created_at",
13
+ if_not_exists: true
14
+
15
+ add_index :ruby_llm_agents_executions, [:model_id, :status],
16
+ name: "idx_executions_model_id_status",
17
+ if_not_exists: true
18
+
19
+ add_index :ruby_llm_agents_executions, [:cache_hit, :created_at],
20
+ name: "idx_executions_cache_hit_created_at",
21
+ if_not_exists: true
22
+ end
23
+ end
@@ -77,6 +77,9 @@ class CreateRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_versi
77
77
  add_index :ruby_llm_agents_executions, :request_id
78
78
  add_index :ruby_llm_agents_executions, :parent_execution_id
79
79
  add_index :ruby_llm_agents_executions, :root_execution_id
80
+ add_index :ruby_llm_agents_executions, [:status, :created_at]
81
+ add_index :ruby_llm_agents_executions, [:model_id, :status]
82
+ add_index :ruby_llm_agents_executions, [:cache_hit, :created_at]
80
83
 
81
84
  # Foreign keys for execution hierarchy
82
85
  add_foreign_key :ruby_llm_agents_executions, :ruby_llm_agents_executions,
@@ -98,6 +98,25 @@ module RubyLlmAgents
98
98
  )
99
99
  end
100
100
 
101
+ # Add dashboard performance indexes
102
+ def create_add_dashboard_performance_indexes_migration
103
+ unless table_exists?(:ruby_llm_agents_executions)
104
+ say_status :skip, "executions table does not exist yet", :yellow
105
+ return
106
+ end
107
+
108
+ if index_exists?(:ruby_llm_agents_executions, [:status, :created_at])
109
+ say_status :skip, "dashboard performance indexes already exist", :yellow
110
+ return
111
+ end
112
+
113
+ say_status :upgrade, "Adding dashboard performance indexes", :blue
114
+ migration_template(
115
+ "add_dashboard_performance_indexes_migration.rb.tt",
116
+ File.join(db_migrate_path, "add_dashboard_performance_indexes.rb")
117
+ )
118
+ end
119
+
101
120
  def suggest_config_consolidation
102
121
  ruby_llm_initializer = File.join(destination_root, "config/initializers/ruby_llm.rb")
103
122
  agents_initializer = File.join(destination_root, "config/initializers/ruby_llm_agents.rb")
@@ -192,5 +211,11 @@ module RubyLlmAgents
192
211
  rescue
193
212
  false
194
213
  end
214
+
215
+ def index_exists?(table, columns)
216
+ ActiveRecord::Base.connection.index_exists?(table, columns)
217
+ rescue
218
+ false
219
+ end
195
220
  end
196
221
  end
@@ -731,7 +731,13 @@ module RubyLLM
731
731
  # @return [RubyLLM::Chat] Configured chat client
732
732
  def build_client(context = nil)
733
733
  effective_model = context&.model || model
734
- client = RubyLLM.chat(model: effective_model)
734
+ chat_opts = {model: effective_model}
735
+
736
+ # Pass scoped RubyLLM context for thread-safe per-tenant API keys
737
+ llm_ctx = context&.llm
738
+ chat_opts[:context] = llm_ctx if llm_ctx.is_a?(RubyLLM::Context)
739
+
740
+ client = RubyLLM.chat(**chat_opts)
735
741
  .with_temperature(temperature)
736
742
 
737
743
  client = client.with_instructions(system_prompt) if system_prompt
@@ -349,6 +349,7 @@ module RubyLLM
349
349
  perplexity_api_key
350
350
  xai_api_key
351
351
  gpustack_api_key
352
+ inception_api_key
352
353
  openai_api_base
353
354
  openai_organization_id
354
355
  openai_project_id
@@ -2,31 +2,27 @@
2
2
 
3
3
  module RubyLLM
4
4
  module Agents
5
- # Instrumentation concern for tracking agent executions
5
+ # @deprecated This module is deprecated and will be removed in a future version.
6
+ # All agents now use {Pipeline::Middleware::Instrumentation} automatically
7
+ # via the middleware pipeline. This module is no longer included in any
8
+ # production class. It remains only for backward compatibility with code
9
+ # that explicitly includes it.
6
10
  #
7
- # Provides comprehensive execution tracking including:
8
- # - Timing metrics (started_at, completed_at, duration_ms)
9
- # - Token usage tracking (input, output, cached)
10
- # - Cost calculation via RubyLLM pricing data
11
- # - Error and timeout handling with status tracking
12
- # - Safe parameter sanitization for logging
13
- #
14
- # Included automatically in {RubyLLM::Agents::Base}.
15
- #
16
- # @example Adding custom metadata to executions
17
- # class MyAgent < ApplicationAgent
18
- # def metadata
19
- # { user_id: Current.user&.id, request_id: request.uuid }
20
- # end
21
- # end
22
- #
23
- # @see RubyLLM::Agents::Execution
24
- # @see RubyLLM::Agents::ExecutionLoggerJob
11
+ # @see Pipeline::Middleware::Instrumentation
25
12
  # @api private
26
13
  module Instrumentation
27
14
  extend ActiveSupport::Concern
28
15
 
29
16
  included do
17
+ if defined?(RubyLLM::Agents::Deprecations)
18
+ RubyLLM::Agents::Deprecations.warn(
19
+ "RubyLLM::Agents::Instrumentation is deprecated. " \
20
+ "All agents now use Pipeline::Middleware::Instrumentation automatically. " \
21
+ "Remove `include RubyLLM::Agents::Instrumentation` from #{name || "your class"}.",
22
+ caller
23
+ )
24
+ end
25
+
30
26
  # @!attribute [rw] execution_id
31
27
  # The ID of the current execution record
32
28
  # @return [Integer, nil]
@@ -4,6 +4,6 @@ module RubyLLM
4
4
  module Agents
5
5
  # Current version of the RubyLLM::Agents gem
6
6
  # @return [String] Semantic version string
7
- VERSION = "3.7.2"
7
+ VERSION = "3.8.0"
8
8
  end
9
9
  end
@@ -83,8 +83,8 @@ module RubyLLM
83
83
  # @return [void]
84
84
  def emit_notification(event, payload)
85
85
  ActiveSupport::Notifications.instrument("ruby_llm_agents.alert.#{event}", payload)
86
- rescue
87
- # Ignore notification failures
86
+ rescue => e
87
+ Rails.logger.debug("[RubyLLM::Agents::AlertManager] Notification failed: #{e.message}") if defined?(Rails) && Rails.logger
88
88
  end
89
89
 
90
90
  # Stores the alert in cache for dashboard display
@@ -106,8 +106,8 @@ module RubyLLM
106
106
  alerts = alerts.first(50)
107
107
 
108
108
  cache.write(key, alerts, expires_in: 24.hours)
109
- rescue
110
- # Ignore cache failures
109
+ rescue => e
110
+ Rails.logger.debug("[RubyLLM::Agents::AlertManager] Cache store failed: #{e.message}") if defined?(Rails) && Rails.logger
111
111
  end
112
112
 
113
113
  # Formats a human-readable message for the event
@@ -44,7 +44,7 @@ module RubyLLM
44
44
  # @raise [Reliability::BudgetExceededError] If hard cap is exceeded
45
45
  # @return [void]
46
46
  def check_budget!(agent_type, tenant_id: nil, tenant_config: nil)
47
- tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
47
+ tenant_id = resolve_tid(tenant_id)
48
48
  budget_config = Budget::ConfigResolver.resolve_budget_config(tenant_id, runtime_config: tenant_config)
49
49
 
50
50
  return unless budget_config[:enabled]
@@ -61,7 +61,7 @@ module RubyLLM
61
61
  # @raise [Reliability::BudgetExceededError] If hard cap is exceeded
62
62
  # @return [void]
63
63
  def check_token_budget!(agent_type, tenant_id: nil, tenant_config: nil)
64
- tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
64
+ tenant_id = resolve_tid(tenant_id)
65
65
  budget_config = Budget::ConfigResolver.resolve_budget_config(tenant_id, runtime_config: tenant_config)
66
66
 
67
67
  return unless budget_config[:enabled]
@@ -80,7 +80,7 @@ module RubyLLM
80
80
  def record_spend!(agent_type, amount, tenant_id: nil, tenant_config: nil)
81
81
  return if amount.nil? || amount <= 0
82
82
 
83
- tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
83
+ tenant_id = resolve_tid(tenant_id)
84
84
  budget_config = Budget::ConfigResolver.resolve_budget_config(tenant_id, runtime_config: tenant_config)
85
85
 
86
86
  Budget::SpendRecorder.record_spend!(agent_type, amount, tenant_id: tenant_id, budget_config: budget_config)
@@ -96,7 +96,7 @@ module RubyLLM
96
96
  def record_tokens!(agent_type, tokens, tenant_id: nil, tenant_config: nil)
97
97
  return if tokens.nil? || tokens <= 0
98
98
 
99
- tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
99
+ tenant_id = resolve_tid(tenant_id)
100
100
  budget_config = Budget::ConfigResolver.resolve_budget_config(tenant_id, runtime_config: tenant_config)
101
101
 
102
102
  Budget::SpendRecorder.record_tokens!(agent_type, tokens, tenant_id: tenant_id, budget_config: budget_config)
@@ -110,7 +110,7 @@ module RubyLLM
110
110
  # @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
111
111
  # @return [Float] Current spend in USD
112
112
  def current_spend(scope, period, agent_type: nil, tenant_id: nil)
113
- tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
113
+ tenant_id = resolve_tid(tenant_id)
114
114
  Budget::BudgetQuery.current_spend(scope, period, agent_type: agent_type, tenant_id: tenant_id)
115
115
  end
116
116
 
@@ -120,7 +120,7 @@ module RubyLLM
120
120
  # @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
121
121
  # @return [Integer] Current token usage
122
122
  def current_tokens(period, tenant_id: nil)
123
- tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
123
+ tenant_id = resolve_tid(tenant_id)
124
124
  Budget::BudgetQuery.current_tokens(period, tenant_id: tenant_id)
125
125
  end
126
126
 
@@ -132,7 +132,7 @@ module RubyLLM
132
132
  # @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
133
133
  # @return [Float, nil] Remaining budget in USD, or nil if no limit configured
134
134
  def remaining_budget(scope, period, agent_type: nil, tenant_id: nil)
135
- tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
135
+ tenant_id = resolve_tid(tenant_id)
136
136
  budget_config = Budget::ConfigResolver.resolve_budget_config(tenant_id)
137
137
 
138
138
  Budget::BudgetQuery.remaining_budget(scope, period, agent_type: agent_type, tenant_id: tenant_id, budget_config: budget_config)
@@ -144,7 +144,7 @@ module RubyLLM
144
144
  # @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
145
145
  # @return [Integer, nil] Remaining token budget, or nil if no limit configured
146
146
  def remaining_token_budget(period, tenant_id: nil)
147
- tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
147
+ tenant_id = resolve_tid(tenant_id)
148
148
  budget_config = Budget::ConfigResolver.resolve_budget_config(tenant_id)
149
149
 
150
150
  Budget::BudgetQuery.remaining_token_budget(period, tenant_id: tenant_id, budget_config: budget_config)
@@ -156,7 +156,7 @@ module RubyLLM
156
156
  # @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
157
157
  # @return [Hash] Budget status information
158
158
  def status(agent_type: nil, tenant_id: nil)
159
- tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
159
+ tenant_id = resolve_tid(tenant_id)
160
160
  budget_config = Budget::ConfigResolver.resolve_budget_config(tenant_id)
161
161
 
162
162
  Budget::BudgetQuery.status(agent_type: agent_type, tenant_id: tenant_id, budget_config: budget_config)
@@ -167,7 +167,7 @@ module RubyLLM
167
167
  # @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
168
168
  # @return [Hash, nil] Forecast information
169
169
  def calculate_forecast(tenant_id: nil)
170
- tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
170
+ tenant_id = resolve_tid(tenant_id)
171
171
  budget_config = Budget::ConfigResolver.resolve_budget_config(tenant_id)
172
172
 
173
173
  Budget::Forecaster.calculate_forecast(tenant_id: tenant_id, budget_config: budget_config)
@@ -178,7 +178,7 @@ module RubyLLM
178
178
  # @param tenant_id [String, nil] Optional tenant identifier to reset only that tenant's counters
179
179
  # @return [void]
180
180
  def reset!(tenant_id: nil)
181
- tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
181
+ tenant_id = resolve_tid(tenant_id)
182
182
  tenant_part = Budget::SpendRecorder.tenant_key_part(tenant_id)
183
183
  today = Budget::SpendRecorder.date_key_part(:daily)
184
184
  month = Budget::SpendRecorder.date_key_part(:monthly)
@@ -192,6 +192,14 @@ module RubyLLM
192
192
 
193
193
  private
194
194
 
195
+ # Resolves tenant ID, falling back to the configured resolver
196
+ #
197
+ # @param tenant_id [String, nil] Explicit tenant ID or nil
198
+ # @return [String, nil] Resolved tenant ID
199
+ def resolve_tid(tenant_id)
200
+ Budget::ConfigResolver.resolve_tenant_id(tenant_id)
201
+ end
202
+
195
203
  # Checks budget limits and raises error if exceeded
196
204
  #
197
205
  # @param agent_type [String] The agent class name