ruby_llm-agents 0.4.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 +225 -34
- data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
- data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +214 -0
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
- data/app/controllers/ruby_llm/agents/{settings_controller.rb → system_config_controller.rb} +3 -3
- data/app/controllers/ruby_llm/agents/tenants_controller.rb +109 -0
- 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/api_configuration.rb +386 -0
- data/app/models/ruby_llm/agents/execution.rb +3 -0
- data/app/models/ruby_llm/agents/tenant_budget.rb +112 -14
- data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
- data/app/views/layouts/ruby_llm/agents/application.html.erb +5 -30
- 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/api_configurations/_api_key_field.html.erb +34 -0
- data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +288 -0
- data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +95 -0
- data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +97 -0
- data/app/views/ruby_llm/agents/api_configurations/show.html.erb +211 -0
- data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +179 -0
- data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +1 -1
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +269 -15
- data/app/views/ruby_llm/agents/executions/show.html.erb +98 -0
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -0
- data/app/views/ruby_llm/agents/{settings → system_config}/show.html.erb +1 -1
- data/app/views/ruby_llm/agents/tenants/_form.html.erb +150 -0
- data/app/views/ruby_llm/agents/tenants/edit.html.erb +13 -0
- data/app/views/ruby_llm/agents/tenants/index.html.erb +129 -0
- data/app/views/ruby_llm/agents/tenants/show.html.erb +374 -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 +13 -1
- data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
- data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +100 -0
- 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/create_api_configurations_migration.rb.tt +90 -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} +93 -4
- data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -0
- data/lib/ruby_llm/agents/core/resolved_config.rb +348 -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 -10
- 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 +189 -35
- 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 -283
- 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 -209
- data/lib/ruby_llm/agents/budget_tracker.rb +0 -471
- data/lib/ruby_llm/agents/configuration.rb +0 -357
- /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/{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
|
@@ -1,471 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "cache_helper"
|
|
4
|
-
|
|
5
|
-
module RubyLLM
|
|
6
|
-
module Agents
|
|
7
|
-
# Cache-based budget tracking for cost governance
|
|
8
|
-
#
|
|
9
|
-
# Tracks spending against configured budget limits using cache counters.
|
|
10
|
-
# Supports daily and monthly budgets at both global and per-agent levels.
|
|
11
|
-
# In multi-tenant mode, budgets are tracked separately per tenant.
|
|
12
|
-
#
|
|
13
|
-
# Note: Uses best-effort enforcement with cache counters. In high-concurrency
|
|
14
|
-
# scenarios, slight overruns may occur due to race conditions. This is an
|
|
15
|
-
# acceptable trade-off for performance.
|
|
16
|
-
#
|
|
17
|
-
# @example Checking budget before execution
|
|
18
|
-
# BudgetTracker.check_budget!("MyAgent") # raises BudgetExceededError if over limit
|
|
19
|
-
#
|
|
20
|
-
# @example Recording spend after execution
|
|
21
|
-
# BudgetTracker.record_spend!("MyAgent", 0.05)
|
|
22
|
-
#
|
|
23
|
-
# @example Multi-tenant usage
|
|
24
|
-
# BudgetTracker.check_budget!("MyAgent", tenant_id: "acme_corp")
|
|
25
|
-
# BudgetTracker.record_spend!("MyAgent", 0.05, tenant_id: "acme_corp")
|
|
26
|
-
#
|
|
27
|
-
# @see RubyLLM::Agents::Configuration
|
|
28
|
-
# @see RubyLLM::Agents::Reliability::BudgetExceededError
|
|
29
|
-
# @see RubyLLM::Agents::TenantBudget
|
|
30
|
-
# @api public
|
|
31
|
-
module BudgetTracker
|
|
32
|
-
extend CacheHelper
|
|
33
|
-
|
|
34
|
-
class << self
|
|
35
|
-
# Checks if the current spend exceeds budget limits
|
|
36
|
-
#
|
|
37
|
-
# @param agent_type [String] The agent class name
|
|
38
|
-
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
39
|
-
# @raise [Reliability::BudgetExceededError] If hard cap is exceeded
|
|
40
|
-
# @return [void]
|
|
41
|
-
def check_budget!(agent_type, tenant_id: nil)
|
|
42
|
-
tenant_id = resolve_tenant_id(tenant_id)
|
|
43
|
-
budget_config = resolve_budget_config(tenant_id)
|
|
44
|
-
|
|
45
|
-
return unless budget_config[:enabled]
|
|
46
|
-
return unless budget_config[:enforcement] == :hard
|
|
47
|
-
|
|
48
|
-
check_budget_limits!(agent_type, tenant_id, budget_config)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# Records spend and checks for soft cap alerts
|
|
52
|
-
#
|
|
53
|
-
# @param agent_type [String] The agent class name
|
|
54
|
-
# @param amount [Float] The amount spent in USD
|
|
55
|
-
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
56
|
-
# @return [void]
|
|
57
|
-
def record_spend!(agent_type, amount, tenant_id: nil)
|
|
58
|
-
return if amount.nil? || amount <= 0
|
|
59
|
-
|
|
60
|
-
tenant_id = resolve_tenant_id(tenant_id)
|
|
61
|
-
|
|
62
|
-
# Increment all relevant counters
|
|
63
|
-
increment_spend(:global, :daily, amount, tenant_id: tenant_id)
|
|
64
|
-
increment_spend(:global, :monthly, amount, tenant_id: tenant_id)
|
|
65
|
-
increment_spend(:agent, :daily, amount, agent_type: agent_type, tenant_id: tenant_id)
|
|
66
|
-
increment_spend(:agent, :monthly, amount, agent_type: agent_type, tenant_id: tenant_id)
|
|
67
|
-
|
|
68
|
-
# Check for soft cap alerts
|
|
69
|
-
budget_config = resolve_budget_config(tenant_id)
|
|
70
|
-
check_soft_cap_alerts(agent_type, tenant_id, budget_config) if budget_config[:enabled]
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# Returns the current spend for a scope and period
|
|
74
|
-
#
|
|
75
|
-
# @param scope [Symbol] :global or :agent
|
|
76
|
-
# @param period [Symbol] :daily or :monthly
|
|
77
|
-
# @param agent_type [String, nil] Required when scope is :agent
|
|
78
|
-
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
79
|
-
# @return [Float] Current spend in USD
|
|
80
|
-
def current_spend(scope, period, agent_type: nil, tenant_id: nil)
|
|
81
|
-
tenant_id = resolve_tenant_id(tenant_id)
|
|
82
|
-
key = budget_cache_key(scope, period, agent_type: agent_type, tenant_id: tenant_id)
|
|
83
|
-
(BudgetTracker.cache_read(key) || 0).to_f
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# Returns the remaining budget for a scope and period
|
|
87
|
-
#
|
|
88
|
-
# @param scope [Symbol] :global or :agent
|
|
89
|
-
# @param period [Symbol] :daily or :monthly
|
|
90
|
-
# @param agent_type [String, nil] Required when scope is :agent
|
|
91
|
-
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
92
|
-
# @return [Float, nil] Remaining budget in USD, or nil if no limit configured
|
|
93
|
-
def remaining_budget(scope, period, agent_type: nil, tenant_id: nil)
|
|
94
|
-
tenant_id = resolve_tenant_id(tenant_id)
|
|
95
|
-
budget_config = resolve_budget_config(tenant_id)
|
|
96
|
-
|
|
97
|
-
limit = case [scope, period]
|
|
98
|
-
when [:global, :daily]
|
|
99
|
-
budget_config[:global_daily]
|
|
100
|
-
when [:global, :monthly]
|
|
101
|
-
budget_config[:global_monthly]
|
|
102
|
-
when [:agent, :daily]
|
|
103
|
-
budget_config[:per_agent_daily]&.dig(agent_type)
|
|
104
|
-
when [:agent, :monthly]
|
|
105
|
-
budget_config[:per_agent_monthly]&.dig(agent_type)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
return nil unless limit
|
|
109
|
-
|
|
110
|
-
[limit - current_spend(scope, period, agent_type: agent_type, tenant_id: tenant_id), 0].max
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
# Returns a summary of all budget statuses
|
|
114
|
-
#
|
|
115
|
-
# @param agent_type [String, nil] Optional agent type for per-agent budgets
|
|
116
|
-
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
117
|
-
# @return [Hash] Budget status information
|
|
118
|
-
def status(agent_type: nil, tenant_id: nil)
|
|
119
|
-
tenant_id = resolve_tenant_id(tenant_id)
|
|
120
|
-
budget_config = resolve_budget_config(tenant_id)
|
|
121
|
-
|
|
122
|
-
{
|
|
123
|
-
tenant_id: tenant_id,
|
|
124
|
-
enabled: budget_config[:enabled],
|
|
125
|
-
enforcement: budget_config[:enforcement],
|
|
126
|
-
global_daily: budget_status(:global, :daily, budget_config[:global_daily], tenant_id: tenant_id),
|
|
127
|
-
global_monthly: budget_status(:global, :monthly, budget_config[:global_monthly], tenant_id: tenant_id),
|
|
128
|
-
per_agent_daily: agent_type ? budget_status(:agent, :daily, budget_config[:per_agent_daily]&.dig(agent_type), agent_type: agent_type, tenant_id: tenant_id) : nil,
|
|
129
|
-
per_agent_monthly: agent_type ? budget_status(:agent, :monthly, budget_config[:per_agent_monthly]&.dig(agent_type), agent_type: agent_type, tenant_id: tenant_id) : nil,
|
|
130
|
-
forecast: calculate_forecast(tenant_id: tenant_id)
|
|
131
|
-
}.compact
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
# Calculates budget forecasts based on current spending trends
|
|
135
|
-
#
|
|
136
|
-
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
137
|
-
# @return [Hash, nil] Forecast information
|
|
138
|
-
def calculate_forecast(tenant_id: nil)
|
|
139
|
-
tenant_id = resolve_tenant_id(tenant_id)
|
|
140
|
-
budget_config = resolve_budget_config(tenant_id)
|
|
141
|
-
|
|
142
|
-
return nil unless budget_config[:enabled]
|
|
143
|
-
return nil unless budget_config[:global_daily] || budget_config[:global_monthly]
|
|
144
|
-
|
|
145
|
-
daily_current = current_spend(:global, :daily, tenant_id: tenant_id)
|
|
146
|
-
monthly_current = current_spend(:global, :monthly, tenant_id: tenant_id)
|
|
147
|
-
|
|
148
|
-
# Calculate hours elapsed today and days elapsed this month
|
|
149
|
-
hours_elapsed = Time.current.hour + (Time.current.min / 60.0)
|
|
150
|
-
hours_elapsed = [hours_elapsed, 1].max # Avoid division by zero
|
|
151
|
-
days_in_month = Time.current.end_of_month.day
|
|
152
|
-
day_of_month = Time.current.day
|
|
153
|
-
days_elapsed = day_of_month - 1 + (hours_elapsed / 24.0)
|
|
154
|
-
days_elapsed = [days_elapsed, 1].max
|
|
155
|
-
|
|
156
|
-
forecast = {}
|
|
157
|
-
|
|
158
|
-
# Daily forecast
|
|
159
|
-
if budget_config[:global_daily]
|
|
160
|
-
daily_rate = daily_current / hours_elapsed
|
|
161
|
-
projected_daily = daily_rate * 24
|
|
162
|
-
forecast[:daily] = {
|
|
163
|
-
current: daily_current.round(4),
|
|
164
|
-
projected: projected_daily.round(4),
|
|
165
|
-
limit: budget_config[:global_daily],
|
|
166
|
-
on_track: projected_daily <= budget_config[:global_daily],
|
|
167
|
-
hours_remaining: (24 - hours_elapsed).round(1),
|
|
168
|
-
rate_per_hour: daily_rate.round(6)
|
|
169
|
-
}
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
# Monthly forecast
|
|
173
|
-
if budget_config[:global_monthly]
|
|
174
|
-
monthly_rate = monthly_current / days_elapsed
|
|
175
|
-
projected_monthly = monthly_rate * days_in_month
|
|
176
|
-
days_remaining = days_in_month - day_of_month
|
|
177
|
-
forecast[:monthly] = {
|
|
178
|
-
current: monthly_current.round(4),
|
|
179
|
-
projected: projected_monthly.round(4),
|
|
180
|
-
limit: budget_config[:global_monthly],
|
|
181
|
-
on_track: projected_monthly <= budget_config[:global_monthly],
|
|
182
|
-
days_remaining: days_remaining,
|
|
183
|
-
rate_per_day: monthly_rate.round(4)
|
|
184
|
-
}
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
forecast.presence
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
# Resets all budget counters (useful for testing)
|
|
191
|
-
#
|
|
192
|
-
# @param tenant_id [String, nil] Optional tenant identifier to reset only that tenant's counters
|
|
193
|
-
# @return [void]
|
|
194
|
-
def reset!(tenant_id: nil)
|
|
195
|
-
tenant_id = resolve_tenant_id(tenant_id)
|
|
196
|
-
today = Date.current.to_s
|
|
197
|
-
month = Date.current.strftime("%Y-%m")
|
|
198
|
-
|
|
199
|
-
tenant_part = tenant_id.present? ? "tenant:#{tenant_id}" : "global"
|
|
200
|
-
|
|
201
|
-
BudgetTracker.cache_delete(BudgetTracker.cache_key("budget", tenant_part, today))
|
|
202
|
-
BudgetTracker.cache_delete(BudgetTracker.cache_key("budget", tenant_part, month))
|
|
203
|
-
|
|
204
|
-
# Reset memoized table existence check (useful for testing)
|
|
205
|
-
remove_instance_variable(:@tenant_budget_table_exists) if defined?(@tenant_budget_table_exists)
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
private
|
|
209
|
-
|
|
210
|
-
# Resolves the current tenant ID
|
|
211
|
-
#
|
|
212
|
-
# @param explicit_tenant_id [String, nil] Explicitly passed tenant ID
|
|
213
|
-
# @return [String, nil] Resolved tenant ID or nil if multi-tenancy disabled
|
|
214
|
-
def resolve_tenant_id(explicit_tenant_id)
|
|
215
|
-
config = RubyLLM::Agents.configuration
|
|
216
|
-
|
|
217
|
-
# Ignore tenant_id entirely when multi-tenancy is disabled
|
|
218
|
-
return nil unless config.multi_tenancy_enabled?
|
|
219
|
-
|
|
220
|
-
# Use explicit tenant_id if provided, otherwise use resolver
|
|
221
|
-
return explicit_tenant_id if explicit_tenant_id.present?
|
|
222
|
-
|
|
223
|
-
config.tenant_resolver&.call
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
# Resolves budget configuration for a tenant
|
|
227
|
-
#
|
|
228
|
-
# @param tenant_id [String, nil] The tenant identifier
|
|
229
|
-
# @return [Hash] Budget configuration
|
|
230
|
-
def resolve_budget_config(tenant_id)
|
|
231
|
-
config = RubyLLM::Agents.configuration
|
|
232
|
-
|
|
233
|
-
# If multi-tenancy is disabled or no tenant, use global config
|
|
234
|
-
if tenant_id.nil? || !config.multi_tenancy_enabled?
|
|
235
|
-
return {
|
|
236
|
-
enabled: config.budgets_enabled?,
|
|
237
|
-
enforcement: config.budget_enforcement,
|
|
238
|
-
global_daily: config.budgets&.dig(:global_daily),
|
|
239
|
-
global_monthly: config.budgets&.dig(:global_monthly),
|
|
240
|
-
per_agent_daily: config.budgets&.dig(:per_agent_daily),
|
|
241
|
-
per_agent_monthly: config.budgets&.dig(:per_agent_monthly)
|
|
242
|
-
}
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
# Look up tenant-specific budget from database (if table exists)
|
|
246
|
-
tenant_budget = lookup_tenant_budget(tenant_id)
|
|
247
|
-
|
|
248
|
-
if tenant_budget
|
|
249
|
-
tenant_budget.to_budget_config
|
|
250
|
-
else
|
|
251
|
-
# Fall back to global config for unknown tenants
|
|
252
|
-
{
|
|
253
|
-
enabled: config.budgets_enabled?,
|
|
254
|
-
enforcement: config.budget_enforcement,
|
|
255
|
-
global_daily: config.budgets&.dig(:global_daily),
|
|
256
|
-
global_monthly: config.budgets&.dig(:global_monthly),
|
|
257
|
-
per_agent_daily: config.budgets&.dig(:per_agent_daily),
|
|
258
|
-
per_agent_monthly: config.budgets&.dig(:per_agent_monthly)
|
|
259
|
-
}
|
|
260
|
-
end
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
# Safely looks up tenant budget, handling missing table
|
|
264
|
-
#
|
|
265
|
-
# @param tenant_id [String] The tenant identifier
|
|
266
|
-
# @return [TenantBudget, nil] The tenant budget or nil
|
|
267
|
-
def lookup_tenant_budget(tenant_id)
|
|
268
|
-
return nil unless tenant_budget_table_exists?
|
|
269
|
-
|
|
270
|
-
TenantBudget.for_tenant(tenant_id)
|
|
271
|
-
rescue StandardError => e
|
|
272
|
-
Rails.logger.warn("[RubyLLM::Agents] Failed to lookup tenant budget: #{e.message}")
|
|
273
|
-
nil
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
# Checks if the tenant_budgets table exists
|
|
277
|
-
#
|
|
278
|
-
# @return [Boolean] true if table exists
|
|
279
|
-
def tenant_budget_table_exists?
|
|
280
|
-
return @tenant_budget_table_exists if defined?(@tenant_budget_table_exists)
|
|
281
|
-
|
|
282
|
-
@tenant_budget_table_exists = ::ActiveRecord::Base.connection.table_exists?(:ruby_llm_agents_tenant_budgets)
|
|
283
|
-
rescue StandardError
|
|
284
|
-
@tenant_budget_table_exists = false
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
# Resets the memoized tenant budget table existence check (useful for testing)
|
|
288
|
-
#
|
|
289
|
-
# @return [void]
|
|
290
|
-
def reset_tenant_budget_table_check!
|
|
291
|
-
remove_instance_variable(:@tenant_budget_table_exists) if defined?(@tenant_budget_table_exists)
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
# Checks budget limits and raises error if exceeded
|
|
295
|
-
#
|
|
296
|
-
# @param agent_type [String] The agent class name
|
|
297
|
-
# @param tenant_id [String, nil] The tenant identifier
|
|
298
|
-
# @param budget_config [Hash] The budget configuration
|
|
299
|
-
# @raise [Reliability::BudgetExceededError] If limit exceeded
|
|
300
|
-
# @return [void]
|
|
301
|
-
def check_budget_limits!(agent_type, tenant_id, budget_config)
|
|
302
|
-
# Check global daily budget
|
|
303
|
-
if budget_config[:global_daily]
|
|
304
|
-
current = current_spend(:global, :daily, tenant_id: tenant_id)
|
|
305
|
-
if current >= budget_config[:global_daily]
|
|
306
|
-
raise Reliability::BudgetExceededError.new(:global_daily, budget_config[:global_daily], current, tenant_id: tenant_id)
|
|
307
|
-
end
|
|
308
|
-
end
|
|
309
|
-
|
|
310
|
-
# Check global monthly budget
|
|
311
|
-
if budget_config[:global_monthly]
|
|
312
|
-
current = current_spend(:global, :monthly, tenant_id: tenant_id)
|
|
313
|
-
if current >= budget_config[:global_monthly]
|
|
314
|
-
raise Reliability::BudgetExceededError.new(:global_monthly, budget_config[:global_monthly], current, tenant_id: tenant_id)
|
|
315
|
-
end
|
|
316
|
-
end
|
|
317
|
-
|
|
318
|
-
# Check per-agent daily budget
|
|
319
|
-
agent_daily_limit = budget_config[:per_agent_daily]&.dig(agent_type)
|
|
320
|
-
if agent_daily_limit
|
|
321
|
-
current = current_spend(:agent, :daily, agent_type: agent_type, tenant_id: tenant_id)
|
|
322
|
-
if current >= agent_daily_limit
|
|
323
|
-
raise Reliability::BudgetExceededError.new(:per_agent_daily, agent_daily_limit, current, agent_type: agent_type, tenant_id: tenant_id)
|
|
324
|
-
end
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
# Check per-agent monthly budget
|
|
328
|
-
agent_monthly_limit = budget_config[:per_agent_monthly]&.dig(agent_type)
|
|
329
|
-
if agent_monthly_limit
|
|
330
|
-
current = current_spend(:agent, :monthly, agent_type: agent_type, tenant_id: tenant_id)
|
|
331
|
-
if current >= agent_monthly_limit
|
|
332
|
-
raise Reliability::BudgetExceededError.new(:per_agent_monthly, agent_monthly_limit, current, agent_type: agent_type, tenant_id: tenant_id)
|
|
333
|
-
end
|
|
334
|
-
end
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
# Increments the spend counter for a scope and period
|
|
338
|
-
#
|
|
339
|
-
# @param scope [Symbol] :global or :agent
|
|
340
|
-
# @param period [Symbol] :daily or :monthly
|
|
341
|
-
# @param amount [Float] Amount to add
|
|
342
|
-
# @param agent_type [String, nil] Required when scope is :agent
|
|
343
|
-
# @param tenant_id [String, nil] The tenant identifier
|
|
344
|
-
# @return [Float] New total
|
|
345
|
-
def increment_spend(scope, period, amount, agent_type: nil, tenant_id: nil)
|
|
346
|
-
key = budget_cache_key(scope, period, agent_type: agent_type, tenant_id: tenant_id)
|
|
347
|
-
ttl = period == :daily ? 1.day : 31.days
|
|
348
|
-
|
|
349
|
-
# Read-modify-write for float values (cache increment is for integers)
|
|
350
|
-
current = (BudgetTracker.cache_read(key) || 0).to_f
|
|
351
|
-
new_total = current + amount
|
|
352
|
-
BudgetTracker.cache_write(key, new_total, expires_in: ttl)
|
|
353
|
-
new_total
|
|
354
|
-
end
|
|
355
|
-
|
|
356
|
-
# Generates a cache key for budget tracking
|
|
357
|
-
#
|
|
358
|
-
# @param scope [Symbol] :global or :agent
|
|
359
|
-
# @param period [Symbol] :daily or :monthly
|
|
360
|
-
# @param agent_type [String, nil] Required when scope is :agent
|
|
361
|
-
# @param tenant_id [String, nil] The tenant identifier
|
|
362
|
-
# @return [String] Cache key
|
|
363
|
-
def budget_cache_key(scope, period, agent_type: nil, tenant_id: nil)
|
|
364
|
-
date_part = period == :daily ? Date.current.to_s : Date.current.strftime("%Y-%m")
|
|
365
|
-
tenant_part = tenant_id.present? ? "tenant:#{tenant_id}" : "global"
|
|
366
|
-
|
|
367
|
-
case scope
|
|
368
|
-
when :global
|
|
369
|
-
BudgetTracker.cache_key("budget", tenant_part, date_part)
|
|
370
|
-
when :agent
|
|
371
|
-
BudgetTracker.cache_key("budget", tenant_part, "agent", agent_type, date_part)
|
|
372
|
-
else
|
|
373
|
-
raise ArgumentError, "Unknown scope: #{scope}"
|
|
374
|
-
end
|
|
375
|
-
end
|
|
376
|
-
|
|
377
|
-
# Returns budget status for a scope/period
|
|
378
|
-
#
|
|
379
|
-
# @param scope [Symbol] :global or :agent
|
|
380
|
-
# @param period [Symbol] :daily or :monthly
|
|
381
|
-
# @param limit [Float, nil] The budget limit
|
|
382
|
-
# @param agent_type [String, nil] Required when scope is :agent
|
|
383
|
-
# @param tenant_id [String, nil] The tenant identifier
|
|
384
|
-
# @return [Hash, nil] Status hash or nil if no limit
|
|
385
|
-
def budget_status(scope, period, limit, agent_type: nil, tenant_id: nil)
|
|
386
|
-
return nil unless limit
|
|
387
|
-
|
|
388
|
-
current = current_spend(scope, period, agent_type: agent_type, tenant_id: tenant_id)
|
|
389
|
-
{
|
|
390
|
-
limit: limit,
|
|
391
|
-
current: current.round(6),
|
|
392
|
-
remaining: [limit - current, 0].max.round(6),
|
|
393
|
-
percentage_used: ((current / limit) * 100).round(2)
|
|
394
|
-
}
|
|
395
|
-
end
|
|
396
|
-
|
|
397
|
-
# Checks for soft cap alerts after recording spend
|
|
398
|
-
#
|
|
399
|
-
# @param agent_type [String] The agent class name
|
|
400
|
-
# @param tenant_id [String, nil] The tenant identifier
|
|
401
|
-
# @param budget_config [Hash] Budget configuration
|
|
402
|
-
# @return [void]
|
|
403
|
-
def check_soft_cap_alerts(agent_type, tenant_id, budget_config)
|
|
404
|
-
config = RubyLLM::Agents.configuration
|
|
405
|
-
return unless config.alerts_enabled?
|
|
406
|
-
return unless config.alert_events.include?(:budget_soft_cap) || config.alert_events.include?(:budget_hard_cap)
|
|
407
|
-
|
|
408
|
-
# Check global daily
|
|
409
|
-
check_budget_alert(:global_daily, budget_config[:global_daily],
|
|
410
|
-
current_spend(:global, :daily, tenant_id: tenant_id),
|
|
411
|
-
agent_type, tenant_id, budget_config)
|
|
412
|
-
|
|
413
|
-
# Check global monthly
|
|
414
|
-
check_budget_alert(:global_monthly, budget_config[:global_monthly],
|
|
415
|
-
current_spend(:global, :monthly, tenant_id: tenant_id),
|
|
416
|
-
agent_type, tenant_id, budget_config)
|
|
417
|
-
|
|
418
|
-
# Check per-agent daily
|
|
419
|
-
agent_daily_limit = budget_config[:per_agent_daily]&.dig(agent_type)
|
|
420
|
-
if agent_daily_limit
|
|
421
|
-
check_budget_alert(:per_agent_daily, agent_daily_limit,
|
|
422
|
-
current_spend(:agent, :daily, agent_type: agent_type, tenant_id: tenant_id),
|
|
423
|
-
agent_type, tenant_id, budget_config)
|
|
424
|
-
end
|
|
425
|
-
|
|
426
|
-
# Check per-agent monthly
|
|
427
|
-
agent_monthly_limit = budget_config[:per_agent_monthly]&.dig(agent_type)
|
|
428
|
-
if agent_monthly_limit
|
|
429
|
-
check_budget_alert(:per_agent_monthly, agent_monthly_limit,
|
|
430
|
-
current_spend(:agent, :monthly, agent_type: agent_type, tenant_id: tenant_id),
|
|
431
|
-
agent_type, tenant_id, budget_config)
|
|
432
|
-
end
|
|
433
|
-
end
|
|
434
|
-
|
|
435
|
-
# Checks if an alert should be fired for a budget
|
|
436
|
-
#
|
|
437
|
-
# @param scope [Symbol] Budget scope
|
|
438
|
-
# @param limit [Float, nil] Budget limit
|
|
439
|
-
# @param current [Float] Current spend
|
|
440
|
-
# @param agent_type [String] Agent type
|
|
441
|
-
# @param tenant_id [String, nil] The tenant identifier
|
|
442
|
-
# @param budget_config [Hash] Budget configuration
|
|
443
|
-
# @return [void]
|
|
444
|
-
def check_budget_alert(scope, limit, current, agent_type, tenant_id, budget_config)
|
|
445
|
-
return unless limit
|
|
446
|
-
return if current <= limit
|
|
447
|
-
|
|
448
|
-
event = budget_config[:enforcement] == :hard ? :budget_hard_cap : :budget_soft_cap
|
|
449
|
-
config = RubyLLM::Agents.configuration
|
|
450
|
-
return unless config.alert_events.include?(event)
|
|
451
|
-
|
|
452
|
-
# Prevent duplicate alerts by using a cache key (include tenant for isolation)
|
|
453
|
-
tenant_part = tenant_id.present? ? "tenant:#{tenant_id}" : "global"
|
|
454
|
-
alert_key = BudgetTracker.cache_key("budget_alert", tenant_part, scope, Date.current.to_s)
|
|
455
|
-
return if BudgetTracker.cache_exist?(alert_key)
|
|
456
|
-
|
|
457
|
-
BudgetTracker.cache_write(alert_key, true, expires_in: 1.hour)
|
|
458
|
-
|
|
459
|
-
AlertManager.notify(event, {
|
|
460
|
-
scope: scope,
|
|
461
|
-
limit: limit,
|
|
462
|
-
total: current.round(6),
|
|
463
|
-
agent_type: agent_type,
|
|
464
|
-
tenant_id: tenant_id,
|
|
465
|
-
timestamp: Date.current.to_s
|
|
466
|
-
})
|
|
467
|
-
end
|
|
468
|
-
end
|
|
469
|
-
end
|
|
470
|
-
end
|
|
471
|
-
end
|