ruby_llm-agents 0.3.3 → 0.3.5
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 +132 -1263
- data/app/controllers/concerns/ruby_llm/agents/filterable.rb +5 -1
- data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +2 -1
- data/app/controllers/ruby_llm/agents/agents_controller.rb +21 -2
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +137 -9
- data/app/controllers/ruby_llm/agents/executions_controller.rb +83 -5
- data/app/models/ruby_llm/agents/execution/analytics.rb +103 -12
- data/app/models/ruby_llm/agents/execution/scopes.rb +25 -0
- data/app/models/ruby_llm/agents/execution/workflow.rb +299 -0
- data/app/models/ruby_llm/agents/execution.rb +28 -59
- data/app/models/ruby_llm/agents/tenant_budget.rb +165 -0
- data/app/services/ruby_llm/agents/agent_registry.rb +118 -7
- data/app/views/layouts/ruby_llm/agents/application.html.erb +430 -0
- data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +23 -0
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +125 -0
- data/app/views/ruby_llm/agents/agents/index.html.erb +93 -0
- data/app/views/ruby_llm/agents/agents/show.html.erb +775 -0
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +112 -0
- data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +75 -0
- data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_execution_item.html.erb +7 -4
- data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +84 -0
- data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +115 -0
- data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +49 -0
- data/app/views/ruby_llm/agents/dashboard/index.html.erb +155 -0
- data/app/views/{rubyllm → ruby_llm}/agents/executions/_execution.html.erb +1 -1
- data/app/views/{rubyllm → ruby_llm}/agents/executions/_filters.html.erb +39 -11
- data/app/views/{rubyllm → ruby_llm}/agents/executions/_list.html.erb +19 -9
- data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +101 -0
- data/app/views/ruby_llm/agents/executions/index.html.erb +88 -0
- data/app/views/{rubyllm → ruby_llm}/agents/executions/show.html.erb +260 -200
- data/app/views/{rubyllm → ruby_llm}/agents/settings/show.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +48 -0
- data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +251 -0
- data/app/views/{rubyllm → ruby_llm}/agents/shared/_filter_dropdown.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +27 -0
- data/app/views/{rubyllm → ruby_llm}/agents/shared/_select_dropdown.html.erb +1 -1
- data/app/views/{rubyllm → ruby_llm}/agents/shared/_status_badge.html.erb +1 -1
- data/app/views/{rubyllm → ruby_llm}/agents/shared/_status_dot.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +26 -0
- data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +61 -0
- data/config/routes.rb +2 -0
- data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +97 -0
- data/lib/generators/ruby_llm_agents/templates/add_attempts_migration.rb.tt +3 -3
- data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +23 -0
- data/lib/generators/ruby_llm_agents/templates/add_tool_calls_migration.rb.tt +2 -2
- data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +38 -0
- data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +45 -0
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +17 -5
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +13 -0
- data/lib/ruby_llm/agents/alert_manager.rb +20 -16
- data/lib/ruby_llm/agents/base/caching.rb +40 -0
- data/lib/ruby_llm/agents/base/cost_calculation.rb +105 -0
- data/lib/ruby_llm/agents/base/dsl.rb +261 -0
- data/lib/ruby_llm/agents/base/execution.rb +258 -0
- data/lib/ruby_llm/agents/base/reliability_execution.rb +136 -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 +37 -801
- data/lib/ruby_llm/agents/budget_tracker.rb +250 -139
- data/lib/ruby_llm/agents/cache_helper.rb +98 -0
- data/lib/ruby_llm/agents/circuit_breaker.rb +48 -30
- data/lib/ruby_llm/agents/configuration.rb +40 -1
- data/lib/ruby_llm/agents/engine.rb +65 -1
- data/lib/ruby_llm/agents/inflections.rb +14 -0
- data/lib/ruby_llm/agents/instrumentation.rb +66 -0
- data/lib/ruby_llm/agents/reliability.rb +8 -2
- data/lib/ruby_llm/agents/version.rb +1 -1
- data/lib/ruby_llm/agents/workflow/instrumentation.rb +254 -0
- data/lib/ruby_llm/agents/workflow/parallel.rb +282 -0
- data/lib/ruby_llm/agents/workflow/pipeline.rb +306 -0
- data/lib/ruby_llm/agents/workflow/result.rb +390 -0
- data/lib/ruby_llm/agents/workflow/router.rb +429 -0
- data/lib/ruby_llm/agents/workflow.rb +232 -0
- data/lib/ruby_llm/agents.rb +1 -0
- metadata +57 -75
- 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/layouts/rubyllm/agents/application.html.erb +0 -626
- data/app/views/rubyllm/agents/agents/index.html.erb +0 -20
- data/app/views/rubyllm/agents/agents/show.html.erb +0 -772
- data/app/views/rubyllm/agents/dashboard/_budgets_bar.html.erb +0 -165
- data/app/views/rubyllm/agents/dashboard/_now_strip.html.erb +0 -10
- data/app/views/rubyllm/agents/dashboard/_now_strip_values.html.erb +0 -71
- data/app/views/rubyllm/agents/dashboard/index.html.erb +0 -197
- data/app/views/rubyllm/agents/executions/index.html.erb +0 -28
- data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +0 -18
- data/app/views/rubyllm/agents/shared/_executions_table.html.erb +0 -193
- /data/app/views/{rubyllm → ruby_llm}/agents/agents/_agent.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/agents/_version_comparison.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_action_center.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_alerts_feed.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_breaker_strip.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/executions/dry_run.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/shared/_stat_card.html.erb +0 -0
|
@@ -74,7 +74,11 @@ module RubyLLM
|
|
|
74
74
|
# @param days [Integer, nil] Number of days to filter by
|
|
75
75
|
# @return [ActiveRecord::Relation] Filtered scope
|
|
76
76
|
def apply_time_filter(scope, days)
|
|
77
|
-
days.present? && days.positive?
|
|
77
|
+
return scope unless days.present? && days.positive?
|
|
78
|
+
|
|
79
|
+
# Qualify column name to avoid ambiguity when joins are present
|
|
80
|
+
table_name = scope.model.table_name
|
|
81
|
+
scope.where("#{table_name}.created_at >= ?", days.days.ago)
|
|
78
82
|
end
|
|
79
83
|
end
|
|
80
84
|
end
|
|
@@ -33,7 +33,8 @@ module RubyLLM
|
|
|
33
33
|
per_page = RubyLLM::Agents.configuration.per_page
|
|
34
34
|
offset = (page - 1) * per_page
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
# Qualify column name to avoid ambiguity when joins are present
|
|
37
|
+
scope = scope.order("#{scope.model.table_name}.created_at DESC") if ordered
|
|
37
38
|
total_count = scope.count
|
|
38
39
|
|
|
39
40
|
{
|
|
@@ -20,14 +20,33 @@ module RubyLLM
|
|
|
20
20
|
#
|
|
21
21
|
# Uses AgentRegistry to discover agents from both file system
|
|
22
22
|
# and execution history, ensuring deleted agents with history
|
|
23
|
-
# are still visible.
|
|
23
|
+
# are still visible. Separates agents and workflows for tabbed display.
|
|
24
24
|
#
|
|
25
25
|
# @return [void]
|
|
26
26
|
def index
|
|
27
|
-
|
|
27
|
+
all_items = AgentRegistry.all_with_details
|
|
28
|
+
|
|
29
|
+
# Separate agents and workflows
|
|
30
|
+
@agents = all_items.reject { |a| a[:is_workflow] }
|
|
31
|
+
@workflows = all_items.select { |a| a[:is_workflow] }
|
|
32
|
+
|
|
33
|
+
# Group workflows by type for sub-tabs
|
|
34
|
+
@workflows_by_type = {
|
|
35
|
+
pipeline: @workflows.select { |w| w[:workflow_type] == "pipeline" },
|
|
36
|
+
parallel: @workflows.select { |w| w[:workflow_type] == "parallel" },
|
|
37
|
+
router: @workflows.select { |w| w[:workflow_type] == "router" }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Counts for tab badges
|
|
41
|
+
@agent_count = @agents.size
|
|
42
|
+
@workflow_count = @workflows.size
|
|
28
43
|
rescue StandardError => e
|
|
29
44
|
Rails.logger.error("[RubyLLM::Agents] Error loading agents: #{e.message}")
|
|
30
45
|
@agents = []
|
|
46
|
+
@workflows = []
|
|
47
|
+
@workflows_by_type = { pipeline: [], parallel: [], router: [] }
|
|
48
|
+
@agent_count = 0
|
|
49
|
+
@workflow_count = 0
|
|
31
50
|
flash.now[:alert] = "Error loading agents list"
|
|
32
51
|
end
|
|
33
52
|
|
|
@@ -14,25 +14,121 @@ 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
|
-
@
|
|
22
|
-
@
|
|
23
|
-
|
|
24
|
-
@
|
|
21
|
+
@selected_range = params[:range].presence || "today"
|
|
22
|
+
@days = range_to_days(@selected_range)
|
|
23
|
+
base_scope = tenant_scoped_executions
|
|
24
|
+
@now_strip = base_scope.now_strip_data(range: @selected_range)
|
|
25
|
+
@critical_alerts = load_critical_alerts(base_scope)
|
|
26
|
+
@recent_executions = base_scope.recent(10)
|
|
27
|
+
@agent_stats = build_agent_comparison(base_scope)
|
|
28
|
+
@top_errors = build_top_errors(base_scope)
|
|
29
|
+
@tenant_budget = load_tenant_budget(base_scope)
|
|
25
30
|
end
|
|
26
31
|
|
|
27
32
|
# Returns chart data as JSON for live updates
|
|
28
33
|
#
|
|
29
|
-
# @
|
|
34
|
+
# @param range [String] Time range: "today", "7d", or "30d"
|
|
35
|
+
# @return [JSON] Chart data with series
|
|
30
36
|
def chart_data
|
|
31
|
-
|
|
37
|
+
range = params[:range].presence || "today"
|
|
38
|
+
render json: tenant_scoped_executions.activity_chart_json(range: range)
|
|
32
39
|
end
|
|
33
40
|
|
|
34
41
|
private
|
|
35
42
|
|
|
43
|
+
# Converts range parameter to number of days
|
|
44
|
+
#
|
|
45
|
+
# @param range [String] Range parameter (today, 7d, 30d)
|
|
46
|
+
# @return [Integer] Number of days
|
|
47
|
+
def range_to_days(range)
|
|
48
|
+
case range
|
|
49
|
+
when "today" then 1
|
|
50
|
+
when "7d" then 7
|
|
51
|
+
when "30d" then 30
|
|
52
|
+
else 1
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Builds per-agent comparison statistics
|
|
57
|
+
#
|
|
58
|
+
# @param base_scope [ActiveRecord::Relation] Base scope to filter from
|
|
59
|
+
# @return [Array<Hash>] Array of all stats sorted by cost descending
|
|
60
|
+
def build_agent_comparison(base_scope = Execution)
|
|
61
|
+
scope = base_scope.last_n_days(@days)
|
|
62
|
+
agent_types = scope.distinct.pluck(:agent_type)
|
|
63
|
+
|
|
64
|
+
all_stats = agent_types.map do |agent_type|
|
|
65
|
+
agent_scope = scope.where(agent_type: agent_type)
|
|
66
|
+
count = agent_scope.count
|
|
67
|
+
total_cost = agent_scope.sum(:total_cost) || 0
|
|
68
|
+
successful = agent_scope.successful.count
|
|
69
|
+
|
|
70
|
+
# Detect if this is a workflow
|
|
71
|
+
agent_class = AgentRegistry.find(agent_type)
|
|
72
|
+
is_workflow = agent_class&.ancestors&.any? { |a| a.name&.include?("Workflow") }
|
|
73
|
+
workflow_type = is_workflow ? detect_workflow_type(agent_class) : nil
|
|
74
|
+
|
|
75
|
+
{
|
|
76
|
+
agent_type: agent_type,
|
|
77
|
+
executions: count,
|
|
78
|
+
total_cost: total_cost,
|
|
79
|
+
avg_cost: count > 0 ? (total_cost / count).round(6) : 0,
|
|
80
|
+
avg_duration_ms: agent_scope.average(:duration_ms)&.round || 0,
|
|
81
|
+
success_rate: count > 0 ? (successful.to_f / count * 100).round(1) : 0,
|
|
82
|
+
is_workflow: is_workflow,
|
|
83
|
+
workflow_type: workflow_type
|
|
84
|
+
}
|
|
85
|
+
end.sort_by { |a| -(a[:total_cost] || 0) }
|
|
86
|
+
|
|
87
|
+
# Split into agents and workflows for tabbed display
|
|
88
|
+
@workflow_stats = all_stats.select { |a| a[:is_workflow] }
|
|
89
|
+
all_stats.reject { |a| a[:is_workflow] }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Detects workflow type from class hierarchy
|
|
93
|
+
#
|
|
94
|
+
# @param agent_class [Class] The agent class
|
|
95
|
+
# @return [String, nil] "pipeline", "parallel", "router", or nil
|
|
96
|
+
def detect_workflow_type(agent_class)
|
|
97
|
+
return nil unless agent_class
|
|
98
|
+
|
|
99
|
+
ancestors = agent_class.ancestors.map { |a| a.name.to_s }
|
|
100
|
+
|
|
101
|
+
if ancestors.include?("RubyLLM::Agents::Workflow::Pipeline")
|
|
102
|
+
"pipeline"
|
|
103
|
+
elsif ancestors.include?("RubyLLM::Agents::Workflow::Parallel")
|
|
104
|
+
"parallel"
|
|
105
|
+
elsif ancestors.include?("RubyLLM::Agents::Workflow::Router")
|
|
106
|
+
"router"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Builds top errors list
|
|
111
|
+
#
|
|
112
|
+
# @param base_scope [ActiveRecord::Relation] Base scope to filter from
|
|
113
|
+
# @return [Array<Hash>] Top 5 error classes with counts
|
|
114
|
+
def build_top_errors(base_scope = Execution)
|
|
115
|
+
scope = base_scope.last_n_days(@days).where(status: "error")
|
|
116
|
+
total_errors = scope.count
|
|
117
|
+
|
|
118
|
+
scope.group(:error_class)
|
|
119
|
+
.select("error_class, COUNT(*) as count, MAX(created_at) as last_seen")
|
|
120
|
+
.order("count DESC")
|
|
121
|
+
.limit(5)
|
|
122
|
+
.map do |row|
|
|
123
|
+
{
|
|
124
|
+
error_class: row.error_class || "Unknown Error",
|
|
125
|
+
count: row.count,
|
|
126
|
+
percentage: total_errors > 0 ? (row.count.to_f / total_errors * 100).round(1) : 0,
|
|
127
|
+
last_seen: row.last_seen
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
36
132
|
# Fetches cached daily statistics for the dashboard
|
|
37
133
|
#
|
|
38
134
|
# Results are cached for 1 minute to reduce database load while
|
|
@@ -123,6 +219,37 @@ module RubyLLM
|
|
|
123
219
|
[]
|
|
124
220
|
end
|
|
125
221
|
|
|
222
|
+
# Loads tenant budget info for the current tenant
|
|
223
|
+
#
|
|
224
|
+
# @param base_scope [ActiveRecord::Relation] Base scope for usage calculation
|
|
225
|
+
# @return [Hash, nil] Tenant budget data with usage info, or nil if not applicable
|
|
226
|
+
def load_tenant_budget(base_scope)
|
|
227
|
+
return nil unless tenant_filter_enabled? && current_tenant_id.present?
|
|
228
|
+
return nil unless TenantBudget.table_exists?
|
|
229
|
+
|
|
230
|
+
budget = TenantBudget.for_tenant(current_tenant_id)
|
|
231
|
+
return nil unless budget
|
|
232
|
+
|
|
233
|
+
# Calculate current usage
|
|
234
|
+
today_scope = base_scope.where("created_at >= ?", Time.current.beginning_of_day)
|
|
235
|
+
month_scope = base_scope.where("created_at >= ?", Time.current.beginning_of_month)
|
|
236
|
+
|
|
237
|
+
daily_spend = today_scope.sum(:total_cost) || 0
|
|
238
|
+
monthly_spend = month_scope.sum(:total_cost) || 0
|
|
239
|
+
|
|
240
|
+
{
|
|
241
|
+
tenant_id: current_tenant_id,
|
|
242
|
+
daily_limit: budget.effective_daily_limit,
|
|
243
|
+
monthly_limit: budget.effective_monthly_limit,
|
|
244
|
+
daily_spend: daily_spend,
|
|
245
|
+
monthly_spend: monthly_spend,
|
|
246
|
+
daily_percentage: budget.effective_daily_limit.to_f > 0 ? (daily_spend / budget.effective_daily_limit * 100).round(1) : 0,
|
|
247
|
+
monthly_percentage: budget.effective_monthly_limit.to_f > 0 ? (monthly_spend / budget.effective_monthly_limit * 100).round(1) : 0,
|
|
248
|
+
enforcement: budget.effective_enforcement,
|
|
249
|
+
per_agent_daily: budget.per_agent_daily || {}
|
|
250
|
+
}
|
|
251
|
+
end
|
|
252
|
+
|
|
126
253
|
# Loads recent alerts from cache
|
|
127
254
|
#
|
|
128
255
|
# @return [Array<Hash>] Array of recent alert events
|
|
@@ -140,8 +267,9 @@ module RubyLLM
|
|
|
140
267
|
# Combines open circuit breakers, budget breaches, and error spikes
|
|
141
268
|
# into a single prioritized list (max 3 items).
|
|
142
269
|
#
|
|
270
|
+
# @param base_scope [ActiveRecord::Relation] Base scope to filter from
|
|
143
271
|
# @return [Array<Hash>] Critical alerts with type and data
|
|
144
|
-
def load_critical_alerts
|
|
272
|
+
def load_critical_alerts(base_scope = Execution)
|
|
145
273
|
alerts = []
|
|
146
274
|
|
|
147
275
|
# Open circuit breakers
|
|
@@ -177,7 +305,7 @@ module RubyLLM
|
|
|
177
305
|
end
|
|
178
306
|
|
|
179
307
|
# Error spike detection (>5 errors in last 15 minutes)
|
|
180
|
-
error_count_15m =
|
|
308
|
+
error_count_15m = base_scope.status_error.where("created_at > ?", 15.minutes.ago).count
|
|
181
309
|
if error_count_15m >= 5
|
|
182
310
|
alerts << { type: :error_spike, data: { count: error_count_15m } }
|
|
183
311
|
end
|
|
@@ -155,32 +155,53 @@ module RubyLLM
|
|
|
155
155
|
# Loads available options for filter dropdowns
|
|
156
156
|
#
|
|
157
157
|
# Populates @agent_types with all agent types that have executions,
|
|
158
|
-
# @model_ids with all distinct models used,
|
|
159
|
-
# possible status values.
|
|
158
|
+
# @model_ids with all distinct models used, @workflow_types with
|
|
159
|
+
# workflow patterns used, and @statuses with all possible status values.
|
|
160
160
|
#
|
|
161
161
|
# @return [void]
|
|
162
162
|
def load_filter_options
|
|
163
163
|
@agent_types = available_agent_types
|
|
164
164
|
@model_ids = available_model_ids
|
|
165
|
+
@workflow_types = available_workflow_types
|
|
165
166
|
@statuses = Execution.statuses.keys
|
|
166
167
|
end
|
|
167
168
|
|
|
168
169
|
# Returns distinct agent types from execution history
|
|
169
170
|
#
|
|
170
171
|
# Memoized to avoid duplicate queries within a request.
|
|
172
|
+
# Uses tenant_scoped_executions to respect multi-tenancy filtering.
|
|
171
173
|
#
|
|
172
174
|
# @return [Array<String>] Agent type names
|
|
173
175
|
def available_agent_types
|
|
174
|
-
@available_agent_types ||=
|
|
176
|
+
@available_agent_types ||= tenant_scoped_executions.distinct.pluck(:agent_type)
|
|
175
177
|
end
|
|
176
178
|
|
|
177
179
|
# Returns distinct model IDs from execution history
|
|
178
180
|
#
|
|
179
181
|
# Memoized to avoid duplicate queries within a request.
|
|
182
|
+
# Uses tenant_scoped_executions to respect multi-tenancy filtering.
|
|
180
183
|
#
|
|
181
184
|
# @return [Array<String>] Model IDs
|
|
182
185
|
def available_model_ids
|
|
183
|
-
@available_model_ids ||=
|
|
186
|
+
@available_model_ids ||= tenant_scoped_executions.where.not(model_id: nil).distinct.pluck(:model_id).sort
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Returns distinct workflow types from execution history
|
|
190
|
+
#
|
|
191
|
+
# Memoized to avoid duplicate queries within a request.
|
|
192
|
+
# Returns empty array if workflow_type column doesn't exist yet.
|
|
193
|
+
# Uses tenant_scoped_executions to respect multi-tenancy filtering.
|
|
194
|
+
#
|
|
195
|
+
# @return [Array<String>] Workflow types (pipeline, parallel, router)
|
|
196
|
+
def available_workflow_types
|
|
197
|
+
return @available_workflow_types if defined?(@available_workflow_types)
|
|
198
|
+
|
|
199
|
+
@available_workflow_types = if Execution.column_names.include?("workflow_type")
|
|
200
|
+
tenant_scoped_executions.where.not(workflow_type: [nil, ""])
|
|
201
|
+
.distinct.pluck(:workflow_type).sort
|
|
202
|
+
else
|
|
203
|
+
[]
|
|
204
|
+
end
|
|
184
205
|
end
|
|
185
206
|
|
|
186
207
|
# Loads paginated executions and associated statistics
|
|
@@ -215,7 +236,7 @@ module RubyLLM
|
|
|
215
236
|
#
|
|
216
237
|
# @return [ActiveRecord::Relation] Filtered execution scope
|
|
217
238
|
def filtered_executions
|
|
218
|
-
scope =
|
|
239
|
+
scope = tenant_scoped_executions
|
|
219
240
|
|
|
220
241
|
# Apply search filter
|
|
221
242
|
scope = scope.search(params[:q]) if params[:q].present?
|
|
@@ -244,8 +265,65 @@ module RubyLLM
|
|
|
244
265
|
model_ids = parse_array_param(:model_ids)
|
|
245
266
|
scope = scope.where(model_id: model_ids) if model_ids.any?
|
|
246
267
|
|
|
268
|
+
# Apply workflow type filter (only if column exists)
|
|
269
|
+
if Execution.column_names.include?("workflow_type")
|
|
270
|
+
workflow_types = parse_array_param(:workflow_types)
|
|
271
|
+
if workflow_types.any?
|
|
272
|
+
includes_single = workflow_types.include?("single")
|
|
273
|
+
other_types = workflow_types - ["single"]
|
|
274
|
+
|
|
275
|
+
if includes_single && other_types.any?
|
|
276
|
+
# Include both single (null workflow_type) and specific workflow types
|
|
277
|
+
scope = scope.where(workflow_type: [nil, ""] + other_types)
|
|
278
|
+
elsif includes_single
|
|
279
|
+
# Only single executions (non-workflow)
|
|
280
|
+
scope = scope.where(workflow_type: [nil, ""])
|
|
281
|
+
else
|
|
282
|
+
# Only specific workflow types
|
|
283
|
+
scope = scope.where(workflow_type: workflow_types)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Apply execution type tab filter (agents vs workflows)
|
|
289
|
+
scope = apply_execution_type_filter(scope)
|
|
290
|
+
|
|
291
|
+
# Only show root executions (not workflow children) - children are nested under parents
|
|
292
|
+
scope = scope.where(parent_execution_id: nil)
|
|
293
|
+
|
|
294
|
+
# Eager load children for workflow grouping
|
|
295
|
+
scope = scope.includes(:child_executions)
|
|
296
|
+
|
|
247
297
|
scope
|
|
248
298
|
end
|
|
299
|
+
|
|
300
|
+
# Applies execution type tab filter (all, agents, workflows)
|
|
301
|
+
#
|
|
302
|
+
# @param scope [ActiveRecord::Relation] The current scope
|
|
303
|
+
# @return [ActiveRecord::Relation] Filtered scope
|
|
304
|
+
def apply_execution_type_filter(scope)
|
|
305
|
+
return scope unless Execution.column_names.include?("workflow_type")
|
|
306
|
+
|
|
307
|
+
execution_type = params[:execution_type]
|
|
308
|
+
case execution_type
|
|
309
|
+
when "agents"
|
|
310
|
+
# Only show executions where workflow_type is null/empty (regular agents)
|
|
311
|
+
scope.where(workflow_type: [nil, ""])
|
|
312
|
+
when "workflows"
|
|
313
|
+
# Only show executions with a workflow_type
|
|
314
|
+
workflow_scope = scope.where.not(workflow_type: [nil, ""])
|
|
315
|
+
|
|
316
|
+
# Apply workflow type sub-filter if specified
|
|
317
|
+
workflow_type_tab = params[:workflow_type_tab]
|
|
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
|
|
323
|
+
else
|
|
324
|
+
scope
|
|
325
|
+
end
|
|
326
|
+
end
|
|
249
327
|
end
|
|
250
328
|
end
|
|
251
329
|
end
|
|
@@ -167,35 +167,126 @@ 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
|
-
|
|
186
|
+
activity_chart_json(range: "today")
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
private
|
|
190
|
+
|
|
191
|
+
# Builds hourly chart data for last 24 hours
|
|
192
|
+
# Optimized: Single GROUP BY query instead of 72 individual queries
|
|
193
|
+
# Database-agnostic: works with both PostgreSQL and SQLite
|
|
194
|
+
def build_hourly_chart_data
|
|
173
195
|
reference_time = Time.current.beginning_of_hour
|
|
196
|
+
start_time = reference_time - 23.hours
|
|
174
197
|
|
|
175
|
-
|
|
198
|
+
# Use database-agnostic aggregation with Ruby post-processing
|
|
199
|
+
results = where(created_at: start_time..(reference_time + 1.hour))
|
|
200
|
+
.select(:status, :total_cost, :created_at)
|
|
201
|
+
.group_by { |r| r.created_at.beginning_of_hour }
|
|
202
|
+
|
|
203
|
+
# Build arrays for all 24 hours (fill missing with zeros)
|
|
176
204
|
success_data = []
|
|
177
205
|
failed_data = []
|
|
206
|
+
cost_data = []
|
|
207
|
+
total_success = 0
|
|
208
|
+
total_failed = 0
|
|
209
|
+
total_cost = 0.0
|
|
178
210
|
|
|
179
|
-
# Create entries for the last 24 hours ending at current hour
|
|
180
211
|
(23.downto(0)).each do |hours_ago|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
categories << start_time.in_time_zone.strftime("%H:%M")
|
|
212
|
+
bucket_time = (reference_time - hours_ago.hours).beginning_of_hour
|
|
213
|
+
rows = results[bucket_time] || []
|
|
184
214
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
215
|
+
s = rows.count { |r| r.status == "success" }
|
|
216
|
+
f = rows.count { |r| r.status.in?(%w[error timeout]) }
|
|
217
|
+
c = rows.sum { |r| r.total_cost.to_f }
|
|
218
|
+
|
|
219
|
+
success_data << s
|
|
220
|
+
failed_data << f
|
|
221
|
+
cost_data << c.round(4)
|
|
222
|
+
|
|
223
|
+
total_success += s
|
|
224
|
+
total_failed += f
|
|
225
|
+
total_cost += c
|
|
188
226
|
end
|
|
189
227
|
|
|
190
228
|
{
|
|
191
|
-
|
|
229
|
+
range: "today",
|
|
230
|
+
totals: { success: total_success, failed: total_failed, cost: total_cost.round(4) },
|
|
192
231
|
series: [
|
|
193
232
|
{ name: "Success", data: success_data },
|
|
194
|
-
{ name: "Failed", data: failed_data }
|
|
233
|
+
{ name: "Failed", data: failed_data },
|
|
234
|
+
{ name: "Cost", data: cost_data }
|
|
195
235
|
]
|
|
196
236
|
}
|
|
197
237
|
end
|
|
198
238
|
|
|
239
|
+
# Builds daily chart data for specified number of days
|
|
240
|
+
# Optimized: Single query instead of 3*days individual queries
|
|
241
|
+
# Database-agnostic: works with both PostgreSQL and SQLite
|
|
242
|
+
def build_daily_chart_data(days)
|
|
243
|
+
end_date = Date.current
|
|
244
|
+
start_date = (days - 1).days.ago.to_date
|
|
245
|
+
|
|
246
|
+
# Use database-agnostic aggregation with Ruby post-processing
|
|
247
|
+
results = where(created_at: start_date.beginning_of_day..end_date.end_of_day)
|
|
248
|
+
.select(:status, :total_cost, :created_at)
|
|
249
|
+
.group_by { |r| r.created_at.to_date }
|
|
250
|
+
|
|
251
|
+
# Build arrays for all days (fill missing with zeros)
|
|
252
|
+
success_data = []
|
|
253
|
+
failed_data = []
|
|
254
|
+
cost_data = []
|
|
255
|
+
total_success = 0
|
|
256
|
+
total_failed = 0
|
|
257
|
+
total_cost = 0.0
|
|
258
|
+
|
|
259
|
+
(days - 1).downto(0).each do |days_ago|
|
|
260
|
+
date = days_ago.days.ago.to_date
|
|
261
|
+
rows = results[date] || []
|
|
262
|
+
|
|
263
|
+
s = rows.count { |r| r.status == "success" }
|
|
264
|
+
f = rows.count { |r| r.status.in?(%w[error timeout]) }
|
|
265
|
+
c = rows.sum { |r| r.total_cost.to_f }
|
|
266
|
+
|
|
267
|
+
success_data << s
|
|
268
|
+
failed_data << f
|
|
269
|
+
cost_data << c.round(4)
|
|
270
|
+
|
|
271
|
+
total_success += s
|
|
272
|
+
total_failed += f
|
|
273
|
+
total_cost += c
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
{
|
|
277
|
+
range: "#{days}d",
|
|
278
|
+
days: days,
|
|
279
|
+
totals: { success: total_success, failed: total_failed, cost: total_cost.round(4) },
|
|
280
|
+
series: [
|
|
281
|
+
{ name: "Success", data: success_data },
|
|
282
|
+
{ name: "Failed", data: failed_data },
|
|
283
|
+
{ name: "Cost", data: cost_data }
|
|
284
|
+
]
|
|
285
|
+
}
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
public
|
|
289
|
+
|
|
199
290
|
# Builds the hourly activity data structure
|
|
200
291
|
# Shows the last 24 hours with current hour on the right
|
|
201
292
|
#
|
|
@@ -47,6 +47,31 @@ module RubyLLM
|
|
|
47
47
|
|
|
48
48
|
# @!endgroup
|
|
49
49
|
|
|
50
|
+
# @!group Tenant Scopes
|
|
51
|
+
|
|
52
|
+
# @!method by_tenant(tenant_id)
|
|
53
|
+
# Filters to a specific tenant
|
|
54
|
+
# @param tenant_id [String] The tenant identifier
|
|
55
|
+
# @return [ActiveRecord::Relation]
|
|
56
|
+
|
|
57
|
+
# @!method for_current_tenant
|
|
58
|
+
# Filters to the current tenant from the resolver
|
|
59
|
+
# @return [ActiveRecord::Relation]
|
|
60
|
+
scope :by_tenant, ->(tenant_id) { where(tenant_id: tenant_id) }
|
|
61
|
+
scope :without_tenant, -> { where(tenant_id: nil) }
|
|
62
|
+
scope :with_tenant, -> { where.not(tenant_id: nil) }
|
|
63
|
+
scope :for_current_tenant, -> {
|
|
64
|
+
config = RubyLLM::Agents.configuration
|
|
65
|
+
if config.multi_tenancy_enabled?
|
|
66
|
+
tenant_id = config.tenant_resolver&.call
|
|
67
|
+
tenant_id ? where(tenant_id: tenant_id) : all
|
|
68
|
+
else
|
|
69
|
+
all
|
|
70
|
+
end
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# @!endgroup
|
|
74
|
+
|
|
50
75
|
# @!group Agent-based Scopes
|
|
51
76
|
|
|
52
77
|
# @!method by_agent(agent_type)
|