ruby_llm-agents 0.5.0 → 1.0.0.beta.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.
Files changed (190) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +189 -31
  3. data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
  4. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
  5. data/app/controllers/ruby_llm/agents/workflows_controller.rb +355 -0
  6. data/app/helpers/ruby_llm/agents/application_helper.rb +25 -0
  7. data/app/models/ruby_llm/agents/execution.rb +3 -0
  8. data/app/models/ruby_llm/agents/tenant_budget.rb +58 -15
  9. data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
  10. data/app/views/layouts/ruby_llm/agents/application.html.erb +2 -29
  11. data/app/views/ruby_llm/agents/agents/_agent.html.erb +13 -1
  12. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +235 -0
  13. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +70 -0
  14. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +152 -0
  15. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +63 -0
  16. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +108 -0
  17. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +91 -0
  18. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +1 -1
  19. data/app/views/ruby_llm/agents/agents/index.html.erb +74 -9
  20. data/app/views/ruby_llm/agents/agents/show.html.erb +18 -378
  21. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +269 -15
  22. data/app/views/ruby_llm/agents/executions/show.html.erb +16 -0
  23. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -0
  24. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +236 -0
  25. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +76 -0
  26. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +74 -0
  27. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +108 -0
  28. data/app/views/ruby_llm/agents/workflows/show.html.erb +442 -0
  29. data/config/routes.rb +1 -0
  30. data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
  31. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +110 -0
  32. data/lib/generators/ruby_llm_agents/embedder_generator.rb +107 -0
  33. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +115 -0
  34. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +108 -0
  35. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +116 -0
  36. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +178 -0
  37. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +109 -0
  38. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +103 -0
  39. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +102 -0
  40. data/lib/generators/ruby_llm_agents/install_generator.rb +76 -4
  41. data/lib/generators/ruby_llm_agents/restructure_generator.rb +292 -0
  42. data/lib/generators/ruby_llm_agents/speaker_generator.rb +121 -0
  43. data/lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt +8 -0
  44. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +99 -84
  45. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +42 -40
  46. data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +26 -0
  47. data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +50 -0
  48. data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +26 -0
  49. data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +20 -0
  50. data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +38 -0
  51. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +139 -0
  52. data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +21 -0
  53. data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +20 -0
  54. data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +20 -0
  55. data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +49 -0
  56. data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +53 -0
  57. data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +44 -0
  58. data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +41 -0
  59. data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +45 -0
  60. data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +35 -0
  61. data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +47 -0
  62. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +50 -0
  63. data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +44 -0
  64. data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +38 -0
  65. data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +33 -0
  66. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +228 -0
  67. data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +131 -0
  68. data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +255 -0
  69. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +120 -0
  70. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +102 -0
  71. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +282 -0
  72. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +228 -0
  73. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +120 -0
  74. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +110 -0
  75. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +120 -0
  76. data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +212 -0
  77. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +227 -0
  78. data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +251 -0
  79. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +300 -0
  80. data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +56 -0
  81. data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +51 -0
  82. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +107 -0
  83. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +152 -1
  84. data/lib/ruby_llm/agents/audio/speaker.rb +553 -0
  85. data/lib/ruby_llm/agents/audio/transcriber.rb +669 -0
  86. data/lib/ruby_llm/agents/base_agent.rb +675 -0
  87. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +181 -0
  88. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +274 -0
  89. data/lib/ruby_llm/agents/core/base.rb +135 -0
  90. data/lib/ruby_llm/agents/core/configuration.rb +981 -0
  91. data/lib/ruby_llm/agents/core/errors.rb +150 -0
  92. data/lib/ruby_llm/agents/{instrumentation.rb → core/instrumentation.rb} +22 -1
  93. data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -0
  94. data/lib/ruby_llm/agents/{version.rb → core/version.rb} +1 -1
  95. data/lib/ruby_llm/agents/dsl/base.rb +110 -0
  96. data/lib/ruby_llm/agents/dsl/caching.rb +142 -0
  97. data/lib/ruby_llm/agents/dsl/reliability.rb +307 -0
  98. data/lib/ruby_llm/agents/dsl.rb +41 -0
  99. data/lib/ruby_llm/agents/image/analyzer/dsl.rb +130 -0
  100. data/lib/ruby_llm/agents/image/analyzer/execution.rb +402 -0
  101. data/lib/ruby_llm/agents/image/analyzer.rb +90 -0
  102. data/lib/ruby_llm/agents/image/background_remover/dsl.rb +154 -0
  103. data/lib/ruby_llm/agents/image/background_remover/execution.rb +240 -0
  104. data/lib/ruby_llm/agents/image/background_remover.rb +89 -0
  105. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +91 -0
  106. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +165 -0
  107. data/lib/ruby_llm/agents/image/editor/dsl.rb +56 -0
  108. data/lib/ruby_llm/agents/image/editor/execution.rb +207 -0
  109. data/lib/ruby_llm/agents/image/editor.rb +92 -0
  110. data/lib/ruby_llm/agents/image/generator/active_storage_support.rb +127 -0
  111. data/lib/ruby_llm/agents/image/generator/content_policy.rb +95 -0
  112. data/lib/ruby_llm/agents/image/generator/pricing.rb +353 -0
  113. data/lib/ruby_llm/agents/image/generator/templates.rb +124 -0
  114. data/lib/ruby_llm/agents/image/generator.rb +455 -0
  115. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +213 -0
  116. data/lib/ruby_llm/agents/image/pipeline/execution.rb +382 -0
  117. data/lib/ruby_llm/agents/image/pipeline.rb +97 -0
  118. data/lib/ruby_llm/agents/image/transformer/dsl.rb +148 -0
  119. data/lib/ruby_llm/agents/image/transformer/execution.rb +223 -0
  120. data/lib/ruby_llm/agents/image/transformer.rb +95 -0
  121. data/lib/ruby_llm/agents/image/upscaler/dsl.rb +83 -0
  122. data/lib/ruby_llm/agents/image/upscaler/execution.rb +219 -0
  123. data/lib/ruby_llm/agents/image/upscaler.rb +81 -0
  124. data/lib/ruby_llm/agents/image/variator/dsl.rb +62 -0
  125. data/lib/ruby_llm/agents/image/variator/execution.rb +189 -0
  126. data/lib/ruby_llm/agents/image/variator.rb +80 -0
  127. data/lib/ruby_llm/agents/{alert_manager.rb → infrastructure/alert_manager.rb} +17 -22
  128. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +145 -0
  129. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +149 -0
  130. data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +68 -0
  131. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +279 -0
  132. data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +275 -0
  133. data/lib/ruby_llm/agents/{execution_logger_job.rb → infrastructure/execution_logger_job.rb} +17 -1
  134. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/executor.rb +2 -1
  135. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/retry_strategy.rb +9 -3
  136. data/lib/ruby_llm/agents/{reliability.rb → infrastructure/reliability.rb} +11 -21
  137. data/lib/ruby_llm/agents/pipeline/builder.rb +215 -0
  138. data/lib/ruby_llm/agents/pipeline/context.rb +255 -0
  139. data/lib/ruby_llm/agents/pipeline/executor.rb +86 -0
  140. data/lib/ruby_llm/agents/pipeline/middleware/base.rb +124 -0
  141. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +95 -0
  142. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +171 -0
  143. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +415 -0
  144. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +276 -0
  145. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +196 -0
  146. data/lib/ruby_llm/agents/pipeline.rb +68 -0
  147. data/lib/ruby_llm/agents/{engine.rb → rails/engine.rb} +79 -11
  148. data/lib/ruby_llm/agents/results/background_removal_result.rb +286 -0
  149. data/lib/ruby_llm/agents/{result.rb → results/base.rb} +73 -1
  150. data/lib/ruby_llm/agents/results/embedding_result.rb +243 -0
  151. data/lib/ruby_llm/agents/results/image_analysis_result.rb +314 -0
  152. data/lib/ruby_llm/agents/results/image_edit_result.rb +250 -0
  153. data/lib/ruby_llm/agents/results/image_generation_result.rb +346 -0
  154. data/lib/ruby_llm/agents/results/image_pipeline_result.rb +399 -0
  155. data/lib/ruby_llm/agents/results/image_transform_result.rb +251 -0
  156. data/lib/ruby_llm/agents/results/image_upscale_result.rb +255 -0
  157. data/lib/ruby_llm/agents/results/image_variation_result.rb +237 -0
  158. data/lib/ruby_llm/agents/results/moderation_result.rb +158 -0
  159. data/lib/ruby_llm/agents/results/speech_result.rb +338 -0
  160. data/lib/ruby_llm/agents/results/transcription_result.rb +408 -0
  161. data/lib/ruby_llm/agents/text/embedder.rb +444 -0
  162. data/lib/ruby_llm/agents/text/moderator.rb +237 -0
  163. data/lib/ruby_llm/agents/workflow/async.rb +220 -0
  164. data/lib/ruby_llm/agents/workflow/async_executor.rb +156 -0
  165. data/lib/ruby_llm/agents/{workflow.rb → workflow/orchestrator.rb} +6 -5
  166. data/lib/ruby_llm/agents/workflow/parallel.rb +34 -17
  167. data/lib/ruby_llm/agents/workflow/thread_pool.rb +185 -0
  168. data/lib/ruby_llm/agents.rb +86 -20
  169. metadata +172 -34
  170. data/lib/ruby_llm/agents/base/caching.rb +0 -40
  171. data/lib/ruby_llm/agents/base/cost_calculation.rb +0 -105
  172. data/lib/ruby_llm/agents/base/dsl.rb +0 -324
  173. data/lib/ruby_llm/agents/base/execution.rb +0 -366
  174. data/lib/ruby_llm/agents/base/reliability_dsl.rb +0 -82
  175. data/lib/ruby_llm/agents/base/reliability_execution.rb +0 -136
  176. data/lib/ruby_llm/agents/base/response_building.rb +0 -86
  177. data/lib/ruby_llm/agents/base/tool_tracking.rb +0 -57
  178. data/lib/ruby_llm/agents/base.rb +0 -210
  179. data/lib/ruby_llm/agents/budget_tracker.rb +0 -733
  180. data/lib/ruby_llm/agents/configuration.rb +0 -394
  181. /data/lib/ruby_llm/agents/{deprecations.rb → core/deprecations.rb} +0 -0
  182. /data/lib/ruby_llm/agents/{inflections.rb → core/inflections.rb} +0 -0
  183. /data/lib/ruby_llm/agents/{resolved_config.rb → core/resolved_config.rb} +0 -0
  184. /data/lib/ruby_llm/agents/{attempt_tracker.rb → infrastructure/attempt_tracker.rb} +0 -0
  185. /data/lib/ruby_llm/agents/{cache_helper.rb → infrastructure/cache_helper.rb} +0 -0
  186. /data/lib/ruby_llm/agents/{circuit_breaker.rb → infrastructure/circuit_breaker.rb} +0 -0
  187. /data/lib/ruby_llm/agents/{redactor.rb → infrastructure/redactor.rb} +0 -0
  188. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/breaker_manager.rb +0 -0
  189. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/execution_constraints.rb +0 -0
  190. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/fallback_routing.rb +0 -0
