ruby_llm-agents 1.1.0 → 1.2.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.
@@ -147,6 +147,26 @@ module RubyLlmAgents
147
147
  )
148
148
  end
149
149
 
150
+ def create_rename_tenant_budgets_migration
151
+ # Skip if already using new table name
152
+ if table_exists?(:ruby_llm_agents_tenants)
153
+ say_status :skip, "ruby_llm_agents_tenants table already exists", :yellow
154
+ return
155
+ end
156
+
157
+ # Only run if old table exists (needs upgrade)
158
+ unless table_exists?(:ruby_llm_agents_tenant_budgets)
159
+ say_status :skip, "No tenant_budgets table to upgrade", :yellow
160
+ return
161
+ end
162
+
163
+ say_status :upgrade, "Renaming tenant_budgets to tenants", :blue
164
+ migration_template(
165
+ "rename_tenant_budgets_to_tenants_migration.rb.tt",
166
+ File.join(db_migrate_path, "rename_tenant_budgets_to_tenants.rb")
167
+ )
168
+ end
169
+
150
170
  def migrate_agents_directory
151
171
  root_dir = RubyLLM::Agents.configuration.root_directory
152
172
  namespace = RubyLLM::Agents.configuration.root_namespace
@@ -211,6 +231,12 @@ module RubyLlmAgents
211
231
  false
212
232
  end
213
233
 
234
+ def table_exists?(table)
235
+ ActiveRecord::Base.connection.table_exists?(table)
236
+ rescue StandardError
237
+ false
238
+ end
239
+
214
240
  def migrate_directory(old_dir, new_dir, namespace)
215
241
  source = Rails.root.join("app", old_dir)
216
242
  destination = Rails.root.join("app", new_dir)
@@ -353,21 +353,21 @@ module RubyLLM
353
353
  # config.async_max_concurrency = 20
354
354
 
355
355
  # @!attribute [rw] root_directory
356
- # The root directory name under app/ for all LLM components.
356
+ # The root directory name under app/ for all agent components.
357
357
  # This allows customization of the directory structure.
358
- # @return [String] Directory name (default: "llm")
358
+ # @return [String] Directory name (default: "agents")
359
359
  # @example
360
- # config.root_directory = "ai" # Creates app/ai/ instead of app/llm/
360
+ # config.root_directory = "ai" # Creates app/ai/ instead of app/agents/
361
361
 
362
362
  # @!attribute [rw] root_namespace
363
- # The root namespace for all LLM component classes.
364
- # This should match the root_directory in camelized form.
365
- # Set to nil or "" to use no namespace (classes at top-level).
366
- # @return [String, nil] Namespace (default: "Llm")
363
+ # The root namespace for all agent component classes.
364
+ # When set, subdirectory namespaces are prefixed with this.
365
+ # Set to nil or "" to use no root namespace.
366
+ # @return [String, nil] Namespace (default: nil)
367
367
  # @example Custom namespace
368
- # config.root_namespace = "AI" # Uses AI:: instead of Llm::
369
- # @example No namespace (top-level classes)
370
- # config.root_namespace = nil # Uses top-level classes (ApplicationAgent, etc.)
368
+ # config.root_namespace = "AI" # app/agents/embedders -> AI::Embedders
369
+ # @example No namespace (default)
370
+ # config.root_namespace = nil # app/agents/embedders -> Embedders
371
371
 
372
372
  # Attributes without validation (simple accessors)
373
373
  attr_accessor :default_model,
@@ -821,26 +821,18 @@ module RubyLLM
821
821
 
822
822
  # Returns the full namespace for a given category
823
823
  #
824
- # @param category [Symbol, nil] Category (:audio, :image, :text, or nil for root)
824
+ # @param category [Symbol, String, nil] Category (e.g., :embedders, :images, or nil for root)
825
825
  # @return [String, nil] Full namespace string, or nil if no namespace configured
