ruby_llm-agents 1.0.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.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +9 -3
  3. data/app/controllers/concerns/ruby_llm/agents/sortable.rb +58 -0
  4. data/app/controllers/ruby_llm/agents/agents_controller.rb +59 -16
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +144 -20
  6. data/app/controllers/ruby_llm/agents/executions_controller.rb +13 -16
  7. data/app/controllers/ruby_llm/agents/workflows_controller.rb +279 -90
  8. data/app/helpers/ruby_llm/agents/application_helper.rb +100 -0
  9. data/app/mailers/ruby_llm/agents/alert_mailer.rb +84 -0
  10. data/app/mailers/ruby_llm/agents/application_mailer.rb +28 -0
  11. data/app/models/ruby_llm/agents/execution/analytics.rb +170 -20
  12. data/app/models/ruby_llm/agents/execution/scopes.rb +0 -31
  13. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -129
  14. data/app/models/ruby_llm/agents/execution.rb +50 -14
  15. data/app/models/ruby_llm/agents/tenant/budgetable.rb +277 -0
  16. data/app/models/ruby_llm/agents/tenant/configurable.rb +135 -0
  17. data/app/models/ruby_llm/agents/tenant/trackable.rb +310 -0
  18. data/app/models/ruby_llm/agents/tenant.rb +146 -0
  19. data/app/models/ruby_llm/agents/tenant_budget.rb +12 -253
  20. data/app/services/ruby_llm/agents/agent_registry.rb +18 -12
  21. data/app/views/layouts/ruby_llm/agents/application.html.erb +72 -76
  22. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -12
  23. data/app/views/ruby_llm/agents/agents/_sortable_header.html.erb +56 -0
  24. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +5 -15
  25. data/app/views/ruby_llm/agents/agents/index.html.erb +271 -100
  26. data/app/views/ruby_llm/agents/agents/show.html.erb +1 -0
  27. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +107 -0
  28. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +18 -0
  29. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +4 -1
  30. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +66 -359
  31. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +56 -0
  32. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +115 -0
  33. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +35 -60
  34. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +17 -6
  35. data/app/views/ruby_llm/agents/dashboard/index.html.erb +373 -72
  36. data/app/views/ruby_llm/agents/executions/_execution.html.erb +0 -1
  37. data/app/views/ruby_llm/agents/executions/_filters.html.erb +51 -39
  38. data/app/views/ruby_llm/agents/executions/_list.html.erb +53 -195
  39. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +5 -20
  40. data/app/views/ruby_llm/agents/executions/index.html.erb +7 -83
  41. data/app/views/ruby_llm/agents/executions/show.html.erb +10 -20
  42. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +2 -1
  43. data/app/views/ruby_llm/agents/shared/_doc_link.html.erb +12 -0
  44. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +3 -15
  45. data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +1 -1
  46. data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +1 -1
  47. data/app/views/ruby_llm/agents/shared/_sortable_header.html.erb +53 -0
  48. data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +7 -0
  49. data/app/views/ruby_llm/agents/shared/_status_dot.html.erb +1 -1
  50. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +9 -35
  51. data/app/views/ruby_llm/agents/system_config/show.html.erb +4 -1
  52. data/app/views/ruby_llm/agents/tenants/index.html.erb +4 -1
  53. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +7 -15
  54. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +539 -0
  55. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +920 -0
  56. data/app/views/ruby_llm/agents/workflows/index.html.erb +179 -0
  57. data/app/views/ruby_llm/agents/workflows/show.html.erb +164 -139
  58. data/config/routes.rb +1 -1
  59. data/lib/generators/ruby_llm_agents/agent_generator.rb +6 -36
  60. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +7 -37
  61. data/lib/generators/ruby_llm_agents/embedder_generator.rb +5 -38
  62. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +7 -37
  63. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +7 -37
  64. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +8 -41
  65. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +18 -46
  66. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +7 -37
  67. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +7 -37
  68. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +7 -37
  69. data/lib/generators/ruby_llm_agents/install_generator.rb +33 -56
  70. data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +480 -0
  71. data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +42 -22
  72. data/lib/generators/ruby_llm_agents/restructure_generator.rb +2 -2
  73. data/lib/generators/ruby_llm_agents/speaker_generator.rb +8 -39
  74. data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +13 -2
  75. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +5 -8
  76. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +40 -42
  77. data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +20 -22
  78. data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +24 -26
  79. data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +20 -22
  80. data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +19 -17
  81. data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +31 -33
  82. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +125 -127
  83. data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +20 -18
  84. data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +19 -17
  85. data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +19 -17
  86. data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +38 -40
  87. data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +42 -44
  88. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +48 -0
  89. data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +19 -21
  90. data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +11 -0
  91. data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +72 -0
  92. data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +19 -21
  93. data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +20 -22
  94. data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +15 -17
  95. data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +25 -27
  96. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +19 -21
  97. data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +20 -22
  98. data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +17 -19
  99. data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +15 -17
  100. data/lib/generators/ruby_llm_agents/templates/rename_tenant_budgets_to_tenants_migration.rb.tt +34 -0
  101. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +87 -24
  102. data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +21 -27
  103. data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +46 -54
  104. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +31 -39
  105. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +22 -28
  106. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +53 -63
  107. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +46 -56
  108. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +23 -31
  109. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +22 -30
  110. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +23 -31
  111. data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +38 -46
  112. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +7 -7
  113. data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +59 -71
  114. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +274 -23
  115. data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +29 -31
  116. data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +28 -30
  117. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +10 -43
  118. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +26 -0
  119. data/lib/ruby_llm/agents/core/configuration.rb +55 -43
  120. data/lib/ruby_llm/agents/core/llm_tenant.rb +60 -60
  121. data/lib/ruby_llm/agents/core/version.rb +1 -1
  122. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +26 -0
  123. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +4 -2
  124. data/lib/ruby_llm/agents/pipeline.rb +69 -0
  125. data/lib/ruby_llm/agents/workflow/approval.rb +205 -0
  126. data/lib/ruby_llm/agents/workflow/approval_store.rb +179 -0
  127. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +467 -0
  128. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +244 -0
  129. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +289 -0
  130. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +107 -0
  131. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +150 -0
  132. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +187 -0
  133. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +352 -0
  134. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +415 -0
  135. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +257 -0
  136. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +317 -0
  137. data/lib/ruby_llm/agents/workflow/dsl.rb +576 -0
  138. data/lib/ruby_llm/agents/workflow/instrumentation.rb +2 -7
  139. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +117 -0
  140. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +117 -0
  141. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +180 -0
  142. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +121 -0
  143. data/lib/ruby_llm/agents/workflow/notifiers.rb +70 -0
  144. data/lib/ruby_llm/agents/workflow/orchestrator.rb +190 -23
  145. data/lib/ruby_llm/agents/workflow/result.rb +202 -0
  146. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +206 -0
  147. data/lib/ruby_llm/agents/workflow/wait_result.rb +213 -0
  148. metadata +43 -6
  149. data/app/views/ruby_llm/agents/dashboard/_execution_item.html.erb +0 -66
  150. data/lib/ruby_llm/agents/workflow/parallel.rb +0 -299
  151. data/lib/ruby_llm/agents/workflow/pipeline.rb +0 -306
  152. data/lib/ruby_llm/agents/workflow/router.rb +0 -429
