ruby_llm-agents 1.3.4 → 2.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/README.md +112 -336
- data/app/controllers/concerns/ruby_llm/agents/sortable.rb +0 -1
- data/app/controllers/ruby_llm/agents/agents_controller.rb +5 -56
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +22 -106
- data/app/controllers/ruby_llm/agents/executions_controller.rb +4 -114
- data/app/controllers/ruby_llm/agents/tenants_controller.rb +30 -2
- data/app/helpers/ruby_llm/agents/application_helper.rb +19 -53
- data/app/models/ruby_llm/agents/execution/analytics.rb +13 -54
- data/app/models/ruby_llm/agents/execution/scopes.rb +61 -14
- data/app/models/ruby_llm/agents/execution.rb +52 -12
- data/app/models/ruby_llm/agents/execution_detail.rb +18 -0
- data/app/models/ruby_llm/agents/tenant/budgetable.rb +132 -24
- data/app/models/ruby_llm/agents/tenant/incrementable.rb +117 -0
- data/app/models/ruby_llm/agents/tenant/resettable.rb +128 -0
- data/app/models/ruby_llm/agents/tenant/trackable.rb +46 -12
- data/app/models/ruby_llm/agents/tenant.rb +2 -3
- data/app/models/ruby_llm/agents/tenant_budget.rb +6 -3
- data/app/services/ruby_llm/agents/agent_registry.rb +6 -112
- data/app/views/layouts/ruby_llm/agents/application.html.erb +89 -252
- data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +71 -218
- data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +20 -63
- data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +44 -131
- data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +16 -57
- data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +39 -104
- data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +29 -82
- data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +4 -14
- data/app/views/ruby_llm/agents/agents/index.html.erb +105 -274
- data/app/views/ruby_llm/agents/agents/show.html.erb +248 -378
- data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +29 -52
- data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +73 -99
- data/app/views/ruby_llm/agents/dashboard/index.html.erb +228 -433
- data/app/views/ruby_llm/agents/executions/_execution.html.erb +1 -1
- data/app/views/ruby_llm/agents/executions/_filters.html.erb +4 -25
- data/app/views/ruby_llm/agents/executions/_list.html.erb +111 -152
- data/app/views/ruby_llm/agents/executions/index.html.erb +5 -7
- data/app/views/ruby_llm/agents/executions/show.html.erb +526 -1037
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +5 -21
- data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +70 -191
- data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +16 -44
- data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +12 -41
- data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +11 -65
- data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +6 -5
- data/app/views/ruby_llm/agents/system_config/show.html.erb +240 -351
- data/app/views/ruby_llm/agents/tenants/_form.html.erb +67 -77
- data/app/views/ruby_llm/agents/tenants/edit.html.erb +7 -9
- data/app/views/ruby_llm/agents/tenants/index.html.erb +100 -122
- data/app/views/ruby_llm/agents/tenants/show.html.erb +146 -336
- data/config/routes.rb +0 -13
- data/lib/generators/ruby_llm_agents/install_generator.rb +13 -17
- data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +2 -12
- data/lib/generators/ruby_llm_agents/restructure_generator.rb +0 -2
- data/lib/generators/ruby_llm_agents/templates/add_usage_counters_to_tenants_migration.rb.tt +37 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +1 -2
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +1 -1
- data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +0 -1
- data/lib/generators/ruby_llm_agents/templates/create_execution_details_migration.rb.tt +27 -0
- data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +25 -0
- data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +0 -1
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +33 -12
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +40 -71
- data/lib/generators/ruby_llm_agents/templates/remove_agent_version_migration.rb.tt +13 -0
- data/lib/generators/ruby_llm_agents/templates/remove_workflow_columns_migration.rb.tt +19 -0
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +2 -4
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +0 -1
- data/lib/generators/ruby_llm_agents/templates/split_execution_details_migration.rb.tt +232 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +77 -259
- data/lib/ruby_llm/agents/audio/speaker.rb +0 -1
- data/lib/ruby_llm/agents/audio/transcriber.rb +0 -1
- data/lib/ruby_llm/agents/base_agent.rb +54 -23
- data/lib/ruby_llm/agents/core/base/callbacks.rb +142 -0
- data/lib/ruby_llm/agents/core/base.rb +23 -55
- data/lib/ruby_llm/agents/core/configuration.rb +97 -117
- data/lib/ruby_llm/agents/core/errors.rb +0 -58
- data/lib/ruby_llm/agents/core/instrumentation.rb +157 -110
- data/lib/ruby_llm/agents/core/llm_tenant.rb +8 -7
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/dsl/base.rb +157 -17
- data/lib/ruby_llm/agents/dsl/caching.rb +33 -2
- data/lib/ruby_llm/agents/dsl/reliability.rb +148 -0
- data/lib/ruby_llm/agents/dsl.rb +1 -2
- data/lib/ruby_llm/agents/image/analyzer/execution.rb +1 -2
- data/lib/ruby_llm/agents/image/background_remover/execution.rb +1 -2
- data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +1 -13
- data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -2
- data/lib/ruby_llm/agents/image/editor/dsl.rb +0 -14
- data/lib/ruby_llm/agents/image/editor/execution.rb +1 -10
- data/lib/ruby_llm/agents/image/editor.rb +0 -1
- data/lib/ruby_llm/agents/image/generator.rb +0 -21
- data/lib/ruby_llm/agents/image/pipeline/dsl.rb +0 -13
- data/lib/ruby_llm/agents/image/pipeline/execution.rb +0 -1
- data/lib/ruby_llm/agents/image/transformer/dsl.rb +0 -13
- data/lib/ruby_llm/agents/image/transformer/execution.rb +1 -10
- data/lib/ruby_llm/agents/image/transformer.rb +0 -1
- data/lib/ruby_llm/agents/image/upscaler/execution.rb +1 -2
- data/lib/ruby_llm/agents/image/variator/execution.rb +1 -2
- data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +78 -173
- data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
- data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
- data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
- data/lib/ruby_llm/agents/infrastructure/execution_logger_job.rb +8 -0
- data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
- data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
- data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
- data/lib/ruby_llm/agents/rails/engine.rb +6 -6
- data/lib/ruby_llm/agents/results/base.rb +1 -49
- data/lib/ruby_llm/agents/text/embedder.rb +0 -1
- data/lib/ruby_llm/agents.rb +1 -9
- data/lib/tasks/ruby_llm_agents.rake +34 -0
- metadata +14 -83
- data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
- data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
- data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
- data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
- data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
- data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
- data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
- data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
- data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
- data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
- data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
- data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
- data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
- data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
- data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
- data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
- data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
- data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
- data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
- data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
- data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
- data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
- data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
- data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
- data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
- data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
- data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
- data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
- data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
- data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
- data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
- data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
- data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
- data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
- data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
- data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
- data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
- data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
- data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
- data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
- data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
- data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
- data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
- data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
- data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
- data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
- data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
- data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
- data/lib/ruby_llm/agents/text/moderator.rb +0 -237
- data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
- data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
- data/lib/ruby_llm/agents/workflow/async.rb +0 -220
- data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
- data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
- data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
- data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
- data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
- data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
- data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
- data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
- data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
- data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
- data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
- data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
- data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
- data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
- data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
- data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
- data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
- data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
- data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
- data/lib/ruby_llm/agents/workflow/result.rb +0 -592
- data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
- data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
- data/lib/ruby_llm/agents/workflow/wait_result.rb +0 -213
|
@@ -48,7 +48,6 @@ module RubyLLM
|
|
|
48
48
|
@agents_by_type = {
|
|
49
49
|
agent: @agents.select { |a| a[:agent_type] == "agent" },
|
|
50
50
|
embedder: @agents.select { |a| a[:agent_type] == "embedder" },
|
|
51
|
-
moderator: @agents.select { |a| a[:agent_type] == "moderator" },
|
|
52
51
|
speaker: @agents.select { |a| a[:agent_type] == "speaker" },
|
|
53
52
|
transcriber: @agents.select { |a| a[:agent_type] == "transcriber" },
|
|
54
53
|
image_generator: @agents.select { |a| a[:agent_type] == "image_generator" }
|
|
@@ -60,7 +59,7 @@ module RubyLLM
|
|
|
60
59
|
Rails.logger.error("[RubyLLM::Agents] Error loading agents: #{e.message}")
|
|
61
60
|
@agents = []
|
|
62
61
|
@deleted_agents = []
|
|
63
|
-
@agents_by_type = { agent: [], embedder: [],
|
|
62
|
+
@agents_by_type = { agent: [], embedder: [], speaker: [], transcriber: [], image_generator: [] }
|
|
64
63
|
@agent_count = 0
|
|
65
64
|
@deleted_count = 0
|
|
66
65
|
@sort_params = { column: DEFAULT_AGENT_SORT_COLUMN, direction: DEFAULT_AGENT_SORT_DIRECTION }
|
|
@@ -119,13 +118,11 @@ module RubyLLM
|
|
|
119
118
|
def load_filter_options
|
|
120
119
|
# Single query to get all filter options (fixes N+1)
|
|
121
120
|
filter_data = Execution.by_agent(@agent_type)
|
|
122
|
-
.where.not(
|
|
123
|
-
.or(Execution.by_agent(@agent_type).where.not(model_id: nil))
|
|
121
|
+
.where.not(model_id: nil)
|
|
124
122
|
.or(Execution.by_agent(@agent_type).where.not(temperature: nil))
|
|
125
|
-
.pluck(:
|
|
123
|
+
.pluck(:model_id, :temperature)
|
|
126
124
|
|
|
127
|
-
@
|
|
128
|
-
@models = filter_data.map { |d| d[1] }.compact.uniq.sort
|
|
125
|
+
@models = filter_data.map(&:first).compact.uniq.sort
|
|
129
126
|
@temperatures = filter_data.map(&:last).compact.uniq.sort
|
|
130
127
|
end
|
|
131
128
|
|
|
@@ -149,7 +146,7 @@ module RubyLLM
|
|
|
149
146
|
|
|
150
147
|
# Builds a filtered scope for the current agent's executions
|
|
151
148
|
#
|
|
152
|
-
# Applies filters in order: status,
|
|
149
|
+
# Applies filters in order: status, model, temperature, time.
|
|
153
150
|
# Each filter is optional and only applied if values are provided.
|
|
154
151
|
#
|
|
155
152
|
# @return [ActiveRecord::Relation] Filtered execution scope
|
|
@@ -160,10 +157,6 @@ module RubyLLM
|
|
|
160
157
|
statuses = parse_array_param(:statuses)
|
|
161
158
|
scope = apply_status_filter(scope, statuses) if statuses.any?
|
|
162
159
|
|
|
163
|
-
# Apply version filter
|
|
164
|
-
versions = parse_array_param(:versions)
|
|
165
|
-
scope = scope.where(agent_version: versions) if versions.any?
|
|
166
|
-
|
|
167
160
|
# Apply model filter
|
|
168
161
|
models = parse_array_param(:models)
|
|
169
162
|
scope = scope.where(model_id: models) if models.any?
|
|
@@ -188,37 +181,6 @@ module RubyLLM
|
|
|
188
181
|
@trend_data = Execution.trend_analysis(agent_type: @agent_type, days: 30)
|
|
189
182
|
@status_distribution = Execution.by_agent(@agent_type).group(:status).count
|
|
190
183
|
@finish_reason_distribution = Execution.by_agent(@agent_type).finish_reason_distribution
|
|
191
|
-
load_version_comparison
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
# Loads version comparison data if multiple versions exist
|
|
195
|
-
#
|
|
196
|
-
# Includes trend data for sparkline charts.
|
|
197
|
-
#
|
|
198
|
-
# @return [void]
|
|
199
|
-
def load_version_comparison
|
|
200
|
-
return unless @versions.size >= 2
|
|
201
|
-
|
|
202
|
-
# Default to comparing two most recent versions
|
|
203
|
-
v1 = params[:compare_v1] || @versions[0]
|
|
204
|
-
v2 = params[:compare_v2] || @versions[1]
|
|
205
|
-
|
|
206
|
-
comparison_data = Execution.compare_versions(@agent_type, v1, v2, period: :this_month)
|
|
207
|
-
|
|
208
|
-
# Fetch trend data for sparklines
|
|
209
|
-
v1_trend = Execution.version_trend_data(@agent_type, v1, days: 14)
|
|
210
|
-
v2_trend = Execution.version_trend_data(@agent_type, v2, days: 14)
|
|
211
|
-
|
|
212
|
-
@version_comparison = {
|
|
213
|
-
v1: v1,
|
|
214
|
-
v2: v2,
|
|
215
|
-
data: comparison_data,
|
|
216
|
-
v1_trend: v1_trend,
|
|
217
|
-
v2_trend: v2_trend
|
|
218
|
-
}
|
|
219
|
-
rescue StandardError => e
|
|
220
|
-
Rails.logger.debug("[RubyLLM::Agents] Version comparison error: #{e.message}")
|
|
221
|
-
@version_comparison = nil
|
|
222
184
|
end
|
|
223
185
|
|
|
224
186
|
# Loads the current agent class configuration
|
|
@@ -234,7 +196,6 @@ module RubyLLM
|
|
|
234
196
|
# Common config for all types
|
|
235
197
|
@config = {
|
|
236
198
|
model: safe_config_call(:model),
|
|
237
|
-
version: safe_config_call(:version) || "N/A",
|
|
238
199
|
description: safe_config_call(:description)
|
|
239
200
|
}
|
|
240
201
|
|
|
@@ -242,8 +203,6 @@ module RubyLLM
|
|
|
242
203
|
case @agent_type_kind
|
|
243
204
|
when "embedder"
|
|
244
205
|
load_embedder_config
|
|
245
|
-
when "moderator"
|
|
246
|
-
load_moderator_config
|
|
247
206
|
when "speaker"
|
|
248
207
|
load_speaker_config
|
|
249
208
|
when "transcriber"
|
|
@@ -284,16 +243,6 @@ module RubyLLM
|
|
|
284
243
|
)
|
|
285
244
|
end
|
|
286
245
|
|
|
287
|
-
# Loads configuration specific to Moderators
|
|
288
|
-
#
|
|
289
|
-
# @return [void]
|
|
290
|
-
def load_moderator_config
|
|
291
|
-
@config.merge!(
|
|
292
|
-
threshold: safe_config_call(:threshold),
|
|
293
|
-
categories: safe_config_call(:categories)
|
|
294
|
-
)
|
|
295
|
-
end
|
|
296
|
-
|
|
297
246
|
# Loads configuration specific to Speakers
|
|
298
247
|
#
|
|
299
248
|
# @return [void]
|
|
@@ -32,37 +32,11 @@ module RubyLLM
|
|
|
32
32
|
|
|
33
33
|
# Returns chart data as JSON for live updates
|
|
34
34
|
#
|
|
35
|
-
# @param range [String] Time range: "today", "7d",
|
|
36
|
-
# @
|
|
37
|
-
# @return [JSON] Chart data with series (and optional comparison series)
|
|
35
|
+
# @param range [String] Time range: "today", "7d", or "30d"
|
|
36
|
+
# @return [JSON] Chart data with series
|
|
38
37
|
def chart_data
|
|
39
38
|
range = params[:range].presence || "today"
|
|
40
|
-
|
|
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
|
-
|
|
39
|
+
data = tenant_scoped_executions.activity_chart_json(range: range)
|
|
66
40
|
render json: data
|
|
67
41
|
end
|
|
68
42
|
|
|
@@ -70,49 +44,17 @@ module RubyLLM
|
|
|
70
44
|
|
|
71
45
|
# Converts range parameter to number of days
|
|
72
46
|
#
|
|
73
|
-
# @param range [String] Range parameter (today, 7d, 30d
|
|
47
|
+
# @param range [String] Range parameter (today, 7d, 30d)
|
|
74
48
|
# @return [Integer] Number of days
|
|
75
49
|
def range_to_days(range)
|
|
76
50
|
case range
|
|
77
51
|
when "today" then 1
|
|
78
52
|
when "7d" then 7
|
|
79
53
|
when "30d" then 30
|
|
80
|
-
|
|
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
|
|
54
|
+
else 1
|
|
96
55
|
end
|
|
97
56
|
end
|
|
98
57
|
|
|
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
|
-
|
|
116
58
|
# Builds per-agent comparison statistics for all agent types
|
|
117
59
|
#
|
|
118
60
|
# Creates separate instance variables for each agent type:
|
|
@@ -121,8 +63,6 @@ module RubyLLM
|
|
|
121
63
|
# - @transcriber_stats: Transcribers
|
|
122
64
|
# - @speaker_stats: Speakers
|
|
123
65
|
# - @image_generator_stats: Image generators
|
|
124
|
-
# - @moderator_stats: Moderators
|
|
125
|
-
# - @workflow_stats: Workflows
|
|
126
66
|
#
|
|
127
67
|
# @param base_scope [ActiveRecord::Relation] Base scope to filter from
|
|
128
68
|
# @return [Array<Hash>] Array of base agent stats (for backward compatibility)
|
|
@@ -138,7 +78,6 @@ module RubyLLM
|
|
|
138
78
|
all_stats = all_agent_types.map do |agent_type|
|
|
139
79
|
agent_class = AgentRegistry.find(agent_type)
|
|
140
80
|
detected_type = AgentRegistry.send(:detect_agent_type, agent_class)
|
|
141
|
-
workflow_type = detected_type == "workflow" ? detect_workflow_type(agent_class) : nil
|
|
142
81
|
|
|
143
82
|
# Get stats from batch or use zeros for never-executed agents
|
|
144
83
|
stats = execution_stats[agent_type] || {
|
|
@@ -152,43 +91,21 @@ module RubyLLM
|
|
|
152
91
|
total_cost: stats[:total_cost],
|
|
153
92
|
avg_cost: stats[:avg_cost],
|
|
154
93
|
avg_duration_ms: stats[:avg_duration_ms],
|
|
155
|
-
success_rate: stats[:success_rate]
|
|
156
|
-
is_workflow: detected_type == "workflow",
|
|
157
|
-
workflow_type: workflow_type
|
|
94
|
+
success_rate: stats[:success_rate]
|
|
158
95
|
}
|
|
159
96
|
end.sort_by { |a| [-(a[:executions] || 0), -(a[:total_cost] || 0)] }
|
|
160
97
|
|
|
161
|
-
# Split stats by agent type for
|
|
98
|
+
# Split stats by agent type for 5-tab display
|
|
162
99
|
@agent_stats = all_stats.select { |a| a[:detected_type] == "agent" }
|
|
163
100
|
@embedder_stats = all_stats.select { |a| a[:detected_type] == "embedder" }
|
|
164
101
|
@transcriber_stats = all_stats.select { |a| a[:detected_type] == "transcriber" }
|
|
165
102
|
@speaker_stats = all_stats.select { |a| a[:detected_type] == "speaker" }
|
|
166
103
|
@image_generator_stats = all_stats.select { |a| a[:detected_type] == "image_generator" }
|
|
167
|
-
@moderator_stats = all_stats.select { |a| a[:detected_type] == "moderator" }
|
|
168
|
-
@workflow_stats = all_stats.select { |a| a[:detected_type] == "workflow" }
|
|
169
104
|
|
|
170
105
|
# Return base agents for backward compatibility
|
|
171
106
|
@agent_stats
|
|
172
107
|
end
|
|
173
108
|
|
|
174
|
-
# Detects workflow type from class hierarchy
|
|
175
|
-
#
|
|
176
|
-
# @param agent_class [Class] The agent class
|
|
177
|
-
# @return [String, nil] "pipeline", "parallel", "router", or nil
|
|
178
|
-
def detect_workflow_type(agent_class)
|
|
179
|
-
return nil unless agent_class
|
|
180
|
-
|
|
181
|
-
ancestors = agent_class.ancestors.map { |a| a.name.to_s }
|
|
182
|
-
|
|
183
|
-
if ancestors.include?("RubyLLM::Agents::Workflow::Pipeline")
|
|
184
|
-
"pipeline"
|
|
185
|
-
elsif ancestors.include?("RubyLLM::Agents::Workflow::Parallel")
|
|
186
|
-
"parallel"
|
|
187
|
-
elsif ancestors.include?("RubyLLM::Agents::Workflow::Router")
|
|
188
|
-
"router"
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
|
|
192
109
|
# Builds per-model statistics for model comparison and cost breakdown
|
|
193
110
|
#
|
|
194
111
|
# @param base_scope [ActiveRecord::Relation] Base scope to filter from
|
|
@@ -337,34 +254,33 @@ module RubyLLM
|
|
|
337
254
|
[]
|
|
338
255
|
end
|
|
339
256
|
|
|
340
|
-
# Loads tenant budget info for the current tenant
|
|
257
|
+
# Loads tenant budget info for the current tenant using counter columns
|
|
341
258
|
#
|
|
342
|
-
# @param base_scope [ActiveRecord::Relation] Base scope for
|
|
259
|
+
# @param base_scope [ActiveRecord::Relation] Base scope (unused, kept for backward compat)
|
|
343
260
|
# @return [Hash, nil] Tenant budget data with usage info, or nil if not applicable
|
|
344
261
|
def load_tenant_budget(base_scope)
|
|
345
262
|
return nil unless tenant_filter_enabled? && current_tenant_id.present?
|
|
346
|
-
return nil unless
|
|
263
|
+
return nil unless Tenant.table_exists?
|
|
347
264
|
|
|
348
|
-
|
|
349
|
-
return nil unless
|
|
265
|
+
tenant = Tenant.for(current_tenant_id)
|
|
266
|
+
return nil unless tenant
|
|
350
267
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
month_scope = base_scope.where("created_at >= ?", Time.current.beginning_of_month)
|
|
268
|
+
tenant.ensure_daily_reset!
|
|
269
|
+
tenant.ensure_monthly_reset!
|
|
354
270
|
|
|
355
|
-
daily_spend =
|
|
356
|
-
monthly_spend =
|
|
271
|
+
daily_spend = tenant.daily_cost_spent
|
|
272
|
+
monthly_spend = tenant.monthly_cost_spent
|
|
357
273
|
|
|
358
274
|
{
|
|
359
275
|
tenant_id: current_tenant_id,
|
|
360
|
-
daily_limit:
|
|
361
|
-
monthly_limit:
|
|
276
|
+
daily_limit: tenant.effective_daily_limit,
|
|
277
|
+
monthly_limit: tenant.effective_monthly_limit,
|
|
362
278
|
daily_spend: daily_spend,
|
|
363
279
|
monthly_spend: monthly_spend,
|
|
364
|
-
daily_percentage:
|
|
365
|
-
monthly_percentage:
|
|
366
|
-
enforcement:
|
|
367
|
-
per_agent_daily:
|
|
280
|
+
daily_percentage: tenant.effective_daily_limit.to_f > 0 ? (daily_spend / tenant.effective_daily_limit * 100).round(1) : 0,
|
|
281
|
+
monthly_percentage: tenant.effective_monthly_limit.to_f > 0 ? (monthly_spend / tenant.effective_monthly_limit * 100).round(1) : 0,
|
|
282
|
+
enforcement: tenant.effective_enforcement,
|
|
283
|
+
per_agent_daily: tenant.per_agent_daily || {}
|
|
368
284
|
}
|
|
369
285
|
end
|
|
370
286
|
|
|
@@ -16,7 +16,7 @@ module RubyLLM
|
|
|
16
16
|
include Filterable
|
|
17
17
|
include Sortable
|
|
18
18
|
|
|
19
|
-
CSV_COLUMNS = %w[id agent_type
|
|
19
|
+
CSV_COLUMNS = %w[id agent_type status model_id total_tokens total_cost
|
|
20
20
|
duration_ms created_at error_class error_message].freeze
|
|
21
21
|
|
|
22
22
|
# Lists all executions with filtering and pagination
|
|
@@ -61,49 +61,6 @@ module RubyLLM
|
|
|
61
61
|
end
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
-
# Reruns an execution with the same parameters
|
|
65
|
-
#
|
|
66
|
-
# Supports both dry-run mode (returns prompt info without API call)
|
|
67
|
-
# and real reruns that create a new execution.
|
|
68
|
-
#
|
|
69
|
-
# @return [void]
|
|
70
|
-
def rerun
|
|
71
|
-
@execution = Execution.find(params[:id])
|
|
72
|
-
dry_run = params[:dry_run] == "true"
|
|
73
|
-
|
|
74
|
-
agent_class = AgentRegistry.find(@execution.agent_type)
|
|
75
|
-
|
|
76
|
-
unless agent_class
|
|
77
|
-
flash[:alert] = "Agent class '#{@execution.agent_type}' not found. Cannot rerun."
|
|
78
|
-
redirect_to execution_path(@execution)
|
|
79
|
-
return
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Prepare parameters from original execution
|
|
83
|
-
original_params = @execution.parameters&.symbolize_keys || {}
|
|
84
|
-
|
|
85
|
-
if dry_run
|
|
86
|
-
# Dry run mode - show what would be sent without making API call
|
|
87
|
-
result = agent_class.call(**original_params, dry_run: true)
|
|
88
|
-
@dry_run_result = result
|
|
89
|
-
|
|
90
|
-
respond_to do |format|
|
|
91
|
-
format.html { render :dry_run }
|
|
92
|
-
format.json { render json: result }
|
|
93
|
-
end
|
|
94
|
-
else
|
|
95
|
-
# Real rerun - execute the agent
|
|
96
|
-
begin
|
|
97
|
-
agent_class.call(**original_params)
|
|
98
|
-
flash[:notice] = "Execution rerun successfully! Check the executions list for the new result."
|
|
99
|
-
rescue StandardError => e
|
|
100
|
-
flash[:alert] = "Rerun failed: #{e.message}"
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
redirect_to executions_path
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
64
|
# Exports filtered executions as CSV
|
|
108
65
|
#
|
|
109
66
|
# Streams CSV data with redacted error messages to protect
|
|
@@ -141,7 +98,6 @@ module RubyLLM
|
|
|
141
98
|
CSV.generate_line([
|
|
142
99
|
execution.id,
|
|
143
100
|
execution.agent_type,
|
|
144
|
-
execution.agent_version,
|
|
145
101
|
execution.status,
|
|
146
102
|
execution.model_id,
|
|
147
103
|
execution.total_tokens,
|
|
@@ -156,14 +112,12 @@ module RubyLLM
|
|
|
156
112
|
# Loads available options for filter dropdowns
|
|
157
113
|
#
|
|
158
114
|
# Populates @agent_types with all agent types that have executions,
|
|
159
|
-
# @model_ids with all distinct models used, @
|
|
160
|
-
# workflow patterns used, and @statuses with all possible status values.
|
|
115
|
+
# @model_ids with all distinct models used, and @statuses with all possible status values.
|
|
161
116
|
#
|
|
162
117
|
# @return [void]
|
|
163
118
|
def load_filter_options
|
|
164
119
|
@agent_types = available_agent_types
|
|
165
120
|
@model_ids = available_model_ids
|
|
166
|
-
@workflow_types = available_workflow_types
|
|
167
121
|
@statuses = Execution.statuses.keys
|
|
168
122
|
end
|
|
169
123
|
|
|
@@ -187,24 +141,6 @@ module RubyLLM
|
|
|
187
141
|
@available_model_ids ||= tenant_scoped_executions.where.not(model_id: nil).distinct.pluck(:model_id).sort
|
|
188
142
|
end
|
|
189
143
|
|
|
190
|
-
# Returns distinct workflow types from execution history
|
|
191
|
-
#
|
|
192
|
-
# Memoized to avoid duplicate queries within a request.
|
|
193
|
-
# Returns empty array if workflow_type column doesn't exist yet.
|
|
194
|
-
# Uses tenant_scoped_executions to respect multi-tenancy filtering.
|
|
195
|
-
#
|
|
196
|
-
# @return [Array<String>] Workflow types (pipeline, parallel, router)
|
|
197
|
-
def available_workflow_types
|
|
198
|
-
return @available_workflow_types if defined?(@available_workflow_types)
|
|
199
|
-
|
|
200
|
-
@available_workflow_types = if Execution.column_names.include?("workflow_type")
|
|
201
|
-
tenant_scoped_executions.where.not(workflow_type: [nil, ""])
|
|
202
|
-
.distinct.pluck(:workflow_type).sort
|
|
203
|
-
else
|
|
204
|
-
[]
|
|
205
|
-
end
|
|
206
|
-
end
|
|
207
|
-
|
|
208
144
|
# Loads paginated executions and associated statistics
|
|
209
145
|
#
|
|
210
146
|
# Sets @executions, @pagination, @sort_params, and @filter_stats instance variables
|
|
@@ -264,63 +200,17 @@ module RubyLLM
|
|
|
264
200
|
model_ids = parse_array_param(:model_ids)
|
|
265
201
|
scope = scope.where(model_id: model_ids) if model_ids.any?
|
|
266
202
|
|
|
267
|
-
# Apply workflow type filter (only if column exists)
|
|
268
|
-
if Execution.column_names.include?("workflow_type")
|
|
269
|
-
workflow_types = parse_array_param(:workflow_types)
|
|
270
|
-
if workflow_types.any?
|
|
271
|
-
includes_single = workflow_types.include?("single")
|
|
272
|
-
other_types = workflow_types - ["single"]
|
|
273
|
-
|
|
274
|
-
if includes_single && other_types.any?
|
|
275
|
-
# Include both single (null workflow_type) and specific workflow types
|
|
276
|
-
scope = scope.where(workflow_type: [nil, ""] + other_types)
|
|
277
|
-
elsif includes_single
|
|
278
|
-
# Only single executions (non-workflow)
|
|
279
|
-
scope = scope.where(workflow_type: [nil, ""])
|
|
280
|
-
else
|
|
281
|
-
# Only specific workflow types
|
|
282
|
-
scope = scope.where(workflow_type: workflow_types)
|
|
283
|
-
end
|
|
284
|
-
end
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
# Apply execution type tab filter (agents vs workflows)
|
|
288
|
-
scope = apply_execution_type_filter(scope)
|
|
289
|
-
|
|
290
203
|
# Apply retries filter (show only executions with multiple attempts)
|
|
291
204
|
scope = scope.where("attempts_count > 1") if params[:has_retries].present?
|
|
292
205
|
|
|
293
|
-
# Only show root executions
|
|
206
|
+
# Only show root executions - children are nested under parents
|
|
294
207
|
scope = scope.where(parent_execution_id: nil)
|
|
295
208
|
|
|
296
|
-
# Eager load children for
|
|
209
|
+
# Eager load children for grouping
|
|
297
210
|
scope = scope.includes(:child_executions)
|
|
298
211
|
|
|
299
212
|
scope
|
|
300
213
|
end
|
|
301
|
-
|
|
302
|
-
# Applies execution type filter (all, agents, workflows, or specific workflow type)
|
|
303
|
-
#
|
|
304
|
-
# @param scope [ActiveRecord::Relation] The current scope
|
|
305
|
-
# @return [ActiveRecord::Relation] Filtered scope
|
|
306
|
-
def apply_execution_type_filter(scope)
|
|
307
|
-
return scope unless Execution.column_names.include?("workflow_type")
|
|
308
|
-
|
|
309
|
-
execution_type = params[:execution_type]
|
|
310
|
-
case execution_type
|
|
311
|
-
when "agents"
|
|
312
|
-
# Only show executions where workflow_type is null/empty (regular agents)
|
|
313
|
-
scope.where(workflow_type: [nil, ""])
|
|
314
|
-
when "workflows"
|
|
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)
|
|
320
|
-
else
|
|
321
|
-
scope
|
|
322
|
-
end
|
|
323
|
-
end
|
|
324
214
|
end
|
|
325
215
|
end
|
|
326
216
|
end
|
|
@@ -10,11 +10,26 @@ module RubyLLM
|
|
|
10
10
|
# @see TenantBudget For budget configuration model
|
|
11
11
|
# @api private
|
|
12
12
|
class TenantsController < ApplicationController
|
|
13
|
-
|
|
13
|
+
TENANT_SORTABLE_COLUMNS = %w[name enforcement daily_limit monthly_limit].freeze
|
|
14
|
+
DEFAULT_TENANT_SORT_COLUMN = "name"
|
|
15
|
+
DEFAULT_TENANT_SORT_DIRECTION = "asc"
|
|
16
|
+
|
|
17
|
+
# Lists all tenant budgets with optional search and sorting
|
|
14
18
|
#
|
|
15
19
|
# @return [void]
|
|
16
20
|
def index
|
|
17
|
-
@
|
|
21
|
+
@sort_params = parse_tenant_sort_params
|
|
22
|
+
scope = TenantBudget.all
|
|
23
|
+
|
|
24
|
+
if params[:q].present?
|
|
25
|
+
@search_query = params[:q].to_s.strip
|
|
26
|
+
scope = scope.where(
|
|
27
|
+
"tenant_id LIKE :q OR name LIKE :q",
|
|
28
|
+
q: "%#{TenantBudget.sanitize_sql_like(@search_query)}%"
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@tenants = scope.order(@sort_params[:column] => @sort_params[:direction].to_sym)
|
|
18
33
|
end
|
|
19
34
|
|
|
20
35
|
# Shows a single tenant's budget details
|
|
@@ -95,6 +110,19 @@ module RubyLLM
|
|
|
95
110
|
}
|
|
96
111
|
end
|
|
97
112
|
|
|
113
|
+
# Parses and validates sort parameters for tenants list
|
|
114
|
+
#
|
|
115
|
+
# @return [Hash] Contains :column and :direction keys
|
|
116
|
+
def parse_tenant_sort_params
|
|
117
|
+
column = params[:sort].to_s
|
|
118
|
+
direction = params[:direction].to_s.downcase
|
|
119
|
+
|
|
120
|
+
{
|
|
121
|
+
column: TENANT_SORTABLE_COLUMNS.include?(column) ? column : DEFAULT_TENANT_SORT_COLUMN,
|
|
122
|
+
direction: %w[asc desc].include?(direction) ? direction : DEFAULT_TENANT_SORT_DIRECTION
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
|
|
98
126
|
# Calculates percentage used
|
|
99
127
|
#
|
|
100
128
|
# @param current [Numeric] Current usage
|