ruby_llm-agents 1.0.0 → 1.2.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 +9 -3
- data/app/controllers/concerns/ruby_llm/agents/sortable.rb +58 -0
- data/app/controllers/ruby_llm/agents/agents_controller.rb +59 -16
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +144 -20
- data/app/controllers/ruby_llm/agents/executions_controller.rb +13 -16
- data/app/controllers/ruby_llm/agents/workflows_controller.rb +279 -90
- data/app/helpers/ruby_llm/agents/application_helper.rb +100 -0
- data/app/mailers/ruby_llm/agents/alert_mailer.rb +84 -0
- data/app/mailers/ruby_llm/agents/application_mailer.rb +28 -0
- data/app/models/ruby_llm/agents/execution/analytics.rb +170 -20
- data/app/models/ruby_llm/agents/execution/scopes.rb +0 -31
- data/app/models/ruby_llm/agents/execution/workflow.rb +0 -129
- data/app/models/ruby_llm/agents/execution.rb +50 -14
- data/app/models/ruby_llm/agents/tenant/budgetable.rb +277 -0
- data/app/models/ruby_llm/agents/tenant/configurable.rb +135 -0
- data/app/models/ruby_llm/agents/tenant/trackable.rb +310 -0
- data/app/models/ruby_llm/agents/tenant.rb +146 -0
- data/app/models/ruby_llm/agents/tenant_budget.rb +12 -253
- data/app/services/ruby_llm/agents/agent_registry.rb +18 -12
- data/app/views/layouts/ruby_llm/agents/application.html.erb +72 -76
- data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -12
- data/app/views/ruby_llm/agents/agents/_sortable_header.html.erb +56 -0
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +5 -15
- data/app/views/ruby_llm/agents/agents/index.html.erb +271 -100
- data/app/views/ruby_llm/agents/agents/show.html.erb +1 -0
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +107 -0
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +18 -0
- data/app/views/ruby_llm/agents/api_configurations/show.html.erb +4 -1
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +66 -359
- data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +56 -0
- data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +115 -0
- data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +35 -60
- data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +17 -6
- data/app/views/ruby_llm/agents/dashboard/index.html.erb +373 -72
- data/app/views/ruby_llm/agents/executions/_execution.html.erb +0 -1
- data/app/views/ruby_llm/agents/executions/_filters.html.erb +51 -39
- data/app/views/ruby_llm/agents/executions/_list.html.erb +53 -195
- data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +5 -20
- data/app/views/ruby_llm/agents/executions/index.html.erb +7 -83
- data/app/views/ruby_llm/agents/executions/show.html.erb +10 -20
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +2 -1
- data/app/views/ruby_llm/agents/shared/_doc_link.html.erb +12 -0
- data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +3 -15
- data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_sortable_header.html.erb +53 -0
- data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +7 -0
- data/app/views/ruby_llm/agents/shared/_status_dot.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +9 -35
- data/app/views/ruby_llm/agents/system_config/show.html.erb +4 -1
- data/app/views/ruby_llm/agents/tenants/index.html.erb +4 -1
- data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +7 -15
- data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +539 -0
- data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +920 -0
- data/app/views/ruby_llm/agents/workflows/index.html.erb +179 -0
- data/app/views/ruby_llm/agents/workflows/show.html.erb +164 -139
- data/config/routes.rb +1 -1
- data/lib/generators/ruby_llm_agents/agent_generator.rb +6 -36
- data/lib/generators/ruby_llm_agents/background_remover_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/embedder_generator.rb +5 -38
- data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_editor_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_generator_generator.rb +8 -41
- data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +18 -46
- data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_variator_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/install_generator.rb +33 -56
- data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +480 -0
- data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +42 -22
- data/lib/generators/ruby_llm_agents/restructure_generator.rb +2 -2
- data/lib/generators/ruby_llm_agents/speaker_generator.rb +8 -39
- data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +13 -2
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +5 -8
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +40 -42
- data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +24 -26
- data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +19 -17
- data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +31 -33
- data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +125 -127
- data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +20 -18
- data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +19 -17
- data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +19 -17
- data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +38 -40
- data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +42 -44
- data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +48 -0
- data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +19 -21
- data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +11 -0
- data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +72 -0
- data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +19 -21
- data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +15 -17
- data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +25 -27
- data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +19 -21
- data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +17 -19
- data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +15 -17
- data/lib/generators/ruby_llm_agents/templates/rename_tenant_budgets_to_tenants_migration.rb.tt +34 -0
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +87 -24
- data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +21 -27
- data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +46 -54
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +31 -39
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +22 -28
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +53 -63
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +46 -56
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +23 -31
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +22 -30
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +23 -31
- data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +38 -46
- data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +7 -7
- data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +59 -71
- data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +274 -23
- data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +29 -31
- data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +28 -30
- data/lib/generators/ruby_llm_agents/transcriber_generator.rb +10 -43
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +26 -0
- data/lib/ruby_llm/agents/core/configuration.rb +55 -43
- data/lib/ruby_llm/agents/core/llm_tenant.rb +60 -60
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +26 -0
- data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +4 -2
- data/lib/ruby_llm/agents/pipeline.rb +69 -0
- data/lib/ruby_llm/agents/workflow/approval.rb +205 -0
- data/lib/ruby_llm/agents/workflow/approval_store.rb +179 -0
- data/lib/ruby_llm/agents/workflow/dsl/executor.rb +467 -0
- data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +244 -0
- data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +289 -0
- data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +107 -0
- data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +150 -0
- data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +187 -0
- data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +352 -0
- data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +415 -0
- data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +257 -0
- data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +317 -0
- data/lib/ruby_llm/agents/workflow/dsl.rb +576 -0
- data/lib/ruby_llm/agents/workflow/instrumentation.rb +2 -7
- data/lib/ruby_llm/agents/workflow/notifiers/base.rb +117 -0
- data/lib/ruby_llm/agents/workflow/notifiers/email.rb +117 -0
- data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +180 -0
- data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +121 -0
- data/lib/ruby_llm/agents/workflow/notifiers.rb +70 -0
- data/lib/ruby_llm/agents/workflow/orchestrator.rb +190 -23
- data/lib/ruby_llm/agents/workflow/result.rb +202 -0
- data/lib/ruby_llm/agents/workflow/throttle_manager.rb +206 -0
- data/lib/ruby_llm/agents/workflow/wait_result.rb +213 -0
- metadata +43 -6
- data/app/views/ruby_llm/agents/dashboard/_execution_item.html.erb +0 -66
- data/lib/ruby_llm/agents/workflow/parallel.rb +0 -299
- data/lib/ruby_llm/agents/workflow/pipeline.rb +0 -306
- data/lib/ruby_llm/agents/workflow/router.rb +0 -429
|
@@ -169,18 +169,33 @@ module RubyLLM
|
|
|
169
169
|
# Returns chart data as arrays for Highcharts live updates
|
|
170
170
|
# Format: { categories: [...], series: [...], range: ... }
|
|
171
171
|
#
|
|
172
|
-
# @param range [String] Time range: "today" (hourly), "7d" or "
|
|
173
|
-
|
|
172
|
+
# @param range [String] Time range: "today" (hourly), "7d", "30d", "60d", or "90d" (daily)
|
|
173
|
+
# @param offset_days [Integer, nil] Optional offset for comparison data (shifts time window back)
|
|
174
|
+
def activity_chart_json(range: "today", offset_days: nil)
|
|
174
175
|
case range
|
|
175
176
|
when "7d"
|
|
176
|
-
build_daily_chart_data(7)
|
|
177
|
+
build_daily_chart_data(7, offset_days: offset_days)
|
|
177
178
|
when "30d"
|
|
178
|
-
build_daily_chart_data(30)
|
|
179
|
+
build_daily_chart_data(30, offset_days: offset_days)
|
|
180
|
+
when "60d"
|
|
181
|
+
build_daily_chart_data(60, offset_days: offset_days)
|
|
182
|
+
when "90d"
|
|
183
|
+
build_daily_chart_data(90, offset_days: offset_days)
|
|
179
184
|
else
|
|
180
|
-
build_hourly_chart_data
|
|
185
|
+
build_hourly_chart_data(offset_days: offset_days)
|
|
181
186
|
end
|
|
182
187
|
end
|
|
183
188
|
|
|
189
|
+
# Returns chart data for a custom date range
|
|
190
|
+
# Format: { categories: [...], series: [...], range: ... }
|
|
191
|
+
#
|
|
192
|
+
# @param from [Date] Start date (inclusive)
|
|
193
|
+
# @param to [Date] End date (inclusive)
|
|
194
|
+
# @return [Hash] Chart data with series arrays
|
|
195
|
+
def activity_chart_json_for_dates(from:, to:)
|
|
196
|
+
build_daily_chart_data_for_dates(from, to)
|
|
197
|
+
end
|
|
198
|
+
|
|
184
199
|
# Alias for backwards compatibility
|
|
185
200
|
def hourly_activity_chart_json
|
|
186
201
|
activity_chart_json(range: "today")
|
|
@@ -191,22 +206,30 @@ module RubyLLM
|
|
|
191
206
|
# Builds hourly chart data for last 24 hours
|
|
192
207
|
# Optimized: Single GROUP BY query instead of 72 individual queries
|
|
193
208
|
# Database-agnostic: works with both PostgreSQL and SQLite
|
|
194
|
-
|
|
195
|
-
|
|
209
|
+
#
|
|
210
|
+
# @param offset_days [Integer, nil] Optional offset for comparison data
|
|
211
|
+
def build_hourly_chart_data(offset_days: nil)
|
|
212
|
+
offset = offset_days ? offset_days.days : 0.days
|
|
213
|
+
reference_time = (Time.current - offset).beginning_of_hour
|
|
196
214
|
start_time = reference_time - 23.hours
|
|
197
215
|
|
|
198
216
|
# Use database-agnostic aggregation with Ruby post-processing
|
|
199
217
|
results = where(created_at: start_time..(reference_time + 1.hour))
|
|
200
|
-
.select(:status, :total_cost, :created_at)
|
|
218
|
+
.select(:status, :total_cost, :duration_ms, :input_tokens, :output_tokens, :created_at)
|
|
201
219
|
.group_by { |r| r.created_at.beginning_of_hour }
|
|
202
220
|
|
|
203
221
|
# Build arrays for all 24 hours (fill missing with zeros)
|
|
204
222
|
success_data = []
|
|
205
223
|
failed_data = []
|
|
206
224
|
cost_data = []
|
|
225
|
+
duration_data = []
|
|
226
|
+
tokens_data = []
|
|
207
227
|
total_success = 0
|
|
208
228
|
total_failed = 0
|
|
209
229
|
total_cost = 0.0
|
|
230
|
+
total_duration_sum = 0
|
|
231
|
+
total_duration_count = 0
|
|
232
|
+
total_tokens = 0
|
|
210
233
|
|
|
211
234
|
(23.downto(0)).each do |hours_ago|
|
|
212
235
|
bucket_time = (reference_time - hours_ago.hours).beginning_of_hour
|
|
@@ -215,23 +238,43 @@ module RubyLLM
|
|
|
215
238
|
s = rows.count { |r| r.status == "success" }
|
|
216
239
|
f = rows.count { |r| r.status.in?(%w[error timeout]) }
|
|
217
240
|
c = rows.sum { |r| r.total_cost.to_f }
|
|
241
|
+
t = rows.sum { |r| (r.input_tokens || 0) + (r.output_tokens || 0) }
|
|
242
|
+
|
|
243
|
+
# Average duration for this bucket
|
|
244
|
+
duration_rows = rows.select { |r| r.duration_ms.to_i > 0 }
|
|
245
|
+
d = duration_rows.any? ? (duration_rows.sum { |r| r.duration_ms.to_i } / duration_rows.count) : 0
|
|
218
246
|
|
|
219
247
|
success_data << s
|
|
220
248
|
failed_data << f
|
|
221
249
|
cost_data << c.round(4)
|
|
250
|
+
duration_data << d.round
|
|
251
|
+
tokens_data << t
|
|
222
252
|
|
|
223
253
|
total_success += s
|
|
224
254
|
total_failed += f
|
|
225
255
|
total_cost += c
|
|
256
|
+
total_tokens += t
|
|
257
|
+
total_duration_sum += duration_rows.sum { |r| r.duration_ms.to_i }
|
|
258
|
+
total_duration_count += duration_rows.count
|
|
226
259
|
end
|
|
227
260
|
|
|
261
|
+
avg_duration_ms = total_duration_count > 0 ? (total_duration_sum / total_duration_count).round : 0
|
|
262
|
+
|
|
228
263
|
{
|
|
229
264
|
range: "today",
|
|
230
|
-
totals: {
|
|
265
|
+
totals: {
|
|
266
|
+
success: total_success,
|
|
267
|
+
failed: total_failed,
|
|
268
|
+
cost: total_cost.round(4),
|
|
269
|
+
duration_ms: avg_duration_ms,
|
|
270
|
+
tokens: total_tokens
|
|
271
|
+
},
|
|
231
272
|
series: [
|
|
232
273
|
{ name: "Success", data: success_data },
|
|
233
|
-
{ name: "
|
|
234
|
-
{ name: "Cost", data: cost_data }
|
|
274
|
+
{ name: "Errors", data: failed_data },
|
|
275
|
+
{ name: "Cost", data: cost_data },
|
|
276
|
+
{ name: "Duration", data: duration_data },
|
|
277
|
+
{ name: "Tokens", data: tokens_data }
|
|
235
278
|
]
|
|
236
279
|
}
|
|
237
280
|
end
|
|
@@ -239,48 +282,155 @@ module RubyLLM
|
|
|
239
282
|
# Builds daily chart data for specified number of days
|
|
240
283
|
# Optimized: Single query instead of 3*days individual queries
|
|
241
284
|
# Database-agnostic: works with both PostgreSQL and SQLite
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
285
|
+
#
|
|
286
|
+
# @param days [Integer] Number of days to include
|
|
287
|
+
# @param offset_days [Integer, nil] Optional offset for comparison data
|
|
288
|
+
def build_daily_chart_data(days, offset_days: nil)
|
|
289
|
+
offset = offset_days || 0
|
|
290
|
+
end_date = Date.current - offset.days
|
|
291
|
+
start_date = end_date - (days - 1).days
|
|
245
292
|
|
|
246
293
|
# Use database-agnostic aggregation with Ruby post-processing
|
|
247
294
|
results = where(created_at: start_date.beginning_of_day..end_date.end_of_day)
|
|
248
|
-
.select(:status, :total_cost, :created_at)
|
|
295
|
+
.select(:status, :total_cost, :duration_ms, :input_tokens, :output_tokens, :created_at)
|
|
249
296
|
.group_by { |r| r.created_at.to_date }
|
|
250
297
|
|
|
251
298
|
# Build arrays for all days (fill missing with zeros)
|
|
252
299
|
success_data = []
|
|
253
300
|
failed_data = []
|
|
254
301
|
cost_data = []
|
|
302
|
+
duration_data = []
|
|
303
|
+
tokens_data = []
|
|
255
304
|
total_success = 0
|
|
256
305
|
total_failed = 0
|
|
257
306
|
total_cost = 0.0
|
|
307
|
+
total_duration_sum = 0
|
|
308
|
+
total_duration_count = 0
|
|
309
|
+
total_tokens = 0
|
|
258
310
|
|
|
259
|
-
(days - 1).downto(0).each do |
|
|
260
|
-
date =
|
|
311
|
+
(days - 1).downto(0).each do |i|
|
|
312
|
+
date = end_date - i.days
|
|
261
313
|
rows = results[date] || []
|
|
262
314
|
|
|
263
315
|
s = rows.count { |r| r.status == "success" }
|
|
264
316
|
f = rows.count { |r| r.status.in?(%w[error timeout]) }
|
|
265
317
|
c = rows.sum { |r| r.total_cost.to_f }
|
|
318
|
+
t = rows.sum { |r| (r.input_tokens || 0) + (r.output_tokens || 0) }
|
|
319
|
+
|
|
320
|
+
# Average duration for this bucket
|
|
321
|
+
duration_rows = rows.select { |r| r.duration_ms.to_i > 0 }
|
|
322
|
+
d = duration_rows.any? ? (duration_rows.sum { |r| r.duration_ms.to_i } / duration_rows.count) : 0
|
|
266
323
|
|
|
267
324
|
success_data << s
|
|
268
325
|
failed_data << f
|
|
269
326
|
cost_data << c.round(4)
|
|
327
|
+
duration_data << d.round
|
|
328
|
+
tokens_data << t
|
|
270
329
|
|
|
271
330
|
total_success += s
|
|
272
331
|
total_failed += f
|
|
273
332
|
total_cost += c
|
|
333
|
+
total_tokens += t
|
|
334
|
+
total_duration_sum += duration_rows.sum { |r| r.duration_ms.to_i }
|
|
335
|
+
total_duration_count += duration_rows.count
|
|
274
336
|
end
|
|
275
337
|
|
|
338
|
+
avg_duration_ms = total_duration_count > 0 ? (total_duration_sum / total_duration_count).round : 0
|
|
339
|
+
|
|
276
340
|
{
|
|
277
341
|
range: "#{days}d",
|
|
278
342
|
days: days,
|
|
279
|
-
totals: {
|
|
343
|
+
totals: {
|
|
344
|
+
success: total_success,
|
|
345
|
+
failed: total_failed,
|
|
346
|
+
cost: total_cost.round(4),
|
|
347
|
+
duration_ms: avg_duration_ms,
|
|
348
|
+
tokens: total_tokens
|
|
349
|
+
},
|
|
350
|
+
series: [
|
|
351
|
+
{ name: "Success", data: success_data },
|
|
352
|
+
{ name: "Errors", data: failed_data },
|
|
353
|
+
{ name: "Cost", data: cost_data },
|
|
354
|
+
{ name: "Duration", data: duration_data },
|
|
355
|
+
{ name: "Tokens", data: tokens_data }
|
|
356
|
+
]
|
|
357
|
+
}
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Builds daily chart data for a custom date range
|
|
361
|
+
# Database-agnostic: works with both PostgreSQL and SQLite
|
|
362
|
+
#
|
|
363
|
+
# @param from_date [Date] Start date (inclusive)
|
|
364
|
+
# @param to_date [Date] End date (inclusive)
|
|
365
|
+
# @return [Hash] Chart data with series arrays
|
|
366
|
+
def build_daily_chart_data_for_dates(from_date, to_date)
|
|
367
|
+
days = (to_date - from_date).to_i + 1
|
|
368
|
+
|
|
369
|
+
# Use database-agnostic aggregation with Ruby post-processing
|
|
370
|
+
results = where(created_at: from_date.beginning_of_day..to_date.end_of_day)
|
|
371
|
+
.select(:status, :total_cost, :duration_ms, :input_tokens, :output_tokens, :created_at)
|
|
372
|
+
.group_by { |r| r.created_at.to_date }
|
|
373
|
+
|
|
374
|
+
# Build arrays for all days (fill missing with zeros)
|
|
375
|
+
success_data = []
|
|
376
|
+
failed_data = []
|
|
377
|
+
cost_data = []
|
|
378
|
+
duration_data = []
|
|
379
|
+
tokens_data = []
|
|
380
|
+
total_success = 0
|
|
381
|
+
total_failed = 0
|
|
382
|
+
total_cost = 0.0
|
|
383
|
+
total_duration_sum = 0
|
|
384
|
+
total_duration_count = 0
|
|
385
|
+
total_tokens = 0
|
|
386
|
+
|
|
387
|
+
(0...days).each do |i|
|
|
388
|
+
date = from_date + i.days
|
|
389
|
+
rows = results[date] || []
|
|
390
|
+
|
|
391
|
+
s = rows.count { |r| r.status == "success" }
|
|
392
|
+
f = rows.count { |r| r.status.in?(%w[error timeout]) }
|
|
393
|
+
c = rows.sum { |r| r.total_cost.to_f }
|
|
394
|
+
t = rows.sum { |r| (r.input_tokens || 0) + (r.output_tokens || 0) }
|
|
395
|
+
|
|
396
|
+
# Average duration for this bucket
|
|
397
|
+
duration_rows = rows.select { |r| r.duration_ms.to_i > 0 }
|
|
398
|
+
d = duration_rows.any? ? (duration_rows.sum { |r| r.duration_ms.to_i } / duration_rows.count) : 0
|
|
399
|
+
|
|
400
|
+
success_data << s
|
|
401
|
+
failed_data << f
|
|
402
|
+
cost_data << c.round(4)
|
|
403
|
+
duration_data << d.round
|
|
404
|
+
tokens_data << t
|
|
405
|
+
|
|
406
|
+
total_success += s
|
|
407
|
+
total_failed += f
|
|
408
|
+
total_cost += c
|
|
409
|
+
total_tokens += t
|
|
410
|
+
total_duration_sum += duration_rows.sum { |r| r.duration_ms.to_i }
|
|
411
|
+
total_duration_count += duration_rows.count
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
avg_duration_ms = total_duration_count > 0 ? (total_duration_sum / total_duration_count).round : 0
|
|
415
|
+
|
|
416
|
+
{
|
|
417
|
+
range: "custom",
|
|
418
|
+
days: days,
|
|
419
|
+
from: from_date.to_s,
|
|
420
|
+
to: to_date.to_s,
|
|
421
|
+
totals: {
|
|
422
|
+
success: total_success,
|
|
423
|
+
failed: total_failed,
|
|
424
|
+
cost: total_cost.round(4),
|
|
425
|
+
duration_ms: avg_duration_ms,
|
|
426
|
+
tokens: total_tokens
|
|
427
|
+
},
|
|
280
428
|
series: [
|
|
281
429
|
{ name: "Success", data: success_data },
|
|
282
|
-
{ name: "
|
|
283
|
-
{ name: "Cost", data: cost_data }
|
|
430
|
+
{ name: "Errors", data: failed_data },
|
|
431
|
+
{ name: "Cost", data: cost_data },
|
|
432
|
+
{ name: "Duration", data: duration_data },
|
|
433
|
+
{ name: "Tokens", data: tokens_data }
|
|
284
434
|
]
|
|
285
435
|
}
|
|
286
436
|
end
|
|
@@ -156,37 +156,6 @@ module RubyLLM
|
|
|
156
156
|
|
|
157
157
|
# @!endgroup
|
|
158
158
|
|
|
159
|
-
# @!group Search Scopes
|
|
160
|
-
|
|
161
|
-
# @!method search(query)
|
|
162
|
-
# Free-text search across error fields and parameters
|
|
163
|
-
# @param query [String] Search query
|
|
164
|
-
# @return [ActiveRecord::Relation]
|
|
165
|
-
scope :search, ->(query) do
|
|
166
|
-
return all if query.blank?
|
|
167
|
-
|
|
168
|
-
sanitized_query = "%#{sanitize_sql_like(query)}%"
|
|
169
|
-
# Use database-agnostic case-insensitive search
|
|
170
|
-
# PostgreSQL: ILIKE, SQLite: LIKE with LOWER() + ESCAPE clause
|
|
171
|
-
if connection.adapter_name.downcase.include?("postgresql")
|
|
172
|
-
where(
|
|
173
|
-
"error_class ILIKE :q OR error_message ILIKE :q OR CAST(parameters AS TEXT) ILIKE :q",
|
|
174
|
-
q: sanitized_query
|
|
175
|
-
)
|
|
176
|
-
else
|
|
177
|
-
# SQLite and other databases need ESCAPE clause for backslash to work
|
|
178
|
-
sanitized_query_lower = sanitized_query.downcase
|
|
179
|
-
where(
|
|
180
|
-
"LOWER(error_class) LIKE :q ESCAPE '\\' OR " \
|
|
181
|
-
"LOWER(error_message) LIKE :q ESCAPE '\\' OR " \
|
|
182
|
-
"LOWER(CAST(parameters AS TEXT)) LIKE :q ESCAPE '\\'",
|
|
183
|
-
q: sanitized_query_lower
|
|
184
|
-
)
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
# @!endgroup
|
|
189
|
-
|
|
190
159
|
# @!group Tracing Scopes
|
|
191
160
|
|
|
192
161
|
# @!method by_trace(trace_id)
|
|
@@ -21,27 +21,6 @@ module RubyLLM
|
|
|
21
21
|
workflow_type.present?
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
# Returns whether this is a pipeline workflow
|
|
25
|
-
#
|
|
26
|
-
# @return [Boolean] true if workflow_type is "pipeline"
|
|
27
|
-
def pipeline_workflow?
|
|
28
|
-
workflow_type == "pipeline"
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Returns whether this is a parallel workflow
|
|
32
|
-
#
|
|
33
|
-
# @return [Boolean] true if workflow_type is "parallel"
|
|
34
|
-
def parallel_workflow?
|
|
35
|
-
workflow_type == "parallel"
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Returns whether this is a router workflow
|
|
39
|
-
#
|
|
40
|
-
# @return [Boolean] true if workflow_type is "router"
|
|
41
|
-
def router_workflow?
|
|
42
|
-
workflow_type == "router"
|
|
43
|
-
end
|
|
44
|
-
|
|
45
24
|
# Returns whether this is a root workflow execution (top-level)
|
|
46
25
|
#
|
|
47
26
|
# @return [Boolean] true if this is a workflow with no parent
|
|
@@ -134,114 +113,6 @@ module RubyLLM
|
|
|
134
113
|
|
|
135
114
|
# @!endgroup
|
|
136
115
|
|
|
137
|
-
# @!group Pipeline-specific Methods
|
|
138
|
-
|
|
139
|
-
# Returns pipeline steps in order with their status
|
|
140
|
-
#
|
|
141
|
-
# @return [Array<Hash>] Array of step hashes with name, status, duration, cost
|
|
142
|
-
def pipeline_steps_detail
|
|
143
|
-
return [] unless pipeline_workflow?
|
|
144
|
-
|
|
145
|
-
workflow_steps.map do |step|
|
|
146
|
-
{
|
|
147
|
-
id: step.id,
|
|
148
|
-
name: step.workflow_step || step.agent_type.gsub(/Agent$/, ""),
|
|
149
|
-
agent_type: step.agent_type,
|
|
150
|
-
status: step.status,
|
|
151
|
-
duration_ms: step.duration_ms,
|
|
152
|
-
total_cost: step.total_cost,
|
|
153
|
-
total_tokens: step.total_tokens,
|
|
154
|
-
model_id: step.model_id
|
|
155
|
-
}
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
# @!endgroup
|
|
160
|
-
|
|
161
|
-
# @!group Parallel-specific Methods
|
|
162
|
-
|
|
163
|
-
# Returns parallel branches with their status and timing
|
|
164
|
-
#
|
|
165
|
-
# @return [Array<Hash>] Array of branch hashes
|
|
166
|
-
def parallel_branches_detail
|
|
167
|
-
return [] unless parallel_workflow?
|
|
168
|
-
|
|
169
|
-
branches = workflow_steps.to_a
|
|
170
|
-
return [] if branches.empty?
|
|
171
|
-
|
|
172
|
-
# Find min/max for timing comparison
|
|
173
|
-
min_duration = branches.map { |b| b.duration_ms || 0 }.min
|
|
174
|
-
max_duration = branches.map { |b| b.duration_ms || 0 }.max
|
|
175
|
-
|
|
176
|
-
branches.map do |branch|
|
|
177
|
-
duration = branch.duration_ms || 0
|
|
178
|
-
{
|
|
179
|
-
id: branch.id,
|
|
180
|
-
name: branch.workflow_step || branch.agent_type.gsub(/Agent$/, ""),
|
|
181
|
-
agent_type: branch.agent_type,
|
|
182
|
-
status: branch.status,
|
|
183
|
-
duration_ms: duration,
|
|
184
|
-
total_cost: branch.total_cost,
|
|
185
|
-
total_tokens: branch.total_tokens,
|
|
186
|
-
model_id: branch.model_id,
|
|
187
|
-
is_fastest: duration == min_duration && branches.size > 1,
|
|
188
|
-
is_slowest: duration == max_duration && branches.size > 1 && min_duration != max_duration
|
|
189
|
-
}
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
# @!endgroup
|
|
194
|
-
|
|
195
|
-
# @!group Router-specific Methods
|
|
196
|
-
|
|
197
|
-
# Returns router classification details
|
|
198
|
-
#
|
|
199
|
-
# @return [Hash] Classification info including method, model, timing
|
|
200
|
-
def router_classification_detail
|
|
201
|
-
return {} unless router_workflow?
|
|
202
|
-
|
|
203
|
-
result = if classification_result.is_a?(String)
|
|
204
|
-
begin
|
|
205
|
-
JSON.parse(classification_result)
|
|
206
|
-
rescue JSON::ParserError
|
|
207
|
-
{}
|
|
208
|
-
end
|
|
209
|
-
else
|
|
210
|
-
classification_result || {}
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
{
|
|
214
|
-
method: result["method"],
|
|
215
|
-
classifier_model: result["classifier_model"],
|
|
216
|
-
classification_time_ms: result["classification_time_ms"],
|
|
217
|
-
routed_to: routed_to,
|
|
218
|
-
confidence: result["confidence"]
|
|
219
|
-
}
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
# Returns available routes and which one was chosen
|
|
223
|
-
#
|
|
224
|
-
# @return [Hash] Routes info with chosen route highlighted
|
|
225
|
-
def router_routes_detail
|
|
226
|
-
return {} unless router_workflow?
|
|
227
|
-
|
|
228
|
-
# Get the routed execution (child)
|
|
229
|
-
routed_child = child_executions.first
|
|
230
|
-
|
|
231
|
-
{
|
|
232
|
-
chosen_route: routed_to,
|
|
233
|
-
routed_execution: routed_child ? {
|
|
234
|
-
id: routed_child.id,
|
|
235
|
-
agent_type: routed_child.agent_type,
|
|
236
|
-
status: routed_child.status,
|
|
237
|
-
duration_ms: routed_child.duration_ms,
|
|
238
|
-
total_cost: routed_child.total_cost
|
|
239
|
-
} : nil
|
|
240
|
-
}
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
# @!endgroup
|
|
244
|
-
|
|
245
116
|
private
|
|
246
117
|
|
|
247
118
|
# Returns empty aggregate stats hash
|
|
@@ -239,23 +239,59 @@ module RubyLLM
|
|
|
239
239
|
# Returns real-time dashboard data for the Now Strip
|
|
240
240
|
#
|
|
241
241
|
# @param range [String] Time range: "today", "7d", or "30d"
|
|
242
|
-
# @return [Hash] Now strip metrics
|
|
242
|
+
# @return [Hash] Now strip metrics with period-over-period comparisons
|
|
243
243
|
def self.now_strip_data(range: "today")
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
244
|
+
current_scope = case range
|
|
245
|
+
when "7d" then last_n_days(7)
|
|
246
|
+
when "30d" then last_n_days(30)
|
|
247
|
+
else today
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
previous_scope = case range
|
|
251
|
+
when "7d" then where(created_at: 14.days.ago.beginning_of_day..7.days.ago.beginning_of_day)
|
|
252
|
+
when "30d" then where(created_at: 60.days.ago.beginning_of_day..30.days.ago.beginning_of_day)
|
|
253
|
+
else yesterday
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
current = {
|
|
251
257
|
running: running.count,
|
|
252
|
-
success_today:
|
|
253
|
-
errors_today:
|
|
254
|
-
timeouts_today:
|
|
255
|
-
cost_today:
|
|
256
|
-
executions_today:
|
|
257
|
-
success_rate: calculate_period_success_rate(
|
|
258
|
+
success_today: current_scope.status_success.count,
|
|
259
|
+
errors_today: current_scope.status_error.count,
|
|
260
|
+
timeouts_today: current_scope.status_timeout.count,
|
|
261
|
+
cost_today: current_scope.sum(:total_cost) || 0,
|
|
262
|
+
executions_today: current_scope.count,
|
|
263
|
+
success_rate: calculate_period_success_rate(current_scope),
|
|
264
|
+
avg_duration_ms: current_scope.avg_duration&.round || 0,
|
|
265
|
+
total_tokens: current_scope.total_tokens_sum || 0
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
previous = {
|
|
269
|
+
success: previous_scope.status_success.count,
|
|
270
|
+
errors: previous_scope.status_error.count,
|
|
271
|
+
cost: previous_scope.sum(:total_cost) || 0,
|
|
272
|
+
avg_duration_ms: previous_scope.avg_duration&.round || 0,
|
|
273
|
+
total_tokens: previous_scope.total_tokens_sum || 0
|
|
258
274
|
}
|
|
275
|
+
|
|
276
|
+
current.merge(
|
|
277
|
+
comparisons: {
|
|
278
|
+
success_change: pct_change(previous[:success], current[:success_today]),
|
|
279
|
+
errors_change: pct_change(previous[:errors], current[:errors_today]),
|
|
280
|
+
cost_change: pct_change(previous[:cost], current[:cost_today]),
|
|
281
|
+
duration_change: pct_change(previous[:avg_duration_ms], current[:avg_duration_ms]),
|
|
282
|
+
tokens_change: pct_change(previous[:total_tokens], current[:total_tokens])
|
|
283
|
+
}
|
|
284
|
+
)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Calculates percentage change between old and new values
|
|
288
|
+
#
|
|
289
|
+
# @param old_val [Numeric, nil] Previous period value
|
|
290
|
+
# @param new_val [Numeric] Current period value
|
|
291
|
+
# @return [Float, nil] Percentage change or nil if old_val is nil/zero
|
|
292
|
+
def self.pct_change(old_val, new_val)
|
|
293
|
+
return nil if old_val.nil? || old_val.zero?
|
|
294
|
+
((new_val - old_val).to_f / old_val * 100).round(1)
|
|
259
295
|
end
|
|
260
296
|
|
|
261
297
|
# Calculates success rate for a given scope
|