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.
Files changed (191) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +112 -336
  3. data/app/controllers/concerns/ruby_llm/agents/sortable.rb +0 -1
  4. data/app/controllers/ruby_llm/agents/agents_controller.rb +5 -56
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +22 -106
  6. data/app/controllers/ruby_llm/agents/executions_controller.rb +4 -114
  7. data/app/controllers/ruby_llm/agents/tenants_controller.rb +30 -2
  8. data/app/helpers/ruby_llm/agents/application_helper.rb +19 -53
  9. data/app/models/ruby_llm/agents/execution/analytics.rb +13 -54
  10. data/app/models/ruby_llm/agents/execution/scopes.rb +61 -14
  11. data/app/models/ruby_llm/agents/execution.rb +52 -12
  12. data/app/models/ruby_llm/agents/execution_detail.rb +18 -0
  13. data/app/models/ruby_llm/agents/tenant/budgetable.rb +132 -24
  14. data/app/models/ruby_llm/agents/tenant/incrementable.rb +117 -0
  15. data/app/models/ruby_llm/agents/tenant/resettable.rb +128 -0
  16. data/app/models/ruby_llm/agents/tenant/trackable.rb +46 -12
  17. data/app/models/ruby_llm/agents/tenant.rb +2 -3
  18. data/app/models/ruby_llm/agents/tenant_budget.rb +6 -3
  19. data/app/services/ruby_llm/agents/agent_registry.rb +6 -112
  20. data/app/views/layouts/ruby_llm/agents/application.html.erb +89 -252
  21. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +71 -218
  22. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +20 -63
  23. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +44 -131
  24. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +16 -57
  25. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +39 -104
  26. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +29 -82
  27. data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +4 -14
  28. data/app/views/ruby_llm/agents/agents/index.html.erb +105 -274
  29. data/app/views/ruby_llm/agents/agents/show.html.erb +248 -378
  30. data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +29 -52
  31. data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +73 -99
  32. data/app/views/ruby_llm/agents/dashboard/index.html.erb +228 -433
  33. data/app/views/ruby_llm/agents/executions/_execution.html.erb +1 -1
  34. data/app/views/ruby_llm/agents/executions/_filters.html.erb +4 -25
  35. data/app/views/ruby_llm/agents/executions/_list.html.erb +111 -152
  36. data/app/views/ruby_llm/agents/executions/index.html.erb +5 -7
  37. data/app/views/ruby_llm/agents/executions/show.html.erb +526 -1037
  38. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +5 -21
  39. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +70 -191
  40. data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +16 -44
  41. data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +12 -41
  42. data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +11 -65
  43. data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +6 -5
  44. data/app/views/ruby_llm/agents/system_config/show.html.erb +240 -351
  45. data/app/views/ruby_llm/agents/tenants/_form.html.erb +67 -77
  46. data/app/views/ruby_llm/agents/tenants/edit.html.erb +7 -9
  47. data/app/views/ruby_llm/agents/tenants/index.html.erb +100 -122
  48. data/app/views/ruby_llm/agents/tenants/show.html.erb +146 -336
  49. data/config/routes.rb +0 -13
  50. data/lib/generators/ruby_llm_agents/install_generator.rb +13 -17
  51. data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +2 -12
  52. data/lib/generators/ruby_llm_agents/restructure_generator.rb +0 -2
  53. data/lib/generators/ruby_llm_agents/templates/add_usage_counters_to_tenants_migration.rb.tt +37 -0
  54. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +1 -2
  55. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +1 -1
  56. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +0 -1
  57. data/lib/generators/ruby_llm_agents/templates/create_execution_details_migration.rb.tt +27 -0
  58. data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +25 -0
  59. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +0 -1
  60. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +33 -12
  61. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +40 -71
  62. data/lib/generators/ruby_llm_agents/templates/remove_agent_version_migration.rb.tt +13 -0
  63. data/lib/generators/ruby_llm_agents/templates/remove_workflow_columns_migration.rb.tt +19 -0
  64. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +2 -4
  65. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +0 -1
  66. data/lib/generators/ruby_llm_agents/templates/split_execution_details_migration.rb.tt +232 -0
  67. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +77 -259
  68. data/lib/ruby_llm/agents/audio/speaker.rb +0 -1
  69. data/lib/ruby_llm/agents/audio/transcriber.rb +0 -1
  70. data/lib/ruby_llm/agents/base_agent.rb +54 -23
  71. data/lib/ruby_llm/agents/core/base/callbacks.rb +142 -0
  72. data/lib/ruby_llm/agents/core/base.rb +23 -55
  73. data/lib/ruby_llm/agents/core/configuration.rb +97 -117
  74. data/lib/ruby_llm/agents/core/errors.rb +0 -58
  75. data/lib/ruby_llm/agents/core/instrumentation.rb +157 -110
  76. data/lib/ruby_llm/agents/core/llm_tenant.rb +8 -7
  77. data/lib/ruby_llm/agents/core/version.rb +1 -1
  78. data/lib/ruby_llm/agents/dsl/base.rb +157 -17
  79. data/lib/ruby_llm/agents/dsl/caching.rb +33 -2
  80. data/lib/ruby_llm/agents/dsl/reliability.rb +148 -0
  81. data/lib/ruby_llm/agents/dsl.rb +1 -2
  82. data/lib/ruby_llm/agents/image/analyzer/execution.rb +1 -2
  83. data/lib/ruby_llm/agents/image/background_remover/execution.rb +1 -2
  84. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +1 -13
  85. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -2
  86. data/lib/ruby_llm/agents/image/editor/dsl.rb +0 -14
  87. data/lib/ruby_llm/agents/image/editor/execution.rb +1 -10
  88. data/lib/ruby_llm/agents/image/editor.rb +0 -1
  89. data/lib/ruby_llm/agents/image/generator.rb +0 -21
  90. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +0 -13
  91. data/lib/ruby_llm/agents/image/pipeline/execution.rb +0 -1
  92. data/lib/ruby_llm/agents/image/transformer/dsl.rb +0 -13
  93. data/lib/ruby_llm/agents/image/transformer/execution.rb +1 -10
  94. data/lib/ruby_llm/agents/image/transformer.rb +0 -1
  95. data/lib/ruby_llm/agents/image/upscaler/execution.rb +1 -2
  96. data/lib/ruby_llm/agents/image/variator/execution.rb +1 -2
  97. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +78 -173
  98. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
  99. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
  100. data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
  101. data/lib/ruby_llm/agents/infrastructure/execution_logger_job.rb +8 -0
  102. data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
  103. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
  104. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
  105. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
  106. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
  107. data/lib/ruby_llm/agents/rails/engine.rb +6 -6
  108. data/lib/ruby_llm/agents/results/base.rb +1 -49
  109. data/lib/ruby_llm/agents/text/embedder.rb +0 -1
  110. data/lib/ruby_llm/agents.rb +1 -9
  111. data/lib/tasks/ruby_llm_agents.rake +34 -0
  112. metadata +14 -83
  113. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
  114. data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
  115. data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
  116. data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
  117. data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
  118. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
  119. data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
  120. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
  121. data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
  122. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
  123. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
  124. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
  125. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
  126. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
  127. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
  128. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
  129. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
  130. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
  131. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
  132. data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
  133. data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
  134. data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
  135. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
  136. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
  137. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
  138. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
  139. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
  140. data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
  141. data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
  142. data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
  143. data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
  144. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
  145. data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
  146. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
  147. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
  148. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
  149. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
  150. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
  151. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
  152. data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
  153. data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
  154. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
  155. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
  156. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
  157. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
  158. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
  159. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
  160. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
  161. data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
  162. data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
  163. data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
  164. data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
  165. data/lib/ruby_llm/agents/text/moderator.rb +0 -237
  166. data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
  167. data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
  168. data/lib/ruby_llm/agents/workflow/async.rb +0 -220
  169. data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
  170. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
  171. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
  172. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
  173. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
  174. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
  175. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
  176. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
  177. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
  178. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
  179. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
  180. data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
  181. data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
  182. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
  183. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
  184. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
  185. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
  186. data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
  187. data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
  188. data/lib/ruby_llm/agents/workflow/result.rb +0 -592
  189. data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
  190. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
  191. 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: [], moderator: [], speaker: [], transcriber: [], image_generator: [] }
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(agent_version: nil)
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(:agent_version, :model_id, :temperature)
123
+ .pluck(:model_id, :temperature)
126
124
 
