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
@@ -170,17 +170,30 @@ module RubyLLM
170
170
  effective_enforcement == :soft
171
171
  end
172
172
 
173
- # Check if within budget for a specific type
173
+ # Check if within budget for a specific type using counter columns
174
174
  #
175
175
  # @param type [Symbol] :daily_cost, :monthly_cost, :daily_tokens,
176
176
  # :monthly_tokens, :daily_executions, :monthly_executions
177
177
  # @return [Boolean]
178
178
  def within_budget?(type: :daily_cost)
179
- status = budget_status
180
- return true unless status[:enabled]
179
+ return true unless budgets_enabled?
181
180
 
182
- key = budget_status_key(type)
183
- (status.dig(key, :percentage_used) || 0) < 100
181
+ case type
182
+ when :daily_cost
183
+ within_daily_cost_budget?
184
+ when :monthly_cost
185
+ within_monthly_cost_budget?
186
+ when :daily_tokens
187
+ within_daily_token_budget?
188
+ when :monthly_tokens
189
+ within_monthly_token_budget?
190
+ when :daily_executions
191
+ within_daily_execution_budget?
192
+ when :monthly_executions
193
+ within_monthly_execution_budget?
194
+ else
195
+ true
196
+ end
184
197
  end
185
198
 
186
199
  # Get remaining budget for a specific type
@@ -188,9 +201,20 @@ module RubyLLM
188
201
  # @param type [Symbol] Budget type (see #within_budget?)
189
202
  # @return [Numeric, nil]
190
203
  def remaining_budget(type: :daily_cost)
191
- status = budget_status
192
- key = budget_status_key(type)
193
- status.dig(key, :remaining)
204
+ case type
205
+ when :daily_cost
206
+ effective_daily_limit && (ensure_daily_reset!; effective_daily_limit - daily_cost_spent)
207
+ when :monthly_cost
208
+ effective_monthly_limit && (ensure_monthly_reset!; effective_monthly_limit - monthly_cost_spent)
209
+ when :daily_tokens
210
+ effective_daily_token_limit && (ensure_daily_reset!; effective_daily_token_limit - daily_tokens_used)
211
+ when :monthly_tokens
212
+ effective_monthly_token_limit && (ensure_monthly_reset!; effective_monthly_token_limit - monthly_tokens_used)
213
+ when :daily_executions
214
+ effective_daily_execution_limit && (ensure_daily_reset!; effective_daily_execution_limit - daily_executions_count)
215
+ when :monthly_executions
216
+ effective_monthly_execution_limit && (ensure_monthly_reset!; effective_monthly_execution_limit - monthly_executions_count)
217
+ end
194
218
  end
195
219
 
196
220
  # Check budget and raise if exceeded (for hard enforcement)
@@ -198,14 +222,98 @@ module RubyLLM
198
222
  # @param agent_type [String] The agent class name
199
223
  # @raise [BudgetExceededError] If hard enforcement and over budget
200
224
  def check_budget!(agent_type = nil)