826
- # @example With root_namespace = "Llm"
827
- # namespace_for(:image) #=> "Llm::Image"
828
- # namespace_for(nil) #=> "Llm"
826
+ # @example With root_namespace = "AI"
827
+ # namespace_for(:embedders) #=> "AI::Embedders"
828
+ # namespace_for(nil) #=> "AI"
829
829
  # @example With no namespace (root_namespace = nil)
830
- # namespace_for(:image) #=> "Image"
831
- # namespace_for(nil) #=> nil
830
+ # namespace_for(:embedders) #=> "Embedders"
831
+ # namespace_for(nil) #=> nil
832
832
  def namespace_for(category = nil)
833
- category_namespace = case category
834
- when :images then "Images"
835
- when :audio then "Audio"
836
- when :embedders then "Embedders"
837
- when :moderators then "Moderators"
838
- when :workflows then "Workflows"
839
- when :text then "Text"
840
- when :image then "Image"
841
- end
833
+ category_namespace = category&.to_s&.camelize
842
834
 
843
- if root_namespace
835
+ if root_namespace.present?
844
836
  category_namespace ? "#{root_namespace}::#{category_namespace}" : root_namespace
845
837
  else
846
838
  category_namespace
@@ -12,21 +12,25 @@ module RubyLLM
12
12
  #
13
13
  # @example Basic usage
14
14
  # class Organization < ApplicationRecord
15
+ # include RubyLLM::Agents::LLMTenant
15
16
  # llm_tenant
16
17
  # end
17
18
  #
18
19
  # @example With custom ID method
19
20
  # class Organization < ApplicationRecord
21
+ # include RubyLLM::Agents::LLMTenant
20
22
  # llm_tenant id: :slug
21
23
  # end
22
24
  #
23
25
  # @example With auto-created budget
24
26
  # class Organization < ApplicationRecord
27
+ # include RubyLLM::Agents::LLMTenant
25
28
  # llm_tenant id: :slug, budget: true
26
29
  # end
27
30
  #
28
31
  # @example With limits (auto-creates budget)
29
32
  # class Organization < ApplicationRecord
33
+ # include RubyLLM::Agents::LLMTenant
30
34
  # llm_tenant(
31
35
  # id: :slug,
32
36
  # name: :company_name,
@@ -41,6 +45,7 @@ module RubyLLM
41
45
  #
42
46
  # @example With API keys from model columns/methods
43
47
  # class Organization < ApplicationRecord
48
+ # include RubyLLM::Agents::LLMTenant
44
49
  # encrypts :openai_api_key, :anthropic_api_key # Rails 7+ encryption
45
50
  #
46
51
  # llm_tenant(
@@ -57,7 +62,7 @@ module RubyLLM
57
62
  # end
58
63
  # end
59
64
  #
60
- # @see RubyLLM::Agents::TenantBudget
65
+ # @see RubyLLM::Agents::Tenant
61
66
  # @api public
62
67
  module LLMTenant
63
68
  extend ActiveSupport::Concern
@@ -69,12 +74,16 @@ module RubyLLM
69
74
  as: :tenant_record,
70
75
  dependent: :nullify
71
76
 
72
- # Budget association (optional)
73
- has_one :llm_budget,
74
- class_name: "RubyLLM::Agents::TenantBudget",
77
+ # Link to gem's Tenant model (new name)
78
+ has_one :llm_tenant_record,
79
+ class_name: "RubyLLM::Agents::Tenant",
75
80
  as: :tenant_record,
76
81
  dependent: :destroy
77
82
 
83
+ # Backward compatible alias (llm_budget points to same Tenant record)
84
+ # @deprecated Use llm_tenant_record instead
85
+ alias_method :llm_budget_association, :llm_tenant_record
86
+
78
87
  # Store options at class level
79
88
  class_attribute :llm_tenant_options, default: {}
80
89
  end
@@ -84,7 +93,7 @@ module RubyLLM
84
93
  #
