ruby_llm-agents 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +273 -0
- data/app/channels/ruby_llm/agents/executions_channel.rb +24 -1
- data/app/controllers/concerns/ruby_llm/agents/filterable.rb +81 -0
- data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +51 -0
- data/app/controllers/ruby_llm/agents/agents_controller.rb +228 -59
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +167 -12
- data/app/controllers/ruby_llm/agents/executions_controller.rb +189 -31
- data/app/controllers/ruby_llm/agents/settings_controller.rb +20 -0
- data/app/helpers/ruby_llm/agents/application_helper.rb +307 -7
- data/app/models/ruby_llm/agents/execution/analytics.rb +224 -20
- data/app/models/ruby_llm/agents/execution/metrics.rb +41 -25
- data/app/models/ruby_llm/agents/execution/scopes.rb +234 -14
- data/app/models/ruby_llm/agents/execution.rb +259 -16
- data/app/services/ruby_llm/agents/agent_registry.rb +49 -12
- data/app/views/layouts/rubyllm/agents/application.html.erb +351 -85
- data/app/views/rubyllm/agents/agents/_version_comparison.html.erb +186 -0
- data/app/views/rubyllm/agents/agents/show.html.erb +233 -10
- data/app/views/rubyllm/agents/dashboard/_action_center.html.erb +62 -0
- data/app/views/rubyllm/agents/dashboard/_alerts_feed.html.erb +62 -0
- data/app/views/rubyllm/agents/dashboard/_breaker_strip.html.erb +47 -0
- data/app/views/rubyllm/agents/dashboard/_budgets_bar.html.erb +165 -0
- data/app/views/rubyllm/agents/dashboard/_now_strip.html.erb +10 -0
- data/app/views/rubyllm/agents/dashboard/_now_strip_values.html.erb +71 -0
- data/app/views/rubyllm/agents/dashboard/index.html.erb +215 -109
- data/app/views/rubyllm/agents/executions/_filters.html.erb +152 -155
- data/app/views/rubyllm/agents/executions/_list.html.erb +103 -12
- data/app/views/rubyllm/agents/executions/dry_run.html.erb +149 -0
- data/app/views/rubyllm/agents/executions/index.html.erb +17 -72
- data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +16 -2
- data/app/views/rubyllm/agents/executions/show.html.erb +693 -14
- data/app/views/rubyllm/agents/settings/show.html.erb +369 -0
- data/app/views/rubyllm/agents/shared/_filter_dropdown.html.erb +121 -0
- data/app/views/rubyllm/agents/shared/_select_dropdown.html.erb +85 -0
- data/config/routes.rb +7 -0
- data/lib/generators/ruby_llm_agents/templates/add_attempts_migration.rb.tt +27 -0
- data/lib/generators/ruby_llm_agents/templates/add_caching_migration.rb.tt +23 -0
- data/lib/generators/ruby_llm_agents/templates/add_finish_reason_migration.rb.tt +19 -0
- data/lib/generators/ruby_llm_agents/templates/add_routing_migration.rb.tt +19 -0
- data/lib/generators/ruby_llm_agents/templates/add_streaming_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm_agents/templates/add_tracing_migration.rb.tt +34 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +66 -4
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +53 -6
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +139 -8
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +38 -1
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +78 -0
- data/lib/ruby_llm/agents/alert_manager.rb +207 -0
- data/lib/ruby_llm/agents/attempt_tracker.rb +295 -0
- data/lib/ruby_llm/agents/base.rb +580 -112
- data/lib/ruby_llm/agents/budget_tracker.rb +360 -0
- data/lib/ruby_llm/agents/circuit_breaker.rb +197 -0
- data/lib/ruby_llm/agents/configuration.rb +279 -1
- data/lib/ruby_llm/agents/engine.rb +59 -6
- data/lib/ruby_llm/agents/execution_logger_job.rb +17 -6
- data/lib/ruby_llm/agents/inflections.rb +13 -2
- data/lib/ruby_llm/agents/instrumentation.rb +538 -87
- data/lib/ruby_llm/agents/redactor.rb +130 -0
- data/lib/ruby_llm/agents/reliability.rb +185 -0
- data/lib/ruby_llm/agents/version.rb +3 -1
- data/lib/ruby_llm/agents.rb +52 -0
- metadata +41 -2
- data/app/controllers/ruby_llm/agents/application_controller.rb +0 -37
|
@@ -2,98 +2,267 @@
|
|
|
2
2
|
|
|
3
3
|
module RubyLLM
|
|
4
4
|
module Agents
|
|
5
|
+
# Controller for viewing agent details and per-agent analytics
|
|
6
|
+
#
|
|
7
|
+
# Provides an overview of all registered agents and detailed views
|
|
8
|
+
# for individual agents including configuration, execution history,
|
|
9
|
+
# and performance metrics.
|
|
10
|
+
#
|
|
11
|
+
# @see AgentRegistry For agent discovery
|
|
12
|
+
# @see Paginatable For pagination implementation
|
|
13
|
+
# @see Filterable For filter parsing and validation
|
|
14
|
+
# @api private
|
|
5
15
|
class AgentsController < ApplicationController
|
|
16
|
+
include Paginatable
|
|
17
|
+
include Filterable
|
|
18
|
+
|
|
19
|
+
# Lists all registered agents with their details
|
|
20
|
+
#
|
|
21
|
+
# Uses AgentRegistry to discover agents from both file system
|
|
22
|
+
# and execution history, ensuring deleted agents with history
|
|
23
|
+
# are still visible.
|
|
24
|
+
#
|
|
25
|
+
# @return [void]
|
|
6
26
|
def index
|
|
7
27
|
@agents = AgentRegistry.all_with_details
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
Rails.logger.error("[RubyLLM::Agents] Error loading agents: #{e.message}")
|
|
30
|
+
@agents = []
|
|
31
|
+
flash.now[:alert] = "Error loading agents list"
|
|
8
32
|
end
|
|
9
33
|
|
|
34
|
+
# Shows detailed view for a specific agent
|
|
35
|
+
#
|
|
36
|
+
# Loads agent configuration (if class exists), statistics,
|
|
37
|
+
# filtered executions, and chart data for visualization.
|
|
38
|
+
# Works for both active agents and deleted agents with history.
|
|
39
|
+
#
|
|
40
|
+
# @return [void]
|
|
10
41
|
def show
|
|
11
42
|
@agent_type = params[:id]
|
|
12
43
|
@agent_class = AgentRegistry.find(@agent_type)
|
|
13
44
|
@agent_active = @agent_class.present?
|
|
14
45
|
|
|
15
|
-
|
|
46
|
+
load_agent_stats
|
|
47
|
+
load_filter_options
|
|
48
|
+
load_filtered_executions
|
|
49
|
+
load_chart_data
|
|
50
|
+
|
|
51
|
+
if @agent_class
|
|
52
|
+
load_agent_config
|
|
53
|
+
load_circuit_breaker_status
|
|
54
|
+
end
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
Rails.logger.error("[RubyLLM::Agents] Error loading agent #{@agent_type}: #{e.message}")
|
|
57
|
+
redirect_to ruby_llm_agents.agents_path, alert: "Error loading agent details"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# Loads all-time and today's statistics for the agent
|
|
63
|
+
#
|
|
64
|
+
# @return [void]
|
|
65
|
+
def load_agent_stats
|
|
16
66
|
@stats = Execution.stats_for(@agent_type, period: :all_time)
|
|
17
67
|
@stats_today = Execution.stats_for(@agent_type, period: :today)
|
|
18
68
|
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
@
|
|
22
|
-
@
|
|
69
|
+
# Additional stats for new schema fields
|
|
70
|
+
agent_scope = Execution.by_agent(@agent_type)
|
|
71
|
+
@cache_hit_rate = agent_scope.cache_hit_rate
|
|
72
|
+
@streaming_rate = agent_scope.streaming_rate
|
|
73
|
+
@avg_ttft = agent_scope.avg_time_to_first_token
|
|
74
|
+
end
|
|
23
75
|
|
|
24
|
-
|
|
25
|
-
|
|
76
|
+
# Loads available filter options from execution history
|
|
77
|
+
#
|
|
78
|
+
# Uses a single optimized query to fetch all filter values
|
|
79
|
+
# (versions, models, temperatures) avoiding N+1 queries.
|
|
80
|
+
#
|
|
81
|
+
# @return [void]
|
|
82
|
+
def load_filter_options
|
|
83
|
+
# Single query to get all filter options (fixes N+1)
|
|
84
|
+
filter_data = Execution.by_agent(@agent_type)
|
|
85
|
+
.where.not(agent_version: nil)
|
|
86
|
+
.or(Execution.by_agent(@agent_type).where.not(model_id: nil))
|
|
87
|
+
.or(Execution.by_agent(@agent_type).where.not(temperature: nil))
|
|
88
|
+
.pluck(:agent_version, :model_id, :temperature)
|
|
26
89
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
90
|
+
@versions = filter_data.map(&:first).compact.uniq.sort.reverse
|
|
91
|
+
@models = filter_data.map { |d| d[1] }.compact.uniq.sort
|
|
92
|
+
@temperatures = filter_data.map(&:last).compact.uniq.sort
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Loads paginated and filtered executions with statistics
|
|
96
|
+
#
|
|
97
|
+
# Sets @executions, @pagination, and @filter_stats for the view.
|
|
98
|
+
#
|
|
99
|
+
# @return [void]
|
|
100
|
+
def load_filtered_executions
|
|
101
|
+
base_scope = build_filtered_scope
|
|
102
|
+
result = paginate(base_scope)
|
|
103
|
+
@executions = result[:records]
|
|
104
|
+
@pagination = result[:pagination]
|
|
105
|
+
|
|
106
|
+
@filter_stats = {
|
|
107
|
+
total_count: result[:pagination][:total_count],
|
|
108
|
+
total_cost: base_scope.sum(:total_cost),
|
|
109
|
+
total_tokens: base_scope.sum(:total_tokens)
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Builds a filtered scope for the current agent's executions
|
|
114
|
+
#
|
|
115
|
+
# Applies filters in order: status, version, model, temperature, time.
|
|
116
|
+
# Each filter is optional and only applied if values are provided.
|
|
117
|
+
#
|
|
118
|
+
# @return [ActiveRecord::Relation] Filtered execution scope
|
|
119
|
+
def build_filtered_scope
|
|
120
|
+
scope = Execution.by_agent(@agent_type)
|
|
121
|
+
|
|
122
|
+
# Apply status filter with validation
|
|
123
|
+
statuses = parse_array_param(:statuses)
|
|
124
|
+
scope = apply_status_filter(scope, statuses) if statuses.any?
|
|
32
125
|
|
|
33
126
|
# Apply version filter
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
base_scope = base_scope.where(agent_version: versions) if versions.any?(&:present?)
|
|
37
|
-
end
|
|
127
|
+
versions = parse_array_param(:versions)
|
|
128
|
+
scope = scope.where(agent_version: versions) if versions.any?
|
|
38
129
|
|
|
39
130
|
# Apply model filter
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
base_scope = base_scope.where(model_id: models) if models.any?(&:present?)
|
|
43
|
-
end
|
|
131
|
+
models = parse_array_param(:models)
|
|
132
|
+
scope = scope.where(model_id: models) if models.any?
|
|
44
133
|
|
|
45
134
|
# Apply temperature filter
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
base_scope = base_scope.where(temperature: temps) if temps.any?(&:present?)
|
|
49
|
-
end
|
|
135
|
+
temperatures = parse_array_param(:temperatures)
|
|
136
|
+
scope = scope.where(temperature: temperatures) if temperatures.any?
|
|
50
137
|
|
|
51
|
-
# Apply time range filter
|
|
52
|
-
|
|
138
|
+
# Apply time range filter with validation
|
|
139
|
+
days = parse_days_param
|
|
140
|
+
scope = apply_time_filter(scope, days)
|
|
53
141
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
142
|
+
scope
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Loads chart data for agent performance visualization
|
|
146
|
+
#
|
|
147
|
+
# Fetches 30-day trend analysis and status/finish_reason distribution for charts.
|
|
148
|
+
#
|
|
149
|
+
# @return [void]
|
|
150
|
+
def load_chart_data
|
|
151
|
+
@trend_data = Execution.trend_analysis(agent_type: @agent_type, days: 30)
|
|
152
|
+
@status_distribution = Execution.by_agent(@agent_type).group(:status).count
|
|
153
|
+
@finish_reason_distribution = Execution.by_agent(@agent_type).finish_reason_distribution
|
|
154
|
+
load_version_comparison
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Loads version comparison data if multiple versions exist
|
|
158
|
+
#
|
|
159
|
+
# Includes trend data for sparkline charts.
|
|
160
|
+
#
|
|
161
|
+
# @return [void]
|
|
162
|
+
def load_version_comparison
|
|
163
|
+
return unless @versions.size >= 2
|
|
164
|
+
|
|
165
|
+
# Default to comparing two most recent versions
|
|
166
|
+
v1 = params[:compare_v1] || @versions[0]
|
|
167
|
+
v2 = params[:compare_v2] || @versions[1]
|
|
58
168
|
|
|
59
|
-
|
|
60
|
-
total_count = filtered_scope.count
|
|
61
|
-
@executions = filtered_scope.limit(per_page).offset(offset)
|
|
169
|
+
comparison_data = Execution.compare_versions(@agent_type, v1, v2, period: :this_month)
|
|
62
170
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
171
|
+
# Fetch trend data for sparklines
|
|
172
|
+
v1_trend = Execution.version_trend_data(@agent_type, v1, days: 14)
|
|
173
|
+
v2_trend = Execution.version_trend_data(@agent_type, v2, days: 14)
|
|
174
|
+
|
|
175
|
+
@version_comparison = {
|
|
176
|
+
v1: v1,
|
|
177
|
+
v2: v2,
|
|
178
|
+
data: comparison_data,
|
|
179
|
+
v1_trend: v1_trend,
|
|
180
|
+
v2_trend: v2_trend
|
|
68
181
|
}
|
|
182
|
+
rescue StandardError => e
|
|
183
|
+
Rails.logger.debug("[RubyLLM::Agents] Version comparison error: #{e.message}")
|
|
184
|
+
@version_comparison = nil
|
|
185
|
+
end
|
|
69
186
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
187
|
+
# Loads the current agent class configuration
|
|
188
|
+
#
|
|
189
|
+
# Extracts DSL-configured values from the agent class for display.
|
|
190
|
+
# Only called if the agent class still exists.
|
|
191
|
+
#
|
|
192
|
+
# @return [void]
|
|
193
|
+
def load_agent_config
|
|
194
|
+
@config = {
|
|
195
|
+
# Basic configuration
|
|
196
|
+
model: @agent_class.model,
|
|
197
|
+
temperature: @agent_class.temperature,
|
|
198
|
+
version: @agent_class.version,
|
|
199
|
+
timeout: @agent_class.timeout,
|
|
200
|
+
cache_enabled: @agent_class.cache_enabled?,
|
|
201
|
+
cache_ttl: @agent_class.cache_ttl,
|
|
202
|
+
params: @agent_class.params,
|
|
203
|
+
|
|
204
|
+
# Reliability configuration
|
|
205
|
+
retries: @agent_class.retries,
|
|
206
|
+
fallback_models: @agent_class.fallback_models,
|
|
207
|
+
total_timeout: @agent_class.total_timeout,
|
|
208
|
+
circuit_breaker: @agent_class.circuit_breaker_config
|
|
75
209
|
}
|
|
210
|
+
end
|
|
76
211
|
|
|
77
|
-
|
|
78
|
-
|
|
212
|
+
# Loads circuit breaker status for the agent's models
|
|
213
|
+
#
|
|
214
|
+
# Checks the primary model and any fallback models configured.
|
|
215
|
+
# Only returns data if reliability features are enabled.
|
|
216
|
+
#
|
|
217
|
+
# @return [void]
|
|
218
|
+
def load_circuit_breaker_status
|
|
219
|
+
return unless @agent_class.respond_to?(:reliability_config)
|
|
79
220
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
.group(:status)
|
|
83
|
-
.count
|
|
221
|
+
config = @agent_class.reliability_config rescue nil
|
|
222
|
+
return unless config
|
|
84
223
|
|
|
85
|
-
#
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
224
|
+
# Collect all models: primary + fallbacks
|
|
225
|
+
models_to_check = [@agent_class.model]
|
|
226
|
+
models_to_check.concat(config[:fallback_models]) if config[:fallback_models].present?
|
|
227
|
+
models_to_check = models_to_check.compact.uniq
|
|
228
|
+
|
|
229
|
+
return if models_to_check.empty?
|
|
230
|
+
|
|
231
|
+
breaker_config = config[:circuit_breaker] || {}
|
|
232
|
+
errors_threshold = breaker_config[:errors] || 10
|
|
233
|
+
window = breaker_config[:within] || 60
|
|
234
|
+
cooldown = breaker_config[:cooldown] || 300
|
|
235
|
+
|
|
236
|
+
@circuit_breaker_status = {}
|
|
237
|
+
|
|
238
|
+
models_to_check.each do |model_id|
|
|
239
|
+
breaker = CircuitBreaker.new(
|
|
240
|
+
@agent_type,
|
|
241
|
+
model_id,
|
|
242
|
+
errors: errors_threshold,
|
|
243
|
+
within: window,
|
|
244
|
+
cooldown: cooldown
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
status = {
|
|
248
|
+
open: breaker.open?,
|
|
249
|
+
threshold: errors_threshold
|
|
95
250
|
}
|
|
251
|
+
|
|
252
|
+
# Get additional details
|
|
253
|
+
if breaker.open?
|
|
254
|
+
# Calculate remaining cooldown (approximate)
|
|
255
|
+
status[:cooldown_remaining] = cooldown
|
|
256
|
+
else
|
|
257
|
+
# Get current failure count if available
|
|
258
|
+
status[:failure_count] = breaker.failure_count
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
@circuit_breaker_status[model_id] = status
|
|
96
262
|
end
|
|
263
|
+
rescue StandardError => e
|
|
264
|
+
Rails.logger.debug("[RubyLLM::Agents] Could not load circuit breaker status: #{e.message}")
|
|
265
|
+
@circuit_breaker_status = {}
|
|
97
266
|
end
|
|
98
267
|
end
|
|
99
268
|
end
|
|
@@ -2,33 +2,188 @@
|
|
|
2
2
|
|
|
3
3
|
module RubyLLM
|
|
4
4
|
module Agents
|
|
5
|
+
# Dashboard controller for the RubyLLM::Agents observability UI
|
|
6
|
+
#
|
|
7
|
+
# Displays high-level statistics, recent executions, and activity charts
|
|
8
|
+
# for monitoring agent performance at a glance.
|
|
9
|
+
#
|
|
10
|
+
# @see ExecutionsController For detailed execution browsing
|
|
11
|
+
# @see AgentsController For per-agent analytics
|
|
12
|
+
# @api private
|
|
5
13
|
class DashboardController < ApplicationController
|
|
14
|
+
# Renders the main dashboard view
|
|
15
|
+
#
|
|
16
|
+
# Loads now strip data, critical alerts, hourly activity,
|
|
17
|
+
# and recent executions for real-time monitoring.
|
|
18
|
+
#
|
|
19
|
+
# @return [void]
|
|
6
20
|
def index
|
|
7
|
-
@
|
|
8
|
-
@
|
|
21
|
+
@now_strip = Execution.now_strip_data
|
|
22
|
+
@critical_alerts = load_critical_alerts
|
|
9
23
|
@hourly_activity = Execution.hourly_activity_chart
|
|
24
|
+
@recent_executions = Execution.recent(10)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Returns chart data as JSON for live updates
|
|
28
|
+
#
|
|
29
|
+
# @return [JSON] Chart data with categories and series
|
|
30
|
+
def chart_data
|
|
31
|
+
render json: Execution.hourly_activity_chart_json
|
|
10
32
|
end
|
|
11
33
|
|
|
12
34
|
private
|
|
13
35
|
|
|
36
|
+
# Fetches cached daily statistics for the dashboard
|
|
37
|
+
#
|
|
38
|
+
# Results are cached for 1 minute to reduce database load while
|
|
39
|
+
# keeping the dashboard reasonably up-to-date.
|
|
40
|
+
#
|
|
41
|
+
# @return [Hash] Daily statistics
|
|
42
|
+
# @option return [Integer] :total_executions Total execution count today
|
|
43
|
+
# @option return [Integer] :successful Successful execution count
|
|
44
|
+
# @option return [Integer] :failed Failed execution count
|
|
45
|
+
# @option return [Float] :total_cost Combined cost of all executions
|
|
46
|
+
# @option return [Integer] :total_tokens Combined token usage
|
|
47
|
+
# @option return [Integer] :avg_duration_ms Average execution duration
|
|
48
|
+
# @option return [Float] :success_rate Percentage of successful executions
|
|
14
49
|
def daily_stats
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
50
|
+
Rails.cache.fetch("ruby_llm_agents/daily_stats/#{Date.current}", expires_in: 1.minute) do
|
|
51
|
+
scope = Execution.today
|
|
52
|
+
{
|
|
53
|
+
total_executions: scope.count,
|
|
54
|
+
successful: scope.successful.count,
|
|
55
|
+
failed: scope.failed.count,
|
|
56
|
+
total_cost: scope.total_cost_sum || 0,
|
|
57
|
+
total_tokens: scope.total_tokens_sum || 0,
|
|
58
|
+
avg_duration_ms: scope.avg_duration&.round || 0,
|
|
59
|
+
success_rate: calculate_success_rate(scope)
|
|
60
|
+
}
|
|
61
|
+
end
|
|
25
62
|
end
|
|
26
63
|
|
|
64
|
+
# Calculates the success rate percentage for a scope
|
|
65
|
+
#
|
|
66
|
+
# @param scope [ActiveRecord::Relation] The execution scope to calculate from
|
|
67
|
+
# @return [Float] Success rate as a percentage (0.0-100.0)
|
|
27
68
|
def calculate_success_rate(scope)
|
|
28
69
|
total = scope.count
|
|
29
70
|
return 0.0 if total.zero?
|
|
30
71
|
(scope.successful.count.to_f / total * 100).round(1)
|
|
31
72
|
end
|
|
73
|
+
|
|
74
|
+
# Loads budget status for display on dashboard
|
|
75
|
+
#
|
|
76
|
+
# @return [Hash] Budget status with global daily and monthly info
|
|
77
|
+
def load_budget_status
|
|
78
|
+
BudgetTracker.status
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Loads all open circuit breakers across agents
|
|
82
|
+
#
|
|
83
|
+
# @return [Array<Hash>] Array of open breaker information
|
|
84
|
+
def load_open_breakers
|
|
85
|
+
open_breakers = []
|
|
86
|
+
|
|
87
|
+
# Get all agents from execution history
|
|
88
|
+
agent_types = Execution.distinct.pluck(:agent_type)
|
|
89
|
+
|
|
90
|
+
agent_types.each do |agent_type|
|
|
91
|
+
# Get the agent class if available
|
|
92
|
+
agent_class = AgentRegistry.find(agent_type)
|
|
93
|
+
next unless agent_class
|
|
94
|
+
|
|
95
|
+
# Get circuit breaker config from class methods
|
|
96
|
+
cb_config = agent_class.respond_to?(:circuit_breaker_config) ? agent_class.circuit_breaker_config : nil
|
|
97
|
+
next unless cb_config
|
|
98
|
+
|
|
99
|
+
# Get models to check (primary + fallbacks)
|
|
100
|
+
primary_model = agent_class.respond_to?(:model) ? agent_class.model : RubyLLM::Agents.configuration.default_model
|
|
101
|
+
fallbacks = agent_class.respond_to?(:fallback_models) ? agent_class.fallback_models : []
|
|
102
|
+
models_to_check = [primary_model, *fallbacks].compact.uniq
|
|
103
|
+
|
|
104
|
+
models_to_check.each do |model_id|
|
|
105
|
+
breaker = CircuitBreaker.from_config(agent_type, model_id, cb_config)
|
|
106
|
+
next unless breaker
|
|
107
|
+
|
|
108
|
+
if breaker.open?
|
|
109
|
+
open_breakers << {
|
|
110
|
+
agent_type: agent_type,
|
|
111
|
+
model_id: model_id,
|
|
112
|
+
cooldown_remaining: breaker.time_until_close,
|
|
113
|
+
failure_count: breaker.failure_count,
|
|
114
|
+
threshold: cb_config[:errors] || 5
|
|
115
|
+
}
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
open_breakers
|
|
121
|
+
rescue StandardError => e
|
|
122
|
+
Rails.logger.debug("[RubyLLM::Agents] Error loading open breakers: #{e.message}")
|
|
123
|
+
[]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Loads recent alerts from cache
|
|
127
|
+
#
|
|
128
|
+
# @return [Array<Hash>] Array of recent alert events
|
|
129
|
+
def load_recent_alerts
|
|
130
|
+
Rails.cache.fetch("ruby_llm_agents/recent_alerts", expires_in: 1.minute) do
|
|
131
|
+
# Fetch from cache-based alert store (ephemeral for Phase 1)
|
|
132
|
+
alerts_key = "ruby_llm_agents:alerts:recent"
|
|
133
|
+
cached_alerts = RubyLLM::Agents.configuration.cache_store.read(alerts_key) || []
|
|
134
|
+
cached_alerts.take(10)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Loads critical alerts for the Action Center
|
|
139
|
+
#
|
|
140
|
+
# Combines open circuit breakers, budget breaches, and error spikes
|
|
141
|
+
# into a single prioritized list (max 3 items).
|
|
142
|
+
#
|
|
143
|
+
# @return [Array<Hash>] Critical alerts with type and data
|
|
144
|
+
def load_critical_alerts
|
|
145
|
+
alerts = []
|
|
146
|
+
|
|
147
|
+
# Open circuit breakers
|
|
148
|
+
load_open_breakers.each do |breaker|
|
|
149
|
+
alerts << { type: :breaker, data: breaker }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Budget breaches (>100% of limit)
|
|
153
|
+
budget_status = load_budget_status
|
|
154
|
+
daily_budget = budget_status&.dig(:global_daily)
|
|
155
|
+
monthly_budget = budget_status&.dig(:global_monthly)
|
|
156
|
+
|
|
157
|
+
if daily_budget && daily_budget[:percentage_used].to_f >= 100
|
|
158
|
+
alerts << {
|
|
159
|
+
type: :budget_breach,
|
|
160
|
+
data: {
|
|
161
|
+
period: :daily,
|
|
162
|
+
current: daily_budget[:current_spend],
|
|
163
|
+
limit: daily_budget[:limit]
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
if monthly_budget && monthly_budget[:percentage_used].to_f >= 100
|
|
169
|
+
alerts << {
|
|
170
|
+
type: :budget_breach,
|
|
171
|
+
data: {
|
|
172
|
+
period: :monthly,
|
|
173
|
+
current: monthly_budget[:current_spend],
|
|
174
|
+
limit: monthly_budget[:limit]
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Error spike detection (>5 errors in last 15 minutes)
|
|
180
|
+
error_count_15m = Execution.status_error.where("created_at > ?", 15.minutes.ago).count
|
|
181
|
+
if error_count_15m >= 5
|
|
182
|
+
alerts << { type: :error_spike, data: { count: error_count_15m } }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
alerts.take(3)
|
|
186
|
+
end
|
|
32
187
|
end
|
|
33
188
|
end
|
|
34
189
|
end
|