201
- BudgetTracker.check_budget!(agent_type || "Unknown", tenant_id: tenant_id)
225
+ return unless budgets_enabled?
226
+ return unless hard_enforcement?
227
+
228
+ ensure_daily_reset!
229
+ ensure_monthly_reset!
230
+
231
+ if effective_daily_limit && daily_cost_spent >= effective_daily_limit
232
+ raise Reliability::BudgetExceededError.new(
233
+ :global_daily, effective_daily_limit, daily_cost_spent, tenant_id: tenant_id
234
+ )
235
+ end
236
+
237
+ if effective_monthly_limit && monthly_cost_spent >= effective_monthly_limit
238
+ raise Reliability::BudgetExceededError.new(
239
+ :global_monthly, effective_monthly_limit, monthly_cost_spent, tenant_id: tenant_id
240
+ )
241
+ end
242
+
243
+ if effective_daily_token_limit && daily_tokens_used >= effective_daily_token_limit
244
+ raise Reliability::BudgetExceededError.new(
245
+ :global_daily_tokens, effective_daily_token_limit, daily_tokens_used, tenant_id: tenant_id
246
+ )
247
+ end
248
+
249
+ if effective_monthly_token_limit && monthly_tokens_used >= effective_monthly_token_limit
250
+ raise Reliability::BudgetExceededError.new(
251
+ :global_monthly_tokens, effective_monthly_token_limit, monthly_tokens_used, tenant_id: tenant_id
252
+ )
253
+ end
254
+
255
+ if effective_daily_execution_limit && daily_executions_count >= effective_daily_execution_limit
256
+ raise Reliability::BudgetExceededError.new(
257
+ :global_daily_executions, effective_daily_execution_limit, daily_executions_count, tenant_id: tenant_id
258
+ )
259
+ end
260
+
261
+ if effective_monthly_execution_limit && monthly_executions_count >= effective_monthly_execution_limit
262
+ raise Reliability::BudgetExceededError.new(
263
+ :global_monthly_executions, effective_monthly_execution_limit, monthly_executions_count, tenant_id: tenant_id
264
+ )
265
+ end
202
266
  end
203
267
 
204
- # Get full budget status from BudgetTracker
268
+ # Get full budget status using counter columns
205
269
  #
206
270
  # @return [Hash] Budget status with usage information
207
271
  def budget_status
208
- BudgetTracker.status(tenant_id: tenant_id)
272
+ ensure_daily_reset!
273
+ ensure_monthly_reset!
274
+
275
+ {
276
+ enabled: budgets_enabled?,
277
+ enforcement: effective_enforcement,
278
+ global_daily: budget_status_for(effective_daily_limit, daily_cost_spent),
279
+ global_monthly: budget_status_for(effective_monthly_limit, monthly_cost_spent),
280
+ global_daily_tokens: budget_status_for(effective_daily_token_limit, daily_tokens_used),
281
+ global_monthly_tokens: budget_status_for(effective_monthly_token_limit, monthly_tokens_used),
282
+ global_daily_executions: budget_status_for(effective_daily_execution_limit, daily_executions_count),
283
+ global_monthly_executions: budget_status_for(effective_monthly_execution_limit, monthly_executions_count)
284
+ }
285
+ end
286
+
287
+ # Individual budget check methods
288
+
289
+ def within_daily_cost_budget?
290
+ ensure_daily_reset!
291
+ effective_daily_limit.nil? || daily_cost_spent < effective_daily_limit
292
+ end
293
+
294
+ def within_monthly_cost_budget?
295
+ ensure_monthly_reset!
296
+ effective_monthly_limit.nil? || monthly_cost_spent < effective_monthly_limit
297
+ end
298
+
299
+ def within_daily_token_budget?
300
+ ensure_daily_reset!
301
+ effective_daily_token_limit.nil? || daily_tokens_used < effective_daily_token_limit
302
+ end
303
+
304
+ def within_monthly_token_budget?
305
+ ensure_monthly_reset!
306
+ effective_monthly_token_limit.nil? || monthly_tokens_used < effective_monthly_token_limit
307
+ end
308
+
309
+ def within_daily_execution_budget?
310
+ ensure_daily_reset!
311
+ effective_daily_execution_limit.nil? || daily_executions_count < effective_daily_execution_limit
312
+ end
313
+
314
+ def within_monthly_execution_budget?
315
+ ensure_monthly_reset!
316
+ effective_monthly_execution_limit.nil? || monthly_executions_count < effective_monthly_execution_limit
209
317
  end
210
318
 
211
319
  # Convert to config hash for BudgetTracker
@@ -256,20 +364,20 @@ module RubyLLM
256
364
  (global_config&.dig(:per_agent_monthly) || {}).merge(per_agent_monthly || {})
257
365
  end
258
366
 
259
- # Maps budget type to status key
367
+ # Builds a status hash for a single budget dimension
260
368
  #
