ruby_llm-agents 0.3.4 → 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 +80 -16
- data/app/controllers/ruby_llm/agents/executions_controller.rb +83 -5
- data/app/models/ruby_llm/agents/execution/analytics.rb +17 -27
- 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 +9 -1
- 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/{rubyllm → ruby_llm}/agents/application.html.erb +91 -29
- 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/{rubyllm → ruby_llm}/agents/agents/show.html.erb +77 -20
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +112 -0
- data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_execution_item.html.erb +7 -4
- data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +115 -0
- data/app/views/{rubyllm → ruby_llm}/agents/dashboard/index.html.erb +9 -6
- 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 +137 -126
- data/app/views/{rubyllm → ruby_llm}/agents/shared/_breadcrumbs.html.erb +2 -2
- 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/{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/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 +4 -7
- data/lib/ruby_llm/agents/base/cost_calculation.rb +5 -3
- data/lib/ruby_llm/agents/base/execution.rb +61 -9
- data/lib/ruby_llm/agents/base/reliability_execution.rb +14 -9
- data/lib/ruby_llm/agents/base.rb +26 -0
- 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 +50 -60
- data/app/views/rubyllm/agents/agents/index.html.erb +0 -20
- data/app/views/rubyllm/agents/dashboard/_agent_comparison.html.erb +0 -46
- 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/dashboard/_budgets_bar.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_now_strip.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_top_errors.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/settings/show.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/shared/_nav_link.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
|
|
|
@@ -20,12 +20,13 @@ module RubyLLM
|
|
|
20
20
|
def index
|
|
21
21
|
@selected_range = params[:range].presence || "today"
|
|
22
22
|
@days = range_to_days(@selected_range)
|
|
23
|
-
|
|
24
|
-
@
|
|
25
|
-
@
|
|
26
|
-
@recent_executions =
|
|
27
|
-
@agent_stats = build_agent_comparison
|
|
28
|
-
@top_errors = build_top_errors
|
|
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)
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
# Returns chart data as JSON for live updates
|
|
@@ -34,7 +35,7 @@ module RubyLLM
|
|
|
34
35
|
# @return [JSON] Chart data with series
|
|
35
36
|
def chart_data
|
|
36
37
|
range = params[:range].presence || "today"
|
|
37
|
-
render json:
|
|
38
|
+
render json: tenant_scoped_executions.activity_chart_json(range: range)
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
private
|
|
@@ -54,33 +55,64 @@ module RubyLLM
|
|
|
54
55
|
|
|
55
56
|
# Builds per-agent comparison statistics
|
|
56
57
|
#
|
|
57
|
-
# @
|
|
58
|
-
|
|
59
|
-
|
|
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)
|
|
60
62
|
agent_types = scope.distinct.pluck(:agent_type)
|
|
61
63
|
|
|
62
|
-
agent_types.map do |agent_type|
|
|
64
|
+
all_stats = agent_types.map do |agent_type|
|
|
63
65
|
agent_scope = scope.where(agent_type: agent_type)
|
|
64
66
|
count = agent_scope.count
|
|
65
67
|
total_cost = agent_scope.sum(:total_cost) || 0
|
|
66
68
|
successful = agent_scope.successful.count
|
|
67
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
|
+
|
|
68
75
|
{
|
|
69
76
|
agent_type: agent_type,
|
|
70
77
|
executions: count,
|
|
71
78
|
total_cost: total_cost,
|
|
72
79
|
avg_cost: count > 0 ? (total_cost / count).round(6) : 0,
|
|
73
80
|
avg_duration_ms: agent_scope.average(:duration_ms)&.round || 0,
|
|
74
|
-
success_rate: count > 0 ? (successful.to_f / count * 100).round(1) : 0
|
|
81
|
+
success_rate: count > 0 ? (successful.to_f / count * 100).round(1) : 0,
|
|
82
|
+
is_workflow: is_workflow,
|
|
83
|
+
workflow_type: workflow_type
|
|
75
84
|
}
|
|
76
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
|
|
77
108
|
end
|
|
78
109
|
|
|
79
110
|
# Builds top errors list
|
|
80
111
|
#
|
|
112
|
+
# @param base_scope [ActiveRecord::Relation] Base scope to filter from
|
|
81
113
|
# @return [Array<Hash>] Top 5 error classes with counts
|
|
82
|
-
def build_top_errors
|
|
83
|
-
scope =
|
|
114
|
+
def build_top_errors(base_scope = Execution)
|
|
115
|
+
scope = base_scope.last_n_days(@days).where(status: "error")
|
|
84
116
|
total_errors = scope.count
|
|
85
117
|
|
|
86
118
|
scope.group(:error_class)
|
|
@@ -187,6 +219,37 @@ module RubyLLM
|
|
|
187
219
|
[]
|
|
188
220
|
end
|
|
189
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
|
+
|
|
190
253
|
# Loads recent alerts from cache
|
|
191
254
|
#
|
|
192
255
|
# @return [Array<Hash>] Array of recent alert events
|
|
@@ -204,8 +267,9 @@ module RubyLLM
|
|
|
204
267
|
# Combines open circuit breakers, budget breaches, and error spikes
|
|
205
268
|
# into a single prioritized list (max 3 items).
|
|
206
269
|
#
|
|
270
|
+
# @param base_scope [ActiveRecord::Relation] Base scope to filter from
|
|
207
271
|
# @return [Array<Hash>] Critical alerts with type and data
|
|
208
|
-
def load_critical_alerts
|
|
272
|
+
def load_critical_alerts(base_scope = Execution)
|
|
209
273
|
alerts = []
|
|
210
274
|
|
|
211
275
|
# Open circuit breakers
|
|
@@ -241,7 +305,7 @@ module RubyLLM
|
|
|
241
305
|
end
|
|
242
306
|
|
|
243
307
|
# Error spike detection (>5 errors in last 15 minutes)
|
|
244
|
-
error_count_15m =
|
|
308
|
+
error_count_15m = base_scope.status_error.where("created_at > ?", 15.minutes.ago).count
|
|
245
309
|
if error_count_15m >= 5
|
|
246
310
|
alerts << { type: :error_spike, data: { count: error_count_15m } }
|
|
247
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
|
|
@@ -190,20 +190,15 @@ module RubyLLM
|
|
|
190
190
|
|
|
191
191
|
# Builds hourly chart data for last 24 hours
|
|
192
192
|
# Optimized: Single GROUP BY query instead of 72 individual queries
|
|
193
|
+
# Database-agnostic: works with both PostgreSQL and SQLite
|
|
193
194
|
def build_hourly_chart_data
|
|
194
195
|
reference_time = Time.current.beginning_of_hour
|
|
195
196
|
start_time = reference_time - 23.hours
|
|
196
197
|
|
|
197
|
-
#
|
|
198
|
+
# Use database-agnostic aggregation with Ruby post-processing
|
|
198
199
|
results = where(created_at: start_time..(reference_time + 1.hour))
|
|
199
|
-
.
|
|
200
|
-
.
|
|
201
|
-
Arel.sql("DATE_TRUNC('hour', created_at) as time_bucket"),
|
|
202
|
-
Arel.sql("COUNT(*) FILTER (WHERE status = 'success') as success_count"),
|
|
203
|
-
Arel.sql("COUNT(*) FILTER (WHERE status IN ('error', 'timeout')) as failed_count"),
|
|
204
|
-
Arel.sql("COALESCE(SUM(total_cost), 0) as total_cost")
|
|
205
|
-
)
|
|
206
|
-
.index_by { |r| r.time_bucket.to_time.beginning_of_hour }
|
|
200
|
+
.select(:status, :total_cost, :created_at)
|
|
201
|
+
.group_by { |r| r.created_at.beginning_of_hour }
|
|
207
202
|
|
|
208
203
|
# Build arrays for all 24 hours (fill missing with zeros)
|
|
209
204
|
success_data = []
|
|
@@ -215,11 +210,11 @@ module RubyLLM
|
|
|
215
210
|
|
|
216
211
|
(23.downto(0)).each do |hours_ago|
|
|
217
212
|
bucket_time = (reference_time - hours_ago.hours).beginning_of_hour
|
|
218
|
-
|
|
213
|
+
rows = results[bucket_time] || []
|
|
219
214
|
|
|
220
|
-
s =
|
|
221
|
-
f =
|
|
222
|
-
c =
|
|
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 }
|
|
223
218
|
|
|
224
219
|
success_data << s
|
|
225
220
|
failed_data << f
|
|
@@ -242,21 +237,16 @@ module RubyLLM
|
|
|
242
237
|
end
|
|
243
238
|
|
|
244
239
|
# Builds daily chart data for specified number of days
|
|
245
|
-
# Optimized: Single
|
|
240
|
+
# Optimized: Single query instead of 3*days individual queries
|
|
241
|
+
# Database-agnostic: works with both PostgreSQL and SQLite
|
|
246
242
|
def build_daily_chart_data(days)
|
|
247
243
|
end_date = Date.current
|
|
248
244
|
start_date = (days - 1).days.ago.to_date
|
|
249
245
|
|
|
250
|
-
#
|
|
246
|
+
# Use database-agnostic aggregation with Ruby post-processing
|
|
251
247
|
results = where(created_at: start_date.beginning_of_day..end_date.end_of_day)
|
|
252
|
-
.
|
|
253
|
-
.
|
|
254
|
-
Arel.sql("DATE_TRUNC('day', created_at) as time_bucket"),
|
|
255
|
-
Arel.sql("COUNT(*) FILTER (WHERE status = 'success') as success_count"),
|
|
256
|
-
Arel.sql("COUNT(*) FILTER (WHERE status IN ('error', 'timeout')) as failed_count"),
|
|
257
|
-
Arel.sql("COALESCE(SUM(total_cost), 0) as total_cost")
|
|
258
|
-
)
|
|
259
|
-
.index_by { |r| r.time_bucket.to_date }
|
|
248
|
+
.select(:status, :total_cost, :created_at)
|
|
249
|
+
.group_by { |r| r.created_at.to_date }
|
|
260
250
|
|
|
261
251
|
# Build arrays for all days (fill missing with zeros)
|
|
262
252
|
success_data = []
|
|
@@ -268,11 +258,11 @@ module RubyLLM
|
|
|
268
258
|
|
|
269
259
|
(days - 1).downto(0).each do |days_ago|
|
|
270
260
|
date = days_ago.days.ago.to_date
|
|
271
|
-
|
|
261
|
+
rows = results[date] || []
|
|
272
262
|
|
|
273
|
-
s =
|
|
274
|
-
f =
|
|
275
|
-
c =
|
|
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 }
|
|
276
266
|
|
|
277
267
|
success_data << s
|
|
278
268
|
failed_data << f
|
|
@@ -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)
|