ruby_llm-agents 3.7.2 → 3.9.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/README.md +30 -10
- 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/controllers/ruby_llm/agents/requests_controller.rb +117 -0
- 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/layouts/ruby_llm/agents/application.html.erb +4 -2
- data/app/views/ruby_llm/agents/executions/_list.html.erb +3 -17
- data/app/views/ruby_llm/agents/requests/index.html.erb +153 -0
- data/app/views/ruby_llm/agents/requests/show.html.erb +136 -0
- data/config/routes.rb +2 -0
- data/lib/generators/ruby_llm_agents/agent_generator.rb +2 -2
- data/lib/generators/ruby_llm_agents/demo_generator.rb +102 -0
- data/lib/generators/ruby_llm_agents/doctor_generator.rb +196 -0
- data/lib/generators/ruby_llm_agents/install_generator.rb +7 -19
- data/lib/generators/ruby_llm_agents/templates/add_dashboard_performance_indexes_migration.rb.tt +23 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +27 -80
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +18 -51
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +19 -17
- 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 +71 -4
- data/lib/ruby_llm/agents/core/base.rb +4 -0
- data/lib/ruby_llm/agents/core/configuration.rb +11 -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 +69 -1
- data/lib/ruby_llm/agents/pipeline/middleware/base.rb +58 -4
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +21 -17
- data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +40 -26
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +126 -120
- data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +13 -11
- data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +29 -31
- 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/rails/engine.rb +11 -0
- data/lib/ruby_llm/agents/results/background_removal_result.rb +7 -1
- data/lib/ruby_llm/agents/results/base.rb +28 -4
- data/lib/ruby_llm/agents/results/embedding_result.rb +4 -0
- data/lib/ruby_llm/agents/results/image_analysis_result.rb +11 -3
- data/lib/ruby_llm/agents/results/image_edit_result.rb +7 -1
- data/lib/ruby_llm/agents/results/image_generation_result.rb +7 -1
- data/lib/ruby_llm/agents/results/image_pipeline_result.rb +7 -1
- data/lib/ruby_llm/agents/results/image_transform_result.rb +7 -1
- data/lib/ruby_llm/agents/results/image_upscale_result.rb +7 -1
- data/lib/ruby_llm/agents/results/image_variation_result.rb +7 -1
- data/lib/ruby_llm/agents/results/speech_result.rb +6 -0
- data/lib/ruby_llm/agents/results/trackable.rb +25 -0
- data/lib/ruby_llm/agents/results/transcription_result.rb +6 -0
- data/lib/ruby_llm/agents/text/embedder.rb +8 -1
- data/lib/ruby_llm/agents/track_report.rb +127 -0
- data/lib/ruby_llm/agents/tracker.rb +32 -0
- data/lib/ruby_llm/agents.rb +212 -0
- data/lib/tasks/ruby_llm_agents.rake +6 -0
- metadata +17 -2
|
@@ -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
|
|
@@ -295,7 +295,8 @@
|
|
|
295
295
|
<% nav_items = [
|
|
296
296
|
[ruby_llm_agents.root_path, "dashboard"],
|
|
297
297
|
[ruby_llm_agents.agents_path, "agents"],
|
|
298
|
-
[ruby_llm_agents.executions_path, "executions"]
|
|
298
|
+
[ruby_llm_agents.executions_path, "executions"],
|
|
299
|
+
[ruby_llm_agents.requests_path, "requests"]
|
|
299
300
|
]
|
|
300
301
|
nav_items << [ruby_llm_agents.tenants_path, "tenants"] if tenant_filter_enabled?
|
|
301
302
|
nav_items.each do |path, label| %>
|
|
@@ -345,7 +346,8 @@
|
|
|
345
346
|
<% mobile_nav_items = [
|
|
346
347
|
[ruby_llm_agents.root_path, "dashboard"],
|
|
347
348
|
[ruby_llm_agents.agents_path, "agents"],
|
|
348
|
-
[ruby_llm_agents.executions_path, "executions"]
|
|
349
|
+
[ruby_llm_agents.executions_path, "executions"],
|
|
350
|
+
[ruby_llm_agents.requests_path, "requests"]
|
|
349
351
|
]
|
|
350
352
|
mobile_nav_items << [ruby_llm_agents.tenants_path, "tenants"] if tenant_filter_enabled?
|
|
351
353
|
mobile_nav_items.each do |path, label| %>
|
|
@@ -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
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<div class="flex items-center gap-3 mb-6">
|
|
2
|
+
<span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">requests</span>
|
|
3
|
+
<div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
|
|
4
|
+
<span class="font-mono text-[10px] text-gray-400 dark:text-gray-600">
|
|
5
|
+
<%= number_with_delimiter(@stats[:total_requests]) %> tracked
|
|
6
|
+
·
|
|
7
|
+
$<%= number_with_precision(@stats[:total_cost] || 0, precision: 4) %> total
|
|
8
|
+
</span>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<%
|
|
12
|
+
sort_column = @sort_column
|
|
13
|
+
sort_direction = @sort_direction
|
|
14
|
+
|
|
15
|
+
sort_link = ->(column, label, extra_class: "") {
|
|
16
|
+
is_active = column == sort_column
|
|
17
|
+
next_dir = is_active && sort_direction == "asc" ? "desc" : "asc"
|
|
18
|
+
url = url_for(request.query_parameters.merge(sort: column, direction: next_dir, page: 1))
|
|
19
|
+
|
|
20
|
+
arrow = if is_active && sort_direction == "asc"
|
|
21
|
+
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>')
|
|
22
|
+
elsif is_active
|
|
23
|
+
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>')
|
|
24
|
+
else
|
|
25
|
+
""
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
active_class = is_active ? "text-gray-700 dark:text-gray-300" : ""
|
|
29
|
+
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>))
|
|
30
|
+
}
|
|
31
|
+
%>
|
|
32
|
+
|
|
33
|
+
<% if @requests.empty? %>
|
|
34
|
+
<div class="font-mono text-xs text-gray-400 dark:text-gray-600 py-8 text-center">
|
|
35
|
+
No tracked requests found. Use <code class="bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded">RubyLLM::Agents.track { ... }</code> to start tracking.
|
|
36
|
+
</div>
|
|
37
|
+
<% else %>
|
|
38
|
+
<!-- Column headers -->
|
|
39
|
+
<div class="flex items-center gap-3 px-2 -mx-2 font-mono text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-wider mb-1">
|
|
40
|
+
<span class="w-36 flex-shrink-0">request_id</span>
|
|
41
|
+
<span class="flex-1 min-w-0">agents</span>
|
|
42
|
+
<span class="w-12 flex-shrink-0 text-right"><%= sort_link.call("call_count", "calls", extra_class: "justify-end w-full") %></span>
|
|
43
|
+
<span class="w-14 flex-shrink-0 text-right hidden sm:block">status</span>
|
|
44
|
+
<span class="w-16 flex-shrink-0 text-right hidden md:block"><%= sort_link.call("total_duration_ms", "duration", extra_class: "justify-end w-full") %></span>
|
|
45
|
+
<span class="w-16 flex-shrink-0 text-right hidden md:block"><%= sort_link.call("total_tokens", "tokens", extra_class: "justify-end w-full") %></span>
|
|
46
|
+
<span class="w-16 flex-shrink-0 text-right"><%= sort_link.call("total_cost", "cost", extra_class: "justify-end w-full") %></span>
|
|
47
|
+
<span class="w-24 flex-shrink-0 text-right"><%= sort_link.call("latest_created_at", "time", extra_class: "justify-end w-full") %></span>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<!-- Rows -->
|
|
51
|
+
<div class="font-mono text-xs space-y-px">
|
|
52
|
+
<% @requests.each do |req| %>
|
|
53
|
+
<%
|
|
54
|
+
statuses = (req.statuses_list || "").split(",")
|
|
55
|
+
has_errors = statuses.include?("error") || statuses.include?("timeout")
|
|
56
|
+
all_success = statuses == ["success"]
|
|
57
|
+
status_class = if has_errors
|
|
58
|
+
"badge-error"
|
|
59
|
+
elsif all_success
|
|
60
|
+
"badge-success"
|
|
61
|
+
else
|
|
62
|
+
"badge-running"
|
|
63
|
+
end
|
|
64
|
+
status_label = if has_errors
|
|
65
|
+
"errors"
|
|
66
|
+
elsif all_success
|
|
67
|
+
"ok"
|
|
68
|
+
else
|
|
69
|
+
"mixed"
|
|
70
|
+
end
|
|
71
|
+
agent_names = (req.agent_types_list || "").split(",").map { |a| a.gsub(/Agent$/, "") }
|
|
72
|
+
%>
|
|
73
|
+
<div class="group flex items-center gap-3 py-1.5 px-2 -mx-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800/50 cursor-pointer"
|
|
74
|
+
onclick="window.location='<%= ruby_llm_agents.request_path(req.request_id) %>'">
|
|
75
|
+
<span class="w-36 flex-shrink-0 truncate text-gray-900 dark:text-gray-200" title="<%= req.request_id %>">
|
|
76
|
+
<%= truncate(req.request_id, length: 20) %>
|
|
77
|
+
</span>
|
|
78
|
+
<span class="flex-1 min-w-0 truncate text-gray-400 dark:text-gray-600">
|
|
79
|
+
<%= agent_names.first(3).join(", ") %><%= agent_names.size > 3 ? " +#{agent_names.size - 3}" : "" %>
|
|
80
|
+
</span>
|
|
81
|
+
<span class="w-12 flex-shrink-0 text-right text-gray-500 dark:text-gray-400"><%= req.call_count %></span>
|
|
82
|
+
<span class="w-14 flex-shrink-0 text-right hidden sm:block">
|
|
83
|
+
<span class="badge badge-sm <%= status_class %>"><%= status_label %></span>
|
|
84
|
+
</span>
|
|
85
|
+
<span class="w-16 flex-shrink-0 text-right text-gray-500 dark:text-gray-400 hidden md:inline">
|
|
86
|
+
<%= req.total_duration_ms ? format_duration_ms(req.total_duration_ms.to_i) : "—" %>
|
|
87
|
+
</span>
|
|
88
|
+
<span class="w-16 flex-shrink-0 text-right text-gray-500 dark:text-gray-400 hidden md:inline"><%= number_with_delimiter(req.total_tokens || 0) %></span>
|
|
89
|
+
<span class="w-16 flex-shrink-0 text-right text-gray-500 dark:text-gray-400">$<%= number_with_precision(req.total_cost || 0, precision: 4) %></span>
|
|
90
|
+
<span class="w-24 flex-shrink-0 text-gray-400 dark:text-gray-600 text-right whitespace-nowrap">
|
|
91
|
+
<% if req.respond_to?(:latest_created_at) && req.latest_created_at %>
|
|
92
|
+
<%= time_ago_in_words(req.latest_created_at) %>
|
|
93
|
+
<% else %>
|
|
94
|
+
—
|
|
95
|
+
<% end %>
|
|
96
|
+
</span>
|
|
97
|
+
</div>
|
|
98
|
+
<% end %>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<%# Pagination %>
|
|
102
|
+
<% if @pagination && @pagination[:total_pages] > 1 %>
|
|
103
|
+
<%
|
|
104
|
+
current_page = @pagination[:current_page]
|
|
105
|
+
total_pages = @pagination[:total_pages]
|
|
106
|
+
total_count = @pagination[:total_count]
|
|
107
|
+
per_page = @pagination[:per_page]
|
|
108
|
+
from_record = ((current_page - 1) * per_page) + 1
|
|
109
|
+
to_record = [current_page * per_page, total_count].min
|
|
110
|
+
%>
|
|
111
|
+
<div class="flex items-center justify-between font-mono text-xs mt-4 pt-4 border-t border-gray-200 dark:border-gray-800">
|
|
112
|
+
<span class="text-gray-400 dark:text-gray-600"><%= from_record %>-<%= to_record %> of <%= number_with_delimiter(total_count) %></span>
|
|
113
|
+
<nav class="flex items-center gap-1">
|
|
114
|
+
<% if current_page > 1 %>
|
|
115
|
+
<%= link_to "prev", url_for(request.query_parameters.merge(page: current_page - 1)),
|
|
116
|
+
class: "px-2 py-0.5 text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300" %>
|
|
117
|
+
<% else %>
|
|
118
|
+
<span class="px-2 py-0.5 text-gray-300 dark:text-gray-700">prev</span>
|
|
119
|
+
<% end %>
|
|
120
|
+
|
|
121
|
+
<%
|
|
122
|
+
window = 2
|
|
123
|
+
pages_to_show = []
|
|
124
|
+
(1..total_pages).each do |page|
|
|
125
|
+
if page <= 1 || page >= total_pages || (page >= current_page - window && page <= current_page + window)
|
|
126
|
+
pages_to_show << page
|
|
127
|
+
elsif pages_to_show.last != :gap
|
|
128
|
+
pages_to_show << :gap
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
%>
|
|
132
|
+
|
|
133
|
+
<% pages_to_show.each do |page| %>
|
|
134
|
+
<% if page == :gap %>
|
|
135
|
+
<span class="px-1 text-gray-400 dark:text-gray-600">...</span>
|
|
136
|
+
<% elsif page == current_page %>
|
|
137
|
+
<span class="px-2 py-0.5 text-gray-900 dark:text-gray-100"><%= page %></span>
|
|
138
|
+
<% else %>
|
|
139
|
+
<%= link_to page, url_for(request.query_parameters.merge(page: page)),
|
|
140
|
+
class: "px-2 py-0.5 text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300" %>
|
|
141
|
+
<% end %>
|
|
142
|
+
<% end %>
|
|
143
|
+
|
|
144
|
+
<% if current_page < total_pages %>
|
|
145
|
+
<%= link_to "next", url_for(request.query_parameters.merge(page: current_page + 1)),
|
|
146
|
+
class: "px-2 py-0.5 text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300" %>
|
|
147
|
+
<% else %>
|
|
148
|
+
<span class="px-2 py-0.5 text-gray-300 dark:text-gray-700">next</span>
|
|
149
|
+
<% end %>
|
|
150
|
+
</nav>
|
|
151
|
+
</div>
|
|
152
|
+
<% end %>
|
|
153
|
+
<% end %>
|