ruby_llm-agents 0.3.6 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +46 -13
- data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +214 -0
- 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/models/ruby_llm/agents/api_configuration.rb +386 -0
- data/app/models/ruby_llm/agents/tenant_budget.rb +62 -7
- data/app/views/layouts/ruby_llm/agents/application.html.erb +3 -1
- 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/executions/show.html.erb +82 -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/config/routes.rb +12 -1
- data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +100 -0
- data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +90 -0
- data/lib/ruby_llm/agents/base/dsl.rb +65 -13
- data/lib/ruby_llm/agents/base/execution.rb +113 -6
- data/lib/ruby_llm/agents/base/reliability_dsl.rb +82 -0
- data/lib/ruby_llm/agents/base.rb +28 -0
- data/lib/ruby_llm/agents/budget_tracker.rb +285 -23
- data/lib/ruby_llm/agents/configuration.rb +38 -1
- data/lib/ruby_llm/agents/deprecations.rb +77 -0
- data/lib/ruby_llm/agents/engine.rb +1 -0
- data/lib/ruby_llm/agents/instrumentation.rb +71 -3
- data/lib/ruby_llm/agents/reliability/breaker_manager.rb +80 -0
- data/lib/ruby_llm/agents/reliability/execution_constraints.rb +69 -0
- data/lib/ruby_llm/agents/reliability/executor.rb +124 -0
- data/lib/ruby_llm/agents/reliability/fallback_routing.rb +72 -0
- data/lib/ruby_llm/agents/reliability/retry_strategy.rb +76 -0
- data/lib/ruby_llm/agents/resolved_config.rb +348 -0
- data/lib/ruby_llm/agents/result.rb +72 -2
- data/lib/ruby_llm/agents/version.rb +1 -1
- data/lib/ruby_llm/agents.rb +6 -0
- metadata +26 -3
|
@@ -36,11 +36,12 @@ module RubyLLM
|
|
|
36
36
|
#
|
|
37
37
|
# @param agent_type [String] The agent class name
|
|
38
38
|
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
39
|
+
# @param tenant_config [Hash, nil] Optional runtime tenant config (takes priority over resolver/DB)
|
|
39
40
|
# @raise [Reliability::BudgetExceededError] If hard cap is exceeded
|
|
40
41
|
# @return [void]
|
|
41
|
-
def check_budget!(agent_type, tenant_id: nil)
|
|
42
|
+
def check_budget!(agent_type, tenant_id: nil, tenant_config: nil)
|
|
42
43
|
tenant_id = resolve_tenant_id(tenant_id)
|
|
43
|
-
budget_config = resolve_budget_config(tenant_id)
|
|
44
|
+
budget_config = resolve_budget_config(tenant_id, runtime_config: tenant_config)
|
|
44
45
|
|
|
45
46
|
return unless budget_config[:enabled]
|
|
46
47
|
return unless budget_config[:enforcement] == :hard
|
|
@@ -48,13 +49,31 @@ module RubyLLM
|
|
|
48
49
|
check_budget_limits!(agent_type, tenant_id, budget_config)
|
|
49
50
|
end
|
|
50
51
|
|
|
52
|
+
# Checks if the current token usage exceeds budget limits
|
|
53
|
+
#
|
|
54
|
+
# @param agent_type [String] The agent class name
|
|
55
|
+
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
56
|
+
# @param tenant_config [Hash, nil] Optional runtime tenant config (takes priority over resolver/DB)
|
|
57
|
+
# @raise [Reliability::BudgetExceededError] If hard cap is exceeded
|
|
58
|
+
# @return [void]
|
|
59
|
+
def check_token_budget!(agent_type, tenant_id: nil, tenant_config: nil)
|
|
60
|
+
tenant_id = resolve_tenant_id(tenant_id)
|
|
61
|
+
budget_config = resolve_budget_config(tenant_id, runtime_config: tenant_config)
|
|
62
|
+
|
|
63
|
+
return unless budget_config[:enabled]
|
|
64
|
+
return unless budget_config[:enforcement] == :hard
|
|
65
|
+
|
|
66
|
+
check_token_limits!(agent_type, tenant_id, budget_config)
|
|
67
|
+
end
|
|
68
|
+
|
|
51
69
|
# Records spend and checks for soft cap alerts
|
|
52
70
|
#
|
|
53
71
|
# @param agent_type [String] The agent class name
|
|
54
72
|
# @param amount [Float] The amount spent in USD
|
|
55
73
|
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
74
|
+
# @param tenant_config [Hash, nil] Optional runtime tenant config (takes priority over resolver/DB)
|
|
56
75
|
# @return [void]
|
|
57
|
-
def record_spend!(agent_type, amount, tenant_id: nil)
|
|
76
|
+
def record_spend!(agent_type, amount, tenant_id: nil, tenant_config: nil)
|
|
58
77
|
return if amount.nil? || amount <= 0
|
|
59
78
|
|
|
60
79
|
tenant_id = resolve_tenant_id(tenant_id)
|
|
@@ -66,10 +85,33 @@ module RubyLLM
|
|
|
66
85
|
increment_spend(:agent, :monthly, amount, agent_type: agent_type, tenant_id: tenant_id)
|
|
67
86
|
|
|
68
87
|
# Check for soft cap alerts
|
|
69
|
-
budget_config = resolve_budget_config(tenant_id)
|
|
88
|
+
budget_config = resolve_budget_config(tenant_id, runtime_config: tenant_config)
|
|
70
89
|
check_soft_cap_alerts(agent_type, tenant_id, budget_config) if budget_config[:enabled]
|
|
71
90
|
end
|
|
72
91
|
|
|
92
|
+
# Records token usage and checks for soft cap alerts
|
|
93
|
+
#
|
|
94
|
+
# @param agent_type [String] The agent class name
|
|
95
|
+
# @param tokens [Integer] The number of tokens used
|
|
96
|
+
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
97
|
+
# @param tenant_config [Hash, nil] Optional runtime tenant config (takes priority over resolver/DB)
|
|
98
|
+
# @return [void]
|
|
99
|
+
def record_tokens!(agent_type, tokens, tenant_id: nil, tenant_config: nil)
|
|
100
|
+
return if tokens.nil? || tokens <= 0
|
|
101
|
+
|
|
102
|
+
tenant_id = resolve_tenant_id(tenant_id)
|
|
103
|
+
|
|
104
|
+
# Increment all relevant token counters
|
|
105
|
+
increment_tokens(:global, :daily, tokens, tenant_id: tenant_id)
|
|
106
|
+
increment_tokens(:global, :monthly, tokens, tenant_id: tenant_id)
|
|
107
|
+
increment_tokens(:agent, :daily, tokens, agent_type: agent_type, tenant_id: tenant_id)
|
|
108
|
+
increment_tokens(:agent, :monthly, tokens, agent_type: agent_type, tenant_id: tenant_id)
|
|
109
|
+
|
|
110
|
+
# Check for soft cap alerts
|
|
111
|
+
budget_config = resolve_budget_config(tenant_id, runtime_config: tenant_config)
|
|
112
|
+
check_soft_token_alerts(agent_type, tenant_id, budget_config) if budget_config[:enabled]
|
|
113
|
+
end
|
|
114
|
+
|
|
73
115
|
# Returns the current spend for a scope and period
|
|
74
116
|
#
|
|
75
117
|
# @param scope [Symbol] :global or :agent
|
|
@@ -83,6 +125,17 @@ module RubyLLM
|
|
|
83
125
|
(BudgetTracker.cache_read(key) || 0).to_f
|
|
84
126
|
end
|
|
85
127
|
|
|
128
|
+
# Returns the current token usage for a period (global only)
|
|
129
|
+
#
|
|
130
|
+
# @param period [Symbol] :daily or :monthly
|
|
131
|
+
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
132
|
+
# @return [Integer] Current token usage
|
|
133
|
+
def current_tokens(period, tenant_id: nil)
|
|
134
|
+
tenant_id = resolve_tenant_id(tenant_id)
|
|
135
|
+
key = token_cache_key(period, tenant_id: tenant_id)
|
|
136
|
+
(BudgetTracker.cache_read(key) || 0).to_i
|
|
137
|
+
end
|
|
138
|
+
|
|
86
139
|
# Returns the remaining budget for a scope and period
|
|
87
140
|
#
|
|
88
141
|
# @param scope [Symbol] :global or :agent
|
|
@@ -110,6 +163,27 @@ module RubyLLM
|
|
|
110
163
|
[limit - current_spend(scope, period, agent_type: agent_type, tenant_id: tenant_id), 0].max
|
|
111
164
|
end
|
|
112
165
|
|
|
166
|
+
# Returns the remaining token budget for a period (global only)
|
|
167
|
+
#
|
|
168
|
+
# @param period [Symbol] :daily or :monthly
|
|
169
|
+
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
170
|
+
# @return [Integer, nil] Remaining token budget, or nil if no limit configured
|
|
171
|
+
def remaining_token_budget(period, tenant_id: nil)
|
|
172
|
+
tenant_id = resolve_tenant_id(tenant_id)
|
|
173
|
+
budget_config = resolve_budget_config(tenant_id)
|
|
174
|
+
|
|
175
|
+
limit = case period
|
|
176
|
+
when :daily
|
|
177
|
+
budget_config[:global_daily_tokens]
|
|
178
|
+
when :monthly
|
|
179
|
+
budget_config[:global_monthly_tokens]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
return nil unless limit
|
|
183
|
+
|
|
184
|
+
[limit - current_tokens(period, tenant_id: tenant_id), 0].max
|
|
185
|
+
end
|
|
186
|
+
|
|
113
187
|
# Returns a summary of all budget statuses
|
|
114
188
|
#
|
|
115
189
|
# @param agent_type [String, nil] Optional agent type for per-agent budgets
|
|
@@ -123,10 +197,14 @@ module RubyLLM
|
|
|
123
197
|
tenant_id: tenant_id,
|
|
124
198
|
enabled: budget_config[:enabled],
|
|
125
199
|
enforcement: budget_config[:enforcement],
|
|
200
|
+
# Cost budgets
|
|
126
201
|
global_daily: budget_status(:global, :daily, budget_config[:global_daily], tenant_id: tenant_id),
|
|
127
202
|
global_monthly: budget_status(:global, :monthly, budget_config[:global_monthly], tenant_id: tenant_id),
|
|
128
203
|
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
204
|
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,
|
|
205
|
+
# Token budgets (global only)
|
|
206
|
+
global_daily_tokens: token_status(:daily, budget_config[:global_daily_tokens], tenant_id: tenant_id),
|
|
207
|
+
global_monthly_tokens: token_status(:monthly, budget_config[:global_monthly_tokens], tenant_id: tenant_id),
|
|
130
208
|
forecast: calculate_forecast(tenant_id: tenant_id)
|
|
131
209
|
}.compact
|
|
132
210
|
end
|
|
@@ -225,41 +303,86 @@ module RubyLLM
|
|
|
225
303
|
|
|
226
304
|
# Resolves budget configuration for a tenant
|
|
227
305
|
#
|
|
306
|
+
# Priority order:
|
|
307
|
+
# 1. runtime_config (passed to run())
|
|
308
|
+
# 2. tenant_config_resolver (configured lambda)
|
|
309
|
+
# 3. TenantBudget database record
|
|
310
|
+
# 4. Global configuration
|
|
311
|
+
#
|
|
228
312
|
# @param tenant_id [String, nil] The tenant identifier
|
|
313
|
+
# @param runtime_config [Hash, nil] Runtime config passed to run()
|
|
229
314
|
# @return [Hash] Budget configuration
|
|
230
|
-
def resolve_budget_config(tenant_id)
|
|
315
|
+
def resolve_budget_config(tenant_id, runtime_config: nil)
|
|
231
316
|
config = RubyLLM::Agents.configuration
|
|
232
317
|
|
|
318
|
+
# Priority 1: Runtime config passed directly to run()
|
|
319
|
+
if runtime_config.present?
|
|
320
|
+
return normalize_budget_config(runtime_config, config)
|
|
321
|
+
end
|
|
322
|
+
|
|
233
323
|
# If multi-tenancy is disabled or no tenant, use global config
|
|
234
324
|
if tenant_id.nil? || !config.multi_tenancy_enabled?
|
|
235
|
-
return
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
325
|
+
return global_budget_config(config)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Priority 2: tenant_config_resolver lambda
|
|
329
|
+
if config.tenant_config_resolver.present?
|
|
330
|
+
resolved_config = config.tenant_config_resolver.call(tenant_id)
|
|
331
|
+
if resolved_config.present?
|
|
332
|
+
return normalize_budget_config(resolved_config, config)
|
|
333
|
+
end
|
|
243
334
|
end
|
|
244
335
|
|
|
245
|
-
# Look up tenant-specific budget from database
|
|
336
|
+
# Priority 3: Look up tenant-specific budget from database
|
|
246
337
|
tenant_budget = lookup_tenant_budget(tenant_id)
|
|
247
338
|
|
|
248
339
|
if tenant_budget
|
|
249
340
|
tenant_budget.to_budget_config
|
|
250
341
|
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
|
-
}
|
|
342
|
+
# Priority 4: Fall back to global config for unknown tenants
|
|
343
|
+
global_budget_config(config)
|
|
260
344
|
end
|
|
261
345
|
end
|
|
262
346
|
|
|
347
|
+
# Builds global budget config from configuration
|
|
348
|
+
#
|
|
349
|
+
# @param config [Configuration] The configuration object
|
|
350
|
+
# @return [Hash] Budget configuration
|
|
351
|
+
def global_budget_config(config)
|
|
352
|
+
{
|
|
353
|
+
enabled: config.budgets_enabled?,
|
|
354
|
+
enforcement: config.budget_enforcement,
|
|
355
|
+
global_daily: config.budgets&.dig(:global_daily),
|
|
356
|
+
global_monthly: config.budgets&.dig(:global_monthly),
|
|
357
|
+
per_agent_daily: config.budgets&.dig(:per_agent_daily),
|
|
358
|
+
per_agent_monthly: config.budgets&.dig(:per_agent_monthly),
|
|
359
|
+
global_daily_tokens: config.budgets&.dig(:global_daily_tokens),
|
|
360
|
+
global_monthly_tokens: config.budgets&.dig(:global_monthly_tokens)
|
|
361
|
+
}
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Normalizes runtime/resolver config to standard budget config format
|
|
365
|
+
#
|
|
366
|
+
# @param raw_config [Hash] Raw config from runtime or resolver
|
|
367
|
+
# @param global_config [Configuration] Global config for fallbacks
|
|
368
|
+
# @return [Hash] Normalized budget configuration
|
|
369
|
+
def normalize_budget_config(raw_config, global_config)
|
|
370
|
+
enforcement = raw_config[:enforcement]&.to_sym || global_config.budget_enforcement
|
|
371
|
+
|
|
372
|
+
{
|
|
373
|
+
enabled: enforcement != :none,
|
|
374
|
+
enforcement: enforcement,
|
|
375
|
+
# Cost/budget limits (USD)
|
|
376
|
+
global_daily: raw_config[:daily_budget_limit],
|
|
377
|
+
global_monthly: raw_config[:monthly_budget_limit],
|
|
378
|
+
per_agent_daily: raw_config[:per_agent_daily] || {},
|
|
379
|
+
per_agent_monthly: raw_config[:per_agent_monthly] || {},
|
|
380
|
+
# Token limits
|
|
381
|
+
global_daily_tokens: raw_config[:daily_token_limit],
|
|
382
|
+
global_monthly_tokens: raw_config[:monthly_token_limit]
|
|
383
|
+
}
|
|
384
|
+
end
|
|
385
|
+
|
|
263
386
|
# Safely looks up tenant budget, handling missing table
|
|
264
387
|
#
|
|
265
388
|
# @param tenant_id [String] The tenant identifier
|
|
@@ -465,6 +588,145 @@ module RubyLLM
|
|
|
465
588
|
timestamp: Date.current.to_s
|
|
466
589
|
})
|
|
467
590
|
end
|
|
591
|
+
|
|
592
|
+
# Increments the token counter for a period
|
|
593
|
+
#
|
|
594
|
+
# @param scope [Symbol] :global (only global supported for tokens)
|
|
595
|
+
# @param period [Symbol] :daily or :monthly
|
|
596
|
+
# @param tokens [Integer] Tokens to add
|
|
597
|
+
# @param tenant_id [String, nil] The tenant identifier
|
|
598
|
+
# @return [Integer] New total
|
|
599
|
+
def increment_tokens(scope, period, tokens, agent_type: nil, tenant_id: nil)
|
|
600
|
+
# For now, we only track global token usage (not per-agent)
|
|
601
|
+
key = token_cache_key(period, tenant_id: tenant_id)
|
|
602
|
+
ttl = period == :daily ? 1.day : 31.days
|
|
603
|
+
|
|
604
|
+
current = (BudgetTracker.cache_read(key) || 0).to_i
|
|
605
|
+
new_total = current + tokens
|
|
606
|
+
BudgetTracker.cache_write(key, new_total, expires_in: ttl)
|
|
607
|
+
new_total
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
# Generates a cache key for token tracking
|
|
611
|
+
#
|
|
612
|
+
# @param period [Symbol] :daily or :monthly
|
|
613
|
+
# @param tenant_id [String, nil] The tenant identifier
|
|
614
|
+
# @return [String] Cache key
|
|
615
|
+
def token_cache_key(period, tenant_id: nil)
|
|
616
|
+
date_part = period == :daily ? Date.current.to_s : Date.current.strftime("%Y-%m")
|
|
617
|
+
tenant_part = tenant_id.present? ? "tenant:#{tenant_id}" : "global"
|
|
618
|
+
|
|
619
|
+
BudgetTracker.cache_key("tokens", tenant_part, date_part)
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
# Checks token limits and raises error if exceeded
|
|
623
|
+
#
|
|
624
|
+
# @param agent_type [String] The agent class name
|
|
625
|
+
# @param tenant_id [String, nil] The tenant identifier
|
|
626
|
+
# @param budget_config [Hash] The budget configuration
|
|
627
|
+
# @raise [Reliability::BudgetExceededError] If limit exceeded
|
|
628
|
+
# @return [void]
|
|
629
|
+
def check_token_limits!(agent_type, tenant_id, budget_config)
|
|
630
|
+
# Check global daily token budget
|
|
631
|
+
if budget_config[:global_daily_tokens]
|
|
632
|
+
current = current_tokens(:daily, tenant_id: tenant_id)
|
|
633
|
+
if current >= budget_config[:global_daily_tokens]
|
|
634
|
+
raise Reliability::BudgetExceededError.new(
|
|
635
|
+
:global_daily_tokens,
|
|
636
|
+
budget_config[:global_daily_tokens],
|
|
637
|
+
current,
|
|
638
|
+
tenant_id: tenant_id
|
|
639
|
+
)
|
|
640
|
+
end
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
# Check global monthly token budget
|
|
644
|
+
if budget_config[:global_monthly_tokens]
|
|
645
|
+
current = current_tokens(:monthly, tenant_id: tenant_id)
|
|
646
|
+
if current >= budget_config[:global_monthly_tokens]
|
|
647
|
+
raise Reliability::BudgetExceededError.new(
|
|
648
|
+
:global_monthly_tokens,
|
|
649
|
+
budget_config[:global_monthly_tokens],
|
|
650
|
+
current,
|
|
651
|
+
tenant_id: tenant_id
|
|
652
|
+
)
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
# Checks for soft cap token alerts after recording usage
|
|
658
|
+
#
|
|
659
|
+
# @param agent_type [String] The agent class name
|
|
660
|
+
# @param tenant_id [String, nil] The tenant identifier
|
|
661
|
+
# @param budget_config [Hash] Budget configuration
|
|
662
|
+
# @return [void]
|
|
663
|
+
def check_soft_token_alerts(agent_type, tenant_id, budget_config)
|
|
664
|
+
config = RubyLLM::Agents.configuration
|
|
665
|
+
return unless config.alerts_enabled?
|
|
666
|
+
return unless config.alert_events.include?(:token_soft_cap) || config.alert_events.include?(:token_hard_cap)
|
|
667
|
+
|
|
668
|
+
# Check global daily tokens
|
|
669
|
+
check_token_alert(:global_daily_tokens, budget_config[:global_daily_tokens],
|
|
670
|
+
current_tokens(:daily, tenant_id: tenant_id),
|
|
671
|
+
agent_type, tenant_id, budget_config)
|
|
672
|
+
|
|
673
|
+
# Check global monthly tokens
|
|
674
|
+
check_token_alert(:global_monthly_tokens, budget_config[:global_monthly_tokens],
|
|
675
|
+
current_tokens(:monthly, tenant_id: tenant_id),
|
|
676
|
+
agent_type, tenant_id, budget_config)
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
# Checks if a token alert should be fired
|
|
680
|
+
#
|
|
681
|
+
# @param scope [Symbol] Token scope
|
|
682
|
+
# @param limit [Integer, nil] Token limit
|
|
683
|
+
# @param current [Integer] Current token usage
|
|
684
|
+
# @param agent_type [String] Agent type
|
|
685
|
+
# @param tenant_id [String, nil] The tenant identifier
|
|
686
|
+
# @param budget_config [Hash] Budget configuration
|
|
687
|
+
# @return [void]
|
|
688
|
+
def check_token_alert(scope, limit, current, agent_type, tenant_id, budget_config)
|
|
689
|
+
return unless limit
|
|
690
|
+
return if current <= limit
|
|
691
|
+
|
|
692
|
+
event = budget_config[:enforcement] == :hard ? :token_hard_cap : :token_soft_cap
|
|
693
|
+
config = RubyLLM::Agents.configuration
|
|
694
|
+
return unless config.alert_events.include?(event)
|
|
695
|
+
|
|
696
|
+
# Prevent duplicate alerts
|
|
697
|
+
tenant_part = tenant_id.present? ? "tenant:#{tenant_id}" : "global"
|
|
698
|
+
alert_key = BudgetTracker.cache_key("token_alert", tenant_part, scope, Date.current.to_s)
|
|
699
|
+
return if BudgetTracker.cache_exist?(alert_key)
|
|
700
|
+
|
|
701
|
+
BudgetTracker.cache_write(alert_key, true, expires_in: 1.hour)
|
|
702
|
+
|
|
703
|
+
AlertManager.notify(event, {
|
|
704
|
+
scope: scope,
|
|
705
|
+
limit: limit,
|
|
706
|
+
total: current,
|
|
707
|
+
agent_type: agent_type,
|
|
708
|
+
tenant_id: tenant_id,
|
|
709
|
+
timestamp: Date.current.to_s
|
|
710
|
+
})
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
# Returns token status for a period
|
|
714
|
+
#
|
|
715
|
+
# @param period [Symbol] :daily or :monthly
|
|
716
|
+
# @param limit [Integer, nil] The token limit
|
|
717
|
+
# @param tenant_id [String, nil] The tenant identifier
|
|
718
|
+
# @return [Hash, nil] Status hash or nil if no limit
|
|
719
|
+
def token_status(period, limit, tenant_id: nil)
|
|
720
|
+
return nil unless limit
|
|
721
|
+
|
|
722
|
+
current = current_tokens(period, tenant_id: tenant_id)
|
|
723
|
+
{
|
|
724
|
+
limit: limit,
|
|
725
|
+
current: current,
|
|
726
|
+
remaining: [limit - current, 0].max,
|
|
727
|
+
percentage_used: ((current.to_f / limit) * 100).round(2)
|
|
728
|
+
}
|
|
729
|
+
end
|
|
468
730
|
end
|
|
469
731
|
end
|
|
470
732
|
end
|
|
@@ -195,6 +195,35 @@ module RubyLLM
|
|
|
195
195
|
# @example Using request store
|
|
196
196
|
# config.tenant_resolver = -> { RequestStore[:tenant_id] }
|
|
197
197
|
|
|
198
|
+
# @!attribute [rw] tenant_config_resolver
|
|
199
|
+
# Lambda that returns tenant configuration without querying the database.
|
|
200
|
+
# Called when resolving tenant budget config. If set, this takes priority
|
|
201
|
+
# over the TenantBudget database lookup.
|
|
202
|
+
# @return [Proc, nil] Tenant config resolver lambda (default: nil)
|
|
203
|
+
# @example Using an external tenant service
|
|
204
|
+
# config.tenant_config_resolver = ->(tenant_id) {
|
|
205
|
+
# tenant = Tenant.find(tenant_id)
|
|
206
|
+
# {
|
|
207
|
+
# name: tenant.name,
|
|
208
|
+
# daily_limit: tenant.subscription.daily_budget,
|
|
209
|
+
# monthly_limit: tenant.subscription.monthly_budget,
|
|
210
|
+
# daily_token_limit: tenant.subscription.daily_tokens,
|
|
211
|
+
# monthly_token_limit: tenant.subscription.monthly_tokens,
|
|
212
|
+
# enforcement: tenant.subscription.hard_limits? ? :hard : :soft
|
|
213
|
+
# }
|
|
214
|
+
# }
|
|
215
|
+
|
|
216
|
+
# @!attribute [rw] persist_messages_summary
|
|
217
|
+
# Whether to persist a summary of conversation messages in execution records.
|
|
218
|
+
# When true, stores message count and first/last messages (truncated).
|
|
219
|
+
# Set to false to disable message summary persistence.
|
|
220
|
+
# @return [Boolean] Enable messages summary persistence (default: true)
|
|
221
|
+
|
|
222
|
+
# @!attribute [rw] messages_summary_max_length
|
|
223
|
+
# Maximum character length for message content in the summary.
|
|
224
|
+
# Content exceeding this length will be truncated with "...".
|
|
225
|
+
# @return [Integer] Max length for message content (default: 500)
|
|
226
|
+
|
|
198
227
|
attr_accessor :default_model,
|
|
199
228
|
:default_temperature,
|
|
200
229
|
:default_timeout,
|
|
@@ -220,7 +249,10 @@ module RubyLLM
|
|
|
220
249
|
:persist_responses,
|
|
221
250
|
:redaction,
|
|
222
251
|
:multi_tenancy_enabled,
|
|
223
|
-
:tenant_resolver
|
|
252
|
+
:tenant_resolver,
|
|
253
|
+
:tenant_config_resolver,
|
|
254
|
+
:persist_messages_summary,
|
|
255
|
+
:messages_summary_max_length
|
|
224
256
|
|
|
225
257
|
attr_writer :cache_store
|
|
226
258
|
|
|
@@ -264,6 +296,11 @@ module RubyLLM
|
|
|
264
296
|
# Multi-tenancy defaults (disabled for backward compatibility)
|
|
265
297
|
@multi_tenancy_enabled = false
|
|
266
298
|
@tenant_resolver = -> { nil }
|
|
299
|
+
@tenant_config_resolver = nil
|
|
300
|
+
|
|
301
|
+
# Messages summary defaults
|
|
302
|
+
@persist_messages_summary = true
|
|
303
|
+
@messages_summary_max_length = 500
|
|
267
304
|
end
|
|
268
305
|
|
|
269
306
|
# Returns the configured cache store, falling back to Rails.cache
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
# Manages deprecation warnings with configurable behavior
|
|
6
|
+
#
|
|
7
|
+
# Provides a centralized mechanism for deprecation warnings that can be
|
|
8
|
+
# configured to raise exceptions in test environments or emit warnings
|
|
9
|
+
# in production.
|
|
10
|
+
#
|
|
11
|
+
# @example Emitting a deprecation warning
|
|
12
|
+
# Deprecations.warn("cache(ttl) is deprecated, use cache_for(ttl) instead")
|
|
13
|
+
#
|
|
14
|
+
# @example Enabling strict mode in tests
|
|
15
|
+
# RubyLLM::Agents::Deprecations.raise_on_deprecation = true
|
|
16
|
+
#
|
|
17
|
+
# @api public
|
|
18
|
+
module Deprecations
|
|
19
|
+
# Error raised when deprecation warnings are configured to raise
|
|
20
|
+
#
|
|
21
|
+
# @api public
|
|
22
|
+
class DeprecationError < StandardError; end
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
# @!attribute [rw] raise_on_deprecation
|
|
26
|
+
# @return [Boolean] Whether to raise exceptions instead of warnings
|
|
27
|
+
attr_accessor :raise_on_deprecation
|
|
28
|
+
|
|
29
|
+
# @!attribute [rw] silenced
|
|
30
|
+
# @return [Boolean] Whether to silence all deprecation warnings
|
|
31
|
+
attr_accessor :silenced
|
|
32
|
+
|
|
33
|
+
# Emits a deprecation warning or raises an error
|
|
34
|
+
#
|
|
35
|
+
# @param message [String] The deprecation message
|
|
36
|
+
# @param callstack [Array<String>] The call stack (defaults to caller)
|
|
37
|
+
# @return [void]
|
|
38
|
+
# @raise [DeprecationError] If raise_on_deprecation is true
|
|
39
|
+
def warn(message, callstack = caller)
|
|
40
|
+
return if silenced
|
|
41
|
+
|
|
42
|
+
full_message = "[RubyLLM::Agents DEPRECATION] #{message}"
|
|
43
|
+
|
|
44
|
+
if raise_on_deprecation
|
|
45
|
+
raise DeprecationError, full_message
|
|
46
|
+
elsif defined?(Rails) && Rails.respond_to?(:application) && Rails.application
|
|
47
|
+
# Use Rails deprecator if available (Rails 7.1+)
|
|
48
|
+
if Rails.application.respond_to?(:deprecators)
|
|
49
|
+
Rails.application.deprecators[:ruby_llm_agents]&.warn(full_message, callstack) ||
|
|
50
|
+
::Kernel.warn("#{full_message}\n #{callstack.first}")
|
|
51
|
+
else
|
|
52
|
+
::Kernel.warn("#{full_message}\n #{callstack.first}")
|
|
53
|
+
end
|
|
54
|
+
else
|
|
55
|
+
::Kernel.warn("#{full_message}\n #{callstack.first}")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Temporarily silence deprecation warnings within a block
|
|
60
|
+
#
|
|
61
|
+
# @yield Block to execute with silenced warnings
|
|
62
|
+
# @return [Object] The return value of the block
|
|
63
|
+
def silence
|
|
64
|
+
old_silenced = silenced
|
|
65
|
+
self.silenced = true
|
|
66
|
+
yield
|
|
67
|
+
ensure
|
|
68
|
+
self.silenced = old_silenced
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Reset to defaults
|
|
73
|
+
self.raise_on_deprecation = false
|
|
74
|
+
self.silenced = false
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -245,7 +245,9 @@ module RubyLLM
|
|
|
245
245
|
metadata: metadata,
|
|
246
246
|
system_prompt: config.persist_prompts ? redacted_system_prompt : nil,
|
|
247
247
|
user_prompt: config.persist_prompts ? redacted_user_prompt : nil,
|
|
248
|
-
streaming: self.class.streaming
|
|
248
|
+
streaming: self.class.streaming,
|
|
249
|
+
messages_count: resolved_messages.size,
|
|
250
|
+
messages_summary: config.persist_messages_summary ? messages_summary : {}
|
|
249
251
|
}
|
|
250
252
|
|
|
251
253
|
# Extract tracing fields from metadata if present
|
|
@@ -326,6 +328,9 @@ module RubyLLM
|
|
|
326
328
|
Rails.logger.warn("[RubyLLM::Agents] Cost calculation failed: #{cost_error.message}")
|
|
327
329
|
end
|
|
328
330
|
end
|
|
331
|
+
|
|
332
|
+
# Record token usage for budget tracking
|
|
333
|
+
record_token_usage(execution)
|
|
329
334
|
rescue ActiveRecord::RecordInvalid => e
|
|
330
335
|
Rails.logger.error("[RubyLLM::Agents] Validation failed for execution #{execution&.id}: #{e.record.errors.full_messages.join(', ')}")
|
|
331
336
|
if Rails.env.development? || Rails.env.test?
|
|
@@ -415,6 +420,9 @@ module RubyLLM
|
|
|
415
420
|
Rails.logger.warn("[RubyLLM::Agents] Cost calculation failed: #{cost_error.message}")
|
|
416
421
|
end
|
|
417
422
|
end
|
|
423
|
+
|
|
424
|
+
# Record token usage for budget tracking
|
|
425
|
+
record_token_usage(execution)
|
|
418
426
|
rescue ActiveRecord::RecordInvalid => e
|
|
419
427
|
Rails.logger.error("[RubyLLM::Agents] Validation failed for execution #{execution&.id}: #{e.record.errors.full_messages.join(', ')}")
|
|
420
428
|
if Rails.env.development? || Rails.env.test?
|
|
@@ -440,6 +448,8 @@ module RubyLLM
|
|
|
440
448
|
# @param error [Exception, nil] The exception if failed
|
|
441
449
|
# @return [void]
|
|
442
450
|
def legacy_log_execution(completed_at:, status:, response: nil, error: nil)
|
|
451
|
+
config = RubyLLM::Agents.configuration
|
|
452
|
+
|
|
443
453
|
execution_data = {
|
|
444
454
|
agent_type: self.class.name,
|
|
445
455
|
agent_version: self.class.version,
|
|
@@ -452,7 +462,9 @@ module RubyLLM
|
|
|
452
462
|
parameters: sanitized_parameters,
|
|
453
463
|
metadata: execution_metadata,
|
|
454
464
|
system_prompt: safe_system_prompt,
|
|
455
|
-
user_prompt: safe_user_prompt
|
|
465
|
+
user_prompt: safe_user_prompt,
|
|
466
|
+
messages_count: resolved_messages.size,
|
|
467
|
+
messages_summary: config.persist_messages_summary ? messages_summary : {}
|
|
456
468
|
}
|
|
457
469
|
|
|
458
470
|
# Add response data if available (using safe extraction)
|
|
@@ -516,6 +528,38 @@ module RubyLLM
|
|
|
516
528
|
Redactor.redact_string(prompt)
|
|
517
529
|
end
|
|
518
530
|
|
|
531
|
+
# Returns a summary of messages (first and last, truncated)
|
|
532
|
+
#
|
|
533
|
+
# Creates a summary of the conversation messages containing the first
|
|
534
|
+
# and last messages (if different) with content truncated for storage.
|
|
535
|
+
#
|
|
536
|
+
# @return [Hash] Summary with :first and :last message hashes, or empty hash
|
|
537
|
+
def messages_summary
|
|
538
|
+
msgs = resolved_messages
|
|
539
|
+
return {} if msgs.blank?
|
|
540
|
+
|
|
541
|
+
max_len = RubyLLM::Agents.configuration.messages_summary_max_length || 500
|
|
542
|
+
|
|
543
|
+
summary = {}
|
|
544
|
+
|
|
545
|
+
if msgs.first
|
|
546
|
+
summary[:first] = {
|
|
547
|
+
role: msgs.first[:role].to_s,
|
|
548
|
+
content: Redactor.redact_string(msgs.first[:content].to_s).truncate(max_len)
|
|
549
|
+
}
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
# Only add last if there are multiple messages and last is different from first
|
|
553
|
+
if msgs.size > 1 && msgs.last
|
|
554
|
+
summary[:last] = {
|
|
555
|
+
role: msgs.last[:role].to_s,
|
|
556
|
+
content: Redactor.redact_string(msgs.last[:content].to_s).truncate(max_len)
|
|
557
|
+
}
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
summary
|
|
561
|
+
end
|
|
562
|
+
|
|
519
563
|
# Returns the response with redaction applied
|
|
520
564
|
#
|
|
521
565
|
# @param response [RubyLLM::Message] The LLM response
|
|
@@ -773,7 +817,9 @@ module RubyLLM
|
|
|
773
817
|
total_cost: 0,
|
|
774
818
|
parameters: redacted_parameters,
|
|
775
819
|
metadata: execution_metadata,
|
|
776
|
-
streaming: self.class.streaming
|
|
820
|
+
streaming: self.class.streaming,
|
|
821
|
+
messages_count: resolved_messages.size,
|
|
822
|
+
messages_summary: config.persist_messages_summary ? messages_summary : {}
|
|
777
823
|
}
|
|
778
824
|
|
|
779
825
|
# Add tracing fields from metadata if present
|
|
@@ -798,6 +844,28 @@ module RubyLLM
|
|
|
798
844
|
Rails.logger.error("[RubyLLM::Agents] Failed to record cache hit execution: #{e.message}")
|
|
799
845
|
end
|
|
800
846
|
|
|
847
|
+
# Records token usage to the BudgetTracker
|
|
848
|
+
#
|
|
849
|
+
# @param execution [Execution] The completed execution record
|
|
850
|
+
# @return [void]
|
|
851
|
+
def record_token_usage(execution)
|
|
852
|
+
return unless execution&.total_tokens && execution.total_tokens > 0
|
|
853
|
+
|
|
854
|
+
begin
|
|
855
|
+
tenant_id = respond_to?(:resolved_tenant_id) ? resolved_tenant_id : nil
|
|
856
|
+
tenant_config = respond_to?(:runtime_tenant_config) ? runtime_tenant_config : nil
|
|
857
|
+
|
|
858
|
+
BudgetTracker.record_tokens!(
|
|
859
|
+
self.class.name,
|
|
860
|
+
execution.total_tokens,
|
|
861
|
+
tenant_id: tenant_id,
|
|
862
|
+
tenant_config: tenant_config
|
|
863
|
+
)
|
|
864
|
+
rescue StandardError => e
|
|
865
|
+
Rails.logger.warn("[RubyLLM::Agents] Failed to record token usage: #{e.message}")
|
|
866
|
+
end
|
|
867
|
+
end
|
|
868
|
+
|
|
801
869
|
# Emergency fallback to mark execution as failed
|
|
802
870
|
#
|
|
803
871
|
# Uses update_all to bypass ActiveRecord callbacks and validations,
|