ruby_llm-agents 0.3.1 → 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/README.md +88 -0
- 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/scopes.rb +10 -0
- data/app/models/ruby_llm/agents/execution.rb +26 -58
- data/app/views/layouts/rubyllm/agents/application.html.erb +103 -352
- data/app/views/rubyllm/agents/agents/_agent.html.erb +87 -0
- data/app/views/rubyllm/agents/agents/index.html.erb +2 -71
- data/app/views/rubyllm/agents/agents/show.html.erb +349 -416
- data/app/views/rubyllm/agents/dashboard/_action_center.html.erb +7 -7
- 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/_execution_item.html.erb +54 -39
- 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 -151
- data/app/views/rubyllm/agents/executions/show.html.erb +256 -93
- 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/generators/ruby_llm_agents/templates/add_tool_calls_migration.rb.tt +28 -0
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +7 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +13 -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 +19 -619
- data/lib/ruby_llm/agents/instrumentation.rb +36 -3
- data/lib/ruby_llm/agents/result.rb +235 -0
- data/lib/ruby_llm/agents/version.rb +1 -1
- data/lib/ruby_llm/agents.rb +1 -0
- metadata +15 -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
|
data/README.md
CHANGED
|
@@ -16,6 +16,7 @@ A powerful Rails engine for building, managing, and monitoring LLM-powered agent
|
|
|
16
16
|
- **🎯 Type Safety** - Structured output with RubyLLM::Schema integration
|
|
17
17
|
- **⚡ Real-time Streaming** - Stream LLM responses with time-to-first-token tracking
|
|
18
18
|
- **📎 Attachments** - Send images, PDFs, and files to vision-capable models
|
|
19
|
+
- **📋 Rich Results** - Access token counts, costs, timing, and model info from every execution
|
|
19
20
|
- **🔄 Reliability** - Automatic retries, model fallbacks, and circuit breakers for resilient agents
|
|
20
21
|
- **💵 Budget Controls** - Daily/monthly spending limits with hard and soft enforcement
|
|
21
22
|
- **🔔 Alerts** - Slack, webhook, and custom notifications for budget and circuit breaker events
|
|
@@ -271,6 +272,93 @@ VisionAgent.call(question: "test", with: "image.png", dry_run: true)
|
|
|
271
272
|
# => { ..., attachments: "image.png", ... }
|
|
272
273
|
```
|
|
273
274
|
|
|
275
|
+
### Execution Results
|
|
276
|
+
|
|
277
|
+
Every agent call returns a `Result` object with full execution metadata:
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
result = SearchAgent.call(query: "red dress")
|
|
281
|
+
|
|
282
|
+
# Access the processed response
|
|
283
|
+
result.content # => { refined_query: "red dress", ... }
|
|
284
|
+
|
|
285
|
+
# Token usage
|
|
286
|
+
result.input_tokens # => 150
|
|
287
|
+
result.output_tokens # => 50
|
|
288
|
+
result.total_tokens # => 200
|
|
289
|
+
result.cached_tokens # => 0
|
|
290
|
+
|
|
291
|
+
# Cost calculation
|
|
292
|
+
result.input_cost # => 0.000150
|
|
293
|
+
result.output_cost # => 0.000100
|
|
294
|
+
result.total_cost # => 0.000250
|
|
295
|
+
|
|
296
|
+
# Model info
|
|
297
|
+
result.model_id # => "gpt-4o"
|
|
298
|
+
result.chosen_model_id # => "gpt-4o" (may differ if fallback used)
|
|
299
|
+
result.temperature # => 0.0
|
|
300
|
+
|
|
301
|
+
# Timing
|
|
302
|
+
result.duration_ms # => 1234
|
|
303
|
+
result.started_at # => 2025-11-27 10:30:00 UTC
|
|
304
|
+
result.completed_at # => 2025-11-27 10:30:01 UTC
|
|
305
|
+
result.time_to_first_token_ms # => 245 (streaming only)
|
|
306
|
+
|
|
307
|
+
# Status
|
|
308
|
+
result.finish_reason # => "stop", "length", "tool_calls", etc.
|
|
309
|
+
result.streaming? # => false
|
|
310
|
+
result.success? # => true
|
|
311
|
+
result.truncated? # => false (true if hit max_tokens)
|
|
312
|
+
|
|
313
|
+
# Tool calls (for agents with tools)
|
|
314
|
+
result.tool_calls # => [{ "id" => "call_abc", "name" => "search", "arguments" => {...} }]
|
|
315
|
+
result.tool_calls_count # => 1
|
|
316
|
+
result.has_tool_calls? # => true
|
|
317
|
+
|
|
318
|
+
# Reliability info
|
|
319
|
+
result.attempts_count # => 1
|
|
320
|
+
result.used_fallback? # => false
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
#### Backward Compatibility
|
|
324
|
+
|
|
325
|
+
The Result object delegates hash methods to content, so existing code continues to work:
|
|
326
|
+
|
|
327
|
+
```ruby
|
|
328
|
+
# Old style (still works)
|
|
329
|
+
result[:refined_query]
|
|
330
|
+
result.dig(:nested, :key)
|
|
331
|
+
|
|
332
|
+
# New style (access metadata)
|
|
333
|
+
result.content[:refined_query]
|
|
334
|
+
result.total_cost
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
#### Full Metadata Hash
|
|
338
|
+
|
|
339
|
+
```ruby
|
|
340
|
+
result.to_h
|
|
341
|
+
# => {
|
|
342
|
+
# content: { refined_query: "red dress", ... },
|
|
343
|
+
# input_tokens: 150,
|
|
344
|
+
# output_tokens: 50,
|
|
345
|
+
# total_tokens: 200,
|
|
346
|
+
# cached_tokens: 0,
|
|
347
|
+
# input_cost: 0.000150,
|
|
348
|
+
# output_cost: 0.000100,
|
|
349
|
+
# total_cost: 0.000250,
|
|
350
|
+
# model_id: "gpt-4o",
|
|
351
|
+
# chosen_model_id: "gpt-4o",
|
|
352
|
+
# temperature: 0.0,
|
|
353
|
+
# duration_ms: 1234,
|
|
354
|
+
# finish_reason: "stop",
|
|
355
|
+
# streaming: false,
|
|
356
|
+
# tool_calls: [...],
|
|
357
|
+
# tool_calls_count: 0,
|
|
358
|
+
# ...
|
|
359
|
+
# }
|
|
360
|
+
```
|
|
361
|
+
|
|
274
362
|
## Usage Guide
|
|
275
363
|
|
|
276
364
|
### Agent DSL
|
|
@@ -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
|
#
|
|
@@ -256,6 +256,16 @@ module RubyLLM
|
|
|
256
256
|
scope :content_filtered, -> { where(finish_reason: "content_filter") }
|
|
257
257
|
scope :tool_calls, -> { where(finish_reason: "tool_calls") }
|
|
258
258
|
|
|
259
|
+
# @!method with_tool_calls
|
|
260
|
+
# Returns executions that made tool calls
|
|
261
|
+
# @return [ActiveRecord::Relation]
|
|
262
|
+
|
|
263
|
+
# @!method without_tool_calls
|
|
264
|
+
# Returns executions that did not make tool calls
|
|
265
|
+
# @return [ActiveRecord::Relation]
|
|
266
|
+
scope :with_tool_calls, -> { where("tool_calls_count > 0") }
|
|
267
|
+
scope :without_tool_calls, -> { where(tool_calls_count: 0) }
|
|
268
|
+
|
|
259
269
|
# @!endgroup
|
|
260
270
|
end
|
|
261
271
|
|
|
@@ -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
|
#
|
|
@@ -219,79 +218,48 @@ module RubyLLM
|
|
|
219
218
|
finish_reason == "content_filter"
|
|
220
219
|
end
|
|
221
220
|
|
|
221
|
+
# Returns whether this execution made tool calls
|
|
222
|
+
#
|
|
223
|
+
# @return [Boolean] true if tool calls were made
|
|
224
|
+
def has_tool_calls?
|
|
225
|
+
tool_calls_count.to_i > 0
|
|
226
|
+
end
|
|
227
|
+
|
|
222
228
|
# Returns real-time dashboard data for the Now Strip
|
|
223
229
|
#
|
|
230
|
+
# @param range [String] Time range: "today", "7d", or "30d"
|
|
224
231
|
# @return [Hash] Now strip metrics
|
|
225
|
-
def self.now_strip_data
|
|
226
|
-
|
|
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
|
+
|
|
227
239
|
{
|
|
228
240
|
running: running.count,
|
|
229
|
-
success_today:
|
|
230
|
-
errors_today:
|
|
231
|
-
timeouts_today:
|
|
232
|
-
cost_today:
|
|
233
|
-
executions_today:
|
|
234
|
-
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)
|
|
235
247
|
}
|
|
236
248
|
end
|
|
237
249
|
|
|
238
|
-
# Calculates
|
|
250
|
+
# Calculates success rate for a given scope
|
|
239
251
|
#
|
|
252
|
+
# @param scope [ActiveRecord::Relation] The execution scope
|
|
240
253
|
# @return [Float] Success rate as percentage
|
|
241
|
-
def self.
|
|
242
|
-
total =
|
|
254
|
+
def self.calculate_period_success_rate(scope)
|
|
255
|
+
total = scope.count
|
|
243
256
|
return 0.0 if total.zero?
|
|
244
257
|
|
|
245
|
-
(
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
# Broadcasts execution changes via ActionCable for real-time dashboard updates
|
|
249
|
-
#
|
|
250
|
-
# Sends JSON with action, id, status, and rendered HTML partials.
|
|
251
|
-
# The JavaScript client handles DOM updates based on the action type.
|
|
252
|
-
#
|
|
253
|
-
# @return [void]
|
|
254
|
-
def broadcast_turbo_streams
|
|
255
|
-
ActionCable.server.broadcast(
|
|
256
|
-
"ruby_llm_agents:executions",
|
|
257
|
-
{
|
|
258
|
-
action: previously_new_record? ? "created" : "updated",
|
|
259
|
-
id: id,
|
|
260
|
-
status: status,
|
|
261
|
-
html: render_execution_html,
|
|
262
|
-
now_strip_html: render_now_strip_html
|
|
263
|
-
}
|
|
264
|
-
)
|
|
265
|
-
rescue StandardError => e
|
|
266
|
-
Rails.logger.error("[RubyLLM::Agents] Failed to broadcast execution: #{e.message}")
|
|
258
|
+
(scope.successful.count.to_f / total * 100).round(1)
|
|
267
259
|
end
|
|
268
260
|
|
|
269
261
|
private
|
|
270
262
|
|
|
271
|
-
# Renders the execution item partial for broadcast
|
|
272
|
-
#
|
|
273
|
-
# @return [String, nil] HTML string or nil if rendering fails
|
|
274
|
-
def render_execution_html
|
|
275
|
-
ApplicationController.render(
|
|
276
|
-
partial: "rubyllm/agents/dashboard/execution_item",
|
|
277
|
-
locals: { execution: self }
|
|
278
|
-
)
|
|
279
|
-
rescue StandardError
|
|
280
|
-
nil
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
# Renders the Now Strip values partial for broadcast
|
|
284
|
-
#
|
|
285
|
-
# @return [String, nil] HTML string or nil if rendering fails
|
|
286
|
-
def render_now_strip_html
|
|
287
|
-
ApplicationController.render(
|
|
288
|
-
partial: "rubyllm/agents/dashboard/now_strip_values",
|
|
289
|
-
locals: { now_strip: self.class.now_strip_data }
|
|
290
|
-
)
|
|
291
|
-
rescue StandardError
|
|
292
|
-
nil
|
|
293
|
-
end
|
|
294
|
-
|
|
295
263
|
# Calculates and sets total_tokens from input and output
|
|
296
264
|
#
|
|
297
265
|
# @return [Integer] The calculated total
|