261
- # @param type [Symbol]
262
- # @return [Symbol]
263
- def budget_status_key(type)
264
- case type
265
- when :daily_cost then :global_daily
266
- when :monthly_cost then :global_monthly
267
- when :daily_tokens then :global_daily_tokens
268
- when :monthly_tokens then :global_monthly_tokens
269
- when :daily_executions then :global_daily_executions
270
- when :monthly_executions then :global_monthly_executions
271
- else :global_daily
272
- end
369
+ # @param limit [Numeric, nil] The configured limit
370
+ # @param current [Numeric] The current usage
371
+ # @return [Hash, nil]
372
+ def budget_status_for(limit, current)
373
+ return nil unless limit
374
+
375
+ {
376
+ limit: limit,
377
+ current_spend: current,
378
+ remaining: [limit - current, 0].max,
379
+ percentage_used: (current.to_f / limit * 100).round(1)
380
+ }
273
381
  end
274
382
  end
275
383
  end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ class Tenant
6
+ # Provides atomic SQL increment of usage counters after each execution.
7
+ #
8
+ # @example Recording an execution
9
+ # tenant.record_execution!(cost: 0.05, tokens: 1200)
10
+ # tenant.record_execution!(cost: 0.01, tokens: 500, error: true)
11
+ #
12
+ # @api public
13
+ module Incrementable
14
+ extend ActiveSupport::Concern
15
+
16
+ # Records an execution by atomically incrementing all counter columns.
17
+ #
18
+ # @param cost [Numeric] The cost of the execution in USD
19
+ # @param tokens [Integer] The number of tokens used
20
+ # @param error [Boolean] Whether the execution was an error
21
+ # @return [void]
22
+ def record_execution!(cost:, tokens:, error: false)
23
+ ensure_daily_reset!
24
+ ensure_monthly_reset!
25
+
26
+ error_inc = error ? 1 : 0
27
+ status = error ? "error" : "success"
28
+
29
+ self.class.where(id: id).update_all(
30
+ self.class.sanitize_sql_array([
31
+ <<~SQL,
32
+ daily_cost_spent = daily_cost_spent + ?,
33
+ monthly_cost_spent = monthly_cost_spent + ?,
34
+ daily_tokens_used = daily_tokens_used + ?,
35
+ monthly_tokens_used = monthly_tokens_used + ?,
36
+ daily_executions_count = daily_executions_count + 1,
37
+ monthly_executions_count = monthly_executions_count + 1,
38
+ daily_error_count = daily_error_count + ?,
39
+ monthly_error_count = monthly_error_count + ?,
40
+ last_execution_at = ?,
41
+ last_execution_status = ?
42
+ SQL
43
+ cost.to_f, cost.to_f,
44
+ tokens.to_i, tokens.to_i,
45
+ error_inc, error_inc,
46
+ Time.current, status
47
+ ])
48
+ )
49
+
50
+ reload
51
+ check_soft_cap_alerts!
52
+ end
53
+
54
+ private
55
+
56
+ # Checks soft cap alerts after recording an execution.
57
+ #
58
+ # @return [void]
59
+ def check_soft_cap_alerts!
60
+ return unless soft_enforcement?
61
+
62
+ check_cost_alerts!
63
+ check_token_alerts!
64
+ check_execution_alerts!
65
+ end
66
+
67
+ # @return [void]
68
+ def check_cost_alerts!
69
+ if effective_daily_limit && daily_cost_spent >= effective_daily_limit
70
+ AlertManager.notify(:budget_soft_cap, {
71
+ tenant_id: tenant_id, type: :daily_cost,
72
+ limit: effective_daily_limit, total: daily_cost_spent
73
+ })
74
+ end
75
+ if effective_monthly_limit && monthly_cost_spent >= effective_monthly_limit
76
+ AlertManager.notify(:budget_soft_cap, {
77
+ tenant_id: tenant_id, type: :monthly_cost,
78
+ limit: effective_monthly_limit, total: monthly_cost_spent
79
+ })
80
+ end
81
+ end
82
+
83
+ # @return [void]
84
+ def check_token_alerts!
85
+ if effective_daily_token_limit && daily_tokens_used >= effective_daily_token_limit
86
+ AlertManager.notify(:token_soft_cap, {
87
+ tenant_id: tenant_id, type: :daily_tokens,
88
+ limit: effective_daily_token_limit, total: daily_tokens_used
89
+ })
90
+ end
91
+ if effective_monthly_token_limit && monthly_tokens_used >= effective_monthly_token_limit
92
+ AlertManager.notify(:token_soft_cap, {
93
+ tenant_id: tenant_id, type: :monthly_tokens,
94
+ limit: effective_monthly_token_limit, total: monthly_tokens_used
95
+ })
96
+ end
97
+ end
98
+
99
+ # @return [void]
100
+ def check_execution_alerts!
101
+ if effective_daily_execution_limit && daily_executions_count >= effective_daily_execution_limit
102
+ AlertManager.notify(:budget_soft_cap, {
103
+ tenant_id: tenant_id, type: :daily_executions,
104
+ limit: effective_daily_execution_limit, total: daily_executions_count
105
+ })
106
+ end
107
+ if effective_monthly_execution_limit && monthly_executions_count >= effective_monthly_execution_limit
108
+ AlertManager.notify(:budget_soft_cap, {
109
+ tenant_id: tenant_id, type: :monthly_executions,
110
+ limit: effective_monthly_execution_limit, total: monthly_executions_count
111
+ })
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ class Tenant
6
+ # Handles lazy reset of usage counters when periods roll over,
7
+ # and provides refresh_counters! for reconciliation from the executions table.
8
+ #
9
+ # @api public
10
+ module Resettable
11
+ extend ActiveSupport::Concern
12
+
13
+ # Resets daily counters if the day has rolled over.
14
+ # Uses a WHERE guard to prevent race conditions with concurrent requests.
15
+ #
16
+ # @return [void]
17
+ def ensure_daily_reset!
18
+ return if daily_reset_date == Date.current
19
+
20
+ rows = self.class.where(id: id)
21
+ .where("daily_reset_date IS NULL OR daily_reset_date < ?", Date.current)
22
+ .update_all(
23
+ daily_cost_spent: 0,
24
+ daily_tokens_used: 0,
25
+ daily_executions_count: 0,
26
+ daily_error_count: 0,
27
+ daily_reset_date: Date.current
28
+ )
29
+
30
+ reload if rows > 0
31
+ end
32
+
33
+ # Resets monthly counters if the month has rolled over.
34
+ # Uses a WHERE guard to prevent race conditions with concurrent requests.
35
+ #
36
+ # @return [void]
37
+ def ensure_monthly_reset!
38
+ bom = Date.current.beginning_of_month
39
+ return if monthly_reset_date == bom
40
+
41
+ rows = self.class.where(id: id)
42
+ .where("monthly_reset_date IS NULL OR monthly_reset_date < ?", bom)
43
+ .update_all(
44
+ monthly_cost_spent: 0,
45
+ monthly_tokens_used: 0,
46
+ monthly_executions_count: 0,
47
+ monthly_error_count: 0,
48
+ monthly_reset_date: bom
49
+ )
50
+
51
+ reload if rows > 0
52
+ end
53
+
54
+ # Recalculates all counters from the source-of-truth executions table.
55
+ #
56
+ # Use when counters have drifted due to manual DB edits, failed writes,
57
+ # or after deleting/updating execution records.
58
+ #
59
+ # @return [void]
60
+ def refresh_counters!
61
+ today = Date.current
62
+ bom = today.beginning_of_month
63
+
64
+ daily_stats = aggregate_stats(
65
+ executions.where("created_at >= ?", today.beginning_of_day)
66
+ )
67
+ monthly_stats = aggregate_stats(
68
+ executions.where("created_at >= ?", bom.beginning_of_day)
69
+ )
70
+
71
+ last_exec = executions.order(created_at: :desc).pick(:created_at, :status)
72
+
73
+ update_columns(
74
+ daily_cost_spent: daily_stats[:cost],
75
+ daily_tokens_used: daily_stats[:tokens],
76
+ daily_executions_count: daily_stats[:count],
77
+ daily_error_count: daily_stats[:errors],
78
+ daily_reset_date: today,
79
+
80
+ monthly_cost_spent: monthly_stats[:cost],
81
+ monthly_tokens_used: monthly_stats[:tokens],
82
+ monthly_executions_count: monthly_stats[:count],
83
+ monthly_error_count: monthly_stats[:errors],
84
+ monthly_reset_date: bom,
85
+
86
+ last_execution_at: last_exec&.first,
87
+ last_execution_status: last_exec&.last
88
+ )
89
+
90
+ reload
91
+ end
92
+
93
+ class_methods do
94
+ # Refresh counters for all tenants
95
+ #
96
+ # @return [void]
97
+ def refresh_all_counters!
98
+ find_each(&:refresh_counters!)
99
+ end
100
+
101
+ # Refresh counters for active tenants only
102
+ #
103
+ # @return [void]
104
+ def refresh_active_counters!
105
+ active.find_each(&:refresh_counters!)
106
+ end
107
+ end
108
+
109
+ private
110
+
111
+ # Aggregates cost, tokens, count, and errors from an executions scope.
112
+ #
113
+ # @param scope [ActiveRecord::Relation]
114
+ # @return [Hash] { cost:, tokens:, count:, errors: }
115
+ def aggregate_stats(scope)
116
+ agg = scope.pick(
117
+ Arel.sql("COALESCE(SUM(total_cost), 0)"),
118
+ Arel.sql("COALESCE(SUM(total_tokens), 0)"),
119
+ Arel.sql("COUNT(*)"),
120
+ Arel.sql("COALESCE(SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END), 0)")
121
+ )
122
+
123
+ { cost: agg[0], tokens: agg[1], count: agg[2], errors: agg[3] }
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -50,11 +50,12 @@ module RubyLLM
50
50
  scope.sum(:total_cost) || 0