@@ -0,0 +1,292 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "fileutils"
5
+
6
+ module RubyLlmAgents
7
+ # Restructure generator for migrating existing apps to new directory structure
8
+ #
9
+ # Migrates ruby_llm-agents directories from the flat structure:
10
+ # app/agents/, app/speakers/, app/embedders/, etc.
11
+ #
12
+ # To the new grouped structure under app/llm/:
13
+ # app/llm/agents/, app/llm/audio/speakers/, app/llm/image/generators/, etc.
14
+ #
15
+ # Usage:
16
+ # rails generate ruby_llm_agents:restructure
17
+ # rails generate ruby_llm_agents:restructure --root=ai
18
+ #
19
+ class RestructureGenerator < ::Rails::Generators::Base
20
+ source_root File.expand_path("templates", __dir__)
21
+
22
+ class_option :root,
23
+ type: :string,
24
+ default: nil,
25
+ desc: "Root directory name (default: uses config or 'llm')"
26
+
27
+ class_option :namespace,
28
+ type: :string,
29
+ default: nil,
30
+ desc: "Root namespace (default: camelized root or config)"
31
+
32
+ class_option :dry_run,
33
+ type: :boolean,
34
+ default: false,
35
+ desc: "Show what would be done without making changes"
36
+
37
+ # Maps old directory -> { category:, type: }
38
+ DIRECTORY_MAPPING = {
39
+ # Top-level under llm/
40
+ "agents" => { category: nil, type: "agents" },
41
+ "workflows" => { category: nil, type: "workflows" },
42
+ "tools" => { category: nil, type: "tools" },
43
+
44
+ # Audio group
45
+ "speakers" => { category: :audio, type: "speakers" },
46
+ "transcribers" => { category: :audio, type: "transcribers" },
47
+
48
+ # Image group
49
+ "image_generators" => { category: :image, type: "generators" },
50
+ "image_editors" => { category: :image, type: "editors" },
51
+ "image_analyzers" => { category: :image, type: "analyzers" },
52
+ "image_transformers" => { category: :image, type: "transformers" },
53
+ "image_upscalers" => { category: :image, type: "upscalers" },
54
+ "image_variators" => { category: :image, type: "variators" },
55
+ "background_removers" => { category: :image, type: "background_removers" },
56
+
57
+ # Text group
58
+ "embedders" => { category: :text, type: "embedders" },
59
+ "moderators" => { category: :text, type: "moderators" }
60
+ }.freeze
61
+
62
+ def validate_root_directory
63
+ unless root_directory.match?(/\A[a-z][a-z0-9_-]*\z/i)
64
+ raise ArgumentError, "Invalid root directory name: #{root_directory}. " \
65
+ "Must start with a letter and contain only letters, numbers, underscores, or hyphens."
66
+ end
67
+ end
68
+
69
+ def create_directory_structure
70
+ say_status :create, "#{root_directory}/ directory structure", :green
71
+
72
+ if options[:dry_run]
73
+ say_status :dry_run, "Would create directory structure under app/#{root_directory}/", :yellow
74
+ return
75
+ end
76
+
77
+ # Create root directory
78
+ empty_directory "app/#{root_directory}"
79
+
80
+ # Create all subdirectories
81
+ config.all_autoload_paths.each do |path|
82
+ empty_directory path
83
+ end
84
+ end
85
+
86
+ def move_directories
87
+ say ""
88
+ say_status :migrate, "Moving directories to new structure", :green
89
+
90
+ directories_moved = 0
91
+
92
+ DIRECTORY_MAPPING.each do |old_dir, mapping|
93
+ source = Rails.root.join("app", old_dir)
94
+ next unless File.directory?(source)
95
+
96
+ destination = Rails.root.join(config.path_for(mapping[:category], mapping[:type]))
97
+
98
+ if options[:dry_run]
99
+ say_status :dry_run, "Would move app/#{old_dir}/* -> #{destination}", :yellow
100
+ directories_moved += 1
101
+ next
102
+ end
103
+
104
+ move_directory_contents(source, destination, old_dir)
105
+ directories_moved += 1
106
+ end
107
+
108
+ if directories_moved == 0
109
+ say_status :skip, "No directories found to migrate", :yellow
110
+ end
111
+ end
112
+
113
+ def update_namespaces
114
+ say ""
115
+ say_status :update, "Adding namespaces to Ruby files", :green
116
+
117
+ DIRECTORY_MAPPING.each do |_old_dir, mapping|
118
+ directory_path = Rails.root.join(config.path_for(mapping[:category], mapping[:type]))
119
+ next unless File.directory?(directory_path)
120
+
121
+ namespace = config.namespace_for(mapping[:category])
122
+
123
+ Dir.glob("#{directory_path}/**/*.rb").each do |file|
124
+ update_file_namespace(file, namespace)
125
+ end
126
+ end
127
+ end
128
+
129
+ def cleanup_empty_directories
130
+ say ""
131
+ say_status :cleanup, "Removing empty old directories", :green
132
+
133
+ DIRECTORY_MAPPING.keys.each do |old_dir|
134
+ path = Rails.root.join("app", old_dir)
135
+ next unless File.directory?(path)
136
+
137
+ if Dir.empty?(path)
138
+ if options[:dry_run]
139
+ say_status :dry_run, "Would remove empty directory app/#{old_dir}", :yellow
140
+ else
141
+ FileUtils.rmdir(path)
142
+ say_status :removed, "app/#{old_dir}", :red
143
+ end
144
+ else
145
+ say_status :warning, "app/#{old_dir} is not empty, skipping removal", :yellow
146
+ end
147
+ end
148
+ end
149
+
150
+ def show_completion_message
151
+ say ""
152
+ say "=" * 60
153
+ say ""
154
+ if options[:dry_run]
155
+ say "Dry run complete! No changes were made.", :yellow
156
+ say ""
157
+ say "To perform the actual migration, run:"
158
+ say " rails generate ruby_llm_agents:restructure"
159
+ else
160
+ say "Migration complete!", :green
161
+ say ""
162
+ say "Your app now uses the new directory structure:"
163
+ say ""
164
+ say " app/#{root_directory}/"
165
+ say " ├── agents/"
166
+ say " ├── audio/"
167
+ say " │ ├── speakers/"
168
+ say " │ └── transcribers/"
169
+ say " ├── image/"
170
+ say " │ ├── analyzers/"
171
+ say " │ ├── generators/"
172
+ say " │ └── ..."
173
+ say " ├── text/"
174
+ say " │ ├── embedders/"
175
+ say " │ └── moderators/"
176
+ say " ├── workflows/"
177
+ say " └── tools/"
178
+ say ""
179
+ say "Namespaces have been updated to use #{root_namespace}::"
180
+ say ""
181
+ say "Next steps:"
182
+ say " 1. Update any explicit class references in your code"
183
+ say " 2. Run your test suite to verify everything works"
184
+ say " 3. Commit the changes"
185
+ end
186
+ say ""
187
+ say "=" * 60
188
+ end
189
+
190
+ private
191
+
192
+ def root_directory
193
+ @root_directory ||= options[:root] || RubyLLM::Agents.configuration.root_directory
194
+ end
195
+
196
+ def root_namespace
197
+ @root_namespace ||= options[:namespace] || camelize(root_directory)
198
+ end
199
+
200
+ def config
201
+ @config ||= begin
202
+ c = RubyLLM::Agents.configuration.dup
203
+ c.root_directory = root_directory
204
+ c.root_namespace = root_namespace
205
+ c
206
+ end
207
+ end
208
+
209
+ def camelize(str)
210
+ # Handle special cases for common abbreviations
211
+ return "AI" if str.downcase == "ai"
212
+ return "ML" if str.downcase == "ml"
213
+ return "LLM" if str.downcase == "llm"
214
+
215
+ # Standard camelization
216
+ str.split(/[-_]/).map(&:capitalize).join
217
+ end
218
+
219
+ def move_directory_contents(source, destination, old_dir_name)
220
+ # Ensure destination exists
221
+ FileUtils.mkdir_p(destination) unless File.directory?(destination)
222
+
223
+ # Move all contents
224
+ Dir.glob("#{source}/**/*", File::FNM_DOTMATCH).each do |item|
225
+ next if item.end_with?(".", "..")
226
+ next if File.directory?(item)
227
+
228
+ relative_path = item.sub("#{source}/", "")
229
+ dest_item = File.join(destination, relative_path)
230
+
231
+ FileUtils.mkdir_p(File.dirname(dest_item))
232
+ FileUtils.mv(item, dest_item)
233
+
234
+ say_status :moved, "app/#{old_dir_name}/#{relative_path}", :green
235
+ end
236
+
237
+ # Remove old directory if empty
238
+ cleanup_empty_subdirs(source)
239
+ FileUtils.rmdir(source) if File.directory?(source) && Dir.empty?(source)
240
+ end
241
+
242
+ def cleanup_empty_subdirs(dir)
243
+ return unless File.directory?(dir)
244
+
245
+ Dir.glob("#{dir}/**/").reverse_each do |subdir|
246
+ FileUtils.rmdir(subdir) if Dir.empty?(subdir)
247
+ rescue SystemCallError
248
+ # Ignore errors if directory is not empty or already removed
249
+ end
250
+ end
251
+
252
+ def update_file_namespace(file, namespace)
253
+ content = File.read(file)
254
+
255
+ # Skip if already has the namespace
256
+ first_module = namespace.split("::").first
257
+ return if content.include?("module #{first_module}")
258
+
259
+ # Add namespace
260
+ updated = add_namespace(content, namespace)
261
+ File.write(file, updated)
262
+
263
+ say_status :namespaced, file.sub(Rails.root.to_s + "/", ""), :blue
264
+ end
265
+
266
+ def add_namespace(content, namespace)
267
+ modules = namespace.split("::")
268
+ indent = ""
269
+
270
+ # Build opening modules
271
+ opening = modules.map do |mod|
272
+ line = "#{indent}module #{mod}"
273
+ indent += " "
274
+ line
275
+ end.join("\n")
276
+
277
+ # Build closing modules
278
+ closing = modules.map { "end" }.join("\n")
279
+
280
+ # Indent original content
281
+ indented_content = content.lines.map do |line|
282
+ if line.strip.empty?
283
+ line
284
+ else
285
+ (" " * modules.size) + line
286
+ end
287
+ end.join
288
+
289
+ "#{opening}\n#{indented_content}#{closing}\n"
290
+ end
291
+ end
292
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module RubyLlmAgents
6
+ # Speaker generator for creating new text-to-speech speakers
7
+ #
8
+ # Usage:
9
+ # rails generate ruby_llm_agents:speaker Narrator
10
+ # rails generate ruby_llm_agents:speaker Narrator --provider elevenlabs
11
+ # rails generate ruby_llm_agents:speaker Narrator --voice alloy --speed 1.25
12
+ # rails generate ruby_llm_agents:speaker Narrator --root=ai
13
+ #
14
+ # This will create:
15
+ # - app/{root}/audio/speakers/narrator_speaker.rb
16
+ #
17
+ class SpeakerGenerator < ::Rails::Generators::NamedBase
18
+ source_root File.expand_path("templates", __dir__)
19
+
20
+ class_option :provider, type: :string, default: "openai",
21
+ desc: "The TTS provider to use (openai, elevenlabs)"
22
+ class_option :model, type: :string, default: nil,
23
+ desc: "The TTS model to use"
24
+ class_option :voice, type: :string, default: "nova",
25
+ desc: "The voice to use"
26
+ class_option :speed, type: :numeric, default: 1.0,
27
+ desc: "Speech speed (0.25-4.0 for OpenAI)"
28
+ class_option :format, type: :string, default: "mp3",
29
+ desc: "Output format (mp3, wav, ogg, flac)"
30
+ class_option :cache, type: :string, default: nil,
31
+ 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
+
41
+ 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"
45
+
46
+ # Create directory if needed
47
+ empty_directory speakers_dir
48
+
49
+ # Create base class if it doesn't exist
50
+ base_class_path = "#{speakers_dir}/application_speaker.rb"
51
+ unless File.exist?(File.join(destination_root, base_class_path))
52
+ template "application_speaker.rb.tt", base_class_path
53
+ end
54
+
55
+ # Create skill file if it doesn't exist
56
+ skill_file_path = "#{speakers_dir}/SPEAKERS.md"
57
+ unless File.exist?(File.join(destination_root, skill_file_path))
58
+ template "skills/SPEAKERS.md.tt", skill_file_path
59
+ end
60
+ end
61
+
62
+ 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"
66
+ speaker_path = name.underscore
67
+ template "speaker.rb.tt", "app/#{root_directory}/audio/speakers/#{speaker_path}_speaker.rb"
68
+ end
69
+
70
+ def show_usage
71
+ # Build full class name from path
72
+ speaker_class_name = name.split("/").map(&:camelize).join("::")
73
+ full_class_name = "#{root_namespace}::Audio::#{speaker_class_name}Speaker"
74
+ say ""
75
+ say "Speaker #{full_class_name} created!", :green
76
+ say ""
77
+ say "Usage:"
78
+ say " # Generate speech"
79
+ say " result = #{full_class_name}.call(text: \"Hello world\")"
80
+ say " result.audio # => Binary audio data"
81
+ say ""
82
+ say " # Save to file"
83
+ say " result.save_to(\"output.mp3\")"
84
+ say ""
85
+ say " # Stream audio"
86
+ say " #{full_class_name}.stream(text: \"Long article...\") do |chunk|"
87
+ say " audio_player.play(chunk.audio)"
88
+ say " end"
89
+ say ""
90
+ end
91
+
92
+ private
93
+
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
+ def default_model
113
+ case options[:provider].to_s
114
+ when "elevenlabs"
115
+ "eleven_monolingual_v1"
116
+ else
117
+ "tts-1"
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddExecutionTypeToRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ add_column :ruby_llm_agents_executions, :execution_type, :string, default: "chat"
6
+ add_index :ruby_llm_agents_executions, :execution_type
7
+ end
8
+ end
@@ -1,108 +1,123 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class <%= class_name %>Agent < ApplicationAgent
4
- # ============================================
5
- # Model Configuration
6
- # ============================================
7
-
8
- model "<%= options[:model] %>"
9
- temperature <%= options[:temperature] %>
10
- version "1.0"
11
- # timeout 30 # Per-request timeout in seconds (default: 60)
12
-
13
- # ============================================
14
- # Caching
15
- # ============================================
3
+ module <%= @root_namespace %>
4
+ <%- if class_name.include?("::") -%>
5
+ <%- class_name.split("::")[0..-2].each_with_index do |mod, i| -%>
6
+ <%= " " * (i + 1) %>module <%= mod %>
7
+ <%- end -%>
8
+ <%= " " * class_name.split("::").length %>class <%= class_name.split("::").last %>Agent < ApplicationAgent
9
+ <%- else -%>
10
+ class <%= class_name %>Agent < ApplicationAgent
11
+ <%- end -%>
12
+ # ============================================
13
+ # Model Configuration
14
+ # ============================================
15
+
16
+ model "<%= options[:model] %>"
17
+ temperature <%= options[:temperature] %>
18
+ version "1.0"
19
+ # timeout 30 # Per-request timeout in seconds (default: 60)
20
+
21
+ # ============================================
22
+ # Caching
23
+ # ============================================
16
24
 
