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 +4 -4
- data/app/models/ruby_llm/agents/tenant/budgetable.rb +277 -0
- data/app/models/ruby_llm/agents/tenant/configurable.rb +135 -0
- data/app/models/ruby_llm/agents/tenant/trackable.rb +310 -0
- data/app/models/ruby_llm/agents/tenant.rb +146 -0
- data/app/models/ruby_llm/agents/tenant_budget.rb +12 -253
- data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +42 -22
- data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +13 -2
- data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +11 -0
- data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +72 -0
- data/lib/generators/ruby_llm_agents/templates/rename_tenant_budgets_to_tenants_migration.rb.tt +34 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +26 -0
- data/lib/ruby_llm/agents/core/llm_tenant.rb +60 -60
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +4 -2
- metadata +7 -1
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
class Tenant
|
|
6
|
+
# Tracks LLM usage for a tenant including costs, tokens, and execution counts.
|
|
7
|
+
#
|
|
8
|
+
# Provides methods for querying usage data with various time periods:
|
|
9
|
+
# - :today, :yesterday, :this_week, :last_week
|
|
10
|
+
# - :this_month, :last_month
|
|
11
|
+
# - Custom date ranges
|
|
12
|
+
#
|
|
13
|
+
# @example Querying costs
|
|
14
|
+
# tenant.cost # Total cost all time
|
|
15
|
+
# tenant.cost_today # Today's cost
|
|
16
|
+
# tenant.cost_this_month # This month's cost
|
|
17
|
+
# tenant.cost(period: 1.week.ago..Time.current)
|
|
18
|
+
#
|
|
19
|
+
# @example Usage summary
|
|
20
|
+
# tenant.usage_summary
|
|
21
|
+
# # => { tenant_id: "acme", cost: 123.45, tokens: 50000, executions: 100 }
|
|
22
|
+
#
|
|
23
|
+
# @example Usage breakdown
|
|
24
|
+
# tenant.usage_by_agent
|
|
25
|
+
# # => { "ChatAgent" => { cost: 50.0, tokens: 20000, count: 40 } }
|
|
26
|
+
#
|
|
27
|
+
# @see Execution
|
|
28
|
+
# @api public
|
|
29
|
+
module Trackable
|
|
30
|
+
extend ActiveSupport::Concern
|
|
31
|
+
|
|
32
|
+
included do
|
|
33
|
+
# Association to executions via tenant_id
|
|
34
|
+
has_many :executions,
|
|
35
|
+
class_name: "RubyLLM::Agents::Execution",
|
|
36
|
+
primary_key: :tenant_id,
|
|
37
|
+
foreign_key: :tenant_id,
|
|
38
|
+
inverse_of: false
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Cost queries
|
|
42
|
+
|
|
43
|
+
# Returns total cost for the given period
|
|
44
|
+
#
|
|
45
|
+
# @param period [Symbol, Range, nil] Time period (:today, :this_month, etc.) or date range
|
|
46
|
+
# @return [Float] Total cost in USD
|
|
47
|
+
def cost(period: nil)
|
|
48
|
+
scope = executions
|
|
49
|
+
scope = apply_period_scope(scope, period) if period
|
|
50
|
+
scope.sum(:total_cost) || 0
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Returns today's cost
|
|
54
|
+
#
|
|
55
|
+
# @return [Float]
|
|
56
|
+
def cost_today
|
|
57
|
+
cost(period: :today)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Returns yesterday's cost
|
|
61
|
+
#
|
|
62
|
+
# @return [Float]
|
|
63
|
+
def cost_yesterday
|
|
64
|
+
cost(period: :yesterday)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Returns this week's cost
|
|
68
|
+
#
|
|
69
|
+
# @return [Float]
|
|
70
|
+
def cost_this_week
|
|
71
|
+
cost(period: :this_week)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Returns last week's cost
|
|
75
|
+
#
|
|
76
|
+
# @return [Float]
|
|
77
|
+
def cost_last_week
|
|
78
|
+
cost(period: :last_week)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Returns this month's cost
|
|
82
|
+
#
|
|
83
|
+
# @return [Float]
|
|
84
|
+
def cost_this_month
|
|
85
|
+
cost(period: :this_month)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Returns last month's cost
|
|
89
|
+
#
|
|
90
|
+
# @return [Float]
|
|
91
|
+
def cost_last_month
|
|
92
|
+
cost(period: :last_month)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Token queries
|
|
96
|
+
|
|
97
|
+
# Returns total tokens for the given period
|
|
98
|
+
#
|
|
99
|
+
# @param period [Symbol, Range, nil] Time period or date range
|
|
100
|
+
# @return [Integer] Total tokens
|
|
101
|
+
def tokens(period: nil)
|
|
102
|
+
scope = executions
|
|
103
|
+
scope = apply_period_scope(scope, period) if period
|
|
104
|
+
scope.sum(:total_tokens) || 0
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Returns today's token usage
|
|
108
|
+
#
|
|
109
|
+
# @return [Integer]
|
|
110
|
+
def tokens_today
|
|
111
|
+
tokens(period: :today)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Returns yesterday's token usage
|
|
115
|
+
#
|
|
116
|
+
# @return [Integer]
|
|
117
|
+
def tokens_yesterday
|
|
118
|
+
tokens(period: :yesterday)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Returns this week's token usage
|
|
122
|
+
#
|
|
123
|
+
# @return [Integer]
|
|
124
|
+
def tokens_this_week
|
|
125
|
+
tokens(period: :this_week)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Returns this month's token usage
|
|
129
|
+
#
|
|
130
|
+
# @return [Integer]
|
|
131
|
+
def tokens_this_month
|
|
132
|
+
tokens(period: :this_month)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Returns last month's token usage
|
|
136
|
+
#
|
|
137
|
+
# @return [Integer]
|
|
138
|
+
def tokens_last_month
|
|
139
|
+
tokens(period: :last_month)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Execution count queries
|
|
143
|
+
|
|
144
|
+
# Returns execution count for the given period
|
|
145
|
+
#
|
|
146
|
+
# @param period [Symbol, Range, nil] Time period or date range
|
|
147
|
+
# @return [Integer] Execution count
|
|
148
|
+
def execution_count(period: nil)
|
|
149
|
+
scope = executions
|
|
150
|
+
scope = apply_period_scope(scope, period) if period
|
|
151
|
+
scope.count
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Returns today's execution count
|
|
155
|
+
#
|
|
156
|
+
# @return [Integer]
|
|
157
|
+
def executions_today
|
|
158
|
+
execution_count(period: :today)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Returns yesterday's execution count
|
|
162
|
+
#
|
|
163
|
+
# @return [Integer]
|
|
164
|
+
def executions_yesterday
|
|
165
|
+
execution_count(period: :yesterday)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Returns this week's execution count
|
|
169
|
+
#
|
|
170
|
+
# @return [Integer]
|
|
171
|
+
def executions_this_week
|
|
172
|
+
execution_count(period: :this_week)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Returns this month's execution count
|
|
176
|
+
#
|
|
177
|
+
# @return [Integer]
|
|
178
|
+
def executions_this_month
|
|
179
|
+
execution_count(period: :this_month)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Returns last month's execution count
|
|
183
|
+
#
|
|
184
|
+
# @return [Integer]
|
|
185
|
+
def executions_last_month
|
|
186
|
+
execution_count(period: :last_month)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Usage summaries
|
|
190
|
+
|
|
191
|
+
# Returns a complete usage summary for the tenant
|
|
192
|
+
#
|
|
193
|
+
# @param period [Symbol, Range] Time period (default: :this_month)
|
|
194
|
+
# @return [Hash] Usage summary
|
|
195
|
+
def usage_summary(period: :this_month)
|
|
196
|
+
{
|
|
197
|
+
tenant_id: tenant_id,
|
|
198
|
+
name: display_name,
|
|
199
|
+
period: period,
|
|
200
|
+
cost: cost(period: period),
|
|
201
|
+
tokens: tokens(period: period),
|
|
202
|
+
executions: execution_count(period: period)
|
|
203
|
+
}
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Returns usage broken down by agent type
|
|
207
|
+
#
|
|
208
|
+
# @param period [Symbol, Range] Time period (default: :this_month)
|
|
209
|
+
# @return [Hash] Usage by agent { "AgentName" => { cost:, tokens:, count: } }
|
|
210
|
+
def usage_by_agent(period: :this_month)
|
|
211
|
+
scope = executions
|
|
212
|
+
scope = apply_period_scope(scope, period) if period
|
|
213
|
+
|
|
214
|
+
scope.group(:agent_type).pluck(
|
|
215
|
+
:agent_type,
|
|
216
|
+
Arel.sql("SUM(total_cost)"),
|
|
217
|
+
Arel.sql("SUM(total_tokens)"),
|
|
218
|
+
Arel.sql("COUNT(*)")
|
|
219
|
+
).to_h do |agent_type, total_cost, total_tokens, count|
|
|
220
|
+
[agent_type, { cost: total_cost || 0, tokens: total_tokens || 0, count: count }]
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Returns usage broken down by model
|
|
225
|
+
#
|
|
226
|
+
# @param period [Symbol, Range] Time period (default: :this_month)
|
|
227
|
+
# @return [Hash] Usage by model { "model-id" => { cost:, tokens:, count: } }
|
|
228
|
+
def usage_by_model(period: :this_month)
|
|
229
|
+
scope = executions
|
|
230
|
+
scope = apply_period_scope(scope, period) if period
|
|
231
|
+
|
|
232
|
+
scope.group(:model_id).pluck(
|
|
233
|
+
:model_id,
|
|
234
|
+
Arel.sql("SUM(total_cost)"),
|
|
235
|
+
Arel.sql("SUM(total_tokens)"),
|
|
236
|
+
Arel.sql("COUNT(*)")
|
|
237
|
+
).to_h do |model_id, total_cost, total_tokens, count|
|
|
238
|
+
[model_id, { cost: total_cost || 0, tokens: total_tokens || 0, count: count }]
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Returns usage broken down by day for a given period
|
|
243
|
+
#
|
|
244
|
+
# @param period [Symbol, Range] Time period (default: :this_month)
|
|
245
|
+
# @return [Hash] Daily usage { Date => { cost:, tokens:, count: } }
|
|
246
|
+
def usage_by_day(period: :this_month)
|
|
247
|
+
scope = executions
|
|
248
|
+
scope = apply_period_scope(scope, period) if period
|
|
249
|
+
|
|
250
|
+
scope.group("DATE(created_at)").pluck(
|
|
251
|
+
Arel.sql("DATE(created_at)"),
|
|
252
|
+
Arel.sql("SUM(total_cost)"),
|
|
253
|
+
Arel.sql("SUM(total_tokens)"),
|
|
254
|
+
Arel.sql("COUNT(*)")
|
|
255
|
+
).to_h do |date, total_cost, total_tokens, count|
|
|
256
|
+
[date.to_date, { cost: total_cost || 0, tokens: total_tokens || 0, count: count }]
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Returns the most recent executions for this tenant
|
|
261
|
+
#
|
|
262
|
+
# @param limit [Integer] Number of executions to return (default: 10)
|
|
263
|
+
# @return [Array<Execution>]
|
|
264
|
+
def recent_executions(limit: 10)
|
|
265
|
+
executions.order(created_at: :desc).limit(limit)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Returns failed executions for this tenant
|
|
269
|
+
#
|
|
270
|
+
# @param period [Symbol, Range, nil] Time period
|
|
271
|
+
# @param limit [Integer, nil] Optional limit
|
|
272
|
+
# @return [ActiveRecord::Relation]
|
|
273
|
+
def failed_executions(period: nil, limit: nil)
|
|
274
|
+
scope = executions.where(status: "error")
|
|
275
|
+
scope = apply_period_scope(scope, period) if period
|
|
276
|
+
scope = scope.limit(limit) if limit
|
|
277
|
+
scope.order(created_at: :desc)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
private
|
|
281
|
+
|
|
282
|
+
# Applies time period scope to a query
|
|
283
|
+
#
|
|
284
|
+
# @param scope [ActiveRecord::Relation]
|
|
285
|
+
# @param period [Symbol, Range]
|
|
286
|
+
# @return [ActiveRecord::Relation]
|
|
287
|
+
def apply_period_scope(scope, period)
|
|
288
|
+
case period
|
|
289
|
+
when :today
|
|
290
|
+
scope.where(created_at: Time.current.all_day)
|
|
291
|
+
when :yesterday
|
|
292
|
+
scope.where(created_at: 1.day.ago.all_day)
|
|
293
|
+
when :this_week
|
|
294
|
+
scope.where(created_at: Time.current.all_week)
|
|
295
|
+
when :last_week
|
|
296
|
+
scope.where(created_at: 1.week.ago.all_week)
|
|
297
|
+
when :this_month
|
|
298
|
+
scope.where(created_at: Time.current.all_month)
|
|
299
|
+
when :last_month
|
|
300
|
+
scope.where(created_at: 1.month.ago.all_month)
|
|
301
|
+
when Range
|
|
302
|
+
scope.where(created_at: period)
|
|
303
|
+
else
|
|
304
|
+
scope
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
# Central model for tenant management in multi-tenant LLM applications.
|
|
6
|
+
#
|
|
7
|
+
# Encapsulates all tenant-related functionality:
|
|
8
|
+
# - Budget limits and enforcement (via Budgetable concern)
|
|
9
|
+
# - Usage tracking: cost, tokens, executions (via Trackable concern)
|
|
10
|
+
# - API configuration per tenant (via Configurable concern)
|
|
11
|
+
#
|
|
12
|
+
# @example Creating a tenant
|
|
13
|
+
# Tenant.create!(
|
|
14
|
+
# tenant_id: "acme_corp",
|
|
15
|
+
# name: "Acme Corporation",
|
|
16
|
+
# daily_limit: 100.0,
|
|
17
|
+
# enforcement: "hard"
|
|
18
|
+
# )
|
|
19
|
+
#
|
|
20
|
+
# @example Linking to user's model
|
|
21
|
+
# Tenant.create!(
|
|
22
|
+
# tenant_id: organization.id.to_s,
|
|
23
|
+
# tenant_record: organization,
|
|
24
|
+
# daily_limit: 100.0
|
|
25
|
+
# )
|
|
26
|
+
#
|
|
27
|
+
# @example Finding a tenant
|
|
28
|
+
# tenant = Tenant.for(organization)
|
|
29
|
+
# tenant = Tenant.for("acme_corp")
|
|
30
|
+
#
|
|
31
|
+
# @see Tenant::Budgetable
|
|
32
|
+
# @see Tenant::Trackable
|
|
33
|
+
# @see Tenant::Configurable
|
|
34
|
+
# @see LLMTenant
|
|
35
|
+
# @api public
|
|
36
|
+
class Tenant < ::ActiveRecord::Base
|
|
37
|
+
self.table_name = "ruby_llm_agents_tenants"
|
|
38
|
+
|
|
39
|
+
# Include concerns for organized functionality
|
|
40
|
+
include Tenant::Budgetable
|
|
41
|
+
include Tenant::Trackable
|
|
42
|
+
include Tenant::Configurable
|
|
43
|
+
|
|
44
|
+
# Polymorphic association to user's tenant model (optional)
|
|
45
|
+
# Allows linking to Organization, Account, or any ActiveRecord model
|
|
46
|
+
belongs_to :tenant_record, polymorphic: true, optional: true
|
|
47
|
+
|
|
48
|
+
# Validations
|
|
49
|
+
validates :tenant_id, presence: true, uniqueness: true
|
|
50
|
+
|
|
51
|
+
# Scopes
|
|
52
|
+
scope :active, -> { where(active: true) }
|
|
53
|
+
scope :inactive, -> { where(active: false) }
|
|
54
|
+
scope :linked, -> { where.not(tenant_record_type: nil) }
|
|
55
|
+
scope :unlinked, -> { where(tenant_record_type: nil) }
|
|
56
|
+
|
|
57
|
+
# Find tenant for given record or ID
|
|
58
|
+
#
|
|
59
|
+
# Supports multiple lookup strategies:
|
|
60
|
+
# 1. ActiveRecord model - looks up by polymorphic association first, then tenant_id
|
|
61
|
+
# 2. Object with llm_tenant_id - looks up by tenant_id
|
|
62
|
+
# 3. String - looks up by tenant_id
|
|
63
|
+
#
|
|
64
|
+
# @param tenant [String, ActiveRecord::Base, Object] Tenant ID, model, or object with llm_tenant_id
|
|
65
|
+
# @return [Tenant, nil] The tenant record or nil if not found
|
|
66
|
+
#
|
|
67
|
+
# @example Find by model
|
|
68
|
+
# Tenant.for(organization)
|
|
69
|
+
#
|
|
70
|
+
# @example Find by string ID
|
|
71
|
+
# Tenant.for("acme_corp")
|
|
72
|
+
#
|
|
73
|
+
def self.for(tenant)
|
|
74
|
+
return nil if tenant.blank?
|
|
75
|
+
|
|
76
|
+
if tenant.is_a?(::ActiveRecord::Base)
|
|
77
|
+
# ActiveRecord model - try polymorphic first, then tenant_id
|
|
78
|
+
find_by(tenant_record: tenant) ||
|
|
79
|
+
find_by(tenant_id: tenant.try(:llm_tenant_id) || tenant.id.to_s)
|
|
80
|
+
elsif tenant.respond_to?(:llm_tenant_id)
|
|
81
|
+
# Object with llm_tenant_id method
|
|
82
|
+
find_by(tenant_id: tenant.llm_tenant_id)
|
|
83
|
+
else
|
|
84
|
+
# String tenant_id
|
|
85
|
+
find_by(tenant_id: tenant.to_s)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Find or create tenant
|
|
90
|
+
#
|
|
91
|
+
# @param tenant_id [String] Unique tenant identifier
|
|
92
|
+
# @param attributes [Hash] Additional attributes for creation
|
|
93
|
+
# @return [Tenant]
|
|
94
|
+
#
|
|
95
|
+
# @example
|
|
96
|
+
# Tenant.for!("acme_corp", name: "Acme Corporation")
|
|
97
|
+
#
|
|
98
|
+
def self.for!(tenant_id, **attributes)
|
|
99
|
+
find_or_create_by!(tenant_id: tenant_id.to_s) do |tenant|
|
|
100
|
+
tenant.assign_attributes(attributes)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Backward compatible class method aliases
|
|
105
|
+
class << self
|
|
106
|
+
alias_method :for_tenant, :for
|
|
107
|
+
alias_method :for_tenant!, :for!
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Display name (name or tenant_id fallback)
|
|
111
|
+
#
|
|
112
|
+
# @return [String]
|
|
113
|
+
def display_name
|
|
114
|
+
name.presence || tenant_id
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Check if tenant is linked to a user model
|
|
118
|
+
#
|
|
119
|
+
# @return [Boolean]
|
|
120
|
+
def linked?
|
|
121
|
+
tenant_record.present?
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Check if tenant is active
|
|
125
|
+
#
|
|
126
|
+
# @return [Boolean]
|
|
127
|
+
def active?
|
|
128
|
+
active != false
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Deactivate the tenant
|
|
132
|
+
#
|
|
133
|
+
# @return [Boolean]
|
|
134
|
+
def deactivate!
|
|
135
|
+
update!(active: false)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Activate the tenant
|
|
139
|
+
#
|
|
140
|
+
# @return [Boolean]
|
|
141
|
+
def activate!
|
|
142
|
+
update!(active: true)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|