85
94
  # @param id [Symbol] Method to call for tenant_id string (default: :id)
86
95
  # @param name [Symbol] Method for budget display name (default: :to_s)
87
- # @param budget [Boolean] Auto-create TenantBudget on model creation (default: false)
96
+ # @param budget [Boolean] Auto-create Tenant record on model creation (default: false)
88
97
  # @param limits [Hash] Default budget limits (implies budget: true)
89
98
  # @param enforcement [Symbol] Budget enforcement mode (:none, :soft, :hard)
90
99
  # @param inherit_global [Boolean] Inherit from global config (default: true)
@@ -101,8 +110,8 @@ module RubyLLM
101
110
  api_keys: api_keys
102
111
  }
103
112
 
104
- # Auto-create budget callback
105
- after_create :create_default_llm_budget if llm_tenant_options[:budget]
113
+ # Auto-create tenant record callback
114
+ after_create :create_default_llm_tenant if llm_tenant_options[:budget]
106
115
  end
107
116
 
108
117
  private
@@ -152,13 +161,42 @@ module RubyLLM
152
161
  end.compact
153
162
  end
154
163
 
164
+ # Returns or builds the associated Tenant record
165
+ #
166
+ # @return [Tenant] The tenant record
167
+ def llm_tenant
168
+ llm_tenant_record || build_llm_tenant_record(tenant_id: llm_tenant_id)
169
+ end
170
+
171
+ # Backward compatible alias for llm_tenant
172
+ # @deprecated Use llm_tenant instead
173
+ alias_method :llm_budget, :llm_tenant
174
+
175
+ # Configure tenant with a block
176
+ #
177
+ # @yield [tenant] The tenant to configure
178
+ # @return [Tenant] The saved tenant
179
+ def llm_configure(&block)
180
+ tenant = llm_tenant
181
+ yield(tenant) if block_given?
182
+ tenant.save!
183
+ tenant
184
+ end
185
+
186
+ # Backward compatible alias
187
+ # @deprecated Use llm_configure instead
188
+ alias_method :llm_configure_budget, :llm_configure
189
+
190
+ # Tracking methods using llm_executions association
191
+ # These query executions via the polymorphic tenant_record association
192
+
155
193
  # Returns cost for a given period
156
194
  #
157
195
  # @param period [Symbol, Range, nil] Time period (:today, :this_month, etc.)
158
196
  # @return [BigDecimal] Total cost
159
197
  def llm_cost(period: nil)
160
198
  scope = llm_executions
161
- scope = apply_period_scope(scope, period) if period
199
+ scope = apply_llm_period_scope(scope, period) if period
162
200
  scope.sum(:total_cost) || 0
163
201
  end
164
202
 
@@ -182,7 +220,7 @@ module RubyLLM
182
220
  # @return [Integer] Total tokens
183
221
  def llm_tokens(period: nil)
184
222
  scope = llm_executions
185
- scope = apply_period_scope(scope, period) if period
223
+ scope = apply_llm_period_scope(scope, period) if period
186
224
  scope.sum(:total_tokens) || 0
187
225
  end
188
226
 
@@ -206,7 +244,7 @@ module RubyLLM
206
244
  # @return [Integer] Execution count
207
245
  def llm_execution_count(period: nil)
208
246
  scope = llm_executions
209
- scope = apply_period_scope(scope, period) if period
247
+ scope = apply_llm_period_scope(scope, period) if period
210
248
  scope.count
211
249
  end
212
250
 
@@ -237,29 +275,13 @@ module RubyLLM
237
275
  }
238
276
  end
239
277
 
240
- # Returns or builds the associated TenantBudget
241
- #
242
- # @return [TenantBudget] The budget record
243
- def llm_budget
244
- super || build_llm_budget(tenant_id: llm_tenant_id)
245
- end
246
-
247
- # Configure budget with a block
248
- #
249
- # @yield [budget] The budget to configure
250
- # @return [TenantBudget] The saved budget
251
- def llm_configure_budget
252
- budget = llm_budget
253
- yield(budget) if block_given?
254
- budget.save!
255
- budget
256
- end
278
+ # Delegate budget methods to the Tenant record
257
279
 