17
25
  <% if options[:cache] -%>
18
- cache <%= options[:cache] %>
26
+ cache <%= options[:cache] %>
19
27
  <% else -%>
20
- # cache 1.hour # Enable response caching with TTL
28
+ # cache 1.hour # Enable response caching with TTL
21
29
  <% end -%>
22
30
 
23
- # ============================================
24
- # Reliability (Retries & Fallbacks)
25
- # ============================================
31
+ # ============================================
32
+ # Reliability (Retries & Fallbacks)
33
+ # ============================================
26
34
 
27
- # Automatic retries with exponential backoff
28
- # - max: Number of retry attempts
29
- # - backoff: :constant or :exponential
30
- # - base: Base delay in seconds
31
- # - max_delay: Maximum delay between retries
32
- # - on: Additional error classes to retry on
33
- # retries max: 2, backoff: :exponential, base: 0.4, max_delay: 3.0
35
+ # Automatic retries with exponential backoff
36
+ # - max: Number of retry attempts
37
+ # - backoff: :constant or :exponential
38
+ # - base: Base delay in seconds
39
+ # - max_delay: Maximum delay between retries
40
+ # - on: Additional error classes to retry on
41
+ # retries max: 2, backoff: :exponential, base: 0.4, max_delay: 3.0
34
42
 
