ruby_llm-agents 1.0.0 → 1.1.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/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/restructure_generator.rb +2 -2
- data/lib/generators/ruby_llm_agents/speaker_generator.rb +8 -39
- 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/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/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/ruby_llm/agents/core/configuration.rb +55 -43
- 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/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 +37 -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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 35819df82cd1d73ad351e6e71ef076ee5131d0e021ba4729acfb274ff703a17e
|
|
4
|
+
data.tar.gz: 7df1b73bafa5d4bbd63b47499ac93ab58091417313b2bd2c07214c2ef8e489a8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 484d69732880f808d6710346e0aa3d11d06cb4090b8ecf127a8620702064df3e6da71e78d3f4445c368787668629623a514d0302366e21b6aab8e23f4572c4c5
|
|
7
|
+
data.tar.gz: 62bb19973149e5dd17e9165d240c238d49d406945a2a91bf7cdbb3fe075198dedb092f2e8410ce0ce84bc31115b04e75ac5b5c02206e82d2f774faf993ecb9ef
|
|
@@ -21,6 +21,7 @@ module RubyLLM
|
|
|
21
21
|
#
|
|
22
22
|
# @param scope [ActiveRecord::Relation] The scope to paginate
|
|
23
23
|
# @param ordered [Boolean] Whether to apply default descending order (default: true)
|
|
24
|
+
# @param sort_params [Hash, nil] Optional custom sort parameters with :column and :direction
|
|
24
25
|
# @return [Hash] Contains :records and :pagination keys
|
|
25
26
|
# @option return [ActiveRecord::Relation] :records Paginated records
|
|
26
27
|
# @option return [Hash] :pagination Pagination metadata
|
|
@@ -28,13 +29,18 @@ module RubyLLM
|
|
|
28
29
|
# - :per_page [Integer] Records per page
|
|
29
30
|
# - :total_count [Integer] Total record count
|
|
30
31
|
# - :total_pages [Integer] Total page count
|
|
31
|
-
def paginate(scope, ordered: true)
|
|
32
|
+
def paginate(scope, ordered: true, sort_params: nil)
|
|
32
33
|
page = [(params[:page] || 1).to_i, 1].max
|
|
33
34
|
per_page = RubyLLM::Agents.configuration.per_page
|
|
34
35
|
offset = (page - 1) * per_page
|
|
35
36
|
|
|
36
|
-
#
|
|
37
|
-
|
|
37
|
+
# Apply sorting - use custom sort_params if provided, otherwise default
|
|
38
|
+
table_name = scope.model.table_name
|
|
39
|
+
if sort_params.present?
|
|
40
|
+
scope = scope.order("#{table_name}.#{sort_params[:column]} #{sort_params[:direction].upcase}")
|
|
41
|
+
elsif ordered
|
|
42
|
+
scope = scope.order("#{table_name}.created_at DESC")
|
|
43
|
+
end
|
|
38
44
|
total_count = scope.count
|
|
39
45
|
|
|
40
46
|
{
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
# Controller concern for sorting
|
|
6
|
+
#
|
|
7
|
+
# Provides secure column sorting with whitelisted columns and direction validation.
|
|
8
|
+
# Prevents SQL injection by only allowing predefined sort columns.
|
|
9
|
+
#
|
|
10
|
+
# @example Using in a controller
|
|
11
|
+
# include Sortable
|
|
12
|
+
# @sort_params = parse_sort_params
|
|
13
|
+
# result = paginate(scope, sort_params: @sort_params)
|
|
14
|
+
#
|
|
15
|
+
# @api private
|
|
16
|
+
module Sortable
|
|
17
|
+
extend ActiveSupport::Concern
|
|
18
|
+
|
|
19
|
+
# Whitelist of allowed sort columns mapped to their database column names
|
|
20
|
+
# Keys are the URL parameter values, values are the actual column names
|
|
21
|
+
SORTABLE_COLUMNS = {
|
|
22
|
+
"agent_type" => "agent_type",
|
|
23
|
+
"status" => "status",
|
|
24
|
+
"model_id" => "model_id",
|
|
25
|
+
"agent_version" => "agent_version",
|
|
26
|
+
"total_tokens" => "total_tokens",
|
|
27
|
+
"total_cost" => "total_cost",
|
|
28
|
+
"duration_ms" => "duration_ms",
|
|
29
|
+
"created_at" => "created_at"
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
SORT_DIRECTIONS = %w[asc desc].freeze
|
|
33
|
+
DEFAULT_SORT_COLUMN = "created_at"
|
|
34
|
+
DEFAULT_SORT_DIRECTION = "desc"
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# Parses and validates sort parameters from the request
|
|
39
|
+
#
|
|
40
|
+
# Returns validated sort column and direction, falling back to defaults
|
|
41
|
+
# if invalid values are provided. This prevents SQL injection by only
|
|
42
|
+
# allowing whitelisted column names.
|
|
43
|
+
#
|
|
44
|
+
# @return [Hash] Contains :column and :direction keys
|
|
45
|
+
# @option return [String] :column The validated database column name
|
|
46
|
+
# @option return [String] :direction Either 'asc' or 'desc'
|
|
47
|
+
def parse_sort_params
|
|
48
|
+
column = params[:sort].to_s
|
|
49
|
+
direction = params[:direction].to_s.downcase
|
|
50
|
+
|
|
51
|
+
{
|
|
52
|
+
column: SORTABLE_COLUMNS[column] || DEFAULT_SORT_COLUMN,
|
|
53
|
+
direction: SORT_DIRECTIONS.include?(direction) ? direction : DEFAULT_SORT_DIRECTION
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -16,21 +16,35 @@ module RubyLLM
|
|
|
16
16
|
include Paginatable
|
|
17
17
|
include Filterable
|
|
18
18
|
|
|
19
|
+
# Allowed sort columns for the agents list (in-memory sorting)
|
|
20
|
+
AGENT_SORTABLE_COLUMNS = %w[name agent_type model execution_count total_cost success_rate last_executed].freeze
|
|
21
|
+
DEFAULT_AGENT_SORT_COLUMN = "name"
|
|
22
|
+
DEFAULT_AGENT_SORT_DIRECTION = "asc"
|
|
23
|
+
|
|
19
24
|
# Lists all registered agents with their details
|
|
20
25
|
#
|
|
21
26
|
# Uses AgentRegistry to discover agents from both file system
|
|
22
27
|
# and execution history, ensuring deleted agents with history
|
|
23
28
|
# are still visible. Separates agents and workflows for tabbed display.
|
|
29
|
+
# Deleted agents are shown in a separate tab.
|
|
24
30
|
#
|
|
25
31
|
# @return [void]
|
|
26
32
|
def index
|
|
27
33
|
all_items = AgentRegistry.all_with_details
|
|
28
34
|
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
# Filter to only agents (not workflows)
|
|
36
|
+
all_agents = all_items.reject { |a| a[:is_workflow] }
|
|
37
|
+
|
|
38
|
+
# Separate active and deleted agents
|
|
39
|
+
@agents = all_agents.select { |a| a[:active] }
|
|
40
|
+
@deleted_agents = all_agents.reject { |a| a[:active] }
|
|
32
41
|
|
|
33
|
-
#
|
|
42
|
+
# Parse and apply sorting to both lists
|
|
43
|
+
@sort_params = parse_agent_sort_params
|
|
44
|
+
@agents = sort_agents(@agents)
|
|
45
|
+
@deleted_agents = sort_agents(@deleted_agents)
|
|
46
|
+
|
|
47
|
+
# Group active agents by type for sub-tabs
|
|
34
48
|
@agents_by_type = {
|
|
35
49
|
agent: @agents.select { |a| a[:agent_type] == "agent" },
|
|
36
50
|
embedder: @agents.select { |a| a[:agent_type] == "embedder" },
|
|
@@ -40,24 +54,16 @@ module RubyLLM
|
|
|
40
54
|
image_generator: @agents.select { |a| a[:agent_type] == "image_generator" }
|
|
41
55
|
}
|
|
42
56
|
|
|
43
|
-
# Group workflows by type for sub-tabs
|
|
44
|
-
@workflows_by_type = {
|
|
45
|
-
pipeline: @workflows.select { |w| w[:workflow_type] == "pipeline" },
|
|
46
|
-
parallel: @workflows.select { |w| w[:workflow_type] == "parallel" },
|
|
47
|
-
router: @workflows.select { |w| w[:workflow_type] == "router" }
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
# Counts for tab badges
|
|
51
57
|
@agent_count = @agents.size
|
|
52
|
-
@
|
|
58
|
+
@deleted_count = @deleted_agents.size
|
|
53
59
|
rescue StandardError => e
|
|
54
60
|
Rails.logger.error("[RubyLLM::Agents] Error loading agents: #{e.message}")
|
|
55
61
|
@agents = []
|
|
56
|
-
@
|
|
62
|
+
@deleted_agents = []
|
|
57
63
|
@agents_by_type = { agent: [], embedder: [], moderator: [], speaker: [], transcriber: [], image_generator: [] }
|
|
58
|
-
@workflows_by_type = { pipeline: [], parallel: [], router: [] }
|
|
59
64
|
@agent_count = 0
|
|
60
|
-
@
|
|
65
|
+
@deleted_count = 0
|
|
66
|
+
@sort_params = { column: DEFAULT_AGENT_SORT_COLUMN, direction: DEFAULT_AGENT_SORT_DIRECTION }
|
|
61
67
|
flash.now[:alert] = "Error loading agents list"
|
|
62
68
|
end
|
|
63
69
|
|
|
@@ -404,6 +410,43 @@ module RubyLLM
|
|
|
404
410
|
Rails.logger.debug("[RubyLLM::Agents] Could not load circuit breaker status: #{e.message}")
|
|
405
411
|
@circuit_breaker_status = {}
|
|
406
412
|
end
|
|
413
|
+
|
|
414
|
+
# Parses and validates sort parameters for agents list
|
|
415
|
+
#
|
|
416
|
+
# @return [Hash] Contains :column and :direction keys
|
|
417
|
+
def parse_agent_sort_params
|
|
418
|
+
column = params[:sort].to_s
|
|
419
|
+
direction = params[:direction].to_s.downcase
|
|
420
|
+
|
|
421
|
+
{
|
|
422
|
+
column: AGENT_SORTABLE_COLUMNS.include?(column) ? column : DEFAULT_AGENT_SORT_COLUMN,
|
|
423
|
+
direction: %w[asc desc].include?(direction) ? direction : DEFAULT_AGENT_SORT_DIRECTION
|
|
424
|
+
}
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# Sorts agents array based on sort params
|
|
428
|
+
#
|
|
429
|
+
# @param agents [Array<Hash>] Array of agent hashes
|
|
430
|
+
# @return [Array<Hash>] Sorted array
|
|
431
|
+
def sort_agents(agents)
|
|
432
|
+
column = @sort_params[:column].to_sym
|
|
433
|
+
direction = @sort_params[:direction]
|
|
434
|
+
|
|
435
|
+
sorted = agents.sort_by do |agent|
|
|
436
|
+
value = agent[column]
|
|
437
|
+
# Handle nil values - put them at the end
|
|
438
|
+
case column
|
|
439
|
+
when :last_executed
|
|
440
|
+
value || Time.at(0)
|
|
441
|
+
when :execution_count, :total_cost, :success_rate
|
|
442
|
+
value || 0
|
|
443
|
+
else
|
|
444
|
+
value.to_s.downcase
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
direction == "desc" ? sorted.reverse : sorted
|
|
449
|
+
end
|
|
407
450
|
end
|
|
408
451
|
end
|
|
409
452
|
end
|
|
@@ -27,32 +27,92 @@ module RubyLLM
|
|
|
27
27
|
@agent_stats = build_agent_comparison(base_scope)
|
|
28
28
|
@top_errors = build_top_errors(base_scope)
|
|
29
29
|
@tenant_budget = load_tenant_budget(base_scope)
|
|
30
|
+
@model_stats = build_model_stats(base_scope)
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
# Returns chart data as JSON for live updates
|
|
33
34
|
#
|
|
34
|
-
# @param range [String] Time range: "today", "7d", or "
|
|
35
|
-
# @
|
|
35
|
+
# @param range [String] Time range: "today", "7d", "30d", "60d", "90d", or custom "YYYY-MM-DD_YYYY-MM-DD"
|
|
36
|
+
# @param compare [String] If "true", include comparison data from previous period
|
|
37
|
+
# @return [JSON] Chart data with series (and optional comparison series)
|
|
36
38
|
def chart_data
|
|
37
39
|
range = params[:range].presence || "today"
|
|
38
|
-
|
|
40
|
+
compare = params[:compare] == "true"
|
|
41
|
+
|
|
42
|
+
if custom_range?(range)
|
|
43
|
+
from_date, to_date = parse_custom_range(range)
|
|
44
|
+
data = tenant_scoped_executions.activity_chart_json_for_dates(from: from_date, to: to_date)
|
|
45
|
+
else
|
|
46
|
+
data = tenant_scoped_executions.activity_chart_json(range: range)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
if compare
|
|
50
|
+
offset_days = range_to_days(range)
|
|
51
|
+
comparison_data = if custom_range?(range)
|
|
52
|
+
from_date, to_date = parse_custom_range(range)
|
|
53
|
+
tenant_scoped_executions.activity_chart_json_for_dates(
|
|
54
|
+
from: from_date - offset_days.days,
|
|
55
|
+
to: to_date - offset_days.days
|
|
56
|
+
)
|
|
57
|
+
else
|
|
58
|
+
tenant_scoped_executions.activity_chart_json(
|
|
59
|
+
range: range,
|
|
60
|
+
offset_days: offset_days
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
data[:comparison] = comparison_data
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
render json: data
|
|
39
67
|
end
|
|
40
68
|
|
|
41
69
|
private
|
|
42
70
|
|
|
43
71
|
# Converts range parameter to number of days
|
|
44
72
|
#
|
|
45
|
-
# @param range [String] Range parameter (today, 7d, 30d)
|
|
73
|
+
# @param range [String] Range parameter (today, 7d, 30d, 60d, 90d, or custom YYYY-MM-DD_YYYY-MM-DD)
|
|
46
74
|
# @return [Integer] Number of days
|
|
47
75
|
def range_to_days(range)
|
|
48
76
|
case range
|
|
49
77
|
when "today" then 1
|
|
50
78
|
when "7d" then 7
|
|
51
79
|
when "30d" then 30
|
|
52
|
-
|
|
80
|
+
when "60d" then 60
|
|
81
|
+
when "90d" then 90
|
|
82
|
+
else
|
|
83
|
+
# Handle custom range format "YYYY-MM-DD_YYYY-MM-DD"
|
|
84
|
+
if range&.include?("_")
|
|
85
|
+
from_str, to_str = range.split("_")
|
|
86
|
+
from_date = Date.parse(from_str) rescue nil
|
|
87
|
+
to_date = Date.parse(to_str) rescue nil
|
|
88
|
+
if from_date && to_date
|
|
89
|
+
(to_date - from_date).to_i + 1
|
|
90
|
+
else
|
|
91
|
+
1
|
|
92
|
+
end
|
|
93
|
+
else
|
|
94
|
+
1
|
|
95
|
+
end
|
|
53
96
|
end
|
|
54
97
|
end
|
|
55
98
|
|
|
99
|
+
# Checks if a range is a custom date range
|
|
100
|
+
#
|
|
101
|
+
# @param range [String] Range parameter
|
|
102
|
+
# @return [Boolean] True if custom date range format
|
|
103
|
+
def custom_range?(range)
|
|
104
|
+
range&.match?(/\A\d{4}-\d{2}-\d{2}_\d{4}-\d{2}-\d{2}\z/)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Parses a custom range string into date objects
|
|
108
|
+
#
|
|
109
|
+
# @param range [String] Custom range in format "YYYY-MM-DD_YYYY-MM-DD"
|
|
110
|
+
# @return [Array<Date>] [from_date, to_date]
|
|
111
|
+
def parse_custom_range(range)
|
|
112
|
+
from_str, to_str = range.split("_")
|
|
113
|
+
[Date.parse(from_str), Date.parse(to_str)]
|
|
114
|
+
end
|
|
115
|
+
|
|
56
116
|
# Builds per-agent comparison statistics for all agent types
|
|
57
117
|
#
|
|
58
118
|
# Creates separate instance variables for each agent type:
|
|
@@ -68,33 +128,35 @@ module RubyLLM
|
|
|
68
128
|
# @return [Array<Hash>] Array of base agent stats (for backward compatibility)
|
|
69
129
|
def build_agent_comparison(base_scope = Execution)
|
|
70
130
|
scope = base_scope.last_n_days(@days)
|
|
71
|
-
agent_types = scope.distinct.pluck(:agent_type)
|
|
72
131
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
132
|
+
# Get ALL agents from registry (file system + execution history)
|
|
133
|
+
all_agent_types = AgentRegistry.all
|
|
134
|
+
|
|
135
|
+
# Batch fetch stats for executed agents (4 queries total)
|
|
136
|
+
execution_stats = batch_fetch_agent_stats(scope)
|
|
78
137
|
|
|
79
|
-
|
|
138
|
+
all_stats = all_agent_types.map do |agent_type|
|
|
80
139
|
agent_class = AgentRegistry.find(agent_type)
|
|
81
140
|
detected_type = AgentRegistry.send(:detect_agent_type, agent_class)
|
|
82
|
-
|
|
83
|
-
# Get workflow type if applicable
|
|
84
141
|
workflow_type = detected_type == "workflow" ? detect_workflow_type(agent_class) : nil
|
|
85
142
|
|
|
143
|
+
# Get stats from batch or use zeros for never-executed agents
|
|
144
|
+
stats = execution_stats[agent_type] || {
|
|
145
|
+
count: 0, total_cost: 0, avg_cost: 0, avg_duration_ms: 0, success_rate: 0
|
|
146
|
+
}
|
|
147
|
+
|
|
86
148
|
{
|
|
87
149
|
agent_type: agent_type,
|
|
88
150
|
detected_type: detected_type,
|
|
89
|
-
executions: count,
|
|
90
|
-
total_cost: total_cost,
|
|
91
|
-
avg_cost:
|
|
92
|
-
avg_duration_ms:
|
|
93
|
-
success_rate:
|
|
151
|
+
executions: stats[:count],
|
|
152
|
+
total_cost: stats[:total_cost],
|
|
153
|
+
avg_cost: stats[:avg_cost],
|
|
154
|
+
avg_duration_ms: stats[:avg_duration_ms],
|
|
155
|
+
success_rate: stats[:success_rate],
|
|
94
156
|
is_workflow: detected_type == "workflow",
|
|
95
157
|
workflow_type: workflow_type
|
|
96
158
|
}
|
|
97
|
-
end.sort_by { |a| -(a[:total_cost] || 0) }
|
|
159
|
+
end.sort_by { |a| [-(a[:executions] || 0), -(a[:total_cost] || 0)] }
|
|
98
160
|
|
|
99
161
|
# Split stats by agent type for 7-tab display
|
|
100
162
|
@agent_stats = all_stats.select { |a| a[:detected_type] == "agent" }
|
|
@@ -127,6 +189,42 @@ module RubyLLM
|
|
|
127
189
|
end
|
|
128
190
|
end
|
|
129
191
|
|
|
192
|
+
# Builds per-model statistics for model comparison and cost breakdown
|
|
193
|
+
#
|
|
194
|
+
# @param base_scope [ActiveRecord::Relation] Base scope to filter from
|
|
195
|
+
# @return [Array<Hash>] Array of model stats sorted by total cost descending
|
|
196
|
+
def build_model_stats(base_scope = Execution)
|
|
197
|
+
scope = base_scope.last_n_days(@days).where.not(model_id: nil)
|
|
198
|
+
|
|
199
|
+
# Batch fetch stats grouped by model
|
|
200
|
+
counts = scope.group(:model_id).count
|
|
201
|
+
costs = scope.group(:model_id).sum(:total_cost)
|
|
202
|
+
tokens = scope.group(:model_id).sum(:total_tokens)
|
|
203
|
+
durations = scope.group(:model_id).average(:duration_ms)
|
|
204
|
+
success_counts = scope.successful.group(:model_id).count
|
|
205
|
+
|
|
206
|
+
total_cost = costs.values.sum
|
|
207
|
+
|
|
208
|
+
model_ids = counts.keys
|
|
209
|
+
model_ids.map do |model_id|
|
|
210
|
+
count = counts[model_id] || 0
|
|
211
|
+
model_cost = costs[model_id] || 0
|
|
212
|
+
model_tokens = tokens[model_id] || 0
|
|
213
|
+
successful = success_counts[model_id] || 0
|
|
214
|
+
|
|
215
|
+
{
|
|
216
|
+
model_id: model_id,
|
|
217
|
+
executions: count,
|
|
218
|
+
total_cost: model_cost,
|
|
219
|
+
total_tokens: model_tokens,
|
|
220
|
+
avg_duration_ms: durations[model_id]&.round || 0,
|
|
221
|
+
success_rate: count > 0 ? (successful.to_f / count * 100).round(1) : 0,
|
|
222
|
+
cost_per_1k_tokens: model_tokens > 0 ? (model_cost / model_tokens * 1000).round(4) : 0,
|
|
223
|
+
cost_percentage: total_cost > 0 ? (model_cost / total_cost * 100).round(1) : 0
|
|
224
|
+
}
|
|
225
|
+
end.sort_by { |m| -(m[:total_cost] || 0) }
|
|
226
|
+
end
|
|
227
|
+
|
|
130
228
|
# Builds top errors list
|
|
131
229
|
#
|
|
132
230
|
# @param base_scope [ActiveRecord::Relation] Base scope to filter from
|
|
@@ -332,6 +430,32 @@ module RubyLLM
|
|
|
332
430
|
|
|
333
431
|
alerts.take(3)
|
|
334
432
|
end
|
|
433
|
+
|
|
434
|
+
# Batch fetches execution stats for all agents in a time period
|
|
435
|
+
#
|
|
436
|
+
# @param scope [ActiveRecord::Relation] Base scope with time filter
|
|
437
|
+
# @return [Hash<String, Hash>] Agent type => stats hash
|
|
438
|
+
def batch_fetch_agent_stats(scope)
|
|
439
|
+
counts = scope.group(:agent_type).count
|
|
440
|
+
costs = scope.group(:agent_type).sum(:total_cost)
|
|
441
|
+
success_counts = scope.successful.group(:agent_type).count
|
|
442
|
+
durations = scope.group(:agent_type).average(:duration_ms)
|
|
443
|
+
|
|
444
|
+
agent_types = (counts.keys + costs.keys).uniq
|
|
445
|
+
agent_types.each_with_object({}) do |agent_type, hash|
|
|
446
|
+
count = counts[agent_type] || 0
|
|
447
|
+
total_cost = costs[agent_type] || 0
|
|
448
|
+
successful = success_counts[agent_type] || 0
|
|
449
|
+
|
|
450
|
+
hash[agent_type] = {
|
|
451
|
+
count: count,
|
|
452
|
+
total_cost: total_cost,
|
|
453
|
+
avg_cost: count > 0 ? (total_cost / count).round(6) : 0,
|
|
454
|
+
avg_duration_ms: durations[agent_type]&.round || 0,
|
|
455
|
+
success_rate: count > 0 ? (successful.to_f / count * 100).round(1) : 0
|
|
456
|
+
}
|
|
457
|
+
end
|
|
458
|
+
end
|
|
335
459
|
end
|
|
336
460
|
end
|
|
337
461
|
end
|
|
@@ -14,6 +14,7 @@ module RubyLLM
|
|
|
14
14
|
class ExecutionsController < ApplicationController
|
|
15
15
|
include Paginatable
|
|
16
16
|
include Filterable
|
|
17
|
+
include Sortable
|
|
17
18
|
|
|
18
19
|
CSV_COLUMNS = %w[id agent_type agent_version status model_id total_tokens total_cost
|
|
19
20
|
duration_ms created_at error_class error_message].freeze
|
|
@@ -206,12 +207,13 @@ module RubyLLM
|
|
|
206
207
|
|
|
207
208
|
# Loads paginated executions and associated statistics
|
|
208
209
|
#
|
|
209
|
-
# Sets @executions, @pagination, and @filter_stats instance variables
|
|
210
|
+
# Sets @executions, @pagination, @sort_params, and @filter_stats instance variables
|
|
210
211
|
# for use in views.
|
|
211
212
|
#
|
|
212
213
|
# @return [void]
|
|
213
214
|
def load_executions_with_stats
|
|
214
|
-
|
|
215
|
+
@sort_params = parse_sort_params
|
|
216
|
+
result = paginate(filtered_executions, sort_params: @sort_params)
|
|
215
217
|
@executions = result[:records]
|
|
216
218
|
@pagination = result[:pagination]
|
|
217
219
|
load_filter_stats
|
|
@@ -238,9 +240,6 @@ module RubyLLM
|
|
|
238
240
|
def filtered_executions
|
|
239
241
|
scope = tenant_scoped_executions
|
|
240
242
|
|
|
241
|
-
# Apply search filter
|
|
242
|
-
scope = scope.search(params[:q]) if params[:q].present?
|
|
243
|
-
|
|
244
243
|
# Apply agent type filter
|
|
245
244
|
agent_types = parse_array_param(:agent_types)
|
|
246
245
|
if agent_types.any?
|
|
@@ -288,6 +287,9 @@ module RubyLLM
|
|
|
288
287
|
# Apply execution type tab filter (agents vs workflows)
|
|
289
288
|
scope = apply_execution_type_filter(scope)
|
|
290
289
|
|
|
290
|
+
# Apply retries filter (show only executions with multiple attempts)
|
|
291
|
+
scope = scope.where("attempts_count > 1") if params[:has_retries].present?
|
|
292
|
+
|
|
291
293
|
# Only show root executions (not workflow children) - children are nested under parents
|
|
292
294
|
scope = scope.where(parent_execution_id: nil)
|
|
293
295
|
|
|
@@ -297,7 +299,7 @@ module RubyLLM
|
|
|
297
299
|
scope
|
|
298
300
|
end
|
|
299
301
|
|
|
300
|
-
# Applies execution type
|
|
302
|
+
# Applies execution type filter (all, agents, workflows, or specific workflow type)
|
|
301
303
|
#
|
|
302
304
|
# @param scope [ActiveRecord::Relation] The current scope
|
|
303
305
|
# @return [ActiveRecord::Relation] Filtered scope
|
|
@@ -310,16 +312,11 @@ module RubyLLM
|
|
|
310
312
|
# Only show executions where workflow_type is null/empty (regular agents)
|
|
311
313
|
scope.where(workflow_type: [nil, ""])
|
|
312
314
|
when "workflows"
|
|
313
|
-
# Only show executions with a workflow_type
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
#
|
|
317
|
-
|
|
318
|
-
if workflow_type_tab.present? && %w[pipeline parallel router].include?(workflow_type_tab)
|
|
319
|
-
workflow_scope = workflow_scope.where(workflow_type: workflow_type_tab)
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
workflow_scope
|
|
315
|
+
# Only show executions with a workflow_type (any workflow)
|
|
316
|
+
scope.where.not(workflow_type: [nil, ""])
|
|
317
|
+
when "pipeline", "parallel", "router"
|
|
318
|
+
# Show specific workflow type
|
|
319
|
+
scope.where(workflow_type: execution_type)
|
|
323
320
|
else
|
|
324
321
|
scope
|
|
325
322
|
end
|