ruby_llm-agents 0.5.0 → 1.0.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 (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,455 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module RubyLLM
6
+ module Agents
7
+ # Image generator base class for text-to-image generation using the middleware pipeline
8
+ #
9
+ # Follows the same patterns as other agents - inherits from BaseAgent for unified
10
+ # execution flow, caching, instrumentation, and budget controls through middleware.
11
+ #
12
+ # @example Basic usage
13
+ # result = RubyLLM::Agents::ImageGenerator.call(prompt: "A sunset over mountains")
14
+ # result.url # => "https://..."
15
+ #
16
+ # @example Custom generator class
17
+ # class LogoGenerator < RubyLLM::Agents::ImageGenerator
18
+ # model "gpt-image-1"
19
+ # size "1024x1024"
20
+ # quality "hd"
21
+ # style "vivid"
22
+ #
23
+ # description "Generates company logos"
24
+ # content_policy :strict
25
+ # end
26
+ #
27
+ # result = LogoGenerator.call(prompt: "Minimalist tech company logo")
28
+ #
29
+ # @api public
30
+ class ImageGenerator < BaseAgent
31
+ class << self
32
+ # Returns the agent type for image generators
33
+ #
34
+ # @return [Symbol] :image
35
+ def agent_type
36
+ :image
37
+ end
38
+
39
+ # @!group Image-specific DSL
40
+
41
+ # Sets or returns the image generation model
42
+ #
43
+ # @param value [String, nil] Model identifier
44
+ # @return [String] The model to use
45
+ def model(value = nil)
46
+ @model = value if value
47
+ return @model if defined?(@model) && @model
48
+
49
+ if superclass.respond_to?(:agent_type) && superclass.agent_type == :image
50
+ superclass.model
51
+ else
52
+ default_image_model
53
+ end
54
+ end
55
+
56
+ # Sets or returns the image size
57
+ #
58
+ # @param value [String, nil] Size (e.g., "1024x1024", "1792x1024")
59
+ # @return [String] The size to use
60
+ def size(value = nil)
61
+ @size = value if value
62
+ @size || inherited_or_default(:size, default_image_size)
63
+ end
64
+
65
+ # Sets or returns the quality level
66
+ #
67
+ # @param value [String, nil] Quality ("standard", "hd")
68
+ # @return [String] The quality to use
69
+ def quality(value = nil)
70
+ @quality = value if value
71
+ @quality || inherited_or_default(:quality, default_image_quality)
72
+ end
73
+
74
+ # Sets or returns the style preset
75
+ #
76
+ # @param value [String, nil] Style ("vivid", "natural")
77
+ # @return [String] The style to use
78
+ def style(value = nil)
79
+ @style = value if value
80
+ @style || inherited_or_default(:style, default_image_style)
81
+ end
82
+
83
+ # Sets or returns the content policy level
84
+ #
85
+ # @param level [Symbol, nil] Policy level (:none, :standard, :moderate, :strict)
86
+ # @return [Symbol] The content policy level
87
+ def content_policy(level = nil)
88
+ @content_policy = level if level
89
+ @content_policy || inherited_or_default(:content_policy, :standard)
90
+ end
91
+
92
+ # Sets or returns negative prompt (things to avoid in generation)
93
+ #
94
+ # @param value [String, nil] Negative prompt text
95
+ # @return [String, nil] The negative prompt
96
+ def negative_prompt(value = nil)
97
+ @negative_prompt = value if value
98
+ @negative_prompt || inherited_or_default(:negative_prompt, nil)
99
+ end
100
+
101
+ # Sets or returns the seed for reproducible generation
102
+ #
103
+ # @param value [Integer, nil] Seed value
104
+ # @return [Integer, nil] The seed
105
+ def seed(value = nil)
106
+ @seed = value if value
107
+ @seed || inherited_or_default(:seed, nil)
108
+ end
109
+
110
+ # Sets or returns guidance scale (CFG scale)
111
+ #
112
+ # @param value [Float, nil] Guidance scale (typically 1.0-20.0)
113
+ # @return [Float, nil] The guidance scale
114
+ def guidance_scale(value = nil)
115
+ @guidance_scale = value if value
116
+ @guidance_scale || inherited_or_default(:guidance_scale, nil)
117
+ end
118
+
119
+ # Sets or returns number of inference steps
120
+ #
121
+ # @param value [Integer, nil] Number of steps
122
+ # @return [Integer, nil] The steps
123
+ def steps(value = nil)
124
+ @steps = value if value
125
+ @steps || inherited_or_default(:steps, nil)
126
+ end
127
+
128
+ # Sets a prompt template (use {prompt} as placeholder)
129
+ #
130
+ # @param value [String, nil] Template string
131
+ # @return [String, nil] The template
132
+ def template(value = nil)
133
+ @template_string = value if value
134
+ @template_string || inherited_or_default(:template_string, nil)
135
+ end
136
+
137
+ # Gets the template string
138
+ #
139
+ # @return [String, nil] The template string
140
+ def template_string
141
+ @template_string || inherited_or_default(:template_string, nil)
142
+ end
143
+
144
+ # @!endgroup
145
+
146
+ # Factory method to execute image generation
147
+ #
148
+ # @param prompt [String] The text prompt for image generation
149
+ # @param options [Hash] Additional options
150
+ # @return [ImageGenerationResult] The result containing generated images
151
+ def call(prompt:, **options)
152
+ new(prompt: prompt, **options).call
153
+ end
154
+
155
+ # Ensure subclasses inherit DSL settings
156
+ def inherited(subclass)
157
+ super
158
+ subclass.instance_variable_set(:@model, @model)
159
+ subclass.instance_variable_set(:@size, @size)
160
+ subclass.instance_variable_set(:@quality, @quality)
161
+ subclass.instance_variable_set(:@style, @style)
162
+ subclass.instance_variable_set(:@version, @version)
163
+ subclass.instance_variable_set(:@description, @description)
164
+ subclass.instance_variable_set(:@cache_ttl, @cache_ttl)
165
+ subclass.instance_variable_set(:@content_policy, @content_policy)
166
+ subclass.instance_variable_set(:@negative_prompt, @negative_prompt)
167
+ subclass.instance_variable_set(:@seed, @seed)
168
+ subclass.instance_variable_set(:@guidance_scale, @guidance_scale)
169
+ subclass.instance_variable_set(:@steps, @steps)
170
+ subclass.instance_variable_set(:@template_string, @template_string)
171
+ end
172
+
173
+ private
174
+
175
+ def inherited_or_default(method, default)
176
+ superclass.respond_to?(method) ? superclass.send(method) : default
177
+ end
178
+
179
+ def default_image_model
180
+ RubyLLM::Agents.configuration.default_image_model
181
+ rescue StandardError
182
+ "dall-e-3"
183
+ end
184
+
185
+ def default_image_size
186
+ RubyLLM::Agents.configuration.default_image_size
187
+ rescue StandardError
188
+ "1024x1024"
189
+ end
190
+
191
+ def default_image_quality
192
+ RubyLLM::Agents.configuration.default_image_quality
193
+ rescue StandardError
194
+ "standard"
195
+ end
196
+
197
+ def default_image_style
198
+ RubyLLM::Agents.configuration.default_image_style
199
+ rescue StandardError
200
+ "vivid"
201
+ end
202
+ end
203
+
204
+ # @!attribute [r] prompt
205
+ # @return [String] The text prompt for image generation
206
+ attr_reader :prompt
207
+
208
+ # Creates a new ImageGenerator instance
209
+ #
210
+ # @param prompt [String] The text prompt for image generation
211
+ # @param options [Hash] Additional options
212
+ def initialize(prompt:, **options)
213
+ @prompt = prompt
214
+ @runtime_count = options.delete(:count) || 1
215
+
216
+ # Set model to image model if not specified
217
+ options[:model] ||= self.class.model
218
+
219
+ super(**options)
220
+ end
221
+
222
+ # Executes the image generation through the middleware pipeline
223
+ #
224
+ # @return [ImageGenerationResult] The result containing generated images
225
+ def call
226
+ context = build_context
227
+ result_context = Pipeline::Executor.execute(context)
228
+ result_context.output
229
+ end
230
+
231
+ # The input for this generation operation
232
+ #
233
+ # @return [String] The prompt
234
+ def user_prompt
235
+ prompt
236
+ end
237
+
238
+ # Core image generation execution
239
+ #
240
+ # This is called by the Pipeline::Executor after middleware
241
+ # has been applied. Only contains the image generation API logic.
242
+ #
243
+ # @param context [Pipeline::Context] The execution context
244
+ # @return [void] Sets context.output with the ImageGenerationResult
245
+ def execute(context)
246
+ execution_started_at = Time.current
247
+
248
+ validate_prompt!
249
+ validate_content_policy!
250
+
251
+ # Generate image(s)
252
+ images = generate_images
253
+
254
+ execution_completed_at = Time.current
255
+ duration_ms = ((execution_completed_at - execution_started_at) * 1000).to_i
256
+
257
+ # Build result
258
+ result = build_result(
259
+ images: images,
260
+ started_at: context.started_at || execution_started_at,
261
+ completed_at: execution_completed_at,
262
+ tenant_id: context.tenant_id
263
+ )
264
+
265
+ # Update context with cost info
266
+ context.input_tokens = result.input_tokens
267
+ context.output_tokens = 0
268
+ context.total_cost = result.total_cost
269
+
270
+ context.output = result
271
+ rescue StandardError => e
272
+ execution_completed_at = Time.current
273
+ context.output = build_error_result(
274
+ e,
275
+ started_at: context.started_at || execution_started_at,
276
+ completed_at: execution_completed_at,
277
+ tenant_id: context.tenant_id
278
+ )
279
+ end
280
+
281
+ # Generates the cache key for this image generation
282
+ #
283
+ # @return [String] Cache key
284
+ def agent_cache_key
285
+ components = [
286
+ "ruby_llm_agents",
287
+ "image_generator",
288
+ self.class.name,
289
+ self.class.version,
290
+ resolved_model,
291
+ resolved_size,
292
+ resolved_quality,
293
+ resolved_style,
294
+ Digest::SHA256.hexdigest(prompt)
295
+ ].compact
296
+
297
+ components.join("/")
298
+ end
299
+
300
+ private
301
+
302
+ # Builds context for pipeline execution
303
+ #
304
+ # @return [Pipeline::Context] The context object
305
+ def build_context
306
+ Pipeline::Context.new(
307
+ input: user_prompt,
308
+ agent_class: self.class,
309
+ agent_instance: self,
310
+ model: resolved_model,
311
+ tenant: @options[:tenant],
312
+ skip_cache: @options[:skip_cache] || !single_image_request?
313
+ )
314
+ end
315
+
316
+ # Validates the prompt
317
+ def validate_prompt!
318
+ raise ArgumentError, "Prompt cannot be blank" if prompt.nil? || prompt.strip.empty?
319
+
320
+ max_length = config.max_image_prompt_length || 4000
321
+ if prompt.length > max_length
322
+ raise ArgumentError, "Prompt exceeds maximum length of #{max_length} characters"
323
+ end
324
+ end
325
+
326
+ # Validates prompt against content policy
327
+ def validate_content_policy!
328
+ policy = self.class.content_policy
329
+ return if policy == :none || policy == :standard
330
+
331
+ ContentPolicy.validate!(prompt, policy)
332
+ end
333
+
334
+ # Generate images using RubyLLM.paint
335
+ def generate_images
336
+ count = @runtime_count
337
+
338
+ Array.new(count) do
339
+ paint_options = build_paint_options
340
+ RubyLLM.paint(apply_template(prompt), **paint_options)
341
+ end
342
+ end
343
+
344
+ # Build options hash for RubyLLM.paint
345
+ def build_paint_options
346
+ opts = {
347
+ model: resolved_model,
348
+ size: resolved_size
349
+ }
350
+
351
+ opts[:quality] = resolved_quality if resolved_quality
352
+ opts[:style] = resolved_style if resolved_style
353
+ opts[:negative_prompt] = resolved_negative_prompt if resolved_negative_prompt
354
+ opts[:seed] = resolved_seed if resolved_seed
355
+ opts[:guidance_scale] = resolved_guidance_scale if resolved_guidance_scale
356
+ opts[:steps] = resolved_steps if resolved_steps
357
+ opts[:assume_model_exists] = true if @options[:assume_model_exists]
358
+
359
+ opts
360
+ end
361
+
362
+ # Apply prompt template if defined
363
+ def apply_template(text)
364
+ template = self.class.try(:template_string)
365
+ return text unless template
366
+
367
+ template.gsub("{prompt}", text)
368
+ end
369
+
370
+ # Build successful result
371
+ def build_result(images:, started_at:, completed_at:, tenant_id:)
372
+ ImageGenerationResult.new(
373
+ images: images,
374
+ prompt: prompt,
375
+ model_id: resolved_model,
376
+ size: resolved_size,
377
+ quality: resolved_quality,
378
+ style: resolved_style,
379
+ started_at: started_at,
380
+ completed_at: completed_at,
381
+ tenant_id: tenant_id,
382
+ generator_class: self.class.name
383
+ )
384
+ end
385
+
386
+ # Build error result
387
+ def build_error_result(error, started_at:, completed_at:, tenant_id:)
388
+ ImageGenerationResult.new(
389
+ images: [],
390
+ prompt: prompt,
391
+ model_id: resolved_model,
392
+ size: resolved_size,
393
+ quality: resolved_quality,
394
+ style: resolved_style,
395
+ started_at: started_at,
396
+ completed_at: completed_at,
397
+ tenant_id: tenant_id,
398
+ generator_class: self.class.name,
399
+ error_class: error.class.name,
400
+ error_message: error.message
401
+ )
402
+ end
403
+
404
+ # Resolution methods (runtime options override class config)
405
+
406
+ def resolved_model
407
+ model = @options[:model] || @model || self.class.model
408
+ # Handle aliases
409
+ config.image_model_aliases&.dig(model.to_sym) || model
410
+ end
411
+
412
+ def resolved_size
413
+ @options[:size] || self.class.size
414
+ end
415
+
416
+ def resolved_quality
417
+ @options[:quality] || self.class.quality
418
+ end
419
+
420
+ def resolved_style
421
+ @options[:style] || self.class.style
422
+ end
423
+
424
+ def resolved_negative_prompt
425
+ @options[:negative_prompt] || self.class.negative_prompt
426
+ end
427
+
428
+ def resolved_seed
429
+ @options[:seed] || self.class.seed
430
+ end
431
+
432
+ def resolved_guidance_scale
433
+ @options[:guidance_scale] || self.class.guidance_scale
434
+ end
435
+
436
+ def resolved_steps
437
+ @options[:steps] || self.class.steps
438
+ end
439
+
440
+ def single_image_request?
441
+ @runtime_count == 1
442
+ end
443
+
444
+ def config
445
+ RubyLLM::Agents.configuration
446
+ end
447
+ end
448
+ end
449
+ end
450
+
451
+ # Load supporting modules after class is defined (they reopen the class)
452
+ require_relative "generator/pricing"
453
+ require_relative "generator/content_policy"
454
+ require_relative "generator/templates"
455
+ require_relative "generator/active_storage_support"
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ class ImagePipeline
6
+ # DSL for defining image pipeline steps and configuration
7
+ #
8
+ # Provides methods for configuring pipeline steps, callbacks,
9
+ # caching, and error handling behavior.
10
+ #
11
+ # @example Defining pipeline steps
12
+ # class MyPipeline < ImagePipeline
13
+ # step :generate, generator: LogoGenerator
14
+ # step :upscale, upscaler: PhotoUpscaler, scale: 4
15
+ # step :analyze, analyzer: ProductAnalyzer
16
+ #
17
+ # version "1.0"
18
+ # description "Complete product image pipeline"
19
+ # stop_on_error true
20
+ # end
21
+ #
22
+ module DSL
23
+ # Define a pipeline step
24
+ #
25
+ # @param name [Symbol] Step name (must be unique)
26
+ # @param config [Hash] Step configuration
27
+ # @option config [Class] :generator ImageGenerator class for generation steps
28
+ # @option config [Class] :variator ImageVariator class for variation steps
29
+ # @option config [Class] :editor ImageEditor class for editing steps
30
+ # @option config [Class] :transformer ImageTransformer class for transformation steps
31
+ # @option config [Class] :upscaler ImageUpscaler class for upscaling steps
32
+ # @option config [Class] :analyzer ImageAnalyzer class for analysis steps
33
+ # @option config [Class] :remover BackgroundRemover class for background removal steps
34
+ # @option config [Proc] :if Conditional proc that receives context and returns boolean
35
+ # @option config [Proc] :unless Conditional proc that receives context and returns boolean
36
+ # @return [void]
37
+ #
38
+ # @example Different step types
39
+ # step :generate, generator: MyGenerator
40
+ # step :upscale, upscaler: MyUpscaler, scale: 2
41
+ # step :transform, transformer: StyleTransformer, strength: 0.7
42
+ # step :analyze, analyzer: ContentAnalyzer
43
+ # step :remove_bg, remover: BackgroundRemover
44
+ #
45
+ # @example Conditional steps
46
+ # step :upscale, upscaler: PhotoUpscaler, if: ->(ctx) { ctx[:high_quality] }
47
+ # step :remove_bg, remover: BackgroundRemover, unless: ->(ctx) { ctx[:keep_background] }
48
+ #
49
+ def step(name, **config)
50
+ @steps ||= []
51
+
52
+ # Validate step configuration
53
+ validate_step_config!(name, config)
54
+
55
+ @steps << {
56
+ name: name,
57
+ config: config,
58
+ type: determine_step_type(config)
59
+ }
60
+ end
61
+
62
+ # Get all defined steps
63
+ #
64
+ # @return [Array<Hash>] Array of step definitions
65
+ def steps
66
+ @steps ||= []
67
+ end
68
+
69
+ # Add a callback to run before the pipeline
70
+ #
71
+ # @param method_name [Symbol] Method to call
72
+ # @yield Block to execute
73
+ # @return [void]
74
+ #
75
+ # @example
76
+ # before_pipeline :validate_inputs
77
+ # before_pipeline { |ctx| ctx[:started_at] = Time.current }
78
+ #
79
+ def before_pipeline(method_name = nil, &block)
80
+ @callbacks ||= { before: [], after: [] }
81
+ @callbacks[:before] << (block || method_name)
82
+ end
83
+
84
+ # Add a callback to run after the pipeline
85
+ #
86
+ # @param method_name [Symbol] Method to call
87
+ # @yield Block to execute
88
+ # @return [void]
89
+ #
90
+ # @example
91
+ # after_pipeline :add_watermark
92
+ # after_pipeline { |result| notify_completion(result) }
93
+ #
94
+ def after_pipeline(method_name = nil, &block)
95
+ @callbacks ||= { before: [], after: [] }
96
+ @callbacks[:after] << (block || method_name)
97
+ end
98
+
99
+ # Get callbacks
100
+ #
101
+ # @return [Hash] Hash with :before and :after arrays
102
+ def callbacks
103
+ @callbacks ||= { before: [], after: [] }
104
+ end
105
+
106
+ # Set or get the version
107
+ #
108
+ # @param value [String, nil] Version identifier
109
+ # @return [String] The version
110
+ def version(value = nil)
111
+ if value
112
+ @version = value
113
+ else
114
+ @version || inherited_or_default(:version, "v1")
115
+ end
116
+ end
117
+
118
+ # Set or get the description
119
+ #
120
+ # @param value [String, nil] Description
121
+ # @return [String, nil] The description
122
+ def description(value = nil)
123
+ if value
124
+ @description = value
125
+ else
126
+ @description || inherited_or_default(:description, nil)
127
+ end
128
+ end
129
+
130
+ # Enable caching with the given TTL
131
+ #
132
+ # @param ttl [ActiveSupport::Duration, Integer] Cache duration
133
+ def cache_for(ttl)
134
+ @cache_ttl = ttl
135
+ end
136
+
137
+ # Get the cache TTL
138
+ #
139
+ # @return [ActiveSupport::Duration, Integer, nil] The cache TTL
140
+ def cache_ttl
141
+ @cache_ttl || inherited_or_default(:cache_ttl, nil)
142
+ end
143
+
144
+ # Check if caching is enabled
145
+ #
146
+ # @return [Boolean] true if caching is enabled
147
+ def cache_enabled?
148
+ !cache_ttl.nil?
149
+ end
150
+
151
+ # Set whether to stop on error (default: true)
152
+ #
153
+ # @param value [Boolean, nil] Whether to stop on first error
154
+ # @return [Boolean] Current setting
155
+ def stop_on_error(value = nil)
156
+ if value.nil?
157
+ return @stop_on_error if defined?(@stop_on_error) && !@stop_on_error.nil?
158
+ inherited_or_default(:stop_on_error, true)
159
+ else
160
+ @stop_on_error = value
161
+ end
162
+ end
163
+
164
+ alias stop_on_error? stop_on_error
165
+
166
+ private
167
+
168
+ def validate_step_config!(name, config)
169
+ # Check for duplicate step names
170
+ if @steps&.any? { |s| s[:name] == name }
171
+ raise ArgumentError, "Step :#{name} is already defined"
172
+ end
173
+
174
+ # Check for valid step type
175
+ valid_keys = %i[generator variator editor transformer upscaler analyzer remover]
176
+ step_keys = config.keys & valid_keys
177
+
178
+ if step_keys.empty?
179
+ raise ArgumentError, "Step :#{name} must specify one of: #{valid_keys.join(', ')}"
180
+ end
181
+
182
+ if step_keys.size > 1
183
+ raise ArgumentError, "Step :#{name} can only specify one step type, got: #{step_keys.join(', ')}"
184
+ end
185
+
186
+ # Validate the class responds to call
187
+ step_class = config[step_keys.first]
188
+ unless step_class.respond_to?(:call)
189
+ raise ArgumentError, "#{step_class} must respond to .call"
190
+ end
191
+ end
192
+
193
+ def determine_step_type(config)
194
+ %i[generator variator editor transformer upscaler analyzer remover].find do |type|
195
+ config.key?(type)
196
+ end
197
+ end
198
+
199
+ def inherited_or_default(attribute, default)
200
+ if superclass.respond_to?(attribute)
201
+ superclass.public_send(attribute)
202
+ else
203
+ default
204
+ end
205
+ end
206
+
207
+ def config
208
+ RubyLLM::Agents.configuration
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end