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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e2d24a8046e58bdadf37d70ddb1effb038f69b1fa01c4dd7f9e08bf819527d94
|
|
4
|
+
data.tar.gz: 0f620acc86ebe001c151c6a95ee784463bb1e10eb66532e7f6d2684ca2021f06
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9bd06ce2399f86359e00a115d269af22d3f73a0ad8a1bc0d21e315c4a41b0a023bd59b9b9504ac3902d65c76195467f6ae232caed955b5b247439e27aca59b7e
|
|
7
|
+
data.tar.gz: bb2b29a3b6d7c3c4b0312103fe98bd8c3744dc42415280fda8bbb41054c414f489954c1ee869f6b21cb4e98a470eb7f7e9cf8fc63cc05c0810d4f1b635c9bd55
|
|
@@ -100,11 +100,12 @@ module RubyLLM
|
|
|
100
100
|
#
|
|
101
101
|
# @return [void]
|
|
102
102
|
def load_agent_stats
|
|
103
|
-
|
|
104
|
-
@
|
|
103
|
+
base = tenant_scoped_executions
|
|
104
|
+
@stats = base.stats_for(@agent_type, period: :all_time)
|
|
105
|
+
@stats_today = base.stats_for(@agent_type, period: :today)
|
|
105
106
|
|
|
106
107
|
# Additional stats for new schema fields
|
|
107
|
-
agent_scope =
|
|
108
|
+
agent_scope = base.by_agent(@agent_type)
|
|
108
109
|
@cache_hit_rate = agent_scope.cache_hit_rate
|
|
109
110
|
@streaming_rate = agent_scope.streaming_rate
|
|
110
111
|
@avg_ttft = agent_scope.avg_time_to_first_token
|
|
@@ -118,9 +119,10 @@ module RubyLLM
|
|
|
118
119
|
# @return [void]
|
|
119
120
|
def load_filter_options
|
|
120
121
|
# Single query to get all filter options (fixes N+1)
|
|
121
|
-
|
|
122
|
+
base = tenant_scoped_executions.by_agent(@agent_type)
|
|
123
|
+
filter_data = base
|
|
122
124
|
.where.not(model_id: nil)
|
|
123
|
-
.or(
|
|
125
|
+
.or(base.where.not(temperature: nil))
|
|
124
126
|
.pluck(:model_id, :temperature)
|
|
125
127
|
|
|
126
128
|
@models = filter_data.map(&:first).compact.uniq.sort
|
|
@@ -152,7 +154,7 @@ module RubyLLM
|
|
|
152
154
|
#
|
|
153
155
|
# @return [ActiveRecord::Relation] Filtered execution scope
|
|
154
156
|
def build_filtered_scope
|
|
155
|
-
scope =
|
|
157
|
+
scope = tenant_scoped_executions.by_agent(@agent_type)
|
|
156
158
|
|
|
157
159
|
# Apply status filter with validation
|
|
158
160
|
statuses = parse_array_param(:statuses)
|
|
@@ -177,9 +179,10 @@ module RubyLLM
|
|
|
177
179
|
#
|
|
178
180
|
# @return [void]
|
|
179
181
|
def load_chart_data
|
|
180
|
-
|
|
181
|
-
@
|
|
182
|
-
@
|
|
182
|
+
base = tenant_scoped_executions
|
|
183
|
+
@trend_data = base.trend_analysis(agent_type: @agent_type, days: 30)
|
|
184
|
+
@status_distribution = base.by_agent(@agent_type).group(:status).count
|
|
185
|
+
@finish_reason_distribution = base.by_agent(@agent_type).finish_reason_distribution
|
|
183
186
|
end
|
|
184
187
|
|
|
185
188
|
# Loads the current agent class configuration
|
|
@@ -189,140 +192,10 @@ module RubyLLM
|
|
|
189
192
|
# Detects agent type and loads appropriate config.
|
|
190
193
|
#
|
|
191
194
|
# @return [void]
|
|
195
|
+
# Loads agent configuration using AgentRegistry
|
|
192
196
|
def load_agent_config
|
|
193
197
|
@agent_type_kind = AgentRegistry.send(:detect_agent_type, @agent_class)
|
|
194
|
-
|
|
195
|
-
# Common config for all types
|
|
196
|
-
@config = {
|
|
197
|
-
model: safe_config_call(:model),
|
|
198
|
-
description: safe_config_call(:description)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
# Type-specific config
|
|
202
|
-
case @agent_type_kind
|
|
203
|
-
when "embedder"
|
|
204
|
-
load_embedder_config
|
|
205
|
-
when "speaker"
|
|
206
|
-
load_speaker_config
|
|
207
|
-
when "transcriber"
|
|
208
|
-
load_transcriber_config
|
|
209
|
-
when "image_generator"
|
|
210
|
-
load_image_generator_config
|
|
211
|
-
when "router"
|
|
212
|
-
load_router_config
|
|
213
|
-
else
|
|
214
|
-
load_base_agent_config
|
|
215
|
-
end
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
# Loads configuration specific to Base agents
|
|
219
|
-
#
|
|
220
|
-
# @return [void]
|
|
221
|
-
def load_base_agent_config
|
|
222
|
-
@config.merge!(
|
|
223
|
-
temperature: safe_config_call(:temperature),
|
|
224
|
-
timeout: safe_config_call(:timeout),
|
|
225
|
-
cache_enabled: safe_config_call(:cache_enabled?) || false,
|
|
226
|
-
cache_ttl: safe_config_call(:cache_ttl),
|
|
227
|
-
params: safe_config_call(:params) || {},
|
|
228
|
-
retries: safe_config_call(:retries),
|
|
229
|
-
fallback_models: safe_config_call(:fallback_models),
|
|
230
|
-
total_timeout: safe_config_call(:total_timeout),
|
|
231
|
-
circuit_breaker: safe_config_call(:circuit_breaker_config)
|
|
232
|
-
)
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
# Loads configuration specific to Embedders
|
|
236
|
-
#
|
|
237
|
-
# @return [void]
|
|
238
|
-
def load_embedder_config
|
|
239
|
-
@config.merge!(
|
|
240
|
-
dimensions: safe_config_call(:dimensions),
|
|
241
|
-
batch_size: safe_config_call(:batch_size),
|
|
242
|
-
cache_enabled: safe_config_call(:cache_enabled?) || false,
|
|
243
|
-
cache_ttl: safe_config_call(:cache_ttl)
|
|
244
|
-
)
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
# Loads configuration specific to Speakers
|
|
248
|
-
#
|
|
249
|
-
# @return [void]
|
|
250
|
-
def load_speaker_config
|
|
251
|
-
@config.merge!(
|
|
252
|
-
provider: safe_config_call(:provider),
|
|
253
|
-
voice: safe_config_call(:voice),
|
|
254
|
-
voice_id: safe_config_call(:voice_id),
|
|
255
|
-
speed: safe_config_call(:speed),
|
|
256
|
-
output_format: safe_config_call(:output_format),
|
|
257
|
-
streaming: safe_config_call(:streaming?),
|
|
258
|
-
ssml_enabled: safe_config_call(:ssml_enabled?),
|
|
259
|
-
cache_enabled: safe_config_call(:cache_enabled?) || false,
|
|
260
|
-
cache_ttl: safe_config_call(:cache_ttl)
|
|
261
|
-
)
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
# Loads configuration specific to Transcribers
|
|
265
|
-
#
|
|
266
|
-
# @return [void]
|
|
267
|
-
def load_transcriber_config
|
|
268
|
-
@config.merge!(
|
|
269
|
-
language: safe_config_call(:language),
|
|
270
|
-
output_format: safe_config_call(:output_format),
|
|
271
|
-
include_timestamps: safe_config_call(:include_timestamps),
|
|
272
|
-
cache_enabled: safe_config_call(:cache_enabled?) || false,
|
|
273
|
-
cache_ttl: safe_config_call(:cache_ttl),
|
|
274
|
-
fallback_models: safe_config_call(:fallback_models)
|
|
275
|
-
)
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
# Loads configuration specific to ImageGenerators
|
|
279
|
-
#
|
|
280
|
-
# @return [void]
|
|
281
|
-
def load_image_generator_config
|
|
282
|
-
@config.merge!(
|
|
283
|
-
size: safe_config_call(:size),
|
|
284
|
-
quality: safe_config_call(:quality),
|
|
285
|
-
style: safe_config_call(:style),
|
|
286
|
-
content_policy: safe_config_call(:content_policy),
|
|
287
|
-
template: safe_config_call(:template_string),
|
|
288
|
-
negative_prompt: safe_config_call(:negative_prompt),
|
|
289
|
-
seed: safe_config_call(:seed),
|
|
290
|
-
guidance_scale: safe_config_call(:guidance_scale),
|
|
291
|
-
steps: safe_config_call(:steps),
|
|
292
|
-
cache_enabled: safe_config_call(:cache_enabled?) || false,
|
|
293
|
-
cache_ttl: safe_config_call(:cache_ttl)
|
|
294
|
-
)
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
# Loads configuration specific to Router agents
|
|
298
|
-
#
|
|
299
|
-
# @return [void]
|
|
300
|
-
def load_router_config
|
|
301
|
-
routes = safe_config_call(:routes) || {}
|
|
302
|
-
@config.merge!(
|
|
303
|
-
temperature: safe_config_call(:temperature),
|
|
304
|
-
timeout: safe_config_call(:timeout),
|
|
305
|
-
cache_enabled: safe_config_call(:cache_enabled?) || false,
|
|
306
|
-
cache_ttl: safe_config_call(:cache_ttl),
|
|
307
|
-
default_route: safe_config_call(:default_route_name),
|
|
308
|
-
routes: routes.transform_values { |v| v[:description] },
|
|
309
|
-
route_count: routes.size,
|
|
310
|
-
retries: safe_config_call(:retries),
|
|
311
|
-
fallback_models: safe_config_call(:fallback_models),
|
|
312
|
-
total_timeout: safe_config_call(:total_timeout),
|
|
313
|
-
circuit_breaker: safe_config_call(:circuit_breaker_config)
|
|
314
|
-
)
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
# Safely calls a method on the agent class, returning nil on error
|
|
318
|
-
#
|
|
319
|
-
# @param method [Symbol] The method to call
|
|
320
|
-
# @return [Object, nil] The result or nil if error
|
|
321
|
-
def safe_config_call(method)
|
|
322
|
-
return nil unless @agent_class&.respond_to?(method)
|
|
323
|
-
@agent_class.public_send(method)
|
|
324
|
-
rescue
|
|
325
|
-
nil
|
|
198
|
+
@config = AgentRegistry.config_for(@agent_class)
|
|
326
199
|
end
|
|
327
200
|
|
|
328
201
|
# Loads circuit breaker status for the agent's models
|
|
@@ -24,7 +24,7 @@ module RubyLLM
|
|
|
24
24
|
base_scope = tenant_scoped_executions
|
|
25
25
|
@now_strip = build_now_strip(base_scope)
|
|
26
26
|
@critical_alerts = load_critical_alerts(base_scope)
|
|
27
|
-
@recent_executions = base_scope.recent(10)
|
|
27
|
+
@recent_executions = base_scope.includes(:detail).recent(10)
|
|
28
28
|
@agent_stats = build_agent_comparison(base_scope)
|
|
29
29
|
@top_errors = build_top_errors(base_scope)
|
|
30
30
|
@tenant_budget = load_tenant_budget(base_scope)
|
|
@@ -194,100 +194,14 @@ module RubyLLM
|
|
|
194
194
|
@agent_stats
|
|
195
195
|
end
|
|
196
196
|
|
|
197
|
-
#
|
|
198
|
-
#
|
|
199
|
-
# @param base_scope [ActiveRecord::Relation] Base scope to filter from
|
|
200
|
-
# @return [Array<Hash>] Array of model stats sorted by total cost descending
|
|
197
|
+
# Delegates to Execution.model_stats with time scoping
|
|
201
198
|
def build_model_stats(base_scope = Execution)
|
|
202
|
-
scope
|
|
203
|
-
|
|
204
|
-
# Batch fetch stats grouped by model
|
|
205
|
-
counts = scope.group(:model_id).count
|
|
206
|
-
costs = scope.group(:model_id).sum(:total_cost)
|
|
207
|
-
tokens = scope.group(:model_id).sum(:total_tokens)
|
|
208
|
-
durations = scope.group(:model_id).average(:duration_ms)
|
|
209
|
-
success_counts = scope.successful.group(:model_id).count
|
|
210
|
-
|
|
211
|
-
total_cost = costs.values.sum
|
|
212
|
-
|
|
213
|
-
model_ids = counts.keys
|
|
214
|
-
model_ids.map do |model_id|
|
|
215
|
-
count = counts[model_id] || 0
|
|
216
|
-
model_cost = costs[model_id] || 0
|
|
217
|
-
model_tokens = tokens[model_id] || 0
|
|
218
|
-
successful = success_counts[model_id] || 0
|
|
219
|
-
|
|
220
|
-
{
|
|
221
|
-
model_id: model_id,
|
|
222
|
-
executions: count,
|
|
223
|
-
total_cost: model_cost,
|
|
224
|
-
total_tokens: model_tokens,
|
|
225
|
-
avg_duration_ms: durations[model_id]&.round || 0,
|
|
226
|
-
success_rate: (count > 0) ? (successful.to_f / count * 100).round(1) : 0,
|
|
227
|
-
cost_per_1k_tokens: (model_tokens > 0) ? (model_cost / model_tokens * 1000).round(4) : 0,
|
|
228
|
-
cost_percentage: (total_cost > 0) ? (model_cost / total_cost * 100).round(1) : 0
|
|
229
|
-
}
|
|
230
|
-
end.sort_by { |m| -(m[:total_cost] || 0) }
|
|
199
|
+
Execution.model_stats(scope: time_scoped(base_scope))
|
|
231
200
|
end
|
|
232
201
|
|
|
233
|
-
#
|
|
234
|
-
#
|
|
235
|
-
# @param base_scope [ActiveRecord::Relation] Base scope to filter from
|
|
236
|
-
# @return [Array<Hash>] Top 5 error classes with counts
|
|
202
|
+
# Delegates to Execution.top_errors with time scoping
|
|
237
203
|
def build_top_errors(base_scope = Execution)
|
|
238
|
-
scope
|
|
239
|
-
total_errors = scope.count
|
|
240
|
-
|
|
241
|
-
scope.group(:error_class)
|
|
242
|
-
.select("error_class, COUNT(*) as count, MAX(created_at) as last_seen")
|
|
243
|
-
.order("count DESC")
|
|
244
|
-
.limit(5)
|
|
245
|
-
.map do |row|
|
|
246
|
-
{
|
|
247
|
-
error_class: row.error_class || "Unknown Error",
|
|
248
|
-
count: row.count,
|
|
249
|
-
percentage: (total_errors > 0) ? (row.count.to_f / total_errors * 100).round(1) : 0,
|
|
250
|
-
last_seen: row.last_seen
|
|
251
|
-
}
|
|
252
|
-
end
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
# Fetches cached daily statistics for the dashboard
|
|
256
|
-
#
|
|
257
|
-
# Results are cached for 1 minute to reduce database load while
|
|
258
|
-
# keeping the dashboard reasonably up-to-date.
|
|
259
|
-
#
|
|
260
|
-
# @return [Hash] Daily statistics
|
|
261
|
-
# @option return [Integer] :total_executions Total execution count today
|
|
262
|
-
# @option return [Integer] :successful Successful execution count
|
|
263
|
-
# @option return [Integer] :failed Failed execution count
|
|
264
|
-
# @option return [Float] :total_cost Combined cost of all executions
|
|
265
|
-
# @option return [Integer] :total_tokens Combined token usage
|
|
266
|
-
# @option return [Integer] :avg_duration_ms Average execution duration
|
|
267
|
-
# @option return [Float] :success_rate Percentage of successful executions
|
|
268
|
-
def daily_stats
|
|
269
|
-
Rails.cache.fetch("ruby_llm_agents/daily_stats/#{Date.current}", expires_in: 1.minute) do
|
|
270
|
-
scope = Execution.today
|
|
271
|
-
{
|
|
272
|
-
total_executions: scope.count,
|
|
273
|
-
successful: scope.successful.count,
|
|
274
|
-
failed: scope.failed.count,
|
|
275
|
-
total_cost: scope.total_cost_sum || 0,
|
|
276
|
-
total_tokens: scope.total_tokens_sum || 0,
|
|
277
|
-
avg_duration_ms: scope.avg_duration&.round || 0,
|
|
278
|
-
success_rate: calculate_success_rate(scope)
|
|
279
|
-
}
|
|
280
|
-
end
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
# Calculates the success rate percentage for a scope
|
|
284
|
-
#
|
|
285
|
-
# @param scope [ActiveRecord::Relation] The execution scope to calculate from
|
|
286
|
-
# @return [Float] Success rate as a percentage (0.0-100.0)
|
|
287
|
-
def calculate_success_rate(scope)
|
|
288
|
-
total = scope.count
|
|
289
|
-
return 0.0 if total.zero?
|
|
290
|
-
(scope.successful.count.to_f / total * 100).round(1)
|
|
204
|
+
Execution.top_errors(scope: time_scoped(base_scope))
|
|
291
205
|
end
|
|
292
206
|
|
|
293
207
|
# Loads budget status for display on dashboard
|
|
@@ -304,7 +218,7 @@ module RubyLLM
|
|
|
304
218
|
open_breakers = []
|
|
305
219
|
|
|
306
220
|
# Get all agents from execution history
|
|
307
|
-
agent_types =
|
|
221
|
+
agent_types = tenant_scoped_executions.distinct.pluck(:agent_type)
|
|
308
222
|
|
|
309
223
|
agent_types.each do |agent_type|
|
|
310
224
|
# Get the agent class if available
|
|
@@ -435,87 +349,19 @@ module RubyLLM
|
|
|
435
349
|
alerts.take(3)
|
|
436
350
|
end
|
|
437
351
|
|
|
438
|
-
#
|
|
439
|
-
#
|
|
440
|
-
# @param base_scope [ActiveRecord::Relation] Base scope to filter from
|
|
441
|
-
# @return [Hash] Cache savings data with count, estimated savings, and hit rate
|
|
352
|
+
# Delegates to Execution.cache_savings with time scoping
|
|
442
353
|
def build_cache_savings(base_scope)
|
|
443
|
-
scope
|
|
444
|
-
total_count = scope.count
|
|
445
|
-
return {count: 0, estimated_savings: 0, hit_rate: 0, total_executions: 0} if total_count.zero?
|
|
446
|
-
|
|
447
|
-
cached_scope = scope.cached
|
|
448
|
-
cache_count = cached_scope.count
|
|
449
|
-
estimated_savings = cached_scope.sum(:total_cost)
|
|
450
|
-
|
|
451
|
-
{
|
|
452
|
-
count: cache_count,
|
|
453
|
-
estimated_savings: estimated_savings,
|
|
454
|
-
hit_rate: (cache_count.to_f / total_count * 100).round(1),
|
|
455
|
-
total_executions: total_count
|
|
456
|
-
}
|
|
354
|
+
Execution.cache_savings(scope: time_scoped(base_scope))
|
|
457
355
|
end
|
|
458
356
|
|
|
459
|
-
#
|
|
460
|
-
#
|
|
461
|
-
# @return [Array<Hash>, nil] Top 5 tenants by monthly spend, or nil if none
|
|
357
|
+
# Delegates to Tenant.top_by_spend
|
|
462
358
|
def build_top_tenants
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
tenants = Tenant.active
|
|
466
|
-
.where("monthly_cost_spent > 0 OR monthly_executions_count > 0")
|
|
467
|
-
.order(monthly_cost_spent: :desc)
|
|
468
|
-
.limit(5)
|
|
469
|
-
|
|
470
|
-
return nil if tenants.empty?
|
|
471
|
-
|
|
472
|
-
tenants.map do |tenant|
|
|
473
|
-
tenant.ensure_daily_reset!
|
|
474
|
-
tenant.ensure_monthly_reset!
|
|
475
|
-
|
|
476
|
-
monthly_limit = tenant.effective_monthly_limit
|
|
477
|
-
daily_limit = tenant.effective_daily_limit
|
|
478
|
-
|
|
479
|
-
{
|
|
480
|
-
id: tenant.id,
|
|
481
|
-
tenant_id: tenant.tenant_id,
|
|
482
|
-
name: tenant.display_name,
|
|
483
|
-
enforcement: tenant.effective_enforcement,
|
|
484
|
-
monthly_spend: tenant.monthly_cost_spent,
|
|
485
|
-
monthly_limit: monthly_limit,
|
|
486
|
-
monthly_percentage: (monthly_limit.to_f > 0) ? (tenant.monthly_cost_spent / monthly_limit * 100).round(1) : 0,
|
|
487
|
-
daily_spend: tenant.daily_cost_spent,
|
|
488
|
-
daily_limit: daily_limit,
|
|
489
|
-
daily_percentage: (daily_limit.to_f > 0) ? (tenant.daily_cost_spent / daily_limit * 100).round(1) : 0,
|
|
490
|
-
monthly_executions: tenant.monthly_executions_count
|
|
491
|
-
}
|
|
492
|
-
end
|
|
359
|
+
Tenant.top_by_spend(limit: 5)
|
|
493
360
|
end
|
|
494
361
|
|
|
495
|
-
#
|
|
496
|
-
#
|
|
497
|
-
# @param scope [ActiveRecord::Relation] Base scope with time filter
|
|
498
|
-
# @return [Hash<String, Hash>] Agent type => stats hash
|
|
362
|
+
# Delegates to Execution.batch_agent_stats with pre-filtered scope
|
|
499
363
|
def batch_fetch_agent_stats(scope)
|
|
500
|
-
|
|
501
|
-
costs = scope.group(:agent_type).sum(:total_cost)
|
|
502
|
-
success_counts = scope.successful.group(:agent_type).count
|
|
503
|
-
durations = scope.group(:agent_type).average(:duration_ms)
|
|
504
|
-
|
|
505
|
-
agent_types = (counts.keys + costs.keys).uniq
|
|
506
|
-
agent_types.each_with_object({}) do |agent_type, hash|
|
|
507
|
-
count = counts[agent_type] || 0
|
|
508
|
-
total_cost = costs[agent_type] || 0
|
|
509
|
-
successful = success_counts[agent_type] || 0
|
|
510
|
-
|
|
511
|
-
hash[agent_type] = {
|
|
512
|
-
count: count,
|
|
513
|
-
total_cost: total_cost,
|
|
514
|
-
avg_cost: (count > 0) ? (total_cost / count).round(6) : 0,
|
|
515
|
-
avg_duration_ms: durations[agent_type]&.round || 0,
|
|
516
|
-
success_rate: (count > 0) ? (successful.to_f / count * 100).round(1) : 0
|
|
517
|
-
}
|
|
518
|
-
end
|
|
364
|
+
Execution.batch_agent_stats(scope: scope)
|
|
519
365
|
end
|
|
520
366
|
end
|
|
521
367
|
end
|
|
@@ -120,6 +120,44 @@ module RubyLLM
|
|
|
120
120
|
end
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
+
# Renders a sortable column header link with arrow indicator
|
|
124
|
+
#
|
|
125
|
+
# Replaces inline `raw()` calls in views with safe content_tag usage.
|
|
126
|
+
#
|
|
127
|
+
# @param column [String] The sort column name
|
|
128
|
+
# @param label [String] Display label for the header
|
|
129
|
+
# @param current_column [String] Currently active sort column
|
|
130
|
+
# @param current_direction [String] Current sort direction ("asc"/"desc")
|
|
131
|
+
# @param extra_class [String] Additional CSS classes
|
|
132
|
+
# @return [ActiveSupport::SafeBuffer] HTML link element
|
|
133
|
+
def sort_header_link(column, label, current_column:, current_direction:, extra_class: "")
|
|
134
|
+
is_active = column == current_column
|
|
135
|
+
next_dir = (is_active && current_direction == "asc") ? "desc" : "asc"
|
|
136
|
+
url = url_for(request.query_parameters.merge(sort: column, direction: next_dir, page: 1))
|
|
137
|
+
|
|
138
|
+
arrow = if is_active && current_direction == "asc"
|
|
139
|
+
content_tag(:svg, class: "w-2.5 h-2.5 inline", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24") do
|
|
140
|
+
content_tag(:path, nil, "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M5 15l7-7 7 7")
|
|
141
|
+
end
|
|
142
|
+
elsif is_active
|
|
143
|
+
content_tag(:svg, class: "w-2.5 h-2.5 inline", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24") do
|
|
144
|
+
content_tag(:path, nil, "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M19 9l-7 7-7-7")
|
|
145
|
+
end
|
|
146
|
+
else
|
|
147
|
+
"".html_safe
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
active_class = is_active ? "text-gray-700 dark:text-gray-300" : ""
|
|
151
|
+
opacity_class = is_active ? "opacity-100" : "opacity-0 group-hover:opacity-50"
|
|
152
|
+
|
|
153
|
+
link_to url, class: "group inline-flex items-center gap-0.5 hover:text-gray-700 dark:hover:text-gray-300 #{active_class} #{extra_class}" do
|
|
154
|
+
safe_join([
|
|
155
|
+
content_tag(:span, label),
|
|
156
|
+
content_tag(:span, arrow, class: "#{opacity_class} transition-opacity")
|
|
157
|
+
])
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
123
161
|
# Syntax-highlights a Ruby object as pretty-printed JSON
|
|
124
162
|
#
|
|
125
163
|
# Converts the object to JSON and applies color highlighting
|