35
- # Fallback models (tried in order when primary model fails)
36
- # fallback_models ["gpt-4o-mini", "claude-3-haiku"]
43
+ # Fallback models (tried in order when primary model fails)
44
+ # fallback_models ["gpt-4o-mini", "claude-3-haiku"]
37
45
 
38
- # Total timeout across all retry/fallback attempts
39
- # total_timeout 30
46
+ # Total timeout across all retry/fallback attempts
47
+ # total_timeout 30
40
48
 
41
- # Circuit breaker (prevents repeated calls to failing models)
42
- # - errors: Number of errors to trigger open state
43
- # - within: Rolling window in seconds
44
- # - cooldown: Time to wait before allowing requests again
45
- # circuit_breaker errors: 5, within: 60, cooldown: 300
49
+ # Circuit breaker (prevents repeated calls to failing models)
50
+ # - errors: Number of errors to trigger open state
51
+ # - within: Rolling window in seconds
52
+ # - cooldown: Time to wait before allowing requests again
53
+ # circuit_breaker errors: 5, within: 60, cooldown: 300
46
54
 
47
- # ============================================
48
- # Parameters
49
- # ============================================
55
+ # ============================================
56
+ # Parameters
57
+ # ============================================
50
58
 
51
59
  <% parsed_params.each do |param| -%>
