ruby_llm-agents 1.3.3 → 2.0.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 (192) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -334
  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 +46 -10
  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 +87 -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 +528 -989
  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 +9 -14
  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 +9 -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 +58 -262
  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 +52 -6
  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 +58 -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/attempt_tracker.rb +1 -0
  99. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
  100. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
  101. data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
  102. data/lib/ruby_llm/agents/infrastructure/reliability.rb +37 -2
  103. data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
  104. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
  105. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
  106. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
  107. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
  108. data/lib/ruby_llm/agents/rails/engine.rb +6 -6
  109. data/lib/ruby_llm/agents/results/base.rb +1 -49
  110. data/lib/ruby_llm/agents/text/embedder.rb +0 -1
  111. data/lib/ruby_llm/agents.rb +1 -9
  112. data/lib/tasks/ruby_llm_agents.rake +34 -0
  113. metadata +12 -81
  114. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
  115. data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
  116. data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
  117. data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
  118. data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
  119. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
  120. data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
  121. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
  122. data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
  123. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
  124. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
  125. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
  126. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
  127. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
  128. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
  129. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
  130. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
  131. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
  132. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
  133. data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
  134. data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
  135. data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
  136. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
  137. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
  138. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
  139. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
  140. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
  141. data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
  142. data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
  143. data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
  144. data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
  145. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
  146. data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
  147. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
  148. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
  149. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
  150. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
  151. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
  152. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
  153. data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
  154. data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
  155. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
  156. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
  157. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
  158. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
  159. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
  160. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
  161. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
  162. data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
  163. data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
  164. data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
  165. data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
  166. data/lib/ruby_llm/agents/text/moderator.rb +0 -237
  167. data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
  168. data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
  169. data/lib/ruby_llm/agents/workflow/async.rb +0 -220
  170. data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
  171. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
  172. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
  173. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
  174. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
  175. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
  176. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
  177. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
  178. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
  179. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
  180. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
  181. data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
  182. data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
  183. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
  184. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
  185. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
  186. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
  187. data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
  188. data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
  189. data/lib/ruby_llm/agents/workflow/result.rb +0 -592
  190. data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
  191. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
  192. data/lib/ruby_llm/agents/workflow/wait_result.rb +0 -213
@@ -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
@@ -19,13 +19,10 @@ module RubyLLM
19
19
  "dashboard/index" => "Dashboard",
20
20
  "agents/index" => "Agent-DSL",
21
21
  "agents/show" => "Agent-DSL",
22
- "workflows/index" => "Workflows",
23
- "workflows/show" => "Workflows",
24
22
  "executions/index" => "Execution-Tracking",
25
23
  "executions/show" => "Execution-Tracking",
26
24
  "tenants/index" => "Multi-Tenancy",
27
- "system_config/show" => "Configuration",
28
- "api_configurations/show" => "Configuration"
25
+ "system_config/show" => "Configuration"
29
26
  }.freeze
30
27
 
31
28
  # Returns the documentation URL for the current page or a specific page key
@@ -59,27 +56,11 @@ module RubyLLM
59
56
 
60
57
  # Returns the URL for "All Tenants" (clears tenant filter)
61
58
  #
62
- # Handles two scenarios:
63
- # 1. Query param routes - removes tenant_id from query params
64
- # 2. Path-based tenant routes - navigates to equivalent global route
59
+ # Removes tenant_id from query params to show unfiltered results.
65
60
  #
66
61
  # @return [String] URL without tenant filtering
67
62
  def all_tenants_url
68
- # Map tenant-specific path routes to their global equivalents
69
- tenant_route_mappings = {
70
- "tenant" => ruby_llm_agents.api_configuration_path,
71
- "edit_tenant" => ruby_llm_agents.edit_api_configuration_path
72
- }
73
-
74
- # Check if current action has a global equivalent
75
- if tenant_route_mappings.key?(action_name)
76
- base_path = tenant_route_mappings[action_name]
77
- query = request.query_parameters.except("tenant_id")
78
- query.any? ? "#{base_path}?#{query.to_query}" : base_path
79
- else
80
- # For query param routes, just remove tenant_id
81
- url_for(request.query_parameters.except("tenant_id"))
82
- end
63
+ url_for(request.query_parameters.except("tenant_id"))
83
64
  end
84
65
 
85
66
  # Formats large numbers with human-readable suffixes (K, M, B)
@@ -121,7 +102,7 @@ module RubyLLM
121
102
  # @return [ActiveSupport::SafeBuffer] HTML badge element
122
103
  def render_enabled_badge(enabled)
123
104
  if enabled
124
- '<span class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300">Enabled</span>'.html_safe
105
+ '<span class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-300">Enabled</span>'.html_safe
125
106
  else
126
107
  '<span class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400">Disabled</span>'.html_safe
127
108
  end
@@ -133,7 +114,7 @@ module RubyLLM
133
114
  # @return [ActiveSupport::SafeBuffer] HTML badge element
134
115
  def render_configured_badge(configured)
135
116
  if configured
136
- '<span class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300">Configured</span>'.html_safe
117
+ '<span class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-300">Configured</span>'.html_safe
137
118
  else
138
119
  '<span class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400">Not configured</span>'.html_safe
139
120
  end
@@ -254,7 +235,7 @@ module RubyLLM
254
235
  end
255
236
 
256
237
  if is_improvement