51
51
  end
52
52
 
53
- # Returns today's cost
53
+ # Returns today's cost from counter columns
54
54
  #
55
55
  # @return [Float]
56
56
  def cost_today
57
- cost(period: :today)
57
+ ensure_daily_reset!
58
+ daily_cost_spent
58
59
  end
59
60
 
60
61
  # Returns yesterday's cost
@@ -78,11 +79,12 @@ module RubyLLM
78
79
  cost(period: :last_week)
79
80
  end
80
81
 
81
- # Returns this month's cost
82
+ # Returns this month's cost from counter columns
82
83
  #
83
84
  # @return [Float]
84
85
  def cost_this_month
85
- cost(period: :this_month)
86
+ ensure_monthly_reset!
87
+ monthly_cost_spent
86
88
  end
87
89
 
88
90
  # Returns last month's cost
@@ -104,11 +106,12 @@ module RubyLLM
104
106
  scope.sum(:total_tokens) || 0
105
107
  end
106
108
 
107
- # Returns today's token usage
109
+ # Returns today's token usage from counter columns
108
110
  #
109
111
  # @return [Integer]
110
112
  def tokens_today
111
- tokens(period: :today)
113
+ ensure_daily_reset!
114
+ daily_tokens_used
112
115
  end
113
116
 
114
117
  # Returns yesterday's token usage
@@ -125,11 +128,12 @@ module RubyLLM
125
128
  tokens(period: :this_week)
126
129
  end
127
130
 
128
- # Returns this month's token usage
131
+ # Returns this month's token usage from counter columns
129
132
  #
130
133
  # @return [Integer]
131
134
  def tokens_this_month
132
- tokens(period: :this_month)
135
+ ensure_monthly_reset!
136
+ monthly_tokens_used
133
137
  end
134
138
 
135
139
  # Returns last month's token usage
@@ -151,11 +155,12 @@ module RubyLLM
151
155
  scope.count
152
156
  end
153
157
 
154
- # Returns today's execution count
158
+ # Returns today's execution count from counter columns
155
159
  #
156
160
  # @return [Integer]
157
161
  def executions_today
158
- execution_count(period: :today)
162
+ ensure_daily_reset!
163
+ daily_executions_count
159
164
  end
160
165
 
161
166
  # Returns yesterday's execution count
@@ -172,11 +177,12 @@ module RubyLLM
172
177
  execution_count(period: :this_week)
173
178
  end
174
179
 
175
- # Returns this month's execution count
180
+ # Returns this month's execution count from counter columns
176
181
  #
177
182
  # @return [Integer]
178
183
  def executions_this_month
