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,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require_relative "../concerns/image_operation_execution"
5
+
6
+ module RubyLLM
7
+ module Agents
8
+ class ImageEditor
9
+ # Execution logic for image editors
10
+ #
11
+ # Handles image/mask validation, content policy checks,
12
+ # budget tracking, caching, image editing, and result building.
13
+ #
14
+ module Execution
15
+ include Concerns::ImageOperationExecution
16
+
17
+ # Execute the image editing pipeline
18
+ #
19
+ # @return [ImageEditResult] The result containing edited image
20
+ def execute
21
+ started_at = Time.current
22
+
23
+ resolve_tenant_context!
24
+ check_budget! if budget_tracking_enabled?
25
+ validate_inputs!
26
+ validate_content_policy!
27
+
28
+ # Check cache
29
+ cached = check_cache(ImageEditResult) if cache_enabled?
30
+ return cached if cached
31
+
32
+ # Edit image(s)
33
+ edited_images = edit_images
34
+
35
+ # Build result
36
+ result = build_result(
37
+ images: edited_images,
38
+ started_at: started_at,
39
+ completed_at: Time.current
40
+ )
41
+
42
+ # Cache result
43
+ write_cache(result) if cache_enabled?
44
+
45
+ # Track execution
46
+ record_execution(result) if execution_tracking_enabled?
47
+
48
+ result
49
+ rescue StandardError => e
50
+ record_failed_execution(e, started_at) if execution_tracking_enabled?
51
+ build_error_result(e, started_at)
52
+ end
53
+
54
+ private
55
+
56
+ def execution_type
57
+ "image_edit"
58
+ end
59
+
60
+ def validate_inputs!
61
+ raise ArgumentError, "Image cannot be blank" if image.nil?
62
+ raise ArgumentError, "Mask cannot be blank" if mask.nil?
63
+ raise ArgumentError, "Prompt cannot be blank" if prompt.nil? || prompt.strip.empty?
64
+
65
+ # Validate image exists if it's a path
66
+ validate_file_exists!(image, "Image")
67
+ validate_file_exists!(mask, "Mask")
68
+
69
+ # Validate prompt length
70
+ max_length = config.max_image_prompt_length || 4000
71
+ if prompt.length > max_length
72
+ raise ArgumentError, "Prompt exceeds maximum length of #{max_length} characters"
73
+ end
74
+ end
75
+
76
+ def validate_file_exists!(file, name)
77
+ return unless file.is_a?(String) && !file.start_with?("http")
78
+
79
+ unless File.exist?(file)
80
+ raise ArgumentError, "#{name} file does not exist: #{file}"
81
+ end
82
+ end
83
+
84
+ def validate_content_policy!
85
+ policy = self.class.content_policy
86
+ return if policy == :none || policy == :standard
87
+
88
+ ImageGenerator::ContentPolicy.validate!(prompt, policy)
89
+ end
90
+
91
+ def edit_images
92
+ count = resolve_count
93
+
94
+ Array.new(count) do
95
+ edit_single_image
96
+ end
97
+ end
98
+
99
+ def edit_single_image
100
+ # Use RubyLLM's edit endpoint if available
101
+ if RubyLLM.respond_to?(:edit_image)
102
+ RubyLLM.edit_image(
103
+ image: image,
104
+ mask: mask,
105
+ prompt: prompt,
106
+ model: resolve_model,
107
+ size: resolve_size,
108
+ **build_edit_options
109
+ )
110
+ else
111
+ # Fallback: Some providers may use paint with mask support
112
+ RubyLLM.paint(
113
+ prompt,
114
+ model: resolve_model,
115
+ size: resolve_size,
116
+ image: image,
117
+ mask: mask,
118
+ **build_edit_options
119
+ )
120
+ end
121
+ end
122
+
123
+ def build_edit_options
124
+ opts = {}
125
+ opts[:assume_model_exists] = true if options[:assume_model_exists]
126
+ opts
127
+ end
128
+
129
+ def build_result(images:, started_at:, completed_at:)
130
+ ImageEditResult.new(
131
+ images: images,
132
+ source_image: image,
133
+ mask: mask,
134
+ prompt: prompt,
135
+ model_id: resolve_model,
136
+ size: resolve_size,
137
+ started_at: started_at,
138
+ completed_at: completed_at,
139
+ tenant_id: @tenant_id,
140
+ editor_class: self.class.name
141
+ )
142
+ end
143
+
144
+ def build_error_result(error, started_at)
145
+ ImageEditResult.new(
146
+ images: [],
147
+ source_image: image,
148
+ mask: mask,
149
+ prompt: prompt,
150
+ model_id: resolve_model,
151
+ size: resolve_size,
152
+ started_at: started_at,
153
+ completed_at: Time.current,
154
+ tenant_id: @tenant_id,
155
+ editor_class: self.class.name,
156
+ error_class: error.class.name,
157
+ error_message: error.message
158
+ )
159
+ end
160
+
161
+ # Resolution methods
162
+
163
+ def resolve_size
164
+ options[:size] || self.class.size
165
+ end
166
+
167
+ def resolve_count
168
+ options[:count] || 1
169
+ end
170
+
171
+ # Cache key components
172
+ def cache_key_components
173
+ [
174
+ "image_editor",
175
+ self.class.name,
176
+ self.class.version,
177
+ resolve_model,
178
+ resolve_size,
179
+ Digest::SHA256.hexdigest(prompt),
180
+ Digest::SHA256.hexdigest(image_digest(image)),
181
+ Digest::SHA256.hexdigest(image_digest(mask))
182
+ ]
183
+ end
184
+
185
+ def image_digest(file)
186
+ if file.is_a?(String) && File.exist?(file)
187
+ File.read(file)
188
+ elsif file.respond_to?(:read)
189
+ content = file.read
190
+ file.rewind if file.respond_to?(:rewind)
191
+ content
192
+ else
193
+ file.to_s
194
+ end
195
+ end
196
+
197
+ def build_execution_metadata(result)
198
+ {
199
+ count: result.count,
200
+ size: result.size,
201
+ prompt_length: prompt.length
202
+ }
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "editor/dsl"
4
+ require_relative "editor/execution"
5
+
6
+ module RubyLLM
7
+ module Agents
8
+ # Image editor for inpainting and image editing
9
+ #
10
+ # Allows editing specific regions of an image using a mask.
11
+ # The mask indicates which parts of the image should be modified.
12
+ # White areas in the mask are edited, black areas are preserved.
13
+ #
14
+ # @example Basic usage
15
+ # result = RubyLLM::Agents::ImageEditor.call(
16
+ # image: "path/to/image.png",
17
+ # mask: "path/to/mask.png",
18
+ # prompt: "Replace with a red car"
19
+ # )
20
+ # result.url # => "https://..."
21
+ #
22
+ # @example Custom editor class
23
+ # class ProductEditor < RubyLLM::Agents::ImageEditor
24
+ # model "gpt-image-1"
25
+ # size "1024x1024"
26
+ #
27
+ # description "Edits product images"
28
+ # end
29
+ #
30
+ # result = ProductEditor.call(
31
+ # image: product_photo,
32
+ # mask: background_mask,
33
+ # prompt: "Professional studio background"
34
+ # )
35
+ #
36
+ class ImageEditor
37
+ extend DSL
38
+ include Execution
39
+
40
+ class << self
41
+ # Execute image editing with the given source image, mask, and prompt
42
+ #
43
+ # @param image [String, IO] Path, URL, or IO object of the source image
44
+ # @param mask [String, IO] Path, URL, or IO object of the mask image
45
+ # @param prompt [String] Description of the desired edit
46
+ # @param options [Hash] Additional options (model, size, etc.)
47
+ # @return [ImageEditResult] The result containing edited image
48
+ def call(image:, mask:, prompt:, **options)
49
+ new(image: image, mask: mask, prompt: prompt, **options).call
50
+ end
51
+
52
+ # Ensure subclasses inherit DSL settings
53
+ def inherited(subclass)
54
+ super
55
+ subclass.instance_variable_set(:@model, @model)
56
+ subclass.instance_variable_set(:@size, @size)
57
+ subclass.instance_variable_set(:@version, @version)
58
+ subclass.instance_variable_set(:@description, @description)
59
+ subclass.instance_variable_set(:@cache_ttl, @cache_ttl)
60
+ subclass.instance_variable_set(:@content_policy, @content_policy)
61
+ end
62
+ end
63
+
64
+ attr_reader :image, :mask, :prompt, :options, :tenant_id
65
+
66
+ # Initialize a new image editor instance
67
+ #
68
+ # @param image [String, IO] Source image (path, URL, or IO object)
69
+ # @param mask [String, IO] Mask image (path, URL, or IO object)
70
+ # @param prompt [String] Description of the desired edit
71
+ # @param options [Hash] Additional options
72
+ # @option options [String] :model Model to use
73
+ # @option options [String] :size Output image size
74
+ # @option options [Integer] :count Number of edits to generate
75
+ # @option options [Object] :tenant Tenant for multi-tenancy
76
+ def initialize(image:, mask:, prompt:, **options)
77
+ @image = image
78
+ @mask = mask
79
+ @prompt = prompt
80
+ @options = options
81
+ @tenant_id = nil
82
+ end
83
+
84
+ # Execute the image edit
85
+ #
86
+ # @return [ImageEditResult] The result containing edited image
87
+ def call
88
+ execute
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ class ImageGenerator
6
+ # ActiveStorage integration for image generators
7
+ #
8
+ # Provides convenience methods for generating images and directly
9
+ # attaching them to ActiveStorage attachments.
10
+ #
11
+ # @example Attaching to a model
12
+ # class Product < ApplicationRecord
13
+ # has_one_attached :hero_image
14
+ # end
15
+ #
16
+ # class ProductImageGenerator < RubyLLM::Agents::ImageGenerator
17
+ # include RubyLLM::Agents::ImageGenerator::ActiveStorageSupport
18
+ #
19
+ # model "gpt-image-1"
20
+ # size "1024x1024"
21
+ # end
22
+ #
23
+ # product = Product.find(1)
24
+ # result = ProductImageGenerator.generate_and_attach(
25
+ # prompt: "Professional product photo of a red sneaker",
26
+ # record: product,
27
+ # attachment_name: :hero_image
28
+ # )
29
+ #
30
+ module ActiveStorageSupport
31
+ extend ActiveSupport::Concern
32
+
33
+ class_methods do
34
+ # Generate an image and attach it to a record
35
+ #
36
+ # @param prompt [String] The generation prompt
37
+ # @param record [ActiveRecord::Base] The record to attach to
38
+ # @param attachment_name [Symbol] Name of the attachment (e.g., :avatar)
39
+ # @param options [Hash] Additional options for generation
40
+ # @return [ImageGenerationResult] The generation result
41
+ def generate_and_attach(prompt:, record:, attachment_name:, **options)
42
+ result = call(prompt: prompt, **options)
43
+
44
+ return result unless result.success?
45
+
46
+ attach_result_to_record(result, record, attachment_name, options)
47
+
48
+ result
49
+ end
50
+
51
+ # Generate multiple images and attach them to a has_many_attached
52
+ #
53
+ # @param prompt [String] The generation prompt
54
+ # @param record [ActiveRecord::Base] The record to attach to
55
+ # @param attachment_name [Symbol] Name of the attachment (e.g., :photos)
56
+ # @param count [Integer] Number of images to generate
57
+ # @param options [Hash] Additional options for generation
58
+ # @return [ImageGenerationResult] The generation result
59
+ def generate_and_attach_multiple(prompt:, record:, attachment_name:, count: 1, **options)
60
+ result = call(prompt: prompt, count: count, **options)
61
+
62
+ return result unless result.success?
63
+
64
+ attachment = record.public_send(attachment_name)
65
+
66
+ result.images.each_with_index do |image, idx|
67
+ attach_image_to_collection(image, attachment, idx, result)
68
+ end
69
+
70
+ result
71
+ end
72
+
73
+ private
74
+
75
+ def attach_result_to_record(result, record, attachment_name, options)
76
+ attachment = record.public_send(attachment_name)
77
+ filename = options[:filename] || generate_filename
78
+
79
+ if result.base64?
80
+ attachment.attach(
81
+ io: StringIO.new(result.to_blob),
82
+ filename: filename,
83
+ content_type: result.mime_type || "image/png"
84
+ )
85
+ else
86
+ attach_from_url(attachment, result.url, filename)
87
+ end
88
+ end
89
+
90
+ def attach_image_to_collection(image, attachment, index, result)
91
+ filename = generate_filename(index)
92
+
93
+ if image.respond_to?(:data) && image.data
94
+ attachment.attach(
95
+ io: StringIO.new(image.to_blob),
96
+ filename: filename,
97
+ content_type: image.mime_type || "image/png"
98
+ )
99
+ elsif image.respond_to?(:url) && image.url
100
+ attach_from_url(attachment, image.url, filename)
101
+ end
102
+ end
103
+
104
+ def attach_from_url(attachment, url, filename)
105
+ require "open-uri"
106
+
107
+ downloaded = URI.parse(url).open
108
+ attachment.attach(
109
+ io: downloaded,
110
+ filename: filename,
111
+ content_type: "image/png"
112
+ )
113
+ end
114
+
115
+ def generate_filename(index = nil)
116
+ timestamp = Time.current.to_i
117
+ if index
118
+ "generated_#{timestamp}_#{index + 1}.png"
119
+ else
120
+ "generated_#{timestamp}.png"
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ class ImageGenerator
6
+ # Content policy enforcement for image generation prompts
7
+ #
8
+ # Validates prompts against configurable policy levels to prevent
9
+ # generation of inappropriate content.
10
+ #
11
+ # @example Using content policy in a generator
12
+ # class SafeImageGenerator < RubyLLM::Agents::ImageGenerator
13
+ # content_policy :strict
14
+ # end
15
+ #
16
+ # @example Manual validation
17
+ # ContentPolicy.validate!("A beautiful sunset", :moderate)
18
+ # # => nil (passes)
19
+ #
20
+ # ContentPolicy.validate!("Violent scene", :strict)
21
+ # # => raises ContentPolicyViolation
22
+ #
23
+ module ContentPolicy
24
+ # Blocked patterns by policy level
25
+ #
26
+ # :strict - Blocks violence, nudity, hate, weapons, drugs
27
+ # :moderate - Blocks explicit content, gore, hate speech
28
+ # :standard - No blocking (relies on model's built-in filters)
29
+ # :none - No validation at all
30
+ #
31
+ BLOCKED_PATTERNS = {
32
+ strict: [
33
+ /\b(violence|violent|gore|blood|death|kill|murder)\b/i,
34
+ /\b(nude|naked|nsfw|explicit|sexual|porn)\b/i,
35
+ /\b(hate|racist|discrimination|slur)\b/i,
36
+ /\b(weapon|gun|knife|bomb|explosive)\b/i,
37
+ /\b(drug|cocaine|heroin|meth)\b/i
38
+ ],
39
+ moderate: [
40
+ /\b(nude|naked|nsfw|explicit|sexual|porn)\b/i,
41
+ /\b(gore|graphic.?violence)\b/i,
42
+ /\b(hate.?speech|slur)\b/i
43
+ ],
44
+ standard: [],
45
+ none: []
46
+ }.freeze
47
+
48
+ class << self
49
+ # Validate a prompt against a policy level
50
+ #
51
+ # @param prompt [String] The prompt to validate
52
+ # @param level [Symbol] Policy level (:none, :standard, :moderate, :strict)
53
+ # @raise [ContentPolicyViolation] If prompt violates the policy
54
+ def validate!(prompt, level)
55
+ return if level == :none || level.nil?
56
+
57
+ patterns = BLOCKED_PATTERNS[level.to_sym] || BLOCKED_PATTERNS[:standard]
58
+
59
+ patterns.each do |pattern|
60
+ if prompt.match?(pattern)
61
+ raise ContentPolicyViolation,
62
+ "Prompt contains content blocked by #{level} policy"
63
+ end
64
+ end
65
+ end
66
+
67
+ # Check if a prompt passes the policy (non-raising version)
68
+ #
69
+ # @param prompt [String] The prompt to check
70
+ # @param level [Symbol] Policy level
71
+ # @return [Boolean] true if prompt passes
72
+ def valid?(prompt, level)
73
+ validate!(prompt, level)
74
+ true
75
+ rescue ContentPolicyViolation
76
+ false
77
+ end
78
+
79
+ # Get the matched pattern for a violation (for debugging)
80
+ #
81
+ # @param prompt [String] The prompt to check
82
+ # @param level [Symbol] Policy level
83
+ # @return [Regexp, nil] The matched pattern or nil
84
+ def matched_pattern(prompt, level)
85
+ patterns = BLOCKED_PATTERNS[level.to_sym] || []
86
+ patterns.find { |pattern| prompt.match?(pattern) }
87
+ end
88
+ end
89
+ end
90
+
91
+ # Exception raised when a prompt violates content policy
92
+ class ContentPolicyViolation < StandardError; end
93
+ end
94
+ end
95
+ end