ruby_llm-agents 3.15.0 → 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/audio/transcriber.rb +7 -121
- 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
|
|
@@ -117,37 +117,10 @@ module RubyLLM
|
|
|
117
117
|
|
|
118
118
|
# @!endgroup
|
|
119
119
|
|
|
120
|
-
#
|
|
121
|
-
|
|
122
|
-
#
|
|
123
|
-
#
|
|
124
|
-
# @yield Block for configuring reliability options
|
|
125
|
-
# @return [ReliabilityConfig] The reliability configuration
|
|
126
|
-
def reliability(&block)
|
|
127
|
-
@reliability_config ||= ReliabilityConfig.new
|
|
128
|
-
@reliability_config.instance_eval(&block) if block_given?
|
|
129
|
-
@reliability_config
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# Returns reliability configuration
|
|
133
|
-
#
|
|
134
|
-
# @return [ReliabilityConfig, nil] The reliability configuration
|
|
135
|
-
def reliability_config
|
|
136
|
-
@reliability_config || inherited_or_default(:reliability_config, nil)
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# Sets fallback models directly (shorthand for reliability block)
|
|
140
|
-
#
|
|
141
|
-
# @param models [Array<String>] Model identifiers to try on failure
|
|
142
|
-
# @return [Array<String>] The fallback models
|
|
143
|
-
def fallback_models(*models)
|
|
144
|
-
if models.any?
|
|
145
|
-
@fallback_models = models.flatten
|
|
146
|
-
end
|
|
147
|
-
@fallback_models || inherited_or_default(:fallback_models, [])
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
# @!endgroup
|
|
120
|
+
# Reliability (retries, fallbacks, circuit breakers) comes from the
|
|
121
|
+
# shared DSL::Reliability on BaseAgent and is executed by the
|
|
122
|
+
# Reliability middleware — the middleware expects the Hash config that
|
|
123
|
+
# DSL produces, so Transcriber must not shadow it with its own object.
|
|
151
124
|
|
|
152
125
|
# Factory method to instantiate and execute transcription
|
|
153
126
|
#
|
|
@@ -197,40 +170,6 @@ module RubyLLM
|
|
|
197
170
|
end
|
|
198
171
|
end
|
|
199
172
|
|
|
200
|
-
# Configuration class for reliability options
|
|
201
|
-
class ReliabilityConfig
|
|
202
|
-
attr_accessor :max_retries, :backoff, :fallback_models_list, :total_timeout_seconds
|
|
203
|
-
|
|
204
|
-
def initialize
|
|
205
|
-
@max_retries = 3
|
|
206
|
-
@backoff = :exponential
|
|
207
|
-
@fallback_models_list = []
|
|
208
|
-
@total_timeout_seconds = nil
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def retries(max: 3, backoff: :exponential)
|
|
212
|
-
@max_retries = max
|
|
213
|
-
@backoff = backoff
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def fallback_models(*models)
|
|
217
|
-
@fallback_models_list = models.flatten
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
def total_timeout(seconds)
|
|
221
|
-
@total_timeout_seconds = seconds
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
def to_h
|
|
225
|
-
{
|
|
226
|
-
max_retries: max_retries,
|
|
227
|
-
backoff: backoff,
|
|
228
|
-
fallback_models: fallback_models_list,
|
|
229
|
-
total_timeout: total_timeout_seconds
|
|
230
|
-
}
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
|
-
|
|
234
173
|
# @!attribute [r] audio
|
|
235
174
|
# @return [String, File, IO] Audio input
|
|
236
175
|
# @!attribute [r] audio_format
|
|
@@ -308,8 +247,9 @@ module RubyLLM
|
|
|
308
247
|
audio_input = normalize_audio_input(@audio, @audio_format)
|
|
309
248
|
validate_audio_input!(audio_input)
|
|
310
249
|
|
|
311
|
-
#
|
|
312
|
-
|
|
250
|
+
# context.model is set per-attempt by the Reliability middleware, so
|
|
251
|
+
# fallback models actually take effect here
|
|
252
|
+
raw_result = execute_transcription(audio_input, context.model || resolved_model)
|
|
313
253
|
|
|
314
254
|
execution_completed_at = Time.current
|
|
315
255
|
duration_ms = ((execution_completed_at - execution_started_at) * 1000).to_i
|
|
@@ -455,37 +395,6 @@ module RubyLLM
|
|
|
455
395
|
end
|
|
456
396
|
end
|
|
457
397
|
|
|
458
|
-
# Executes transcription with reliability features
|
|
459
|
-
#
|
|
460
|
-
# @param audio_input [Hash] Normalized audio input
|
|
461
|
-
# @return [Hash] Raw transcription result
|
|
462
|
-
def execute_with_reliability(audio_input)
|
|
463
|
-
models_to_try = [resolved_model] + self.class.fallback_models
|
|
464
|
-
last_error = nil
|
|
465
|
-
|
|
466
|
-
models_to_try.each do |model|
|
|
467
|
-
retries = 0
|
|
468
|
-
max_retries = reliability_max_retries
|
|
469
|
-
|
|
470
|
-
begin
|
|
471
|
-
return execute_transcription(audio_input, model)
|
|
472
|
-
rescue => e
|
|
473
|
-
last_error = e
|
|
474
|
-
retries += 1
|
|
475
|
-
|
|
476
|
-
if retryable_error?(e) && retries < max_retries
|
|
477
|
-
sleep(calculate_backoff(retries))
|
|
478
|
-
retry
|
|
479
|
-
end
|
|
480
|
-
|
|
481
|
-
# Try next model
|
|
482
|
-
next
|
|
483
|
-
end
|
|
484
|
-
end
|
|
485
|
-
|
|
486
|
-
raise last_error || StandardError.new("All transcription models exhausted")
|
|
487
|
-
end
|
|
488
|
-
|
|
489
398
|
# Executes the actual transcription API call
|
|
490
399
|
#
|
|
491
400
|
# @param audio_input [Hash] Normalized audio input
|
|
@@ -663,29 +572,6 @@ module RubyLLM
|
|
|
663
572
|
def resolved_language
|
|
664
573
|
@runtime_language || self.class.language
|
|
665
574
|
end
|
|
666
|
-
|
|
667
|
-
# Returns max retries from reliability config
|
|
668
|
-
def reliability_max_retries
|
|
669
|
-
config = self.class.reliability_config
|
|
670
|
-
config&.max_retries || 3
|
|
671
|
-
end
|
|
672
|
-
|
|
673
|
-
# Checks if error is retryable
|
|
674
|
-
def retryable_error?(error)
|
|
675
|
-
message = error.message.to_s.downcase
|
|
676
|
-
retryable_patterns = ["rate limit", "timeout", "503", "502", "429", "overloaded"]
|
|
677
|
-
retryable_patterns.any? { |pattern| message.include?(pattern) }
|
|
678
|
-
end
|
|
679
|
-
|
|
680
|
-
# Calculates exponential backoff delay
|
|
681
|
-
def calculate_backoff(attempt)
|
|
682
|
-
config = self.class.reliability_config
|
|
683
|
-
base = (config&.backoff == :constant) ? 1.0 : 0.4
|
|
684
|
-
max_delay = 10.0
|
|
685
|
-
|
|
686
|
-
delay = base * (2**(attempt - 1))
|
|
687
|
-
[delay, max_delay].min
|
|
688
|
-
end
|
|
689
575
|
end
|
|
690
576
|
end
|
|
691
577
|
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
|
|