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,382 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module RubyLLM
6
+ module Agents
7
+ class ImagePipeline
8
+ # Execution logic for image pipelines
9
+ #
10
+ # Handles step execution, context passing between steps,
11
+ # error handling, caching, and execution tracking.
12
+ #
13
+ module Execution
14
+ private
15
+
16
+ def execute
17
+ @started_at = Time.current
18
+
19
+ resolve_tenant_context!
20
+ check_budget! if budget_tracking_enabled?
21
+
22
+ # Check cache for deterministic pipelines
23
+ if cache_enabled?
24
+ cached = check_cache
25
+ return cached if cached
26
+ end
27
+
28
+ # Run before callbacks
29
+ run_callbacks(:before)
30
+
31
+ # Execute pipeline steps
32
+ current_image = options[:image]
33
+ @step_results = []
34
+
35
+ self.class.steps.each do |step_def|
36
+ # Check if step should run
37
+ next unless should_run_step?(step_def)
38
+
39
+ result = execute_step(step_def, current_image)
40
+ @step_results << { name: step_def[:name], type: step_def[:type], result: result }
41
+
42
+ # Update context with result
43
+ @context[step_def[:name]] = result
44
+
45
+ if result.error?
46
+ break if self.class.stop_on_error?
47
+ else
48
+ # Pass image to next step (except for analyze steps which don't produce images)
49
+ current_image = extract_image_from_result(result) unless step_def[:type] == :analyzer
50
+ end
51
+ end
52
+
53
+ # Build result
54
+ result = build_result
55
+
56
+ # Run after callbacks
57
+ run_callbacks(:after, result)
58
+
59
+ # Cache successful results
60
+ write_cache(result) if cache_enabled? && result.success?
61
+
62
+ # Track execution
63
+ record_execution(result) if execution_tracking_enabled?
64
+
65
+ result
66
+ rescue StandardError => e
67
+ record_failed_execution(e) if execution_tracking_enabled?
68
+ build_error_result(e)
69
+ end
70
+
71
+ def should_run_step?(step_def)
72
+ config = step_def[:config]
73
+
74
+ # Check :if condition
75
+ if config[:if]
76
+ return false unless evaluate_condition(config[:if])
77
+ end
78
+
79
+ # Check :unless condition
80
+ if config[:unless]
81
+ return false if evaluate_condition(config[:unless])
82
+ end
83
+
84
+ true
85
+ end
86
+
87
+ def evaluate_condition(condition)
88
+ case condition
89
+ when Proc
90
+ condition.call(@context)
91
+ when Symbol
92
+ respond_to?(condition, true) ? send(condition) : @context[condition]
93
+ else
94
+ condition
95
+ end
96
+ end
97
+
98
+ def execute_step(step_def, current_image)
99
+ step_type = step_def[:type]
100
+ step_config = step_def[:config]
101
+ step_class = step_config[step_type]
102
+
103
+ # Build options for the step (exclude meta options)
104
+ step_options = step_config.except(:if, :unless, step_type)
105
+ step_options[:tenant] = options[:tenant] if options[:tenant]
106
+
107
+ case step_type
108
+ when :generator
109
+ execute_generator(step_class, step_options)
110
+ when :variator
111
+ execute_variator(step_class, current_image, step_options)
112
+ when :editor
113
+ execute_editor(step_class, current_image, step_options)
114
+ when :transformer
115
+ execute_transformer(step_class, current_image, step_options)
116
+ when :upscaler
117
+ execute_upscaler(step_class, current_image, step_options)
118
+ when :analyzer
119
+ execute_analyzer(step_class, current_image, step_options)
120
+ when :remover
121
+ execute_remover(step_class, current_image, step_options)
122
+ else
123
+ raise ArgumentError, "Unknown step type: #{step_type}"
124
+ end
125
+ end
126
+
127
+ def execute_generator(step_class, step_options)
128
+ prompt = step_options.delete(:prompt) || options[:prompt]
129
+ raise ArgumentError, "Generator step requires a prompt" unless prompt
130
+
131
+ step_class.call(prompt: prompt, **step_options)
132
+ end
133
+
134
+ def execute_variator(step_class, image, step_options)
135
+ raise ArgumentError, "Variator step requires an input image" unless image
136
+
137
+ step_class.call(image: image, **step_options)
138
+ end
139
+
140
+ def execute_editor(step_class, image, step_options)
141
+ raise ArgumentError, "Editor step requires an input image" unless image
142
+
143
+ mask = step_options.delete(:mask) || options[:mask]
144
+ prompt = step_options.delete(:prompt) || options[:edit_prompt]
145
+ raise ArgumentError, "Editor step requires a mask and prompt" unless mask && prompt
146
+
147
+ step_class.call(image: image, mask: mask, prompt: prompt, **step_options)
148
+ end
149
+
150
+ def execute_transformer(step_class, image, step_options)
151
+ raise ArgumentError, "Transformer step requires an input image" unless image
152
+
153
+ prompt = step_options.delete(:prompt) || options[:transform_prompt]
154
+ raise ArgumentError, "Transformer step requires a prompt" unless prompt
155
+
156
+ step_class.call(image: image, prompt: prompt, **step_options)
157
+ end
158
+
159
+ def execute_upscaler(step_class, image, step_options)
160
+ raise ArgumentError, "Upscaler step requires an input image" unless image
161
+
162
+ step_class.call(image: image, **step_options)
163
+ end
164
+
165
+ def execute_analyzer(step_class, image, step_options)
166
+ raise ArgumentError, "Analyzer step requires an input image" unless image
167
+
168
+ step_class.call(image: image, **step_options)
169
+ end
170
+
171
+ def execute_remover(step_class, image, step_options)
172
+ raise ArgumentError, "Remover step requires an input image" unless image
173
+
174
+ step_class.call(image: image, **step_options)
175
+ end
176
+
177
+ def extract_image_from_result(result)
178
+ # Try common methods for getting image data
179
+ if result.respond_to?(:url) && result.url
180
+ result.url
181
+ elsif result.respond_to?(:data) && result.data
182
+ result.data
183
+ elsif result.respond_to?(:to_blob)
184
+ result.to_blob
185
+ else
186
+ result
187
+ end
188
+ end
189
+
190
+ def build_result
191
+ ImagePipelineResult.new(
192
+ step_results: @step_results,
193
+ started_at: @started_at,
194
+ completed_at: Time.current,
195
+ tenant_id: @tenant_id,
196
+ pipeline_class: self.class.name,
197
+ context: @context
198
+ )
199
+ end
200
+
201
+ def build_error_result(error)
202
+ ImagePipelineResult.new(
203
+ step_results: @step_results || [],
204
+ started_at: @started_at || Time.current,
205
+ completed_at: Time.current,
206
+ tenant_id: @tenant_id,
207
+ pipeline_class: self.class.name,
208
+ context: @context || {},
209
+ error_class: error.class.name,
210
+ error_message: error.message
211
+ )
212
+ end
213
+
214
+ # Tenant resolution
215
+
216
+ def resolve_tenant_context!
217
+ tenant = options[:tenant]
218
+ return unless tenant
219
+
220
+ @tenant_id = case tenant
221
+ when Hash then tenant[:id]
222
+ when Integer, String then tenant
223
+ else
224
+ tenant.try(:llm_tenant_id) || tenant.try(:id)
225
+ end
226
+ end
227
+
228
+ # Budget tracking
229
+
230
+ def budget_tracking_enabled?
231
+ config.budgets_enabled? && defined?(BudgetTracker)
232
+ end
233
+
234
+ def check_budget!
235
+ BudgetTracker.check!(
236
+ agent_type: self.class.name,
237
+ tenant_id: @tenant_id,
238
+ execution_type: "image_pipeline"
239
+ )
240
+ end
241
+
242
+ # Caching
243
+
244
+ def cache_enabled?
245
+ self.class.cache_enabled? && !options[:skip_cache]
246
+ end
247
+
248
+ def cache_key
249
+ components = [
250
+ "ruby_llm_agents",
251
+ "image_pipeline",
252
+ self.class.name,
253
+ self.class.version,
254
+ Digest::SHA256.hexdigest(cache_key_input)
255
+ ]
256
+ components.join(":")
257
+ end
258
+
259
+ def cache_key_input
260
+ # Include relevant options and step configuration
261
+ {
262
+ prompt: options[:prompt],
263
+ image: options[:image].to_s,
264
+ steps: self.class.steps.map { |s| [s[:name], s[:type]] }
265
+ }.to_json
266
+ end
267
+
268
+ def check_cache
269
+ return nil unless defined?(Rails) && Rails.cache
270
+
271
+ cached_data = Rails.cache.read(cache_key)
272
+ return nil unless cached_data
273
+
274
+ ImagePipelineResult.from_cache(cached_data)
275
+ end
276
+
277
+ def write_cache(result)
278
+ return unless defined?(Rails) && Rails.cache
279
+ return unless result.success?
280
+
281
+ Rails.cache.write(cache_key, result.to_cache, expires_in: self.class.cache_ttl)
282
+ end
283
+
284
+ # Callbacks
285
+
286
+ def run_callbacks(type, *args)
287
+ callbacks = self.class.callbacks[type] || []
288
+
289
+ callbacks.each do |callback|
290
+ case callback
291
+ when Symbol
292
+ send(callback, *args)
293
+ when Proc
294
+ instance_exec(*args, &callback)
295
+ end
296
+ end
297
+ end
298
+
299
+ # Execution tracking
300
+
301
+ def execution_tracking_enabled?
302
+ config.track_image_generation
303
+ end
304
+
305
+ def record_execution(result)
306
+ return unless defined?(RubyLLM::Agents::Execution)
307
+
308
+ execution_data = {
309
+ agent_type: self.class.name,
310
+ tenant_id: @tenant_id,
311
+ execution_type: "image_pipeline",
312
+ model_id: result.primary_model_id,
313
+ status: result.success? ? "success" : "error",
314
+ input_tokens: 0,
315
+ output_tokens: 0,
316
+ total_cost: result.total_cost,
317
+ duration_ms: result.duration_ms,
318
+ started_at: result.started_at,
319
+ completed_at: result.completed_at,
320
+ metadata: {
321
+ step_count: result.step_count,
322
+ successful_steps: result.successful_step_count,
323
+ failed_steps: result.failed_step_count
324
+ }
325
+ }
326
+
327
+ if config.async_logging && defined?(ExecutionLoggerJob)
328
+ ExecutionLoggerJob.perform_later(execution_data)
329
+ else
330
+ RubyLLM::Agents::Execution.create!(execution_data)
331
+ end
332
+ rescue StandardError => e
333
+ Rails.logger.error("[RubyLLM::Agents] Failed to record pipeline execution: #{e.message}") if defined?(Rails)
334
+ end
335
+
336
+ def record_failed_execution(error)
337
+ return unless defined?(RubyLLM::Agents::Execution)
338
+
339
+ execution_data = {
340
+ agent_type: self.class.name,
341
+ tenant_id: @tenant_id,
342
+ execution_type: "image_pipeline",
343
+ model_id: nil,
344
+ status: "error",
345
+ input_tokens: 0,
346
+ output_tokens: 0,
347
+ total_cost: calculate_partial_cost,
348
+ duration_ms: ((@started_at ? (Time.current - @started_at) : 0) * 1000).round,
349
+ started_at: @started_at || Time.current,
350
+ completed_at: Time.current,
351
+ error_class: error.class.name,
352
+ error_message: error.message.truncate(1000),
353
+ metadata: {
354
+ step_count: self.class.steps.size,
355
+ completed_steps: @step_results&.size || 0
356
+ }
357
+ }
358
+
359
+ if config.async_logging && defined?(ExecutionLoggerJob)
360
+ ExecutionLoggerJob.perform_later(execution_data)
361
+ else
362
+ RubyLLM::Agents::Execution.create!(execution_data)
363
+ end
364
+ rescue StandardError => e
365
+ Rails.logger.error("[RubyLLM::Agents] Failed to record failed pipeline execution: #{e.message}") if defined?(Rails)
366
+ end
367
+
368
+ def calculate_partial_cost
369
+ return 0 unless @step_results
370
+
371
+ @step_results.sum do |step|
372
+ step[:result]&.total_cost || 0
373
+ end
374
+ end
375
+
376
+ def config
377
+ RubyLLM::Agents.configuration
378
+ end
379
+ end
380
+ end
381
+ end
382
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "pipeline/dsl"
4
+ require_relative "pipeline/execution"
5
+
6
+ module RubyLLM
7
+ module Agents
8
+ # Image pipeline for chaining multiple image operations
9
+ #
10
+ # Orchestrates complex image workflows by chaining generators,
11
+ # transformers, upscalers, analyzers, and other image operations
12
+ # into a single pipeline with aggregated results and costs.
13
+ #
14
+ # @example Basic pipeline
15
+ # class ProductPipeline < RubyLLM::Agents::ImagePipeline
16
+ # step :generate, generator: ProductGenerator
17
+ # step :upscale, upscaler: PhotoUpscaler
18
+ # step :remove_background, remover: BackgroundRemover
19
+ # end
20
+ #
21
+ # result = ProductPipeline.call(prompt: "Professional laptop photo")
22
+ # result.final_image # => The processed image
23
+ # result.total_cost # => Combined cost of all steps
24
+ #
25
+ # @example Pipeline with analysis
26
+ # class AnalysisPipeline < RubyLLM::Agents::ImagePipeline
27
+ # step :generate, generator: ProductGenerator
28
+ # step :analyze, analyzer: ProductAnalyzer
29
+ #
30
+ # description "Generates and analyzes product images"
31
+ # end
32
+ #
33
+ # result = AnalysisPipeline.call(prompt: "Wireless earbuds")
34
+ # result.analysis # => ImageAnalysisResult from analyzer step
35
+ #
36
+ # @example Conditional pipeline
37
+ # class SmartPipeline < RubyLLM::Agents::ImagePipeline
38
+ # step :generate, generator: ProductGenerator
39
+ # step :upscale, upscaler: PhotoUpscaler, if: ->(ctx) { ctx[:upscale] }
40
+ # step :remove_background, remover: BackgroundRemover, if: ->(ctx) { ctx[:transparent] }
41
+ # end
42
+ #
43
+ # result = SmartPipeline.call(prompt: "...", upscale: true, transparent: false)
44
+ #
45
+ class ImagePipeline
46
+ extend DSL
47
+ include Execution
48
+
49
+ class << self
50
+ # Execute pipeline with the given options
51
+ #
52
+ # @param options [Hash] Pipeline options
53
+ # @option options [String] :prompt Prompt for generation steps
54
+ # @option options [String, IO] :image Input image for non-generation pipelines
55
+ # @option options [Object] :tenant Tenant for multi-tenancy
56
+ # @return [ImagePipelineResult] The combined result of all steps
57
+ def call(**options)
58
+ new(**options).call
59
+ end
60
+
61
+ # Ensure subclasses inherit DSL settings and steps
62
+ def inherited(subclass)
63
+ super
64
+ # Copy steps to subclass
65
+ subclass.instance_variable_set(:@steps, @steps&.dup || [])
66
+ subclass.instance_variable_set(:@callbacks, @callbacks&.dup || { before: [], after: [] })
67
+ subclass.instance_variable_set(:@version, @version)
68
+ subclass.instance_variable_set(:@description, @description)
69
+ subclass.instance_variable_set(:@cache_ttl, @cache_ttl)
70
+ subclass.instance_variable_set(:@stop_on_error, @stop_on_error)
71
+ end
72
+ end
73
+
74
+ attr_reader :options, :tenant_id, :step_results, :context
75
+
76
+ # Initialize a new pipeline instance
77
+ #
78
+ # @param options [Hash] Pipeline options
79
+ # @option options [String] :prompt Prompt for generation steps
80
+ # @option options [String, IO] :image Input image
81
+ # @option options [Object] :tenant Tenant for multi-tenancy
82
+ def initialize(**options)
83
+ @options = options
84
+ @tenant_id = nil
85
+ @step_results = []
86
+ @context = options.dup
87
+ end
88
+
89
+ # Execute the pipeline
90
+ #
91
+ # @return [ImagePipelineResult] The combined result of all steps
92
+ def call
93
+ execute
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../concerns/image_operation_dsl"
4
+ require_relative "../generator/content_policy"
5
+
6
+ module RubyLLM
7
+ module Agents
8
+ class ImageTransformer
9
+ # DSL for configuring image transformers
10
+ #
11
+ # Provides class-level methods to configure model, strength,
12
+ # and other image transformation parameters.
13
+ #
14
+ # @example
15
+ # class AnimeTransformer < RubyLLM::Agents::ImageTransformer
16
+ # model "sdxl"
17
+ # strength 0.8
18
+ # size "1024x1024"
19
+ # template "anime style, {prompt}"
20
+ # end
21
+ #
22
+ module DSL
23
+ include Concerns::ImageOperationDSL
24
+
25
+ # Set or get the output image size
26
+ #
27
+ # @param value [String, nil] Size (e.g., "1024x1024")
28
+ # @return [String] The size to use
29
+ def size(value = nil)
30
+ if value
31
+ @size = value
32
+ else
33
+ @size || inherited_or_default(:size, config.default_image_size)
34
+ end
35
+ end
36
+
37
+ # Set or get the transformation strength
38
+ #
39
+ # Controls how much the image is transformed (0.0-1.0).
40
+ # Lower values preserve more of the original image.
41
+ # Higher values allow more creative freedom.
42
+ #
43
+ # @param value [Float, nil] Strength (0.0-1.0)
44
+ # @return [Float] The transformation strength
45
+ def strength(value = nil)
46
+ if value
47
+ unless value.is_a?(Numeric) && value.between?(0.0, 1.0)
48
+ raise ArgumentError, "Strength must be between 0.0 and 1.0"
49
+ end
50
+ @strength = value.to_f
51
+ else
52
+ @strength || inherited_or_default(:strength, 0.75)
53
+ end
54
+ end
55
+
56
+ # Set or get whether to preserve composition
57
+ #
58
+ # When true, maintains the overall structure and layout
59
+ # of the original image.
60
+ #
61
+ # @param value [Boolean, nil] Preserve composition flag
62
+ # @return [Boolean] Whether to preserve composition
63
+ def preserve_composition(value = nil)
64
+ if value.nil?
65
+ result = @preserve_composition
66
+ result = inherited_or_default(:preserve_composition, true) if result.nil?
67
+ result
68
+ else
69
+ @preserve_composition = value
70
+ end
71
+ end
72
+
73
+ # Set or get the content policy level
74
+ #
75
+ # @param level [Symbol, nil] Policy level (:none, :standard, :moderate, :strict)
76
+ # @return [Symbol] The content policy level
77
+ def content_policy(level = nil)
78
+ if level
79
+ @content_policy = level
80
+ else
81
+ @content_policy || inherited_or_default(:content_policy, :standard)
82
+ end
83
+ end
84
+
85
+ # Set a prompt template (use {prompt} as placeholder)
86
+ #
87
+ # @param value [String, nil] Template string
88
+ # @return [String, nil] The template
89
+ def template(value = nil)
90
+ if value
91
+ @template_string = value
92
+ else
93
+ @template_string || inherited_or_default(:template_string, nil)
94
+ end
95
+ end
96
+
97
+ # Get the template string
98
+ #
99
+ # @return [String, nil] The template string
100
+ def template_string
101
+ @template_string || inherited_or_default(:template_string, nil)
102
+ end
103
+
104
+ # Set or get negative prompt
105
+ #
106
+ # @param value [String, nil] Negative prompt text
107
+ # @return [String, nil] The negative prompt
108
+ def negative_prompt(value = nil)
109
+ if value
110
+ @negative_prompt = value
111
+ else
112
+ @negative_prompt || inherited_or_default(:negative_prompt, nil)
113
+ end
114
+ end
115
+
116
+ # Set or get guidance scale (CFG scale)
117
+ #
118
+ # @param value [Float, nil] Guidance scale
119
+ # @return [Float, nil] The guidance scale
120
+ def guidance_scale(value = nil)
121
+ if value
122
+ @guidance_scale = value
123
+ else
124
+ @guidance_scale || inherited_or_default(:guidance_scale, nil)
125
+ end
126
+ end
127
+
128
+ # Set or get number of inference steps
129
+ #
130
+ # @param value [Integer, nil] Number of steps
131
+ # @return [Integer, nil] The steps
132
+ def steps(value = nil)
133
+ if value
134
+ @steps = value
135
+ else
136
+ @steps || inherited_or_default(:steps, nil)
137
+ end
138
+ end
139
+
140
+ private
141
+
142
+ def default_model
143
+ config.default_transformer_model || "sdxl"
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end