52
- param :<%= param.name %><%= ", required: true" if param.required? %><%= ", default: #{param.default.inspect}" if param.default && !param.required? %>
60
+ param :<%= param.name %><%= ", required: true" if param.required? %><%= ", default: #{param.default.inspect}" if param.default && !param.required? %>
53
61
  <% end -%>
54
62
 
55
- private
63
+ private
56
64
 
57
- # ============================================
58
- # Prompts (required)
59
- # ============================================
65
+ # ============================================
66
+ # Prompts (required)
67
+ # ============================================
60
68
 
61
- def system_prompt
62
- <<~PROMPT
63
- You are a helpful assistant.
64
- # Define your system instructions here
65
- PROMPT
66
- end
69
+ def system_prompt
70
+ <<~PROMPT
71
+ You are a helpful assistant.
72
+ # Define your system instructions here
73
+ PROMPT
74
+ end
67
75
 
68
- def user_prompt
69
- # Build the prompt from parameters
76
+ def user_prompt
77
+ # Build the prompt from parameters
70
78
  <% if parsed_params.any? -%>
71
- <%= parsed_params.first.name %>
79
+ <%= parsed_params.first.name %>
72
80
  <% else -%>
73
- "Your prompt here"
81
+ "Your prompt here"
74
82
  <% end -%>