@@ -0,0 +1,480 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "fileutils"
5
+
6
+ module RubyLlmAgents
7
+ # Migration generator for moving from old directory structure to new
8
+ #
9
+ # Migrates from:
10
+ # app/{root}/agents/
11
+ # app/{root}/image/generators/
12
+ # app/{root}/audio/speakers/
13
+ # app/{root}/text/embedders/
14
+ # app/{root}/workflows/
15
+ #
16
+ # To:
17
+ # app/agents/
18
+ # app/agents/images/
19
+ # app/agents/audio/
20
+ # app/agents/embedders/
21
+ # app/workflows/
22
+ #
23
+ # Usage:
24
+ # rails generate ruby_llm_agents:migrate_structure
25
+ # rails generate ruby_llm_agents:migrate_structure --dry-run
26
+ # rails generate ruby_llm_agents:migrate_structure --source-root=ai
27
+ #
28
+ class MigrateStructureGenerator < ::Rails::Generators::Base
29
+ source_root File.expand_path("templates", __dir__)
30
+
31
+ class_option :source_root,
32
+ type: :string,
33
+ default: nil,
34
+ desc: "Source root directory to migrate from (default: auto-detect or 'llm')"
35
+
36
+ class_option :dry_run,
37
+ type: :boolean,
38
+ default: false,
39
+ desc: "Show what would be done without making changes"
40
+
41
+ class_option :skip_namespace_update,
42
+ type: :boolean,
43
+ default: false,
44
+ desc: "Skip updating namespaces in Ruby files"
45
+
46
+ class_option :use_git,
47
+ type: :boolean,
48
+ default: true,
49
+ desc: "Use git mv when in a git repository"
50
+
51
+ # Maps old paths to new paths
52
+ # Format: old_subpath => new_path (relative to app/)
53
+ PATH_MAPPING = {
54
+ # Task agents
55
+ "agents" => "agents",
56
+
57
+ # Image agents (flatten operation types)
58
+ "image/generators" => "agents/images",
59
+ "image/analyzers" => "agents/images",
60
+ "image/editors" => "agents/images",
61
+ "image/upscalers" => "agents/images",
62
+ "image/variators" => "agents/images",
63
+ "image/transformers" => "agents/images",
64
+ "image/background_removers" => "agents/images",
65
+ "image/pipelines" => "agents/images",
66
+
67
+ # Audio agents
68
+ "audio/speakers" => "agents/audio",
69
+ "audio/transcribers" => "agents/audio",
70
+
71
+ # Text operations
72
+ "text/embedders" => "agents/embedders",
73
+ "text/moderators" => "agents/moderators",
74
+
75
+ # Workflows
76
+ "workflows" => "workflows",
77
+
78
+ # Tools
79
+ "tools" => "tools"
80
+ }.freeze
81
+
82
+ # Namespace transformations
83
+ # Format: old_namespace_pattern => new_namespace
84
+ NAMESPACE_MAPPING = {
85
+ # Remove root namespace from task agents
86
+ /\A(\w+)::(\w+Agent)\z/ => '\2',
87
+
88
+ # Image namespaces
89
+ /\A(\w+)::Image::(\w+)\z/ => 'Images::\2',
90
+
91
+ # Audio namespaces
92
+ /\A(\w+)::Audio::(\w+)\z/ => 'Audio::\2',
93
+
94
+ # Text namespaces -> Embedders/Moderators
95
+ /\A(\w+)::Text::(\w+Embedder)\z/ => 'Embedders::\2',
96
+ /\A(\w+)::Text::(\w+Moderator)\z/ => 'Moderators::\2',
97
+
98
+ # Workflows (remove root namespace)
99
+ /\A(\w+)::(\w+Workflow)\z/ => '\2'
100
+ }.freeze
101
+
102
+ def check_prerequisites
103
+ @source_root_dir = detect_source_root
104
+
105
+ if @source_root_dir.nil?
106
+ say_status :skip, "No old structure detected. Nothing to migrate.", :yellow
107
+ say ""
108
+ say "If you have an existing structure, specify the source root:"
109
+ say " rails generate ruby_llm_agents:migrate_structure --source-root=llm"
110
+ raise Thor::Error, "Migration aborted: no source found"
111
+ end
112
+
113
+ say_status :found, "Old structure at app/#{@source_root_dir}/", :green
114
+ end
115
+
116
+ def show_migration_plan
117
+ say ""
118
+ say "=" * 60
119
+ say "Migration Plan", :bold
120
+ say "=" * 60
121
+ say ""
122
+ say "Source: app/#{@source_root_dir}/"
123
+ say "Target: app/agents/ and app/workflows/"
124
+ say ""
125
+
126
+ @files_to_migrate = []
127
+
128
+ PATH_MAPPING.each do |old_subpath, new_path|
129
+ old_full_path = Rails.root.join("app", @source_root_dir, old_subpath)
130
+ next unless File.directory?(old_full_path)
131
+
132
+ files = Dir.glob("#{old_full_path}/**/*.rb")
133
+ next if files.empty?
134
+
135
+ files.each do |file|
136
+ relative = file.sub("#{old_full_path}/", "")
137
+ new_file = Rails.root.join("app", new_path, relative)
138
+ @files_to_migrate << {
139
+ old: file,
140
+ new: new_file.to_s,
141
+ old_display: "app/#{@source_root_dir}/#{old_subpath}/#{relative}",
142
+ new_display: "app/#{new_path}/#{relative}"
143
+ }
144
+ end
145
+ end
146
+
147
+ if @files_to_migrate.empty?
148
+ say_status :skip, "No Ruby files found to migrate", :yellow
149
+ return
150
+ end
151
+
152
+ say "Files to migrate (#{@files_to_migrate.size}):"
153
+ @files_to_migrate.each do |f|
154
+ say " #{f[:old_display]}"
155
+ say " -> #{f[:new_display]}", :green
156
+ end
157
+ say ""
158
+ end
159
+
160
+ def create_new_directories
161
+ return if @files_to_migrate.nil? || @files_to_migrate.empty?
162
+
163
+ say_status :create, "Creating new directory structure", :green
164
+
165
+ directories = @files_to_migrate.map { |f| File.dirname(f[:new]) }.uniq
166
+
167
+ directories.each do |dir|
168
+ if options[:dry_run]
169
+ say_status :dry_run, "Would create #{dir.sub(Rails.root.to_s + '/', '')}", :yellow
170
+ else
171
+ FileUtils.mkdir_p(dir)
172
+ say_status :mkdir, dir.sub(Rails.root.to_s + "/", ""), :green
173
+ end
174
+ end
175
+ end
176
+
177
+ def move_files
178
+ return if @files_to_migrate.nil? || @files_to_migrate.empty?
179
+
180
+ say ""
181
+ say_status :move, "Moving files to new locations", :green
182
+
183
+ @files_to_migrate.each do |f|
184
+ if options[:dry_run]
185
+ say_status :dry_run, "Would move #{f[:old_display]} -> #{f[:new_display]}", :yellow
186
+ else
187
+ move_file(f[:old], f[:new])
188
+ say_status :moved, f[:new_display], :green
189
+ end
190
+ end
191
+ end
192
+
193
+ def update_namespaces
194
+ return if options[:skip_namespace_update]
195
+ return if @files_to_migrate.nil? || @files_to_migrate.empty?
196
+
197
+ say ""
198
+ say_status :update, "Updating namespaces in Ruby files", :green
199
+
200
+ @files_to_migrate.each do |f|
201
+ file_path = options[:dry_run] ? f[:old] : f[:new]
202
+ next unless File.exist?(file_path)
203
+
204
+ if options[:dry_run]
205
+ say_status :dry_run, "Would update namespaces in #{f[:new_display]}", :yellow
206
+ else
207
+ update_file_namespaces(f[:new])
208
+ end
209
+ end
210
+ end
211
+
212
+ def update_base_classes
213
+ return if options[:skip_namespace_update]
214
+ return if @files_to_migrate.nil? || @files_to_migrate.empty?
215
+
216
+ say ""
217
+ say_status :update, "Updating base class references", :green
218
+
219
+ @files_to_migrate.each do |f|
220
+ file_path = options[:dry_run] ? f[:old] : f[:new]
221
+ next unless File.exist?(file_path)
222
+
223
+ if options[:dry_run]
224
+ say_status :dry_run, "Would update base classes in #{f[:new_display]}", :yellow
225
+ else
226
+ update_file_base_classes(f[:new])
227
+ end
228
+ end
229
+ end
230
+
231
+ def cleanup_old_directories
232
+ return if @files_to_migrate.nil? || @files_to_migrate.empty?
233
+
234
+ say ""
235
+ say_status :cleanup, "Cleaning up old directories", :green
236
+
237
+ # Collect all old directories
238
+ old_dirs = PATH_MAPPING.keys.map do |old_subpath|
239
+ Rails.root.join("app", @source_root_dir, old_subpath)
240
+ end.select { |d| File.directory?(d) }
241
+
242
+ # Sort by depth (deepest first) for proper cleanup
243
+ old_dirs.sort_by { |d| -d.to_s.count("/") }.each do |dir|
244
+ cleanup_directory(dir)
245
+ end
246
+
247
+ # Try to remove the root directory if empty
248
+ root_dir = Rails.root.join("app", @source_root_dir)
249
+ if File.directory?(root_dir)
250
+ cleanup_directory(root_dir)
251
+ end
252
+ end
253
+
254
+ def show_completion_message
255
+ say ""
256
+ say "=" * 60
257
+ say ""
258
+
259
+ if options[:dry_run]
260
+ say "Dry run complete! No changes were made.", :yellow
261
+ say ""
262
+ say "To perform the actual migration, run:"
263
+ say " rails generate ruby_llm_agents:migrate_structure"
264
+ else
265
+ say "Migration complete!", :green
266
+ say ""
267
+ say "Your app now uses the new directory structure:"
268
+ say ""
269
+ say " app/"
270
+ say " ├── agents/"
271
+ say " │ ├── application_agent.rb"
272
+ say " │ ├── your_agent.rb"
273
+ say " │ ├── images/"
274
+ say " │ ├── audio/"
275
+ say " │ ├── embedders/"
276
+ say " │ └── moderators/"
277
+ say " └── workflows/"
278
+ say ""
279
+ say "Next steps:"
280
+ say " 1. Update class references in your code:"
281
+ say " - Llm::Image::ProductGenerator -> Images::ProductGenerator"
282
+ say " - Llm::Audio::MeetingTranscriber -> Audio::MeetingTranscriber"
283
+ say " - Llm::Text::SemanticEmbedder -> Embedders::SemanticEmbedder"
284
+ say " 2. Run your test suite: bundle exec rspec"
285
+ say " 3. Commit the changes: git add -A && git commit -m 'Migrate to new agent structure'"
286
+ end
287
+
288
+ say ""
289
+ say "=" * 60
290
+ end
291
+
292
+ private
293
+
294
+ def detect_source_root
295
+ # Check for explicitly provided source root
296
+ if options[:source_root]
297
+ path = Rails.root.join("app", options[:source_root])
298
+ return options[:source_root] if File.directory?(path)
299
+ say_status :warning, "Specified source root 'app/#{options[:source_root]}/' not found", :yellow
300
+ return nil
301
+ end
302
+
303
+ # Try to get from configuration
304
+ if defined?(RubyLLM::Agents) && RubyLLM::Agents.respond_to?(:configuration)
305
+ config_root = RubyLLM::Agents.configuration.root_directory
306
+ if config_root.present?
307
+ path = Rails.root.join("app", config_root)
308
+ return config_root if File.directory?(path)
309
+ end
310
+ end
311
+
312
+ # Auto-detect common root directories
313
+ %w[llm ai ml agents].each do |candidate|
314
+ path = Rails.root.join("app", candidate)
315
+ if File.directory?(path) && has_old_structure?(path)
316
+ return candidate
317
+ end
318
+ end
319
+
320
+ nil
321
+ end
322
+
323
+ def has_old_structure?(path)
324
+ # Check if this looks like an old ruby_llm-agents structure
325
+ old_indicators = %w[
326
+ agents
327
+ image/generators
328
+ audio/speakers
329
+ text/embedders
330
+ workflows
331
+ ]
332
+
333
+ old_indicators.any? do |indicator|
334
+ File.directory?(File.join(path, indicator))
335
+ end
336
+ end
337
+
338
+ def in_git_repo?
339
+ @in_git_repo ||= File.directory?(Rails.root.join(".git"))
340
+ end
341
+
342
+ def move_file(old_path, new_path)
343
+ if options[:use_git] && in_git_repo?
344
+ # Use git mv to preserve history
345
+ old_relative = old_path.sub(Rails.root.to_s + "/", "")
346
+ new_relative = new_path.sub(Rails.root.to_s + "/", "")
347
+ system("git", "mv", old_relative, new_relative, chdir: Rails.root.to_s)
348
+ else
349
+ FileUtils.mv(old_path, new_path)
350
+ end
351
+ end
352
+
353
+ def update_file_namespaces(file_path)
354
+ content = File.read(file_path)
355
+ original_content = content.dup
356
+ modified = false
357
+
358
+ # Find and replace module/class declarations with old namespaces
359
+ # Pattern: module OldRoot ... end wrapping
360
+ old_root_pattern = /^module\s+#{Regexp.escape(camelize(@source_root_dir))}\s*$/
361
+
362
+ if content.match?(old_root_pattern)
363
+ # Remove the old root module wrapper
364
+ content = remove_root_module_wrapper(content, camelize(@source_root_dir))
365
+ modified = true
366
+ end
367
+
368
+ # Update module declarations for nested namespaces
369
+ # Image:: -> Images::
370
+ content.gsub!(/\bmodule\s+Image\b/, "module Images") && (modified = true)
371
+
372
+ # Text:: -> nothing (embedders/moderators are top-level now)
373
+ # This is handled by the module removal above
374
+
375
+ if modified && content != original_content
376
+ File.write(file_path, content)
377
+ say_status :updated, file_path.sub(Rails.root.to_s + "/", ""), :blue
378
+ end
379
+ end
380
+
381
+ def update_file_base_classes(file_path)
382
+ content = File.read(file_path)
383
+ original_content = content.dup
384
+
385
+ # Update base class references
386
+ replacements = {
387
+ # Application bases with old namespace
388
+ /class\s+Application(\w+)\s*<\s*#{Regexp.escape(camelize(@source_root_dir))}::/ =>
389
+ 'class Application\1 < ',
390
+
391
+ # Specific base classes
392
+ /ApplicationImageGenerator/ => "ApplicationImageGenerator",
393
+ /ApplicationImageAnalyzer/ => "ApplicationImageAnalyzer",
394
+ /ApplicationImageEditor/ => "ApplicationImageEditor",
395
+ /ApplicationTranscriber/ => "ApplicationTranscriber",
396
+ /ApplicationSpeaker/ => "ApplicationSpeaker",
397
+ /ApplicationEmbedder/ => "ApplicationEmbedder"
398
+ }
399
+
400
+ replacements.each do |pattern, replacement|
401
+ content.gsub!(pattern, replacement)
402
+ end
403
+
404
+ if content != original_content
405
+ File.write(file_path, content)
406
+ say_status :updated, "base classes in #{file_path.sub(Rails.root.to_s + '/', '')}", :blue
407
+ end
408
+ end
409
+
410
+ def remove_root_module_wrapper(content, root_module)
411
+ lines = content.lines
412
+ result = []
413
+ module_depth = 0
414
+ root_module_line = nil
415
+
416
+ lines.each_with_index do |line, idx|
417
+ if line.match?(/^\s*module\s+#{Regexp.escape(root_module)}\s*$/)
418
+ root_module_line = idx
419
+ module_depth = 1
420
+ next
421
+ end
422
+
423
+ if root_module_line
424
+ # Track module/class/def depth
425
+ if line.match?(/^\s*(module|class)\s+\w/)
426
+ module_depth += 1
427
+ elsif line.match?(/^\s*end\s*$/)
428
+ module_depth -= 1
429
+ if module_depth == 0
430
+ # This is the closing end of the root module, skip it
431
+ root_module_line = nil
432
+ next
433
+ end
434
+ end
435
+
436
+ # Dedent by 2 spaces
437
+ result << line.sub(/^ /, "")
438
+ else
439
+ result << line
440
+ end
441
+ end
442
+
443
+ result.join
444
+ end
445
+
446
+ def cleanup_directory(dir)
447
+ return unless File.directory?(dir)
448
+
449
+ # First clean up any empty subdirectories
450
+ Dir.glob("#{dir}/**/").sort_by { |d| -d.count("/") }.each do |subdir|
451
+ next unless File.directory?(subdir) && Dir.empty?(subdir)
452
+
453
+ if options[:dry_run]
454
+ say_status :dry_run, "Would remove empty #{subdir.sub(Rails.root.to_s + '/', '')}", :yellow
455
+ else
456
+ FileUtils.rmdir(subdir)
457
+ say_status :removed, subdir.sub(Rails.root.to_s + "/", ""), :red
458
+ end
459
+ end
460
+
461
+ # Then try to remove the directory itself if empty
462
+ return unless File.directory?(dir) && Dir.empty?(dir)
463
+
464
+ if options[:dry_run]
465
+ say_status :dry_run, "Would remove empty #{dir.sub(Rails.root.to_s + '/', '')}", :yellow
466
+ else
467
+ FileUtils.rmdir(dir)
468
+ say_status :removed, dir.sub(Rails.root.to_s + "/", ""), :red
469
+ end
470
+ end
471
+
472
+ def camelize(str)
473
+ return "AI" if str.downcase == "ai"
474
+ return "ML" if str.downcase == "ml"
475
+ return "LLM" if str.downcase == "llm"
476
+
477
+ str.split(/[-_]/).map(&:capitalize).join
478
+ end
479
+ end
480
+ end
@@ -10,8 +10,11 @@ module RubyLlmAgents
10
10
  # rails generate ruby_llm_agents:multi_tenancy
11
11
  #
12
12
  # This will create migrations for:
13
- # - ruby_llm_agents_tenant_budgets table for per-tenant budget configuration
14
- # - Adding tenant_id column to ruby_llm_agents_executions
13
+ # - ruby_llm_agents_tenants table for per-tenant configuration
14
+ # - Adding tenant columns to ruby_llm_agents_executions
15
+ #
16
+ # For users upgrading from an older version:
17
+ # - Renames ruby_llm_agents_tenant_budgets to ruby_llm_agents_tenants
15
18
  #
16
19
  class MultiTenancyGenerator < ::Rails::Generators::Base
17
20
  include ::ActiveRecord::Generators::Migration
@@ -20,21 +23,31 @@ module RubyLlmAgents
20
23
 
21
24
  desc "Adds multi-tenancy support to RubyLLM::Agents"
22
25
 
23
- def create_tenant_budgets_migration
24
- if table_exists?(:ruby_llm_agents_tenant_budgets)
25
- say_status :skip, "ruby_llm_agents_tenant_budgets table already exists", :yellow
26
+ def create_tenants_migration
27
+ if table_exists?(:ruby_llm_agents_tenants)
28
+ say_status :skip, "ruby_llm_agents_tenants table already exists", :yellow
26
29
  return
27
30
  end
28
31
 
29
- migration_template(
30
- "create_tenant_budgets_migration.rb.tt",
31
- File.join(db_migrate_path, "create_ruby_llm_agents_tenant_budgets.rb")
32
- )
32
+ if table_exists?(:ruby_llm_agents_tenant_budgets)
33
+ # Upgrade path: rename existing table
34
+ say_status :upgrade, "Renaming tenant_budgets to tenants", :blue
35
+ migration_template(
36
+ "rename_tenant_budgets_to_tenants_migration.rb.tt",
37
+ File.join(db_migrate_path, "rename_tenant_budgets_to_tenants.rb")
38
+ )
39
+ else
40
+ # Fresh install: create new table
41
+ migration_template(
42
+ "create_tenants_migration.rb.tt",
43
+ File.join(db_migrate_path, "create_ruby_llm_agents_tenants.rb")
44
+ )
45
+ end
33
46
  end
34
47
 
35
48
  def create_add_tenant_to_executions_migration
36
49
  if column_exists?(:ruby_llm_agents_executions, :tenant_id)
37
- say_status :skip, "tenant_id column already exists", :yellow
50
+ say_status :skip, "tenant_id column already exists on executions", :yellow
38
51
  return
39
52
  end
40
53
 
@@ -50,23 +63,30 @@ module RubyLlmAgents
50
63
  say ""
51
64
  say "Next steps:"
52
65
  say " 1. Run: rails db:migrate"
53
- say " 2. Configure multi-tenancy in your initializer:"
66
+ say " 2. Add llm_tenant to your tenant model:"
54
67
  say ""
55
- say " RubyLLM::Agents.configure do |config|"
56
- say " config.multi_tenancy_enabled = true"
57
- say " config.tenant_resolver = -> { Current.tenant&.id }"
68
+ say " class Organization < ApplicationRecord"
69
+ say " include RubyLLM::Agents::LLMTenant"
70
+ say ""
71
+ say " llm_tenant id: :id, # Method for tenant_id"
72
+ say " budget: true, # Auto-create budget on creation"
73
+ say " limits: { # Optional default limits"
74
+ say " daily_cost: 100,"
75
+ say " monthly_cost: 1000"
76
+ say " },"
77
+ say " enforcement: :hard # :none, :soft, or :hard"
58
78
  say " end"
59
79
  say ""
60
- say " 3. Set Current.tenant in your ApplicationController"
80
+ say " 3. Pass tenant to agents:"
81
+ say ""
82
+ say " MyAgent.call(prompt, tenant: current_organization)"
61
83
  say ""
62
- say " 4. Create tenant budgets:"
84
+ say " 4. Query usage:"
63
85
  say ""
64
- say " RubyLLM::Agents::TenantBudget.create!("
65
- say " tenant_id: 'acme_corp',"
66
- say " daily_limit: 50.0,"
67
- say " monthly_limit: 500.0,"
68
- say " enforcement: 'hard'"
69
- say " )"
86
+ say " tenant = RubyLLM::Agents::Tenant.for(organization)"
87
+ say " tenant.cost_today # => 12.34"
88
+ say " tenant.tokens_this_month # => 50000"
89
+ say " tenant.usage_summary # => { cost: ..., tokens: ..., ... }"
70
90
  say ""
71
91
  end
72
92
 
@@ -121,7 +121,7 @@ module RubyLlmAgents
121
121
  namespace = config.namespace_for(mapping[:category])
122
122
 
123
123
  Dir.glob("#{directory_path}/**/*.rb").each do |file|
124
- update_file_namespace(file, namespace)
124
+ update_file_namespace(file, namespace) if namespace
125
125
  end
126
126
  end
127
127
  end
@@ -190,7 +190,7 @@ module RubyLlmAgents
190
190
  private
191
191
 
192
192
  def root_directory
193
- @root_directory ||= options[:root] || RubyLLM::Agents.configuration.root_directory
193
+ @root_directory ||= options[:root] || RubyLLM::Agents.configuration.root_directory || "agents"
194
194
  end
195
195
 
196
196
  def root_namespace
@@ -9,10 +9,9 @@ module RubyLlmAgents
9
9
  # rails generate ruby_llm_agents:speaker Narrator
10
10
  # rails generate ruby_llm_agents:speaker Narrator --provider elevenlabs
11
11
  # rails generate ruby_llm_agents:speaker Narrator --voice alloy --speed 1.25
12
- # rails generate ruby_llm_agents:speaker Narrator --root=ai
13
12
  #
14
13
  # This will create:
15
- # - app/{root}/audio/speakers/narrator_speaker.rb
14
+ # - app/agents/audio/narrator_speaker.rb
16
15
  #
17
16
  class SpeakerGenerator < ::Rails::Generators::NamedBase
18
17
  source_root File.expand_path("templates", __dir__)
@@ -29,48 +28,36 @@ module RubyLlmAgents
29
28
  desc: "Output format (mp3, wav, ogg, flac)"
30
29
  class_option :cache, type: :string, default: nil,
31
30
  desc: "Cache TTL (e.g., '7.days')"
32
- class_option :root,
33
- type: :string,
34
- default: nil,
35
- desc: "Root directory name (default: uses config or 'llm')"
36
- class_option :namespace,
37
- type: :string,
38
- default: nil,
39
- desc: "Root namespace (default: camelized root or config)"
40
31
 
41
32
  def ensure_base_class_and_skill_file
42
- @root_namespace = root_namespace
43
- @audio_namespace = "#{root_namespace}::Audio"
44
- speakers_dir = "app/#{root_directory}/audio/speakers"
33
+ audio_dir = "app/agents/audio"
45
34
 
46
35
  # Create directory if needed
47
- empty_directory speakers_dir
36
+ empty_directory audio_dir
48
37
 
49
38
  # Create base class if it doesn't exist
50
- base_class_path = "#{speakers_dir}/application_speaker.rb"
39
+ base_class_path = "#{audio_dir}/application_speaker.rb"
51
40
  unless File.exist?(File.join(destination_root, base_class_path))
52
41
  template "application_speaker.rb.tt", base_class_path
53
42
  end
54
43
 
55
44
  # Create skill file if it doesn't exist
56
- skill_file_path = "#{speakers_dir}/SPEAKERS.md"
45
+ skill_file_path = "#{audio_dir}/SPEAKERS.md"
57
46
  unless File.exist?(File.join(destination_root, skill_file_path))
58
47
  template "skills/SPEAKERS.md.tt", skill_file_path
59
48
  end
60
49
  end
61
50
 
62
51
  def create_speaker_file
63
- # Support nested paths: "article/narrator" -> "app/{root}/audio/speakers/article/narrator_speaker.rb"
64
- @root_namespace = root_namespace
65
- @audio_namespace = "#{root_namespace}::Audio"
52
+ # Support nested paths: "article/narrator" -> "app/agents/audio/article/narrator_speaker.rb"
66
53
  speaker_path = name.underscore
67
- template "speaker.rb.tt", "app/#{root_directory}/audio/speakers/#{speaker_path}_speaker.rb"
54
+ template "speaker.rb.tt", "app/agents/audio/#{speaker_path}_speaker.rb"
68
55
  end
69
56
 
70
57
  def show_usage
71
58
  # Build full class name from path
72
59
  speaker_class_name = name.split("/").map(&:camelize).join("::")
73
- full_class_name = "#{root_namespace}::Audio::#{speaker_class_name}Speaker"
60
+ full_class_name = "Audio::#{speaker_class_name}Speaker"
74
61
  say ""
75
62
  say "Speaker #{full_class_name} created!", :green
76
63
  say ""
@@ -91,24 +78,6 @@ module RubyLlmAgents
91
78
 
92
79
  private
93
80
 
94
- def root_directory
95
- @root_directory ||= options[:root] || RubyLLM::Agents.configuration.root_directory
96
- end
97
-
98
- def root_namespace
99
- @root_namespace ||= options[:namespace] || camelize(root_directory)
100
- end
101
-
102
- def camelize(str)
103
- # Handle special cases for common abbreviations
104
- return "AI" if str.downcase == "ai"
105
- return "ML" if str.downcase == "ml"
106
- return "LLM" if str.downcase == "llm"
107
-
108
- # Standard camelization
109
- str.split(/[-_]/).map(&:capitalize).join
110
- end
111
-
112
81
  def default_model
113
82
  case options[:provider].to_s
114
83
  when "elevenlabs"