ruby_llm-agents 1.1.0 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 35819df82cd1d73ad351e6e71ef076ee5131d0e021ba4729acfb274ff703a17e
4
- data.tar.gz: 7df1b73bafa5d4bbd63b47499ac93ab58091417313b2bd2c07214c2ef8e489a8
3
+ metadata.gz: a14dc618496842dd6db2fc197ab5c4f4190fb635b1bad8765a0983cc00e2550d
4
+ data.tar.gz: 22132d6e59964206dccb41db60dd3a9939217d47d6925074523bc1027173f4ed
5
5
  SHA512:
6
- metadata.gz: 484d69732880f808d6710346e0aa3d11d06cb4090b8ecf127a8620702064df3e6da71e78d3f4445c368787668629623a514d0302366e21b6aab8e23f4572c4c5
7
- data.tar.gz: 62bb19973149e5dd17e9165d240c238d49d406945a2a91bf7cdbb3fe075198dedb092f2e8410ce0ce84bc31115b04e75ac5b5c02206e82d2f774faf993ecb9ef
6
+ metadata.gz: c4936cf3773b4864bb9d1dd8031fb23e8321435bc4c1730ac088f299cc60f5a88992496b317f3391a131a303a6c82666ca7c4d1b77e4ea7da2f8c033e01a8c43
7
+ data.tar.gz: ca34cddc439b2f80652f9748dc62bd5fa1d365feac68101f7567e15f1bc5c2fef4dcec97e552b1dde7a20744782143c1ef5bffc2c7aaa97083b7d2f36bcf600c
@@ -0,0 +1,277 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ class Tenant
6
+ # Handles budget limits and enforcement for tenants.
7
+ #
8
+ # Supports three types of limits:
9
+ # - Cost limits (USD): daily_limit, monthly_limit
10
+ # - Token limits: daily_token_limit, monthly_token_limit
11
+ # - Execution limits: daily_execution_limit, monthly_execution_limit
12
+ #
13
+ # Enforcement modes:
14
+ # - :none - No enforcement, tracking only
15
+ # - :soft - Log warnings when limits exceeded
16
+ # - :hard - Block execution when limits exceeded
17
+ #
18
+ # @example Setting limits
19
+ # tenant.daily_limit = 100.0
20
+ # tenant.monthly_limit = 1000.0
21
+ # tenant.enforcement = "hard"
22
+ #
23
+ # @example Checking budget
24
+ # tenant.within_budget? # => true
25
+ # tenant.within_budget?(type: :monthly_cost)
26
+ # tenant.remaining_budget(type: :daily_tokens)
27
+ #
28
+ # @see BudgetTracker
29
+ # @api public
30
+ module Budgetable
31
+ extend ActiveSupport::Concern
32
+
33
+ # Valid enforcement modes
34
+ ENFORCEMENT_MODES = %w[none soft hard].freeze
35
+
36
+ included do
37
+ # Validations
38
+ validates :enforcement, inclusion: { in: ENFORCEMENT_MODES }, allow_nil: true
39
+ validates :daily_limit, :monthly_limit,
40
+ numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
41
+ validates :daily_token_limit, :monthly_token_limit,
42
+ numericality: { greater_than_or_equal_to: 0, only_integer: true }, allow_nil: true
43
+ validates :daily_execution_limit, :monthly_execution_limit,
44
+ numericality: { greater_than_or_equal_to: 0, only_integer: true }, allow_nil: true
45
+
46
+ # Scopes
47
+ scope :with_budgets, -> { where.not(daily_limit: nil).or(where.not(monthly_limit: nil)) }
48
+ scope :with_enforcement, ->(mode) { where(enforcement: mode) }
49
+ scope :hard_enforcement, -> { where(enforcement: "hard") }
50
+ scope :soft_enforcement, -> { where(enforcement: "soft") }
51
+ scope :no_enforcement, -> { where(enforcement: "none") }
52
+ end
53
+
54
+ # Effective limits (considering inheritance from global config)
55
+
56
+ # Returns the effective daily cost limit
57
+ #
58
+ # @return [Float, nil] The daily limit or nil if not set
59
+ def effective_daily_limit
60
+ return daily_limit if daily_limit.present?
61
+ return nil unless inherit_global_defaults
62
+
63
+ global_config&.dig(:global_daily)
64
+ end
65
+
66
+ # Returns the effective monthly cost limit
67
+ #
68
+ # @return [Float, nil] The monthly limit or nil if not set
69
+ def effective_monthly_limit
70
+ return monthly_limit if monthly_limit.present?
71
+ return nil unless inherit_global_defaults
72
+
73
+ global_config&.dig(:global_monthly)
74
+ end
75
+
76
+ # Returns the effective daily token limit
77
+ #
78
+ # @return [Integer, nil] The daily token limit or nil if not set
79
+ def effective_daily_token_limit
80
+ return daily_token_limit if daily_token_limit.present?
81
+ return nil unless inherit_global_defaults
82
+
83
+ global_config&.dig(:global_daily_tokens)
84
+ end
85
+
86
+ # Returns the effective monthly token limit
87
+ #
88
+ # @return [Integer, nil] The monthly token limit or nil if not set
89
+ def effective_monthly_token_limit
90
+ return monthly_token_limit if monthly_token_limit.present?
91
+ return nil unless inherit_global_defaults
92
+
93
+ global_config&.dig(:global_monthly_tokens)
94
+ end
95
+
96
+ # Returns the effective daily execution limit
97
+ #
98
+ # @return [Integer, nil] The daily execution limit or nil if not set
99
+ def effective_daily_execution_limit
100
+ return daily_execution_limit if daily_execution_limit.present?
101
+ return nil unless inherit_global_defaults
102
+
103
+ global_config&.dig(:global_daily_executions)
104
+ end
105
+
106
+ # Returns the effective monthly execution limit
107
+ #
108
+ # @return [Integer, nil] The monthly execution limit or nil if not set
109
+ def effective_monthly_execution_limit
110
+ return monthly_execution_limit if monthly_execution_limit.present?
111
+ return nil unless inherit_global_defaults
112
+
113
+ global_config&.dig(:global_monthly_executions)
114
+ end
115
+
116
+ # Returns the effective enforcement mode
117
+ #
118
+ # @return [Symbol] :none, :soft, or :hard
119
+ def effective_enforcement
120
+ return enforcement.to_sym if enforcement.present?
121
+ return :soft unless inherit_global_defaults
122
+
123
+ RubyLLM::Agents.configuration.budget_enforcement
124
+ end
125
+
126
+ # Returns the effective per-agent daily limit
127
+ #
128
+ # @param agent_type [String] The agent class name
129
+ # @return [Float, nil] The limit or nil if not set
130
+ def effective_per_agent_daily(agent_type)
131
+ limit = per_agent_daily&.dig(agent_type)
132
+ return limit if limit.present?
133
+ return nil unless inherit_global_defaults
134
+
135
+ global_config&.dig(:per_agent_daily, agent_type)
136
+ end
137
+
138
+ # Returns the effective per-agent monthly limit
139
+ #
140
+ # @param agent_type [String] The agent class name
141
+ # @return [Float, nil] The limit or nil if not set
142
+ def effective_per_agent_monthly(agent_type)
143
+ limit = per_agent_monthly&.dig(agent_type)
144
+ return limit if limit.present?
145
+ return nil unless inherit_global_defaults
146
+
147
+ global_config&.dig(:per_agent_monthly, agent_type)
148
+ end
149
+
150
+ # Budget status checks
151
+
152
+ # Checks if budget enforcement is enabled
153
+ #
154
+ # @return [Boolean] true if enforcement is :soft or :hard
155
+ def budgets_enabled?
156
+ effective_enforcement != :none
157
+ end
158
+
159
+ # Checks if hard enforcement is enabled
160
+ #
161
+ # @return [Boolean]
162
+ def hard_enforcement?
163
+ effective_enforcement == :hard
164
+ end
165
+
166
+ # Checks if soft enforcement is enabled
167
+ #
168
+ # @return [Boolean]
169
+ def soft_enforcement?
170
+ effective_enforcement == :soft
171
+ end
172
+
173
+ # Check if within budget for a specific type
174
+ #
175
+ # @param type [Symbol] :daily_cost, :monthly_cost, :daily_tokens,
176
+ # :monthly_tokens, :daily_executions, :monthly_executions
177
+ # @return [Boolean]
178
+ def within_budget?(type: :daily_cost)
179
+ status = budget_status
180
+ return true unless status[:enabled]
181
+
182
+ key = budget_status_key(type)
183
+ (status.dig(key, :percentage_used) || 0) < 100
184
+ end
185
+
186
+ # Get remaining budget for a specific type
187
+ #
188
+ # @param type [Symbol] Budget type (see #within_budget?)
189
+ # @return [Numeric, nil]
190
+ def remaining_budget(type: :daily_cost)
191
+ status = budget_status
192
+ key = budget_status_key(type)
193
+ status.dig(key, :remaining)
194
+ end
195
+
196
+ # Check budget and raise if exceeded (for hard enforcement)
197
+ #
198
+ # @param agent_type [String] The agent class name
199
+ # @raise [BudgetExceededError] If hard enforcement and over budget
200
+ def check_budget!(agent_type = nil)
201
+ BudgetTracker.check_budget!(agent_type || "Unknown", tenant_id: tenant_id)
202
+ end
203
+
204
+ # Get full budget status from BudgetTracker
205
+ #
206
+ # @return [Hash] Budget status with usage information
207
+ def budget_status
208
+ BudgetTracker.status(tenant_id: tenant_id)
209
+ end
210
+
211
+ # Convert to config hash for BudgetTracker
212
+ #
213
+ # @return [Hash] Budget configuration hash
214
+ def to_budget_config
215
+ {
216
+ enabled: budgets_enabled?,
217
+ enforcement: effective_enforcement,
218
+ # Cost limits
219
+ global_daily: effective_daily_limit,
220
+ global_monthly: effective_monthly_limit,
221
+ per_agent_daily: merged_per_agent_daily,
222
+ per_agent_monthly: merged_per_agent_monthly,
223
+ # Token limits
224
+ global_daily_tokens: effective_daily_token_limit,
225
+ global_monthly_tokens: effective_monthly_token_limit,
226
+ # Execution limits
227
+ global_daily_executions: effective_daily_execution_limit,
228
+ global_monthly_executions: effective_monthly_execution_limit
229
+ }
230
+ end
231
+
232
+ private
233
+
234
+ # Returns the global budgets configuration
235
+ #
236
+ # @return [Hash, nil]
237
+ def global_config
238
+ RubyLLM::Agents.configuration.budgets
239
+ end
240
+
241
+ # Merges per-agent daily limits with global defaults
242
+ #
243
+ # @return [Hash]
244
+ def merged_per_agent_daily
245
+ return per_agent_daily || {} unless inherit_global_defaults
246
+
247
+ (global_config&.dig(:per_agent_daily) || {}).merge(per_agent_daily || {})
248
+ end
249
+
250
+ # Merges per-agent monthly limits with global defaults
251
+ #
252
+ # @return [Hash]
253
+ def merged_per_agent_monthly
254
+ return per_agent_monthly || {} unless inherit_global_defaults
255
+
256
+ (global_config&.dig(:per_agent_monthly) || {}).merge(per_agent_monthly || {})
257
+ end
258
+
259
+ # Maps budget type to status key
260
+ #
261
+ # @param type [Symbol]
262
+ # @return [Symbol]
263
+ def budget_status_key(type)
264
+ case type
265
+ when :daily_cost then :global_daily
266
+ when :monthly_cost then :global_monthly
267
+ when :daily_tokens then :global_daily_tokens
268
+ when :monthly_tokens then :global_monthly_tokens
269
+ when :daily_executions then :global_daily_executions
270
+ when :monthly_executions then :global_monthly_executions
271
+ else :global_daily
272
+ end
273
+ end
274
+ end
275
+ end
276
+ end
277
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module RubyLLM
6
+ module Agents
7
+ class Tenant
8
+ # Manages API configuration for a tenant.
9
+ #
10
+ # Links to the ApiConfiguration model to provide per-tenant API keys
11
+ # and settings. Supports inheritance from global configuration when
12
+ # tenant-specific settings are not defined.
13
+ #
14
+ # @example Accessing tenant API keys
15
+ # tenant = Tenant.for("acme_corp")
16
+ # tenant.api_key_for(:openai) # => "sk-..."
17
+ # tenant.has_custom_api_keys? # => true
18
+ #
19
+ # @example Getting effective configuration
20
+ # config = tenant.effective_api_configuration
21
+ # config.apply_to_ruby_llm!
22
+ #
23
+ # @see ApiConfiguration
24
+ # @api public
25
+ module Configurable
26
+ extend ActiveSupport::Concern
27
+
28
+ included do
29
+ # Link to tenant-specific API configuration
30
+ has_one :api_configuration,
31
+ -> { where(scope_type: "tenant") },
32
+ class_name: "RubyLLM::Agents::ApiConfiguration",
33
+ foreign_key: :scope_id,
34
+ primary_key: :tenant_id,
35
+ dependent: :destroy
36
+ end
37
+
38
+ # Get API key for a specific provider
39
+ #
40
+ # @param provider [Symbol] Provider name (:openai, :anthropic, :gemini, etc.)
41
+ # @return [String, nil] The API key or nil if not configured
42
+ #
43
+ # @example
44
+ # tenant.api_key_for(:openai) # => "sk-abc123..."
45
+ # tenant.api_key_for(:anthropic) # => "sk-ant-xyz..."
46
+ def api_key_for(provider)
47
+ attr_name = "#{provider}_api_key"
48
+ api_configuration&.send(attr_name) if api_configuration&.respond_to?(attr_name)
49
+ end
50
+
51
+ # Check if tenant has custom API keys configured
52
+ #
53
+ # @return [Boolean] true if tenant has an ApiConfiguration record
54
+ def has_custom_api_keys?
55
+ api_configuration.present?
56
+ end
57
+
58
+ # Get effective API configuration for this tenant
59
+ #
60
+ # Returns the resolved configuration that combines tenant-specific
61
+ # settings with global defaults.
62
+ #
63
+ # @return [ResolvedConfig] The resolved configuration
64
+ #
65
+ # @example
66
+ # config = tenant.effective_api_configuration
67
+ # config.openai_api_key # Tenant's key or global fallback
68
+ def effective_api_configuration
69
+ ApiConfiguration.resolve(tenant_id: tenant_id)
70
+ end
71
+
72
+ # Get or create the API configuration for this tenant
73
+ #
74
+ # @return [ApiConfiguration] The tenant's API configuration record
75
+ def api_configuration!
76
+ api_configuration || create_api_configuration!(
77
+ scope_type: "tenant",
78
+ scope_id: tenant_id
79
+ )
80
+ end
81
+
82
+ # Configure API settings for this tenant
83
+ #
84
+ # @yield [config] Block to configure the API settings
85
+ # @yieldparam config [ApiConfiguration] The configuration to modify
86
+ # @return [ApiConfiguration] The saved configuration
87
+ #
88
+ # @example
89
+ # tenant.configure_api do |config|
90
+ # config.openai_api_key = "sk-..."
91
+ # config.default_model = "gpt-4o"
92
+ # end
93
+ def configure_api(&block)
94
+ config = api_configuration!
95
+ yield(config) if block_given?
96
+ config.save!
97
+ config
98
+ end
99
+
100
+ # Check if a specific provider is configured for this tenant
101
+ #
102
+ # @param provider [Symbol] Provider name
103
+ # @return [Boolean] true if the provider has an API key set
104
+ def provider_configured?(provider)
105
+ api_key_for(provider).present?
106
+ end
107
+
108
+ # Get all configured providers for this tenant
109
+ #
110
+ # @return [Array<Symbol>] List of configured provider symbols
111
+ def configured_providers
112
+ return [] unless api_configuration
113
+
114
+ ApiConfiguration::PROVIDERS.keys.select do |provider|
115
+ provider_configured?(provider)
116
+ end
117
+ end
118
+
119
+ # Get the default model for this tenant
120
+ #
121
+ # @return [String, nil] The default model or nil
122
+ def default_model
123
+ api_configuration&.default_model
124
+ end
125
+
126
+ # Get the default embedding model for this tenant
127
+ #
128
+ # @return [String, nil] The default embedding model or nil
129
+ def default_embedding_model
130
+ api_configuration&.default_embedding_model
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end