258
280
  # Returns the budget status from BudgetTracker
259
281
  #
260
282
  # @return [Hash] Budget status
261
283
  def llm_budget_status
262
- BudgetTracker.status(tenant_id: llm_tenant_id)
284
+ llm_tenant.budget_status
263
285
  end
264
286
 
265
287
  # Checks if within budget for a given limit type
@@ -267,11 +289,7 @@ module RubyLLM
267
289
  # @param type [Symbol] Limit type (:daily_cost, :monthly_cost, :daily_tokens, etc.)
268
290
  # @return [Boolean] true if within budget
269
291
  def llm_within_budget?(type: :daily_cost)
270
- status = llm_budget_status
271
- return true unless status[:enabled]
272
-
273
- key = budget_status_key(type)
274
- status.dig(key, :percentage_used).to_f < 100
292
+ llm_tenant.within_budget?(type: type)
275
293
  end
276
294
 
277
295
  # Returns remaining budget for a given limit type
@@ -279,9 +297,7 @@ module RubyLLM
279
297
  # @param type [Symbol] Limit type
280
298
  # @return [Numeric, nil] Remaining amount
281
299
  def llm_remaining_budget(type: :daily_cost)
282
- status = llm_budget_status
283
- key = budget_status_key(type)
284
- status.dig(key, :remaining)
300
+ llm_tenant.remaining_budget(type: type)
285
301
  end
286
302
 
287
303
  # Raises an error if over budget
@@ -289,7 +305,7 @@ module RubyLLM
289
305
  # @raise [BudgetExceededError] if budget is exceeded
290
306
  # @return [void]
291
307
  def llm_check_budget!
292
- BudgetTracker.check_budget!(self.class.name, tenant_id: llm_tenant_id)
308
+ llm_tenant.check_budget!(self.class.name)
293
309
  end
294
310
 
295
311
  private
@@ -299,7 +315,7 @@ module RubyLLM
299
315
  # @param scope [ActiveRecord::Relation] The query scope
300
316
  # @param period [Symbol, Range] The period to filter by
301
317
  # @return [ActiveRecord::Relation] Filtered scope
302
- def apply_period_scope(scope, period)
318
+ def apply_llm_period_scope(scope, period)
303
319
  case period
304
320
  when :today then scope.where(created_at: Time.current.all_day)
305
321
  when :yesterday then scope.where(created_at: 1.day.ago.all_day)
@@ -310,34 +326,18 @@ module RubyLLM
310
326
  end
311
327
  end
312
328
 
313
- # Maps user-friendly type to budget status key
314
- #
315
- # @param type [Symbol] User-friendly type
316
- # @return [Symbol] Status key
317
- def budget_status_key(type)
318
- case type
319
- when :daily_cost then :global_daily
320
- when :monthly_cost then :global_monthly
321
- when :daily_tokens then :global_daily_tokens
322
- when :monthly_tokens then :global_monthly_tokens
323
- when :daily_executions then :global_daily_executions
324
- when :monthly_executions then :global_monthly_executions
325
- else :global_daily
326
- end
327
- end
328
-
329
- # Creates the default budget on model creation
329
+ # Creates the default tenant record on model creation
330
330
  #
331
331
  # @return [void]
332
- def create_default_llm_budget
332
+ def create_default_llm_tenant
333
333
  return if self.class.llm_tenant_options.blank?
334
- return if llm_budget&.persisted?
334
+ return if llm_tenant_record&.persisted?
335
335
 
336
336
  options = self.class.llm_tenant_options
337
337
  limits = options[:limits] || {}
338
338
  name_method = options[:name] || :to_s