179
- execution_count(period: :this_month)
184
+ ensure_monthly_reset!
185
+ monthly_executions_count
180
186
  end
181
187
 
182
188
  # Returns last month's execution count
@@ -186,6 +192,34 @@ module RubyLLM
186
192
  execution_count(period: :last_month)
187
193
  end
188
194
 
195
+ # Error count queries
196
+
197
+ # Returns today's error count from counter columns
198
+ #
199
+ # @return [Integer]
200
+ def errors_today
201
+ ensure_daily_reset!
202
+ daily_error_count
203
+ end
204
+
205
+ # Returns this month's error count from counter columns
206
+ #
207
+ # @return [Integer]
208
+ def errors_this_month
209
+ ensure_monthly_reset!
210
+ monthly_error_count
211
+ end
212
+
213
+ # Returns today's success rate from counter columns
214
+ #
215
+ # @return [Float] Percentage (0.0-100.0)
216
+ def success_rate_today
217
+ ensure_daily_reset!
218
+ return 100.0 if daily_executions_count.zero?
219
+
220
+ ((daily_executions_count - daily_error_count).to_f / daily_executions_count * 100).round(1)
221
+ end
222
+
189
223
  # Usage summaries
190
224
 
191
225
  # Returns a complete usage summary for the tenant
@@ -7,7 +7,6 @@ module RubyLLM
7
7
  # Encapsulates all tenant-related functionality:
8
8
  # - Budget limits and enforcement (via Budgetable concern)
9
9
  # - Usage tracking: cost, tokens, executions (via Trackable concern)
10
- # - API configuration per tenant (via Configurable concern)
11
10
  #
12
11
  # @example Creating a tenant
13
12
  # Tenant.create!(
@@ -30,7 +29,6 @@ module RubyLLM
30
29
  #
31
30
  # @see Tenant::Budgetable
32
31
  # @see Tenant::Trackable
33
- # @see Tenant::Configurable
34
32
  # @see LLMTenant
35
33
  # @api public
36
34
  class Tenant < ::ActiveRecord::Base
@@ -39,7 +37,8 @@ module RubyLLM
39
37
  # Include concerns for organized functionality
40
38
  include Tenant::Budgetable
41
39
  include Tenant::Trackable
42
- include Tenant::Configurable
40
+ include Tenant::Resettable
41
+ include Tenant::Incrementable
43
42
 
44
43
  # Polymorphic association to user's tenant model (optional)
45
44
  # Allows linking to Organization, Account, or any ActiveRecord model
@@ -8,15 +8,18 @@ module RubyLLM
8
8
  # All functionality has been moved to the Tenant model with organized concerns.
9
9
  #
10
10
  # @example Migration path
11
- # # Old usage (still works)
11
+ # # Old usage (still works but emits deprecation warning)
12
12
  # TenantBudget.for_tenant("acme_corp")
13
- # TenantBudget.create!(tenant_id: "acme", daily_limit: 100)
14
13
  #
15
14
  # # New usage (preferred)
16
15
  # Tenant.for("acme_corp")
17
- # Tenant.create!(tenant_id: "acme", daily_limit: 100)
18
16
  #
19
17
  # @see Tenant
20
18
  TenantBudget = Tenant
19
+
20
+ ActiveSupport.deprecator.warn(
21
+ "RubyLLM::Agents::TenantBudget is deprecated. Use RubyLLM::Agents::Tenant instead. " \
22
+ "This alias will be removed in the next major version."
23
+ )
21
24
  end
22
25
  end