ruby_llm-agents 0.3.3 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +68 -4
- data/app/models/ruby_llm/agents/execution/analytics.rb +114 -13
- data/app/models/ruby_llm/agents/execution.rb +19 -58
- data/app/views/layouts/rubyllm/agents/application.html.erb +92 -350
- data/app/views/rubyllm/agents/agents/show.html.erb +331 -385
- data/app/views/rubyllm/agents/dashboard/_agent_comparison.html.erb +46 -0
- data/app/views/rubyllm/agents/dashboard/_budgets_bar.html.erb +0 -90
- data/app/views/rubyllm/agents/dashboard/_now_strip.html.erb +79 -5
- data/app/views/rubyllm/agents/dashboard/_top_errors.html.erb +49 -0
- data/app/views/rubyllm/agents/dashboard/index.html.erb +76 -121
- data/app/views/rubyllm/agents/executions/show.html.erb +134 -85
- data/app/views/rubyllm/agents/settings/show.html.erb +1 -1
- data/app/views/rubyllm/agents/shared/_breadcrumbs.html.erb +48 -0
- data/app/views/rubyllm/agents/shared/_nav_link.html.erb +27 -0
- data/config/routes.rb +2 -0
- data/lib/ruby_llm/agents/base/caching.rb +43 -0
- data/lib/ruby_llm/agents/base/cost_calculation.rb +103 -0
- data/lib/ruby_llm/agents/base/dsl.rb +261 -0
- data/lib/ruby_llm/agents/base/execution.rb +206 -0
- data/lib/ruby_llm/agents/base/reliability_execution.rb +131 -0
- data/lib/ruby_llm/agents/base/response_building.rb +86 -0
- data/lib/ruby_llm/agents/base/tool_tracking.rb +57 -0
- data/lib/ruby_llm/agents/base.rb +15 -805
- data/lib/ruby_llm/agents/version.rb +1 -1
- metadata +12 -20
- data/app/channels/ruby_llm/agents/executions_channel.rb +0 -46
- data/app/javascript/ruby_llm/agents/controllers/filter_controller.js +0 -56
- data/app/javascript/ruby_llm/agents/controllers/index.js +0 -12
- data/app/javascript/ruby_llm/agents/controllers/refresh_controller.js +0 -83
- data/app/views/rubyllm/agents/dashboard/_now_strip_values.html.erb +0 -71
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 26167fa0299a5be7e45f5742829e8d30b6b5eb6c96ce341af7895358897b5e88
|
|
4
|
+
data.tar.gz: ea883ffe277ac2595b2866cdc427a95876bf0fcb4f3b6f5d9678b7c1e54279cb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8d2f5c7e95d86da7c22ec97e90b205ec2b61918cf5d9c7fedcfca86275515d76409429ea4bdb5fe524dd58ae4691ef7d5602edee6233d77ad243a1e1ae9778e6
|
|
7
|
+
data.tar.gz: b32a708e9576ee8378a298febd4657cf72cccac481e5b31e929ef160c71be45a32a02093be06d7d62403cc03f08bb48465c8a217cd3f808f406149fa4ba6a92a
|
|
@@ -14,25 +14,89 @@ module RubyLLM
|
|
|
14
14
|
# Renders the main dashboard view
|
|
15
15
|
#
|
|
16
16
|
# Loads now strip data, critical alerts, hourly activity,
|
|
17
|
-
#
|
|
17
|
+
# recent executions, agent comparison, and top errors.
|
|
18
18
|
#
|
|
19
19
|
# @return [void]
|
|
20
20
|
def index
|
|
21
|
-
@
|
|
21
|
+
@selected_range = params[:range].presence || "today"
|
|
22
|
+
@days = range_to_days(@selected_range)
|
|
23
|
+
@now_strip = Execution.now_strip_data(range: @selected_range)
|
|
22
24
|
@critical_alerts = load_critical_alerts
|
|
23
25
|
@hourly_activity = Execution.hourly_activity_chart
|
|
24
26
|
@recent_executions = Execution.recent(10)
|
|
27
|
+
@agent_stats = build_agent_comparison
|
|
28
|
+
@top_errors = build_top_errors
|
|
25
29
|
end
|
|
26
30
|
|
|
27
31
|
# Returns chart data as JSON for live updates
|
|
28
32
|
#
|
|
29
|
-
# @
|
|
33
|
+
# @param range [String] Time range: "today", "7d", or "30d"
|
|
34
|
+
# @return [JSON] Chart data with series
|
|
30
35
|
def chart_data
|
|
31
|
-
|
|
36
|
+
range = params[:range].presence || "today"
|
|
37
|
+
render json: Execution.activity_chart_json(range: range)
|
|
32
38
|
end
|
|
33
39
|
|
|
34
40
|
private
|
|
35
41
|
|
|
42
|
+
# Converts range parameter to number of days
|
|
43
|
+
#
|
|
44
|
+
# @param range [String] Range parameter (today, 7d, 30d)
|
|
45
|
+
# @return [Integer] Number of days
|
|
46
|
+
def range_to_days(range)
|
|
47
|
+
case range
|
|
48
|
+
when "today" then 1
|
|
49
|
+
when "7d" then 7
|
|
50
|
+
when "30d" then 30
|
|
51
|
+
else 1
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Builds per-agent comparison statistics
|
|
56
|
+
#
|
|
57
|
+
# @return [Array<Hash>] Array of agent stats sorted by cost descending
|
|
58
|
+
def build_agent_comparison
|
|
59
|
+
scope = Execution.last_n_days(@days)
|
|
60
|
+
agent_types = scope.distinct.pluck(:agent_type)
|
|
61
|
+
|
|
62
|
+
agent_types.map do |agent_type|
|
|
63
|
+
agent_scope = scope.where(agent_type: agent_type)
|
|
64
|
+
count = agent_scope.count
|
|
65
|
+
total_cost = agent_scope.sum(:total_cost) || 0
|
|
66
|
+
successful = agent_scope.successful.count
|
|
67
|
+
|
|
68
|
+
{
|
|
69
|
+
agent_type: agent_type,
|
|
70
|
+
executions: count,
|
|
71
|
+
total_cost: total_cost,
|
|
72
|
+
avg_cost: count > 0 ? (total_cost / count).round(6) : 0,
|
|
73
|
+
avg_duration_ms: agent_scope.average(:duration_ms)&.round || 0,
|
|
74
|
+
success_rate: count > 0 ? (successful.to_f / count * 100).round(1) : 0
|
|
75
|
+
}
|
|
76
|
+
end.sort_by { |a| -(a[:total_cost] || 0) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Builds top errors list
|
|
80
|
+
#
|
|
81
|
+
# @return [Array<Hash>] Top 5 error classes with counts
|
|
82
|
+
def build_top_errors
|
|
83
|
+
scope = Execution.last_n_days(@days).where(status: "error")
|
|
84
|
+
total_errors = scope.count
|
|
85
|
+
|
|
86
|
+
scope.group(:error_class)
|
|
87
|
+
.select("error_class, COUNT(*) as count, MAX(created_at) as last_seen")
|
|
88
|
+
.order("count DESC")
|
|
89
|
+
.limit(5)
|
|
90
|
+
.map do |row|
|
|
91
|
+
{
|
|
92
|
+
error_class: row.error_class || "Unknown Error",
|
|
93
|
+
count: row.count,
|
|
94
|
+
percentage: total_errors > 0 ? (row.count.to_f / total_errors * 100).round(1) : 0,
|
|
95
|
+
last_seen: row.last_seen
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
36
100
|
# Fetches cached daily statistics for the dashboard
|
|
37
101
|
#
|
|
38
102
|
# Results are cached for 1 minute to reduce database load while
|
|
@@ -167,35 +167,136 @@ module RubyLLM
|
|
|
167
167
|
end
|
|
168
168
|
|
|
169
169
|
# Returns chart data as arrays for Highcharts live updates
|
|
170
|
-
# Format: { categories: [...], series: [...] }
|
|
170
|
+
# Format: { categories: [...], series: [...], range: ... }
|
|
171
|
+
#
|
|
172
|
+
# @param range [String] Time range: "today" (hourly), "7d" or "30d" (daily)
|
|
173
|
+
def activity_chart_json(range: "today")
|
|
174
|
+
case range
|
|
175
|
+
when "7d"
|
|
176
|
+
build_daily_chart_data(7)
|
|
177
|
+
when "30d"
|
|
178
|
+
build_daily_chart_data(30)
|
|
179
|
+
else
|
|
180
|
+
build_hourly_chart_data
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Alias for backwards compatibility
|
|
171
185
|
def hourly_activity_chart_json
|
|
172
|
-
|
|
173
|
-
|
|
186
|
+
activity_chart_json(range: "today")
|
|
187
|
+
end
|
|
174
188
|
|
|
175
|
-
|
|
189
|
+
private
|
|
190
|
+
|
|
191
|
+
# Builds hourly chart data for last 24 hours
|
|
192
|
+
# Optimized: Single GROUP BY query instead of 72 individual queries
|
|
193
|
+
def build_hourly_chart_data
|
|
194
|
+
reference_time = Time.current.beginning_of_hour
|
|
195
|
+
start_time = reference_time - 23.hours
|
|
196
|
+
|
|
197
|
+
# Single query with GROUP BY - reduces 72 queries to 1
|
|
198
|
+
results = where(created_at: start_time..(reference_time + 1.hour))
|
|
199
|
+
.group(Arel.sql("DATE_TRUNC('hour', created_at)"))
|
|
200
|
+
.select(
|
|
201
|
+
Arel.sql("DATE_TRUNC('hour', created_at) as time_bucket"),
|
|
202
|
+
Arel.sql("COUNT(*) FILTER (WHERE status = 'success') as success_count"),
|
|
203
|
+
Arel.sql("COUNT(*) FILTER (WHERE status IN ('error', 'timeout')) as failed_count"),
|
|
204
|
+
Arel.sql("COALESCE(SUM(total_cost), 0) as total_cost")
|
|
205
|
+
)
|
|
206
|
+
.index_by { |r| r.time_bucket.to_time.beginning_of_hour }
|
|
207
|
+
|
|
208
|
+
# Build arrays for all 24 hours (fill missing with zeros)
|
|
176
209
|
success_data = []
|
|
177
210
|
failed_data = []
|
|
211
|
+
cost_data = []
|
|
212
|
+
total_success = 0
|
|
213
|
+
total_failed = 0
|
|
214
|
+
total_cost = 0.0
|
|
178
215
|
|
|
179
|
-
# Create entries for the last 24 hours ending at current hour
|
|
180
216
|
(23.downto(0)).each do |hours_ago|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
categories << start_time.in_time_zone.strftime("%H:%M")
|
|
217
|
+
bucket_time = (reference_time - hours_ago.hours).beginning_of_hour
|
|
218
|
+
row = results[bucket_time]
|
|
184
219
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
220
|
+
s = row&.success_count.to_i
|
|
221
|
+
f = row&.failed_count.to_i
|
|
222
|
+
c = row&.total_cost.to_f
|
|
223
|
+
|
|
224
|
+
success_data << s
|
|
225
|
+
failed_data << f
|
|
226
|
+
cost_data << c.round(4)
|
|
227
|
+
|
|
228
|
+
total_success += s
|
|
229
|
+
total_failed += f
|
|
230
|
+
total_cost += c
|
|
188
231
|
end
|
|
189
232
|
|
|
190
233
|
{
|
|
191
|
-
|
|
234
|
+
range: "today",
|
|
235
|
+
totals: { success: total_success, failed: total_failed, cost: total_cost.round(4) },
|
|
192
236
|
series: [
|
|
193
237
|
{ name: "Success", data: success_data },
|
|
194
|
-
{ name: "Failed", data: failed_data }
|
|
238
|
+
{ name: "Failed", data: failed_data },
|
|
239
|
+
{ name: "Cost", data: cost_data }
|
|
195
240
|
]
|
|
196
241
|
}
|
|
197
242
|
end
|
|
198
243
|
|
|
244
|
+
# Builds daily chart data for specified number of days
|
|
245
|
+
# Optimized: Single GROUP BY query instead of 3*days individual queries
|
|
246
|
+
def build_daily_chart_data(days)
|
|
247
|
+
end_date = Date.current
|
|
248
|
+
start_date = (days - 1).days.ago.to_date
|
|
249
|
+
|
|
250
|
+
# Single query with GROUP BY - reduces 3*days queries to 1
|
|
251
|
+
results = where(created_at: start_date.beginning_of_day..end_date.end_of_day)
|
|
252
|
+
.group(Arel.sql("DATE_TRUNC('day', created_at)"))
|
|
253
|
+
.select(
|
|
254
|
+
Arel.sql("DATE_TRUNC('day', created_at) as time_bucket"),
|
|
255
|
+
Arel.sql("COUNT(*) FILTER (WHERE status = 'success') as success_count"),
|
|
256
|
+
Arel.sql("COUNT(*) FILTER (WHERE status IN ('error', 'timeout')) as failed_count"),
|
|
257
|
+
Arel.sql("COALESCE(SUM(total_cost), 0) as total_cost")
|
|
258
|
+
)
|
|
259
|
+
.index_by { |r| r.time_bucket.to_date }
|
|
260
|
+
|
|
261
|
+
# Build arrays for all days (fill missing with zeros)
|
|
262
|
+
success_data = []
|
|
263
|
+
failed_data = []
|
|
264
|
+
cost_data = []
|
|
265
|
+
total_success = 0
|
|
266
|
+
total_failed = 0
|
|
267
|
+
total_cost = 0.0
|
|
268
|
+
|
|
269
|
+
(days - 1).downto(0).each do |days_ago|
|
|
270
|
+
date = days_ago.days.ago.to_date
|
|
271
|
+
row = results[date]
|
|
272
|
+
|
|
273
|
+
s = row&.success_count.to_i
|
|
274
|
+
f = row&.failed_count.to_i
|
|
275
|
+
c = row&.total_cost.to_f
|
|
276
|
+
|
|
277
|
+
success_data << s
|
|
278
|
+
failed_data << f
|
|
279
|
+
cost_data << c.round(4)
|
|
280
|
+
|
|
281
|
+
total_success += s
|
|
282
|
+
total_failed += f
|
|
283
|
+
total_cost += c
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
{
|
|
287
|
+
range: "#{days}d",
|
|
288
|
+
days: days,
|
|
289
|
+
totals: { success: total_success, failed: total_failed, cost: total_cost.round(4) },
|
|
290
|
+
series: [
|
|
291
|
+
{ name: "Success", data: success_data },
|
|
292
|
+
{ name: "Failed", data: failed_data },
|
|
293
|
+
{ name: "Cost", data: cost_data }
|
|
294
|
+
]
|
|
295
|
+
}
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
public
|
|
299
|
+
|
|
199
300
|
# Builds the hourly activity data structure
|
|
200
301
|
# Shows the last 24 hours with current hour on the right
|
|
201
302
|
#
|
|
@@ -85,7 +85,6 @@ module RubyLLM
|
|
|
85
85
|
|
|
86
86
|
before_save :calculate_total_tokens, if: -> { input_tokens_changed? || output_tokens_changed? }
|
|
87
87
|
before_save :calculate_total_cost, if: -> { input_cost_changed? || output_cost_changed? }
|
|
88
|
-
after_commit :broadcast_turbo_streams, on: %i[create update]
|
|
89
88
|
|
|
90
89
|
# Aggregates costs from all attempts using each attempt's model pricing
|
|
91
90
|
#
|
|
@@ -228,77 +227,39 @@ module RubyLLM
|
|
|
228
227
|
|
|
229
228
|
# Returns real-time dashboard data for the Now Strip
|
|
230
229
|
#
|
|
230
|
+
# @param range [String] Time range: "today", "7d", or "30d"
|
|
231
231
|
# @return [Hash] Now strip metrics
|
|
232
|
-
def self.now_strip_data
|
|
233
|
-
|
|
232
|
+
def self.now_strip_data(range: "today")
|
|
233
|
+
scope = case range
|
|
234
|
+
when "7d" then last_n_days(7)
|
|
235
|
+
when "30d" then last_n_days(30)
|
|
236
|
+
else today
|
|
237
|
+
end
|
|
238
|
+
|
|
234
239
|
{
|
|
235
240
|
running: running.count,
|
|
236
|
-
success_today:
|
|
237
|
-
errors_today:
|
|
238
|
-
timeouts_today:
|
|
239
|
-
cost_today:
|
|
240
|
-
executions_today:
|
|
241
|
-
success_rate:
|
|
241
|
+
success_today: scope.status_success.count,
|
|
242
|
+
errors_today: scope.status_error.count,
|
|
243
|
+
timeouts_today: scope.status_timeout.count,
|
|
244
|
+
cost_today: scope.sum(:total_cost) || 0,
|
|
245
|
+
executions_today: scope.count,
|
|
246
|
+
success_rate: calculate_period_success_rate(scope)
|
|
242
247
|
}
|
|
243
248
|
end
|
|
244
249
|
|
|
245
|
-
# Calculates
|
|
250
|
+
# Calculates success rate for a given scope
|
|
246
251
|
#
|
|
252
|
+
# @param scope [ActiveRecord::Relation] The execution scope
|
|
247
253
|
# @return [Float] Success rate as percentage
|
|
248
|
-
def self.
|
|
249
|
-
total =
|
|
254
|
+
def self.calculate_period_success_rate(scope)
|
|
255
|
+
total = scope.count
|
|
250
256
|
return 0.0 if total.zero?
|
|
251
257
|
|
|
252
|
-
(
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
# Broadcasts execution changes via ActionCable for real-time dashboard updates
|
|
256
|
-
#
|
|
257
|
-
# Sends JSON with action, id, status, and rendered HTML partials.
|
|
258
|
-
# The JavaScript client handles DOM updates based on the action type.
|
|
259
|
-
#
|
|
260
|
-
# @return [void]
|
|
261
|
-
def broadcast_turbo_streams
|
|
262
|
-
ActionCable.server.broadcast(
|
|
263
|
-
"ruby_llm_agents:executions",
|
|
264
|
-
{
|
|
265
|
-
action: previously_new_record? ? "created" : "updated",
|
|
266
|
-
id: id,
|
|
267
|
-
status: status,
|
|
268
|
-
html: render_execution_html,
|
|
269
|
-
now_strip_html: render_now_strip_html
|
|
270
|
-
}
|
|
271
|
-
)
|
|
272
|
-
rescue StandardError => e
|
|
273
|
-
Rails.logger.error("[RubyLLM::Agents] Failed to broadcast execution: #{e.message}")
|
|
258
|
+
(scope.successful.count.to_f / total * 100).round(1)
|
|
274
259
|
end
|
|
275
260
|
|
|
276
261
|
private
|
|
277
262
|
|
|
278
|
-
# Renders the execution item partial for broadcast
|
|
279
|
-
#
|
|
280
|
-
# @return [String, nil] HTML string or nil if rendering fails
|
|
281
|
-
def render_execution_html
|
|
282
|
-
ApplicationController.render(
|
|
283
|
-
partial: "rubyllm/agents/dashboard/execution_item",
|
|
284
|
-
locals: { execution: self }
|
|
285
|
-
)
|
|
286
|
-
rescue StandardError
|
|
287
|
-
nil
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
# Renders the Now Strip values partial for broadcast
|
|
291
|
-
#
|
|
292
|
-
# @return [String, nil] HTML string or nil if rendering fails
|
|
293
|
-
def render_now_strip_html
|
|
294
|
-
ApplicationController.render(
|
|
295
|
-
partial: "rubyllm/agents/dashboard/now_strip_values",
|
|
296
|
-
locals: { now_strip: self.class.now_strip_data }
|
|
297
|
-
)
|
|
298
|
-
rescue StandardError
|
|
299
|
-
nil
|
|
300
|
-
end
|
|
301
|
-
|
|
302
263
|
# Calculates and sets total_tokens from input and output
|
|
303
264
|
#
|
|
304
265
|
# @return [Integer] The calculated total
|