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.
- checksums.yaml +4 -4
- data/app/controllers/ruby_llm/agents/agents_controller.rb +14 -141
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +12 -166
- data/app/controllers/ruby_llm/agents/executions_controller.rb +1 -1
- data/app/helpers/ruby_llm/agents/application_helper.rb +38 -0
- data/app/models/ruby_llm/agents/execution/analytics.rb +302 -103
- data/app/models/ruby_llm/agents/execution.rb +76 -54
- data/app/models/ruby_llm/agents/execution_detail.rb +2 -0
- data/app/models/ruby_llm/agents/tenant.rb +39 -0
- data/app/services/ruby_llm/agents/agent_registry.rb +98 -0
- data/app/views/ruby_llm/agents/executions/_list.html.erb +3 -17
- data/lib/generators/ruby_llm_agents/templates/add_dashboard_performance_indexes_migration.rb.tt +23 -0
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +3 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +25 -0
- data/lib/ruby_llm/agents/base_agent.rb +7 -1
- data/lib/ruby_llm/agents/core/configuration.rb +1 -0
- data/lib/ruby_llm/agents/core/instrumentation.rb +15 -19
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +4 -4
- data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +19 -11
- data/lib/ruby_llm/agents/pipeline/builder.rb +8 -4
- data/lib/ruby_llm/agents/pipeline/context.rb +43 -1
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +6 -4
- data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +6 -4
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +26 -75
- data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +6 -6
- data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +23 -27
- data/lib/ruby_llm/agents/providers/inception/capabilities.rb +107 -0
- data/lib/ruby_llm/agents/providers/inception/chat.rb +17 -0
- data/lib/ruby_llm/agents/providers/inception/configuration.rb +9 -0
- data/lib/ruby_llm/agents/providers/inception/models.rb +38 -0
- data/lib/ruby_llm/agents/providers/inception/registry.rb +45 -0
- data/lib/ruby_llm/agents/providers/inception.rb +50 -0
- data/lib/ruby_llm/agents/results/base.rb +4 -2
- data/lib/ruby_llm/agents/results/image_analysis_result.rb +4 -2
- data/lib/ruby_llm/agents/text/embedder.rb +4 -0
- data/lib/ruby_llm/agents.rb +4 -0
- 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
|
|
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
|
-
|
|
296
|
-
|
|
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
|
-
|
|
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(
|
|
318
|
-
errors_change: pct_change(
|
|
319
|
-
cost_change: pct_change(
|
|
320
|
-
duration_change: pct_change(
|
|
321
|
-
tokens_change: pct_change(
|
|
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
|
-
|
|
342
|
-
|
|
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
|
-
|
|
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(
|
|
364
|
-
errors_change: pct_change(
|
|
365
|
-
cost_change: pct_change(
|
|
366
|
-
duration_change: pct_change(
|
|
367
|
-
tokens_change: pct_change(
|
|
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
|
|
@@ -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
|
-
#
|
|
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,
|
|
10
|
-
|
|
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
|
|
data/lib/generators/ruby_llm_agents/templates/add_dashboard_performance_indexes_migration.rb.tt
ADDED
|
@@ -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
|
-
|
|
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
|
|
@@ -2,31 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
module RubyLLM
|
|
4
4
|
module Agents
|
|
5
|
-
#
|
|
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
|
-
#
|
|
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]
|
|
@@ -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
|
-
#
|
|
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
|
-
#
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|