ruby_llm-agents 0.4.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 (208) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +225 -34
  3. data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
  4. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +214 -0
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
  6. data/app/controllers/ruby_llm/agents/{settings_controller.rb → system_config_controller.rb} +3 -3
  7. data/app/controllers/ruby_llm/agents/tenants_controller.rb +109 -0
  8. data/app/controllers/ruby_llm/agents/workflows_controller.rb +355 -0
  9. data/app/helpers/ruby_llm/agents/application_helper.rb +25 -0
  10. data/app/models/ruby_llm/agents/api_configuration.rb +386 -0
  11. data/app/models/ruby_llm/agents/execution.rb +3 -0
  12. data/app/models/ruby_llm/agents/tenant_budget.rb +112 -14
  13. data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
  14. data/app/views/layouts/ruby_llm/agents/application.html.erb +5 -30
  15. data/app/views/ruby_llm/agents/agents/_agent.html.erb +13 -1
  16. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +235 -0
  17. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +70 -0
  18. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +152 -0
  19. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +63 -0
  20. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +108 -0
  21. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +91 -0
  22. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +1 -1
  23. data/app/views/ruby_llm/agents/agents/index.html.erb +74 -9
  24. data/app/views/ruby_llm/agents/agents/show.html.erb +18 -378
  25. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +34 -0
  26. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +288 -0
  27. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +95 -0
  28. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +97 -0
  29. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +211 -0
  30. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +179 -0
  31. data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +1 -1
  32. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +269 -15
  33. data/app/views/ruby_llm/agents/executions/show.html.erb +98 -0
  34. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -0
  35. data/app/views/ruby_llm/agents/{settings → system_config}/show.html.erb +1 -1
  36. data/app/views/ruby_llm/agents/tenants/_form.html.erb +150 -0
  37. data/app/views/ruby_llm/agents/tenants/edit.html.erb +13 -0
  38. data/app/views/ruby_llm/agents/tenants/index.html.erb +129 -0
  39. data/app/views/ruby_llm/agents/tenants/show.html.erb +374 -0
  40. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +236 -0
  41. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +76 -0
  42. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +74 -0
  43. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +108 -0
  44. data/app/views/ruby_llm/agents/workflows/show.html.erb +442 -0
  45. data/config/routes.rb +13 -1
  46. data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
  47. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +100 -0
  48. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +110 -0
  49. data/lib/generators/ruby_llm_agents/embedder_generator.rb +107 -0
  50. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +115 -0
  51. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +108 -0
  52. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +116 -0
  53. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +178 -0
  54. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +109 -0
  55. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +103 -0
  56. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +102 -0
  57. data/lib/generators/ruby_llm_agents/install_generator.rb +76 -4
  58. data/lib/generators/ruby_llm_agents/restructure_generator.rb +292 -0
  59. data/lib/generators/ruby_llm_agents/speaker_generator.rb +121 -0
  60. data/lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt +8 -0
  61. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +99 -84
  62. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +42 -40
  63. data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +26 -0
  64. data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +50 -0
  65. data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +26 -0
  66. data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +20 -0
  67. data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +38 -0
  68. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +139 -0
  69. data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +21 -0
  70. data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +20 -0
  71. data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +20 -0
  72. data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +49 -0
  73. data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +53 -0
  74. data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +44 -0
  75. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +90 -0
  76. data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +41 -0
  77. data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +45 -0
  78. data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +35 -0
  79. data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +47 -0
  80. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +50 -0
  81. data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +44 -0
  82. data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +38 -0
  83. data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +33 -0
  84. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +228 -0
  85. data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +131 -0
  86. data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +255 -0
  87. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +120 -0
  88. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +102 -0
  89. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +282 -0
  90. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +228 -0
  91. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +120 -0
  92. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +110 -0
  93. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +120 -0
  94. data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +212 -0
  95. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +227 -0
  96. data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +251 -0
  97. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +300 -0
  98. data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +56 -0
  99. data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +51 -0
  100. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +107 -0
  101. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +152 -1
  102. data/lib/ruby_llm/agents/audio/speaker.rb +553 -0
  103. data/lib/ruby_llm/agents/audio/transcriber.rb +669 -0
  104. data/lib/ruby_llm/agents/base_agent.rb +675 -0
  105. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +181 -0
  106. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +274 -0
  107. data/lib/ruby_llm/agents/core/base.rb +135 -0
  108. data/lib/ruby_llm/agents/core/configuration.rb +981 -0
  109. data/lib/ruby_llm/agents/core/errors.rb +150 -0
  110. data/lib/ruby_llm/agents/{instrumentation.rb → core/instrumentation.rb} +93 -4
  111. data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -0
  112. data/lib/ruby_llm/agents/core/resolved_config.rb +348 -0
  113. data/lib/ruby_llm/agents/{version.rb → core/version.rb} +1 -1
  114. data/lib/ruby_llm/agents/dsl/base.rb +110 -0
  115. data/lib/ruby_llm/agents/dsl/caching.rb +142 -0
  116. data/lib/ruby_llm/agents/dsl/reliability.rb +307 -0
  117. data/lib/ruby_llm/agents/dsl.rb +41 -0
  118. data/lib/ruby_llm/agents/image/analyzer/dsl.rb +130 -0
  119. data/lib/ruby_llm/agents/image/analyzer/execution.rb +402 -0
  120. data/lib/ruby_llm/agents/image/analyzer.rb +90 -0
  121. data/lib/ruby_llm/agents/image/background_remover/dsl.rb +154 -0
  122. data/lib/ruby_llm/agents/image/background_remover/execution.rb +240 -0
  123. data/lib/ruby_llm/agents/image/background_remover.rb +89 -0
  124. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +91 -0
  125. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +165 -0
  126. data/lib/ruby_llm/agents/image/editor/dsl.rb +56 -0
  127. data/lib/ruby_llm/agents/image/editor/execution.rb +207 -0
  128. data/lib/ruby_llm/agents/image/editor.rb +92 -0
  129. data/lib/ruby_llm/agents/image/generator/active_storage_support.rb +127 -0
  130. data/lib/ruby_llm/agents/image/generator/content_policy.rb +95 -0
  131. data/lib/ruby_llm/agents/image/generator/pricing.rb +353 -0
  132. data/lib/ruby_llm/agents/image/generator/templates.rb +124 -0
  133. data/lib/ruby_llm/agents/image/generator.rb +455 -0
  134. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +213 -0
  135. data/lib/ruby_llm/agents/image/pipeline/execution.rb +382 -0
  136. data/lib/ruby_llm/agents/image/pipeline.rb +97 -0
  137. data/lib/ruby_llm/agents/image/transformer/dsl.rb +148 -0
  138. data/lib/ruby_llm/agents/image/transformer/execution.rb +223 -0
  139. data/lib/ruby_llm/agents/image/transformer.rb +95 -0
  140. data/lib/ruby_llm/agents/image/upscaler/dsl.rb +83 -0
  141. data/lib/ruby_llm/agents/image/upscaler/execution.rb +219 -0
  142. data/lib/ruby_llm/agents/image/upscaler.rb +81 -0
  143. data/lib/ruby_llm/agents/image/variator/dsl.rb +62 -0
  144. data/lib/ruby_llm/agents/image/variator/execution.rb +189 -0
  145. data/lib/ruby_llm/agents/image/variator.rb +80 -0
  146. data/lib/ruby_llm/agents/{alert_manager.rb → infrastructure/alert_manager.rb} +17 -22
  147. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +145 -0
  148. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +149 -0
  149. data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +68 -0
  150. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +279 -0
  151. data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +275 -0
  152. data/lib/ruby_llm/agents/{execution_logger_job.rb → infrastructure/execution_logger_job.rb} +17 -1
  153. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/executor.rb +2 -1
  154. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/retry_strategy.rb +9 -3
  155. data/lib/ruby_llm/agents/{reliability.rb → infrastructure/reliability.rb} +11 -21
  156. data/lib/ruby_llm/agents/pipeline/builder.rb +215 -0
  157. data/lib/ruby_llm/agents/pipeline/context.rb +255 -0
  158. data/lib/ruby_llm/agents/pipeline/executor.rb +86 -0
  159. data/lib/ruby_llm/agents/pipeline/middleware/base.rb +124 -0
  160. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +95 -0
  161. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +171 -0
  162. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +415 -0
  163. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +276 -0
  164. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +196 -0
  165. data/lib/ruby_llm/agents/pipeline.rb +68 -0
  166. data/lib/ruby_llm/agents/{engine.rb → rails/engine.rb} +79 -10
  167. data/lib/ruby_llm/agents/results/background_removal_result.rb +286 -0
  168. data/lib/ruby_llm/agents/{result.rb → results/base.rb} +73 -1
  169. data/lib/ruby_llm/agents/results/embedding_result.rb +243 -0
  170. data/lib/ruby_llm/agents/results/image_analysis_result.rb +314 -0
  171. data/lib/ruby_llm/agents/results/image_edit_result.rb +250 -0
  172. data/lib/ruby_llm/agents/results/image_generation_result.rb +346 -0
  173. data/lib/ruby_llm/agents/results/image_pipeline_result.rb +399 -0
  174. data/lib/ruby_llm/agents/results/image_transform_result.rb +251 -0
  175. data/lib/ruby_llm/agents/results/image_upscale_result.rb +255 -0
  176. data/lib/ruby_llm/agents/results/image_variation_result.rb +237 -0
  177. data/lib/ruby_llm/agents/results/moderation_result.rb +158 -0
  178. data/lib/ruby_llm/agents/results/speech_result.rb +338 -0
  179. data/lib/ruby_llm/agents/results/transcription_result.rb +408 -0
  180. data/lib/ruby_llm/agents/text/embedder.rb +444 -0
  181. data/lib/ruby_llm/agents/text/moderator.rb +237 -0
  182. data/lib/ruby_llm/agents/workflow/async.rb +220 -0
  183. data/lib/ruby_llm/agents/workflow/async_executor.rb +156 -0
  184. data/lib/ruby_llm/agents/{workflow.rb → workflow/orchestrator.rb} +6 -5
  185. data/lib/ruby_llm/agents/workflow/parallel.rb +34 -17
  186. data/lib/ruby_llm/agents/workflow/thread_pool.rb +185 -0
  187. data/lib/ruby_llm/agents.rb +86 -20
  188. metadata +189 -35
  189. data/lib/ruby_llm/agents/base/caching.rb +0 -40
  190. data/lib/ruby_llm/agents/base/cost_calculation.rb +0 -105
  191. data/lib/ruby_llm/agents/base/dsl.rb +0 -324
  192. data/lib/ruby_llm/agents/base/execution.rb +0 -283
  193. data/lib/ruby_llm/agents/base/reliability_dsl.rb +0 -82
  194. data/lib/ruby_llm/agents/base/reliability_execution.rb +0 -136
  195. data/lib/ruby_llm/agents/base/response_building.rb +0 -86
  196. data/lib/ruby_llm/agents/base/tool_tracking.rb +0 -57
  197. data/lib/ruby_llm/agents/base.rb +0 -209
  198. data/lib/ruby_llm/agents/budget_tracker.rb +0 -471
  199. data/lib/ruby_llm/agents/configuration.rb +0 -357
  200. /data/lib/ruby_llm/agents/{deprecations.rb → core/deprecations.rb} +0 -0
  201. /data/lib/ruby_llm/agents/{inflections.rb → core/inflections.rb} +0 -0
  202. /data/lib/ruby_llm/agents/{attempt_tracker.rb → infrastructure/attempt_tracker.rb} +0 -0
  203. /data/lib/ruby_llm/agents/{cache_helper.rb → infrastructure/cache_helper.rb} +0 -0
  204. /data/lib/ruby_llm/agents/{circuit_breaker.rb → infrastructure/circuit_breaker.rb} +0 -0
  205. /data/lib/ruby_llm/agents/{redactor.rb → infrastructure/redactor.rb} +0 -0
  206. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/breaker_manager.rb +0 -0
  207. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/execution_constraints.rb +0 -0
  208. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/fallback_routing.rb +0 -0
