ruby_llm-agents 3.15.1 → 3.16.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/concerns/ruby_llm/agents/paginatable.rb +29 -8
- data/app/controllers/ruby_llm/agents/agents_controller.rb +48 -44
- data/app/controllers/ruby_llm/agents/analytics_controller.rb +38 -72
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +69 -56
- data/app/controllers/ruby_llm/agents/executions_controller.rb +25 -41
- data/app/controllers/ruby_llm/agents/requests_controller.rb +117 -45
- data/app/controllers/ruby_llm/agents/tenants_controller.rb +67 -47
- data/app/models/ruby_llm/agents/execution/analytics.rb +174 -133
- data/app/models/ruby_llm/agents/execution/breakdown.rb +243 -0
- data/app/models/ruby_llm/agents/execution/scopes.rb +39 -3
- data/app/models/ruby_llm/agents/execution.rb +23 -5
- data/app/services/ruby_llm/agents/agent_registry.rb +75 -12
- data/app/views/ruby_llm/agents/agents/index.html.erb +24 -7
- data/app/views/ruby_llm/agents/agents/show.html.erb +3 -2
- data/app/views/ruby_llm/agents/executions/_filters.html.erb +12 -7
- data/app/views/ruby_llm/agents/executions/_list.html.erb +1 -52
- data/app/views/ruby_llm/agents/requests/index.html.erb +8 -57
- data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +1 -52
- data/app/views/ruby_llm/agents/shared/_pagination.html.erb +66 -0
- data/app/views/ruby_llm/agents/shared/query_timeout.html.erb +18 -0
- data/app/views/ruby_llm/agents/system_config/show.html.erb +4 -0
- data/app/views/ruby_llm/agents/tenants/index.html.erb +1 -1
- data/app/views/ruby_llm/agents/tenants/show.html.erb +6 -5
- data/config/routes.rb +2 -2
- data/lib/generators/ruby_llm_agents/templates/add_executions_analytics_index_migration.rb.tt +50 -0
- data/lib/generators/ruby_llm_agents/templates/add_root_execution_list_index_migration.rb.tt +16 -0
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +5 -0
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +26 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +44 -0
- data/lib/ruby_llm/agents/core/configuration.rb +20 -0
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +39 -1
- data/lib/ruby_llm/agents/rails/engine.rb +87 -7
- metadata +7 -2
|
@@ -153,9 +153,9 @@ module RubyLLM
|
|
|
153
153
|
joined.where("json_extract(#{detail_table}.parameters, ?) IS NOT NULL", "$.#{key}")
|
|
154
154
|
end
|
|
155
155
|
elsif value
|
|
156
|
-
joined.where("#{detail_table}.parameters @> ?", {key => value}.to_json)
|
|
156
|
+
joined.where("#{detail_table}.parameters::jsonb @> ?", {key => value}.to_json)
|
|
157
157
|
else
|
|
158
|
-
joined.where("#{detail_table}.parameters ? :key", key: key.to_s)
|
|
158
|
+
joined.where("#{detail_table}.parameters::jsonb ? :key", key: key.to_s)
|
|
159
159
|
end
|
|
160
160
|
end
|
|
161
161
|
|
|
@@ -338,7 +338,7 @@ module RubyLLM
|
|
|
338
338
|
if connection.adapter_name.downcase.include?("sqlite")
|
|
339
339
|
where("json_extract(metadata, ?) = 1", "$.#{key}")
|
|
340
340
|
else
|
|
341
|
-
where("metadata @> ?", {key.to_s => true}.to_json)
|
|
341
|
+
where("metadata::jsonb @> ?", {key.to_s => true}.to_json)
|
|
342
342
|
end
|
|
343
343
|
end
|
|
344
344
|
|
|
@@ -355,6 +355,42 @@ module RubyLLM
|
|
|
355
355
|
end
|
|
356
356
|
end
|
|
357
357
|
|
|
358
|
+
# Distinct non-null values of a column, sorted
|
|
359
|
+
#
|
|
360
|
+
# `SELECT DISTINCT col` reads every row (or every index entry) to
|
|
361
|
+
# return a handful of values, which takes seconds on a table with
|
|
362
|
+
# millions of executions, and PostgreSQL before 18 has no skip scan
|
|
363
|
+
# to avoid it. When an index leads with the column this walks the
|
|
364
|
+
# index instead: take the smallest value, then repeatedly the
|
|
365
|
+
# smallest value greater than the last one. The cost is one index
|
|
366
|
+
# probe per distinct value, whatever the row count.
|
|
367
|
+
#
|
|
368
|
+
# Falls back to a plain DISTINCT when called on anything narrower
|
|
369
|
+
# than the whole table (the walk would ignore the filter) or when no
|
|
370
|
+
# index leads with the column (each probe would be a full scan).
|
|
371
|
+
#
|
|
372
|
+
# @param column [Symbol, String] A column of the executions table
|
|
373
|
+
# @return [Array] Sorted distinct values, nil excluded
|
|
374
|
+
def distinct_values(column)
|
|
375
|
+
column = column.to_s
|
|
376
|
+
raise ArgumentError, "Unknown column: #{column}" unless column_names.include?(column)
|
|
377
|
+
|
|
378
|
+
walkable = all.to_sql == unscoped.to_sql &&
|
|
379
|
+
connection.schema_cache.indexes(table_name).any? { |index| Array(index.columns).first == column }
|
|
380
|
+
return where.not(column => nil).distinct.pluck(column).sort unless walkable
|
|
381
|
+
|
|
382
|
+
col = connection.quote_column_name(column)
|
|
383
|
+
connection.select_values(<<~SQL.squish, "#{name} Distinct Values").sort
|
|
384
|
+
WITH RECURSIVE walk(v) AS (
|
|
385
|
+
SELECT MIN(#{col}) FROM #{quoted_table_name}
|
|
386
|
+
UNION ALL
|
|
387
|
+
SELECT (SELECT MIN(#{col}) FROM #{quoted_table_name} WHERE #{col} > walk.v)
|
|
388
|
+
FROM walk WHERE walk.v IS NOT NULL
|
|
389
|
+
)
|
|
390
|
+
SELECT v FROM walk WHERE v IS NOT NULL
|
|
391
|
+
SQL
|
|
392
|
+
end
|
|
393
|
+
|
|
358
394
|
# Returns sum of total_cost for the current scope
|
|
359
395
|
#
|
|
360
396
|
# @return [Float, nil] Total cost in USD
|
|
@@ -74,6 +74,18 @@ module RubyLLM
|
|
|
74
74
|
has_one :detail, class_name: "RubyLLM::Agents::ExecutionDetail",
|
|
75
75
|
foreign_key: :execution_id, dependent: :destroy
|
|
76
76
|
|
|
77
|
+
# Same row as :detail, but selecting only error_message.
|
|
78
|
+
#
|
|
79
|
+
# List and CSV views read nothing but the error message, while the full
|
|
80
|
+
# detail row carries prompts and response JSON that routinely run to
|
|
81
|
+
# hundreds of kilobytes each. Preloading :detail for a page of rows drags
|
|
82
|
+
# all of that into memory; preloading :error_detail costs a few bytes per
|
|
83
|
+
# row. Never use this association for anything but error_message — the
|
|
84
|
+
# other attributes are not selected and will raise.
|
|
85
|
+
has_one :error_detail, -> { select(:id, :execution_id, :error_message) },
|
|
86
|
+
class_name: "RubyLLM::Agents::ExecutionDetail",
|
|
87
|
+
foreign_key: :execution_id, inverse_of: false
|
|
88
|
+
|
|
77
89
|
# Individual tool call records (real-time tracking)
|
|
78
90
|
has_many :tool_executions, class_name: "RubyLLM::Agents::ToolExecution",
|
|
79
91
|
foreign_key: :execution_id, dependent: :destroy
|
|
@@ -94,7 +106,8 @@ module RubyLLM
|
|
|
94
106
|
#
|
|
95
107
|
# @return [String, nil]
|
|
96
108
|
def error_message
|
|
97
|
-
|
|
109
|
+
row = association(:detail).loaded? ? detail : error_detail
|
|
110
|
+
row&.error_message || metadata&.dig("error_message")
|
|
98
111
|
end
|
|
99
112
|
|
|
100
113
|
# Validations
|
|
@@ -348,8 +361,11 @@ module RubyLLM
|
|
|
348
361
|
# instead of ~15 individual count/sum/average queries.
|
|
349
362
|
#
|
|
350
363
|
# @param range [String] Time range: "today", "7d", "30d", or "90d"
|
|
364
|
+
# @param current [Hash, nil] Stats for the current period, when the caller
|
|
365
|
+
# already has them (a {Breakdown#totals} of the same window) — saves
|
|
366
|
+
# scanning that window a second time
|
|
351
367
|
# @return [Hash] Now strip metrics with period-over-period comparisons
|
|
352
|
-
def self.now_strip_data(range: "today")
|
|
368
|
+
def self.now_strip_data(range: "today", current: nil)
|
|
353
369
|
current_scope = case range
|
|
354
370
|
when "7d" then last_n_days(7)
|
|
355
371
|
when "30d" then last_n_days(30)
|
|
@@ -364,7 +380,7 @@ module RubyLLM
|
|
|
364
380
|
else yesterday
|
|
365
381
|
end
|
|
366
382
|
|
|
367
|
-
curr = aggregate_period_stats(current_scope)
|
|
383
|
+
curr = current || aggregate_period_stats(current_scope)
|
|
368
384
|
prev = aggregate_period_stats(previous_scope)
|
|
369
385
|
|
|
370
386
|
{
|
|
@@ -395,15 +411,17 @@ module RubyLLM
|
|
|
395
411
|
#
|
|
396
412
|
# @param from [Date] Start date (inclusive)
|
|
397
413
|
# @param to [Date] End date (inclusive)
|
|
414
|
+
# @param current [Hash, nil] Precomputed stats for the selected range (see
|
|
415
|
+
# {.now_strip_data})
|
|
398
416
|
# @return [Hash] Now strip metrics with period-over-period comparisons
|
|
399
|
-
def self.now_strip_data_for_dates(from:, to:)
|
|
417
|
+
def self.now_strip_data_for_dates(from:, to:, current: nil)
|
|
400
418
|
span_days = (to - from).to_i + 1
|
|
401
419
|
current_scope = where(created_at: from.beginning_of_day..to.end_of_day)
|
|
402
420
|
previous_from = from - span_days.days
|
|
403
421
|
previous_to = from - 1.day
|
|
404
422
|
previous_scope = where(created_at: previous_from.beginning_of_day..previous_to.end_of_day)
|
|
405
423
|
|
|
406
|
-
curr = aggregate_period_stats(current_scope)
|
|
424
|
+
curr = current || aggregate_period_stats(current_scope)
|
|
407
425
|
prev = aggregate_period_stats(previous_scope)
|
|
408
426
|
|
|
409
427
|
{
|
|
@@ -21,6 +21,14 @@ module RubyLLM
|
|
|
21
21
|
#
|
|
22
22
|
# @api public
|
|
23
23
|
class AgentRegistry
|
|
24
|
+
# Window the per-agent stats in {.all_with_details} cover
|
|
25
|
+
#
|
|
26
|
+
# All-time stats mean reading the whole executions table, which stops
|
|
27
|
+
# being viable in the millions of rows. A recent window keeps the list
|
|
28
|
+
# proportional to current traffic, and is the more useful health signal
|
|
29
|
+
# anyway.
|
|
30
|
+
STATS_WINDOW = 30.days
|
|
31
|
+
|
|
24
32
|
class << self
|
|
25
33
|
# Returns all unique agent type names
|
|
26
34
|
#
|
|
@@ -47,10 +55,18 @@ module RubyLLM
|
|
|
47
55
|
|
|
48
56
|
# Returns detailed info about all agents
|
|
49
57
|
#
|
|
58
|
+
# Usage figures (:execution_count, :total_cost, :total_tokens,
|
|
59
|
+
# :avg_duration_ms, :success_rate, :error_rate) cover the last
|
|
60
|
+
# STATS_WINDOW and come from one grouped query shared by every agent.
|
|
61
|
+
# They are nil when that query could not finish within
|
|
62
|
+
# config.dashboard_query_timeout. :last_executed is all-time.
|
|
63
|
+
#
|
|
50
64
|
# @return [Array<Hash>] Agent info hashes with configuration and stats
|
|
51
65
|
def all_with_details
|
|
66
|
+
recent = recent_usage
|
|
67
|
+
|
|
52
68
|
all.map do |agent_type|
|
|
53
|
-
build_agent_info(agent_type)
|
|
69
|
+
build_agent_info(agent_type, recent)
|
|
54
70
|
end
|
|
55
71
|
end
|
|
56
72
|
|
|
@@ -183,9 +199,16 @@ module RubyLLM
|
|
|
183
199
|
|
|
184
200
|
# Finds agent types from execution history
|
|
185
201
|
#
|
|
202
|
+
# Cached briefly: it only exists to keep deleted agents visible, so a
|
|
203
|
+
# few minutes of staleness is invisible.
|
|
204
|
+
#
|
|
186
205
|
# @return [Array<String>] Agent class names with execution records
|
|
187
206
|
def execution_agents
|
|
188
|
-
|
|
207
|
+
Rails.cache.fetch(["ruby_llm_agents", "agent_registry", "execution_agents"], expires_in: 5.minutes) do
|
|
208
|
+
Execution.distinct_values(:agent_type)
|
|
209
|
+
end
|
|
210
|
+
rescue ::ActiveRecord::QueryCanceled
|
|
211
|
+
raise
|
|
189
212
|
rescue => e
|
|
190
213
|
Rails.logger.error("[RubyLLM::Agents] Error loading agents from executions: #{e.message}")
|
|
191
214
|
[]
|
|
@@ -213,10 +236,11 @@ module RubyLLM
|
|
|
213
236
|
# Builds detailed info hash for an agent
|
|
214
237
|
#
|
|
215
238
|
# @param agent_type [String] The agent class name
|
|
239
|
+
# @param recent [Execution::Breakdown, nil] Usage over STATS_WINDOW
|
|
216
240
|
# @return [Hash] Agent info including config and stats
|
|
217
|
-
def build_agent_info(agent_type)
|
|
241
|
+
def build_agent_info(agent_type, recent)
|
|
218
242
|
agent_class = find(agent_type)
|
|
219
|
-
stats = fetch_stats(agent_type)
|
|
243
|
+
stats = fetch_stats(agent_type, recent)
|
|
220
244
|
|
|
221
245
|
# Detect the agent type (agent, embedder, speaker, transcriber, image_generator)
|
|
222
246
|
detected_type = detect_agent_type(agent_class)
|
|
@@ -240,7 +264,7 @@ module RubyLLM
|
|
|
240
264
|
avg_duration_ms: stats[:avg_duration_ms],
|
|
241
265
|
success_rate: stats[:success_rate],
|
|
242
266
|
error_rate: stats[:error_rate],
|
|
243
|
-
last_executed: last_execution_time(agent_type)
|
|
267
|
+
last_executed: stats[:last_seen] || last_execution_time(agent_type)
|
|
244
268
|
}
|
|
245
269
|
end
|
|
246
270
|
|
|
@@ -258,22 +282,61 @@ module RubyLLM
|
|
|
258
282
|
nil
|
|
259
283
|
end
|
|
260
284
|
|
|
261
|
-
#
|
|
285
|
+
# Usage of every agent over STATS_WINDOW, from one grouped query
|
|
286
|
+
#
|
|
287
|
+
# Cached, since the agents list is the same for everyone looking at
|
|
288
|
+
# it, and best-effort: nil (also cached) when the database cannot
|
|
289
|
+
# answer within config.dashboard_query_timeout.
|
|
290
|
+
#
|
|
291
|
+
# @return [Execution::Breakdown, nil]
|
|
292
|
+
def recent_usage
|
|
293
|
+
rows = Rails.cache.fetch(["ruby_llm_agents", "agent_registry", "recent_usage"], expires_in: 5.minutes) do
|
|
294
|
+
Execution.best_effort(timeout: RubyLLM::Agents.configuration.dashboard_query_timeout) do
|
|
295
|
+
Execution.where("created_at >= ?", STATS_WINDOW.ago).breakdown.rows
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
rows && Execution::Breakdown.new(rows)
|
|
300
|
+
rescue => e
|
|
301
|
+
Rails.logger.error("[RubyLLM::Agents] Error loading agent stats: #{e.message}")
|
|
302
|
+
nil
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Picks one agent's statistics out of the shared usage breakdown
|
|
306
|
+
#
|
|
307
|
+
# Executions recorded under the agent's previous names (aliases) count
|
|
308
|
+
# towards it.
|
|
262
309
|
#
|
|
263
310
|
# @param agent_type [String] The agent class name
|
|
264
|
-
# @
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
311
|
+
# @param recent [Execution::Breakdown, nil] Usage over STATS_WINDOW
|
|
312
|
+
# @return [Hash] Statistics hash; values are nil when usage is unavailable
|
|
313
|
+
def fetch_stats(agent_type, recent)
|
|
314
|
+
return {} unless recent
|
|
315
|
+
|
|
316
|
+
totals = recent.for_agents(Execution.resolve_agent_names(agent_type)).totals
|
|
317
|
+
|
|
318
|
+
{
|
|
319
|
+
count: totals[:total],
|
|
320
|
+
total_cost: totals[:cost],
|
|
321
|
+
total_tokens: totals[:tokens],
|
|
322
|
+
avg_duration_ms: totals[:avg_duration_ms],
|
|
323
|
+
success_rate: totals[:success_rate],
|
|
324
|
+
error_rate: totals[:error_rate],
|
|
325
|
+
last_seen: totals[:last_seen]
|
|
326
|
+
}
|
|
269
327
|
end
|
|
270
328
|
|
|
271
329
|
# Gets the timestamp of the last execution for an agent
|
|
272
330
|
#
|
|
331
|
+
# Only needed for agents with no runs inside STATS_WINDOW. One index
|
|
332
|
+
# probe per name the agent has gone by.
|
|
333
|
+
#
|
|
273
334
|
# @param agent_type [String] The agent class name
|
|
274
335
|
# @return [Time, nil] Last execution time or nil
|
|
275
336
|
def last_execution_time(agent_type)
|
|
276
|
-
Execution.
|
|
337
|
+
Execution.resolve_agent_names(agent_type)
|
|
338
|
+
.filter_map { |name| Execution.where(agent_type: name).maximum(:created_at) }
|
|
339
|
+
.max
|
|
277
340
|
rescue
|
|
278
341
|
nil
|
|
279
342
|
end
|
|
@@ -42,6 +42,11 @@
|
|
|
42
42
|
<span x-text="currentCount"></span>
|
|
43
43
|
<%= render "ruby_llm/agents/shared/doc_link" %>
|
|
44
44
|
</div>
|
|
45
|
+
<% if @stats_available %>
|
|
46
|
+
<span class="font-mono text-[10px] text-gray-400 dark:text-gray-600">runs, cost and ok% cover the last <%= @stats_window_days %> days</span>
|
|
47
|
+
<% else %>
|
|
48
|
+
<span class="font-mono text-[10px] text-yellow-600 dark:text-yellow-500" role="status">usage stats took too long to load and were skipped</span>
|
|
49
|
+
<% end %>
|
|
45
50
|
<div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
|
|
46
51
|
<div class="flex font-mono text-xs gap-0.5">
|
|
47
52
|
<button type="button" @click="activeSubTab = null; updateUrl()"
|
|
@@ -108,7 +113,7 @@
|
|
|
108
113
|
"activeSubTab !== 'deleted' && (activeSubTab === null || activeSubTab === '#{agent[:agent_type]}')"
|
|
109
114
|
end
|
|
110
115
|
success_rate = agent[:success_rate] || 0
|
|
111
|
-
dot_color = agent[:active] ? (success_rate >= 95 ? 'bg-green-500' : success_rate >= 80 ? 'bg-yellow-500' : 'bg-red-500')
|
|
116
|
+
dot_color = !agent[:active] || !@stats_available ? 'bg-gray-400' : (success_rate >= 95 ? 'bg-green-500' : success_rate >= 80 ? 'bg-yellow-500' : 'bg-red-500')
|
|
112
117
|
%>
|
|
113
118
|
<div x-show="<%= show_condition %>"
|
|
114
119
|
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"
|
|
@@ -120,9 +125,15 @@
|
|
|
120
125
|
<span class="flex-1 min-w-0 truncate text-gray-500 dark:text-gray-400 hidden sm:inline"><%= agent[:agent_type]&.to_s || 'agent' %></span>
|
|
121
126
|
<span class="w-1.5 h-1.5 rounded-full flex-shrink-0 <%= dot_color %>"></span>
|
|
122
127
|
<span class="flex-[2] min-w-0 truncate text-gray-400 dark:text-gray-600 hidden lg:inline"><%= agent[:model]&.split("/")&.last || "—" %></span>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
128
|
+
<% if @stats_available %>
|
|
129
|
+
<span class="w-14 flex-shrink-0 text-right text-gray-500 dark:text-gray-400"><%= number_with_delimiter(agent[:execution_count] || 0) %><span class="text-gray-400 dark:text-gray-600">r</span></span>
|
|
130
|
+
<span class="w-16 flex-shrink-0 text-right text-gray-500 dark:text-gray-400 hidden md:inline">$<%= number_with_precision(agent[:total_cost] || 0, precision: 2) %></span>
|
|
131
|
+
<span class="w-12 flex-shrink-0 text-right <%= success_rate >= 95 ? 'text-green-500' : success_rate >= 80 ? 'text-yellow-500' : 'text-red-500' %>"><%= success_rate.round %>%</span>
|
|
132
|
+
<% else %>
|
|
133
|
+
<span class="w-14 flex-shrink-0 text-right text-gray-400 dark:text-gray-600">—</span>
|
|
134
|
+
<span class="w-16 flex-shrink-0 text-right text-gray-400 dark:text-gray-600 hidden md:inline">—</span>
|
|
135
|
+
<span class="w-12 flex-shrink-0 text-right text-gray-400 dark:text-gray-600">—</span>
|
|
136
|
+
<% end %>
|
|
126
137
|
<span class="w-24 flex-shrink-0 text-gray-400 dark:text-gray-600 text-right whitespace-nowrap">
|
|
127
138
|
<% if agent[:last_executed] %>
|
|
128
139
|
<%= time_ago_in_words(agent[:last_executed]) %>
|
|
@@ -144,9 +155,15 @@
|
|
|
144
155
|
<span class="flex-1 min-w-0 truncate text-gray-400 dark:text-gray-600 hidden sm:inline"><%= agent[:agent_type]&.to_s || 'agent' %></span>
|
|
145
156
|
<span class="w-1.5 h-1.5 rounded-full flex-shrink-0 bg-gray-400 dark:bg-gray-600"></span>
|
|
146
157
|
<span class="flex-[2] min-w-0 truncate text-gray-400 dark:text-gray-600 hidden lg:inline"><%= agent[:model]&.split("/")&.last || "—" %></span>
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
158
|
+
<% if @stats_available %>
|
|
159
|
+
<span class="w-14 flex-shrink-0 text-right text-gray-400 dark:text-gray-600"><%= number_with_delimiter(agent[:execution_count] || 0) %><span class="text-gray-400 dark:text-gray-600">r</span></span>
|
|
160
|
+
<span class="w-16 flex-shrink-0 text-right text-gray-400 dark:text-gray-600 hidden md:inline">$<%= number_with_precision(agent[:total_cost] || 0, precision: 2) %></span>
|
|
161
|
+
<span class="w-12 flex-shrink-0 text-right text-gray-400 dark:text-gray-600"><%= success_rate.round %>%</span>
|
|
162
|
+
<% else %>
|
|
163
|
+
<span class="w-14 flex-shrink-0 text-right text-gray-400 dark:text-gray-600">—</span>
|
|
164
|
+
<span class="w-16 flex-shrink-0 text-right text-gray-400 dark:text-gray-600 hidden md:inline">—</span>
|
|
165
|
+
<span class="w-12 flex-shrink-0 text-right text-gray-400 dark:text-gray-600">—</span>
|
|
166
|
+
<% end %>
|
|
150
167
|
<span class="w-24 flex-shrink-0 text-gray-400 dark:text-gray-600 text-right whitespace-nowrap">
|
|
151
168
|
<% if agent[:last_executed] %>
|
|
152
169
|
<%= time_ago_in_words(agent[:last_executed]) %>
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
<div class="flex-shrink-0 text-right font-mono text-xs text-gray-400 dark:text-gray-500 space-y-1">
|
|
76
76
|
<% success_rate = @stats[:success_rate] || 0 %>
|
|
77
77
|
<div class="flex items-center justify-end gap-x-4">
|
|
78
|
+
<span class="text-gray-300 dark:text-gray-700"><%= @stats_window_days %>d</span>
|
|
78
79
|
<span><span class="text-gray-800 dark:text-gray-200"><%= number_with_delimiter(@stats[:count]) %></span> runs</span>
|
|
79
80
|
<span class="<%= success_rate >= 95 ? 'text-green-500' : success_rate >= 80 ? 'text-yellow-500' : 'text-red-500' %>"><%= success_rate %>% ok</span>
|
|
80
81
|
<span><span class="text-gray-800 dark:text-gray-200">$<%= number_with_precision(@stats[:total_cost] || 0, precision: 4) %></span> cost</span>
|
|
@@ -311,11 +312,11 @@
|
|
|
311
312
|
|
|
312
313
|
<%= render "ruby_llm/agents/shared/executions_table", executions: @executions, pagination: @pagination, hide_agent_column: true %>
|
|
313
314
|
|
|
314
|
-
<% if @
|
|
315
|
+
<% if @executions.any? %>
|
|
315
316
|
<div class="mt-3 font-mono text-xs">
|
|
316
317
|
<%= link_to ruby_llm_agents.executions_path("agent_types[]": @agent_type),
|
|
317
318
|
class: "text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300" do %>
|
|
318
|
-
show all
|
|
319
|
+
show all executions →
|
|
319
320
|
<% end %>
|
|
320
321
|
</div>
|
|
321
322
|
<% end %>
|
|
@@ -156,13 +156,18 @@
|
|
|
156
156
|
<%# Stats %>
|
|
157
157
|
<div id="filter-stats" class="flex items-center justify-between md:justify-end gap-2 py-2 px-3 md:p-0 bg-gray-50 md:bg-transparent dark:bg-gray-700/50 md:dark:bg-transparent rounded-lg md:rounded-none">
|
|
158
158
|
<span class="text-xs text-gray-400 dark:text-gray-500 md:hidden">Results</span>
|
|
159
|
-
|
|
160
|
-
<
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
159
|
+
<% if filter_stats %>
|
|
160
|
+
<div class="flex items-center gap-2 text-xs text-gray-400 dark:text-gray-500">
|
|
161
|
+
<span class="tabular-nums"><%= number_to_human_short(filter_stats[:total_count]) %><span class="md:hidden"> executions</span></span>
|
|
162
|
+
<span class="text-gray-300 dark:text-gray-600">/</span>
|
|
163
|
+
<span class="tabular-nums"><%= number_to_human_short(filter_stats[:total_cost], prefix: "$", precision: 2) %></span>
|
|
164
|
+
<span class="hidden md:inline text-gray-300 dark:text-gray-600">/</span>
|
|
165
|
+
<span class="hidden md:inline tabular-nums"><%= number_to_human_short(filter_stats[:total_tokens]) %>t</span>
|
|
166
|
+
</div>
|
|
167
|
+
<% else %>
|
|
168
|
+
<span class="text-xs text-gray-400 dark:text-gray-500 cursor-help"
|
|
169
|
+
title="Too many executions match to total them quickly. Pick a time range to see totals.">totals: pick a time range</span>
|
|
170
|
+
<% end %>
|
|
166
171
|
</div>
|
|
167
172
|
|
|
168
173
|
<%# Actions %>
|
|
@@ -77,57 +77,6 @@
|
|
|
77
77
|
<% end %>
|
|
78
78
|
</div>
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
<% if pagination && pagination[:total_pages] > 1 %>
|
|
82
|
-
<%
|
|
83
|
-
current_page = pagination[:current_page]
|
|
84
|
-
total_pages = pagination[:total_pages]
|
|
85
|
-
total_count = pagination[:total_count]
|
|
86
|
-
per_page = pagination[:per_page]
|
|
87
|
-
from_record = ((current_page - 1) * per_page) + 1
|
|
88
|
-
to_record = [current_page * per_page, total_count].min
|
|
89
|
-
%>
|
|
90
|
-
<div class="flex items-center justify-between font-mono text-xs mt-4 pt-4 border-t border-gray-200 dark:border-gray-800">
|
|
91
|
-
<span class="text-gray-400 dark:text-gray-600"><%= from_record %>-<%= to_record %> of <%= number_with_delimiter(total_count) %></span>
|
|
92
|
-
<nav class="flex items-center gap-1">
|
|
93
|
-
<% if current_page > 1 %>
|
|
94
|
-
<%= link_to "prev", url_for(request.query_parameters.merge(page: current_page - 1)),
|
|
95
|
-
class: "px-2 py-0.5 text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300" %>
|
|
96
|
-
<% else %>
|
|
97
|
-
<span class="px-2 py-0.5 text-gray-300 dark:text-gray-700">prev</span>
|
|
98
|
-
<% end %>
|
|
99
|
-
|
|
100
|
-
<%
|
|
101
|
-
window = 2
|
|
102
|
-
pages_to_show = []
|
|
103
|
-
(1..total_pages).each do |page|
|
|
104
|
-
if page <= 1 || page >= total_pages || (page >= current_page - window && page <= current_page + window)
|
|
105
|
-
pages_to_show << page
|
|
106
|
-
elsif pages_to_show.last != :gap
|
|
107
|
-
pages_to_show << :gap
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
%>
|
|
111
|
-
|
|
112
|
-
<% pages_to_show.each do |page| %>
|
|
113
|
-
<% if page == :gap %>
|
|
114
|
-
<span class="px-1 text-gray-400 dark:text-gray-600">...</span>
|
|
115
|
-
<% elsif page == current_page %>
|
|
116
|
-
<span class="px-2 py-0.5 text-gray-900 dark:text-gray-100"><%= page %></span>
|
|
117
|
-
<% else %>
|
|
118
|
-
<%= link_to page, url_for(request.query_parameters.merge(page: page)),
|
|
119
|
-
class: "px-2 py-0.5 text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300" %>
|
|
120
|
-
<% end %>
|
|
121
|
-
<% end %>
|
|
122
|
-
|
|
123
|
-
<% if current_page < total_pages %>
|
|
124
|
-
<%= link_to "next", url_for(request.query_parameters.merge(page: current_page + 1)),
|
|
125
|
-
class: "px-2 py-0.5 text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300" %>
|
|
126
|
-
<% else %>
|
|
127
|
-
<span class="px-2 py-0.5 text-gray-300 dark:text-gray-700">next</span>
|
|
128
|
-
<% end %>
|
|
129
|
-
</nav>
|
|
130
|
-
</div>
|
|
131
|
-
<% end %>
|
|
80
|
+
<%= render "ruby_llm/agents/shared/pagination", pagination: pagination, shown: executions.size if pagination %>
|
|
132
81
|
<% end %>
|
|
133
82
|
</div>
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
<div class="flex items-center gap-3 mb-6">
|
|
2
2
|
<span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">requests</span>
|
|
3
3
|
<div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
<% if @stats %>
|
|
5
|
+
<span class="font-mono text-[10px] text-gray-400 dark:text-gray-600">
|
|
6
|
+
<%= number_with_delimiter(@stats[:total_requests]) %> tracked
|
|
7
|
+
·
|
|
8
|
+
$<%= number_with_precision(@stats[:total_cost] || 0, precision: 4) %> total
|
|
9
|
+
</span>
|
|
10
|
+
<% end %>
|
|
9
11
|
</div>
|
|
10
12
|
|
|
11
13
|
<%
|
|
@@ -98,56 +100,5 @@
|
|
|
98
100
|
<% end %>
|
|
99
101
|
</div>
|
|
100
102
|
|
|
101
|
-
|
|
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 %>
|
|
103
|
+
<%= render "ruby_llm/agents/shared/pagination", pagination: @pagination, shown: @requests.size if @pagination %>
|
|
153
104
|
<% end %>
|
|
@@ -63,56 +63,5 @@
|
|
|
63
63
|
<% end %>
|
|
64
64
|
</div>
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
<% if pagination[:total_pages] > 1 %>
|
|
68
|
-
<%
|
|
69
|
-
current_page = pagination[:current_page]
|
|
70
|
-
total_pages = pagination[:total_pages]
|
|
71
|
-
total_count = pagination[:total_count]
|
|
72
|
-
per_page = pagination[:per_page]
|
|
73
|
-
from_record = ((current_page - 1) * per_page) + 1
|
|
74
|
-
to_record = [current_page * per_page, total_count].min
|
|
75
|
-
%>
|
|
76
|
-
<div class="flex items-center justify-between font-mono text-xs mt-4 pt-4 border-t border-gray-200 dark:border-gray-800">
|
|
77
|
-
<span class="text-gray-400 dark:text-gray-600"><%= from_record %>-<%= to_record %> of <%= number_with_delimiter(total_count) %></span>
|
|
78
|
-
<nav class="flex items-center gap-1">
|
|
79
|
-
<% if current_page > 1 %>
|
|
80
|
-
<%= link_to "prev", url_for(request.query_parameters.merge(page: current_page - 1)),
|
|
81
|
-
class: "px-2 py-0.5 text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300" %>
|
|
82
|
-
<% else %>
|
|
83
|
-
<span class="px-2 py-0.5 text-gray-300 dark:text-gray-700">prev</span>
|
|
84
|
-
<% end %>
|
|
85
|
-
|
|
86
|
-
<%
|
|
87
|
-
window = 2
|
|
88
|
-
pages_to_show = []
|
|
89
|
-
(1..total_pages).each do |page|
|
|
90
|
-
if page <= 1 || page >= total_pages || (page >= current_page - window && page <= current_page + window)
|
|
91
|
-
pages_to_show << page
|
|
92
|
-
elsif pages_to_show.last != :gap
|
|
93
|
-
pages_to_show << :gap
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
%>
|
|
97
|
-
|
|
98
|
-
<% pages_to_show.each do |page| %>
|
|
99
|
-
<% if page == :gap %>
|
|
100
|
-
<span class="px-1 text-gray-400 dark:text-gray-600">...</span>
|
|
101
|
-
<% elsif page == current_page %>
|
|
102
|
-
<span class="px-2 py-0.5 text-gray-900 dark:text-gray-100"><%= page %></span>
|
|
103
|
-
<% else %>
|
|
104
|
-
<%= link_to page, url_for(request.query_parameters.merge(page: page)),
|
|
105
|
-
class: "px-2 py-0.5 text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300" %>
|
|
106
|
-
<% end %>
|
|
107
|
-
<% end %>
|
|
108
|
-
|
|
109
|
-
<% if current_page < total_pages %>
|
|
110
|
-
<%= link_to "next", url_for(request.query_parameters.merge(page: current_page + 1)),
|
|
111
|
-
class: "px-2 py-0.5 text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300" %>
|
|
112
|
-
<% else %>
|
|
113
|
-
<span class="px-2 py-0.5 text-gray-300 dark:text-gray-700">next</span>
|
|
114
|
-
<% end %>
|
|
115
|
-
</nav>
|
|
116
|
-
</div>
|
|
117
|
-
<% end %>
|
|
66
|
+
<%= render "ruby_llm/agents/shared/pagination", pagination: pagination, shown: executions.size %>
|
|
118
67
|
<% end %>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Pagination links for a list page.
|
|
3
|
+
|
|
4
|
+
pagination: the hash built by Paginatable#paginate. When :total_count is
|
|
5
|
+
known this renders "1-25 of 1,234" with numbered pages; when it is not
|
|
6
|
+
(counting was skipped or timed out on a very large table) it renders the
|
|
7
|
+
current range with prev/next only.
|
|
8
|
+
%>
|
|
9
|
+
<%
|
|
10
|
+
current_page = pagination[:current_page]
|
|
11
|
+
per_page = pagination[:per_page]
|
|
12
|
+
total_count = pagination[:total_count]
|
|
13
|
+
total_pages = pagination[:total_pages]
|
|
14
|
+
has_next = total_pages ? current_page < total_pages : pagination[:next_page]
|
|
15
|
+
from_record = ((current_page - 1) * per_page) + 1
|
|
16
|
+
link_class = "px-2 py-0.5 text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
|
|
17
|
+
%>
|
|
18
|
+
<% if current_page > 1 || has_next %>
|
|
19
|
+
<div class="flex items-center justify-between font-mono text-xs mt-4 pt-4 border-t border-gray-200 dark:border-gray-800">
|
|
20
|
+
<span class="text-gray-400 dark:text-gray-600">
|
|
21
|
+
<% if total_count %>
|
|
22
|
+
<%= from_record %>-<%= [current_page * per_page, total_count].min %> of <%= number_with_delimiter(total_count) %>
|
|
23
|
+
<% else %>
|
|
24
|
+
<%= number_with_delimiter(from_record) %>-<%= number_with_delimiter(from_record + local_assigns.fetch(:shown, per_page) - 1) %>
|
|
25
|
+
<% end %>
|
|
26
|
+
</span>
|
|
27
|
+
<nav class="flex items-center gap-1" aria-label="Pagination">
|
|
28
|
+
<% if current_page > 1 %>
|
|
29
|
+
<%= link_to "prev", url_for(request.query_parameters.merge(page: current_page - 1)), class: link_class %>
|
|
30
|
+
<% else %>
|
|
31
|
+
<span class="px-2 py-0.5 text-gray-300 dark:text-gray-700">prev</span>
|
|
32
|
+
<% end %>
|
|
33
|
+
|
|
34
|
+
<% if total_pages %>
|
|
35
|
+
<%
|
|
36
|
+
window = 2
|
|
37
|
+
pages_to_show = []
|
|
38
|
+
(1..total_pages).each do |page|
|
|
39
|
+
if page <= 1 || page >= total_pages || (page >= current_page - window && page <= current_page + window)
|
|
40
|
+
pages_to_show << page
|
|
41
|
+
elsif pages_to_show.last != :gap
|
|
42
|
+
pages_to_show << :gap
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
%>
|
|
46
|
+
<% pages_to_show.each do |page| %>
|
|
47
|
+
<% if page == :gap %>
|
|
48
|
+
<span class="px-1 text-gray-400 dark:text-gray-600">...</span>
|
|
49
|
+
<% elsif page == current_page %>
|
|
50
|
+
<span class="px-2 py-0.5 text-gray-900 dark:text-gray-100" aria-current="page"><%= page %></span>
|
|
51
|
+
<% else %>
|
|
52
|
+
<%= link_to page, url_for(request.query_parameters.merge(page: page)), class: link_class %>
|
|
53
|
+
<% end %>
|
|
54
|
+
<% end %>
|
|
55
|
+
<% else %>
|
|
56
|
+
<span class="px-2 py-0.5 text-gray-900 dark:text-gray-100" aria-current="page"><%= current_page %></span>
|
|
57
|
+
<% end %>
|
|
58
|
+
|
|
59
|
+
<% if has_next %>
|
|
60
|
+
<%= link_to "next", url_for(request.query_parameters.merge(page: current_page + 1)), class: link_class %>
|
|
61
|
+
<% else %>
|
|
62
|
+
<span class="px-2 py-0.5 text-gray-300 dark:text-gray-700">next</span>
|
|
63
|
+
<% end %>
|
|
64
|
+
</nav>
|
|
65
|
+
</div>
|
|
66
|
+
<% end %>
|