ruby_llm-agents 0.5.0 → 1.0.0.beta.1
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 +189 -31
- data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
- data/app/controllers/ruby_llm/agents/workflows_controller.rb +355 -0
- data/app/helpers/ruby_llm/agents/application_helper.rb +25 -0
- data/app/models/ruby_llm/agents/execution.rb +3 -0
- data/app/models/ruby_llm/agents/tenant_budget.rb +58 -15
- data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
- data/app/views/layouts/ruby_llm/agents/application.html.erb +2 -29
- data/app/views/ruby_llm/agents/agents/_agent.html.erb +13 -1
- data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +235 -0
- data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +70 -0
- data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +152 -0
- data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +63 -0
- data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +108 -0
- data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +91 -0
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +1 -1
- data/app/views/ruby_llm/agents/agents/index.html.erb +74 -9
- data/app/views/ruby_llm/agents/agents/show.html.erb +18 -378
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +269 -15
- data/app/views/ruby_llm/agents/executions/show.html.erb +16 -0
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -0
- data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +236 -0
- data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +76 -0
- data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +74 -0
- data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +108 -0
- data/app/views/ruby_llm/agents/workflows/show.html.erb +442 -0
- data/config/routes.rb +1 -0
- data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
- data/lib/generators/ruby_llm_agents/background_remover_generator.rb +110 -0
- data/lib/generators/ruby_llm_agents/embedder_generator.rb +107 -0
- data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +115 -0
- data/lib/generators/ruby_llm_agents/image_editor_generator.rb +108 -0
- data/lib/generators/ruby_llm_agents/image_generator_generator.rb +116 -0
- data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +178 -0
- data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +109 -0
- data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +103 -0
- data/lib/generators/ruby_llm_agents/image_variator_generator.rb +102 -0
- data/lib/generators/ruby_llm_agents/install_generator.rb +76 -4
- data/lib/generators/ruby_llm_agents/restructure_generator.rb +292 -0
- data/lib/generators/ruby_llm_agents/speaker_generator.rb +121 -0
- data/lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +99 -84
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +42 -40
- data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +26 -0
- data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +50 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +26 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +38 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +139 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +21 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +49 -0
- data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +53 -0
- data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +44 -0
- data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +41 -0
- data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +45 -0
- data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +35 -0
- data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +47 -0
- data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +50 -0
- data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +44 -0
- data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +38 -0
- data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +33 -0
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +228 -0
- data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +131 -0
- data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +255 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +102 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +282 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +228 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +110 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +212 -0
- data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +227 -0
- data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +251 -0
- data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +300 -0
- data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +56 -0
- data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +51 -0
- data/lib/generators/ruby_llm_agents/transcriber_generator.rb +107 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +152 -1
- data/lib/ruby_llm/agents/audio/speaker.rb +553 -0
- data/lib/ruby_llm/agents/audio/transcriber.rb +669 -0
- data/lib/ruby_llm/agents/base_agent.rb +675 -0
- data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +181 -0
- data/lib/ruby_llm/agents/core/base/moderation_execution.rb +274 -0
- data/lib/ruby_llm/agents/core/base.rb +135 -0
- data/lib/ruby_llm/agents/core/configuration.rb +981 -0
- data/lib/ruby_llm/agents/core/errors.rb +150 -0
- data/lib/ruby_llm/agents/{instrumentation.rb → core/instrumentation.rb} +22 -1
- data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -0
- data/lib/ruby_llm/agents/{version.rb → core/version.rb} +1 -1
- data/lib/ruby_llm/agents/dsl/base.rb +110 -0
- data/lib/ruby_llm/agents/dsl/caching.rb +142 -0
- data/lib/ruby_llm/agents/dsl/reliability.rb +307 -0
- data/lib/ruby_llm/agents/dsl.rb +41 -0
- data/lib/ruby_llm/agents/image/analyzer/dsl.rb +130 -0
- data/lib/ruby_llm/agents/image/analyzer/execution.rb +402 -0
- data/lib/ruby_llm/agents/image/analyzer.rb +90 -0
- data/lib/ruby_llm/agents/image/background_remover/dsl.rb +154 -0
- data/lib/ruby_llm/agents/image/background_remover/execution.rb +240 -0
- data/lib/ruby_llm/agents/image/background_remover.rb +89 -0
- data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +91 -0
- data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +165 -0
- data/lib/ruby_llm/agents/image/editor/dsl.rb +56 -0
- data/lib/ruby_llm/agents/image/editor/execution.rb +207 -0
- data/lib/ruby_llm/agents/image/editor.rb +92 -0
- data/lib/ruby_llm/agents/image/generator/active_storage_support.rb +127 -0
- data/lib/ruby_llm/agents/image/generator/content_policy.rb +95 -0
- data/lib/ruby_llm/agents/image/generator/pricing.rb +353 -0
- data/lib/ruby_llm/agents/image/generator/templates.rb +124 -0
- data/lib/ruby_llm/agents/image/generator.rb +455 -0
- data/lib/ruby_llm/agents/image/pipeline/dsl.rb +213 -0
- data/lib/ruby_llm/agents/image/pipeline/execution.rb +382 -0
- data/lib/ruby_llm/agents/image/pipeline.rb +97 -0
- data/lib/ruby_llm/agents/image/transformer/dsl.rb +148 -0
- data/lib/ruby_llm/agents/image/transformer/execution.rb +223 -0
- data/lib/ruby_llm/agents/image/transformer.rb +95 -0
- data/lib/ruby_llm/agents/image/upscaler/dsl.rb +83 -0
- data/lib/ruby_llm/agents/image/upscaler/execution.rb +219 -0
- data/lib/ruby_llm/agents/image/upscaler.rb +81 -0
- data/lib/ruby_llm/agents/image/variator/dsl.rb +62 -0
- data/lib/ruby_llm/agents/image/variator/execution.rb +189 -0
- data/lib/ruby_llm/agents/image/variator.rb +80 -0
- data/lib/ruby_llm/agents/{alert_manager.rb → infrastructure/alert_manager.rb} +17 -22
- data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +145 -0
- data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +149 -0
- data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +68 -0
- data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +279 -0
- data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +275 -0
- data/lib/ruby_llm/agents/{execution_logger_job.rb → infrastructure/execution_logger_job.rb} +17 -1
- data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/executor.rb +2 -1
- data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/retry_strategy.rb +9 -3
- data/lib/ruby_llm/agents/{reliability.rb → infrastructure/reliability.rb} +11 -21
- data/lib/ruby_llm/agents/pipeline/builder.rb +215 -0
- data/lib/ruby_llm/agents/pipeline/context.rb +255 -0
- data/lib/ruby_llm/agents/pipeline/executor.rb +86 -0
- data/lib/ruby_llm/agents/pipeline/middleware/base.rb +124 -0
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +95 -0
- data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +171 -0
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +415 -0
- data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +276 -0
- data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +196 -0
- data/lib/ruby_llm/agents/pipeline.rb +68 -0
- data/lib/ruby_llm/agents/{engine.rb → rails/engine.rb} +79 -11
- data/lib/ruby_llm/agents/results/background_removal_result.rb +286 -0
- data/lib/ruby_llm/agents/{result.rb → results/base.rb} +73 -1
- data/lib/ruby_llm/agents/results/embedding_result.rb +243 -0
- data/lib/ruby_llm/agents/results/image_analysis_result.rb +314 -0
- data/lib/ruby_llm/agents/results/image_edit_result.rb +250 -0
- data/lib/ruby_llm/agents/results/image_generation_result.rb +346 -0
- data/lib/ruby_llm/agents/results/image_pipeline_result.rb +399 -0
- data/lib/ruby_llm/agents/results/image_transform_result.rb +251 -0
- data/lib/ruby_llm/agents/results/image_upscale_result.rb +255 -0
- data/lib/ruby_llm/agents/results/image_variation_result.rb +237 -0
- data/lib/ruby_llm/agents/results/moderation_result.rb +158 -0
- data/lib/ruby_llm/agents/results/speech_result.rb +338 -0
- data/lib/ruby_llm/agents/results/transcription_result.rb +408 -0
- data/lib/ruby_llm/agents/text/embedder.rb +444 -0
- data/lib/ruby_llm/agents/text/moderator.rb +237 -0
- data/lib/ruby_llm/agents/workflow/async.rb +220 -0
- data/lib/ruby_llm/agents/workflow/async_executor.rb +156 -0
- data/lib/ruby_llm/agents/{workflow.rb → workflow/orchestrator.rb} +6 -5
- data/lib/ruby_llm/agents/workflow/parallel.rb +34 -17
- data/lib/ruby_llm/agents/workflow/thread_pool.rb +185 -0
- data/lib/ruby_llm/agents.rb +86 -20
- metadata +172 -34
- data/lib/ruby_llm/agents/base/caching.rb +0 -40
- data/lib/ruby_llm/agents/base/cost_calculation.rb +0 -105
- data/lib/ruby_llm/agents/base/dsl.rb +0 -324
- data/lib/ruby_llm/agents/base/execution.rb +0 -366
- data/lib/ruby_llm/agents/base/reliability_dsl.rb +0 -82
- data/lib/ruby_llm/agents/base/reliability_execution.rb +0 -136
- data/lib/ruby_llm/agents/base/response_building.rb +0 -86
- data/lib/ruby_llm/agents/base/tool_tracking.rb +0 -57
- data/lib/ruby_llm/agents/base.rb +0 -210
- data/lib/ruby_llm/agents/budget_tracker.rb +0 -733
- data/lib/ruby_llm/agents/configuration.rb +0 -394
- /data/lib/ruby_llm/agents/{deprecations.rb → core/deprecations.rb} +0 -0
- /data/lib/ruby_llm/agents/{inflections.rb → core/inflections.rb} +0 -0
- /data/lib/ruby_llm/agents/{resolved_config.rb → core/resolved_config.rb} +0 -0
- /data/lib/ruby_llm/agents/{attempt_tracker.rb → infrastructure/attempt_tracker.rb} +0 -0
- /data/lib/ruby_llm/agents/{cache_helper.rb → infrastructure/cache_helper.rb} +0 -0
- /data/lib/ruby_llm/agents/{circuit_breaker.rb → infrastructure/circuit_breaker.rb} +0 -0
- /data/lib/ruby_llm/agents/{redactor.rb → infrastructure/redactor.rb} +0 -0
- /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/breaker_manager.rb +0 -0
- /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/execution_constraints.rb +0 -0
- /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/fallback_routing.rb +0 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
module Budget
|
|
6
|
+
# Budget forecasting based on current spending trends
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
9
|
+
module Forecaster
|
|
10
|
+
class << self
|
|
11
|
+
# Calculates budget forecasts based on current spending trends
|
|
12
|
+
#
|
|
13
|
+
# @param tenant_id [String, nil] The tenant identifier
|
|
14
|
+
# @param budget_config [Hash] Budget configuration
|
|
15
|
+
# @return [Hash, nil] Forecast information
|
|
16
|
+
def calculate_forecast(tenant_id: nil, budget_config:)
|
|
17
|
+
return nil unless budget_config[:enabled]
|
|
18
|
+
return nil unless budget_config[:global_daily] || budget_config[:global_monthly]
|
|
19
|
+
|
|
20
|
+
daily_current = BudgetQuery.current_spend(:global, :daily, tenant_id: tenant_id)
|
|
21
|
+
monthly_current = BudgetQuery.current_spend(:global, :monthly, tenant_id: tenant_id)
|
|
22
|
+
|
|
23
|
+
# Calculate hours elapsed today and days elapsed this month
|
|
24
|
+
hours_elapsed = Time.current.hour + (Time.current.min / 60.0)
|
|
25
|
+
hours_elapsed = [hours_elapsed, 1].max # Avoid division by zero
|
|
26
|
+
days_in_month = Time.current.end_of_month.day
|
|
27
|
+
day_of_month = Time.current.day
|
|
28
|
+
days_elapsed = day_of_month - 1 + (hours_elapsed / 24.0)
|
|
29
|
+
days_elapsed = [days_elapsed, 1].max
|
|
30
|
+
|
|
31
|
+
forecast = {}
|
|
32
|
+
|
|
33
|
+
# Daily forecast
|
|
34
|
+
if budget_config[:global_daily]
|
|
35
|
+
daily_rate = daily_current / hours_elapsed
|
|
36
|
+
projected_daily = daily_rate * 24
|
|
37
|
+
forecast[:daily] = {
|
|
38
|
+
current: daily_current.round(4),
|
|
39
|
+
projected: projected_daily.round(4),
|
|
40
|
+
limit: budget_config[:global_daily],
|
|
41
|
+
on_track: projected_daily <= budget_config[:global_daily],
|
|
42
|
+
hours_remaining: (24 - hours_elapsed).round(1),
|
|
43
|
+
rate_per_hour: daily_rate.round(6)
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Monthly forecast
|
|
48
|
+
if budget_config[:global_monthly]
|
|
49
|
+
monthly_rate = monthly_current / days_elapsed
|
|
50
|
+
projected_monthly = monthly_rate * days_in_month
|
|
51
|
+
days_remaining = days_in_month - day_of_month
|
|
52
|
+
forecast[:monthly] = {
|
|
53
|
+
current: monthly_current.round(4),
|
|
54
|
+
projected: projected_monthly.round(4),
|
|
55
|
+
limit: budget_config[:global_monthly],
|
|
56
|
+
on_track: projected_monthly <= budget_config[:global_monthly],
|
|
57
|
+
days_remaining: days_remaining,
|
|
58
|
+
rate_per_day: monthly_rate.round(4)
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
forecast.presence
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../cache_helper"
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module Agents
|
|
7
|
+
module Budget
|
|
8
|
+
# Records spend and token usage, and handles soft cap alerting
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module SpendRecorder
|
|
12
|
+
extend CacheHelper
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
# Records spend and checks for soft cap alerts
|
|
16
|
+
#
|
|
17
|
+
# @param agent_type [String] The agent class name
|
|
18
|
+
# @param amount [Float] The amount spent in USD
|
|
19
|
+
# @param tenant_id [String, nil] The tenant identifier
|
|
20
|
+
# @param budget_config [Hash] Budget configuration
|
|
21
|
+
# @return [void]
|
|
22
|
+
def record_spend!(agent_type, amount, tenant_id:, budget_config:)
|
|
23
|
+
return if amount.nil? || amount <= 0
|
|
24
|
+
|
|
25
|
+
# Increment all relevant counters
|
|
26
|
+
increment_spend(:global, :daily, amount, tenant_id: tenant_id)
|
|
27
|
+
increment_spend(:global, :monthly, amount, tenant_id: tenant_id)
|
|
28
|
+
increment_spend(:agent, :daily, amount, agent_type: agent_type, tenant_id: tenant_id)
|
|
29
|
+
increment_spend(:agent, :monthly, amount, agent_type: agent_type, tenant_id: tenant_id)
|
|
30
|
+
|
|
31
|
+
# Check for soft cap alerts
|
|
32
|
+
check_soft_cap_alerts(agent_type, tenant_id, budget_config) if budget_config[:enabled]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Records token usage and checks for soft cap alerts
|
|
36
|
+
#
|
|
37
|
+
# @param agent_type [String] The agent class name
|
|
38
|
+
# @param tokens [Integer] The number of tokens used
|
|
39
|
+
# @param tenant_id [String, nil] The tenant identifier
|
|
40
|
+
# @param budget_config [Hash] Budget configuration
|
|
41
|
+
# @return [void]
|
|
42
|
+
def record_tokens!(agent_type, tokens, tenant_id:, budget_config:)
|
|
43
|
+
return if tokens.nil? || tokens <= 0
|
|
44
|
+
|
|
45
|
+
# Increment global token counters (daily and monthly)
|
|
46
|
+
# Note: We only track global token usage, not per-agent (scope is ignored in increment_tokens)
|
|
47
|
+
increment_tokens(:global, :daily, tokens, tenant_id: tenant_id)
|
|
48
|
+
increment_tokens(:global, :monthly, tokens, tenant_id: tenant_id)
|
|
49
|
+
|
|
50
|
+
# Check for soft cap alerts
|
|
51
|
+
check_soft_token_alerts(agent_type, tenant_id, budget_config) if budget_config[:enabled]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Increments the spend counter for a scope and period
|
|
55
|
+
#
|
|
56
|
+
# @param scope [Symbol] :global or :agent
|
|
57
|
+
# @param period [Symbol] :daily or :monthly
|
|
58
|
+
# @param amount [Float] Amount to add
|
|
59
|
+
# @param agent_type [String, nil] Required when scope is :agent
|
|
60
|
+
# @param tenant_id [String, nil] The tenant identifier
|
|
61
|
+
# @return [Float] New total
|
|
62
|
+
def increment_spend(scope, period, amount, agent_type: nil, tenant_id: nil)
|
|
63
|
+
key = budget_cache_key(scope, period, agent_type: agent_type, tenant_id: tenant_id)
|
|
64
|
+
ttl = period == :daily ? 1.day : 31.days
|
|
65
|
+
|
|
66
|
+
# Read-modify-write for float values (cache increment is for integers)
|
|
67
|
+
current = (SpendRecorder.cache_read(key) || 0).to_f
|
|
68
|
+
new_total = current + amount
|
|
69
|
+
SpendRecorder.cache_write(key, new_total, expires_in: ttl)
|
|
70
|
+
new_total
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Increments the token counter for a period
|
|
74
|
+
#
|
|
75
|
+
# @param scope [Symbol] :global (only global supported for tokens)
|
|
76
|
+
# @param period [Symbol] :daily or :monthly
|
|
77
|
+
# @param tokens [Integer] Tokens to add
|
|
78
|
+
# @param agent_type [String, nil] Not used for tokens
|
|
79
|
+
# @param tenant_id [String, nil] The tenant identifier
|
|
80
|
+
# @return [Integer] New total
|
|
81
|
+
def increment_tokens(scope, period, tokens, agent_type: nil, tenant_id: nil)
|
|
82
|
+
# For now, we only track global token usage (not per-agent)
|
|
83
|
+
key = token_cache_key(period, tenant_id: tenant_id)
|
|
84
|
+
ttl = period == :daily ? 1.day : 31.days
|
|
85
|
+
|
|
86
|
+
current = (SpendRecorder.cache_read(key) || 0).to_i
|
|
87
|
+
new_total = current + tokens
|
|
88
|
+
SpendRecorder.cache_write(key, new_total, expires_in: ttl)
|
|
89
|
+
new_total
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns the tenant key part for cache keys
|
|
93
|
+
#
|
|
94
|
+
# @param tenant_id [String, nil] The tenant identifier
|
|
95
|
+
# @return [String] "tenant:{id}" or "global"
|
|
96
|
+
def tenant_key_part(tenant_id)
|
|
97
|
+
tenant_id.present? ? "tenant:#{tenant_id}" : "global"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Returns the date key part for cache keys based on period
|
|
101
|
+
#
|
|
102
|
+
# @param period [Symbol] :daily or :monthly
|
|
103
|
+
# @return [String] Date string
|
|
104
|
+
def date_key_part(period)
|
|
105
|
+
period == :daily ? Date.current.to_s : Date.current.strftime("%Y-%m")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Generates an alert cache key
|
|
109
|
+
#
|
|
110
|
+
# @param alert_type [String] Type of alert (e.g., "budget_alert", "token_alert")
|
|
111
|
+
# @param scope [Symbol] Alert scope
|
|
112
|
+
# @param tenant_id [String, nil] The tenant identifier
|
|
113
|
+
# @return [String] Cache key
|
|
114
|
+
def alert_cache_key(alert_type, scope, tenant_id)
|
|
115
|
+
SpendRecorder.cache_key(alert_type, tenant_key_part(tenant_id), scope, Date.current.to_s)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Generates a cache key for budget tracking
|
|
119
|
+
#
|
|
120
|
+
# @param scope [Symbol] :global or :agent
|
|
121
|
+
# @param period [Symbol] :daily or :monthly
|
|
122
|
+
# @param agent_type [String, nil] Required when scope is :agent
|
|
123
|
+
# @param tenant_id [String, nil] The tenant identifier
|
|
124
|
+
# @return [String] Cache key
|
|
125
|
+
def budget_cache_key(scope, period, agent_type: nil, tenant_id: nil)
|
|
126
|
+
date_part = date_key_part(period)
|
|
127
|
+
tenant_part = tenant_key_part(tenant_id)
|
|
128
|
+
|
|
129
|
+
case scope
|
|
130
|
+
when :global
|
|
131
|
+
SpendRecorder.cache_key("budget", tenant_part, date_part)
|
|
132
|
+
when :agent
|
|
133
|
+
SpendRecorder.cache_key("budget", tenant_part, "agent", agent_type, date_part)
|
|
134
|
+
else
|
|
135
|
+
raise ArgumentError, "Unknown scope: #{scope}"
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Generates a cache key for token tracking
|
|
140
|
+
#
|
|
141
|
+
# @param period [Symbol] :daily or :monthly
|
|
142
|
+
# @param tenant_id [String, nil] The tenant identifier
|
|
143
|
+
# @return [String] Cache key
|
|
144
|
+
def token_cache_key(period, tenant_id: nil)
|
|
145
|
+
SpendRecorder.cache_key("tokens", tenant_key_part(tenant_id), date_key_part(period))
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
# Checks for soft cap alerts after recording spend
|
|
151
|
+
#
|
|
152
|
+
# @param agent_type [String] The agent class name
|
|
153
|
+
# @param tenant_id [String, nil] The tenant identifier
|
|
154
|
+
# @param budget_config [Hash] Budget configuration
|
|
155
|
+
# @return [void]
|
|
156
|
+
def check_soft_cap_alerts(agent_type, tenant_id, budget_config)
|
|
157
|
+
config = RubyLLM::Agents.configuration
|
|
158
|
+
return unless config.alerts_enabled?
|
|
159
|
+
return unless config.alert_events.include?(:budget_soft_cap) || config.alert_events.include?(:budget_hard_cap)
|
|
160
|
+
|
|
161
|
+
# Check global daily
|
|
162
|
+
check_budget_alert(:global_daily, budget_config[:global_daily],
|
|
163
|
+
BudgetQuery.current_spend(:global, :daily, tenant_id: tenant_id),
|
|
164
|
+
agent_type, tenant_id, budget_config)
|
|
165
|
+
|
|
166
|
+
# Check global monthly
|
|
167
|
+
check_budget_alert(:global_monthly, budget_config[:global_monthly],
|
|
168
|
+
BudgetQuery.current_spend(:global, :monthly, tenant_id: tenant_id),
|
|
169
|
+
agent_type, tenant_id, budget_config)
|
|
170
|
+
|
|
171
|
+
# Check per-agent daily
|
|
172
|
+
agent_daily_limit = budget_config[:per_agent_daily]&.dig(agent_type)
|
|
173
|
+
if agent_daily_limit
|
|
174
|
+
check_budget_alert(:per_agent_daily, agent_daily_limit,
|
|
175
|
+
BudgetQuery.current_spend(:agent, :daily, agent_type: agent_type, tenant_id: tenant_id),
|
|
176
|
+
agent_type, tenant_id, budget_config)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Check per-agent monthly
|
|
180
|
+
agent_monthly_limit = budget_config[:per_agent_monthly]&.dig(agent_type)
|
|
181
|
+
if agent_monthly_limit
|
|
182
|
+
check_budget_alert(:per_agent_monthly, agent_monthly_limit,
|
|
183
|
+
BudgetQuery.current_spend(:agent, :monthly, agent_type: agent_type, tenant_id: tenant_id),
|
|
184
|
+
agent_type, tenant_id, budget_config)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Checks if an alert should be fired for a budget
|
|
189
|
+
#
|
|
190
|
+
# @param scope [Symbol] Budget scope
|
|
191
|
+
# @param limit [Float, nil] Budget limit
|
|
192
|
+
# @param current [Float] Current spend
|
|
193
|
+
# @param agent_type [String] Agent type
|
|
194
|
+
# @param tenant_id [String, nil] The tenant identifier
|
|
195
|
+
# @param budget_config [Hash] Budget configuration
|
|
196
|
+
# @return [void]
|
|
197
|
+
def check_budget_alert(scope, limit, current, agent_type, tenant_id, budget_config)
|
|
198
|
+
return unless limit
|
|
199
|
+
return if current <= limit
|
|
200
|
+
|
|
201
|
+
event = budget_config[:enforcement] == :hard ? :budget_hard_cap : :budget_soft_cap
|
|
202
|
+
config = RubyLLM::Agents.configuration
|
|
203
|
+
return unless config.alert_events.include?(event)
|
|
204
|
+
|
|
205
|
+
# Prevent duplicate alerts by using a cache key (include tenant for isolation)
|
|
206
|
+
key = alert_cache_key("budget_alert", scope, tenant_id)
|
|
207
|
+
return if SpendRecorder.cache_exist?(key)
|
|
208
|
+
|
|
209
|
+
SpendRecorder.cache_write(key, true, expires_in: 1.hour)
|
|
210
|
+
|
|
211
|
+
AlertManager.notify(event, {
|
|
212
|
+
scope: scope,
|
|
213
|
+
limit: limit,
|
|
214
|
+
total: current.round(6),
|
|
215
|
+
agent_type: agent_type,
|
|
216
|
+
tenant_id: tenant_id,
|
|
217
|
+
timestamp: Date.current.to_s
|
|
218
|
+
})
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Checks for soft cap token alerts after recording usage
|
|
222
|
+
#
|
|
223
|
+
# @param agent_type [String] The agent class name
|
|
224
|
+
# @param tenant_id [String, nil] The tenant identifier
|
|
225
|
+
# @param budget_config [Hash] Budget configuration
|
|
226
|
+
# @return [void]
|
|
227
|
+
def check_soft_token_alerts(agent_type, tenant_id, budget_config)
|
|
228
|
+
config = RubyLLM::Agents.configuration
|
|
229
|
+
return unless config.alerts_enabled?
|
|
230
|
+
return unless config.alert_events.include?(:token_soft_cap) || config.alert_events.include?(:token_hard_cap)
|
|
231
|
+
|
|
232
|
+
# Check global daily tokens
|
|
233
|
+
check_token_alert(:global_daily_tokens, budget_config[:global_daily_tokens],
|
|
234
|
+
BudgetQuery.current_tokens(:daily, tenant_id: tenant_id),
|
|
235
|
+
agent_type, tenant_id, budget_config)
|
|
236
|
+
|
|
237
|
+
# Check global monthly tokens
|
|
238
|
+
check_token_alert(:global_monthly_tokens, budget_config[:global_monthly_tokens],
|
|
239
|
+
BudgetQuery.current_tokens(:monthly, tenant_id: tenant_id),
|
|
240
|
+
agent_type, tenant_id, budget_config)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Checks if a token alert should be fired
|
|
244
|
+
#
|
|
245
|
+
# @param scope [Symbol] Token scope
|
|
246
|
+
# @param limit [Integer, nil] Token limit
|
|
247
|
+
# @param current [Integer] Current token usage
|
|
248
|
+
# @param agent_type [String] Agent type
|
|
249
|
+
# @param tenant_id [String, nil] The tenant identifier
|
|
250
|
+
# @param budget_config [Hash] Budget configuration
|
|
251
|
+
# @return [void]
|
|
252
|
+
def check_token_alert(scope, limit, current, agent_type, tenant_id, budget_config)
|
|
253
|
+
return unless limit
|
|
254
|
+
return if current <= limit
|
|
255
|
+
|
|
256
|
+
event = budget_config[:enforcement] == :hard ? :token_hard_cap : :token_soft_cap
|
|
257
|
+
config = RubyLLM::Agents.configuration
|
|
258
|
+
return unless config.alert_events.include?(event)
|
|
259
|
+
|
|
260
|
+
# Prevent duplicate alerts
|
|
261
|
+
key = alert_cache_key("token_alert", scope, tenant_id)
|
|
262
|
+
return if SpendRecorder.cache_exist?(key)
|
|
263
|
+
|
|
264
|
+
SpendRecorder.cache_write(key, true, expires_in: 1.hour)
|
|
265
|
+
|
|
266
|
+
AlertManager.notify(event, {
|
|
267
|
+
scope: scope,
|
|
268
|
+
limit: limit,
|
|
269
|
+
total: current,
|
|
270
|
+
agent_type: agent_type,
|
|
271
|
+
tenant_id: tenant_id,
|
|
272
|
+
timestamp: Date.current.to_s
|
|
273
|
+
})
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "cache_helper"
|
|
4
|
+
require_relative "budget/config_resolver"
|
|
5
|
+
require_relative "budget/spend_recorder"
|
|
6
|
+
require_relative "budget/budget_query"
|
|
7
|
+
require_relative "budget/forecaster"
|
|
8
|
+
|
|
9
|
+
module RubyLLM
|
|
10
|
+
module Agents
|
|
11
|
+
# Cache-based budget tracking for cost governance
|
|
12
|
+
#
|
|
13
|
+
# Tracks spending against configured budget limits using cache counters.
|
|
14
|
+
# Supports daily and monthly budgets at both global and per-agent levels.
|
|
15
|
+
# In multi-tenant mode, budgets are tracked separately per tenant.
|
|
16
|
+
#
|
|
17
|
+
# Note: Uses best-effort enforcement with cache counters. In high-concurrency
|
|
18
|
+
# scenarios, slight overruns may occur due to race conditions. This is an
|
|
19
|
+
# acceptable trade-off for performance.
|
|
20
|
+
#
|
|
21
|
+
# @example Checking budget before execution
|
|
22
|
+
# BudgetTracker.check_budget!("MyAgent") # raises BudgetExceededError if over limit
|
|
23
|
+
#
|
|
24
|
+
# @example Recording spend after execution
|
|
25
|
+
# BudgetTracker.record_spend!("MyAgent", 0.05)
|
|
26
|
+
#
|
|
27
|
+
# @example Multi-tenant usage
|
|
28
|
+
# BudgetTracker.check_budget!("MyAgent", tenant_id: "acme_corp")
|
|
29
|
+
# BudgetTracker.record_spend!("MyAgent", 0.05, tenant_id: "acme_corp")
|
|
30
|
+
#
|
|
31
|
+
# @see RubyLLM::Agents::Configuration
|
|
32
|
+
# @see RubyLLM::Agents::Reliability::BudgetExceededError
|
|
33
|
+
# @see RubyLLM::Agents::TenantBudget
|
|
34
|
+
# @api public
|
|
35
|
+
module BudgetTracker
|
|
36
|
+
extend CacheHelper
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
# Checks if the current spend exceeds budget limits
|
|
40
|
+
#
|
|
41
|
+
# @param agent_type [String] The agent class name
|
|
42
|
+
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
43
|
+
# @param tenant_config [Hash, nil] Optional runtime tenant config (takes priority over resolver/DB)
|
|
44
|
+
# @raise [Reliability::BudgetExceededError] If hard cap is exceeded
|
|
45
|
+
# @return [void]
|
|
46
|
+
def check_budget!(agent_type, tenant_id: nil, tenant_config: nil)
|
|
47
|
+
tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
|
|
48
|
+
budget_config = Budget::ConfigResolver.resolve_budget_config(tenant_id, runtime_config: tenant_config)
|
|
49
|
+
|
|
50
|
+
return unless budget_config[:enabled]
|
|
51
|
+
return unless budget_config[:enforcement] == :hard
|
|
52
|
+
|
|
53
|
+
check_budget_limits!(agent_type, tenant_id, budget_config)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Checks if the current token usage exceeds budget limits
|
|
57
|
+
#
|
|
58
|
+
# @param agent_type [String] The agent class name
|
|
59
|
+
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
60
|
+
# @param tenant_config [Hash, nil] Optional runtime tenant config (takes priority over resolver/DB)
|
|
61
|
+
# @raise [Reliability::BudgetExceededError] If hard cap is exceeded
|
|
62
|
+
# @return [void]
|
|
63
|
+
def check_token_budget!(agent_type, tenant_id: nil, tenant_config: nil)
|
|
64
|
+
tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
|
|
65
|
+
budget_config = Budget::ConfigResolver.resolve_budget_config(tenant_id, runtime_config: tenant_config)
|
|
66
|
+
|
|
67
|
+
return unless budget_config[:enabled]
|
|
68
|
+
return unless budget_config[:enforcement] == :hard
|
|
69
|
+
|
|
70
|
+
check_token_limits!(agent_type, tenant_id, budget_config)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Records spend and checks for soft cap alerts
|
|
74
|
+
#
|
|
75
|
+
# @param agent_type [String] The agent class name
|
|
76
|
+
# @param amount [Float] The amount spent in USD
|
|
77
|
+
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
78
|
+
# @param tenant_config [Hash, nil] Optional runtime tenant config (takes priority over resolver/DB)
|
|
79
|
+
# @return [void]
|
|
80
|
+
def record_spend!(agent_type, amount, tenant_id: nil, tenant_config: nil)
|
|
81
|
+
return if amount.nil? || amount <= 0
|
|
82
|
+
|
|
83
|
+
tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
|
|
84
|
+
budget_config = Budget::ConfigResolver.resolve_budget_config(tenant_id, runtime_config: tenant_config)
|
|
85
|
+
|
|
86
|
+
Budget::SpendRecorder.record_spend!(agent_type, amount, tenant_id: tenant_id, budget_config: budget_config)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Records token usage and checks for soft cap alerts
|
|
90
|
+
#
|
|
91
|
+
# @param agent_type [String] The agent class name
|
|
92
|
+
# @param tokens [Integer] The number of tokens used
|
|
93
|
+
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
94
|
+
# @param tenant_config [Hash, nil] Optional runtime tenant config (takes priority over resolver/DB)
|
|
95
|
+
# @return [void]
|
|
96
|
+
def record_tokens!(agent_type, tokens, tenant_id: nil, tenant_config: nil)
|
|
97
|
+
return if tokens.nil? || tokens <= 0
|
|
98
|
+
|
|
99
|
+
tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
|
|
100
|
+
budget_config = Budget::ConfigResolver.resolve_budget_config(tenant_id, runtime_config: tenant_config)
|
|
101
|
+
|
|
102
|
+
Budget::SpendRecorder.record_tokens!(agent_type, tokens, tenant_id: tenant_id, budget_config: budget_config)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Returns the current spend for a scope and period
|
|
106
|
+
#
|
|
107
|
+
# @param scope [Symbol] :global or :agent
|
|
108
|
+
# @param period [Symbol] :daily or :monthly
|
|
109
|
+
# @param agent_type [String, nil] Required when scope is :agent
|
|
110
|
+
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
111
|
+
# @return [Float] Current spend in USD
|
|
112
|
+
def current_spend(scope, period, agent_type: nil, tenant_id: nil)
|
|
113
|
+
tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
|
|
114
|
+
Budget::BudgetQuery.current_spend(scope, period, agent_type: agent_type, tenant_id: tenant_id)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Returns the current token usage for a period (global only)
|
|
118
|
+
#
|
|
119
|
+
# @param period [Symbol] :daily or :monthly
|
|
120
|
+
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
121
|
+
# @return [Integer] Current token usage
|
|
122
|
+
def current_tokens(period, tenant_id: nil)
|
|
123
|
+
tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
|
|
124
|
+
Budget::BudgetQuery.current_tokens(period, tenant_id: tenant_id)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Returns the remaining budget for a scope and period
|
|
128
|
+
#
|
|
129
|
+
# @param scope [Symbol] :global or :agent
|
|
130
|
+
# @param period [Symbol] :daily or :monthly
|
|
131
|
+
# @param agent_type [String, nil] Required when scope is :agent
|
|
132
|
+
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
133
|
+
# @return [Float, nil] Remaining budget in USD, or nil if no limit configured
|
|
134
|
+
def remaining_budget(scope, period, agent_type: nil, tenant_id: nil)
|
|
135
|
+
tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
|
|
136
|
+
budget_config = Budget::ConfigResolver.resolve_budget_config(tenant_id)
|
|
137
|
+
|
|
138
|
+
Budget::BudgetQuery.remaining_budget(scope, period, agent_type: agent_type, tenant_id: tenant_id, budget_config: budget_config)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Returns the remaining token budget for a period (global only)
|
|
142
|
+
#
|
|
143
|
+
# @param period [Symbol] :daily or :monthly
|
|
144
|
+
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
145
|
+
# @return [Integer, nil] Remaining token budget, or nil if no limit configured
|
|
146
|
+
def remaining_token_budget(period, tenant_id: nil)
|
|
147
|
+
tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
|
|
148
|
+
budget_config = Budget::ConfigResolver.resolve_budget_config(tenant_id)
|
|
149
|
+
|
|
150
|
+
Budget::BudgetQuery.remaining_token_budget(period, tenant_id: tenant_id, budget_config: budget_config)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Returns a summary of all budget statuses
|
|
154
|
+
#
|
|
155
|
+
# @param agent_type [String, nil] Optional agent type for per-agent budgets
|
|
156
|
+
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
157
|
+
# @return [Hash] Budget status information
|
|
158
|
+
def status(agent_type: nil, tenant_id: nil)
|
|
159
|
+
tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
|
|
160
|
+
budget_config = Budget::ConfigResolver.resolve_budget_config(tenant_id)
|
|
161
|
+
|
|
162
|
+
Budget::BudgetQuery.status(agent_type: agent_type, tenant_id: tenant_id, budget_config: budget_config)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Calculates budget forecasts based on current spending trends
|
|
166
|
+
#
|
|
167
|
+
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
168
|
+
# @return [Hash, nil] Forecast information
|
|
169
|
+
def calculate_forecast(tenant_id: nil)
|
|
170
|
+
tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
|
|
171
|
+
budget_config = Budget::ConfigResolver.resolve_budget_config(tenant_id)
|
|
172
|
+
|
|
173
|
+
Budget::Forecaster.calculate_forecast(tenant_id: tenant_id, budget_config: budget_config)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Resets all budget counters (useful for testing)
|
|
177
|
+
#
|
|
178
|
+
# @param tenant_id [String, nil] Optional tenant identifier to reset only that tenant's counters
|
|
179
|
+
# @return [void]
|
|
180
|
+
def reset!(tenant_id: nil)
|
|
181
|
+
tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
|
|
182
|
+
tenant_part = Budget::SpendRecorder.tenant_key_part(tenant_id)
|
|
183
|
+
today = Budget::SpendRecorder.date_key_part(:daily)
|
|
184
|
+
month = Budget::SpendRecorder.date_key_part(:monthly)
|
|
185
|
+
|
|
186
|
+
BudgetTracker.cache_delete(BudgetTracker.cache_key("budget", tenant_part, today))
|
|
187
|
+
BudgetTracker.cache_delete(BudgetTracker.cache_key("budget", tenant_part, month))
|
|
188
|
+
|
|
189
|
+
# Reset memoized table existence check (useful for testing)
|
|
190
|
+
Budget::ConfigResolver.reset_tenant_budget_table_check!
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
private
|
|
194
|
+
|
|
195
|
+
# Checks budget limits and raises error if exceeded
|
|
196
|
+
#
|
|
197
|
+
# @param agent_type [String] The agent class name
|
|
198
|
+
# @param tenant_id [String, nil] The tenant identifier
|
|
199
|
+
# @param budget_config [Hash] The budget configuration
|
|
200
|
+
# @raise [Reliability::BudgetExceededError] If limit exceeded
|
|
201
|
+
# @return [void]
|
|
202
|
+
def check_budget_limits!(agent_type, tenant_id, budget_config)
|
|
203
|
+
# Check global daily budget
|
|
204
|
+
if budget_config[:global_daily]
|
|
205
|
+
current = Budget::BudgetQuery.current_spend(:global, :daily, tenant_id: tenant_id)
|
|
206
|
+
if current >= budget_config[:global_daily]
|
|
207
|
+
raise Reliability::BudgetExceededError.new(:global_daily, budget_config[:global_daily], current, tenant_id: tenant_id)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Check global monthly budget
|
|
212
|
+
if budget_config[:global_monthly]
|
|
213
|
+
current = Budget::BudgetQuery.current_spend(:global, :monthly, tenant_id: tenant_id)
|
|
214
|
+
if current >= budget_config[:global_monthly]
|
|
215
|
+
raise Reliability::BudgetExceededError.new(:global_monthly, budget_config[:global_monthly], current, tenant_id: tenant_id)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Check per-agent daily budget
|
|
220
|
+
agent_daily_limit = budget_config[:per_agent_daily]&.dig(agent_type)
|
|
221
|
+
if agent_daily_limit
|
|
222
|
+
current = Budget::BudgetQuery.current_spend(:agent, :daily, agent_type: agent_type, tenant_id: tenant_id)
|
|
223
|
+
if current >= agent_daily_limit
|
|
224
|
+
raise Reliability::BudgetExceededError.new(:per_agent_daily, agent_daily_limit, current, agent_type: agent_type, tenant_id: tenant_id)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Check per-agent monthly budget
|
|
229
|
+
agent_monthly_limit = budget_config[:per_agent_monthly]&.dig(agent_type)
|
|
230
|
+
if agent_monthly_limit
|
|
231
|
+
current = Budget::BudgetQuery.current_spend(:agent, :monthly, agent_type: agent_type, tenant_id: tenant_id)
|
|
232
|
+
if current >= agent_monthly_limit
|
|
233
|
+
raise Reliability::BudgetExceededError.new(:per_agent_monthly, agent_monthly_limit, current, agent_type: agent_type, tenant_id: tenant_id)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Checks token limits and raises error if exceeded
|
|
239
|
+
#
|
|
240
|
+
# @param agent_type [String] The agent class name
|
|
241
|
+
# @param tenant_id [String, nil] The tenant identifier
|
|
242
|
+
# @param budget_config [Hash] The budget configuration
|
|
243
|
+
# @raise [Reliability::BudgetExceededError] If limit exceeded
|
|
244
|
+
# @return [void]
|
|
245
|
+
def check_token_limits!(agent_type, tenant_id, budget_config)
|
|
246
|
+
# Check global daily token budget
|
|
247
|
+
if budget_config[:global_daily_tokens]
|
|
248
|
+
current = Budget::BudgetQuery.current_tokens(:daily, tenant_id: tenant_id)
|
|
249
|
+
if current >= budget_config[:global_daily_tokens]
|
|
250
|
+
raise Reliability::BudgetExceededError.new(
|
|
251
|
+
:global_daily_tokens,
|
|
252
|
+
budget_config[:global_daily_tokens],
|
|
253
|
+
current,
|
|
254
|
+
tenant_id: tenant_id
|
|
255
|
+
)
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Check global monthly token budget
|
|
260
|
+
if budget_config[:global_monthly_tokens]
|
|
261
|
+
current = Budget::BudgetQuery.current_tokens(:monthly, tenant_id: tenant_id)
|
|
262
|
+
if current >= budget_config[:global_monthly_tokens]
|
|
263
|
+
raise Reliability::BudgetExceededError.new(
|
|
264
|
+
:global_monthly_tokens,
|
|
265
|
+
budget_config[:global_monthly_tokens],
|
|
266
|
+
current,
|
|
267
|
+
tenant_id: tenant_id
|
|
268
|
+
)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
@@ -25,7 +25,9 @@ module RubyLLM
|
|
|
25
25
|
# @param execution_data [Hash] Execution attributes from instrumentation
|
|
26
26
|
# @return [void]
|
|
27
27
|
def perform(execution_data)
|
|
28
|
-
|
|
28
|
+
# Filter to only known attributes to prevent schema mismatches
|
|
29
|
+
filtered_data = filter_known_attributes(execution_data)
|
|
30
|
+
execution = Execution.create!(filtered_data)
|
|
29
31
|
|
|
30
32
|
# Calculate costs if token data is available
|
|
31
33
|
if execution.input_tokens && execution.output_tokens
|
|
@@ -39,6 +41,20 @@ module RubyLLM
|
|
|
39
41
|
|
|
40
42
|
private
|
|
41
43
|
|
|
44
|
+
# Filters data to only include attributes that exist on the Execution model
|
|
45
|
+
#
|
|
46
|
+
# This provides a safety net against schema mismatches, such as when
|
|
47
|
+
# tenant_id is passed but the column doesn't exist in the database.
|
|
48
|
+
#
|
|
49
|
+
# @param data [Hash] The raw execution data
|
|
50
|
+
# @return [Hash] Filtered data with only known attributes
|
|
51
|
+
def filter_known_attributes(data)
|
|
52
|
+
return data unless defined?(Execution) && Execution.respond_to?(:column_names)
|
|
53
|
+
|
|
54
|
+
known_columns = Execution.column_names
|
|
55
|
+
data.select { |key, _| known_columns.include?(key.to_s) }
|
|
56
|
+
end
|
|
57
|
+
|
|
42
58
|
# Checks if execution should be flagged as anomalous
|
|
43
59
|
#
|
|
44
60
|
# @param execution [Execution] The execution to check
|
|
@@ -32,7 +32,8 @@ module RubyLLM
|
|
|
32
32
|
backoff: retries_config[:backoff] || :exponential,
|
|
33
33
|
base: retries_config[:base] || 0.4,
|
|
34
34
|
max_delay: retries_config[:max_delay] || 3.0,
|
|
35
|
-
on: retries_config[:on] || []
|
|
35
|
+
on: retries_config[:on] || [],
|
|
36
|
+
patterns: config[:retryable_patterns]
|
|
36
37
|
)
|
|
37
38
|
|
|
38
39
|
@fallback_routing = FallbackRouting.new(
|