@@ -0,0 +1,223 @@
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 ImageTransformer
9
+ # Execution logic for image transformers
10
+ #
11
+ # Handles image validation, content policy checks, budget tracking,
12
+ # caching, image transformation, and result building.
13
+ #
14
+ module Execution
15
+ include Concerns::ImageOperationExecution
16
+
17
+ # Execute the image transformation pipeline
18
+ #
19
+ # @return [ImageTransformResult] The result containing transformed 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(ImageTransformResult) if cache_enabled?
30
+ return cached if cached
31
+
32
+ # Transform image(s)
33
+ transformed_images = transform_images
34
+
35
+ # Build result
36
+ result = build_result(
37
+ images: transformed_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_transform"
58
+ end
59
+
60
+ def validate_inputs!
61
+ raise ArgumentError, "Image cannot be blank" if image.nil?
62
+ raise ArgumentError, "Prompt cannot be blank" if prompt.nil? || prompt.strip.empty?
63
+
64
+ # Validate image exists if it's a path
65
+ if image.is_a?(String) && !image.start_with?("http")
66
+ unless File.exist?(image)
67
+ raise ArgumentError, "Image file does not exist: #{image}"
68
+ end
69
+ end
70
+
71
+ # Validate prompt length
72
+ max_length = config.max_image_prompt_length || 4000
73
+ if prompt.length > max_length
74
+ raise ArgumentError, "Prompt exceeds maximum length of #{max_length} characters"
75
+ end
76
+ end
77
+
78
+ def validate_content_policy!
79
+ policy = self.class.content_policy
80
+ return if policy == :none || policy == :standard
81
+
82
+ ImageGenerator::ContentPolicy.validate!(prompt, policy)
83
+ end
84
+
85
+ def transform_images
86
+ count = resolve_count
87
+
88
+ Array.new(count) do
89
+ transform_single_image
90
+ end
91
+ end
92
+
93
+ def transform_single_image
94
+ final_prompt = apply_template(prompt)
95
+
96
+ # Use img2img/transformation API
97
+ RubyLLM.paint(
98
+ final_prompt,
99
+ model: resolve_model,
100
+ size: resolve_size,
101
+ image: image,
102
+ strength: resolve_strength,
103
+ **build_transform_options
104
+ )
105
+ end
106
+
107
+ def apply_template(text)
108
+ template = self.class.template_string
109
+ return text unless template
110
+
111
+ template.gsub("{prompt}", text)
112
+ end
113
+
114
+ def build_transform_options
115
+ opts = {}
116
+ opts[:negative_prompt] = resolve_negative_prompt if resolve_negative_prompt
117
+ opts[:guidance_scale] = resolve_guidance_scale if resolve_guidance_scale
118
+ opts[:steps] = resolve_steps if resolve_steps
119
+ opts[:preserve_composition] = resolve_preserve_composition
120
+ opts[:assume_model_exists] = true if options[:assume_model_exists]
121
+ opts
122
+ end
123
+
124
+ def build_result(images:, started_at:, completed_at:)
125
+ ImageTransformResult.new(
126
+ images: images,
127
+ source_image: image,
128
+ prompt: prompt,
129
+ model_id: resolve_model,
130
+ size: resolve_size,
131
+ strength: resolve_strength,
132
+ started_at: started_at,
133
+ completed_at: completed_at,
134
+ tenant_id: @tenant_id,
135
+ transformer_class: self.class.name
136
+ )
137
+ end
138
+
139
+ def build_error_result(error, started_at)
140
+ ImageTransformResult.new(
141
+ images: [],
142
+ source_image: image,
143
+ prompt: prompt,
144
+ model_id: resolve_model,
145
+ size: resolve_size,
146
+ strength: resolve_strength,
147
+ started_at: started_at,
148
+ completed_at: Time.current,
149
+ tenant_id: @tenant_id,
150
+ transformer_class: self.class.name,
151
+ error_class: error.class.name,
152
+ error_message: error.message
153
+ )
154
+ end
155
+
156
+ # Resolution methods
157
+
158
+ def resolve_size
159
+ options[:size] || self.class.size
160
+ end
161
+
162
+ def resolve_strength
163
+ options[:strength] || self.class.strength
164
+ end
165
+
166
+ def resolve_preserve_composition
167
+ options.fetch(:preserve_composition, self.class.preserve_composition)
168
+ end
169
+
170
+ def resolve_negative_prompt
171
+ options[:negative_prompt] || self.class.negative_prompt
172
+ end
173
+
174
+ def resolve_guidance_scale
175
+ options[:guidance_scale] || self.class.guidance_scale
176
+ end
177
+
178
+ def resolve_steps
179
+ options[:steps] || self.class.steps
180
+ end
181
+
182
+ def resolve_count
183
+ options[:count] || 1
184
+ end
185
+
186
+ # Cache key components
187
+ def cache_key_components
188
+ [
189
+ "image_transformer",
190
+ self.class.name,
191
+ self.class.version,
192
+ resolve_model,
193
+ resolve_size,
194
+ resolve_strength.to_s,
195
+ Digest::SHA256.hexdigest(prompt),
196
+ Digest::SHA256.hexdigest(image_digest)
197
+ ]
198
+ end
199
+
200
+ def image_digest
201
+ if image.is_a?(String) && File.exist?(image)
202
+ File.read(image)
203
+ elsif image.respond_to?(:read)
204
+ content = image.read
205
+ image.rewind if image.respond_to?(:rewind)
206
+ content
207
+ else
208
+ image.to_s
209
+ end
210
+ end
211
+
212
+ def build_execution_metadata(result)
213
+ {
214
+ count: result.count,
215
+ size: result.size,
216
+ strength: result.strength,
217
+ prompt_length: prompt.length
218
+ }
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "transformer/dsl"
4
+ require_relative "transformer/execution"
5
+
6
+ module RubyLLM
7
+ module Agents
8
+ # Image transformer for style transfer and image-to-image generation
9
+ #
10
+ # Transforms an existing image based on a text prompt while
11
+ # maintaining the overall structure. The strength parameter
12
+ # controls how much the image is transformed.
13
+ #
14
+ # @example Basic usage
15
+ # result = RubyLLM::Agents::ImageTransformer.call(
16
+ # image: "path/to/photo.jpg",
17
+ # prompt: "Convert to watercolor painting style"
18
+ # )
19
+ # result.url # => "https://..."
20
+ #
21
+ # @example Custom transformer class
22
+ # class AnimeTransformer < RubyLLM::Agents::ImageTransformer
23
+ # model "sdxl"
24
+ # strength 0.8
25
+ # template "anime style, studio ghibli, {prompt}"
26
+ #
27
+ # description "Transforms photos into anime style"
28
+ # end
29
+ #
30
+ # result = AnimeTransformer.call(
31
+ # image: user_photo,
32
+ # prompt: "portrait of a person"
33
+ # )
34
+ #
35
+ class ImageTransformer
36
+ extend DSL
37
+ include Execution
38
+
39
+ class << self
40
+ # Execute image transformation
41
+ #
42
+ # @param image [String, IO] Path, URL, or IO object of the source image
43
+ # @param prompt [String] Description of the desired transformation
44
+ # @param options [Hash] Additional options (model, strength, size, etc.)
45
+ # @return [ImageTransformResult] The result containing transformed image
46
+ def call(image:, prompt:, **options)
47
+ new(image: image, prompt: prompt, **options).call
48
+ end
49
+
50
+ # Ensure subclasses inherit DSL settings
51
+ def inherited(subclass)
52
+ super
53
+ subclass.instance_variable_set(:@model, @model)
54
+ subclass.instance_variable_set(:@size, @size)
55
+ subclass.instance_variable_set(:@strength, @strength)
56
+ subclass.instance_variable_set(:@preserve_composition, @preserve_composition)
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
+ subclass.instance_variable_set(:@template_string, @template_string)
62
+ subclass.instance_variable_set(:@negative_prompt, @negative_prompt)
63
+ subclass.instance_variable_set(:@guidance_scale, @guidance_scale)
64
+ subclass.instance_variable_set(:@steps, @steps)
65
+ end
66
+ end
67
+
68
+ attr_reader :image, :prompt, :options, :tenant_id
69
+
70
+ # Initialize a new image transformer instance
71
+ #
72
+ # @param image [String, IO] Source image (path, URL, or IO object)
73
+ # @param prompt [String] Description of the desired transformation
74
+ # @param options [Hash] Additional options
75
+ # @option options [String] :model Model to use
76
+ # @option options [String] :size Output image size
77
+ # @option options [Float] :strength Transformation strength (0.0-1.0)
78
+ # @option options [Integer] :count Number of transformations to generate
79
+ # @option options [Object] :tenant Tenant for multi-tenancy
80
+ def initialize(image:, prompt:, **options)
81
+ @image = image
82
+ @prompt = prompt
83
+ @options = options
84
+ @tenant_id = nil
85
+ end
86
+
87
+ # Execute the image transformation
88
+ #
89
+ # @return [ImageTransformResult] The result containing transformed image
90
+ def call
91
+ execute
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../concerns/image_operation_dsl"
4
+
5
+ module RubyLLM
6
+ module Agents
7
+ class ImageUpscaler
8
+ # DSL for configuring image upscalers
9
+ #
10
+ # Provides class-level methods to configure model, scale factor,
11
+ # and other upscaling parameters.
12
+ #
13
+ # @example
14
+ # class PhotoUpscaler < RubyLLM::Agents::ImageUpscaler
15
+ # model "real-esrgan"
16
+ # scale 4
17
+ # face_enhance true
18
+ # end
19
+ #
20
+ module DSL
21
+ include Concerns::ImageOperationDSL
22
+
23
+ VALID_SCALES = [2, 4, 8].freeze
24
+
25
+ # Set or get the upscale factor
26
+ #
27
+ # @param value [Integer, nil] Scale factor (2, 4, or 8)
28
+ # @return [Integer] The scale factor
29
+ def scale(value = nil)
30
+ if value
31
+ unless VALID_SCALES.include?(value)
32
+ raise ArgumentError, "Scale must be one of: #{VALID_SCALES.join(', ')}"
33
+ end
34
+ @scale = value
35
+ else
36
+ @scale || inherited_or_default(:scale, 4)
37
+ end
38
+ end
39
+
40
+ # Set or get face enhancement
41
+ #
42
+ # When enabled, applies additional enhancement to detected faces.
43
+ # Uses models like GFPGAN for face restoration.
44
+ #
45
+ # @param value [Boolean, nil] Enable face enhancement
46
+ # @return [Boolean] Whether face enhancement is enabled
47
+ def face_enhance(value = nil)
48
+ if value.nil?
49
+ result = @face_enhance
50
+ result = inherited_or_default(:face_enhance, false) if result.nil?
51
+ result
52
+ else
53
+ @face_enhance = value
54
+ end
55
+ end
56
+
57
+ # Set or get denoise strength
58
+ #
59
+ # Controls how much noise reduction is applied.
60
+ # Higher values remove more noise but may lose detail.
61
+ #
62
+ # @param value [Float, nil] Denoise strength (0.0-1.0)
63
+ # @return [Float] The denoise strength
64
+ def denoise_strength(value = nil)
65
+ if value
66
+ unless value.is_a?(Numeric) && value.between?(0.0, 1.0)
67
+ raise ArgumentError, "Denoise strength must be between 0.0 and 1.0"
68
+ end
69
+ @denoise_strength = value.to_f
70
+ else
71
+ @denoise_strength || inherited_or_default(:denoise_strength, 0.5)
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def default_model
78
+ config.default_upscaler_model || "real-esrgan"
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,219 @@
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 ImageUpscaler
9
+ # Execution logic for image upscalers
10
+ #
11
+ # Handles image validation, budget tracking, caching,
12
+ # upscaling execution, and result building.
13
+ #
14
+ module Execution
15
+ include Concerns::ImageOperationExecution
16
+
17
+ # Execute the image upscaling pipeline
18
+ #
19
+ # @return [ImageUpscaleResult] The result containing upscaled image
20
+ def execute
21
+ started_at = Time.current
22
+
23
+ resolve_tenant_context!
24
+ check_budget! if budget_tracking_enabled?
25
+ validate_image!
26
+
27
+ # Check cache
28
+ cached = check_cache(ImageUpscaleResult) if cache_enabled?
29
+ return cached if cached
30
+
31
+ # Upscale image
32
+ upscaled_image = upscale_image
33
+
34
+ # Build result
35
+ result = build_result(
36
+ image: upscaled_image,
37
+ started_at: started_at,
38
+ completed_at: Time.current
39
+ )
40
+
41
+ # Cache result
42
+ write_cache(result) if cache_enabled?
43
+
44
+ # Track execution
45
+ record_execution(result) if execution_tracking_enabled?
46
+
47
+ result
48
+ rescue StandardError => e
49
+ record_failed_execution(e, started_at) if execution_tracking_enabled?
50
+ build_error_result(e, started_at)
51
+ end
52
+
53
+ private
54
+
55
+ def execution_type
56
+ "image_upscale"
57
+ end
58
+
59
+ def validate_image!
60
+ raise ArgumentError, "Image cannot be blank" if image.nil?
61
+
62
+ # Validate image exists if it's a path
63
+ if image.is_a?(String) && !image.start_with?("http")
64
+ unless File.exist?(image)
65
+ raise ArgumentError, "Image file does not exist: #{image}"
66
+ end
67
+ end
68
+ end
69
+
70
+ def upscale_image
71
+ # Use RubyLLM's upscale endpoint if available
72
+ if RubyLLM.respond_to?(:upscale_image)
73
+ RubyLLM.upscale_image(
74
+ image: image,
75
+ model: resolve_model,
76
+ scale: resolve_scale,
77
+ **build_upscale_options
78
+ )
79
+ else
80
+ # For models that support upscaling through custom endpoints
81
+ # This would typically be handled by a Replicate or custom provider
82
+ upscale_via_provider
83
+ end
84
+ end
85
+
86
+ def upscale_via_provider
87
+ # Attempt to use the model through RubyLLM's paint with upscale mode
88
+ # This is a fallback for when direct upscale isn't supported
89
+ RubyLLM.paint(
90
+ "Upscale this image",
91
+ model: resolve_model,
92
+ image: image,
93
+ upscale_factor: resolve_scale,
94
+ face_enhance: resolve_face_enhance,
95
+ denoise_strength: resolve_denoise_strength,
96
+ **build_upscale_options
97
+ )
98
+ end
99
+
100
+ def build_upscale_options
101
+ opts = {}
102
+ opts[:face_enhance] = resolve_face_enhance if resolve_face_enhance
103
+ opts[:denoise_strength] = resolve_denoise_strength if resolve_denoise_strength
104
+ opts[:assume_model_exists] = true if options[:assume_model_exists]
105
+ opts
106
+ end
107
+
108
+ def build_result(image:, started_at:, completed_at:)
109
+ # Calculate output size based on scale
110
+ output_size = calculate_output_size
111
+
112
+ ImageUpscaleResult.new(
113
+ image: image,
114
+ source_image: self.image,
115
+ model_id: resolve_model,
116
+ scale: resolve_scale,
117
+ output_size: output_size,
118
+ face_enhance: resolve_face_enhance,
119
+ started_at: started_at,
120
+ completed_at: completed_at,
121
+ tenant_id: @tenant_id,
122
+ upscaler_class: self.class.name
123
+ )
124
+ end
125
+
126
+ def build_error_result(error, started_at)
127
+ ImageUpscaleResult.new(
128
+ image: nil,
129
+ source_image: self.image,
130
+ model_id: resolve_model,
131
+ scale: resolve_scale,
132
+ output_size: nil,
133
+ face_enhance: resolve_face_enhance,
134
+ started_at: started_at,
135
+ completed_at: Time.current,
136
+ tenant_id: @tenant_id,
137
+ upscaler_class: self.class.name,
138
+ error_class: error.class.name,
139
+ error_message: error.message
140
+ )
141
+ end
142
+
143
+ def calculate_output_size
144
+ input_size = get_input_image_size
145
+ return nil unless input_size
146
+
147
+ width, height = input_size
148
+ scale = resolve_scale
149
+ "#{width * scale}x#{height * scale}"
150
+ end
151
+
152
+ def get_input_image_size
153
+ return nil unless image.is_a?(String) && File.exist?(image)
154
+
155
+ # Try to get image dimensions
156
+ # This requires an image processing library like MiniMagick or ImageMagick
157
+ if defined?(MiniMagick)
158
+ img = MiniMagick::Image.open(image)
159
+ [img.width, img.height]
160
+ elsif defined?(Vips)
161
+ img = Vips::Image.new_from_file(image)
162
+ [img.width, img.height]
163
+ else
164
+ nil
165
+ end
166
+ rescue StandardError
167
+ nil
168
+ end
169
+
170
+ # Resolution methods
171
+
172
+ def resolve_scale
173
+ options[:scale] || self.class.scale
174
+ end
175
+
176
+ def resolve_face_enhance
177
+ options.fetch(:face_enhance, self.class.face_enhance)
178
+ end
179
+
180
+ def resolve_denoise_strength
181
+ options[:denoise_strength] || self.class.denoise_strength
182
+ end
183
+
184
+ # Cache key components
185
+ def cache_key_components
186
+ [
187
+ "image_upscaler",
188
+ self.class.name,
189
+ self.class.version,
190
+ resolve_model,
191
+ resolve_scale.to_s,
192
+ resolve_face_enhance.to_s,
193
+ Digest::SHA256.hexdigest(image_digest)
194
+ ]
195
+ end
196
+
197
+ def image_digest
198
+ if image.is_a?(String) && File.exist?(image)
199
+ File.read(image)
200
+ elsif image.respond_to?(:read)
201
+ content = image.read
202
+ image.rewind if image.respond_to?(:rewind)
203
+ content
204
+ else
205
+ image.to_s
206
+ end
207
+ end
208
+
209
+ def build_execution_metadata(result)
210
+ {
211
+ scale: result.scale,
212
+ output_size: result.output_size,
213
+ face_enhance: result.face_enhance
214
+ }
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "upscaler/dsl"
4
+ require_relative "upscaler/execution"
5
+
6
+ module RubyLLM
7
+ module Agents
8
+ # Image upscaler for resolution enhancement
9
+ #
10
+ # Increases the resolution of images using AI upscaling models.
11
+ # Supports 2x, 4x, and 8x upscaling with optional face enhancement.
12
+ #
13
+ # @example Basic usage
14
+ # result = RubyLLM::Agents::ImageUpscaler.call(image: "path/to/low_res.jpg")
15
+ # result.url # => "https://..." (high resolution version)
16
+ #
17
+ # @example Custom upscaler class
18
+ # class PhotoUpscaler < RubyLLM::Agents::ImageUpscaler
19
+ # model "real-esrgan"
20
+ # scale 4
21
+ # face_enhance true
22
+ #
23
+ # description "Upscales photos with face enhancement"
24
+ # end
25
+ #
26
+ # result = PhotoUpscaler.call(image: portrait_photo)
27
+ # result.size # => "4096x4096" (if input was 1024x1024)
28
+ #
29
+ class ImageUpscaler
30
+ extend DSL
31
+ include Execution
32
+
33
+ class << self
34
+ # Execute image upscaling
35
+ #
36
+ # @param image [String, IO] Path, URL, or IO object of the source image
37
+ # @param options [Hash] Additional options (model, scale, etc.)
38
+ # @return [ImageUpscaleResult] The result containing upscaled image
39
+ def call(image:, **options)
40
+ new(image: image, **options).call
41
+ end
42
+
43
+ # Ensure subclasses inherit DSL settings
44
+ def inherited(subclass)
45
+ super
46
+ subclass.instance_variable_set(:@model, @model)
47
+ subclass.instance_variable_set(:@scale, @scale)
48
+ subclass.instance_variable_set(:@face_enhance, @face_enhance)
49
+ subclass.instance_variable_set(:@denoise_strength, @denoise_strength)
50
+ subclass.instance_variable_set(:@version, @version)
51
+ subclass.instance_variable_set(:@description, @description)
52
+ subclass.instance_variable_set(:@cache_ttl, @cache_ttl)
53
+ end
54
+ end
55
+
56
+ attr_reader :image, :options, :tenant_id
57
+
58
+ # Initialize a new image upscaler instance
59
+ #
60
+ # @param image [String, IO] Source image (path, URL, or IO object)
61
+ # @param options [Hash] Additional options
62
+ # @option options [String] :model Model to use
63
+ # @option options [Integer] :scale Upscale factor (2, 4, or 8)
64
+ # @option options [Boolean] :face_enhance Enable face enhancement
65
+ # @option options [Float] :denoise_strength Denoising strength (0.0-1.0)
66
+ # @option options [Object] :tenant Tenant for multi-tenancy
67
+ def initialize(image:, **options)
68
+ @image = image
69
+ @options = options
70
+ @tenant_id = nil
71
+ end
72
+
73
+ # Execute the image upscaling
74
+ #
75
+ # @return [ImageUpscaleResult] The result containing upscaled image
76
+ def call
77
+ execute
78
+ end
79
+ end
80
+ end
81
+ end