339
339
 
340
- budget = build_llm_budget(
340
+ tenant = build_llm_tenant_record(
341
341
  tenant_id: llm_tenant_id,
342
342
  name: send(name_method).to_s,
343
343
  daily_limit: limits[:daily_cost],
@@ -350,8 +350,8 @@ module RubyLLM
350
350
  inherit_global_defaults: options.fetch(:inherit_global, true)
351
351
  )
352
352
 
353
- budget.tenant_record = self
354
- budget.save!
353
+ tenant.tenant_record = self
354
+ tenant.save!
355
355
  end
356
356
  end
357
357
  end
@@ -4,6 +4,6 @@ module RubyLLM
4
4
  module Agents
5
5
  # Current version of the RubyLLM::Agents gem
6
6
  # @return [String] Semantic version string
7
- VERSION = "1.1.0"
7
+ VERSION = "1.2.1"
8
8
  end
9
9
  end
@@ -125,13 +125,15 @@ module RubyLLM
125
125
  nil
126
126
  end
127
127
 
128
- # Checks if the tenant_budgets table exists
128
+ # Checks if the tenants table exists (supports old and new table names)
129
129
  #
130
130
  # @return [Boolean] true if table exists
131
131
  def tenant_budget_table_exists?
132
132
  return @tenant_budget_table_exists if defined?(@tenant_budget_table_exists)
133
133
 
134
- @tenant_budget_table_exists = ::ActiveRecord::Base.connection.table_exists?(:ruby_llm_agents_tenant_budgets)
134
+ # Check for new table name (tenants) or old table name (tenant_budgets) for backward compatibility
135
+ @tenant_budget_table_exists = ::ActiveRecord::Base.connection.table_exists?(:ruby_llm_agents_tenants) ||
136
+ ::ActiveRecord::Base.connection.table_exists?(:ruby_llm_agents_tenant_budgets)
135
137
  rescue StandardError
136
138
  @tenant_budget_table_exists = false
137
139
  end
@@ -170,14 +170,16 @@ module RubyLLM
170
170
  g.factory_bot dir: "spec/factories"
171
171
  end
172
172
 
173
- # Adds the host app's LLM directories to Rails autoload paths
173
+ # Adds the host app's agent directories to Rails autoload paths
174
174
  #
175
- # This allows agent classes and other LLM components defined in app/llm/
175
+ # This allows agent classes and other components defined in app/agents/
176
176
  # to be automatically loaded without explicit requires.
177
177
  #
178
- # Supports two structures:
179
- # 1. New grouped structure: app/llm/agents/, app/llm/tools/, etc.
180
- # 2. Legacy flat structure: app/agents/ (for backwards compatibility)
178
+ # Supports subdirectory namespacing:
179
+ # - app/agents/ (top-level, no namespace)
180
+ # - app/agents/embedders/ -> Embedders namespace
181
+ # - app/agents/images/ -> Images namespace
182
+ # - app/workflows/ (top-level, no namespace)
181
183
  #
182
184
  # @api private
183
185
  initializer "ruby_llm_agents.autoload_agents", before: :set_autoload_paths do |app|
@@ -210,42 +212,34 @@ module RubyLLM
210
212
 
211
213
  # Determines the namespace constant for a given path
212
214
  #
213
- # @param path [String] Relative path like "app/llm/agents"
215
+ # @param path [String] Relative path like "app/agents/embedders"
214
216
  # @param config [Configuration] Current configuration
215
217
  # @return [Module, nil] Namespace module or nil for top-level
216
218
  # @api private
217
219
  def self.namespace_for_path(path, config)
218
- # Parse the path to determine namespace
219
220
  parts = path.split("/")
220
- return nil unless parts.length >= 3
221
221
 
222
- category = parts[2] # e.g., "agents", "audio", "image", "text"
222
+ # app/workflows -> no namespace (top-level workflows)
223
+ return nil if parts == ["app", "workflows"]
223
224
 
224
- # Determine the namespace name based on category and root_namespace setting
225
+ # Need at least app/{root_directory}
226
+ return nil unless parts.length >= 2 && parts[0] == "app"
227
+ return nil unless parts[1] == config.root_directory
228
+
229
+ # app/agents -> no namespace (root level)
230
+ return nil if parts.length == 2
231
+
232
+ # app/agents/embedders -> Embedders namespace
233
+ subdirectory = parts[2]
225
234
  namespace_name = if config.root_namespace.blank?
226
- # No root namespace - use category namespace only for audio/image/text
227
- case category
228
- when "audio", "image", "text"
229
- category.camelize # "Audio", "Image", "Text"
230
- else
231
- nil # Top-level for agents, workflows, tools
232
- end
235
+ subdirectory.camelize
233
236
  else
234
- # With root namespace - prefix category with root namespace
235
- case category
236
- when "audio", "image", "text"
237
- "#{config.root_namespace}::#{category.camelize}"
238
- else
239
- config.root_namespace
240
- end
237
+ "#{config.root_namespace}::#{subdirectory.camelize}"
241
238
  end
242
239
 
243
- return nil if namespace_name.nil?
244
-
245
- # Return the constant, creating intermediate modules if needed
240
+ # Create the namespace module if needed
246
241
  namespace_name.constantize
247
242
  rescue NameError
248
- # Create the namespace module if it doesn't exist
249
243
  namespace_name.split("::").inject(Object) do |mod, name|
250
244
  mod.const_defined?(name, false) ? mod.const_get(name) : mod.const_set(name, Module.new)
251
245
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm-agents
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - adham90
@@ -95,6 +95,10 @@ files:
95
95
  - app/models/ruby_llm/agents/execution/metrics.rb
96
96
  - app/models/ruby_llm/agents/execution/scopes.rb
97
97
  - app/models/ruby_llm/agents/execution/workflow.rb
98
+ - app/models/ruby_llm/agents/tenant.rb
99
+ - app/models/ruby_llm/agents/tenant/budgetable.rb
100
+ - app/models/ruby_llm/agents/tenant/configurable.rb
101
+ - app/models/ruby_llm/agents/tenant/trackable.rb
98
102
  - app/models/ruby_llm/agents/tenant_budget.rb
99
103
  - app/services/ruby_llm/agents/agent_registry.rb
100
104
  - app/views/layouts/ruby_llm/agents/application.html.erb
@@ -155,6 +159,7 @@ files:
155
159
  - app/views/ruby_llm/agents/tenants/edit.html.erb
156
160
  - app/views/ruby_llm/agents/tenants/index.html.erb
157
161
  - app/views/ruby_llm/agents/tenants/show.html.erb
162
+ - app/views/ruby_llm/agents/workflows/_empty_state.html.erb
158
163
  - app/views/ruby_llm/agents/workflows/_step_performance.html.erb
159
164
  - app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb
160
165
  - app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb
@@ -208,6 +213,7 @@ files:
208
213
  - lib/generators/ruby_llm_agents/templates/background_remover.rb.tt
209
214
  - lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt
210
215
  - lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt
216
+ - lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt
211
217
  - lib/generators/ruby_llm_agents/templates/embedder.rb.tt
212
218
  - lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt
213
219
  - lib/generators/ruby_llm_agents/templates/image_editor.rb.tt
@@ -218,6 +224,7 @@ files:
218
224
  - lib/generators/ruby_llm_agents/templates/image_variator.rb.tt
219
225
  - lib/generators/ruby_llm_agents/templates/initializer.rb.tt
220
226
  - lib/generators/ruby_llm_agents/templates/migration.rb.tt
227
+ - lib/generators/ruby_llm_agents/templates/rename_tenant_budgets_to_tenants_migration.rb.tt
221
228
  - lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt
222
229
  - lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt
223
230
  - lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt