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
|
@@ -0,0 +1,18 @@
|
|
|
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">query timed out</span>
|
|
3
|
+
<div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div class="font-mono text-xs space-y-3" role="alert">
|
|
7
|
+
<p class="text-gray-900 dark:text-gray-100">
|
|
8
|
+
This view needed more than <%= @query_timeout %>s of database time, so the query was cancelled.
|
|
9
|
+
</p>
|
|
10
|
+
<p class="text-gray-500 dark:text-gray-400">
|
|
11
|
+
It aggregates every execution in the selected range. Try a shorter time range or a narrower filter.
|
|
12
|
+
The limit is <code class="text-gray-700 dark:text-gray-300">config.dashboard_query_timeout</code>.
|
|
13
|
+
</p>
|
|
14
|
+
<p>
|
|
15
|
+
<%= link_to "← back to dashboard", ruby_llm_agents.root_path,
|
|
16
|
+
class: "text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 focus:outline-none focus:underline" %>
|
|
17
|
+
</p>
|
|
18
|
+
</div>
|
|
@@ -106,6 +106,10 @@
|
|
|
106
106
|
<span class="w-36 flex-shrink-0 text-gray-500 dark:text-gray-400">recent limit</span>
|
|
107
107
|
<span class="text-gray-900 dark:text-gray-200"><%= @config.recent_executions_limit %></span>
|
|
108
108
|
</div>
|
|
109
|
+
<div class="flex items-center gap-3 py-0.5">
|
|
110
|
+
<span class="w-36 flex-shrink-0 text-gray-500 dark:text-gray-400">query timeout</span>
|
|
111
|
+
<span class="text-gray-900 dark:text-gray-200"><%= @config.dashboard_query_timeout ? "#{@config.dashboard_query_timeout}s" : "off" %></span>
|
|
112
|
+
</div>
|
|
109
113
|
<div class="flex items-center gap-3 py-0.5">
|
|
110
114
|
<span class="w-36 flex-shrink-0 text-gray-500 dark:text-gray-400">parent controller</span>
|
|
111
115
|
<span class="text-gray-900 dark:text-gray-200"><%= @config.dashboard_parent_controller %></span>
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
class: "w-20 flex-shrink-0 text-right hidden sm:block hover:text-gray-600 dark:hover:text-gray-300 #{sort_active.call('monthly_limit')}" %>
|
|
52
52
|
<%= link_to "enforcement#{sort_arrow.call('enforcement')}".html_safe, sort_url.call("enforcement"),
|
|
53
53
|
class: "w-16 flex-shrink-0 hidden md:block hover:text-gray-600 dark:hover:text-gray-300 #{sort_active.call('enforcement')}" %>
|
|
54
|
-
<span class="w-16 flex-shrink-0 text-right hidden md:block">cost</span>
|
|
54
|
+
<span class="w-16 flex-shrink-0 text-right hidden md:block" title="Cost this month">cost/mo</span>
|
|
55
55
|
<span class="w-24 flex-shrink-0 text-right">last run</span>
|
|
56
56
|
</div>
|
|
57
57
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
total_executions = @usage_stats[:total_executions]
|
|
9
9
|
total_cost = @usage_stats[:total_cost]
|
|
10
10
|
total_tokens = @usage_stats[:total_tokens]
|
|
11
|
-
success_count = @
|
|
11
|
+
success_count = @usage_stats[:success_count]
|
|
12
12
|
success_rate = total_executions > 0 ? (success_count.to_f / total_executions * 100).round : 100
|
|
13
13
|
|
|
14
14
|
has_any_limit = @tenant.effective_daily_limit || @tenant.effective_monthly_limit ||
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
<!-- Right: stats -->
|
|
65
65
|
<div class="flex-shrink-0 text-right font-mono text-xs text-gray-400 dark:text-gray-500 space-y-1">
|
|
66
66
|
<div class="flex items-center justify-end gap-x-4">
|
|
67
|
+
<span class="text-gray-300 dark:text-gray-700">this month</span>
|
|
67
68
|
<span><span class="text-gray-800 dark:text-gray-200"><%= number_with_delimiter(total_executions) %></span> runs</span>
|
|
68
69
|
<span class="<%= success_rate >= 95 ? 'text-green-500' : success_rate >= 80 ? 'text-yellow-500' : 'text-red-500' %>"><%= success_rate %>% ok</span>
|
|
69
70
|
<span><span class="text-gray-800 dark:text-gray-200">$<%= number_with_precision(total_cost, precision: 4) %></span> cost</span>
|
|
@@ -127,10 +128,10 @@
|
|
|
127
128
|
<% if @error_count > 0 %>
|
|
128
129
|
<div class="mt-3 font-mono text-xs text-gray-500 dark:text-gray-400">
|
|
129
130
|
<span class="text-red-500">$<%= number_with_precision(@error_cost, precision: 4) %></span>
|
|
130
|
-
wasted on <span class="text-red-500"><%= number_with_delimiter(@error_count) %></span> failed executions
|
|
131
|
+
wasted on <span class="text-red-500"><%= number_with_delimiter(@error_count) %></span> failed executions this month
|
|
131
132
|
<% error_pct = total_cost.to_f > 0 ? (@error_cost.to_f / total_cost * 100).round(1) : 0 %>
|
|
132
133
|
<% if error_pct > 0 %>
|
|
133
|
-
(<%= error_pct %>% of
|
|
134
|
+
(<%= error_pct %>% of this month's cost)
|
|
134
135
|
<% end %>
|
|
135
136
|
</div>
|
|
136
137
|
<% end %>
|
|
@@ -321,7 +322,7 @@
|
|
|
321
322
|
<% if @usage_by_agent.present? %>
|
|
322
323
|
<!-- ── usage by agent ──────────────────── -->
|
|
323
324
|
<div class="flex items-center gap-3 mt-6 mb-3">
|
|
324
|
-
<span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">usage by agent</span>
|
|
325
|
+
<span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">usage by agent · this month</span>
|
|
325
326
|
<div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
|
|
326
327
|
</div>
|
|
327
328
|
|
|
@@ -359,7 +360,7 @@
|
|
|
359
360
|
<% if @usage_by_model.present? %>
|
|
360
361
|
<!-- ── usage by model ──────────────────── -->
|
|
361
362
|
<div class="flex items-center gap-3 mt-6 mb-3">
|
|
362
|
-
<span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">usage by model</span>
|
|
363
|
+
<span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">usage by model · this month</span>
|
|
363
364
|
<div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
|
|
364
365
|
</div>
|
|
365
366
|
|
data/config/routes.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
RubyLLM::Agents::Engine.routes.draw do
|
|
4
4
|
root to: "dashboard#index"
|
|
5
|
-
get "chart_data", to: "dashboard#chart_data"
|
|
5
|
+
get "chart_data", to: "dashboard#chart_data", defaults: {format: :json}
|
|
6
6
|
|
|
7
7
|
resources :agents, only: [:index, :show, :update] do
|
|
8
8
|
member do
|
|
@@ -26,6 +26,6 @@ RubyLLM::Agents::Engine.routes.draw do
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
get "analytics", to: "analytics#index", as: :analytics
|
|
29
|
-
get "analytics/chart_data", to: "analytics#chart_data", as: :analytics_chart_data
|
|
29
|
+
get "analytics/chart_data", to: "analytics#chart_data", as: :analytics_chart_data, defaults: {format: :json}
|
|
30
30
|
resource :system_config, only: [:show], controller: "system_config"
|
|
31
31
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Migration to add the covering index behind the dashboard's aggregate queries
|
|
4
|
+
#
|
|
5
|
+
# Dashboard, analytics and agent stats all aggregate executions over a time
|
|
6
|
+
# range, reading a handful of narrow columns from each row. Fetching those
|
|
7
|
+
# rows from the table drags the wide ones along (metadata JSON, in practice
|
|
8
|
+
# most of each row), so the cost of every figure grows with the size of the
|
|
9
|
+
# range: seconds for 30 days on a table with millions of executions.
|
|
10
|
+
#
|
|
11
|
+
# This index stores the aggregated columns next to created_at, so those
|
|
12
|
+
# queries are answered from the index alone. PostgreSQL keeps them out of the
|
|
13
|
+
# key with INCLUDE; SQLite takes them as trailing key columns.
|
|
14
|
+
#
|
|
15
|
+
# The index is built CONCURRENTLY on PostgreSQL so executions keep being
|
|
16
|
+
# recorded while it builds. On a large table that takes a while; an
|
|
17
|
+
# interrupted build leaves an invalid index behind, which re-running this
|
|
18
|
+
# migration replaces.
|
|
19
|
+
class AddExecutionsAnalyticsIndex < ActiveRecord::Migration<%= migration_version %>
|
|
20
|
+
disable_ddl_transaction!
|
|
21
|
+
|
|
22
|
+
TABLE = :ruby_llm_agents_executions
|
|
23
|
+
INDEX = "idx_executions_analytics"
|
|
24
|
+
COVERED = %i[
|
|
25
|
+
agent_type model_id chosen_model_id status error_class tenant_id parent_execution_id
|
|
26
|
+
total_cost total_tokens input_tokens output_tokens duration_ms cache_hit streaming
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
def up
|
|
30
|
+
covered = COVERED.select { |column| column_exists?(TABLE, column) }
|
|
31
|
+
|
|
32
|
+
case connection.adapter_name.downcase
|
|
33
|
+
when /postg/
|
|
34
|
+
execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX}"
|
|
35
|
+
execute "CREATE INDEX CONCURRENTLY #{INDEX} ON #{TABLE} (created_at) INCLUDE (#{covered.join(", ")})"
|
|
36
|
+
when /sqlite/
|
|
37
|
+
add_index TABLE, [:created_at, *covered], name: INDEX, if_not_exists: true
|
|
38
|
+
else
|
|
39
|
+
say "Skipping #{INDEX}: covering index is only defined for PostgreSQL and SQLite"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def down
|
|
44
|
+
if connection.adapter_name.downcase.include?("postg")
|
|
45
|
+
execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX}"
|
|
46
|
+
else
|
|
47
|
+
remove_index TABLE, name: INDEX, if_exists: true
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Migration to add the index behind the executions dashboard list
|
|
4
|
+
#
|
|
5
|
+
# The list page runs `WHERE parent_execution_id IS NULL ORDER BY created_at DESC`
|
|
6
|
+
# plus a COUNT over the same predicate. With only the single-column
|
|
7
|
+
# parent_execution_id and created_at indexes available, the planner has to pick
|
|
8
|
+
# one and filter the rest, which degrades to a sequential scan once child
|
|
9
|
+
# executions make up a meaningful share of the table.
|
|
10
|
+
class AddRootExecutionListIndex < ActiveRecord::Migration<%= migration_version %>
|
|
11
|
+
def change
|
|
12
|
+
add_index :ruby_llm_agents_executions, [:parent_execution_id, :created_at],
|
|
13
|
+
name: "idx_executions_parent_created_at",
|
|
14
|
+
if_not_exists: true
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -132,6 +132,11 @@ RubyLLM::Agents.configure do |config|
|
|
|
132
132
|
# Number of recent executions shown on the dashboard home
|
|
133
133
|
# config.recent_executions_limit = 10
|
|
134
134
|
|
|
135
|
+
# Seconds a single dashboard query may run before the database cancels it
|
|
136
|
+
# (PostgreSQL only). Keeps a wide time range on a large executions table from
|
|
137
|
+
# tying up a web worker and the database; nil disables it.
|
|
138
|
+
# config.dashboard_query_timeout = 5
|
|
139
|
+
|
|
135
140
|
# ============================================
|
|
136
141
|
# Reliability Defaults
|
|
137
142
|
# ============================================
|
|
@@ -76,11 +76,37 @@ class CreateRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_versi
|
|
|
76
76
|
add_index :ruby_llm_agents_executions, :trace_id
|
|
77
77
|
add_index :ruby_llm_agents_executions, :request_id
|
|
78
78
|
add_index :ruby_llm_agents_executions, :parent_execution_id
|
|
79
|
+
add_index :ruby_llm_agents_executions, [:parent_execution_id, :created_at]
|
|
79
80
|
add_index :ruby_llm_agents_executions, :root_execution_id
|
|
80
81
|
add_index :ruby_llm_agents_executions, [:status, :created_at]
|
|
81
82
|
add_index :ruby_llm_agents_executions, [:model_id, :status]
|
|
82
83
|
add_index :ruby_llm_agents_executions, [:cache_hit, :created_at]
|
|
83
84
|
|
|
85
|
+
# Covering index behind the dashboard's aggregate queries. They read a
|
|
86
|
+
# time range and only these columns, so with this index they are answered
|
|
87
|
+
# without fetching the (much wider) table rows. PostgreSQL keeps the extra
|
|
88
|
+
# columns out of the key with INCLUDE; SQLite takes them as key columns.
|
|
89
|
+
analytics_columns = %w[
|
|
90
|
+
agent_type model_id chosen_model_id status error_class tenant_id parent_execution_id
|
|
91
|
+
total_cost total_tokens input_tokens output_tokens duration_ms cache_hit streaming
|
|
92
|
+
]
|
|
93
|
+
case connection.adapter_name.downcase
|
|
94
|
+
when /postg/
|
|
95
|
+
reversible do |dir|
|
|
96
|
+
dir.up do
|
|
97
|
+
execute "CREATE INDEX idx_executions_analytics ON ruby_llm_agents_executions " \
|
|
98
|
+
"(created_at) INCLUDE (#{analytics_columns.join(", ")})"
|
|
99
|
+
end
|
|
100
|
+
# Dropped explicitly: both this and the plain created_at index are
|
|
101
|
+
# keyed on created_at alone, so reverting that one by column would
|
|
102
|
+
# otherwise be ambiguous.
|
|
103
|
+
dir.down { execute "DROP INDEX IF EXISTS idx_executions_analytics" }
|
|
104
|
+
end
|
|
105
|
+
when /sqlite/
|
|
106
|
+
add_index :ruby_llm_agents_executions, ["created_at", *analytics_columns],
|
|
107
|
+
name: "idx_executions_analytics"
|
|
108
|
+
end
|
|
109
|
+
|
|
84
110
|
# Foreign keys for execution hierarchy
|
|
85
111
|
add_foreign_key :ruby_llm_agents_executions, :ruby_llm_agents_executions,
|
|
86
112
|
column: :parent_execution_id, on_delete: :nullify
|
|
@@ -117,6 +117,44 @@ module RubyLlmAgents
|
|
|
117
117
|
)
|
|
118
118
|
end
|
|
119
119
|
|
|
120
|
+
# Add the composite index behind the executions dashboard list
|
|
121
|
+
def create_add_root_execution_list_index_migration
|
|
122
|
+
unless table_exists?(:ruby_llm_agents_executions)
|
|
123
|
+
say_status :skip, "executions table does not exist yet", :yellow
|
|
124
|
+
return
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
if index_exists?(:ruby_llm_agents_executions, [:parent_execution_id, :created_at])
|
|
128
|
+
say_status :skip, "root execution list index already exists", :yellow
|
|
129
|
+
return
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
say_status :upgrade, "Adding root execution list index", :blue
|
|
133
|
+
migration_template(
|
|
134
|
+
"add_root_execution_list_index_migration.rb.tt",
|
|
135
|
+
File.join(db_migrate_path, "add_root_execution_list_index.rb")
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Add the covering index behind the dashboard's aggregate queries
|
|
140
|
+
def create_add_executions_analytics_index_migration
|
|
141
|
+
unless table_exists?(:ruby_llm_agents_executions)
|
|
142
|
+
say_status :skip, "executions table does not exist yet", :yellow
|
|
143
|
+
return
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
if index_name_exists?(:ruby_llm_agents_executions, "idx_executions_analytics")
|
|
147
|
+
say_status :skip, "executions analytics index already exists", :yellow
|
|
148
|
+
return
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
say_status :upgrade, "Adding executions analytics index", :blue
|
|
152
|
+
migration_template(
|
|
153
|
+
"add_executions_analytics_index_migration.rb.tt",
|
|
154
|
+
File.join(db_migrate_path, "add_executions_analytics_index.rb")
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
|
|
120
158
|
# Create overrides table for dashboard-managed agent settings
|
|
121
159
|
def create_overrides_migration
|
|
122
160
|
if table_exists?(:ruby_llm_agents_overrides)
|
|
@@ -231,5 +269,11 @@ module RubyLlmAgents
|
|
|
231
269
|
rescue
|
|
232
270
|
false
|
|
233
271
|
end
|
|
272
|
+
|
|
273
|
+
def index_name_exists?(table, name)
|
|
274
|
+
ActiveRecord::Base.connection.index_name_exists?(table, name)
|
|
275
|
+
rescue
|
|
276
|
+
false
|
|
277
|
+
end
|
|
234
278
|
end
|
|
235
279
|
end
|
|
@@ -109,6 +109,14 @@ module RubyLLM
|
|
|
109
109
|
# Number of recent executions shown on the dashboard home.
|
|
110
110
|
# @return [Integer] Limit for recent executions (default: 10)
|
|
111
111
|
|
|
112
|
+
# @!attribute [rw] dashboard_query_timeout
|
|
113
|
+
# Seconds a single dashboard query may run before the database cancels
|
|
114
|
+
# it. A cancelled query renders a "narrow the time range" notice
|
|
115
|
+
# instead of leaving the request (and the database) grinding on a scan
|
|
116
|
+
# of the whole executions table. Enforced on PostgreSQL only; a
|
|
117
|
+
# stricter statement_timeout already set on the connection wins.
|
|
118
|
+
# @return [Numeric, nil] Timeout in seconds, or nil to disable (default: 5)
|
|
119
|
+
|
|
112
120
|
# @!attribute [rw] job_retry_attempts
|
|
113
121
|
# Number of retry attempts for the async logging job on failure.
|
|
114
122
|
# @return [Integer] Retry attempts (default: 3)
|
|
@@ -494,6 +502,7 @@ module RubyLLM
|
|
|
494
502
|
:anomaly_duration_threshold,
|
|
495
503
|
:per_page,
|
|
496
504
|
:recent_executions_limit,
|
|
505
|
+
:dashboard_query_timeout,
|
|
497
506
|
:job_retry_attempts,
|
|
498
507
|
:messages_summary_max_length,
|
|
499
508
|
:dashboard_auth,
|
|
@@ -560,6 +569,15 @@ module RubyLLM
|
|
|
560
569
|
@recent_executions_limit = value
|
|
561
570
|
end
|
|
562
571
|
|
|
572
|
+
# Sets dashboard_query_timeout with validation
|
|
573
|
+
#
|
|
574
|
+
# @param value [Numeric, nil] Seconds (must be > 0), or nil to disable
|
|
575
|
+
# @raise [ArgumentError] If value is not positive
|
|
576
|
+
def dashboard_query_timeout=(value)
|
|
577
|
+
validate_positive!(:dashboard_query_timeout, value) unless value.nil?
|
|
578
|
+
@dashboard_query_timeout = value
|
|
579
|
+
end
|
|
580
|
+
|
|
563
581
|
# Sets job_retry_attempts with validation
|
|
564
582
|
#
|
|
565
583
|
# @param value [Integer] Retry attempts (must be >= 0)
|
|
@@ -702,6 +720,7 @@ module RubyLLM
|
|
|
702
720
|
@basic_auth_password = nil
|
|
703
721
|
@per_page = 25
|
|
704
722
|
@recent_executions_limit = 10
|
|
723
|
+
@dashboard_query_timeout = 5
|
|
705
724
|
@job_retry_attempts = 3
|
|
706
725
|
|
|
707
726
|
# Reliability defaults (all disabled by default for backward compatibility)
|
|
@@ -1031,6 +1050,7 @@ module RubyLLM
|
|
|
1031
1050
|
dashboard: {
|
|
1032
1051
|
per_page: per_page,
|
|
1033
1052
|
recent_executions_limit: recent_executions_limit,
|
|
1053
|
+
dashboard_query_timeout: dashboard_query_timeout,
|
|
1034
1054
|
dashboard_parent_controller: dashboard_parent_controller,
|
|
1035
1055
|
basic_auth_username: basic_auth_username,
|
|
1036
1056
|
dashboard_auth: dashboard_auth&.class&.name
|
|
@@ -172,6 +172,12 @@ module RubyLLM
|
|
|
172
172
|
# Falls back to creating a new record if the initial record is nil.
|
|
173
173
|
# Errors are re-raised to allow the ensure block to handle them.
|
|
174
174
|
#
|
|
175
|
+
# The record is locked and reloaded before updating: the instance
|
|
176
|
+
# held here was created when the pipeline started and may be stale —
|
|
177
|
+
# a concurrent transition (e.g. operator cancellation or a timeout
|
|
178
|
+
# sweep) can have written a terminal status in the meantime, and the
|
|
179
|
+
# completion must not overwrite it.
|
|
180
|
+
#
|
|
175
181
|
# @param execution [Execution, nil] The execution record to update
|
|
176
182
|
# @param context [Context] The execution context
|
|
177
183
|
# @param status [String] Final status ("success", "error", "timeout")
|
|
@@ -188,7 +194,7 @@ module RubyLLM
|
|
|
188
194
|
end
|
|
189
195
|
|
|
190
196
|
update_data = build_completion_data(context, status)
|
|
191
|
-
execution
|
|
197
|
+
return unless complete_running_execution(execution, update_data)
|
|
192
198
|
|
|
193
199
|
# Save detail data (prompts, responses, tool calls, etc.)
|
|
194
200
|
save_execution_details(execution, context, status)
|
|
@@ -197,6 +203,31 @@ module RubyLLM
|
|
|
197
203
|
raise # Re-raise for ensure block to handle via mark_execution_failed!
|
|
198
204
|
end
|
|
199
205
|
|
|
206
|
+
# Applies the completion update unless the execution already left
|
|
207
|
+
# "running" state.
|
|
208
|
+
#
|
|
209
|
+
# Locks the row and reloads it, so the status check and the update
|
|
210
|
+
# are atomic with respect to concurrent status transitions: whichever
|
|
211
|
+
# side wins the row lock first keeps the row. A completion that finds
|
|
212
|
+
# a terminal status is dropped instead of overwriting it.
|
|
213
|
+
#
|
|
214
|
+
# @param execution [Execution] The execution record to update
|
|
215
|
+
# @param update_data [Hash] The completion attributes
|
|
216
|
+
# @return [Boolean] Whether the completion update was applied
|
|
217
|
+
def complete_running_execution(execution, update_data)
|
|
218
|
+
return execution.update!(update_data) unless execution.persisted?
|
|
219
|
+
|
|
220
|
+
execution.with_lock do
|
|
221
|
+
if execution.status == "running"
|
|
222
|
+
execution.update!(update_data)
|
|
223
|
+
true
|
|
224
|
+
else
|
|
225
|
+
debug("Skipped completion for execution #{execution.id}: already #{execution.status}")
|
|
226
|
+
false
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
200
231
|
# Emergency fallback to mark execution as failed
|
|
201
232
|
#
|
|
202
233
|
# Uses update_all to bypass ActiveRecord callbacks and validations,
|
|
@@ -431,6 +462,13 @@ module RubyLLM
|
|
|
431
462
|
data[:attempts_count] = context[:reliability_attempts].size
|
|
432
463
|
end
|
|
433
464
|
|
|
465
|
+
# Carry the tracker request_id/tags that
|
|
466
|
+
# build_running_execution_data injected through the metadata
|
|
467
|
+
# rebuild — otherwise a completed execution loses them. Only
|
|
468
|
+
# merge into a rebuild that actually produced metadata: when
|
|
469
|
+
# none did, the running record's metadata stays untouched.
|
|
470
|
+
inject_tracker_data(context, data) if data[:metadata]
|
|
471
|
+
|
|
434
472
|
data
|
|
435
473
|
end
|
|
436
474
|
|
|
@@ -53,6 +53,7 @@ module RubyLLM
|
|
|
53
53
|
layout "ruby_llm/agents/application"
|
|
54
54
|
helper RubyLLM::Agents::ApplicationHelper
|
|
55
55
|
before_action :authenticate_dashboard!
|
|
56
|
+
around_action :limit_query_time, if: -> { request.get? }
|
|
56
57
|
|
|
57
58
|
rescue_from ::ActiveRecord::StatementInvalid do |e|
|
|
58
59
|
if e.message.include?("ruby_llm_agents_")
|
|
@@ -65,8 +66,54 @@ module RubyLLM
|
|
|
65
66
|
end
|
|
66
67
|
end
|
|
67
68
|
|
|
69
|
+
# Declared after the StatementInvalid handler so it wins: a cancelled
|
|
70
|
+
# query is a StatementInvalid too, and must not be reported as a
|
|
71
|
+
# pending migration.
|
|
72
|
+
rescue_from ::ActiveRecord::QueryCanceled do
|
|
73
|
+
@query_timeout = RubyLLM::Agents.configuration.dashboard_query_timeout
|
|
74
|
+
|
|
75
|
+
if request.format.json?
|
|
76
|
+
render json: {error: "query_timeout"}, status: :service_unavailable
|
|
77
|
+
else
|
|
78
|
+
render "ruby_llm/agents/shared/query_timeout", formats: [:html], status: :service_unavailable
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
68
82
|
private
|
|
69
83
|
|
|
84
|
+
# Runs the page under config.dashboard_query_timeout
|
|
85
|
+
#
|
|
86
|
+
# Every dashboard page aggregates over a user-chosen window of the
|
|
87
|
+
# executions table. Without a ceiling, a wide window on a large
|
|
88
|
+
# table keeps running after the web server has timed the request
|
|
89
|
+
# out — the user sees a 500 and the database keeps scanning. With
|
|
90
|
+
# it, the database cancels the query and the QueryCanceled handler
|
|
91
|
+
# above explains what to narrow.
|
|
92
|
+
#
|
|
93
|
+
# @return [void]
|
|
94
|
+
# @api private
|
|
95
|
+
def limit_query_time(&block)
|
|
96
|
+
RubyLLM::Agents::Execution.with_statement_timeout(
|
|
97
|
+
RubyLLM::Agents.configuration.dashboard_query_timeout, &block
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Caches an expensive dashboard aggregate for a short while
|
|
102
|
+
#
|
|
103
|
+
# Dashboard figures are read far more often than they meaningfully
|
|
104
|
+
# change (reloads, pagination, several people watching the same
|
|
105
|
+
# page), and each one is a scan of a time window. The current
|
|
106
|
+
# tenant is always part of the key.
|
|
107
|
+
#
|
|
108
|
+
# @param key [Array] Identifies the figure and its filters
|
|
109
|
+
# @param expires_in [ActiveSupport::Duration] How stale it may get
|
|
110
|
+
# @return [Object] The block's value (nil is cached too)
|
|
111
|
+
# @api private
|
|
112
|
+
def cached_stats(*key, expires_in: 1.minute, &block)
|
|
113
|
+
Rails.cache.fetch(["ruby_llm_agents", "stats", current_tenant_id, *key],
|
|
114
|
+
expires_in: expires_in, race_condition_ttl: 10.seconds, &block)
|
|
115
|
+
end
|
|
116
|
+
|
|
70
117
|
# Authenticates dashboard access using configured method
|
|
71
118
|
#
|
|
72
119
|
# Authentication priority:
|
|
@@ -160,18 +207,12 @@ module RubyLLM
|
|
|
160
207
|
# configured name; legacy or string-only tenants fall back to the raw
|
|
161
208
|
# tenant_id so nothing disappears from the filter.
|
|
162
209
|
#
|
|
163
|
-
# Two queries total — one DISTINCT pluck on executions, one pluck on
|
|
164
|
-
# tenants — regardless of how many tenant_ids exist.
|
|
165
|
-
#
|
|
166
210
|
# @return [Array<Hash>] Entries shaped as { value:, label: }
|
|
167
211
|
# @api public
|
|
168
212
|
def available_tenants
|
|
169
213
|
return @available_tenants if defined?(@available_tenants)
|
|
170
214
|
|
|
171
|
-
tenant_ids =
|
|
172
|
-
.where.not(tenant_id: nil)
|
|
173
|
-
.distinct
|
|
174
|
-
.pluck(:tenant_id)
|
|
215
|
+
tenant_ids = cached_filter_options(:tenant_id)
|
|
175
216
|
|
|
176
217
|
names_by_id = RubyLLM::Agents::Tenant
|
|
177
218
|
.where(tenant_id: tenant_ids)
|
|
@@ -183,6 +224,45 @@ module RubyLLM
|
|
|
183
224
|
.sort_by { |t| t[:label].downcase }
|
|
184
225
|
end
|
|
185
226
|
helper_method :available_tenants
|
|
227
|
+
|
|
228
|
+
# Distinct agent types in the execution history, sorted
|
|
229
|
+
#
|
|
230
|
+
# @return [Array<String>]
|
|
231
|
+
# @api public
|
|
232
|
+
def available_agent_types
|
|
233
|
+
cached_filter_options(:agent_type)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Distinct model IDs in the execution history, sorted
|
|
237
|
+
#
|
|
238
|
+
# @return [Array<String>]
|
|
239
|
+
# @api public
|
|
240
|
+
def available_model_ids
|
|
241
|
+
cached_filter_options(:model_id)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Distinct values of an executions column, for a filter dropdown
|
|
245
|
+
#
|
|
246
|
+
# Read with Execution.distinct_values, which walks the column's
|
|
247
|
+
# index rather than scanning the table, and cached on top of that:
|
|
248
|
+
# the lists only change when a brand-new agent, model or tenant
|
|
249
|
+
# appears, and several pages need two or three of them per request.
|
|
250
|
+
# A new value shows up in the dropdown within five minutes;
|
|
251
|
+
# filtering by it via URL params works immediately.
|
|
252
|
+
#
|
|
253
|
+
# The lists are deliberately not narrowed to the current tenant.
|
|
254
|
+
# Doing so means reading every one of that tenant's executions, and
|
|
255
|
+
# agent and model names are application configuration rather than
|
|
256
|
+
# tenant data.
|
|
257
|
+
#
|
|
258
|
+
# @param column [Symbol] :agent_type, :model_id or :tenant_id
|
|
259
|
+
# @return [Array<String>]
|
|
260
|
+
# @api private
|
|
261
|
+
def cached_filter_options(column)
|
|
262
|
+
Rails.cache.fetch(["ruby_llm_agents", "filter_options", column], expires_in: 5.minutes) do
|
|
263
|
+
RubyLLM::Agents::Execution.distinct_values(column)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
186
266
|
end)
|
|
187
267
|
end
|
|
188
268
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_llm-agents
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.16.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- adham90
|
|
@@ -90,6 +90,7 @@ files:
|
|
|
90
90
|
- app/models/ruby_llm/agents/agent_override.rb
|
|
91
91
|
- app/models/ruby_llm/agents/execution.rb
|
|
92
92
|
- app/models/ruby_llm/agents/execution/analytics.rb
|
|
93
|
+
- app/models/ruby_llm/agents/execution/breakdown.rb
|
|
93
94
|
- app/models/ruby_llm/agents/execution/metrics.rb
|
|
94
95
|
- app/models/ruby_llm/agents/execution/replayable.rb
|
|
95
96
|
- app/models/ruby_llm/agents/execution/scopes.rb
|
|
@@ -131,11 +132,13 @@ files:
|
|
|
131
132
|
- app/views/ruby_llm/agents/shared/_doc_link.html.erb
|
|
132
133
|
- app/views/ruby_llm/agents/shared/_executions_table.html.erb
|
|
133
134
|
- app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb
|
|
135
|
+
- app/views/ruby_llm/agents/shared/_pagination.html.erb
|
|
134
136
|
- app/views/ruby_llm/agents/shared/_select_dropdown.html.erb
|
|
135
137
|
- app/views/ruby_llm/agents/shared/_sortable_header.html.erb
|
|
136
138
|
- app/views/ruby_llm/agents/shared/_status_badge.html.erb
|
|
137
139
|
- app/views/ruby_llm/agents/shared/_status_dot.html.erb
|
|
138
140
|
- app/views/ruby_llm/agents/shared/_tenant_filter.html.erb
|
|
141
|
+
- app/views/ruby_llm/agents/shared/query_timeout.html.erb
|
|
139
142
|
- app/views/ruby_llm/agents/system_config/show.html.erb
|
|
140
143
|
- app/views/ruby_llm/agents/tenants/_form.html.erb
|
|
141
144
|
- app/views/ruby_llm/agents/tenants/edit.html.erb
|
|
@@ -165,8 +168,10 @@ files:
|
|
|
165
168
|
- lib/generators/ruby_llm_agents/templates/add_caching_migration.rb.tt
|
|
166
169
|
- lib/generators/ruby_llm_agents/templates/add_dashboard_performance_indexes_migration.rb.tt
|
|
167
170
|
- lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt
|
|
171
|
+
- lib/generators/ruby_llm_agents/templates/add_executions_analytics_index_migration.rb.tt
|
|
168
172
|
- lib/generators/ruby_llm_agents/templates/add_finish_reason_migration.rb.tt
|
|
169
173
|
- lib/generators/ruby_llm_agents/templates/add_prompts_migration.rb.tt
|
|
174
|
+
- lib/generators/ruby_llm_agents/templates/add_root_execution_list_index_migration.rb.tt
|
|
170
175
|
- lib/generators/ruby_llm_agents/templates/add_routing_migration.rb.tt
|
|
171
176
|
- lib/generators/ruby_llm_agents/templates/add_streaming_migration.rb.tt
|
|
172
177
|
- lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt
|
|
@@ -359,7 +364,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
359
364
|
- !ruby/object:Gem::Version
|
|
360
365
|
version: '0'
|
|
361
366
|
requirements: []
|
|
362
|
-
rubygems_version:
|
|
367
|
+
rubygems_version: 3.6.9
|
|
363
368
|
specification_version: 4
|
|
364
369
|
summary: Agent framework for building LLM-powered agents with RubyLLM
|
|
365
370
|
test_files: []
|