ruby_llm-agents 1.3.4 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +112 -336
- data/app/controllers/concerns/ruby_llm/agents/sortable.rb +0 -1
- data/app/controllers/ruby_llm/agents/agents_controller.rb +5 -56
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +22 -106
- data/app/controllers/ruby_llm/agents/executions_controller.rb +4 -114
- data/app/controllers/ruby_llm/agents/tenants_controller.rb +30 -2
- data/app/helpers/ruby_llm/agents/application_helper.rb +19 -53
- data/app/models/ruby_llm/agents/execution/analytics.rb +13 -54
- data/app/models/ruby_llm/agents/execution/scopes.rb +61 -14
- data/app/models/ruby_llm/agents/execution.rb +52 -12
- data/app/models/ruby_llm/agents/execution_detail.rb +18 -0
- data/app/models/ruby_llm/agents/tenant/budgetable.rb +132 -24
- data/app/models/ruby_llm/agents/tenant/incrementable.rb +117 -0
- data/app/models/ruby_llm/agents/tenant/resettable.rb +128 -0
- data/app/models/ruby_llm/agents/tenant/trackable.rb +46 -12
- data/app/models/ruby_llm/agents/tenant.rb +2 -3
- data/app/models/ruby_llm/agents/tenant_budget.rb +6 -3
- data/app/services/ruby_llm/agents/agent_registry.rb +6 -112
- data/app/views/layouts/ruby_llm/agents/application.html.erb +89 -252
- data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +71 -218
- data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +20 -63
- data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +44 -131
- data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +16 -57
- data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +39 -104
- data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +29 -82
- data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +4 -14
- data/app/views/ruby_llm/agents/agents/index.html.erb +105 -274
- data/app/views/ruby_llm/agents/agents/show.html.erb +248 -378
- data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +29 -52
- data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +73 -99
- data/app/views/ruby_llm/agents/dashboard/index.html.erb +228 -433
- data/app/views/ruby_llm/agents/executions/_execution.html.erb +1 -1
- data/app/views/ruby_llm/agents/executions/_filters.html.erb +4 -25
- data/app/views/ruby_llm/agents/executions/_list.html.erb +111 -152
- data/app/views/ruby_llm/agents/executions/index.html.erb +5 -7
- data/app/views/ruby_llm/agents/executions/show.html.erb +526 -1037
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +5 -21
- data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +70 -191
- data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +16 -44
- data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +12 -41
- data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +11 -65
- data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +6 -5
- data/app/views/ruby_llm/agents/system_config/show.html.erb +240 -351
- data/app/views/ruby_llm/agents/tenants/_form.html.erb +67 -77
- data/app/views/ruby_llm/agents/tenants/edit.html.erb +7 -9
- data/app/views/ruby_llm/agents/tenants/index.html.erb +100 -122
- data/app/views/ruby_llm/agents/tenants/show.html.erb +146 -336
- data/config/routes.rb +0 -13
- data/lib/generators/ruby_llm_agents/install_generator.rb +13 -17
- data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +2 -12
- data/lib/generators/ruby_llm_agents/restructure_generator.rb +0 -2
- data/lib/generators/ruby_llm_agents/templates/add_usage_counters_to_tenants_migration.rb.tt +37 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +1 -2
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +1 -1
- data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +0 -1
- data/lib/generators/ruby_llm_agents/templates/create_execution_details_migration.rb.tt +27 -0
- data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +25 -0
- data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +0 -1
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +33 -12
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +40 -71
- data/lib/generators/ruby_llm_agents/templates/remove_agent_version_migration.rb.tt +13 -0
- data/lib/generators/ruby_llm_agents/templates/remove_workflow_columns_migration.rb.tt +19 -0
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +2 -4
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +0 -1
- data/lib/generators/ruby_llm_agents/templates/split_execution_details_migration.rb.tt +232 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +77 -259
- data/lib/ruby_llm/agents/audio/speaker.rb +0 -1
- data/lib/ruby_llm/agents/audio/transcriber.rb +0 -1
- data/lib/ruby_llm/agents/base_agent.rb +54 -23
- data/lib/ruby_llm/agents/core/base/callbacks.rb +142 -0
- data/lib/ruby_llm/agents/core/base.rb +23 -55
- data/lib/ruby_llm/agents/core/configuration.rb +97 -117
- data/lib/ruby_llm/agents/core/errors.rb +0 -58
- data/lib/ruby_llm/agents/core/instrumentation.rb +157 -110
- data/lib/ruby_llm/agents/core/llm_tenant.rb +8 -7
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/dsl/base.rb +157 -17
- data/lib/ruby_llm/agents/dsl/caching.rb +33 -2
- data/lib/ruby_llm/agents/dsl/reliability.rb +148 -0
- data/lib/ruby_llm/agents/dsl.rb +1 -2
- data/lib/ruby_llm/agents/image/analyzer/execution.rb +1 -2
- data/lib/ruby_llm/agents/image/background_remover/execution.rb +1 -2
- data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +1 -13
- data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -2
- data/lib/ruby_llm/agents/image/editor/dsl.rb +0 -14
- data/lib/ruby_llm/agents/image/editor/execution.rb +1 -10
- data/lib/ruby_llm/agents/image/editor.rb +0 -1
- data/lib/ruby_llm/agents/image/generator.rb +0 -21
- data/lib/ruby_llm/agents/image/pipeline/dsl.rb +0 -13
- data/lib/ruby_llm/agents/image/pipeline/execution.rb +0 -1
- data/lib/ruby_llm/agents/image/transformer/dsl.rb +0 -13
- data/lib/ruby_llm/agents/image/transformer/execution.rb +1 -10
- data/lib/ruby_llm/agents/image/transformer.rb +0 -1
- data/lib/ruby_llm/agents/image/upscaler/execution.rb +1 -2
- data/lib/ruby_llm/agents/image/variator/execution.rb +1 -2
- data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +78 -173
- data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
- data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
- data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
- data/lib/ruby_llm/agents/infrastructure/execution_logger_job.rb +8 -0
- data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
- data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
- data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
- data/lib/ruby_llm/agents/rails/engine.rb +6 -6
- data/lib/ruby_llm/agents/results/base.rb +1 -49
- data/lib/ruby_llm/agents/text/embedder.rb +0 -1
- data/lib/ruby_llm/agents.rb +1 -9
- data/lib/tasks/ruby_llm_agents.rake +34 -0
- metadata +14 -83
- data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
- data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
- data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
- data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
- data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
- data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
- data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
- data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
- data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
- data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
- data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
- data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
- data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
- data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
- data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
- data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
- data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
- data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
- data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
- data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
- data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
- data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
- data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
- data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
- data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
- data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
- data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
- data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
- data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
- data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
- data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
- data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
- data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
- data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
- data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
- data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
- data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
- data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
- data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
- data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
- data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
- data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
- data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
- data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
- data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
- data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
- data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
- data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
- data/lib/ruby_llm/agents/text/moderator.rb +0 -237
- data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
- data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
- data/lib/ruby_llm/agents/workflow/async.rb +0 -220
- data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
- data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
- data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
- data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
- data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
- data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
- data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
- data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
- data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
- data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
- data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
- data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
- data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
- data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
- data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
- data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
- data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
- data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
- data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
- data/lib/ruby_llm/agents/workflow/result.rb +0 -592
- data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
- data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
- data/lib/ruby_llm/agents/workflow/wait_result.rb +0 -213
|
@@ -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
|
-
|
|
180
|
-
return true unless status[:enabled]
|
|
179
|
+
return true unless budgets_enabled?
|
|
181
180
|
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
367
|
+
# Builds a status hash for a single budget dimension
|
|
260
368
|
#
|
|
261
|
-
# @param
|
|
262
|
-
# @
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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::
|
|
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
|