83
+ end
84
+
85
+ # ============================================
86
+ # Optional Overrides
87
+ # ============================================
88
+
89
+ # Structured output schema (returns parsed hash instead of raw text)
90
+ # def schema
91
+ # @schema ||= RubyLLM::Schema.create do
92
+ # string :result, description: "The result"
93
+ # integer :confidence, description: "Confidence score 1-100"
94
+ # array :tags, description: "Relevant tags" do
95
+ # string
96
+ # end
97
+ # end
98
+ # end
99
+
100
+ # Custom response processing (default: symbolize hash keys)
101
+ # def process_response(response)
102
+ # content = response.content
103
+ # # Transform or validate the response
104
+ # content
105
+ # end
106
+
107
+ # Custom metadata to include in execution logs
108
+ # def execution_metadata
109
+ # { custom_field: "value", request_id: params[:request_id] }
110
+ # end
111
+
112
+ # Custom cache key data (default: all params except skip_cache, dry_run)
113
+ # def cache_key_data
114
+ # { query: params[:query], locale: I18n.locale }
115
+ # end
116
+ <%- if class_name.include?("::") -%>
117
+ <%- (class_name.split("::").length).times do |i| -%>
118
+ <%= " " * (class_name.split("::").length - i) %>end
119
+ <%- end -%>
120
+ <%- else -%>
75
121
  end
76
-
77
- # ============================================
78
- # Optional Overrides
79
- # ============================================
80
-
81
- # Structured output schema (returns parsed hash instead of raw text)
82
- # def schema
83
- # @schema ||= RubyLLM::Schema.create do
84
- # string :result, description: "The result"
85
- # integer :confidence, description: "Confidence score 1-100"
86
- # array :tags, description: "Relevant tags" do
87
- # string
88
- # end
89
- # end
90
- # end
91
-
92
- # Custom response processing (default: symbolize hash keys)
93
- # def process_response(response)
94
- # content = response.content
95
- # # Transform or validate the response
96
- # content
97
- # end
98
-
99
- # Custom metadata to include in execution logs
100
- # def execution_metadata
101
- # { custom_field: "value", request_id: params[:request_id] }
102
- # end
103
-
104
- # Custom cache key data (default: all params except skip_cache, dry_run)
105
- # def cache_key_data
106
- # { query: params[:query], locale: I18n.locale }
107
- # end
122
+ <%- end -%>
108
123
  end