127
- @versions = filter_data.map(&:first).compact.uniq.sort.reverse
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, version, model, temperature, time.
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", "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)
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
- 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
-
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, 60d, 90d, or custom YYYY-MM-DD_YYYY-MM-DD)
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
- 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
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 7-tab display
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 usage calculation
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 TenantBudget.table_exists?
263
+ return nil unless Tenant.table_exists?
347
264
 
348
- budget = TenantBudget.for_tenant(current_tenant_id)
349
- return nil unless budget
265
+ tenant = Tenant.for(current_tenant_id)
266
+ return nil unless tenant
350
267
 
351
- # Calculate current usage
352
- today_scope = base_scope.where("created_at >= ?", Time.current.beginning_of_day)
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 = today_scope.sum(:total_cost) || 0
356
- monthly_spend = month_scope.sum(:total_cost) || 0
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: budget.effective_daily_limit,
361
- monthly_limit: budget.effective_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: budget.effective_daily_limit.to_f > 0 ? (daily_spend / budget.effective_daily_limit * 100).round(1) : 0,
365
- monthly_percentage: budget.effective_monthly_limit.to_f > 0 ? (monthly_spend / budget.effective_monthly_limit * 100).round(1) : 0,
366
- enforcement: budget.effective_enforcement,
367
- per_agent_daily: budget.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 agent_version status model_id total_tokens total_cost
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, @workflow_types with
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 (not workflow children) - children are nested under parents
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 workflow grouping
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
- # Lists all tenant budgets
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
- @tenants = TenantBudget.order(:name, :tenant_id)
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