257
- content_tag(:span, class: "inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium text-green-700 dark:text-green-300 bg-green-100 dark:bg-green-900/50 rounded-full") do
238
+ content_tag(:span, class: "inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium text-green-700 dark:text-green-300 bg-green-100 dark:bg-green-500/20 rounded-full") do
258
239
  safe_join([
259
240
  content_tag(:svg, class: "w-3 h-3", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24") do
260
241
  content_tag(:path, nil, "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M5 10l7-7m0 0l7 7m-7-7v18")
@@ -263,7 +244,7 @@ module RubyLLM
263
244
  ])
264
245
  end
265
246
  elsif is_regression
266
- content_tag(:span, class: "inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium text-red-700 dark:text-red-300 bg-red-100 dark:bg-red-900/50 rounded-full") do
247
+ content_tag(:span, class: "inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium text-red-700 dark:text-red-300 bg-red-100 dark:bg-red-500/20 rounded-full") do
267
248
  safe_join([
268
249
  content_tag(:svg, class: "w-3 h-3", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24") do
269
250
  content_tag(:path, nil, "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M19 14l-7 7m0 0l-7-7m7 7V3")
@@ -306,31 +287,16 @@ module RubyLLM
306
287
 
307
288
  # Returns human-readable display name for time range
308
289
  #
309
- # @param range [String] Range parameter (today, 7d, 30d, 60d, 90d, or custom YYYY-MM-DD_YYYY-MM-DD)
290
+ # @param range [String] Range parameter (today, 7d, 30d)
310
291
  # @return [String] Human-readable range name
311
292
  # @example
312
- # range_display_name("7d") #=> "Last 7 Days"
313
- # range_display_name("2024-01-01_2024-01-15") #=> "Jan 1 - Jan 15"
293
+ # range_display_name("7d") #=> "7 Days"
314
294
  def range_display_name(range)
315
295
  case range
316
296
  when "today" then "Today"
317
- when "7d" then "Last 7 Days"
318
- when "30d" then "Last 30 Days"
319
- when "60d" then "Last 60 Days"
320
- when "90d" then "Last 90 Days"
321
- else
322
- if range&.include?("_")
323
- from_str, to_str = range.split("_")
324
- from_date = Date.parse(from_str) rescue nil
325
- to_date = Date.parse(to_str) rescue nil
326
- if from_date && to_date
327
- "#{from_date.strftime('%b %-d')} - #{to_date.strftime('%b %-d')}"
328
- else
329
- "Custom Range"
330
- end
331
- else
332
- "Today"
333
- end
297
+ when "7d" then "7 Days"
298
+ when "30d" then "30 Days"
299
+ else "Today"
334
300
  end
335
301
  end
336
302
 
@@ -391,7 +357,7 @@ module RubyLLM
391
357
  # @return [ActiveSupport::SafeBuffer] HTML summary banner
392
358
  def comparison_summary_badge(improvements_count, regressions_count, v2_label)
393
359
  if improvements_count >= 3 && regressions_count == 0
394
- content_tag(:span, class: "inline-flex items-center gap-1 px-3 py-1 text-sm font-medium text-green-700 dark:text-green-300 bg-green-100 dark:bg-green-900/50 rounded-lg") do
360
+ content_tag(:span, class: "inline-flex items-center gap-1 px-3 py-1 text-sm font-medium text-green-700 dark:text-green-300 bg-green-100 dark:bg-green-500/20 rounded-lg") do
395
361
  safe_join([
396
362
  content_tag(:svg, class: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24") do
397
363
  content_tag(:path, nil, "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z")
@@ -400,7 +366,7 @@ module RubyLLM
400
366
  ])
401
367
  end
402
368
  elsif regressions_count >= 3 && improvements_count == 0
403
- content_tag(:span, class: "inline-flex items-center gap-1 px-3 py-1 text-sm font-medium text-red-700 dark:text-red-300 bg-red-100 dark:bg-red-900/50 rounded-lg") do
369
+ content_tag(:span, class: "inline-flex items-center gap-1 px-3 py-1 text-sm font-medium text-red-700 dark:text-red-300 bg-red-100 dark:bg-red-500/20 rounded-lg") do
404
370
  safe_join([
405
371
  content_tag(:svg, class: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24") do
406
372
  content_tag(:path, nil, "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z")
@@ -409,7 +375,7 @@ module RubyLLM
409
375
  ])
410
376
  end
411
377
  elsif improvements_count > 0 || regressions_count > 0
412
- content_tag(:span, class: "inline-flex items-center gap-1 px-3 py-1 text-sm font-medium text-amber-700 dark:text-amber-300 bg-amber-100 dark:bg-amber-900/50 rounded-lg") do
378
+ content_tag(:span, class: "inline-flex items-center gap-1 px-3 py-1 text-sm font-medium text-amber-700 dark:text-amber-300 bg-amber-100 dark:bg-amber-500/20 rounded-lg") do
413
379
  safe_join([
414
380
  content_tag(:svg, class: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24") do
415
381
  content_tag(:path, nil, "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4")
@@ -552,14 +518,14 @@ module RubyLLM
552
518
 
553
519
  escaped_value = ERB::Util.html_escape(token[:value])
554
520
  if is_key
555
- result << %(<span class="text-purple-600">#{escaped_value}</span>)
521
+ result << %(<span class="text-purple-600 dark:text-purple-400">#{escaped_value}</span>)
556
522
  else
557
- result << %(<span class="text-green-600">#{escaped_value}</span>)
523
+ result << %(<span class="text-green-600 dark:text-green-400">#{escaped_value}</span>)
558
524
  end
559
525
  when :number
560
- result << %(<span class="text-blue-600">#{token[:value]}</span>)
526
+ result << %(<span class="text-blue-600 dark:text-blue-400">#{token[:value]}</span>)
561
527
  when :boolean
562
- result << %(<span class="text-amber-600">#{token[:value]}</span>)
528
+ result << %(<span class="text-amber-600 dark:text-amber-400">#{token[:value]}</span>)
563
529
  when :null
564
530
  result << %(<span class="text-gray-400">#{token[:value]}</span>)
565
531
  else